Files
TerminalBasic/crates/tb-vm/tests/vm.rs
Chili Palmer 9b58e0ec43 ISAM-Datenbankunterstuetzung
Setzt den OpenSpec-Change phase-3-isam um (49/49 Aufgaben) und schliesst
damit Phase 3 ab. Alle 22 ISAM-Elemente des Inventars sind implementiert;
der Abdeckungsstand steigt auf 217 implementiert / 15 offen / 53
Non-Feature.

Frontend
- ISAM-Anweisungen als eigener Zweig im Parser: NAME [#]n [, arg ...],
  Sonderform ROLLBACK ALL ueber eine Sentinel-Kennung
- 22 Signaturen in builtin_stmt/builtin_fn, neue Argumentart R
  (Satzvariable eines benutzerdefinierten Typs)
- Satzargumente von INSERT/RETRIEVE/UPDATE werden gegen den Typ der
  Dateinummer geprueft, sofern beide literal bekannt sind
- Die Unsupported-Absenkung von OPEN ... FOR ISAM entfaellt

Speicherschicht (tb-runtime::isam, einziger Ort mit redb)
- Je Tabelle eine Satztabelle satz-id -> Satzbytes, je Index eine Tabelle
  Schluesselbytes -> satz-id. Satz-IDs sind monoton und werden nie
  wiederverwendet: der Cursor merkt sich eine ID, eine neu vergebene
  koennte still auf einen fremden Satz zeigen
- Satzbytes entstehen mit fileio::wert_schreiben/wert_lesen, also mit den
  Recordpuffern und der UTF-32-Festtextkodierung aus datei-eio
- Ordnungserhaltende Schluesselkodierung je Spaltentyp; Text als UTF-8,
  dessen Bytereihenfolge die Codepoint-Reihenfolge ist und damit dieselbe
  Ordnung wie CmpStr der VM. Ein Eigenschaftstest ueber 5000 zufaellige
  Wertepaare je Typ haelt fest, dass der Bytevergleich dem fachlichen
  Vergleich entspricht -- eine Ordnungsverletzung faellt sonst erst bei
  bestimmten Datenwerten auf
- Cursor als Wert (aktiver Index, letzte Satz-ID, unpositioniert), nicht
  als gehaltener Iterator: er ueberlebt Satzaenderungen und
  Transaktionsgrenzen
- Sicherungspunkte ueber ein eigenes Ruecknahmeprotokoll; ROLLBACK ALL
  bricht die Bibliothekstransaktion direkt ab

Semantik durchweg aus der Original-Hilfe
- Die Argumentformen stammen erstmals aus den Einzelseiten, nicht nur aus
  der Themenliste. Das korrigierte eine Annahme des Entwurfs: die
  Spaltenliste von CREATEINDEX ist keine Zeichenkette mit Trennzeichen,
  sondern eine Folge einzelner Stringargumente
- SEEKEQ mit unvollstaendigem Schluessel schlaegt immer fehl, SEEKGT mit
  Teilschluessel positioniert wie SEEKGE
- Nach SETINDEX ist der erste Satz der neuen Ordnung aktuell, nach DELETE
  der folgende
- Suche ueber den NULL-Index meldet 87, nicht 83; damit hat jeder Code von
  81 bis 89 einen Ausloeser
- Einzige Erweiterung: ein - vor dem Spaltennamen ordnet absteigend. Die
  Original-Hilfe kennt bei CREATEINDEX keine Sortierrichtung, die
  Anforderung verlangt sie. Kollisionsfrei, weil ein TYPE-Feldname nie mit
  - beginnen kann

CLOSE beendet keine Transaktion
- Erst schrieb es sie fest, womit das Schliessen irgendeiner Dateinummer
  die Transaktion aller anderen mit beendete und ein folgendes ROLLBACK
  ALL ins Leere lief. Ausstehende Aenderungen sind ohnehin festgeschrieben,
  weil jede Operation ausserhalb einer Transaktion fuer sich eine ist
- Ueber das Ende entscheiden allein COMMITTRANS und ROLLBACK ALL; eine
  beim Programmende offene Transaktion verfaellt, in beiden Wegen gleich

SETMEM und Fehler 89
- Ohne DOS-Speichermodell ist SETMEM die Obergrenze des ISAM-Puffers
  (Vorgabe 65536 Bytes); ohne echte Grenze waere Code 89 nie erreichbar
  und die Anweisung eine Attrappe

Tests
- 18 Einheitentests (Kodierung, Formatversion, Satz-IDs, Roundtrip ueber
  alle Feldtypen, Indexordnung, Cursor, Protokoll)
- 6 Korpusprogramme: Tabellen, Indizes, Cursor, Saetze, Transaktionen,
  Puffer und Vergleichsordnung -- je in einem temporaeren Arbeits-
  verzeichnis, der Projektbaum bleibt sauber
- Der VM-Test zu Fehler 73 prueft ISAM nicht mehr, sondern SETUEVENT;
  dafuer belegt ein neuer Test, dass OPEN ... FOR ISAM arbeitet

Dokumentation: Datenbankdateiformat in dateiformate.md, Abschnitt 10a
samt Sortierordnung und SETMEM-Abweichung in sprachreferenz.md,
ISAM-Abschnitt in bibliothek.md, Inventar auf implementiert, PLAN.md
schaerft die Leitplanke Referenzverhalten -- sie gilt auch gegenueber den
eigenen Spezifikationen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 11:37:55 +02:00

777 lines
25 KiB
Rust

//! Interpreter-Tests: Spec-Szenarien aus vm-ausfuehrung und
//! vm-fehlerbehandlung (Phase-2-Änderung).
use tb_runtime::host::CaptureHost;
use tb_runtime::value::Value;
use tb_vm::interp::{RunEvent, Vm};
fn run(src: &str) -> (RunEvent, String) {
run_with_input(src, &[])
}
fn run_with_input(src: &str, input: &[&str]) -> (RunEvent, String) {
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
panic!("Compile-Fehler: {d:?}");
});
let mut vm = Vm::new(module);
let mut host = CaptureHost::with_input(input);
let ev = vm.run(&mut host);
// Die Tests vergleichen Textausgabe; der Bildschirm-Snapshot ist der
// Nachfolger des früheren Byte-Stroms (Entscheidung 2026-09-03, D3).
(ev, tb_runtime::snapshot::text(&vm.rt.screen))
}
fn out(src: &str) -> String {
let (ev, output) = run(src);
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{output}");
output
}
fn err_code(src: &str) -> u16 {
match run(src).0 {
RunEvent::Error { code, .. } => code,
other => panic!("Fehler erwartet, war {other:?}"),
}
}
/// Arbeitsverzeichnis für Tests mit Datei-E/A: ein frisches temporäres
/// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme
/// hinterlassen dadurch nichts im Projektbaum.
struct TempVerzeichnis {
vorher: std::path::PathBuf,
dir: std::path::PathBuf,
}
impl TempVerzeichnis {
fn neu(name: &str) -> TempVerzeichnis {
let vorher = std::env::current_dir().unwrap();
let dir = std::env::temp_dir().join(format!(
"tb_vm_{name}_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::env::set_current_dir(&dir).unwrap();
TempVerzeichnis { vorher, dir }
}
}
impl Drop for TempVerzeichnis {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.vorher);
let _ = std::fs::remove_dir_all(&self.dir);
}
}
// ---- 5.1 Ausdrücke und Konvertierungsmatrix --------------------------------
#[test]
fn print_hallo_welt() {
assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n");
}
#[test]
fn banker_rounding_cint() {
// Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 "
assert_eq!(out("PRINT CINT(0.5); CINT(1.5); CINT(2.5)"), " 0 2 2 \n");
}
#[test]
fn overflow_bei_zuweisung() {
// Spec-Szenario: INTEGER-Variable = 40000 → Fehler 6
assert_eq!(err_code("i% = 40000"), 6);
}
#[test]
fn intdiv_rundet_operanden_vor() {
// Spec-Szenario: PRINT 7.5 \ 2 → 8 \ 2 = 4
assert_eq!(out("PRINT 7.5 \\ 2"), " 4 \n");
}
#[test]
fn gemischte_arithmetik() {
assert_eq!(out("i% = 2\nd# = i% + 1.5#\nPRINT d#"), " 3.5 \n");
assert_eq!(out("PRINT 1 / 3"), " .3333333 \n"); // SINGLE-Division
assert_eq!(out("PRINT 7 MOD 3; -7 MOD 3"), " 1 -1 \n"); // Vorzeichen wie Dividend
assert_eq!(out("PRINT 2 ^ 10"), " 1024 \n");
}
#[test]
fn logik_bitweise() {
assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n");
// Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
}
#[test]
fn division_durch_null() {
assert_eq!(err_code("PRINT 1 / 0"), 11);
assert_eq!(err_code("PRINT 1 \\ 0"), 11);
assert_eq!(err_code("PRINT 0 ^ -1"), 11);
}
#[test]
fn stringvergleich_und_verkettung() {
assert_eq!(out("PRINT \"a\" + \"b\""), "ab\n");
assert_eq!(out("PRINT (\"abc\" < \"abd\")"), "-1 \n");
}
// ---- 5.2 Kontrollfluss -------------------------------------------------------
#[test]
fn for_ohne_durchlauf() {
// Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n");
}
#[test]
fn for_mit_negativem_step() {
assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n");
}
#[test]
fn for_mit_dynamischem_step() {
assert_eq!(
out("s% = -2\nFOR i% = 5 TO 1 STEP s%\nPRINT i%;\nNEXT\nPRINT"),
" 5 3 1 \n"
);
}
#[test]
fn select_case_bereiche() {
let src = "FOR i% = 1 TO 4\nSELECT CASE i%\nCASE 1: PRINT \"eins\"\nCASE 2 TO 3: PRINT \"mittel\"\nCASE ELSE: PRINT \"rest\"\nEND SELECT\nNEXT";
assert_eq!(out(src), "eins\nmittel\nmittel\nrest\n");
}
#[test]
fn do_loop_varianten() {
assert_eq!(
out("n% = 3\nDO WHILE n% > 0\nPRINT n%;\nn% = n% - 1\nLOOP\nPRINT"),
" 3 2 1 \n"
);
assert_eq!(
out("n% = 0\nDO\nn% = n% + 1\nLOOP UNTIL n% >= 3\nPRINT n%"),
" 3 \n"
);
assert_eq!(
out("n% = 5\nWHILE n% > 3\nn% = n% - 1\nWEND\nPRINT n%"),
" 3 \n"
);
}
#[test]
fn exit_for_und_do() {
assert_eq!(
out("FOR i% = 1 TO 10\nIF i% = 3 THEN EXIT FOR\nNEXT\nPRINT i%"),
" 3 \n"
);
}
#[test]
fn kontrollfluss_korpusdatei() {
// 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"),
)
.unwrap();
let want = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"),
)
.unwrap();
assert_eq!(out(&src), want);
}
#[test]
fn gosub_return_und_fehler_3() {
assert_eq!(
out("GOSUB U\nPRINT \"zurueck\"\nEND\nU:\nPRINT \"unten\"\nRETURN"),
"unten\nzurueck\n"
);
// Spec-Szenario: RETURN ohne GOSUB → Fehler 3
assert_eq!(err_code("RETURN"), 3);
}
#[test]
fn on_goto_berechnet() {
let src = "FOR i% = 0 TO 3\nON i% GOTO A, B\nPRINT \"kein\";\nGOTO W\nA:\nPRINT \"a\";\nGOTO W\nB:\nPRINT \"b\";\nW:\nNEXT\nPRINT";
assert_eq!(out(src), "keinabkein\n");
}
// ---- 5.3 Prozeduren ------------------------------------------------------------
#[test]
fn byref_wirkt_zurueck() {
// Spec-Szenario BYREF
assert_eq!(
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nPRINT n%"),
" 2 \n"
);
}
#[test]
fn klammern_erzwingen_byval() {
// Spec-Szenario BYVAL
assert_eq!(
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc (n%)\nPRINT n%"),
" 1 \n"
);
}
#[test]
fn byref_auf_arrayelement() {
assert_eq!(
out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nDIM a%(5)\na%(2) = 7\nInc a%(2)\nPRINT a%(2)"),
" 8 \n"
);
}
#[test]
fn function_und_rekursion() {
assert_eq!(
out("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\nPRINT Quad(3)"),
" 9 \n"
);
let fak = "FUNCTION Fak& (n%)\nIF n% <= 1 THEN\nFak& = 1\nELSE\nFak& = n% * Fak&(n% - 1)\nEND IF\nEND FUNCTION\nPRINT Fak&(10)";
assert_eq!(out(fak), " 3628800 \n");
}
#[test]
fn static_behaelt_werte() {
let src = "SUB Zaehl\nSTATIC n%\nn% = n% + 1\nPRINT n%;\nEND SUB\nZaehl\nZaehl\nZaehl\nPRINT";
assert_eq!(out(src), " 1 2 3 \n");
}
#[test]
fn def_fn_im_modulkontext() {
// DEF FN: Parameter lokal (BYVAL), freie Namen binden an Modulvariablen.
let src = "faktor = 10\nDEF FNmal (x) = x * faktor\nPRINT FNmal(3)";
assert_eq!(out(src), " 30 \n");
}
#[test]
fn ganzes_array_uebergeben() {
let src = "SUB Summe (a%(), s%)\ns% = 0\nFOR i% = LBOUND(a%) TO UBOUND(a%)\ns% = s% + a%(i%)\nNEXT\nEND SUB\nDIM w%(3)\nFOR i% = 0 TO 3\nw%(i%) = i%\nNEXT\nSumme w%(), erg%\nPRINT erg%";
assert_eq!(out(src), " 6 \n");
}
#[test]
fn udt_wertsemantik_und_felder() {
let src = "TYPE Punkt\nx AS INTEGER\ny AS INTEGER\nEND TYPE\nDIM a AS Punkt, b AS Punkt\na.x = 1\nb = a\nb.x = 9\nPRINT a.x; b.x";
assert_eq!(out(src), " 1 9 \n");
}
// ---- 5.4 DATA/READ/RESTORE -----------------------------------------------------
#[test]
fn data_read_restore() {
// Anmerkung: unquotierte DATA-Texte verlieren derzeit die
// Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
// Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
assert_eq!(out(src), " 1 2.5 hallo\n 1 \n");
}
#[test]
fn out_of_data_fehler_4() {
assert_eq!(err_code("DATA 1\nREAD a%, b%"), 4);
}
#[test]
fn data_typkonflikt_fehler_13() {
assert_eq!(err_code("DATA hallo\nREAD a%"), 13);
}
#[test]
fn restore_mit_label() {
let src = "DATA 1\nMarke:\nDATA 2\nREAD a%\nRESTORE Marke\nREAD b%\nPRINT a%; b%";
assert_eq!(out(src), " 1 2 \n");
}
// ---- 5.5 Fehlerbehandlung -------------------------------------------------------
#[test]
fn modulweiter_handler_faengt_prozedurfehler() {
// Spec-Szenario: Hauptprogramm setzt Handler, SUB löst Fehler 6 aus.
let src = "SUB Knall\ni% = 40000\nEND SUB\nON ERROR GOTO Fehler\nKnall\nPRINT \"nie\"\nEND\nFehler:\nPRINT \"ERR=\"; ERR\nEND";
assert_eq!(out(src), "ERR= 6 \n");
}
#[test]
fn lokaler_handler_verdeckt_modulweiten() {
let src = "SUB Tu\nON LOCAL ERROR GOTO L\nERROR 5\nEXIT SUB\nL:\nPRINT \"lokal\"; ERR\nRESUME Weiter\nWeiter:\nEND SUB\nON ERROR GOTO M\nTu\nEND\nM:\nPRINT \"modul\"\nEND";
assert_eq!(out(src), "lokal 5 \n");
}
#[test]
fn ohne_handler_bricht_ab() {
// Spec-Szenario: Fehler 9 ohne Handler → Abbruch mit Meldung.
let (ev, _) = run("DIM a%(3)\nPRINT a%(7)");
match ev {
RunEvent::Error { code, message, .. } => {
assert_eq!(code, 9);
assert_eq!(message, "Subscript out of range");
}
other => panic!("{other:?}"),
}
}
#[test]
fn erl_liefert_zeilennummer() {
let src = "ON ERROR GOTO H\n10 ERROR 5\nEND\nH:\nPRINT ERL\nEND";
assert_eq!(out(src), " 10 \n");
}
#[test]
fn erl_null_ohne_zeilennummern() {
// Spec-Szenario: keine numerischen Zeilennummern → ERL = 0
let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\nPRINT ERL\nEND";
assert_eq!(out(src), " 0 \n");
}
#[test]
fn error_anweisung() {
// Spec-Szenario: ERROR 53 → Handler mit ERR = 53
let src = "ON ERROR GOTO H\nERROR 53\nEND\nH:\nPRINT ERR\nEND";
assert_eq!(out(src), " 53 \n");
}
#[test]
fn resume_wiederholt_anweisung() {
let src = "ON ERROR GOTO H\nn% = 0\nversuch% = 0\n10 versuch% = versuch% + 1\nIF versuch% < 3 THEN ERROR 5\nPRINT versuch%\nEND\nH:\nRESUME";
// RESUME wiederholt die IF-Anweisung; versuch% bleibt 1? Nein:
// Fehler in IF-Zeile, RESUME wiederholt IF — Endlosschleife ohne
// Zählerénderung wäre falsch. Zähler steht in Zeile 10, daher: der
// Fehler passiert im IF, RESUME wiederholt das IF, versuch% ist noch
// < 3 … Um Determinismus zu sichern, zählt der Handler mit.
let _ = src;
let src2 = "ON ERROR GOTO H\nversuch% = 0\nERROR 5\nPRINT \"nach\"; versuch%\nEND\nH:\nversuch% = versuch% + 1\nIF versuch% < 3 THEN RESUME\nRESUME NEXT";
assert_eq!(out(src2), "nach 3 \n");
}
#[test]
fn resume_next_faehrt_fort() {
// Spec-Szenario: RESUME NEXT nach Division durch 0.
let src = "ON ERROR GOTO H\nx = 1 / 0\nPRINT \"weiter\"\nEND\nH:\nRESUME NEXT";
assert_eq!(out(src), "weiter\n");
}
#[test]
fn resume_ohne_fehler_20() {
assert_eq!(err_code("RESUME"), 20);
}
#[test]
fn fehler_im_handler_ist_fatal() {
// Spec-Szenario: kein Kaskadieren.
let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\ni% = 40000\nEND";
let (ev, _) = run(src);
match ev {
RunEvent::Error { code, .. } => assert_eq!(code, 6),
other => panic!("{other:?}"),
}
}
#[test]
fn on_error_goto_0_deaktiviert() {
let src = "ON ERROR GOTO H\nON ERROR GOTO 0\nERROR 5\nEND\nH:\nPRINT \"nie\"\nEND";
assert_eq!(err_code(src), 5);
}
// ---- 5.6 Unterbrechbarkeit -------------------------------------------------------
#[test]
fn breakpoint_haelt_an_und_setzt_fort() {
let module =
tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nc% = 3\nPRINT a% + b% + c%").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
vm.add_breakpoint(3);
// Spec-Szenario: hält VOR der Anweisung in Zeile 3.
match vm.run(&mut host) {
RunEvent::Breakpoint { line } => assert_eq!(line, 3),
other => panic!("{other:?}"),
}
// Spec-Szenario Inspektion: b% ist gesetzt, c% noch 0.
assert!(matches!(vm.inspect("b%"), Some(Value::Int(2))));
assert!(matches!(vm.inspect("c%"), Some(Value::Int(0))));
// Fortsetzen (Breakpoint entfernen, sonst hält Zeile 3 erneut).
vm.remove_breakpoint(3);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 6 \n");
}
#[test]
fn einzelschritt() {
let module = tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nEND").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
vm.set_step(true);
let mut lines = Vec::new();
loop {
match vm.run(&mut host) {
RunEvent::Stepped { line } => lines.push(line),
RunEvent::Ended => break,
other => panic!("{other:?}"),
}
}
assert_eq!(lines, vec![1, 2, 3]);
}
// ---- 5.7 Programmende --------------------------------------------------------------
#[test]
fn stop_liefert_zeile_und_ist_fortsetzbar() {
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nSTOP\nPRINT \"b\"").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
match vm.run(&mut host) {
RunEvent::Stopped { line } => assert_eq!(line, 2),
other => panic!("{other:?}"),
}
// IDE-Semantik: CONT = weiterlaufen.
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "a\nb\n");
}
#[test]
fn end_und_system() {
assert_eq!(run("PRINT \"x\"\nEND").0, RunEvent::Ended);
assert_eq!(run("SYSTEM").0, RunEvent::Ended);
}
// ---- Eingabe -----------------------------------------------------------------------
#[test]
fn input_mit_redo() {
let (ev, output) = run_with_input(
"INPUT \"Zahl\"; n%\nPRINT n% * 2",
&["abc", "21"],
);
assert_eq!(ev, RunEvent::Ended);
assert!(output.contains("Redo from start"));
assert!(output.ends_with(" 42 \n"), "{output}");
}
#[test]
fn line_input_liest_ganze_zeile() {
let (ev, output) = run_with_input("LINE INPUT s$\nPRINT s$", &["a, b, c"]);
assert_eq!(ev, RunEvent::Ended);
assert!(output.ends_with("a, b, c\n"));
}
// ---- Strings/PRINT über die VM -------------------------------------------------------
#[test]
fn mid_anweisung_mutiert() {
assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n");
}
#[test]
fn print_zonen_und_tab() {
assert_eq!(out("PRINT \"a\", \"b\""), "a b\n");
assert_eq!(out("PRINT TAB(5); \"x\""), " x\n");
assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a b\n");
}
#[test]
fn unsupported_feature_fehler_73() {
// Dokumentiert, aber noch offen: `SETUEVENT` kommt mit Phase 4
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
let (ev, _) = run("SETUEVENT");
match ev {
RunEvent::Error { code, message, .. } => {
assert_eq!(code, 73);
assert_eq!(message, "Feature unavailable");
}
other => panic!("{other:?}"),
}
}
/// Aufgabe 2.4 des Changes `phase-3-isam`: `OPEN … FOR ISAM` senkt nicht
/// mehr auf den „nicht verfügbar"-Marker ab, sondern arbeitet. Der Lauf
/// findet in einem temporären Verzeichnis statt und lässt nichts zurück.
#[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
let _dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run(
"TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
PRINT \"offen\"\n\
CLOSE #1",
);
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
assert!(ausgabe.contains("offen"), "{ausgabe}");
}
// ---- Zustellpunkte des Hosts (Spec konsolen-basisbibliothek) ---------------
/// Vor blockierender Eingabe wird der Bildschirm angezeigt — sonst stünde
/// die Eingabeaufforderung unsichtbar da.
#[test]
fn ausgabe_erscheint_vor_der_eingabe() {
use tb_runtime::host::{CaptureHost, Host};
let module =
tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::with_input(&["7"]);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
// Der Host hat den Bildschirm mehrfach gesehen, und schon beim ersten
// Mal stand der Text da.
assert!(host.presents > 0, "Host wurde nie zur Anzeige aufgefordert");
let text = tb_runtime::snapshot::text(&vm.rt.screen);
assert!(text.starts_with("Frage:\n"), "{text:?}");
assert!(text.contains(" 7 "), "{text:?}");
}
/// Eingegangene Zeichen werden auf dem Bildschirm wiederholt (Echo), und
/// Rückschritt löscht das zuletzt getippte Zeichen.
#[test]
fn eingabe_wird_geechot_und_rueckschritt_loescht() {
use tb_runtime::host::{taste, CaptureHost, Ereignis};
let module = tb_vm::compile_source("TEST", "INPUT a$\nPRINT a$\n").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for c in ['a', 'b', 'x'] {
host.ereignis(Ereignis::Taste(c.to_string()));
}
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string()));
host.ereignis(Ereignis::Taste(taste::ENTER.to_string()));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
let text = tb_runtime::snapshot::text(&vm.rt.screen);
// Erste Zeile: das Echo ohne das gelöschte Zeichen.
assert_eq!(text.lines().next(), Some("? ab"));
assert!(text.contains("ab\n"), "{text:?}");
}
/// Größenänderung während des Laufs wirkt auf den Bildschirm.
#[test]
fn groessenaenderung_wirkt_zur_laufzeit() {
use tb_runtime::host::{CaptureHost, Ereignis};
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nFOR i% = 1 TO 50\nNEXT i%\n").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
// Inhalt bleibt oben links erhalten.
assert_eq!(vm.rt.screen.cell(1, 1).ch, 'a');
}
// ---- Bildschirm und Tastatur (Spec textbildschirm) -------------------------
#[test]
fn locate_ausserhalb_loest_fehler_5_aus() {
assert_eq!(err_code("LOCATE 26, 1"), 5);
assert_eq!(err_code("LOCATE 1, 81"), 5);
assert_eq!(err_code("LOCATE 0, 1"), 5);
}
#[test]
fn screen_funktion_ausserhalb_loest_fehler_5_aus() {
assert_eq!(err_code("x% = SCREEN(30, 1)"), 5);
assert_eq!(err_code("x% = SCREEN(1, 200)"), 5);
}
#[test]
fn color_prueft_wertebereich() {
assert_eq!(err_code("COLOR 32, 0"), 5);
assert_eq!(err_code("COLOR 7, 8"), 5);
}
#[test]
fn screen_anweisung_nur_textmodus() {
// Literaler Grafikmodus schon zur Compile-Zeit.
let d = tb_vm::compile_source("T", "SCREEN 1").unwrap_err();
assert!(
d.iter().any(|x| x.message.contains("Feature unavailable")),
"{d:?}"
);
// Textmodus ist folgenlos zulässig.
assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n");
// Berechneter Modus erst zur Laufzeit.
assert_eq!(err_code("m% = 2\nSCREEN m%"), 73);
}
#[test]
fn inkey_ohne_taste_ist_leer() {
assert_eq!(out("a$ = INKEY$\nPRINT \"[\"; a$; \"]\""), "[]\n");
}
#[test]
fn inkey_liefert_zeichen_und_sondertaste() {
use tb_runtime::host::{taste, CaptureHost, Ereignis};
let module = tb_vm::compile_source(
"T",
"a$ = INKEY$\nb$ = INKEY$\nPRINT a$; LEN(b$); ASC(b$)\n",
)
.unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Taste("q".into()));
host.ereignis(Ereignis::Taste(taste::sonder(59))); // F1
assert_eq!(vm.run(&mut host), RunEvent::Ended);
let t = tb_runtime::snapshot::text(&vm.rt.screen);
// "q", dann Länge 2 und führendes Nullzeichen.
assert_eq!(t, "q 2 0 \n");
}
#[test]
fn input_s_liest_genau_n_zeichen_ohne_echo() {
use tb_runtime::host::{CaptureHost, Ereignis};
let module = tb_vm::compile_source("T", "a$ = INPUT$(3)\nPRINT a$\n").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for c in ['x', 'y', 'z', 'w'] {
host.ereignis(Ereignis::Taste(c.to_string()));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
// Nur das PRINT erscheint — INPUT$ echot nicht.
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "xyz\n");
}
#[test]
fn cls_setzt_cursor_zurueck() {
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT \"neu\""), "neu\n");
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT CSRLIN; POS(0)"), " 1 4 \n");
}
#[test]
fn width_hebt_die_groesse_an() {
assert_eq!(
out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"),
" 32 \n"
);
}
// ---- ON ERROR-Scoping (Spec sprach-frontend) -------------------------------
/// Ein modulweiter Handler ist auch aus einer Prozedur heraus ansprechbar;
/// `RESUME NEXT` setzt dann hinter dem Prozeduraufruf fort.
#[test]
fn modulweites_on_error_aus_prozedur() {
let src = "DECLARE SUB T ()\n\
ON ERROR GOTO Handler\n\
T\n\
PRINT \"zurueck\"\n\
END\n\
Handler:\n\
PRINT \"Handler\"; ERR\n\
RESUME NEXT\n\
SUB T\n\
ON ERROR GOTO Handler\n\
x = 1 / 0\n\
END SUB\n";
assert_eq!(out(src), "Handler 11 \nzurueck\n");
}
/// `ON LOCAL ERROR` verlangt weiterhin ein Label im eigenen Rumpf.
#[test]
fn on_local_error_bleibt_prozedurlokal() {
let src = "DECLARE SUB T ()\n\
ON ERROR GOTO Handler\n\
T\n\
END\n\
Handler:\n\
RESUME NEXT\n\
SUB T\n\
ON LOCAL ERROR GOTO Handler\n\
END SUB\n";
let d = tb_vm::compile_source("T", src).unwrap_err();
assert!(
d.iter().any(|x| x.message.contains("Label not defined")),
"{d:?}"
);
}
/// Ein prozedurlokaler Handler fängt vor dem modulweiten.
#[test]
fn lokaler_handler_hat_vorrang() {
let src = "DECLARE SUB T ()\n\
ON ERROR GOTO Modul\n\
T\n\
END\n\
Modul:\n\
PRINT \"Modul\"\n\
RESUME NEXT\n\
SUB T\n\
ON LOCAL ERROR GOTO Lokal\n\
x = 1 / 0\n\
EXIT SUB\n\
Lokal:\n\
PRINT \"Lokal\"\n\
RESUME NEXT\n\
END SUB\n";
assert_eq!(out(src), "Lokal\n");
}
// ---- Ortszeit (Spec konsolen-basisbibliothek) ------------------------------
use tb_runtime::datetime::Zeitzone;
/// Programm mit festgelegter Zeitzone laufen lassen.
fn out_mit_zone(src: &str, zone: Zeitzone) -> String {
use tb_runtime::host::CaptureHost;
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| panic!("{d:?}"));
let mut vm = Vm::new(module);
vm.rt.zeitzone = zone;
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
tb_runtime::snapshot::text(&vm.rt.screen)
}
/// `TIME$` und `HOUR(NOW)` nennen dieselbe Stunde — sie rechnen beide
/// über den Zonenzustand.
#[test]
fn time_und_now_stimmen_ueberein() {
let t = out_mit_zone("PRINT LEFT$(TIME$, 2); HOUR(NOW)", Zeitzone::Fest(7200));
let zeile = t.lines().next().unwrap();
let aus_time: i32 = zeile[..2].parse().unwrap();
let aus_now: i32 = zeile[2..].trim().parse().unwrap();
assert_eq!(aus_time, aus_now, "{zeile:?}");
}
/// Ein fester Versatz verschiebt die Uhr um genau diesen Betrag.
#[test]
fn fester_versatz_verschiebt_die_uhr() {
let src = "PRINT INT(TIMER)";
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt).trim().parse().unwrap();
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600)).trim().parse().unwrap();
// Modulo Tageslänge, damit ein Mitternachtsübergang nichts kaputt macht.
let diff = (mit - ohne).rem_euclid(86_400.0);
assert!((diff - 3600.0).abs() <= 2.0, "Differenz {diff}");
}
/// `TIMER` zählt ab der lokalen Mitternacht, nicht ab der UTC-Mitternacht.
#[test]
fn timer_zaehlt_ab_lokaler_mitternacht() {
let t = out_mit_zone("PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))", Zeitzone::Fest(-18000));
let zahlen: Vec<f64> = t
.split_whitespace()
.filter_map(|w| w.parse().ok())
.collect();
assert_eq!(zahlen.len(), 2, "{t:?}");
assert_eq!(zahlen[0], zahlen[1], "TIMER-Stunde ≠ TIME$-Stunde: {t:?}");
}
/// `TIME$ =` wirkt auf die Ortszeit und alle davon abgeleiteten Werte.
#[test]
fn programmversatz_wirkt_auf_alle_zeitfunktionen() {
let t = out_mit_zone(
"TIME$ = \"10:20:30\"\nDATE$ = \"01-02-1993\"\nPRINT TIME$; \" \"; DATE$; HOUR(NOW); YEAR(NOW)",
Zeitzone::Fest(3600),
);
assert!(t.starts_with("10:20:30 01-02-1993 10 1993"), "{t:?}");
}
/// Ohne ermittelbare Zone gilt UTC — ohne Fehler.
#[test]
fn rueckfall_auf_utc_laeuft_fehlerfrei() {
let t = out_mit_zone(
"PRINT LEN(DATE$); LEN(TIME$); TIMER >= 0; YEAR(NOW) > 2000",
Zeitzone::Unbekannt,
);
assert_eq!(t.trim(), "10 8 -1 -1", "{t:?}");
}