Skip to content

Commit

Permalink
Test ctx.abort()
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-rc committed Dec 2, 2023
1 parent 130b7e9 commit 742b6a3
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 12 deletions.
60 changes: 49 additions & 11 deletions spec/commands/root.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import process from "node:process";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { inspect } from "node:util";
import { afterEach, assert, beforeEach, describe, expect, it, vi } from "vitest";
import { spyOnImplementing } from "vitest-mock-process";
import { command } from "../../src/commands/root.js";
import { AvailableCommands, importCommandModule, type CommandModule } from "../../src/services/command/command.js";
import { command as root } from "../../src/commands/root.js";
import * as command from "../../src/services/command/command.js";
import { importCommandModule, type CommandModule } from "../../src/services/command/command.js";
import { config } from "../../src/services/config/config.js";
import { Level } from "../../src/services/output/log/level.js";
import * as update from "../../src/services/output/update.js";
import { noop, noopThis } from "../../src/services/util/function.js";
import { noop, noopThis, type AnyFunction } from "../../src/services/util/function.js";
import { isAbortError } from "../../src/services/util/is.js";
import { PromiseSignal } from "../../src/services/util/promise.js";
import { withEnv } from "../__support__/env.js";
import { expectProcessExit } from "../__support__/process.js";
import { expectStdout } from "../__support__/stdout.js";
Expand All @@ -29,7 +33,7 @@ describe("root", () => {
it("prints root usage when no command is given", async () => {
process.argv = ["node", "ggt"];

await expectProcessExit(command);
await expectProcessExit(root);

expectStdout().toMatchInlineSnapshot(`
"The command-line interface for Gadget
Expand Down Expand Up @@ -58,7 +62,7 @@ describe("root", () => {
it("prints out a helpful message when an unknown command is given", async () => {
process.argv = ["node", "ggt", "foobar"];

await expectProcessExit(command, 1);
await expectProcessExit(root, 1);

expectStdout().toMatchInlineSnapshot(`
"Unknown command foobar
Expand All @@ -75,7 +79,7 @@ describe("root", () => {
expect(config.logFormat).toBe("pretty");

process.argv = ["node", "ggt", "--json"];
await expectProcessExit(command);
await expectProcessExit(root);

expect(process.env["GGT_LOG_FORMAT"]).toBe("json");
expect(config.logFormat).toBe("json");
Expand All @@ -91,14 +95,48 @@ describe("root", () => {
expect(config.logLevel).toBe(Level.PRINT);

process.argv = ["node", "ggt", flag];
await expectProcessExit(command);
await expectProcessExit(root);

expect(process.env["GGT_LOG_LEVEL"]).toBe(String(level));
expect(config.logLevel).toBe(level);
});
});

describe.each(AvailableCommands)("when %s is given", (name) => {
const signals = ["SIGINT", "SIGTERM"] as const;
it.each(signals)("calls ctx.abort() on %s", async (expectedSignal) => {
const aborted = new PromiseSignal();

vi.spyOn(command, "isAvailableCommand").mockReturnValueOnce(true);
vi.spyOn(command, "importCommandModule").mockResolvedValueOnce({
usage: () => "abort test",
command: (ctx) => {
ctx.signal.addEventListener("abort", (reason) => {
assert(isAbortError(reason), `reason isn't an AbortError: ${inspect(reason)}`);
aborted.resolve();
});
},
});

let signalled = false;
let onSignal: AnyFunction;

spyOnImplementing(process, "once", (actualSignal, cb) => {
signalled ||= actualSignal === expectedSignal;
expect(signals).toContain(actualSignal);
onSignal = cb;
return process;
});

process.argv = ["node", "ggt", "test"];
await root();

expect(signalled).toBe(true);
onSignal!();

await aborted;
});

describe.each(command.AvailableCommands)("when %s is given", (name) => {
let mod: CommandModule;

beforeEach(async () => {
Expand All @@ -109,15 +147,15 @@ describe("root", () => {
it.each(["--help", "-h"])("prints the usage when %s is passed", async (flag) => {
process.argv = ["node", "ggt", name, flag];

await expectProcessExit(command);
await expectProcessExit(root);

expectStdout().toEqual(mod.usage() + "\n");
});

it("runs the command", async () => {
process.argv = ["node", "ggt", name];

await command();
await root();

expect(mod.command).toHaveBeenCalled();
});
Expand Down
2 changes: 2 additions & 0 deletions src/commands/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ export const command = async (): Promise<void> => {
await commandModule.command(ctx);

for (const signal of ["SIGINT", "SIGTERM"] as const) {
log.trace("registering signal", { signal });
process.once(signal, () => {
log.trace("received signal", { signal });
log.println` Stopping... {gray Press Ctrl+C again to force}`;
ctx.abort();

Expand Down
2 changes: 1 addition & 1 deletion src/services/util/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ export const isNever = (value: never): never => {
};

export const isAbortError = (error: unknown): error is Error => {
return error instanceof Error && error.name === "AbortError";
return (error instanceof Error && error.name === "AbortError") || (error instanceof Event && error.type === "abort");
};

0 comments on commit 742b6a3

Please sign in to comment.