Files
TerminalBasic/openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/review-probes.rs

195 lines
6.5 KiB
Rust

//! Unabhängige Reviewproben, 06.09.2026. Als Rust-Testbinary gegen tb-vm/tb-runtime.
use std::{path::PathBuf, rc::Rc};
use tb_runtime::{
builtins::{builtin_table, ids, RtState},
datetime::Zeitzone,
host::{CaptureHost, Ereignis},
value::Value,
};
use tb_vm::{
bytecode::CompiledModule,
interp::{RunEvent, Vm},
};
struct Temp(PathBuf);
impl Temp {
fn new(name: &str) -> Self {
let p =
std::env::temp_dir().join(format!("tb_runtime_review_{name}_{}", std::process::id()));
std::fs::create_dir_all(&p).unwrap();
Self(p)
}
fn file(&self) -> String {
self.0.join("data.txt").to_str().unwrap().to_owned()
}
}
impl Drop for Temp {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn text(vm: &Vm, name: &str) -> String {
match vm.inspect(name).unwrap() {
Value::Str(s) => s.to_string(),
other => panic!("{other:?}"),
}
}
fn expr(s: &str) -> String {
let escaped = s
.replace('"', "\"\"")
.replace('\r', "\" + CHR$(13) + \"")
.replace('\n', "\" + CHR$(10) + \"")
.replace('\0', "\" + CHR$(0) + \"");
format!("\"{escaped}\"")
}
#[test]
fn write_input_roundtrip_string_matrix() {
let tmp = Temp::new("roundtrip");
let p = tmp.file();
let parts = ["", "a", " ", "\t", "\"", ",", "\r\n", "ä😀", "\0", "\u{a0}"];
for a in parts {
for b in parts {
let expected = format!("{a}{b}");
let src = format!("OPEN \"{p}\" FOR OUTPUT AS #1\nWRITE #1, {}, \"next\"\nCLOSE\nOPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, got$\nINPUT #1, next$\nEND", expr(&expected));
let mut vm = Vm::new(tb_vm::compile_source("REVIEW", &src).unwrap());
assert_eq!(
vm.run(&mut CaptureHost::default()),
RunEvent::Ended,
"{expected:?}"
);
assert_eq!(text(&vm, "got"), expected);
assert_eq!(text(&vm, "next"), "next");
}
}
}
#[test]
fn all_nine_file_read_transitions() {
let tmp = Temp::new("transitions");
let p = tmp.file();
for first in 0..3 {
for second in 0..3 {
std::fs::write(&p, "ä,😀\r\nZ").unwrap();
let statement = |mode, variable| match mode {
0 => format!("INPUT #1, {variable}$"),
1 => format!("{variable}$ = INPUT$(1, #1)"),
_ => format!("LINE INPUT #1, {variable}$"),
};
let src = format!(
"OPEN \"{p}\" FOR INPUT AS #1\n{}\n{}\nEND",
statement(first, "a"),
statement(second, "b")
);
let mut vm = Vm::new(tb_vm::compile_source("REVIEW", &src).unwrap());
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(text(&vm, "a"), if first == 2 { "ä,😀" } else { "ä" });
let expected = match (first, second) {
(0, _) => "😀",
(1, 0) => "",
(1, 1) => ",",
(1, 2) => ",😀",
_ => "Z",
};
assert_eq!(text(&vm, "b"), expected, "transition {first}/{second}");
}
}
}
#[test]
fn every_mk_cv_pair_preserves_target_values_and_width() {
let mut st = RtState::default();
let mut host = CaptureHost::default();
let cases = [
(0, Value::Int(i16::MIN)),
(0, Value::Int(i16::MAX)),
(1, Value::Lng(i32::MIN)),
(1, Value::Lng(i32::MAX)),
(2, Value::Sng(-0.0)),
(2, Value::Sng(f32::MAX)),
(3, Value::Dbl(-0.0)),
(3, Value::Dbl(f64::MIN_POSITIVE)),
(4, Value::Cur(i64::MIN)),
(4, Value::Cur(i64::MAX)),
];
for (kind, value) in cases {
let encoded = builtin_table()[ids::MK_S as usize](
&mut st,
&mut host,
&mut [value.clone(), Value::Lng(kind)],
)
.unwrap()
.unwrap();
let decoded = builtin_table()[ids::CV_F as usize](
&mut st,
&mut host,
&mut [encoded.clone(), Value::Lng(kind)],
)
.unwrap()
.unwrap();
match (&value, &decoded) {
(Value::Int(a), Value::Int(b)) => assert_eq!(a, b),
(Value::Lng(a), Value::Lng(b)) => assert_eq!(a, b),
(Value::Cur(a), Value::Cur(b)) => assert_eq!(a, b),
(Value::Sng(a), Value::Sng(b)) => assert_eq!(a.to_bits(), b.to_bits()),
(Value::Dbl(a), Value::Dbl(b)) => assert_eq!(a.to_bits(), b.to_bits()),
_ => panic!("{value:?} -> {decoded:?}"),
}
let Value::Str(encoded) = encoded else {
panic!()
};
let len = encoded.chars().count();
for wrong in [
encoded.chars().take(len - 1).collect::<String>(),
format!("{encoded}\0"),
] {
let err = builtin_table()[ids::CV_F as usize](
&mut st,
&mut host,
&mut [Value::Str(Rc::from(wrong)), Value::Lng(kind)],
)
.unwrap_err();
assert_eq!(err.0, 5);
}
}
}
#[test]
fn timezoneknown_survives_tbc_and_source_failure_recovery() {
thread_local! { static ZONE: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) }; }
let module = tb_vm::compile_source(
"REVIEW",
"a% = TIMEZONEKNOWN\nSTOP\nb% = TIMEZONEKNOWN\nSTOP\nc% = TIMEZONEKNOWN\nEND",
)
.unwrap();
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
vm.rt.zonen_ermittler = |_| ZONE.get();
let mut host = CaptureHost::default();
for (name, zone, expected) in [("a", Some(3600), -1), ("b", None, 0), ("c", Some(0), -1)] {
ZONE.set(zone);
let ev = vm.run(&mut host);
assert!(matches!(ev, RunEvent::Stopped { .. } | RunEvent::Ended));
assert!(matches!(vm.inspect(name), Some(Value::Int(n)) if n == expected));
assert_eq!(vm.rt.zeitzone, Zeitzone::System);
assert_eq!(vm.rt.uhr_offset, 0);
}
}
#[test]
fn successive_function_keys_are_split_in_order() {
let module = tb_vm::compile_source(
"REVIEW",
"a$ = INPUT$(3)\nb$ = INPUT$(1)\nc$ = INPUT$(1)\nEND",
)
.unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for s in ["\0;", "\0<", "😀"] {
host.ereignis(Ereignis::Taste(s.into(), 0));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(text(&vm, "a"), "\0;\0");
assert_eq!(text(&vm, "b"), "<");
assert_eq!(text(&vm, "c"), "😀");
}