Thanks to visit codestin.com
Credit goes to github.com

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
133 changes: 88 additions & 45 deletions __tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,106 @@
const mockCreateCommand = jest.fn();
const mockShowHelp = jest.fn();
const mockExecute = jest.fn();
jest.mock("../../package.json", () => ({ version: "0.1.0-test" }), { virtual: true });

jest.mock("../script/command-parser", () => ({
createCommand: mockCreateCommand,
showHelp: mockShowHelp,
jest.mock("parse-duration", () => ({
default: (input: string): number => {
const units: Record<string, number> = {
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000,
y: 365 * 24 * 60 * 60 * 1000,
};
const match = String(input).match(/^(\d+)\s*([smhdy])$/);
if (!match) return NaN;
return parseInt(match[1], 10) * units[match[2]];
},
}));

jest.mock("../script/command-executor", () => ({
execute: mockExecute,
}));
function runCli(args: string[]): { exitCode: number | null; helpShown: boolean; executeCalled: boolean } {
let exitCode: number | null = null;
let helpShown = false;
let executeCalled = false;

jest.isolateModules(() => {
const originalArgv = process.argv;
const originalExit = process.exit;

process.argv = ["node", "aether", ...args];
process.exit = ((code?: number) => {
exitCode = code ?? 0;
throw new Error(`__EXIT_${code ?? 0}__`);
}) as any;

jest.doMock("../script/command-executor", () => ({
execute: jest.fn(() => {
executeCalled = true;
return Promise.resolve();
}),
}));

try {
const parser = require("../script/command-parser");
const originalShowHelp = parser.showHelp;
parser.showHelp = (...rest: any[]) => {
helpShown = true;
return originalShowHelp.apply(parser, rest);
};

try {
require("../script/cli");
} catch (e: any) {
if (!String(e.message).startsWith("__EXIT_")) {
throw e;
}
}
} finally {
process.argv = originalArgv;
process.exit = originalExit;
}
});

describe("cli", () => {
let consoleErrorSpy: jest.SpyInstance;
let processExitSpy: jest.SpyInstance;
return { exitCode, helpShown, executeCalled };
}

describe("cli entry point", () => {
beforeEach(() => {
mockCreateCommand.mockReset();
mockShowHelp.mockReset();
mockExecute.mockReset();
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
processExitSpy = jest.spyOn(process, "exit").mockImplementation(((_code?: number) => undefined) as never);
jest.spyOn(console, "log").mockImplementation(() => undefined);
jest.spyOn(console, "error").mockImplementation(() => undefined);
});

afterEach(() => {
jest.restoreAllMocks();
});

function runCli(): void {
jest.isolateModules(() => {
require("../script/cli");
describe("exit codes", () => {
it("exits 1 when an unknown command category is passed", () => {
const { exitCode, executeCalled } = runCli(["nonsense-command"]);
expect(exitCode).toBe(1);
expect(executeCalled).toBe(false);
});
}

it("calls showHelp without root description when createCommand returns undefined", () => {
mockCreateCommand.mockReturnValue(undefined);
runCli();
expect(mockShowHelp).toHaveBeenCalledWith(false);
expect(mockExecute).not.toHaveBeenCalled();
});
it("exits 1 when a known command is missing required args", () => {
const { exitCode, executeCalled } = runCli(["app", "rm"]);
expect(exitCode).toBe(1);
expect(executeCalled).toBe(false);
});

it("dispatches to execute when createCommand returns a valid command", () => {
const cmd = { type: 0 };
mockCreateCommand.mockReturnValue(cmd);
mockExecute.mockResolvedValue(undefined);
runCli();
expect(mockExecute).toHaveBeenCalledWith(cmd);
expect(mockShowHelp).not.toHaveBeenCalled();
expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
it("exits 1 when an unknown subcommand is passed under a known category", () => {
const { exitCode, executeCalled } = runCli(["app", "bogus-subcommand"]);
expect(exitCode).toBe(1);
expect(executeCalled).toBe(false);
});

it("on execute rejection logs the red [Error] message and exits with code 1", async () => {
const cmd = { type: 0 };
mockCreateCommand.mockReturnValue(cmd);
mockExecute.mockReturnValue(Promise.reject(new Error("boom")));
runCli();
await new Promise((r) => setImmediate(r));
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringMatching(/\[Error\]\s+boom/));
expect(processExitSpy).toHaveBeenCalledWith(1);
it("exits 0 (no exit call) when invoked with no args at all", () => {
const { exitCode, helpShown, executeCalled } = runCli([]);
expect(exitCode).toBeNull();
expect(helpShown).toBe(true);
expect(executeCalled).toBe(false);
});

it("proceeds to execute() when a valid command is parsed", () => {
const { exitCode, executeCalled } = runCli(["app", "list"]);
expect(exitCode).toBeNull();
expect(executeCalled).toBe(true);
});
});
});
58 changes: 58 additions & 0 deletions __tests__/command-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,64 @@ describe("command-parser", () => {
});
});

function parseArgsWithState(args: string[]): { cmd: any; parseFailed: boolean } {
let cmd: any;
let parseFailed = false;
jest.isolateModules(() => {
const originalArgv = process.argv;
process.argv = ["node", "aether", ...args];
try {
const mod = require("../script/command-parser");
cmd = mod.createCommand();
parseFailed = mod.parseFailed;
} finally {
process.argv = originalArgv;
}
});
return { cmd, parseFailed };
}

describe("parseFailed flag", () => {
it("is false for a valid command", () => {
const { cmd, parseFailed } = parseArgsWithState(["app", "list"]);
expect(cmd).toBeDefined();
expect(parseFailed).toBe(false);
});

it("is true for an unknown top-level command category", () => {
const { parseFailed } = parseArgsWithState(["nonsense-command"]);
expect(parseFailed).toBe(true);
});

it("is true when a known command is missing required positional args", () => {
const { parseFailed } = parseArgsWithState(["app", "rm"]);
expect(parseFailed).toBe(true);
});

it("is true when a known command is given too many positional args", () => {
const { parseFailed } = parseArgsWithState(["app", "rm", "MyApp", "ExtraArg"]);
expect(parseFailed).toBe(true);
});

it("is true for an unknown subcommand under a known category", () => {
const { parseFailed } = parseArgsWithState(["app", "bogus-subcommand"]);
expect(parseFailed).toBe(true);
});

it("is false when invoked with no args at all (user wants help)", () => {
const { cmd, parseFailed } = parseArgsWithState([]);
expect(cmd).toBeUndefined();
expect(parseFailed).toBe(false);
});

it("is reset between invocations via isolateModules", () => {
const first = parseArgsWithState(["nonsense"]);
expect(first.parseFailed).toBe(true);
const second = parseArgsWithState(["app", "list"]);
expect(second.parseFailed).toBe(false);
});
});

describe("api-key", () => {
it("'api-key add <name> --scopes deploy,read' parses scopes as array", () => {
const cmd = parseArgs(["api-key", "add", "ci-deploy", "--scopes", "deploy,read"]);
Expand Down
6 changes: 5 additions & 1 deletion script/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import * as parser from "./command-parser";
import * as execute from "./command-executor";
import * as chalk from "chalk";

function run() {
function run(): void {
const command = parser.createCommand();

if (parser.parseFailed) {
process.exit(1);
}

if (!command) {
parser.showHelp(/*showRootDescription*/ false);
return;
Expand Down
22 changes: 20 additions & 2 deletions script/command-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ let isValidCommandCategory = false;
// Commands are the verb following the command category (e.g.: "add" in "app add").
let isValidCommand = false;
let wasHelpShown = false;
export let parseFailed = false;

export function resetParserState(): void {
isValidCommandCategory = false;
isValidCommand = false;
wasHelpShown = false;
parseFailed = false;
}

export function showHelp(showRootDescription?: boolean): void {
if (!wasHelpShown) {
Expand Down Expand Up @@ -216,7 +224,10 @@ function addCommonConfiguration(yargs: yargs.Argv): void {
yargs
.wrap(/*columnLimit*/ null)
.string("_") // Interpret non-hyphenated arguments as strings (e.g. an app version of '1.10').
.fail((msg: string) => showHelp()); // Suppress the default error message.
.fail((msg: string) => {
parseFailed = true;
showHelp();
}); // Suppress the default error message.
}

function appList(commandName: string, yargs: yargs.Argv): void {
Expand Down Expand Up @@ -998,7 +1009,10 @@ yargs
.alias("v", "version")
.version(packageJson.version)
.wrap(/*columnLimit*/ null)
.fail((msg: string) => showHelp(/*showRootDescription*/ true)).argv; // Suppress the default error message.
.fail((msg: string) => {
parseFailed = true;
showHelp(/*showRootDescription*/ true);
}).argv; // Suppress the default error message.

export function createCommand(): cli.ICommand {
let cmd: cli.ICommand;
Expand Down Expand Up @@ -1435,8 +1449,12 @@ export function createCommand(): cli.ICommand {
break;
}

parseFailed = wasHelpShown || cmd === undefined;
return cmd;
}

parseFailed = wasHelpShown && (!argv._ || argv._.length > 0);
return cmd;
}

function isValidRollout(args: any): boolean {
Expand Down