use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use crate::core::clock::GlobalTime;
use crate::host::chardev::{CharPort, ports};
use crate::machine::{Machine, build, catalog};
use super::monitor::RSMON_IMAGE;
use super::wozmon::WOZMON_IMAGE;
fn boot(port_name: &str, paced: bool, image: &[u8]) -> (Machine, Arc<CharPort>) {
let mut options = catalog::build_options().expect("this build has the classes");
options.realize.media.insert("rom", image);
let options = options
.with_param("console", port_name)
.with_param("pace", if paced { "true" } else { "false" });
let registry = catalog::registry().expect("this build has the classes");
let machine = match build(
"beneater-6502.machine",
catalog::BENEATER_6502.source,
®istry,
&options,
) {
Ok(m) => m,
Err(e) => panic!("beneater-6502.machine: {e}"),
};
let port = ports::open(&options.realize.hosts, port_name).expect("the ACIA opened it");
(machine, port)
}
fn exchange(machine: &mut Machine, port: &CharPort, input: &str, millis: u64) -> Vec<u8> {
port.feed(input.as_bytes());
machine
.run_for(GlobalTime::from_nanos(millis * 1_000_000))
.expect("the machine runs");
port.drain()
}
fn shown(bytes: &[u8]) -> String {
let mut out = String::new();
for &b in bytes {
match b {
b'\r' => out.push('\n'),
b'\n' => {}
0x20..=0x7e => out.push(b as char),
other => out.push_str(&alloc::format!("\\x{other:02x}")),
}
}
out
}
fn peek(machine: &Machine, addr: u64) -> u8 {
machine
.space("cpubus")
.expect("cpubus")
.read(
addr,
crate::core::value::Width::U8,
crate::core::space::MemAttrs::DEBUG,
)
.expect("open bus answers everything") as u8
}
#[test]
fn the_address_decoding_is_the_boards() {
let (mut machine, _port) = boot("beneater.test.map", false, RSMON_IMAGE);
machine
.run_for(GlobalTime::from_nanos(1_000_000))
.expect("runs");
let space = machine.space("cpubus").expect("cpubus");
space
.write(
0x3fff,
crate::core::value::Width::U8,
0x5a,
crate::core::space::MemAttrs::DEFAULT,
)
.expect("the top of RAM is writable");
assert_eq!(peek(&machine, 0x3fff), 0x5a);
assert_eq!(peek(&machine, 0x5003), 0x1f, "the control register");
assert_eq!(
peek(&machine, 0x4003),
0x1f,
"and at the bottom of the window"
);
assert_eq!(peek(&machine, 0x5fff), 0x1f, "and at the top of it");
assert_eq!(peek(&machine, 0x5002), 0x0b, "the command register");
assert_eq!(peek(&machine, 0x600e), 0x80, "the VIA's IER");
assert_eq!(peek(&machine, 0x601e), 0x80, "sixteen bytes up");
assert_eq!(peek(&machine, 0x7ffe), 0x80, "and at the top of the window");
assert_eq!(peek(&machine, 0x8000), RSMON_IMAGE[0]);
assert_eq!(peek(&machine, 0xfe00), RSMON_IMAGE[0x7e00]);
assert_eq!(peek(&machine, 0xfffc), 0x00);
assert_eq!(peek(&machine, 0xfffd), 0x80, "RESET points at $8000");
}
#[test]
fn the_cpu_runs_at_one_megahertz_exactly() {
let (mut machine, _port) = boot("beneater.test.clock", false, RSMON_IMAGE);
machine
.run_for(GlobalTime::from_nanos(1_000_000_000))
.expect("runs");
let cpu = machine
.device("cpu")
.and_then(crate::machine::machine::DeviceEntry::domain)
.expect("the cpu has a clock domain");
let ticks = machine.clocks().ticks(cpu).expect("a tick count");
assert_eq!(ticks, 1_000_000, "one virtual second is not 10^6 cycles");
let via = machine
.device("via")
.and_then(crate::machine::machine::DeviceEntry::domain)
.expect("the via has a clock domain");
assert_eq!(machine.clocks().ticks(via).expect("ticks"), ticks);
let acia = machine
.device("acia")
.and_then(crate::machine::machine::DeviceEntry::domain)
.expect("the acia has a clock domain");
let chars = machine.clocks().ticks(acia).expect("ticks");
assert!(
chars.abs_diff(1920) <= 1,
"one virtual second is {chars} character times, not 1920"
);
}
#[test]
fn it_boots_prints_a_prompt_and_dumps_memory_on_request() {
let (mut machine, port) = boot("beneater.test.dump", false, RSMON_IMAGE);
let banner = exchange(&mut machine, &port, "", 20);
assert_eq!(banner, b"RSMON\r\n>", "got {:?}", shown(&banner));
let dump = exchange(&mut machine, &port, "8000\r", 20);
assert_eq!(
dump,
b"8000\r\n8000: D8 A2 FF 9A A9 1F 8D 03\r\n>",
"got {:?}",
shown(&dump)
);
assert_eq!(
&RSMON_IMAGE[..8],
&[0xd8, 0xa2, 0xff, 0x9a, 0xa9, 0x1f, 0x8d, 0x03]
);
let next = exchange(&mut machine, &port, "\r", 20);
assert_eq!(
next,
b"\r\n8008: 50 A9 0B 8D 02 50 A2 00\r\n>",
"got {:?}",
shown(&next)
);
}
#[test]
fn it_deposits_a_byte_and_reads_it_back() {
let (mut machine, port) = boot("beneater.test.deposit", false, RSMON_IMAGE);
let _banner = exchange(&mut machine, &port, "", 20);
let deposit = exchange(&mut machine, &port, "0200:AA\r", 20);
assert_eq!(deposit, b"0200:AA\r\n>", "got {:?}", shown(&deposit));
let readback = exchange(&mut machine, &port, "0200\r", 20);
assert_eq!(
readback,
b"0200\r\n0200: AA 00 00 00 00 00 00 00\r\n>",
"got {:?}",
shown(&readback)
);
assert_eq!(peek(&machine, 0x0200), 0xaa);
}
#[test]
fn a_paced_port_sends_about_nineteen_thousand_bits_a_second() {
let (mut machine, port) = boot("beneater.test.paced", true, RSMON_IMAGE);
let early = exchange(&mut machine, &port, "", 3);
assert!(
early.len() <= 8,
"{} bytes in 3 ms is faster than 19200 baud: {:?}",
early.len(),
shown(&early)
);
let mut total = early;
total.extend(exchange(&mut machine, &port, "", 50));
assert_eq!(total, b"RSMON\r\n>", "got {:?}", shown(&total));
}
#[test]
fn a_scripted_session_is_deterministic_and_snapshots() {
let script = "8000\r0200:5A\r0200\r";
let (mut first, port_a) = boot("beneater.test.determinism.a", false, RSMON_IMAGE);
let a = exchange(&mut first, &port_a, script, 50);
let (mut second, port_b) = boot("beneater.test.determinism.b", false, RSMON_IMAGE);
let b = exchange(&mut second, &port_b, script, 50);
assert_eq!(a, b, "the same script produced different bytes");
assert_eq!(
first.state_hash().expect("hashes"),
second.state_hash().expect("hashes"),
"identical output but divergent state"
);
let saved = first.save().expect("saves");
let (mut restored, port_c) = boot("beneater.test.determinism.c", false, RSMON_IMAGE);
restored.load(&saved).expect("loads");
assert_eq!(
restored.state_hash().expect("hashes"),
first.state_hash().expect("hashes")
);
let _ = port_c.drain();
let after_a = exchange(&mut first, &port_a, "0200\r", 20);
let after_c = exchange(&mut restored, &port_c, "0200\r", 20);
assert_eq!(after_a, after_c);
assert_eq!(
after_a,
b"0200\r\n0200: 5A 00 00 00 00 00 00 00\r\n>",
"got {:?}",
shown(&after_a)
);
}
#[test]
fn a_via_timer_expires_on_the_cycle_it_should() {
use crate::core::space::MemAttrs;
use crate::core::value::Width;
let (mut machine, _port) = boot("beneater.test.timer", false, RSMON_IMAGE);
let poke = |machine: &Machine, addr: u64, value: u64| {
machine
.space("cpubus")
.expect("cpubus")
.write(addr, Width::U8, value, MemAttrs::DEFAULT)
.expect("the VIA answers");
};
machine
.run_for(GlobalTime::from_nanos(1_000_000))
.expect("runs");
poke(&machine, 0x6004, 0xe7); poke(&machine, 0x6005, 0x03); assert_eq!(peek(&machine, 0x600d) & 0x40, 0, "not yet");
machine
.run_for(GlobalTime::from_nanos(500_000))
.expect("runs");
assert_eq!(peek(&machine, 0x600d) & 0x40, 0, "still counting");
machine
.run_for(GlobalTime::from_nanos(600_000))
.expect("runs");
assert_eq!(peek(&machine, 0x600d) & 0x40, 0x40, "T1 timed out");
machine
.space("cpubus")
.expect("cpubus")
.read(0x6004, Width::U8, MemAttrs::DEFAULT)
.expect("T1C-L");
assert_eq!(peek(&machine, 0x600d) & 0x40, 0, "and the read cleared it");
}
#[cfg(feature = "std")]
#[test]
fn a_session_transcript() {
let (mut machine, port) = boot("beneater.test.transcript", false, RSMON_IMAGE);
let mut transcript = exchange(&mut machine, &port, "", 20);
for line in ["8000\r", "\r", "0200:C0\r", "0200\r"] {
transcript.extend(exchange(&mut machine, &port, line, 20));
}
std::println!("--- rsemu run beneater-6502 ---\n{}", shown(&transcript));
assert!(transcript.starts_with(b"RSMON"));
}
#[test]
fn the_woz_monitor_boots_and_examines_a_range() {
let (mut machine, port) = boot("beneater.test.wozmon", false, WOZMON_IMAGE);
let banner = exchange(&mut machine, &port, "", 20);
assert_eq!(banner, b"\\\r", "got {:?}", shown(&banner));
let dump = exchange(&mut machine, &port, "FF00.FF0F\r", 20);
assert_eq!(
dump,
b"FF00.FF0F\r\rFF00: D8 58 A0 7F A9 1F 8D 03\rFF08: 50 A9 0B 8D 02 50 EA C9\r",
"got {:?}",
shown(&dump)
);
let deposit = exchange(&mut machine, &port, "0300: AA BB CC\r", 20);
assert_eq!(
deposit,
b"0300: AA BB CC\r\r0300: 00\r",
"got {:?}",
shown(&deposit)
);
assert_eq!(peek(&machine, 0x0300), 0xaa);
assert_eq!(peek(&machine, 0x0301), 0xbb);
assert_eq!(peek(&machine, 0x0302), 0xcc);
let readback = exchange(&mut machine, &port, "0300.0302\r", 20);
assert_eq!(
readback,
b"0300.0302\r\r0300: AA BB CC\r",
"got {:?}",
shown(&readback)
);
}
#[cfg(feature = "std")]
#[test]
fn a_woz_monitor_transcript() {
let (mut machine, port) = boot("beneater.test.wozmon.transcript", false, WOZMON_IMAGE);
let mut transcript = exchange(&mut machine, &port, "", 20);
for line in ["FF00.FF0F\r", "0300: AA BB CC\r", "0300.0302\r"] {
transcript.extend(exchange(&mut machine, &port, line, 20));
}
std::println!(
"--- rsemu run beneater-6502 --monitor wozmon ---\n{}",
shown(&transcript)
);
assert!(transcript.starts_with(b"\\"));
}
#[cfg(feature = "std")]
#[test]
fn ben_eaters_own_image_boots() {
let Ok(path) = std::env::var("RSEMU_BENEATER_ROM") else {
std::println!("SKIP: set RSEMU_BENEATER_ROM to a 32 KiB image for this board");
return;
};
let image = std::fs::read(&path).expect("RSEMU_BENEATER_ROM is readable");
let needs_cmos = image.windows(3).any(|w| w == [0xa9, 0xff, 0x3a]);
let (mut machine, port) = boot("beneater.test.foreign", false, &image);
let banner = exchange(&mut machine, &port, "", 50);
std::println!("beneater-6502 + {path}: banner {:?}", shown(&banner));
let dump = exchange(&mut machine, &port, "FF00.FF07\r", 50);
let text = shown(&dump);
std::println!("beneater-6502 + {path}: dump {text:?}");
if dump.is_empty() && needs_cmos {
std::println!(
"SKIP: {path} has `LDA #$FF / DEC A` in it and needs a 65C02. \
`cpu.mos6502` has no CMOS variant yet, so that delay loop cannot \
terminate and the image stops after its first character \
({:?} came out). See machines/beneater-6502.machine.",
shown(&banner)
);
return;
}
assert!(
!banner.is_empty(),
"no output at all from {path}: {:?}",
shown(&banner)
);
assert!(text.contains("FF00:"), "no dump from {path}: {text:?}");
for byte in &image[0x7f00..0x7f08] {
assert!(
text.contains(&alloc::format!("{byte:02X}")),
"byte {byte:02X} missing from {text:?}"
);
}
}