import { EOL } from "os"; import { Writable } from "stream"; import { createPipelineInternal } from "../pipeline"; import { Task, Globals } from "../publicInterfaces"; describe("task scheduling", () => { const graph = { A: { location: "a", dependencies: ["B"] }, B: { location: "b", dependencies: [] }, }; test("tological steps wait for dependencies to be done", async () => { const tracingContext = makeTestEnvironment(); const task = tracingContext.makeTask(); task.topoDeps = [task.name]; await createPipelineInternal(graph, getGlobals()).addTask(task).go(); const expected = [ task.started("/b"), task.finished("/b"), task.started("/a"), task.finished("/a"), ]; expected.forEach((e, i) => expect(e).toBe(tracingContext.logs[i])); }); test("parallel steps dont wait for dependencies to be done", async () => { const tracingContext = makeTestEnvironment(); const task = tracingContext.makeTask(); await createPipelineInternal(graph, getGlobals()).addTask(task).go(); const expected = [ task.started("/a"), task.started("/b"), task.finished("/a"), task.finished("/b"), ]; expected.forEach((e, i) => expect(e).toBe(tracingContext.logs[i])); }); test("tological steps wait for the previous step", async () => { const tracingContext = makeTestEnvironment(); const task1 = tracingContext.makeTask(); const task2 = tracingContext.makeTask(); task2.deps = [task1.name]; await createPipelineInternal(graph, getGlobals()) .addTask(task1) .addTask(task2) .go(); const expected = [ task1.started("/b"), task1.finished("/b"), task2.started("/b"), task2.finished("/b"), ]; expected.forEach((e, i) => expect(e).toBe( tracingContext.logs.filter((line) => line.includes("/b"))[i] ) ); }); test("parallel steps run in parallel for same package", async () => { const tracingContext = makeTestEnvironment(); const task1 = tracingContext.makeTask(); const task2 = tracingContext.makeTask(); await createPipelineInternal(graph, getGlobals()) .addTask(task1) .addTask(task2) .go(); const expected = [ task1.started("/b"), task2.started("/b"), task1.finished("/b"), task2.finished("/b"), ]; expected.forEach((e, i) => expect(e).toBe( tracingContext.logs.filter((line) => line.includes("/b"))[i] ) ); }); }); describe("failing steps", () => { test("a failing step fails the entire process", async () => { const graph = { A: { location: "a", dependencies: [] }, }; const tracingContext = makeTestEnvironment(); const step = tracingContext.makeTask({ success: false }); const globals = getGlobals(); await createPipelineInternal(graph, globals).addTask(step).go(); expect(globals.exitCode).toBe(1); }); test("the second step is not run if the first one fails", async () => { const graph = { A: { location: "a", dependencies: [] }, }; const tracingContext = makeTestEnvironment(); const task1 = tracingContext.makeTask({ success: false }); const task2 = tracingContext.makeTask(); task2.deps = [task1.name]; await createPipelineInternal(graph, getGlobals()) .addTask(task1) .addTask(task2) .go(); expect( tracingContext.logs.filter((l) => l.includes(task1.started("/a"))).length ).toBe(1); expect( tracingContext.logs.filter((l) => l.includes(task2.started("/a"))).length ).toBe(0); }); }); describe("output", () => { test("validating step output", async () => { const graph = { A: { location: "a", dependencies: [] }, }; const tracingContext = makeTestEnvironment(); const task = tracingContext.makeTask({ stdout: "task stdout", stderr: "task stderr", }); const globals = getGlobals(); await createPipelineInternal(graph, globals).addTask(task).go(); const expectedStdout: string[] = [ ` / Done ${task.name} in A`, ` | STDOUT`, ` | | task stdout`, ` | STDERR`, ` | | task stderr`, ` \\ Done ${task.name} in A`, ``, ]; const expectedStderr: string[] = []; globals.validateOuput(expectedStdout, expectedStderr); }); test("validating step output with nothing written to console", async () => { const graph = { A: { location: "a", dependencies: [] }, }; const tracingContext = makeTestEnvironment(); const task = tracingContext.makeTask(); const globals = getGlobals(); await createPipelineInternal(graph, globals).addTask(task).go(); const expectedStdout: string[] = [`Done ${task.name} in A`, ""]; const expectedStderr: string[] = []; globals.validateOuput(expectedStdout, expectedStderr); }); test("validating failing step output with nothing written to console", async () => { const graph = { A: { location: "a", dependencies: [] }, }; const tracingContext = makeTestEnvironment(); const task = tracingContext.makeTask({ success: false }); const globals = getGlobals(); await createPipelineInternal(graph, globals).addTask(task).go(); const expectedStdout: string[] = []; const expectedStderr: string[] = [`Failed ${task.name} in A`, ``]; globals.validateOuput(expectedStdout, expectedStderr); }); test("validating throwing step output", async () => { const graph = { A: { location: "a", dependencies: [] }, }; const tracingContext = makeTestEnvironment(); const task = tracingContext.makeTask({ success: new Error("failing miserably"), stderr: "task stderr", stdout: "task stdout", }); const globals = getGlobals(); await createPipelineInternal(graph, globals).addTask(task).go(); const expectedStderr: string[] = [ ` / Failed ${task.name} in A`, ` | STDOUT`, ` | | task stdout`, ` | STDERR`, ` | | task stderr`, ` | | stack trace for following error: failing miserably`, ` \\ Failed ${task.name} in A`, ``, ]; const expectedStdout: string[] = []; globals.validateOuput(expectedStdout, expectedStderr); }); test("validate output with two steps", async () => { const graph = { A: { location: "a", dependencies: [] }, }; const tracingContext = makeTestEnvironment(); const task1 = tracingContext.makeTask({ stdout: "task1 stdout", }); const task2 = tracingContext.makeTask({ stdout: "task2 stdout", }); task2.deps = [task1.name]; const globals = getGlobals(); await createPipelineInternal(graph, globals) .addTask(task1) .addTask(task2) .go(); const expectedStdout: string[] = [ ` / Done ${task1.name} in A`, ` | STDOUT`, ` | | task1 stdout`, ` \\ Done ${task1.name} in A`, ``, ` / Done ${task2.name} in A`, ` | STDOUT`, ` | | task2 stdout`, ` \\ Done ${task2.name} in A`, ``, ]; const expectedStderr: string[] = []; globals.validateOuput(expectedStdout, expectedStderr); }); test("the message of the failing step is output at the end", async () => { const graph = { A: { location: "a", dependencies: ["B"] }, B: { location: "b", dependencies: [] }, }; const run = async ( cwd: string, stdout: Writable, stderr: Writable ): Promise<boolean> => { if (cwd.replace(/\\/g, "/") === "/a") { stdout.write(`task1 stdout${EOL}`); return true; } else { stderr.write(`task1 failed${EOL}`); return false; } }; const globals = getGlobals(true); await createPipelineInternal(graph, globals) .addTask({ name: "task1", run }) .go(); const expectedStdout: string[] = [ ` / Done task1 in A`, ` | STDOUT`, ` | | task1 stdout`, ` \\ Done task1 in A`, ``, ` / Failed task1 in B`, ` | STDERR`, ` | | task1 failed`, ` \\ Failed task1 in B`, ``, ]; globals.validateOuput(expectedStdout, expectedStdout); }); }); type TestingGlobals = Globals & { validateOuput(expectedStdout: string[], expectedStderr: string[]): void; stdout: string[]; stderr: string[]; exitCode: number; }; function getGlobals(stdoutAsStderr = false): TestingGlobals { const _stdout: string[] = []; const _stderr: string[] = stdoutAsStderr ? _stdout : []; let _exitCode = 0; return { validateOuput(expectedStdout: string[], expectedStderr: string[]): void { expect(_stderr.length).toBe(expectedStderr.length); expect(_stdout.length).toBe(expectedStdout.length); expectedStdout.forEach((m, i) => expect(_stdout[i]).toBe(m)); expectedStderr.forEach((m, i) => expect(_stderr[i]).toBe(m)); }, logger: { log(message: string): void { message.split(EOL).forEach((m) => _stdout.push(m)); }, error(message: string): void { message.split(EOL).forEach((m) => _stderr.push(m)); }, }, cwd(): string { return "/"; }, exit(int: number): void { _exitCode = int; }, get stdout(): string[] { return _stdout; }, get stderr(): string[] { return _stderr; }, get exitCode(): number { return _exitCode; }, errorFormatter(err: Error): string { return `stack trace for following error: ${err.message}`; }, targetsOnly: false, }; } type TaskResult = { success: true | false | Error; stdout: string; stderr: string; }; type TaskResultOverride = { success?: true | false | Error; stdout?: string; stderr?: string; }; type TaskMock = Task & { started: (cwd: string) => string; finished: (cwd: string) => string; }; async function wait(): Promise<void> { return new Promise<void>((resolve) => setTimeout(resolve, 50)); } function makeTestEnvironment(): { logs: string[]; makeTask: (desiredResult?: TaskResultOverride) => TaskMock; } { const logs: string[] = []; return { logs, makeTask(desiredResult?: TaskResultOverride): TaskMock { const name = Math.random().toString(36); const defaultResult: TaskResult = { success: true, stdout: "", stderr: "", }; const result = desiredResult ? { ...defaultResult, ...desiredResult } : defaultResult; const messages = { started(cwd: string): string { return `called ${name} for ${cwd.replace(/\\/g, "/")}`; }, finished(cwd: string): string { return `finished ${name} for ${cwd.replace(/\\/g, "/")}`; }, }; const run = async ( cwd: string, stdout: Writable, stderr: Writable ): Promise<boolean> => { logs.push(messages.started(cwd)); stdout.write(result.stdout); stderr.write(result.stderr); await wait(); if (typeof result.success === "object") { logs.push(messages.finished(cwd)); throw result.success; } else { logs.push(messages.finished(cwd)); return result.success; } }; return { run, name, ...messages }; }, }; }