340 lines
12 KiB
Rust
340 lines
12 KiB
Rust
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());
|
|
}
|