kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use calamine::{Data, Reader, open_workbook_auto};
use k580_core::{Cpu8080State, Flags};
use k580_ui::persistence::{
    ExportFlagKind, ExportModel, ExportOptions, ExportRegisterKind, ExportXlsxPage, Exporters,
    Importers,
};
use std::path::PathBuf;

#[test]
fn export_options_filter_memory_range_and_registers() {
    let mut cpu = Cpu8080State::default();
    cpu.registers.a = 0x11;
    cpu.registers.w = 0x22;
    cpu.registers.z = 0x33;
    cpu.flags.zero = true;
    cpu.flags.carry = true;
    cpu.pc = 0x1234;
    cpu.memory.write(0x000F, 0xAA);
    cpu.memory.write(0x0010, 0xBB);
    cpu.memory.write(0x0011, 0xCC);

    let options = ExportOptions {
        memory_start: 0x0010,
        memory_end: 0x0010,
        registers: vec![
            ExportRegisterKind::W,
            ExportRegisterKind::Z,
            ExportRegisterKind::ProgramCounter,
        ],
        flags: vec![ExportFlagKind::Zero, ExportFlagKind::Carry],
        ..ExportOptions::default()
    };

    let model = ExportModel::from_cpu_with_options(&cpu, &options);

    assert_eq!(
        model.registers,
        vec![
            ("W".to_owned(), "22".to_owned()),
            ("Z".to_owned(), "33".to_owned()),
            ("PC".to_owned(), "1234".to_owned()),
        ]
    );
    assert_eq!(
        model.flags,
        vec![("Z".to_owned(), true), ("C".to_owned(), true)]
    );
    assert_eq!(model.memory, vec![(0x0010, 0xBB)]);
}

#[test]
fn default_export_flags_follow_visible_flag_strip_order() {
    let options = ExportOptions::default();

    assert_eq!(
        options.flags,
        vec![
            ExportFlagKind::Zero,
            ExportFlagKind::Sign,
            ExportFlagKind::Parity,
            ExportFlagKind::Carry,
            ExportFlagKind::AuxiliaryCarry,
        ]
    );
}

#[test]
fn xlsx_options_set_sheet_name_and_optional_memory_columns() {
    let model = ExportModel {
        registers: vec![("A".to_owned(), "42".to_owned())],
        flags: vec![("Z".to_owned(), true)],
        memory: vec![(0x0000, 0x76)],
    };
    let options = ExportOptions {
        page_name: "Page:1".to_owned(),
        include_memory_command: true,
        include_comment_column: true,
        ..ExportOptions::default()
    };
    let dir = unique_temp_dir();
    std::fs::create_dir_all(&dir).unwrap();
    let xlsx = dir.join("state.xlsx");

    Exporters::write_xlsx_with_options(&xlsx, &model, &options).unwrap();

    let mut workbook = open_workbook_auto(&xlsx).unwrap();
    assert_eq!(workbook.sheet_names().first().unwrap(), "Page_1");
    let sheet = workbook.worksheet_range("Page_1").unwrap();
    let memory_header = sheet
        .rows()
        .find(|row| row.first().is_some_and(|cell| cell_text(cell) == "Address"))
        .unwrap();
    assert_eq!(cell_text(&memory_header[1]), "Value");
    assert_eq!(cell_text(&memory_header[2]), "Command");
    assert_eq!(cell_text(&memory_header[3]), "Comment");
    assert_eq!(Importers::read_xlsx(&xlsx).unwrap(), model);

    std::fs::remove_file(xlsx).ok();
    std::fs::remove_dir(dir).ok();
}

#[test]
fn txt_sections_are_written_as_named_subprogram_blocks() {
    let first = ExportModel {
        registers: vec![("A".to_owned(), "11".to_owned())],
        flags: vec![("Z".to_owned(), true)],
        memory: vec![(0x0001, 0xAA)],
    };
    let second = ExportModel {
        registers: vec![("B".to_owned(), "22".to_owned())],
        flags: vec![("C".to_owned(), false)],
        memory: vec![(0x0100, 0xBB)],
    };

    let text = Exporters::to_text_sections(&[
        ("Подпрограмма 1".to_owned(), first),
        ("Подпрограмма 2".to_owned(), second),
    ]);

    assert!(text.starts_with("[Подпрограмма 1]\n[Registers]\nA=11\n"));
    assert!(text.contains("\n[Подпрограмма 2]\n[Registers]\nB=22\n"));
    assert_eq!(text.matches("[Registers]").count(), 2);
    assert_eq!(text.matches("[Memory]").count(), 2);
}

#[test]
fn xlsx_pages_are_written_as_separate_worksheets() {
    let first = ExportModel {
        registers: vec![("A".to_owned(), "11".to_owned())],
        flags: vec![("Z".to_owned(), true)],
        memory: vec![(0x0001, 0xAA)],
    };
    let second = ExportModel {
        registers: vec![("B".to_owned(), "22".to_owned())],
        flags: vec![("C".to_owned(), false)],
        memory: vec![(0x0100, 0xBB)],
    };
    let dir = unique_temp_dir();
    std::fs::create_dir_all(&dir).unwrap();
    let xlsx = dir.join("pages.xlsx");

    Exporters::write_xlsx_pages(
        &xlsx,
        &[
            ("Page:1".to_owned(), first, ExportOptions::default()),
            ("Page:2".to_owned(), second, ExportOptions::default()),
        ],
    )
    .unwrap();

    let mut workbook = open_workbook_auto(&xlsx).unwrap();
    assert_eq!(workbook.sheet_names(), &["Page_1", "Page_2"]);
    let first_sheet = workbook.worksheet_range("Page_1").unwrap();
    let second_sheet = workbook.worksheet_range("Page_2").unwrap();
    assert!(first_sheet.rows().any(|row| {
        row.first().is_some_and(|cell| cell_text(cell) == "0001")
            && row.get(1).is_some_and(|cell| cell_text(cell) == "AA")
    }));
    assert!(second_sheet.rows().any(|row| {
        row.first().is_some_and(|cell| cell_text(cell) == "0100")
            && row.get(1).is_some_and(|cell| cell_text(cell) == "BB")
    }));

    std::fs::remove_file(xlsx).ok();
    std::fs::remove_dir(dir).ok();
}

#[test]
fn xlsx_sheet_names_and_selected_sheet_import_are_available() {
    let first = ExportModel {
        registers: Vec::new(),
        flags: Vec::new(),
        memory: vec![(0x0100, 0xAA)],
    };
    let second = ExportModel {
        registers: Vec::new(),
        flags: Vec::new(),
        memory: vec![(0x0200, 0xBB)],
    };
    let dir = unique_temp_dir();
    std::fs::create_dir_all(&dir).unwrap();
    let xlsx = dir.join("pages.xlsx");

    Exporters::write_xlsx_pages(
        &xlsx,
        &[
            ("Подпрограмма 1".to_owned(), first, ExportOptions::default()),
            (
                "Подпрограмма 2".to_owned(),
                second,
                ExportOptions::default(),
            ),
        ],
    )
    .unwrap();

    assert_eq!(
        Importers::xlsx_sheet_names(&xlsx).unwrap(),
        vec!["Подпрограмма 1", "Подпрограмма 2"]
    );
    assert_eq!(
        Importers::read_xlsx_sheet(&xlsx, "Подпрограмма 2")
            .unwrap()
            .memory,
        vec![(0x0200, 0xBB)]
    );

    std::fs::remove_file(xlsx).ok();
    std::fs::remove_dir(dir).ok();
}

#[test]
fn txt_section_names_and_selected_section_import_are_available() {
    let first = ExportModel {
        registers: Vec::new(),
        flags: Vec::new(),
        memory: vec![(0x0100, 0xAA)],
    };
    let second = ExportModel {
        registers: Vec::new(),
        flags: Vec::new(),
        memory: vec![(0x0200, 0xBB)],
    };
    let dir = unique_temp_dir();
    std::fs::create_dir_all(&dir).unwrap();
    let txt = dir.join("sections.txt");
    std::fs::write(
        &txt,
        Exporters::to_text_sections(&[
            ("Раздел 1".to_owned(), first),
            ("Раздел 2".to_owned(), second),
        ]),
    )
    .unwrap();

    assert_eq!(
        Importers::txt_section_names(&txt).unwrap(),
        vec!["Раздел 1", "Раздел 2"]
    );
    assert_eq!(
        Importers::read_txt_section(&txt, "Раздел 2")
            .unwrap()
            .memory,
        vec![(0x0200, 0xBB)]
    );

    std::fs::remove_file(txt).ok();
    std::fs::remove_dir(dir).ok();
}

#[test]
fn xlsx_page_options_keep_comment_column_per_page() {
    let page = ExportXlsxPage {
        name: "Sheet".to_owned(),
        memory_start: 0x0100,
        memory_end: 0x0101,
        include_memory_address: true,
        include_memory_value: true,
        include_memory_command: true,
        include_comment_column: true,
        registers: vec![ExportRegisterKind::Accumulator],
        flags: vec![ExportFlagKind::Zero],
    };

    let options = page.to_options();

    assert_eq!(options.page_name, "Sheet");
    assert_eq!(options.memory_start, 0x0100);
    assert!(options.include_comment_column);
    assert_eq!(options.registers, vec![ExportRegisterKind::Accumulator]);
    assert_eq!(options.flags, vec![ExportFlagKind::Zero]);
}

#[test]
fn exporters_write_stable_direct_files() {
    let mut cpu = Cpu8080State::default();
    cpu.registers.a = 0x42;
    cpu.memory.write(0, 0x76);
    let model = ExportModel::from_cpu(&cpu);
    let dir = unique_temp_dir();
    std::fs::create_dir_all(&dir).unwrap();
    let txt = dir.join("state.txt");
    let xlsx = dir.join("state.xlsx");

    Exporters::write_txt(&txt, &model).unwrap();
    Exporters::write_xlsx(&xlsx, &model).unwrap();

    let text = std::fs::read_to_string(&txt).unwrap();
    assert!(text.contains("[Registers]"));
    assert!(text.contains("A=42"));
    assert!(std::fs::metadata(&xlsx).unwrap().len() > 0);

    std::fs::remove_file(txt).ok();
    std::fs::remove_file(xlsx).ok();
    std::fs::remove_dir(dir).ok();
}

#[test]
fn importers_round_trip_txt_and_xlsx() {
    let mut cpu = Cpu8080State::default();
    cpu.registers.a = 0xA5;
    cpu.registers.b = 0x12;
    cpu.registers.c = 0x34;
    cpu.pc = 0x1234;
    cpu.sp = 0xABCD;
    cpu.flags = Flags {
        sign: true,
        zero: false,
        auxiliary_carry: true,
        parity: false,
        carry: true,
    };
    cpu.memory.write(0, 0x76);
    cpu.memory.write(1, 0xC3);
    cpu.memory.write(2, 0x00);
    cpu.memory.write(3, 0x10);
    cpu.cycle_count = 4242;

    let model = ExportModel::from_cpu(&cpu);
    let dir = unique_temp_dir();
    std::fs::create_dir_all(&dir).unwrap();
    let txt = dir.join("state.txt");
    let xlsx = dir.join("state.xlsx");

    Exporters::write_txt(&txt, &model).unwrap();
    Exporters::write_xlsx(&xlsx, &model).unwrap();

    let from_txt = Importers::read_txt(&txt).unwrap();
    assert_eq!(from_txt, model);

    let from_xlsx = Importers::read_xlsx(&xlsx).unwrap();
    assert_eq!(from_xlsx, model);

    let mut restored = Cpu8080State::default();
    from_txt.apply_to(&mut restored).unwrap();
    assert_eq!(restored.registers, cpu.registers);
    assert_eq!(restored.flags, cpu.flags);
    assert_eq!(restored.pc, cpu.pc);
    assert_eq!(restored.sp, cpu.sp);
    assert_eq!(restored.cycle_count, cpu.cycle_count);
    assert_eq!(restored.memory.read(0), 0x76);
    assert_eq!(restored.memory.read(3), 0x10);

    std::fs::remove_file(txt).ok();
    std::fs::remove_file(xlsx).ok();
    std::fs::remove_dir(dir).ok();
}

fn unique_temp_dir() -> PathBuf {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    std::env::temp_dir().join(format!("k580-export-options-{nanos}"))
}

fn cell_text(cell: &Data) -> &str {
    match cell {
        Data::String(value) => value,
        _ => "",
    }
}