metronome/rust/track-format/tests/conformance.rs
Me Here be524ce1ea Rust port Stage 1: track-format codec crate (passes the golden vectors)
A third implementation of the track DSL alongside engine.js and app.py, validated
against the same tests/fixtures/track-format.json:

- rust/track-format/: pure parse()/serialize() codec (std + alloc for now; no_std is
  a later refinement). Ports the app.py/engine.js semantics exactly — grouping,
  subdivisions, swing, ghost, polymeter, euclid, GM note-number aliases, unknown->beep,
  default groove (group-start accents), tempo clamp, empty->beep, and the playback-flow
  tokens (rep/end/relative-goto). Carries vol/cd too, so it's the most spec-complete
  of the three.
- tests/conformance.rs: the Rust adapter — reads the shared fixtures, asserts each
  case's normalized form (number-tolerant deep-equal) + serialize idempotency.
- rust/Containerfile + run.sh: Rust toolchain in a container (mirrors hardware/eda/),
  with the thumbv8m.main-none-eabihf target for the eventual RP2350 firmware. Never
  on the host.

Verified: ./rust/run.sh -> cargo test -> conformance + idempotent both pass.
docs/rust-port.md Stage 1 marked done.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:36:59 -05:00

75 lines
2.9 KiB
Rust

//! The Rust adapter for the shared golden vectors — the third implementation alongside
//! `tests/adapters/js_adapter.mjs` and `tests/adapters/py_adapter.py`. Reads the same
//! `tests/fixtures/track-format.json` and asserts each case's normalized form + round-trip.
use serde_json::{json, Value};
use track_format::{parse, serialize, End, Track};
/// Build the neutral normalized structure (docs/track-format.md §5) from a parsed Track.
fn norm(t: &Track) -> Value {
json!({
"bpm": t.bpm,
"bars": t.bars,
"volume": t.volume,
"countMs": t.count_ms,
"ramp": t.ramp.as_ref().map(|r| json!({"start": r.start, "amt": r.amt, "every": r.every})),
"trainer": t.trainer.as_ref().map(|x| json!({"play": x.play, "mute": x.mute})),
"rep": match t.end { None => Value::Null, Some(_) => json!(t.rep.unwrap_or(1)) },
"end": match &t.end {
None => Value::Null,
Some(End::Stop) => json!("stop"),
Some(End::Goto(n)) => json!(n),
},
"lanes": t.lanes.iter().map(|l| json!({
"sound": l.sound, "groups": l.groups, "sub": l.sub, "swing": l.swing,
"poly": l.poly, "mute": l.mute, "gainDb": l.gain_db, "levels": l.levels
})).collect::<Vec<_>>(),
})
}
/// Deep-equal that treats all JSON numbers as f64 (so 0 == 0.0 across the int/float boundary).
fn json_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(),
(Value::Array(x), Value::Array(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(p, q)| json_eq(p, q))
}
(Value::Object(x), Value::Object(y)) => {
x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).map_or(false, |w| json_eq(v, w)))
}
_ => a == b,
}
}
fn fixtures() -> Value {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/fixtures/track-format.json");
let raw = std::fs::read_to_string(path).expect("read fixtures json");
serde_json::from_str(&raw).expect("parse fixtures json")
}
#[test]
fn conformance() {
let doc = fixtures();
let mut fails = 0;
for case in doc["cases"].as_array().unwrap() {
let id = case["id"].as_str().unwrap_or("?");
let patch = case["in"].as_str().unwrap_or("");
let got = norm(&parse(patch));
if !json_eq(&case["norm"], &got) {
fails += 1;
eprintln!("FAIL {id}\n expected: {}\n got: {got}", case["norm"]);
}
}
assert_eq!(fails, 0, "{fails} conformance mismatch(es)");
}
#[test]
fn idempotent() {
let doc = fixtures();
for case in doc["cases"].as_array().unwrap() {
let patch = case["in"].as_str().unwrap_or("");
let c1 = serialize(&parse(patch));
let c2 = serialize(&parse(&c1));
assert_eq!(c1, c2, "non-idempotent serialize for {}", case["id"]);
}
}