====== Testen ====== Lyx bringt kein Test-Framework mit — und braucht keins. Ein Test ist ein Programm, das etwas prüft und ''PASS''/''FAIL'' druckt. Das klingt karg, hat aber einen Vorteil: Ein Test ist genau so schwer zu verstehen wie ein gewöhnliches Programm, und er läuft ohne Zusatzwerkzeug überall, wo der Compiler läuft. Der Compiler selbst wird so getestet — über 600 Testskripte im ''aurum''-Repository, dazu eine Liste von 505 lauffähigen ''.lyx''-Tests. → [[lyx_-_programmiersprache:guides:fehlersuche|Fehlersuche]] · [[lyx_-_programmiersprache:tools:compiler-parameter|Compiler-Parameter]] · [[lyx_-_programmiersprache:units|Standard Library]] ---- ===== Ein Test in Lyx ===== Die zu prüfende Unit: unit tst.rechner; pub fn Addiere(a: int64, b: int64): int64 { return a + b; } pub fn Teile(a: int64, b: int64, out_rest: int64): int64 { if (b == 0) { return 0 - 1; } poke64(out_rest, a - (a / b) * b); return a / b; } Der Test dazu — hier eigenständig, damit er ohne die Nachbardatei übersetzt: import std.io; import std.alloc; // Im Guide steht die gepruefte Unit daneben; hier sind ihre beiden // Funktionen mit eingerueckt, damit das Beispiel fuer sich uebersetzt. fn Addiere(a: int64, b: int64): int64 { return a + b; } fn Teile(a: int64, b: int64, out_rest: int64): int64 { if (b == 0) { return 0 - 1; } poke64(out_rest, a - (a / b) * b); return a / b; } var g_pass: int64; var g_fail: int64; fn Pruefe(name: pchar, ist: int64, soll: int64): void { if (ist == soll) { g_pass := g_pass + 1; PrintLn(StrConcat("PASS: ", name)); } else { g_fail := g_fail + 1; PrintLn(StrConcat(StrConcat("FAIL: ", name), StrConcat(" — erwartet ", StrConcat(IntToStr(soll), StrConcat(", erhalten ", IntToStr(ist)))))); } } fn main(): int64 { g_pass := 0; g_fail := 0; Pruefe("Addiere: gewoehnlich", Addiere(2, 3), 5); Pruefe("Addiere: Null", Addiere(0, 0), 0); Pruefe("Addiere: negativ", Addiere(-2, 5), 3); var rest: int64 := alloc(8); Pruefe("Teile: Ergebnis", Teile(7, 2, rest), 3); Pruefe("Teile: Rest", peek64(rest), 1); Pruefe("Teile: durch Null meldet -1", Teile(7, 0, rest), -1); free(rest, 8); PrintLn(StrConcat(StrConcat(IntToStr(g_pass), " PASS, "), StrConcat(IntToStr(g_fail), " FAIL"))); if (g_fail > 0) { return 1; } return 0; } PASS: Addiere: gewoehnlich PASS: Addiere: Null PASS: Addiere: negativ PASS: Teile: Ergebnis PASS: Teile: Rest PASS: Teile: durch Null meldet -1 6 PASS, 0 FAIL Übersetzt und ausgeführt mit ''lyxc 1.1.3I''; Exit-Code ''0''. Drei Dinge daran sind Absicht: * **Die Meldung nennt Soll und Ist.** Ein ''FAIL'' ohne Zahlen zwingt zum Nachbauen des Falls. * **Der Test prüft die Ränder mit**: Null, negative Werte, Division durch Null. Der gewöhnliche Fall geht fast immer; die Fehler stecken an den Rändern. * **Der Exit-Code folgt dem Ergebnis** (''1'' bei Fehlern) — damit taugt der Test für ein Skript. ---- ===== Testskripte drumherum ===== Wo die Prüfung nicht im Programm stehen kann — Compilerfehler, Ausgabe eines Werkzeugs, Dateigrößen — nimmt man ein Shell-Skript. Das Muster aus dem Compiler-Repository: #!/bin/bash # #1670 — ClassName(): Klassenname zur Laufzeit. set -u cd "$(dirname "$0")/.." LYXC=${LYXC:-./lyxc} TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT PASS=0; FAIL=0 ok() { echo "PASS: $1"; PASS=$((PASS+1)); } bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } pruefe() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (erwartet '$3', erhalten '$2')"; fi; } cat > "$TMP/t.lyx" <<'EOF' … Testprogramm … EOF if ! "$LYXC" --std-path=. "$TMP/t.lyx" -o "$TMP/t" > "$TMP/t.log" 2>&1; then bad "uebersetzt"; grep -E "error" "$TMP/t.log" | head -3 else ok "uebersetzt" timeout 60 "$TMP/t" > "$TMP/t.out" 2>&1 pruefe "dynamischer Name" "$(grep '^dyn=' "$TMP/t.out" | cut -d= -f2)" "TAbl" fi Bewährte Punkte daraus: * **''mktemp -d'' plus ''trap''** — der Test hinterlässt nichts. * **''timeout''** um jeden Lauf; ein hängender Test blockiert sonst die ganze Reihe. * **Ausgabe in eine Datei, dann darin suchen.** ''lyxc'' nie in eine Pipe schreiben lassen, die früh schließt (''head'', ''grep -m''): SIGPIPE bricht ihn still ab, es entsteht kein Binary, und der Exit-Code bleibt ''0''. * **Zeilen mit Präfix ausgeben** (''dyn=TAbl'') und im Skript danach greifen — robuster als Zeilennummern. ---- ===== Was ein guter Test prüft ===== Ein Test, der nur den einfachen Fall abdeckt, ist auch grün, wenn die Umsetzung falsch ist. Beispiel aus dem Bestand: ''ClassName()'' auf einer direkt deklarierten Variablen wäre auch mit einer rein **statischen** Antwort grün — der eigentliche Punkt ist die Variable vom Basistyp, die die tatsächliche Klasse melden muss. Daraus die Regel: **Prüfe das, was kaputtgehen kann, nicht das, was offensichtlich geht.** * Ränder: 0, 1, negativ, leer, maximal * Fehlerpfad: Was liefert die Funktion, wenn es schiefgeht? * Gegenprobe: Bleibt das Nachbarverhalten unverändert? * Regression: Zu jedem behobenen Fehler ein Test, der genau ihn festhält ---- ===== Vor dem Test: übersetzen lassen ===== ''--compile-unit'' übersetzt eine Unit für sich allein — bis in den Codegen ([[https://github.com/SEOLizer/LyX-Compiler/issues/1587|#1587]]). Das ist die billigste Prüfung überhaupt und findet alles, was die Semantikprüfung sieht: lyxc meineunit.lyx --compile-unit -o meineunit.lyu Seit lyxc 1.1.x meldet die Prüfung deutlich mehr als früher: unbekannte Felder über Unit-Grenzen, falsche Stelligkeiten, doppelte Mitglieder, Vorwärtsverweise. **Eine grüne Übersetzung sagt heute mehr aus als vor einem Jahr.** ---- ===== Testreihen organisieren ===== Im Compiler-Repository hängen die Tests an ''make''-Zielen: ^ Ziel ^ Inhalt ^ | ''make test'' | Die Skripte aus ''tests/*.sh'' | | ''make test-lyx'' | Die Liste ''tests/suite-full.txt'' (505 Einträge) | | ''make test-known-red'' | Die **bekannt roten** aus ''tests/known-red.txt'' — wird rot, wenn einer davon wieder grün ist | Der letzte Punkt ist der interessante: Ein bekannt roter Test, der repariert wurde, meldet sich selbst — sonst veraltet die Liste still. Ebenfalls aus dem Bestand gelernt: Die Erfolgskonvention war uneinheitlich (manche Tests liefern 0, manche 42, eine Familie druckt „ALL PASS" und endet mit 1). Der Runner urteilt deshalb nach der **Ausgabe**: eine ''FAIL''-Zeile ist rot, ''PASS''/''OK:'' ohne ''FAIL'' ist grün, ein Absturz (rc ≥ 128) ist immer rot. Wer eine eigene Reihe aufbaut, legt die Konvention besser von Anfang an fest. ---- ===== Beispiele in der Doku prüfen ===== Für dieses Wiki gibt es ''doku-pruefer.py'': Es zieht jeden ''%%%%''-Block mit ''fn main('' heraus, übersetzt ihn und hält zusätzlich jede Aufruf-Stelligkeit gegen die echte ''pub fn''-Zeile der Standardbibliothek. Dasselbe Verfahren taugt für jede Dokumentation, die Codebeispiele enthält — ein Beispiel, das nie übersetzt wurde, ist eine Behauptung. ---- ===== Fallstricke ===== * **Test ohne ''timeout''.** Ein Fehler, der zur Endlosschleife führt, hält die Reihe an, statt aufzufallen. * **Nur der Exit-Code geprüft.** Ein Programm, das gar nicht erst übersetzt, liefert je nach Skript auch ''0''. * **Test gegen die installierte statt die gebaute Fassung.** ''LYXC=${LYXC:-./lyxc}'' macht die Wahl sichtbar. * **Zufall im Test** (Zeit, Zufallszahlen, Netz). Ein Test, dessen Ergebnis von der Umgebung abhängt, misst nicht mehr, was er zu messen vorgibt — ein DNS-Test im Bestand hing genau daran. Letzte Aktualisierung: 2026-08-19 — Beispiel mit ''lyxc 1.1.3I'' übersetzt und ausgeführt; Skriptmuster und Suite-Mechanik aus dem ''aurum''-Repository erhoben.