perf(application): scan version artifacts in parallel
This commit is contained in:
parent
2d1e021782
commit
c00739bb1f
|
|
@ -34,6 +34,43 @@ export interface InventoryEvidencePages {
|
|||
readonly right: readonly Evidence[];
|
||||
}
|
||||
|
||||
/** Injectable artifact scan boundary used by paired version inventory. */
|
||||
export type VersionInventoryScanner = typeof scanAuthorizedArtifactInventory;
|
||||
|
||||
/** Inputs shared by the two scans in one version comparison. */
|
||||
export interface VersionInventoryScanOptions {
|
||||
readonly leftPath: string;
|
||||
readonly rightPath: string;
|
||||
readonly inputRoots: readonly string[];
|
||||
readonly limits: ArtifactLimits;
|
||||
readonly signal?: AbortSignal;
|
||||
readonly integrity: ArtifactIntegrityPolicy;
|
||||
}
|
||||
|
||||
/** Start both independent version scans while preserving left/right identity. */
|
||||
export const scanVersionInventories = async (
|
||||
options: VersionInventoryScanOptions,
|
||||
scanner: VersionInventoryScanner = scanAuthorizedArtifactInventory,
|
||||
): Promise<VersionSnapshots> => {
|
||||
const [left, right] = await Promise.all([
|
||||
scanner(
|
||||
options.leftPath,
|
||||
options.inputRoots,
|
||||
options.limits,
|
||||
options.signal,
|
||||
options.integrity,
|
||||
),
|
||||
scanner(
|
||||
options.rightPath,
|
||||
options.inputRoots,
|
||||
options.limits,
|
||||
options.signal,
|
||||
options.integrity,
|
||||
),
|
||||
]);
|
||||
return { left, right };
|
||||
};
|
||||
|
||||
/** Authorize and scan both inputs once under the same bounded traversal policy. */
|
||||
export const scanVersions = async (
|
||||
input: CrossVersionInvestigationInput,
|
||||
|
|
@ -51,21 +88,16 @@ export const scanVersions = async (
|
|||
enabled: integrityContinueEnabled,
|
||||
maxMismatches: input.max_integrity_mismatches,
|
||||
};
|
||||
const left = await scanAuthorizedArtifactInventory(
|
||||
input.left_path,
|
||||
inputRoots,
|
||||
limits,
|
||||
signal,
|
||||
integrity,
|
||||
return ok(
|
||||
await scanVersionInventories({
|
||||
leftPath: input.left_path,
|
||||
rightPath: input.right_path,
|
||||
inputRoots,
|
||||
limits,
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
integrity,
|
||||
}),
|
||||
);
|
||||
const right = await scanAuthorizedArtifactInventory(
|
||||
input.right_path,
|
||||
inputRoots,
|
||||
limits,
|
||||
signal,
|
||||
integrity,
|
||||
);
|
||||
return ok({ left, right });
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof ArtifactReaderFailure)
|
||||
return err(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { scanArtifactInventory } from "../src/application/ArtifactInventory.js";
|
||||
import {
|
||||
scanVersionInventories,
|
||||
type VersionInventoryScanner,
|
||||
} from "../src/application/CrossVersionInventory.js";
|
||||
|
||||
const LIMITS = {
|
||||
maxEntries: 10,
|
||||
maxTotalBytes: 1_024,
|
||||
maxEntryBytes: 512,
|
||||
maxCompressionRatio: 10,
|
||||
maxDepth: 8,
|
||||
maxPathBytes: 256,
|
||||
} as const;
|
||||
|
||||
const INTEGRITY = {
|
||||
mode: "fail",
|
||||
approved: false,
|
||||
enabled: false,
|
||||
maxMismatches: 1,
|
||||
} as const;
|
||||
|
||||
const deferred = <Value>() => {
|
||||
let resolvePromise: (value: Value) => void = () => undefined;
|
||||
let rejectPromise: (reason: unknown) => void = () => undefined;
|
||||
const promise = new Promise<Value>((resolve, reject) => {
|
||||
resolvePromise = resolve;
|
||||
rejectPromise = reject;
|
||||
});
|
||||
return { promise, resolve: resolvePromise, reject: rejectPromise };
|
||||
};
|
||||
|
||||
describe("cross-version inventory", () => {
|
||||
it("starts both scans together and preserves left/right result order", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-version-inventory-"));
|
||||
try {
|
||||
const artifact = join(root, "artifact.js");
|
||||
await writeFile(artifact, "export const value = 1;\n");
|
||||
const snapshot = await scanArtifactInventory(artifact, LIMITS);
|
||||
const left = deferred<typeof snapshot>();
|
||||
const right = deferred<typeof snapshot>();
|
||||
const calls: string[] = [];
|
||||
const controller = new AbortController();
|
||||
const scanner: VersionInventoryScanner = (...arguments_) => {
|
||||
const [path, roots, limits, signal, integrity] = arguments_;
|
||||
calls.push(path);
|
||||
expect(roots).toEqual([root]);
|
||||
expect(limits).toBe(LIMITS);
|
||||
expect(signal).toBe(controller.signal);
|
||||
expect(integrity).toBe(INTEGRITY);
|
||||
return path === "left" ? left.promise : right.promise;
|
||||
};
|
||||
|
||||
const result = scanVersionInventories(
|
||||
{
|
||||
leftPath: "left",
|
||||
rightPath: "right",
|
||||
inputRoots: [root],
|
||||
limits: LIMITS,
|
||||
signal: controller.signal,
|
||||
integrity: INTEGRITY,
|
||||
},
|
||||
scanner,
|
||||
);
|
||||
|
||||
expect(calls).toEqual(["left", "right"]);
|
||||
right.resolve(snapshot);
|
||||
left.resolve(snapshot);
|
||||
await expect(result).resolves.toEqual({
|
||||
left: snapshot,
|
||||
right: snapshot,
|
||||
});
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects promptly when either parallel scan fails", async () => {
|
||||
const left = deferred<never>();
|
||||
const failure = new Error("right scan failed");
|
||||
const scanner: VersionInventoryScanner = (path) =>
|
||||
path === "left" ? left.promise : Promise.reject(failure);
|
||||
|
||||
await expect(
|
||||
scanVersionInventories(
|
||||
{
|
||||
leftPath: "left",
|
||||
rightPath: "right",
|
||||
inputRoots: ["/approved"],
|
||||
limits: LIMITS,
|
||||
integrity: INTEGRITY,
|
||||
},
|
||||
scanner,
|
||||
),
|
||||
).rejects.toBe(failure);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue