Phase 6: IDE-Exportanbindung abschließen und Change archivieren

This commit is contained in:
2026-09-07 17:00:29 +02:00
parent 25176947ba
commit 69f2002481
31 changed files with 1310 additions and 136 deletions

View File

@@ -8,7 +8,7 @@ use std::{
sync::atomic::{AtomicUsize, Ordering},
};
use tb_ide::{
app::{App, DialogKind, Execution, Mode, WindowKind, WindowState},
app::{App, DialogKind, Execution, Hit, Mode, WindowKind, WindowState},
commands::{self, Command},
export::{ExportStatus, ProjectStamp},
options::Options,
@@ -347,23 +347,33 @@ fn status_and_project_buttons_use_the_keyboard_command_path() {
fn export_dialogs_validate_and_render_all_results_without_creating_files() {
let t = Temp::new();
let mut app = t.app();
app.export_backend = false;
let stamp = ProjectStamp::capture(&app.project);
for command in [Command::MakeExe, Command::MakeLibrary] {
menu(&mut app, command);
while app.dialog.as_ref().unwrap().focus != 1 {
plain(&mut app, K::Tab);
}
plain(&mut app, K::Right);
let system = app.dialog.as_ref().unwrap().fields[1].string();
plain(&mut app, K::Tab);
plain(&mut app, K::Right);
let architecture = app.dialog.as_ref().unwrap().fields[2].string();
let output = if command == Command::MakeLibrary {
1
} else {
3
};
let (system, architecture) = if command == Command::MakeExe {
(
app.dialog.as_ref().unwrap().fields[1].string(),
app.dialog.as_ref().unwrap().fields[2].string(),
)
} else {
(String::new(), String::new())
};
let (text, _) = draw(&mut app);
assert!(text.contains("Erzeugen: Phase 6"));
field(&mut app, 3, "");
field(&mut app, output, "");
plain(&mut app, K::Enter);
assert!(app.dialog.as_ref().unwrap().error.contains("Ausgabepfad"));
field(&mut app, 3, &t.0.join("notthere/out").display().to_string());
field(
&mut app,
output,
&t.0.join("notthere/out").display().to_string(),
);
plain(&mut app, K::Enter);
assert!(app
.dialog
@@ -371,12 +381,12 @@ fn export_dialogs_validate_and_render_all_results_without_creating_files() {
.unwrap()
.error
.contains("Zielverzeichnis"));
field(&mut app, 3, &t.0.join("bad.tbc").display().to_string());
field(&mut app, output, &t.0.join("bad.tbc").display().to_string());
plain(&mut app, K::Enter);
assert!(app.dialog.as_ref().unwrap().error.contains("TBC"));
let existing = t.0.join("existing");
let existing = t.0.join("existing.tbl");
fs::write(&existing, b"keep").unwrap();
field(&mut app, 3, &existing.display().to_string());
field(&mut app, output, &existing.display().to_string());
plain(&mut app, K::Enter);
assert!(app
.dialog
@@ -389,8 +399,8 @@ fn export_dialogs_validate_and_render_all_results_without_creating_files() {
plain(&mut app, K::Enter);
assert_eq!(fs::read(&existing).unwrap(), b"keep");
assert!(app.dialog.as_ref().unwrap().request.is_some());
let target = t.0.join("result");
field(&mut app, 3, &target.display().to_string());
let target = t.0.join("result.tbl");
field(&mut app, output, &target.display().to_string());
plain(&mut app, K::Enter);
let request = app.dialog.as_ref().unwrap().request.clone().unwrap();
assert_eq!(request.system, system);
@@ -702,3 +712,215 @@ fn options_apply_persist_across_bases_and_preserve_unsaved_values_on_error() {
assert_eq!(defaults, Options::default());
assert_eq!(errors.len(), 4);
}
fn await_export(app: &mut App) -> ExportStatus {
let start = std::time::Instant::now();
loop {
app.tick_export();
let status = app.dialog.as_ref().unwrap().export_status.clone();
if status != ExportStatus::Running {
return status;
}
assert!(start.elapsed().as_secs() < 10, "Export hängt");
std::thread::sleep(std::time::Duration::from_millis(2));
}
}
#[test]
fn real_library_export_uses_dirty_include_and_mouse_without_runtime_template() {
let t = Temp::new();
fs::write(
t.0.join("lib.bas"),
"SUB Answer\n'$INCLUDE: 'body.bi'\nEND SUB\n",
)
.unwrap();
fs::write(t.0.join("body.bi"), "PRINT 1\n").unwrap();
let mut app = t.app();
app.load_initial_project(t.0.join("lib.bas")).unwrap();
let include = app.project.find_document(&t.0.join("body.bi")).unwrap();
app.project
.replace_text(include, 0..8, "PRINT 42\n")
.unwrap();
let stamp = ProjectStamp::capture(&app.project);
menu(&mut app, Command::MakeLibrary);
assert_eq!(app.dialog.as_ref().unwrap().fields.len(), 3);
let out = t.0.join("out.tbl");
field(&mut app, 1, &out.display().to_string());
draw(&mut app);
let r = app
.hits
.iter()
.find(|(_, h)| matches!(h, Hit::DialogSubmit))
.unwrap()
.0;
app.handle(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: r.x,
row: r.y,
modifiers: M::NONE,
}));
assert_eq!(await_export(&mut app), ExportStatus::Success);
assert_eq!(ProjectStamp::capture(&app.project), stamp);
assert!(app.project.document(include).unwrap().is_dirty());
assert_eq!(
fs::read_to_string(t.0.join("body.bi")).unwrap(),
"PRINT 1\n"
);
plain(&mut app, K::Esc);
assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
fs::remove_file(t.0.join("lib.bas")).unwrap();
fs::remove_file(t.0.join("body.bi")).unwrap();
fs::write(t.0.join("consumer.bas"), "CALL Answer\nEND\n").unwrap();
fs::write(t.0.join("consumer.mak"), "consumer.bas\nout.tbl\n").unwrap();
app.load_initial_project(t.0.join("consumer.mak")).unwrap();
app.start_execution().unwrap();
for n in 0..100 {
app.tick_execution(n);
if app.execution == Execution::Ended {
break;
}
}
assert_eq!(tb_runtime::snapshot::text(app.session.screen()), " 42 \n");
}
#[test]
fn executable_target_validation_and_missing_template_are_honest() {
use tb_ide::{
documents::Destination,
export::{Artifact, ExportRequest},
};
let t = Temp::new();
let mut app = t.app();
for (system, arch) in [
("windows", "aarch64"),
("macos", "x86_64"),
("unknown", "x86_64"),
] {
assert!(ExportRequest::validate(
&app.project,
Artifact::Executable,
system,
arch,
&Destination::new(t.0.join("out"))
)
.is_err());
}
app.runtime_dir = t.0.join("missing-runtimes");
menu(&mut app, Command::MakeExe);
let out = t.0.join("out");
field(&mut app, 3, &out.display().to_string());
plain(&mut app, K::Enter);
let status = await_export(&mut app);
assert!(
matches!(&status, ExportStatus::Failed(e) if e.contains("Runtime-Vorlage fehlt")),
"{status:?}"
);
assert!(!out.exists());
assert!(app.dialog.is_some());
}
#[test]
fn debugger_reports_absent_library_source_and_can_step_back_to_caller() {
let t = Temp::new();
let path = t.0.join("lib.bas");
fs::write(&path, "SUB Answer\nPRINT 42\nEND SUB\n").unwrap();
let mut compiler = tb_vm::project::ProjectCompiler::default();
compiler.debug_symbols = true;
let bytes = tb_vm::project_io::SourceLoader::default()
.load(&path)
.unwrap()
.compile_library(&mut compiler)
.unwrap()
.to_tbl()
.unwrap();
fs::write(t.0.join("lib.tbl"), bytes).unwrap();
fs::remove_file(&path).unwrap();
fs::write(t.0.join("main.bas"), "CALL Answer\nPRINT 9\nEND\n").unwrap();
fs::write(t.0.join("main.mak"), "main.bas\nlib.tbl\n").unwrap();
let mut app = t.app();
app.load_initial_project(t.0.join("main.mak")).unwrap();
let documents = app.project.documents().count();
let mut missing = false;
for _ in 0..10 {
app.execute(Command::Step);
for n in 0..100 {
app.tick_execution(n);
if app.execution != Execution::Running {
break;
}
}
if app.message.contains("Bibliotheksquelle nicht verfügbar") {
missing = true;
break;
}
}
assert!(missing, "{}", app.message);
assert_eq!(app.project.documents().count(), documents);
assert!(!path.exists());
app.execute(Command::ProcedureStep);
for n in 0..100 {
app.tick_execution(n);
if app.execution != Execution::Running {
break;
}
}
app.execute(Command::Continue);
for n in 0..100 {
app.tick_execution(n);
if app.execution == Execution::Ended {
break;
}
}
assert_eq!(app.execution, Execution::Ended, "{}", app.message);
assert_eq!(
tb_runtime::snapshot::text(app.session.screen()),
" 42 \n 9 \n"
);
}
#[test]
fn finalization_failure_is_reported_and_preserves_existing_output() {
let t = Temp::new();
let mut app = t.app();
let target = tb_export::Target::MacosArm64;
app.runtime_dir = t.0.join("runtimes");
let template = app
.runtime_dir
.join(target.triple())
.join(target.runtime_name());
fs::create_dir_all(template.parent().unwrap()).unwrap();
// Header für den negativen Finalisierungstest, kein ausführbarer Zielnachweis.
let mut header = vec![0; 256];
header[..4].copy_from_slice(&[0xcf, 0xfa, 0xed, 0xfe]);
header[4..8].copy_from_slice(&0x0100000cu32.to_le_bytes());
header[12..16].copy_from_slice(&2u32.to_le_bytes());
header.extend_from_slice(&tb_export::runtime_marker(target));
fs::write(&template, header).unwrap();
tb_export::prepare_template(&template, target).unwrap();
menu(&mut app, Command::MakeExe);
for (n, value) in [(1, "macos"), (2, "aarch64")] {
while app.dialog.as_ref().unwrap().focus != n {
plain(&mut app, K::Tab);
}
while app.dialog.as_ref().unwrap().fields[n].string() != value {
plain(&mut app, K::Right);
}
}
let out = t.0.join("existing");
fs::write(&out, b"old").unwrap();
field(&mut app, 3, &out.display().to_string());
plain(&mut app, K::Tab);
plain(&mut app, K::Char(' '));
plain(&mut app, K::Enter);
let status = await_export(&mut app);
assert!(
matches!(&status,ExportStatus::Failed(e) if e.contains("Finalisierung")),
"{status:?}"
);
assert_eq!(fs::read(&out).unwrap(), b"old");
assert!(fs::read_dir(&t.0).unwrap().all(|p| !p
.unwrap()
.file_name()
.to_string_lossy()
.starts_with(".tb-export-")));
}

View File

@@ -488,3 +488,51 @@ fn cursor_after_shortening_uses_original_position_and_open_after_save_uses_fresh
.unwrap());
assert_eq!(p.document(p.members()[0]).unwrap().code(), "PRINT 7\n");
}
#[test]
fn libraries_are_binary_members_with_relative_save_as_and_no_source_views() {
let t = Temp::new();
let lib = t.write("lib.bas", "SUB Answer\nPRINT 42\nEND SUB\n");
let mut compiler = tb_vm::project::ProjectCompiler::default();
let bytes = SourceLoader::default()
.load(&lib)
.unwrap()
.compile_library(&mut compiler)
.unwrap()
.to_tbl()
.unwrap();
let tbl = t.0.join("lib.tbl");
fs::write(&tbl, &bytes).unwrap();
fs::remove_file(lib).unwrap();
let main = t.write("main.bas", "CALL Answer\nEND\n");
let mak = t.write("p.mak", "lib.tbl\nmain.bas\n'$STARTUP: \"main.bas\"\n");
let mut p = Project::open(&mak, vec![]).unwrap();
let id = p.members()[0];
assert!(p.document(id).unwrap().is_library());
assert!(p.open_view(id).is_err());
assert!(p.replace_text(id, 0..0, "bad").is_err());
assert!(p.save_file(id, None).is_err());
assert!(p.set_startup(Some(id)).is_err());
assert_eq!(p.source_members().len(), 1);
p.sources()
.unwrap()
.compile(&mut compiler, "consumer")
.unwrap();
fs::create_dir(t.0.join("sub")).unwrap();
p.save_project(&t.plan("sub/p.mak")).unwrap();
let text = fs::read_to_string(t.0.join("sub/p.mak")).unwrap();
assert!(text.contains("../lib.tbl"), "{text}");
assert_eq!(fs::read(&tbl).unwrap(), bytes);
let mut reopened = Project::open(&t.0.join("sub/p.mak"), vec![]).unwrap();
let id = reopened.members()[0];
reopened.remove_file(id, None).unwrap();
assert!(tbl.exists());
reopened.add_file(&tbl).unwrap();
assert_eq!(reopened.manifest().members().last().unwrap(), &tbl);
assert!(reopened.find_document(&main).is_some());
reopened
.sources()
.unwrap()
.compile(&mut compiler, "consumer")
.unwrap();
}

View File

@@ -305,7 +305,8 @@ fn twenty_back_steps_end_of_contents_and_all_entries() {
h.back();
assert_eq!(h.position, p);
assert!(h.notice.contains("20"));
h.visit(11, 0, None);
let (last, _) = Catalog::embedded().resolve("", "PLAN.md").unwrap();
h.visit(last, 0, None);
let p = h.position.clone();
h.next();
assert_eq!(h.position, p);