Advance M8-M11 parity workflows
This commit is contained in:
132
web/tests/e2e/sequencer-media-cache.spec.ts
Normal file
132
web/tests/e2e/sequencer-media-cache.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = path.resolve(import.meta.dirname, "../../..");
|
||||
const codecGolden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-09/sequencer-codec-probe.json"), "utf8")) as {
|
||||
assets: Array<{
|
||||
stripType: "IMAGE" | "SOUND" | "MOVIE";
|
||||
mimeType: string;
|
||||
path: string;
|
||||
byteLength: number;
|
||||
sha256: string;
|
||||
decoded: Record<string, number>;
|
||||
}>;
|
||||
};
|
||||
const cacheGolden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-10/sequencer-media-cache.json"), "utf8")) as {
|
||||
sourceSha256: string;
|
||||
profile: { kind: "MOVIE_RGBA8_FRAME"; width: number; height: number; colorSpace: "SRGB8"; alphaMode: "STRAIGHT" };
|
||||
identitySha256: string;
|
||||
proxyByteLength: number;
|
||||
};
|
||||
|
||||
test("M11-10 binds a real movie proxy cache to source hash and runtime decode capability", async ({ page }) => {
|
||||
const movie = codecGolden.assets.find((asset) => asset.stripType === "MOVIE")!;
|
||||
const bytes = fs.readFileSync(path.join(root, movie.path));
|
||||
expect(crypto.createHash("sha256").update(bytes).digest("hex")).toBe(cacheGolden.sourceSha256);
|
||||
await page.goto("/");
|
||||
const result = await page.evaluate(async ({ movie, profile, bytes }) => {
|
||||
const [probeModule, cacheModule, storageModule] = await Promise.all([
|
||||
import("/src/sequencer/SequencerCodecProbe.ts"),
|
||||
import("/src/sequencer/SequencerMediaProxyCache.ts"),
|
||||
import("/src/storage/StorageClient.ts"),
|
||||
]);
|
||||
const request = probeModule.createSequencerCodecProbeRequest("MOVIE", movie.mimeType, movie.byteLength, movie.sha256);
|
||||
const sourceData = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
const capability = await probeModule.probeSequencerCodec(request, sourceData.slice(0));
|
||||
const generated = await cacheModule.generateInitialSequencerMovieProxyFrame(request, capability, profile, sourceData.slice(0));
|
||||
const cache = new cacheModule.SequencerMediaProxyCache(generated.data.byteLength);
|
||||
const firstKey = await cache.put(generated.manifest, generated.data.slice(0), request, capability);
|
||||
const hit = await cache.get(request, capability, profile, 0);
|
||||
const secondManifest = await import("/src/sequencer/SequencerTimeline.ts").then((module) =>
|
||||
module.createSequencerMediaCacheManifest(request, capability, profile, 1, generated.data.slice(0)));
|
||||
const secondKey = await cache.put(secondManifest, generated.data.slice(0), request, capability);
|
||||
const evicted = await cache.get(request, capability, profile, 0);
|
||||
|
||||
const projectId = `m11-10-${Date.now()}`;
|
||||
const writer = new storageModule.StorageClient();
|
||||
const storedPayload = await writer.putAsset(
|
||||
projectId,
|
||||
generated.data.slice(0),
|
||||
"application/vnd.blender.sequencer-proxy-rgba8",
|
||||
"cache/sequencer/proxy-frame-0.rgba8",
|
||||
);
|
||||
const manifestData = new TextEncoder().encode(JSON.stringify(generated.manifest)).buffer as ArrayBuffer;
|
||||
const storedManifest = await writer.putAsset(
|
||||
projectId,
|
||||
manifestData,
|
||||
"application/vnd.blender.sequencer-proxy-cache+json",
|
||||
"cache/sequencer/proxy-frame-0.json",
|
||||
);
|
||||
writer.terminate();
|
||||
|
||||
const reader = new storageModule.StorageClient();
|
||||
const [reopenedPayload, reopenedManifestAsset] = await Promise.all([
|
||||
reader.readAsset(projectId, storedPayload.sha256),
|
||||
reader.readAsset(projectId, storedManifest.sha256),
|
||||
]);
|
||||
reader.terminate();
|
||||
const reopenedManifest = JSON.parse(new TextDecoder().decode(reopenedManifestAsset.data));
|
||||
const verified = await import("/src/sequencer/SequencerTimeline.ts").then((module) =>
|
||||
module.verifySequencerMediaCacheEntry(reopenedManifest, reopenedPayload.data, request, capability));
|
||||
|
||||
const changedSource = { ...request, sourceSha256: "e".repeat(64) };
|
||||
const changedSourceCapability = { ...capability, sourceSha256: changedSource.sourceSha256 };
|
||||
const changedCapability = {
|
||||
...capability,
|
||||
decoded: { ...capability.decoded!, durationMicros: capability.decoded!.durationMicros! + 1 },
|
||||
};
|
||||
const corruptPayload = reopenedPayload.data.slice(0);
|
||||
new Uint8Array(corruptPayload)[0] ^= 0xff;
|
||||
const code = async (operation: () => Promise<unknown>): Promise<string> => {
|
||||
try { await operation(); return "unexpected-success"; }
|
||||
catch (error) { return (error as { code?: string }).code ?? String(error); }
|
||||
};
|
||||
const protocol = await import("/src/sequencer/SequencerTimeline.ts");
|
||||
const errors = {
|
||||
source: await code(() => protocol.verifySequencerMediaCacheEntry(reopenedManifest, reopenedPayload.data, changedSource, changedSourceCapability)),
|
||||
capability: await code(() => protocol.verifySequencerMediaCacheEntry(reopenedManifest, reopenedPayload.data, request, changedCapability)),
|
||||
payload: await code(() => protocol.verifySequencerMediaCacheEntry(reopenedManifest, corruptPayload, request, capability)),
|
||||
sourceBytes: await code(() => cacheModule.generateInitialSequencerMovieProxyFrame(request, capability, profile, new ArrayBuffer(request.byteLength))),
|
||||
};
|
||||
const statsBeforeClear = cache.stats();
|
||||
const releasedBytes = cache.clear();
|
||||
return {
|
||||
capability,
|
||||
manifest: generated.manifest,
|
||||
firstKey,
|
||||
secondKey,
|
||||
hitBytes: hit?.data.byteLength,
|
||||
evicted: evicted === undefined,
|
||||
statsBeforeClear,
|
||||
releasedBytes,
|
||||
statsAfterClear: cache.stats(),
|
||||
storedPayloadSha256: storedPayload.sha256,
|
||||
verifiedIdentity: verified.identitySha256,
|
||||
errors,
|
||||
};
|
||||
}, { movie, profile: cacheGolden.profile, bytes: new Uint8Array(bytes) });
|
||||
|
||||
expect(result.capability).toMatchObject({ status: "READY", backend: "HTML_MEDIA", decoded: movie.decoded });
|
||||
expect(result.manifest).toMatchObject({
|
||||
identitySha256: cacheGolden.identitySha256,
|
||||
payloadByteLength: cacheGolden.proxyByteLength,
|
||||
profile: cacheGolden.profile,
|
||||
});
|
||||
expect(result.manifest.payloadSha256).toBe(result.storedPayloadSha256);
|
||||
expect(result.verifiedIdentity).toBe(cacheGolden.identitySha256);
|
||||
expect(result.firstKey).toBe(`sequencer-media-cache:v1:${cacheGolden.identitySha256}`);
|
||||
expect(result.secondKey).not.toBe(result.firstKey);
|
||||
expect(result.hitBytes).toBe(cacheGolden.proxyByteLength);
|
||||
expect(result.evicted).toBe(true);
|
||||
expect(result.statsBeforeClear).toMatchObject({ entries: 1, bytes: cacheGolden.proxyByteLength, hits: 1, misses: 1, evictions: 1 });
|
||||
expect(result.releasedBytes).toBe(cacheGolden.proxyByteLength);
|
||||
expect(result.statsAfterClear).toMatchObject({ entries: 0, bytes: 0 });
|
||||
expect(result.errors).toEqual({
|
||||
source: "SEQUENCER_CACHE_SOURCE_MISMATCH",
|
||||
capability: "SEQUENCER_CACHE_CAPABILITY_MISMATCH",
|
||||
payload: "SEQUENCER_CACHE_HASH_MISMATCH",
|
||||
sourceBytes: "SEQUENCER_CACHE_SOURCE_MISMATCH",
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user