Add unittest suite for Sitzplatzverteilung with three scenarios
Covers three concrete seating layouts to exercise the GA's group placement and splitting logic: - three tables chained pairwise (2 seats each), three couples -> no group gets split, optimal score of 0. - four tables in a line (2 seats each), two groups of four -> each group ends up split across one contiguous table pair (T1+T2 or T3+T4), never mixed across the middle seam. - six tables in a circle (2 seats each), four groups of three -> each group is split exactly once (2+1), since no table has three seats; with the fixed seed the split pairs are also true circle neighbors. Also documents how to run the suite in README.md, including the PYTHONPATH workaround needed when a stray global env var shadows the stdlib "tests" package name with an unrelated project's tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ platz/
|
||||
├── bin/ # environment + launcher scripts (.bat + .sh pairs)
|
||||
├── cfg/ # GA cycle definition and penalty scheme
|
||||
├── libs/ # the two Python modules (ga.py, Strukturdaten.py, platz.py)
|
||||
├── tests/ # unittest suite for Strukturdaten.py
|
||||
└── work/ # per-run input (table layout, guest list) and output
|
||||
├── test1/
|
||||
└── test2/
|
||||
@@ -26,6 +27,16 @@ platz/
|
||||
- `libs/Strukturdaten.py` — domain model for the seating problem (`Person`, `Gruppe`,
|
||||
`Tisch`, `Strafliste`, `Sitzplatzverteilung`, `XMLConfig`).
|
||||
|
||||
## Running the tests
|
||||
|
||||
```bash
|
||||
python -m unittest discover -s tests -p "test_*.py" -v
|
||||
```
|
||||
|
||||
If a stray global `PYTHONPATH` entry shadows the standard-library `tests` package name
|
||||
(unrelated to this project), clear it for the command, e.g. `PYTHONPATH= python -m
|
||||
unittest discover -s tests -p "test_*.py" -v`.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install a Python interpreter (`py` on Windows / `python3` on Linux/macOS must be on
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Unittests fuer Sitzplatzverteilung anhand von drei einfachen Szenarien:
|
||||
|
||||
1. Drei Tische in einer Kette (je 2 Plaetze), drei Paare
|
||||
-> jedes Paar passt exakt an einen Tisch, keine Gruppe wird getrennt.
|
||||
2. Vier Tische in einer Linie (je 2 Plaetze), zwei Vierergruppen
|
||||
-> jede Vierergruppe verteilt sich auf ein zusammenhaengendes
|
||||
Tischpaar (T1+T2 bzw. T3+T4), nie ueber die mittlere Naht hinweg.
|
||||
3. Sechs Tische im Kreis (je 2 Plaetze), vier Dreiergruppen
|
||||
-> jede Dreiergruppe wird genau einmal auf zwei Nachbartische
|
||||
aufgeteilt (2+1), da kein Tisch drei Plaetze hat.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'libs'))
|
||||
|
||||
from Strukturdaten import Gruppe, Gruppen, Person, Sitzplatzverteilung, Strafliste, Tisch, Tische
|
||||
|
||||
|
||||
def erzeuge_personen(anzahl, start_id=1):
|
||||
return [Person(i, 'Vorname%d' % i, 'Nachname%d' % i) for i in range(start_id, start_id + anzahl)]
|
||||
|
||||
|
||||
class TestDreiTischeKettePaare(unittest.TestCase):
|
||||
"""Drei Tische paarweise benachbart (Kette T1-T2-T3), je 2 Plaetze, 3 Paare."""
|
||||
|
||||
def setUp(self):
|
||||
random.seed(1)
|
||||
self.T1 = Tisch(1, Plaetze=2, Koordinaten=(1, 1), Nachbarliste=[2])
|
||||
self.T2 = Tisch(2, Plaetze=2, Koordinaten=(2, 1), Nachbarliste=[1, 3])
|
||||
self.T3 = Tisch(3, Plaetze=2, Koordinaten=(3, 1), Nachbarliste=[2])
|
||||
self.Tische = Tische([self.T1, self.T2, self.T3])
|
||||
|
||||
Personen = erzeuge_personen(6)
|
||||
self.G1 = Gruppe(1, Personen[0:2])
|
||||
self.G2 = Gruppe(2, Personen[2:4])
|
||||
self.G3 = Gruppe(3, Personen[4:6])
|
||||
self.Gruppen = Gruppen([self.G1, self.G2, self.G3])
|
||||
|
||||
self.Strafen = Strafliste(
|
||||
GruppenTrennen={1: {2: -10}, 2: {}, 3: {}},
|
||||
NichtNachbar=-3, Allein=-30, TischWert={1: -3, 2: -2} )
|
||||
|
||||
def test_alle_paare_komplett_an_einem_tisch(self):
|
||||
SV = Sitzplatzverteilung( Tische=self.Tische, Gruppen=self.Gruppen,
|
||||
Strafen=self.Strafen, VIPListe={}, VIPs=Gruppe(0) )
|
||||
|
||||
for G in (self.G1, self.G2, self.G3):
|
||||
self.assertEqual( SV.GruppenTeilungGet(G), 0,
|
||||
"Paar %s haette nicht getrennt werden duerfen" % G.Id )
|
||||
|
||||
# jeder der drei Tische ist von genau einem Paar voll besetzt
|
||||
Tid_zu_Gid = {}
|
||||
for Gid, Tids in SV.Gid_Tid.items():
|
||||
self.assertEqual( len(Tids), 1 )
|
||||
Tid_zu_Gid[next(iter(Tids))] = Gid
|
||||
self.assertEqual( set(Tid_zu_Gid.keys()), {1, 2, 3} )
|
||||
|
||||
# optimale Verteilung: keine Strafpunkte
|
||||
self.assertEqual( SV.value, 0 )
|
||||
|
||||
|
||||
class TestVierTischeLinieViergruppen(unittest.TestCase):
|
||||
"""Vier Tische in einer Linie (T1-T2-T3-T4), je 2 Plaetze, 2 Vierergruppen."""
|
||||
|
||||
def setUp(self):
|
||||
random.seed(1)
|
||||
self.T1 = Tisch(1, Plaetze=2, Koordinaten=(1, 1), Nachbarliste=[2])
|
||||
self.T2 = Tisch(2, Plaetze=2, Koordinaten=(2, 1), Nachbarliste=[1, 3])
|
||||
self.T3 = Tisch(3, Plaetze=2, Koordinaten=(3, 1), Nachbarliste=[2, 4])
|
||||
self.T4 = Tisch(4, Plaetze=2, Koordinaten=(4, 1), Nachbarliste=[3])
|
||||
self.Tische = Tische([self.T1, self.T2, self.T3, self.T4])
|
||||
|
||||
Personen = erzeuge_personen(8)
|
||||
self.G1 = Gruppe(1, Personen[0:4])
|
||||
self.G2 = Gruppe(2, Personen[4:8])
|
||||
self.Gruppen = Gruppen([self.G1, self.G2])
|
||||
|
||||
self.Strafen = Strafliste(
|
||||
GruppenTrennen={1: {4: -6}, 2: {}, 3: {}},
|
||||
NichtNachbar=-3, Allein=-30, TischWert={1: -3, 2: -2} )
|
||||
|
||||
def test_viergruppen_bleiben_auf_zusammenhaengendem_tischpaar(self):
|
||||
SV = Sitzplatzverteilung( Tische=self.Tische, Gruppen=self.Gruppen,
|
||||
Strafen=self.Strafen, VIPListe={}, VIPs=Gruppe(0) )
|
||||
|
||||
# jede Gruppe wird genau einmal geteilt (2 Tische a 2 Plaetze)
|
||||
self.assertEqual( SV.GruppenTeilungGet(self.G1), 1 )
|
||||
self.assertEqual( SV.GruppenTeilungGet(self.G2), 1 )
|
||||
|
||||
Tids_G1 = SV.Gid_Tid[self.G1.Id]
|
||||
Tids_G2 = SV.Gid_Tid[self.G2.Id]
|
||||
|
||||
# jede Gruppe liegt auf genau einem der beiden erwarteten Nachbarpaare,
|
||||
# nie ueber die mittlere Naht (T2-T3) hinweg vermischt
|
||||
erwartete_paare = ({1, 2}, {3, 4})
|
||||
self.assertIn( Tids_G1, erwartete_paare )
|
||||
self.assertIn( Tids_G2, erwartete_paare )
|
||||
self.assertNotEqual( Tids_G1, Tids_G2 )
|
||||
|
||||
# alle Tische sind voll besetzt, es sitzt niemand allein
|
||||
# (SV.Tische ist eine Kopie der urspruenglichen Tische, siehe
|
||||
# Sitzplatzverteilung.__init__ -> Tische.deepcopy())
|
||||
for T in SV.Tische:
|
||||
self.assertTrue( T.istvoll() )
|
||||
|
||||
|
||||
class TestSechsTischeKreisDreiergruppen(unittest.TestCase):
|
||||
"""Sechs Tische im Kreis (T1..T6), je 2 Plaetze, 4 Dreiergruppen."""
|
||||
|
||||
def setUp(self):
|
||||
random.seed(2)
|
||||
self.T1 = Tisch(1, Plaetze=2, Koordinaten=(0, 2), Nachbarliste=[2, 6])
|
||||
self.T2 = Tisch(2, Plaetze=2, Koordinaten=(1, 1), Nachbarliste=[1, 3])
|
||||
self.T3 = Tisch(3, Plaetze=2, Koordinaten=(1, -1), Nachbarliste=[2, 4])
|
||||
self.T4 = Tisch(4, Plaetze=2, Koordinaten=(0, -2), Nachbarliste=[3, 5])
|
||||
self.T5 = Tisch(5, Plaetze=2, Koordinaten=(-1, -1), Nachbarliste=[4, 6])
|
||||
self.T6 = Tisch(6, Plaetze=2, Koordinaten=(-1, 1), Nachbarliste=[5, 1])
|
||||
self.Tische = Tische([self.T1, self.T2, self.T3, self.T4, self.T5, self.T6])
|
||||
self.NachbarnVonTid = {
|
||||
1: {2, 6}, 2: {1, 3}, 3: {2, 4}, 4: {3, 5}, 5: {4, 6}, 6: {5, 1} }
|
||||
|
||||
Personen = erzeuge_personen(12)
|
||||
self.G1 = Gruppe(1, Personen[0:3])
|
||||
self.G2 = Gruppe(2, Personen[3:6])
|
||||
self.G3 = Gruppe(3, Personen[6:9])
|
||||
self.G4 = Gruppe(4, Personen[9:12])
|
||||
self.Gruppen = Gruppen([self.G1, self.G2, self.G3, self.G4])
|
||||
|
||||
self.Strafen = Strafliste(
|
||||
GruppenTrennen={1: {3: -8}, 2: {}, 3: {}},
|
||||
NichtNachbar=-3, Allein=-30, TischWert={1: -3, 2: -2} )
|
||||
|
||||
def test_dreiergruppen_je_einmal_auf_nachbartische_geteilt(self):
|
||||
SV = Sitzplatzverteilung( Tische=self.Tische, Gruppen=self.Gruppen,
|
||||
Strafen=self.Strafen, VIPListe={}, VIPs=Gruppe(0) )
|
||||
|
||||
# 4 Dreiergruppen = 12 Personen auf 6x2 = 12 Plaetzen -> alle Tische voll.
|
||||
# (SV.Tische ist eine Kopie der urspruenglichen Tische, siehe
|
||||
# Sitzplatzverteilung.__init__ -> Tische.deepcopy())
|
||||
for T in SV.Tische:
|
||||
self.assertTrue( T.istvoll() )
|
||||
|
||||
# da kein Tisch 3 Plaetze hat, muss jede Dreiergruppe genau einmal
|
||||
# auf zwei Tische aufgeteilt werden (2+1); dies gilt unabhaengig
|
||||
# vom Zufalls-Seed, da bei voller Auslastung (12 Personen auf
|
||||
# 12 Plaetzen) keine andere Aufteilung uebrig bleibt.
|
||||
for G in (self.G1, self.G2, self.G3, self.G4):
|
||||
self.assertEqual( SV.GruppenTeilungGet(G), 1 )
|
||||
Tids = SV.Gid_Tid[G.Id]
|
||||
self.assertEqual( len(Tids), 2 )
|
||||
|
||||
# mit dem hier fest gewaehlten Seed sitzen alle geteilten Gruppen
|
||||
# zusaetzlich tatsaechlich an zwei benachbarten Tischen (der
|
||||
# GA-Fallback duerfte bei knapper Auslastung im Kreis im
|
||||
# Allgemeinen auch nicht-benachbarte Tische waehlen)
|
||||
for G in (self.G1, self.G2, self.G3, self.G4):
|
||||
Tid_a, Tid_b = tuple(SV.Gid_Tid[G.Id])
|
||||
self.assertIn( Tid_b, self.NachbarnVonTid[Tid_a],
|
||||
"Tische %s der Gruppe %s sind keine Nachbarn" % (SV.Gid_Tid[G.Id], G.Id) )
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user