diff --git a/bin/mocha.js b/bin/mocha.js old mode 100644 new mode 100755 index fb58e65fcd..5fdb3209c1 --- 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-helpers.js b/lib/cli/run-helpers.js index 078ca7e434..1e17fa16c1 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -16,6 +16,7 @@ const {format} = require('util'); const {createInvalidLegacyPluginError} = require('../errors'); const {requireOrImport} = require('../nodejs/esm-utils'); const PluginLoader = require('../plugin-loader'); +const EXIT_FAILURE = 1; /** * Exits Mocha when tests + code under test has finished execution (default) @@ -25,7 +26,10 @@ const PluginLoader = require('../plugin-loader'); */ const exitMochaLater = code => { process.on('exit', () => { - process.exitCode = Math.min(code, 255); + const usePosixExitCodes = process.argv.includes('--posix-exit-codes'); + const exitCode = + usePosixExitCodes && code > 0 ? EXIT_FAILURE : Math.min(code, 255); + process.exitCode = exitCode; }); }; @@ -37,7 +41,9 @@ const exitMochaLater = code => { * @private */ const exitMocha = code => { - const clampedCode = Math.min(code, 255); + const usePosixExitCodes = process.argv.includes('--posix-exit-codes'); + const clampedCode = + usePosixExitCodes && code > 0 ? EXIT_FAILURE : Math.min(code, 255); let draining = 0; // Eagerly set the process's exit code in case stream.write doesn't 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/helpers.js b/test/integration/helpers.js index 61c6ec01ca..2aea56c624 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -7,6 +7,7 @@ const {format} = require('util'); const path = require('path'); const Base = require('../../lib/reporters/base'); const debug = require('debug')('mocha:test:integration:helpers'); +const SIGNAL_OFFSET = 128; /** * Path to `mocha` executable @@ -357,6 +358,18 @@ function createSubprocess(args, done, opts = {}) { }); }); + /** + * Emulate node's exit code for fatal signal. Allows tests to see the same + * exit code as the mocha cli. + */ + mocha.on('exit', (code, signal) => { + if (signal) { + mocha.exitCode = + SIGNAL_OFFSET + + (typeof signal == 'string' ? os.constants.signals[signal] : signal); + } + }); + return mocha; } diff --git a/test/integration/options/posixExitCodes.spec.js b/test/integration/options/posixExitCodes.spec.js new file mode 100644 index 0000000000..fcf1fa09d7 --- /dev/null +++ b/test/integration/options/posixExitCodes.spec.js @@ -0,0 +1,218 @@ +'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 () { + describe('when enabled', function () { + describe('when mocha is run as a child process', () => { + // 'no-warnings' node option makes mocha run as a child process + const 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) { + // SIGTERM is not supported on Windows + if (os.platform() !== 'win32') { + 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(); + }); + } else { + 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 mocha is run in-process', () => { + // Without node-specific cli options, mocha runs in-process + const args = ['--posix-exit-codes']; + + it('should exit with the 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 the correct POSIX shell code on SIGTERM', function (done) { + // SIGTERM is not supported on Windows + if (os.platform() !== 'win32') { + 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(); + }); + } else { + 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', function () { + const fixture = 'failing.fixture.js'; // contains three failing tests + const numFailures = 3; + + describe('when mocha is run as a child process', () => { + // 'no-warnings' node option makes mocha run as a child process + var args = ['--no-warnings']; + + it('should exit with the 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 the correct POSIX shell code on SIGTERM', function (done) { + // SIGTERM is not supported on Windows + if (os.platform() !== 'win32') { + 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(); + }); + } else { + done(); + } + }); + + it('should exit with the number of failed tests', function (done) { + runMocha(fixture, args, function postmortem(err, res) { + if (err) { + return done(err); + } + expect(res.code, 'to be', numFailures); + done(); + }); + }); + }); + + describe('when mocha is run in-process', () => { + var args = []; + it('should exit with the 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 the correct POSIX shell code on SIGTERM', function (done) { + // SIGTERM is not supported on Windows + if (os.platform() !== 'win32') { + 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(); + }); + } else { + done(); + } + }); + + it('should exit with the number of failed tests', function (done) { + runMocha(fixture, args, function postmortem(err, res) { + if (err) { + return done(err); + } + expect(res.code, 'to be', numFailures); + done(); + }); + }); + }); + }); +});