diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /packages/turbo-ignore/__tests__ | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'packages/turbo-ignore/__tests__')
| -rw-r--r-- | packages/turbo-ignore/__tests__/args.test.ts | 109 | ||||
| -rw-r--r-- | packages/turbo-ignore/__tests__/checkCommit.test.ts | 229 | ||||
| -rw-r--r-- | packages/turbo-ignore/__tests__/errors.test.ts | 46 | ||||
| -rw-r--r-- | packages/turbo-ignore/__tests__/getComparison.test.ts | 61 | ||||
| -rw-r--r-- | packages/turbo-ignore/__tests__/getTask.test.ts | 27 | ||||
| -rw-r--r-- | packages/turbo-ignore/__tests__/getWorkspace.test.ts | 62 | ||||
| -rw-r--r-- | packages/turbo-ignore/__tests__/ignore.test.ts | 578 |
7 files changed, 1112 insertions, 0 deletions
diff --git a/packages/turbo-ignore/__tests__/args.test.ts b/packages/turbo-ignore/__tests__/args.test.ts new file mode 100644 index 0000000..f546247 --- /dev/null +++ b/packages/turbo-ignore/__tests__/args.test.ts @@ -0,0 +1,109 @@ +import parseArgs, { help } from "../src/args"; +import pkg from "../package.json"; +import { spyConsole, spyExit } from "@turbo/test-utils"; + +describe("parseArgs()", () => { + const mockConsole = spyConsole(); + const mockExit = spyExit(); + + it("does not throw with no args", async () => { + const result = parseArgs({ argv: [] }); + expect(result.workspace).toBe(undefined); + expect(result.fallback).toBe(undefined); + expect(result.task).toBe(undefined); + }); + + it("outputs help text (--help)", async () => { + parseArgs({ argv: ["--help"] }); + expect(mockExit.exit).toHaveBeenCalledWith(0); + expect(mockConsole.log).toHaveBeenCalledWith(help); + }); + + it("outputs help text (-h)", async () => { + parseArgs({ argv: ["-h"] }); + expect(mockExit.exit).toHaveBeenCalledWith(0); + expect(mockConsole.log).toHaveBeenCalledWith(help); + }); + + it("outputs version text (--version)", async () => { + parseArgs({ argv: ["--version"] }); + expect(mockExit.exit).toHaveBeenCalledWith(0); + expect(mockConsole.log).toHaveBeenCalledWith(pkg.version); + }); + + it("outputs version text (-v)", async () => { + parseArgs({ argv: ["-v"] }); + expect(mockExit.exit).toHaveBeenCalledWith(0); + expect(mockConsole.log).toHaveBeenCalledWith(pkg.version); + }); + + it("correctly finds workspace", async () => { + const result = parseArgs({ argv: ["this-workspace"] }); + expect(result.workspace).toBe("this-workspace"); + expect(result.fallback).toBe(undefined); + expect(result.task).toBe(undefined); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); + + it("correctly finds fallback", async () => { + const result = parseArgs({ argv: ["--fallback=HEAD^"] }); + expect(result.workspace).toBe(undefined); + expect(result.fallback).toBe("HEAD^"); + expect(result.task).toBe(undefined); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); + + it("correctly finds task", async () => { + const result = parseArgs({ argv: ["--task=some-workspace#build"] }); + expect(result.workspace).toBe(undefined); + expect(result.fallback).toBe(undefined); + expect(result.task).toBe("some-workspace#build"); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); + + it("uses default fallback if incorrectly specified", async () => { + const result = parseArgs({ argv: ["--fallback"] }); + expect(result.workspace).toBe(undefined); + expect(result.fallback).toBe(undefined); + expect(result.task).toBe(undefined); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); + + it("uses default fallback if empty string", async () => { + const result = parseArgs({ argv: ["--fallback="] }); + expect(result.workspace).toBe(undefined); + expect(result.fallback).toBe(undefined); + expect(result.task).toBe(undefined); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); + + it("uses default task if incorrectly specified", async () => { + const result = parseArgs({ argv: ["--task"] }); + expect(result.workspace).toBe(undefined); + expect(result.fallback).toBe(undefined); + expect(result.task).toBe(undefined); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); + + it("uses default task if empty string", async () => { + const result = parseArgs({ argv: ["--task="] }); + expect(result.workspace).toBe(undefined); + expect(result.fallback).toBe(undefined); + expect(result.task).toBe(undefined); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); + + it("correctly finds fallback and workspace", async () => { + const result = parseArgs({ + argv: [ + "this-workspace", + "--fallback=HEAD~10", + "--task=some-workspace#build", + ], + }); + expect(result.workspace).toBe("this-workspace"); + expect(result.fallback).toBe("HEAD~10"); + expect(result.task).toBe("some-workspace#build"); + expect(mockExit.exit).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/turbo-ignore/__tests__/checkCommit.test.ts b/packages/turbo-ignore/__tests__/checkCommit.test.ts new file mode 100644 index 0000000..e7e4a5f --- /dev/null +++ b/packages/turbo-ignore/__tests__/checkCommit.test.ts @@ -0,0 +1,229 @@ +import child_process from "child_process"; +import { checkCommit } from "../src/checkCommit"; +import { mockEnv } from "@turbo/test-utils"; + +describe("checkCommit()", () => { + describe("on Vercel", () => { + mockEnv(); + + describe("for all workspaces", () => { + it("results in continue when no special commit messages are found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = "fixing a test"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "continue", + scope: "global", + reason: "No deploy or skip string found in commit message.", + }); + }); + + it("results in conflict when deploy and skip commit messages are found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = + "deploying [vercel deploy] and skipping [vercel skip]"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "conflict", + scope: "global", + reason: + "Conflicting commit messages found: [vercel deploy] and [vercel skip]", + }); + }); + + it("results in deploy when deploy commit message is found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = "deploying [vercel deploy]"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "deploy", + scope: "global", + reason: "Found commit message: [vercel deploy]", + }); + }); + + it("results in skip when skip commit message is found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = "skip deployment [vercel skip]"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "skip", + scope: "global", + reason: "Found commit message: [vercel skip]", + }); + }); + }); + + describe("for specific workspaces", () => { + it("results in continue when no special commit messages are found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = + "fixing a test in test-workspace"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "continue", + scope: "global", + reason: "No deploy or skip string found in commit message.", + }); + }); + + it("results in conflict when deploy and skip commit messages are found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = + "deploying [vercel deploy test-workspace] and skipping [vercel skip test-workspace]"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "conflict", + scope: "workspace", + reason: + "Conflicting commit messages found: [vercel deploy test-workspace] and [vercel skip test-workspace]", + }); + }); + + it("results in deploy when deploy commit message is found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = + "deploying [vercel deploy test-workspace]"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "deploy", + scope: "workspace", + reason: "Found commit message: [vercel deploy test-workspace]", + }); + }); + + it("results in skip when skip commit message is found", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = + "skip deployment [vercel skip test-workspace]"; + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "skip", + scope: "workspace", + reason: "Found commit message: [vercel skip test-workspace]", + }); + }); + }); + }); + describe("Not on Vercel", () => { + describe("for all workspaces", () => { + it("results in continue when no special commit messages are found", async () => { + const commitBody = "fixing a test"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "continue", + scope: "global", + reason: "No deploy or skip string found in commit message.", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + + it("results in conflict when deploy and skip commit messages are found", async () => { + const commitBody = + "deploying [vercel deploy] and skipping [vercel skip]"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "conflict", + scope: "global", + reason: + "Conflicting commit messages found: [vercel deploy] and [vercel skip]", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + + it("results in deploy when deploy commit message is found", async () => { + const commitBody = "deploying [vercel deploy]"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "deploy", + scope: "global", + reason: "Found commit message: [vercel deploy]", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + + it("results in skip when skip commit message is found", async () => { + const commitBody = "skip deployment [vercel skip]"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "skip", + scope: "global", + reason: "Found commit message: [vercel skip]", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + }); + + describe("for specific workspaces", () => { + it("results in continue when no special commit messages are found", async () => { + const commitBody = "fixing a test in test-workspace"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "continue", + scope: "global", + reason: "No deploy or skip string found in commit message.", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + + it("results in conflict when deploy and skip commit messages are found", async () => { + const commitBody = + "deploying [vercel deploy test-workspace] and skipping [vercel skip test-workspace]"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "conflict", + scope: "workspace", + reason: + "Conflicting commit messages found: [vercel deploy test-workspace] and [vercel skip test-workspace]", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + + it("results in deploy when deploy commit message is found", async () => { + const commitBody = "deploying [vercel deploy test-workspace]"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "deploy", + scope: "workspace", + reason: "Found commit message: [vercel deploy test-workspace]", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + + it("results in skip when skip commit message is found", async () => { + const commitBody = "skip deployment [vercel skip test-workspace]"; + const mockExecSync = jest + .spyOn(child_process, "execSync") + .mockImplementation((_) => commitBody); + + expect(checkCommit({ workspace: "test-workspace" })).toEqual({ + result: "skip", + scope: "workspace", + reason: "Found commit message: [vercel skip test-workspace]", + }); + expect(mockExecSync).toHaveBeenCalledWith("git show -s --format=%B"); + mockExecSync.mockRestore(); + }); + }); + }); +}); diff --git a/packages/turbo-ignore/__tests__/errors.test.ts b/packages/turbo-ignore/__tests__/errors.test.ts new file mode 100644 index 0000000..18f26bd --- /dev/null +++ b/packages/turbo-ignore/__tests__/errors.test.ts @@ -0,0 +1,46 @@ +import { shouldWarn, NON_FATAL_ERRORS } from "../src/errors"; + +describe("shouldWarn()", () => { + it("it detects errors when packageManager is missing", async () => { + const result = shouldWarn({ + err: `run failed: We did not detect an in-use package manager for your project. Please set the "packageManager" property in your root package.json (https://nodejs.org/api/packages.html#packagemanager) or run \`npx @turbo/codemod add-package-manager\` in the root of your monorepo.`, + }); + expect(result.code).toBe("NO_PACKAGE_MANAGER"); + expect(result.level).toBe("warn"); + expect(result.message).toBe(NON_FATAL_ERRORS.NO_PACKAGE_MANAGER.message); + }); + + it("it detects errors when yarn lockfile is missing", async () => { + const result = shouldWarn({ + err: `* reading yarn.lock: open /test/../yarn.lock: no such file or directory`, + }); + expect(result.code).toBe("MISSING_LOCKFILE"); + expect(result.level).toBe("warn"); + expect(result.message).toBe(NON_FATAL_ERRORS.MISSING_LOCKFILE.message); + }); + + it("it detects errors when pnpm lockfile is missing", async () => { + const result = shouldWarn({ + err: `* reading pnpm-lock.yaml: open /test/../pnpm-lock.yaml: no such file or directory`, + }); + expect(result.code).toBe("MISSING_LOCKFILE"); + expect(result.level).toBe("warn"); + expect(result.message).toBe(NON_FATAL_ERRORS.MISSING_LOCKFILE.message); + }); + + it("it detects errors when npm lockfile is missing", async () => { + const result = shouldWarn({ + err: `* reading package-lock.json: open /test/../package-lock.json: no such file or directory`, + }); + expect(result.code).toBe("MISSING_LOCKFILE"); + expect(result.level).toBe("warn"); + expect(result.message).toBe(NON_FATAL_ERRORS.MISSING_LOCKFILE.message); + }); + + it("it returns unknown errors", async () => { + const result = shouldWarn({ err: `something bad happened` }); + expect(result.code).toBe("UNKNOWN_ERROR"); + expect(result.level).toBe("error"); + expect(result.message).toBe(`something bad happened`); + }); +}); diff --git a/packages/turbo-ignore/__tests__/getComparison.test.ts b/packages/turbo-ignore/__tests__/getComparison.test.ts new file mode 100644 index 0000000..b5c74c7 --- /dev/null +++ b/packages/turbo-ignore/__tests__/getComparison.test.ts @@ -0,0 +1,61 @@ +import { getComparison } from "../src/getComparison"; +import { spyConsole, mockEnv } from "@turbo/test-utils"; + +describe("getComparison()", () => { + mockEnv(); + const mockConsole = spyConsole(); + it("uses headRelative comparison when not running Vercel CI", async () => { + expect(getComparison({ workspace: "test-workspace" })) + .toMatchInlineSnapshot(` + Object { + "ref": "HEAD^", + "type": "headRelative", + } + `); + }); + + it("returns null when running in Vercel CI with no VERCEL_GIT_PREVIOUS_SHA", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + expect(getComparison({ workspace: "test-workspace" })).toBeNull(); + expect(mockConsole.log).toHaveBeenCalledWith( + "≫ ", + 'No previous deployments found for "test-workspace" on branch "my-branch".' + ); + }); + + it("uses custom fallback when running in Vercel CI with no VERCEL_GIT_PREVIOUS_SHA", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + expect(getComparison({ workspace: "test-workspace", fallback: "HEAD^2" })) + .toMatchInlineSnapshot(` + Object { + "ref": "HEAD^2", + "type": "customFallback", + } + `); + expect(mockConsole.log).toHaveBeenNthCalledWith( + 1, + "≫ ", + 'No previous deployments found for "test-workspace" on branch "my-branch".' + ); + expect(mockConsole.log).toHaveBeenNthCalledWith( + 2, + "≫ ", + "Falling back to ref HEAD^2" + ); + }); + + it("uses previousDeploy when running in Vercel CI with VERCEL_GIT_PREVIOUS_SHA", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_PREVIOUS_SHA = "mygitsha"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + expect(getComparison({ workspace: "test-workspace" })) + .toMatchInlineSnapshot(` + Object { + "ref": "mygitsha", + "type": "previousDeploy", + } + `); + }); +}); diff --git a/packages/turbo-ignore/__tests__/getTask.test.ts b/packages/turbo-ignore/__tests__/getTask.test.ts new file mode 100644 index 0000000..a184893 --- /dev/null +++ b/packages/turbo-ignore/__tests__/getTask.test.ts @@ -0,0 +1,27 @@ +import { getTask } from "../src/getTask"; +import { spyConsole, validateLogs } from "@turbo/test-utils"; + +describe("getWorkspace()", () => { + const mockConsole = spyConsole(); + it("getTask defaults to build", async () => { + expect(getTask({})).toEqual("build"); + validateLogs( + ['Using "build" as the task as it was unspecified'], + mockConsole.log, + { prefix: "≫ " } + ); + }); + + it("getTask returns a quoted task if user-supplied", async () => { + expect( + getTask({ + task: "workspace#task", + }) + ).toEqual(`"workspace#task"`); + validateLogs( + ['Using "workspace#task" as the task from the arguments'], + mockConsole.log, + { prefix: "≫ " } + ); + }); +}); diff --git a/packages/turbo-ignore/__tests__/getWorkspace.test.ts b/packages/turbo-ignore/__tests__/getWorkspace.test.ts new file mode 100644 index 0000000..6d97fe2 --- /dev/null +++ b/packages/turbo-ignore/__tests__/getWorkspace.test.ts @@ -0,0 +1,62 @@ +import { getWorkspace } from "../src/getWorkspace"; +import { spyConsole, validateLogs } from "@turbo/test-utils"; + +describe("getWorkspace()", () => { + const mockConsole = spyConsole(); + it("getWorkspace returns workspace from arg", async () => { + expect( + getWorkspace({ + workspace: "test-workspace", + }) + ).toEqual("test-workspace"); + validateLogs( + ['Using "test-workspace" as workspace from arguments'], + mockConsole.log, + { prefix: "≫ " } + ); + }); + + it("getWorkspace returns workspace from package.json", async () => { + expect( + getWorkspace({ + directory: "./__fixtures__/app", + }) + ).toEqual("test-app"); + expect(mockConsole.log).toHaveBeenCalledWith( + "≫ ", + 'Inferred "test-app" as workspace from "package.json"' + ); + }); + + it("getWorkspace used current directory if not specified", async () => { + expect(getWorkspace({})).toEqual("turbo-ignore"); + expect(mockConsole.log).toHaveBeenCalledWith( + "≫ ", + 'Inferred "turbo-ignore" as workspace from "package.json"' + ); + }); + + it("getWorkspace returns null when no arg is provided and package.json is missing name field", async () => { + expect( + getWorkspace({ + directory: "./__fixtures__/invalid-app", + }) + ).toEqual(null); + expect(mockConsole.error).toHaveBeenCalledWith( + "≫ ", + '"__fixtures__/invalid-app/package.json" is missing the "name" field (required).' + ); + }); + + it("getWorkspace returns null when no arg is provided and package.json can be found", async () => { + expect( + getWorkspace({ + directory: "./__fixtures__/no-app", + }) + ).toEqual(null); + expect(mockConsole.error).toHaveBeenCalledWith( + "≫ ", + '"__fixtures__/no-app/package.json" could not be found. turbo-ignore inferencing failed' + ); + }); +}); diff --git a/packages/turbo-ignore/__tests__/ignore.test.ts b/packages/turbo-ignore/__tests__/ignore.test.ts new file mode 100644 index 0000000..37908c5 --- /dev/null +++ b/packages/turbo-ignore/__tests__/ignore.test.ts @@ -0,0 +1,578 @@ +import child_process, { ChildProcess, ExecException } from "child_process"; +import turboIgnore from "../src/ignore"; +import { + spyConsole, + spyExit, + SpyExit, + mockEnv, + validateLogs, +} from "@turbo/test-utils"; + +function expectBuild(mockExit: SpyExit) { + expect(mockExit.exit).toHaveBeenCalledWith(1); +} + +function expectIgnore(mockExit: SpyExit) { + expect(mockExit.exit).toHaveBeenCalledWith(0); +} + +describe("turboIgnore()", () => { + mockEnv(); + const mockExit = spyExit(); + const mockConsole = spyConsole(); + + it("throws error and allows build when exec fails", async () => { + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + "error" as unknown as ExecException, + "stdout", + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore({ + args: { workspace: "test-workspace" }, + }); + + expect(mockExec).toHaveBeenCalledWith( + "npx turbo run build --filter=test-workspace...[HEAD^] --dry=json", + expect.anything(), + expect.anything() + ); + + validateLogs(["UNKNOWN_ERROR: error"], mockConsole.error, { + prefix: "≫ ", + }); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("throws pretty error and allows build when exec fails", async () => { + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + { + message: + "run failed: We did not detect an in-use package manager for your project", + } as unknown as ExecException, + "stdout", + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore({ + args: { workspace: "test-workspace" }, + }); + + expect(mockExec).toHaveBeenCalledWith( + "npx turbo run build --filter=test-workspace...[HEAD^] --dry=json", + expect.anything(), + expect.anything() + ); + + validateLogs( + [ + `turbo-ignore could not complete - no package manager detected, please commit a lockfile, or set "packageManager" in your root "package.json"`, + ], + mockConsole.warn, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("throws pretty error and allows build when can't find previous sha", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_PREVIOUS_SHA = "too-far-back"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + { + message: + " ERROR run failed: failed to resolve packages to run: commit too-far-back does not exist", + } as unknown as ExecException, + "stdout", + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore({ + args: { workspace: "test-workspace" }, + }); + + expect(mockExec).toHaveBeenCalledWith( + "npx turbo run build --filter=test-workspace...[too-far-back] --dry=json", + expect.anything(), + expect.anything() + ); + + validateLogs( + [ + `turbo-ignore could not complete - commit does not exist or is unreachable`, + ], + mockConsole.warn, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("throws pretty error and allows build when fallback fails", async () => { + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + { + message: + "ERROR run failed: failed to resolve packages to run: commit HEAD^ does not exist", + } as unknown as ExecException, + "stdout", + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore({ + args: { workspace: "test-workspace", fallback: "HEAD^" }, + }); + + expect(mockExec).toHaveBeenCalledWith( + "npx turbo run build --filter=test-workspace...[HEAD^] --dry=json", + expect.anything(), + expect.anything() + ); + + validateLogs( + [ + `turbo-ignore could not complete - parent commit does not exist or is unreachable`, + ], + mockConsole.warn, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("skips checks and allows build when no workspace can be found", async () => { + turboIgnore({ + args: { + directory: "__fixtures__/no-app", + }, + }); + validateLogs( + [ + () => [ + "≫ ", + expect.stringContaining( + " could not be found. turbo-ignore inferencing failed" + ), + ], + ], + mockConsole.error, + { prefix: "≫ " } + ); + expectBuild(mockExit); + }); + + it("skips checks and allows build when a workspace with no name is found", async () => { + turboIgnore({ + args: { + directory: "__fixtures__/invalid-app", + }, + }); + validateLogs( + [ + () => [ + "≫ ", + expect.stringContaining(' is missing the "name" field (required).'), + ], + ], + mockConsole.error, + { prefix: "≫ " } + ); + expectBuild(mockExit); + }); + + it("skips checks and allows build when no monorepo root can be found", async () => { + turboIgnore({ + args: { directory: "/" }, + }); + expectBuild(mockExit); + expect(mockConsole.error).toHaveBeenLastCalledWith( + "≫ ", + "Monorepo root not found. turbo-ignore inferencing failed" + ); + }); + + it("skips checks and allows build when TURBO_FORCE is set", async () => { + process.env.TURBO_FORCE = "true"; + turboIgnore({ + args: { workspace: "test-workspace" }, + }); + expect(mockConsole.log).toHaveBeenNthCalledWith( + 2, + "≫ ", + "`TURBO_FORCE` detected" + ); + expectBuild(mockExit); + }); + + it("allows build when no comparison is returned", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_PREVIOUS_SHA = ""; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + turboIgnore({ + args: { + workspace: "test-app", + directory: "__fixtures__/app", + }, + }); + expect(mockConsole.log).toHaveBeenNthCalledWith( + 4, + "≫ ", + 'No previous deployments found for "test-app" on branch "my-branch".' + ); + expectBuild(mockExit); + }); + + it("skips build for `previousDeploy` comparison with no changes", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + null, + '{"packages":[],"tasks":[]}', + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + validateLogs( + [ + "Using Turborepo to determine if this project is affected by the commit...\n", + 'Inferred "test-app" as workspace from "package.json"', + 'Using "build" as the task as it was unspecified', + `Found previous deployment ("last-deployed-sha") for \"test-app\" on branch \"my-branch\"`, + "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`", + "This project and its dependencies are not affected", + () => expect.stringContaining("⏭ Ignoring the change"), + ], + mockConsole.log, + { prefix: "≫ " } + ); + + expectIgnore(mockExit); + mockExec.mockRestore(); + }); + + it("allows build for `previousDeploy` comparison with changes", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + null, + '{"packages":["test-app"],"tasks":[]}', + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + turboIgnore({ + args: { + task: "workspace#build", + directory: "__fixtures__/app", + }, + }); + validateLogs( + [ + "Using Turborepo to determine if this project is affected by the commit...\n", + 'Inferred "test-app" as workspace from "package.json"', + 'Using "workspace#build" as the task from the arguments', + 'Found previous deployment ("last-deployed-sha") for "test-app" on branch "my-branch"', + 'Analyzing results of `npx turbo run "workspace#build" --filter=test-app...[last-deployed-sha] --dry=json`', + 'This commit affects "test-app"', + () => expect.stringContaining("✓ Proceeding with deployment"), + ], + mockConsole.log, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("allows build for `previousDeploy` comparison with single dependency change", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + null, + '{"packages":["test-app", "ui"],"tasks":[]}', + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + validateLogs( + [ + "Using Turborepo to determine if this project is affected by the commit...\n", + 'Inferred "test-app" as workspace from "package.json"', + 'Using "build" as the task as it was unspecified', + 'Found previous deployment ("last-deployed-sha") for "test-app" on branch "my-branch"', + "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`", + 'This commit affects "test-app" and 1 dependency (ui)', + () => expect.stringContaining("✓ Proceeding with deployment"), + ], + mockConsole.log, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("allows build for `previousDeploy` comparison with multiple dependency changes", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + null, + '{"packages":["test-app", "ui", "tsconfig"],"tasks":[]}', + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + validateLogs( + [ + "Using Turborepo to determine if this project is affected by the commit...\n", + 'Inferred "test-app" as workspace from "package.json"', + 'Using "build" as the task as it was unspecified', + 'Found previous deployment ("last-deployed-sha") for "test-app" on branch "my-branch"', + "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`", + 'This commit affects "test-app" and 2 dependencies (ui, tsconfig)', + () => expect.stringContaining("✓ Proceeding with deployment"), + ], + mockConsole.log, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("throws error and allows build when json cannot be parsed", async () => { + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback(null, "stdout", "stderr") as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + + expect(mockExec).toHaveBeenCalledWith( + "npx turbo run build --filter=test-app...[HEAD^] --dry=json", + expect.anything(), + expect.anything() + ); + validateLogs( + [ + "Failed to parse JSON output from `npx turbo run build --filter=test-app...[HEAD^] --dry=json`.", + ], + mockConsole.error, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("throws error and allows build when stdout is null", async () => { + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + null, + null as unknown as string, + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + + expect(mockExec).toHaveBeenCalledWith( + "npx turbo run build --filter=test-app...[HEAD^] --dry=json", + expect.anything(), + expect.anything() + ); + validateLogs( + [ + "Failed to parse JSON output from `npx turbo run build --filter=test-app...[HEAD^] --dry=json`.", + ], + mockConsole.error, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + mockExec.mockRestore(); + }); + + it("skips when commit message contains a skip string", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = "[vercel skip]"; + + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + + validateLogs( + [ + "Using Turborepo to determine if this project is affected by the commit...\n", + 'Inferred "test-app" as workspace from "package.json"', + 'Using "build" as the task as it was unspecified', + "Found commit message: [vercel skip]", + () => expect.stringContaining("⏭ Ignoring the change"), + ], + mockConsole.log, + { prefix: "≫ " } + ); + + expectIgnore(mockExit); + }); + + it("deploys when commit message contains a deploy string", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = "[vercel deploy]"; + + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + + validateLogs( + [ + "Using Turborepo to determine if this project is affected by the commit...\n", + 'Inferred "test-app" as workspace from "package.json"', + 'Using "build" as the task as it was unspecified', + "Found commit message: [vercel deploy]", + () => expect.stringContaining("✓ Proceeding with deployment"), + ], + mockConsole.log, + { prefix: "≫ " } + ); + + expectBuild(mockExit); + }); + + it("runs full turbo-ignore check when commit message contains a conflicting string", async () => { + process.env.VERCEL = "1"; + process.env.VERCEL_GIT_COMMIT_MESSAGE = "[vercel deploy] [vercel skip]"; + process.env.VERCEL_GIT_PREVIOUS_SHA = "last-deployed-sha"; + process.env.VERCEL_GIT_COMMIT_REF = "my-branch"; + + const mockExec = jest + .spyOn(child_process, "exec") + .mockImplementation((command, options, callback) => { + if (callback) { + return callback( + null, + '{"packages":[],"tasks":[]}', + "stderr" + ) as unknown as ChildProcess; + } + return {} as unknown as ChildProcess; + }); + + turboIgnore({ + args: { + directory: "__fixtures__/app", + }, + }); + + validateLogs( + [ + "Using Turborepo to determine if this project is affected by the commit...\n", + 'Inferred "test-app" as workspace from "package.json"', + 'Using "build" as the task as it was unspecified', + "Conflicting commit messages found: [vercel deploy] and [vercel skip]", + `Found previous deployment ("last-deployed-sha") for \"test-app\" on branch \"my-branch\"`, + "Analyzing results of `npx turbo run build --filter=test-app...[last-deployed-sha] --dry=json`", + "This project and its dependencies are not affected", + () => expect.stringContaining("⏭ Ignoring the change"), + ], + mockConsole.log, + { prefix: "≫ " } + ); + + expectIgnore(mockExit); + mockExec.mockRestore(); + }); +}); |
