Phase 4: Formularmodell und Objektsprache
This commit is contained in:
@@ -99,7 +99,10 @@ fn gemischte_arithmetik() {
|
||||
|
||||
#[test]
|
||||
fn logik_bitweise() {
|
||||
assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n");
|
||||
assert_eq!(
|
||||
out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"),
|
||||
" 2 7 5 -1 \n"
|
||||
);
|
||||
// Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
|
||||
assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
|
||||
}
|
||||
@@ -122,12 +125,18 @@ fn stringvergleich_und_verkettung() {
|
||||
#[test]
|
||||
fn for_ohne_durchlauf() {
|
||||
// Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
|
||||
assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n");
|
||||
assert_eq!(
|
||||
out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""),
|
||||
"ende\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_mit_negativem_step() {
|
||||
assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n");
|
||||
assert_eq!(
|
||||
out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"),
|
||||
" 3 2 1 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -172,11 +181,13 @@ fn exit_for_und_do() {
|
||||
fn kontrollfluss_korpusdatei() {
|
||||
// 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
|
||||
let src = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"),
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../tests/compat/kontrollfluss.bas"),
|
||||
)
|
||||
.unwrap();
|
||||
let want = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"),
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../tests/compat/kontrollfluss.out"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out(&src), want);
|
||||
@@ -268,7 +279,8 @@ fn data_read_restore() {
|
||||
// Anmerkung: unquotierte DATA-Texte verlieren derzeit die
|
||||
// Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
|
||||
// Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
|
||||
let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
|
||||
let src =
|
||||
"DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
|
||||
assert_eq!(out(src), " 1 2.5 hallo\n 1 \n");
|
||||
}
|
||||
|
||||
@@ -444,10 +456,7 @@ fn end_und_system() {
|
||||
|
||||
#[test]
|
||||
fn input_mit_redo() {
|
||||
let (ev, output) = run_with_input(
|
||||
"INPUT \"Zahl\"; n%\nPRINT n% * 2",
|
||||
&["abc", "21"],
|
||||
);
|
||||
let (ev, output) = run_with_input("INPUT \"Zahl\"; n%\nPRINT n% * 2", &["abc", "21"]);
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert!(output.contains("Redo from start"));
|
||||
assert!(output.ends_with(" 42 \n"), "{output}");
|
||||
@@ -464,7 +473,10 @@ fn line_input_liest_ganze_zeile() {
|
||||
|
||||
#[test]
|
||||
fn mid_anweisung_mutiert() {
|
||||
assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n");
|
||||
assert_eq!(
|
||||
out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"),
|
||||
"hEYlo\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -495,12 +507,10 @@ fn unsupported_feature_fehler_73() {
|
||||
#[test]
|
||||
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
|
||||
let _dir = TempVerzeichnis::neu("open_isam");
|
||||
let (ev, ausgabe) = run(
|
||||
"TYPE T\n f AS INTEGER\nEND TYPE\n\
|
||||
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\
|
||||
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
|
||||
PRINT \"offen\"\n\
|
||||
CLOSE #1",
|
||||
);
|
||||
CLOSE #1");
|
||||
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
|
||||
assert!(ausgabe.contains("offen"), "{ausgabe}");
|
||||
}
|
||||
@@ -552,7 +562,10 @@ fn groessenaenderung_wirkt_zur_laufzeit() {
|
||||
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nFOR i% = 1 TO 50\nNEXT i%\n").unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
|
||||
host.ereignis(Ereignis::Groesse {
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
});
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
|
||||
// Inhalt bleibt oben links erhalten.
|
||||
@@ -639,10 +652,7 @@ fn cls_setzt_cursor_zurueck() {
|
||||
|
||||
#[test]
|
||||
fn width_hebt_die_groesse_an() {
|
||||
assert_eq!(
|
||||
out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"),
|
||||
" 32 \n"
|
||||
);
|
||||
assert_eq!(out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"), " 32 \n");
|
||||
}
|
||||
|
||||
// ---- ON ERROR-Scoping (Spec sprach-frontend) -------------------------------
|
||||
@@ -736,8 +746,14 @@ fn time_und_now_stimmen_ueberein() {
|
||||
#[test]
|
||||
fn fester_versatz_verschiebt_die_uhr() {
|
||||
let src = "PRINT INT(TIMER)";
|
||||
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt).trim().parse().unwrap();
|
||||
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600)).trim().parse().unwrap();
|
||||
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt)
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap();
|
||||
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600))
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap();
|
||||
// Modulo Tageslänge, damit ein Mitternachtsübergang nichts kaputt macht.
|
||||
let diff = (mit - ohne).rem_euclid(86_400.0);
|
||||
assert!((diff - 3600.0).abs() <= 2.0, "Differenz {diff}");
|
||||
@@ -746,7 +762,10 @@ fn fester_versatz_verschiebt_die_uhr() {
|
||||
/// `TIMER` zählt ab der lokalen Mitternacht, nicht ab der UTC-Mitternacht.
|
||||
#[test]
|
||||
fn timer_zaehlt_ab_lokaler_mitternacht() {
|
||||
let t = out_mit_zone("PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))", Zeitzone::Fest(-18000));
|
||||
let t = out_mit_zone(
|
||||
"PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))",
|
||||
Zeitzone::Fest(-18000),
|
||||
);
|
||||
let zahlen: Vec<f64> = t
|
||||
.split_whitespace()
|
||||
.filter_map(|w| w.parse().ok())
|
||||
@@ -1093,3 +1112,169 @@ fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
|
||||
110 RESUME NEXT\n");
|
||||
assert_eq!(out.trim(), "20");
|
||||
}
|
||||
|
||||
fn forms_catalog() -> tb_frontend::forms::FormCatalog {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
let mut c = tb_frontend::forms::FormCatalog::default();
|
||||
c.add("Form1", ObjectClass::Form, None, false);
|
||||
c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
|
||||
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
|
||||
c
|
||||
}
|
||||
|
||||
fn form_vm(src: &str) -> Vm {
|
||||
let module = tb_vm::compile_source_with_forms("FORM1", src, &forms_catalog())
|
||||
.unwrap_or_else(|d| panic!("Compile-Fehler: {d:?}"));
|
||||
Vm::new(module)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
|
||||
let mut vm = form_vm("Form1!Text1.Text = \"hallo\"\nPRINT Form1!Text1.Text");
|
||||
let ev = vm.run(&mut CaptureHost::default());
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
|
||||
let mut vm = form_vm(
|
||||
"LOAD Command1(3)\nCommand1(3).Caption = \"drei\"\nPRINT Command1(3).Caption\n\
|
||||
CALL Aus(Text1)\nPRINT Text1.Visible\nCALL FormAus(Form1)\nPRINT Form1.Caption\nEND\n\
|
||||
SUB Aus(c AS CONTROL)\nPRINT TYPEOF c IS TextBox\nc.Visible = 0\nEND SUB\n\
|
||||
SUB FormAus(f AS FORM)\nPRINT TYPEOF f IS Form\nf.Caption = \"Formular\"\nEND SUB",
|
||||
);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen).trim(),
|
||||
"drei\n-1 \n 0 \n-1 \nFormular"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modales_show_setzt_nach_unload_fort() {
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\n\
|
||||
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
|
||||
SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
|
||||
);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis_nach(
|
||||
2,
|
||||
tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
|
||||
art: tb_runtime::host::MausArt::Druck,
|
||||
taste: 1,
|
||||
shift: 0,
|
||||
zeile: 1,
|
||||
spalte: 1,
|
||||
}),
|
||||
);
|
||||
let ev = vm.run(&mut host);
|
||||
assert_eq!(ev, RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "1");
|
||||
assert!(host.presents >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
|
||||
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
|
||||
vm.queue_form_event(tb_ui::forms::FormEvent {
|
||||
object: 0,
|
||||
array_index: None,
|
||||
name: "CLICK".into(),
|
||||
args: vec![],
|
||||
});
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "ja");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mausargumente_kommen_in_zellen_an() {
|
||||
use tb_runtime::host::{Ereignis, MausArt, MausEreignis};
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED b%, s%, gx!, gy!\nForm1.Show\nSTOP\nDOEVENTS\nEND\n\
|
||||
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
|
||||
SHARED b%, s%, gx!, gy!\nb% = Button: s% = Shift: gx! = X: gy! = Y\nEND SUB",
|
||||
);
|
||||
let mut host = CaptureHost::default();
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
host.ereignis(Ereignis::Maus(MausEreignis {
|
||||
art: MausArt::Druck,
|
||||
taste: 1,
|
||||
shift: 2,
|
||||
zeile: 5,
|
||||
spalte: 7,
|
||||
}));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("b"), Some(Value::Int(1))));
|
||||
assert!(matches!(vm.inspect("s"), Some(Value::Int(2))));
|
||||
assert!(matches!(vm.inspect("gx"), Some(Value::Sng(7.0))));
|
||||
assert!(matches!(vm.inspect("gy"), Some(Value::Sng(5.0))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_load_laeuft_vor_dem_sichtbarsetzen() {
|
||||
let mut vm = form_vm(
|
||||
"Form1.Show\nEND\n\
|
||||
SUB Form_Load()\nPRINT Form1.Visible\nEND SUB",
|
||||
);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steuerarray_ereignis_stellt_index_voran() {
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED seen%\nSTOP\nDOEVENTS\nEND\n\
|
||||
SUB Command1_Click(Index AS INTEGER)\nSHARED seen%\nseen% = Index\nEND SUB",
|
||||
);
|
||||
let mut host = CaptureHost::default();
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
vm.forms.load_array(2, 3).unwrap();
|
||||
vm.queue_form_event(tb_ui::forms::FormEvent {
|
||||
object: 2,
|
||||
array_index: Some(3),
|
||||
name: "CLICK".into(),
|
||||
args: vec![],
|
||||
});
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("seen"), Some(Value::Int(3))));
|
||||
let active =
|
||||
tb_frontend::forms::property(tb_frontend::forms::ObjectClass::Screen, "ACTIVECONTROL")
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(
|
||||
vm.forms.get(3, active).unwrap(),
|
||||
tb_ui::forms::PropertyValue::Object(Some((2, Some(3))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_load_laeuft_einmal_und_unload_cancel_verhindert_entladen() {
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED loads%\nx$ = Form1.Caption\nForm1.Hide\nx$ = Form1.Caption\n\
|
||||
Form1.Show\nUNLOAD Form1\nEND\n\
|
||||
SUB Form_Load()\nSHARED loads%\nloads% = loads% + 1\nEND SUB\n\
|
||||
SUB Form_Unload(Cancel AS INTEGER)\nCancel = 1\nEND SUB",
|
||||
);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("loads"), Some(Value::Int(1))));
|
||||
assert!(vm.forms.objects[0].loaded);
|
||||
assert!(vm.forms.objects[0].visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
|
||||
let module = tb_vm::compile_source_with_forms(
|
||||
"FORM1",
|
||||
"Form1.Caption = \"x\"\nDOEVENTS\nx$ = Form1.Caption",
|
||||
&forms_catalog(),
|
||||
)
|
||||
.unwrap();
|
||||
let n = module.procs[0]
|
||||
.code
|
||||
.iter()
|
||||
.filter(|i| matches!(i, tb_vm::bytecode::Instr::Doevents))
|
||||
.count();
|
||||
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user