Projektmodule und vollständiges TBC-Kompilat umsetzen und Change archivieren
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! `tbc` — Standalone-Compiler von Terminal Basic.
|
||||
//!
|
||||
//! Unterbefehle (Phase 2):
|
||||
//! - `tbc run <datei.bas|datei.frm|projekt.mak>` Kompilieren und ausführen
|
||||
//! - `tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>` Kompilieren und ausführen
|
||||
//! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
|
||||
//! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
|
||||
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
|
||||
@@ -10,7 +10,6 @@
|
||||
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
||||
//! 1 = Compile-Fehler/Bedienfehler.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
@@ -26,7 +25,7 @@ fn main() -> ExitCode {
|
||||
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
||||
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
||||
);
|
||||
ExitCode::from(1)
|
||||
}
|
||||
@@ -128,33 +127,47 @@ fn include_name(line: &str) -> Option<String> {
|
||||
Some(rest.split('\'').next().unwrap_or_default().to_string())
|
||||
}
|
||||
|
||||
fn expand_includes(path: &Path, source: &str, stack: &mut Vec<PathBuf>) -> Result<String, String> {
|
||||
fn expand_includes(
|
||||
path: &Path,
|
||||
source: &str,
|
||||
first_line: u32,
|
||||
stack: &mut Vec<PathBuf>,
|
||||
) -> Result<Vec<tb_frontend::source::SourceSegment>, String> {
|
||||
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
if stack.contains(&canonical) {
|
||||
return Err(format!("{}: zyklisches $INCLUDE", path.display()));
|
||||
}
|
||||
stack.push(canonical);
|
||||
let mut out = String::new();
|
||||
for line in source.split_inclusive('\n') {
|
||||
let mut segments = vec![tb_frontend::source::SourceSegment {
|
||||
file: path.display().to_string(),
|
||||
first_line,
|
||||
text: String::new(),
|
||||
}];
|
||||
for (line_no, line) in source.split_inclusive('\n').enumerate() {
|
||||
if let Some(name) = include_name(line) {
|
||||
if name.is_empty() {
|
||||
return Err(format!("{}: leeres $INCLUDE", path.display()));
|
||||
return Err(format!(
|
||||
"{}:{}: leeres $INCLUDE",
|
||||
path.display(),
|
||||
first_line + line_no as u32
|
||||
));
|
||||
}
|
||||
let included =
|
||||
relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))?;
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
let text = std::fs::read_to_string(&included)
|
||||
.map_err(|error| format!("{}: {error}", included.display()))?;
|
||||
out.push_str(&expand_includes(&included, &text, stack)?);
|
||||
if !out.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
.map_err(|e| format!("{}: {e}", included.display()))?;
|
||||
segments.extend(expand_includes(&included, &text, 1, stack)?);
|
||||
} else {
|
||||
out.push_str(line);
|
||||
segments.push(tb_frontend::source::SourceSegment {
|
||||
file: path.display().to_string(),
|
||||
first_line: first_line + line_no as u32,
|
||||
text: line.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
stack.pop();
|
||||
Ok(out)
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
||||
@@ -162,9 +175,8 @@ fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
||||
if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") {
|
||||
let source = std::str::from_utf8(&bytes)
|
||||
.map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?;
|
||||
let mut form = tb_ui::frm::read_text(&path.display().to_string(), source)
|
||||
let form = tb_ui::frm::read_text(&path.display().to_string(), source)
|
||||
.map_err(|error| error.to_string())?;
|
||||
form.code = expand_includes(path, &form.code, &mut Vec::new())?;
|
||||
Ok(form)
|
||||
} else {
|
||||
tb_ui::frm::read_binary(&path.display().to_string(), &bytes)
|
||||
@@ -173,65 +185,15 @@ fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ProjectSymbols {
|
||||
constants: HashSet<String>,
|
||||
types: HashSet<String>,
|
||||
}
|
||||
|
||||
fn append_project_source(target: &mut String, source: &str, symbols: &mut ProjectSymbols) {
|
||||
let mut in_proc = false;
|
||||
let mut skip_type = false;
|
||||
for line in source.split_inclusive('\n') {
|
||||
let upper = line.trim().to_ascii_uppercase();
|
||||
if skip_type {
|
||||
if upper == "END TYPE" {
|
||||
skip_type = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if !in_proc {
|
||||
if let Some(name) = upper
|
||||
.strip_prefix("TYPE ")
|
||||
.and_then(|rest| rest.split_whitespace().next())
|
||||
{
|
||||
if !symbols.types.insert(name.to_string()) {
|
||||
skip_type = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(rest) = upper.strip_prefix("CONST ") {
|
||||
let names = rest
|
||||
.split(',')
|
||||
.filter_map(|part| part.split('=').next())
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if !names.is_empty() && names.iter().all(|name| symbols.constants.contains(*name)) {
|
||||
continue;
|
||||
}
|
||||
symbols
|
||||
.constants
|
||||
.extend(names.into_iter().map(str::to_string));
|
||||
}
|
||||
}
|
||||
target.push_str(line);
|
||||
if upper.starts_with("SUB ")
|
||||
|| upper.starts_with("FUNCTION ")
|
||||
|| upper.starts_with("STATIC SUB ")
|
||||
|| upper.starts_with("STATIC FUNCTION ")
|
||||
{
|
||||
in_proc = true;
|
||||
} else if matches!(upper.as_str(), "END SUB" | "END FUNCTION") {
|
||||
in_proc = false;
|
||||
}
|
||||
}
|
||||
if !target.ends_with('\n') {
|
||||
target.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), String> {
|
||||
fn input_sources(
|
||||
path: &Path,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<tb_frontend::source::SourceUnit>,
|
||||
Vec<tb_ui::frm::FormFile>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||
let paths = if extension.eq_ignore_ascii_case("mak") {
|
||||
let project = std::fs::read_to_string(path)
|
||||
@@ -240,7 +202,7 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
||||
project
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('\''))
|
||||
.map(|line| {
|
||||
relative_case_insensitive(base, line)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))
|
||||
@@ -249,9 +211,8 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
||||
} else {
|
||||
vec![path.to_path_buf()]
|
||||
};
|
||||
let mut source = String::new();
|
||||
let mut source = Vec::new();
|
||||
let mut forms = Vec::new();
|
||||
let mut symbols = ProjectSymbols::default();
|
||||
for member in paths {
|
||||
if member
|
||||
.extension()
|
||||
@@ -259,13 +220,18 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
|
||||
{
|
||||
let form = read_form(&member)?;
|
||||
append_project_source(&mut source, &form.code, &mut symbols);
|
||||
source.push(tb_frontend::source::SourceUnit {
|
||||
name: form.root.name.clone(),
|
||||
segments: expand_includes(&member, &form.code, form.code_line(), &mut Vec::new())?,
|
||||
});
|
||||
forms.push(form);
|
||||
} else {
|
||||
let text = std::fs::read_to_string(&member)
|
||||
.map_err(|error| format!("{}: {error}", member.display()))?;
|
||||
let expanded = expand_includes(&member, &text, &mut Vec::new())?;
|
||||
append_project_source(&mut source, &expanded, &mut symbols);
|
||||
source.push(tb_frontend::source::SourceUnit {
|
||||
name: module_name(&member),
|
||||
segments: expand_includes(&member, &text, 1, &mut Vec::new())?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok((source, forms))
|
||||
@@ -273,19 +239,26 @@ fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), Str
|
||||
|
||||
fn compile(
|
||||
path_arg: Option<&String>,
|
||||
) -> Result<
|
||||
(
|
||||
PathBuf,
|
||||
tb_vm::bytecode::CompiledModule,
|
||||
Vec<tb_ui::frm::FormFile>,
|
||||
),
|
||||
ExitCode,
|
||||
> {
|
||||
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
let Some(path) = path_arg else {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak>");
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc>");
|
||||
return Err(ExitCode::from(1));
|
||||
};
|
||||
let path = PathBuf::from(path);
|
||||
if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("tbc"))
|
||||
{
|
||||
let bytes = std::fs::read(&path).map_err(|e| {
|
||||
eprintln!("{}: {e}", path.display());
|
||||
ExitCode::from(1)
|
||||
})?;
|
||||
let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes).map_err(|e| {
|
||||
eprintln!("{}: {e}", path.display());
|
||||
ExitCode::from(1)
|
||||
})?;
|
||||
return Ok((path, module));
|
||||
}
|
||||
let (source, forms) = match input_sources(&path) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
@@ -293,22 +266,16 @@ fn compile(
|
||||
return Err(ExitCode::from(1));
|
||||
}
|
||||
};
|
||||
let compiled = if forms.is_empty() {
|
||||
tb_vm::compile_source(&module_name(&path), &source)
|
||||
} else {
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for form in &forms {
|
||||
for object in form.catalog().objects {
|
||||
catalog.objects.push(object);
|
||||
}
|
||||
}
|
||||
tb_vm::compile_source_with_forms(&forms[0].root.name, &source, &catalog)
|
||||
};
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for form in &forms {
|
||||
catalog.append(&form.catalog());
|
||||
}
|
||||
let compiled = tb_vm::compile_project(&module_name(&path), &source, &catalog, &forms);
|
||||
match compiled {
|
||||
Ok(m) => Ok((path, m, forms)),
|
||||
Ok(m) => Ok((path, m)),
|
||||
Err(diags) => {
|
||||
for d in &diags {
|
||||
eprintln!("{}:{d}", path.display());
|
||||
eprintln!("{d}");
|
||||
}
|
||||
eprintln!("{} Fehler.", diags.len());
|
||||
Err(ExitCode::from(1))
|
||||
@@ -324,7 +291,7 @@ fn cmd_check(args: &[String]) -> ExitCode {
|
||||
}
|
||||
|
||||
fn cmd_build(args: &[String]) -> ExitCode {
|
||||
let (path, module, _) = match compile(args.first()) {
|
||||
let (path, module) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
@@ -371,7 +338,7 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
RunEvent::Ended => ExitCode::SUCCESS,
|
||||
RunEvent::Stopped { line } => {
|
||||
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
||||
eprintln!("STOP in line {line}");
|
||||
eprintln!("{}:{line}: STOP in line {line}", vm.current_file());
|
||||
ExitCode::from(3)
|
||||
}
|
||||
RunEvent::Error {
|
||||
@@ -379,7 +346,11 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
line,
|
||||
message,
|
||||
} => {
|
||||
eprintln!("Runtime error {code}: {message} in line {line}");
|
||||
eprintln!(
|
||||
"{}:{line}:{}: Runtime error {code}: {message} in line {line}",
|
||||
vm.current_file(),
|
||||
vm.current_source_pos().column
|
||||
);
|
||||
ExitCode::from(2)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
@@ -398,7 +369,7 @@ fn run_target(current: &Path, program: &str) -> Result<PathBuf, String> {
|
||||
if Path::new(program).extension().is_some() {
|
||||
return relative_case_insensitive(base, program).map_err(|error| error.to_string());
|
||||
}
|
||||
for extension in ["bas", "frm", "mak"] {
|
||||
for extension in ["bas", "frm", "mak", "tbc"] {
|
||||
let candidate = format!("{program}.{extension}");
|
||||
if let Ok(path) = relative_case_insensitive(base, &candidate) {
|
||||
return Ok(path);
|
||||
@@ -413,7 +384,7 @@ fn run_chain(
|
||||
size: Option<(usize, usize)>,
|
||||
) -> Result<(RunEvent, Vm), ExitCode> {
|
||||
let Some(first) = args.first() else {
|
||||
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak>");
|
||||
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>");
|
||||
return Err(ExitCode::from(1));
|
||||
};
|
||||
let mut current = PathBuf::from(first);
|
||||
@@ -421,26 +392,8 @@ fn run_chain(
|
||||
let command = args[1..].join(" ");
|
||||
loop {
|
||||
let current_arg = current.display().to_string();
|
||||
let (path, module, forms) = compile(Some(¤t_arg))?;
|
||||
let (path, module) = compile(Some(¤t_arg))?;
|
||||
let mut vm = Vm::new(module);
|
||||
if !forms.is_empty() {
|
||||
let applied = forms.iter().try_for_each(|form| form.apply(&mut vm.forms));
|
||||
let first = vm
|
||||
.forms
|
||||
.objects
|
||||
.iter()
|
||||
.position(|object| {
|
||||
object
|
||||
.description
|
||||
.name
|
||||
.eq_ignore_ascii_case(&forms[0].root.name)
|
||||
})
|
||||
.unwrap() as u16;
|
||||
if let Err(error) = applied.and_then(|_| vm.forms.show(first, false).map(|_| ())) {
|
||||
eprintln!("Runtime error {}: {}", error.0, error);
|
||||
return Err(ExitCode::from(2));
|
||||
}
|
||||
}
|
||||
if let Some(line) = start_line.take() {
|
||||
if let Err(error) = vm.start_at_line(line) {
|
||||
eprintln!("Runtime error {}: {}", error.0, error);
|
||||
|
||||
130
crates/tb-cli/tests/project.rs
Normal file
130
crates/tb-cli/tests/project.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user