Phase 6: Kompatibilitaetsabnahme abschliessen und archivieren
This commit is contained in:
@@ -206,6 +206,9 @@ fn ereignis_direktive(name: &str, src: &str) -> Vec<Ereignisschritt> {
|
||||
ms.parse()
|
||||
.unwrap_or_else(|_| panic!("{name}: ungültige Zeit {ms:?}")),
|
||||
));
|
||||
} else if let Some(size) = teil.strip_prefix("resize:") {
|
||||
let (cols, rows) = groesse_lesen(name, size);
|
||||
out.push(Ereignisschritt::Ereignis(Ereignis::Groesse { cols, rows }));
|
||||
} else if let Some(mouse) = teil.strip_prefix("mouse:") {
|
||||
let fields: Vec<_> = mouse.split(',').map(str::trim).collect();
|
||||
let [kind, button, shift, row, col] = fields.as_slice() else {
|
||||
@@ -516,15 +519,22 @@ fn assert_output_matches(file: &str, want: &str, got: &str) {
|
||||
(Some(w), Some(g)) => {
|
||||
for (z, (wz, gz)) in w.lines().zip(g.lines()).enumerate() {
|
||||
if wz != gz {
|
||||
// Je Zelle zwei Hexziffern → Spalte aus dem Byteoffset.
|
||||
let spalte = wz
|
||||
// Der Snapshot lässt Fortsetzungszellen breiter Zeichen aus.
|
||||
let index = wz
|
||||
.chars()
|
||||
.zip(gz.chars())
|
||||
.position(|(a, b)| a != b)
|
||||
.map(|i| i / 2 + 1)
|
||||
.unwrap_or(0);
|
||||
let paar =
|
||||
|s: &str| s.chars().skip((spalte - 1) * 2).take(2).collect::<String>();
|
||||
.map(|i| i / 2)
|
||||
.unwrap_or_else(|| wz.len().min(gz.len()) / 2);
|
||||
let prefix: String = want_text
|
||||
.lines()
|
||||
.nth(z)
|
||||
.unwrap_or("")
|
||||
.chars()
|
||||
.take(index)
|
||||
.collect();
|
||||
let spalte = ratatui::text::Line::raw(prefix).width() + 1;
|
||||
let paar = |s: &str| s.chars().skip(index * 2).take(2).collect::<String>();
|
||||
panic!(
|
||||
"{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \
|
||||
Soll: {}\n Ist: {}",
|
||||
@@ -596,9 +606,9 @@ fn uhr_direktive_wird_gelesen() {
|
||||
fn ereignisfolge_liefert_taste_maus_und_zeit_in_reihenfolge() {
|
||||
let steps = ereignis_direktive(
|
||||
"form.frm",
|
||||
"' tb-events: key:%o | mouse:down,1,0,5,7 | time:250ms | key:<ENTER>",
|
||||
"' tb-events: key:%o | mouse:down,1,0,5,7 | time:250ms | key:<ENTER> | resize:100x30",
|
||||
);
|
||||
assert_eq!(steps.len(), 4);
|
||||
assert_eq!(steps.len(), 5);
|
||||
assert!(
|
||||
matches!(&steps[0], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Taste(key, shift)) if key == "o" && *shift == 4)
|
||||
);
|
||||
@@ -609,6 +619,13 @@ fn ereignisfolge_liefert_taste_maus_und_zeit_in_reihenfolge() {
|
||||
assert!(
|
||||
matches!(&steps[3], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Taste(key, _)) if key == tb_runtime::host::taste::ENTER)
|
||||
);
|
||||
assert!(matches!(
|
||||
steps[4],
|
||||
Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Groesse {
|
||||
cols: 100,
|
||||
rows: 30
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -786,6 +803,38 @@ fn farbabweichung_nennt_zeile_spalte_soll_und_ist() {
|
||||
assert!(m.contains("Zeile 1"), "{m}");
|
||||
assert!(m.contains("Spalte 2"), "{m}");
|
||||
assert!(m.contains("70") && m.contains("e1"), "{m}");
|
||||
let kurz = format!("ab\n{marke}\n70\n");
|
||||
let m = meldung(&want, &kurz);
|
||||
assert!(m.contains("Spalte 2") && m.contains("Soll: 70"), "{m}");
|
||||
let m = meldung(
|
||||
&format!("中a\n{marke}\n7070\n"),
|
||||
&format!("中a\n{marke}\n70e1\n"),
|
||||
);
|
||||
assert!(m.contains("Spalte 3") && m.contains("Ist: e1"), "{m}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_formular_vergleicht_zeichen_und_attribute_unabhaengig() {
|
||||
let path = compat_dir().join("release-formular.frm");
|
||||
let want = std::fs::read_to_string(path.with_extension("out")).unwrap();
|
||||
let got = run_corpus_file(&path, None);
|
||||
assert_output_matches("release-formular.frm", &want, &got);
|
||||
assert_eq!(got, run_corpus_file(&path, None));
|
||||
let falscher_text = want.replacen("Click", "Falsch", 1);
|
||||
assert!(meldung(&falscher_text, &got).contains("Abweichung in Zeile 1"));
|
||||
let (text, attr) = want
|
||||
.split_once(tb_runtime::snapshot::ATTRIBUT_MARKE)
|
||||
.unwrap();
|
||||
let falsche_farbe = format!(
|
||||
"{text}{}{}",
|
||||
tb_runtime::snapshot::ATTRIBUT_MARKE,
|
||||
attr.replacen("e1", "71", 1)
|
||||
);
|
||||
let m = meldung(&falsche_farbe, &got);
|
||||
assert!(
|
||||
m.contains("Farbabweichung") && m.contains("Spalte 1"),
|
||||
"{m}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -439,6 +439,33 @@ fn abdeckungsstand_wird_ausgewiesen() {
|
||||
"Abdeckung: implementiert {i} · offen {o} · Non-Feature {n} · gesamt {}",
|
||||
inv.len()
|
||||
);
|
||||
for (name, forms) in [("Sprache", false), ("Forms", true)] {
|
||||
let entries: Vec<_> = inv
|
||||
.iter()
|
||||
.filter(|e| matches!(e.art.as_str(), "Eigenschaft" | "Methode" | "Ereignis") == forms)
|
||||
.collect();
|
||||
let count = |status: &str| entries.iter().filter(|e| e.status == status).count();
|
||||
println!(
|
||||
"{name}: implementiert {} · offen {} · Non-Feature {} · gesamt {}",
|
||||
count(IMPLEMENTIERT),
|
||||
count(OFFEN),
|
||||
count(NON_FEATURE),
|
||||
entries.len()
|
||||
);
|
||||
}
|
||||
let offene: Vec<_> = inv
|
||||
.iter()
|
||||
.filter(|e| e.status == OFFEN)
|
||||
.map(|e| e.name.as_str())
|
||||
.collect();
|
||||
println!(
|
||||
"Offene Einträge: {}",
|
||||
if offene.is_empty() {
|
||||
"keine".into()
|
||||
} else {
|
||||
offene.join(", ")
|
||||
}
|
||||
);
|
||||
|
||||
// Der Kopf der Tabelle nennt dieselben Zahlen — sonst ist die Doku stale.
|
||||
let text = std::fs::read_to_string(wurzel().join("docs/inventar.md")).unwrap();
|
||||
|
||||
@@ -129,6 +129,66 @@ fn cli_meldet_physische_quellorte_auch_nach_verschachtelten_includes() {
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_projekt_prueft_anfangsdateien_dateieffekte_und_include_fehler() {
|
||||
struct Aufraeumen(std::path::PathBuf);
|
||||
impl Drop for Aufraeumen {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("tb-release-files-{}", std::process::id()));
|
||||
{
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
let _cleanup = Aufraeumen(dir.clone());
|
||||
std::fs::write(dir.join("main.bas"), "CALL Kopie\nEND\n").unwrap();
|
||||
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 Kopie\nOPEN \"seed.txt\" FOR INPUT AS #1\nLINE INPUT #1, s$\nCLOSE #1\nOPEN \"result.txt\" FOR APPEND AS #2\nPRINT #2, s$\nCLOSE #2\n200 ERROR 6\nEND SUB\n").unwrap();
|
||||
std::fs::write(dir.join("app.mak"), "MAIN.BAS\nLIB.BAS\n").unwrap();
|
||||
let build = tbc(&dir, "build", "app.mak");
|
||||
assert!(build.status.success(), "{build:?}");
|
||||
for entry in ["app.mak", "app.tbc"] {
|
||||
if entry == "app.tbc" {
|
||||
for source in ["main.bas", "lib.bas", "outer.bi", "inner.bi", "app.mak"] {
|
||||
std::fs::remove_file(dir.join(source)).unwrap();
|
||||
}
|
||||
}
|
||||
for _ in 0..2 {
|
||||
std::fs::write(dir.join("seed.txt"), "Grüße\n").unwrap();
|
||||
std::fs::write(dir.join("result.txt"), "Start\n").unwrap();
|
||||
let out = tbc(&dir, "run", entry);
|
||||
assert_eq!(out.status.code(), Some(2), "{out:?}");
|
||||
assert!(out.stdout.is_empty(), "{out:?}");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
.to_ascii_lowercase()
|
||||
.contains("inner.bi:8:5:"),
|
||||
"{out:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(dir.join("seed.txt")).unwrap(),
|
||||
"Grüße\n".as_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(dir.join("result.txt")).unwrap(),
|
||||
"Start\nGrüße\n".as_bytes()
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||
.unwrap()
|
||||
.map(|f| f.unwrap().file_name())
|
||||
.collect();
|
||||
files.sort();
|
||||
assert_eq!(files, ["app.tbc", "result.txt", "seed.txt"]);
|
||||
}
|
||||
assert!(
|
||||
!dir.exists(),
|
||||
"Releaseprobe hat ihr Arbeitsverzeichnis nicht entfernt"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erlaubte_grafik_und_benannte_nonfeatures_werden_per_cli_geprueft() {
|
||||
let dir = std::env::temp_dir().join(format!("tb-graphics-check-{}", std::process::id()));
|
||||
|
||||
@@ -1050,7 +1050,7 @@ impl Vm {
|
||||
|
||||
/// Polls modeless forms after the module body has ended.
|
||||
pub fn poll_visible_forms(&mut self, host: &mut dyn Host, budget: usize) -> PollResult {
|
||||
if self.terminated {
|
||||
if self.terminated || (self.dialog.is_some() && self.rt.ende) {
|
||||
return PollResult::Event(RunEvent::Ended);
|
||||
}
|
||||
if self
|
||||
|
||||
@@ -392,6 +392,33 @@ fn eof_leert_angenommene_eingaben_bei_modalen_und_modellosen_forms() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_im_dialog_eines_formularhandlers_beendet_die_ereignispumpe() {
|
||||
use tb_vm::interp::PollResult;
|
||||
let mut vm = vm("DIM SHARED s$\nForm1.Show\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = \"vorher\"\nr% = MSGBOX(\"Speichern?\", 4)\ns$ = \"nachher\"\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
host.ereignis(mouse());
|
||||
for _ in 0..100 {
|
||||
match vm.poll_visible_forms(&mut host, 32) {
|
||||
PollResult::Event(RunEvent::Ended) => {
|
||||
assert!(vm.rt.ende);
|
||||
assert_eq!(string(&vm, "s"), "vorher");
|
||||
assert!(tb_runtime::snapshot::text(&vm.rt.screen).contains("Speichern?"));
|
||||
return;
|
||||
}
|
||||
PollResult::Waiting { deadline } => {
|
||||
if let Some(event) = host.warten(deadline) {
|
||||
vm.rt.ereignis(event);
|
||||
}
|
||||
}
|
||||
PollResult::Yield => {}
|
||||
other => panic!("unerwartetes Ergebnis: {other:?}"),
|
||||
}
|
||||
}
|
||||
panic!("Dialog-EOF wurde als Yield verschluckt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu() {
|
||||
struct MenuHost {
|
||||
|
||||
Reference in New Issue
Block a user