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>
75 lines
2.9 KiB
Rust
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"]);
|
|
}
|
|
}
|