//! Regressionen zu Ereigniszustellung, Hostgrenzen und Forms-Zustand. use tb_frontend::forms::{FormCatalog, ObjectClass}; use tb_runtime::host::{CaptureHost, Ereignis, Host, MausArt, MausEreignis}; use tb_runtime::screen::TextScreen; use tb_runtime::value::Value; use tb_vm::interp::{RunEvent, Vm}; #[path = "../../../tests/support/prozessfrist.rs"] mod prozessfrist; fn vm(src: &str) -> Vm { let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); catalog.add("Text1", ObjectClass::TextBox, Some("Form1"), false); catalog.add("Text2", ObjectClass::TextBox, Some("Form1"), false); catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false); catalog.add("Menu1", ObjectClass::Menu, Some("Form1"), false); catalog.add("Item1", ObjectClass::Menu, Some("Menu1"), false); Vm::new( tb_vm::compile_source_with_forms("FORM1", src, &catalog) .unwrap_or_else(|d| panic!("{d:?}")), ) } fn stop(vm: &mut Vm, host: &mut dyn Host) { assert!(matches!(vm.run(host), RunEvent::Stopped { .. })); } fn string(vm: &Vm, name: &str) -> String { match vm.inspect(name) { Some(Value::Str(s)) => s.to_string(), other => panic!("{other:?}"), } } fn integer(vm: &Vm, name: &str) -> i16 { match vm.inspect(name) { Some(Value::Int(n)) => n, other => panic!("{other:?}"), } } fn key(s: &str) -> Ereignis { Ereignis::Taste(s.into(), 0) } fn mouse() -> Ereignis { Ereignis::Maus(MausEreignis { art: MausArt::Druck, taste: 1, shift: 0, zeile: 1, spalte: 1, }) } #[test] fn beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge() { let mut vm = vm("ON SIGNAL(1) GOSUB IntHandler\nON SIGNAL(2) GOSUB TermHandler\nSIGNAL(1) ON\nSIGNAL(2) ON\nSTOP\nDOEVENTS\nEND\nIntHandler:\ns$ = s$ + \"1\"\nRETURN\nTermHandler:\ns$ = s$ + \"2\"\nRETURN"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(Ereignis::Signal(2)); host.ereignis(Ereignis::Signal(1)); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "12"); for n in [0, 3] { assert!( tb_vm::compile_source("TEST", &format!("ON SIGNAL({n}) GOSUB H\nEND\nH:\nRETURN")) .is_err() ); let mut bad = vm_for_source(&format!("n% = {n}\nON SIGNAL(n%) GOSUB H\nEND\nH:\nRETURN")); assert!(matches!( bad.run(&mut host), RunEvent::Error { code: 5, .. } )); } } fn vm_for_source(src: &str) -> Vm { Vm::new(tb_vm::compile_source("TEST", src).unwrap()) } #[test] fn key_on_off_stop_gelten_fuer_alle_konsoleneingaben() { for state in ["ON", "OFF", "STOP"] { for input in ["s$ = INKEY$", "INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] { let mut vm = vm_for_source(&format!("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) {state}\nSTOP\n{input}\nKEY(15) ON\nDOEVENTS\nEND\nH:\nn% = n% + 1\nRETURN")); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key("a")); if state == "ON" { host.ereignis(key("b")); } host.ereignis(key("\r")); assert_eq!(vm.run(&mut host), RunEvent::Ended, "{state}: {input}"); assert_eq!( string(&vm, "s"), if state == "ON" { "b" } else { "a" }, "{state}: {input}" ); assert_eq!( integer(&vm, "n"), if state == "OFF" { 0 } else { 1 }, "{state}: {input}" ); } } } #[test] fn taste_maus_und_trap_behalten_die_annahmereihenfolge() { for (events, expected) in [ (vec![key("a"), mouse()], "aM"), (vec![mouse(), key("a")], "Ma"), ( vec![key("a"), mouse(), key("b"), key("t"), mouse()], "aMbTM", ), ] { let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nText1.SetFocus\nSTOP\nn% = DOEVENTS + LEN(s$)\nDOEVENTS\nDOEVENTS\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"T\"\nRETURN\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nEND SUB\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); for event in events { host.ereignis(event); } assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), expected); assert!( matches!(vm.inspect("n"), Some(Value::Int(n)) if n > 0), "DOEVENTS muss vor der Auswertung von LEN zustellen" ); } } #[test] fn fokus_handler_laufen_geordnet_bis_zur_rueckkehr() { let mut vm = vm("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\ns$ = s$ + \"L\"\ns$ = s$ + \"l\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key("\t")); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "LlGg"); } #[test] fn doevents_verhindert_rekursion_derselben_forms_prozedur() { let mut vm = vm("DIM SHARED s$\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nDOEVENTS\ns$ = s$ + \"r\"\nEND SUB"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key("a")); host.ereignis(key("b")); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "arbr"); } #[test] fn offenes_menu_sperrt_traps_und_wartende_forms_timer_an_allen_zustellpunkten() { let mut vm = vm("DIM SHARED n%\nON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nCALL SetUEvent\nn% = n% + 1\nDOEVENTS\nSLEEP .001\nLINE INPUT s$\nSTOP\nDOEVENTS\nEND\nH:\nn% = n% + 10\nRETURN\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 100\nEND SUB"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); vm.forms.handle_key("d", 4); assert!(vm.forms.menu_is_open()); vm.queue_form_event(tb_ui::forms::FormEvent { object: 3, array_index: None, name: "TIMER".into(), args: vec![], }); // Erst die blockierende Konsole holt Enter ab, damit das Menü offen bleibt. struct InputHost(CaptureHost); impl Host for InputHost { fn present(&mut self, s: &TextScreen) { self.0.present(s); } fn next_event(&mut self, block: bool) -> Option { block.then(|| key("\r")) } fn jetzt_ms(&mut self) -> u64 { self.0.jetzt_ms() } fn warten(&mut self, deadline: Option) -> Option { if deadline.is_none() { self.next_event(true) } else { self.0.warten(deadline) } } } stop(&mut vm, &mut InputHost(CaptureHost::default())); assert_eq!(integer(&vm, "n"), 1); assert!(vm.forms.menu_is_open()); host.ereignis(key("\u{1b}")); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(integer(&vm, "n"), 111); } #[test] fn ohne_zeitbedarf_keine_uhrabfrage_und_keine_mausansammlung() { struct OhneUhr(CaptureHost); impl Host for OhneUhr { fn present(&mut self, s: &TextScreen) { self.0.present(s); } fn next_event(&mut self, b: bool) -> Option { self.0.next_event(b) } fn jetzt_ms(&mut self) -> u64 { panic!("unerwartete Uhrabfrage") } fn warten(&mut self, _: Option) -> Option { panic!("unerwartetes Warten") } } for src in ["a% = 1\na% = 2\nEND", "Form1.Show\na% = 1\nDOEVENTS\nEND"] { let mut vm = vm(src); let mut host = OhneUhr(CaptureHost::default()); for _ in 0..500 { host.0.ereignis(mouse()); } assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(vm.rt.eingaben.is_empty()); } } #[test] fn sleep_folgt_nur_der_hostfrist_und_forms_timer_beendet_es() { let _frist = prozessfrist::Prozessfrist::neu("Host-SLEEP", std::time::Duration::from_secs(5)); struct WarteHost { now: u64, deadlines: Vec>, timer: bool, } impl Host for WarteHost { fn present(&mut self, _: &TextScreen) {} fn next_event(&mut self, _: bool) -> Option { None } fn jetzt_ms(&mut self) -> u64 { self.now } fn warten(&mut self, deadline: Option) -> Option { self.deadlines.push(deadline); if self.deadlines.len() == 1 && !self.timer { // Reale Zeit verstreicht bei eingefrorener Hostuhr. Erst die // folgende Host-Wartephase darf die Deadline erreichen. std::thread::sleep(std::time::Duration::from_millis(10)); return Some(Ereignis::Groesse { cols: 80, rows: 25 }); } self.now = deadline.expect("Deadline fehlt"); None } } let mut host = WarteHost { now: 0, deadlines: vec![], timer: false, }; let mut plain = vm_for_source("SLEEP .001\nEND"); assert_eq!(plain.run(&mut host), RunEvent::Ended); assert_eq!(host.deadlines, [Some(1), Some(1)]); let mut vm = vm("DIM SHARED n%\nForm1.Show\nTimer1.Interval = 100\nTimer1.Enabled = -1\nSLEEP 60\nEND\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nEND SUB"); let mut host = WarteHost { now: 0, deadlines: vec![], timer: true, }; assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(host.deadlines, [Some(100)]); assert_eq!(integer(&vm, "n"), 1); } #[test] fn dialog_erhaelt_stop_markierung_unverbrauchter_tasten() { let mut vm = vm_for_source("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) STOP\nSTOP\nx% = MSGBOX(\"Weiter\")\nKEY(15) ON\nDOEVENTS\ns$ = INKEY$\nEND\nH:\nn% = n% + 1\nRETURN"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key("\r")); host.ereignis(key("a")); vm.rt.pump(&mut host, false); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "a"); assert_eq!(integer(&vm, "n"), 1); } #[test] fn unload_aus_unload_handler_ruft_dieselbe_prozedur_nicht_rekursiv_auf() { let mut vm = vm("DIM SHARED s$\nForm1.Show\nUNLOAD Form1\nEND\nSUB Form_Unload(Cancel AS INTEGER)\nSHARED s$\ns$ = s$ + \"u\"\nUNLOAD Form1\ns$ = s$ + \"r\"\nEND SUB"); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert_eq!(string(&vm, "s"), "ur"); assert!(!vm.forms.is_loaded(0)); } // Nachverifikation V1–V5: unveränderte Sollwerte aus den Review-Proben. #[test] fn review_signal_waehrend_blockierender_eingabe() { struct Input { reads: usize, } impl Host for Input { fn present(&mut self, _: &TextScreen) {} fn next_event(&mut self, block: bool) -> Option { if !block { return None; } self.reads += 1; Some(if self.reads == 1 { Ereignis::Signal(2) } else { Ereignis::Ende }) } fn jetzt_ms(&mut self) -> u64 { 0 } fn warten(&mut self, _: Option) -> Option { self.next_event(true) } } let mut results = vec![]; for statement in ["INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] { let src = format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nH:\nPRINT \"H\"\nEND"); let mut vm = Vm::new(tb_vm::compile_source("TEST", &src).unwrap()); let result = vm.run(&mut Input { reads: 0 }); println!( "{statement}: {result:?}; Ausgabe={:?}", tb_runtime::snapshot::text(&vm.rt.screen) ); results.push(result); } assert!(results.iter().all(|result| *result == RunEvent::Ended)); } #[test] fn review_objektargument_darf_gotfocus_nicht_vorziehen() { for action in [ "CALL Mark(Text1)", "n% = Text1.TabIndex\nCALL Mark(Text1)", "Text1.Text = \"neu\"\nCALL Mark(Text1)", "Text1.Refresh\nCALL Mark(Text1)", "LOAD Form1\nCALL Mark(Text1)", ] { let mut vm = vm(&format!("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\n{action}\ns$ = s$ + \"l\"\nEND SUB\nSUB Mark(c AS CONTROL)\nSHARED s$\ns$ = s$ + \"L\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB")); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key("\t")); assert_eq!(vm.run(&mut host), RunEvent::Ended, "{action}"); assert_eq!(string(&vm, "s"), "LlGg", "{action}"); } } #[test] fn review_gleichzeitig_key_und_uevent_behalten_prioritaet() { let mut vm = vm("ON KEY(15) GOSUB K\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nON UEVENT GOSUB U\nUEVENT ON\nSTOP\nDOEVENTS\nEND\nK:\ns$ = s$ + \"K\"\nRETURN\nU:\ns$ = s$ + \"U\"\nRETURN"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key("t")); vm.rt.pump(&mut host, false); vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "KU"); } #[test] fn review_modale_eingabe_vor_eof_wird_zugestellt() { let mut vm = vm("DIM SHARED s$\nForm1.Show 1\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = \"M\"\nUNLOAD Form1\nEND SUB"); let mut host = CaptureHost::default(); host.ereignis_nach(2, mouse()); host.ereignis_nach(2, Ereignis::Ende); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "M"); } #[test] fn eof_leert_angenommene_eingaben_bei_modalen_und_modellosen_forms() { for modal in [false, true] { let show = if modal { "Form1.Show 1" } else { "Form1.Show" }; let mut vm = vm(&format!("DIM SHARED s$\n{show}\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB")); let mut host = CaptureHost::default(); if !modal { assert_eq!(vm.run(&mut host), RunEvent::Ended); } host.ereignis_nach(2, mouse()); host.ereignis_nach(2, mouse()); host.ereignis_nach(2, Ereignis::Ende); let result = if modal { vm.run(&mut host) } else { vm.run_visible_forms(&mut host) }; assert_eq!(result, RunEvent::Ended, "modal={modal}"); assert_eq!(string(&vm, "s"), "MM", "modal={modal}"); } } #[test] fn review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu() { struct MenuHost { time: u64, waits: usize, } impl Host for MenuHost { fn present(&mut self, _: &TextScreen) {} fn next_event(&mut self, _: bool) -> Option { None } fn jetzt_ms(&mut self) -> u64 { self.time } fn warten(&mut self, deadline: Option) -> Option { self.waits += 1; if self.waits == 1 { return Some(key("\u{1b}")); } self.time = deadline.unwrap(); None } } let mut vm = vm("ON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nSLEEP 60\nEND\nH:\ns$ = \"U\"\nRETURN"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); vm.forms.handle_key("d", 4); vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent); let mut host = MenuHost { time: 0, waits: 0 }; assert_eq!(vm.run(&mut host), RunEvent::Ended); println!( "Menü/SLEEP: Zeit={}, Warteaufrufe={}, Handler={}", host.time, host.waits, string(&vm, "s") ); assert_eq!(host.waits, 1); } #[test] fn review_maus_vor_getrappter_taste() { let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nSTOP\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"K\"\nRETURN\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(mouse()); host.ereignis(key("t")); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "MK"); } /// Ereignisse treffen erst beim Warten ein, auch mitten in einer Eingabe. struct EingabeHost { events: std::collections::VecDeque, now: u64, deadlines: Vec>, } impl Host for EingabeHost { fn present(&mut self, _: &TextScreen) {} fn next_event(&mut self, _: bool) -> Option { None } fn jetzt_ms(&mut self) -> u64 { self.now } fn warten(&mut self, deadline: Option) -> Option { self.deadlines.push(deadline); if let Some(deadline) = deadline { self.now = deadline; None } else { Some(self.events.pop_front().unwrap_or(Ereignis::Ende)) } } } fn eingabe_host(events: Vec) -> EingabeHost { EingabeHost { events: events.into(), now: 0, deadlines: vec![], } } #[test] fn input_fortsetzung_erhaelt_teiltext_ausdruck_und_byref_ziel() { for (statement, expected, enter) in [ ("INPUT s$", "ab", true), ("LINE INPUT s$", "ab", true), ("s$ = \"x\" + INPUT$(2) + \"y\"", "xaby", false), ] { let src = format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nn% = n% + 1\nRETURN\nSUB Lesen(s$)\n{statement}\nEND SUB"); let mut vm = vm_for_source(&src); let mut events = vec![key("a"), Ereignis::Signal(2), key("b")]; if enter { events.push(key("\r")); } let mut host = eingabe_host(events); assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}"); assert_eq!(string(&vm, "s"), expected); assert_eq!(integer(&vm, "n"), 1); assert_eq!(host.deadlines.len(), if enter { 4 } else { 3 }); } } #[test] fn handler_eingabe_und_stop_bewahren_die_aeussere_eingabe() { let mut vm = vm_for_source( "ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT s$\nEND\nH:\nSTOP\nINPUT t$\nRETURN", ); let mut host = eingabe_host(vec![ key("a"), Ereignis::Signal(2), key("h"), key("\r"), key("b"), key("\r"), ]); stop(&mut vm, &mut host); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "ab"); assert_eq!(string(&vm, "t"), "h"); } #[test] fn timer_waehrend_input_wird_vor_erneutem_warten_zugestellt() { let mut vm = vm_for_source( "ON TIMER(1) GOSUB H\nTIMER ON\nINPUT s$\nEND\nH:\nn% = n% + 1\nTIMER OFF\nRETURN", ); let mut host = eingabe_host(vec![key("a"), key("\r")]); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "a"); assert_eq!(integer(&vm, "n"), 1); assert_eq!(host.deadlines, [Some(1000), None, None]); } #[test] fn input_redo_verliert_weder_referenzen_noch_teiltext_beim_trap() { let mut vm = vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT \"Zahl:\"; n%\nEND\nH:\nRETURN"); let mut host = eingabe_host(vec![ key("x"), key("\r"), key("4"), Ereignis::Signal(2), key("2"), key("\r"), ]); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(integer(&vm, "n"), 42); assert_eq!( tb_runtime::snapshot::text(&vm.rt.screen) .matches("Zahl:") .count(), 2 ); } #[test] fn lokaler_error_handler_nach_trap_verwirft_abgebrochene_teileingabe() { let mut vm = vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nERROR 5\nRETURN\nSUB Lesen(s$)\nON LOCAL ERROR GOTO Fehler\nINPUT s$\nEXIT SUB\nFehler:\nINPUT s$\nRESUME NEXT\nEND SUB"); let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(string(&vm, "s"), "b"); } #[test] fn randomize_eingabe_teilt_den_fortsetzbaren_konsolenpfad() { let mut vm = vm_for_source( "ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nRANDOMIZE\nEND\nH:\nn% = n% + 1\nRETURN", ); let mut host = eingabe_host(vec![key("4"), Ereignis::Signal(2), key("2"), key("\r")]); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(integer(&vm, "n"), 1); assert_eq!( tb_runtime::snapshot::text(&vm.rt.screen) .matches("Random Number Seed") .count(), 1 ); } #[test] fn return_label_verwirft_abgebrochene_eingabe_und_ausdruck() { for statement in ["INPUT s$", "s$ = \"alt\" + INPUT$(2)"] { let mut vm = vm_for_source(&format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nWeiter:\nINPUT t$\nEND\nH:\nRETURN Weiter")); let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]); assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}"); assert_eq!(string(&vm, "s"), ""); assert_eq!(string(&vm, "t"), "b"); } } #[test] fn spaete_timeraktivierung_nutzt_hostzeit_auch_ueber_control_parameter() { for (setup, start) in [ ("", "Timer1.Interval = 100"), ("", "CALL Starten(Timer1)"), ( "Timer1.Enabled = 0\nTimer1.Interval = 100", "Timer1.Enabled = -1", ), ] { let mut vm = vm(&format!("DIM SHARED n%\nForm1.Show\n{setup}\nSTOP\n{start}\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nEND\nSUB Starten(t AS CONTROL)\nt.Interval = 100\nEND SUB\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nEND SUB")); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.uhr_vorruecken(1000); stop(&mut vm, &mut host); assert_eq!(vm.forms.next_deadline(), Some(1100)); host.uhr_vorruecken(99); stop(&mut vm, &mut host); assert_eq!(integer(&vm, "n"), 0); host.uhr_vorruecken(1); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(integer(&vm, "n"), 1); } } #[test] fn intervallaenderung_im_handler_verwirft_bereits_faellige_altintervalle() { let mut vm = vm("DIM SHARED n%\nForm1.Show\nTimer1.Interval = 100\nSLEEP 60\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nEND\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nIF n% = 1 THEN\nSTOP\nTimer1.Interval = 200\nELSE\nTimer1.Interval = 0\nEND IF\nEND SUB"); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); // Im ersten Handler bei 100 ms. assert_eq!(host.jetzt_ms(), 100); host.uhr_vorruecken(350); stop(&mut vm, &mut host); // Alter Rückstau darf nach dem Handler nicht laufen. assert_eq!(integer(&vm, "n"), 1); assert_eq!(vm.forms.next_deadline(), Some(650)); host.uhr_vorruecken(199); stop(&mut vm, &mut host); assert_eq!(integer(&vm, "n"), 1); host.uhr_vorruecken(1); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(integer(&vm, "n"), 2); assert_eq!(vm.forms.next_deadline(), None); } #[test] fn formular_tastatur_fallback_fuehrt_alle_drei_basic_handler_aus() { let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); for (input, expected) in [("a", "DPU"), ("\0;", "DU")] { let src = "DIM SHARED s$\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"D\"\nEND SUB\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + \"P\"\nEND SUB\nSUB Form_KeyUp(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"U\"\nEND SUB"; let mut vm = Vm::new(tb_vm::compile_source_with_forms("FORM1", src, &catalog).unwrap()); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key(input)); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!( string(&vm, "s"), expected, "FORM_KEYDOWN/KEYPRESS/KEYUP: Status implementiert, Hosttaste {input:?}" ); } } #[test] fn registrierte_form_ereignisse_erreichen_basic_aus_realen_quellen() { use tb_frontend::forms::{self, EventParamType}; for name in forms::events(ObjectClass::Form) { let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false); let args = forms::event_params(name) .unwrap() .iter() .map(|(n, t)| { format!( "{n} AS {}", match t { EventParamType::Integer => "INTEGER", EventParamType::Single => "SINGLE", EventParamType::Control => "CONTROL", } ) }) .collect::>() .join(", "); let operation = match *name { "LOAD" | "GOTFOCUS" => "Form1.Show", "LOSTFOCUS" => "Form1.Hide", "UNLOAD" => "Form1.Unload", "PAINT" => "Form1.Refresh", "RESIZE" => "Form1.Width = 70", "DRAGOVER" | "DRAGDROP" => "Source.Drag 1", "KEYDOWN" | "KEYPRESS" | "KEYUP" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "CLICK" | "DBLCLICK" => "", other => panic!("FORM_{other}: Status implementiert, Auslöser fehlt"), }; let setup = if matches!(*name, "LOAD" | "GOTFOCUS") { "" } else { "Form1.Show" }; let src = format!("DIM SHARED n%\nSource.Left=2\nSource.Top=2\n{setup}\nn%=0\nSTOP\n{operation}\nDOEVENTS\nEND\nSUB Form_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB"); let mut vm = Vm::new( tb_vm::compile_source_with_forms("FORM1", &src, &catalog) .unwrap_or_else(|d| panic!("{name}: {d:?}")), ); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); if name.starts_with("KEY") { host.ereignis(key("a")); } let events: &[MausArt] = match *name { "MOUSEDOWN" => &[MausArt::Druck], "MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung], "MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen], "CLICK" => &[MausArt::Druck, MausArt::Loslassen], "DBLCLICK" => &[ MausArt::Druck, MausArt::Loslassen, MausArt::Druck, MausArt::Loslassen, ], _ => &[], }; for art in events { host.ereignis(Ereignis::Maus(MausEreignis { art: *art, taste: 1, shift: 0, zeile: 1, spalte: 1, })); } assert_eq!(vm.run(&mut host), RunEvent::Ended, "FORM_{name}"); event_nachgewiesen(&format!("FORM_{name}"), integer(&vm, "n")).unwrap(); } } #[test] fn registrierte_control_ereignisse_erreichen_basic_aus_realen_quellen() { use tb_frontend::forms::{self, EventParamType}; let mut failures = Vec::new(); for class in ObjectClass::ALL .iter() .copied() .filter(|c| *c != ObjectClass::Form) { for name in forms::events(class) { let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); if class == ObjectClass::Menu { catalog.add("Bar", ObjectClass::Menu, Some("Form1"), false); } catalog.add( "Probe", class, Some(if class == ObjectClass::Menu { "Bar" } else { "Form1" }), false, ); catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false); let args = forms::event_params(name) .unwrap() .iter() .map(|(n, t)| { format!( "{n} AS {}", match t { EventParamType::Integer => "INTEGER", EventParamType::Single => "SINGLE", EventParamType::Control => "CONTROL", } ) }) .collect::>() .join(", "); let mut setup = String::from( "Form1.Width=60\nForm1.Height=15\nForm1.Show\nSource.Left=40\nSource.Top=2\n", ); if !matches!(class, ObjectClass::Timer | ObjectClass::Menu) { setup.push_str("Probe.Left=10\nProbe.Top=4\n"); } let operation = match *name { "GOTFOCUS" => String::new(), "LOSTFOCUS" => "Source.SetFocus".into(), "PAINT" => "Probe.Refresh".into(), "DRAGOVER" | "DRAGDROP" => "Source.Drag 1".into(), "CHANGE" => match class { ObjectClass::Label => "Probe.Caption=\"changed\"".into(), ObjectClass::HScrollBar | ObjectClass::VScrollBar => "Probe.Value=1".into(), ObjectClass::DirListBox => { format!("Probe.Path=\"{}\"", std::env::temp_dir().display()) } ObjectClass::DriveListBox => "Probe.Drive=\"/\"".into(), _ => "Probe.Text=\"changed\"".into(), }, "PATHCHANGE" => format!("Probe.Path=\"{}\"", std::env::temp_dir().display()), "PATTERNCHANGE" => "Probe.Pattern=\"*.bas\"".into(), "TIMER" => "Probe.Interval=1\nProbe.Enabled=-1".into(), "CLICK" if class == ObjectClass::Menu => { setup.push_str("Probe.Caption=\"&Go\"\nProbe.Shortcut=\"F3\"\n"); String::new() } "CLICK" | "DBLCLICK" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "KEYDOWN" | "KEYPRESS" | "KEYUP" | "DROPDOWN" | "CUSTOM" => String::new(), other => panic!( "{}_{other}: Status implementiert, Auslöser fehlt", class.name() ), }; let src=format!("DIM SHARED n%\n{setup}\nn%=0\nSTOP\n{operation}\nSTOP\nDOEVENTS\nEND\nSUB Probe_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB"); let mut vm = Vm::new( tb_vm::compile_source_with_forms("FORM1", &src, &catalog) .unwrap_or_else(|d| panic!("{}_{name}: {d:?}", class.name())), ); let mut host = CaptureHost::default(); let initial = vm.run(&mut host); assert!( matches!(initial, RunEvent::Stopped { .. }), "{}_{name}: {initial:?}", class.name() ); if *name == "LOSTFOCUS" { vm.forms.focus((1, None)).unwrap(); } let second = vm.run(&mut host); assert!( matches!(second, RunEvent::Stopped { .. }), "{}_{name}: {second:?}", class.name() ); if name.starts_with("KEY") || matches!(*name, "DROPDOWN" | "CUSTOM") { vm.forms.focus((1, None)).unwrap(); } if *name == "TIMER" { host.uhr_vorruecken(2); } if name.starts_with("KEY") { host.ereignis(key("a")); } if *name == "DROPDOWN" { host.ereignis(Ereignis::Taste("\0P".into(), 4)); } if *name == "CUSTOM" { host.ereignis(key("\0H")); } if class == ObjectClass::Menu { host.ereignis(key("\0=")); } let events: &[MausArt] = match *name { "MOUSEDOWN" | "GOTFOCUS" => &[MausArt::Druck], "MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung], "MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen], "CLICK" if class != ObjectClass::Menu => &[MausArt::Druck, MausArt::Loslassen], "DBLCLICK" => &[ MausArt::Druck, MausArt::Loslassen, MausArt::Druck, MausArt::Loslassen, ], _ => &[], }; for art in events { host.ereignis(Ereignis::Maus(MausEreignis { art: *art, taste: 1, shift: 0, zeile: 5, spalte: 11, })); } let result = vm.run(&mut host); if result != RunEvent::Ended || integer(&vm, "n") == 0 { failures.push(format!( "{}_{name}: Status implementiert, Handler unerreicht: {result:?}", class.name() )); } } } assert!(failures.is_empty(), "{}", failures.join("\n")); } #[test] fn shell_und_irr_funktionssignaturen_erreichen_die_runtime() { let mut vm = vm_for_source( "DIM a(1) AS DOUBLE\na(0)=-100\na(1)=110\nr#=IRR#(a,.1)\ns&=SHELL(\"exit 7\")\nEND", ); assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); assert!(matches!(vm.inspect("r"),Some(Value::Dbl(n)) if (n-0.1).abs()<1e-8)); assert!(matches!(vm.inspect("s"), Some(Value::Lng(7)))); } fn event_nachgewiesen(name: &str, count: i16) -> Result<(), String> { if count > 0 { Ok(()) } else { Err(format!( "{name}: Status implementiert, fehlender Auslösepfad zum BASIC-Handler" )) } } #[test] fn fehlender_ereignispfad_wird_namentlich_erkannt() { let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); let mut module=tb_vm::compile_source_with_forms("FORM1","DIM SHARED n%\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED n%\nn%=n%+1\nEND SUB",&catalog).unwrap(); module.event_procs.clear(); // künstlich fehlende Dispatch-Bindung let mut vm = Vm::new(module); let mut host = CaptureHost::default(); stop(&mut vm, &mut host); host.ereignis(key("a")); assert_eq!(vm.run(&mut host), RunEvent::Ended); let error = event_nachgewiesen("FORM_KEYPRESS", integer(&vm, "n")).unwrap_err(); assert!( error.contains("FORM_KEYPRESS") && error.contains("implementiert") && error.contains("Auslösepfad") ); }