feat(test-runner): add deterministic gameplay scenarios
This commit is contained in:
parent
f7c29429e3
commit
bf2ab54fc2
8 changed files with 419 additions and 38 deletions
|
|
@ -13,12 +13,14 @@ use std::{
|
||||||
use gba_core::{
|
use gba_core::{
|
||||||
bios,
|
bios,
|
||||||
cpu::InstructionSet,
|
cpu::InstructionSet,
|
||||||
|
input::Buttons,
|
||||||
machine::{Machine, StepKind},
|
machine::{Machine, StepKind},
|
||||||
|
video::{Framebuffer, HEIGHT, WIDTH},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use super::{load, Stats, MAX_STEPS};
|
use super::{load, Stats, MAX_FRAMES, MAX_STEPS};
|
||||||
|
|
||||||
const MAX_MANIFEST_BYTES: u64 = 1024 * 1024;
|
const MAX_MANIFEST_BYTES: u64 = 1024 * 1024;
|
||||||
const MAX_CASES: usize = 256;
|
const MAX_CASES: usize = 256;
|
||||||
|
|
@ -76,15 +78,30 @@ struct Case {
|
||||||
rom: PathBuf,
|
rom: PathBuf,
|
||||||
step_limit: u64,
|
step_limit: u64,
|
||||||
completion: Completion,
|
completion: Completion,
|
||||||
|
#[serde(default)]
|
||||||
|
inputs: Option<Vec<Input>>,
|
||||||
checks: Vec<Check>,
|
checks: Vec<Check>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(untagged, deny_unknown_fields)]
|
||||||
|
enum Completion {
|
||||||
|
Pc {
|
||||||
|
#[serde(deserialize_with = "word")]
|
||||||
|
pc: u32,
|
||||||
|
instruction_set: State,
|
||||||
|
},
|
||||||
|
VBlanks {
|
||||||
|
vblanks: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct Completion {
|
struct Input {
|
||||||
|
vblank: u64,
|
||||||
#[serde(deserialize_with = "word")]
|
#[serde(deserialize_with = "word")]
|
||||||
pc: u32,
|
buttons: u32,
|
||||||
instruction_set: State,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
||||||
|
|
@ -106,6 +123,12 @@ impl State {
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||||
enum Check {
|
enum Check {
|
||||||
|
Pixel {
|
||||||
|
x: usize,
|
||||||
|
y: usize,
|
||||||
|
#[serde(deserialize_with = "word")]
|
||||||
|
equals: u32,
|
||||||
|
},
|
||||||
Register {
|
Register {
|
||||||
index: usize,
|
index: usize,
|
||||||
#[serde(deserialize_with = "word")]
|
#[serde(deserialize_with = "word")]
|
||||||
|
|
@ -126,8 +149,8 @@ enum Check {
|
||||||
impl Suite {
|
impl Suite {
|
||||||
fn validate(&self) -> io::Result<()> {
|
fn validate(&self) -> io::Result<()> {
|
||||||
let invalid = |message: &str| io::Error::new(io::ErrorKind::InvalidInput, message);
|
let invalid = |message: &str| io::Error::new(io::ErrorKind::InvalidInput, message);
|
||||||
if self.version != 1 {
|
if !matches!(self.version, 1 | 2) {
|
||||||
return Err(invalid("suite version must be 1"));
|
return Err(invalid("suite version must be 1 or 2"));
|
||||||
}
|
}
|
||||||
if self.cases.is_empty() || self.cases.len() > MAX_CASES {
|
if self.cases.is_empty() || self.cases.len() > MAX_CASES {
|
||||||
return Err(invalid("suite must contain 1..=256 cases"));
|
return Err(invalid("suite must contain 1..=256 cases"));
|
||||||
|
|
@ -150,18 +173,62 @@ impl Suite {
|
||||||
if total > MAX_STEPS {
|
if total > MAX_STEPS {
|
||||||
return Err(invalid("sum of case step limits must not exceed 100000000"));
|
return Err(invalid("sum of case step limits must not exceed 100000000"));
|
||||||
}
|
}
|
||||||
let alignment = match case.completion.instruction_set {
|
match case.completion {
|
||||||
State::Arm => 4,
|
Completion::Pc {
|
||||||
State::Thumb => 2,
|
pc,
|
||||||
};
|
instruction_set,
|
||||||
if case.completion.pc % alignment != 0 {
|
} => {
|
||||||
return Err(invalid("completion PC must be aligned for its instruction set; do not set the Thumb address bit"));
|
let alignment = match instruction_set {
|
||||||
|
State::Arm => 4,
|
||||||
|
State::Thumb => 2,
|
||||||
|
};
|
||||||
|
if pc % alignment != 0 {
|
||||||
|
return Err(invalid("completion PC must be aligned for its instruction set; do not set the Thumb address bit"));
|
||||||
|
}
|
||||||
|
if case.inputs.is_some() {
|
||||||
|
return Err(invalid("input schedules require VBlank completion"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Completion::VBlanks { vblanks } => {
|
||||||
|
if self.version != 2 || !(1..=MAX_FRAMES).contains(&vblanks) {
|
||||||
|
return Err(invalid(
|
||||||
|
"VBlank completion requires version 2 and 1..=100000 vblanks",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(inputs) = &case.inputs {
|
||||||
|
if inputs.len() > 256 {
|
||||||
|
return Err(invalid(
|
||||||
|
"input schedule must contain at most 256 snapshots",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut previous = None;
|
||||||
|
for input in inputs {
|
||||||
|
if input.buttons > 0x3ff
|
||||||
|
|| input.vblank >= vblanks
|
||||||
|
|| previous.is_some_and(|frame| input.vblank <= frame)
|
||||||
|
{
|
||||||
|
return Err(invalid("inputs need 10-bit button masks and strictly increasing VBlanks before completion"));
|
||||||
|
}
|
||||||
|
previous = Some(input.vblank);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if case.checks.is_empty() || case.checks.len() > MAX_CHECKS {
|
if case.checks.is_empty() || case.checks.len() > MAX_CHECKS {
|
||||||
return Err(invalid("each case must contain 1..=256 checks"));
|
return Err(invalid("each case must contain 1..=256 checks"));
|
||||||
}
|
}
|
||||||
for check in &case.checks {
|
for check in &case.checks {
|
||||||
match check {
|
match check {
|
||||||
|
Check::Pixel { x, y, equals } => {
|
||||||
|
if self.version != 2
|
||||||
|
|| !matches!(case.completion, Completion::VBlanks { .. })
|
||||||
|
|| *x >= WIDTH
|
||||||
|
|| *y >= HEIGHT
|
||||||
|
|| *equals > 0xff_ffff
|
||||||
|
{
|
||||||
|
return Err(invalid("pixel checks require version 2, VBlank completion, x < 240, y < 160, and RGB888 values"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Check::Register { index, .. } if *index > 15 => {
|
Check::Register { index, .. } if *index > 15 => {
|
||||||
return Err(invalid("register index must be 0..=15"))
|
return Err(invalid("register index must be 0..=15"))
|
||||||
}
|
}
|
||||||
|
|
@ -205,20 +272,53 @@ enum Outcome {
|
||||||
StepLimit,
|
StepLimit,
|
||||||
Stopped,
|
Stopped,
|
||||||
EmulationError(String),
|
EmulationError(String),
|
||||||
|
RenderError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_to_checkpoint(machine: &mut Machine, case: &Case) -> (Stats, Outcome) {
|
fn run_to_checkpoint(machine: &mut Machine, case: &Case) -> (Stats, Outcome) {
|
||||||
let mut stats = Stats::default();
|
let mut stats = Stats::default();
|
||||||
|
let capture = matches!(case.completion, Completion::VBlanks { .. });
|
||||||
|
machine.memory_mut().set_scanline_rendering(capture);
|
||||||
|
let mut frame = Framebuffer::default();
|
||||||
|
let mut last_vblank = 0;
|
||||||
|
let mut inputs = case.inputs.iter().flatten().peekable();
|
||||||
loop {
|
loop {
|
||||||
if machine.stopped() {
|
if machine.stopped() {
|
||||||
return (stats, Outcome::Stopped);
|
return (stats, Outcome::Stopped);
|
||||||
}
|
}
|
||||||
// Check after successful progress, before trying to execute the checkpoint.
|
let vblanks = machine.memory().display_position().vblanks;
|
||||||
// The final budgeted step can reach it. No initial/reset-state pass is allowed.
|
if capture && vblanks != last_vblank {
|
||||||
if stats.steps != 0
|
match machine.memory().present_frame(&mut frame) {
|
||||||
&& machine.cpu().pc() == case.completion.pc
|
Ok(true) => {}
|
||||||
&& machine.cpu().instruction_set() == case.completion.instruction_set.instruction_set()
|
Ok(false) => {
|
||||||
{
|
return (
|
||||||
|
stats,
|
||||||
|
Outcome::RenderError("no complete captured frame at VBlank".into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(error) => return (stats, Outcome::RenderError(error.to_string())),
|
||||||
|
}
|
||||||
|
last_vblank = vblanks;
|
||||||
|
}
|
||||||
|
// Snapshots apply after this VBlank's capture, before the next machine step.
|
||||||
|
while inputs.peek().is_some_and(|input| input.vblank <= vblanks) {
|
||||||
|
let input = inputs.next().unwrap();
|
||||||
|
machine
|
||||||
|
.memory_mut()
|
||||||
|
.set_buttons(Buttons::from_bits(input.buttons as u16));
|
||||||
|
}
|
||||||
|
// The final budgeted step can complete a case; reset alone cannot.
|
||||||
|
let complete = match case.completion {
|
||||||
|
Completion::Pc {
|
||||||
|
pc,
|
||||||
|
instruction_set,
|
||||||
|
} => {
|
||||||
|
machine.cpu().pc() == pc
|
||||||
|
&& machine.cpu().instruction_set() == instruction_set.instruction_set()
|
||||||
|
}
|
||||||
|
Completion::VBlanks { vblanks: target } => vblanks >= target,
|
||||||
|
};
|
||||||
|
if stats.steps != 0 && complete {
|
||||||
return (stats, Outcome::Checkpoint);
|
return (stats, Outcome::Checkpoint);
|
||||||
}
|
}
|
||||||
if stats.steps == case.step_limit {
|
if stats.steps == case.step_limit {
|
||||||
|
|
@ -234,8 +334,23 @@ fn run_to_checkpoint(machine: &mut Machine, case: &Case) -> (Stats, Outcome) {
|
||||||
|
|
||||||
fn check(machine: &Machine, check: &Check) -> Value {
|
fn check(machine: &Machine, check: &Check) -> Value {
|
||||||
let (expected, actual) = match check {
|
let (expected, actual) = match check {
|
||||||
|
Check::Pixel { x, y, equals } => {
|
||||||
|
let mut frame = Framebuffer::default();
|
||||||
|
let actual = match machine.memory().present_frame(&mut frame) {
|
||||||
|
Ok(true) => Ok(frame.pixels()[y * WIDTH + x]),
|
||||||
|
Ok(false) => Err("no complete captured frame".to_owned()),
|
||||||
|
Err(error) => Err(error.to_string()),
|
||||||
|
};
|
||||||
|
(*equals, actual)
|
||||||
|
}
|
||||||
Check::Register { index, equals } => (*equals, Ok(machine.cpu().registers()[*index])),
|
Check::Register { index, equals } => (*equals, Ok(machine.cpu().registers()[*index])),
|
||||||
Check::Memory32 { address, equals } => (*equals, machine.memory().read32(*address)),
|
Check::Memory32 { address, equals } => (
|
||||||
|
*equals,
|
||||||
|
machine
|
||||||
|
.memory()
|
||||||
|
.read32(*address)
|
||||||
|
.map_err(|error| error.to_string()),
|
||||||
|
),
|
||||||
Check::Cpsr { equals } => (*equals, Ok(machine.cpu().cpsr())),
|
Check::Cpsr { equals } => (*equals, Ok(machine.cpu().cpsr())),
|
||||||
};
|
};
|
||||||
match actual {
|
match actual {
|
||||||
|
|
@ -285,6 +400,11 @@ fn run_case(case: &Case, base: &Path) -> Value {
|
||||||
};
|
};
|
||||||
let (stats, outcome) = run_to_checkpoint(&mut machine, case);
|
let (stats, outcome) = run_to_checkpoint(&mut machine, case);
|
||||||
report["state"] = state(&machine, &stats);
|
report["state"] = state(&machine, &stats);
|
||||||
|
if matches!(case.completion, Completion::VBlanks { .. }) {
|
||||||
|
report["inputs"] = json!(case.inputs.as_deref().unwrap_or_default());
|
||||||
|
report["state"]["vblanks"] = json!(machine.memory().display_position().vblanks);
|
||||||
|
report["state"]["captured_vblank"] = json!(machine.memory().captured_vblank());
|
||||||
|
}
|
||||||
let reason = match outcome {
|
let reason = match outcome {
|
||||||
Outcome::Checkpoint => {
|
Outcome::Checkpoint => {
|
||||||
let checks: Vec<_> = case
|
let checks: Vec<_> = case
|
||||||
|
|
@ -303,6 +423,10 @@ fn run_case(case: &Case, base: &Path) -> Value {
|
||||||
}
|
}
|
||||||
Outcome::StepLimit => "step_limit",
|
Outcome::StepLimit => "step_limit",
|
||||||
Outcome::Stopped => "stopped",
|
Outcome::Stopped => "stopped",
|
||||||
|
Outcome::RenderError(error) => {
|
||||||
|
report["error"] = json!(error);
|
||||||
|
"render_error"
|
||||||
|
}
|
||||||
Outcome::EmulationError(error) => {
|
Outcome::EmulationError(error) => {
|
||||||
report["error"] = json!(error);
|
report["error"] = json!(error);
|
||||||
"emulation_error"
|
"emulation_error"
|
||||||
|
|
@ -330,7 +454,7 @@ pub fn execute(path: &Path, writer: &mut impl Write) -> Result<(), Box<dyn Error
|
||||||
.collect();
|
.collect();
|
||||||
let failed = cases.iter().filter(|case| case["passed"] != true).count();
|
let failed = cases.iter().filter(|case| case["passed"] != true).count();
|
||||||
let report = json!({
|
let report = json!({
|
||||||
"format_version": 1, "bios": "original", "passed": failed == 0,
|
"format_version": suite.version, "bios": "original", "passed": failed == 0,
|
||||||
"case_count": cases.len(), "failed_count": failed, "cases": cases,
|
"case_count": cases.len(), "failed_count": failed, "cases": cases,
|
||||||
});
|
});
|
||||||
write_json_report(writer, &report)?;
|
write_json_report(writer, &report)?;
|
||||||
|
|
@ -340,5 +464,7 @@ pub fn execute(path: &Path, writer: &mut impl Write) -> Result<(), Box<dyn Error
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod gameplay_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
||||||
142
crates/desktop/src/cartridge/suite/gameplay_tests.rs
Normal file
142
crates/desktop/src/cartridge/suite/gameplay_tests.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
use super::tests::{boot, suite};
|
||||||
|
use super::*;
|
||||||
|
use gba_core::io::{DISPCNT, KEYINPUT};
|
||||||
|
|
||||||
|
fn value() -> Value {
|
||||||
|
json!({"name":"input", "rom":"original.gba", "step_limit":1000000,
|
||||||
|
"completion":{"vblanks":4},
|
||||||
|
"inputs":[{"vblank":0,"buttons":1}, {"vblank":1,"buttons":17}, {"vblank":3,"buttons":0}],
|
||||||
|
"checks":[{"kind":"pixel","x":0,"y":0,"equals":255}]})
|
||||||
|
}
|
||||||
|
fn config(value: Value) -> Case {
|
||||||
|
suite(json!({"version":2,"cases":[value]}))
|
||||||
|
.unwrap()
|
||||||
|
.cases
|
||||||
|
.remove(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strict_gameplay_schema_and_bounds() {
|
||||||
|
assert!(suite(json!({"version":1,"cases":[value()]})).is_err());
|
||||||
|
assert!(suite(json!({"version":2,"cases":[value()]})).is_ok());
|
||||||
|
for (pointer, replacement) in [
|
||||||
|
("/completion", json!({"vblanks":0})),
|
||||||
|
("/completion", json!({"vblanks":100001})),
|
||||||
|
(
|
||||||
|
"/completion",
|
||||||
|
json!({"vblanks":1,"pc":0,"instruction_set":"arm"}),
|
||||||
|
),
|
||||||
|
("/completion", json!({"vblanks":1,"typo":0})),
|
||||||
|
("/completion", json!({"pc":0})),
|
||||||
|
("/completion", json!({"pc":0,"instruction_set":"arm"})),
|
||||||
|
("/inputs", json!([{"vblank":0,"buttons":1024}])),
|
||||||
|
("/inputs", json!([{"vblank":4,"buttons":0}])),
|
||||||
|
(
|
||||||
|
"/inputs",
|
||||||
|
json!([{"vblank":1,"buttons":0},{"vblank":1,"buttons":1}]),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/inputs",
|
||||||
|
json!([{"vblank":2,"buttons":0},{"vblank":1,"buttons":1}]),
|
||||||
|
),
|
||||||
|
("/inputs", json!([{"vblank":0,"buttons":0,"typo":0}])),
|
||||||
|
("/inputs", json!(vec![json!({"vblank":0,"buttons":0}); 257])),
|
||||||
|
("/inputs/0/buttons", json!(-1)),
|
||||||
|
("/inputs/0/vblank", json!(0.5)),
|
||||||
|
("/checks/0/x", json!(240)),
|
||||||
|
("/checks/0/y", json!(160)),
|
||||||
|
("/checks/0/equals", json!(0x1000000)),
|
||||||
|
] {
|
||||||
|
let mut input = value();
|
||||||
|
*input.pointer_mut(pointer).unwrap() = replacement;
|
||||||
|
assert!(
|
||||||
|
suite(json!({"version":2,"cases":[input]})).is_err(),
|
||||||
|
"{pointer}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut input = value();
|
||||||
|
input["completion"]["vblanks"] = json!(100000);
|
||||||
|
input["inputs"] = json!([{"vblank":99999,"buttons":"0x3ff"}]);
|
||||||
|
input["checks"][0] = json!({"kind":"pixel","x":239,"y":159,"equals":"0xffffff"});
|
||||||
|
config(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cpu_observes_initial_held_replaced_and_released_snapshots() {
|
||||||
|
for (target, expected, keys) in [
|
||||||
|
(1, 0xff0000, 0x3fe),
|
||||||
|
(2, 0x00ff00, 0x3ee),
|
||||||
|
(3, 0x00ff00, 0x3ee),
|
||||||
|
(4, 0x0000ff, 0x3ff),
|
||||||
|
] {
|
||||||
|
let mut input = value();
|
||||||
|
input["completion"]["vblanks"] = json!(target);
|
||||||
|
input["inputs"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.retain(|event| event["vblank"].as_u64().unwrap() < target);
|
||||||
|
input["checks"][0]["equals"] = json!(expected);
|
||||||
|
let case = config(input);
|
||||||
|
let mut machine = bios::boot(gba_demos::input_rom()).unwrap();
|
||||||
|
let (_, outcome) = run_to_checkpoint(&mut machine, &case);
|
||||||
|
assert_eq!(outcome, Outcome::Checkpoint);
|
||||||
|
assert_eq!(machine.memory().captured_vblank(), Some(target));
|
||||||
|
assert_eq!(machine.memory().read16(KEYINPUT).unwrap(), keys);
|
||||||
|
assert_eq!(check(&machine, &case.checks[0])["passed"], true);
|
||||||
|
// A post-capture palette write must not change the asserted image.
|
||||||
|
machine.memory_mut().write16(0x05000000, 0x7fff).unwrap();
|
||||||
|
assert_eq!(check(&machine, &case.checks[0])["actual"], expected);
|
||||||
|
let mut frame = Framebuffer::default();
|
||||||
|
machine.memory().present_frame(&mut frame).unwrap();
|
||||||
|
assert!(frame.pixels().iter().all(|pixel| *pixel == expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_completion_accepts_final_step_but_not_a_short_budget() {
|
||||||
|
let mut case = config(value());
|
||||||
|
let run =
|
||||||
|
|case: &Case| run_to_checkpoint(&mut bios::boot(gba_demos::input_rom()).unwrap(), case);
|
||||||
|
let (stats, outcome) = run(&case);
|
||||||
|
assert_eq!(outcome, Outcome::Checkpoint);
|
||||||
|
case.step_limit = stats.steps;
|
||||||
|
assert_eq!(run(&case).1, Outcome::Checkpoint);
|
||||||
|
case.step_limit -= 1;
|
||||||
|
assert_eq!(run(&case).1, Outcome::StepLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_frame_is_not_a_black_pixel_pass_and_mismatch_is_reported() {
|
||||||
|
let mut machine = bios::boot(gba_demos::input_rom()).unwrap();
|
||||||
|
let black = Check::Pixel {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
equals: 0,
|
||||||
|
};
|
||||||
|
let result = check(&machine, &black);
|
||||||
|
assert_eq!(result["passed"], false);
|
||||||
|
assert!(result["actual"].is_null());
|
||||||
|
assert!(result["error"].is_string());
|
||||||
|
run_to_checkpoint(&mut machine, &config(value()));
|
||||||
|
let result = check(&machine, &black);
|
||||||
|
assert_eq!(result["passed"], false);
|
||||||
|
assert_eq!(result["actual"], 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_render_diagnostic_stops_before_later_checkpoint() {
|
||||||
|
let mut machine = boot(&[0xeaff_fffe]);
|
||||||
|
machine.memory_mut().write16(DISPCNT, 7).unwrap();
|
||||||
|
let (stats, outcome) = run_to_checkpoint(&mut machine, &config(value()));
|
||||||
|
assert!(matches!(outcome, Outcome::RenderError(_)));
|
||||||
|
assert_eq!(machine.memory().display_position().vblanks, 1);
|
||||||
|
assert!(stats.steps > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn future_input_does_not_advance_stopped_hardware() {
|
||||||
|
let mut machine = boot(&[0xef03_0000, 0xeaff_fffe]);
|
||||||
|
let (_, outcome) = run_to_checkpoint(&mut machine, &config(value()));
|
||||||
|
assert_eq!(outcome, Outcome::Stopped);
|
||||||
|
assert_eq!(machine.memory().display_position().vblanks, 0);
|
||||||
|
}
|
||||||
|
|
@ -13,12 +13,12 @@ fn case_value() -> Value {
|
||||||
fn case() -> Case {
|
fn case() -> Case {
|
||||||
serde_json::from_value(case_value()).unwrap()
|
serde_json::from_value(case_value()).unwrap()
|
||||||
}
|
}
|
||||||
fn suite(value: Value) -> Result<Suite, Box<dyn Error>> {
|
pub(super) fn suite(value: Value) -> Result<Suite, Box<dyn Error>> {
|
||||||
let suite: Suite = serde_json::from_value(value)?;
|
let suite: Suite = serde_json::from_value(value)?;
|
||||||
suite.validate()?;
|
suite.validate()?;
|
||||||
Ok(suite)
|
Ok(suite)
|
||||||
}
|
}
|
||||||
fn boot(words: &[u32]) -> Machine {
|
pub(super) fn boot(words: &[u32]) -> Machine {
|
||||||
bios::boot(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
|
bios::boot(words.iter().flat_map(|word| word.to_le_bytes()).collect()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ fn unknown_fields_and_unknown_check_kinds_are_rejected() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manifest_rejects_empty_duplicate_excessive_or_invalid_cases() {
|
fn manifest_rejects_empty_duplicate_excessive_or_invalid_cases() {
|
||||||
assert!(suite(json!({"version": 2, "cases": [case_value()]})).is_err());
|
assert!(suite(json!({"version": 3, "cases": [case_value()]})).is_err());
|
||||||
assert!(suite(json!({"version": 1, "cases": []})).is_err());
|
assert!(suite(json!({"version": 1, "cases": []})).is_err());
|
||||||
assert!(suite(json!({"version": 1, "cases": [case_value(), case_value()]})).is_err());
|
assert!(suite(json!({"version": 1, "cases": [case_value(), case_value()]})).is_err());
|
||||||
for (field, invalid) in [
|
for (field, invalid) in [
|
||||||
|
|
@ -184,7 +184,10 @@ fn json_report_propagates_write_and_flush_failures() {
|
||||||
#[test]
|
#[test]
|
||||||
fn reset_pc_is_not_an_immediate_pass_without_execution() {
|
fn reset_pc_is_not_an_immediate_pass_without_execution() {
|
||||||
let mut config = case();
|
let mut config = case();
|
||||||
config.completion.pc = 0;
|
config.completion = Completion::Pc {
|
||||||
|
pc: 0,
|
||||||
|
instruction_set: State::Arm,
|
||||||
|
};
|
||||||
config.step_limit = 1;
|
config.step_limit = 1;
|
||||||
let (stats, outcome) = run_to_checkpoint(&mut boot(&[0xeaff_fffe]), &config);
|
let (stats, outcome) = run_to_checkpoint(&mut boot(&[0xeaff_fffe]), &config);
|
||||||
assert_eq!(stats.steps, 1);
|
assert_eq!(stats.steps, 1);
|
||||||
|
|
@ -215,14 +218,19 @@ fn checkpoint_is_checked_before_execution_and_on_the_last_budgeted_step() {
|
||||||
#[test]
|
#[test]
|
||||||
fn matching_assertions_do_not_pass_without_the_checkpoint() {
|
fn matching_assertions_do_not_pass_without_the_checkpoint() {
|
||||||
let mut config = case();
|
let mut config = case();
|
||||||
config.completion.pc = ROM_START + 8;
|
config.completion = Completion::Pc {
|
||||||
|
pc: ROM_START + 8,
|
||||||
|
instruction_set: State::Arm,
|
||||||
|
};
|
||||||
let mut machine = boot(&[0xe3a0_002a, 0xeaff_fffe]);
|
let mut machine = boot(&[0xe3a0_002a, 0xeaff_fffe]);
|
||||||
let (stats, outcome) = run_to_checkpoint(&mut machine, &config);
|
let (stats, outcome) = run_to_checkpoint(&mut machine, &config);
|
||||||
assert_eq!(outcome, Outcome::StepLimit);
|
assert_eq!(outcome, Outcome::StepLimit);
|
||||||
assert_eq!(stats.steps, config.step_limit);
|
assert_eq!(stats.steps, config.step_limit);
|
||||||
assert_eq!(check(&machine, &config.checks[0])["passed"], true);
|
assert_eq!(check(&machine, &config.checks[0])["passed"], true);
|
||||||
config.completion.pc = ROM_START + 4;
|
config.completion = Completion::Pc {
|
||||||
config.completion.instruction_set = State::Thumb;
|
pc: ROM_START + 4,
|
||||||
|
instruction_set: State::Thumb,
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
run_to_checkpoint(&mut boot(&[0xe3a0_002a, 0xeaff_fffe]), &config).1,
|
run_to_checkpoint(&mut boot(&[0xe3a0_002a, 0xeaff_fffe]), &config).1,
|
||||||
Outcome::StepLimit
|
Outcome::StepLimit
|
||||||
|
|
@ -238,8 +246,10 @@ fn thumb_checkpoint_uses_aligned_architectural_pc() {
|
||||||
bytes.extend([0x2a, 0x20, 0xfe, 0xe7]);
|
bytes.extend([0x2a, 0x20, 0xfe, 0xe7]);
|
||||||
let mut machine = bios::boot(bytes).unwrap();
|
let mut machine = bios::boot(bytes).unwrap();
|
||||||
let mut config = case();
|
let mut config = case();
|
||||||
config.completion.pc = ROM_START + 14;
|
config.completion = Completion::Pc {
|
||||||
config.completion.instruction_set = State::Thumb;
|
pc: ROM_START + 14,
|
||||||
|
instruction_set: State::Thumb,
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
run_to_checkpoint(&mut machine, &config).1,
|
run_to_checkpoint(&mut machine, &config).1,
|
||||||
Outcome::Checkpoint
|
Outcome::Checkpoint
|
||||||
|
|
@ -250,7 +260,10 @@ fn thumb_checkpoint_uses_aligned_architectural_pc() {
|
||||||
#[test]
|
#[test]
|
||||||
fn memory_and_cpsr_checks_report_actual_values_and_read_errors() {
|
fn memory_and_cpsr_checks_report_actual_values_and_read_errors() {
|
||||||
let mut config = case();
|
let mut config = case();
|
||||||
config.completion.pc = ROM_START + 12;
|
config.completion = Completion::Pc {
|
||||||
|
pc: ROM_START + 12,
|
||||||
|
instruction_set: State::Arm,
|
||||||
|
};
|
||||||
let mut machine = boot(&[0xe3a0_002a, 0xe3a0_1402, 0xe581_0000, 0xeaff_fffe]);
|
let mut machine = boot(&[0xe3a0_002a, 0xe3a0_1402, 0xe581_0000, 0xeaff_fffe]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
run_to_checkpoint(&mut machine, &config).1,
|
run_to_checkpoint(&mut machine, &config).1,
|
||||||
|
|
@ -291,7 +304,10 @@ fn memory_and_cpsr_checks_report_actual_values_and_read_errors() {
|
||||||
#[test]
|
#[test]
|
||||||
fn stop_is_failure_while_halt_consumes_a_bounded_budget() {
|
fn stop_is_failure_while_halt_consumes_a_bounded_budget() {
|
||||||
let mut config = case();
|
let mut config = case();
|
||||||
config.completion.pc = ROM_START + 8;
|
config.completion = Completion::Pc {
|
||||||
|
pc: ROM_START + 8,
|
||||||
|
instruction_set: State::Arm,
|
||||||
|
};
|
||||||
let (stats, outcome) = run_to_checkpoint(&mut boot(&[0xef03_0000, 0xeaff_fffe]), &config);
|
let (stats, outcome) = run_to_checkpoint(&mut boot(&[0xef03_0000, 0xeaff_fffe]), &config);
|
||||||
assert_eq!(outcome, Outcome::Stopped);
|
assert_eq!(outcome, Outcome::Stopped);
|
||||||
assert!(stats.steps < config.step_limit);
|
assert!(stats.steps < config.step_limit);
|
||||||
|
|
@ -322,7 +338,10 @@ fn dma_and_irq_each_consume_a_budget_step_and_dma_errors_fail() {
|
||||||
let mut machine = boot(&[0xe3a0_002a, 0xeaff_fffe]);
|
let mut machine = boot(&[0xe3a0_002a, 0xeaff_fffe]);
|
||||||
run_to_checkpoint(&mut machine, &case());
|
run_to_checkpoint(&mut machine, &case());
|
||||||
let mut config = case();
|
let mut config = case();
|
||||||
config.completion.pc = ROM_START + 8;
|
config.completion = Completion::Pc {
|
||||||
|
pc: ROM_START + 8,
|
||||||
|
instruction_set: State::Arm,
|
||||||
|
};
|
||||||
config.step_limit = 3;
|
config.step_limit = 3;
|
||||||
let bus = machine.memory_mut();
|
let bus = machine.memory_mut();
|
||||||
bus.write32(DMA_BASE, 0x0200_0000).unwrap();
|
bus.write32(DMA_BASE, 0x0200_0000).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,49 @@ fn malformed_or_oversized_suites_fail_before_any_case_execution() {
|
||||||
assert!(output.stdout.is_empty());
|
assert!(output.stdout.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gameplay_reports_inputs_captured_pixels_and_failures_repeatably() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fs::write(fixture.0.join("input.gba"), gba_demos::input_rom()).unwrap();
|
||||||
|
fixture.rom(
|
||||||
|
"bad-video.gba",
|
||||||
|
&[0xe3a0_1301, 0xe3a0_0007, 0xe1c1_00b0, 0xeaff_fffe],
|
||||||
|
);
|
||||||
|
let good = json!({"name":"gameplay", "rom":"input.gba", "step_limit":1000000,
|
||||||
|
"completion":{"vblanks":3},
|
||||||
|
"inputs":[{"vblank":0,"buttons":1},{"vblank":1,"buttons":17},{"vblank":2,"buttons":0}],
|
||||||
|
"checks":[{"kind":"pixel","x":239,"y":159,"equals":255}]});
|
||||||
|
let mut mismatch = good.clone();
|
||||||
|
mismatch["name"] = json!("mismatch");
|
||||||
|
mismatch["checks"][0]["equals"] = json!(0);
|
||||||
|
let mut render_error = good.clone();
|
||||||
|
render_error["name"] = json!("bad-video");
|
||||||
|
render_error["rom"] = json!("bad-video.gba");
|
||||||
|
let path = fixture.0.join("gameplay.json");
|
||||||
|
fs::write(
|
||||||
|
&path,
|
||||||
|
serde_json::to_vec(&json!({"version":2,"cases":[good,mismatch,render_error]})).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let first = run(&path);
|
||||||
|
assert_eq!(first.status.code(), Some(1));
|
||||||
|
assert_eq!(first.stdout, run(&path).stdout);
|
||||||
|
let report = report(&first);
|
||||||
|
assert_eq!(report["format_version"], 2);
|
||||||
|
assert_eq!(report["failed_count"], 2);
|
||||||
|
let good = &report["cases"][0];
|
||||||
|
assert_eq!(good["passed"], true);
|
||||||
|
assert_eq!(good["state"]["vblanks"], 3);
|
||||||
|
assert_eq!(good["state"]["captured_vblank"], 3);
|
||||||
|
assert_eq!(good["inputs"][1]["buttons"], 17);
|
||||||
|
assert_eq!(good["checks"][0]["actual"], 255);
|
||||||
|
assert_eq!(report["cases"][1]["reason"], "assertion_failed");
|
||||||
|
assert_eq!(report["cases"][2]["reason"], "render_error");
|
||||||
|
assert_eq!(report["cases"][2]["state"]["vblanks"], 1);
|
||||||
|
assert_eq!(report["cases"][2]["checks"], json!([]));
|
||||||
|
assert!(report["cases"][2]["error"].is_string());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn suite_mode_rejects_other_modes_and_help_advertises_it() {
|
fn suite_mode_rejects_other_modes_and_help_advertises_it() {
|
||||||
for args in [
|
for args in [
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ The repository is a Cargo workspace with three crates:
|
||||||
| `crates/desktop/src/cartridge.rs` | Read-only ROM files, mode/limit checks, bounded terminal execution, and shared state reports |
|
| `crates/desktop/src/cartridge.rs` | Read-only ROM files, mode/limit checks, bounded terminal execution, and shared state reports |
|
||||||
| `crates/desktop/src/cartridge/suite.rs` | Strict JSON manifests, bounded checkpoint execution, assertions, and structured reports |
|
| `crates/desktop/src/cartridge/suite.rs` | Strict JSON manifests, bounded checkpoint execution, assertions, and structured reports |
|
||||||
| `crates/desktop/src/cartridge/suite/tests.rs` | Manifest limits, checkpoint boundaries, memory/register checks, and diagnostic accounting |
|
| `crates/desktop/src/cartridge/suite/tests.rs` | Manifest limits, checkpoint boundaries, memory/register checks, and diagnostic accounting |
|
||||||
|
| `crates/desktop/src/cartridge/suite/gameplay_tests.rs` | VBlank completion, input snapshots, captured pixels, and rendering failures |
|
||||||
| `crates/desktop/tests/rom_suite_cli.rs` | File-backed suite execution, case isolation, output schema, determinism, and exit status |
|
| `crates/desktop/tests/rom_suite_cli.rs` | File-backed suite execution, case isolation, output schema, determinism, and exit status |
|
||||||
| `crates/desktop/examples/write_test_suite.rs` | Original ARM/Thumb/BIOS fixture generator with a local JSON suite |
|
| `crates/desktop/examples/write_test_suite.rs` | Original ARM/Thumb/BIOS fixture generator with a local JSON suite |
|
||||||
| `crates/desktop/src/cartridge/window.rs` | ROM-window session, bounded slices, capture, STOP/input handling, and progress limits |
|
| `crates/desktop/src/cartridge/window.rs` | ROM-window session, bounded slices, capture, STOP/input handling, and progress limits |
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Headless ROM test suites
|
# Headless ROM test suites
|
||||||
|
|
||||||
Use `--test-suite PATH.json` to run repeatable ROM checks without opening a window.
|
Use `--test-suite PATH.json` to run repeatable ROM checks without opening a window.
|
||||||
Each case has a machine-step budget, a completion address, and explicit assertions.
|
Each case has a machine-step budget, a completion condition, and explicit assertions.
|
||||||
A timeout never counts as a pass. Existing `--rom ... --steps ...` remains a diagnostic mode, not a test verdict.
|
A timeout never counts as a pass. Existing `--rom ... --steps ...` remains a diagnostic mode, not a test verdict.
|
||||||
|
|
||||||
Original programs cover the runner itself. Pinned public [ARM](public-arm-tests.md), [Thumb](public-thumb-tests.md), [memory](public-memory-tests.md), and [BIOS](public-bios-tests.md) ROMs provide independent compatibility results.
|
Original programs cover the runner itself. Pinned public [ARM](public-arm-tests.md), [Thumb](public-thumb-tests.md), [memory](public-memory-tests.md), and [BIOS](public-bios-tests.md) ROMs provide independent compatibility results.
|
||||||
|
|
@ -71,12 +71,55 @@ The runner validates the whole manifest before opening or executing any ROM.
|
||||||
It uses the same read-only [ROM loader](hardware/cartridge.md#loading-and-boot), with its 4-byte minimum and 32 MiB maximum.
|
It uses the same read-only [ROM loader](hardware/cartridge.md#loading-and-boot), with its 4-byte minimum and 32 MiB maximum.
|
||||||
The CLI accepts no other options with `--test-suite`.
|
The CLI accepts no other options with `--test-suite`.
|
||||||
|
|
||||||
|
## Manifest version 2: gameplay checks
|
||||||
|
|
||||||
|
Version 2 accepts version 1 PC checkpoints and adds VBlank completion, button snapshots, and captured-pixel checks.
|
||||||
|
Version 1 behavior and report fields remain unchanged.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"cases": [{
|
||||||
|
"name": "original-input-release",
|
||||||
|
"rom": "input.gba",
|
||||||
|
"step_limit": 1000000,
|
||||||
|
"completion": {"vblanks": 3},
|
||||||
|
"inputs": [
|
||||||
|
{"vblank": 0, "buttons": "0x001"},
|
||||||
|
{"vblank": 1, "buttons": "0x011"},
|
||||||
|
{"vblank": 2, "buttons": 0}
|
||||||
|
],
|
||||||
|
"checks": [{"kind": "pixel", "x": 239, "y": 159, "equals": "0x0000ff"}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example uses the original input ROM from `cargo run --example write_rom_demo -- /tmp/input.gba`.
|
||||||
|
Place the manifest alongside that ROM. The CPU writes red for A, green for Right, and blue after release.
|
||||||
|
|
||||||
|
- Use exactly one completion form: PC/state or `vblanks`. Mixed forms are invalid.
|
||||||
|
- VBlank completion requires an integer count from 1 through 100,000, measured from machine reset.
|
||||||
|
- The existing step budget still applies. A VBlank target does not permit unbounded execution.
|
||||||
|
- `inputs` is optional and only valid with VBlank completion. Supply at most 256 snapshots.
|
||||||
|
- Snapshot VBlank numbers must increase strictly and remain below the completion count. Zero applies before the first step.
|
||||||
|
- Each snapshot replaces all held buttons. Its state remains active until the next snapshot.
|
||||||
|
- Button masks use bits 0–9: A, B, Select, Start, Right, Left, Up, Down, R, L. One means pressed.
|
||||||
|
- Snapshots apply at the first machine-step boundary at or after their VBlank, after capture and before the next step.
|
||||||
|
- Pixel checks require VBlank completion, `x` from 0–239, `y` from 0–159, and packed `0xRRGGBB` expected values.
|
||||||
|
- Pixel checks read the latest completed scanline capture, not a snapshot of current video RAM.
|
||||||
|
- All VBlank cases enable capture. The first published render diagnostic fails the case, even before its target.
|
||||||
|
- A missing captured frame is an error, never an implicit black image.
|
||||||
|
|
||||||
|
Input timing is deterministic but not sub-instruction accurate. A snapshot cannot affect the frame already captured at that VBlank.
|
||||||
|
STOP still fails: emulated VBlanks cannot advance to a future scheduled wake-up event.
|
||||||
|
This is a regression runner, not a replacement for manual gameplay and host-keyboard checks.
|
||||||
|
|
||||||
## Completion and failure semantics
|
## Completion and failure semantics
|
||||||
|
|
||||||
Each case starts with fresh RAM, devices, CPU state, and the original BIOS replacement.
|
Each case starts with fresh RAM, devices, CPU state, and the original BIOS replacement.
|
||||||
The budget includes BIOS boot, instructions, DMA units, IRQ entries, and HALT idle batches.
|
The budget includes BIOS boot, instructions, DMA units, IRQ entries, and HALT idle batches.
|
||||||
A failed CPU or DMA step does not consume a successful step or advance the reported state.
|
A failed CPU or DMA step does not consume a successful step or advance the reported state.
|
||||||
The runner supplies no input and does not enable frame capture. Device clocks still advance through normal machine stepping.
|
PC-checkpoint cases supply no input and do not enable frame capture. Device clocks still advance through normal machine stepping.
|
||||||
|
|
||||||
After successful progress, the runner checks PC and instruction set at machine-step boundaries.
|
After successful progress, the runner checks PC and instruction set at machine-step boundaries.
|
||||||
It stops before executing the instruction at the completion address.
|
It stops before executing the instruction at the completion address.
|
||||||
|
|
@ -96,13 +139,13 @@ A failure does not skip later cases. No machine state passes from one case to an
|
||||||
The budget bounds machine steps, not elapsed host time. Use an external process timeout in continuous integration if needed.
|
The budget bounds machine steps, not elapsed host time. Use an external process timeout in continuous integration if needed.
|
||||||
This is not a filesystem security sandbox. Run trusted suite configurations and use files you may lawfully use.
|
This is not a filesystem security sandbox. Run trusted suite configurations and use files you may lawfully use.
|
||||||
|
|
||||||
## Report version 1 and exit status
|
## Reports and exit status
|
||||||
|
|
||||||
Top-level fields:
|
Top-level fields:
|
||||||
|
|
||||||
| Field | Meaning |
|
| Field | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `format_version` | Report schema version, currently 1 |
|
| `format_version` | Manifest version: 1 or 2 |
|
||||||
| `bios` | `original`; no external BIOS option |
|
| `bios` | `original`; no external BIOS option |
|
||||||
| `passed` | True only when every case passed |
|
| `passed` | True only when every case passed |
|
||||||
| `case_count`, `failed_count` | Number of executed cases and failures |
|
| `case_count`, `failed_count` | Number of executed cases and failures |
|
||||||
|
|
@ -115,6 +158,9 @@ A load failure has null `state` and `rom_bytes`.
|
||||||
`checks` contains the configured check, actual value, pass flag, and optional read error when the checkpoint was reached.
|
`checks` contains the configured check, actual value, pass flag, and optional read error when the checkpoint was reached.
|
||||||
Otherwise `checks` is empty. A failed read has a null actual value.
|
Otherwise `checks` is empty. A failed read has a null actual value.
|
||||||
|
|
||||||
|
Version 2 VBlank cases also report their normalized `inputs` and state counters `vblanks` and `captured_vblank`.
|
||||||
|
These state fields are absent when loading fails. A PC case retains the version 1 case layout.
|
||||||
|
|
||||||
Case reasons are:
|
Case reasons are:
|
||||||
|
|
||||||
- `checkpoint`: completion reached and all assertions passed.
|
- `checkpoint`: completion reached and all assertions passed.
|
||||||
|
|
@ -123,6 +169,7 @@ Case reasons are:
|
||||||
- `stopped`: STOP prevented further execution.
|
- `stopped`: STOP prevented further execution.
|
||||||
- `emulation_error`: an instruction or DMA diagnostic stopped execution; `error` contains its message.
|
- `emulation_error`: an instruction or DMA diagnostic stopped execution; `error` contains its message.
|
||||||
- `load_error`: loading or machine creation failed; `error` contains its message.
|
- `load_error`: loading or machine creation failed; `error` contains its message.
|
||||||
|
- `render_error`: frame capture failed or no complete frame was available at VBlank; `error` contains its message.
|
||||||
|
|
||||||
Exit status 0 means all cases passed and the report was written successfully.
|
Exit status 0 means all cases passed and the report was written successfully.
|
||||||
Any failed case, invalid configuration, or output failure returns a nonzero status.
|
Any failed case, invalid configuration, or output failure returns a nonzero status.
|
||||||
|
|
@ -143,5 +190,6 @@ The ROM, assertions, checkpoint, and budget remain unchanged. This is not a comp
|
||||||
For further public ARM7TDMI/GBA tests, inspect their source, license, entry assumptions, and result protocol.
|
For further public ARM7TDMI/GBA tests, inspect their source, license, entry assumptions, and result protocol.
|
||||||
Pin source revisions and build instructions before comparing emulator changes.
|
Pin source revisions and build instructions before comparing emulator changes.
|
||||||
Adapt suites to verified completion addresses and result locations; do not guess them or treat timeouts as passes.
|
Adapt suites to verified completion addresses and result locations; do not guess them or treat timeouts as passes.
|
||||||
Some tests need debug-port logging, input, different firmware behavior, or rendered-image checks that this runner does not support yet.
|
Version 2 supports scheduled input and exact captured-pixel checks.
|
||||||
|
Debug-port logging, external firmware selection, audio assertions, and whole-image comparisons remain unsupported.
|
||||||
Keep external binaries local. Record consulted sources in [references.md](references.md).
|
Keep external binaries local. Record consulted sources in [references.md](references.md).
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ Update this file when a feature lands or a limit is removed.
|
||||||
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
- Raw ROM-file loading with original BIOS boot, terminal diagnostics, and final CPU/step reports.
|
||||||
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
|
- ROM windows with scanline capture, keyboard input, focus-loss release, bounded execution slices, and interactive keypad wake from STOP.
|
||||||
- Optional ROM-window frame limits and an original input-test ROM generator.
|
- Optional ROM-window frame limits and an original input-test ROM generator.
|
||||||
- Headless JSON ROM suites with per-case checkpoints, register/CPSR/memory assertions, bounded budgets, and structured failure reports.
|
- Headless JSON ROM suites with PC or VBlank completion, register/CPSR/memory/pixel assertions, scheduled buttons, bounded budgets, and structured failure reports.
|
||||||
- Hash-pinned preparation of public `jsmolka/gba-tests` ARM, Thumb, memory, and BIOS ROMs, with verified result-register checkpoints.
|
- Hash-pinned preparation of public `jsmolka/gba-tests` ARM, Thumb, memory, and BIOS ROMs, with verified result-register checkpoints.
|
||||||
- These pinned ROMs pass their checkpoints on Darwin arm64 in debug and release builds; coverage limits remain documented.
|
- These pinned ROMs pass their checkpoints on Darwin arm64 in debug and release builds; coverage limits remain documented.
|
||||||
- The original BIOS exposes documented boot, SoftReset, SWI, and IRQ protected-read words through its actual exit layout.
|
- The original BIOS exposes documented boot, SoftReset, SWI, and IRQ protected-read words through its actual exit layout.
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ Do not set a Linux cross-compilation target for this validation.
|
||||||
|
|
||||||
The [headless ROM suite runner](rom-tests.md) provides explicit checkpoint and assertion results through `--test-suite PATH.json`.
|
The [headless ROM suite runner](rom-tests.md) provides explicit checkpoint and assertion results through `--test-suite PATH.json`.
|
||||||
Its tests verify manifest limits, whole-suite validation, exact step boundaries, ARM/Thumb checkpoints, and register/CPSR/memory checks.
|
Its tests verify manifest limits, whole-suite validation, exact step boundaries, ARM/Thumb checkpoints, and register/CPSR/memory checks.
|
||||||
|
Version 2 tests cover VBlank targets, initial/held/released input, captured RGB888 pixels, and render diagnostics.
|
||||||
|
The original input ROM verifies CPU-observed button snapshots and every captured pixel without external game assets.
|
||||||
Additional tests cover DMA/IRQ accounting, HALT/STOP, diagnostic state, case isolation, repeatable JSON output, and failure exit status.
|
Additional tests cover DMA/IRQ accounting, HALT/STOP, diagnostic state, case isolation, repeatable JSON output, and failure exit status.
|
||||||
These original tests validate the runner. The [public ARM adapter](public-arm-tests.md) provides a separate passing checkpoint result for its pinned ROM.
|
These original tests validate the runner. The [public ARM adapter](public-arm-tests.md) provides a separate passing checkpoint result for its pinned ROM.
|
||||||
The [public Thumb adapter](public-thumb-tests.md) uses a separate lock and r7 assertion after its return to ARM state.
|
The [public Thumb adapter](public-thumb-tests.md) uses a separate lock and r7 assertion after its return to ARM state.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue