Files

476 lines
18 KiB
Rust

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());
}
}
#[test]
fn library_cli_links_without_sources_and_protects_all_inputs_and_outputs() {
let dir = std::env::temp_dir().join(format!("tb-library-cli-{}", 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());
let call = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_tbc"))
.current_dir(&dir)
.env("PATH", "")
.args(args)
.output()
.unwrap()
};
let success = |args: &[&str]| {
let out = call(args);
assert!(out.status.success(), "{args:?}: {out:?}");
out
};
std::fs::write(
dir.join("a.bas"),
"DECLARE SUB Second(n%)\nSUB First(n%)\nSecond n%\nEND SUB\n",
)
.unwrap();
std::fs::write(dir.join("b.bas"), "SUB Second(n%)\nn%=n%+7\nEND SUB\n").unwrap();
success(&["build", "a.bas", "--library"]);
success(&["build", "b.bas", "--library", "--output", "b.tbl"]);
std::fs::remove_file(dir.join("a.bas")).unwrap();
std::fs::remove_file(dir.join("b.bas")).unwrap();
std::fs::write(dir.join("main.bas"), "CALL First(n%)\nPRINT n%\nEND\n").unwrap();
assert!(!call(&["link", "main.bas", "a.tbl"]).status.success());
success(&["link", "main.bas", "a.tbl", "b.tbl", "-o", "linked.tbc"]);
assert_eq!(success(&["run", "linked.tbc"]).stdout, b" 7 \n");
std::fs::write(dir.join("app.mak"), "main.bas\na.tbl\nb.tbl\n").unwrap();
success(&["check", "app.mak"]);
success(&["build", "app.mak"]);
assert_eq!(success(&["run", "app.mak"]).stdout, b" 7 \n");
let before = std::fs::read(dir.join("a.tbl")).unwrap();
let old = std::fs::read(dir.join("linked.tbc")).unwrap();
for args in [
vec![
"link", "main.bas", "a.tbl", "b.tbl", "-o", "a.tbl", "--force",
],
vec!["link", "main.bas", "a.tbl", "b.tbl", "-o", "linked.tbc"],
vec!["build", "main.bas", "--library", "--exe"],
vec![
"build",
"main.bas",
"--library",
"--target",
"aarch64-apple-darwin",
],
vec!["link", "linked.tbc", "a.tbl", "--exe"],
vec!["link", "linked.tbc"],
vec!["run", "a.tbl"],
vec!["check", "a.tbl"],
vec!["link", "a.tbl"],
vec!["link", "main.bas", "main.bas"],
vec!["link", "main.bas", "a.tbl", "a.tbl"],
vec!["link", "app.mak", "-o", "app.mak", "--force"],
vec!["link", "app.mak", "-o", "missing/output.tbc"],
] {
assert!(!call(&args).status.success(), "accepted {args:?}");
assert_eq!(std::fs::read(dir.join("a.tbl")).unwrap(), before);
assert_eq!(std::fs::read(dir.join("linked.tbc")).unwrap(), old);
}
success(&["link", "app.mak", "-o", "linked.tbc", "--force"]);
let old = std::fs::read(dir.join("linked.tbc")).unwrap();
std::fs::write(dir.join("a.tbl"), &before[..before.len() - 1]).unwrap();
assert!(!call(&["link", "app.mak", "-o", "linked.tbc", "--force"])
.status
.success());
assert_eq!(std::fs::read(dir.join("linked.tbc")).unwrap(), old);
for p in ["a.tbl", "b.tbl", "main.bas", "app.mak"] {
std::fs::remove_file(dir.join(p)).unwrap();
}
assert_eq!(success(&["run", "linked.tbc"]).stdout, b" 7 \n");
assert!(!std::fs::read_dir(&dir).unwrap().any(|e| e
.unwrap()
.file_name()
.to_string_lossy()
.contains(".tmp")));
}