xenia-analysis: unify disasm via xenia-cpu, split ingest/analyze, add sinks
The old src/ppc.rs that re-implemented PPC formatting collapses into a 30-line shim that delegates to xenia-cpu's single-source-of-truth disasm. A new disasm.rs wraps the shared iterator and feeds enriched items (analysis context: function membership, xrefs, mnemonics) into pluggable sinks. Sinks split: text.rs (objdump-like output), json.rs (JSONL stream matching the new xenia dis --json mode), duckdb.rs (the analysis DB ingest). db.rs is restructured into ingest_instructions + write_analysis_results so a run can stop after raw ingest, and a new target_hex column lands on the instructions table. sql_views.rs adds five additive views layered on top of the raw tables. Tests: assert-based JSON-fixture goldens (disasm_goldens) and a PRAGMA-table_info schema golden (db_schema_golden) covering all ingested tables and the SQL views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
123
crates/xenia-analysis/tests/disasm_goldens.rs
Normal file
123
crates/xenia-analysis/tests/disasm_goldens.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! 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<String>,
|
||||
#[serde(default)]
|
||||
ext_operands: Option<String>,
|
||||
#[serde(default)]
|
||||
branch_target: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GoldenFile {
|
||||
rows: Vec<GoldenRow>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user