155 lines
8.9 KiB
JavaScript
155 lines
8.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");
|
|
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}`);
|
|
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`);
|
|
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}\n`);
|