Files
TerminalBasic/crates/tb-vm/tests/debugger.rs

421 lines
18 KiB
Rust

use tb_frontend::{forms::FormCatalog, source::SourceUnit};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_vm::{
interp::{RunEvent, Vm},
project::ProjectCompiler,
};
fn vm(source: &str) -> Vm {
let mut compiler = ProjectCompiler::default();
let code = compiler
.compile(
"TEST",
&[SourceUnit::new("TEST", "test.bas", source)],
&FormCatalog::default(),
&[],
)
.unwrap();
let mut vm = Vm::new(code);
vm.debug.enabled = true;
vm.debug.compiler = Some(compiler.debug_compiler());
vm
}
#[test]
fn break_continue_loop_and_statement_steps() {
let mut vm = vm("FOR i% = 1 TO 2\nx% = x% + 1: y% = y% + 1\nNEXT\nEND\n");
let mut host = CaptureHost::default();
assert!(vm.add_source_breakpoint(0, 0, 2));
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
assert!(matches!(vm.inspect("x%"), Some(Value::Int(0))));
vm.set_step(true);
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 });
assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
vm.set_step(false);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
assert!(matches!(vm.inspect("y%"), Some(Value::Int(1))));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
}
#[test]
fn watches_immediate_byref_and_errors_preserve_continuation() {
let mut vm=vm("DIM SHARED x AS INTEGER\nx = 3\nCALL work(x)\nEND\nSUB work(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nn=n+1\nEND SUB\nSUB bump(n AS INTEGER)\nn=n+5\nEND SUB\n");
let mut host = CaptureHost::default();
assert!(vm.add_source_breakpoint(0, 0, 8));
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 8 });
let location = vm.debug_location().unwrap();
let frame = location.frame;
for _ in 0..3 {
assert!(matches!(
vm.evaluate_watch(frame, "a(1) + CINT(2.5)").unwrap(),
Value::Int(5)
));
assert!(vm.evaluate_watch(frame, "RND").is_err());
assert_eq!(vm.debug_location().unwrap(), location);
}
assert!(vm.start_immediate(frame, "n =").is_err());
vm.start_immediate(frame, "n = 10: CALL bump(n): PRINT n")
.unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 });
assert_eq!(vm.debug_location().unwrap(), location);
assert!(matches!(vm.inspect("n"), Some(Value::Int(15))));
vm.start_immediate(frame, "ERROR 5").unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 });
assert!(vm.debug.immediate_error.is_some());
assert_eq!(vm.debug_location().unwrap(), location);
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x"), Some(Value::Int(16))));
}
#[test]
fn immediate_handler_resume_and_break_on_errors() {
let mut vm = vm("ON ERROR GOTO handler\nx%=1\nx%=x%+1\nEND\nhandler:\ny%=ERR\nRESUME NEXT\n");
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 3);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 });
let location = vm.debug_location().unwrap();
vm.debug.break_errors = true;
vm.start_immediate(location.frame, "ERROR 5: x%=10")
.unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
assert_eq!(vm.debug.error.as_ref().unwrap().code, 5);
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 3 });
assert_eq!(vm.debug_location().unwrap(), location);
assert!(matches!(vm.inspect("y%"), Some(Value::Int(5))));
assert!(matches!(vm.inspect("x%"), Some(Value::Int(10))));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(11))));
}
#[test]
fn step_over_recursive_frames_history_and_safe_jump() {
let mut vm=vm("CALL rec(2)\nx%=1\nx%=2\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN\nCALL rec(n-1)\nEND IF\nPRINT n\nEND SUB\n");
let mut host = CaptureHost::default();
vm.debug.history_on = true;
vm.add_source_breakpoint(0, 0, 7);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 7 });
let first = vm.debug_location().unwrap();
vm.clear_source_breakpoints();
vm.step_over();
assert!(matches!(vm.run(&mut host), RunEvent::Stepped { .. }));
assert_eq!(vm.debug_location().unwrap().frame, first.frame);
vm.set_step(false);
vm.run_to_cursor(0, 0, 2).unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
let at = vm.debug_location().unwrap();
vm.set_next_statement(at.frame, 0, 3).unwrap();
let moved = vm.debug_location().unwrap();
assert!(vm.set_next_statement(at.frame, 0, 7).is_err());
assert_eq!(vm.debug_location().unwrap(), moved);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
assert!(!vm.debug.history.is_empty());
}
#[test]
fn include_identity_qualified_globals_udts_and_older_frames() {
use tb_frontend::source::SourceSegment;
let mut compiler = ProjectCompiler::default();
let first=SourceUnit{name:"FIRST".into(),segments:vec![SourceSegment{file:"one.bi".into(),first_line:1,text:"'one\nx%=1\n".into()},SourceSegment{file:"two.bi".into(),first_line:1,text:"'two\nx%=2\nCALL rec(2)\nEND\nSUB rec(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n".into()}]};
let second=SourceUnit::new("SECOND","second.bas","TYPE pair\nvalue AS INTEGER\nEND TYPE\nDIM SHARED r AS pair\nDIM SHARED a(1 TO 2) AS INTEGER\n");
let code = compiler
.compile("P", &[first, second], &FormCatalog::default(), &[])
.unwrap();
let mut vm = Vm::new(code);
vm.debug.compiler = Some(compiler.debug_compiler());
vm.add_source_breakpoint(0, 1, 2);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
assert_eq!(vm.current_file(), "two.bi");
assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
vm.clear_source_breakpoints();
vm.add_source_breakpoint(0, 1, 9);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 9 });
let frames = vm.debug_frames();
assert!(frames.len() >= 4);
let top = vm.debug_location();
assert!(matches!(
vm.evaluate_watch(frames[0].id, "a(1)").unwrap(),
Value::Int(0)
));
assert!(matches!(
vm.evaluate_watch(frames[1].id, "a(1)").unwrap(),
Value::Int(1)
));
assert!(matches!(
vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)")
.unwrap(),
Value::Int(0)
));
assert_eq!(vm.debug_location(), top);
vm.start_immediate(frames[1].id, "SECOND!r.value=7: SECOND!a(1)=8")
.unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(
vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)")
.unwrap(),
Value::Int(15)
));
}
#[test]
fn watchpoint_history_ring_and_error_differential() {
use tb_vm::interp::DebugWatch;
let mut vm = vm("FOR i%=1 TO 1500\nx%=i%\nNEXT\nEND\n");
vm.debug.enabled = true;
vm.debug.history_on = true;
vm.debug.watches.push(DebugWatch {
expression: "x%=2".into(),
condition: true,
frame: None,
value: Err("pending".into()),
});
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 });
assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
vm.debug.watches.clear();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(vm.debug.history.len(), 1024);
let source="10 ON ERROR GOTO handler\n20 ERROR 6\n30 x%=x%+1\n40 END\nhandler:\ne%=ERR: l&=ERL\nRESUME NEXT\n";
let run = |break_errors| {
let mut vm = self::vm(source);
vm.debug.break_errors = break_errors;
let mut host = CaptureHost::default();
if break_errors {
assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
assert_eq!(vm.debug.error.as_ref().unwrap().code, 6);
assert!(matches!(vm.inspect("e%"), Some(Value::Int(0))));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
format!(
"{:?}{:?}{:?}",
vm.inspect("e%"),
vm.inspect("l&"),
vm.inspect("x%")
)
};
assert_eq!(run(true), run(false));
}
#[test]
fn gosub_step_over_breakpoint_priority_and_temporary_target_cleanup() {
let mut vm = vm("GOSUB work\nx%=4\nEND\nwork:\nx%=1\nx%=2\nRETURN\n");
let mut host = CaptureHost::default();
vm.set_step(true);
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 1 });
vm.step_over();
vm.add_source_breakpoint(0, 0, 6);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 6 });
vm.clear_source_breakpoints();
vm.set_step(false);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
let mut vm = self::vm("FOR i%=1 TO 2\nx%=x%+1\ny%=y%+1\nNEXT\nEND\n");
vm.add_source_breakpoint(0, 0, 2);
vm.run_to_cursor(0, 0, 3).unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn immediate_wait_interrupt_nested_error_end_and_run() {
use tb_vm::interp::PollResult;
let source="x%=1\nx%=x%+1\nEND\nSUB ask(n AS INTEGER)\nINPUT n\nEND SUB\nSUB fail()\nON LOCAL ERROR GOTO handler\nERROR 6\nEXIT SUB\nhandler:\nx%=ERR\nRESUME NEXT\nEND SUB\nSUB done()\nEND\nEND SUB\nSUB restart()\nRUN\nEND SUB\n";
let mut vm = vm(source);
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 2);
vm.run(&mut host);
let location = vm.debug_location().unwrap();
vm.start_immediate(location.frame, "CALL ask(x%)").unwrap();
assert!(matches!(
vm.poll(&mut host, 1000),
PollResult::Waiting { .. }
));
assert!(vm.immediate_active());
vm.rt.abbruch = true;
assert!(matches!(
vm.poll(&mut host, 1000),
PollResult::Event(RunEvent::Interrupted { .. })
));
vm.rt.abbruch = false;
host.tippe("9\r");
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert_eq!(vm.debug_location().unwrap(), location);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(9))));
vm.start_immediate(location.frame, "CALL fail()").unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert_eq!(vm.debug_location().unwrap(), location);
vm.start_immediate(location.frame, "CALL restart()")
.unwrap();
assert_eq!(
vm.run(&mut host),
RunEvent::Restart {
program: None,
line: None
}
);
let mut vm = self::vm(source);
vm.add_source_breakpoint(0, 0, 2);
vm.run(&mut host);
let frame = vm.debug_location().unwrap().frame;
vm.start_immediate(frame, "CALL done()").unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(vm.is_terminated());
}
#[test]
fn form_event_breakpoint_and_step_over_keep_the_event_frame() {
use tb_frontend::forms::ObjectClass;
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
let source="Form1.Show\nx%=1\nEND\nSUB Form_Load()\nCALL bump\nPRINT 4\nEND SUB\nSUB bump()\nPRINT 3\nEND SUB\n";
let mut compiler = ProjectCompiler::default();
let code = compiler
.compile(
"Form1",
&[SourceUnit::new("Form1", "form1.frm", source)],
&catalog,
&[],
)
.unwrap();
let mut vm = Vm::new(code);
vm.debug.compiler = Some(compiler.debug_compiler());
vm.add_source_breakpoint(0, 0, 5);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 5 });
let frame = vm.debug_location().unwrap().frame;
vm.clear_source_breakpoints();
vm.step_over();
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 6 });
assert_eq!(vm.debug_location().unwrap().frame, frame);
vm.add_source_breakpoint(0, 0, 2);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn set_next_rejects_loop_gosub_handler_and_pending_operations_atomically() {
let source = "x%=1\nFOR i%=1 TO 2\nx%=x%+1\nNEXT\nGOSUB worker\nEND\nworker:\nx%=3\nRETURN\n";
let mut vm = vm(source);
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 1);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
for line in [3, 4, 8] {
assert!(
vm.set_next_statement(before.frame, 0, line).is_err(),
"line {line}"
);
assert_eq!(vm.debug_location().unwrap(), before);
}
vm.clear_source_breakpoints();
vm.add_source_breakpoint(0, 0, 8);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
assert!(vm.set_next_statement(before.frame, 0, 6).is_err());
assert_eq!(vm.debug_location().unwrap(), before);
}
#[test]
fn same_line_calls_and_immediate_do_not_retrigger_a_line_breakpoint() {
let mut vm = vm("x%=1: CALL bump(x%): x%=x%+1\nEND\nSUB bump(n AS INTEGER)\nn=n+1\nEND SUB\n");
vm.debug.history_on = true;
vm.add_source_breakpoint(0, 0, 1);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 1 });
assert!(vm.debug.history.is_empty());
let frame = vm.debug_location().unwrap().frame;
vm.start_immediate(frame, "x%=5").unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 1 });
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(3))));
assert_eq!(
vm.debug
.history
.iter()
.filter(|p| p.procedure == 0 && p.line == 1)
.count(),
3
);
}
#[test]
fn immediate_handler_exit_and_cached_new_imports_are_safe() {
let mut compiler = ProjectCompiler::default();
compiler.debug_symbols = true;
let first = SourceUnit::new("ONE", "one.bas", "x%=1\nSTOP\nx%=x%+1\nEND\n");
let old = SourceUnit::new("TWO", "two.bas", "SUB oldproc()\nPRINT 1\nEND SUB\n");
compiler
.compile("P", &[first.clone(), old], &FormCatalog::default(), &[])
.unwrap();
let second = SourceUnit::new(
"TWO",
"two.bas",
"SUB newproc(n AS INTEGER)\nn=n+5\nEND SUB\n",
);
let code = compiler
.compile("P", &[first, second], &FormCatalog::default(), &[])
.unwrap();
let mut vm = Vm::new(code);
vm.debug.compiler = Some(compiler.debug_compiler());
let mut host = CaptureHost::default();
vm.run(&mut host);
let frame = vm.debug_location().unwrap().frame;
assert_eq!(vm.execution_location().unwrap().line, 3);
vm.start_immediate(frame, "CALL newproc(x%)").unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(vm.inspect("ONE!x%"), Some(Value::Int(6))));
let mut vm=self::vm("CALL work\nEND\nSUB work()\nON LOCAL ERROR GOTO handler\nx%=1\nx%=2\nEXIT SUB\nhandler:\nx%=7\nEND SUB\n");
vm.add_source_breakpoint(0, 0, 6);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
vm.start_immediate(before.frame, "ERROR 5").unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert_eq!(vm.debug_location().unwrap(), before);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(7))));
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn set_next_rejects_else_and_case_entry_but_allows_peer_statements() {
let source="x%=1\nIF x% THEN\nx%=2\nELSE\nx%=3\nEND IF\nSELECT CASE x%\nCASE 1\nx%=4\nCASE ELSE\nx%=5\nEND SELECT\nx%=6\nEND\n";
let mut vm = vm(source);
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 1);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
for line in [3, 5, 9, 11] {
assert!(
vm.set_next_statement(before.frame, 0, line).is_err(),
"line {line}"
);
assert_eq!(vm.debug_location().unwrap(), before);
}
vm.set_next_statement(before.frame, 0, 13).unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(6))));
}
#[test]
fn watches_never_initialize_arrays_or_run_user_functions_and_keep_selected_context() {
let mut vm=vm("DIM SHARED changed%\nSTOP\nx%=a%(2)\nCALL rec(2)\nEND\nFUNCTION impure%()\nchanged%=changed%+1\nimpure%=changed%\nEND FUNCTION\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n");
let mut host = CaptureHost::default();
vm.run(&mut host);
let frame = vm.debug_location().unwrap().frame;
let frames = format!("{:?}", vm.debug_frames());
for _ in 0..4 {
assert!(vm.evaluate_watch(frame, "a%(2)").is_err());
assert!(matches!(vm.inspect("a%"), Some(Value::Empty)));
assert!(vm.evaluate_watch(frame, "impure%").is_err());
assert!(matches!(vm.inspect("changed%"), Some(Value::Int(0))));
assert!(vm.evaluate_watch(frame, "1 / 0").is_err());
assert!(matches!(
vm.evaluate_watch(frame, "ERR").unwrap(),
Value::Lng(0)
));
}
assert_eq!(format!("{:?}", vm.debug_frames()), frames);
vm.add_source_breakpoint(0, 0, 12);
vm.run(&mut host);
let frames = vm.debug_frames();
let older = frames[1].id;
vm.clear_source_breakpoints();
vm.debug.watches.push(tb_vm::interp::DebugWatch {
expression: "n=1".into(),
condition: true,
frame: Some(older),
value: Err("pending".into()),
});
assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
assert!(matches!(vm.debug.watches[0].value, Ok(Value::Int(-1))));
}