""" 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, JSONConfig, Person, Sitzplatzverteilung, Strafliste, Tisch, Tische from ga import Abstammung, Entwicklung, Farm, Zyklus, _StringAusdruck IMAGES_DIR = os.path.join(os.path.dirname(__file__), '..', 'doc', 'images') 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) ) SV.alsSVG( os.path.join(IMAGES_DIR, 'drei_tische_kette_paare.svg') ) 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) ) SV.alsSVG( os.path.join(IMAGES_DIR, 'vier_tische_linie_viergruppen.svg') ) # 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) ) SV.alsSVG( os.path.join(IMAGES_DIR, 'sechs_tische_kreis_dreiergruppen.svg') ) # 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) ) class TestJSONConfig(unittest.TestCase): """Parsen der bestellung.json: Gruppen, VIP-Bindung, Anzahl-Expansion.""" def test_gruppen_vips_und_sammelbuchung(self): Daten = { "gruppen": [ { "name": "Brautpaar", "tisch": 1, "personen": [ {"vorname": "Julia", "nachname": "Brandt"}, {"vorname": "Daniel", "nachname": "Sommer", "titel": "Dr."}, ], }, { "name": "Familie Huber", "personen": [ {"vorname": "Anna", "nachname": "Huber"}, {"vorname": "Berta", "nachname": "Huber"}, ], }, { "name": "Musikverein", "anzahl": 5, "personen": [ {"vorname": "Gast", "nachname": "Musikverein"} ], }, ] } PN, GN, VIPGruppe, VipHash, GruppenNamen = JSONConfig().HandleBestellung(Daten) # VIP-Gruppe: beide ans Tisch 1 gebunden, nicht in den GA-Gruppen self.assertEqual( VIPGruppe.Anzahl(), 2 ) self.assertEqual( set(VipHash.values()), {1} ) self.assertEqual( len(VipHash), 2 ) # regulaere Gruppen: Familie Huber (2) + Musikverein (5x expandiert) = 7 self.assertEqual( GN.NLeute, 7 ) self.assertEqual( len(GN.GruppenListe), 2 ) # Sammelbuchung: die eine Musikverein-Person wurde 5x vervielfacht Groessen = sorted( G.Anzahl() for G in GN.GruppenListe ) self.assertEqual( Groessen, [2, 5] ) # Anzeigenamen kommen durch self.assertIn( "Familie Huber", GruppenNamen.values() ) self.assertIn( "Musikverein", GruppenNamen.values() ) def test_fehlende_pflichtfelder_werfen_fehler(self): with self.assertRaises(ValueError): JSONConfig().HandleBestellung( {"keine_gruppen": []} ) with self.assertRaises(ValueError): JSONConfig().HandleBestellung( {"gruppen": [ {"personen": [ {"vorname": "Nur"} ]} ]} ) class TestStringAusdruck(unittest.TestCase): """Der sichere Zyklus-Ausdruck-Auswerter (Ersatz fuer eval).""" def test_verkettung_und_vervielfachung(self): self.assertEqual( _StringAusdruck("'e,'+'s,z,m,j,z,'*3+'s'"), 'e,s,z,m,j,z,s,z,m,j,z,s,z,m,j,z,s' ) self.assertEqual( _StringAusdruck("'e,s'"), 'e,s' ) def test_lehnt_code_ab(self): # kein Funktionsaufruf, keine Namen, kein Import for boese in ("__import__('os')", "open('x')", "1+1", "a+'b'"): with self.assertRaises(ValueError): _StringAusdruck(boese) class TestEntwicklung(unittest.TestCase): """Das Beobachtungsobjekt zeichnet den Verlauf auf und rendert SVG.""" class _L: def __init__(self, value): self.value = value def test_erfassen_und_svg(self): E = Entwicklung() E.erfassen( 'e', [self._L(-200), self._L(-150), self._L(-90)] ) E.erfassen( 's', [self._L(-90), self._L(-70)] ) E.erfassen( 'm', [self._L(-70), self._L(-40)] ) self.assertEqual( len(E.Punkte), 3 ) # je Punkt: (index, aktion, best, schnitt, schlecht, N) _i, aktion, best, schnitt, schlecht, n = E.Punkte[0] self.assertEqual( aktion, 'e' ) self.assertEqual( best, -90 ) self.assertEqual( schlecht, -200 ) self.assertEqual( n, 3 ) Pfad = os.path.join( IMAGES_DIR, 'test_entwicklung.svg' ) E.alsSVG( Pfad ) with open(Pfad) as f: svg = f.read() self.assertIn( ' Abbruch nach 'Geduld').""" def __init__(self, *a, **k): self.value = -100 def mutieren(self): return self def __eq__(self, other): return self.value == other.value def __lt__(self, other): return self.value < other.value def _zyklus(self, **params): Z = Zyklus() Z.Modus = 'adaptiv' Z.Adaptiv = dict(Zyklus.AdaptivDefaults) Z.Adaptiv.update(params) return Z def test_population_schrumpft_und_bricht_bei_konvergenz_ab(self): Z = self._zyklus( Start=40, Schrumpfung=0.85, MinPopulation=5, MaxGenerationen=50, Geduld=3, Schwelle=1.0 ) F = Farm( Z, self._L ) # Pool-Groessen je 's'-Aufzeichnung (Ende jeder Generation) pop_pro_gen = [ p[5] for p in F.Entwicklung.Punkte if p[1] == 's' ] # monoton fallend (Population schrumpft) ... self.assertTrue( all( a >= b for a, b in zip(pop_pro_gen, pop_pro_gen[1:]) ), "Population muss von Generation zu Generation schrumpfen: %s" % pop_pro_gen ) self.assertLess( pop_pro_gen[-1], pop_pro_gen[0] ) # ... und da nichts besser wird, nach Geduld(=3) Generationen Abbruch, # also klar vor MaxGenerationen(=50). self.assertLessEqual( len(pop_pro_gen), 4 ) def test_population_faellt_nicht_unter_min(self): Z = self._zyklus( Start=8, Schrumpfung=0.5, MinPopulation=5, MaxGenerationen=10, Geduld=99, Schwelle=1.0 ) F = Farm( Z, self._L ) pop_pro_gen = [ p[5] for p in F.Entwicklung.Punkte if p[1] == 's' ] self.assertTrue( all( n >= 5 for n in pop_pro_gen ), "MinPopulation=5 darf nicht unterschritten werden: %s" % pop_pro_gen ) class TestAbstammung(unittest.TestCase): """Der Stammbaum vergibt hierarchische Ids, aus denen sich die Abstammung ablesen laesst, und zeichnet einen Baum.""" class _L: """GA-Loesung, deren Mutation den Wert leicht verbessert (damit ueber Generationen ein mehrstufiger Ast entsteht).""" def __init__(self, *a, **k): self.value = -100 def mutieren(self): self.value = self.value + 1 return self def __eq__(self, other): return self.value == other.value def __lt__(self, other): return self.value < other.value def _zyklus(self, **params): Z = Zyklus() Z.Modus = 'adaptiv' Z.Adaptiv = dict(Zyklus.AdaptivDefaults) Z.Adaptiv.update(params) return Z def test_fortlaufende_nummern_ids(self): Z = self._zyklus( Start=5, Schrumpfung=1.0, MinPopulation=5, MaxGenerationen=3, Geduld=99, Schwelle=0.0 ) F = Farm( Z, self._L ) Knoten = F.Abstammung.Knoten # alle Ids sind fortlaufende Nummern 1..N ueber alle Loesungen Ids = sorted(Knoten) self.assertTrue( all( isinstance(Id, int) for Id in Ids ) ) self.assertEqual( Ids, list(range(1, len(Ids) + 1)) ) # genau die 5 Startloesungen sind Wurzeln (kein Vorgaenger), # alle uebrigen haben einen Vorgaenger, der als Knoten existiert wurzeln = [ Id for Id, K in Knoten.items() if K['eltern'] is None ] self.assertEqual( len(wurzeln), 5 ) for Id, K in Knoten.items(): if K['eltern'] is None: continue self.assertIn( K['eltern'], Knoten ) self.assertNotEqual( K['eltern'], Id ) def test_abstammung_ueber_vorgaenger(self): # Abstammung wird ueber den gemerkten Vorgaenger verfolgt, nicht ueber # die Id selbst (Ids sind schlichte Nummern) A = Abstammung() A.erfassen( 1, None, 0, -100, 'e' ) A.erfassen( 2, 1, 1, -99, 'm' ) A.erfassen( 3, 2, 2, -98, 'm' ) kette = [] Id = 3 while Id is not None: kette.append(Id) Id = A.Knoten[Id]['eltern'] self.assertEqual( kette, [3, 2, 1] ) def test_svg_hebt_beste_hervor(self): Z = self._zyklus( Start=4, Schrumpfung=1.0, MinPopulation=4, MaxGenerationen=2, Geduld=99, Schwelle=0.0 ) F = Farm( Z, self._L ) Bester = F.Bester() Pfad = os.path.join( IMAGES_DIR, 'test_abstammung.svg' ) F.Abstammung.alsSVG( Pfad, HervorId=Bester._absid ) with open(Pfad) as f: svg = f.read() self.assertIn( '%s' % Bester._absid, svg ) def test_leere_abstammung_wirft_beim_svg(self): with self.assertRaises(ValueError): Abstammung().alsSVG( os.path.join(IMAGES_DIR, 'leer_abst.svg') ) if __name__ == '__main__': unittest.main()