195 lines
6.5 KiB
Rust
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"), "😀");
|
|
}
|