Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 74 additions & 7 deletions runs/contextful-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { contextfulGate, mergeStages, parseStages } from "./contextful-gate";
const SHA = "a".repeat(40);
const BASE_SHA = "b".repeat(40);
const DISCOVER = "cargo run --locked -q -p contextful-ci -- stages --parts";
const DISCOVER_HEAD = `${DISCOVER} --base ${BASE_SHA}`;
const input = { repo: "fractalboxdev/contextful", sha: SHA, baseSha: BASE_SHA } as const;

describe("contextful-gate", () => {
Expand All @@ -20,9 +21,12 @@ describe("contextful-gate", () => {
expect(parseStages("pins\npins\n")).toBeUndefined();
expect(parseStages("pins\nunsafe; command\n")).toBeUndefined();
expect(
parseStages(Array.from({ length: 33 }, (_, i) => `part${i}`).join("\n")),
parseStages(Array.from({ length: 65 }, (_, i) => `part${i}`).join("\n")),
).toBeUndefined();
expect(parseStages(" ".repeat(2049))).toBeUndefined();
expect(parseStages(" ".repeat(4097))).toBeUndefined();
expect(parseStages(Array.from({ length: 48 }, (_, i) => `part${i}`).join("\n"))).toHaveLength(
48,
);
});

it("keeps base stages when the head removes them", () => {
Expand Down Expand Up @@ -78,14 +82,20 @@ describe("contextful-gate", () => {

it.effect("spawns at most two stages per wave and reports every stage", () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: { [DISCOVER]: { stdout: "pins\nworkspace.compile\nformal\n", exitCode: 0 } },
sandboxProgram: {
[DISCOVER_HEAD]: {
stdout: "pins\nworkspace.compile\nformal\ntest-first.contextful-core\n",
exitCode: 0,
},
[DISCOVER]: { stdout: "pins\nworkspace.compile\nformal\n", exitCode: 0 },
},
childRuns: {
pollFn: (ids) => ids.map((executionId) => ({ executionId, status: "success" })),
},
});
return Effect.gen(function* () {
const output = yield* contextfulGate.run(input);
expect(output).toEqual({ stages: 3, failed: [] });
expect(output).toEqual({ stages: 4, failed: [] });
expect(handles.childRuns.spawned.map((spawn) => spawn.input)).toEqual([
expect.objectContaining({
checkLabel: "pins",
Expand All @@ -99,6 +109,10 @@ describe("contextful-gate", () => {
checkLabel: "formal",
command: expect.stringContaining(`--stage formal --base ${BASE_SHA}`),
}),
expect.objectContaining({
checkLabel: "test-first.contextful-core",
command: expect.stringContaining(`--stage test-first.contextful-core --base ${BASE_SHA}`),
}),
]);
expect(handles.executions.steps.map((entry) => entry.name)).toEqual([
"checkout",
Expand All @@ -119,7 +133,10 @@ describe("contextful-gate", () => {

it.effect("fails its own check when discovery fails", () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: { [DISCOVER]: { stdout: "", exitCode: 1 } },
sandboxProgram: {
[DISCOVER_HEAD]: { stdout: "", exitCode: 1 },
[DISCOVER]: { stdout: "", exitCode: 1 },
},
});
return Effect.gen(function* () {
const exit = yield* Effect.exit(contextfulGate.run(input));
Expand All @@ -128,9 +145,56 @@ describe("contextful-gate", () => {
}).pipe(Effect.provide(layer));
});

it.effect("discovers older heads only when clap rejects the base argument", () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: {
[DISCOVER_HEAD]: {
stdout: "",
stderr: "error: unexpected argument '--base' found\nUsage: contextful-ci stages --parts",
exitCode: 2,
},
[DISCOVER]: { stdout: "pins\n", exitCode: 0 },
},
childRuns: {
pollFn: (ids) => ids.map((executionId) => ({ executionId, status: "success" })),
},
});
return Effect.gen(function* () {
const output = yield* contextfulGate.run(input);
expect(output).toEqual({ stages: 1, failed: [] });
expect(
handles.sandbox.execs
.map((exec) => exec.command)
.filter((command) => command.startsWith("cargo ")),
).toEqual([DISCOVER_HEAD, DISCOVER, DISCOVER]);
}).pipe(Effect.provide(layer));
});

it.effect("keeps a compilation failure red without compatibility discovery", () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: {
[DISCOVER_HEAD]: { stdout: "", stderr: "compilation failed", exitCode: 101 },
[DISCOVER]: { stdout: "pins\n", exitCode: 0 },
},
});
return Effect.gen(function* () {
const output = yield* Effect.exit(contextfulGate.run(input));
expect(Exit.isFailure(output)).toBe(true);
expect(
handles.sandbox.execs
.map((exec) => exec.command)
.filter((command) => command.startsWith("cargo ")),
).toEqual([DISCOVER_HEAD, DISCOVER]);
expect(handles.childRuns.spawned).toHaveLength(0);
}).pipe(Effect.provide(layer));
});

it.effect("continues after a red child and makes the parent red", () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: { [DISCOVER]: { stdout: "pins\nworkspace.compile\nformal\n", exitCode: 0 } },
sandboxProgram: {
[DISCOVER_HEAD]: { stdout: "pins\nworkspace.compile\nformal\n", exitCode: 0 },
[DISCOVER]: { stdout: "pins\nworkspace.compile\nformal\n", exitCode: 0 },
},
childRuns: {
pollFn: (ids) =>
ids.map((executionId) => ({
Expand All @@ -151,7 +215,10 @@ describe("contextful-gate", () => {

it.effect("makes the parent red when a queued child never settles", () => {
const { layer, handles } = makeCFRuntimeTest({
sandboxProgram: { [DISCOVER]: { stdout: "pins\n", exitCode: 0 } },
sandboxProgram: {
[DISCOVER_HEAD]: { stdout: "pins\n", exitCode: 0 },
[DISCOVER]: { stdout: "pins\n", exitCode: 0 },
},
childRuns: {
pollFn: (ids) => ids.map((executionId) => ({ executionId, status: "missing" })),
},
Expand Down
34 changes: 25 additions & 9 deletions runs/contextful-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
const REPO = "fractalboxdev/contextful";
const DISCOVER = "cargo run --locked -q -p contextful-ci -- stages --parts";
const STAGE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
const MAX_STAGES = 32;
const MAX_STAGE_LIST_BYTES = 2048;
const MAX_STAGES = 64;
const MAX_STAGE_LIST_BYTES = 4096;
const STAGE_CONCURRENCY = 2;

const Input = Schema.Struct({
Expand Down Expand Up @@ -88,12 +88,28 @@ export const contextfulGate = defineRun({
() =>
ensureWorkspace({ current: { container, dir }, repo: input.repo, sha: input.sha }).pipe(
Effect.flatMap((ws) =>
sandbox.exec({
container: ws.container,
cwd: ws.dir,
command: DISCOVER,
timeoutSec: 1800,
}),
sandbox
.exec({
container: ws.container,
cwd: ws.dir,
command: `${DISCOVER} --base ${input.baseSha}`,
timeoutSec: 1800,
})
.pipe(
Effect.flatMap((result) =>
result.exitCode === 2 &&
result.stdout.trim() === "" &&
/unexpected argument '--base' found/.test(result.stderr) &&
/Usage:/.test(result.stderr)
? sandbox.exec({
container: ws.container,
cwd: ws.dir,
command: DISCOVER,
timeoutSec: 1800,
})
: Effect.succeed(result),
),
),
),
),
{ timeoutSec: 1920, retries: 3, retryOn: ["ExecFailed", "StepFailed", "CheckoutFailed"] },
Expand Down Expand Up @@ -138,7 +154,7 @@ export const contextfulGate = defineRun({
return yield* Effect.fail(
new AcceptanceFailed({
exitCode: 1,
summaryMd: "The combined head and base gate stage lists exceed 32 parts.",
summaryMd: "The combined head and base gate stage lists exceed 64 parts.",
}),
);
}
Expand Down
Loading