Phase 4: Ereignisschleife und klassische Traps
This commit is contained in:
@@ -153,11 +153,16 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
struct PipeHost {
|
||||
puffer: std::collections::VecDeque<Ereignis>,
|
||||
eof: bool,
|
||||
start: std::time::Instant,
|
||||
}
|
||||
|
||||
impl PipeHost {
|
||||
fn new() -> Self {
|
||||
PipeHost { puffer: std::collections::VecDeque::new(), eof: false }
|
||||
PipeHost {
|
||||
puffer: std::collections::VecDeque::new(),
|
||||
eof: false,
|
||||
start: std::time::Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Eine Zeile von stdin in Tastendrücke zerlegen.
|
||||
@@ -177,10 +182,12 @@ impl PipeHost {
|
||||
zeile.pop();
|
||||
}
|
||||
for c in zeile.chars() {
|
||||
self.puffer.push_back(Ereignis::Taste(c.to_string()));
|
||||
self.puffer.push_back(Ereignis::Taste(c.to_string(), 0));
|
||||
}
|
||||
self.puffer
|
||||
.push_back(Ereignis::Taste(tb_runtime::host::taste::ENTER.to_string()));
|
||||
self.puffer.push_back(Ereignis::Taste(
|
||||
tb_runtime::host::taste::ENTER.to_string(),
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,4 +202,8 @@ impl Host for PipeHost {
|
||||
}
|
||||
self.puffer.pop_front()
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.start.elapsed().as_millis() as u64
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ fn groesse_lesen(name: &str, wert: &str) -> (usize, usize) {
|
||||
/// Wert einer Kopfzeilen-Direktive `' <marke>: <wert>` aus den ersten Zeilen.
|
||||
fn direktive(src: &str, marke: &str) -> Option<String> {
|
||||
src.lines()
|
||||
.take(4)
|
||||
// Kopfzeilenfenster: Groesse, Zeitzone, Uhr, Tasten, Tempdir,
|
||||
// Resize passen zusammen in die ersten Zeilen.
|
||||
.take(8)
|
||||
.find(|l| l.contains(marke))
|
||||
.map(|l| l.split(marke).nth(1).unwrap().trim().to_string())
|
||||
}
|
||||
@@ -94,6 +96,120 @@ fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vorbereitete Tastendrücke: `' tb-keys: <folge>`. Sondertasten als
|
||||
/// `<F1>`…`<F12>`, sonst je ein Zeichen; `+` davor bedeutet Umschalt,
|
||||
/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten.
|
||||
fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
|
||||
use tb_runtime::host::{taste, umschalt, Ereignis};
|
||||
let Some(wert) = direktive(src, "tb-keys:") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
let zeichen: Vec<char> = wert.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < zeichen.len() {
|
||||
let mut shift = 0u8;
|
||||
while i < zeichen.len() && matches!(zeichen[i], '+' | '^' | '%') {
|
||||
shift |= match zeichen[i] {
|
||||
'+' => umschalt::SHIFT,
|
||||
'^' => umschalt::CTRL,
|
||||
_ => umschalt::ALT,
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
if i >= zeichen.len() {
|
||||
panic!("{name}: tb-keys endet mit einem Umschaltzeichen");
|
||||
}
|
||||
if zeichen[i] == '<' {
|
||||
let ende = zeichen[i..]
|
||||
.iter()
|
||||
.position(|c| *c == '>')
|
||||
.unwrap_or_else(|| panic!("{name}: tb-keys: `<` ohne `>`"));
|
||||
let nm: String = zeichen[i + 1..i + ende].iter().collect();
|
||||
let code = match nm.to_uppercase().as_str() {
|
||||
"F1" => 0x3B,
|
||||
"F2" => 0x3C,
|
||||
"F3" => 0x3D,
|
||||
"F4" => 0x3E,
|
||||
"F5" => 0x3F,
|
||||
"F6" => 0x40,
|
||||
"F7" => 0x41,
|
||||
"F8" => 0x42,
|
||||
"F9" => 0x43,
|
||||
"F10" => 0x44,
|
||||
"F11" => 0x85,
|
||||
"F12" => 0x86,
|
||||
"UP" => 0x48,
|
||||
"LEFT" => 0x4B,
|
||||
"RIGHT" => 0x4D,
|
||||
"DOWN" => 0x50,
|
||||
other => panic!("{name}: tb-keys kennt `<{other}>` nicht"),
|
||||
};
|
||||
out.push(Ereignis::Taste(taste::sonder(code), shift));
|
||||
i += ende + 1;
|
||||
} else {
|
||||
out.push(Ereignis::Taste(zeichen[i].to_string(), shift));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Virtuelle Uhr: `' tb-clock: <n>ms/Zustellpunkt`. Ohne die Direktive
|
||||
/// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe
|
||||
/// dann endlos. Der Harness weist es deshalb ab, statt auf die Systemuhr
|
||||
/// auszuweichen (design.md, D6).
|
||||
fn clock_direktive(name: &str, src: &str) -> Option<u64> {
|
||||
let wert = direktive(src, "tb-clock:")?;
|
||||
let ms = wert
|
||||
.trim()
|
||||
.strip_suffix("ms/Zustellpunkt")
|
||||
.unwrap_or_else(|| panic!("{name}: tb-clock erwartet `<n>ms/Zustellpunkt`, fand {wert:?}"))
|
||||
.trim();
|
||||
Some(
|
||||
ms.parse()
|
||||
.unwrap_or_else(|_| panic!("{name}: {ms:?} ist keine Zahl")),
|
||||
)
|
||||
}
|
||||
|
||||
/// Nutzt das Programm zeitgesteuerte Traps? Grobe, aber ausreichende
|
||||
/// Prüfung: ohne `ON TIMER` gibt es keine Fälligkeit zu rechnen.
|
||||
fn braucht_uhr(src: &str) -> bool {
|
||||
src.to_uppercase().contains("ON TIMER")
|
||||
}
|
||||
|
||||
fn clock_schritt(name: &str, src: &str) -> Option<u64> {
|
||||
let schritt = clock_direktive(name, src);
|
||||
assert!(
|
||||
schritt.is_some() || !braucht_uhr(src),
|
||||
"{name}: nutzt zeitgesteuerte Traps, deklariert aber keinen Zeitverlauf — \
|
||||
erste Zeilen brauchen `' tb-clock: <n>ms/Zustellpunkt`"
|
||||
);
|
||||
schritt
|
||||
}
|
||||
|
||||
/// Testhost mit virtueller Uhr: sie rückt bei jedem Blick auf die Zeit um
|
||||
/// einen festen Schritt vor. Damit ist der Lauf reproduzierbar und
|
||||
/// braucht keine Wartezeit.
|
||||
struct UhrHost {
|
||||
inner: CaptureHost,
|
||||
schritt_ms: 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, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||
self.inner.next_event(blockierend)
|
||||
}
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
let jetzt = self.inner.jetzt_ms();
|
||||
self.inner.uhr_vorruecken(self.schritt_ms);
|
||||
jetzt
|
||||
}
|
||||
}
|
||||
|
||||
/// Arbeitsverzeichnis für die Dauer eines Korpusprogramms. Programme mit
|
||||
/// Datei-E/A laufen in einem frischen temporären Verzeichnis und hinterlassen
|
||||
/// nichts im Projektbaum; beim Verlassen wird es samt Inhalt entfernt.
|
||||
@@ -146,6 +262,24 @@ fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
|
||||
tb_runtime::host::Ereignis::Groesse { cols: c, rows: r },
|
||||
);
|
||||
}
|
||||
for t in tasten_direktive(&name, &src) {
|
||||
host.ereignis(t);
|
||||
}
|
||||
let schritt = clock_schritt(&name, &src);
|
||||
if let Some(schritt_ms) = schritt {
|
||||
let mut host = UhrHost {
|
||||
inner: host,
|
||||
schritt_ms,
|
||||
};
|
||||
return match vm.run(&mut host) {
|
||||
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
|
||||
other => panic!(
|
||||
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
|
||||
path.display(),
|
||||
tb_runtime::snapshot::snapshot(&vm.rt.screen)
|
||||
),
|
||||
};
|
||||
}
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
|
||||
other => panic!(
|
||||
@@ -207,12 +341,8 @@ fn assert_output_matches(file: &str, want: &str, got: &str) {
|
||||
.position(|(a, b)| a != b)
|
||||
.map(|i| i / 2 + 1)
|
||||
.unwrap_or(0);
|
||||
let paar = |s: &str| {
|
||||
s.chars()
|
||||
.skip((spalte - 1) * 2)
|
||||
.take(2)
|
||||
.collect::<String>()
|
||||
};
|
||||
let paar =
|
||||
|s: &str| s.chars().skip((spalte - 1) * 2).take(2).collect::<String>();
|
||||
panic!(
|
||||
"{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \
|
||||
Soll: {}\n Ist: {}",
|
||||
@@ -257,12 +387,31 @@ fn korpus_laeuft_mit_korrekter_ausgabe() {
|
||||
let got = run_corpus_file(&path, Some((*cols, *rows)));
|
||||
let etikett = format!("{name} @ {cols}x{rows}");
|
||||
assert_output_matches(&etikett, &want, &got);
|
||||
let wiederholt = run_corpus_file(&path, Some((*cols, *rows)));
|
||||
assert_eq!(
|
||||
got, wiederholt,
|
||||
"{etikett}: zwei Läufe weichen voneinander ab"
|
||||
);
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uhr_direktive_wird_gelesen() {
|
||||
assert_eq!(
|
||||
clock_schritt("uhr.bas", "' tb-clock: 25ms/Zustellpunkt\nON TIMER(1)"),
|
||||
Some(25)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "deklariert aber keinen Zeitverlauf")]
|
||||
fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() {
|
||||
clock_schritt("uhr.bas", "ON TIMER(1) GOSUB Tick");
|
||||
}
|
||||
|
||||
// ---- tbc-Binary (Exit-Codes nach D6) ----------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user