428f1ced70
Stufe 1 des Fahrplans in doc/TODO-plan-vf-interactive.md, fertig fuer
Modus 1. Eine SPEC beschreibt eine Kette in Domaenenwerten ("links",
"winkel", "aussen", ja/nein) statt in Menue-Codes; vsp-journal uebersetzt sie
in ein Eingabe-Journal, vsp-bau-aus-spec spielt es per
vfl-journal-replay-start durch den UNVERAENDERTEN vf-linienzug-modus. Der
fertige Block traegt damit dasselbe SSG_VF_EDIT-Journal wie eine handgebaute
Kette und bleibt per Doppelklick editier- und 2D/3D-konvertierbar - kein
Journal-Konsument muss etwas von Specs wissen.
CAD-Lauf VF_SPEC_BAU: 5 OK, 0 Fehler. Je Kette status "executed",
prompts 0 (keine Live-Eingabe erreicht), eingaben_offen 0, keine
Spec-Fehler, keine Bau-Meldungen, glieder_ist == glieder_soll,
Einfuegepunkt gleich dem Kettenstart der Spec.
Neu:
- Lisp/vf_spec.lsp: Code-Tabellen, Emitter vsp-journal, Validierung
vsp-pruefen, flacher JSON-Loader, Headless-Rahmen vsp-headless-an/-aus,
Ergebnis-Record, vsp-bau-aus-spec/-liste, c:VF_SPEC_BAU. Von vf_core.lsp
NACH vf_linienzug.lsp geladen - damit sind beide Ladewege abgedeckt (Menue
und der .scr-Loader), das MNL bleibt unveraendert.
- lib/vf_spec_export.py: derselbe Emitter in Python PLUS die Umkehrung
(spec_aus_journal), erzeugt die Spec-Testdaten.
- tests/testdata/vf_spec_hundm05.json: die 5 echten Ketten als Spec,
erzeugt statt handgeschrieben.
- tests/test_vf_spec.lsp (C:TEST_VF_SPEC, in alltests.json): 7 Faelle je
Glied-Typ, 7 Validierungsfaelle, Gruppieren, und die 5 echten Ketten gegen
ihr aufgezeichnetes Journal.
- tests/test_vf_spec.py: 36 Tests (Rundlauf, Spec-Daten, Validierung,
Erwartungswert-Abgleich, Ergebnisse von VF_SPEC_BAU).
Drei Regeln, die der Uebersetzer besitzen muss - jede wuerde sonst die ganze
Replay-Queue verschieben:
- Der Menue-Code haengt am Frame: erste Sektion 4 Optionen (kein GF-Bogen,
es gibt noch keine Richtung), jede spaetere 5.
- hz steht genau EINMAL im Journal, beim ersten Segment (vfl-in-abstand
journalisiert die Richtung nur bei freier Richtungswahl).
- Nur ein HORIZONTALER Erstkoerper einer VF-Einheit stellt die Separator-/
Endpunktfragen vorab (winkel1=0 laeuft durch vfl-baue-horizontal-koerper);
ein gewinkelter wird ohne jede Frage gebaut.
Wie die Richtigkeit belegt ist - drei Implementierungen derselben Grammatik,
die einander pruefen:
- Rundlauf gegen echte Daten: journal_aus_spec(spec_aus_journal(tok)) == tok
fuer alle 5 HundM-Ketten, tokenweise numerisch verglichen. Damit ist
bewiesen, dass die Spec das Journal verlustfrei abbildet.
- Der Weg, den LISP nimmt (flaches JSON -> Spec -> Journal), ergibt separat
geprueft dieselben Journale wie das aufgezeichnete Protokoll.
- Der unabhaengige Dekoder aus lib/vf_journal_export.py verdaut alle 7
synthetischen Emitter-Ausgaben restlos, auch die zwei Zweige, die in den
echten Daten fehlen (Linie-VF, gewinkelter Erstkoerper).
- Die Erwartungswerte im LISP-Test kommen aus dem geprueften Python-Emitter;
TestLispErwartungen liest sie zurueck und vergleicht erneut. Ohne das
koennte ein Uebertragungsfehler einen falschen Erwartungswert
festschreiben und der CAD-Lauf waere gruen, obwohl der Emitter falsch
liegt.
Abweichungen von der Planung (Begruendungen in doc/TODO-plan-vf-interactive.md
Abschnitt 4.6): vsp-pruefen ruft den Emitter statt eine zweite Regelmenge zu
pflegen; das vollstaendige Journal steht nicht im Record (statt dessen
glieder_soll/glieder_ist als Desync-Detektor); Zusatzfeld segment_typ am
Glied "Linie", weil dort vfl-segment-entscheidung selbst zwischen GF und VF
waehlt; vf_ende-Werte "automatisch"/"zielpunkt-ohne-es" fuer die Faelle ohne
Frage; Praefix vsp- statt vfs- (das gehoert vf_standard.lsp).
Zusaetzlich:
- tests/test_vf_headless_statisch.py prueft jetzt auch, dass
Lisp/vf_spec.lsp UEBERHAUPT keine Eingabe-/Dialogfunktion aufruft - beim
Daten-Pfad ist die Null die Vorgabe.
- tests/testdata/mubea.json: die drei einzeln eingefuegten S-LP-Separatoren
entfernt (Fortsetzung von 699744e - die Separatoren stecken jetzt in den
Staustreckenbloecken).
Noch offen: die Uebersetzer fuer Modus 2 und 3. Modus 2 braucht laut
Fahrplan erst 2-3 echte "linienzug2"-Journale als Referenz -
tests/testdata/hundm05.json enthaelt ausschliesslich Modus-1-Ketten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
342 lines
14 KiB
Python
342 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
test_vf_headless_statisch.py - Statischer Waechter fuer den nicht-interaktiven
|
|
Betrieb des VF-Linienzugs. Laeuft ohne BricsCAD und ohne Zeichnung.
|
|
|
|
Warum statisch: die dynamischen Beweise (Ergebnis-Record mit "prompts": 0,
|
|
"headless_fehler": null in tests/output/hundm05_results.json) sagen nur, dass
|
|
der EINE getestete Ablauf sauber lief. Sie schlagen nicht an, wenn jemand
|
|
einem selten erreichten Zweig ein neues (alert ...) oder ein rohes
|
|
(getreal ...) hinzufuegt. Genau das haelt dieser Test fest: die Fundstellen
|
|
sind eingecheckt, jede neue muss hier bewusst eingetragen werden.
|
|
|
|
Hintergrund und Fahrplan: doc/TODO-plan-vf-interactive.md,
|
|
Mechanik: doc/variofoerderer.md Abschnitt 8b.
|
|
"""
|
|
|
|
import collections
|
|
import io
|
|
import os
|
|
|
|
import pytest
|
|
|
|
|
|
LISP = os.path.join(os.path.dirname(__file__), "..", "Lisp",
|
|
"vf_linienzug.lsp")
|
|
CORE = os.path.join(os.path.dirname(__file__), "..", "Lisp", "vf_core.lsp")
|
|
|
|
# Gesuchte Aufrufe: alles, was den Anwender fragt oder ihm ein Fenster zeigt.
|
|
GESUCHT = ("alert", "getstring", "getint", "getreal", "getpoint", "getkword",
|
|
"ssget", "entsel", "new_dialog")
|
|
|
|
|
|
# ============================================================
|
|
# Minimaler LISP-Scanner
|
|
# ============================================================
|
|
# Kein voller Parser: er braucht nur die Klammertiefe (fuer die Grenzen der
|
|
# Top-Level-Formen) und muss Strings sowie Kommentare ueberspringen, damit ein
|
|
# "alert" im Kommentar oder in einem i18n-Text nicht mitgezaehlt wird.
|
|
|
|
def funktions_inventar(quelltext, gesucht=GESUCHT):
|
|
"""{Funktionsname: Counter({Aufruf: Anzahl})} fuer alle Top-Level-defuns.
|
|
Code ausserhalb eines defun laeuft unter dem Namen "<top-level>".
|
|
gesucht = die Aufrufnamen, die gezaehlt werden."""
|
|
inventar = collections.defaultdict(collections.Counter)
|
|
tiefe = 0
|
|
aktuell = "<top-level>"
|
|
i = 0
|
|
n = len(quelltext)
|
|
while i < n:
|
|
c = quelltext[i]
|
|
if c == ";": # Kommentar bis Zeilenende
|
|
while i < n and quelltext[i] != "\n":
|
|
i += 1
|
|
continue
|
|
if c == '"': # String ueberspringen
|
|
i += 1
|
|
while i < n:
|
|
if quelltext[i] == "\\":
|
|
i += 2
|
|
continue
|
|
if quelltext[i] == '"':
|
|
i += 1
|
|
break
|
|
i += 1
|
|
continue
|
|
if c == "(":
|
|
wort = _wort_nach(quelltext, i + 1)
|
|
if tiefe == 0:
|
|
aktuell = ("<top-level>" if wort != "defun"
|
|
else _wort_nach(quelltext,
|
|
i + 1 + len("defun") + 1))
|
|
if wort in gesucht:
|
|
inventar[aktuell][wort] += 1
|
|
tiefe += 1
|
|
i += 1
|
|
continue
|
|
if c == ")":
|
|
tiefe -= 1
|
|
if tiefe <= 0:
|
|
tiefe = 0
|
|
aktuell = "<top-level>"
|
|
i += 1
|
|
continue
|
|
i += 1
|
|
return inventar
|
|
|
|
|
|
def _wort_nach(text, pos):
|
|
"""Erstes Symbol ab pos (Leerraum davor wird uebersprungen)."""
|
|
while pos < len(text) and text[pos] in " \t\r\n":
|
|
pos += 1
|
|
ende = pos
|
|
while ende < len(text) and text[ende] not in " \t\r\n()\";'":
|
|
ende += 1
|
|
return text[pos:ende]
|
|
|
|
|
|
def _funktionskoerper(quelltext, name):
|
|
"""Quelltext einer Top-Level-Funktion (Rohtext, genuegt fuer eine
|
|
Namenssuche)."""
|
|
start = quelltext.find("(defun %s " % name)
|
|
if start < 0:
|
|
start = quelltext.find("(defun %s(" % name)
|
|
if start < 0:
|
|
return None
|
|
tiefe, i = 0, start
|
|
while i < len(quelltext):
|
|
if quelltext[i] == "(":
|
|
tiefe += 1
|
|
elif quelltext[i] == ")":
|
|
tiefe -= 1
|
|
if tiefe == 0:
|
|
return quelltext[start:i + 1]
|
|
i += 1
|
|
return quelltext[start:]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def quelltext():
|
|
if not os.path.isfile(LISP):
|
|
pytest.skip("Lisp/vf_linienzug.lsp nicht gefunden")
|
|
return io.open(LISP, encoding="cp1252").read()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def inventar(quelltext):
|
|
return funktions_inventar(quelltext)
|
|
|
|
|
|
# ============================================================
|
|
# 1. alert - nur noch in reinen Interaktiv-Pfaden
|
|
# ============================================================
|
|
# Alle Bau-Pfad-Alerts sind zu vfl-meldung geworden (P1 im Fahrplan): der Text
|
|
# wird gesammelt statt modal gezeigt, damit ein Batch-Lauf nicht blockiert und
|
|
# die Auskunft "welche Sektion war nicht baubar" nicht verloren geht.
|
|
# Stehen bleiben darf, was nur bei echter Benutzerbedienung entstehen kann -
|
|
# plus vfl-meldung selbst, das den Alert bei erlaubter GUI weiterhin zeigt.
|
|
ALERT_ERLAUBT = {
|
|
"vfl-meldung": 1, # zeigt den Text nur bei (ssg-gui-p)
|
|
"vfl-glied-gf-bogen-dialog": 1, # dcl/vf_linienzug_wizard.dcl fehlt
|
|
"vfl-glied-as-es-dialog": 1, # dito
|
|
"vfl-glied-vario-dialog": 1, # dito
|
|
"vfl-dlg-modus": 1, # dcl/vfl_edit.dcl fehlt
|
|
"vfl-dlg-position": 1, # dito
|
|
"vfl-edit-glied": 1, # "Glied nicht einzeln editierbar"
|
|
"vfl-edit-ent": 1, # "kein Journal am Block"
|
|
}
|
|
|
|
|
|
def test_alert_nur_in_interaktiv_pfaden(inventar):
|
|
ist = {name: z["alert"] for name, z in inventar.items() if z["alert"]}
|
|
neu = sorted(k for k in ist if k not in ALERT_ERLAUBT)
|
|
assert not neu, (
|
|
"Neue (alert ...)-Aufrufe in %s. Im Bau-Pfad statt alert vfl-meldung "
|
|
"verwenden (sammelt den Text und zeigt ihn nur bei erlaubter GUI). "
|
|
"Ist der Aufruf wirklich rein interaktiv, hier eintragen." % neu)
|
|
assert ist == ALERT_ERLAUBT, (
|
|
"Alert-Inventar veraendert: ist %s, erwartet %s" % (ist,
|
|
ALERT_ERLAUBT))
|
|
|
|
|
|
# ============================================================
|
|
# 2. Rohe Eingaben nur in den Wrappern und in Interaktiv-Befehlen
|
|
# ============================================================
|
|
# Vollstaendiges Soll-Inventar. Ein neues getreal/getpoint/ssget in einer
|
|
# Bau-Funktion faellt damit auf, auch wenn sein Zweig im Test nie erreicht
|
|
# wird. Kommentar je Eintrag = warum die Stelle in Ordnung ist.
|
|
EINGABE_ERLAUBT = {
|
|
# --- die Journal-Wrapper selbst: hier laeuft der Replay durch ---
|
|
"vfl-getpoint": {"getpoint": 2},
|
|
"vfl-in-string": {"getstring": 1},
|
|
"vfl-in-int": {"getint": 1},
|
|
"vfl-in-real": {"getreal": 1},
|
|
"vfl-in-selection": {"ssget": 1},
|
|
"vfl-winkel-live": {"getint": 1}, # livefn fuer vfl-in-value
|
|
# --- Wizard-Schicht (DCL); bei ssg-gui-aus greift sie nicht ---
|
|
"vflw-zahl": {"getreal": 1}, # Fallback ohne Dialog
|
|
"vflw-zahl-impl": {"new_dialog": 1},
|
|
"vflw-wahl-impl": {"new_dialog": 1},
|
|
"vflw-gruppe-as-impl": {"new_dialog": 1},
|
|
"vflw-gruppe-gefaelle-impl": {"new_dialog": 1},
|
|
"vflw-gruppe-horizontal-impl": {"new_dialog": 1},
|
|
"vflw-gruppe-punkt-hoehe-impl": {"new_dialog": 1},
|
|
"vflw-gruppe-variokurve-impl": {"new_dialog": 1},
|
|
"vflw-gruppe-winkel-seite-impl": {"new_dialog": 1},
|
|
"vflw-gruppe-ziel-hoehe-impl": {"new_dialog": 1},
|
|
"vflw-seg-bogen-impl": {"new_dialog": 1},
|
|
"vflw-seg-linie-m2-impl": {"new_dialog": 1},
|
|
"vflw-seg-linie-m3-impl": {"new_dialog": 1},
|
|
# --- Edit-Dialoge: nur per Doppelklick erreichbar ---
|
|
"vfl-dlg-modus": {"new_dialog": 1, "alert": 1},
|
|
"vfl-dlg-position": {"new_dialog": 1, "alert": 1},
|
|
"vfl-glied-as-es-dialog": {"new_dialog": 1, "alert": 1},
|
|
"vfl-glied-gf-bogen-dialog": {"new_dialog": 1, "alert": 1},
|
|
"vfl-glied-vario-dialog": {"new_dialog": 1, "alert": 1},
|
|
"vfl-edit-ent": {"alert": 1},
|
|
"vfl-edit-glied": {"alert": 1},
|
|
"vfl-meldung": {"alert": 1},
|
|
# --- reine Benutzer-Befehle (kein Bau-Pfad, nicht journalisiert) ---
|
|
"vfl-praeambel-erfragen": {"getpoint": 1, "getreal": 1, "getkword": 2},
|
|
"c:VF_SEKTION_RESTORE": {"ssget": 1},
|
|
"c:VFL_KS_DIAG": {"getstring": 1},
|
|
"c:Vario_Kette_Merge": {"entsel": 1},
|
|
# --- Zeichnung durchsuchen, keine Benutzerfrage ---
|
|
"vfl-kette-sammle-alle": {"ssget": 1},
|
|
}
|
|
|
|
|
|
def test_eingabe_fundstellen_unveraendert(inventar):
|
|
ist = {name: dict(z) for name, z in inventar.items() if z}
|
|
neu = sorted(k for k in ist if k not in EINGABE_ERLAUBT)
|
|
assert not neu, (
|
|
"Neue Eingabe-/Dialog-Aufrufe in %s. Im Bau-Pfad gehoeren Eingaben "
|
|
"durch die vfl-in-*-Wrapper (Journal + Replay + Headless-Riegel), "
|
|
"nicht direkt an get*/ssget. Ist die Stelle rein interaktiv, hier "
|
|
"eintragen." % neu)
|
|
weg = sorted(k for k in EINGABE_ERLAUBT if k not in ist)
|
|
assert not weg, ("Erwartete Fundstellen verschwunden (umbenannt?): %s"
|
|
% weg)
|
|
abweichung = {k: (ist[k], EINGABE_ERLAUBT[k]) for k in ist
|
|
if ist[k] != EINGABE_ERLAUBT[k]}
|
|
assert not abweichung, ("Anzahl der Aufrufe geaendert (ist, erwartet): %s"
|
|
% abweichung)
|
|
|
|
|
|
# ============================================================
|
|
# 3. Der Headless-Riegel sitzt an allen Eingabe-Engstellen
|
|
# ============================================================
|
|
# Jede interaktive Eingabe des Linienzugs laeuft durch einen dieser Wrapper.
|
|
# Fehlt dort die Headless-Pruefung, faellt eine erschoepfte Replay-Queue
|
|
# wieder still auf Live-Eingabe zurueck - der Fehler, den P2 behoben hat.
|
|
RIEGEL_ERWARTET = {
|
|
"vfl-in-value": ["vfl-headless-p", "vfl-headless-notausgang"],
|
|
"vfl-in-value-p": ["vfl-headless-p", "vfl-headless-notausgang"],
|
|
"vfl-in-selection": ["vfl-headless-p", "vfl-headless-abbruch"],
|
|
"vfl-in-abstand": ["vfl-headless-p", "vfl-headless-abbruch"],
|
|
"vfl-view-refresh": ["vfl-headless-p"],
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(RIEGEL_ERWARTET))
|
|
def test_headless_riegel_vorhanden(quelltext, name):
|
|
koerper = _funktionskoerper(quelltext, name)
|
|
assert koerper, "Funktion %s nicht gefunden" % name
|
|
for gesucht in RIEGEL_ERWARTET[name]:
|
|
assert gesucht in koerper, (
|
|
"%s ruft %s nicht - ohne den Riegel fragt der Wrapper bei leerer "
|
|
"Replay-Queue wieder live (siehe doc/variofoerderer.md 8b)"
|
|
% (name, gesucht))
|
|
|
|
|
|
def test_headless_symbole_definiert(quelltext):
|
|
for gesucht in ("(defun vfl-headless-p ", "(defun vfl-headless-abbruch ",
|
|
"(defun vfl-headless-notausgang ",
|
|
"(defun vfl-headless-ort ", "(defun vfl-meldung ",
|
|
"*vfl-headless-antwort-fn*", "*vfl-meldungen*",
|
|
"*vfl-headless-fehler*"):
|
|
assert gesucht in quelltext, "%s fehlt" % gesucht
|
|
|
|
|
|
def test_journal_reset_loescht_diagnose(quelltext):
|
|
"""Meldungen und Headless-Diagnose gehoeren zum LAUF: bleiben sie stehen,
|
|
traegt der naechste Bau die Befunde des vorherigen im Ergebnis."""
|
|
koerper = _funktionskoerper(quelltext, "vfl-journal-reset")
|
|
assert koerper
|
|
assert "*vfl-meldungen*" in koerper
|
|
assert "*vfl-headless-fehler*" in koerper
|
|
|
|
|
|
# ============================================================
|
|
# 3b. vf_spec.lsp fragt ueberhaupt nichts
|
|
# ============================================================
|
|
# Das Spec-Modul ist der Daten-Pfad: es baut aus einer Datei und darf nie
|
|
# einen Prompt oder Dialog oeffnen. Anders als bei vf_linienzug.lsp (das den
|
|
# interaktiven Pfad ja weiter braucht) ist hier die Null die Vorgabe. Seine
|
|
# Stubs SETZEN getstring & Co. nur - sie rufen sie nicht auf, der Scanner
|
|
# zaehlt also korrekt null Treffer.
|
|
|
|
SPEC = os.path.join(os.path.dirname(__file__), "..", "Lisp", "vf_spec.lsp")
|
|
|
|
|
|
def test_vf_spec_ohne_eingaben():
|
|
if not os.path.isfile(SPEC):
|
|
pytest.skip("Lisp/vf_spec.lsp nicht gefunden")
|
|
inv = funktions_inventar(io.open(SPEC, encoding="cp1252").read())
|
|
treffer = dict((name, dict(z)) for name, z in inv.items() if z)
|
|
assert not treffer, (
|
|
"Lisp/vf_spec.lsp ruft Eingabe-/Dialogfunktionen auf: %s. Der "
|
|
"Daten-Pfad baut aus einer Datei und darf nichts fragen." % treffer)
|
|
|
|
|
|
# ============================================================
|
|
# 4. Funktionen, die es in AutoLISP nicht gibt
|
|
# ============================================================
|
|
# Ein Tippfehler oder eine erfundene Funktion faellt erst auf, wenn der Zweig
|
|
# tatsaechlich durchlaufen wird - im Testlauf hat genau das (entprev ...)
|
|
# gekostet: AutoLISP kennt nur entnext und entlast, ein Rueckwaertslauf durch
|
|
# die Zeichnung ist nicht moeglich. Solche Faelle sind hier billig zu fangen.
|
|
# Erweitern, sobald wieder eine erfundene Funktion auffliegt.
|
|
NICHT_EXISTENT = ("entprev", "entfirst", "ssprev", "vl-string-replace")
|
|
|
|
LISP_DIRS = ("Lisp", "tests")
|
|
|
|
|
|
def test_keine_nicht_existierenden_funktionen():
|
|
wurzel = os.path.join(os.path.dirname(__file__), "..")
|
|
treffer = []
|
|
for verz in LISP_DIRS:
|
|
pfad = os.path.join(wurzel, verz)
|
|
if not os.path.isdir(pfad):
|
|
continue
|
|
for name in sorted(os.listdir(pfad)):
|
|
if not name.lower().endswith(".lsp"):
|
|
continue
|
|
text = io.open(os.path.join(pfad, name), encoding="cp1252").read()
|
|
# Gleicher Scanner, andere Trefferliste - er ueberspringt
|
|
# Kommentare und Strings, ein erklaerender Kommentartext wie
|
|
# "kennt kein entprev" schlaegt also nicht an.
|
|
for wo, zaehler in funktions_inventar(text,
|
|
NICHT_EXISTENT).items():
|
|
for fn in sorted(zaehler):
|
|
treffer.append("%s/%s: (%s in %s)" % (verz, name, fn, wo))
|
|
assert not treffer, (
|
|
"Aufrufe nicht existierender AutoLISP-Funktionen: %s" % treffer)
|
|
|
|
|
|
def test_dispatcher_setzt_journal_zurueck():
|
|
"""P0: der Reset muss VOR der Modus-Verzweigung stehen, nicht nur im
|
|
Modus-1-Zweig - sonst erbt ein frischer Modus-2-Lauf das Journal des
|
|
Vorlaufs und schreibt es in die XDATA."""
|
|
if not os.path.isfile(CORE):
|
|
pytest.skip("Lisp/vf_core.lsp nicht gefunden")
|
|
text = io.open(CORE, encoding="cp1252").read()
|
|
cond = text.find("(cond ((= wahl 2) (vf-linienzug-modus2))")
|
|
assert cond >= 0, "Dispatcher-cond nicht gefunden"
|
|
reset = text.rfind("(vfl-journal-reset)", 0, cond)
|
|
assert reset >= 0, "vfl-journal-reset steht nicht vor der Modus-cond"
|
|
zweig = text[cond:text.find("(exit)", cond)]
|
|
assert "vfl-journal-reset" not in zweig, (
|
|
"Reset steht (wieder) IN der cond - dann trifft er nur einen Modus")
|