Setzt den OpenSpec-Change phase-3-ortszeit um (21/21 Aufgaben) und zieht
damit die in Phase 3 eingegangene Abweichung zurueck: DATE$, TIME$, TIMER
und NOW rechnen in der Zeitzone des Rechners, TIMER zaehlt ab der lokalen
Mitternacht.
Versatzermittlung
- jiff 0.2.35 als Abhaengigkeit von tb-runtime
- datetime::utc_versatz(zeitpunkt) ist der einzige Aufrufpunkt der
Bibliothek; die Kalenderrechnung darunter bleibt Eigenimplementierung
und sieht sie nie -- zwei Kalendermodelle nebeneinander waeren eine
dauerhafte Fehlerquelle
- Der Versatz gilt je Zeitpunkt, nicht je Programmlauf: ein Programm, das
ueber eine Zeitumstellung hinweg laeuft, folgt ihr
Zonenzustand
- Zeitzone { System, Fest(i32), Unbekannt } im RtState
- Fest(n) macht Tests und Korpusprogramme unabhaengig von der Zone des
Testrechners; TZ waere prozessweit und set_var seit Rust 2024 unsafe
Rueckfallebene
- Ohne ermittelbare Zone gilt UTC, das Programm laeuft weiter
- tbc run meldet den Rueckfall einmal beim Start auf der Fehlerausgabe,
nicht je Abfrage -- ein stiller Wechsel waere ein Verstoss gegen den
Guiding Principle
Korpus
- Neue Direktive ' tb-tz: +-HH:MM im Harness
- zeitzone.bas prueft Ortszeit, TIMER ab lokaler Mitternacht und den
Programmversatz gegen eine gesetzte Uhrzeit
- datumzeit.bas auf die Direktive umgestellt; Sollausgabe unveraendert
Nebenbefund
- TIMER trennt jetzt ganze Sekunden (Ortszeit) vom Bruchteil
(Systemuhr). Ohne die Trennung waere TIMER in Zonen mit halbstuendigem
Versatz gesprungen.
Dokumentation: die Abweichung "Uhr laeuft in UTC" entfaellt aus
sprachreferenz.md; bibliothek.md und PLAN.md nachgezogen, jiff steht im
Stufe-2-Ideenspeicher neben crossterm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
732 lines
23 KiB
Rust
732 lines
23 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:?}"),
|
|
}
|
|
}
|
|
|
|
// ---- 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: ISAM kommt mit `phase-3-isam`,
|
|
// die Ereignissteuerung mit Phase 4 → Laufzeitfehler 73.
|
|
let (ev, _) = run("TYPE T\n f AS INTEGER\nEND TYPE\nOPEN \"db\" FOR ISAM T \"tab\" AS #1");
|
|
match ev {
|
|
RunEvent::Error { code, message, .. } => {
|
|
assert_eq!(code, 73);
|
|
// Katalogtext des Vorbilds (VBDOS) für Code 73:
|
|
assert_eq!(message, "Feature unavailable");
|
|
}
|
|
other => panic!("{other:?}"),
|
|
}
|
|
}
|
|
|
|
// ---- 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:?}");
|
|
}
|