//! Kompatibilitäts-Harness (Phase-2-Meilenstein): jede Korpusdatei //! `tests/compat/*.bas` wird kompiliert, im Capture-Host ausgeführt und //! byte-genau gegen ihre `.out` verglichen. Bei Abweichung nennt der //! Test Datei, erste abweichende Zeile sowie Soll und Ist. use std::path::{Path, PathBuf}; use std::process::Command; use tb_runtime::host::CaptureHost; use tb_vm::interp::{RunEvent, Vm}; fn compat_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat") } fn run_corpus_file(path: &Path) -> String { let src = std::fs::read_to_string(path).unwrap(); let name = path.file_stem().unwrap().to_string_lossy().to_uppercase(); let module = tb_vm::compile_source(&name, &src) .unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display())); let mut vm = Vm::new(module); let mut host = CaptureHost::default(); match vm.run(&mut host) { RunEvent::Ended => host.output, other => panic!( "{}: unerwartetes Laufzeitende {other:?}\nAusgabe bisher:\n{}", path.display(), host.output ), } } /// Erste abweichende Zeile melden (byte-genau, inkl. Leerzeichen am Ende). fn assert_output_matches(file: &str, want: &str, got: &str) { if want == got { return; } let want_lines: Vec<&str> = want.split('\n').collect(); let got_lines: Vec<&str> = got.split('\n').collect(); for (i, (w, g)) in want_lines.iter().zip(got_lines.iter()).enumerate() { if w != g { panic!( "{file}: Abweichung in Zeile {}:\n Soll: {w:?}\n Ist: {g:?}", i + 1 ); } } panic!( "{file}: Zeilenanzahl weicht ab (Soll {} / Ist {}).\nSoll:\n{want}\nIst:\n{got}", want_lines.len(), got_lines.len() ); } #[test] fn korpus_laeuft_mit_korrekter_ausgabe() { let dir = compat_dir(); let mut checked = 0; let mut entries: Vec = std::fs::read_dir(&dir) .expect("tests/compat fehlt") .map(|e| e.unwrap().path()) .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas")) .collect(); entries.sort(); for path in entries { let name = path.file_name().unwrap().to_string_lossy().to_string(); let out_path = path.with_extension("out"); let want = std::fs::read_to_string(&out_path) .unwrap_or_else(|_| panic!("{name}: Sollausgabe {} fehlt", out_path.display())); // .out-Dateien sind LF-normiert (.gitattributes); zur Sicherheit // CRLF des Checkouts entfernen. let want = want.replace("\r\n", "\n"); let got = run_corpus_file(&path); assert_output_matches(&name, &want, &got); checked += 1; } assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}"); } // ---- tbc-Binary (Exit-Codes nach D6) ---------------------------------------- #[test] fn tbc_run_hello() { let exe = env!("CARGO_BIN_EXE_tbc"); let out = Command::new(exe) .args(["run"]) .arg(compat_dir().join("hello.bas")) .output() .expect("tbc startet"); assert!(out.status.success(), "{out:?}"); assert_eq!(String::from_utf8_lossy(&out.stdout), "Hallo, Welt!\n"); } #[test] fn tbc_run_stop_exitcode() { let exe = env!("CARGO_BIN_EXE_tbc"); let dir = std::env::temp_dir(); let f = dir.join("tb_phase2_stop_test.bas"); std::fs::write(&f, "PRINT \"x\"\nSTOP\n").unwrap(); let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap(); assert_eq!(out.status.code(), Some(3), "{out:?}"); let err = String::from_utf8_lossy(&out.stderr); assert!(err.contains("STOP in line 2"), "{err}"); let _ = std::fs::remove_file(&f); } #[test] fn tbc_run_laufzeitfehler_exitcode() { let exe = env!("CARGO_BIN_EXE_tbc"); let dir = std::env::temp_dir(); let f = dir.join("tb_phase2_err_test.bas"); std::fs::write(&f, "i% = 40000\n").unwrap(); let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap(); assert_eq!(out.status.code(), Some(2), "{out:?}"); let err = String::from_utf8_lossy(&out.stderr); assert!(err.contains("Overflow"), "{err}"); let _ = std::fs::remove_file(&f); } #[test] fn tbc_build_erzeugt_tbc() { let exe = env!("CARGO_BIN_EXE_tbc"); let dir = std::env::temp_dir(); let f = dir.join("tb_phase2_build_test.bas"); std::fs::write(&f, "PRINT 1\n").unwrap(); let out = Command::new(exe).args(["build"]).arg(&f).output().unwrap(); assert!(out.status.success(), "{out:?}"); let tbc = f.with_extension("tbc"); let bytes = std::fs::read(&tbc).unwrap(); assert_eq!(&bytes[..4], b"TBC\0"); let _ = std::fs::remove_file(&f); let _ = std::fs::remove_file(&tbc); }