Vorwärtskanten weichen Elementen über freie Linien aus (#22)

- build_bpmn() prüft jeden Standardweg einer Vorwärtskante gegen alle
  fremden Elemente. Nur bei einer Kreuzung sucht umweg_points() einen
  rechtwinkligen Weg von Stub zu Stub über freie Linien (Lückenmitten
  und 15 neben jedem Element), erst mit einem, dann mit zwei
  Zwischenknicken; der kürzeste gewinnt. Saubere Kanten bleiben
  byte-gleich, die Beispiel-.bpmn ändern sich nicht.
- Belastungstest aus #12 als dauerhafter Test (126 Varianten: keine
  Kreuzung, rechtwinklige Anschlüsse), Test für das Beispiel aus dem
  Issue und zwei Einzeltests für umweg_points().
- Im Belastungstest werden 23 Kanten umgeleitet (3 bis 4
  Zwischenpunkte); Kreuzungen vorher 24, jetzt 0.
- Tests: python -m pytest -q -> 253 bestanden, 1 übersprungen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 19:36:52 +02:00
co-authored by Claude Opus 5.5
parent 12152eb3b0
commit f3b291c6fc
5 changed files with 166 additions and 0 deletions
+52
View File
@@ -275,6 +275,58 @@ def test_kanten_setzen_an_aufgaben_rechtwinklig_an(md):
assert funde == [], "nicht rechtwinklig (Kante, Element): " + ", ".join(f"{k} → {e}" for k, e in funde)
# Belastungstest (#12, #22): kleine Modelle, in die je eine zusätzliche Kante "zurück -> <früherer Schritt>" eingefügt
# wird – für alle Paare, ohne Lanes, mit einer Lane und mit zwei abwechselnden Lanes. Querverbindungen zwischen
# Geschwisterzweigen verschieben dabei das Layout (eine Aufgabe rutscht eine Ebene tiefer).
BELASTUNGSMODELLE = {
"drei": ["S1 Start: A", " -> S2", "S2 Gateway XOR: W?", " a -> S3", " b -> S4", " c -> S5",
"S3 Task: Links", " -> S6", "S4 Task: Mitte", " -> S6", "S5 Task: Rechts", " -> S6",
"S6 Gateway XOR: Z", " -> S7", "S7 Task: Danach", " -> S8", "S8 Ende: E"],
"parallel": ["S1 Start: A", " -> S2", "S2 Task: Erfassen", " -> S3", "S3 Gateway AND: T", " -> S4", " -> S5",
"S4 Task: L", " -> S6", "S5 Task: R", " -> S6", "S6 Gateway AND: Z", " -> S7",
"S7 Gateway XOR: Ok?", " Ja -> S8", "S8 Ende: E"],
}
def _mit_lanes(zeilen, lane_von):
return [z.replace(": ", f": {lane_von(z.split()[0])}: ", 1) if z[:1] == "S" else z for z in zeilen]
def _belastungsfaelle():
faelle = []
for name, zeilen in BELASTUNGSMODELLE.items():
ids = [z.split()[0] for z in zeilen if z[:1] == "S"]
for quelle in ids[:-1]:
for ziel in ids[1:ids.index(quelle) + 1]:
mit = []
for z in zeilen:
mit.append(z)
if z.startswith(quelle + " "):
mit.append(f" zurück -> {ziel}")
for variante, zz in (("ohne", mit), ("1lane", _mit_lanes(mit, lambda s: "A")),
("2lanes", _mit_lanes(mit, lambda s: "A" if int(s[1:]) % 2 else "B"))):
faelle.append(pytest.param("\n".join(zz), id=f"{name}-{quelle}-{ziel}-{variante}"))
return faelle
@pytest.mark.parametrize("text", _belastungsfaelle())
def test_belastung_keine_kante_durch_ein_element_und_rechtwinklig(text):
"""Prüft an allen Varianten des Belastungstests, dass keine Kante durch ein Element läuft und jede Kante an Aufgaben
rechtwinklig ansetzt – auch wenn eine Querverbindung zwischen Zweigen das Layout verschiebt (Issue #22)."""
defs = m.build_bpmn(*m.parse_markdown(text))
assert kreuzungen(defs) == []
assert schraege_anschluesse(defs) == []
def test_beispiel_aus_issue_22_ohne_kreuzung():
"""Prüft das Beispiel aus Issue #22: Sprung von hinten in den rechten Zweig; die Kante vom Gateway zu diesem Zweig
läuft nicht durch die linke Aufgabe."""
zeilen = list(BELASTUNGSMODELLE["drei"])
zeilen.insert(zeilen.index("S7 Task: Danach") + 1, " zurück -> S5")
text = "\n".join(zeilen)
assert kreuzungen(m.build_bpmn(*m.parse_markdown(text))) == []
def test_kreuzungspruefung_findet_eine_kante_durch_eine_aufgabe():
"""Prüft die Prüfung selbst: eine absichtlich durch eine Aufgabe gelegte Kante wird gefunden, eine daneben nicht."""
defs = baue(SKILL / "beispiele" / "beispiel_prozess.md")