====== 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.