Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
@@ -476,9 +476,10 @@ fn print_zonen_und_tab() {
|
||||
|
||||
#[test]
|
||||
fn unsupported_feature_fehler_73() {
|
||||
// Dokumentiert, aber noch offen: `SETUEVENT` kommt mit Phase 4
|
||||
// Dokumentiert, aber noch offen: `RUN` kommt mit Phase 5
|
||||
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
|
||||
let (ev, _) = run("SETUEVENT");
|
||||
// (`SETUEVENT` stand hier bis zum Change `phase-4-ereignisschleife`.)
|
||||
let (ev, _) = run("RUN");
|
||||
match ev {
|
||||
RunEvent::Error { code, message, .. } => {
|
||||
assert_eq!(code, 73);
|
||||
@@ -510,9 +511,8 @@ fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
|
||||
/// die Eingabeaufforderung unsichtbar da.
|
||||
#[test]
|
||||
fn ausgabe_erscheint_vor_der_eingabe() {
|
||||
use tb_runtime::host::{CaptureHost, Host};
|
||||
let module =
|
||||
tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
|
||||
use tb_runtime::host::CaptureHost;
|
||||
let module = tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::with_input(&["7"]);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
@@ -534,10 +534,10 @@ fn eingabe_wird_geechot_und_rueckschritt_loescht() {
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
for c in ['a', 'b', 'x'] {
|
||||
host.ereignis(Ereignis::Taste(c.to_string()));
|
||||
host.ereignis(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string()));
|
||||
host.ereignis(Ereignis::Taste(taste::ENTER.to_string()));
|
||||
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string(), 0));
|
||||
host.ereignis(Ereignis::Taste(taste::ENTER.to_string(), 0));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
let text = tb_runtime::snapshot::text(&vm.rt.screen);
|
||||
// Erste Zeile: das Echo ohne das gelöschte Zeichen.
|
||||
@@ -609,8 +609,8 @@ fn inkey_liefert_zeichen_und_sondertaste() {
|
||||
.unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Taste("q".into()));
|
||||
host.ereignis(Ereignis::Taste(taste::sonder(59))); // F1
|
||||
host.ereignis(Ereignis::Taste("q".into(), 0));
|
||||
host.ereignis(Ereignis::Taste(taste::sonder(59), 0)); // F1
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
let t = tb_runtime::snapshot::text(&vm.rt.screen);
|
||||
// "q", dann Länge 2 und führendes Nullzeichen.
|
||||
@@ -624,7 +624,7 @@ fn input_s_liest_genau_n_zeichen_ohne_echo() {
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
for c in ['x', 'y', 'z', 'w'] {
|
||||
host.ereignis(Ereignis::Taste(c.to_string()));
|
||||
host.ereignis(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
// Nur das PRINT erscheint — INPUT$ echot nicht.
|
||||
@@ -774,3 +774,322 @@ fn rueckfall_auf_utc_laeuft_fehlerfrei() {
|
||||
);
|
||||
assert_eq!(t.trim(), "10 8 -1 -1", "{t:?}");
|
||||
}
|
||||
|
||||
// ---- Ereignis-Traps (Change `phase-4-ereignisschleife`) ---------------------
|
||||
|
||||
/// Programm mit virtueller Uhr laufen lassen: die Uhr rückt an jedem
|
||||
/// Zustellpunkt um `schritt_ms` vor (design.md, D6).
|
||||
fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
|
||||
struct UhrHost {
|
||||
inner: CaptureHost,
|
||||
schritt: u64,
|
||||
}
|
||||
impl tb_runtime::host::Host for UhrHost {
|
||||
fn present(&mut self, s: &tb_runtime::screen::TextScreen) {
|
||||
self.inner.present(s);
|
||||
}
|
||||
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||
self.inner.next_event(b)
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
let j = self.inner.jetzt_ms();
|
||||
self.inner.uhr_vorruecken(self.schritt);
|
||||
j
|
||||
}
|
||||
}
|
||||
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
|
||||
panic!("Compile-Fehler: {d:?}");
|
||||
});
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = UhrHost {
|
||||
inner: CaptureHost::default(),
|
||||
schritt: schritt_ms,
|
||||
};
|
||||
let ev = vm.run(&mut host);
|
||||
(ev, tb_runtime::snapshot::text(&vm.rt.screen))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeit_trap_laeuft_und_kehrt_zurueck() {
|
||||
// Die Schleife druckt x, der Trap dazwischen T. Nach RETURN zaehlt die
|
||||
// Schleife unveraendert weiter — der unterbrochene Zustand bleibt.
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"i=\"; i%\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('T'), "Zeit-Trap lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains("i= 201"),
|
||||
"Schleife lief nicht sauber weiter: {out:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ohne_timer_on_kein_trap() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"fertig\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(!out.contains('T'), "Trap lief ohne TIMER ON: {out:?}");
|
||||
assert!(out.contains("fertig"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trap_aus_einer_sub_heraus_kehrt_dorthin_zurueck() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
CALL Rechne\n\
|
||||
PRINT \"ende\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n\
|
||||
SUB Rechne\n\
|
||||
FOR j% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"sub\"; j%\n\
|
||||
END SUB\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains("sub 201"),
|
||||
"SUB lief nicht sauber weiter: {out:?}"
|
||||
);
|
||||
assert!(out.contains("ende"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_off_klammert_die_zustellung() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
EVENT OFF\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
PRINT \"|\";\n\
|
||||
EVENT ON\n\
|
||||
FOR i% = 1 TO 200\n\
|
||||
NEXT\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
let text = out.trim();
|
||||
let strich = text.find('|').expect("Trennzeichen fehlt");
|
||||
assert!(
|
||||
!text[..strich].contains('T'),
|
||||
"waehrend EVENT OFF wurde zugestellt: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
text[strich..].contains('T'),
|
||||
"nach EVENT ON wurde nicht zugestellt: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_off_im_handler_ueberlebt_das_return() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
FOR i% = 1 TO 400\n\
|
||||
NEXT\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
TIMER OFF\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert_eq!(
|
||||
out.trim().matches('T').count(),
|
||||
1,
|
||||
"RETURN hat den Trap wieder eingeschaltet: {out:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gosub_null_schaltet_den_trap_ab() {
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
ON TIMER(1) GOSUB 0\n\
|
||||
FOR i% = 1 TO 300\n\
|
||||
NEXT\n\
|
||||
PRINT \"fertig\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(!out.contains('T'), "abgeschalteter Trap lief: {out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setuevent_loest_den_uevent_trap_aus() {
|
||||
let (ev, out) = run("ON UEVENT GOSUB Nutzer\n\
|
||||
UEVENT ON\n\
|
||||
CALL SetUEvent\n\
|
||||
FOR i% = 1 TO 20\n\
|
||||
NEXT\n\
|
||||
PRINT \"|\";\n\
|
||||
END\n\
|
||||
Nutzer:\n\
|
||||
PRINT \"U\";\n\
|
||||
RETURN\n");
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('U'), "UEVENT-Trap lief nicht: {out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kennung_ausserhalb_des_bereichs_ist_fehler_5() {
|
||||
// Nicht konstant, also erst zur Laufzeit pruefbar.
|
||||
let src = "n% = 99\nON KEY(n%) GOSUB M\nEND\nM:\nRETURN\n";
|
||||
assert_eq!(err_code(src), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leere_warteschleife_hat_eine_anweisungsgrenze() {
|
||||
// `FOR i = 1 TO n: NEXT` ist die klassische Warteschleife. Ohne
|
||||
// Anweisungsgrenze im Kreis würde dort nie ein Ereignis zugestellt,
|
||||
// kein Breakpoint erreicht und kein Abbruch bemerkt.
|
||||
use tb_vm::bytecode::Instr;
|
||||
for src in [
|
||||
"FOR i% = 1 TO 3\nNEXT\n",
|
||||
"WHILE i% < 3\nWEND\n",
|
||||
"DO WHILE i% < 3\nLOOP\n",
|
||||
"DO\nLOOP UNTIL i% > 3\n",
|
||||
] {
|
||||
let m = tb_vm::compile_source("T", src).unwrap();
|
||||
let code = &m.procs[0].code;
|
||||
let mut kreise = 0;
|
||||
for (j, i) in code.iter().enumerate() {
|
||||
let ziel = match i {
|
||||
Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => *t as usize,
|
||||
_ => continue,
|
||||
};
|
||||
if ziel >= j {
|
||||
continue;
|
||||
}
|
||||
kreise += 1;
|
||||
assert!(
|
||||
code[ziel..=j].iter().any(|i| matches!(i, Instr::Stmt(_))),
|
||||
"Schleife ohne Anweisungsgrenze im Kreis: {src:?}"
|
||||
);
|
||||
}
|
||||
assert!(kreise > 0, "kein Rücksprung gefunden: {src:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doevents_stellt_zu() {
|
||||
// Die Warteschleife ruft DOEVENTS; der Trap muss darin laufen.
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
FOR i% = 1 TO 5\n\
|
||||
x% = DOEVENTS\n\
|
||||
NEXT\n\
|
||||
PRINT \"|\";\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
let t = out.trim();
|
||||
let strich = t.find('|').expect("Trennzeichen fehlt");
|
||||
assert!(
|
||||
t[..strich].contains('T'),
|
||||
"DOEVENTS stellte nicht zu: {t:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doevents_liefert_null() {
|
||||
assert_eq!(out("PRINT DOEVENTS").trim(), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_endet_durch_ein_ereignis() {
|
||||
// SLEEP 60 wuerde ohne Zustellung eine Minute warten. Der faellige
|
||||
// Trap beendet es; danach laeuft das Programm hinter dem SLEEP weiter.
|
||||
let t = std::time::Instant::now();
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"ON TIMER(1) GOSUB Tick\n\
|
||||
TIMER ON\n\
|
||||
SLEEP 60\n\
|
||||
PRINT \"danach\"\n\
|
||||
END\n\
|
||||
Tick:\n\
|
||||
PRINT \"T\";\n\
|
||||
RETURN\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains("danach"),
|
||||
"SLEEP kehrte nicht zurueck: {out:?}"
|
||||
);
|
||||
assert!(
|
||||
t.elapsed().as_secs() < 5,
|
||||
"SLEEP wartete die vollen 60 Sekunden ab"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehler_im_trap_handler_erreicht_den_modulweiten_handler() {
|
||||
// `ERL` nennt die zuletzt ausgefuehrte **Zeilennummer** — deshalb ist
|
||||
// das Programm nummeriert. Der Fehler entsteht in Zeile 90 (im
|
||||
// Handler), nicht an der unterbrochenen Stelle.
|
||||
let (ev, out) = run_mit_uhr(
|
||||
"10 ON ERROR GOTO 200\n 20 ON TIMER(1) GOSUB 90\n 30 TIMER ON\n 40 FOR i% = 1 TO 300\n 50 NEXT\n 60 PRINT \"|\";\n 70 END\n 90 ERROR 7\n 100 RETURN\n 200 PRINT \"E\"; ERR; ERL;\n 210 RESUME NEXT\n",
|
||||
600,
|
||||
);
|
||||
assert_eq!(ev, RunEvent::Ended, "{out}");
|
||||
assert!(out.contains("E 7"), "Fehlerbehandlung lief nicht: {out:?}");
|
||||
assert!(
|
||||
out.contains(" 90"),
|
||||
"ERL nennt nicht die Zeile im Handler: {out:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
|
||||
// Regression: der Handler steht selbst auf einer nummerierten Zeile.
|
||||
// `ERL` muss trotzdem die Zeile des Fehlers melden (Spec
|
||||
// vm-fehlerbehandlung, „ERR und ERL").
|
||||
let out = out("10 ON ERROR GOTO 100\n\
|
||||
20 ERROR 7\n\
|
||||
30 END\n\
|
||||
100 PRINT ERL;\n\
|
||||
110 RESUME NEXT\n");
|
||||
assert_eq!(out.trim(), "20");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user