Phase 6: Kompatibilitaetsabnahme abschliessen und archivieren

This commit is contained in:
2026-09-07 12:34:06 +02:00
parent 54ee427c1c
commit 993c3e7638
21 changed files with 752 additions and 58 deletions

View File

@@ -1,5 +1,9 @@
//! Optionaler öffentlicher Bestand; kein Netzwerk und keine GUI.
use std::{path::PathBuf, process::Command, time::Duration};
use std::{
path::{Path, PathBuf},
process::Command,
time::Duration,
};
use tb_runtime::host::{CaptureHost, Ereignis, Host};
use tb_vm::{
bytecode::CompiledModule,
@@ -13,14 +17,17 @@ struct PruefHost {
inner: CaptureHost,
dialogantwort: Option<Ereignis>,
dialog_gesehen: bool,
dialogtext: Option<&'static str>,
}
impl Host for PruefHost {
fn present(&mut self, screen: &tb_runtime::screen::TextScreen) {
self.dialog_gesehen |= tb_runtime::snapshot::text(screen).contains("Save changes to");
self.dialog_gesehen |= self
.dialogtext
.is_some_and(|text| tb_runtime::snapshot::text(screen).contains(text));
self.inner.present(screen);
}
fn next_event(&mut self, block: bool) -> Option<Ereignis> {
if block {
if self.dialog_gesehen {
if let Some(event) = self.dialogantwort.take() {
return Some(event);
}
@@ -37,23 +44,78 @@ impl Host for PruefHost {
const REVISION: &str = "1cdd2b32b829fe1721d0b6aecc433abc47a96fb6";
fn pruefe_bestand(repo: Option<&Path>) -> Result<(), String> {
let repo = repo.ok_or_else(|| {
format!("TB_VBDOS_REPO fehlt: cout/vbdos bei Revision {REVISION} erforderlich")
})?;
let rev = Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-parse", "HEAD"])
.output()
.map_err(|e| {
format!(
"Fremdprogrammbestand {}: git nicht ausführbar: {e}",
repo.display()
)
})?;
if !rev.status.success() {
return Err(format!("Fremdprogrammbestand {} fehlt oder ist kein lesbares Git-Repository; benötigt {REVISION}", repo.display()));
}
let found = String::from_utf8_lossy(&rev.stdout);
if found.trim() != REVISION {
return Err(format!(
"Fremdprogrammbestand {}: falsche Revision, Soll {REVISION}, Ist {}",
repo.display(),
found.trim()
));
}
Ok(())
}
#[test]
fn fehlender_oder_falscher_fremdbestand_wird_benannt() {
assert!(pruefe_bestand(None)
.unwrap_err()
.contains("TB_VBDOS_REPO fehlt"));
let missing = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
assert!(pruefe_bestand(Some(&missing))
.unwrap_err()
.contains("kein lesbares Git-Repository"));
let error = pruefe_bestand(Some(Path::new(env!("CARGO_MANIFEST_DIR")))).unwrap_err();
assert!(
error.contains("falsche Revision") && error.contains(REVISION),
"{error}"
);
}
struct TempBestand {
original: PathBuf,
base: PathBuf,
}
impl Drop for TempBestand {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
let _ = std::fs::remove_dir_all(&self.base);
}
}
#[test]
#[ignore = "TB_VBDOS_REPO muss auf cout/vbdos in der dokumentierten Revision zeigen"]
fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() {
let _frist =
prozessfrist::Prozessfrist::neu("öffentlicher VBDOS-Bestand", Duration::from_secs(60));
let repo = PathBuf::from(std::env::var_os("TB_VBDOS_REPO").expect("TB_VBDOS_REPO fehlt"));
let rev = Command::new("git")
.arg("-C")
.arg(&repo)
.args(["rev-parse", "HEAD"])
.output()
.unwrap();
assert!(rev.status.success());
assert_eq!(String::from_utf8_lossy(&rev.stdout).trim(), REVISION);
let repo = std::env::var_os("TB_VBDOS_REPO").map(PathBuf::from);
pruefe_bestand(repo.as_deref()).unwrap_or_else(|e| panic!("{e}"));
let repo = repo.unwrap();
let original = std::env::current_dir().unwrap();
let base = std::env::temp_dir().join(format!("tb-foreign-{}", std::process::id()));
std::fs::create_dir_all(&base).unwrap();
let _temp = TempBestand {
original: original.clone(),
base: base.clone(),
};
let archive = base.join("source.tar");
assert!(Command::new("git")
.arg("-C")
@@ -78,6 +140,7 @@ fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() {
] {
let mut results = Vec::new();
for repeat in 0..2 {
eprintln!("{entry}: Lauf {} kompilieren", repeat + 1);
let dir = base.join(format!("run-{repeat}"));
std::fs::create_dir_all(&dir).unwrap();
assert!(Command::new("tar")
@@ -105,7 +168,17 @@ fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() {
vm.rt.screen.resize(100, 30);
vm.forms.resize(100, 30);
std::env::set_current_dir(path.parent().unwrap()).unwrap();
let mut host = PruefHost::default();
let mut host = PruefHost {
dialogtext: match entry {
"microsoft/check.mak" | "misc/mentors/mentors.frm" => {
Some("Save these records?")
}
"microsoft/notepad.frm" => Some("Save changes to"),
_ => None,
},
..Default::default()
};
eprintln!("{entry}: Start und initiale Ereignisse");
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{entry}: Start");
assert_eq!(
vm.run_visible_forms(&mut host),
@@ -124,8 +197,9 @@ fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() {
let before = tb_runtime::snapshot::snapshot(&vm.rt.screen);
let mut states = Vec::new();
for (key, shift) in &keys {
eprintln!("{entry}: Taste {key:?}, Modifikatoren {shift}");
vm.rt.ende = false;
if entry == "microsoft/notepad.frm" && *key == "x" {
if host.dialogtext.is_some() && *key == "x" {
host.dialogantwort = Some(Ereignis::Taste("n".into(), 0));
}
host.inner.ereignis(Ereignis::Taste((*key).into(), *shift));
@@ -167,10 +241,10 @@ fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() {
"{entry}: Menü öffnen und Exit auswählen"
);
}
if entry == "microsoft/notepad.frm" {
if host.dialogtext.is_some() {
assert!(
host.dialog_gesehen && host.dialogantwort.is_none(),
"Notepad: Save-Dialog mit N beantworten"
"{entry}: Save-Dialog mit N beantworten"
);
}
assert!(
@@ -184,5 +258,9 @@ fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() {
}
assert_eq!(results[0], results[1], "{entry}: nicht reproduzierbar");
}
std::fs::remove_dir_all(base).unwrap();
std::fs::remove_dir_all(&base).unwrap();
assert!(
!base.exists(),
"temporärer Fremdprogrammbestand blieb zurück"
);
}