diff --git a/README.md b/README.md index 943a0c2..ed31d2e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/tests/test_sitzplatzverteilung.py b/tests/test_sitzplatzverteilung.py new file mode 100644 index 0000000..0064480 --- /dev/null +++ b/tests/test_sitzplatzverteilung.py @@ -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()