From 3dd6e091e02f97e1b57d0693e0aede9e31b28293 Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Thu, 2 Jul 2026 12:43:23 +0200 Subject: [PATCH] =?UTF-8?q?Gef=C3=A4llestrecke=20nach=20dem=20gleichen=20M?= =?UTF-8?q?uster=20bedaten=20wie=20die=20anderen=20Tests.=20Eingabedaten?= =?UTF-8?q?=20aus=20dem=20Test=20in=20json=20Datei=20gezogen.=20Erg=C3=A4n?= =?UTF-8?q?zung=20des=20Testsfalls=20durch=20Erg=C3=A4nzung=20des=20testsd?= =?UTF-8?q?ata/*=5Ftests.json=20m=C3=B6glich?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 31 ++++ tests/test_foerderer.lsp | 16 +- tests/test_gefaellestrecke.py | 216 ++++++++++++++++++++++ tests/testdata/foerderer_tests.json | 2 +- tests/testdata/gefaellestrecke_tests.json | 59 ++++++ 5 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 tests/test_gefaellestrecke.py create mode 100644 tests/testdata/gefaellestrecke_tests.json diff --git a/tests/conftest.py b/tests/conftest.py index 9651941..045825b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,3 +123,34 @@ def kreisel_ref_dxf(): if not os.path.exists(path): pytest.skip("kreisel_ref.dxf nicht vorhanden - Referenz noch nicht erstellt") return ezdxf.readfile(path) + + +# --- Gefaellestrecke Fixtures --- + +@pytest.fixture +def gefaellestrecke_testdata(): + """Laedt die Gefaellestrecke-Testdefinitionen aus testdata/gefaellestrecke_tests.json.""" + path = os.path.join(_testdata_dir(), "gefaellestrecke_tests.json") + data = _load_json(path) + return [t for t in data if "test_id" in t] + + +@pytest.fixture +def gefaellestrecke_results(): + """Laedt die Gefaellestrecke-Ergebnisse aus output/gefaellestrecke_results.json.""" + path = os.path.join(_output_dir(), "gefaellestrecke_results.json") + if not os.path.exists(path): + pytest.skip("gefaellestrecke_results.json nicht vorhanden - TEST_GEFAELLESTRECKE in BricsCAD ausfuehren") + data = _load_json(path) + if not data: + pytest.skip("gefaellestrecke_results.json ist leer") + return data + + +@pytest.fixture +def gefaellestrecke_dxf(): + """Laedt die erzeugte Gefaellestrecke-DXF aus output/gefaellestrecke_tests.dxf.""" + path = os.path.join(_output_dir(), "gefaellestrecke_tests.dxf") + if not os.path.exists(path): + pytest.skip("gefaellestrecke_tests.dxf nicht vorhanden - TESTRUN in BricsCAD ausfuehren") + return ezdxf.readfile(path) diff --git a/tests/test_foerderer.lsp b/tests/test_foerderer.lsp index 1049bdd..8d740aa 100644 --- a/tests/test_foerderer.lsp +++ b/tests/test_foerderer.lsp @@ -75,6 +75,7 @@ y-offset y-abstand idx anz-gebaut anz-nicht-gebaut results-list startpunkt vf-nummer hoehe-von hoehe-bis lastEnt + sub-ent sub-typ tests-out-dir old-dxfm-results) ;; Bibliothek initialisieren (laedt Block-Library und extrahiert Masse) @@ -103,7 +104,7 @@ ;; Konfiguration aus erstem Eintrag (ohne test_id) (setq cfg (car testdaten)) (setq y-abstand (ssg-val cfg "y_abstand")) - (if (or (null y-abstand) (= y-abstand 0)) (setq y-abstand 400)) + (if (or (null y-abstand) (= y-abstand 0)) (setq y-abstand 800)) ;; Nur Eintraege mit test_id sind Testfaelle (setq tests @@ -174,6 +175,19 @@ hoehe-von hoehe-bis deltaH deltaL L_VF L_GF richtung best-winkel startpunkt lastEnt) + ;; lastEnt hinter den SEQEND des VF-Blocks schieben. + ;; Ohne diesen Schritt wuerde entnext vom INSERT-Entity + ;; in die ATTRIB-Kette des gerade erstellten VF-Blocks + ;; springen und diese beim naechsten _.-BLOCK-Aufruf + ;; in den neuen Block verschieben (Datenverlust). + (setq lastEnt (entlast)) + (setq sub-ent (entnext lastEnt)) + (while (and sub-ent + (not (equal (cdr (assoc 0 (entget sub-ent))) "SEQEND"))) + (setq lastEnt sub-ent) + (setq sub-ent (entnext lastEnt))) + (if sub-ent (setq lastEnt sub-ent)) + (setq results-list (cons (foerderer:test-result-json test-id richtung deltaL deltaH "GEBAUT" best-winkel L_GF L_VF) diff --git a/tests/test_gefaellestrecke.py b/tests/test_gefaellestrecke.py new file mode 100644 index 0000000..b2357b6 --- /dev/null +++ b/tests/test_gefaellestrecke.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_gefaellestrecke.py - Validiert GEFAELLESTRECKE Testergebnisse. + +Prueft: + 1. Status-Werte und Vollstaendigkeit (gefaellestrecke_results.json) + 2. Mathematische Invarianten (Hoehen, Winkel, deltaH) + 3. Konsistenz zwischen den Modi (Modus 1 Richtungsunabhaengigkeit, + Modus 3 uebernimmt deltaH/deltaL aus Modus 2) +""" + +import json +import os +import pytest + + +# --------------------------------------------------------------------------- +# Hilfsfunktionen +# --------------------------------------------------------------------------- + +def _testdata_dir(): + return os.getenv("DXFM_TESTDATA", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "testdata")) + + +def _output_dir(): + return os.getenv("DXFM_TESTOUT", + os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")) + + +def _load_json(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def _load_gefaellestrecke_results(): + path = os.path.join(_output_dir(), "gefaellestrecke_results.json") + if not os.path.exists(path): + pytest.skip( + "gefaellestrecke_results.json nicht vorhanden - " + "TEST_GEFAELLESTRECKE in BricsCAD ausfuehren" + ) + data = _load_json(path) + if not data: + pytest.skip("gefaellestrecke_results.json ist leer") + return data + + +def _load_gefaellestrecke_testdata(): + path = os.path.join(_testdata_dir(), "gefaellestrecke_tests.json") + return _load_json(path) + + +# --------------------------------------------------------------------------- +# Klasse 1: Status und Vollstaendigkeit +# --------------------------------------------------------------------------- + +class TestGefaellestreckeStatus: + """Prueft Status-Werte und Vollstaendigkeit der Ergebnisse.""" + + @pytest.fixture(autouse=True) + def setup(self): + self.results = _load_gefaellestrecke_results() + testdata = _load_gefaellestrecke_testdata() + self.tests = [t for t in testdata if "test_id" in t] + self.results_by_id = {r["test_id"]: r for r in self.results} + + def test_alle_testfaelle_vorhanden(self): + """Alle Testfaelle muessen in den Ergebnissen vorhanden sein.""" + expected_ids = {t["test_id"] for t in self.tests} + result_ids = {r["test_id"] for r in self.results} + missing = expected_ids - result_ids + assert not missing, f"Fehlende Testfaelle in Ergebnissen: {missing}" + + def test_status_werte_gueltig(self): + """Status muss GEBAUT oder FEHLER (bzw. FEHLER_LINIE) sein.""" + gueltige_status = {"GEBAUT", "FEHLER", "FEHLER_LINIE"} + for r in self.results: + assert r["status"] in gueltige_status, ( + f'{r["test_id"]}: Unbekannter Status "{r["status"]}"' + ) + + def test_alle_modus1_gebaut(self): + """Alle vier Modus-1-Richtungen (hz=0/90/180/270) muessen GEBAUT sein.""" + modus1 = [r for r in self.results if r["modus"] == 1] + assert len(modus1) == 4, f"Erwartet 4 Modus-1-Testfaelle, gefunden {len(modus1)}" + for r in modus1: + assert r["status"] == "GEBAUT", ( + f'{r["test_id"]}: erwartet GEBAUT, ist "{r["status"]}"' + ) + + def test_modus2_und_modus3_gebaut(self): + """Modus 2 und Modus 3 muessen GEBAUT sein.""" + for test_id in ("M2_hz000", "M3_hz270"): + r = self.results_by_id.get(test_id) + assert r is not None, f"{test_id} fehlt in den Ergebnissen" + assert r["status"] == "GEBAUT", ( + f'{test_id}: erwartet GEBAUT, ist "{r["status"]}"' + ) + + def test_mindestens_ein_gebaut(self): + """Mindestens ein Testfall muss GEBAUT sein.""" + gebaut = [r for r in self.results if r["status"] == "GEBAUT"] + assert len(gebaut) > 0, "Keine einzige Gefaellestrecke konnte gebaut werden" + + +# --------------------------------------------------------------------------- +# Klasse 2: Mathematische Invarianten +# --------------------------------------------------------------------------- + +class TestGefaellestreckeMathematik: + """Prueft mathematische Korrektheit der berechneten Werte.""" + + @pytest.fixture(autouse=True) + def setup(self): + self.results = _load_gefaellestrecke_results() + self.gebaut = [r for r in self.results if r["status"] == "GEBAUT"] + self.results_by_id = {r["test_id"]: r for r in self.results} + + def test_gebaut_hat_pflichtfelder(self): + """GEBAUT-Ergebnisse muessen alle Kernfelder enthalten.""" + pflichtfelder = { + "modus", "hz_grad", "deltaL_mm", "winkel_grad", + "hoehe_von_mm", "hoehe_bis_mm", "delta_h_mm", + } + for r in self.gebaut: + missing = pflichtfelder - set(r.keys()) + assert not missing, f'{r["test_id"]}: fehlende Felder {missing}' + + def test_deltaL_positiv(self): + """deltaL_mm muss fuer alle GEBAUT-Faelle groesser als 0 sein.""" + for r in self.gebaut: + assert r["deltaL_mm"] > 0, ( + f'{r["test_id"]}: deltaL_mm={r["deltaL_mm"]} muss > 0 sein' + ) + + def test_winkel_positiv(self): + """Der berechnete Gefaellewinkel muss groesser als 0 Grad sein.""" + for r in self.gebaut: + assert r["winkel_grad"] > 0, ( + f'{r["test_id"]}: winkel_grad={r["winkel_grad"]} muss > 0 sein' + ) + + def test_delta_h_konsistent_mit_hoehen(self): + """delta_h_mm muss der absoluten Differenz von hoehe_von/hoehe_bis entsprechen.""" + for r in self.gebaut: + erwartet = abs(r["hoehe_von_mm"] - r["hoehe_bis_mm"]) + assert erwartet == r["delta_h_mm"], ( + f'{r["test_id"]}: delta_h_mm={r["delta_h_mm"]}, ' + f'erwartet |{r["hoehe_von_mm"]}-{r["hoehe_bis_mm"]}|={erwartet}' + ) + + def test_hoehe_bis_kleiner_hoehe_von(self): + """Bei einer Gefaellestrecke muss die Endhoehe unter der Starthoehe liegen.""" + for r in self.gebaut: + assert r["hoehe_bis_mm"] < r["hoehe_von_mm"], ( + f'{r["test_id"]}: hoehe_bis_mm={r["hoehe_bis_mm"]} ' + f'nicht < hoehe_von_mm={r["hoehe_von_mm"]}' + ) + + +# --------------------------------------------------------------------------- +# Klasse 3: Konsistenz zwischen den Modi +# --------------------------------------------------------------------------- + +class TestGefaellestreckeModusKonsistenz: + """Prueft Zusammenhaenge zwischen Modus 1/2/3 (Richtungsunabhaengigkeit, + Uebernahme von deltaH/deltaL aus Modus 2 in Modus 3).""" + + # Toleranz fuer die 2-Pass-Winkelkorrektur in Modus 3 (mm) + DELTA_H_TOLERANZ_MM = 5 + + @pytest.fixture(autouse=True) + def setup(self): + self.results = _load_gefaellestrecke_results() + self.results_by_id = {r["test_id"]: r for r in self.results} + self.testdata = _load_gefaellestrecke_testdata() + self.tests_by_id = {t["test_id"]: t for t in self.testdata if "test_id" in t} + + def test_modus1_richtungsunabhaengig(self): + """Alle vier Modus-1-Richtungen muessen dieselbe Hoehe/Winkel liefern, + da nur die Fahrtrichtung (hz), nicht die Geometrie variiert.""" + modus1 = [r for r in self.results + if r["modus"] == 1 and r["status"] == "GEBAUT"] + assert len(modus1) >= 2, "Zu wenige Modus-1-Ergebnisse fuer Vergleich" + referenz = modus1[0] + for r in modus1[1:]: + assert r["delta_h_mm"] == referenz["delta_h_mm"], ( + f'{r["test_id"]}: delta_h_mm={r["delta_h_mm"]} weicht von ' + f'{referenz["test_id"]}={referenz["delta_h_mm"]} ab (Richtungsabhaengigkeit!)' + ) + assert r["winkel_grad"] == referenz["winkel_grad"], ( + f'{r["test_id"]}: winkel_grad={r["winkel_grad"]} weicht von ' + f'{referenz["test_id"]}={referenz["winkel_grad"]} ab' + ) + + def test_modus3_uebernimmt_deltaH_aus_modus2(self): + """Modus 3 (Linienzug mit 2-Pass-Korrektur) muss innerhalb einer kleinen + Toleranz dieselbe Hoehendifferenz wie die Quelle aus Modus 2 erreichen.""" + m3_test = self.tests_by_id.get("M3_hz270") + if m3_test is None: + pytest.skip("M3_hz270 nicht in Testdaten definiert") + quelle_id = m3_test.get("deltaH_quelle") + m2 = self.results_by_id.get(quelle_id) + m3 = self.results_by_id.get("M3_hz270") + if m2 is None or m3 is None: + pytest.skip("M2- oder M3-Ergebnis fehlt") + if m2["status"] != "GEBAUT" or m3["status"] != "GEBAUT": + pytest.skip("M2 oder M3 nicht GEBAUT") + abweichung = abs(m3["delta_h_mm"] - m2["delta_h_mm"]) + assert abweichung <= self.DELTA_H_TOLERANZ_MM, ( + f'M3_hz270: delta_h_mm={m3["delta_h_mm"]} weicht um {abweichung} mm ' + f'von {quelle_id}={m2["delta_h_mm"]} ab ' + f'(Toleranz {self.DELTA_H_TOLERANZ_MM} mm)' + ) diff --git a/tests/testdata/foerderer_tests.json b/tests/testdata/foerderer_tests.json index 84e7692..e306055 100644 --- a/tests/testdata/foerderer_tests.json +++ b/tests/testdata/foerderer_tests.json @@ -1,6 +1,6 @@ [ { - "y_abstand": 400 + "y_abstand": 600 }, { "test_id": "VF_Auf_0", diff --git a/tests/testdata/gefaellestrecke_tests.json b/tests/testdata/gefaellestrecke_tests.json new file mode 100644 index 0000000..00eb37c --- /dev/null +++ b/tests/testdata/gefaellestrecke_tests.json @@ -0,0 +1,59 @@ +[ + { + "deltaL_mm": 10000, + "winkel_grad": 3.0, + "z_start_mm": 5000 + }, + { + "test_id": "M1_hz000", + "modus": 1, + "hz_grad": 0.0, + "start_mm": [0, 0, 5000], + "as_seite": "links", + "es_seite": "links" + }, + { + "test_id": "M1_hz090", + "modus": 1, + "hz_grad": 90.0, + "start_mm": [15000, 0, 5000], + "as_seite": "links", + "es_seite": "links" + }, + { + "test_id": "M1_hz180", + "modus": 1, + "hz_grad": 180.0, + "start_mm": [0, -15000, 5000], + "as_seite": "links", + "es_seite": "links" + }, + { + "test_id": "M1_hz270", + "modus": 1, + "hz_grad": 270.0, + "start_mm": [15000, -15000, 5000], + "as_seite": "links", + "es_seite": "links" + }, + { + "test_id": "M2_hz000", + "modus": 2, + "hz_grad": 0.0, + "start_mm": [0, -35000, 5000], + "linie_start_mm": [0, -35000, 0], + "linie_ende_mm": [10000, -35000, 0], + "as_seite": "links", + "es_seite": "links" + }, + { + "test_id": "M3_hz270", + "modus": 3, + "richtung": "Sued", + "start_mm": [0, -50000, 5000], + "deltaH_quelle": "M2_hz000", + "deltaL_quelle": "M2_hz000", + "as_seite": "links", + "es_seite": "links" + } +]