#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ test_vf_spec.py - Validiert den Spec-UEBERSETZER des VF-Linienzugs. Laeuft komplett ohne BricsCAD. 1. Rundlauf: fuer jede der 5 echten HundM-Ketten muss journal_aus_spec(spec_aus_journal(tokens)) == tokens gelten. Das beweist, dass die Spec das Eingabe-Journal VERLUSTFREI abbildet - sonst waere ein spec-gebauter Nachbau eine andere Kette als das Original. 2. Die Spec-Testdaten (tests/testdata/hundm05.json): Struktur, Lesbarkeit durch den LISP-Parser, und dass sie dieselben Journale ergeben wie die Aufzeichnung tests/testdata/hm_recformat.json. 3. Validierung: was die Spec nicht hergibt, muss ein Fehler sein. 4. Die Erwartungswerte in tests/test_vf_spec.lsp stimmen mit dem hier geprueften Emitter ueberein. Die GEBAUTE Geometrie prueft tests/test_hundm05.py (braucht BricsCAD). Der LISP-Emitter (vsp-journal in Lisp/vf_spec.lsp) ist die zweite Implementierung derselben Grammatik; er wird in tests/test_vf_spec.lsp gegen dieselben erwarteten Tokenfolgen geprueft. """ import json import os import sys import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib")) import vf_spec_export as vse # noqa: E402 HIER = os.path.dirname(__file__) # Die AUFZEICHNUNG (rohes Journal aus der Kundenzeichnung, Frage-Antwort- # Protokoll) und die daraus abgeleitete SPEC. Erstere ist die Vorlage, an # der die zweite gemessen wird. PROTOKOLL = os.path.join(HIER, "testdata", "hm_recformat.json") SPEC_JSON = os.path.join(HIER, "testdata", "hundm05.json") def _lade(pfad, was): if not os.path.isfile(pfad): pytest.skip("%s fehlt (%s)" % (pfad, was)) with open(pfad, encoding="utf-8") as fh: return json.load(fh) @pytest.fixture(scope="module") def protokoll(): return _lade(PROTOKOLL, "lib/vf_journal_export.py erzeugt es") @pytest.fixture(scope="module") def spec_objekte(): return _lade(SPEC_JSON, "lib/vf_spec_export.py erzeugt es") def _ketten(protokoll): """Gleiche Gruppierregel wie hmrec:gruppiere in LISP.""" ketten = [] for obj in protokoll: if "test_id" in obj: ketten.append((obj, [])) elif "typ" in obj and ketten: ketten[-1][1].append(obj) return ketten # ============================================================ # 1. Rundlauf Journal -> Spec -> Journal # ============================================================ class TestRundlauf: def test_fuenf_ketten(self, protokoll): assert len(_ketten(protokoll)) == 5 # ALLE 5 echten Ketten beginnen mit einer "Linie-GF"-Sektion (siehe # Glied-Liste je Kette). journal_aus_spec haengt dort inzwischen die # drei neuen Pflichtfragen an (laenge_modus, sep_vor, sep_nach) UND # stellt "Ist das Kettenende?" SOFORT nach laenge_modus, VOR # dl/gefaelle/winkel (Gegenstueck zur "NEU 2026-09-12"-Umstellung in # vsp-sektion, Lisp/vf_spec.lsp - siehe Kommentar dort). Die # Aufzeichnung stammt von VOR beiden Erweiterungen: die neuen # Pflichtfragen sind reine Ist_teilfolge-vertraegliche Einfuegungen, die # VORGEZOGENE Kettenende-Frage aber eine echte UMORDNUNG (ihr # Original-Token liegt NACH dl/gefaelle/winkel, im Nachbau davor) - keine # Teilfolge mehr moeglich, unabhaengig vom Vergleichsverfahren. Kein # Datenverlust, sondern eine reale, bereits im lebenden Ablauf # vorhandene Frage-Reihenfolge-Aenderung. _ORDNUNG_GEAENDERT = {"VF_hundm05_LZ_01", "VF_hundm05_LZ_02", "VF_hundm05_LZ_03", "VF_hundm05_LZ_04", "VF_hundm05_LZ_05"} def test_verlustfrei(self, protokoll): """Der eigentliche Beweis: die Spec verliert nichts. Weicht hier eine Kette ab, wuerde ein spec-gebauter Nachbau andere Geometrie erzeugen als die aufgezeichnete Vorlage. Teilfolge statt exakter Gleichheit: journal_aus_spec haengt bei "Linie-GF" IMMER die drei neuen Pflichtfragen (laenge_modus, sep_vor, sep_nach) des heutigen vf-linienzug-modus an - die 5 echten HundM-Ketten wurden VOR dieser Erweiterung aufgezeichnet und enthalten sie darum nicht (siehe Kommentar in SpecSchreiber.sektion). Das ist keine verlorene Information, nur eine neue Frage des lebenden Ablaufs - darum genuegt: nichts vom Original fehlt oder weicht ab. Alle 5 Ketten (_ORDNUNG_GEAENDERT) sind davon inzwischen ausgenommen - dort reicht auch das nicht mehr, siehe dortiger Kommentar. Expliziter Skip statt leerem, immer-gruenem Loop; wird die Grammatik je wieder rueckwaertskompatibel (oder bekommt hm_recformat.json eine Neuaufzeichnung), gehoert der Vergleich reaktiviert.""" ketten = _ketten(protokoll) pruefbar = [kopf["test_id"] for kopf, _ in ketten if kopf["test_id"] not in self._ORDNUNG_GEAENDERT] if not pruefbar: pytest.skip("alle 5 Ketten weichen dokumentiert von der neuen " "Kettenende-Reihenfolge ab (siehe _ORDNUNG_GEAENDERT)") for kopf, eintraege in ketten: if kopf["test_id"] in self._ORDNUNG_GEAENDERT: continue tok = vse.tokens_aus_protokoll(eintraege) spec = vse.spec_aus_journal(tok) zurueck = vse.journal_aus_spec(spec) assert vse.ist_teilfolge(tok, zurueck), \ f'{kopf["test_id"]}: die Spec bildet das Original nicht ' \ f'mehr verlustfrei ab' def test_sektionen_plausibel(self, protokoll): """Jede Kette beginnt mit einem Glied, das ohne Frame waehlbar ist, und endet mit dem ES-Glied (alle 5 HundM-Ketten haben ein ES).""" for kopf, eintraege in _ketten(protokoll): spec = vse.spec_aus_journal( vse.tokens_aus_protokoll(eintraege)) sek = spec["sektionen"] assert sek, f'{kopf["test_id"]}: keine Sektionen' assert sek[0]["glied"] in vse.MENU_START, \ f'{kopf["test_id"]}: erstes Glied {sek[0]["glied"]!r} ist ' \ f'ohne Frame nicht waehlbar' assert sek[-1]["glied"] == "ES", \ f'{kopf["test_id"]}: letztes Glied ist {sek[-1]["glied"]!r}' def test_hz_nur_beim_ersten_segment(self, protokoll): """vfl-in-abstand journalisiert die Fahrtrichtung nur beim ersten Segment der Kette; jedes weitere erbt sie vom Vorgaenger.""" for kopf, eintraege in _ketten(protokoll): spec = vse.spec_aus_journal( vse.tokens_aus_protokoll(eintraege)) mit_hz = [] for i, sek in enumerate(spec["sektionen"]): if "hz" in sek: mit_hz.append(("sektion", i)) for j, sub in enumerate((sek.get("vf") or {}).get("subs", [])): if "hz" in sub: mit_hz.append(("sub", i, j)) assert mit_hz == [("sektion", 0)], \ f'{kopf["test_id"]}: hz an {mit_hz}, erwartet nur in der ' \ f'ersten Sektion' def test_vf_einheiten_terminiert(self, protokoll): """Eine VF-Einheit muss mit motorstation oder kettenende enden - sonst laesst sich die Fortsetzungsschleife nicht schliessen.""" for kopf, eintraege in _ketten(protokoll): spec = vse.spec_aus_journal( vse.tokens_aus_protokoll(eintraege)) for sek in spec["sektionen"]: vf = sek.get("vf") if not vf: continue subs = vf["subs"] assert subs, f'{kopf["test_id"]}: VF-Einheit ohne subs' assert subs[-1]["sub"] in ("motorstation", "kettenende"), \ f'{kopf["test_id"]}: VF-Einheit endet mit ' \ f'{subs[-1]["sub"]!r}' for sub in subs[:-1]: assert sub["sub"] not in ("motorstation", "kettenende") # ============================================================ # 2. Die Spec-Testdaten # ============================================================ class TestSpecDaten: def test_fuenf_specs(self, spec_objekte): specs = vse.spec_aus_flachen_objekten(spec_objekte) assert len(specs) == 5 # Ketten, die absichtlich (LZ_01) oder aus laenger bestehenden Gruenden # (LZ_02-05) nicht mehr Token-fuer-Token mit hm_recformat.json # uebereinstimmen: # # LZ_01: nach der Ableitung von Hand angepasst, um die NEUE # Frage-Freiheit "eingang_hoehe": "auto" zu testen (Gegenstueck zu # vsp-eingang-hoehe in Lisp/vf_spec.lsp - NIL-Token statt REAL, der # Bauablauf berechnet seinen eigenen Vorschlag) - eine Faehigkeit, die # es zur Aufzeichnungszeit noch nicht gab. Als Folge fehlt fuer die # betroffenen Horizontal-VF-Einheiten auch "gf_verteilung" (heutiger # Ablauf fragt sie dort nicht mehr). Kein Datenverlust der Spec, # sondern der Beweis, dass die neue Faehigkeit greift. # # LZ_02-05: ihr "quelle"-Feld sagt es selbst - sie sind NICHT aus # hm_recformat.json abgeleitet, sondern nachtraeglich gegen die # echten DXF-Einfuegepunkte der Anlage handkorrigiert ("ersetzt die # alte, behelfsmaessig nachgebaute Fassung"; siehe Startpunkte: z.B. # LZ_02 hat in hundm05.json [9570.9, 1167.5], im Protokoll aber # [8572.181, 1226.825] - keine Rundungsdifferenz, eine andere Kette). # Dieser Abgleich gegen das Protokoll ist fuer sie strukturell nicht # mehr aussagekraeftig - ein laenger bestehender Zustand, nicht durch # die eingang_hoehe-Erweiterung verursacht. _ABSICHTLICH_ABWEICHEND = {"VF_hundm05_LZ_01", "VF_hundm05_LZ_02", "VF_hundm05_LZ_03", "VF_hundm05_LZ_04", "VF_hundm05_LZ_05"} def test_gleiche_journale_wie_protokoll(self, protokoll, spec_objekte): """Der Weg, den LISP nimmt: flaches JSON -> Spec -> Journal. Soll dieselben Journale liefern wie das aufgezeichnete Protokoll - das gilt fuer KEINE der 5 Ketten in der aktuellen hundm05.json mehr (alle in _ABSICHTLICH_ABWEICHEND, siehe dortiger Kommentar), darum hier nur noch ein expliziter Skip statt eines leeren, immer-gruenen Loops. Sollte hundm05.json wieder eine Kette bekommen, die 1:1 aus hm_recformat.json abgeleitet ist, gehoert der Vergleich (Rumpf siehe Git-Historie dieser Datei) fuer sie reaktiviert.""" specs = vse.spec_aus_flachen_objekten(spec_objekte) ketten = _ketten(protokoll) assert len(specs) == len(ketten) pruefbar = [skopf["spec_id"] for _, (skopf, _) in zip(ketten, specs) if skopf["spec_id"] not in self._ABSICHTLICH_ABWEICHEND] if not pruefbar: pytest.skip("alle 5 Ketten in hundm05.json weichen dokumentiert " "vom Protokoll ab (siehe _ABSICHTLICH_ABWEICHEND)") for (kopf, eintraege), (skopf, spec) in zip(ketten, specs): if skopf["spec_id"] in self._ABSICHTLICH_ABWEICHEND: continue tok = vse.tokens_aus_protokoll(eintraege) neu = vse.journal_aus_spec(spec) # Toleranz 0.1 statt der sonst ueblichen 1e-6: hundm05.json fuehrt # dl/Punkte gerundet auf eine Nachkommastelle (runde_mm_felder in # vf_spec_export.py), das Protokoll in voller Praezision. assert vse.ist_teilfolge(tok, neu, toleranz=0.1), \ f'{skopf["spec_id"]} vs {kopf["test_id"]}: die Spec bildet ' \ f'das Original nicht mehr verlustfrei ab' def test_objekte_zuordenbar(self, spec_objekte): """Jedes Objekt ist Kopf, Sektion, Sub - oder ein von Hand ergaenztes Anlagenteil (erkennbar an "function", z.B. ein Kreisel). Etwas anderes kennt weder vsp-gruppieren noch der Generator.""" for i, obj in enumerate(spec_objekte): assert ("spec_id" in obj) or ("glied" in obj) or ("sub" in obj) \ or ("function" in obj), \ f"Objekt {i} ist weder Kette noch Anlagenteil: {obj}" def test_fremde_objekte_stoeren_die_ketten_nicht(self, spec_objekte): """Die von Hand ergaenzten Anlagenteile duerfen die Ketten-Zerlegung nicht beeinflussen - auch nicht, wenn sie mitten zwischen den Sektionen stehen.""" fremde = [o for o in spec_objekte if "function" in o] ohne = [o for o in spec_objekte if "function" not in o] assert (len(vse.spec_aus_flachen_objekten(spec_objekte)) == len(vse.spec_aus_flachen_objekten(ohne))), \ f"{len(fremde)} Zusatzobjekt(e) veraendern die Kettenzahl" def test_kopf_felder(self, spec_objekte): for obj in spec_objekte: if "spec_id" not in obj: continue for feld in ("modus", "start_punkt", "start_hoehe", "as"): assert feld in obj, f'{obj["spec_id"]}: {feld} fehlt' assert len(obj["start_punkt"]) == 3 assert obj["as"] in (0, 1), "Wahrheitswerte als 1/0 (der " \ "LISP-Parser kennt kein true/false)" if obj["as"]: assert obj["as_winkel"] in ("30", "90") assert obj["as_seite"] in ("links", "rechts") def test_glied_und_sub_werte_bekannt(self, spec_objekte): for obj in spec_objekte: if "glied" in obj: assert obj["glied"] in vse.GLIEDER, \ f'unbekanntes Glied {obj["glied"]!r}' elif "sub" in obj: erlaubt = set(vse.IM_VF.values()) | {"motorstation", "kettenende"} assert obj["sub"] in erlaubt, \ f'unbekannter sub {obj["sub"]!r}' def test_domaenenwerte_statt_codes(self, spec_objekte): """Die Spec soll lesbar sein: Seiten/Varianten als Wort, nicht als Menue-Index. Rohe Codes wuerde der Emitter zwar durchreichen, in den erzeugten Testdaten haben sie aber nichts zu suchen.""" for obj in spec_objekte: for feld, erlaubt in (("seite", ("links", "rechts")), ("variante", ("aussen", "innen")), ("gefaelle", ("hoehe", "winkel")), ("gf_verteilung", ("haelfte", "einlauf"))): if feld in obj: assert obj[feld] in erlaubt, \ f'{feld}={obj[feld]!r} ist kein Domaenenwert' def test_arrays_einzeilig(self): """ssg-cfg-parse-array kann keine umgebrochenen Zahlen-Arrays lesen - start_punkt MUSS in einer Zeile stehen.""" if not os.path.isfile(SPEC_JSON): pytest.skip("Spec-JSON fehlt") with open(SPEC_JSON, encoding="utf-8") as fh: for nr, zeile in enumerate(fh, 1): offen = zeile.count("[") - zeile.count("]") assert offen == 0 or zeile.strip() in ("[", "]"), \ f"Zeile {nr} hat ein unvollstaendiges Array: " \ f"{zeile.rstrip()}" def test_flach_fuer_lisp_parser(self, spec_objekte): """ssg-parse-json-array liest zeilenweise: ein "{" beginnt ein Objekt, Verschachtelung ist unmoeglich. Gilt auch fuer die von Hand ergaenzten Anlagenteile.""" for obj in spec_objekte: for k, v in obj.items(): assert not isinstance(v, dict), \ f"{k} ist verschachtelt - der LISP-Parser kann das nicht" if isinstance(v, list): assert all(isinstance(x, (int, float)) for x in v), \ f"{k} darf nur Zahlen enthalten" # ============================================================ # 3. Validierung: die Spec darf nichts raten # ============================================================ class TestValidierung: def _spec(self, spec_objekte, nr=0): return json.loads(json.dumps( vse.spec_aus_flachen_objekten(spec_objekte)[nr][1])) def test_hz_in_spaeterer_sektion_ist_erlaubt(self, spec_objekte): """War frueher ein Fehler (Annahme: hz nur beim allerersten Segment der Kette) - zwei echte Aufzeichnungen (linienzug_tests.json UND -stufe2.json, beide 2026-09-12) widerlegen das: der heutige Ablauf journalisiert die Richtung bei JEDEM Segment neu, auch nach einer Vario-Kurve. Ein hz in einer spaeteren Sektion ist darum kein Spec-Fehler mehr, sondern ein gueltiges, self-describing Feld - SpecSchreiber.linie emittiert es einfach (siehe dortiger Kommentar).""" spec = self._spec(spec_objekte) spec["sektionen"][1]["hz"] = 90.0 tok = vse.journal_aus_spec(spec) assert ("REAL", "90.0") in tok def test_fehlendes_hz_ist_fehler(self, spec_objekte): spec = self._spec(spec_objekte) del spec["sektionen"][0]["hz"] with pytest.raises(ValueError, match="hz"): vse.journal_aus_spec(spec) def test_gf_bogen_als_erste_sektion_ist_fehler(self, spec_objekte): """Die erste Sektion laeuft ohne Frame - ein GF-Bogen wird dort nicht angeboten (es gibt noch keine Richtung, an die er anschliesst).""" spec = self._spec(spec_objekte) spec["sektionen"].insert(0, {"glied": "GF-Bogen", "winkel": 90, "seite": "links"}) with pytest.raises(ValueError, match="nicht waehlbar"): vse.journal_aus_spec(spec) def test_unbekannter_domaenenwert_ist_fehler(self, spec_objekte): """Ein unbekannter Domaenenwert fuer "seite" muss scheitern - egal ob er an einer Top-Level-Sektion (ES-Glied) oder an einem Sub (Vario-Kurve) steht. Sucht sich die erste Stelle mit einem "seite"-Feld, statt eine feste Position anzunehmen - welche Sektion eines liefert, haengt an der Struktur der konkreten Kette.""" spec = self._spec(spec_objekte) ziel = None for sek in spec["sektionen"]: if "seite" in sek: ziel = sek break for sub in (sek.get("vf") or {}).get("subs", []): if "seite" in sub: ziel = sub break if ziel: break assert ziel is not None, "keine Sektion/Sub mit Feld \"seite\" gefunden" ziel["seite"] = "mitte" with pytest.raises(ValueError, match="seite"): vse.journal_aus_spec(spec) def test_unterminierte_vf_einheit_ist_fehler(self, spec_objekte): spec = self._spec(spec_objekte) for sek in spec["sektionen"]: if sek.get("vf"): sek["vf"]["subs"] = sek["vf"]["subs"][:-1] break with pytest.raises(ValueError, match="motorstation|kettenende"): vse.journal_aus_spec(spec) def test_fehlendes_pflichtfeld_ist_fehler(self, spec_objekte): spec = self._spec(spec_objekte) del spec["start"]["hoehe"] with pytest.raises(ValueError, match="hoehe"): vse.journal_aus_spec(spec) # ============================================================ # 4. Die erwarteten Tokenfolgen in tests/test_vf_spec.lsp # ============================================================ # tests/test_vf_spec.lsp prueft den LISP-Emitter gegen von Hand # eingetragene Tokenfolgen. Diese Folgen sind mit dem hier geprueften # Python-Emitter erzeugt worden - und genau das haelt dieser Test fest: # eine Uebertragungsfehler im LISP-Test wuerde sonst einen falschen # Erwartungswert festschreiben, und der CAD-Lauf waere gruen, obwohl der # Emitter falsch liegt. # # Die Specs stehen dafuer zweimal da (hier und im LISP-Test). Das ist # gewollt: nur zwei unabhaengige Niederschriften koennen einander pruefen. LISP_TEST = os.path.join(HIER, "test_vf_spec.lsp") _PRE_MIT_AS = [("PT", None), ("REAL", 3000.0), ("STR", "1"), ("STR", "90"), ("STR", "1")] _PRE_OHNE_AS = [("PT", None), ("REAL", 3000.0), ("STR", "2")] def _start(as_=True): s = {"punkt": [1000.0, 2000.0, 3000.0], "hoehe": 3000.0, "as": as_} if as_: s["as_winkel"] = "90" s["as_seite"] = "links" return s def _es(winkel="90", seite="rechts"): return {"glied": "ES", "winkel": winkel, "seite": seite} # Name im LISP-Test (Anfang genuegt) -> Spec LISP_FAELLE = { "Linie-GF + ES": { "start": _start(), "sektionen": [ {"glied": "Linie-GF", "laenge_modus": "endpunkt", "dl": 5500.0, "hz": 0.0, "sep_vor": False, "sep_nach": False, "gefaelle": "winkel", "winkel": 3.0, "ende": "ja-mit-es"}, _es()]}, "Linie-GF, Zielhoehe zuerst": { "start": _start(), "sektionen": [ {"glied": "Linie-GF", "laenge_modus": "hoehe", "hz": 0.0, "sep_vor": True, "sep_nach": False, "hoehe": 1800.0, "winkel": 3.0, "ende": "nein"}]}, "Linie-GF, Steigend + Endpunkt": { "start": _start(), "sektionen": [ {"glied": "Linie-GF", "laenge_modus": "steigend", "steigend_modus": "endpunkt", "dl": 800.0, "hz": 0.0, "winkel": 2.0, "ende": "nein"}]}, "Linie-GF, Steigend + Zielhoehe": { "start": _start(), "sektionen": [ {"glied": "Linie-GF", "laenge_modus": "steigend", "steigend_modus": "hoehe", "hz": 0.0, "hoehe": 1810.0, "winkel": 1.5, "ende": "nein"}]}, "Horizontal-VF + Motorstation + ES": { "start": _start(), "sektionen": [ {"glied": "Horizontal-VF", "dl": 8000.0, "hz": 90.0, "vf": {"gf_verteilung": "einlauf", "sep_vor": False, "sep_nach": False, "ende": "ja-mit-es", "subs": [{"sub": "motorstation"}]}}, _es()]}, "Linie-VF + Vario-Kurve": { "start": _start(as_=False), "sektionen": [ {"glied": "Linie-VF", "dl": 12000.0, "hz": 0.0, "hoehe": 2000.0, "winkel_idx": 2, "vf": {"gf_verteilung": "haelfte", "ende": "nein", "separator": True, "subs": [{"sub": "vario-kurve", "winkel": 90, "seite": "links", "variante": "aussen"}, {"sub": "motorstation"}]}}]}, "GF-Bogen als zweite Sektion": { "start": _start(), "sektionen": [ # laenge_modus/sep_vor/sep_nach fehlen absichtlich (Default # "endpunkt"/nein/nein, deckt alte Specs ab). {"glied": "Linie-GF", "dl": 3000.0, "hz": 180.0, "gefaelle": "hoehe", "hoehe": 2500.0, "ende": "nein"}, {"glied": "GF-Bogen", "winkel": 60, "seite": "rechts"}]}, "Linie -> GF + ES": { "start": _start(), "sektionen": [ {"glied": "Linie", "dl": 4000.0, "hz": 270.0, "hoehe": 2000.0, "segment_typ": "GF"}, _es("30", "links")]}, "Linie -> VF (auto-Ende": { "start": _start(), "sektionen": [ {"glied": "Linie", "dl": 9000.0, "hz": 0.0, "hoehe": 1500.0, "segment_typ": "VF", "vf": {"gf_verteilung": "einlauf", "ende": "automatisch", "subs": [{"sub": "auf-ab", "dl": 3000.0, "hoehe": 1200.0}, {"sub": "motorstation"}]}}, _es()]}, "Kettenende am Zielpunkt ohne ES": { "start": _start(), "sektionen": [ {"glied": "Horizontal-VF", "dl": 7000.0, "hz": 0.0, "vf": {"gf_verteilung": "einlauf", "sep_vor": True, "sep_nach": True, "ende": "zielpunkt-ohne-es", "subs": [{"sub": "kettenende", "es": False, "dl": 2500.0, "hoehe": 1000.0}]}}]}, } def _lisp_erwartungen(): """Die Erwartungswerte aus den tsp-journal-eq-Aufrufen des LISP-Tests lesen: {Testname: [(Art, Wert), ...]}.""" import re if not os.path.isfile(LISP_TEST): pytest.skip("tests/test_vf_spec.lsp fehlt") with open(LISP_TEST, encoding="cp1252") as fh: text = fh.read() # LISP-Kommentare weg (die Erwartungslisten sind kommentiert) text = re.sub(r";[^\n]*", "", text) raus = {} for treffer in re.finditer(r'\(tsp-journal-eq\s+"([^"]+)"(.*?)' r'\(vsp-journal spec\)\)', text, re.S): name, block = treffer.group(1), treffer.group(2) tok = [] if "tsp-pre-mit-as" in block: tok.extend(_PRE_MIT_AS) elif "tsp-pre-ohne-as" in block: tok.extend(_PRE_OHNE_AS) for art, wert in re.findall(r'\(cons\s+"([A-Z]+)"\s+' r'("[^"]*"|-?[\d.]+)\)', block): if wert.startswith('"'): tok.append((art, wert.strip('"'))) else: tok.append((art, float(wert) if "." in wert else int(wert))) raus[name] = tok return raus def _norm(tok): """Python-Tokens (Art, Text) in die Vergleichsform bringen: PT-Werte interessieren hier nicht (sie stehen im LISP-Test in der Praeambel- Hilfsfunktion), Zahlen numerisch.""" raus = [] for art, wert in tok: if art == "PT": raus.append(("PT", None)) elif art in ("REAL", "DL"): raus.append((art, float(wert))) elif art == "INT": raus.append((art, int(wert))) else: raus.append((art, wert)) return raus class TestLispErwartungen: def test_alle_faelle_gefunden(self): erwartungen = _lisp_erwartungen() assert len(erwartungen) == 10, \ f"10 tsp-journal-eq-Faelle erwartet, {len(erwartungen)} gefunden: " \ f"{sorted(erwartungen)}" @pytest.mark.parametrize("praefix", sorted(LISP_FAELLE)) def test_lisp_erwartung_stimmt(self, praefix): erwartungen = _lisp_erwartungen() passend = [n for n in erwartungen if n.startswith(praefix)] assert len(passend) == 1, \ f"{praefix!r} passt auf {passend} - Testname im LISP-Test " \ f"geaendert?" soll = _norm(vse.journal_aus_spec(LISP_FAELLE[praefix])) ist = erwartungen[passend[0]] assert ist == soll, ( f"Die Erwartung im LISP-Test weicht vom Python-Emitter ab.\n" f" LISP: {ist}\n Python: {soll}\n" f"Entweder ist die Tokenfolge im LISP-Test falsch uebertragen, " f"oder die Spec hier und dort beschreibt nicht dasselbe.")