Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix --inspect and its ilk; closes #3681 #3699

Merged
merged 4 commits into from Feb 8, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mocharc.yml
Expand Up @@ -4,4 +4,4 @@ global:
- okGlobalA,okGlobalB
- okGlobalC
- callback*
timeout: 200
timeout: 300
27 changes: 16 additions & 11 deletions bin/mocha
Expand Up @@ -12,10 +12,15 @@
const {deprecate, warn} = require('../lib/utils');
const {spawn} = require('child_process');
const {loadOptions} = require('../lib/cli/options');
const {isNodeFlag, impliesNoTimeouts} = require('../lib/cli/node-flags');
const {
unparseNodeFlags,
isNodeFlag,
impliesNoTimeouts
} = require('../lib/cli/node-flags');
const unparse = require('yargs-unparser');
const debug = require('debug')('mocha:cli');
const debug = require('debug')('mocha:cli:mocha');
const {aliases} = require('../lib/cli/run-option-metadata');
const nodeEnv = require('node-environment-flags');

const mochaPath = require.resolve('./_mocha');
const mochaArgs = {};
Expand All @@ -32,7 +37,7 @@ debug('loaded opts', opts);
const disableTimeouts = value => {
if (impliesNoTimeouts(value)) {
debug(`option "${value}" disabled timeouts`);
mochaArgs.timeout = false;
mochaArgs.timeout = 0;
delete mochaArgs.timeouts;
delete mochaArgs.t;
}
Expand All @@ -45,13 +50,12 @@ const disableTimeouts = value => {
* @ignore
boneskull marked this conversation as resolved.
Show resolved Hide resolved
*/
const trimV8Option = value =>
value !== 'v8-options' && /^v8-/.test(value) ? value.slice(2) : value;
value !== 'v8-options' && /^v8-/.test(value) ? value.slice(3) : value;

// sort options into "node" and "mocha" buckets
Object.keys(opts).forEach(opt => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just noted, there is a Mocha flag -r and also one in nodeEnv . In this case flag is put into mocha bucket. Could lead to conflicts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mocha's --require and -r are both preferred over Node's, see isNodeFlag in lib/cli/node-flags.js

opt = trimV8Option(opt);
if (isNodeFlag(opt)) {
nodeArgs[opt] = opts[opt];
nodeArgs[trimV8Option(opt)] = opts[opt];
disableTimeouts(opt);
} else {
mochaArgs[opt] = opts[opt];
Expand Down Expand Up @@ -90,20 +94,19 @@ if (/^(debug|inspect)$/.test(mochaArgs._[0])) {
}

// allow --debug to invoke --inspect on Node.js v8 or newer.
// these show up in childOpts because they are not recognized as valid node flags in this version of node.
['debug', 'debug-brk']
.filter(opt => opt in mochaArgs)
.filter(opt => opt in nodeArgs && !nodeEnv.has(opt))
.forEach(opt => {
const newOpt = opt === 'debug' ? 'inspect' : 'inspect-brk';
warn(
`"--${opt}" is not available in Node.js ${
process.version
}; use "--${newOpt}" instead.`
);
nodeArgs[newOpt] = mochaArgs[opt];
nodeArgs[newOpt] = nodeArgs[opt];
mochaArgs.timeout = false;
debug(`--${opt} -> ${newOpt}`);
delete mochaArgs[opt];
delete nodeArgs[opt];
});

// historical
Expand All @@ -115,8 +118,10 @@ if (nodeArgs.gc) {
delete nodeArgs.gc;
}

debug('final node args', nodeArgs);

const args = [].concat(
unparse(nodeArgs),
unparseNodeFlags(nodeArgs),
mochaPath,
unparse(mochaArgs, {alias: aliases})
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

down L131:
when Node env variable NODE_OPTIONS is used, it just feeds the CL arguments list, I guess. Does the child process inherit these variables correctly?

Copy link
Member Author

@boneskull boneskull Feb 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the short answer is "yes". the longer answer is we ignore NODE_OPTIONS, and whatever's there does not go into any arguments list. when node args and NODE_OPTIONS conflict, node does whatever node does to resolve it.

Expand Down
21 changes: 21 additions & 0 deletions lib/cli/node-flags.js
Expand Up @@ -7,6 +7,7 @@
*/

const nodeFlags = require('node-environment-flags');
const unparse = require('yargs-unparser');

/**
* These flags are considered "debug" flags.
Expand Down Expand Up @@ -34,6 +35,7 @@ const debugFlags = new Set(['debug', 'debug-brk', 'inspect', 'inspect-brk']);
exports.isNodeFlag = flag =>
!/^(?:require|r)$/.test(flag) &&
(nodeFlags.has(flag) ||
debugFlags.has(flag) ||
/(?:preserve-symlinks(?:-main)?|harmony(?:[_-]|$)|(?:trace[_-].+$)|gc(?:[_-]global)?$|es[_-]staging$|use[_-]strict$|v8[_-](?!options).+?$)/.test(
flag
));
Expand All @@ -46,3 +48,22 @@ exports.isNodeFlag = flag =>
* @private
*/
exports.impliesNoTimeouts = flag => debugFlags.has(flag);

/**
* All non-strictly-boolean arguments to node--those with values--must specify those values using `=`, e.g., `--inspect=0.0.0.0`.
* Unparse these arguments using `yargs-unparser` (which would result in `--inspect 0.0.0.0`), then supply `=` where we have values.
* There's probably an easier or more robust way to do this; fixes welcome
* @param {Object} opts - Arguments object
* @returns {string[]} Unparsed arguments using `=` to specify values
* @private
*/
exports.unparseNodeFlags = opts => {
var args = unparse(opts);
return args.length
? args
.join(' ')
.split(/\b/)
.map(arg => (arg === ' ' ? '=' : arg))
.join('')
: [];
};
boneskull marked this conversation as resolved.
Show resolved Hide resolved
49 changes: 37 additions & 12 deletions lib/cli/options.js
Expand Up @@ -18,6 +18,7 @@ const findup = require('findup-sync');
const {deprecate} = require('../utils');
const debug = require('debug')('mocha:cli:options');
const {createMissingArgumentError} = require('../errors');
const {isNodeFlag} = require('./node-flags');

/**
* The `yargs-parser` namespace
Expand Down Expand Up @@ -75,22 +76,46 @@ const nargOpts = types.array
* @ignore
*/
const parse = (args = [], ...configObjects) => {
const result = yargsParser.detailed(
args,
Object.assign(
{
configuration,
configObjects,
coerce: coerceOpts,
narg: nargOpts,
alias: aliases
},
types
)
// save node-specific args for special handling.
// 1. when these args have a "=" they should be considered to have values
// 2. if they don't, they just boolean flags
// 3. to avoid explicitly defining the set of them, we tell yargs-parser they
// are ALL boolean flags.
// 4. we can then reapply the values after yargs-parser is done.
const nodeArgs = (Array.isArray(args) ? args : args.split(' ')).reduce(
(acc, arg) => {
const pair = arg.split('=');
const flag = pair[0].replace(/^--?/, '');
if (isNodeFlag(flag)) {
return arg.includes('=')
? acc.concat([[flag, pair[1]]])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(sic)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we save ALL found node args here so we can use them in the boolean array below.
the values are saved as whatever they are; if there is no value (e.g., --inspect), then the value is saved as true, which is what yargs-unparser understands.

: acc.concat([[flag, true]]);
}
return acc;
},
[]
);

const result = yargsParser.detailed(args, {
configuration,
configObjects,
coerce: coerceOpts,
narg: nargOpts,
alias: aliases,
string: types.string,
array: types.array,
number: types.number,
boolean: types.boolean.concat(nodeArgs.map(pair => pair[0]))
});
if (result.error) {
throw createMissingArgumentError(result.error.message);
}

// reapply "=" arg values from above
nodeArgs.forEach(([key, value]) => {
result.argv[key] = value;
});

return result.argv;
};

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -498,7 +498,7 @@
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"ms": "2.1.1",
"node-environment-flags": "1.0.2",
"node-environment-flags": "1.0.4",
"object.assign": "4.1.0",
"strip-json-comments": "2.0.1",
"supports-color": "6.0.0",
Expand Down
12 changes: 9 additions & 3 deletions test/assertions.js
Expand Up @@ -9,7 +9,7 @@ exports.mixinMochaAssertions = function(expect) {
return (
Object.prototype.toString.call(v) === '[object Object]' &&
typeof v.output === 'string' &&
typeof v.code === 'number' &&
'code' in v && // may be null
Array.isArray(v.args)
);
}
Expand Down Expand Up @@ -59,9 +59,9 @@ exports.mixinMochaAssertions = function(expect) {
}
)
.addAssertion(
'<RawResult|RawRunResult|JSONRunResult> [not] to have [completed with] [exit] code <number>',
'<RawRunResult|JSONRunResult> [not] to have completed with [exit] code <number>',
function(expect, result, code) {
expect(result, '[not] to have property', 'code', code);
expect(result.code, '[not] to be', code);
}
)
.addAssertion(
Expand Down Expand Up @@ -295,5 +295,11 @@ exports.mixinMochaAssertions = function(expect) {
function(expect, result, output) {
expect(result.output, '[not] to satisfy', output);
}
)
.addAssertion(
'<RawRunResult|JSONRunResult> to have [exit] code <number>',
function(expect, result, code) {
expect(result.code, 'to be', code);
}
);
};
8 changes: 4 additions & 4 deletions test/integration/helpers.js
Expand Up @@ -81,10 +81,10 @@ module.exports = {
var path;

path = resolveFixturePath(fixturePath);
args = args || [];
args = (args || []).concat('--reporter', 'json', path);

return invokeMocha(
args.concat(['--reporter', 'json', path]),
args,
function(err, res) {
if (err) return fn(err);

Expand All @@ -95,8 +95,8 @@ module.exports = {
fn(
new Error(
format(
'Failed to parse JSON reporter output.\nArgs: %O\nResult:\n\n%O',
args,
'Failed to parse JSON reporter output. Error:\n%O\nResponse:\n%O',
err,
res
)
)
Expand Down
106 changes: 102 additions & 4 deletions test/integration/options/debug.spec.js
Expand Up @@ -14,19 +14,117 @@ describe('--debug', function() {

it('should invoke --inspect', function(done) {
invokeMocha(
['--debug', '--file', DEFAULT_FIXTURE],
['--debug', DEFAULT_FIXTURE],
function(err, res) {
if (err) {
return done(err);
}
expect(res, 'to have passed').and(
expect(res, 'to contain output', /Debugger listening/i);
done();
},
'pipe'
);
});

it('should invoke --inspect-brk', function(done) {
var proc = invokeMocha(
Copy link
Member

@juergba juergba Feb 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is failing with "Uncaught UnexpectedError" on my Windows. I need to investigate further.
The other three "Node.js v8+" tests do pass.
"Node.js v6" tests are skipped.

Copy link
Member Author

@boneskull boneskull Feb 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I don't know how to fix this. that's where I was hoping you could help (eventually)

killing the subprocess using our method (because inspect-brk means you're in a debugging session) doesn't return any process output nor an exit code on windows. the method is likely wrong for windows (using SIGINT on posix), but I don't know what the correct one is.

['--debug-brk', DEFAULT_FIXTURE],
function(err, res) {
if (err) {
return done(err);
}
expect(res, 'to contain output', /Debugger listening/i);
done();
},
'pipe'
);

// debugger must be manually killed
setTimeout(function() {
process.kill(proc.pid, 'SIGINT');
}, 2000);
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change to:

setTimeout(function() {
  process.kill(proc.pid, 'SIGINT');
}, 2000);

This way the err.code is 1 (not undefined which crashes to have failed).
A timeout of 1000 is too short, res may not be ready, yet => to contain output fails.

On my windows platform the four 'Node.js v8+' tests do pass.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand what you're asking for


it('should respect custom host/port', function(done) {
invokeMocha(
['--debug=127.0.0.1:9229', DEFAULT_FIXTURE],
function(err, res) {
if (err) {
return done(err);
}
expect(
res,
'to contain output',
/Debugger listening on .*127.0.0.1:9229/i
);
done();
},
'pipe'
);
});

it('should warn about incorrect usage for version', function(done) {
invokeMocha(
['--debug=127.0.0.1:9229', DEFAULT_FIXTURE],
function(err, res) {
if (err) {
return done(err);
}
expect(res, 'to contain output', /"--debug" is not available/i);
done();
},
'pipe'
);
});
});
boneskull marked this conversation as resolved.
Show resolved Hide resolved

describe('Node.js v6', function() {
// note that v6.3.0 and newer supports --inspect but still supports --debug.
before(function() {
if (process.version.substring(0, 2) !== 'v6') {
this.skip();
}
});

it('should start debugger', function(done) {
var proc = invokeMocha(
['--debug', DEFAULT_FIXTURE],
function(err, res) {
if (err) {
return done(err);
}
expect(res, 'to contain output', /Debugger listening/i);
done();
},
'pipe'
);

// debugger must be manually killed
setTimeout(function() {
process.kill(proc.pid, 'SIGINT');
}, 2000);
});

it('should respect custom host/port', function(done) {
var proc = invokeMocha(
['--debug=127.0.0.1:9229', DEFAULT_FIXTURE],
function(err, res) {
if (err) {
return done(err);
}
expect(
res,
'to contain output',
/Debugger listening/i
/Debugger listening on .*127.0.0.1:9229/i
);
done();
},
{stdio: 'pipe'}
'pipe'
);

setTimeout(function() {
process.kill(proc.pid, 'SIGINT');
}, 2000);
});
});
});