diff --git a/src/packages/cli/package.json b/src/packages/cli/package.json index f286f224b3..d1de92dc2a 100644 --- a/src/packages/cli/package.json +++ b/src/packages/cli/package.json @@ -30,7 +30,7 @@ }, "scripts": { "tsc": "tsc --build", - "test": "nyc npm run mocha", + "test": "nyc --reporter lcov npm run mocha", "mocha": "cross-env TS_NODE_FILES=true mocha --exit --check-leaks --throw-deprecation --trace-warnings --require ts-node/register 'tests/**/*.test.ts'", "start": "cross-env node --require ts-node/register --inspect src/cli.ts" }, diff --git a/src/packages/cli/src/args.ts b/src/packages/cli/src/args.ts index 5899d980c0..a36fae8690 100644 --- a/src/packages/cli/src/args.ts +++ b/src/packages/cli/src/args.ts @@ -1,5 +1,10 @@ +// `import Yargs from "yargs"` is the Yargs singleton and namespace +// `import yargs from "yargs/yargs"` is the non-singleton interface +// See https://github.com/yargs/yargs/issues/1648 +import Yargs, { Options } from "yargs"; +import yargs from "yargs/yargs"; + import { TruffleColors } from "@ganache/colors"; -import yargs, { Options } from "yargs"; import { DefaultFlavor, FilecoinFlavorName, @@ -26,7 +31,7 @@ marked.setOptions({ }) }); -const wrapWidth = Math.min(120, yargs.terminalWidth()); +const wrapWidth = Math.min(120, Yargs.terminalWidth()); const NEED_HELP = "Need more help? Reach out to the Truffle community at"; const COMMUNITY_LINK = "https://trfl.io/support"; @@ -43,7 +48,7 @@ const highlight = (t: string) => unescapeEntities(marked.parseInline(t)); const center = (str: string) => " ".repeat(Math.max(0, Math.floor((wrapWidth - str.length) / 2))) + str; -const addAliases = (args: yargs.Argv<{}>, aliases: string[], key: string) => { +const addAliases = (args: Yargs.Argv<{}>, aliases: string[], key: string) => { const options = { hidden: true, alias: key }; return aliases.reduce((args, a) => args.option(a, options), args); }; @@ -54,7 +59,7 @@ function processOption( group: string, option: string, optionObj: Definitions[string], - argv: yargs.Argv, + argv: Yargs.Argv, flavor: string ) { if (optionObj.disableInCLI !== true) { @@ -122,7 +127,7 @@ function applyDefaults( flavorDefaults: | typeof DefaultOptionsByName[keyof typeof DefaultOptionsByName] | typeof _DefaultServerOptions, - flavorArgs: yargs.Argv<{}>, + flavorArgs: Yargs.Argv<{}>, flavor: keyof typeof DefaultOptionsByName ) { for (const category in flavorDefaults) { @@ -160,8 +165,9 @@ export default function ( // disable dot-notation because yargs just can't coerce args properly... // ...on purpose! https://github.com/yargs/yargs/issues/1021#issuecomment-352324693 - yargs + const yargsParser = yargs() .parserConfiguration({ "dot-notation": false }) + .exitProcess(false) .strict() .usage(versionUsageOutputText) .epilogue( @@ -188,12 +194,9 @@ export default function ( command = ["$0", flavor]; defaultPort = 8545; break; - default: - command = flavor; - defaultPort = 8545; } - yargs.command( + yargsParser.command( command, chalk`Use the {bold ${flavor}} flavor of Ganache`, flavorArgs => { @@ -214,14 +217,19 @@ export default function ( description: chalk`Port to listen on.${EOL}{dim deprecated aliases: --port}${EOL}`, alias: ["p", "port"], type: "number", - default: defaultPort + default: defaultPort, + // `string: true` to allow raw value to be used in validation below + // (otherwise string values becomes NaN) + string: true, + coerce: port => (isFinite(port) ? +port : port) }) .check(argv => { const { "server.port": port, "server.host": host } = argv; - if (port < 1 || port > 65535) { - throw new Error(`Invalid port number '${port}'`); + if (!isFinite(port) || port < 1 || port > 65535) { + throw new Error( + `Port should be >= 0 and < 65536. Received ${port}.` + ); } - if (host.trim() === "") { throw new Error("Cannot leave host blank; please provide a host"); } @@ -244,7 +252,7 @@ export default function ( ); } - yargs + yargsParser .command( "instances", highlight( @@ -272,6 +280,14 @@ export default function ( stopArgs.action = "stop"; } ) + .check(instancesArgs => { + if (instancesArgs["_"].length <= 1) { + throw new Error( + "No sub-command given. See `ganache instances --help` for more information." + ); + } + return true; + }) .version(false); } ) @@ -280,7 +296,7 @@ export default function ( .wrap(wrapWidth) .version(version); - const parsedArgs = yargs.parse(rawArgs); + const parsedArgs = yargsParser.parse(rawArgs); let finalArgs: GanacheArgs; if (parsedArgs.action === "stop") { @@ -308,7 +324,7 @@ export default function ( >) }; } else { - throw new Error(`Unknown action: ${parsedArgs.action}`); + finalArgs = { action: "none" }; } return finalArgs; diff --git a/src/packages/cli/src/cli.ts b/src/packages/cli/src/cli.ts index bcb9a17e74..830397a6bb 100644 --- a/src/packages/cli/src/cli.ts +++ b/src/packages/cli/src/cli.ts @@ -18,6 +18,7 @@ import { import { TruffleColors } from "@ganache/colors"; import Table from "cli-table"; import chalk from "chalk"; +import { GanacheArgs } from "./types"; const porscheColor = chalk.hex(TruffleColors.porsche); @@ -49,181 +50,189 @@ const detailedVersion = `ganache v${version} (@ganache/cli: ${cliVersion}, @gana const isDocker = "DOCKER" in process.env && process.env.DOCKER.toLowerCase() === "true"; -const argv = args(detailedVersion, isDocker); -if (argv.action === "start") { - const flavor = argv.flavor; - const cliSettings = argv.server; +let argv: GanacheArgs; +try { + argv = args(detailedVersion, isDocker); +} catch (err: any) { + // yargs has already output the error message, so we don't need to do anything here +} - console.log(detailedVersion); +if (argv !== undefined) { + if (argv.action === "start") { + const flavor = argv.flavor; + const cliSettings = argv.server; - let server: ReturnType; - try { - server = Ganache.server(argv); - } catch (error: any) { - console.error(error.message); - process.exit(1); - } + console.log(detailedVersion); - let started = false; - process.on("uncaughtException", function (e) { - if (started) { - logAndForceExit([e], 1); - } else { - logAndForceExit([e.stack], 1); - } - }); - - let receivedShutdownSignal: boolean = false; - const handleSignal = async (signal: NodeJS.Signals) => { - console.log(`\nReceived shutdown signal: ${signal}`); - closeHandler(); - }; - const closeHandler = async () => { + let server: ReturnType; try { - // graceful shutdown - switch (server.status) { - case ServerStatus.opening: - receivedShutdownSignal = true; - console.log("Server is currently starting; waiting…"); - return; - case ServerStatus.open: - console.log("Shutting down…"); - await server.close(); - console.log("Server has been shut down"); - break; - } - // don't just call `process.exit()` here, as we don't want to hide shutdown - // errors behind a forced shutdown. Note: `process.exitCode` doesn't do - // anything other than act as a place to anchor this comment :-) - process.exitCode = 0; - } catch (err: any) { - logAndForceExit( - [ - "\nReceived an error while attempting to shut down the server: ", - err.stack || err - ], - 1 - ); + server = Ganache.server(argv); + } catch (error: any) { + console.error(error.message); + process.exit(1); } - }; - - // See http://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js - if (process.platform === "win32") { - const rl = (require("readline") as typeof Readline) - .createInterface({ - input: process.stdin, - output: process.stdout - }) - .on("SIGINT", () => { - // we must "close" the RL interface otherwise the process will think we - // are still listening - // https://nodejs.org/api/readline.html#readline_event_sigint - rl.close(); - handleSignal("SIGINT"); - }); - } - process.on("SIGINT", handleSignal); - process.on("SIGTERM", handleSignal); - process.on("SIGHUP", handleSignal); - - console.log("Starting RPC server"); - server.listen(cliSettings.port, cliSettings.host, async (err: Error) => { - if (err) { - console.error(err); - process.exitCode = 1; - return; - } else if (receivedShutdownSignal) { + let started = false; + process.on("uncaughtException", function (e) { + if (started) { + logAndForceExit([e], 1); + } else { + logAndForceExit([e.stack], 1); + } + }); + + let receivedShutdownSignal: boolean = false; + const handleSignal = async (signal: NodeJS.Signals) => { + console.log(`\nReceived shutdown signal: ${signal}`); closeHandler(); - return; - } - started = true; - switch (flavor) { - case FilecoinFlavorName: { - await initializeFilecoin( - server.provider as FilecoinProvider, - cliSettings + }; + const closeHandler = async () => { + try { + // graceful shutdown + switch (server.status) { + case ServerStatus.opening: + receivedShutdownSignal = true; + console.log("Server is currently starting; waiting…"); + return; + case ServerStatus.open: + console.log("Shutting down…"); + await server.close(); + console.log("Server has been shut down"); + break; + } + // don't just call `process.exit()` here, as we don't want to hide shutdown + // errors behind a forced shutdown. Note: `process.exitCode` doesn't do + // anything other than act as a place to anchor this comment :-) + process.exitCode = 0; + } catch (err: any) { + logAndForceExit( + [ + "\nReceived an error while attempting to shut down the server: ", + err.stack || err + ], + 1 ); - break; - } - case EthereumFlavorName: - default: { - initializeEthereum(server.provider as EthereumProvider, cliSettings); - break; } + }; + + // See http://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js + if (process.platform === "win32") { + const rl = (require("readline") as typeof Readline) + .createInterface({ + input: process.stdin, + output: process.stdout + }) + .on("SIGINT", () => { + // we must "close" the RL interface otherwise the process will think we + // are still listening + // https://nodejs.org/api/readline.html#readline_event_sigint + rl.close(); + handleSignal("SIGINT"); + }); } - // if process.send is defined, this is a child_process (we assume a detached - // instance), so we need to notify that we are ready. - const isDetachedInstance = process.send !== undefined; - if (isDetachedInstance) { - notifyDetachedInstanceReady(); - } - }); -} else if (argv.action === "stop") { - const instanceName = argv.name; - - stopDetachedInstance(instanceName).then(instanceFound => { - if (instanceFound) { - console.log("Instance stopped"); - } else { - console.error("Instance not found"); - } - }); -} else if (argv.action === "start-detached") { - startDetachedInstance(process.argv, argv, version) - .then(instance => { - const highlightedName = porscheColor(instance.name); - // output only the instance name to allow users to capture stdout and use to - // programmatically stop the instance - console.log(highlightedName); - }) - .catch(err => { - // the child process would have output its error to stdout, so no need to - // output anything more - }); -} else if (argv.action === "list") { - getDetachedInstances().then(instances => { - if (instances.length === 0) { - console.log( - `No detached instances found - try ${porscheColor( - "ganache --detach" - )} to start a detached instance` - ); - } else { - const now = Date.now(); - - const table = new Table({ - head: [ - chalk.bold("PID"), - chalk.bold("Name"), - chalk.bold("Flavor"), - chalk.bold("Version"), - chalk.bold("Host"), - chalk.bold("Uptime") - ], - colAligns: ["right", "left", "left", "left", "left", "right"], - style: { - head: ["white", "white", "white", "white", "white", "white"] + process.on("SIGINT", handleSignal); + process.on("SIGTERM", handleSignal); + process.on("SIGHUP", handleSignal); + + console.log("Starting RPC server"); + server.listen(cliSettings.port, cliSettings.host, async (err: Error) => { + if (err) { + console.error(err); + process.exitCode = 1; + return; + } else if (receivedShutdownSignal) { + closeHandler(); + return; + } + started = true; + switch (flavor) { + case FilecoinFlavorName: { + await initializeFilecoin( + server.provider as FilecoinProvider, + cliSettings + ); + break; } - }); + case EthereumFlavorName: + default: { + initializeEthereum(server.provider as EthereumProvider, cliSettings); + break; + } + } - instances.sort((a, b) => b.startTime - a.startTime); - for (let i = 0; i < instances.length; i++) { - const instance = instances[i]; - - const uptime = now - instance.startTime; - table.push([ - instance.pid.toString(), - porscheColor(instance.name), - instance.flavor, - instance.version, - `${instance.host}:${instance.port.toString()}`, - formatUptime(uptime) - ]); + // if process.send is defined, this is a child_process (we assume a detached + // instance), so we need to notify that we are ready. + const isDetachedInstance = process.send !== undefined; + if (isDetachedInstance) { + notifyDetachedInstanceReady(); + } + }); + } else if (argv.action === "stop") { + const instanceName = argv.name; + + stopDetachedInstance(instanceName).then(instanceFound => { + if (instanceFound) { + console.log("Instance stopped"); + } else { + console.error("Instance not found"); } + }); + } else if (argv.action === "start-detached") { + startDetachedInstance(process.argv, argv, version) + .then(instance => { + const highlightedName = porscheColor(instance.name); + // output only the instance name to allow users to capture stdout and use to + // programmatically stop the instance + console.log(highlightedName); + }) + .catch(err => { + // the child process would have output its error to stdout, so no need to + // output anything more + }); + } else if (argv.action === "list") { + getDetachedInstances().then(instances => { + if (instances.length === 0) { + console.log( + `No detached instances found - try ${porscheColor( + "ganache --detach" + )} to start a detached instance` + ); + } else { + const now = Date.now(); + + const table = new Table({ + head: [ + chalk.bold("PID"), + chalk.bold("Name"), + chalk.bold("Flavor"), + chalk.bold("Version"), + chalk.bold("Host"), + chalk.bold("Uptime") + ], + colAligns: ["right", "left", "left", "left", "left", "right"], + style: { + head: ["white", "white", "white", "white", "white", "white"] + } + }); + + instances.sort((a, b) => b.startTime - a.startTime); + for (let i = 0; i < instances.length; i++) { + const instance = instances[i]; + + const uptime = now - instance.startTime; + table.push([ + instance.pid.toString(), + porscheColor(instance.name), + instance.flavor, + instance.version, + `${instance.host}:${instance.port.toString()}`, + formatUptime(uptime) + ]); + } - console.log(table.toString()); - } - }); + console.log(table.toString()); + } + }); + } } diff --git a/src/packages/cli/src/process-name.ts b/src/packages/cli/src/process-name.ts index 0c8dd6f4c1..11f98ed142 100644 --- a/src/packages/cli/src/process-name.ts +++ b/src/packages/cli/src/process-name.ts @@ -1,20 +1,23 @@ -function pick(source: string[]) { - const partIndex = Math.floor(Math.random() * source.length); +function pick(source: string[], random: () => number) { + const partIndex = Math.floor(random() * source.length); return source[partIndex]; } /** * Generates a random name to assign to an instance of Ganache. The name is * generated from an adjective, a flavor and a type of desert, in the form of - * `__`, eg., `salted_caramel_ganache`. + * `__`, eg., `salted_caramel_ganache`. */ -export default function createInstanceName() { - const name = `${pick(adjectives)}_${pick(flavors)}_${pick(kinds)}`; - return name; +export default function createInstanceName(random: () => number = Math.random) { + return `${pick(adjectives, random)}_${pick(flavors, random)}_${pick( + kinds, + random + )}`; } const adjectives = [ "baked", "candied", + "creamy", "deepfried", "frozen", "hot", diff --git a/src/packages/cli/src/types.ts b/src/packages/cli/src/types.ts index 1cb603e559..d6ad328a73 100644 --- a/src/packages/cli/src/types.ts +++ b/src/packages/cli/src/types.ts @@ -6,7 +6,7 @@ type CliServerOptions = { port: number; }; -type Action = "start" | "start-detached" | "list" | "stop"; +type Action = "start" | "start-detached" | "list" | "stop" | "none"; type AbstractArgs = { action: TAction; @@ -21,6 +21,7 @@ export type StartArgs = export type GanacheArgs = | (AbstractArgs<"stop"> & { name: string }) | AbstractArgs<"list"> + | AbstractArgs<"none"> | StartArgs; export type CliSettings = CliServerOptions; diff --git a/src/packages/cli/tests/args.test.ts b/src/packages/cli/tests/args.test.ts index 870bbe4bbf..39585e4e25 100644 --- a/src/packages/cli/tests/args.test.ts +++ b/src/packages/cli/tests/args.test.ts @@ -38,7 +38,7 @@ describe("args", () => { }); }); - it("should remove arguments who are kebab-cased", () => { + it("should remove arguments which are kebab-cased", () => { const input = { "namespace.name": "value", "namespace.kebab-case": "value", @@ -54,10 +54,134 @@ describe("args", () => { }); describe("parse args", () => { - describe("detach", () => { - const versionString = "Version string"; - const isDocker = false; + const versionString = "Version string"; + const isDocker = false; + + describe("help", () => { + it("should accept a help parameter", () => { + const rawArgs = ["--help"]; + const options = args(versionString, isDocker, rawArgs); + + assert.deepStrictEqual(options, { action: "none" }); + }); + }); + + describe("flavor", () => { + it("should default to ethereum", () => { + const options = args(versionString, isDocker, []); + + assert.strictEqual(options.action, "start"); + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above. + if (options.action === "start") { + assert.strictEqual(options.flavor, "ethereum"); + } + }); + + it("should accept a flavor of ethereum", () => { + const options = args(versionString, isDocker, ["ethereum"]); + + assert.strictEqual(options.action, "start"); + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above. + if (options.action === "start") { + assert.strictEqual(options.flavor, "ethereum"); + } + }); + + it("should accept a flavor of filecoin", () => { + const options = args(versionString, isDocker, ["filecoin"]); + + assert.strictEqual(options.action, "start"); + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above. + if (options.action === "start") { + assert.strictEqual(options.flavor, "filecoin"); + } + }); + + it("should reject a non-standard flavor", () => { + assert.throws(() => args(versionString, isDocker, ["not-a-flavor"]), { + name: "YError", + message: "Unknown argument: not-a-flavor" + }); + }); + }); + + describe("host and port", () => { + it("should default configuration to 127.0.0.1:8545", () => { + const options = args(versionString, isDocker, []); + + assert.strictEqual(options.action, "start"); + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above. + if (options.action === "start") { + assert.strictEqual(options.server.host, "127.0.0.1"); + assert.strictEqual(options.server.port, 8545); + } + }); + + it("should default configuration to 0.0.0.0:8545 if running within docker", () => { + const options = args(versionString, true, []); + + assert.strictEqual(options.action, "start"); + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above. + if (options.action === "start") { + assert.strictEqual(options.server.host, "0.0.0.0"); + assert.strictEqual(options.server.port, 8545); + } + }); + + it("should parse the provided host configuration", () => { + const options = args(versionString, true, ["--host", "localhost"]); + assert.strictEqual(options.action, "start"); + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above + if (options.action === "start") { + assert.strictEqual(options.server.host, "localhost"); + assert.strictEqual(options.server.port, 8545); + } + }); + + it("should parse the provided port configuration", () => { + const options = args(versionString, isDocker, ["--port", "1234"]); + + assert.strictEqual(options.action, "start"); + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above + if (options.action === "start") { + assert.strictEqual(options.server.host, "127.0.0.1"); + assert.strictEqual(options.server.port, 1234); + } + }); + + it("should reject invalid host argument", () => { + const invalidHost = ""; + const rawArgs = ["--host", invalidHost]; + + assert.throws( + () => args(versionString, isDocker, rawArgs), + new Error("Cannot leave host blank; please provide a host") + ); + }); + + it("should reject invalid port argument", () => { + const invalidPorts = ["0", "-1", "65536", "absolutely-not-a-port"]; + for (const port of invalidPorts) { + const rawArgs = ["--port", port]; + + assert.throws( + () => args(versionString, isDocker, rawArgs), + new Error(`Port should be >= 0 and < 65536. Received ${port}.`), + `Expected to throw with supplied port of '${port}'` + ); + } + }); + }); + + describe("detach", () => { const detachModeArgs = [ "--detach", "--D", @@ -120,5 +244,94 @@ describe("args", () => { assert.strictEqual(options.action, "start"); }); }); + + describe("instances", () => { + it("should fail when no sub-command is specified", () => { + const rawArgs = ["instances"]; + assert.throws(() => args(versionString, false, rawArgs), { + name: "Error", + message: + "No sub-command given. See `ganache instances --help` for more information." + }); + }); + + it("should fail when invalid sub-command is specified", () => { + const rawArgs = ["instances", "invalid-command"]; + assert.throws(() => args(versionString, false, rawArgs), { + name: "YError", + message: "Unknown argument: invalid-command" + }); + }); + + describe("list", () => { + it("should parse the `list` sub command", () => { + const rawArgs = ["instances", "list"]; + const options = args(versionString, isDocker, rawArgs); + assert.strictEqual( + options.action, + "list", + "Expected action to be 'list' when 'list' sub-command is specified" + ); + }); + + it("should not accept options", () => { + const invalidOptions = ["detach", "port", "host", "chain.blockTime"]; + + for (let option of invalidOptions) { + const rawArgs = ["instances", "list", `--${option}`]; + + assert.throws( + () => args(versionString, isDocker, rawArgs), + { + name: "YError", + message: `Unknown argument: ${option}` + }, + `Expected to throw with arguments of '${JSON.stringify(rawArgs)}'` + ); + } + }); + + it("should not accept an additional command", () => { + const rawArgs = ["instances", "list", `additional-command`]; + + assert.throws( + () => args(versionString, isDocker, rawArgs), + { + name: "YError", + message: "Unknown argument: additional-command" + }, + `Expected to throw with arguments of '${JSON.stringify(rawArgs)}'` + ); + }); + }); + + describe("stop", () => { + it("should fail if no instance name is supplied", () => { + const rawArgs = ["instances", "stop"]; + assert.throws(() => args(versionString, isDocker, rawArgs)); + }); + + it("should parse the `stop` sub command", () => { + const rawArgs = ["instances", "stop", "instance-name"]; + const options = args(versionString, isDocker, rawArgs); + + assert.strictEqual( + options.action, + "stop", + "Expected action to be 'list' when 'list' sub-command is specified" + ); + + // We check the value of `options.action` here in order to narrow the + // type of options. The negative case fails in the assert above + if (options.action === "stop") { + assert.strictEqual( + options.name, + "instance-name", + "Instance name should have been the specified argument" + ); + } + }); + }); + }); }); }); diff --git a/src/packages/cli/tests/process-name.test.ts b/src/packages/cli/tests/process-name.test.ts new file mode 100644 index 0000000000..af6048a666 --- /dev/null +++ b/src/packages/cli/tests/process-name.test.ts @@ -0,0 +1,64 @@ +import assert from "assert"; +import createProcessName from "../src/process-name"; + +// generates a "random" number generator function that, when called, returns (in +// order) each value from the provided array, followed by -1 when there are none +// left. +function createRandom(...values: number[]): () => number { + return () => (values.length > 0 ? (values.shift() as number) : -1); +} + +const MAX_RANDOM = 0.9999999999999999; + +// the values passed to createRandom specify the "random" number required to +// pick the specified part. This is calculated by: +// `source.indexOf("term") / source.length` +const wellKnownNames = [ + { + random: createRandom(0, 0, 0), + name: "baked_almond_bar" + }, + { + random: createRandom(MAX_RANDOM, MAX_RANDOM, MAX_RANDOM), + name: "sticky_tiramisu_waffle" + }, + { + random: createRandom( + 0.2727272727272727, + 0.21428571428571427, + 0.4444444444444444 + ), + name: "deepfried_chocolate_ganache" + }, + { + random: createRandom( + 0.18181818181818182, + 0.07142857142857142, + 0.9259259259259259 + ), + name: "creamy_banana_truffle" + } +]; + +// it's not necessarily important that this generates the correct names, but +// it's good to test that it's doing what we expect. +describe("createProcessName", () => { + for (const wellKnownName of wellKnownNames) { + it(`should create the correct instance name: ${wellKnownName.name}`, () => { + const generatedName = createProcessName(wellKnownName.random); + + assert.strictEqual(generatedName, wellKnownName.name); + }); + } + + it("should create a process name, without a mocked RNG", () => { + const generatedName = createProcessName(); + // each part must be at least 3 chars long, and at most 20 chars + const nameRegex = /^([a-z]{3,20}_){2}[a-z]{3,20}$/; + assert.match( + generatedName, + nameRegex, + `Exepected to have generated a reasonable name, got "${generatedName}"` + ); + }); +});