Spezifikationsabgleich und Regressionsnachweise abschließen

This commit is contained in:
2026-09-06 10:31:16 +02:00
parent c8b92f0619
commit 024336e29c
39 changed files with 2485 additions and 120 deletions

View File

@@ -632,6 +632,37 @@ impl CompiledModule {
}
}
}
// PARENT overrides carry design-array indices in the existing TBC4 property format.
let mut parents: std::collections::HashMap<_, _> = self
.objects
.iter()
.enumerate()
.map(|(id, object)| ((id as u16, 0), object.parent.map(|id| (id, 0))))
.collect();
for initial in &self.form_initial {
let object = &self.objects[initial.object as usize];
let parent = tb_frontend::forms::property(object.class, "PARENT")
.and_then(|(id, _)| initial.properties.get(&id));
parents.insert(
(initial.object, initial.index),
match parent {
Some(PropertyValue::Object(parent)) => {
parent.map(|(id, index)| (id, index.unwrap_or(0)))
}
_ => parents[&(initial.object, 0)],
},
);
}
for key in parents.keys() {
let mut seen = std::collections::HashSet::new();
let mut current = Some(*key);
while let Some(key) = current {
if !seen.insert(key) {
return Err(bad());
}
current = *parents.get(&key).ok_or_else(bad)?;
}
}
for p in &self.procs {
if p.module as usize >= self.modules.len()
|| p.n_params as usize != p.params.len()

View File

@@ -546,7 +546,19 @@ impl Vm {
}
fn dispatch_form_event(&mut self, event: FormEvent, on_return: FormEventReturn) -> bool {
if self
let Some(binding) = self
.module
.event_procs
.iter()
.find(|e| e.object == event.object && e.event.eq_ignore_ascii_case(&event.name))
.cloned()
else {
return false;
};
if matches!(
event.name.as_str(),
"CLICK" | "DBLCLICK" | "GOTFOCUS" | "KEYDOWN" | "KEYPRESS" | "KEYUP" | "MOUSEDOWN"
) && self
.module
.objects
.get(event.object as usize)
@@ -561,15 +573,6 @@ impl Vm {
.forms
.set_active_control(event.object, event.array_index);
}
let Some(binding) = self
.module
.event_procs
.iter()
.find(|e| e.object == event.object && e.event.eq_ignore_ascii_case(&event.name))
.cloned()
else {
return false;
};
if let Some(index) = event.array_index {
self.push(Value::Int(index as i16));
}

View File

@@ -626,3 +626,284 @@ fn intervallaenderung_im_handler_verwirft_bereits_faellige_altintervalle() {
assert_eq!(integer(&vm, "n"), 2);
assert_eq!(vm.forms.next_deadline(), None);
}
#[test]
fn formular_tastatur_fallback_fuehrt_alle_drei_basic_handler_aus() {
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
for (input, expected) in [("a", "DPU"), ("\0;", "DU")] {
let src = "DIM SHARED s$\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"D\"\nEND SUB\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + \"P\"\nEND SUB\nSUB Form_KeyUp(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"U\"\nEND SUB";
let mut vm = Vm::new(tb_vm::compile_source_with_forms("FORM1", src, &catalog).unwrap());
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key(input));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(
string(&vm, "s"),
expected,
"FORM_KEYDOWN/KEYPRESS/KEYUP: Status implementiert, Hosttaste {input:?}"
);
}
}
#[test]
fn registrierte_form_ereignisse_erreichen_basic_aus_realen_quellen() {
use tb_frontend::forms::{self, EventParamType};
for name in forms::events(ObjectClass::Form) {
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false);
let args = forms::event_params(name)
.unwrap()
.iter()
.map(|(n, t)| {
format!(
"{n} AS {}",
match t {
EventParamType::Integer => "INTEGER",
EventParamType::Single => "SINGLE",
EventParamType::Control => "CONTROL",
}
)
})
.collect::<Vec<_>>()
.join(", ");
let operation = match *name {
"LOAD" | "GOTFOCUS" => "Form1.Show",
"LOSTFOCUS" => "Form1.Hide",
"UNLOAD" => "Form1.Unload",
"PAINT" => "Form1.Refresh",
"RESIZE" => "Form1.Width = 70",
"DRAGOVER" | "DRAGDROP" => "Source.Drag 1",
"KEYDOWN" | "KEYPRESS" | "KEYUP" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "CLICK"
| "DBLCLICK" => "",
other => panic!("FORM_{other}: Status implementiert, Auslöser fehlt"),
};
let setup = if matches!(*name, "LOAD" | "GOTFOCUS") {
""
} else {
"Form1.Show"
};
let src = format!("DIM SHARED n%\nSource.Left=2\nSource.Top=2\n{setup}\nn%=0\nSTOP\n{operation}\nDOEVENTS\nEND\nSUB Form_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB");
let mut vm = Vm::new(
tb_vm::compile_source_with_forms("FORM1", &src, &catalog)
.unwrap_or_else(|d| panic!("{name}: {d:?}")),
);
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
if name.starts_with("KEY") {
host.ereignis(key("a"));
}
let events: &[MausArt] = match *name {
"MOUSEDOWN" => &[MausArt::Druck],
"MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung],
"MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen],
"CLICK" => &[MausArt::Druck, MausArt::Loslassen],
"DBLCLICK" => &[
MausArt::Druck,
MausArt::Loslassen,
MausArt::Druck,
MausArt::Loslassen,
],
_ => &[],
};
for art in events {
host.ereignis(Ereignis::Maus(MausEreignis {
art: *art,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
}));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended, "FORM_{name}");
event_nachgewiesen(&format!("FORM_{name}"), integer(&vm, "n")).unwrap();
}
}
#[test]
fn registrierte_control_ereignisse_erreichen_basic_aus_realen_quellen() {
use tb_frontend::forms::{self, EventParamType};
let mut failures = Vec::new();
for class in ObjectClass::ALL
.iter()
.copied()
.filter(|c| *c != ObjectClass::Form)
{
for name in forms::events(class) {
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
if class == ObjectClass::Menu {
catalog.add("Bar", ObjectClass::Menu, Some("Form1"), false);
}
catalog.add(
"Probe",
class,
Some(if class == ObjectClass::Menu {
"Bar"
} else {
"Form1"
}),
false,
);
catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false);
let args = forms::event_params(name)
.unwrap()
.iter()
.map(|(n, t)| {
format!(
"{n} AS {}",
match t {
EventParamType::Integer => "INTEGER",
EventParamType::Single => "SINGLE",
EventParamType::Control => "CONTROL",
}
)
})
.collect::<Vec<_>>()
.join(", ");
let mut setup = String::from(
"Form1.Width=60\nForm1.Height=15\nForm1.Show\nSource.Left=40\nSource.Top=2\n",
);
if !matches!(class, ObjectClass::Timer | ObjectClass::Menu) {
setup.push_str("Probe.Left=10\nProbe.Top=4\n");
}
let operation = match *name {
"GOTFOCUS" => String::new(),
"LOSTFOCUS" => "Source.SetFocus".into(),
"PAINT" => "Probe.Refresh".into(),
"DRAGOVER" | "DRAGDROP" => "Source.Drag 1".into(),
"CHANGE" => match class {
ObjectClass::Label => "Probe.Caption=\"changed\"".into(),
ObjectClass::HScrollBar | ObjectClass::VScrollBar => "Probe.Value=1".into(),
ObjectClass::DirListBox => {
format!("Probe.Path=\"{}\"", std::env::temp_dir().display())
}
ObjectClass::DriveListBox => "Probe.Drive=\"/\"".into(),
_ => "Probe.Text=\"changed\"".into(),
},
"PATHCHANGE" => format!("Probe.Path=\"{}\"", std::env::temp_dir().display()),
"PATTERNCHANGE" => "Probe.Pattern=\"*.bas\"".into(),
"TIMER" => "Probe.Interval=1\nProbe.Enabled=-1".into(),
"CLICK" if class == ObjectClass::Menu => {
setup.push_str("Probe.Caption=\"&Go\"\nProbe.Shortcut=\"F3\"\n");
String::new()
}
"CLICK" | "DBLCLICK" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "KEYDOWN"
| "KEYPRESS" | "KEYUP" | "DROPDOWN" | "CUSTOM" => String::new(),
other => panic!(
"{}_{other}: Status implementiert, Auslöser fehlt",
class.name()
),
};
let src=format!("DIM SHARED n%\n{setup}\nn%=0\nSTOP\n{operation}\nSTOP\nDOEVENTS\nEND\nSUB Probe_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB");
let mut vm = Vm::new(
tb_vm::compile_source_with_forms("FORM1", &src, &catalog)
.unwrap_or_else(|d| panic!("{}_{name}: {d:?}", class.name())),
);
let mut host = CaptureHost::default();
let initial = vm.run(&mut host);
assert!(
matches!(initial, RunEvent::Stopped { .. }),
"{}_{name}: {initial:?}",
class.name()
);
if *name == "LOSTFOCUS" {
vm.forms.focus((1, None)).unwrap();
}
let second = vm.run(&mut host);
assert!(
matches!(second, RunEvent::Stopped { .. }),
"{}_{name}: {second:?}",
class.name()
);
if name.starts_with("KEY") || matches!(*name, "DROPDOWN" | "CUSTOM") {
vm.forms.focus((1, None)).unwrap();
}
if *name == "TIMER" {
host.uhr_vorruecken(2);
}
if name.starts_with("KEY") {
host.ereignis(key("a"));
}
if *name == "DROPDOWN" {
host.ereignis(Ereignis::Taste("\0P".into(), 4));
}
if *name == "CUSTOM" {
host.ereignis(key("\0H"));
}
if class == ObjectClass::Menu {
host.ereignis(key("\0="));
}
let events: &[MausArt] = match *name {
"MOUSEDOWN" | "GOTFOCUS" => &[MausArt::Druck],
"MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung],
"MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen],
"CLICK" if class != ObjectClass::Menu => &[MausArt::Druck, MausArt::Loslassen],
"DBLCLICK" => &[
MausArt::Druck,
MausArt::Loslassen,
MausArt::Druck,
MausArt::Loslassen,
],
_ => &[],
};
for art in events {
host.ereignis(Ereignis::Maus(MausEreignis {
art: *art,
taste: 1,
shift: 0,
zeile: 5,
spalte: 11,
}));
}
let result = vm.run(&mut host);
if result != RunEvent::Ended || integer(&vm, "n") == 0 {
failures.push(format!(
"{}_{name}: Status implementiert, Handler unerreicht: {result:?}",
class.name()
));
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn shell_und_irr_funktionssignaturen_erreichen_die_runtime() {
let mut vm = vm_for_source(
"DIM a(1) AS DOUBLE\na(0)=-100\na(1)=110\nr#=IRR#(a,.1)\ns&=SHELL(\"exit 7\")\nEND",
);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert!(matches!(vm.inspect("r"),Some(Value::Dbl(n)) if (n-0.1).abs()<1e-8));
assert!(matches!(vm.inspect("s"), Some(Value::Lng(7))));
}
fn event_nachgewiesen(name: &str, count: i16) -> Result<(), String> {
if count > 0 {
Ok(())
} else {
Err(format!(
"{name}: Status implementiert, fehlender Auslösepfad zum BASIC-Handler"
))
}
}
#[test]
fn fehlender_ereignispfad_wird_namentlich_erkannt() {
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
let mut module=tb_vm::compile_source_with_forms("FORM1","DIM SHARED n%\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED n%\nn%=n%+1\nEND SUB",&catalog).unwrap();
module.event_procs.clear(); // künstlich fehlende Dispatch-Bindung
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("a"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
let error = event_nachgewiesen("FORM_KEYPRESS", integer(&vm, "n")).unwrap_err();
assert!(
error.contains("FORM_KEYPRESS")
&& error.contains("implementiert")
&& error.contains("Auslösepfad")
);
}

View File

@@ -249,7 +249,7 @@ fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() {
let mut duplicate = bytes.clone();
duplicate[24..28].copy_from_slice(b"MODN");
assert!(CompiledModule::from_tbc(&duplicate).is_err());
for mutation in 0..8 {
for mutation in 0..10 {
let mut bad = CompiledModule::from_tbc(&bytes).unwrap();
match mutation {
0 => bad.sources[0].module = u16::MAX,
@@ -265,6 +265,23 @@ fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() {
);
}
7 => bad.procs[0].code.push(Instr::Source(u32::MAX, 1)),
8 | 9 => {
let initial = &mut bad.form_initial[1];
let parent = tb_frontend::forms::property(
tb_frontend::forms::ObjectClass::TextBox,
"PARENT",
)
.unwrap()
.0;
initial.properties.insert(
parent,
tb_ui::forms::PropertyValue::Object(Some(if mutation == 8 {
(initial.object, Some(initial.index))
} else {
(0, Some(99))
})),
);
}
_ => unreachable!(),
}
assert!(