Files
homarr/scripts/benchmarks/docker-build-lib.spec.ts

225 lines
8.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
applyDockerBuildSyntheticSourceChange,
fingerprintDockerBuildSource,
getBuildCacheContinuityIneligibleReasons,
getBuildCacheStateIneligibleReasons,
getDockerBuildClaimIneligibleReasons,
getMeasuredRevision,
nextBuildCacheTarget,
normalizeDockerArchitecture,
parseDockerBuildCacheMountDescription,
parseDockerBuildOutput,
parseDockerfileCacheMounts,
} from "./docker-build-lib.mts";
const pnpmMount = { logicalId: "pnpm-store", target: "/pnpm/store", mustIncreaseUsage: false };
const nextMount = {
logicalId: "homarr-next-build-linux/arm64",
target: nextBuildCacheTarget,
mustIncreaseUsage: true,
};
const cacheRecord = ({
logicalId,
recordId,
target,
usageCount,
}: {
logicalId: string;
recordId: string;
target: string;
usageCount: number | null;
}) => ({
description: `cached mount ${target} from exec benchmark with id "/${logicalId}"`,
logicalId,
recordId,
size: "1MB",
target,
usageCount,
});
describe("Docker build benchmark", () => {
it("fingerprints the exact imported source mutation used to force incremental compilation", () => {
const entries = [
{
file: "apps/nextjs/src/app/[locale]/layout.tsx",
mode: 0o100644,
content: Buffer.from('openGraph: {\n siteName: "Homarr",\n },\n'),
},
];
const before = fingerprintDockerBuildSource(entries);
const { entries: changedEntries, change } = applyDockerBuildSyntheticSourceChange(entries, "paired-edit-1");
expect(change).toEqual({
file: "apps/nextjs/src/app/[locale]/layout.tsx",
id: "paired-edit-1",
from: 'siteName: "Homarr",',
to: 'siteName: "Homarr build benchmark paired-edit-1",',
});
expect(changedEntries[0]?.content?.toString("utf8")).toContain(change.to);
expect(fingerprintDockerBuildSource(changedEntries)).not.toBe(before);
});
it("fails closed when the real source marker is absent or ambiguous", () => {
expect(() =>
applyDockerBuildSyntheticSourceChange(
[{ file: "apps/nextjs/src/app/[locale]/layout.tsx", mode: 0o100644, content: Buffer.from("no marker") }],
"edit",
),
).toThrow("Expected exactly one build benchmark source marker");
expect(() => applyDockerBuildSyntheticSourceChange([], "edit")).toThrow("source marker file is missing");
});
it("derives the exact context-specific cache mounts for the target platform", () => {
const dockerfile = `RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store echo fetch
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store echo install
RUN --mount=type=cache,id=homarr-next-build-\${TARGETPLATFORM},target=${nextBuildCacheTarget},sharing=locked echo build`;
expect(parseDockerfileCacheMounts(dockerfile, "linux/arm64")).toEqual([nextMount, pnpmMount]);
expect(parseDockerfileCacheMounts(dockerfile.split("\n").slice(0, 2).join("\n"), "linux/arm64")).toEqual([
pnpmMount,
]);
});
it("fails closed for implicit or unresolved Dockerfile cache mount identity", () => {
expect(() => parseDockerfileCacheMounts("RUN --mount=type=cache,target=/cache echo build", "linux/arm64")).toThrow(
"explicit id and target",
);
expect(() =>
parseDockerfileCacheMounts(
"RUN --mount=type=cache,id=homarr-${BUILDPLATFORM},target=/cache echo build",
"linux/arm64",
),
).toThrow("unresolved variable ${BUILDPLATFORM}");
expect(() =>
parseDockerfileCacheMounts(
"RUN --mount=type=cache,id=homarr-${INVALID-NAME},target=/cache echo build",
"linux/arm64",
),
).toThrow("unresolved variable");
});
it("reads exact logical cache identity from real buildx du descriptions", () => {
expect(
parseDockerBuildCacheMountDescription(
'cached mount /app/apps/nextjs/.next/cache from exec /bin/sh -c pnpm build with id "/homarr-next-build-linux/arm64"',
),
).toEqual({ logicalId: "homarr-next-build-linux/arm64", target: nextBuildCacheTarget });
expect(parseDockerBuildCacheMountDescription("unrecognized output")).toBeNull();
});
it("extracts cache and Next compilation measurements from plain BuildKit output", () => {
const output = `#7 CACHED
#8 DONE 0.4s
#9 12.0 ✓ Compiled successfully in 21.7s
#9 DONE 22.3s`;
expect(parseDockerBuildOutput(output)).toEqual({
cachedStepCount: 1,
completedStepCount: 2,
nextCompileMs: 21_700,
});
});
it("only permits claims for a native image labeled with the exact measured source", () => {
expect(
getDockerBuildClaimIneligibleReasons({
expectedRevision: "head+worktree",
expectedSourceFingerprint: "source-hash",
imageArchitecture: "amd64",
imageRevision: "head+worktree",
imageSourceFingerprint: "source-hash",
expectedImageArchitecture: "amd64",
}),
).toEqual([]);
});
it("normalizes Docker server architectures into OCI platform names", () => {
expect(normalizeDockerArchitecture("x86_64")).toBe("amd64");
expect(normalizeDockerArchitecture("aarch64")).toBe("arm64");
expect(normalizeDockerArchitecture("armv7l")).toBe("arm/v7");
});
it("derives revisions from Git state instead of trusting image-label input", () => {
expect(getMeasuredRevision("abc123", false)).toBe("abc123");
expect(getMeasuredRevision("abc123", true)).toBe("abc123+worktree");
});
it("verifies declared cold and warm cache preconditions", () => {
const empty = { cacheMountCount: 0, cacheMounts: [], recordCount: 0 };
const warm = {
cacheMountCount: 2,
cacheMounts: [
cacheRecord({ logicalId: pnpmMount.logicalId, recordId: "pnpm", target: pnpmMount.target, usageCount: 2 }),
cacheRecord({ logicalId: nextMount.logicalId, recordId: "next", target: nextMount.target, usageCount: 4 }),
],
recordCount: 4,
};
expect(getBuildCacheStateIneligibleReasons("cold", empty, [pnpmMount])).toEqual([]);
expect(getBuildCacheStateIneligibleReasons("cold", warm, [pnpmMount])).toEqual([
"cold builder already contains 4 cache records",
]);
expect(getBuildCacheStateIneligibleReasons("warm", warm, [pnpmMount, nextMount])).toEqual([]);
const staleNext = {
...warm,
cacheMounts: [
warm.cacheMounts[0]!,
cacheRecord({
logicalId: "homarr-next-build",
recordId: "old-next",
target: nextBuildCacheTarget,
usageCount: 9,
}),
],
};
expect(getBuildCacheStateIneligibleReasons("warm", staleNext, [pnpmMount, nextMount])).toEqual([
`warm builder is missing cache mount ${nextMount.logicalId} at ${nextBuildCacheTarget}`,
]);
});
it("requires every warm cache record to survive and the Next cache to be used", () => {
const before = {
cacheMountCount: 2,
cacheMounts: [
cacheRecord({ logicalId: pnpmMount.logicalId, recordId: "pnpm", target: pnpmMount.target, usageCount: 2 }),
cacheRecord({ logicalId: nextMount.logicalId, recordId: "next", target: nextMount.target, usageCount: 4 }),
],
recordCount: 4,
};
const after = {
cacheMountCount: 2,
cacheMounts: [
cacheRecord({ logicalId: pnpmMount.logicalId, recordId: "pnpm", target: pnpmMount.target, usageCount: 2 }),
cacheRecord({ logicalId: nextMount.logicalId, recordId: "next", target: nextMount.target, usageCount: 6 }),
],
recordCount: 8,
};
expect(getBuildCacheContinuityIneligibleReasons("warm", before, after, [pnpmMount, nextMount])).toEqual([]);
expect(
getBuildCacheContinuityIneligibleReasons(
"warm",
before,
{
...after,
cacheMounts: [after.cacheMounts[0]!, { ...after.cacheMounts[1]!, recordId: "replacement" }],
},
[pnpmMount, nextMount],
),
).toEqual([`warm cache mount ${nextMount.logicalId} at ${nextBuildCacheTarget} was replaced during the build`]);
expect(
getBuildCacheContinuityIneligibleReasons(
"warm",
before,
{
...after,
cacheMounts: [after.cacheMounts[0]!, { ...after.cacheMounts[1]!, usageCount: 4 }],
},
[pnpmMount, nextMount],
),
).toEqual([`warm cache mount ${nextMount.logicalId} was not used by the application build`]);
});
});