All checks were successful
Vierziel-Build und Pflichtabnahme / checks (push) Successful in 6m6s
Vierziel-Build und Pflichtabnahme / build (ghcr.io/rust-cross/cargo-zigbuild@sha256:82af75c41958c2af2787e8bedd912da7678a9438937e223e9d83d006d747b38b, aarch64-apple-darwin) (push) Successful in 3m43s
Vierziel-Build und Pflichtabnahme / build (ghcr.io/rust-cross/cargo-zigbuild@sha256:82af75c41958c2af2787e8bedd912da7678a9438937e223e9d83d006d747b38b, aarch64-unknown-linux-gnu) (push) Successful in 3m35s
Vierziel-Build und Pflichtabnahme / build (ghcr.io/rust-cross/cargo-zigbuild@sha256:82af75c41958c2af2787e8bedd912da7678a9438937e223e9d83d006d747b38b, x86_64-unknown-linux-gnu) (push) Successful in 3m35s
Vierziel-Build und Pflichtabnahme / build (messense/cargo-xwin@sha256:4696dd4e79edf8569fa99c4b06bd99273e0501c7adc983aa61d57945f795bef0, x86_64-pc-windows-msvc) (push) Successful in 6m16s
Vierziel-Build und Pflichtabnahme / stage (push) Has been skipped
269 lines
9.5 KiB
Rust
269 lines
9.5 KiB
Rust
//! Optionaler öffentlicher Bestand; kein Netzwerk und keine GUI.
|
|
use std::{
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
time::Duration,
|
|
};
|
|
use tb_runtime::host::{CaptureHost, Ereignis, Host};
|
|
use tb_vm::{
|
|
bytecode::CompiledModule,
|
|
interp::{RunEvent, Vm},
|
|
};
|
|
#[path = "../../../tests/support/prozessfrist.rs"]
|
|
mod prozessfrist;
|
|
|
|
#[derive(Default)]
|
|
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 |= 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 self.dialog_gesehen {
|
|
if let Some(event) = self.dialogantwort.take() {
|
|
return Some(event);
|
|
}
|
|
}
|
|
self.inner.next_event(block)
|
|
}
|
|
fn jetzt_ms(&mut self) -> u64 {
|
|
self.inner.jetzt_ms()
|
|
}
|
|
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
|
|
self.inner.warten(deadline)
|
|
}
|
|
}
|
|
|
|
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 = 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")
|
|
.arg(&repo)
|
|
.args(["archive", "--format=tar", "-o"])
|
|
.arg(&archive)
|
|
.arg(REVISION)
|
|
.status()
|
|
.unwrap()
|
|
.success());
|
|
for (entry, keys) in [
|
|
("graphics/graphics.mak", vec![("x", 4)]),
|
|
("microsoft/check.mak", vec![("f", 4), ("x", 0)]),
|
|
("microsoft/qlbview.mak", vec![("\u{1b}", 0)]),
|
|
("microsoft/seek.mak", vec![("x", 4)]),
|
|
(
|
|
"microsoft/spindemo.mak",
|
|
vec![("\t", 0), ("\t", 0), ("\0H", 0)],
|
|
),
|
|
("microsoft/notepad.frm", vec![("f", 4), ("x", 0)]),
|
|
("misc/mentors/mentors.frm", vec![("f", 4), ("x", 0)]),
|
|
] {
|
|
let mut results = Vec::new();
|
|
for repeat in 0..2 {
|
|
eprintln!("{entry}: Lauf {} kompilieren", repeat + 1);
|
|
// Jeder Lauf startet frisch am selben Pfad: CURDIR$ erscheint
|
|
// bei kurzen Temp-Pfaden im sichtbaren Formulartitel.
|
|
let dir = base.join("run");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
assert!(Command::new("tar")
|
|
.arg("-xf")
|
|
.arg(&archive)
|
|
.arg("-C")
|
|
.arg(&dir)
|
|
.status()
|
|
.unwrap()
|
|
.success());
|
|
let path = dir.join(entry);
|
|
let build = Command::new(env!("CARGO_BIN_EXE_tbc"))
|
|
.arg("build")
|
|
.arg(&path)
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
build.status.success() && build.stderr.is_empty(),
|
|
"{entry}: {build:?}"
|
|
);
|
|
let module =
|
|
CompiledModule::from_tbc(&std::fs::read(path.with_extension("tbc")).unwrap())
|
|
.unwrap();
|
|
let mut vm = Vm::new(module);
|
|
vm.rt.screen.resize(100, 30);
|
|
vm.forms.resize(100, 30);
|
|
std::env::set_current_dir(path.parent().unwrap()).unwrap();
|
|
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),
|
|
RunEvent::Ended,
|
|
"{entry}: initiale Ereignisse"
|
|
);
|
|
assert!(
|
|
vm.forms.has_visible_forms(),
|
|
"{entry}: kein sichtbares Startformular"
|
|
);
|
|
let text1 = vm
|
|
.forms
|
|
.objects
|
|
.iter()
|
|
.position(|o| o.description.name.eq_ignore_ascii_case("Text1"));
|
|
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 host.dialogtext.is_some() && *key == "x" {
|
|
host.dialogantwort = Some(Ereignis::Taste("n".into(), 0));
|
|
}
|
|
host.inner.ereignis(Ereignis::Taste((*key).into(), *shift));
|
|
assert_eq!(
|
|
vm.run_visible_forms(&mut host),
|
|
RunEvent::Ended,
|
|
"{entry}: Taste {key:?}"
|
|
);
|
|
states.push((
|
|
vm.forms.has_visible_forms(),
|
|
vm.forms.menu_is_open(),
|
|
vm.forms.active_form(),
|
|
tb_runtime::snapshot::snapshot(&vm.rt.screen),
|
|
));
|
|
}
|
|
if entry.contains("spindemo") {
|
|
let text_id =
|
|
tb_frontend::forms::property(tb_frontend::forms::ObjectClass::TextBox, "TEXT")
|
|
.unwrap()
|
|
.0;
|
|
assert_eq!(
|
|
vm.forms.get(text1.unwrap() as u16, text_id).unwrap(),
|
|
tb_ui::forms::PropertyValue::String(" 1".into()),
|
|
"{entry}: Spin-Handler hat Text1 nicht aktualisiert"
|
|
);
|
|
assert!(
|
|
states[2].3 != states[1].3,
|
|
"Pfeiltaste muss den sichtbaren Wert ändern"
|
|
);
|
|
} else {
|
|
assert!(
|
|
!vm.forms.has_visible_forms(),
|
|
"{entry}: Exit/Cancel hat das Formular nicht entladen"
|
|
);
|
|
}
|
|
if keys.len() == 2 {
|
|
assert!(
|
|
states[0].1 && !states[1].1,
|
|
"{entry}: Menü öffnen und Exit auswählen"
|
|
);
|
|
}
|
|
if host.dialogtext.is_some() {
|
|
assert!(
|
|
host.dialog_gesehen && host.dialogantwort.is_none(),
|
|
"{entry}: Save-Dialog mit N beantworten"
|
|
);
|
|
}
|
|
assert!(
|
|
states.iter().any(|s| !s.0 || s.3 != before),
|
|
"{entry}: keine sichtbare Zustandsänderung"
|
|
);
|
|
eprintln!("{entry}: Lauf {} bestanden", repeat + 1);
|
|
results.push(states);
|
|
std::env::set_current_dir(&original).unwrap();
|
|
std::fs::remove_dir_all(&dir).unwrap();
|
|
}
|
|
assert_eq!(results[0], results[1], "{entry}: nicht reproduzierbar");
|
|
}
|
|
std::fs::remove_dir_all(&base).unwrap();
|
|
assert!(
|
|
!base.exists(),
|
|
"temporärer Fremdprogrammbestand blieb zurück"
|
|
);
|
|
}
|