298 lines
10 KiB
Rust
298 lines
10 KiB
Rust
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
};
|
|
use tb_vm::project_io::{identity, relative_path, Content, Manifest, SourceLoader};
|
|
struct Temp(PathBuf);
|
|
impl Temp {
|
|
fn new() -> Self {
|
|
static N: AtomicUsize = AtomicUsize::new(0);
|
|
let p = std::env::temp_dir().join(format!(
|
|
"tb-project-io-{}-{}",
|
|
std::process::id(),
|
|
N.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
fs::create_dir_all(&p).unwrap();
|
|
Self(p.canonicalize().unwrap())
|
|
}
|
|
fn write(&self, name: &str, text: &str) -> PathBuf {
|
|
let p = self.0.join(name);
|
|
fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
fs::write(&p, text).unwrap();
|
|
p
|
|
}
|
|
}
|
|
impl Drop for Temp {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
#[test]
|
|
fn overlays_keep_original_files_and_distinct_module_contexts() {
|
|
let t = Temp::new();
|
|
let p = t.write("app.mak", "a.bas\nb.bas\n");
|
|
t.write("a.bas", "'$INCLUDE: 'common.bi'\nPRINT n\nEND\n");
|
|
t.write("b.bas", "'$INCLUDE: 'common.bi'\nSUB Test\nEND SUB\n");
|
|
let include = t.write("common.bi", "CONST n=1\n");
|
|
let mut loader = SourceLoader::default();
|
|
loader
|
|
.insert(&include, Content::Text("CONST n=7\n".into()))
|
|
.unwrap();
|
|
let input = loader.load(&p).unwrap();
|
|
let mut origins = Vec::new();
|
|
for (i, u) in input.units.iter().enumerate() {
|
|
let (_, errors) = u.parse(i as u16, &mut origins);
|
|
assert!(errors.is_empty());
|
|
assert!(u
|
|
.segments
|
|
.iter()
|
|
.any(|s| s.text == "CONST n=7\n" && s.first_line == 1));
|
|
}
|
|
let positions: Vec<_> = origins
|
|
.iter()
|
|
.filter(|o| Path::new(&o.path).file_name().unwrap() == "common.bi")
|
|
.collect();
|
|
assert_eq!(positions.len(), 2);
|
|
assert_ne!(positions[0].module, positions[1].module);
|
|
assert_eq!(fs::read_to_string(include).unwrap(), "CONST n=1\n");
|
|
}
|
|
#[test]
|
|
fn relative_include_precedes_search_paths_and_virtual_files_are_resolved() {
|
|
let t = Temp::new();
|
|
let main = t.write("src/main.bas", "'$INCLUDE: 'Mixed Name.bi'\n");
|
|
t.write("library/mixed name.bi", "PRINT 8\n");
|
|
let mut loader = SourceLoader::default();
|
|
loader.include_paths.push(t.0.join("library"));
|
|
assert!(loader.load(&main).unwrap().units[0]
|
|
.segments
|
|
.iter()
|
|
.any(|s| s.text == "PRINT 8\n"));
|
|
loader
|
|
.insert(
|
|
&t.0.join("src/MIXED NAME.BI"),
|
|
Content::Text("PRINT 9\n".into()),
|
|
)
|
|
.unwrap();
|
|
assert!(loader.load(&main).unwrap().units[0]
|
|
.segments
|
|
.iter()
|
|
.any(|s| s.text == "PRINT 9\n"));
|
|
let virtual_main = t.0.join("virtual.bas");
|
|
loader
|
|
.insert(&virtual_main, Content::Text("END\n".into()))
|
|
.unwrap();
|
|
assert_eq!(loader.load(&virtual_main).unwrap().units[0].name, "VIRTUAL");
|
|
assert!(!virtual_main.exists());
|
|
}
|
|
#[test]
|
|
fn cycle_missing_include_and_literal_directives_are_distinguished() {
|
|
let t = Temp::new();
|
|
let main = t.write("main.bas", "'$INCLUDE: 'outer.bi'\n");
|
|
t.write("outer.bi", "'$INCLUDE: 'inner.bi'\n");
|
|
t.write("inner.bi", "'$INCLUDE: 'outer.bi'\n");
|
|
let loader = SourceLoader::default();
|
|
let error = loader.load(&main).unwrap_err();
|
|
assert!(
|
|
error.contains("zyklisches") && error.contains("outer.bi") && error.contains("inner.bi")
|
|
);
|
|
t.write("inner.bi", "'$INCLUDE: 'missing.bi'\n");
|
|
let error = loader.load(&main).unwrap_err();
|
|
assert!(error.contains("inner.bi:1") && error.contains("missing.bi"));
|
|
t.write("main.bas","PRINT \"$INCLUDE: 'missing.bi'\"\n' Dieser Kommentar erwähnt $INCLUDE: 'missing.bi'\nDATA $INCLUDE: 'missing.bi'\nEND\n");
|
|
let input = loader.load(&main).unwrap();
|
|
assert!(input.units[0]
|
|
.segments
|
|
.iter()
|
|
.any(|s| s.text.starts_with("PRINT")));
|
|
t.write("main.bas", "PRINT 1: '$INCLUDE: 'good.bi'\nEND\n");
|
|
t.write("good.bi", "PRINT 2\n");
|
|
let input = loader.load(&main).unwrap();
|
|
let code = input.units[0]
|
|
.segments
|
|
.iter()
|
|
.map(|s| s.text.as_str())
|
|
.collect::<String>();
|
|
assert!(code.contains("PRINT 1:") && code.contains("PRINT 2"));
|
|
t.write("main.bas", "'$INCLUDE: 'unterminated\n");
|
|
assert!(loader.load(&main).unwrap_err().contains("leeres $INCLUDE"));
|
|
}
|
|
#[test]
|
|
fn startup_metadata_roundtrip_and_invalid_contracts() {
|
|
let t = Temp::new();
|
|
t.write("a.bas", "END\n");
|
|
t.write("b.bas", "");
|
|
let p = t.write(
|
|
"p.mak",
|
|
"' keep this\nA.BAS\n\n' $STARTUP: \"b.bas\"\nb.bas\n",
|
|
);
|
|
let loader = SourceLoader::default();
|
|
let input = loader.load(&p).unwrap();
|
|
let output = input.manifest.text(&t.0.join("nested/new.mak")).unwrap();
|
|
assert!(output.contains("../a.bas") || output.contains("../A.BAS"));
|
|
assert!(output.contains("' keep this"));
|
|
let roundtrip = Manifest::parse(&t.0.join("nested/new.mak"), &output, &loader).unwrap();
|
|
assert_eq!(roundtrip.members().count(), 2);
|
|
assert_eq!(
|
|
identity(roundtrip.startup.as_ref().unwrap()).unwrap(),
|
|
identity(&t.0.join("b.bas")).unwrap()
|
|
);
|
|
for text in [
|
|
"' $STARTUP: \"a.bas\"\n' $STARTUP: \"a.bas\"\na.bas\n",
|
|
"' $STARTUP: a.bas\na.bas\n",
|
|
"' $STARTUP: \"b.bas\"\na.bas\n",
|
|
"' $STARTUP: \"\"\na.bas\n",
|
|
"' $STARTUP: \"missing.bas\"\na.bas\n",
|
|
] {
|
|
assert!(Manifest::parse(&p, text, &loader).is_err(), "{text}");
|
|
}
|
|
assert!(Manifest::parse(&p, "a.bas\n", &loader)
|
|
.unwrap()
|
|
.startup
|
|
.is_none());
|
|
assert_eq!(
|
|
relative_path(&t.0.join("nested"), &t.0.join("a.bas")).unwrap(),
|
|
"../a.bas"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn documented_mak_example_is_accepted_by_the_shared_loader() {
|
|
let t = Temp::new();
|
|
t.write("main.bas", "END\n");
|
|
t.write("lib.bas", "");
|
|
t.write("form.frm", "VERSION 1.00\nBegin Form F\nEnd\n");
|
|
let docs = include_str!("../../../docs/dateiformate.md");
|
|
let example = docs
|
|
.split("```mak\n")
|
|
.nth(1)
|
|
.unwrap()
|
|
.split("```")
|
|
.next()
|
|
.unwrap();
|
|
let path = t.write("example.mak", example);
|
|
let result = SourceLoader::default().load(&path).unwrap();
|
|
assert_eq!(result.units.len(), 3);
|
|
assert_eq!(result.forms.len(), 1);
|
|
assert_eq!(
|
|
result.manifest.startup.unwrap().file_name().unwrap(),
|
|
"main.bas"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn case_insensitive_parent_lookup_works_from_relative_base_without_changing_cwd() {
|
|
let t = Temp::new();
|
|
let sibling = t.write("Sibling.bas", "END\n");
|
|
fs::create_dir(t.0.join("child")).unwrap();
|
|
let base = PathBuf::from(
|
|
relative_path(&std::env::current_dir().unwrap(), &t.0.join("child")).unwrap(),
|
|
);
|
|
let found = tb_vm::project_io::relative_case_insensitive(&base, "../SIBLING.BAS").unwrap();
|
|
assert_eq!(identity(&found).unwrap(), identity(&sibling).unwrap());
|
|
}
|
|
#[test]
|
|
fn tbl_members_move_save_as_order_startup_and_same_size_cache_replacement() {
|
|
use tb_frontend::{forms::FormCatalog, source::SourceUnit};
|
|
use tb_vm::{
|
|
interp::{RunEvent, Vm},
|
|
project::ProjectCompiler,
|
|
};
|
|
let t = Temp::new();
|
|
let library = |n, value| {
|
|
ProjectCompiler::default()
|
|
.compile_library(
|
|
&[SourceUnit::new(
|
|
"LIB",
|
|
"removed.bas",
|
|
&format!("CONST N={n}\nSUB Work\nPRINT {value}\nEND SUB\nPRINT \"LIB\""),
|
|
)],
|
|
&FormCatalog::default(),
|
|
&[],
|
|
&[],
|
|
)
|
|
.unwrap()
|
|
.to_tbl()
|
|
.unwrap()
|
|
};
|
|
let first = library(1, 1);
|
|
let second = library(2, 2);
|
|
assert_eq!(first.len(), second.len());
|
|
t.write("old/main.bas", "PRINT N\nCALL Work\n");
|
|
t.write("old/last.bas", "PRINT \"LAST\"\n");
|
|
let lib = t.0.join("old/Mixed.tbl");
|
|
fs::write(&lib, &first).unwrap();
|
|
let mak = t.write(
|
|
"old/app.mak",
|
|
"mixed.TBL\nlast.bas\nmain.bas\n' $STARTUP: \"main.bas\"\n",
|
|
);
|
|
let loader = SourceLoader::default();
|
|
let input = loader.load(&mak).unwrap();
|
|
assert_eq!(input.units.len(), 2);
|
|
assert_eq!(input.libraries.len(), 1);
|
|
let run = |code| {
|
|
let mut vm = Vm::new(code);
|
|
assert_eq!(
|
|
vm.run(&mut tb_runtime::host::CaptureHost::default()),
|
|
RunEvent::Ended
|
|
);
|
|
tb_runtime::snapshot::text(&vm.rt.screen)
|
|
};
|
|
let mut c = ProjectCompiler::default();
|
|
assert_eq!(
|
|
run(input.compile(&mut c, "APP").unwrap()),
|
|
" 1 \n 1 \nLIB\nLAST\n"
|
|
);
|
|
fs::write(&lib, &second).unwrap();
|
|
assert_eq!(
|
|
run(input.compile(&mut c, "APP").unwrap()),
|
|
" 2 \n 2 \nLIB\nLAST\n"
|
|
);
|
|
let body_only = library(2, 3);
|
|
assert_eq!(second.len(), body_only.len());
|
|
fs::write(&lib, &body_only).unwrap();
|
|
assert_eq!(
|
|
run(input.compile(&mut c, "APP").unwrap()),
|
|
" 2 \n 3 \nLIB\nLAST\n"
|
|
);
|
|
assert_eq!(
|
|
c.stats.compiled, 0,
|
|
"Unveränderte Deklarationen dürfen Quellprodukte wiederverwenden"
|
|
);
|
|
fs::create_dir_all(t.0.join("save")).unwrap();
|
|
let saved = t.0.join("save/app.mak");
|
|
fs::write(&saved, input.manifest.text(&saved).unwrap()).unwrap();
|
|
assert_eq!(
|
|
identity(&loader.load(&saved).unwrap().libraries[0]).unwrap(),
|
|
identity(&input.libraries[0]).unwrap()
|
|
);
|
|
fs::rename(t.0.join("old"), t.0.join("moved")).unwrap();
|
|
assert_eq!(
|
|
run(loader
|
|
.load(&t.0.join("moved/app.mak"))
|
|
.unwrap()
|
|
.compile(&mut c, "APP")
|
|
.unwrap()),
|
|
" 2 \n 3 \nLIB\nLAST\n"
|
|
);
|
|
let moved = t.0.join("moved/app.mak");
|
|
for text in [
|
|
"Mixed.tbl\n' $STARTUP: \"Mixed.tbl\"",
|
|
"Mixed.tbl\nmixed.TBL\n",
|
|
"absent.tbl\n",
|
|
] {
|
|
assert!(Manifest::parse(&moved, text, &loader).is_err(), "{text}");
|
|
}
|
|
assert!(
|
|
tb_vm::project_io::read_document(&t.0.join("moved/Mixed.tbl"))
|
|
.unwrap_err()
|
|
.contains("kein Textdokument")
|
|
);
|
|
fs::remove_file(t.0.join("moved/Mixed.tbl")).unwrap();
|
|
let error = loader.load(&moved).unwrap_err();
|
|
assert!(
|
|
error.contains("app.mak") && error.contains("mixed.TBL"),
|
|
"{error}"
|
|
);
|
|
}
|