133 lines
6.8 KiB
TypeScript
133 lines
6.8 KiB
TypeScript
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",
|
|
});
|
|
});
|