Skip to content

Commit 9d8aba1

Browse files
committed
CLI: Avoid MaxListeners warning in long-running watch mode
The three process event handlers all stateless and can be re-used and naturally find the latest 'QUnit' instance in lexical scope. I considered attaching these once early on, and then leaving them there across restarts. However, this caused certain early failures to become silent, such as the test for `qunit single.js --require does-not-exist`, because once you attach 'uncaughtException', Node.js removes its own implicit default handler that prints the error and sets exit code 1, and we're able to print the error yet that early as the reporters are not yet set up yet at that point. Instead, we can keep the timing of when they are attached as-is and remove them again as-needed during watch/restart/cleanupQUnit. Fixes #1692.
1 parent bc1cc38 commit 9d8aba1

File tree

1 file changed

+42
-32
lines changed

1 file changed

+42
-32
lines changed

src/cli/run.js

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ const DEBOUNCE_RESTART_LENGTH = 200 - DEBOUNCE_WATCH_LENGTH;
1414
const changedPendingPurge = [];
1515

1616
let QUnit;
17+
let running = false;
18+
let restartDebounceTimer;
19+
20+
function onUnhandledRejection (reason, _promise) {
21+
QUnit.onUncaughtException(reason);
22+
}
23+
function onUncaughtException (error, _origin) {
24+
QUnit.onUncaughtException(error);
25+
}
26+
function onExit () {
27+
if (running) {
28+
console.error('Error: Process exited before tests finished running');
29+
30+
const currentTest = QUnit.config.current;
31+
if (currentTest && currentTest.pauses.size > 0) {
32+
const name = currentTest.testName;
33+
console.error('Last test to run (' + name + ') has an async hold. ' +
34+
'Ensure all assert.async() callbacks are invoked and Promises resolve. ' +
35+
'You should also set a standard timeout via QUnit.config.testTimeout.');
36+
}
37+
}
38+
}
1739

1840
async function run (args, options) {
1941
// Default to non-zero exit code to avoid false positives
@@ -105,27 +127,11 @@ async function run (args, options) {
105127
}
106128

107129
// Handle the unhandled
108-
process.on('unhandledRejection', (reason, _promise) => {
109-
QUnit.onUncaughtException(reason);
110-
});
111-
process.on('uncaughtException', (error, _origin) => {
112-
QUnit.onUncaughtException(error);
113-
});
130+
process.on('unhandledRejection', onUnhandledRejection);
131+
process.on('uncaughtException', onUncaughtException);
114132

115-
let running = true;
116-
process.on('exit', function () {
117-
if (running) {
118-
console.error('Error: Process exited before tests finished running');
119-
120-
const currentTest = QUnit.config.current;
121-
if (currentTest && currentTest.pauses.size > 0) {
122-
const name = currentTest.testName;
123-
console.error('Last test to run (' + name + ') has an async hold. ' +
124-
'Ensure all assert.async() callbacks are invoked and Promises resolve. ' +
125-
'You should also set a standard timeout via QUnit.config.testTimeout.');
126-
}
127-
}
128-
});
133+
running = true;
134+
process.on('exit', onExit);
129135

130136
QUnit.on('error', function (_error) {
131137
// Set exitCode directly, to make sure it is set to fail even if "runEnd" will never be
@@ -146,10 +152,10 @@ async function run (args, options) {
146152
QUnit.start();
147153
}
148154

149-
run.restart = function (args) {
150-
clearTimeout(this._restartDebounceTimer);
155+
run.restart = function restart (args, options) {
156+
clearTimeout(restartDebounceTimer);
151157

152-
this._restartDebounceTimer = setTimeout(() => {
158+
restartDebounceTimer = setTimeout(() => {
153159
changedPendingPurge.forEach(file => delete require.cache[path.resolve(file)]);
154160
changedPendingPurge.length = 0;
155161

@@ -159,12 +165,17 @@ run.restart = function (args) {
159165
console.log('Restarting...');
160166
}
161167

162-
run.abort(() => run.apply(null, args));
168+
abort(() => run(args, options));
163169
}, DEBOUNCE_RESTART_LENGTH);
164170
};
165171

166-
run.abort = function (callback) {
172+
function abort (callback) {
167173
function clearQUnit () {
174+
process.off('unhandledRejection', onUnhandledRejection);
175+
process.off('uncaughtException', onUncaughtException);
176+
process.off('exit', onExit);
177+
running = false;
178+
168179
delete global.QUnit;
169180
QUnit = null;
170181
if (callback) {
@@ -179,11 +190,10 @@ run.abort = function (callback) {
179190
} else {
180191
clearQUnit();
181192
}
182-
};
193+
}
183194

184-
run.watch = function watch (_, options) {
195+
run.watch = function watch (args, options) {
185196
const watch = require('node-watch');
186-
const args = Array.prototype.slice.call(arguments);
187197
const baseDir = process.cwd();
188198

189199
QUnit = requireQUnit();
@@ -206,7 +216,7 @@ run.watch = function watch (_, options) {
206216
persistent: true,
207217
recursive: true,
208218

209-
// Bare minimum delay, we have another debounce in run.restart().
219+
// Bare minimum delay, we have another debounce in restart().
210220
delay: DEBOUNCE_WATCH_LENGTH,
211221
filter: (fullpath, skip) => {
212222
if (/\/node_modules\//.test(fullpath) ||
@@ -219,18 +229,18 @@ run.watch = function watch (_, options) {
219229
}, (event, fullpath) => {
220230
console.log(`File ${event}: ${path.relative(baseDir, fullpath)}`);
221231
changedPendingPurge.push(fullpath);
222-
run.restart(args);
232+
run.restart(args, options);
223233
});
224234

225235
watcher.on('ready', () => {
226-
run.apply(null, args);
236+
run(args, options);
227237
});
228238

229239
function stop () {
230240
console.log('Stopping QUnit...');
231241

232242
watcher.close();
233-
run.abort(() => {
243+
abort(() => {
234244
process.exit();
235245
});
236246
}

0 commit comments

Comments
 (0)