Files
TerminalBasic/crates/tb-vm/tests/events.rs

910 lines
35 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<Ereignis> {
block.then(|| key("\r"))
}
fn jetzt_ms(&mut self) -> u64 {
self.0.jetzt_ms()
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
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<Ereignis> {
self.0.next_event(b)
}
fn jetzt_ms(&mut self) -> u64 {
panic!("unerwartete Uhrabfrage")
}
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
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<Option<u64>>,
timer: bool,
}
impl Host for WarteHost {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.now
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
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 V1V5: 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<Ereignis> {
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<u64>) -> Option<Ereignis> {
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<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.time
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
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<Ereignis>,
now: u64,
deadlines: Vec<Option<u64>>,
}
impl Host for EingabeHost {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.now
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
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<Ereignis>) -> 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::<Vec<_>>()
.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::<Vec<_>>()
.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")
);
}