Skip to content

Commit

Permalink
Move to Jest as testing framework (#1035)
Browse files Browse the repository at this point in the history
* Prototype Jest as testing framework

* Add comment to direct ports

* Add bool tests, not direct ports

* Add tests for option with required value

* Add jest tests for optional option

* Add tests for options follow by options

* Add multiple short flags test

* Improve test description

* Add single dash test

* Add idea for value tests

* Add old camelcase tests and new flags tests

* Add more testing ideas

* Add testing for custom option processing

* Add extra tests for non-boolean default

* Add placeholder for test file skipped on first pass

* Add comments above describe groups to make more obvious

* Change option names to make easier to understand (more realistic)

* Expand overview comment

* Add tests for the ways values are specified

* Add Jest tests for .opts()

* Add negative number as an interesting dash case

* Add Jest dashdash test for stop processing options

* Add Jest regex tests

* Move dash testing and include full variety of ways values are specified

* Rename spy to mock

* Add Jest .version tests

* Port literal test and subsume dashdash test

* Remove implemented test notes

* Port some of command.name tests

* Add tests when have version command and .version

* Add note that whitebox test

* Add variadic test

* Add experimental _exitOverride to allow throw instead of process.exit

* Rework version tests using experimental exitOverride

* Add empty string test to option values

* Add test for command alias appearing in help

* Add unknownOption support for exitOverride

* Add test for allowUnknownOption

* Call added commands subcommand to clarify

* Add tests for .usage

* Note to tidy up how testing strings for better error messages

* Add first signal test in Jest

* Make new signal listener more predictable

* Add @types/jest

* Add full set of signals to test

* Add Jest --inspect tests

* Add Jest lookup tests

* Add exitOveride support to variadicArgNotLast

* Switch to execFile

* Switch to execFIle (and formally async)

* Fix typos

* Make case consistent in filename

* Add more Jest alias tests

* Add Jest executableSubcommand tests for default and executableFile

* Add Jest subsubcommand test

* Add to Jest alias tests

* Add Jest test for .commandHelp

* Delete a couple of ported tests covered better elsewhere

* Replace more process.exit with exit override

* Add Jest help tests

* Add Jest helpOption tests

* Add Jest noHelp test

* Add Jest asterisk test

* Not porting complex generic command tests

* Jest test for executable with no command

* Add default throw for _exitOverride, and tidy _exit and CommanderError usage

* Add Jest tests for _exitOverride and simplify usage with new default behaviour

* Added commented out tests for process.exit called after spawn

* Fix typo

* Add test on error class, because calling code may rely on it

* Clarify parameter type for exitOverride callback

* Lint

* Add test matching issue #1039

* Add test showing #1032

* Skip open issues normally

* Switch to Jest

- remove sinon and should dependencies
- switch over package script
- delete old tests
- rename folder to tests
- fix paths for executable tests

* Fix error detection on node 8

* Change exitOveride to allow carrying on, and intercept exits from executable subcommand.

* Rework exitOverride for async

- _exit never returns (as before)
- callback handled explicitly in executeSubcommand
- mark _exit as private (copy and paste omission)
- support executeSubCommandAsync i default override handler to prevent call to process.exit, as expected

* Replace listen with listen2 code: lint, and produce output so caller knows ready

* Add explanation in functional empty file

* Lint on pm test files, and remove the listen2 missed in earlier commit

* Add more test for open issues

* Remove testing ideas, not tracking them in file

* Add tests to `npm run lint` and fix errors

* Rename skipped tests for open issues to avoid warning in normal lint
  • Loading branch information
shadowspawn committed Sep 20, 2019
1 parent 138a32e commit d7f9cd4
Show file tree
Hide file tree
Showing 117 changed files with 9,001 additions and 2,519 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
@@ -1,5 +1,6 @@
{
"extends": "standard",
"extends": ["standard", "plugin:jest/recommended"],
"plugins": ["jest"],
"rules": {
"one-var": "off",
"semi": ["error", "always"],
Expand Down
119 changes: 105 additions & 14 deletions index.js
Expand Up @@ -87,6 +87,30 @@ Option.prototype.is = function(arg) {
return this.short === arg || this.long === arg;
};

/**
* CommanderError class
* @class
*/
class CommanderError extends Error {
/**
* Constructs the CommanderError class
* @param {Number} exitCode suggested exit code which could be used with process.exit
* @param {String} code an id string representing the error
* @param {String} message human-readable description of the error
* @constructor
*/
constructor(exitCode, code, message) {
super(message);
// properly capture stack trace in Node.js
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.code = code;
this.exitCode = exitCode;
}
}

exports.CommanderError = CommanderError;

/**
* Initialize a new `Command`.
*
Expand Down Expand Up @@ -157,6 +181,8 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts
cmd._helpDescription = this._helpDescription;
cmd._helpShortFlag = this._helpShortFlag;
cmd._helpLongFlag = this._helpLongFlag;
cmd._exitCallback = this._exitCallback;

cmd._executableFile = opts.executableFile; // Custom name for executable file
this.commands.push(cmd);
cmd.parseExpectedArgs(args);
Expand Down Expand Up @@ -228,6 +254,47 @@ Command.prototype.parseExpectedArgs = function(args) {
return this;
};

/**
* Register callback `fn` to use as replacement for calling process.exit.
*
* @param {Function} fn callback which will be passed a CommanderError
* @return {Command} for chaining
* @api public
*/

Command.prototype._exitOverride = function(fn) {
if (fn) {
this._exitCallback = fn;
} else {
this._exitCallback = function(err) {
if (err.code !== 'commander.executeSubCommandAsync') {
throw err;
} else {
// Async callback from spawn events, not useful to throw.
}
};
}
return this;
};

/**
* Call process.exit, and _exitCallback if defined.
*
* @param {Number} exitCode exit code for using with process.exit
* @param {String} code an id string representing the error
* @param {String} message human-readable description of the error
* @return never
* @api private
*/

Command.prototype._exit = function(exitCode, code, message) {
if (this._exitCallback) {
this._exitCallback(new CommanderError(exitCode, code, message));
// Expecting this line is not reached.
}
process.exit(exitCode);
};

/**
* Register callback `fn` for the command.
*
Expand Down Expand Up @@ -586,14 +653,30 @@ Command.prototype.executeSubCommand = function(argv, args, unknown, executableFi
}
});
});
proc.on('close', process.exit.bind(process));

// By default terminate process when spawned process terminates.
// Suppressing the exit if exitCallback defined is a bit messy and of limited use, but does allow process to stay running!
const exitCallback = this._exitCallback;
if (!exitCallback) {
proc.on('close', process.exit.bind(process));
} else {
proc.on('close', () => {
exitCallback(new CommanderError(process.exitCode || 0, 'commander.executeSubCommandAsync', '(close)'));
});
}
proc.on('error', function(err) {
if (err.code === 'ENOENT') {
console.error('error: %s(1) does not exist, try --help', bin);
} else if (err.code === 'EACCES') {
console.error('error: %s(1) not executable. try chmod or run with root', bin);
}
process.exit(1);
if (!exitCallback) {
process.exit(1);
} else {
const wrappedError = new CommanderError(1, 'commander.executeSubCommandAsync', '(error)');
wrappedError.nestedError = err;
exitCallback(wrappedError);
}
});

// Store the reference to the child process
Expand Down Expand Up @@ -810,8 +893,9 @@ Command.prototype.opts = function() {
*/

Command.prototype.missingArgument = function(name) {
console.error("error: missing required argument '%s'", name);
process.exit(1);
const message = `error: missing required argument '${name}'`;
console.error(message);
this._exit(1, 'commander.missingArgument', message);
};

/**
Expand All @@ -823,12 +907,14 @@ Command.prototype.missingArgument = function(name) {
*/

Command.prototype.optionMissingArgument = function(option, flag) {
let message;
if (flag) {
console.error("error: option '%s' argument missing, got '%s'", option.flags, flag);
message = `error: option '${option.flags}' argument missing, got '${flag}'`;
} else {
console.error("error: option '%s' argument missing", option.flags);
message = `error: option '${option.flags}' argument missing`;
}
process.exit(1);
console.error(message);
this._exit(1, 'commander.optionMissingArgument', message);
};

/**
Expand All @@ -840,8 +926,9 @@ Command.prototype.optionMissingArgument = function(option, flag) {

Command.prototype.unknownOption = function(flag) {
if (this._allowUnknownOption) return;
console.error("error: unknown option '%s'", flag);
process.exit(1);
const message = `error: unknown option '${flag}'`;
console.error(message);
this._exit(1, 'commander.unknownOption', message);
};

/**
Expand All @@ -852,8 +939,9 @@ Command.prototype.unknownOption = function(flag) {
*/

Command.prototype.variadicArgNotLast = function(name) {
console.error("error: variadic arguments must be last '%s'", name);
process.exit(1);
const message = `error: variadic arguments must be last '${name}'`;
console.error(message);
this._exit(1, 'commander.variadicArgNotLast', message);
};

/**
Expand Down Expand Up @@ -881,7 +969,7 @@ Command.prototype.version = function(str, flags, description) {
this.options.push(versionOption);
this.on('option:' + this._versionOptionName, function() {
process.stdout.write(str + '\n');
process.exit(0);
this._exit(0, 'commander.version', str);
});
return this;
};
Expand Down Expand Up @@ -1208,7 +1296,9 @@ Command.prototype.helpOption = function(flags, description) {

Command.prototype.help = function(cb) {
this.outputHelp(cb);
process.exit();
// exitCode: preserving original behaviour which was calling process.exit()
// message: do not have all displayed text available so only passing placeholder.
this._exit(process.exitCode || 0, 'commander.help', '(outputHelp)');
};

/**
Expand Down Expand Up @@ -1253,7 +1343,8 @@ function outputHelpIfNecessary(cmd, options) {
for (var i = 0; i < options.length; i++) {
if (options[i] === cmd._helpLongFlag || options[i] === cmd._helpShortFlag) {
cmd.outputHelp();
process.exit(0);
// (Do not have all displayed text available so only passing placeholder.)
cmd._exit(0, 'commander.helpDisplayed', '(outputHelp)');
}
}
}
Expand Down

0 comments on commit d7f9cd4

Please sign in to comment.