Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b419072
Option to use posix exit code upon fatal signal
73rhodes May 23, 2023
7b36d1e
Update test/integration/options/posixExitCodes.spec.js
73rhodes Mar 15, 2024
702ec53
Address PR review comments mocha org
73rhodes Mar 18, 2024
cb2ba84
Address PR review comments mocha org
73rhodes Mar 18, 2024
d000c32
Update docs/index.md
73rhodes Mar 25, 2024
859abe6
Fix exit code when no signal caught
73rhodes Apr 11, 2024
34e196d
Coverage for posix exit code with normally failing tests
73rhodes Apr 11, 2024
7c85e3a
Update docs/index.md
73rhodes Apr 15, 2024
abdd3d5
Address PR comments; use os.constants
73rhodes Apr 15, 2024
e94876f
Remove test that asserts the problematic behavior
73rhodes Apr 23, 2024
169927a
Omit win32 from signals test suite
73rhodes Apr 26, 2024
73259a2
Update docs/index.md
73rhodes May 1, 2024
5938f49
WIP - support posix-exit-codes option for both child-process and in-p…
73rhodes Jul 9, 2024
b155129
Update bin/mocha.js
73rhodes Apr 23, 2025
5af8365
Update lib/cli/run-helpers.js
73rhodes Apr 23, 2025
d09c2bc
Update lib/cli/run-helpers.js
73rhodes Apr 23, 2025
9c394f0
Update lib/cli/run-helpers.js
73rhodes Apr 23, 2025
bfe7388
Use preferred node import syntax
73rhodes Apr 23, 2025
b3bc5c5
Removed unused constants
73rhodes Apr 23, 2025
f365cbc
update run-helpers.js: Fix variable name
73rhodes Apr 23, 2025
f9ae3f5
Simplification of child process exit handlers
73rhodes Apr 23, 2025
8b3f2e3
Add test case for numerical signal
73rhodes Apr 23, 2025
7e0efd2
Update signal-handling tests; remove unsupported cases
73rhodes May 6, 2025
55cbd8b
Merge pull request #13 from zendesk/dderidder/unit-tests
73rhodes May 6, 2025
6613a02
Don't test for posix signals on windows
73rhodes May 6, 2025
7347748
Update lib/cli/run-helpers.js
73rhodes May 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions bin/mocha.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* @private
*/

const os = require('node:os');
const {loadOptions} = require('../lib/cli/options');
const {
unparseNodeFlags,
Expand All @@ -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));
Expand Down Expand Up @@ -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));
}
});
});
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 + <signal>` if given a signal, such as:
- 134: `SIGABRT` (`128 + 6`)
- 143: `SIGTERM` (`128 + 15`)

### `--retries <n>`

Retries failed tests `n` times.
Expand Down
4 changes: 3 additions & 1 deletion lib/cli/run-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link

Copilot AI Jun 12, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider extracting the check for '--posix-exit-codes' into a dedicated helper variable or function to improve readability and avoid repetition.

Suggested change
process.exitCode = Math.min(clampedCode, process.argv.includes('--posix-exit-codes') ? 1 : 255);
process.exitCode = Math.min(clampedCode, usePosixExitCodes() ? 1 : 255);

Copilot uses AI. Check for mistakes.
});
};

Expand All @@ -39,6 +39,8 @@ const exitMochaLater = clampedCode => {
* @private
*/
const exitMocha = clampedCode => {
const usePosixExitCodes = process.argv.includes('--posix-exit-codes');
Copy link

Copilot AI Jun 12, 2025

Choose a reason for hiding this comment

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

[nitpick] Similar flag-check logic appears in multiple places; consider consolidating this behavior into a single helper function for consistency and easier future maintenance.

Suggested change
const usePosixExitCodes = process.argv.includes('--posix-exit-codes');
const usePosixExitCodes = shouldUsePosixExitCodes();

Copilot uses AI. Check for mistakes.
clampedCode = Math.min(clampedCode, usePosixExitCodes ? 1 : 255);
let draining = 0;

// Eagerly set the process's exit code in case stream.write doesn't
Expand Down
1 change: 1 addition & 0 deletions lib/cli/run-option-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const TYPES = (exports.types = {
'list-reporters',
'no-colors',
'parallel',
'posix-exit-codes',
'recursive',
'sort',
'watch'
Expand Down
4 changes: 4 additions & 0 deletions lib/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions test/integration/fixtures/failing.fixture.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
7 changes: 7 additions & 0 deletions test/integration/fixtures/signals-sigabrt.fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

describe('signal suite', function () {
it('test SIGABRT', function () {
process.kill(process.pid, 'SIGABRT');
});
});
8 changes: 8 additions & 0 deletions test/integration/fixtures/signals-sigterm-numeric.fixture.js
Original file line number Diff line number Diff line change
@@ -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']);
});
});
7 changes: 7 additions & 0 deletions test/integration/fixtures/signals-sigterm.fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

describe('signal suite', function () {
it('test SIGTERM', function () {
process.kill(process.pid, 'SIGTERM');
});
});
13 changes: 13 additions & 0 deletions test/integration/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
172 changes: 172 additions & 0 deletions test/integration/options/posixExitCodes.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
}
});
Loading