Implement and archive Phase 5 debugger

This commit is contained in:
2026-09-06 21:24:38 +02:00
parent ce97700a98
commit e236306657
25 changed files with 3102 additions and 52 deletions

View File

@@ -252,7 +252,7 @@ fn dispatcher_keeps_function_keys_and_copy_out_of_basic_input() {
app.execution = Execution::Paused;
plain(&mut app, K::F(10));
assert_eq!(app.last_command, Some(Command::ProcedureStep));
assert!(app.message.contains("06"));
assert_eq!(app.availability(Command::ProcedureStep), None);
menu(&mut app, Command::Output);
app.execution = Execution::Running;
plain(&mut app, K::Char('a'));

View File

@@ -0,0 +1,339 @@
use crossterm::event::{Event, KeyCode as K, KeyEvent, KeyModifiers as M};
use std::{
fs,
path::PathBuf,
sync::atomic::{AtomicUsize, Ordering},
};
use tb_ide::{
app::{App, Execution, Field, WindowKind},
commands::Command,
};
use tb_runtime::value::Value;
struct Temp(PathBuf);
impl Temp {
fn new() -> Self {
static N: AtomicUsize = AtomicUsize::new(0);
let p = std::env::temp_dir().join(format!(
"tb-debug-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&p).unwrap();
Self(p.canonicalize().unwrap())
}
fn app(&self, source: &str) -> App {
let p = self.0.join("main.bas");
fs::write(&p, source).unwrap();
let mut a = App::new(&self.0, self.0.join("config"), (100, 30)).unwrap();
a.load_initial_project(p).unwrap();
a.options.syntax_checking = false;
a
}
}
impl Drop for Temp {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn key(a: &mut App, k: K, m: M) {
a.handle(Event::Key(KeyEvent::new(k, m)));
}
fn tick(a: &mut App) {
for _ in 0..30 {
a.tick_execution(0);
if !matches!(a.execution, Execution::Running | Execution::Waiting) {
break;
}
}
}
fn cursor(a: &mut App, line: usize) {
let (id, v) = a
.windows
.iter()
.find_map(|w| {
if let WindowKind::Code(v) = w.kind {
Some((w.id, v))
} else {
None
}
})
.unwrap();
a.execute(Command::FocusWindow(id));
let doc = a.project.view(v).unwrap().document();
let code = a.project.document(doc).unwrap().code();
let at = code
.split_inclusive('\n')
.take(line - 1)
.map(str::len)
.sum();
a.project.view_mut(v).unwrap().cursor = at;
}
fn watch(a: &mut App, c: Command, text: &str) {
a.execute(c);
a.dialog.as_mut().unwrap().fields[0] = Field::text("BASIC-Ausdruck", text);
key(a, K::Enter, M::NONE);
assert!(a.dialog.is_none(), "{:?}", a.dialog);
}
#[test]
fn dispatcher_marks_edits_rebind_and_readonly_watches() {
let t = Temp::new();
let mut a = t.app("x%=1\nx%=2\nPRINT x%\nEND\n");
cursor(&mut a, 2);
key(&mut a, K::F(9), M::NONE);
let doc = a.active_document().unwrap();
a.project.replace_text(doc, 0..0, "' inserted\n").unwrap();
assert_eq!(
a.project.document(doc).unwrap().breakpoints[0].at,
"' inserted\nx%=1\n".len()
);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
assert!(a.project.document(doc).unwrap().breakpoints[0].bound);
watch(&mut a, Command::AddWatch, "x% + 1");
assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(2))));
let before = a.project.document(doc).unwrap().code().to_string();
key(&mut a, K::Char('z'), M::NONE);
assert_eq!(a.project.document(doc).unwrap().code(), before);
watch(&mut a, Command::InstantWatch, "RND");
assert!(a.debugger.instant.as_ref().unwrap().value.is_err());
a.execute(Command::Immediate);
for c in "x%=7".chars() {
key(&mut a, K::Char(c), M::NONE);
}
key(&mut a, K::Enter, M::NONE);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
assert!(matches!(
a.session.vm.as_ref().unwrap().inspect("x%"),
Some(Value::Int(7))
));
a.execute(Command::DeleteWatches);
assert!(a.debugger.watches.is_empty());
key(&mut a, K::F(8), M::NONE);
tick(&mut a);
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 4);
assert!(matches!(
a.session.vm.as_ref().unwrap().inspect("x%"),
Some(Value::Int(2))
));
a.execute(Command::ClearBreakpoints);
key(&mut a, K::F(5), M::NONE);
tick(&mut a);
assert_eq!(a.execution, Execution::Ended);
a.project.replace_text(doc, 0..0, "' again\n").unwrap();
cursor(&mut a, 1);
key(&mut a, K::F(9), M::NONE);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a);
assert_eq!(a.execution, Execution::Ended);
assert!(!a.project.document(doc).unwrap().breakpoints[0].bound);
}
#[test]
fn calls_procedure_step_cursor_history_and_restart() {
let t = Temp::new();
let mut a=t.app("CALL rec(2)\nx%=1\nx%=2\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n");
a.execute(Command::History);
cursor(&mut a, 6);
key(&mut a, K::F(9), M::NONE);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a);
a.execute(Command::ClearBreakpoints);
let frame = a
.session
.vm
.as_ref()
.unwrap()
.debug_location()
.unwrap()
.frame;
key(&mut a, K::F(10), M::NONE);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused);
assert_eq!(
a.session
.vm
.as_ref()
.unwrap()
.debug_location()
.unwrap()
.frame,
frame
);
cursor(&mut a, 3);
key(&mut a, K::F(7), M::NONE);
tick(&mut a);
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
watch(&mut a, Command::AddWatch, "x%");
let current = a.session.vm.as_ref().unwrap().debug_location();
key(&mut a, K::F(8), M::SHIFT);
key(&mut a, K::F(8), M::SHIFT);
assert!(a.debugger.historical.is_some());
assert!(a.message.contains("HISTORIE"));
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(1))));
key(&mut a, K::F(10), M::SHIFT);
a.execute(Command::NextStatement);
assert!(a.debugger.historical.is_none());
a.execute(Command::Restart);
assert!(a.debugger.frame.is_none());
assert!(a.debugger.watches[0].value.is_err());
assert!(a.session.vm.as_ref().unwrap().debug.history.is_empty());
assert_eq!(a.debugger.watches.len(), 1);
}
#[test]
fn includes_and_deleted_marks_do_not_bind_to_foreign_statements() {
let t = Temp::new();
fs::write(t.0.join("a.bi"), "'a\nx%=1\n").unwrap();
fs::write(t.0.join("b.bi"), "'b\nx%=2\n").unwrap();
let mut a = t.app("'$INCLUDE: 'a.bi'\n'$INCLUDE: 'b.bi'\nEND\n");
cursor(&mut a, 2);
a.execute(Command::IncludedFile);
let doc = a.active_document().unwrap();
let v = match a.active_window().unwrap().kind {
WindowKind::Code(v) => v,
_ => panic!(),
};
a.project.view_mut(v).unwrap().cursor = 3;
key(&mut a, K::F(9), M::NONE);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
let vm = a.session.vm.as_ref().unwrap();
assert!(vm.current_file().ends_with("b.bi"));
assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
a.project.replace_text(doc, 3..8, "").unwrap();
assert!(!a.project.document(doc).unwrap().breakpoints[0].valid);
a.execute(Command::Restart);
tick(&mut a);
assert_eq!(a.execution, Execution::Ended);
assert!(!a.project.document(doc).unwrap().breakpoints[0].bound);
a.project.undo(doc).unwrap();
assert!(a.project.document(doc).unwrap().breakpoints[0].valid);
}
#[test]
fn calls_mouse_inspection_instant_key_delete_and_error_window() {
let t = Temp::new();
let mut a = t.app(
"CALL rec(2)\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n",
);
cursor(&mut a, 5);
key(&mut a, K::F(9), M::NONE);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a);
let current = a.session.vm.as_ref().unwrap().debug_location();
a.execute(Command::Calls);
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(100, 30)).unwrap();
terminal.draw(|f| a.render(f)).unwrap();
let rect = a
.hits
.iter()
.find_map(|(r, h)| matches!(h, tb_ide::app::Hit::DebugFrame(1)).then_some(*r))
.unwrap();
a.handle(Event::Mouse(crossterm::event::MouseEvent {
kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
column: rect.x,
row: rect.y,
modifiers: M::NONE,
}));
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
key(&mut a, K::F(9), M::SHIFT);
a.dialog.as_mut().unwrap().fields[0] = Field::text("Ausdruck", "n");
key(&mut a, K::Enter, M::NONE);
assert!(matches!(
a.debugger.instant.as_ref().unwrap().value,
Ok(Value::Int(1))
));
watch(&mut a, Command::AddWatch, "n+2");
watch(&mut a, Command::AddWatch, "1/0");
assert!(a.debugger.watches[1].value.is_err());
a.execute(Command::DeleteWatch);
key(&mut a, K::Enter, M::NONE);
assert_eq!(a.debugger.watches.len(), 1);
terminal.draw(|f| a.render(f)).unwrap();
let display = terminal
.backend()
.buffer()
.content
.iter()
.map(|c| c.symbol())
.collect::<String>();
assert!(display.contains("FEHLER"));
a.execute(Command::Immediate);
for c in "ERROR 5".chars() {
key(&mut a, K::Char(c), M::NONE);
}
key(&mut a, K::Enter, M::NONE);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused);
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
terminal.draw(|f| a.render(f)).unwrap();
let display = terminal
.backend()
.buffer()
.content
.iter()
.map(|c| c.symbol())
.collect::<String>();
assert!(display.contains("Error"));
}
#[test]
fn form_handler_keys_and_old_revision_cursor_guard() {
let t = Temp::new();
let p = t.0.join("window.frm");
fs::write(&p,"VERSION 1.00\nBEGIN Form Form1\nEND\nSUB Form_Load()\nCALL work\nPRINT 2\nEND SUB\nSUB work()\nPRINT 1\nEND SUB\n").unwrap();
let mut a = App::new(&t.0, t.0.join("config"), (100, 30)).unwrap();
a.load_initial_project(p).unwrap();
a.options.syntax_checking = false;
cursor(&mut a, 2);
key(&mut a, K::F(9), M::NONE);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 5);
key(&mut a, K::F(10), M::NONE);
tick(&mut a);
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 6);
let before = a.session.vm.as_ref().unwrap().debug_location();
let doc = a.active_document().unwrap();
a.project.replace_text(doc, 0..0, "' edit\n").unwrap();
a.execute(Command::SetStatement);
assert!(a.message.contains("Quellstand"));
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), before);
}
#[test]
fn immediate_wait_run_transition_watchpoint_and_trace_are_session_owned() {
let t = Temp::new();
let mut a=t.app("x%=1\nx%=2\nx%=3\nEND\nSUB ask(n AS INTEGER)\nINPUT n\nEND SUB\nSUB restart()\nRUN\nEND SUB\n");
watch(&mut a, Command::Watchpoint, "x%=2");
a.execute(Command::Trace);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
let before = a.session.vm.as_ref().unwrap().debug_location();
a.execute(Command::DeleteWatches);
a.execute(Command::Immediate);
a.submit_immediate("CALL ask(x%)").unwrap();
tick(&mut a);
assert_eq!(a.execution, Execution::Waiting);
a.execute(Command::Pause);
assert_eq!(a.execution, Execution::Paused);
assert!(a.session.vm.as_ref().unwrap().immediate_active());
key(&mut a, K::F(5), M::NONE);
a.execute(Command::Output);
key(&mut a, K::Char('9'), M::NONE);
key(&mut a, K::Enter, M::NONE);
tick(&mut a);
assert_eq!(a.execution, Execution::Paused);
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), before);
assert!(matches!(
a.session.vm.as_ref().unwrap().inspect("x%"),
Some(Value::Int(9))
));
a.submit_immediate("CALL restart()").unwrap();
tick(&mut a);
assert_eq!(a.execution, Execution::Ended);
assert!(a.session.vm.is_none());
}