All checks were successful
WAI operation catalogue / does this change touch it (push) Successful in 17s
wai-fleet service / does this change touch it (push) Successful in 16s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 25s
wai-app and Create / wai-app · does this change touch it (push) Successful in 23s
wai-ledger service / does this change touch it (push) Successful in 19s
wai-sink-swift native sink package / does this change touch it (push) Successful in 22s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 53s
WAI Studio / does this change touch it (push) Successful in 18s
wai-quantum for JavaScript / does this change touch it (push) Successful in 54s
wai-audit collector and player integration / does this change touch it (push) Successful in 57s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m7s
wai-key-proxy / does this change touch it (push) Successful in 57s
wai-quantum studio and desktop / does this change touch it (push) Successful in 55s
wai-sink-android native sink library / does this change touch it (push) Successful in 55s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m46s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 5m2s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m5s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 6m41s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 16s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m2s
wai-fleet service / service · linux (push) Successful in 3m16s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m14s
wai-ledger service / service · linux (push) Successful in 4m28s
wai-audit collector and player integration / collector · linux (push) Successful in 5m1s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 8m15s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m28s
wai-quantum studio and desktop / studio · browser (push) Successful in 5m18s
wai-key-proxy / proxy · linux (push) Successful in 3m1s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m1s
wai-quantum for JavaScript / javascript · linux (push) Successful in 7m14s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m23s
wai-fleet service / service · macos (push) Successful in 38s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 7m35s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 9m15s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 9m29s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 12m24s
WAI Studio / Studio in WebKit · linux (push) Successful in 20m25s
wai-ledger service / service · macos (push) Successful in 1m28s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 7m54s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 20m38s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m54s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m9s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 49s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m34s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m34s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m59s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m42s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m0s
browser-sink §14.9: where a device has no decoder of wai.graphics.vector, a composing page presents each graphics piece's floor (graphics-vector §6.3), the piece's transcript as wai.text.zstd, as text: the blocks active at the clock's tick, in block order, each with its language, direction and writing mode. A piece presents its overlay or its floor, never both. Before presenting, the page holds the floor object to its piece: its derived_from names the piece's base (floor_base), its payload is the raw frame and no other Zstandard frame (floor_frame), its target states the transcript's length and a timebase (floor_target), and its content is a transcript in graphics-vector §5.4's one encoding (floor_transcript). A caption or a subtitle block is never presented from the floor (caption_track): the text component of its language carries it, and the text layer presents it or not as the viewer set it. A forced_narrative block is presented from the floor unless the text layer shows a cue of its language with its string at that millisecond (forced_in_cue), so the words are on the page once. The text layer presents one text component of a package, or none. wai-rs browser_sink::floor is the reference (read, present), which wai-web's compose bundle calls as TextFloor. Corpus live-conformance/browser-floor/ (49 floors, 25 presentations), written by wai_browser_floor_vectors and regenerating byte for byte on x86_64 and aarch64; tests/browser_floor.rs also reads the floors wai-pack wrote for pack-conformance/multi/multi-captions-01, and wai-web's compose::floor_tests holds them to wai-vector's transcripts. browser_floor_verify.py, the second implementation, refuses 72 mutated floors each with its defect's reason, fails 23 mutated copies of the corpus, and each of its 32 checks, dropped, is caught. compose/run.mjs gains case 10. Not here: a base that is routed and then refused when loaded still refuses the package, and the overlay path does not yet leave out caption items (graphics-vector §5.7). Co-Authored-By: Claude <[email protected]>
832 lines
63 KiB
JavaScript
832 lines
63 KiB
JavaScript
// The composing sink's gate: the reference page (index.html) plays the reference package
|
||
// (package/, which wai-pack writes, packaging §5.8) in browser engines, and every outcome
|
||
// is held to what this script reads from the package's own signed indexes and from
|
||
// vector-conformance/.
|
||
//
|
||
// node run.mjs --engines chromium,webkit --pkg <dir holding wai_compose.js> [<wai dir>]
|
||
//
|
||
// It serves <wai dir> (default ../.. from here) from a local server, with the bundle at
|
||
// wai-web/compose/pkg/ taken from --pkg, launches each engine named, and in each:
|
||
//
|
||
// 1. this device: the package opens, every check holds (the receipt's signature and
|
||
// indexes, every chosen object against its index entry, each decoded output with a
|
||
// signed digest against that digest), every component is presented, and it plays to
|
||
// the end: video frames, overlay frames, cues, state updates and the score's audio
|
||
// all presented on the one clock;
|
||
// 2. the score's samples, synthesised in the tab piece by piece in the channels and at
|
||
// the rate each piece's target states (two at 48 kHz in the reference package), have
|
||
// the SHA-256s the signed index states for each piece (which wai-pack computed
|
||
// natively); every overlay frame rasterised and hashed in the page equals
|
||
// vector-conformance's expected frame, and each piece's clip digest the one its
|
||
// index entry states; each
|
||
// text piece is held to its segment's digest and each feed piece to its seal, the
|
||
// second chained to the first;
|
||
// 3. forced without the AV1 decoder: every integer video base is decoded in WebAssembly
|
||
// and its clip digest is the one its index entry states;
|
||
// 4. forced without the integer rung and the still: every video piece plays its AV1
|
||
// floor, the base's route is none for want of a WebAssembly decoder, and the page
|
||
// says the floor plays;
|
||
// 5. the forged receipt (mutations/forged.receipt.json, signed by another key) is
|
||
// refused receipt-signature, and nothing is presented;
|
||
// 6. one byte of the graphics component's first object flipped in delivery is refused
|
||
// object-hash, and nothing is presented;
|
||
// 7. forced without the score: every score piece plays its FLAC floor, decoded in
|
||
// WebAssembly, the page says the floor plays, and each piece's decoded samples have
|
||
// the SHA-256 the score's signed base entry states for its mixdown (the floor is that
|
||
// mixdown, losslessly; the page holds the floor to no digest, this script does);
|
||
// 8. pack-conformance's multi-opus-01, a floor-only lead and a sound track with an Opus
|
||
// floor (packaging §7.9), forced without FLAC: where the engine's AudioDecoder takes
|
||
// Opus, every piece of the sound track plays its Opus floor through it and comes to
|
||
// exactly the piece's samples, which this script reads from the golden's index; where
|
||
// it does not, the sound track is not presented and the picture is. An engine whose
|
||
// VideoDecoder refuses the golden's 64 x 36 AV1 picture (WebKit's) is held to refusing
|
||
// the package at the lead's first piece, where the sound is not reached: there this
|
||
// case checks no Opus;
|
||
// 9. the staged packages (staged/, which src/compose_stage.rs seals from the corpora), held to
|
||
// what volumetric-conformance/fields/ and staged-conformance/cond/ state, independently of
|
||
// the signed indexes, which the script also reads:
|
||
// - fields, each level's delivery held by this script until it releases it: every field
|
||
// presents its render at its pinned view, computed in the tab, whose SHA-256 (read back
|
||
// from the canvas) is the frame digest the corpus states; the coarse field is presented
|
||
// before its first level is asked for, and each level released presents the next stage,
|
||
// whose digest is the stage hash the corpus states, before the next level is asked for;
|
||
// - fields-digest, its last level's signed digest another stage's: that stage is computed
|
||
// and refused prefix-digest, and the stage below stays presented;
|
||
// - a byte of the field's first level flipped in delivery: refused object-hash, its second
|
||
// level then refused layer-gap, and the coarse field stays presented;
|
||
// - cond: each picture refined by its wai.image.int_cond_refine layer, from two exact
|
||
// planes, three G-buffers, and the frame of a splat set rendered in the tab at its pinned
|
||
// pose, presents the stage hash the corpus states for its case;
|
||
// - a missing plane, withheld in delivery, and the splat decoder left out of the profile:
|
||
// the layer refused side-absent, the base presented, and the page says so;
|
||
// - cond-not-exact, a plane the page decodes only through its fallback: side-not-exact,
|
||
// the base presented;
|
||
// - a byte of a side object flipped in delivery: the object refused object-hash, never held,
|
||
// the layer refused side-absent, the base presented.
|
||
//
|
||
// 10. pack-conformance's multi-captions-01, a title card whose graphics component carries a
|
||
// text floor and captions in two languages (browser-sink §14.9), each expectation read by
|
||
// this script from the package's own objects with readers of its own:
|
||
// - on this device every graphics piece presents its overlay, the floor an alternative
|
||
// that is routed and not chosen, and no text is presented from a floor;
|
||
// - forced without wai.graphics.vector: every graphics piece presents its floor, the base
|
||
// unrouted (no_wasm), each floor's transcript the one this script reads from the floor
|
||
// object, and the page says so; at every frame of the programme no overlay frame is
|
||
// drawn, the text the page shows from the floor is exactly the blocks active at that
|
||
// tick that are no caption and no subtitle, each as a text node with its language and
|
||
// direction, and the text layer shows exactly the cues of the English component at
|
||
// that time, so a caption's words are on the page once; it then plays to its end;
|
||
// - with captions=fr the text layer shows the French component's cues, and with
|
||
// captions=off none, the text from the floor the same in both;
|
||
// - a byte of a floor object flipped in delivery, and two floors of one length exchanged
|
||
// in delivery: each refused object-hash, nothing presented;
|
||
// - the bundle's reader, called in the page: a floor with its own base reads; with another
|
||
// piece's base it is refused floor_base, and with a byte of its frame's head changed
|
||
// floor_frame.
|
||
//
|
||
// node run.mjs ... --only staged runs case 9 alone, --only package cases 1 to 8,
|
||
// --only floor case 10.
|
||
//
|
||
// A missing engine is a failure, never a skip, and so is an engine that runs fewer checks
|
||
// than this script makes, a page error at any time, and an error of this script itself.
|
||
// Exit 0 only when every check passed in every engine; 1 on any failure; 2 on usage.
|
||
import { createHash } from "node:crypto";
|
||
import { existsSync, readFileSync, statSync } from "node:fs";
|
||
import { createServer } from "node:http";
|
||
import { dirname, extname, join, resolve, sep } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const ENGINES = ["chromium", "webkit", "firefox"];
|
||
const TYPES = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".mjs": "text/javascript", ".wasm": "application/wasm", ".json": "application/json" };
|
||
|
||
let failures = 0;
|
||
const passed = {};
|
||
// Anything this script did not expect fails the run: it never ends 0 on an error.
|
||
const crash = (what, e) => {
|
||
console.log(` FAIL run.mjs: ${what}: ${(e && e.stack) || e}`);
|
||
process.exit(1);
|
||
};
|
||
process.on("unhandledRejection", (e) => crash("an unhandled rejection", e));
|
||
process.on("uncaughtException", (e) => crash("an uncaught exception", e));
|
||
|
||
function usage(why) {
|
||
if (why) console.error(`run.mjs: ${why}`);
|
||
console.error("usage: node run.mjs --engines chromium,webkit --pkg <dir> [--only package|staged|floor] [<wai dir>]");
|
||
process.exit(2);
|
||
}
|
||
|
||
let engines = null, pkg = null, root = resolve(HERE, "..", ".."), only = null;
|
||
for (let a = process.argv.slice(2), i = 0; i < a.length; i++) {
|
||
if (a[i] === "--engines" && i + 1 < a.length) engines = [...new Set(a[++i].split(",").filter(Boolean))];
|
||
else if (a[i] === "--pkg" && i + 1 < a.length) pkg = resolve(a[++i]);
|
||
else if (a[i] === "--only" && i + 1 < a.length && ["package", "staged", "floor"].includes(a[i + 1])) only = a[++i];
|
||
else if (!a[i].startsWith("--")) root = resolve(a[i]);
|
||
else usage(`unexpected argument ${a[i]}`);
|
||
}
|
||
if (!engines || !engines.length) usage("--engines is required");
|
||
if (!pkg) usage("--pkg is required");
|
||
for (const e of engines) if (!ENGINES.includes(e)) usage(`no engine named ${e}`);
|
||
const COMPOSE = join(root, "wai-web", "compose");
|
||
const PKG_DIR = join(COMPOSE, "package");
|
||
const FORGED = join(COMPOSE, "mutations", "forged.receipt.json");
|
||
const EXPECTED = join(root, "vector-conformance", "expected", "motion", "stroke-titles.json");
|
||
for (const p of [join(pkg, "wai_compose.js"), join(pkg, "wai_compose_bg.wasm"), join(PKG_DIR, "package.receipt.json"), FORGED, EXPECTED]) {
|
||
if (!existsSync(p)) {
|
||
console.log(` FAIL run.mjs: ${p} is missing`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
const sha256 = (b) => createHash("sha256").update(b).digest("hex");
|
||
console.log(`the bundle: ${join(pkg, "wai_compose_bg.wasm")}, SHA-256 ${sha256(readFileSync(join(pkg, "wai_compose_bg.wasm")))}`);
|
||
const expectedFile = JSON.parse(readFileSync(EXPECTED, "utf8"));
|
||
const receipt = JSON.parse(readFileSync(join(PKG_DIR, "package.receipt.json"), "utf8"));
|
||
|
||
/// A stage index (packaging §6.2), read here: each piece's start, duration and entries, and
|
||
/// each entry's content hash, role (0 base, 2 floor), tier (2 decode-equivalence) and
|
||
/// prefix digest.
|
||
function readIndex(b) {
|
||
const u32 = (at) => b.readUInt32LE(at);
|
||
const nP = u32(16), nE = u32(20);
|
||
const pieces = [];
|
||
for (let k = 0; k < nP; k++) {
|
||
const at = 160 + 32 * k;
|
||
pieces.push({ start: Number(b.readBigUInt64LE(at)), duration: u32(at + 8), first: u32(at + 12), n: b.readUInt16LE(at + 16) });
|
||
}
|
||
const entries = [];
|
||
for (let k = 0; k < nE; k++) {
|
||
const at = 160 + 32 * nP + 128 * k;
|
||
entries.push({ hash: b.subarray(at, at + 32).toString("hex"), piece: Number(b.readBigUInt64LE(at + 40)), role: b[at + 54], layer: b[at + 55], tier: b[at + 56], digest: b.subarray(at + 64, at + 96).toString("hex") });
|
||
}
|
||
return { tb: [u32(8), u32(12)], pieces, entries };
|
||
}
|
||
const index = Object.fromEntries(receipt.components.map((c) => [c.id, readIndex(readFileSync(join(PKG_DIR, c.index)))]));
|
||
// The golden with an Opus floor (packaging §7.9), served from the corpus.
|
||
const OPUS_DIR = join(root, "pack-conformance", "multi", "multi-opus-01", "package");
|
||
if (!existsSync(join(OPUS_DIR, "index-a.wsix"))) {
|
||
console.log(` FAIL run.mjs: ${OPUS_DIR} has no index-a.wsix`);
|
||
process.exit(1);
|
||
}
|
||
const opusIndex = readIndex(readFileSync(join(OPUS_DIR, "index-a.wsix")));
|
||
// Each piece's base entry, by component.
|
||
const bases = (id) => index[id].pieces.map((p) => index[id].entries.slice(p.first, p.first + p.n).find((e) => e.role === 0));
|
||
/// The `target` of an object's manifest (packaging §5.8): a WAI1 envelope is `WAI1`, the
|
||
/// manifest's length (u32), the manifest, then the payload.
|
||
function targetOf(hash) {
|
||
const b = readFileSync(join(PKG_DIR, "objects", `${hash}.wai`));
|
||
return JSON.parse(b.subarray(8, 8 + b.readUInt32LE(4)).toString("utf8")).target;
|
||
}
|
||
for (const id of ["v", "g", "t", "s", "f"]) {
|
||
if (!index[id]) {
|
||
console.log(` FAIL run.mjs: the package has no component ${id}`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
// The object flipped in delivery: the graphics component's first object, at its middle.
|
||
const FLIP = bases("g")[0].hash;
|
||
|
||
// The staged packages, and what the corpora they were sealed from state.
|
||
const STAGED = join(COMPOSE, "staged");
|
||
const FIELDS = join(root, "volumetric-conformance", "fields");
|
||
const COND = join(root, "staged-conformance", "cond");
|
||
for (const p of ["fields/index-f.wsix", "fields-digest/index-f.wsix", "cond/index-c.wsix", "cond-not-exact/index-c.wsix"].map((f) => join(STAGED, f)).concat([join(FIELDS, "vectors.json")])) {
|
||
if (!existsSync(p)) {
|
||
console.log(` FAIL run.mjs: ${p} is missing`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
const sIndex = (name, id) => readIndex(readFileSync(join(STAGED, name, `index-${id}.wsix`)));
|
||
const fIx = sIndex("fields", "f"), cIx = sIndex("cond", "c"), nIx = sIndex("cond-not-exact", "c");
|
||
const entriesOf = (ix, pi, role) => ix.entries.filter((e) => e.piece === pi && e.role === role);
|
||
const fieldVectors = JSON.parse(readFileSync(join(FIELDS, "vectors.json"), "utf8"));
|
||
// A scene's frame digest at its own view, stage by stage (each prefix of its levels).
|
||
const ownRenders = (name) => fieldVectors.scenes.find((s) => s.name === name).stages.map((st) => st.renders.find((r) => r.view === "own").rgb_sha256);
|
||
const FIELD_SCENES = ["orb", "mlp", "edge"]; // the field package's pieces, in order
|
||
const ORB = ownRenders("orb");
|
||
const caseOf = (name) => JSON.parse(readFileSync(join(COND, name, "case.json"), "utf8"));
|
||
const COND_CASES = ["cond_side_planes", "cond_dual_gbuffer", "cond_render"].map(caseOf); // the cond package's pieces
|
||
const NOT_EXACT = caseOf("cond_side_not_exact");
|
||
const finalStage = (c) => c.expect.bases[0].units[0].stage_sha256;
|
||
const stage0 = (c) => c.bases[0].stage0_sha256;
|
||
// Delivery faults: the field's first level and a side object flipped, a side object withheld,
|
||
// and each level held until this script releases it.
|
||
const FIELD_LEVELS = entriesOf(fIx, 0, 1).map((e) => e.hash);
|
||
const STAGED_FLIPS = new Set([FIELD_LEVELS[0], entriesOf(cIx, 0, 6)[1].hash]);
|
||
const WITHHELD = entriesOf(cIx, 0, 6)[0].hash;
|
||
const heldWaiting = new Map(); // hash -> the response waiting for release
|
||
const released = new Set();
|
||
const release = (h) => {
|
||
released.add(h);
|
||
const go = heldWaiting.get(h);
|
||
heldWaiting.delete(h);
|
||
if (go) go();
|
||
};
|
||
const objectHash = (file) => (file.match(/([0-9a-f]{64})\.wai$/) || [])[1] || null;
|
||
|
||
// The golden with a text floor and captions (graphics-vector §6.3, §6.4), served from the
|
||
// corpus, and what this script reads of it with its own readers: each graphics piece's base
|
||
// and floor, each floor's transcript, and each text component's cues.
|
||
const CAP_DIR = join(root, "pack-conformance", "multi", "multi-captions-01", "package");
|
||
for (const f of ["index-g.wsix", "index-t.wsix", "index-u.wsix", "index-v.wsix"]) {
|
||
if (!existsSync(join(CAP_DIR, f))) {
|
||
console.log(` FAIL run.mjs: ${CAP_DIR} has no ${f}`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
const capIndex = (id) => readIndex(readFileSync(join(CAP_DIR, `index-${id}.wsix`)));
|
||
/// A WAI1 envelope's manifest and payload (SPEC §2).
|
||
function envelopeOf(b) {
|
||
const n = b.readUInt32LE(4);
|
||
return { manifest: JSON.parse(b.subarray(8, 8 + n).toString("utf8")), payload: b.subarray(12 + n) };
|
||
}
|
||
const capObject = (hash) => envelopeOf(readFileSync(join(CAP_DIR, "objects", `${hash}.wai`)));
|
||
/// The content of the raw frame of graphics-vector §6.3, one block: the corpus's floors are
|
||
/// far below 2^17 bytes. Anything else stops the run.
|
||
function rawFrame(p) {
|
||
const n = p.readUInt32LE(5);
|
||
const h = p[9] | (p[10] << 8) | (p[11] << 16);
|
||
if (p.subarray(0, 5).toString("hex") !== "28b52ffda0" || h !== ((n << 3) | 1) || p.length !== 12 + n) crash("a floor of multi-captions-01", "not one raw block");
|
||
return p.subarray(12).toString("utf8");
|
||
}
|
||
const gIx = capIndex("g"), capV = capIndex("v");
|
||
const CAP_FRAMES = capV.pieces.reduce((n, p) => Math.max(n, p.start + p.duration), 0);
|
||
const CAP_FPS = capV.tb[0] / capV.tb[1];
|
||
const capG = gIx.pieces.map((p, i) => {
|
||
const es = gIx.entries.slice(p.first, p.first + p.n);
|
||
const base = es.find((e) => e.role === 0), floor = es.find((e) => e.role === 2);
|
||
const o = capObject(floor.hash);
|
||
const transcript = rawFrame(o.payload);
|
||
return { piece: i, start: p.start, duration: p.duration, base: base.hash, floor: floor.hash, bytes: readFileSync(join(CAP_DIR, "objects", `${floor.hash}.wai`)).length, tb: o.manifest.target.tb, motif: o.manifest.derived_from.motif, transcript, blocks: JSON.parse(transcript) };
|
||
});
|
||
/// A text component's cues, piece by piece, from its WebVTT segments (timed-text §5).
|
||
function capCues(id) {
|
||
const ix = capIndex(id);
|
||
const stamp = (h, m, s2, ms) => ((+h * 60 + +m) * 60 + +s2) * 1000 + +ms;
|
||
return ix.pieces.map((p) => {
|
||
const e = ix.entries.slice(p.first, p.first + p.n).find((x) => x.role === 0);
|
||
const lines = capObject(e.hash).payload.toString("utf8").split("\n");
|
||
const cues = [];
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const m = lines[i].match(/^(\d\d):(\d\d):(\d\d)\.(\d{3}) --> (\d\d):(\d\d):(\d\d)\.(\d{3})/);
|
||
if (!m) continue;
|
||
const text = [];
|
||
for (i++; i < lines.length && lines[i] !== ""; i++) text.push(lines[i]);
|
||
cues.push({ start: stamp(m[1], m[2], m[3], m[4]), end: stamp(m[5], m[6], m[7], m[8]), text: text.join("\n") });
|
||
}
|
||
return { start: p.start, duration: p.duration, tb: ix.tb, cues };
|
||
});
|
||
}
|
||
const CAP_CUES = { en: capCues("t"), fr: capCues("u") };
|
||
/// What browser-sink §14.9 has on the page at the middle of frame `n`: the blocks from the
|
||
/// floor, the caption blocks withheld, and the cues of the text component `lang` (or none).
|
||
function capExpected(n, lang) {
|
||
const t = (n + 0.5) / CAP_FPS;
|
||
const g = capG.find((p) => n >= p.start && n < p.start + p.duration);
|
||
const tick = Math.floor(t * g.tb + 1e-6);
|
||
const active = g.blocks.filter((b) => b.active.some(([on, off]) => on <= tick && tick < off));
|
||
const ms = t * 1000;
|
||
const seg = lang ? CAP_CUES[lang].find((p) => Math.floor((t * p.tb[0]) / p.tb[1] + 1e-9) >= p.start && Math.floor((t * p.tb[0]) / p.tb[1] + 1e-9) < p.start + p.duration) : null;
|
||
return {
|
||
t,
|
||
blocks: active.filter((b) => b.purpose !== 4 && b.purpose !== 5).map((b) => ({ text: b.text, lang: b.lang, dir: b.dir ? "rtl" : "ltr", index: b.index, markup: 0 })),
|
||
withheld: active.filter((b) => b.purpose === 4 || b.purpose === 5).map((b) => b.index),
|
||
captions: active.filter((b) => b.purpose === 4 || b.purpose === 5),
|
||
cues: seg ? seg.cues.filter((c) => ms >= c.start && ms < c.end).map((c) => c.text) : [],
|
||
};
|
||
}
|
||
// The floor flipped in delivery, and two floors of one length exchanged in delivery.
|
||
const CAP_FLIP = capG[0].floor;
|
||
const CAP_SWAP = (() => {
|
||
for (const a of capG) for (const b of capG) if (a.piece < b.piece && a.bytes === b.bytes) return new Map([[a.floor, b.floor], [b.floor, a.floor]]);
|
||
return null;
|
||
})();
|
||
|
||
const server = createServer((req, res) => {
|
||
let path;
|
||
try {
|
||
path = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
|
||
} catch {
|
||
res.writeHead(400).end();
|
||
return;
|
||
}
|
||
let file = null, body = null;
|
||
if (path.startsWith("/wai/wai-web/compose/pkg/")) file = resolve(pkg, path.slice("/wai/wai-web/compose/pkg/".length));
|
||
else if (path.startsWith("/wai/wai-web/compose/package-flip/")) {
|
||
file = resolve(PKG_DIR, path.slice("/wai/wai-web/compose/package-flip/".length));
|
||
if (file.startsWith(PKG_DIR + sep) && file.endsWith(`${FLIP}.wai`) && existsSync(file)) {
|
||
body = Buffer.from(readFileSync(file));
|
||
body[body.length >> 1] ^= 0x01;
|
||
}
|
||
} else if (path.startsWith("/wai/wai-web/compose/captions-")) {
|
||
// /captions-<mode>/...: multi-captions-01 with a delivery fault.
|
||
const m = path.match(/^\/wai\/wai-web\/compose\/captions-(flip|swap)\/(.*)$/);
|
||
if (m) {
|
||
file = resolve(CAP_DIR, m[2]);
|
||
const h = objectHash(file);
|
||
if (file.startsWith(CAP_DIR + sep) && h && existsSync(file)) {
|
||
if (m[1] === "flip" && h === CAP_FLIP) {
|
||
body = Buffer.from(readFileSync(file));
|
||
body[body.length >> 1] ^= 0x01;
|
||
} else if (m[1] === "swap" && CAP_SWAP && CAP_SWAP.has(h)) body = readFileSync(join(CAP_DIR, "objects", `${CAP_SWAP.get(h)}.wai`));
|
||
}
|
||
}
|
||
} else if (path.startsWith("/wai/wai-web/compose/staged-")) {
|
||
// /staged-<mode>/<package>/...: a staged package with a delivery fault.
|
||
const m = path.match(/^\/wai\/wai-web\/compose\/staged-(flip|withhold|held)\/(.*)$/);
|
||
if (m) {
|
||
file = resolve(STAGED, m[2]);
|
||
const h = objectHash(file);
|
||
if (file.startsWith(STAGED + sep) && h && existsSync(file)) {
|
||
if (m[1] === "flip" && STAGED_FLIPS.has(h)) {
|
||
body = Buffer.from(readFileSync(file));
|
||
body[body.length >> 1] ^= 0x01;
|
||
} else if (m[1] === "withhold" && h === WITHHELD) {
|
||
res.writeHead(404).end();
|
||
return;
|
||
} else if (m[1] === "held" && FIELD_LEVELS.includes(h) && !released.has(h)) {
|
||
const b = readFileSync(file);
|
||
heldWaiting.set(h, () => res.writeHead(200, { "content-type": "application/octet-stream", "content-length": b.length, "cache-control": "no-store" }).end(b));
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
} else if (path.startsWith("/wai/")) file = resolve(root, path.slice("/wai/".length));
|
||
const inside = file && (file.startsWith(root + sep) || file.startsWith(pkg + sep));
|
||
if (req.method !== "GET" || !inside || !existsSync(file) || !statSync(file).isFile()) {
|
||
res.writeHead(404).end();
|
||
return;
|
||
}
|
||
body = body || readFileSync(file);
|
||
res.writeHead(200, { "content-type": TYPES[extname(file)] || "application/octet-stream", "content-length": body.length, "cache-control": "no-store" }).end(body);
|
||
});
|
||
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
|
||
const origin = `http://127.0.0.1:${server.address().port}`;
|
||
const PAGE = `${origin}/wai/wai-web/compose/index.html`;
|
||
|
||
let pw;
|
||
try {
|
||
pw = await import("playwright");
|
||
} catch (e) {
|
||
console.log(` FAIL run.mjs: playwright is not installed here (npm ci): ${e.message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const fail = (engine, what) => {
|
||
failures++;
|
||
console.log(` FAIL ${engine}: ${what}`);
|
||
};
|
||
const pass = (engine, what) => {
|
||
passed[engine] = (passed[engine] || 0) + 1;
|
||
console.log(` ok ${engine}: ${what}`);
|
||
};
|
||
const expect = (engine, cond, what) => (cond ? pass(engine, what) : fail(engine, what));
|
||
// The checks an engine makes when everything holds; fewer is a failure.
|
||
const CHECKS = { package: 42, staged: 42, floor: 39 };
|
||
const CHECKS_PER_ENGINE = only ? CHECKS[only] : CHECKS.package + CHECKS.staged + CHECKS.floor;
|
||
// Wait for `cond()` for up to `ms`.
|
||
const until = async (cond, ms) => {
|
||
for (const t0 = Date.now(); Date.now() - t0 < ms; await new Promise((ok) => setTimeout(ok, 25))) if (cond()) return true;
|
||
return cond();
|
||
};
|
||
|
||
async function open(browser, engine, query, { refined = false } = {}) {
|
||
const page = await browser.newPage({ viewport: { width: 1200, height: 900 } });
|
||
const errors = [];
|
||
page.on("pageerror", (e) => errors.push(String(e)));
|
||
await page.goto(`${PAGE}${query}`);
|
||
await page.waitForFunction(() => window.__wai_compose && ["ready", "refused"].includes(window.__wai_compose.state), null, { timeout: 300000 });
|
||
// A staged package: its layers taken as they arrive, to the last.
|
||
if (refined) await page.waitForFunction(() => window.__wai_compose.state !== "ready" || !window.__wai_compose.staged || window.__wai_compose.staged.state === "done", null, { timeout: 120000 });
|
||
const hook = await page.evaluate(() => {
|
||
const h = window.__wai_compose;
|
||
return { state: h.state, refusal: h.refusal, dispatch: h.dispatch, checks: h.checks, audio: h.audio || null, floor: h.floor || null, text: h.text || null, staged: h.staged ? JSON.parse(JSON.stringify(h.staged)) : null, status: document.getElementById("status").textContent };
|
||
});
|
||
// Every page error, from opening to closing, fails the run.
|
||
const close = async () => {
|
||
await page.close();
|
||
expect(engine, errors.length === 0, `${query || "(this device)"}: no page error${errors.length ? "s: " + errors.join(" | ") : ""}`);
|
||
};
|
||
return { page, hook, close };
|
||
}
|
||
|
||
const checksAbout = (hook, re) => hook.checks.filter((c) => re.test(c.what));
|
||
|
||
const layersOf = (hook, id) => hook.dispatch.components.find((c) => c.id === id).pieces.flatMap((p) => (p.layers || []).map((L) => ({ ...L, piece: p.piece })));
|
||
const outcomes = (ls) => ls.map((L) => `${L.name.split("/").slice(0, 3).join("/")} ${L.outcome}${L.reason ? " " + L.reason : ""}`).join(", ");
|
||
const sideState = (x) => (x.held ? (x.exact ? "held exact" : "held not exact") : "not held");
|
||
const readback = (page, t) => page.evaluate((u) => window.__wai_compose.presentAt(u), t);
|
||
|
||
/// Case 9: the staged packages.
|
||
async function stagedCases(browser, engine) {
|
||
// 9a. Fields, each level delivered only when this script releases it.
|
||
{
|
||
released.clear();
|
||
heldWaiting.clear();
|
||
const { page, hook, close } = await open(browser, engine, "?pkg=staged-held/fields/");
|
||
expect(engine, hook.state === "ready", `fields: opens (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const f = hook.dispatch.components.find((c) => c.id === "f");
|
||
expect(engine, f.pieces.length === 3 && f.pieces.every((p) => p.capability === "wai.field.int_grid" && p.route === "wasm" && p.alternatives.every((a) => a.legs.ua === "no_ua_mapping" && a.legs.wasm === "taken")), `fields: every piece's field routed wasm, the user agent never asked (${f.pieces.map((p) => `${p.capability}@${p.route}`).join(", ")})`);
|
||
const ls0 = layersOf(hook, "f");
|
||
expect(engine, hook.dispatch.device.wasm.includes("wai.field.int_refine") && ls0.length === 2 && ls0.every((L) => L.route === "wasm" && L.outcome === "pending"), `fields: the orb's two levels routed wasm, the report's wasm leg naming wai.field.int_refine (${ls0.map((L) => L.route).join(", ")})`);
|
||
const want = FIELD_SCENES.map((n) => ownRenders(n)[0]);
|
||
const base = checksAbout(hook, /^field .*its render at its own view/);
|
||
expect(engine, base.length === 3 && base.every((c, i) => c.ok && c.got === want[i] && c.expected === entriesOf(fIx, i, 0)[0].digest), `fields: each of ${base.length} fields (${FIELD_SCENES.join(", ")}) rendered in the tab at its pinned view to the frame digest volumetric-conformance states, the digest its signed entry states`);
|
||
const drawn = [];
|
||
for (let i = 0; i < 3; i++) drawn.push(await readback(page, i + 0.5));
|
||
expect(engine, drawn.every((d, i) => d === want[i]), `fields: each, drawn, reads back from the canvas as that frame (${drawn.map((d) => d.slice(0, 8)).join(", ")})`);
|
||
await readback(page, 0);
|
||
// The levels, one at a time.
|
||
const asked = await until(() => heldWaiting.has(FIELD_LEVELS[0]), 30000);
|
||
const st0 = await page.evaluate(() => ({ n: window.__wai_compose.staged.stages.length, px: window.__wai_compose.pictureSha256() }));
|
||
expect(engine, asked && !heldWaiting.has(FIELD_LEVELS[1]) && st0.n === 1 && st0.px === ORB[0], `fields: the coarse orb, 2 of its 4 levels, presented (${st0.px.slice(0, 12)}) while its first level is asked for and not delivered, and its second not yet asked for`);
|
||
let ok = asked;
|
||
const seen = [];
|
||
for (let k = 0; k < FIELD_LEVELS.length && ok; k++) {
|
||
release(FIELD_LEVELS[k]);
|
||
const came = await page.waitForFunction((n) => window.__wai_compose.staged.stages.length > n || window.__wai_compose.staged.state === "done", k + 1, { timeout: 60000 }).then(() => true, () => false);
|
||
const ev = await page.evaluate((n) => window.__wai_compose.staged.stages[n] || null, k + 1);
|
||
seen.push(ev);
|
||
const next = k + 1 < FIELD_LEVELS.length ? await until(() => heldWaiting.has(FIELD_LEVELS[k + 1]), 30000) : true;
|
||
ok = came && ev && ev.stage === k + 1 && ev.sha256 === ORB[k + 1] && ev.presented_sha256 === ORB[k + 1] && next;
|
||
}
|
||
expect(engine, ok && seen.length === FIELD_LEVELS.length, `fields: each level, released, presents the next stage, computed in the tab, its frame digest the stage hash the corpus states and read back so from the canvas, before the next level is asked for (${seen.map((e) => (e ? `stage ${e.stage} ${e.sha256.slice(0, 8)}` : "none")).join(", ")})`);
|
||
for (const h of FIELD_LEVELS) release(h);
|
||
await page.waitForFunction(() => window.__wai_compose.staged.state === "done", null, { timeout: 60000 }).catch(() => {});
|
||
const h2 = await page.evaluate(() => ({ state: window.__wai_compose.staged.state, dispatch: window.__wai_compose.dispatch, checks: window.__wai_compose.checks }));
|
||
const ls = layersOf(h2, "f");
|
||
expect(engine, h2.state === "done" && ls.length === 2 && ls.every((L, i) => L.outcome === "applied" && L.stage === i + 1 && L.sha256 === entriesOf(fIx, 0, 1)[i].digest), `fields: both levels applied, each stage held to the digest its signed entry states (${outcomes(ls)})`);
|
||
const bad = h2.checks.filter((c) => !c.ok);
|
||
expect(engine, h2.checks.length > 0 && bad.length === 0, `fields: ${h2.checks.length} checks, every one holds${bad.length ? ": " + JSON.stringify(bad).slice(0, 300) : ""}`);
|
||
}
|
||
await close();
|
||
}
|
||
// 9b. A level whose stage is not the signed digest.
|
||
{
|
||
const { hook, close } = await open(browser, engine, "?pkg=staged/fields-digest/", { refined: true });
|
||
const ls = hook.state === "ready" ? layersOf(hook, "f") : [];
|
||
expect(engine, ls.length === 2 && ls[0].outcome === "applied" && ls[1].outcome === "refused" && ls[1].reason === "prefix-digest", `stage digest: a level whose stage is not the digest its signed entry states is refused prefix-digest (${hook.state}: ${outcomes(ls)})`);
|
||
const c = checksAbout(hook, /^field f, piece 0: stage 2/)[0];
|
||
expect(engine, c && !c.ok && c.got === ORB[2] && c.expected === sIndex("fields-digest", "f").entries[2].digest && c.expected === ORB[1], "stage digest: the page computed the corpus's stage 2 and compared it with the signed digest, which is stage 1's");
|
||
const evs = hook.staged ? hook.staged.stages : [];
|
||
expect(engine, evs.length === 2 && evs[1].stage === 1 && evs[1].presented_sha256 === ORB[1] && /f\/0\/L2\/wai\.field\.int_refine was not applied \(prefix-digest\)/.test(hook.status), `stage digest: stage 1 stays presented, and the page says why ("${hook.status.slice(-110)}")`);
|
||
await close();
|
||
}
|
||
// 9c. A byte of the field's first level flipped in delivery.
|
||
{
|
||
const { hook, close } = await open(browser, engine, "?pkg=staged-flip/fields/", { refined: true });
|
||
const ls = hook.state === "ready" ? layersOf(hook, "f") : [];
|
||
expect(engine, ls.length === 2 && ls[0].outcome === "refused" && ls[0].reason === "object-hash", `flipped level: a byte of the field's first level flipped in delivery is refused object-hash (${hook.state}: ${outcomes(ls)})`);
|
||
expect(engine, ls.length === 2 && ls[1].outcome === "refused" && ls[1].reason === "layer-gap", "flipped level: the second level, with no first below it, is refused layer-gap (staged-delivery §4.1 step 8)");
|
||
const evs = hook.staged ? hook.staged.stages : [];
|
||
expect(engine, evs.length === 1 && evs[0].presented_sha256 === ORB[0] && /not applied \(object-hash\)/.test(hook.status), `flipped level: the coarse field stays presented, and the page says why ("${hook.status.slice(-110)}")`);
|
||
await close();
|
||
}
|
||
// 9d. Pictures refined by layers conditioned on side planes.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, "?pkg=staged/cond/", { refined: true });
|
||
expect(engine, hook.state === "ready" && hook.staged && hook.staged.state === "done", `cond: opens and takes its layers (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const c = hook.dispatch.components.find((x) => x.id === "c");
|
||
const ls = layersOf(hook, "c");
|
||
expect(engine, c.pieces.length === 3 && c.pieces.every((p) => p.capability === "wai.image.png" && p.route === "wasm") && ls.length === 3 && ls.every((L) => L.route === "wasm" && L.capability === "wai.image.int_cond_refine"), "cond: each piece's PNG base routed wasm, its wai.image.int_cond_refine layer applied in WebAssembly");
|
||
const want = COND_CASES.map(finalStage);
|
||
expect(engine, ls.every((L, i) => L.outcome === "applied" && L.stage === 1 && L.sha256 === want[i] && L.sha256 === entriesOf(cIx, i, 1)[0].digest), `cond: each layer applied in the tab to the stage hash its case in staged-conformance/cond/ states, the digest its signed entry states (${outcomes(ls)})`);
|
||
const drawn = [];
|
||
for (let i = 0; i < 3; i++) drawn.push(await readback(page, i + 0.5));
|
||
const evs = hook.staged.stages.filter((e) => e.stage === 1);
|
||
expect(engine, evs.length === 3 && evs.every((e, i) => e.presented_sha256 === want[i]) && drawn.every((d, i) => d === want[i]), `cond: each refined picture presented, read back from the canvas as that stage (${drawn.map((d) => d.slice(0, 8)).join(", ")})`);
|
||
const sides = ls.map((L) => L.sides.map(sideState).join("+"));
|
||
expect(engine, ls[0].sides.length === 2 && ls[1].sides.length === 3 && ls[2].sides.length === 1 && ls[2].sides[0].render && ls[2].sides[0].capability === "wai.splat.int_codec" && ls.every((L) => L.sides.every((x) => x.held && x.exact)), `cond: two planes, three G-buffers and a splat set's render, every one held and exact (${sides.join(" | ")})`);
|
||
const r = checksAbout(hook, /^render of /);
|
||
expect(engine, r.length === 1 && r[0].ok, "cond: the splat set decoded in the tab, held to its signed digest, and its frame rendered in the tab at the pinned pose by wai-splat-render:i1");
|
||
const bad = hook.checks.filter((x) => !x.ok);
|
||
expect(engine, bad.length === 0, `cond: ${hook.checks.length} checks, every one holds${bad.length ? ": " + JSON.stringify(bad).slice(0, 300) : ""}`);
|
||
}
|
||
await close();
|
||
}
|
||
// 9e. A side plane withheld in delivery.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, "?pkg=staged-withhold/cond/", { refined: true });
|
||
const ls = hook.state === "ready" ? layersOf(hook, "c") : [];
|
||
expect(engine, ls.length === 3 && ls[0].outcome === "refused" && ls[0].reason === "side-absent" && !ls[0].sides[0].held && /fetch-failed/.test(ls[0].sides[0].why), `missing plane: a side object withheld in delivery is not held, and the layer is refused side-absent (${hook.state}: ${outcomes(ls)})`);
|
||
expect(engine, ls.length === 3 && ls[1].outcome === "applied" && ls[2].outcome === "applied", "missing plane: the other pieces' layers apply");
|
||
const d = hook.state === "ready" ? await readback(page, 0.5) : null;
|
||
expect(engine, d === stage0(COND_CASES[0]) && /c\/0\/L1\/wai\.image\.int_cond_refine was not applied \(side-absent\)/.test(hook.status), `missing plane: the base presents (${d && d.slice(0, 12)}), and the page says why`);
|
||
await close();
|
||
}
|
||
// 9f. The splat decoder left out of the profile: no render plane.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, "?pkg=staged/cond/&without=wai.splat.int_codec", { refined: true });
|
||
const ls = hook.state === "ready" ? layersOf(hook, "c") : [];
|
||
expect(engine, ls.length === 3 && ls[2].outcome === "refused" && ls[2].reason === "side-absent" && ls[2].sides[0].render && !ls[2].sides[0].held && /no_wasm/.test(ls[2].sides[0].why), `without the splat decoder: the splat set is not decoded, its render not held, and the layer refused side-absent (${hook.state}: ${outcomes(ls)})`);
|
||
expect(engine, ls.length === 3 && ls[0].outcome === "applied" && ls[1].outcome === "applied", "without the splat decoder: the layers over held planes apply");
|
||
const d = hook.state === "ready" ? await readback(page, 2.5) : null;
|
||
expect(engine, d === stage0(COND_CASES[2]) && /c\/2\/L1\/wai\.image\.int_cond_refine was not applied \(side-absent\)/.test(hook.status), `without the splat decoder: the base presents (${d && d.slice(0, 12)}), and the page says why`);
|
||
await close();
|
||
}
|
||
// 9g. A plane the page decodes only through its fallback.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, "?pkg=staged/cond-not-exact/", { refined: true });
|
||
const ls = hook.state === "ready" ? layersOf(hook, "c") : [];
|
||
expect(engine, ls.length === 1 && ls[0].outcome === "refused" && ls[0].reason === "side-not-exact", `not exact: the layer is refused side-not-exact (${hook.state}: ${outcomes(ls)})`);
|
||
const x = ls.length ? ls[0].sides[0] : null;
|
||
expect(engine, x && x.held && x.exact === false && x.via_fallback === true && x.capability === "wai.image.png", `not exact: its plane is held, decoded in the tab through the object's fallback wai.image.png, and so not exact (${x && sideState(x)})`);
|
||
const d = hook.state === "ready" ? await readback(page, 0.5) : null;
|
||
expect(engine, d === stage0(NOT_EXACT) && /not applied \(side-not-exact\)/.test(hook.status), `not exact: the base presents (${d && d.slice(0, 12)}), and the page says why`);
|
||
await close();
|
||
}
|
||
// 9h. A byte of a side object flipped in delivery.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, "?pkg=staged-flip/cond/", { refined: true });
|
||
const flipped = entriesOf(cIx, 0, 6)[1].hash;
|
||
const c = checksAbout(hook, new RegExp(`^object ${flipped.slice(0, 12)} .*asset`))[0];
|
||
expect(engine, c && !c.ok && c.code === "object-hash", `flipped side: a side object with one byte flipped in delivery is refused object-hash (${c ? c.code : "no check"})`);
|
||
const ls = hook.state === "ready" ? layersOf(hook, "c") : [];
|
||
expect(engine, ls.length === 3 && ls[0].outcome === "refused" && ls[0].reason === "side-absent" && !ls[0].sides[1].held && ls[1].outcome === "applied" && ls[2].outcome === "applied", `flipped side: never held, so its layer is refused side-absent; the other pieces' apply (${outcomes(ls)})`);
|
||
const d = hook.state === "ready" ? await readback(page, 0.5) : null;
|
||
expect(engine, d === stage0(COND_CASES[0]), `flipped side: the base presents (${d && d.slice(0, 12)})`);
|
||
await close();
|
||
}
|
||
}
|
||
|
||
|
||
/// Case 10: a graphics component's text floor (browser-sink §14.9).
|
||
const CAP_PKG = "?pkg=../../pack-conformance/multi/multi-captions-01/package/";
|
||
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||
/// Every frame of the programme presented in the page, each held to what this script expects of
|
||
/// it: the count of frames whose floor text, withheld blocks and cues are the expected ones, and
|
||
/// the first that is not.
|
||
async function capFrames(page, lang) {
|
||
const shown = await page.evaluate((ts) => ts.map((t) => window.__wai_compose.shownAt(t)), Array.from({ length: CAP_FRAMES }, (_, n) => (n + 0.5) / CAP_FPS));
|
||
const out = { blocks: 0, withheld: 0, cues: 0, overlay: 0, once: 0, captionFrames: 0, titleFrames: 0, announced: 0, first: null };
|
||
shown.forEach((s, n) => {
|
||
const want = capExpected(n, lang);
|
||
const b = same(s.blocks, want.blocks), w = same(s.floor ? s.floor.withheld.map((x) => x.index) : [], want.withheld), c = same(s.cues, want.cues);
|
||
out.blocks += b;
|
||
out.withheld += w && (!s.floor || s.floor.withheld.every((x) => x.reason === "caption_track"));
|
||
out.cues += c;
|
||
out.overlay += s.overlay === null;
|
||
out.titleFrames += want.blocks.length > 0;
|
||
out.announced += s.announced.length > 0;
|
||
if (want.captions.length) {
|
||
out.captionFrames++;
|
||
// Each caption active here: never in the floor's text, and in the text layer exactly
|
||
// once where the layer presents its language.
|
||
const everywhere = [...s.blocks.map((x) => x.text), ...s.cues];
|
||
out.once += want.captions.every((k) => everywhere.filter((x) => x === k.text).length === (k.lang === lang ? 1 : 0) && !s.blocks.some((x) => x.text === k.text));
|
||
}
|
||
if (!(b && w && c) && !out.first) out.first = `frame ${n}: floor ${JSON.stringify(s.blocks.map((x) => x.text))} for ${JSON.stringify(want.blocks.map((x) => x.text))}, withheld ${JSON.stringify(s.floor && s.floor.withheld)} for ${JSON.stringify(want.withheld)}, cues ${JSON.stringify(s.cues)} for ${JSON.stringify(want.cues)}`;
|
||
});
|
||
return out;
|
||
}
|
||
|
||
async function floorCases(browser, engine) {
|
||
const titleFrames = Array.from({ length: CAP_FRAMES }, (_, n) => capExpected(n, "en")).filter((w) => w.blocks.length).length;
|
||
const captionFrames = Array.from({ length: CAP_FRAMES }, (_, n) => capExpected(n, "en")).filter((w) => w.captions.length).length;
|
||
// 10a. This device: the overlay, and nothing from the floor.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, `${CAP_PKG}&without=wai.video.av1`);
|
||
expect(engine, hook.state === "ready", `text floor, this device: multi-captions-01 opens (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const g = hook.dispatch.components.find((c) => c.id === "g");
|
||
const alts = g.pieces.map((p) => p.alternatives.find((a) => a.role === "floor"));
|
||
expect(engine, g.pieces.length === capG.length && g.pieces.every((p) => p.role === "base" && p.capability === "wai.graphics.vector" && p.layer === "overlay" && p.route === "wasm") && alts.every((a) => a && a.capability === "wai.text.zstd" && a.route === "wasm" && a.rank === "classical") && hook.dispatch.device.wasm.includes("wai.text.zstd"), `text floor, this device: every graphics piece presents its overlay, its floor routed wasm and not chosen (${g.pieces.map((p) => p.why).join(" | ").slice(0, 120)})`);
|
||
const shown = await page.evaluate((ts) => ts.map((t) => window.__wai_compose.shownAt(t)), Array.from({ length: CAP_FRAMES }, (_, n) => (n + 0.5) / CAP_FPS));
|
||
expect(engine, hook.floor === null && shown.every((s) => s.floor === null && s.blocks.length === 0 && s.overlay !== null), `text floor, this device: at each of ${shown.length} frames an overlay frame is drawn and no text is presented from a floor`);
|
||
const bad = hook.checks.filter((c) => !c.ok);
|
||
expect(engine, bad.length === 0 && checksAbout(hook, /^floor /).length === 0, `text floor, this device: ${hook.checks.length} checks, every one holds, and no floor is read${bad.length ? ": " + JSON.stringify(bad).slice(0, 300) : ""}`);
|
||
}
|
||
await close();
|
||
}
|
||
// 10b. Without the vector decoder: the floor, as text.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, `${CAP_PKG}&without=wai.video.av1,wai.graphics.vector`);
|
||
expect(engine, hook.state === "ready", `text floor: opens without wai.graphics.vector (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const g = hook.dispatch.components.find((c) => c.id === "g");
|
||
const floor = g.presented && g.layer === "floor" && g.pieces.length === capG.length && g.pieces.every((p) => p.role === "floor" && p.capability === "wai.text.zstd" && p.layer === "floor" && p.route === "wasm" && /^the floor/.test(p.why));
|
||
const why = g.pieces.every((p) => p.alternatives.some((a) => a.role === "base" && a.route === "none" && a.legs.wasm === "no_wasm"));
|
||
expect(engine, floor && why, `text floor: every graphics piece presents its floor, the base unrouted for want of a decoder (${g.pieces.map((p) => p.why).join(" | ").slice(0, 150)})`);
|
||
expect(engine, /floor plays for g\/0/.test(hook.status) && /presented as text from their floor/.test(hook.status), `text floor: the page says so ("${hook.status.slice(0, 150)}")`);
|
||
const read = checksAbout(hook, /^floor .*the transcript of its piece's base/);
|
||
expect(engine, read.length === capG.length && read.every((c, i) => c.ok && c.base === capG[i].base && c.base === capG[i].motif && c.tb === capG[i].tb && c.blocks === capG[i].blocks.length), `text floor: each of ${read.length} floors read and held to its piece's base, the base its derived_from names in the package`);
|
||
expect(engine, hook.floor && hook.floor.pieces.length === capG.length && hook.floor.pieces.every((p, i) => p.transcript === capG[i].transcript && p.start === capG[i].start && p.duration === capG[i].duration), "text floor: each floor's transcript is, byte for byte, the one this script reads from the floor object");
|
||
const bad = hook.checks.filter((c) => !c.ok);
|
||
expect(engine, bad.length === 0 && hook.checks.length > 0, `text floor: ${hook.checks.length} checks, every one holds${bad.length ? ": " + JSON.stringify(bad).slice(0, 300) : ""}`);
|
||
const f = await capFrames(page, "en");
|
||
expect(engine, f.overlay === CAP_FRAMES, `text floor: no overlay frame is drawn at any of ${CAP_FRAMES} frames (${f.overlay})`);
|
||
expect(engine, f.blocks === CAP_FRAMES && f.titleFrames === titleFrames && titleFrames > 0, `text floor: at every frame the text from the floor is exactly the blocks active at its tick that are no caption, each a text node with its language and direction (${f.blocks} of ${CAP_FRAMES}; the title at ${titleFrames})${f.first ? ": " + f.first : ""}`);
|
||
expect(engine, f.withheld === CAP_FRAMES && captionFrames > 0, `text floor: every caption and subtitle active at a tick is withheld caption_track (${captionFrames} frames hold one)`);
|
||
expect(engine, f.cues === CAP_FRAMES, `text floor: at every frame the text layer shows exactly the English component's cues (${f.cues} of ${CAP_FRAMES})`);
|
||
expect(engine, f.once === f.captionFrames && f.captionFrames === captionFrames, `text floor: at each of ${f.captionFrames} frames with a caption block, its words are on the page once where the layer presents its language, from the cue and never from the floor (${f.once})`);
|
||
expect(engine, f.announced > 0, `text floor: the title, shown for more than a second, is in the polite live region (${f.announced} frames)`);
|
||
expect(engine, hook.text && hook.text.presented === "t" && hook.text.lang === "en" && same(hook.text.components, [{ id: "t", lang: "en" }, { id: "u", lang: "fr" }]), `text floor: the text layer presents the first text component, English (${JSON.stringify(hook.text)})`);
|
||
await page.click("#play");
|
||
await page.waitForFunction(() => window.__wai_compose.state === "ended", null, { timeout: 120000 });
|
||
const stats = await page.evaluate(() => window.__wai_compose.stats);
|
||
expect(engine, stats.ended && stats.overlay_frames === 0 && stats.floor_blocks >= 1 && stats.cues_shown >= 3 && stats.video_frames >= 50, `text floor: plays to its end with ${stats.floor_blocks} blocks drawn as text, ${stats.cues_shown} cues, ${stats.video_frames} video frames and no overlay frame`);
|
||
}
|
||
await close();
|
||
}
|
||
// 10c, 10d. Another text component, and none.
|
||
for (const [q, lang] of [["fr", "fr"], ["off", null]]) {
|
||
const { page, hook, close } = await open(browser, engine, `${CAP_PKG}&without=wai.video.av1,wai.graphics.vector&captions=${q}`);
|
||
const ok = hook.state === "ready" && hook.text && hook.text.lang === lang && hook.text.presented === (lang ? "u" : null);
|
||
expect(engine, ok, `text floor, captions=${q}: the text layer presents ${lang ? "the French component" : "no component"} (${hook.state}: ${JSON.stringify(hook.text)})`);
|
||
const f = ok ? await capFrames(page, lang) : { blocks: 0, cues: 0, withheld: 0, once: 0, captionFrames: -1 };
|
||
expect(engine, f.cues === CAP_FRAMES && f.blocks === CAP_FRAMES && f.withheld === CAP_FRAMES, `text floor, captions=${q}: at every frame the layer shows ${lang ? "exactly the French cues" : "no cue"}, and the text from the floor is unchanged, no caption among it${f.first ? ": " + f.first : ""}`);
|
||
expect(engine, f.once === f.captionFrames, `text floor, captions=${q}: a caption's words are on the page ${lang ? "once where the layer presents its language and not otherwise" : "nowhere: captions are off, and the floor does not put them back"} (${f.once} of ${f.captionFrames} frames)`);
|
||
await close();
|
||
}
|
||
// 10e. A byte of a floor object flipped in delivery.
|
||
{
|
||
const { hook, close } = await open(browser, engine, "?pkg=captions-flip/&without=wai.video.av1,wai.graphics.vector");
|
||
expect(engine, hook.state === "refused" && hook.refusal.stage === "object" && hook.refusal.code === "object-hash" && /^g\/0\/floor/.test(hook.refusal.where || ""), `flipped floor: a byte of ${CAP_FLIP.slice(0, 12)} flipped in delivery is refused ${hook.refusal ? hook.refusal.code + " at " + hook.refusal.where : hook.state}`);
|
||
expect(engine, hook.dispatch === null && /Refused, nothing presented/.test(hook.status), "flipped floor: nothing presented, and the page says why");
|
||
await close();
|
||
}
|
||
// 10f. Two floors exchanged in delivery: each consistent in itself, neither its entry's.
|
||
{
|
||
expect(engine, CAP_SWAP !== null, "exchanged floors: the golden holds two floor objects of one length");
|
||
const { hook, close } = await open(browser, engine, "?pkg=captions-swap/&without=wai.video.av1,wai.graphics.vector");
|
||
expect(engine, hook.state === "refused" && hook.refusal.stage === "object" && hook.refusal.code === "object-hash" && /^g\/\d\/floor/.test(hook.refusal.where || ""), `exchanged floors: another piece's floor, of the same length, served in a floor's place is refused ${hook.refusal ? hook.refusal.code + " at " + hook.refusal.where : hook.state}`);
|
||
expect(engine, hook.dispatch === null && /Refused, nothing presented/.test(hook.status), "exchanged floors: nothing presented");
|
||
await close();
|
||
}
|
||
// 10g. The bundle's reader, in the page: a floor against its own base and against another's.
|
||
{
|
||
const { page, hook, close } = await open(browser, engine, `${CAP_PKG}&without=wai.video.av1,wai.graphics.vector`);
|
||
const r = hook.state !== "ready" ? null : await page.evaluate(async ({ floor, own, other }) => {
|
||
const mod = await import("./pkg/wai_compose.js");
|
||
const bytes = new Uint8Array(await (await fetch(`../../pack-conformance/multi/multi-captions-01/package/objects/${floor}.wai`)).arrayBuffer());
|
||
const env = mod.composeEnvelope(bytes);
|
||
const attempt = (payload, base) => {
|
||
try {
|
||
const f = new mod.TextFloor(env.manifest, payload, base);
|
||
return `read ${f.blocks}`;
|
||
} catch (e) {
|
||
return String(e.message || e);
|
||
}
|
||
};
|
||
const head = env.payload.slice();
|
||
head[4] ^= 0x04;
|
||
return { own: attempt(env.payload, own), other: attempt(env.payload, other), frame: attempt(head, own) };
|
||
}, { floor: capG[1].floor, own: capG[1].base, other: capG[0].base });
|
||
expect(engine, r && r.own === `read ${capG[1].blocks.length}`, `the reader: a floor held to its own piece's base reads (${r && r.own})`);
|
||
expect(engine, r && r.other === "floor_base", `the reader: the same floor held to another piece's base is refused ${r && r.other}`);
|
||
expect(engine, r && r.frame === "floor_frame", `the reader: with a descriptor that announces a checksum it is refused ${r && r.frame}`);
|
||
await close();
|
||
}
|
||
}
|
||
|
||
for (const engine of engines) {
|
||
console.log(`== ${engine}`);
|
||
let browser;
|
||
try {
|
||
browser = await pw[engine].launch(engine === "chromium" ? { args: ["--autoplay-policy=no-user-gesture-required"] } : {});
|
||
} catch (e) {
|
||
fail(engine, `cannot launch: ${String(e.message).split("\n")[0]}`);
|
||
continue;
|
||
}
|
||
try {
|
||
// 1. this device.
|
||
if (!only || only === "package") {
|
||
const { page, hook, close } = await open(browser, engine, "");
|
||
expect(engine, hook.state === "ready", `this device: the package opens (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const bad = hook.checks.filter((c) => !c.ok);
|
||
expect(engine, bad.length === 0 && hook.checks.length > 0, `this device: ${hook.checks.length} checks, every one holds${bad.length ? ": " + JSON.stringify(bad) : ""}`);
|
||
expect(engine, hook.checks.some((c) => c.what.startsWith("receipt: Ed25519") && c.ok), "this device: the receipt's Ed25519 signature is checked");
|
||
const objectChecks = checksAbout(hook, /^object /);
|
||
const ids = new Set(objectChecks.map((c) => c.what.match(/\((\w+), piece/)[1]));
|
||
expect(engine, receipt.components.every((c) => ids.has(c.id)), `this device: an object of every component is checked against its signed index entry (${[...ids].join(", ")}; ${objectChecks.length} objects)`);
|
||
const presented = hook.dispatch.components.filter((c) => c.presented).map((c) => `${c.id}:${c.layer}`);
|
||
expect(engine, presented.length === receipt.components.length, `this device: every component presented (${presented.join(", ")})`);
|
||
console.log(` dispatch: ${hook.dispatch.components.map((c) => `${c.id}=` + [...new Set(c.pieces.map((p) => `${p.capability}@${p.route}`))].join("+")).join(" ")}`);
|
||
// 2. each decoded output against what the signed indexes state.
|
||
const sWant = bases("s").map((e) => e.digest);
|
||
const sGot = hook.audio ? hook.audio.pieces.map((p) => p.pcm_sha256) : [];
|
||
expect(engine, sGot.length === sWant.length && sGot.every((d, i) => d === sWant[i]), `score: each piece's samples synthesised in the tab have the SHA-256 its index entry states (${sGot.length} of ${sWant.length})`);
|
||
const sSamples = index.s.pieces.reduce((n, p) => n + p.duration, 0);
|
||
expect(engine, hook.audio && hook.audio.samples === sSamples, `score: ${hook.audio && hook.audio.samples} samples end to end, the programme's ${sSamples}`);
|
||
const sTarget = bases("s").map((e) => targetOf(e.hash));
|
||
expect(engine, hook.audio && sTarget.length > 0 && sTarget.every((t) => t && t.channels === hook.audio.channels && t.rate === hook.audio.sample_rate), `score: synthesised in ${hook.audio && hook.audio.channels} channel(s) at ${hook.audio && hook.audio.sample_rate} Hz, as each piece's target states`);
|
||
const tChecks = checksAbout(hook, /^text .*SHA-256 of the segment/);
|
||
expect(engine, tChecks.length === index.t.pieces.length && tChecks.every((c, i) => c.ok && c.got === bases("t")[i].digest), `text: each of ${index.t.pieces.length} segments held to its signed digest`);
|
||
const fSeals = checksAbout(hook, /^state .*sealed state hash/);
|
||
const fChain = checksAbout(hook, /^state .*starts from the state/);
|
||
expect(engine, fSeals.length === index.f.pieces.length && fSeals.every((c) => c.ok) && fChain.length === index.f.pieces.length - 1 && fChain.every((c) => c.ok), `feed: each of ${index.f.pieces.length} pieces replayed to its seal, each after the first from the state the one before seals`);
|
||
// Play it, by a click: a user gesture, which Web Audio may need.
|
||
await page.click("#play");
|
||
await page.waitForFunction(() => window.__wai_compose.state === "ended", null, { timeout: 120000 });
|
||
const stats = await page.evaluate(() => window.__wai_compose.stats);
|
||
expect(engine, stats.ended && stats.video_frames >= 100, `plays: ${stats.video_frames} video frames presented on the clock to its end (${stats.clock_s.toFixed(2)} s)`);
|
||
expect(engine, stats.overlay_frames >= 10, `plays: ${stats.overlay_frames} overlay frames rasterised and drawn`);
|
||
expect(engine, stats.cues_shown >= 3, `plays: ${stats.cues_shown} cues drawn as text`);
|
||
expect(engine, stats.state_updates >= 3, `plays: ${stats.state_updates} feed states drawn from the signed ops`);
|
||
expect(engine, stats.audio_started, `plays: the score started on Web Audio (context ${stats.audio_context}, re-placed ${stats.audio_resyncs}×)`);
|
||
// The overlay, every frame, hashed in the page.
|
||
const o = await page.evaluate(() => window.__wai_compose.overlayDigests());
|
||
const want = expectedFile.frames.find((f) => f.w === o.size[0] && f.h === o.size[1] && f.num === o.rate[0] && f.den === o.rate[1]);
|
||
expect(engine, want && o.digests.length === want.frames.length && want.frames.every(([g, d]) => o.digests[g - o.frames[0]] === d) && want.clip === o.clip, `graphics: all ${o.digests.length} overlay frames at ${o.size.join("x")} equal vector-conformance's expected frames (clip ${o.clip})`);
|
||
const gWant = bases("g").map((e) => e.digest);
|
||
expect(engine, o.pieces.length === gWant.length && o.pieces.every((p, i) => p.clip === gWant[i]), `graphics: each piece's clip digest is the one its index entry states (${o.pieces.map((p) => `${p.frames.join("-")}`).join(", ")})`);
|
||
}
|
||
await close();
|
||
}
|
||
// 3. without the AV1 decoder: the integer bases decoded here, held to signed digests.
|
||
if (!only || only === "package") {
|
||
const { hook, close } = await open(browser, engine, "?without=wai.video.av1");
|
||
expect(engine, hook.state === "ready", `without AV1: opens (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const clips = checksAbout(hook, /^video .*clip digest/);
|
||
const vWant = bases("v").filter((e) => e.tier === 2).map((e) => e.digest);
|
||
expect(engine, clips.length === index.v.pieces.length && clips.every((c) => c.ok && c.got === c.expected), `without AV1: ${clips.length} video bases decoded in WebAssembly, each clip digest the signed prefix digest`);
|
||
expect(engine, vWant.length > 0 && vWant.every((d) => clips.some((c) => c.got === d)), `without AV1: and each is the digest this script reads from the index (${vWant.length})`);
|
||
const v = hook.dispatch.components.find((c) => c.id === "v");
|
||
expect(engine, v.pieces.every((p) => p.role === "base" && p.route === "wasm"), "without AV1: every video piece's base routed wasm");
|
||
}
|
||
await close();
|
||
}
|
||
// 4. without the integer rung and the still: the floor.
|
||
if (!only || only === "package") {
|
||
const { hook, close } = await open(browser, engine, "?without=wai.video.int_motion,wai.image.png");
|
||
expect(engine, hook.state === "ready", `forced profile: opens (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const v = hook.dispatch.components.find((c) => c.id === "v");
|
||
const floor = v.pieces.every((p) => p.role === "floor" && p.capability === "wai.video.av1" && p.route === "ua");
|
||
const why = v.pieces.every((p) => p.alternatives.some((a) => a.role === "base" && a.route === "none" && a.legs.wasm === "no_wasm"));
|
||
expect(engine, floor && why, `forced profile: every video piece chose the AV1 floor, the base unrouted for want of a WebAssembly decoder (${v.pieces.map((p) => p.why).join(" | ").slice(0, 160)})`);
|
||
expect(engine, /floor plays/.test(hook.status) && v.pieces.every((p) => /^the floor/.test(p.why)), `forced profile: the page says so ("${hook.status}")`);
|
||
expect(engine, hook.dispatch.device.forced_without.includes("wai.video.int_motion"), "forced profile: the dispatch record names the forced profile");
|
||
const decoded = checksAbout(hook, /through the user agent's decoder/);
|
||
expect(engine, decoded.length === v.pieces.length && decoded.every((c) => c.ok), `forced profile: every floor piece decoded by the user agent (${decoded.map((c) => c.frames).join("+")} frames)`);
|
||
}
|
||
await close();
|
||
}
|
||
// 5. the forged receipt.
|
||
if (!only || only === "package") {
|
||
const { hook, close } = await open(browser, engine, "?receipt=mutations/forged.receipt.json");
|
||
expect(engine, hook.state === "refused" && hook.refusal.stage === "receipt" && hook.refusal.code === "receipt-signature", `forged receipt: refused ${hook.refusal ? hook.refusal.code : hook.state}`);
|
||
expect(engine, hook.dispatch === null && /Refused, nothing presented/.test(hook.status), "forged receipt: nothing presented, and the page says why");
|
||
await close();
|
||
}
|
||
// 6. a flipped byte.
|
||
if (!only || only === "package") {
|
||
const { hook, close } = await open(browser, engine, "?pkg=package-flip/");
|
||
expect(engine, hook.state === "refused" && hook.refusal.stage === "object" && hook.refusal.code === "object-hash", `flipped byte in ${FLIP.slice(0, 12)}: refused ${hook.refusal ? hook.refusal.code + " at " + hook.refusal.where : hook.state}`);
|
||
expect(engine, hook.dispatch === null && /Refused, nothing presented/.test(hook.status), "flipped byte: nothing presented, and the page says why");
|
||
await close();
|
||
}
|
||
// 7. without the score: its FLAC floor, decoded in WebAssembly.
|
||
if (!only || only === "package") {
|
||
const { hook, close } = await open(browser, engine, "?without=wai.audio.score");
|
||
expect(engine, hook.state === "ready", `without the score: opens (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (hook.state === "ready") {
|
||
const s = hook.dispatch.components.find((c) => c.id === "s");
|
||
const floor = s.presented && s.pieces.every((p) => p.role === "floor" && p.capability === "wai.audio.flac" && p.route === "wasm" && /^the floor/.test(p.why));
|
||
expect(engine, floor && /floor plays/.test(hook.status), `without the score: every score piece plays its FLAC floor, decoded in WebAssembly, and the page says so ("${hook.status}")`);
|
||
const sWant = bases("s").map((e) => e.digest);
|
||
const sGot = hook.audio ? hook.audio.pieces.map((p) => p.pcm_sha256) : [];
|
||
expect(engine, sGot.length === sWant.length && sGot.every((d, i) => d === sWant[i]), `without the score: each FLAC piece decodes to the samples whose SHA-256 the score's signed base states (${sGot.length} of ${sWant.length})`);
|
||
}
|
||
await close();
|
||
}
|
||
// 8. a sound track's Opus floor, through the user agent's AudioDecoder.
|
||
if (!only || only === "package") {
|
||
const { hook, close } = await open(browser, engine, "?pkg=../../pack-conformance/multi/multi-opus-01/package/&without=wai.audio.flac");
|
||
const lead = hook.state === "refused" && hook.refusal.stage === "decode" && hook.refusal.code === "ua-decode-error" && hook.refusal.where === "v, piece 0";
|
||
expect(engine, hook.state === "ready" || lead, `Opus floor: multi-opus-01 opens without FLAC, or is refused only for its 64 x 36 picture (${hook.state}${hook.refusal ? " " + JSON.stringify(hook.refusal) : ""})`);
|
||
if (lead) {
|
||
expect(engine, hook.dispatch === null && /Refused, nothing presented/.test(hook.status), "Opus floor: this engine's VideoDecoder does not take the golden's picture: nothing presented, and the sound not reached (no Opus checked here)");
|
||
expect(engine, !["prefix-digest", "object-hash", "compose_frames"].includes(hook.refusal.code), "Opus floor: no object of the golden failed its check");
|
||
} else if (hook.state === "ready") {
|
||
const a = hook.dispatch.components.find((c) => c.id === "a");
|
||
const v = hook.dispatch.components.find((c) => c.id === "v");
|
||
const takes = a.pieces.some((p) => p.capability === "wai.audio.opus" && p.route === "ua");
|
||
if (takes) {
|
||
const floor = a.presented && a.pieces.every((p) => p.role === "floor" && p.capability === "wai.audio.opus" && p.route === "ua");
|
||
expect(engine, floor && v.presented, "Opus floor: every piece of the sound track plays its Opus floor through the user agent");
|
||
const want = opusIndex.pieces.map((p) => p.duration);
|
||
const got = hook.audio ? hook.audio.pieces.map((p) => p.samples) : [];
|
||
expect(engine, got.length === want.length && got.every((n, i) => n === want[i]) && hook.audio.channels === 2 && hook.audio.sample_rate === 48000, `Opus floor: each piece comes to exactly its samples, two channels at 48 kHz (${got.join(", ")} of ${want.join(", ")})`);
|
||
} else {
|
||
expect(engine, !a.presented && v.presented, "Opus floor: this engine's AudioDecoder does not take Opus: the sound track is not presented, the picture is");
|
||
expect(engine, hook.audio === null, "Opus floor: and no audio is played");
|
||
}
|
||
}
|
||
await close();
|
||
}
|
||
if (!only || only === "staged") await stagedCases(browser, engine);
|
||
if (!only || only === "floor") await floorCases(browser, engine);
|
||
} catch (e) {
|
||
fail(engine, `the run stopped: ${e.stack || e}`);
|
||
}
|
||
await browser.close();
|
||
if ((passed[engine] || 0) < CHECKS_PER_ENGINE) fail(engine, `${passed[engine] || 0} checks passed, fewer than the ${CHECKS_PER_ENGINE} this script makes`);
|
||
}
|
||
server.close();
|
||
console.log(failures ? `run.mjs: ${failures} failure(s)` : `run.mjs: every check passed in ${engines.join(", ")}`);
|
||
process.exit(failures ? 1 : 0);
|