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::(); 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::(); 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()); }