use std::collections::VecDeque; use tb_runtime::{ host::{Ereignis, Host}, screen::TextScreen, snapshot, value::Value, }; use tb_vm::interp::{PollResult, RunEvent, Vm}; #[derive(Default)] struct HostStub { now: u64, clocks: usize, input: VecDeque, } impl Host for HostStub { fn present(&mut self, _: &TextScreen) {} fn next_event(&mut self, blocking: bool) -> Option { assert!(!blocking); self.input.pop_front() } fn warten(&mut self, _: Option) -> Option { panic!("poll blocked") } fn jetzt_ms(&mut self) -> u64 { self.clocks += 1; self.now } } fn vm(source: &str) -> Vm { Vm::new(tb_vm::compile_source("TEST", source).unwrap()) } fn text(vm: &Vm, name: &str) -> String { match vm.inspect(name) { Some(Value::Str(s)) => s.to_string(), v => panic!("{v:?}"), } } fn input(host: &mut HostStub, chars: &str) { host.input .extend(chars.chars().map(|c| Ereignis::Taste(c.to_string(), 0))); } fn poll(vm: &mut Vm, host: &mut HostStub) -> PollResult { vm.poll(host, 200) } fn end(vm: &mut Vm, host: &mut HostStub) { for _ in 0..1000 { match poll(vm, host) { PollResult::Event(RunEvent::Ended) => return, PollResult::Yield | PollResult::Waiting { .. } => {} other => panic!("{other:?}"), } } panic!("did not end"); } fn pause(vm: &mut Vm, host: &mut HostStub) { vm.rt.abbruch = true; assert!(matches!( poll(vm, host), PollResult::Event(RunEvent::Interrupted { .. }) )); vm.rt.abbruch = false; } #[test] fn finite_slices_preserve_events_without_clock_queries() { for source in ["DO\nLOOP", "10 GOTO 10"] { let mut vm = vm(source); vm.set_poll_interrupt(true); let mut host = HostStub::default(); for _ in 0..10 { assert_eq!(poll(&mut vm, &mut host), PollResult::Yield); } assert_eq!(host.clocks, 0); pause(&mut vm, &mut host); } let mut vm = vm("STOP\nPRINT 42\nEND"); let mut host = HostStub::default(); assert!(matches!( poll(&mut vm, &mut host), PollResult::Event(RunEvent::Stopped { .. }) )); end(&mut vm, &mut host); assert!(snapshot::text(&vm.rt.screen).contains("42")); assert_eq!(host.clocks, 0); } #[test] fn partial_console_operations_keep_prompt_arguments_and_assignment() { for instruction in [ "INPUT \"Name\"; s$", "LINE INPUT \"Name\"; s$", "s$=INPUT$(3)", ] { let mut vm = vm(&format!("{instruction}\nn%=n%+1\nEND")); let mut host = HostStub::default(); input(&mut host, "a"); assert!(matches!( poll(&mut vm, &mut host), PollResult::Waiting { .. } )); let before = snapshot::text(&vm.rt.screen); pause(&mut vm, &mut host); assert_eq!(snapshot::text(&vm.rt.screen), before); input( &mut host, if instruction.contains("INPUT$(") { "bc" } else { "bc\r" }, ); end(&mut vm, &mut host); assert_eq!(text(&vm, "s"), "abc"); assert!(matches!(vm.inspect("n"), Some(Value::Int(1)))); assert!(snapshot::text(&vm.rt.screen).matches("Name").count() <= 1); } let mut vm = vm("RANDOMIZE\nPRINT RND\nEND"); let mut host = HostStub::default(); input(&mut host, "12"); assert!(matches!( poll(&mut vm, &mut host), PollResult::Waiting { .. } )); pause(&mut vm, &mut host); input(&mut host, "3\r"); end(&mut vm, &mut host); assert_eq!( snapshot::text(&vm.rt.screen) .matches("Random Number Seed") .count(), 1 ); } #[test] fn sleep_deadline_survives_pause_and_does_not_pop_argument_twice() { let mut vm = vm("SLEEP 2\nn%=n%+1\nEND"); let mut host = HostStub::default(); assert_eq!( poll(&mut vm, &mut host), PollResult::Waiting { deadline: Some(2000) } ); host.now = 1000; pause(&mut vm, &mut host); assert_eq!( poll(&mut vm, &mut host), PollResult::Waiting { deadline: Some(2000) } ); host.now = 2000; end(&mut vm, &mut host); assert!(matches!(vm.inspect("n"), Some(Value::Int(1)))); } #[test] fn dialogs_keep_partial_text_focus_and_caller_arguments() { for (source, chars) in [ ("s$=INPUTBOX$(\"Prompt\",\"Title\")\nEND", "ab"), ("n%=MSGBOX(\"Prompt\",256+1,\"Title\")\nEND", ""), ] { let mut vm = vm(source); let mut host = HostStub::default(); input(&mut host, chars); for _ in 0..5 { assert!(matches!( poll(&mut vm, &mut host), PollResult::Waiting { .. } )); } pause(&mut vm, &mut host); input(&mut host, "\r"); end(&mut vm, &mut host); if chars.is_empty() { assert!(matches!(vm.inspect("n"), Some(Value::Int(2)))); } else { assert_eq!(text(&vm, "s"), "ab"); } } } #[test] fn modal_and_modeless_forms_resume_handlers_across_tiny_budgets() { use tb_frontend::forms::{FormCatalog, ObjectClass}; let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false); for modal in [false, true] { let source = format!("Timer1.Interval=100\nTimer1.Enabled=-1\nForm1.Show {}\n{}\nSUB Timer1_Timer\nFOR i%=1 TO 100\nNEXT\nForm1.Hide\nEND SUB", if modal {"1"} else {"0"}, if modal {"END"} else {""}); let mut vm = Vm::new(tb_vm::compile_source_with_forms("FORM1", &source, &catalog).unwrap()); let mut host = HostStub::default(); let result = poll(&mut vm, &mut host); if modal { assert!(matches!(result, PollResult::Waiting { .. })); } else { assert_eq!(result, PollResult::Event(RunEvent::Ended)); } pause(&mut vm, &mut host); host.now = 100; for _ in 0..1000 { let result = if modal { vm.poll(&mut host, 3) } else { vm.poll_visible_forms(&mut host, 3) }; if result == PollResult::Event(RunEvent::Ended) && !vm.forms.has_visible_forms() { break; } } assert!(!vm.forms.has_visible_forms()); } } #[test] fn pending_dialog_keeps_physical_resize_and_modeless_handler_finishes_after_hide() { let mut vm = vm("s$=INPUTBOX$(\"Prompt\")\nEND"); let mut host = HostStub::default(); assert!(matches!( poll(&mut vm, &mut host), PollResult::Waiting { .. } )); vm.rt.ereignis(Ereignis::Groesse { cols: 120, rows: 40, }); input(&mut host, "\r"); end(&mut vm, &mut host); assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40)); use tb_frontend::forms::{FormCatalog, ObjectClass}; let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); let module=tb_vm::compile_source_with_forms("FORM1","Form1.Show\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nForm1.Hide\nPRINT \"after hide\"\nEND SUB",&catalog).unwrap(); let mut vm = Vm::new(module); assert_eq!(poll(&mut vm, &mut host), PollResult::Event(RunEvent::Ended)); input(&mut host, "a"); for _ in 0..100 { if vm.poll_visible_forms(&mut host, 1) == PollResult::Event(RunEvent::Ended) { break; } } assert!(snapshot::text(&vm.rt.screen).contains("after hide")); } #[test] fn common_dialog_external_call_keeps_byref_arguments_until_completion() { let mut vm=vm("DECLARE SUB FindText (s AS STRING, flags AS INTEGER, cancelled AS INTEGER)\ns$=\"before\"\nFindText s$, flags%, cancelled%\nEND"); let mut host = HostStub::default(); assert!(matches!( poll(&mut vm, &mut host), PollResult::Waiting { .. } )); pause(&mut vm, &mut host); input(&mut host, "\r"); end(&mut vm, &mut host); assert_eq!(text(&vm, "s"), "before"); assert!(matches!(vm.inspect("cancelled"), Some(Value::Int(0)))); }