//! 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 { dir: std::path::PathBuf, } impl TempVerzeichnis { fn neu(name: &str) -> TempVerzeichnis { 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(); TempVerzeichnis { dir } } fn pfad(&self, name: &str) -> String { self.dir.join(name).to_string_lossy().replace('"', "\"\"") } } impl Drop for TempVerzeichnis { fn drop(&mut self) { 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 grafik_line_paint_und_view_zeichnen_in_den_zellenpuffer() { assert_eq!( out("SCREEN 2\nVIEW (0,0)-(31,15),0,1\nLINE (0,0)-(15,7),1,BF\nPAINT (24,8),1\nEND"), "████\n████\n" ); } #[test] fn run_gibt_datei_oder_zeile_an_den_runner_weiter() { assert_eq!( run("RUN \"next\"").0, RunEvent::Restart { program: Some("next".into()), line: None } ); assert_eq!( run("RUN 100").0, RunEvent::Restart { program: None, line: Some(100) } ); } #[test] fn common_dialog_registrierung_setzt_byref_erfolg() { assert_eq!( out("DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nCmnDlgRegister ok%\nPRINT ok%\nEND"), "-1 \n" ); } #[test] fn len_liefert_die_feste_satzbreite_eines_udt() { assert_eq!( out("TYPE Satz\nText AS STRING * 3\nZahl AS INTEGER\nEND TYPE\nDIM Wert AS Satz\nPRINT LEN(Wert)"), " 14 \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"); } /// 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(&format!( "TYPE T\n f AS INTEGER\nEND TYPE\n\ OPEN \"{}\" FOR ISAM T \"Tab\" AS #1\n\ PRINT \"offen\"\n\ CLOSE #1", dir.pfad("db.isam") )); 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; 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(), 0)); } host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string(), 0)); host.ereignis(Ereignis::Taste(taste::ENTER.to_string(), 0)); 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_bildet_grafikmodi_auf_den_zellenpuffer_ab() { assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n"); assert_eq!(out("SCREEN 13\nPRINT \"ok\""), "ok\n"); assert_eq!(err_code("m% = 14\nSCREEN m%"), 5); } #[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(), 0)); host.ereignis(Ereignis::Taste(taste::sonder(59), 0)); // 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(), 0)); } 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 input_s_liest_aus_einer_binaerdatei() { let tmp = TempVerzeichnis::neu("input_s_datei"); let pfad = tmp.pfad("daten.bin"); std::fs::write(&pfad, b"abcdef").unwrap(); assert_eq!( out(&format!( "OPEN \"{pfad}\" FOR BINARY AS #1\na$ = INPUT$(3, #1)\nPRINT a$\nCLOSE #1" )), "abc\n" ); } #[test] fn clipboard_und_printer_objekte_haben_laufzeitwirkung() { let module = tb_vm::compile_source( "T", "CLIPBOARD.ADDITEM \"abc\"\nPRINT CLIPBOARD.GETTEXT\nPRINTER.PRINT \"Seite\"\nPRINTER.NEWPAGE\nPRINTER.ENDDOC", ) .unwrap(); let mut vm = Vm::new(module); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "abc\n"); assert_eq!(vm.rt.print.drucker, "Seite\n\u{c}"); } #[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 = 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:?}"); } // ---- Ereignis-Traps (Change `phase-4-ereignisschleife`) --------------------- /// Programm mit virtueller Uhr laufen lassen: die Uhr rückt an jedem /// Zustellpunkt um `schritt_ms` vor (design.md, D6). fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) { struct UhrHost { inner: CaptureHost, schritt: u64, } impl tb_runtime::host::Host for UhrHost { fn present(&mut self, s: &tb_runtime::screen::TextScreen) { self.inner.present(s); } fn next_event(&mut self, b: bool) -> Option { self.inner.next_event(b) } fn warten(&mut self, deadline_ms: Option) -> Option { if let Some(event) = self.next_event(false) { return Some(event); } self.inner.warten(deadline_ms) } fn jetzt_ms(&mut self) -> u64 { let j = self.inner.jetzt_ms(); self.inner.uhr_vorruecken(self.schritt); j } } 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 = UhrHost { inner: CaptureHost::default(), schritt: schritt_ms, }; let ev = vm.run(&mut host); (ev, tb_runtime::snapshot::text(&vm.rt.screen)) } #[test] fn zeit_trap_laeuft_und_kehrt_zurueck() { // Die Schleife druckt x, der Trap dazwischen T. Nach RETURN zaehlt die // Schleife unveraendert weiter — der unterbrochene Zustand bleibt. let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ TIMER ON\n\ FOR i% = 1 TO 200\n\ NEXT\n\ PRINT \"i=\"; i%\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ RETURN\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); assert!(out.contains('T'), "Zeit-Trap lief nicht: {out:?}"); assert!( out.contains("i= 201"), "Schleife lief nicht sauber weiter: {out:?}" ); } #[test] fn ohne_timer_on_kein_trap() { let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ FOR i% = 1 TO 200\n\ NEXT\n\ PRINT \"fertig\"\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ RETURN\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); assert!(!out.contains('T'), "Trap lief ohne TIMER ON: {out:?}"); assert!(out.contains("fertig")); } #[test] fn trap_aus_einer_sub_heraus_kehrt_dorthin_zurueck() { let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ TIMER ON\n\ CALL Rechne\n\ PRINT \"ende\"\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ RETURN\n\ SUB Rechne\n\ FOR j% = 1 TO 200\n\ NEXT\n\ PRINT \"sub\"; j%\n\ END SUB\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); assert!(out.contains('T'), "Trap lief nicht: {out:?}"); assert!( out.contains("sub 201"), "SUB lief nicht sauber weiter: {out:?}" ); assert!(out.contains("ende")); } #[test] fn event_off_klammert_die_zustellung() { let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ TIMER ON\n\ EVENT OFF\n\ FOR i% = 1 TO 200\n\ NEXT\n\ PRINT \"|\";\n\ EVENT ON\n\ FOR i% = 1 TO 200\n\ NEXT\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ RETURN\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); let text = out.trim(); let strich = text.find('|').expect("Trennzeichen fehlt"); assert!( !text[..strich].contains('T'), "waehrend EVENT OFF wurde zugestellt: {text:?}" ); assert!( text[strich..].contains('T'), "nach EVENT ON wurde nicht zugestellt: {text:?}" ); } #[test] fn timer_off_im_handler_ueberlebt_das_return() { let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ TIMER ON\n\ FOR i% = 1 TO 400\n\ NEXT\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ TIMER OFF\n\ RETURN\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); assert_eq!( out.trim().matches('T').count(), 1, "RETURN hat den Trap wieder eingeschaltet: {out:?}" ); } #[test] fn gosub_null_schaltet_den_trap_ab() { let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ TIMER ON\n\ ON TIMER(1) GOSUB 0\n\ FOR i% = 1 TO 300\n\ NEXT\n\ PRINT \"fertig\"\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ RETURN\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); assert!(!out.contains('T'), "abgeschalteter Trap lief: {out:?}"); } #[test] fn setuevent_loest_den_uevent_trap_aus() { let (ev, out) = run("ON UEVENT GOSUB Nutzer\n\ UEVENT ON\n\ CALL SetUEvent\n\ FOR i% = 1 TO 20\n\ NEXT\n\ PRINT \"|\";\n\ END\n\ Nutzer:\n\ PRINT \"U\";\n\ RETURN\n"); assert_eq!(ev, RunEvent::Ended, "{out}"); assert!(out.contains('U'), "UEVENT-Trap lief nicht: {out:?}"); } #[test] fn kennung_ausserhalb_des_bereichs_ist_fehler_5() { // Nicht konstant, also erst zur Laufzeit pruefbar. let src = "n% = 99\nON KEY(n%) GOSUB M\nEND\nM:\nRETURN\n"; assert_eq!(err_code(src), 5); } #[test] fn leere_warteschleife_hat_eine_anweisungsgrenze() { // `FOR i = 1 TO n: NEXT` ist die klassische Warteschleife. Ohne // Anweisungsgrenze im Kreis würde dort nie ein Ereignis zugestellt, // kein Breakpoint erreicht und kein Abbruch bemerkt. use tb_vm::bytecode::Instr; for src in [ "FOR i% = 1 TO 3\nNEXT\n", "WHILE i% < 3\nWEND\n", "DO WHILE i% < 3\nLOOP\n", "DO\nLOOP UNTIL i% > 3\n", ] { let m = tb_vm::compile_source("T", src).unwrap(); let code = &m.procs[0].code; let mut kreise = 0; for (j, i) in code.iter().enumerate() { let ziel = match i { Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => *t as usize, _ => continue, }; if ziel >= j { continue; } kreise += 1; assert!( code[ziel..=j].iter().any(|i| matches!(i, Instr::Stmt(_))), "Schleife ohne Anweisungsgrenze im Kreis: {src:?}" ); } assert!(kreise > 0, "kein Rücksprung gefunden: {src:?}"); } } #[test] fn doevents_stellt_zu() { // Die Warteschleife ruft DOEVENTS; der Trap muss darin laufen. let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ TIMER ON\n\ FOR i% = 1 TO 5\n\ x% = DOEVENTS\n\ NEXT\n\ PRINT \"|\";\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ RETURN\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); let t = out.trim(); let strich = t.find('|').expect("Trennzeichen fehlt"); assert!( t[..strich].contains('T'), "DOEVENTS stellte nicht zu: {t:?}" ); } #[test] fn doevents_liefert_null() { assert_eq!(out("PRINT DOEVENTS").trim(), "0"); } #[test] fn sleep_endet_durch_ein_ereignis() { // SLEEP 60 wuerde ohne Zustellung eine Minute warten. Der faellige // Trap beendet es; danach laeuft das Programm hinter dem SLEEP weiter. let t = std::time::Instant::now(); let (ev, out) = run_mit_uhr( "ON TIMER(1) GOSUB Tick\n\ TIMER ON\n\ SLEEP 60\n\ PRINT \"danach\"\n\ END\n\ Tick:\n\ PRINT \"T\";\n\ RETURN\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); assert!(out.contains('T'), "Trap lief nicht: {out:?}"); assert!( out.contains("danach"), "SLEEP kehrte nicht zurueck: {out:?}" ); assert!( t.elapsed().as_secs() < 5, "SLEEP wartete die vollen 60 Sekunden ab" ); } #[test] fn fehler_im_trap_handler_erreicht_den_modulweiten_handler() { // `ERL` nennt die zuletzt ausgefuehrte **Zeilennummer** — deshalb ist // das Programm nummeriert. Der Fehler entsteht in Zeile 90 (im // Handler), nicht an der unterbrochenen Stelle. let (ev, out) = run_mit_uhr( "10 ON ERROR GOTO 200\n 20 ON TIMER(1) GOSUB 90\n 30 TIMER ON\n 40 FOR i% = 1 TO 300\n 50 NEXT\n 60 PRINT \"|\";\n 70 END\n 90 ERROR 7\n 100 RETURN\n 200 PRINT \"E\"; ERR; ERL;\n 210 RESUME NEXT\n", 600, ); assert_eq!(ev, RunEvent::Ended, "{out}"); assert!(out.contains("E 7"), "Fehlerbehandlung lief nicht: {out:?}"); assert!( out.contains(" 90"), "ERL nennt nicht die Zeile im Handler: {out:?}" ); } #[test] fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() { // Regression: der Handler steht selbst auf einer nummerierten Zeile. // `ERL` muss trotzdem die Zeile des Fehlers melden (Spec // vm-fehlerbehandlung, „ERR und ERL"). let out = out("10 ON ERROR GOTO 100\n\ 20 ERROR 7\n\ 30 END\n\ 100 PRINT ERL;\n\ 110 RESUME NEXT\n"); assert_eq!(out.trim(), "20"); } fn forms_catalog() -> tb_frontend::forms::FormCatalog { use tb_frontend::forms::ObjectClass; let mut c = tb_frontend::forms::FormCatalog::default(); c.add("Form1", ObjectClass::Form, None, false); c.add("Text1", ObjectClass::TextBox, Some("Form1"), false); c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true); c } fn form_vm(src: &str) -> Vm { let module = tb_vm::compile_source_with_forms("FORM1", src, &forms_catalog()) .unwrap_or_else(|d| panic!("Compile-Fehler: {d:?}")); Vm::new(module) } #[test] fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() { let mut vm = form_vm("Form1!Text1.Text = \"hallo\"\nPRINT Form1!Text1.Text"); let ev = vm.run(&mut CaptureHost::default()); assert_eq!(ev, RunEvent::Ended); assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo"); } #[test] fn screen_controlpanel_hat_einen_indexierten_laufzeitwert() { assert_eq!( out("SCREEN.CONTROLPANEL(5) = 3\nPRINT SCREEN.CONTROLPANEL(5)"), " 3 \n" ); } #[test] fn steuerarray_eigenschaften_und_objektparameter_funktionieren() { let mut vm = form_vm( "LOAD Command1(3)\nCommand1(3).Caption = \"drei\"\nPRINT Command1(3).Caption\n\ CALL Aus(Text1)\nPRINT Text1.Visible\nCALL FormAus(Form1)\nPRINT Form1.Caption\nEND\n\ SUB Aus(c AS CONTROL)\nPRINT TYPEOF c IS TextBox\nc.Visible = 0\nEND SUB\n\ SUB FormAus(f AS FORM)\nPRINT TYPEOF f IS Form\nf.Caption = \"Formular\"\nEND SUB", ); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert_eq!( tb_runtime::snapshot::text(&vm.rt.screen).trim(), "drei\n-1 \n 0 \n-1 \nFormular" ); } #[test] fn modales_show_setzt_nach_unload_fort() { let mut vm = form_vm( "DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\n\ SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\ SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB", ); let mut host = CaptureHost::default(); host.ereignis_nach( 2, tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis { art: tb_runtime::host::MausArt::Druck, taste: 1, shift: 0, zeile: 1, spalte: 1, }), ); let ev = vm.run(&mut host); assert_eq!(ev, RunEvent::Ended); assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "1"); assert!(host.presents >= 2); } #[test] fn modelloses_formular_bleibt_nach_programmende_bedienbar() { let mut vm = form_vm( "DIM SHARED gesehen%\nEND\n\ SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\ SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB", ); vm.forms.show(0, false).unwrap(); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); let mut host = CaptureHost::default(); host.ereignis_nach( 1, tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis { art: tb_runtime::host::MausArt::Druck, taste: 1, shift: 0, zeile: 1, spalte: 1, }), ); assert_eq!(vm.run_visible_forms(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1)))); assert!(!vm.forms.has_visible_forms()); } #[test] fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() { let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\""); vm.queue_form_event(tb_ui::forms::FormEvent { object: 0, array_index: None, name: "CLICK".into(), args: vec![], }); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "ja"); } #[test] fn mausargumente_kommen_in_zellen_an() { use tb_runtime::host::{Ereignis, MausArt, MausEreignis}; let mut vm = form_vm( "DIM SHARED b%, s%, gx!, gy!\nForm1.Show\nSTOP\nDOEVENTS\nEND\n\ SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\ SHARED b%, s%, gx!, gy!\nb% = Button: s% = Shift: gx! = X: gy! = Y\nEND SUB", ); let mut host = CaptureHost::default(); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); host.ereignis(Ereignis::Maus(MausEreignis { art: MausArt::Druck, taste: 1, shift: 2, zeile: 5, spalte: 7, })); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("b"), Some(Value::Int(1)))); assert!(matches!(vm.inspect("s"), Some(Value::Int(2)))); assert!(matches!(vm.inspect("gx"), Some(Value::Sng(7.0)))); assert!(matches!(vm.inspect("gy"), Some(Value::Sng(5.0)))); } #[test] fn form_load_laeuft_vor_dem_sichtbarsetzen() { let mut vm = form_vm( "Form1.Show\nEND\n\ SUB Form_Load()\nPRINT Form1.Visible\nEND SUB", ); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "0"); } #[test] fn steuerarray_ereignis_stellt_index_voran() { let mut vm = form_vm( "DIM SHARED seen%\nSTOP\nDOEVENTS\nEND\n\ SUB Command1_Click(Index AS INTEGER)\nSHARED seen%\nseen% = Index\nEND SUB", ); let mut host = CaptureHost::default(); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); vm.forms.load_array(2, 3).unwrap(); vm.queue_form_event(tb_ui::forms::FormEvent { object: 2, array_index: Some(3), name: "CLICK".into(), args: vec![], }); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("seen"), Some(Value::Int(3)))); let active = tb_frontend::forms::property(tb_frontend::forms::ObjectClass::Screen, "ACTIVECONTROL") .unwrap() .0; assert_eq!( vm.forms.get(3, active).unwrap(), tb_ui::forms::PropertyValue::Object(Some((2, Some(3)))) ); } #[test] fn form_load_laeuft_einmal_und_unload_cancel_verhindert_entladen() { let mut vm = form_vm( "DIM SHARED loads%\nx$ = Form1.Caption\nForm1.Hide\nx$ = Form1.Caption\n\ Form1.Show\nUNLOAD Form1\nEND\n\ SUB Form_Load()\nSHARED loads%\nloads% = loads% + 1\nEND SUB\n\ SUB Form_Unload(Cancel AS INTEGER)\nCancel = 1\nEND SUB", ); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert!(matches!(vm.inspect("loads"), Some(Value::Int(1)))); assert!(vm.forms.objects[0].loaded); assert!(vm.forms.objects[0].visible); } #[test] fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() { let module = tb_vm::compile_source_with_forms( "FORM1", "Form1.Caption = \"x\"\nDOEVENTS\nx$ = Form1.Caption", &forms_catalog(), ) .unwrap(); let n = module.procs[0] .code .iter() .filter(|i| matches!(i, tb_vm::bytecode::Instr::Doevents)) .count(); assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode"); } #[test] fn listenmethoden_indexeigenschaft_und_picture_messung_laufen_in_der_vm() { use tb_frontend::forms::ObjectClass; let mut catalog = tb_frontend::forms::FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); catalog.add("List1", ObjectClass::ListBox, Some("Form1"), false); catalog.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false); let module = tb_vm::compile_source_with_forms( "FORM1", "List1.Sorted = -1\nList1.ADDITEM \"b\"\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT List1.ListCount\nPRINT Picture1.TEXTWIDTH(\"abc\")", &catalog, ) .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}")); let mut vm = Vm::new(module); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert_eq!( tb_runtime::snapshot::text(&vm.rt.screen).trim(), "a\n 2 \n 3" ); } #[test] fn listenmethoden_und_indexeigenschaft_laufen_auf_control_arrays() { use tb_frontend::forms::ObjectClass; let mut catalog = tb_frontend::forms::FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); catalog.add("List1", ObjectClass::ListBox, Some("Form1"), true); let module = tb_vm::compile_source_with_forms( "FORM1", "LOAD List1(1)\nList1(1).ADDITEM \"x\"\nPRINT List1(1).List(0)", &catalog, ) .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}")); let mut vm = Vm::new(module); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "x"); } #[test] fn msgbox_und_inputbox_sind_keine_unsupported_opcodes_mehr() { use tb_runtime::host::{taste, Ereignis}; let module = tb_vm::compile_source( "DIALOG", "r% = MSGBOX(\"Weiter?\", 4, \"Frage\")\ns$ = INPUTBOX$(\"Name\")\nPRINT r%; s$", ) .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}")); assert!(!module.procs[0] .code .iter() .any(|instruction| matches!(instruction, tb_vm::bytecode::Instr::Unsupported(_)))); let mut vm = Vm::new(module); let mut host = CaptureHost::default(); for key in [taste::ENTER, "A", taste::ENTER] { host.ereignis(Ereignis::Taste(key.into(), 0)); } assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "6 A"); } #[test] fn default_button_hat_vorrang_und_beendet_modales_formular() { use tb_frontend::forms::ObjectClass; use tb_runtime::host::{taste, Ereignis}; let mut catalog = tb_frontend::forms::FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); catalog.add("Command1", ObjectClass::CommandButton, Some("Form1"), false); let module = tb_vm::compile_source_with_forms( "FORM1", "DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\nSUB Command1_Click()\nSHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB", &catalog, ) .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}")); let mut vm = Vm::new(module); let default = tb_frontend::forms::property(ObjectClass::CommandButton, "DEFAULT") .unwrap() .0; vm.forms .set(1, default, tb_ui::forms::PropertyValue::Boolean(true)) .unwrap(); let mut host = CaptureHost::default(); host.ereignis(Ereignis::Taste(taste::ENTER.into(), 0)); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1)))); } #[test] fn printform_schreibt_das_textformular_in_den_druckerkanal() { use tb_frontend::forms::ObjectClass; let mut catalog = tb_frontend::forms::FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); let module = tb_vm::compile_source_with_forms("FORM1", "Form1.Show\nForm1.PRINTFORM", &catalog) .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}")); let mut vm = Vm::new(module); for (name, value) in [("WIDTH", 12), ("HEIGHT", 4)] { let property = tb_frontend::forms::property(ObjectClass::Form, name) .unwrap() .0; vm.forms .set_initial(0, property, tb_ui::forms::PropertyValue::Integer(value)) .unwrap(); } let caption = tb_frontend::forms::property(ObjectClass::Form, "CAPTION") .unwrap() .0; vm.forms .set_initial( 0, caption, tb_ui::forms::PropertyValue::String("Druck".into()), ) .unwrap(); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert!(vm.rt.print.drucker.contains("Druck")); } #[test] fn klassischer_timer_trap_ruht_solange_das_menu_offen_ist() { use tb_frontend::forms::ObjectClass; use tb_runtime::host::umschalt; let mut catalog = tb_frontend::forms::FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); catalog.add("mnuDatei", ObjectClass::Menu, Some("Form1"), false); let module = tb_vm::compile_source_with_forms( "FORM1", "DIM SHARED n%\nON TIMER(1) GOSUB Tick\nTIMER ON\nForm1.Show\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nPRINT n%\nEND\nTick:\nn% = n% + 1\nRETURN", &catalog, ) .unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}")); let mut vm = Vm::new(module); let caption = tb_frontend::forms::property(ObjectClass::Menu, "CAPTION") .unwrap() .0; vm.forms .set_initial( 1, caption, tb_ui::forms::PropertyValue::String("&Datei".into()), ) .unwrap(); let mut host = CaptureHost::default(); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); assert!(vm.forms.handle_key("d", umschalt::ALT)); host.uhr_vorruecken(1_500); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); assert!(matches!(vm.inspect("n"), Some(Value::Int(0)))); assert!(vm.forms.handle_key(tb_runtime::host::taste::ESC, 0)); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("n"), Some(Value::Int(1)))); }