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.
82 lines
3.2 KiB
Rust
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"]);
|
|
}
|
|
}
|