miden-debug 0.17.0

An interactive debugger for Miden VM programs
Documentation
use std::string::String;

use ratatui::{Terminal, backend::TestBackend, crossterm::event::KeyModifiers};
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};

use super::*;
use crate::{DebuggerConfig, program_loader::test_package_input, ui::tui::Event};

fn setup() -> (Home, State, UnboundedReceiver<Action>) {
    let state = State::new(Box::new(DebuggerConfig {
        input: Some(test_package_input()),
        ..Default::default()
    }))
    .unwrap();
    let mut home = Home::new().unwrap();
    let (sender, receiver) = unbounded_channel();
    home.register_action_handler(sender).unwrap();
    home.init(&state).unwrap();
    (home, state, receiver)
}

fn draw(home: &mut Home, state: &State) -> String {
    let mut terminal = Terminal::new(TestBackend::new(120, 80)).unwrap();
    terminal.draw(|frame| home.draw(frame, frame.area(), state).unwrap()).unwrap();
    let buffer = terminal.backend().buffer();
    (0..buffer.area.height)
        .map(|row| {
            (0..buffer.area.width)
                .map(|column| buffer[(column, row)].symbol())
                .collect::<String>()
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn command(
    home: &mut Home,
    state: &mut State,
    receiver: &mut UnboundedReceiver<Action>,
    text: &str,
) -> Action {
    home.update(Action::FooterResult(":".into(), Some(text.into())), state).unwrap();
    receiver.try_recv().unwrap()
}

#[test]
fn focus_navigation_wraps_and_fullscreen_renders_only_selected_pane() {
    let (mut home, mut state, mut receiver) = setup();
    home.focus().unwrap();
    assert!(
        matches!(receiver.try_recv().unwrap(), Action::StatusLine(message) if message.contains("pane movement"))
    );
    assert_eq!(home.focused_pane_index, 0);
    home.update(Action::FocusPrev, &mut state).unwrap();
    assert_eq!(home.focused_pane_index, 4);
    home.update(Action::FocusNext, &mut state).unwrap();
    assert_eq!(home.focused_pane_index, 0);
    let normal = draw(&mut home, &state);
    assert!(normal.contains("Operand Stack"));
    assert!(normal.contains("Breakpoints"), "{normal}");
    for _ in 0..3 {
        home.update(Action::FocusNext, &mut state).unwrap();
    }
    assert_eq!(home.focused_pane_index, 3);
    home.update(Action::ToggleFullScreen, &mut state).unwrap();
    assert_eq!(home.fullscreen_pane_index, Some(3));
    let fullscreen = draw(&mut home, &state);
    assert!(fullscreen.contains("Operand Stack"));
    assert!(!fullscreen.contains("Breakpoints"));
    home.update(Action::ToggleFullScreen, &mut state).unwrap();
    assert_eq!(home.fullscreen_pane_index, None);
    assert!(draw(&mut home, &state).contains("Breakpoints"));
    home.update(Action::FocusFooter(":".into(), None), &mut state).unwrap();
    home.update(Action::FooterResult(":".into(), None), &mut state).unwrap();
    home.update(Action::Tick, &mut state).unwrap();
    home.update(Action::Update, &mut state).unwrap();
}

#[test]
fn footer_commands_manage_breakpoints_and_report_parse_errors() {
    let (mut home, mut state, mut receiver) = setup();
    assert_eq!(
        command(&mut home, &mut state, &mut receiver, "b at 3"),
        Action::TimedStatusLine("breakpoint created".into(), 1)
    );
    assert_eq!(state.breakpoints.len(), 1);
    assert_eq!(state.breakpoints[0].ty, BreakpointType::StepTo(3));
    assert!(
        matches!(command(&mut home, &mut state, &mut receiver, "b at invalid"), Action::TimedStatusLine(message, 5) if message.contains("invalid breakpoint"))
    );
    assert_eq!(state.breakpoints.len(), 1);
    for text in ["unknown", "unknown argument"] {
        assert_eq!(
            command(&mut home, &mut state, &mut receiver, text),
            Action::TimedStatusLine("unknown command".into(), 1)
        );
    }
    for text in ["vars", "variables all", "locals"] {
        assert!(
            matches!(command(&mut home, &mut state, &mut receiver, text), Action::StatusLine(message) if message.contains("No debug variables tracked"))
        );
    }
    assert_eq!(
        command(&mut home, &mut state, &mut receiver, "where"),
        Action::StatusLine("proc: <unknown>".into())
    );
    assert_eq!(command(&mut home, &mut state, &mut receiver, "debug"), Action::ShowDebug);
    assert_eq!(command(&mut home, &mut state, &mut receiver, "reload"), Action::Reload);
    assert_eq!(command(&mut home, &mut state, &mut receiver, "q"), Action::Quit);
    assert!(matches!(
        command(&mut home, &mut state, &mut receiver, "r invalid"),
        Action::TimedStatusLine(_, 5)
    ));
    assert_eq!(
        command(&mut home, &mut state, &mut receiver, "r 0 -t u32"),
        Action::StatusLine("0".into())
    );
    assert!(
        matches!(command(&mut home, &mut state, &mut receiver, "r 0 -c 2"), Action::TimedStatusLine(message, 5) if message.contains("not yet implemented"))
    );
}

#[test]
fn key_events_dispatch_navigation_and_respect_input_mode() {
    let (mut home, mut state, _receiver) = setup();
    for (key, action) in [
        (KeyCode::Right, Action::FocusNext),
        (KeyCode::Left, Action::FocusPrev),
        (KeyCode::Down, Action::Down),
        (KeyCode::Up, Action::Up),
        (KeyCode::Char('g'), Action::Go),
        (KeyCode::Backspace, Action::Back),
        (KeyCode::Char('f'), Action::ToggleFullScreen),
        (KeyCode::Char('2'), Action::Tab(1)),
        (KeyCode::Char(']'), Action::TabNext),
        (KeyCode::Char('['), Action::TabPrev),
        (KeyCode::Char(':'), Action::FocusFooter(":".into(), None)),
        (KeyCode::Char('q'), Action::Quit),
        (KeyCode::Char('d'), Action::Delete),
    ] {
        let response = home
            .handle_events(Event::Key(KeyEvent::new(key, KeyModifiers::NONE)), &mut state)
            .unwrap();
        assert!(matches!(response, Some(EventResponse::Stop(actual)) if actual == action));
    }
    assert!(home.handle_events(Event::Tick, &mut state).unwrap().is_none());
    assert!(
        home.handle_key_events(KeyEvent::new(KeyCode::F(12), KeyModifiers::NONE), &mut state)
            .unwrap()
            .is_none()
    );
    for mode in [InputMode::Insert, InputMode::Command] {
        state.input_mode = mode;
        assert!(
            home.handle_key_events(
                KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
                &mut state
            )
            .unwrap()
            .is_none()
        );
    }
}

#[test]
fn execution_keys_step_continue_and_reject_completed_programs() {
    for key in ['s', 'n', 'c', 'e'] {
        let (mut home, mut state, mut receiver) = setup();
        let response = home
            .handle_key_events(KeyEvent::new(KeyCode::Char(key), KeyModifiers::NONE), &mut state)
            .unwrap();
        assert!(matches!(response, Some(EventResponse::Stop(Action::Continue))));
        assert!(!state.stopped);
        home.update(Action::Continue, &mut state).unwrap();
        assert!(state.executor().cycle > 0);
        home.update(Action::Reload, &mut state).unwrap();
        assert_eq!(state.executor().cycle, 0);
        while receiver.try_recv().is_ok() {}
    }
    let (mut home, mut state, mut receiver) = setup();
    assert_eq!(command(&mut home, &mut state, &mut receiver, "nl"), Action::Continue);
    home.update(Action::Continue, &mut state).unwrap();
    state.breakpoints.clear();
    home.update(Action::Continue, &mut state).unwrap();
    assert!(state.executor().stopped);
    while receiver.try_recv().is_ok() {}
    for key in ['s', 'n', 'c'] {
        let response = home
            .handle_key_events(KeyEvent::new(KeyCode::Char(key), KeyModifiers::NONE), &mut state)
            .unwrap();
        assert!(
            matches!(response, Some(EventResponse::Stop(Action::TimedStatusLine(message, 3))) if message.contains("terminated"))
        );
    }
    assert!(
        matches!(command(&mut home, &mut state, &mut receiver, "nl"), Action::TimedStatusLine(message, 3) if message.contains("terminated"))
    );
}