Files
workinf_Blender_Wasm/tools/web/check-source-dynamic-inventory.mjs
mes123456 f68c6c9678
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Add full source parity validation workflow
2026-09-07 16:45:07 +08:00

176 lines
9.9 KiB
JavaScript

import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const outputPath = path.join(root, "tests/golden/WBV2/dynamic-inventory.json");
const generatorPath = path.join(root, "tools/web/generate-source-dynamic-inventory.mjs");
const reportPath = path.join(root, "tests/golden/WBV2/dynamic-inventory-check-report.json");
const manifestPath = path.join(root, "tests/golden/WBV2/dynamic-inventory-manifest.json");
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const taskArgument = process.argv.indexOf("--task");
const write = process.argv.includes("--write");
if (taskArgument >= 0) {
const task = process.argv[taskArgument + 1];
if (!task) throw new Error("--task requires a task id");
const taskManifestPath = path.join(root, `tests/golden/${task}/manifest.json`);
const taskManifest = readJson(taskManifestPath);
assert.equal(taskManifest.status, "done");
const taskPlan = readJson(path.join(root, taskManifest.artifacts.plan.path));
const taskRecord = taskPlan.tasks.find((candidate) => candidate.id === task);
const selectedUnits = taskRecord?.module?.coverageBindings ?? [];
assert.ok(selectedUnits.length, `task ${task} has no selected coverage units`);
const inventory = readJson(outputPath);
assert.equal(inventory.authority.sourceTreeSha256, inventory.coverageLedger?.[0]?.sourceTreeSha256 ?? inventory.authority.sourceTreeSha256);
assert.equal(inventory.coverage.verifiedUnits + inventory.coverage.unverifiedUnits, inventory.summary.unitCount);
const sourceArtifacts = Object.values(taskManifest.artifacts ?? {}).filter((artifact) => artifact?.path?.startsWith("blender-5.2.0/source/blender/"));
assert.ok(sourceArtifacts.length, `task ${task} has no source artifacts`);
for (const artifact of sourceArtifacts) {
assert.ok(fs.existsSync(path.join(root, artifact.path)), artifact.path);
assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path);
}
for (const unitId of selectedUnits) {
const unit = inventory.units.find((candidate) => candidate.id === unitId);
assert.ok(unit, `dynamic unit missing: ${unitId}`);
assert.equal(unit.coverageStatus, "VERIFIED", unitId);
assert.equal(unit.coverage?.task, task, `${unitId} is bound to another task`);
}
const report = {
schemaVersion: 1,
operation: "BLENDER_52_SOURCE_DYNAMIC_INVENTORY_CHECK",
scope: "batch",
task,
status: "PASS",
sourceTreeSha256: inventory.authority.sourceTreeSha256,
sourceFiles: sourceArtifacts.length,
unitCount: inventory.summary.unitCount,
planningEstimate: inventory.policy.planningEstimate,
verifiedUnits: inventory.coverage.verifiedUnits,
unverifiedUnits: inventory.coverage.unverifiedUnits,
selectedUnits,
deterministic: "batch_hash_bound",
};
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
process.stdout.write(`source-dynamic-inventory-batch-ok task=${task} sourceFiles=${sourceArtifacts.length} units=${inventory.summary.unitCount} selected=${selectedUnits.length} verified=${inventory.coverage.verifiedUnits} unverified=${inventory.coverage.unverifiedUnits} sourceTree=${inventory.authority.sourceTreeSha256}\n`);
process.exit(0);
}
const inventory = readJson(outputPath);
const manifest = readJson(manifestPath);
assert.equal(inventory.schemaVersion, 1);
assert.equal(inventory.operation, "BLENDER_52_SOURCE_DYNAMIC_INVENTORY");
assert.ok(["SOURCE_DERIVED_UNVERIFIED", "SOURCE_DERIVED_WITH_EXPLICIT_COVERAGE"].includes(inventory.status));
assert.equal(inventory.authority.sourceRoot, "blender-5.2.0/source/blender");
const expectedSourceDirectories = fs.readdirSync(path.join(root, "blender-5.2.0/source/blender"), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "tests" && entry.name !== "test")
.map((entry) => entry.name)
.sort();
assert.deepEqual(inventory.authority.sourceDirectories, expectedSourceDirectories);
assert.deepEqual(inventory.authority.excludedDirectories, ["tests", "test"]);
assert.equal(inventory.policy.initialLegacyEstimate, 6900);
assert.equal(inventory.policy.planningEstimate, 5200);
assert.equal(inventory.policy.hardTarget, false);
assert.equal(inventory.coverage.verifiedUnits + inventory.coverage.unverifiedUnits, inventory.summary.unitCount);
assert.ok(["forbidden", "explicit_source_case_bindings_only"].includes(inventory.coverage.inventoryMutation));
assert.equal(inventory.coverageLedger?.length ?? 0, inventory.coverage.verifiedUnits);
assert.equal(new Set((inventory.coverageLedger ?? []).map((binding) => binding.unitId)).size, inventory.coverage.verifiedUnits);
assert.equal(inventory.sourceFiles.length, inventory.summary.sourceFiles);
assert.equal(inventory.units.length, inventory.summary.unitCount);
assert.equal(manifest.schemaVersion, 1);
assert.equal(manifest.operation, "BLENDER_52_SOURCE_DYNAMIC_INVENTORY");
assert.equal(manifest.status, "done");
assert.equal(inventory.summary.unitCount, Object.values(inventory.summary.byKind).reduce((total, count) => total + count, 0));
assert.equal(inventory.summary.unitCount, Object.values(inventory.summary.byFamily).reduce((total, count) => total + count, 0));
assert.deepEqual(inventory.units.map((unit) => unit.id), [...inventory.units].map((unit) => unit.id).sort());
assert.equal(new Set(inventory.units.map((unit) => unit.id)).size, inventory.units.length);
assert.equal(inventory.sourceTreeSha256 ?? inventory.authority.sourceTreeSha256, inventory.authority.sourceTreeSha256);
const sourceFiles = new Map(inventory.sourceFiles.map((file) => [file.path, file]));
assert.equal(sourceFiles.size, inventory.sourceFiles.length);
for (const file of inventory.sourceFiles) {
const absolute = path.join(root, file.path);
assert.ok(fs.existsSync(absolute), file.path);
const bytes = fs.readFileSync(absolute);
assert.equal(bytes.length, file.bytes, file.path);
assert.equal(sha256(bytes), file.sha256, file.path);
assert.ok(!file.sourcePath.includes("/tests/"), file.sourcePath);
}
for (const [name, artifact] of Object.entries(manifest.artifacts)) {
assert.ok(fs.existsSync(path.join(root, artifact.path)), `${name}: ${artifact.path}`);
if (!write) assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, `${name}: ${artifact.path}`);
}
const treeDigest = sha256(inventory.sourceFiles.map((file) => `${file.sourcePath}\0${file.sha256}\n`).join(""));
assert.equal(treeDigest, inventory.authority.sourceTreeSha256);
for (const unit of inventory.units) {
assert.match(unit.id, /^[a-z-]+:[^\n]+$/u);
assert.ok(["operator", "operator-macro", "rna-struct", "node", "modifier-source", "web-command"].includes(unit.kind));
assert.ok(["EDITOR", "RNA", "NODES", "MODIFIER", "ANIMATION", "SEQUENCER", "CORE", "WINDOW_MANAGER", "WEB_ENGINE", "OTHER"].includes(unit.family));
assert.ok(["UNVERIFIED", "VERIFIED"].includes(unit.coverageStatus));
assert.ok(unit.locations.length > 0);
for (const location of unit.locations) {
const source = sourceFiles.get(location.path);
assert.ok(source, location.path);
assert.ok(Number.isSafeInteger(location.line) && location.line > 0, `${unit.id}:${location.line}`);
}
if (unit.coverageStatus === "VERIFIED") assert.ok(unit.coverage?.task && unit.coverage?.evidenceManifest && unit.coverage?.productionEvidence);
}
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "wbv2-source-inventory-"));
try {
const regenerated = path.join(temporary, "dynamic-inventory.json");
execFileSync(process.execPath, [generatorPath, "--output", regenerated], { cwd: root, stdio: "pipe" });
assert.equal(
sha256(fs.readFileSync(regenerated)),
sha256(fs.readFileSync(outputPath)),
"dynamic inventory is not deterministic",
);
}
finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
const report = {
schemaVersion: 1,
operation: "BLENDER_52_SOURCE_DYNAMIC_INVENTORY_CHECK",
status: "PASS",
sourceTreeSha256: inventory.authority.sourceTreeSha256,
sourceFiles: inventory.summary.sourceFiles,
sourceBytes: inventory.summary.sourceBytes,
unitCount: inventory.summary.unitCount,
byKind: inventory.summary.byKind,
byFamily: inventory.summary.byFamily,
planningEstimate: inventory.policy.planningEstimate,
hardTarget: inventory.policy.hardTarget,
verifiedUnits: inventory.coverage.verifiedUnits,
unverifiedUnits: inventory.coverage.unverifiedUnits,
deterministic: true,
};
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
if (write) {
const artifactSha256 = (name) => sha256(fs.readFileSync(path.join(root, manifest.artifacts[name].path)));
for (const name of Object.keys(manifest.artifacts)) manifest.artifacts[name].sha256 = artifactSha256(name);
manifest.source = {
root: inventory.authority.sourceRoot,
treeSha256: inventory.authority.sourceTreeSha256,
fileCount: inventory.summary.sourceFiles,
byteCount: inventory.summary.sourceBytes,
};
manifest.inventory = {
...manifest.inventory,
unitCount: inventory.summary.unitCount,
byKind: inventory.summary.byKind,
planningEstimate: inventory.policy.planningEstimate,
hardTarget: inventory.policy.hardTarget,
verifiedUnits: inventory.coverage.verifiedUnits,
unverifiedUnits: inventory.coverage.unverifiedUnits,
};
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
}
process.stdout.write(`source-dynamic-inventory-ok files=${report.sourceFiles} units=${report.unitCount} planningEstimate=${report.planningEstimate} verified=${report.verifiedUnits} unverified=${report.unverifiedUnits} sourceTree=${report.sourceTreeSha256} write=${write}\n`);