// wai-conformance: int-kernels //! `wai generate` and `wai score build|render`, run as a user runs them: the native //! pins a page checks its own output against. Each output is checked against a //! committed vector — `tests/vectors/generate/` for pictures, `score-conformance/` //! for scores — and every refusal is checked to write nothing. //! //! Cargo.toml declares this target with required-features = ["generate", "score"], so a //! run without them stops with an error instead of reporting an empty binary as passed. //! The two larger listed sizes of `instr-v0.json` take seconds in a debug build and run //! in the release step. use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use serde_json::Value; use wai::container::Wai; fn root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } fn gen_dir() -> PathBuf { root().join("tests/vectors/generate") } fn score_dir() -> PathBuf { root().join("../score-conformance") } fn model() -> PathBuf { gen_dir().join("generate_spectral_model.json") } fn jload(p: &Path) -> Value { serde_json::from_slice(&std::fs::read(p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))).unwrap() } fn sha256_hex(b: &[u8]) -> String { wai::pin::sha256(b).iter().map(|x| format!("{x:02x}")).collect() } fn scratch(name: &str) -> PathBuf { let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_generate_score"); std::fs::create_dir_all(&dir).unwrap(); let p = dir.join(name); let _ = std::fs::remove_file(&p); p } fn wai(args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_wai")).args(args).output().expect("run wai") } fn ok(run: &Output) -> String { assert!(run.status.success(), "wai failed: {}", String::from_utf8_lossy(&run.stderr)); String::from_utf8(run.stdout.clone()).unwrap() } fn refused(run: &Output, out: &Path, what: &str) -> String { let stderr = String::from_utf8_lossy(&run.stderr).into_owned(); assert!(!run.status.success(), "{what}: must be refused; stdout: {}", String::from_utf8_lossy(&run.stdout)); assert!(!out.exists(), "{what}: a refusal writes nothing"); stderr } /// The `data` of the JSON envelope (envelope/1) that `wai ... --json` prints. fn data(stdout: &str) -> Value { let env: Value = serde_json::from_str(stdout.trim()).expect("one JSON envelope"); assert_eq!((env["wai"].as_str(), env["ok"].as_bool()), (Some("envelope/1"), Some(true)), "{env}"); env["data"].clone() } /// The `--json` data `wai generate` prints, checked against the file it wrote. fn made(stdout: &str, out: &Path, w: u64, h: u64) -> String { let v: Value = data(stdout); let rgb = std::fs::read(out).unwrap(); assert_eq!((v["w"].as_u64(), v["h"].as_u64()), (Some(w), Some(h))); assert_eq!(v["bytes"].as_u64(), Some(w * h * 3)); assert_eq!(rgb.len() as u64, w * h * 3, "RGB8, w·h·3 bytes"); let sha = v["sha256"].as_str().unwrap().to_string(); assert_eq!(sha256_hex(&rgb), sha, "the printed SHA-256 is the file's"); sha } fn m() -> String { model().to_string_lossy().into_owned() } fn p(path: &Path) -> &str { path.to_str().unwrap() } #[test] fn generate_reproduces_the_committed_vectors() { // 96×96: the committed vector and its SHA-256 sidecar. let pin = jload(&gen_dir().join("generate.json")); let side = jload(&gen_dir().join("sha256.json")); let out = scratch("p96.rgb"); let seed = pin["seed"].to_string(); let steps = pin["steps"].to_string(); let run = wai(&["generate", "--model", &m(), "--prompt", pin["prompt"].as_str().unwrap(), "--seed", &seed, "--steps", &steps, "--size", "96x96", "-o", p(&out), "--json"]); assert_eq!(made(&ok(&run), &out, 96, 96), side["generate"]["sha256"].as_str().unwrap()); // sizes.json: the same prompt, seed and steps at its non-square sizes. let sizes = jload(&gen_dir().join("sizes.json")); for s in sizes["sizes"].as_array().unwrap() { let (w, h) = (s["w"].as_u64().unwrap(), s["h"].as_u64().unwrap()); let out = scratch(&format!("p{w}x{h}.rgb")); let run = wai(&["generate", "--model", &m(), "--prompt", sizes["prompt"].as_str().unwrap(), "--seed", &seed, "--steps", &steps, "--size", &format!("{w}x{h}"), "-o", p(&out), "--json"]); assert_eq!(made(&ok(&run), &out, w, h), s["sha256"].as_str().unwrap(), "{w}x{h}"); } } #[test] fn generate_refuses_flags_outside_form_v0() { let base = ["generate", "--model", &m(), "--prompt", "calm ocean", "--seed", "3", "--steps", "4"]; for (extra, what) in [ (&["--size", "0x128"][..], "a side of 0"), (&["--size", "4097x1"][..], "a side past 4096"), (&["--size", "4096x4096"][..], "more than 2^23 pixels"), (&["--size", "256"][..], "not WxH"), ] { let out = scratch("flags.rgb"); let mut args: Vec<&str> = base.to_vec(); args.extend_from_slice(extra); args.extend_from_slice(&["-o", p(&out)]); refused(&wai(&args), &out, what); } for (flag, value, what) in [ ("--seed", "4294967296", "a seed past 2^32 - 1"), ("--steps", "0", "steps below 1"), ("--steps", "65", "steps past 64"), ("--prompt", "a \"quoted\" prompt", "a quotation mark in the prompt"), ] { let out = scratch("flags.rgb"); let mut args: Vec = base.iter().map(|s| s.to_string()).collect(); let i = args.iter().position(|a| a == flag).unwrap(); args[i + 1] = value.into(); args.extend(["--size", "64x32", "-o", p(&out)].map(String::from)); let args: Vec<&str> = args.iter().map(String::as_str).collect(); refused(&wai(&args), &out, what); } } /// The vector's object, written as `--write-instr` writes it from the same flags. #[test] fn write_instr_writes_the_vectors_object() { let v = jload(&gen_dir().join("instr-v0.json")); let object = v["object"].as_str().unwrap(); let o: Value = serde_json::from_str(object).unwrap(); let sizes: Vec = o["sizes"].as_array().unwrap().iter().map(|s| format!("{}x{}", s[0], s[1])).collect(); let out = scratch("written.instr"); ok(&wai(&["generate", "--model", &m(), "--prompt", o["prompt"].as_str().unwrap(), "--seed", &o["seed"].to_string(), "--steps", &o["steps"].to_string(), "--sizes", &sizes.join(","), "--write-instr", p(&out)])); assert_eq!(std::fs::read(&out).unwrap(), object.as_bytes()); } fn instr_sizes(min_pixels: u64, max_pixels: u64) -> usize { let v = jload(&gen_dir().join("instr-v0.json")); // A file of its own per caller: the two tests that call this run at once, and each // removes and rewrites its object before its `wai` children read it. let object = scratch(&format!("object-{min_pixels}.instr")); std::fs::write(&object, v["object"].as_str().unwrap()).unwrap(); let mut n = 0; for e in v["outputs"].as_array().unwrap() { let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap()); if !(min_pixels..=max_pixels).contains(&(w * h)) { continue; } let out = scratch(&format!("instr{w}x{h}.rgb")); let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"), "-o", p(&out), "--json"]); assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{w}x{h}"); n += 1; } n } #[test] fn instr_makes_the_listed_small_sizes() { assert_eq!(instr_sizes(0, 640 * 320), 2); } #[test] #[cfg_attr(debug_assertions, ignore = "1280x720 and 1920x1080 take seconds in a debug build; the release step runs them")] fn instr_makes_the_listed_large_sizes() { assert_eq!(instr_sizes(640 * 320 + 1, u64::MAX), 2); } /// Every object the vector lists as refused is refused by `wai generate --instr` and /// by `wai wrap`, each for the reason the vector states (the first rule the reader /// finds broken), and neither writes anything. Holding the reason, not only the /// refusal, notices a rule that stops refusing while another rule still refuses the /// same object: a quotation mark in the prompt, say, which the canonical-bytes check /// would refuse if the prompt rule let it through. The vector's own object is accepted /// by both, and the envelope carries it byte for byte with intent `create`. #[test] fn the_vectors_refused_objects_are_refused() { let v = jload(&gen_dir().join("instr-v0.json")); let cases = v["refused"].as_array().unwrap(); assert!(cases.len() >= 30, "the vector lists its refusals"); let object = scratch("refused.instr"); for c in cases { let why = c["why"].as_str().unwrap(); let reason = c["reason"].as_str().unwrap_or_else(|| panic!("{why}: the vector states no reason")); std::fs::write(&object, c["object"].as_str().unwrap()).unwrap(); let out = scratch("refused.rgb"); let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "256x128", "-o", p(&out)]), &out, why); assert!(stderr.contains(&format!("refused: {reason}: ")), "{why}: refused, but not as {reason}: {stderr}"); let env = scratch("refused.wai"); let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]), &env, why); assert!(stderr.contains("instruction object of form v0") && stderr.contains(&format!(": {reason}: ")), "{why}: refused, but not as {reason}: {stderr}"); } std::fs::write(&object, v["object"].as_str().unwrap()).unwrap(); let env = scratch("accepted.wai"); ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)])); let e = Wai::read(&env).unwrap(); assert_eq!((e.capability(), e.intent()), ("wai.generate.spectral", "create")); assert_eq!(e.payload, v["object"].as_str().unwrap().as_bytes()); } /// Every object the vector lists as accepted — one at each inclusive bound of form v0 — /// is accepted by `wai generate --instr`, which makes its first listed size with the /// SHA-256 the vector states, and by `wai wrap`, whose envelope carries it byte for /// byte. A reader stricter than form v0 by one refuses one of them. #[test] fn the_vectors_accepted_objects_are_accepted() { let v = jload(&gen_dir().join("instr-v0.json")); let cases = v["accepted"].as_array().unwrap(); assert!(cases.len() >= 12, "the vector lists an object at each bound"); let object = scratch("accepted.instr"); for c in cases { let why = c["why"].as_str().unwrap(); std::fs::write(&object, c["object"].as_str().unwrap()).unwrap(); for e in c["outputs"].as_array().unwrap() { let (w, h) = (e["w"].as_u64().unwrap(), e["h"].as_u64().unwrap()); let out = scratch("accepted.rgb"); let run = wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", &format!("{w}x{h}"), "-o", p(&out), "--json"]); assert!(run.status.success(), "{why}: refused: {}", String::from_utf8_lossy(&run.stderr)); assert_eq!(made(&ok(&run), &out, w, h), e["sha256"].as_str().unwrap(), "{why}: {w}x{h}"); } let env = scratch("accepted-bound.wai"); let run = wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)]); assert!(run.status.success(), "{why}: wrap refused it: {}", String::from_utf8_lossy(&run.stderr)); assert_eq!(Wai::read(&env).unwrap().payload, c["object"].as_str().unwrap().as_bytes(), "{why}"); } } #[test] fn instr_refuses_another_model_and_an_unlisted_size() { let v = jload(&gen_dir().join("instr-v0.json")); let object = scratch("model.instr"); std::fs::write(&object, v["object"].as_str().unwrap()).unwrap(); // The same model with one more byte (a line feed): the same parameters, another digest. let other = scratch("other-model.json"); let mut bytes = std::fs::read(model()).unwrap(); bytes.push(b'\n'); std::fs::write(&other, bytes).unwrap(); let out = scratch("model.rgb"); let stderr = refused(&wai(&["generate", "--model", p(&other), "--instr", p(&object), "--size", "256x128", "-o", p(&out)]), &out, "another model file"); assert!(stderr.contains("the object's model is sha256:"), "{stderr}"); let stderr = refused(&wai(&["generate", "--model", &m(), "--instr", p(&object), "--size", "96x96", "-o", p(&out)]), &out, "an unlisted size"); assert!(stderr.contains("not a listed size"), "{stderr}"); } #[test] fn wrap_refuses_replicate_over_the_generator() { let v = jload(&gen_dir().join("instr-v0.json")); let object = scratch("replicate.instr"); std::fs::write(&object, v["object"].as_str().unwrap()).unwrap(); for extra in [&[][..], &["--intent", "replicate"][..]] { let env = scratch("replicate.wai"); let mut args = vec!["wrap", "wai.generate.spectral", p(&object), "-o", p(&env)]; args.extend_from_slice(extra); let stderr = refused(&wai(&args), &env, "replicate over a generator"); assert!(stderr.contains("GenerativeDeterministic"), "{stderr}"); } } /// SPEC §5: an envelope that names the generator carries the intent `create`. `wai wrap` /// writes no other: not `improve`, not a string the manifest schema leaves open, and not /// with the generator as the fallback of another capability. #[test] fn wrap_writes_the_generator_only_with_intent_create() { let v = jload(&gen_dir().join("instr-v0.json")); let object = scratch("intent.instr"); std::fs::write(&object, v["object"].as_str().unwrap()).unwrap(); for intent in ["improve", "banana", "Create", ""] { let env = scratch("intent.wai"); let stderr = refused(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", intent, "-o", p(&env)]), &env, &format!("intent {intent:?}")); assert!(stderr.contains("carries the intent `create`"), "{intent:?}: {stderr}"); } let env = scratch("intent.wai"); let stderr = refused(&wai(&["wrap", "wai.image.png", p(&object), "--fallback", "wai.generate.spectral", "--intent", "improve", "-o", p(&env)]), &env, "the generator as the fallback"); assert!(stderr.contains("carries the intent `create`"), "{stderr}"); ok(&wai(&["wrap", "wai.generate.spectral", p(&object), "--intent", "create", "-o", p(&env)])); } /// SPEC §5: `wai inspect` and `wai unwrap` report an envelope that names the generator — /// its payload, its intent and any pin — ✓ when it conforms, ✗ with the reason when it /// does not. Each bad `WAI1` differs from a good one in one respect: one prompt byte /// changed to 0x01 in place (the envelope still parses; the payload is no longer JSON, /// since a string holds no raw control character), the intent `improve`, or a pin /// over the very model file `model` names. A `WAI2` component of the generator is /// reported the same way, with and without a pin. #[test] fn inspect_and_unwrap_report_the_generators_envelope() { use wai::container::{ManifestV2, Prior, RenditionMeta, WaiMulti}; let v = jload(&gen_dir().join("instr-v0.json")); let object = v["object"].as_str().unwrap(); let input = scratch("report.instr"); std::fs::write(&input, object).unwrap(); let good = scratch("report-good.wai"); ok(&wai(&["wrap", "wai.generate.spectral", p(&input), "--intent", "create", "-o", p(&good)])); let report = |env: &Path| (ok(&wai(&["unwrap", p(env)])), ok(&wai(&["inspect", p(env)]))); let (u, i) = report(&good); assert!(u.contains("conformance \u{2713} an instruction object of form v0, intent create, no pin"), "{u}"); assert!(i.contains("instruction: \u{2713}") && !i.contains("does not conform"), "{i}"); let mut bytes = std::fs::read(&good).unwrap(); let at = bytes.windows(4).position(|w| w == b"calm").unwrap(); bytes[at] = 0x01; let bad_payload = scratch("report-payload.wai"); std::fs::write(&bad_payload, &bytes).unwrap(); let mut e = Wai::read(&good).unwrap(); e.manifest.intent = "improve".into(); let bad_intent = scratch("report-intent.wai"); e.write(&bad_intent).unwrap(); let model_sha = sha256_hex(&std::fs::read(model()).unwrap()); let pin = Prior::new("the spectral model", model_sha, "integer generator"); let mut e = Wai::read(&good).unwrap(); e.manifest.conditioning.prior = Some(pin.clone()); let bad_pin = scratch("report-pin.wai"); e.write(&bad_pin).unwrap(); for (env, said) in [(&bad_payload, "not_json"), (&bad_intent, "the intent is `improve`"), (&bad_pin, "carries a pin")] { let (u, i) = report(env); assert!(u.contains("conformance \u{2717}") && u.contains(said), "{said}: {u}"); assert!(i.contains("instruction: \u{2717}") && i.contains(said) && i.contains("does not conform"), "{said}: {i}"); } for (pinned, name) in [(false, "report-two.wai"), (true, "report-two-pin.wai")] { let mut meta = RenditionMeta::new("wai.generate.spectral", "bytes"); if pinned { meta = meta.with_prior(pin.clone()); } let two = WaiMulti::from_parts(ManifestV2::new("image", "create", vec![meta]), vec![object.as_bytes().to_vec()]).unwrap(); let path = scratch(name); two.write(&path).unwrap(); let u = ok(&wai(&["unwrap", p(&path), "--component", "0"])); let i = ok(&wai(&["inspect", p(&path)])); let mark = if pinned { "\u{2717}" } else { "\u{2713}" }; assert!(u.contains(&format!("conformance [0] {mark}")), "{name}: {u}"); assert!(i.contains(&format!("[0] instruction: {mark}")), "{name}: {i}"); assert_eq!(pinned, u.contains("carries a pin") && i.contains("carries a pin"), "{name}"); } } /// Every score of the corpus: `build` from its authoring form writes the committed /// `WSCR` byte for byte, and `render` prints, and `--pcm` writes, the digests /// `expected_hashes.json` states. #[test] fn score_build_and_render_reproduce_the_corpus() { let expected = jload(&score_dir().join("expected_hashes.json")); let entries = expected.as_object().unwrap(); assert_eq!(entries.len(), 8); for (file, spec) in entries { let name = file.strip_suffix(".wscr").unwrap(); let committed = std::fs::read(score_dir().join("vectors").join(file)).unwrap(); let built = scratch(&format!("{name}.wscr")); let notes = score_dir().join("notes").join(format!("{name}.json")); let stdout = ok(&wai(&["score", "build", p(¬es), "-o", p(&built), "--json"])); assert_eq!(std::fs::read(&built).unwrap(), committed, "{name}: built byte for byte"); let b: Value = data(&stdout); assert_eq!(b["bytes"].as_u64(), Some(committed.len() as u64)); assert_eq!(b["score_hash"], spec["score_hash"], "{name}"); let pcm = scratch(&format!("{name}.s16")); let stdout = ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"])); let r: Value = data(&stdout); let duration = spec["duration"].as_u64().unwrap(); let channels = spec["channels"].as_u64().unwrap(); assert_eq!(r["samples"].as_u64(), Some(duration), "{name}: the contract's duration, whole"); assert_eq!(r["sample_rate"], spec["sample_rate"], "{name}"); assert_eq!(r["channels"].as_u64(), Some(channels), "{name}"); assert_eq!(r["notes"], spec["notes"]); assert_eq!(r["pcm_sha256"], spec["pcm_sha256"], "{name}"); assert_eq!(r["mixdown_blake3"], spec["mixdown_hash"], "{name}"); assert_eq!(r["score_hash"], spec["score_hash"], "{name}"); let samples = std::fs::read(&pcm).unwrap(); assert_eq!(samples.len() as u64, duration * channels * 2, "{name}: i16 samples of every channel"); assert_eq!(Some(sha256_hex(&samples).as_str()), spec["pcm_sha256"].as_str(), "{name}: the written PCM"); // The score travels under intent `replicate`: its class is StandardDefined. let env = scratch(&format!("{name}.wai")); ok(&wai(&["wrap", "wai.audio.score", p(&built), "--intent", "replicate", "-o", p(&env)])); let e = Wai::read(&env).unwrap(); assert_eq!((e.capability(), e.intent()), ("wai.audio.score", "replicate")); assert_eq!(e.payload, committed); } } /// The voiced two-channel example of score §2.3, as the extension prints it, builds and /// renders: 72 000 frames of two channels at 48 kHz, interleaved, the note panned left /// (its left louder than its right) and its echoes crossing to the right. #[test] fn the_extension_s_two_channel_example_builds_and_renders() { let md = std::fs::read_to_string(root().join("../extensions/score.md")).unwrap(); let doc = md .split("```json") .skip(1) .map(|b| b.split("```").next().unwrap()) .find(|b| b.contains("\"channels\": 2")) .expect("score §2.3's two-channel example"); let input = scratch("example.json"); std::fs::write(&input, doc).unwrap(); let built = scratch("example.wscr"); ok(&wai(&["score", "build", p(&input), "-o", p(&built)])); let pcm = scratch("example.s16"); let r: Value = data(&ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"]))); assert_eq!((r["samples"].as_u64(), r["channels"].as_u64(), r["sample_rate"].as_u64()), (Some(72000), Some(2), Some(48000)), "{r}"); let b = std::fs::read(&pcm).unwrap(); assert_eq!(b.len(), 72000 * 2 * 2); assert_eq!(Some(sha256_hex(&b).as_str()), r["pcm_sha256"].as_str()); let samples = |b: &[u8]| b.chunks(2).map(|c| i16::from_le_bytes([c[0], c[1]]) as i64).collect::>(); let s = samples(&b); // The same note with no echoes: what the first echo adds before the second starts. std::fs::write(&input, doc.replace("\"echo_taps\": 3", "\"echo_taps\": 0")).unwrap(); let dry_built = scratch("example-dry.wscr"); ok(&wai(&["score", "build", p(&input), "-o", p(&dry_built)])); let dry_pcm = scratch("example-dry.s16"); ok(&wai(&["score", "render", p(&dry_built), "--pcm", p(&dry_pcm)])); let dry = samples(&std::fs::read(&dry_pcm).unwrap()); let energy = |x: &[i64], from: usize, to: usize, ch: usize| (from..to).map(|f| x[2 * f + ch].pow(2)).sum::(); assert!(energy(&s, 0, 12000, 0) > 2 * energy(&s, 0, 12000, 1), "the note sits left"); assert_eq!(s[..2 * 12000], dry[..2 * 12000], "nothing echoes before the first echo"); let echo: Vec = s.iter().zip(&dry).map(|(a, b)| a - b).collect(); assert!(energy(&echo, 12000, 24000, 1) > 2 * energy(&echo, 12000, 24000, 0), "its first echo crosses right"); } #[test] fn score_build_refuses_what_is_not_integer_notes_rendered_whole() { let note = r#"{"start":0,"dur":100,"phase_inc":50960238,"gain":1717986918,"waveform":2,"attack":10,"decay":10,"sustain":2147483648,"release":50}"#; let score = |note: &str, top: &str| format!(r#"{{"sample_rate":22050,"channels":1,"duration":200{top},"notes":[{note}]}}"#); // The note with a voice of the members given. let voiced = |members: &str| note.replace(",\"release\":50}", &format!(",\"release\":50,\"voice\":{{{members}}}}}")); // The same in two channels at 48 kHz. let stereo = |members: &str| score(&voiced(members), "").replace("\"channels\":1", "\"channels\":2").replace("22050", "48000"); // The base case builds, and so does each form of it with a voice. let input = scratch("base.json"); std::fs::write(&input, score(note, "")).unwrap(); let out = scratch("base.wscr"); ok(&wai(&["score", "build", p(&input), "-o", p(&out)])); for doc in [ score(&voiced(""), ""), score(&voiced(r#""decay_curve":8,"lp_poles":4,"lp_a":0,"lp_env":4294967296,"glide_to":0,"glide":200,"seed":4294967295,"echo_taps":1,"echo_delay":50,"echo_gain":4294967296"#), ""), stereo(r#""pan":-4294967296,"echo_pan":4294967296,"echo_taps":8,"echo_delay":6,"echo_gain":2147483648"#), ] { std::fs::write(&input, &doc).unwrap(); ok(&wai(&["score", "build", p(&input), "-o", p(&out)])); let s = wai::score::Score::from_bytes(&std::fs::read(&out).unwrap()).unwrap(); assert!(s.voices.is_some(), "{doc}: a voiced score"); } for (doc, what) in [ (score(¬e.replace("1717986918", "0.4"), ""), "a gain with a fraction"), (score(¬e.replace("1717986918", "4294967297"), ""), "a gain past 1.0"), (score(¬e.replace("50960238", "4294967296"), ""), "a phase_inc of a whole cycle"), (score(¬e.replace("\"waveform\":2", "\"waveform\":9"), ""), "a waveform past 8"), (score(¬e.replace("\"start\":0", "\"start\":-1"), ""), "a negative start"), (score(¬e.replace("\"release\":50", "\"release\":101"), ""), "a release the duration would cut"), (score(¬e.replace("\"dur\":100,", "\"dur\":100,\"pitch\":60,"), ""), "a member that is not a note field"), (score(¬e.replace(",\"release\":50", ""), ""), "a missing note field"), (score(¬e.replace("\"start\":0", "\"start\":0,\"start\":5"), ""), "a note field given twice"), (score(note, ",\"duration\":300"), "a document member given twice"), (score(note, ",\"numeric\":\"wai.det.float\""), "another numeric profile"), (score(note, "").replace("\"channels\":1", "\"channels\":3"), "three channels"), (score(&voiced(r#""pan":1073741824"#), ""), "a pan in one channel"), (score(&voiced(r#""decay_curve":0"#), ""), "a curve of 0"), (score(&voiced(r#""release_curve":9"#), ""), "a curve past 8"), (score(&voiced(r#""lp_poles":5"#), ""), "five low-pass sections"), (score(&voiced(r#""lp_a":4294967297"#), ""), "a low-pass coefficient past 1.0"), (score(&voiced(r#""glide_to":4294967296"#), ""), "a glide to a whole cycle"), (score(&voiced(r#""glide":10"#), ""), "a glide to no pitch"), (score(&voiced(r#""echo_taps":9,"echo_delay":1"#), ""), "nine echoes"), (score(&voiced(r#""echo_taps":1"#), ""), "an echo of no delay"), (score(&voiced(r#""echo_taps":1,"echo_delay":51"#), ""), "an echo the duration would cut"), (score(&voiced(r#""echo_gain":4294967297"#), ""), "an echo louder than its note"), (score(&voiced(r#""seed":4294967296"#), ""), "a seed past 32 bits"), (score(&voiced(r#""cutoff":1"#), ""), "a member that is not a voice field"), (score(&voiced(r#""seed":1,"seed":2"#), ""), "a voice field given twice"), (score(¬e.replace(",\"release\":50}", ",\"release\":50,\"voice\":[]}"), ""), "a voice that is not an object"), (stereo(r#""pan":4294967297"#), "a pan past 1.0"), (stereo(r#""echo_pan":-4294967297"#), "an echo pan past -1.0"), (stereo(r#""pan":0.5"#), "a pan with a fraction"), (score(note, "").replace("\"channels\":1", "\"channels\":2").replace("\"duration\":200", "\"duration\":33554433"), "two channels past 2^26 samples"), (score(note, "").replace("\"duration\":200", "\"duration\":67108865"), "a duration past 2^26 samples"), ("[]".to_string(), "not an object"), ] { std::fs::write(&input, &doc).unwrap(); let out = scratch("refused.wscr"); let stderr = refused(&wai(&["score", "build", p(&input), "-o", p(&out)]), &out, what); assert!(stderr.contains("refused"), "{what}: {stderr}"); } } /// `render` refuses a contract it would misreport: the reference mixdown is one channel /// or two in `wai.det.fixed64`, so a contract stating three channels or another numeric /// profile is refused, as `build` refuses to write one. Each case is the committed /// scale.wscr with its contract patched in place. #[test] fn score_render_refuses_a_contract_it_does_not_render() { let committed = std::fs::read(score_dir().join("vectors/scale.wscr")).unwrap(); let patch = |from: &str, to: &str| { assert_eq!(from.len(), to.len(), "a patch in place keeps every offset"); let at = committed.windows(from.len()).position(|w| w == from.as_bytes()).unwrap(); let mut b = committed.clone(); b[at..at + to.len()].copy_from_slice(to.as_bytes()); b }; for (bytes, what, said) in [ (patch("\"channels\":1", "\"channels\":3"), "three channels", "`channels` is 3"), (patch("wai.det.fixed64", "wai.det.float64"), "another numeric profile", "wai.det.float64"), ] { let input = scratch("contract.wscr"); std::fs::write(&input, bytes).unwrap(); let pcm = scratch("contract.s16"); let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm), "--json"]), &pcm, what); assert!(stderr.contains("refused") && stderr.contains(said), "{what}: {stderr}"); } } /// A `WSCR` that declares more notes than its notes section holds is refused before the /// reader reserves room for them: a file of a few bytes can declare 2^32 − 1. The file /// carries a well-formed contract (section 0x01) and a notes section (0x02) holding only /// the count, so without the check the reader would pass every other rule and reach the /// reservation; the check, not a missing section, is what refuses it. #[test] fn score_render_refuses_a_note_count_its_bytes_cannot_hold() { let contract = wai::score::ScoreContract { sample_rate: 22050, numeric: wai::world::NUMERIC_FIXED64.into(), channels: 1, duration: 100, } .to_canonical_json(); let mut b = b"WSCR".to_vec(); b.extend_from_slice(&2u16.to_le_bytes()); for (kind, off, len) in [(0x01u8, 0u32, contract.len() as u32), (0x02, contract.len() as u32, 4)] { b.push(kind); b.extend_from_slice(&off.to_le_bytes()); b.extend_from_slice(&len.to_le_bytes()); } b.extend_from_slice(&contract); b.extend_from_slice(&u32::MAX.to_le_bytes()); // The same file declaring no notes renders: the contract and the layout are good. let mut empty = b.clone(); let n = empty.len(); empty[n - 4..].copy_from_slice(&0u32.to_le_bytes()); let input = scratch("empty.wscr"); std::fs::write(&input, empty).unwrap(); let r: Value = data(&ok(&wai(&["score", "render", p(&input), "--json"]))); assert_eq!((r["samples"].as_u64(), r["notes"].as_u64()), (Some(100), Some(0)), "{r}"); let input = scratch("huge.wscr"); std::fs::write(&input, b).unwrap(); let pcm = scratch("huge.s16"); let stderr = refused(&wai(&["score", "render", p(&input), "--pcm", p(&pcm)]), &pcm, "2^32 - 1 notes in 4 bytes"); assert!(stderr.contains("declares 4294967295 notes"), "{stderr}"); }