//! 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, 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 { 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) -> Option { 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); let dir = base.join(format!("run-{repeat}")); 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" ); }