Skip to content

Commit 3078892

Browse files
committed
Send options to worker process over IPC
In large projects, the options may be too big to be passed through the process arguments. Fixes #2032.
1 parent 010914b commit 3078892

File tree

5 files changed

+122
-105
lines changed

5 files changed

+122
-105
lines changed

lib/fork.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ module.exports = (file, opts, execArgv) => {
5050
}
5151
}, opts);
5252

53-
const args = [JSON.stringify(opts), opts.color ? '--color' : '--no-color'].concat(opts.workerArgv);
53+
const args = [opts.color ? '--color' : '--no-color'].concat(opts.workerArgv);
5454

5555
const subprocess = childProcess.fork(workerPath, args, {
5656
cwd: opts.projectDir,
@@ -85,6 +85,11 @@ module.exports = (file, opts, execArgv) => {
8585
return;
8686
}
8787

88+
if (message.ava.type === 'ready-for-options') {
89+
send({type: 'options', options: opts});
90+
return;
91+
}
92+
8893
if (message.ava.type === 'ping') {
8994
send({type: 'pong'});
9095
} else {

lib/worker/consume-argv.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
'use strict';
2-
require('./options').set(JSON.parse(process.argv[2]));
3-
42
// Remove arguments received from fork.js and leave those specified by the user.
5-
process.argv.splice(2, 2);
3+
process.argv.splice(2, 1);

lib/worker/ipc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ process.on('message', message => {
1212
}
1313

1414
switch (message.ava.type) {
15+
case 'options':
16+
emitter.emit('options', message.ava.options);
17+
break;
1518
case 'peer-failed':
1619
emitter.emit('peerFailed');
1720
break;
@@ -23,6 +26,7 @@ process.on('message', message => {
2326
}
2427
});
2528

29+
exports.options = emitter.once('options');
2630
exports.peerFailed = emitter.once('peerFailed');
2731

2832
function send(evt) {

lib/worker/subprocess.js

Lines changed: 108 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -5,126 +5,138 @@ const currentlyUnhandled = require('currently-unhandled')();
55
require('./ensure-forked');
66
require('./load-chalk');
77
require('./consume-argv');
8-
require('./fake-tty');
98
/* eslint-enable import/no-unassigned-import */
109

11-
const nowAndTimers = require('../now-and-timers');
12-
const Runner = require('../runner');
13-
const serializeError = require('../serialize-error');
14-
const dependencyTracking = require('./dependency-tracker');
1510
const ipc = require('./ipc');
16-
const options = require('./options').get();
17-
const precompilerHook = require('./precompiler-hook');
1811

19-
function exit(code) {
20-
if (!process.exitCode) {
21-
process.exitCode = code;
22-
}
12+
ipc.send({type: 'ready-for-options'});
13+
ipc.options.then(options => {
14+
require('./options').set(options);
15+
require('./fake-tty'); // eslint-disable-line import/no-unassigned-import
2316

24-
dependencyTracking.flush();
25-
return ipc.flush().then(() => process.exit()); // eslint-disable-line unicorn/no-process-exit
26-
}
27-
28-
const runner = new Runner({
29-
failFast: options.failFast,
30-
failWithoutAssertions: options.failWithoutAssertions,
31-
file: options.file,
32-
match: options.match,
33-
projectDir: options.projectDir,
34-
runOnlyExclusive: options.runOnlyExclusive,
35-
serial: options.serial,
36-
snapshotDir: options.snapshotDir,
37-
updateSnapshots: options.updateSnapshots
38-
});
17+
const nowAndTimers = require('../now-and-timers');
18+
const Runner = require('../runner');
19+
const serializeError = require('../serialize-error');
20+
const dependencyTracking = require('./dependency-tracker');
21+
const precompilerHook = require('./precompiler-hook');
3922

40-
ipc.peerFailed.then(() => {
41-
runner.interrupt();
42-
});
23+
function exit(code) {
24+
if (!process.exitCode) {
25+
process.exitCode = code;
26+
}
4327

44-
const attributedRejections = new Set();
45-
process.on('unhandledRejection', (reason, promise) => {
46-
if (runner.attributeLeakedError(reason)) {
47-
attributedRejections.add(promise);
28+
dependencyTracking.flush();
29+
return ipc.flush().then(() => process.exit()); // eslint-disable-line unicorn/no-process-exit
4830
}
49-
});
5031

51-
runner.on('dependency', dependencyTracking.track);
52-
runner.on('stateChange', state => ipc.send(state));
32+
const runner = new Runner({
33+
failFast: options.failFast,
34+
failWithoutAssertions: options.failWithoutAssertions,
35+
file: options.file,
36+
match: options.match,
37+
projectDir: options.projectDir,
38+
runOnlyExclusive: options.runOnlyExclusive,
39+
serial: options.serial,
40+
snapshotDir: options.snapshotDir,
41+
updateSnapshots: options.updateSnapshots
42+
});
5343

54-
runner.on('error', error => {
55-
ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, error)});
56-
exit(1);
57-
});
44+
ipc.peerFailed.then(() => {
45+
runner.interrupt();
46+
});
5847

59-
runner.on('finish', () => {
60-
try {
61-
const touchedFiles = runner.saveSnapshotState();
62-
if (touchedFiles) {
63-
ipc.send({type: 'touched-files', files: touchedFiles});
48+
const attributedRejections = new Set();
49+
process.on('unhandledRejection', (reason, promise) => {
50+
if (runner.attributeLeakedError(reason)) {
51+
attributedRejections.add(promise);
6452
}
65-
} catch (error) {
53+
});
54+
55+
runner.on('dependency', dependencyTracking.track);
56+
runner.on('stateChange', state => ipc.send(state));
57+
58+
runner.on('error', error => {
6659
ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, error)});
6760
exit(1);
68-
return;
69-
}
61+
});
7062

71-
nowAndTimers.setImmediate(() => {
72-
currentlyUnhandled()
73-
.filter(rejection => !attributedRejections.has(rejection.promise))
74-
.forEach(rejection => {
75-
ipc.send({type: 'unhandled-rejection', err: serializeError('Unhandled rejection', true, rejection.reason)});
76-
});
63+
runner.on('finish', () => {
64+
try {
65+
const touchedFiles = runner.saveSnapshotState();
66+
if (touchedFiles) {
67+
ipc.send({type: 'touched-files', files: touchedFiles});
68+
}
69+
} catch (error) {
70+
ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, error)});
71+
exit(1);
72+
return;
73+
}
7774

78-
exit(0);
79-
});
80-
});
75+
nowAndTimers.setImmediate(() => {
76+
currentlyUnhandled()
77+
.filter(rejection => !attributedRejections.has(rejection.promise))
78+
.forEach(rejection => {
79+
ipc.send({type: 'unhandled-rejection', err: serializeError('Unhandled rejection', true, rejection.reason)});
80+
});
8181

82-
process.on('uncaughtException', error => {
83-
if (runner.attributeLeakedError(error)) {
84-
return;
85-
}
82+
exit(0);
83+
});
84+
});
8685

87-
ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error)});
88-
exit(1);
89-
});
86+
process.on('uncaughtException', error => {
87+
if (runner.attributeLeakedError(error)) {
88+
return;
89+
}
9090

91-
let accessedRunner = false;
92-
exports.getRunner = () => {
93-
accessedRunner = true;
94-
return runner;
95-
};
91+
ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error)});
92+
exit(1);
93+
});
9694

97-
// Store value in case to prevent required modules from modifying it.
98-
const testPath = options.file;
95+
let accessedRunner = false;
96+
exports.getRunner = () => {
97+
accessedRunner = true;
98+
return runner;
99+
};
99100

100-
// Install before processing options.require, so if helpers are added to the
101-
// require configuration the *compiled* helper will be loaded.
102-
dependencyTracking.install(testPath);
103-
precompilerHook.install();
101+
// Store value in case to prevent required modules from modifying it.
102+
const testPath = options.file;
104103

105-
try {
106-
for (const mod of (options.require || [])) {
107-
const required = require(mod);
104+
// Install before processing options.require, so if helpers are added to the
105+
// require configuration the *compiled* helper will be loaded.
106+
dependencyTracking.install(testPath);
107+
precompilerHook.install();
108108

109-
try {
110-
if (required[Symbol.for('esm\u200D:package')]) {
111-
require = required(module); // eslint-disable-line no-global-assign
112-
}
113-
} catch (_) {}
114-
}
109+
try {
110+
for (const mod of (options.require || [])) {
111+
const required = require(mod);
112+
113+
try {
114+
if (required[Symbol.for('esm\u200D:package')]) {
115+
require = required(module); // eslint-disable-line no-global-assign
116+
}
117+
} catch (_) {}
118+
}
115119

116-
require(testPath);
120+
require(testPath);
117121

118-
if (accessedRunner) {
119-
// Unreference the IPC channel if the test file required AVA. This stops it
120-
// from keeping the event loop busy, which means the `beforeExit` event can be
121-
// used to detect when tests stall.
122-
ipc.unref();
123-
} else {
124-
ipc.send({type: 'missing-ava-import'});
122+
if (accessedRunner) {
123+
// Unreference the IPC channel if the test file required AVA. This stops it
124+
// from keeping the event loop busy, which means the `beforeExit` event can be
125+
// used to detect when tests stall.
126+
ipc.unref();
127+
} else {
128+
ipc.send({type: 'missing-ava-import'});
129+
exit(1);
130+
}
131+
} catch (error) {
132+
ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error)});
125133
exit(1);
126134
}
127-
} catch (error) {
128-
ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error)});
129-
exit(1);
130-
}
135+
}).catch(error => {
136+
// There shouldn't be any errors, but if there are we may not have managed
137+
// to bootstrap enough code to serialize them. Re-throw and let the process
138+
// crash.
139+
setImmediate(() => {
140+
throw error;
141+
});
142+
});

profile.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ runStatus.observeWorker({
116116
process.send = data => {
117117
if (data && data.ava) {
118118
const evt = data.ava;
119-
if (evt.type === 'ping') {
119+
if (evt.type === 'ready-for-options') {
120+
process.emit('message', {ava: {type: 'options', options: opts}});
121+
} else if (evt.type === 'ping') {
120122
if (console.profileEnd) {
121123
console.profileEnd();
122124
}
@@ -152,10 +154,6 @@ process.on('beforeExit', () => {
152154
process.exitCode = process.exitCode || runStatus.suggestExitCode({matching: false});
153155
});
154156

155-
// The "subprocess" will read process.argv[2] for options
156-
process.argv[2] = JSON.stringify(opts);
157-
process.argv.length = 3;
158-
159157
if (console.profile) {
160158
console.profile('AVA test-worker process');
161159
}

0 commit comments

Comments
 (0)