miden-debug 0.17.0

An interactive debugger for Miden VM programs
Documentation
use std::{boxed::Box, string::String, vec::Vec};

use miden_core::Felt;
use ratatui::{Terminal, backend::TestBackend, layout::Rect, prelude::Style};

use super::{
    Pane, breakpoints::BreakpointsPane, debug::DebugPane, disasm::DisassemblyPane,
    error::ErrorPane, footer::FooterPane, header::HeaderPane, stack::OperandStackPane,
    stacktrace::StackTracePane,
};
use crate::{
    config::DebuggerConfig,
    program_loader::test_package_input,
    ui::{action::Action, state::State},
};

fn state() -> State {
    State::new(Box::new(DebuggerConfig {
        input: Some(test_package_input()),
        ..Default::default()
    }))
    .expect("test state should build")
}

fn render_pane<P: Pane + ?Sized>(pane: &mut P, state: &State, width: u16, height: u16) -> String {
    let backend = TestBackend::new(width, height);
    let mut terminal = Terminal::new(backend).expect("test terminal should build");
    terminal
        .draw(|frame| {
            pane.draw(frame, Rect::new(0, 0, width, height), state)
                .expect("pane should render");
        })
        .expect("pane should render");
    let buffer = terminal.backend().buffer();
    (0..height)
        .map(|y| {
            (0..width)
                .map(|x| buffer.cell((x, y)).expect("cell should exist").symbol())
                .collect::<String>()
        })
        .collect::<Vec<_>>()
        .join("\n")
}

#[test]
fn renders_header_and_error_popup() {
    let state = state();
    let mut header = HeaderPane::new();
    let header_text = render_pane(&mut header, &state, 60, 1);
    assert!(header_text.contains("Miden Debugger"));

    let mut error = ErrorPane::new("execution failed");
    let error_text = render_pane(&mut error, &state, 40, 6);
    assert!(error_text.contains("Program Error"));
    assert!(error_text.contains("execution failed"));
    assert!(error_text.contains("Esc/Enter/q close"));
}

#[test]
fn renders_empty_and_populated_execution_panes() {
    let mut state = state();
    state.executor_mut().current_stack = vec![Felt::from(12345u32), Felt::from(67890u32)];

    let style = Style::default();
    let mut stack = OperandStackPane::new(true, style);
    let stack_text = render_pane(&mut stack, &state, 32, 6);
    assert!(stack_text.contains("Operand Stack"));
    assert!(stack_text.contains("depth is 2"));
    assert!(stack_text.lines().nth(1).unwrap().contains("67890"));
    assert!(stack_text.lines().nth(2).unwrap().contains("12345"));

    state.executor_mut().current_stack.clear();
    let empty_stack_text = render_pane(&mut stack, &state, 32, 6);
    assert!(empty_stack_text.contains("depth is 0"));
    assert!(!empty_stack_text.contains("12345"));
    assert!(!empty_stack_text.contains("67890"));

    let mut disassembly = DisassemblyPane::new(false, style);
    let disassembly_text = render_pane(&mut disassembly, &state, 48, 6);
    assert!(disassembly_text.contains("Disassembly"));
    assert!(disassembly_text.contains("at cycle"));

    let mut stacktrace = StackTracePane::new(false, style);
    let stacktrace_text = render_pane(&mut stacktrace, &state, 48, 6);
    assert!(stacktrace_text.contains("Stack Trace"));

    let mut breakpoints = BreakpointsPane::new(false, style);
    let breakpoints_text = render_pane(&mut breakpoints, &state, 48, 6);
    assert!(breakpoints_text.contains("Breakpoints"));
}

#[test]
fn renders_debug_log_and_footer_modes() {
    let mut state = state();
    let mut debug = DebugPane::default();
    let debug_text = render_pane(&mut debug, &state, 48, 6);
    assert!(debug_text.contains("Debug Log"));

    let mut footer = FooterPane::new();
    let status_text = render_pane(&mut footer, &state, 48, 1);
    assert!(status_text.contains("[N]"));

    footer
        .update(Action::FocusFooter(":".into(), Some("vars".into())), &mut state)
        .expect("footer should enter command mode");
    let command_text = render_pane(&mut footer, &state, 48, 2);
    assert!(command_text.contains("[C]"));
    assert!(command_text.contains(":vars"));
}

#[test]
fn pane_updates_change_focus_state_without_terminal_io() {
    let mut state = state();
    let style = Style::default();

    let mut panes: Vec<Box<dyn Pane>> = vec![
        Box::new(OperandStackPane::new(false, style)),
        Box::new(DisassemblyPane::new(false, style)),
        Box::new(StackTracePane::new(false, style)),
        Box::new(BreakpointsPane::new(false, style)),
    ];
    for pane in &mut panes {
        let unfocused = render_pane(pane.as_mut(), &state, 48, 6);
        assert!(unfocused.starts_with('┌'));

        pane.update(Action::Focus, &mut state).expect("pane should focus");
        let focused = render_pane(pane.as_mut(), &state, 48, 6);
        assert!(focused.starts_with('┏'));
        assert_ne!(focused, unfocused);

        pane.update(Action::UnFocus, &mut state).expect("pane should unfocus");
        assert_eq!(render_pane(pane.as_mut(), &state, 48, 6), unfocused);
    }
}