Phase 6: IDE-Exportanbindung abschließen und Change archivieren
This commit is contained in:
@@ -13,6 +13,7 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
tb-frontend.workspace = true
|
||||
tb-vm.workspace = true
|
||||
tb-export.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
tb-ui = { workspace = true, features = ["terminal"] }
|
||||
ratatui.workspace = true
|
||||
|
||||
@@ -301,6 +301,9 @@ pub struct App {
|
||||
pub last_command: Option<Command>,
|
||||
pub hits: Vec<(Rect, Hit)>,
|
||||
pub basic_events: Vec<Event>,
|
||||
pub export_backend: bool,
|
||||
pub runtime_dir: PathBuf,
|
||||
pub(crate) export_job: Option<crate::export::Job>,
|
||||
pub last_export: Option<(ExportRequest, ExportStatus)>,
|
||||
pub config_path: PathBuf,
|
||||
pub saved_options: Options,
|
||||
@@ -351,6 +354,12 @@ impl App {
|
||||
last_command: None,
|
||||
hits: vec![],
|
||||
basic_events: vec![],
|
||||
export_backend: true,
|
||||
runtime_dir: std::env::current_exe()?
|
||||
.parent()
|
||||
.unwrap_or(base)
|
||||
.join("runtimes"),
|
||||
export_job: None,
|
||||
last_export: None,
|
||||
next_window: 1,
|
||||
geometry_action: None,
|
||||
@@ -674,14 +683,18 @@ impl App {
|
||||
SaveAs => self.save_dialog(false, vec![id.unwrap()], AfterSave::Stay)?,
|
||||
SaveProject => self.save_dialog(
|
||||
true,
|
||||
self.project.documents().map(|(id, _)| id).collect(),
|
||||
self.project
|
||||
.documents()
|
||||
.filter(|(_, d)| !d.is_library())
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
AfterSave::Stay,
|
||||
)?,
|
||||
RemoveFile => {
|
||||
let ids = self.project.members();
|
||||
let names = self.names(&ids);
|
||||
let mut replacements = vec!["Bisheriger Standard".into()];
|
||||
replacements.extend(names.clone());
|
||||
replacements.extend(self.names(&self.project.source_members()));
|
||||
self.open_dialog(
|
||||
"Remove File",
|
||||
DialogKind::Remove,
|
||||
@@ -692,7 +705,7 @@ impl App {
|
||||
);
|
||||
}
|
||||
Startup => {
|
||||
let ids = self.project.members();
|
||||
let ids = self.project.source_members();
|
||||
let mut names = vec!["Bisheriger Standard".into()];
|
||||
names.extend(self.names(&ids));
|
||||
let selected = self
|
||||
@@ -711,7 +724,10 @@ impl App {
|
||||
let ids: Vec<_> = self
|
||||
.project
|
||||
.documents()
|
||||
.filter(|(_, d)| command == Code || matches!(d.content(), Content::Form(_)))
|
||||
.filter(|(_, d)| {
|
||||
!d.is_library()
|
||||
&& (command == Code || matches!(d.content(), Content::Form(_)))
|
||||
})
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
let selected = id
|
||||
@@ -880,12 +896,22 @@ impl App {
|
||||
} else {
|
||||
Artifact::Library
|
||||
};
|
||||
let systems = vec!["linux".into(), "macos".into(), "windows".into()];
|
||||
let mut systems: Vec<String> = tb_export::Target::ALL
|
||||
.iter()
|
||||
.map(|t| crate::export::target_parts(*t).0.into())
|
||||
.collect();
|
||||
systems.sort();
|
||||
systems.dedup();
|
||||
let selected = systems
|
||||
.iter()
|
||||
.position(|s| s == std::env::consts::OS)
|
||||
.unwrap_or(0);
|
||||
let architectures = vec!["x86_64".into(), "aarch64".into()];
|
||||
let mut architectures: Vec<String> = tb_export::Target::ALL
|
||||
.iter()
|
||||
.map(|t| crate::export::target_parts(*t).1.into())
|
||||
.collect();
|
||||
architectures.sort();
|
||||
architectures.dedup();
|
||||
let arch = architectures
|
||||
.iter()
|
||||
.position(|s| s == std::env::consts::ARCH)
|
||||
@@ -906,12 +932,15 @@ impl App {
|
||||
self.output_default(if artifact == Artifact::Executable {
|
||||
"program"
|
||||
} else {
|
||||
"library"
|
||||
"library.tbl"
|
||||
}),
|
||||
),
|
||||
Field::toggle("Bestehendes Ziel überschreiben", false),
|
||||
],
|
||||
);
|
||||
if artifact == Artifact::Library {
|
||||
self.dialog.as_mut().unwrap().fields.drain(1..3);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -968,6 +997,13 @@ impl App {
|
||||
}
|
||||
}
|
||||
pub(crate) fn show_document(&mut self, id: DocumentId, form: bool) -> Result<()> {
|
||||
if self.project.document(id)?.is_library() {
|
||||
self.message = format!(
|
||||
"TBL-Bibliothek: {} · Binärabhängigkeit, kein editierbarer Quelltext",
|
||||
self.project.document(id)?.source_path().display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if form {
|
||||
ensure!(
|
||||
matches!(self.project.document(id)?.content(), Content::Form(_)),
|
||||
@@ -1078,7 +1114,7 @@ impl App {
|
||||
self.selected_member = 0;
|
||||
self.mode = Mode::Environment;
|
||||
self.properties = false;
|
||||
if let Some(id) = self.project.members().first().copied() {
|
||||
if let Some(id) = self.project.source_members().first().copied() {
|
||||
self.show_document(id, false)?;
|
||||
}
|
||||
let active = self.active;
|
||||
@@ -1198,7 +1234,9 @@ impl App {
|
||||
let ids: Vec<_> = self
|
||||
.project
|
||||
.documents()
|
||||
.filter(|(_, d)| !form || matches!(d.content(), Content::Form(_)))
|
||||
.filter(|(_, d)| {
|
||||
!d.is_library() && (!form || matches!(d.content(), Content::Form(_)))
|
||||
})
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
let id = *ids
|
||||
@@ -1215,7 +1253,7 @@ impl App {
|
||||
Some(if replacement == 0 {
|
||||
StartupRemoval::Default
|
||||
} else {
|
||||
StartupRemoval::Replace(ids[replacement - 1])
|
||||
StartupRemoval::Replace(self.project.source_members()[replacement - 1])
|
||||
}),
|
||||
)?;
|
||||
self.selected_member = self
|
||||
@@ -1223,7 +1261,7 @@ impl App {
|
||||
.min(self.project.members().len().saturating_sub(1));
|
||||
}
|
||||
DialogKind::Startup => {
|
||||
let ids = self.project.members();
|
||||
let ids = self.project.source_members();
|
||||
let choice = d.fields[0].index();
|
||||
self.project.set_startup(if choice == 0 {
|
||||
None
|
||||
@@ -1234,7 +1272,11 @@ impl App {
|
||||
DialogKind::Dirty(after) => match d.fields[0].index() {
|
||||
1 => self.save_dialog(
|
||||
true,
|
||||
self.project.documents().map(|(id, _)| id).collect(),
|
||||
self.project
|
||||
.documents()
|
||||
.filter(|(_, d)| !d.is_library())
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
after,
|
||||
)?,
|
||||
2 => self.finish_change(after)?,
|
||||
@@ -1352,25 +1394,83 @@ impl App {
|
||||
self.message = format!("Optionen gespeichert: {}", self.config_path.display());
|
||||
}
|
||||
DialogKind::Export(artifact) => {
|
||||
let library = artifact == Artifact::Library;
|
||||
let output = if library { 1 } else { 3 };
|
||||
let request = ExportRequest::validate(
|
||||
&self.project,
|
||||
artifact,
|
||||
&d.fields[1].string(),
|
||||
&d.fields[2].string(),
|
||||
&if library {
|
||||
String::new()
|
||||
} else {
|
||||
d.fields[1].string()
|
||||
},
|
||||
&if library {
|
||||
String::new()
|
||||
} else {
|
||||
d.fields[2].string()
|
||||
},
|
||||
&Destination {
|
||||
path: d.fields[3].string().into(),
|
||||
overwrite: d.fields[4].flag(),
|
||||
path: d.fields[output].string().into(),
|
||||
overwrite: d.fields[output + 1].flag(),
|
||||
},
|
||||
)?;
|
||||
d.request = Some(request);
|
||||
d.export_status = ExportStatus::Ready;
|
||||
d.error = crate::export::UNAVAILABLE.into();
|
||||
self.export_job = None;
|
||||
d.request = Some(request.clone());
|
||||
if self.export_backend {
|
||||
match crate::export::Job::start(request, &self.project, &self.runtime_dir) {
|
||||
Ok(job) => {
|
||||
self.export_job = Some(job);
|
||||
d.export_status = ExportStatus::Running;
|
||||
d.error.clear();
|
||||
}
|
||||
Err(e) => {
|
||||
d.export_status = ExportStatus::Failed(format!("{e:#}"));
|
||||
d.error.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
d.export_status = ExportStatus::Ready;
|
||||
d.error = crate::export::UNAVAILABLE.into();
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
/// Die spätere Anbindung und Oberflächentests liefern denselben konkreten Auftrag zurück.
|
||||
/// Der UI-Thread veröffentlicht ausschließlich den weiterhin gültigen Auftrag.
|
||||
pub fn tick_export(&mut self) {
|
||||
let Some(job) = &self.export_job else {
|
||||
return;
|
||||
};
|
||||
let request = job.request.clone();
|
||||
let same_dialog = self.dialog.as_ref().and_then(|d| d.request.as_ref()) == Some(&request);
|
||||
if !same_dialog || request.project != crate::export::ProjectStamp::capture(&self.project) {
|
||||
self.export_job = None;
|
||||
if same_dialog {
|
||||
if let Some(d) = &mut self.dialog {
|
||||
d.export_status = ExportStatus::Failed(
|
||||
"Projekt wurde seit dem Exportauftrag geändert".into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(result) = job.take_result() {
|
||||
let status = match result.and_then(|prepared| {
|
||||
ensure!(
|
||||
std::fs::read(&request.path).ok() == request.previous_output,
|
||||
"Ziel wurde seit dem Exportauftrag verändert"
|
||||
);
|
||||
prepared.publish(&|| false)
|
||||
}) {
|
||||
Ok(()) => ExportStatus::Success,
|
||||
Err(e) => ExportStatus::Failed(format!("{e:#}")),
|
||||
};
|
||||
self.export_job = None;
|
||||
let _ = self.export_result(&request, status);
|
||||
}
|
||||
}
|
||||
/// Oberflächentests und die geprüfte Veröffentlichung melden denselben Auftrag zurück.
|
||||
pub fn export_result(&mut self, request: &ExportRequest, status: ExportStatus) -> Result<()> {
|
||||
let d = self
|
||||
.dialog
|
||||
@@ -1639,13 +1739,16 @@ impl App {
|
||||
}
|
||||
}
|
||||
fn cancel_dialog(&mut self) {
|
||||
self.export_job = None;
|
||||
if let Some(d) = self.dialog.take() {
|
||||
if let DialogKind::Browse { return_to, .. } = d.kind {
|
||||
self.dialog = Some(*return_to);
|
||||
return;
|
||||
}
|
||||
if let Some(request) = d.request {
|
||||
self.last_export = Some((request, ExportStatus::Cancelled));
|
||||
if d.export_status != ExportStatus::Success {
|
||||
self.last_export = Some((request, ExportStatus::Cancelled));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1958,7 +2061,9 @@ impl App {
|
||||
| DialogKind::SaveText
|
||||
| DialogKind::Print => index == 0,
|
||||
DialogKind::Save { .. } => index % 2 == 0,
|
||||
DialogKind::Export(_) => index == 3,
|
||||
DialogKind::Export(artifact) => {
|
||||
index == if artifact == Artifact::Library { 1 } else { 3 }
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !allowed || index >= dialog.fields.len() {
|
||||
|
||||
@@ -427,6 +427,15 @@ impl App {
|
||||
.is_ok_and(|doc| module_name(doc).eq_ignore_ascii_case(&owner_name))
|
||||
});
|
||||
self.session.fullscreen = false;
|
||||
if self
|
||||
.project
|
||||
.find_document(std::path::Path::new(&file))
|
||||
.is_none()
|
||||
&& !std::path::Path::new(&file).is_file()
|
||||
{
|
||||
self.message = format!("Bibliotheksquelle nicht verfügbar: {file}:{} · Fortsetzen oder Prozedurschritt verwenden", location.line);
|
||||
return Ok(());
|
||||
}
|
||||
self.goto_diagnostic(&Diagnostic {
|
||||
file: Some(file.clone()),
|
||||
pos: SourcePos {
|
||||
|
||||
@@ -102,6 +102,9 @@ fn remap_breakpoints(
|
||||
.collect()
|
||||
}
|
||||
impl Document {
|
||||
pub fn is_library(&self) -> bool {
|
||||
has_extension(&self.source_path, "tbl")
|
||||
}
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
}
|
||||
@@ -247,6 +250,12 @@ impl Project {
|
||||
.filter_map(|p| self.find_document(p))
|
||||
.collect()
|
||||
}
|
||||
pub fn source_members(&self) -> Vec<DocumentId> {
|
||||
self.members()
|
||||
.into_iter()
|
||||
.filter(|id| !self.documents[id].is_library())
|
||||
.collect()
|
||||
}
|
||||
pub fn startup(&self) -> Option<DocumentId> {
|
||||
self.manifest
|
||||
.startup
|
||||
@@ -260,7 +269,7 @@ impl Project {
|
||||
pub fn loader(&self) -> Result<SourceLoader> {
|
||||
let mut loader = SourceLoader::default();
|
||||
loader.include_paths = self.include_paths.clone();
|
||||
for doc in self.documents.values() {
|
||||
for doc in self.documents.values().filter(|d| !d.is_library()) {
|
||||
loaded(loader.insert(&doc.source_path, doc.content.clone()))?;
|
||||
}
|
||||
Ok(loader)
|
||||
@@ -279,7 +288,18 @@ impl Project {
|
||||
return Ok(id);
|
||||
}
|
||||
ensure!(!has_extension(&path, "mak"), "MAK über Open Project öffnen");
|
||||
let read = loaded(read_document(&path))?;
|
||||
let read = if has_extension(&path, "tbl") {
|
||||
let bytes = fs::read(&path)?;
|
||||
tb_vm::library::Library::from_tbl(&bytes).map_err(|e| anyhow!(e))?;
|
||||
tb_vm::project_io::ReadDocument {
|
||||
content: Content::Text(String::new()),
|
||||
bytes,
|
||||
binary: false,
|
||||
warnings: vec![],
|
||||
}
|
||||
} else {
|
||||
loaded(read_document(&path))?
|
||||
};
|
||||
let id = DocumentId(next_id());
|
||||
self.documents.insert(
|
||||
id,
|
||||
@@ -301,15 +321,17 @@ impl Project {
|
||||
}
|
||||
pub fn add_file(&mut self, path: &Path) -> Result<DocumentId> {
|
||||
ensure!(
|
||||
has_extension(path, "bas") || has_extension(path, "frm"),
|
||||
"Projektmitglied muss BAS oder FRM sein"
|
||||
has_extension(path, "bas") || has_extension(path, "frm") || has_extension(path, "tbl"),
|
||||
"Projektmitglied muss BAS, FRM oder TBL sein"
|
||||
);
|
||||
// Auch die hinzugefügte Include-Kette muss vor jeder Änderung gültig sein.
|
||||
let loader = self.loader()?;
|
||||
let path = identity(&loaded(
|
||||
loader.resolve(&self.base, &path.to_string_lossy()),
|
||||
)?)?;
|
||||
loaded(loader.load(&path))?;
|
||||
if !has_extension(&path, "tbl") {
|
||||
loaded(loader.load(&path))?;
|
||||
}
|
||||
let id = self.open_document(&path)?;
|
||||
if !self.manifest.members().any(|p| p == &path) {
|
||||
self.manifest.lines.push(ProjectLine::File(path));
|
||||
@@ -403,6 +425,7 @@ impl Project {
|
||||
.map(|id| self.document(id).map(|d| d.source_path.clone()))
|
||||
.transpose()?;
|
||||
if let Some(path) = &path {
|
||||
ensure!(!has_extension(path, "tbl"), "TBL ist keine Startdatei");
|
||||
ensure!(
|
||||
self.manifest.members().any(|p| p == path),
|
||||
"Startdatei ist kein Projektmitglied"
|
||||
@@ -436,7 +459,10 @@ impl Project {
|
||||
Ok(true)
|
||||
}
|
||||
pub fn open_view(&mut self, document: DocumentId) -> Result<ViewId> {
|
||||
self.document(document)?;
|
||||
ensure!(
|
||||
!self.document(document)?.is_library(),
|
||||
"TBL ist eine Binärbibliothek, kein editierbarer Quelltext"
|
||||
);
|
||||
let id = ViewId(next_id());
|
||||
self.views.insert(
|
||||
id,
|
||||
@@ -546,6 +572,7 @@ impl Project {
|
||||
}
|
||||
}
|
||||
fn commit_edit(&mut self, id: DocumentId, content: Content) -> Result<()> {
|
||||
ensure!(!self.document(id)?.is_library(), "TBL ist schreibgeschützt");
|
||||
self.design_ids(id)?;
|
||||
let old = self.document(id)?;
|
||||
if old.content == content {
|
||||
@@ -740,6 +767,7 @@ impl Project {
|
||||
target: &Destination,
|
||||
) -> Result<()> {
|
||||
let doc = self.document(id)?;
|
||||
ensure!(!doc.is_library(), "TBL ist kein Textdokument");
|
||||
let text = match range {
|
||||
Some(r) => doc
|
||||
.code()
|
||||
@@ -806,6 +834,10 @@ impl Project {
|
||||
Ok(())
|
||||
}
|
||||
fn file_target(&self, id: DocumentId, target: Option<&Destination>) -> Result<(PathBuf, bool)> {
|
||||
ensure!(
|
||||
!self.document(id)?.is_library(),
|
||||
"TBL wird als Binärabhängigkeit referenziert und nicht gespeichert"
|
||||
);
|
||||
let doc = self.document(id)?;
|
||||
let path = match target {
|
||||
Some(t) => self.target(&t.path)?,
|
||||
@@ -832,8 +864,10 @@ impl Project {
|
||||
ensure!(has_extension(&path, "bas"), "Modulziel muss .bas sein")
|
||||
}
|
||||
Content::Text(_) => ensure!(
|
||||
!has_extension(&path, "frm") && !has_extension(&path, "mak"),
|
||||
"Textziel darf kein FRM/MAK sein"
|
||||
!has_extension(&path, "frm")
|
||||
&& !has_extension(&path, "mak")
|
||||
&& !has_extension(&path, "tbl"),
|
||||
"Textziel darf kein FRM/MAK/TBL sein"
|
||||
),
|
||||
}
|
||||
Ok((path, target.is_some_and(|t| t.overwrite)))
|
||||
|
||||
@@ -118,7 +118,7 @@ impl App {
|
||||
let target = if project {
|
||||
self.project.path().map(PathBuf::from).or_else(|| {
|
||||
self.project
|
||||
.members()
|
||||
.source_members()
|
||||
.first()
|
||||
.and_then(|id| self.project.document(*id).ok())
|
||||
.map(|d| d.source_path().to_path_buf())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Konkreter Vertrag für die spätere native Erzeugung, ohne Phase-5-Dateiausgabe.
|
||||
//! Revisionsgebundene Exportaufträge und abwerfbare Hintergrundresultate.
|
||||
use crate::documents::{Destination, DocumentId, Project};
|
||||
use anyhow::{ensure, Result};
|
||||
use std::path::PathBuf;
|
||||
@@ -14,7 +14,7 @@ impl Artifact {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Executable => "Natives Standalone-Executable",
|
||||
Self::Library => "Native Systembibliothek",
|
||||
Self::Library => "Portable P-Code-Bibliothek (.tbl)",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ impl Artifact {
|
||||
pub struct ProjectStamp {
|
||||
pub path: Option<PathBuf>,
|
||||
pub manifest: Manifest,
|
||||
pub inputs: Vec<(PathBuf, Option<Vec<u8>>)>,
|
||||
pub include_paths: Vec<PathBuf>,
|
||||
pub revisions: Vec<(DocumentId, u64)>,
|
||||
}
|
||||
impl ProjectStamp {
|
||||
@@ -29,6 +31,20 @@ impl ProjectStamp {
|
||||
Self {
|
||||
path: project.path().map(PathBuf::from),
|
||||
manifest: project.manifest().clone(),
|
||||
include_paths: project.include_paths.clone(),
|
||||
inputs: project
|
||||
.sources()
|
||||
.map(|s| {
|
||||
s.protected_inputs()
|
||||
.into_iter()
|
||||
.filter(|p| has_extension(p, "tbl") || project.find_document(p).is_none())
|
||||
.map(|p| {
|
||||
let b = std::fs::read(&p).ok();
|
||||
(p, b)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
revisions: project
|
||||
.documents()
|
||||
.map(|(id, d)| (id, d.revision()))
|
||||
@@ -38,12 +54,14 @@ impl ProjectStamp {
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExportRequest {
|
||||
pub generation: u64,
|
||||
pub project: ProjectStamp,
|
||||
pub artifact: Artifact,
|
||||
pub system: String,
|
||||
pub architecture: String,
|
||||
pub path: PathBuf,
|
||||
pub overwrite: bool,
|
||||
pub previous_output: Option<Vec<u8>>,
|
||||
}
|
||||
impl ExportRequest {
|
||||
pub fn validate(
|
||||
@@ -53,24 +71,25 @@ impl ExportRequest {
|
||||
architecture: &str,
|
||||
destination: &Destination,
|
||||
) -> Result<Self> {
|
||||
ensure!(
|
||||
["linux", "macos", "windows"].contains(&system),
|
||||
"Unbekanntes Zielsystem"
|
||||
);
|
||||
ensure!(
|
||||
["x86_64", "aarch64"].contains(&architecture),
|
||||
"Unbekannte Zielarchitektur"
|
||||
);
|
||||
if artifact == Artifact::Executable {
|
||||
target(system, architecture)?;
|
||||
}
|
||||
ensure!(
|
||||
!has_extension(&destination.path, "tbc"),
|
||||
"TBC ist kein natives Exportartefakt"
|
||||
);
|
||||
let path = project.validate_export_target(destination)?;
|
||||
if artifact == Artifact::Library {
|
||||
ensure!(has_extension(&path, "tbl"), "Library-Ausgabe benötigt .tbl");
|
||||
}
|
||||
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
Ok(Self {
|
||||
generation: NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
project: ProjectStamp::capture(project),
|
||||
artifact,
|
||||
system: system.into(),
|
||||
architecture: architecture.into(),
|
||||
previous_output: std::fs::read(&path).ok(),
|
||||
path,
|
||||
overwrite: destination.overwrite,
|
||||
})
|
||||
@@ -95,3 +114,333 @@ impl ExportStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Die unterstützten Kombinationen werden ausschließlich aus dem Exportkatalog gewählt.
|
||||
pub fn target(system: &str, architecture: &str) -> Result<tb_export::Target> {
|
||||
tb_export::Target::ALL
|
||||
.into_iter()
|
||||
.find(|t| target_parts(*t) == (system, architecture))
|
||||
.ok_or_else(|| anyhow::anyhow!("Unzulässiges EXE-Ziel: {system}/{architecture}"))
|
||||
}
|
||||
pub fn target_parts(target: tb_export::Target) -> (&'static str, &'static str) {
|
||||
let triple = target.triple();
|
||||
(
|
||||
if triple.contains("windows") {
|
||||
"windows"
|
||||
} else if triple.contains("apple") {
|
||||
"macos"
|
||||
} else {
|
||||
"linux"
|
||||
},
|
||||
triple.split('-').next().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Job {
|
||||
pub request: ExportRequest,
|
||||
thread: Option<std::thread::JoinHandle<()>>,
|
||||
cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
result: std::sync::mpsc::Receiver<Result<tb_export::PreparedPublication>>,
|
||||
}
|
||||
impl Drop for Job {
|
||||
fn drop(&mut self) {
|
||||
self.cancelled
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
// Auch beim Beenden der IDE müssen kontrollierte Tools und Staging enden.
|
||||
if let Some(thread) = self.thread.take() {
|
||||
let _ = thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Job {
|
||||
pub fn start(
|
||||
request: ExportRequest,
|
||||
project: &Project,
|
||||
runtime_dir: &std::path::Path,
|
||||
) -> Result<Self> {
|
||||
let sources = project.sources()?;
|
||||
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
||||
compiler.debug_symbols = true;
|
||||
let diagnostics = |errors: Vec<tb_frontend::Diagnostic>| {
|
||||
anyhow::anyhow!(errors
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"))
|
||||
};
|
||||
let bytes = match request.artifact {
|
||||
Artifact::Library => sources
|
||||
.compile_library(&mut compiler)
|
||||
.map_err(diagnostics)?
|
||||
.to_tbl()
|
||||
.map_err(|e| anyhow::anyhow!(e))?,
|
||||
Artifact::Executable => sources
|
||||
.compile(&mut compiler, "IDE")
|
||||
.map_err(diagnostics)?
|
||||
.to_tbc(),
|
||||
};
|
||||
ensure!(
|
||||
request.project == ProjectStamp::capture(project),
|
||||
"Projekt während der Übersetzung geändert"
|
||||
);
|
||||
let mut protected = sources.protected_inputs();
|
||||
protected.extend(
|
||||
project
|
||||
.documents()
|
||||
.map(|(_, d)| d.source_path().to_path_buf()),
|
||||
);
|
||||
if let Some(path) = project.path() {
|
||||
protected.push(path.into());
|
||||
}
|
||||
let runtime_dir = runtime_dir.to_path_buf();
|
||||
let cancelled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let flag = cancelled.clone();
|
||||
let (sender, result) = std::sync::mpsc::channel();
|
||||
let work = request.clone();
|
||||
let thread = std::thread::Builder::new()
|
||||
.name("tb-export".into())
|
||||
.spawn(move || {
|
||||
let cancelled = || flag.load(std::sync::atomic::Ordering::Acquire);
|
||||
let prepare = || -> Result<tb_export::PreparedPublication> {
|
||||
if work.artifact == Artifact::Library {
|
||||
tb_export::prepare_publication(
|
||||
&work.path,
|
||||
work.overwrite,
|
||||
&protected,
|
||||
&cancelled,
|
||||
|p| {
|
||||
std::fs::write(p, &bytes)?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let target = target(&work.system, &work.architecture)?;
|
||||
let template = runtime_dir
|
||||
.join(target.triple())
|
||||
.join(target.runtime_name());
|
||||
let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
tb_export::prepare_export(
|
||||
tb_export::Export {
|
||||
module: &module,
|
||||
target,
|
||||
template: &template,
|
||||
output: &work.path,
|
||||
overwrite: work.overwrite,
|
||||
protected: &protected,
|
||||
},
|
||||
&cancelled,
|
||||
)
|
||||
}
|
||||
};
|
||||
// Ein geschlossener Empfänger verwirft das Staging-Ergebnis per Drop.
|
||||
let _ = sender.send(prepare());
|
||||
})?;
|
||||
Ok(Self {
|
||||
thread: Some(thread),
|
||||
request,
|
||||
cancelled,
|
||||
result,
|
||||
})
|
||||
}
|
||||
pub fn take_result(&self) -> Option<Result<tb_export::PreparedPublication>> {
|
||||
match self.result.try_recv() {
|
||||
Ok(result) => Some(result),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => None,
|
||||
Err(_) => Some(Err(anyhow::anyhow!("Exportworker beendet ohne Ergebnis"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{app::App, commands::Command};
|
||||
use std::{
|
||||
fs,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
mpsc, Arc,
|
||||
},
|
||||
};
|
||||
|
||||
struct Dir(PathBuf);
|
||||
impl Dir {
|
||||
fn new() -> Self {
|
||||
static N: AtomicU64 = AtomicU64::new(0);
|
||||
let p = std::env::temp_dir().join(format!(
|
||||
"tb-export-ui-{}-{}",
|
||||
std::process::id(),
|
||||
N.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
Self(p.canonicalize().unwrap())
|
||||
}
|
||||
}
|
||||
impl Drop for Dir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
fn delayed(
|
||||
app: &mut App,
|
||||
output: &std::path::Path,
|
||||
) -> (
|
||||
mpsc::Sender<Result<tb_export::PreparedPublication>>,
|
||||
Arc<AtomicBool>,
|
||||
) {
|
||||
app.execute(Command::MakeLibrary);
|
||||
let request = ExportRequest::validate(
|
||||
&app.project,
|
||||
Artifact::Library,
|
||||
"",
|
||||
"",
|
||||
&Destination {
|
||||
path: output.into(),
|
||||
overwrite: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let d = app.dialog.as_mut().unwrap();
|
||||
d.request = Some(request.clone());
|
||||
d.export_status = ExportStatus::Running;
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
app.export_job = Some(Job {
|
||||
thread: None,
|
||||
request,
|
||||
cancelled: cancelled.clone(),
|
||||
result: rx,
|
||||
});
|
||||
(tx, cancelled)
|
||||
}
|
||||
fn prepared(output: &std::path::Path) -> tb_export::PreparedPublication {
|
||||
tb_export::prepare_publication(output, true, &[], &|| false, |p| {
|
||||
fs::write(p, b"new")?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
fn no_staging(dir: &std::path::Path) {
|
||||
assert!(fs::read_dir(dir).unwrap().all(|p| !p
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with(".tb-export-")));
|
||||
}
|
||||
#[test]
|
||||
fn delayed_result_is_published_only_after_ui_validation_and_success_stays_success() {
|
||||
let dir = Dir::new();
|
||||
let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
|
||||
let output = dir.0.join("out.tbl");
|
||||
fs::write(&output, b"old").unwrap();
|
||||
let (tx, _) = delayed(&mut app, &output);
|
||||
let staged = prepared(&output);
|
||||
app.tick_export();
|
||||
app.handle(crossterm::event::Event::Resize(110, 35));
|
||||
assert_eq!(app.size, (110, 35));
|
||||
assert_eq!(fs::read(&output).unwrap(), b"old");
|
||||
tx.send(Ok(staged)).ok().unwrap();
|
||||
app.tick_export();
|
||||
assert_eq!(fs::read(&output).unwrap(), b"new");
|
||||
assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
|
||||
app.handle(crossterm::event::Event::Key(
|
||||
crossterm::event::KeyEvent::new(
|
||||
crossterm::event::KeyCode::Esc,
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
),
|
||||
));
|
||||
assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
|
||||
no_staging(&dir.0);
|
||||
}
|
||||
#[test]
|
||||
fn cancelled_superseded_changed_and_foreign_jobs_never_publish() {
|
||||
for mode in 0..5 {
|
||||
let dir = Dir::new();
|
||||
let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
|
||||
let output = dir.0.join("out.tbl");
|
||||
fs::write(&output, b"old").unwrap();
|
||||
let (tx, flag) = delayed(&mut app, &output);
|
||||
let staged = prepared(&output);
|
||||
match mode {
|
||||
0 => app.handle(crossterm::event::Event::Key(
|
||||
crossterm::event::KeyEvent::new(
|
||||
crossterm::event::KeyCode::Esc,
|
||||
crossterm::event::KeyModifiers::NONE,
|
||||
),
|
||||
)),
|
||||
1 => {
|
||||
let id = app.project.members()[0];
|
||||
app.project.replace_text(id, 0..0, "PRINT 2\n").unwrap();
|
||||
}
|
||||
2 => {
|
||||
app.project = Project::new(&dir.0).unwrap();
|
||||
}
|
||||
3 => {
|
||||
let old = app.export_job.as_ref().unwrap().request.generation;
|
||||
let (_new, _) = delayed(&mut app, &output);
|
||||
assert_ne!(old, app.export_job.as_ref().unwrap().request.generation);
|
||||
}
|
||||
_ => {
|
||||
fs::write(&output, b"external").unwrap();
|
||||
}
|
||||
}
|
||||
let _ = tx.send(Ok(staged));
|
||||
app.tick_export();
|
||||
assert!(flag.load(Ordering::Acquire));
|
||||
assert_eq!(
|
||||
fs::read(&output).unwrap(),
|
||||
if mode == 4 {
|
||||
b"external".as_slice()
|
||||
} else {
|
||||
b"old".as_slice()
|
||||
}
|
||||
);
|
||||
assert!(!app
|
||||
.last_export
|
||||
.as_ref()
|
||||
.is_some_and(|(_, s)| *s == ExportStatus::Success));
|
||||
no_staging(&dir.0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn replaced_library_invalidates_pending_export_even_with_same_length() {
|
||||
let dir = Dir::new();
|
||||
let source = dir.0.join("lib.bas");
|
||||
fs::write(&source, "SUB Answer\nPRINT 1\nEND SUB\n").unwrap();
|
||||
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
||||
let build = |compiler: &mut tb_vm::project::ProjectCompiler| {
|
||||
tb_vm::project_io::SourceLoader::default()
|
||||
.load(&source)
|
||||
.unwrap()
|
||||
.compile_library(compiler)
|
||||
.unwrap()
|
||||
.to_tbl()
|
||||
.unwrap()
|
||||
};
|
||||
let a = build(&mut compiler);
|
||||
fs::write(&source, "SUB Answer\nPRINT 2\nEND SUB\n").unwrap();
|
||||
let b = build(&mut compiler);
|
||||
assert_eq!(a.len(), b.len());
|
||||
let lib = dir.0.join("lib.tbl");
|
||||
fs::write(&lib, &a).unwrap();
|
||||
fs::write(dir.0.join("main.bas"), "CALL Answer\nEND\n").unwrap();
|
||||
fs::write(dir.0.join("main.mak"), "main.bas\nlib.tbl\n").unwrap();
|
||||
let mut app = App::new(&dir.0, dir.0.join("options"), (100, 30)).unwrap();
|
||||
app.load_initial_project(dir.0.join("main.mak")).unwrap();
|
||||
let before = app.compile_current().unwrap().to_tbc();
|
||||
assert!(app.current_compilation().is_some());
|
||||
let out = dir.0.join("out.tbl");
|
||||
fs::write(&out, b"old").unwrap();
|
||||
let (tx, flag) = delayed(&mut app, &out);
|
||||
let staged = prepared(&out);
|
||||
fs::write(&lib, &b).unwrap();
|
||||
assert!(app.current_compilation().is_none());
|
||||
let _ = tx.send(Ok(staged));
|
||||
app.tick_export();
|
||||
assert!(flag.load(Ordering::Acquire));
|
||||
assert_eq!(fs::read(&out).unwrap(), b"old");
|
||||
assert_ne!(before, app.compile_current().unwrap().to_tbc());
|
||||
no_staging(&dir.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,14 @@ const DOCUMENTS: &[(&str, &str)] = &[
|
||||
"docs/tbvm-design.md",
|
||||
include_str!("../../../docs/tbvm-design.md"),
|
||||
),
|
||||
(
|
||||
"docs/pcode-bibliotheken.md",
|
||||
include_str!("../../../docs/pcode-bibliotheken.md"),
|
||||
),
|
||||
(
|
||||
"docs/native-executables.md",
|
||||
include_str!("../../../docs/native-executables.md"),
|
||||
),
|
||||
("PLAN.md", include_str!("../../../PLAN.md")),
|
||||
];
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -70,6 +70,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
app.handle(event::read()?);
|
||||
}
|
||||
app.tick_export();
|
||||
app.tick_execution(start.elapsed().as_millis() as u64);
|
||||
let timeout = if app.execution == Execution::Running {
|
||||
std::time::Duration::ZERO
|
||||
|
||||
@@ -661,7 +661,15 @@ impl App {
|
||||
put(
|
||||
f,
|
||||
submit,
|
||||
if export { "[Prüfen]" } else { "[OK / Enter]" },
|
||||
if export {
|
||||
if self.export_backend {
|
||||
"[Erzeugen]"
|
||||
} else {
|
||||
"[Prüfen]"
|
||||
}
|
||||
} else {
|
||||
"[OK / Enter]"
|
||||
},
|
||||
if d.focus == d.fields.len() {
|
||||
Style::default().fg(Color::White).bg(Color::Black)
|
||||
} else {
|
||||
@@ -671,7 +679,7 @@ impl App {
|
||||
put(f, cancel, "[Abbrechen]", st);
|
||||
self.hits.push((submit, Hit::DialogSubmit));
|
||||
self.hits.push((cancel, Hit::DialogCancel));
|
||||
if export {
|
||||
if export && !self.export_backend {
|
||||
put(
|
||||
f,
|
||||
Rect::new(rect.x + 36, rect.bottom() - 3, width - 38, 1),
|
||||
@@ -689,7 +697,11 @@ impl App {
|
||||
d.error
|
||||
);
|
||||
} else if status.is_empty() {
|
||||
status = crate::export::UNAVAILABLE.into();
|
||||
status = if self.export_backend {
|
||||
"Bereit zum Erzeugen".into()
|
||||
} else {
|
||||
crate::export::UNAVAILABLE.into()
|
||||
};
|
||||
}
|
||||
}
|
||||
f.render_widget(
|
||||
|
||||
@@ -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-")));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user