Ereigniszustellung und Hostgrenzen korrigieren und Change archivieren

This commit is contained in:
2026-09-05 19:15:31 +02:00
parent 57d6386a5a
commit 644a86212f
39 changed files with 2013 additions and 271 deletions

View File

@@ -78,11 +78,27 @@ struct Frame {
waiting_form: Option<u16>,
pending_show: Option<u16>,
form_event: Option<FormEventReturn>,
handler_start: bool,
eingabe: Option<KonsolenEingabe>,
}
struct KonsolenEingabe {
text: String,
/// None = Zeile bis Enter; Some = noch erwartete Tasten für INPUT$.
rest: Option<usize>,
}
#[derive(Clone, Copy, PartialEq)]
enum Zustellpunkt {
Anweisung,
Kooperativ,
Konsole,
}
#[derive(Clone, Copy)]
enum FormEventReturn {
Normal,
Load,
Unload(u16),
}
@@ -225,6 +241,8 @@ impl Vm {
waiting_form: None,
pending_show: None,
form_event: None,
handler_start: false,
eingabe: None,
});
}
@@ -262,36 +280,15 @@ impl Vm {
}
}
fn take_dialog_input(&mut self) -> VecDeque<Ereignis> {
let mut events = VecDeque::new();
events.extend(
self.rt
.tasten
.drain(..)
.map(|(key, shift)| Ereignis::Taste(key, shift)),
);
events.extend(self.rt.maus.drain(..).map(Ereignis::Maus));
events
fn take_dialog_input(&mut self) -> VecDeque<tb_runtime::builtins::Eingabe> {
std::mem::take(&mut self.rt.eingaben)
}
fn restore_dialog_input(&mut self, events: VecDeque<Ereignis>) {
for event in events {
match event {
Ereignis::Taste(key, shift) => self.rt.tasten.push_back((key, shift)),
Ereignis::Maus(event) => self.rt.maus.push_back(event),
Ereignis::Abbruch => self.rt.abbruch = true,
Ereignis::Ende => self.rt.ende = true,
Ereignis::Signal(number) => {
if number == 1
&& !self
.rt
.traps
.melden(tb_runtime::traps::Quelle::Signal(number))
{
self.rt.abbruch = true;
}
}
Ereignis::Groesse { cols, rows } => self.rt.screen.resize(cols, rows),
fn restore_dialog_input(&mut self, events: VecDeque<tb_runtime::builtins::Eingabe>) {
for input in events {
match input.ereignis {
Ereignis::Taste(..) | Ereignis::Maus(_) => self.rt.eingaben.push_back(input),
event => self.rt.ereignis(event),
}
}
}
@@ -563,13 +560,47 @@ impl Vm {
);
if let Some(frame) = self.frames.last_mut() {
frame.form_event = Some(on_return);
frame.handler_start = true;
}
true
}
fn dispatch_next_form_event(&mut self) -> bool {
while let Some(event) = self.forms.next_event() {
if self.dispatch_form_event(event, FormEventReturn::Normal) {
fn dispatch_next_form_event(&mut self, load_only: bool) -> bool {
let mut i = 0;
while let Some(event) = self.forms.events.get(i) {
if load_only && !event.name.eq_ignore_ascii_case("LOAD") {
i += 1;
continue;
}
let laeuft = self
.module
.event_procs
.iter()
.find(|binding| {
binding.object == event.object
&& binding.event.eq_ignore_ascii_case(&event.name)
})
.is_some_and(|binding| {
self.frames.iter().any(|frame| {
frame.form_event.is_some() && frame.proc == binding.proc as usize
})
});
// Gesperrte Timer und laufende Prozeduren bleiben anstehend.
// Menübedienung und explizite modale Kooperation müssen trotzdem
// andere Handler erreichen können.
if laeuft || (self.forms.menu_is_open() && event.name.eq_ignore_ascii_case("TIMER")) {
i += 1;
continue;
}
let event = self.forms.events.remove(i).unwrap();
if self.dispatch_form_event(
event,
if load_only {
FormEventReturn::Load
} else {
FormEventReturn::Normal
},
) {
return true;
}
}
@@ -577,6 +608,12 @@ impl Vm {
}
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
if self.frames.iter().any(|frame| {
matches!(frame.form_event,
Some(FormEventReturn::Unload(running)) if running == object)
}) {
return Ok(());
}
let event = FormEvent {
object,
array_index: None,
@@ -590,80 +627,136 @@ impl Vm {
}
fn tick(&mut self, host: &mut dyn Host) {
self.rt.traps.suspendieren(self.forms.menu_is_open());
self.forms.render(&mut self.rt.screen);
if self.forms.menu_is_open() {
host.present(&self.rt.screen);
self.rt.pump(host, false);
} else {
self.rt.tick(host);
host.present(&self.rt.screen);
self.rt.pump(host, false);
if !self.forms.menu_is_open() && self.rt.traps.naechste_frist().is_some() {
self.rt.traps.zeit_pruefen(host.jetzt_ms());
}
}
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool {
self.forms
.resize(self.rt.screen.cols(), self.rt.screen.rows());
self.forms.timers(host.jetzt_ms());
if self.forms.active_form().is_some() {
while let Some((key, shift)) = self.rt.tasten.pop_front() {
self.forms.handle_key(&key, shift);
/// Ein gemeinsamer Zustellpfad für Statements, DOEVENTS, SLEEP und
/// blockierende Eingabe. Nur explizite Kooperation verschachtelt Forms.
fn zustellen(&mut self, host: &mut dyn Host, punkt: Zustellpunkt) -> bool {
let kooperativ = punkt != Zustellpunkt::Anweisung;
'zustellung: loop {
self.rt.traps.suspendieren(self.forms.menu_is_open());
self.forms
.resize(self.rt.screen.cols(), self.rt.screen.rows());
if self.forms.next_deadline().is_some() {
self.forms.timers(host.jetzt_ms());
}
}
let now_ms = host.jetzt_ms();
while let Some(m) = self.rt.maus.pop_front() {
self.forms.handle_mouse_at(m, now_ms);
}
self.forms.render(&mut self.rt.screen);
self.dispatch_next_form_event()
}
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
/// `true`, wenn ein Handler aufgesetzt wurde.
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
if self.forms.menu_is_open() || !self.rt.traps.aktiv() {
let forms_frei = kooperativ || !self.frames.iter().any(|f| f.form_event.is_some());
if forms_frei && self.dispatch_next_form_event(false) {
return true;
}
if self.rt.traps.aktiv() {
self.rt.tasten_traps_pruefen();
if (kooperativ || self.tick_zaehler.is_multiple_of(64))
&& self.rt.traps.naechste_frist().is_some()
{
self.rt.traps.zeit_pruefen(host.jetzt_ms());
}
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return true;
}
}
// Unverbrauchte Konsolentasten bleiben liegen. Maus ohne Formular
// wird verworfen; mit Formular wird jede Eingabe samt ihrer Handler
// abgearbeitet, bevor die nächste Eingabe angenommen wird.
let mut i = 0;
while i < self.rt.eingaben.len() {
if !self.forms.menu_is_open() && self.rt.taste_trappen(i) {
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return true;
}
continue;
}
if punkt == Zustellpunkt::Konsole
&& matches!(self.rt.eingaben[i].ereignis, Ereignis::Taste(..))
{
i += 1;
continue;
}
if self.forms.active_form().is_some() {
if !forms_frei {
break;
}
match self.rt.eingaben.remove(i).unwrap().ereignis {
Ereignis::Taste(key, shift) => {
let menu_war_offen = self.forms.menu_is_open();
self.forms.handle_key(&key, shift);
self.rt.traps.suspendieren(self.forms.menu_is_open());
if menu_war_offen && !self.forms.menu_is_open() {
continue 'zustellung;
}
}
Ereignis::Maus(m) => {
if m.zeile > 0
&& m.zeile <= self.rt.screen.rows()
&& m.spalte > 0
&& m.spalte <= self.rt.screen.cols()
{
let now = if self.forms.mouse_needs_time(m) {
host.jetzt_ms()
} else {
0
};
self.forms.handle_mouse_at(m, now);
}
}
_ => unreachable!(),
}
if self.dispatch_next_form_event(false) {
return true;
}
} else if matches!(self.rt.eingaben[i].ereignis, Ereignis::Maus(_)) {
self.rt.eingaben.remove(i);
} else {
i += 1;
}
}
self.forms.render(&mut self.rt.screen);
return false;
}
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
self.rt.tasten_traps_pruefen();
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return true;
}
false
}
/// `SLEEP [n]`: wartet `n` Sekunden, ohne Argument bis zum nächsten
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter
/// dem `SLEEP` weiter.
fn warten(&mut self, host: &mut dyn Host, deadline: Option<u64>) {
let deadline = deadline
.into_iter()
.chain(self.forms.next_deadline())
.chain(if self.forms.menu_is_open() {
None
} else {
self.rt.traps.naechste_frist()
})
.min();
if let Some(event) = host.warten(deadline) {
self.rt.ereignis(event);
}
}
/// Ein zugestelltes Ereignis beendet SLEEP; Fristen und Warten gehören
/// ausschließlich dem Host, einschließlich einer virtuellen Uhr.
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
let start = host.jetzt_ms();
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64;
// ponytail: zusätzlich eine echte Zeitschranke, damit ein Host mit
// stehender Uhr (Testhost) nicht ewig wartet. Ceiling: bei einer
// virtuellen Uhr endet `SLEEP` nach der realen Dauer, wenn die
// virtuelle nie vorrückt.
let real_start = std::time::Instant::now();
let deadline =
(sekunden > 0.0).then(|| host.jetzt_ms().saturating_add((sekunden * 1000.0) as u64));
loop {
self.tick(host);
if self.zustellen(host) {
if self.zustellen(host, Zustellpunkt::Kooperativ) || self.rt.ende || self.rt.abbruch {
return;
}
if self.rt.ende || self.rt.abbruch {
if let Some(deadline) = deadline {
if host.jetzt_ms() >= deadline {
return;
}
} else if self.rt.naechste_taste(host, false).is_some() {
return;
}
if sekunden <= 0.0 {
// Ohne Argument: bis zum Tastendruck.
if self.rt.naechste_taste(host, false).is_some() {
return;
}
} else {
let vergangen = host.jetzt_ms().saturating_sub(start);
if vergangen >= dauer_ms || real_start.elapsed().as_millis() as u64 >= dauer_ms {
return;
}
}
std::thread::sleep(std::time::Duration::from_millis(5));
self.warten(host, deadline);
}
}
@@ -686,6 +779,8 @@ impl Vm {
waiting_form: None,
pending_show: None,
form_event: None,
handler_start: true,
eingabe: None,
});
}
@@ -699,7 +794,11 @@ impl Vm {
fn pop_frame(&mut self) {
let f = self.frames.pop().expect("Frame-Unterlauf");
self.locals.truncate(f.locals_base);
if let Some(q) = f.trap {
self.rt.traps.handler_beendet(q);
} else {
self.locals.truncate(f.locals_base);
}
self.stack.truncate(f.stack_base);
}
@@ -806,14 +905,14 @@ impl Vm {
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
while self.forms.has_visible_forms() {
self.tick(host);
let dispatched = self.forms_zustellen(host);
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
if dispatched {
match self.run(host) {
RunEvent::Ended => {}
event => return event,
}
}
if self.rt.ende {
if !dispatched && self.rt.ende {
return RunEvent::Ended;
}
if self.rt.abbruch {
@@ -821,7 +920,9 @@ impl Vm {
line: self.current_line(),
};
}
std::thread::sleep(std::time::Duration::from_millis(1));
if !dispatched && self.forms.has_visible_forms() {
self.warten(host, None);
}
}
RunEvent::Ended
}
@@ -833,11 +934,18 @@ impl Vm {
self.frames.last_mut().unwrap().waiting_form = None;
} else {
self.tick(host);
self.forms_zustellen(host);
if self.rt.ende {
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
if !dispatched && self.rt.ende {
return RunEvent::Ended;
}
std::thread::yield_now();
if self.rt.abbruch {
return RunEvent::Interrupted {
line: self.current_line(),
};
}
if !dispatched {
self.warten(host, None);
}
continue;
}
}
@@ -898,6 +1006,7 @@ impl Vm {
(f.stack_base, f.last_stmt_pc, f.proc)
};
self.stack.truncate(stack_base);
self.frames.last_mut().unwrap().eingabe = None;
self.err = code;
match handler {
Handler::Goto(pc) => {
@@ -1131,27 +1240,11 @@ impl Vm {
self.tick(host);
self.rt.screen.veraenderung_quittieren();
}
if self.forms_zustellen(host) {
let erster_eintritt =
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
if !erster_eintritt && self.zustellen(host, Zustellpunkt::Anweisung) {
return Ok(Flow::Normal);
}
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
// ein Trap definiert ist — sonst kostet die Grenze nichts.
if self.rt.traps.aktiv() {
// ponytail: die Uhr nur alle 64 Anweisungen lesen, die
// Tastenschlange dagegen jedes Mal. Ceiling: die
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
// reicht das nicht, wird daraus ein eigener Zähler je
// Trap.
if self.tick_zaehler.is_multiple_of(64) {
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
}
self.rt.tasten_traps_pruefen();
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return Ok(Flow::Normal);
}
}
if self.flags != 0 {
if self.flags & F_STEP != 0 {
return Ok(Flow::Event(RunEvent::Stepped { line }));
@@ -1193,7 +1286,7 @@ impl Vm {
1 => tb_runtime::traps::Zustand::Aus,
_ => tb_runtime::traps::Zustand::Gestoppt,
};
let jetzt = host.jetzt_ms();
let jetzt = if art == ART_TIMER { host.jetzt_ms() } else { 0 };
self.rt.traps.setzen(q, z, jetzt);
Ok(Flow::Normal)
}
@@ -1203,8 +1296,7 @@ impl Vm {
// lässt den Wert unberührt.
self.stack.push(Value::Int(0));
self.tick(host);
self.forms_zustellen(host);
self.zustellen(host);
self.zustellen(host, Zustellpunkt::Kooperativ);
Ok(Flow::Normal)
}
I::Sleep(mit_arg) => {
@@ -1258,7 +1350,7 @@ impl Vm {
};
if !self.forms.is_loaded_at(object, index) {
self.forms.ensure_loaded_at(object, index)?;
if index.is_none() && self.dispatch_next_form_event() {
if index.is_none() && self.dispatch_next_form_event(true) {
let caller = self.frames.len() - 2;
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
return Ok(Flow::Normal);
@@ -1280,7 +1372,7 @@ impl Vm {
if index.is_none() {
self.push(value.clone());
}
if index.is_none() && self.dispatch_next_form_event() {
if index.is_none() && self.dispatch_next_form_event(true) {
let caller = self.frames.len() - 2;
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
return Ok(Flow::Normal);
@@ -1304,7 +1396,7 @@ impl Vm {
};
self.forms.ensure_loaded_at(object, index)?;
self.push(Value::Obj(object, index));
self.dispatch_next_form_event();
self.dispatch_next_form_event(true);
Ok(Flow::Normal)
}
I::LoadDynamicObjectProperty(property) => {
@@ -1434,7 +1526,7 @@ impl Vm {
&& !self.forms.is_loaded(object)
{
self.forms.ensure_loaded(object)?;
if self.dispatch_next_form_event() {
if self.dispatch_next_form_event(true) {
let caller = self.frames.len() - 2;
self.frames[caller].pending_show = Some(object);
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
@@ -1493,7 +1585,7 @@ impl Vm {
self.forms.object_method_at(object, index, name, args)?;
}
}
self.dispatch_next_form_event();
self.dispatch_next_form_event(true);
Ok(Flow::Normal)
}
I::ObjectLoad(object, unload, has_index) => {
@@ -1508,7 +1600,7 @@ impl Vm {
self.request_unload(object)?;
} else {
self.forms.ensure_loaded(object)?;
self.dispatch_next_form_event();
self.dispatch_next_form_event(true);
}
Ok(Flow::Normal)
}
@@ -2058,6 +2150,8 @@ impl Vm {
self.pop_frame();
}
let f = self.frames.last_mut().unwrap();
f.eingabe = None;
self.stack.truncate(f.stack_base);
f.pc = t as usize;
Ok(Flow::Normal)
}
@@ -2122,8 +2216,10 @@ impl Vm {
if let Some(FormEventReturn::Unload(object)) = event {
self.forms.unload_with(object, |c| *c = cancel)?;
}
if event.is_some() {
self.dispatch_next_form_event();
if matches!(event, Some(FormEventReturn::Normal))
&& !self.frames.iter().any(|f| f.form_event.is_some())
{
self.dispatch_next_form_event(false);
}
Ok(Flow::Normal)
}
@@ -2134,6 +2230,38 @@ impl Vm {
Ok(Flow::Normal)
}
I::CallBuiltin(id, argc) => {
if id == ids::RANDOMIZE && argc == 0 {
if self.frames.last().unwrap().eingabe.is_none() {
self.rt
.screen
.print("Random Number Seed (-32768 to 32767)? ");
}
let caller = self.frames.len() - 1;
if let Some(text) = self.konsoleneingabe(host, None)? {
self.rt.saat_setzen(tb_runtime::format::val(&text));
} else {
self.frames[caller].pc = pc;
}
return Ok(Flow::Normal);
}
if id == ids::INPUT_S && argc == 1 {
let n = match self.stack.last() {
Some(Value::Int(n)) => i32::from(*n),
Some(Value::Lng(n)) => *n,
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let caller = self.frames.len() - 1;
if let Some(text) = self.konsoleneingabe(host, Some(n as usize))? {
self.pop()?;
self.push(Value::Str(Rc::from(text)));
} else {
self.frames[caller].pc = pc;
}
return Ok(Flow::Normal);
}
let mut args = Vec::with_capacity(argc as usize);
for _ in 0..argc {
args.push(self.pop()?);
@@ -2223,7 +2351,10 @@ impl Vm {
Ok(Flow::Normal)
}
I::Input(argc, line_mode, prompt, question) => {
self.do_input(argc, line_mode, prompt, question, host)?;
let caller = self.frames.len() - 1;
if !self.do_input(argc, line_mode, prompt, question, host)? {
self.frames[caller].pc = pc;
}
Ok(Flow::Normal)
}
I::InputFile(argc, line_mode) => {
@@ -2272,6 +2403,64 @@ impl Vm {
Ok(Flow::Normal)
}
/// Fortsetzbarer Leser: Text und Restlänge gehören zum unterbrochenen
/// Frame; Referenzen/Argumente bleiben bis zum Abschluss auf dessen Stack.
fn konsoleneingabe(
&mut self,
host: &mut dyn Host,
rest: Option<usize>,
) -> Result<Option<String>, RuntimeError> {
let caller = self.frames.len() - 1;
self.frames[caller]
.eingabe
.get_or_insert_with(|| KonsolenEingabe {
text: String::new(),
rest,
});
loop {
if self.frames[caller].eingabe.as_ref().unwrap().rest == Some(0) {
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
}
self.tick(host);
if self.zustellen(host, Zustellpunkt::Konsole) {
return Ok(None);
}
if let Some(taste) = self.rt.gepufferte_taste() {
let eingabe = self.frames[caller].eingabe.as_mut().unwrap();
if let Some(rest) = &mut eingabe.rest {
eingabe.text.push_str(&taste);
*rest -= 1;
} else {
match taste.as_str() {
tb_runtime::host::taste::ENTER => {
self.rt.screen.print("\n");
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
}
tb_runtime::host::taste::BACKSPACE => {
if eingabe.text.pop().is_some() {
self.rt.screen.rueckschritt();
}
}
_ => {
if taste.chars().count() == 1
&& taste.chars().next().is_some_and(|c| c as u32 >= 0x20)
{
eingabe.text.push_str(&taste);
self.rt.screen.print(&taste);
}
}
}
}
continue;
}
if self.rt.ende || self.rt.abbruch {
self.frames[caller].eingabe = None;
return Err(RuntimeError(62));
}
self.warten(host, None);
}
}
fn do_input(
&mut self,
argc: u8,
@@ -2279,38 +2468,41 @@ impl Vm {
prompt: u16,
question: bool,
host: &mut dyn Host,
) -> Result<(), RuntimeError> {
// Referenzen in Reihenfolge einsammeln.
let mut refs = Vec::with_capacity(argc as usize);
for _ in 0..argc {
match self.pop()? {
Value::Ref(r) => refs.push(r),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}
}
refs.reverse();
) -> Result<bool, RuntimeError> {
let start = self
.stack
.len()
.checked_sub(argc as usize)
.ok_or(RuntimeError(51))?;
let refs = self.stack[start..]
.iter()
.map(|v| match v {
Value::Ref(r) => Ok(r.clone()),
_ => Err(RuntimeError::TYPE_MISMATCH),
})
.collect::<Result<Vec<_>, _>>()?;
loop {
if prompt != 0xFFFF {
let p = self.module.strings[prompt as usize].clone();
self.rt.screen.print(&p);
if self.frames.last().unwrap().eingabe.is_none() {
if prompt != 0xFFFF {
self.rt.screen.print(&self.module.strings[prompt as usize]);
}
if question {
self.rt.screen.print("? ");
}
}
if question {
self.rt.screen.print("? ");
}
let Some(mut line) = self.rt.read_line(host) else {
return Err(RuntimeError(62)); // Input past end of file
let Some(line) = self.konsoleneingabe(host, None)? else {
return Ok(false);
};
if line_mode {
let r = refs.first().ok_or(RuntimeError(51))?;
self.write_ref(r, Value::Str(Rc::from(line.as_str())))?;
return Ok(());
self.write_ref(r, Value::Str(Rc::from(line)))?;
self.stack.truncate(start);
return Ok(true);
}
// Felder trennen (Quotes respektieren).
let fields = split_input_fields(&line);
if fields.len() != refs.len() {
self.rt.screen.print("Redo from start\n");
line.clear();
continue;
}
// Konvertieren nach aktuellem Zieltyp; Fehler → Redo.
@@ -2333,7 +2525,8 @@ impl Vm {
for (r, v) in refs.iter().zip(vals) {
self.write_ref(r, v)?;
}
return Ok(());
self.stack.truncate(self.stack.len() - argc as usize);
return Ok(true);
}
}

View File

@@ -0,0 +1,584 @@
//! Regressionen zum Change ereigniszustellung-und-hostgrenzen.
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");
}
}

View File

@@ -859,6 +859,12 @@ fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
self.inner.next_event(b)
}
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
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);