use std::{ path::Path, process::{Command, Output, Stdio}, time::{Duration, Instant}, }; fn tbc(dir: &Path, command: &str, file: &str) -> Output { let mut child = Command::new(env!("CARGO_BIN_EXE_tbc")) .current_dir(dir) .args([command, file]) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let start = Instant::now(); while child.try_wait().unwrap().is_none() { if start.elapsed() > Duration::from_secs(10) { child.kill().unwrap(); child.wait().unwrap(); panic!("tbc {command} {file}: Frist überschritten"); } std::thread::sleep(Duration::from_millis(10)); } child.wait_with_output().unwrap() } #[test] fn projekt_und_formular_laufen_nach_entfernen_saemtlicher_quellen() { let dir = std::env::temp_dir().join(format!("tb-tbc-v4-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("main.bas"), "a$=Form1!Text1.Text\nb$=Form1!Text1(2).Text\nForm1.Hide\nCLS\nPRINT a$\nPRINT b$\nCALL Ausgabe\nEND\n").unwrap(); std::fs::write( dir.join("lib.bas"), "'$INCLUDE: 'outer.bi'\nSUB Ausgabe\nPRINT N\nEND SUB\n", ) .unwrap(); std::fs::write(dir.join("outer.bi"), "'$INCLUDE: 'inner.bi'\n").unwrap(); std::fs::write(dir.join("inner.bi"), "CONST N=7\n").unwrap(); std::fs::write(dir.join("form.frm"), "VERSION 1.00\nBEGIN Form Form1\n BEGIN TextBox Text1\n Index = 0\n Text = \"hello\"\n END\n BEGIN TextBox Text1\n Index = 2\n Text = \"world\"\n END\nEND\n").unwrap(); std::fs::write( dir.join("app.mak"), "' Projekt\nMAIN.BAS\nLIB.BAS\nFORM.FRM\n", ) .unwrap(); let before = tbc(&dir, "run", "app.mak"); assert!(before.status.success(), "{before:?}"); assert_eq!( String::from_utf8_lossy(&before.stdout), "hello\nworld\n 7 \n" ); let build = tbc(&dir, "build", "app.mak"); assert!(build.status.success(), "{build:?}"); let bytes = std::fs::read(dir.join("app.tbc")).unwrap(); assert_eq!(&bytes[..6], b"TBC\0\x04\0"); for file in [ "main.bas", "lib.bas", "outer.bi", "inner.bi", "form.frm", "app.mak", ] { std::fs::remove_file(dir.join(file)).unwrap(); } let after = tbc(&dir, "run", "app.tbc"); assert!(after.status.success(), "{after:?}"); assert_eq!(after.stdout, before.stdout); for version in [1u16, 2, 3, 5, 32767] { let mut invalid = bytes.clone(); invalid[4..6].copy_from_slice(&version.to_le_bytes()); std::fs::write(dir.join("old.tbc"), invalid).unwrap(); let out = tbc(&dir, "run", "old.tbc"); assert!(!out.status.success()); assert!( String::from_utf8_lossy(&out.stderr).contains(&format!("Formatversion {version}")), "{out:?}" ); } std::fs::remove_dir_all(dir).unwrap(); } #[test] fn cli_meldet_physische_quellorte_auch_nach_verschachtelten_includes() { let dir = std::env::temp_dir().join(format!("tb-project-origins-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("main.bas"), "CALL Fehler\nEND\n").unwrap(); std::fs::write(dir.join("lib.bas"), "SUB Fehler\n200 ERROR 6\nEND SUB\n").unwrap(); std::fs::write(dir.join("app.mak"), "main.bas\nlib.bas\n").unwrap(); let out = tbc(&dir, "run", "app.mak"); assert_eq!(out.status.code(), Some(2)); assert!( String::from_utf8_lossy(&out.stderr).contains("lib.bas:2:5:"), "{out:?}" ); std::fs::write(dir.join("lib.bas"), "SUB Fehler\ns$=42\nEND SUB\n").unwrap(); let out = tbc(&dir, "check", "app.mak"); assert!(!out.status.success()); assert!( String::from_utf8_lossy(&out.stderr).contains("lib.bas:2:1:"), "{out:?}" ); std::fs::write(dir.join("lib.bas"), "'$INCLUDE: 'outer.bi'\n").unwrap(); std::fs::write(dir.join("outer.bi"), "'$INCLUDE: 'inner.bi'\n").unwrap(); std::fs::write(dir.join("inner.bi"), "SUB Fehler\ns$=42\nEND SUB\n").unwrap(); let out = tbc(&dir, "check", "app.mak"); assert!(!out.status.success()); assert!( String::from_utf8_lossy(&out.stderr).contains("inner.bi:2:1:"), "{out:?}" ); std::fs::write(dir.join("inner.bi"), "SUB Fehler\n200 ERROR 6\nEND SUB\n").unwrap(); let out = tbc(&dir, "run", "app.mak"); assert_eq!(out.status.code(), Some(2)); assert!( String::from_utf8_lossy(&out.stderr).contains("inner.bi:2:5:"), "{out:?}" ); std::fs::write( dir.join("error.frm"), "VERSION 1.00\nBEGIN Form F\nEND\n\nSUB Form_Load\nERROR 6\nEND SUB\n", ) .unwrap(); assert!(tbc(&dir, "build", "error.frm").status.success()); std::fs::remove_file(dir.join("error.frm")).unwrap(); let out = tbc(&dir, "run", "error.tbc"); assert_eq!(out.status.code(), Some(2)); assert!( String::from_utf8_lossy(&out.stderr).contains("error.frm:6:1:"), "{out:?}" ); std::fs::write(dir.join("empty.bas"), "").unwrap(); assert!(tbc(&dir, "run", "empty.bas").status.success()); std::fs::remove_dir_all(dir).unwrap(); } #[test] fn release_projekt_prueft_anfangsdateien_dateieffekte_und_include_fehler() { struct Aufraeumen(std::path::PathBuf); impl Drop for Aufraeumen { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } let dir = std::env::temp_dir().join(format!("tb-release-files-{}", std::process::id())); { std::fs::create_dir(&dir).unwrap(); let _cleanup = Aufraeumen(dir.clone()); std::fs::write(dir.join("main.bas"), "CALL Kopie\nEND\n").unwrap(); std::fs::write(dir.join("lib.bas"), "'$INCLUDE: 'OUTER.BI'\n").unwrap(); std::fs::write(dir.join("outer.bi"), "'$INCLUDE: 'INNER.BI'\n").unwrap(); std::fs::write(dir.join("inner.bi"), "SUB Kopie\nOPEN \"seed.txt\" FOR INPUT AS #1\nLINE INPUT #1, s$\nCLOSE #1\nOPEN \"result.txt\" FOR APPEND AS #2\nPRINT #2, s$\nCLOSE #2\n200 ERROR 6\nEND SUB\n").unwrap(); std::fs::write(dir.join("app.mak"), "MAIN.BAS\nLIB.BAS\n").unwrap(); let build = tbc(&dir, "build", "app.mak"); assert!(build.status.success(), "{build:?}"); for entry in ["app.mak", "app.tbc"] { if entry == "app.tbc" { for source in ["main.bas", "lib.bas", "outer.bi", "inner.bi", "app.mak"] { std::fs::remove_file(dir.join(source)).unwrap(); } } for _ in 0..2 { std::fs::write(dir.join("seed.txt"), "Grüße\n").unwrap(); std::fs::write(dir.join("result.txt"), "Start\n").unwrap(); let out = tbc(&dir, "run", entry); assert_eq!(out.status.code(), Some(2), "{out:?}"); assert!(out.stdout.is_empty(), "{out:?}"); assert!( String::from_utf8_lossy(&out.stderr) .to_ascii_lowercase() .contains("inner.bi:8:5:"), "{out:?}" ); assert_eq!( std::fs::read(dir.join("seed.txt")).unwrap(), "Grüße\n".as_bytes() ); assert_eq!( std::fs::read(dir.join("result.txt")).unwrap(), "Start\nGrüße\n".as_bytes() ); } } let mut files: Vec<_> = std::fs::read_dir(&dir) .unwrap() .map(|f| f.unwrap().file_name()) .collect(); files.sort(); assert_eq!(files, ["app.tbc", "result.txt", "seed.txt"]); } assert!( !dir.exists(), "Releaseprobe hat ihr Arbeitsverzeichnis nicht entfernt" ); } #[test] fn erlaubte_grafik_und_benannte_nonfeatures_werden_per_cli_geprueft() { let dir = std::env::temp_dir().join(format!("tb-graphics-check-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("probe.bas"); for mode in 0..=13 { std::fs::write( &path, format!("SCREEN {mode}\nLINE (1,1)-(4,4),1\nPAINT (2,2),1\nVIEW (0,0)-(10,10)\n"), ) .unwrap(); let out = tbc(&dir, "check", "probe.bas"); assert!( out.status.success() && out.stderr.is_empty(), "SCREEN {mode}: {out:?}" ); } for (name, source) in [ ("POKE", "POKE 1,2"), ("PEEK", "PRINT PEEK(1)"), ("CIRCLE", "CIRCLE (1,1),1"), ("WINDOW", "WINDOW (0,0)-(10,10)"), ("IOCTL", "IOCTL #1, \"x\""), ("PSET", "PSET (1,1),2"), ("PSET", "PSET STEP (1,1),2"), ("PRESET", "PRESET (1,1),2"), ("PRESET", "PRESET STEP (1,1),2"), ("CIRCLE", "CIRCLE STEP (1,1),2"), ("WINDOW", "WINDOW SCREEN (0,0)-(10,10)"), ("WINDOW", "WINDOW"), ("PALETTE", "DIM a%(15)\nPALETTE USING a%(0)"), ("PALETTE", "PALETTE"), ("PALETTE", "IF 1 THEN PALETTE USING a%(0)"), ] { std::fs::write(&path, source).unwrap(); let out = tbc(&dir, "check", "probe.bas"); let message = String::from_utf8_lossy(&out.stderr); assert!( !out.status.success() && message.contains("Feature unavailable") && message.contains(name), "{out:?}" ); } // Bibliotheksnamen sind keine reservierten Variablennamen. std::fs::write( &path, "DIM PSET(2), PALETTE(2)\nPSET(1)=3\nPALETTE(1)=4\nPRINT PSET(1),PALETTE(1)", ) .unwrap(); let out = tbc(&dir, "check", "probe.bas"); assert!(out.status.success(), "{out:?}"); std::fs::remove_dir_all(dir).unwrap(); } #[test] fn explicit_startup_order_and_form_selection_survive_tbc() { let dir = std::env::temp_dir().join(format!("tb-startup-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("a.bas"), "PRINT \"A\"\n").unwrap(); std::fs::write(dir.join("b.bas"), "PRINT \"B\"\n").unwrap(); for (file, name) in [("one.frm", "One"), ("two.frm", "Two")] { std::fs::write( dir.join(file), format!("VERSION 1.00\nBEGIN Form {name}\nEND\n"), ) .unwrap(); } for startup in [None, Some("b.bas"), Some("two.frm")] { std::fs::write( dir.join("p.mak"), format!( "a.bas\nb.bas\none.frm\ntwo.frm\n{}", startup .map(|s| format!("' $STARTUP: \"{s}\"\n")) .unwrap_or_default() ), ) .unwrap(); let out = tbc(&dir, "build", "p.mak"); assert!(out.status.success(), "{out:?}"); let m = tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(dir.join("p.tbc")).unwrap()) .unwrap(); let chosen = m .startup_form .map(|i| m.objects[i as usize].name.to_ascii_uppercase()); assert_eq!( chosen.as_deref(), match startup { None => Some("ONE"), Some("two.frm") => Some("TWO"), _ => None, } ); if startup == Some("b.bas") { let direct = tbc(&dir, "run", "p.mak"); let serialized = tbc(&dir, "run", "p.tbc"); assert!(direct.status.success() && serialized.status.success()); assert_eq!(direct.stdout, b"B\nA\n"); assert_eq!(direct.stdout, serialized.stdout); } } std::fs::remove_dir_all(dir).unwrap(); } #[test] fn designer_frm_is_accepted_by_the_standalone_cli_compiler() { use tb_frontend::forms::ObjectClass; use tb_ide::{app::App, commands::Command as IdeCommand, documents::Destination}; let dir = std::env::temp_dir().join(format!("tb-designer-cli-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let dir = dir.canonicalize().unwrap(); let mut app = App::new(&dir, dir.join("options"), (100, 30)).unwrap(); app.execute(IdeCommand::NewForm); let root = app.design_objects().unwrap()[0].0; let button = app.design_place(ObjectClass::CommandButton, 0, root, Default::default()); assert!(button.is_err()); // ungültige Größe schreibt nichts let mut rect = app.area(); rect.x = 2; rect.y = 2; rect.width = 8; rect.height = 2; let control = app .design_place(ObjectClass::CommandButton, 0, root, rect) .unwrap(); app.design_property("INDEX", "0").unwrap(); app.design_event(control, "CLICK").unwrap(); let doc = app.active_document().unwrap(); let path = dir.join("designed.frm"); app.project .save_file(doc, Some(&Destination::new(&path))) .unwrap(); let built = tbc(&dir, "build", "designed.frm"); assert!(built.status.success(), "{built:?}"); let bytes = std::fs::read(dir.join("designed.tbc")).unwrap(); assert_eq!(&bytes[..6], b"TBC\0\x04\0"); tb_vm::project_io::load_program(&dir.join("designed.tbc")).unwrap(); std::fs::remove_dir_all(dir).unwrap(); } #[test] fn native_build_optionen_sind_explizit_und_tbc_bleibt_standard() { let dir = std::env::temp_dir().join(format!("tb-native-options-{}", std::process::id())); std::fs::create_dir(&dir).unwrap(); struct Cleanup(std::path::PathBuf); impl Drop for Cleanup { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } let _cleanup = Cleanup(dir.clone()); std::fs::write(dir.join("main.bas"), "PRINT 42\nEND\n").unwrap(); assert!(tbc(&dir, "build", "main.bas").status.success()); let original = std::fs::read(dir.join("main.tbc")).unwrap(); assert_eq!(&original[..4], b"TBC\0"); for (options, diagnostic) in [ (vec!["--bogus"], "Unbekannte Build-Option"), (vec!["--exe", "--target"], "Wert fehlt"), ( vec!["--exe", "--target", "x86_64-apple-darwin"], "Nicht unterstütztes Target", ), (vec!["--exe", "--exe"], "Doppeltes --exe"), (vec!["--output", "program"], "benötigen --exe"), ( vec!["--exe", "--template", "missing", "-o", "program"], "Runtime-Vorlage fehlt", ), ( vec![ "--exe", "--target", "aarch64-apple-darwin", "--target", "aarch64-apple-darwin", ], "Doppelte Option", ), ] { let out = std::process::Command::new(env!("CARGO_BIN_EXE_tbc")) .current_dir(&dir) .args(["build", "main.bas"]) .args(options) .output() .unwrap(); assert_eq!(out.status.code(), Some(1), "{out:?}"); assert!( String::from_utf8_lossy(&out.stderr).contains(diagnostic), "{out:?}" ); assert_eq!(std::fs::read(dir.join("main.tbc")).unwrap(), original); assert!(!dir.join("program").exists()); } }