Implement and archive Phase 5 debugger
This commit is contained in:
339
crates/tb-ide/tests/debugger.rs
Normal file
339
crates/tb-ide/tests/debugger.rs
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user