356 lines
12 KiB
Rust
356 lines
12 KiB
Rust
//! Process-level checks use only original instructions and temporary raw files.
|
|
use std::{
|
|
fs::{self, File},
|
|
path::{Path, PathBuf},
|
|
process::{Command, Output},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
};
|
|
|
|
use gba_core::memory::ROM_CAPACITY;
|
|
|
|
struct Fixture(PathBuf);
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
|
let path = std::env::temp_dir().join(format!(
|
|
"gameboy-rust-rom-{}-{}",
|
|
std::process::id(),
|
|
NEXT.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
fs::create_dir(&path).unwrap();
|
|
Self(path)
|
|
}
|
|
|
|
fn rom(&self, code: &[u32]) -> PathBuf {
|
|
let path = self.0.join("original program.gba");
|
|
let bytes: Vec<u8> = code.iter().flat_map(|word| word.to_le_bytes()).collect();
|
|
fs::write(&path, bytes).unwrap();
|
|
path
|
|
}
|
|
}
|
|
impl Drop for Fixture {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
fn command() -> Command {
|
|
Command::new(env!("CARGO_BIN_EXE_gameboy-rust"))
|
|
}
|
|
fn run(path: &Path, steps: &str) -> Output {
|
|
command()
|
|
.arg("--rom")
|
|
.arg(path)
|
|
.args(["--steps", steps])
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
fn stdout(output: &Output) -> String {
|
|
String::from_utf8(output.stdout.clone()).unwrap()
|
|
}
|
|
fn stderr(output: &Output) -> String {
|
|
String::from_utf8(output.stderr.clone()).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn raw_original_program_runs_with_a_bounded_report_without_changing_the_file() {
|
|
let fixture = Fixture::new();
|
|
let path = fixture.rom(&[0xe3a0_002a, 0xeaff_fffe]);
|
|
let original = fs::read(&path).unwrap();
|
|
let mut permissions = fs::metadata(&path).unwrap().permissions();
|
|
permissions.set_readonly(true);
|
|
fs::set_permissions(&path, permissions).unwrap();
|
|
let output = run(&path, "100");
|
|
assert!(output.status.success(), "{}", stderr(&output));
|
|
let text = stdout(&output);
|
|
for expected in [
|
|
"(8 bytes)",
|
|
"Original BIOS replacement",
|
|
"Result: step limit reached",
|
|
"Steps: 100",
|
|
"PC=0x08000004",
|
|
"r0=0x0000002a",
|
|
] {
|
|
assert!(text.contains(expected), "{text}");
|
|
}
|
|
assert!(output.stderr.is_empty());
|
|
assert_eq!(fs::read(path).unwrap(), original);
|
|
}
|
|
|
|
fn rtc_setup() -> Vec<u32> {
|
|
// Original LDR/MOV/STRH/LDRH/loop: enable GPIO reads, then inspect control.
|
|
vec![
|
|
0xe59f100c, 0xe3a00001, 0xe1c100b0, 0xe1d120b0, 0xeafffffe, 0x080000c8,
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
|
let fixture = Fixture::new();
|
|
let path = fixture.rom(&rtc_setup());
|
|
let original = fs::read(&path).unwrap();
|
|
let absent = run(&path, "100");
|
|
assert_eq!(absent.status.code(), Some(1));
|
|
assert!(stderr(&absent).contains("read-only memory at 0x080000c8"));
|
|
let selected = command()
|
|
.arg("--rtc")
|
|
.arg("--rom")
|
|
.arg(&path)
|
|
.args(["--steps", "100"])
|
|
.output()
|
|
.unwrap();
|
|
assert!(selected.status.success(), "{}", stderr(&selected));
|
|
assert!(stdout(&selected).contains("RTC selected: GPIO and command/control only"));
|
|
assert!(stdout(&selected).contains("r2=0x00000001"));
|
|
assert_eq!(fs::read(&path).unwrap(), original);
|
|
let duplicate = command()
|
|
.arg("--rom")
|
|
.arg(&path)
|
|
.args(["--rtc", "--rtc", "--steps", "100"])
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(duplicate.status.code(), Some(1));
|
|
assert!(stderr(&duplicate).contains("usage:"));
|
|
assert!(duplicate.stdout.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn stop_is_a_normal_bounded_outcome() {
|
|
let fixture = Fixture::new();
|
|
let path = fixture.rom(&[0xef03_0000, 0xeaff_fffe]);
|
|
let output = run(&path, "1000");
|
|
assert!(output.status.success(), "{}", stderr(&output));
|
|
assert!(stdout(&output).contains("Result: STOP; input required"));
|
|
assert!(stdout(&output).contains("stopped=true"));
|
|
assert!(!stdout(&output).contains("Steps: 1000;"));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_instruction_and_fallthrough_fail_with_machine_state() {
|
|
let fixture = Fixture::new();
|
|
for code in [[0xee00_0000], [0xe3a0_002a]] {
|
|
let path = fixture.rom(&code);
|
|
let output = run(&path, "100");
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stdout(&output).contains("Result: emulation diagnostic"));
|
|
assert!(stdout(&output).contains("r15=0x080000"));
|
|
assert!(!output.stderr.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_short_directory_and_oversized_files_fail_before_execution() {
|
|
let fixture = Fixture::new();
|
|
let path = fixture.0.join("input.gba");
|
|
for candidate in [&path, &fixture.0] {
|
|
let output = run(candidate, "1");
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stderr(&output).contains("cannot load ROM"));
|
|
assert!(output.stdout.is_empty());
|
|
}
|
|
for size in [0, 1, 2, 3, ROM_CAPACITY + 1] {
|
|
File::create(&path).unwrap().set_len(size as u64).unwrap();
|
|
let output = run(&path, "1");
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stderr(&output).contains(if size < 4 {
|
|
"at least 4 bytes"
|
|
} else {
|
|
"32 MiB"
|
|
}));
|
|
assert!(output.stdout.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_capacity_is_accepted_and_odd_lengths_are_not_padded() {
|
|
let fixture = Fixture::new();
|
|
let path = fixture.rom(&[0xeaff_fffe]);
|
|
File::options()
|
|
.write(true)
|
|
.open(&path)
|
|
.unwrap()
|
|
.set_len(ROM_CAPACITY as u64)
|
|
.unwrap();
|
|
let output = run(&path, "100");
|
|
assert!(output.status.success(), "{}", stderr(&output));
|
|
assert!(stdout(&output).contains("(33554432 bytes)"));
|
|
fs::write(&path, [0xfe, 0xff, 0xff, 0xea, 0x42]).unwrap();
|
|
let output = run(&path, "100");
|
|
assert!(output.status.success());
|
|
assert!(stdout(&output).contains("(5 bytes)"));
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_options_fail_before_opening_a_file() {
|
|
for count in ["0", "-1", "100000001", "18446744073709551616"] {
|
|
let output = run(Path::new("does-not-exist.gba"), count);
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stderr(&output).contains("usage:"));
|
|
assert!(!stderr(&output).contains("cannot load"));
|
|
}
|
|
for extra in ["--cpu-demo", "--help", "--rom", "--steps"] {
|
|
let output = command()
|
|
.args(["--rom", "missing.gba", "--steps", "1", extra])
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stderr(&output).contains("usage:"));
|
|
}
|
|
let output = command().args(["--steps", "1"]).output().unwrap();
|
|
assert_eq!(output.status.code(), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn help_describes_terminal_rom_mode_and_existing_cpu_demo_still_runs() {
|
|
let output = command().arg("--help").output().unwrap();
|
|
assert!(output.status.success());
|
|
assert!(stdout(&output).contains("--rom PATH --steps COUNT"));
|
|
assert!(stdout(&output).contains("--rom PATH --window"));
|
|
assert!(stdout(&output).contains("ROM modes have no audio or saves"));
|
|
let output = command().arg("--cpu-demo").output().unwrap();
|
|
assert!(output.status.success());
|
|
assert!(stdout(&output).contains("Exception demo"));
|
|
}
|
|
|
|
#[test]
|
|
fn window_options_and_bad_files_fail_without_opening_a_window() {
|
|
let fixture = Fixture::new();
|
|
let missing = fixture.0.join("missing.gba");
|
|
for extra in [
|
|
vec!["--steps", "1"],
|
|
vec!["--frames", "0"],
|
|
vec!["--frames", "100001"],
|
|
vec!["--window"],
|
|
vec!["--help"],
|
|
] {
|
|
let output = command()
|
|
.arg("--rom")
|
|
.arg(&missing)
|
|
.arg("--window")
|
|
.args(extra)
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stderr(&output).contains("usage:"));
|
|
assert!(output.stdout.is_empty());
|
|
}
|
|
let output = command()
|
|
.arg("--rom")
|
|
.arg(&missing)
|
|
.arg("--window")
|
|
.output()
|
|
.unwrap();
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stderr(&output).contains("cannot load ROM"));
|
|
assert!(output.stdout.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires a logged-in native desktop session"]
|
|
fn native_rom_window_presents_file_bytes_and_reports_stop_cpu_and_video_errors() {
|
|
use std::{
|
|
process::Stdio,
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
fn window(path: &Path, rtc: bool) -> Output {
|
|
let mut command = command();
|
|
if rtc {
|
|
command.arg("--rtc");
|
|
}
|
|
let mut child = command
|
|
.arg("--rom")
|
|
.arg(path)
|
|
.args(["--window", "--frames", "3"])
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
let start = Instant::now();
|
|
while child.try_wait().unwrap().is_none() {
|
|
if start.elapsed() > Duration::from_secs(20) {
|
|
child.kill().unwrap();
|
|
let output = child.wait_with_output().unwrap();
|
|
panic!(
|
|
"ROM window did not exit: {} {}",
|
|
stdout(&output),
|
|
stderr(&output)
|
|
);
|
|
}
|
|
thread::sleep(Duration::from_millis(10));
|
|
}
|
|
child.wait_with_output().unwrap()
|
|
}
|
|
let fixture = Fixture::new();
|
|
let path = fixture.0.join("original-input.gba");
|
|
fs::write(&path, gba_demos::input_rom()).unwrap();
|
|
let output = window(&path, false);
|
|
assert!(
|
|
output.status.success(),
|
|
"{} {}",
|
|
stdout(&output),
|
|
stderr(&output)
|
|
);
|
|
assert!(stdout(&output).contains("Captured ROM frames: 3"));
|
|
assert!(stdout(&output).contains("Result: window frame limit reached"));
|
|
let rtc_path = fixture.rom(&rtc_setup());
|
|
let rtc_output = window(&rtc_path, true);
|
|
assert!(rtc_output.status.success(), "{}", stderr(&rtc_output));
|
|
assert!(stdout(&rtc_output).contains("Captured ROM frames: 3"));
|
|
assert!(stdout(&rtc_output).contains("r2=0x00000001"));
|
|
for (code, error) in [
|
|
(vec![0xef03_0000, 0xeaff_fffe], "entered STOP"),
|
|
(vec![0xee00_0000], "instruction"),
|
|
(
|
|
vec![0xe3a0_0006, 0xe3a0_1301, 0xe1c1_00b0, 0xeaff_fffe],
|
|
"6",
|
|
),
|
|
] {
|
|
fs::write(
|
|
&path,
|
|
code.iter()
|
|
.flat_map(|word: &u32| word.to_le_bytes())
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.unwrap();
|
|
let output = window(&path, false);
|
|
assert_eq!(output.status.code(), Some(1), "{}", stdout(&output));
|
|
assert!(stdout(&output).contains("Result: ROM window diagnostic"));
|
|
assert!(stdout(&output).contains("Captured ROM frames: 0"));
|
|
assert!(stdout(&output).contains("PC="));
|
|
assert!(stderr(&output).contains(error), "{}", stderr(&output));
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn non_utf8_paths_reach_the_loader_without_panicking() {
|
|
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
|
|
let fixture = Fixture::new();
|
|
// Darwin filesystems can reject invalid UTF-8. Do not require creating one.
|
|
let path = fixture.0.join(OsStr::from_bytes(b"missing-\xff.gba"));
|
|
let output = run(&path, "1");
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(stderr(&output).contains("cannot load ROM"));
|
|
assert!(!stderr(&output).contains("usage:"));
|
|
assert!(output.stdout.is_empty());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn unicode_paths_and_regular_file_symlinks_work() {
|
|
use std::os::unix::fs::symlink;
|
|
let fixture = Fixture::new();
|
|
let original = fixture.rom(&[0xeaff_fffe]);
|
|
for name in ["original-λ.gba", "original-link.gba"] {
|
|
let path = fixture.0.join(name);
|
|
symlink(&original, &path).unwrap();
|
|
let output = command()
|
|
.args(["--steps", "100", "--rom"])
|
|
.arg(path)
|
|
.output()
|
|
.unwrap();
|
|
assert!(output.status.success(), "{}", stderr(&output));
|
|
assert!(stdout(&output).contains("Result: step limit reached"));
|
|
}
|
|
}
|