miden-debug 0.17.0

An interactive debugger for Miden VM programs
Documentation
use std::{fs, path::PathBuf};

use miden_assembly::DefaultSourceManager;
use miden_debug_types::{ByteIndex, SourceManagerExt};

use super::*;

#[test]
fn rendering_source_preserves_text_selection_focus_and_navigation() {
    use ratatui::{Terminal, backend::TestBackend};

    use crate::{
        config::{ColorChoice, DebuggerConfig},
        program_loader::test_package_input,
    };

    let mut state = State::new(Box::new(DebuggerConfig {
        input: Some(test_package_input()),
        color: ColorChoice::Always,
        ..Default::default()
    }))
    .unwrap();
    let (location, path) = test_location("render-navigation");
    let mut pane = SourceCodePane::new(false, Style::new().fg(Color::Blue));
    pane.init(&state).unwrap();
    pane.update_location(Some(location.clone()), 0);
    assert_eq!(pane.current_span, location.span);
    assert_eq!(pane.height_constraint(), Constraint::Fill(3));
    let mut terminal = Terminal::new(TestBackend::new(100, 10)).unwrap();
    terminal.draw(|frame| pane.draw(frame, frame.area(), &state).unwrap()).unwrap();
    let text = terminal
        .backend()
        .buffer()
        .content
        .iter()
        .map(|cell| cell.symbol())
        .collect::<String>();
    assert!(text.contains("line one"));
    assert!(text.contains("line two"));
    assert!(text.contains(&format!("1 of {}", pane.num_lines)));
    assert!(
        terminal
            .backend()
            .buffer()
            .content
            .iter()
            .any(|cell| cell.modifier.contains(Modifier::REVERSED))
    );
    for _ in 0..pane.num_lines {
        pane.update(Action::Down, &mut state).unwrap();
    }
    assert_eq!(pane.selected_line, pane.num_lines);
    pane.update(Action::Down, &mut state).unwrap();
    assert_eq!(pane.selected_line, pane.num_lines);
    pane.update(Action::Up, &mut state).unwrap();
    assert_eq!(pane.selected_line, pane.num_lines - 1);
    assert!(matches!(
        pane.update(Action::Focus, &mut state).unwrap(),
        Some(Action::TimedStatusLine(_, 3))
    ));
    assert_eq!(pane.border_type(), BorderType::Thick);
    assert_eq!(pane.border_style(), Style::new().fg(Color::Blue));
    pane.update(Action::UnFocus, &mut state).unwrap();
    assert_eq!(pane.border_type(), BorderType::Plain);
    let mut next = location;
    next.line = 2;
    next.col = 1;
    next.span = SourceSpan::new(next.source_file.id(), ByteIndex::new(9)..ByteIndex::new(13));
    pane.update_location(Some(next), 0);
    assert_eq!(pane.selected_line, 2);
    terminal.draw(|frame| pane.draw(frame, frame.area(), &state).unwrap()).unwrap();
    pane.update(Action::Submit, &mut state).unwrap();
    pane.update(Action::Reload, &mut state).unwrap();
    assert!(pane.current_file.is_none());
    fs::remove_dir_all(path.parent().unwrap()).unwrap();
}

#[test]
fn clears_cached_source_when_selection_becomes_unresolvable() {
    let (location, path) = test_location("clear-unresolvable");
    let mut pane = SourceCodePane::new(false, Style::default());
    pane.update_location(Some(location), 0);
    assert!(pane.current_file.is_some());

    pane.update_location(None, 1);

    assert!(pane.current_file.is_none());
    assert!(pane.current_source_id.is_unknown());
    assert_eq!(pane.current_line, 0);
    fs::remove_dir_all(path.parent().unwrap()).ok();
}

#[test]
fn retains_cached_source_for_unresolved_instruction_in_same_frame() {
    let (location, path) = test_location("retain-transient");
    let mut pane = SourceCodePane::new(false, Style::default());
    pane.update_location(Some(location), 0);

    pane.update_location(None, 0);

    assert!(pane.current_file.is_some());
    assert!(!pane.current_source_id.is_unknown());
    assert_eq!(pane.current_line, 1);
    fs::remove_dir_all(path.parent().unwrap()).ok();
}

fn test_location(test_name: &str) -> (ResolvedLocation, PathBuf) {
    let path = PathBuf::from("target")
        .join("source-pane-tests")
        .join(format!("{test_name}-{}", std::process::id()))
        .join("source.masm");
    fs::create_dir_all(path.parent().unwrap()).unwrap();
    fs::write(&path, "line one\nline two\n").unwrap();

    let source_manager = DefaultSourceManager::default();
    let source_file = source_manager.load_file(&path).expect("source should load");
    let span = SourceSpan::new(source_file.id(), ByteIndex::new(0)..ByteIndex::new(4));
    (
        ResolvedLocation {
            source_file,
            line: 1,
            col: 1,
            span,
        },
        path,
    )
}