metronome/rust/track-format/tests/conformance.rs
Me Here cb54b4d689 Preserve notation + grammar feature work (verified complete + green)
The parallel agent's full session, committed now that it's solo:
- Grammar: flam/drag/roll ornaments (f/F d/D z/Z, per-lane orns channel) across
  src/engine.js, pico-cp/pico-explorer/pico-scroll app.py, pico/main.py, rust/track-format,
  + golden vectors / conformance (tests/, rust/track-format/tests).
- Live-sync deep-sync: SysEx 0x44 SLSYNC + 0x45 LOGSYNC (docs/livesync-protocol.md, src/livesync.js).
- PM_E-2 notation: web engine (pm_e-2.html, build/deploy/index/embed wiring) + Rust device port
  (pm-ui draw_notation rewrite + LaneView.groups, pm-kit ViewMode, uisim notesim).

Verified: node tests/run.mjs 47 pass / 1 known; ./rust/run.sh green; pm-kit firmware + uisim compile.
2026-06-02 13:45:26 -05:00

82 lines
3.2 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| {
let mut o = 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
});
// `orns` defaults to all-zeros and is omitted then, so legacy vectors (no `orns`) still match.
if l.orns.iter().any(|&v| v != 0) {
o.as_object_mut().unwrap().insert("orns".into(), json!(l.orns));
}
o
}).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"]);
}
}