//! Analysis-side goldens: every row in the xenia-cpu fixtures must //! round-trip cleanly through the [`xenia_analysis::ppc`] shim. This //! pins the shim's behaviour to the canonical `xenia_cpu::disasm::format` //! output so that any future refactor of the shim layer surfaces here. //! //! Loads the same JSON fixtures committed under //! `crates/xenia-cpu/tests/golden/`. No separate analysis-side fixture //! files — the cpu canon is the source of truth. use std::path::PathBuf; use serde::Deserialize; #[derive(Debug, Deserialize)] struct GoldenRow { label: String, raw: String, addr: String, mnemonic: String, operands: String, #[serde(default)] ext_mnemonic: Option, #[serde(default)] ext_operands: Option, #[serde(default)] branch_target: Option, } #[derive(Debug, Deserialize)] struct GoldenFile { rows: Vec, } fn cpu_fixture(name: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("xenia-cpu") .join("tests") .join("golden") .join(name) } fn parse_hex(s: &str) -> u32 { let trimmed = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s); u32::from_str_radix(trimmed, 16).expect("hex u32") } /// Verify the shim's `Decoded { base, ext }` mirrors the canonical fields /// from `xenia_cpu::disasm::format` for every fixture row. fn check_fixture(fixture_name: &str) { let path = cpu_fixture(fixture_name); assert!( path.exists(), "missing fixture {} — run `cargo test -p xenia-cpu --test disasm_goldens` to (re)generate it", path.display() ); let src = std::fs::read_to_string(&path).unwrap(); let golden: GoldenFile = serde_json::from_str(&src).unwrap(); for row in &golden.rows { let raw = parse_hex(&row.raw); let addr = parse_hex(&row.addr); let canonical = xenia_cpu::disasm::format(&xenia_cpu::decode(raw, addr)); let shim = xenia_analysis::ppc::disasm(raw, addr); assert_eq!( shim.base, canonical.disasm, "shim.base drifted for {} (raw={})", row.label, row.raw, ); assert_eq!( shim.ext, canonical.ext_disasm, "shim.ext drifted for {} (raw={})", row.label, row.raw, ); // Also pin against the fixture's structured fields — guards against // someone changing the cpu canon without regenerating the fixture. assert_eq!(canonical.mnemonic, row.mnemonic, "mnemonic drift: {}", row.label); assert_eq!(canonical.operands, row.operands, "operands drift: {}", row.label); assert_eq!(canonical.ext_mnemonic, row.ext_mnemonic, "ext_mnemonic drift: {}", row.label); assert_eq!(canonical.ext_operands, row.ext_operands, "ext_operands drift: {}", row.label); let target_str = canonical.branch_target.map(|t| format!("0x{t:08X}")); assert_eq!(target_str, row.branch_target, "branch_target drift: {}", row.label); } } #[test] fn analysis_shim_matches_base_mnemonics() { check_fixture("base_mnemonics.json"); } #[test] fn analysis_shim_matches_extended_mnemonics() { check_fixture("extended_mnemonics.json"); } #[test] fn analysis_shim_matches_vmx128_registers() { check_fixture("vmx128_registers.json"); } /// Spot-check that the shim's `display()` returns the extended form when /// present and falls back to the base otherwise. This is the contract /// `formatter.rs` and the .asm output rely on. #[test] fn shim_display_prefers_extended() { // ori r0, r0, 0 → base "ori r0, r0, 0x0", ext "nop" let d = xenia_analysis::ppc::disasm(0x60000000, 0); assert_eq!(d.display(), "nop"); // addi r3, r1, 16 → no extended form, display falls back to base let raw = (14u32 << 26) | (3 << 21) | (1 << 16) | 16; let d = xenia_analysis::ppc::disasm(raw, 0); assert!( d.ext.is_none(), "addi r3, r1, 16 has no extended form (only addi r3, r0, … → li)" ); assert_eq!(d.display(), d.base); }