diff --git a/bin/mocha.js b/bin/mocha.js old mode 100644 new mode 100755 index fb58e65fcd..27e02fd5e9 --- a/bin/mocha.js +++ b/bin/mocha.js @@ -10,6 +10,7 @@ * @private */ +const os = require('os'); const {loadOptions} = require('../lib/cli/options'); const { unparseNodeFlags, @@ -22,6 +23,9 @@ const {aliases} = require('../lib/cli/run-option-metadata'); const mochaArgs = {}; const nodeArgs = {}; +const EXIT_SUCCESS = 0; +const EXIT_FAILURE = 1; +const SIGNAL_OFFSET= 128; let hasInspect = false; const opts = loadOptions(process.argv.slice(2)); @@ -109,7 +113,15 @@ if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) { proc.on('exit', (code, signal) => { process.on('exit', () => { if (signal) { - process.kill(process.pid, signal); + const numericSignal = + typeof signal === 'string' ? os.constants.signals[signal] : signal; + if (mochaArgs['posix-exit-codes'] === true) { + process.exit(SIGNAL_OFFSET + numericSignal); + } else { + process.kill(process.pid, signal); + } + } else if (code !== 0 && mochaArgs['posix-exit-codes'] === true) { + process.exit(EXIT_FAILURE); } else { process.exit(code); } @@ -126,7 +138,7 @@ if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) { // be needed. if (!args.parallel || args.jobs < 2) { // win32 does not support SIGTERM, so use next best thing. - if (require('os').platform() === 'win32') { + if (os.platform() === 'win32') { proc.kill('SIGKILL'); } else { // using SIGKILL won't cleanly close the output streams, which can result diff --git a/docs/index.md b/docs/index.md index 997a90e553..a2d701b164 100644 --- a/docs/index.md +++ b/docs/index.md @@ -936,6 +936,18 @@ Define a global variable name. For example, suppose your app deliberately expose By using this option in conjunction with `--check-leaks`, you can specify a whitelist of known global variables that you _expect_ to leak into global scope. +### `--posix-exit-codes` + +Exits with standard POSIX exit codes instead of the number of failed tests. + +Those exit codes are: + +- `0`: if all tests passed +- `1`: if any test failed +- `128 + ` if given a signal, such as: + - 134: `SIGABRT` (`128 + 6`) + - 143: `SIGTERM` (`128 + 15`) + ### `--retries ` Retries failed tests `n` times. diff --git a/lib/cli/run-option-metadata.js b/lib/cli/run-option-metadata.js index 492608fbdd..91a0282e7b 100644 --- a/lib/cli/run-option-metadata.js +++ b/lib/cli/run-option-metadata.js @@ -45,6 +45,7 @@ const TYPES = (exports.types = { 'list-reporters', 'no-colors', 'parallel', + 'posix-exit-codes', 'recursive', 'sort', 'watch' diff --git a/lib/cli/run.js b/lib/cli/run.js index 66c8cbbb66..dc10d24fd9 100644 --- a/lib/cli/run.js +++ b/lib/cli/run.js @@ -190,6 +190,10 @@ exports.builder = yargs => description: 'Run tests in parallel', group: GROUPS.RULES }, + 'posix-exit-codes': { + description: 'Use POSIX and UNIX shell exit codes as Mocha\'s return value', + group: GROUPS.RULES + }, recursive: { description: 'Look for tests in subdirectories', group: GROUPS.FILES diff --git a/package.json b/package.json index ed5d6ea47e..064f392a2c 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "log-symbols": "4.1.0", "minimatch": "5.0.1", "ms": "2.1.3", - "serialize-javascript": "6.0.0", + "serialize-javascript": "^6.0.2", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "workerpool": "6.2.1", diff --git a/test/integration/fixtures/failing.fixture.js b/test/integration/fixtures/failing.fixture.js new file mode 100644 index 0000000000..337d628991 --- /dev/null +++ b/test/integration/fixtures/failing.fixture.js @@ -0,0 +1,23 @@ +'use strict'; + +// One passing test and three failing tests + +var assert = require('assert'); + +describe('suite', function () { + it('test1', function () { + assert(true); + }); + + it('test2', function () { + assert(false); + }); + + it('test3', function () { + assert(false); + }); + + it('test4', function () { + assert(false); + }); +}); diff --git a/test/integration/fixtures/signals-sigabrt.fixture.js b/test/integration/fixtures/signals-sigabrt.fixture.js new file mode 100644 index 0000000000..037111097b --- /dev/null +++ b/test/integration/fixtures/signals-sigabrt.fixture.js @@ -0,0 +1,7 @@ +'use strict'; + +describe('signal suite', function () { + it('test SIGABRT', function () { + process.kill(process.pid, 'SIGABRT'); + }); +}); diff --git a/test/integration/fixtures/signals-sigterm.fixture.js b/test/integration/fixtures/signals-sigterm.fixture.js new file mode 100644 index 0000000000..2f1d01c700 --- /dev/null +++ b/test/integration/fixtures/signals-sigterm.fixture.js @@ -0,0 +1,7 @@ +'use strict'; + +describe('signal suite', function () { + it('test SIGTERM', function () { + process.kill(process.pid, 'SIGTERM'); + }); +}); diff --git a/test/integration/options/posixExitCodes.spec.js b/test/integration/options/posixExitCodes.spec.js new file mode 100644 index 0000000000..5fd82f1951 --- /dev/null +++ b/test/integration/options/posixExitCodes.spec.js @@ -0,0 +1,65 @@ +'use strict'; + +var helpers = require('../helpers'); +var runMocha = helpers.runMocha; +var os = require('os'); + +const EXIT_SUCCESS = 0; +const EXIT_FAILURE = 1; +const SIGNAL_OFFSET = 128; + +describe('--posix-exit-codes', function () { + if (os.platform() !== 'win32') { // SIGTERM is not supported on Windows + describe('when enabled, and with node options', function () { + var args = ['--no-warnings', '--posix-exit-codes']; + + it('should exit with correct POSIX shell code on SIGABRT', function (done) { + var fixture = 'signals-sigabrt.fixture.js'; + runMocha(fixture, args, function postmortem(err, res) { + if (err) { + return done(err); + } + expect(res.code, 'to be', SIGNAL_OFFSET + os.constants.signals.SIGABRT); + done(); + }); + }); + + it('should exit with correct POSIX shell code on SIGTERM', function (done) { + var fixture = 'signals-sigterm.fixture.js'; + runMocha(fixture, args, function postmortem(err, res) { + if (err) { + return done(err); + } + expect(res.code, 'to be', SIGNAL_OFFSET + os.constants.signals.SIGTERM); + done(); + }); + }); + + it('should exit with code 1 if there are test failures', function (done) { + var fixture = 'failing.fixture.js'; + runMocha(fixture, args, function postmortem(err, res) { + if (err) { + return done(err); + } + expect(res.code, 'to be', EXIT_FAILURE); + done(); + }); + }); + }); + } + + describe('when not enabled, and with node options', function () { + it('should exit with the number of failed tests', function (done) { + var fixture = 'failing.fixture.js'; // contains three failing tests + var numFailures = 3; + var args = ['--no-warnings']; + runMocha(fixture, args, function postmortem(err, res) { + if (err) { + return done(err); + } + expect(res.code, 'to be', numFailures); + done(); + }); + }); + }); +});