diff --git a/bin/mocha.js b/bin/mocha.js old mode 100644 new mode 100755 index 8970d1bdd6..d6359cd348 --- a/bin/mocha.js +++ b/bin/mocha.js @@ -10,6 +10,7 @@ * @private */ +const os = require('node:os'); const {loadOptions} = require('../lib/cli/options'); const { unparseNodeFlags, @@ -22,6 +23,7 @@ const {aliases} = require('../lib/cli/run-option-metadata'); const mochaArgs = {}; const nodeArgs = {}; +const SIGNAL_OFFSET = 128; let hasInspect = false; const opts = loadOptions(process.argv.slice(2)); @@ -109,9 +111,13 @@ if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) { proc.on('exit', (code, signal) => { process.on('exit', () => { if (signal) { + signal = typeof signal === 'string' ? os.constants.signals[signal] : signal; + if (mochaArgs['posix-exit-codes'] === true) { + process.exitCode = SIGNAL_OFFSET + signal; + } process.kill(process.pid, signal); } else { - process.exit(code); + process.exit(Math.min(code, mochaArgs['posix-exit-codes'] ? 1 : 255)); } }); }); @@ -126,7 +132,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('node: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 4f2ff4cb72..c42baab2c2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -963,6 +963,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 a7e631b778..9939697417 100644 --- a/lib/cli/run-helpers.js +++ b/lib/cli/run-helpers.js @@ -27,7 +27,7 @@ const {UnmatchedFile} = require('./collect-files'); */ const exitMochaLater = clampedCode => { process.on('exit', () => { - process.exitCode = clampedCode; + process.exitCode = Math.min(clampedCode, process.argv.includes('--posix-exit-codes') ? 1 : 255); }); }; @@ -39,6 +39,8 @@ const exitMochaLater = clampedCode => { * @private */ const exitMocha = clampedCode => { + const usePosixExitCodes = process.argv.includes('--posix-exit-codes'); + clampedCode = Math.min(clampedCode, usePosixExitCodes ? 1 : 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 df967097a7..de553ef92c 100644 --- a/lib/cli/run-option-metadata.js +++ b/lib/cli/run-option-metadata.js @@ -46,6 +46,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 5f42c5082b..561592b916 100644 --- a/lib/cli/run.js +++ b/lib/cli/run.js @@ -195,6 +195,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/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-numeric.fixture.js b/test/integration/fixtures/signals-sigterm-numeric.fixture.js new file mode 100644 index 0000000000..a396e1264a --- /dev/null +++ b/test/integration/fixtures/signals-sigterm-numeric.fixture.js @@ -0,0 +1,8 @@ +'use strict'; +const os = require('node:os'); + +describe('signal suite', function () { + it('test SIGTERM', function () { + process.kill(process.pid, os.constants.signals['SIGTERM']); + }); +}); 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 16a2ed8975..a6ab867abe 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -8,6 +8,7 @@ const {format} = require('node:util'); const path = require('node:path'); const Base = require('../../lib/reporters/base'); const debug = require('debug')('mocha:test:integration:helpers'); +const SIGNAL_OFFSET = 128; /** * Path to `mocha` executable @@ -358,6 +359,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..5109933957 --- /dev/null +++ b/test/integration/options/posixExitCodes.spec.js @@ -0,0 +1,172 @@ +'use strict'; + +var helpers = require('../helpers'); +var runMocha = helpers.runMocha; +var os = require('node:os'); + +const EXIT_SUCCESS = 0; +const EXIT_FAILURE = 1; +const SIGNAL_OFFSET = 128; + +describe('--posix-exit-codes', function () { + if (os.platform() !== 'win32') { + 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 the correct POSIX shell code on numeric fatal signal', function (done) { + // not supported on Windows + if (os.platform() !== 'win32') { + var fixture = 'signals-sigterm-numeric.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 () { + describe('when mocha is run as a child process', () => { + // any node-specific option makes mocha run as a child process + var args = ['--no-warnings']; + + it('should exit with the number of failed tests', function (done) { + var fixture = 'failing.fixture.js'; + var numFailures = 3; + 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 number of failed tests', function (done) { + var fixture = 'failing.fixture.js'; + var numFailures = 3; + runMocha(fixture, args, function postmortem(err, res) { + if (err) { + return done(err); + } + expect(res.code, 'to be', numFailures); + done(); + }); + }); + }); + }); + } +});