Skip to content

Commit 91bbf85

Browse files
73rhodesJoshuaKGoldbergvoxpelli
authored
feat: add option to use posix exit code upon fatal signal (#4989)
Co-authored-by: Josh Goldberg ✨ <[email protected]> Co-authored-by: Pelle Wessman <[email protected]>
1 parent 6c96545 commit 91bbf85

11 files changed

+258
-3
lines changed

bin/mocha.js

100644100755
+8-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* @private
1111
*/
1212

13+
const os = require('node:os');
1314
const {loadOptions} = require('../lib/cli/options');
1415
const {
1516
unparseNodeFlags,
@@ -22,6 +23,7 @@ const {aliases} = require('../lib/cli/run-option-metadata');
2223

2324
const mochaArgs = {};
2425
const nodeArgs = {};
26+
const SIGNAL_OFFSET = 128;
2527
let hasInspect = false;
2628

2729
const opts = loadOptions(process.argv.slice(2));
@@ -109,9 +111,13 @@ if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) {
109111
proc.on('exit', (code, signal) => {
110112
process.on('exit', () => {
111113
if (signal) {
114+
signal = typeof signal === 'string' ? os.constants.signals[signal] : signal;
115+
if (mochaArgs['posix-exit-codes'] === true) {
116+
process.exitCode = SIGNAL_OFFSET + signal;
117+
}
112118
process.kill(process.pid, signal);
113119
} else {
114-
process.exit(code);
120+
process.exit(Math.min(code, mochaArgs['posix-exit-codes'] ? 1 : 255));
115121
}
116122
});
117123
});
@@ -126,7 +132,7 @@ if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) {
126132
// be needed.
127133
if (!args.parallel || args.jobs < 2) {
128134
// win32 does not support SIGTERM, so use next best thing.
129-
if (require('node:os').platform() === 'win32') {
135+
if (os.platform() === 'win32') {
130136
proc.kill('SIGKILL');
131137
} else {
132138
// using SIGKILL won't cleanly close the output streams, which can result

docs/index.md

+12
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,18 @@ Define a global variable name. For example, suppose your app deliberately expose
963963

964964
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.
965965

966+
### `--posix-exit-codes`
967+
968+
Exits with standard POSIX exit codes instead of the number of failed tests.
969+
970+
Those exit codes are:
971+
972+
- `0`: if all tests passed
973+
- `1`: if any test failed
974+
- `128 + <signal>` if given a signal, such as:
975+
- 134: `SIGABRT` (`128 + 6`)
976+
- 143: `SIGTERM` (`128 + 15`)
977+
966978
### `--retries <n>`
967979

968980
Retries failed tests `n` times.

lib/cli/run-helpers.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const {UnmatchedFile} = require('./collect-files');
2727
*/
2828
const exitMochaLater = clampedCode => {
2929
process.on('exit', () => {
30-
process.exitCode = clampedCode;
30+
process.exitCode = Math.min(clampedCode, process.argv.includes('--posix-exit-codes') ? 1 : 255);
3131
});
3232
};
3333

@@ -39,6 +39,8 @@ const exitMochaLater = clampedCode => {
3939
* @private
4040
*/
4141
const exitMocha = clampedCode => {
42+
const usePosixExitCodes = process.argv.includes('--posix-exit-codes');
43+
clampedCode = Math.min(clampedCode, usePosixExitCodes ? 1 : 255);
4244
let draining = 0;
4345

4446
// Eagerly set the process's exit code in case stream.write doesn't

lib/cli/run-option-metadata.js

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const TYPES = (exports.types = {
4646
'list-reporters',
4747
'no-colors',
4848
'parallel',
49+
'posix-exit-codes',
4950
'recursive',
5051
'sort',
5152
'watch'

lib/cli/run.js

+4
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ exports.builder = yargs =>
195195
description: 'Run tests in parallel',
196196
group: GROUPS.RULES
197197
},
198+
'posix-exit-codes': {
199+
description: 'Use POSIX and UNIX shell exit codes as Mocha\'s return value',
200+
group: GROUPS.RULES
201+
},
198202
recursive: {
199203
description: 'Look for tests in subdirectories',
200204
group: GROUPS.FILES
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
// One passing test and three failing tests
4+
5+
var assert = require('assert');
6+
7+
describe('suite', function () {
8+
it('test1', function () {
9+
assert(true);
10+
});
11+
12+
it('test2', function () {
13+
assert(false);
14+
});
15+
16+
it('test3', function () {
17+
assert(false);
18+
});
19+
20+
it('test4', function () {
21+
assert(false);
22+
});
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
describe('signal suite', function () {
4+
it('test SIGABRT', function () {
5+
process.kill(process.pid, 'SIGABRT');
6+
});
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
const os = require('node:os');
3+
4+
describe('signal suite', function () {
5+
it('test SIGTERM', function () {
6+
process.kill(process.pid, os.constants.signals['SIGTERM']);
7+
});
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
describe('signal suite', function () {
4+
it('test SIGTERM', function () {
5+
process.kill(process.pid, 'SIGTERM');
6+
});
7+
});

test/integration/helpers.js

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {format} = require('node:util');
88
const path = require('node:path');
99
const Base = require('../../lib/reporters/base');
1010
const debug = require('debug')('mocha:test:integration:helpers');
11+
const SIGNAL_OFFSET = 128;
1112

1213
/**
1314
* Path to `mocha` executable
@@ -358,6 +359,18 @@ function createSubprocess(args, done, opts = {}) {
358359
});
359360
});
360361

362+
/**
363+
* Emulate node's exit code for fatal signal. Allows tests to see the same
364+
* exit code as the mocha cli.
365+
*/
366+
mocha.on('exit', (code, signal) => {
367+
if (signal) {
368+
mocha.exitCode =
369+
SIGNAL_OFFSET +
370+
(typeof signal == 'string' ? os.constants.signals[signal] : signal);
371+
}
372+
});
373+
361374
return mocha;
362375
}
363376

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
'use strict';
2+
3+
var helpers = require('../helpers');
4+
var runMocha = helpers.runMocha;
5+
var os = require('node:os');
6+
7+
const EXIT_SUCCESS = 0;
8+
const EXIT_FAILURE = 1;
9+
const SIGNAL_OFFSET = 128;
10+
11+
describe('--posix-exit-codes', function () {
12+
if (os.platform() !== 'win32') {
13+
describe('when enabled', function () {
14+
describe('when mocha is run as a child process', () => {
15+
// 'no-warnings' node option makes mocha run as a child process
16+
const args = ['--no-warnings', '--posix-exit-codes'];
17+
18+
it('should exit with correct POSIX shell code on SIGABRT', function (done) {
19+
var fixture = 'signals-sigabrt.fixture.js';
20+
runMocha(fixture, args, function postmortem(err, res) {
21+
if (err) {
22+
return done(err);
23+
}
24+
expect(
25+
res.code,
26+
'to be',
27+
SIGNAL_OFFSET + os.constants.signals.SIGABRT
28+
);
29+
done();
30+
});
31+
});
32+
33+
it('should exit with correct POSIX shell code on SIGTERM', function (done) {
34+
// SIGTERM is not supported on Windows
35+
if (os.platform() !== 'win32') {
36+
var fixture = 'signals-sigterm.fixture.js';
37+
runMocha(fixture, args, function postmortem(err, res) {
38+
if (err) {
39+
return done(err);
40+
}
41+
expect(
42+
res.code,
43+
'to be',
44+
SIGNAL_OFFSET + os.constants.signals.SIGTERM
45+
);
46+
done();
47+
});
48+
} else {
49+
done();
50+
}
51+
});
52+
53+
it('should exit with the correct POSIX shell code on numeric fatal signal', function (done) {
54+
// not supported on Windows
55+
if (os.platform() !== 'win32') {
56+
var fixture = 'signals-sigterm-numeric.fixture.js';
57+
runMocha(fixture, args, function postmortem(err, res) {
58+
if (err) {
59+
return done(err);
60+
}
61+
expect(
62+
res.code,
63+
'to be',
64+
SIGNAL_OFFSET + os.constants.signals.SIGTERM
65+
);
66+
done();
67+
});
68+
} else {
69+
done();
70+
}
71+
});
72+
73+
it('should exit with code 1 if there are test failures', function (done) {
74+
var fixture = 'failing.fixture.js';
75+
runMocha(fixture, args, function postmortem(err, res) {
76+
if (err) {
77+
return done(err);
78+
}
79+
expect(res.code, 'to be', EXIT_FAILURE);
80+
done();
81+
});
82+
});
83+
});
84+
85+
describe('when mocha is run in-process', () => {
86+
// Without node-specific cli options, mocha runs in-process
87+
const args = ['--posix-exit-codes'];
88+
89+
it('should exit with the correct POSIX shell code on SIGABRT', function (done) {
90+
var fixture = 'signals-sigabrt.fixture.js';
91+
runMocha(fixture, args, function postmortem(err, res) {
92+
if (err) {
93+
return done(err);
94+
}
95+
expect(
96+
res.code,
97+
'to be',
98+
SIGNAL_OFFSET + os.constants.signals.SIGABRT
99+
);
100+
done();
101+
});
102+
});
103+
104+
it('should exit with the correct POSIX shell code on SIGTERM', function (done) {
105+
// SIGTERM is not supported on Windows
106+
if (os.platform() !== 'win32') {
107+
var fixture = 'signals-sigterm.fixture.js';
108+
runMocha(fixture, args, function postmortem(err, res) {
109+
if (err) {
110+
return done(err);
111+
}
112+
expect(
113+
res.code,
114+
'to be',
115+
SIGNAL_OFFSET + os.constants.signals.SIGTERM
116+
);
117+
done();
118+
});
119+
} else {
120+
done();
121+
}
122+
});
123+
124+
it('should exit with code 1 if there are test failures', function (done) {
125+
var fixture = 'failing.fixture.js';
126+
runMocha(fixture, args, function postmortem(err, res) {
127+
if (err) {
128+
return done(err);
129+
}
130+
expect(res.code, 'to be', EXIT_FAILURE);
131+
done();
132+
});
133+
});
134+
});
135+
});
136+
137+
describe('when not enabled', function () {
138+
describe('when mocha is run as a child process', () => {
139+
// any node-specific option makes mocha run as a child process
140+
var args = ['--no-warnings'];
141+
142+
it('should exit with the number of failed tests', function (done) {
143+
var fixture = 'failing.fixture.js';
144+
var numFailures = 3;
145+
runMocha(fixture, args, function postmortem(err, res) {
146+
if (err) {
147+
return done(err);
148+
}
149+
expect(res.code, 'to be', numFailures);
150+
done();
151+
});
152+
});
153+
});
154+
155+
describe('when mocha is run in-process', () => {
156+
var args = [];
157+
158+
it('should exit with the number of failed tests', function (done) {
159+
var fixture = 'failing.fixture.js';
160+
var numFailures = 3;
161+
runMocha(fixture, args, function postmortem(err, res) {
162+
if (err) {
163+
return done(err);
164+
}
165+
expect(res.code, 'to be', numFailures);
166+
done();
167+
});
168+
});
169+
});
170+
});
171+
}
172+
});

0 commit comments

Comments
 (0)