//! 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::(), 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> = 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"), "๐Ÿ˜€"); }