ava 3.12.1 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api.js +8 -1
- package/lib/assert.js +11 -3
- package/lib/cli.js +5 -2
- package/lib/fork.js +84 -20
- package/lib/ipc-flow-control.js +39 -0
- package/lib/load-config.js +8 -2
- package/lib/plugin-support/shared-worker-loader.js +252 -0
- package/lib/plugin-support/shared-workers.js +138 -0
- package/lib/reporters/default.js +33 -2
- package/lib/run-status.js +5 -0
- package/lib/runner.js +3 -0
- package/lib/test.js +2 -1
- package/lib/worker/ipc.js +174 -29
- package/lib/worker/plugin.js +121 -0
- package/lib/worker/subprocess.js +24 -1
- package/package.json +16 -14
- package/plugin.d.ts +81 -0
- package/plugin.js +9 -0
package/lib/api.js
CHANGED
|
@@ -17,6 +17,7 @@ const RunStatus = require('./run-status');
|
|
|
17
17
|
const fork = require('./fork');
|
|
18
18
|
const serializeError = require('./serialize-error');
|
|
19
19
|
const {getApplicableLineNumbers} = require('./line-numbers');
|
|
20
|
+
const sharedWorkers = require('./plugin-support/shared-workers');
|
|
20
21
|
|
|
21
22
|
function resolveModules(modules) {
|
|
22
23
|
return arrify(modules).map(name => {
|
|
@@ -206,6 +207,8 @@ class Api extends Emittery {
|
|
|
206
207
|
concurrency = 1;
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
const deregisteredSharedWorkers = [];
|
|
211
|
+
|
|
209
212
|
// Try and run each file, limited by `concurrency`.
|
|
210
213
|
await pMap(selectedFiles, async file => {
|
|
211
214
|
// No new files should be run once a test has timed out or failed,
|
|
@@ -231,6 +234,7 @@ class Api extends Emittery {
|
|
|
231
234
|
|
|
232
235
|
const worker = fork(file, options, apiOptions.nodeArguments);
|
|
233
236
|
runStatus.observeWorker(worker, file, {selectingLines: lineNumbers.length > 0});
|
|
237
|
+
deregisteredSharedWorkers.push(sharedWorkers.observeWorkerProcess(worker, runStatus));
|
|
234
238
|
|
|
235
239
|
pendingWorkers.add(worker);
|
|
236
240
|
worker.promise.then(() => {
|
|
@@ -238,8 +242,11 @@ class Api extends Emittery {
|
|
|
238
242
|
});
|
|
239
243
|
restartTimer();
|
|
240
244
|
|
|
241
|
-
|
|
245
|
+
await worker.promise;
|
|
242
246
|
}, {concurrency, stopOnError: false});
|
|
247
|
+
|
|
248
|
+
// Allow shared workers to clean up before the run ends.
|
|
249
|
+
await Promise.all(deregisteredSharedWorkers);
|
|
243
250
|
} catch (error) {
|
|
244
251
|
if (error && error.name === 'AggregateError') {
|
|
245
252
|
for (const err of error) {
|
package/lib/assert.js
CHANGED
|
@@ -87,8 +87,16 @@ function getErrorWithLongStackTrace() {
|
|
|
87
87
|
return err;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function validateExpectations(assertion, expectations, numberArgs) { // eslint-disable-line complexity
|
|
90
|
+
function validateExpectations(assertion, expectations, numberArgs, experiments) { // eslint-disable-line complexity
|
|
91
91
|
if (numberArgs === 1 || expectations === null || expectations === undefined) {
|
|
92
|
+
if (experiments.disableNullExpectations && expectations === null) {
|
|
93
|
+
throw new AssertionError({
|
|
94
|
+
assertion,
|
|
95
|
+
message: `The second argument to \`t.${assertion}()\` must be an expectation object or \`undefined\``,
|
|
96
|
+
values: [formatWithLabel('Called with:', expectations)]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
expectations = {};
|
|
93
101
|
} else if (
|
|
94
102
|
typeof expectations === 'function' ||
|
|
@@ -465,7 +473,7 @@ class Assertions {
|
|
|
465
473
|
}
|
|
466
474
|
|
|
467
475
|
try {
|
|
468
|
-
expectations = validateExpectations('throws', expectations, args.length);
|
|
476
|
+
expectations = validateExpectations('throws', expectations, args.length, experiments);
|
|
469
477
|
} catch (error) {
|
|
470
478
|
fail(error);
|
|
471
479
|
return;
|
|
@@ -531,7 +539,7 @@ class Assertions {
|
|
|
531
539
|
}
|
|
532
540
|
|
|
533
541
|
try {
|
|
534
|
-
expectations = validateExpectations('throwsAsync', expectations, args.length);
|
|
542
|
+
expectations = validateExpectations('throwsAsync', expectations, args.length, experiments);
|
|
535
543
|
} catch (error) {
|
|
536
544
|
fail(error);
|
|
537
545
|
return Promise.resolve();
|
package/lib/cli.js
CHANGED
|
@@ -430,14 +430,17 @@ exports.run = async () => { // eslint-disable-line complexity
|
|
|
430
430
|
reporter.startRun(plan);
|
|
431
431
|
|
|
432
432
|
if (process.env.AVA_EMIT_RUN_STATUS_OVER_IPC === 'I\'ll find a payphone baby / Take some time to talk to you') {
|
|
433
|
+
const {controlFlow} = require('./ipc-flow-control');
|
|
434
|
+
const bufferedSend = controlFlow(process);
|
|
435
|
+
|
|
433
436
|
if (process.versions.node >= '12.16.0') {
|
|
434
437
|
plan.status.on('stateChange', evt => {
|
|
435
|
-
|
|
438
|
+
bufferedSend(evt);
|
|
436
439
|
});
|
|
437
440
|
} else {
|
|
438
441
|
const v8 = require('v8');
|
|
439
442
|
plan.status.on('stateChange', evt => {
|
|
440
|
-
|
|
443
|
+
bufferedSend([...v8.serialize(evt)]);
|
|
441
444
|
});
|
|
442
445
|
}
|
|
443
446
|
}
|
package/lib/fork.js
CHANGED
|
@@ -3,6 +3,7 @@ const childProcess = require('child_process');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const Emittery = require('emittery');
|
|
6
|
+
const {controlFlow} = require('./ipc-flow-control');
|
|
6
7
|
|
|
7
8
|
if (fs.realpathSync(__filename) !== __filename) {
|
|
8
9
|
console.warn('WARNING: `npm link ava` and the `--preserve-symlink` flag are incompatible. We have detected that AVA is linked via `npm link`, and that you are using either an early version of Node 6, or the `--preserve-symlink` flag. This breaks AVA. You should upgrade to Node 6.2.0+, avoid the `--preserve-symlink` flag, or avoid using `npm link ava`.');
|
|
@@ -11,10 +12,57 @@ if (fs.realpathSync(__filename) !== __filename) {
|
|
|
11
12
|
// In case the test file imports a different AVA install,
|
|
12
13
|
// the presence of this variable allows it to require this one instead
|
|
13
14
|
const AVA_PATH = path.resolve(__dirname, '..');
|
|
15
|
+
const WORKER_PATH = require.resolve('./worker/subprocess');
|
|
16
|
+
|
|
17
|
+
class SharedWorkerChannel extends Emittery {
|
|
18
|
+
constructor({channelId, filename, initialData}, sendToFork) {
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
this.id = channelId;
|
|
22
|
+
this.filename = filename;
|
|
23
|
+
this.initialData = initialData;
|
|
24
|
+
this.sendToFork = sendToFork;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
signalReady() {
|
|
28
|
+
this.sendToFork({
|
|
29
|
+
type: 'shared-worker-ready',
|
|
30
|
+
channelId: this.id
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
signalError() {
|
|
35
|
+
this.sendToFork({
|
|
36
|
+
type: 'shared-worker-error',
|
|
37
|
+
channelId: this.id
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
emitMessage({messageId, replyTo, serializedData}) {
|
|
42
|
+
this.emit('message', {
|
|
43
|
+
messageId,
|
|
44
|
+
replyTo,
|
|
45
|
+
serializedData
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
forwardMessageToFork({messageId, replyTo, serializedData}) {
|
|
50
|
+
this.sendToFork({
|
|
51
|
+
type: 'shared-worker-message',
|
|
52
|
+
channelId: this.id,
|
|
53
|
+
messageId,
|
|
54
|
+
replyTo,
|
|
55
|
+
serializedData
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
14
59
|
|
|
15
|
-
|
|
60
|
+
let forkCounter = 0;
|
|
16
61
|
|
|
17
62
|
module.exports = (file, options, execArgv = process.execArgv) => {
|
|
63
|
+
const forkId = `fork/${++forkCounter}`;
|
|
64
|
+
const sharedWorkerChannels = new Map();
|
|
65
|
+
|
|
18
66
|
let finished = false;
|
|
19
67
|
|
|
20
68
|
const emitter = new Emittery();
|
|
@@ -25,12 +73,13 @@ module.exports = (file, options, execArgv = process.execArgv) => {
|
|
|
25
73
|
};
|
|
26
74
|
|
|
27
75
|
options = {
|
|
28
|
-
file,
|
|
29
76
|
baseDir: process.cwd(),
|
|
77
|
+
file,
|
|
78
|
+
forkId,
|
|
30
79
|
...options
|
|
31
80
|
};
|
|
32
81
|
|
|
33
|
-
const subprocess = childProcess.fork(
|
|
82
|
+
const subprocess = childProcess.fork(WORKER_PATH, options.workerArgv, {
|
|
34
83
|
cwd: options.projectDir,
|
|
35
84
|
silent: true,
|
|
36
85
|
env: {NODE_ENV: 'test', ...process.env, ...options.environmentVariables, AVA_PATH},
|
|
@@ -45,12 +94,12 @@ module.exports = (file, options, execArgv = process.execArgv) => {
|
|
|
45
94
|
emitStateChange({type: 'worker-stderr', chunk});
|
|
46
95
|
});
|
|
47
96
|
|
|
97
|
+
const bufferedSend = controlFlow(subprocess);
|
|
98
|
+
|
|
48
99
|
let forcedExit = false;
|
|
49
100
|
const send = evt => {
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
// Disregard errors.
|
|
53
|
-
});
|
|
101
|
+
if (!finished && !forcedExit) {
|
|
102
|
+
bufferedSend({ava: evt});
|
|
54
103
|
}
|
|
55
104
|
};
|
|
56
105
|
|
|
@@ -65,15 +114,25 @@ module.exports = (file, options, execArgv = process.execArgv) => {
|
|
|
65
114
|
return;
|
|
66
115
|
}
|
|
67
116
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
117
|
+
switch (message.ava.type) {
|
|
118
|
+
case 'ready-for-options':
|
|
119
|
+
send({type: 'options', options});
|
|
120
|
+
break;
|
|
121
|
+
case 'shared-worker-connect': {
|
|
122
|
+
const channel = new SharedWorkerChannel(message.ava, send);
|
|
123
|
+
sharedWorkerChannels.set(channel.id, channel);
|
|
124
|
+
emitter.emit('connectSharedWorker', channel);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'shared-worker-message':
|
|
129
|
+
sharedWorkerChannels.get(message.ava.channelId).emitMessage(message.ava);
|
|
130
|
+
break;
|
|
131
|
+
case 'ping':
|
|
132
|
+
send({type: 'pong'});
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
emitStateChange(message.ava);
|
|
77
136
|
}
|
|
78
137
|
});
|
|
79
138
|
|
|
@@ -98,6 +157,10 @@ module.exports = (file, options, execArgv = process.execArgv) => {
|
|
|
98
157
|
});
|
|
99
158
|
|
|
100
159
|
return {
|
|
160
|
+
file,
|
|
161
|
+
forkId,
|
|
162
|
+
promise,
|
|
163
|
+
|
|
101
164
|
exit() {
|
|
102
165
|
forcedExit = true;
|
|
103
166
|
subprocess.kill();
|
|
@@ -107,11 +170,12 @@ module.exports = (file, options, execArgv = process.execArgv) => {
|
|
|
107
170
|
send({type: 'peer-failed'});
|
|
108
171
|
},
|
|
109
172
|
|
|
110
|
-
|
|
111
|
-
return emitter.on('
|
|
173
|
+
onConnectSharedWorker(listener) {
|
|
174
|
+
return emitter.on('connectSharedWorker', listener);
|
|
112
175
|
},
|
|
113
176
|
|
|
114
|
-
|
|
115
|
-
|
|
177
|
+
onStateChange(listener) {
|
|
178
|
+
return emitter.on('stateChange', listener);
|
|
179
|
+
}
|
|
116
180
|
};
|
|
117
181
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function controlFlow(channel) {
|
|
2
|
+
let errored = false;
|
|
3
|
+
let deliverImmediately = true;
|
|
4
|
+
|
|
5
|
+
const backlog = [];
|
|
6
|
+
const deliverNext = error => {
|
|
7
|
+
if (error !== null) {
|
|
8
|
+
errored = true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (errored || !channel.connected) {
|
|
12
|
+
backlog.length = 0; // Free memory.
|
|
13
|
+
return; // We can't send.
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let ok = true;
|
|
17
|
+
while (ok && backlog.length > 0) { // Stop sending after backpressure.
|
|
18
|
+
ok = channel.send(backlog.shift(), deliverNext);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Re-enable immediate delivery if there is no backpressure and the backlog
|
|
22
|
+
// has been cleared.
|
|
23
|
+
deliverImmediately = ok && backlog.length === 0;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return message => {
|
|
27
|
+
if (errored || !channel.connected) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (deliverImmediately) {
|
|
32
|
+
deliverImmediately = channel.send(message, deliverNext);
|
|
33
|
+
} else {
|
|
34
|
+
backlog.push(message);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
exports.controlFlow = controlFlow;
|
package/lib/load-config.js
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const vm = require('vm');
|
|
5
|
-
const isPlainObject = require('is-plain-object');
|
|
5
|
+
const {isPlainObject} = require('is-plain-object');
|
|
6
6
|
const pkgConf = require('pkg-conf');
|
|
7
7
|
|
|
8
8
|
const NO_SUCH_FILE = Symbol('no ava.config.js file');
|
|
9
9
|
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
|
|
10
|
-
const EXPERIMENTS = new Set([
|
|
10
|
+
const EXPERIMENTS = new Set([
|
|
11
|
+
'configurableModuleFormat',
|
|
12
|
+
'disableNullExpectations',
|
|
13
|
+
'disableSnapshotsInHooks',
|
|
14
|
+
'reverseTeardowns',
|
|
15
|
+
'sharedWorkers'
|
|
16
|
+
]);
|
|
11
17
|
|
|
12
18
|
// *Very* rudimentary support for loading ava.config.js files containing an `export default` statement.
|
|
13
19
|
const evaluateJsConfig = configFile => {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const {EventEmitter, on} = require('events');
|
|
2
|
+
const v8 = require('v8');
|
|
3
|
+
const {workerData, parentPort} = require('worker_threads'); // eslint-disable-line node/no-unsupported-features/node-builtins
|
|
4
|
+
const pkg = require('../../package.json');
|
|
5
|
+
|
|
6
|
+
// Used to forward messages received over the `parentPort`. Every subscription
|
|
7
|
+
// adds a listener, so do not enforce any maximums.
|
|
8
|
+
const events = new EventEmitter().setMaxListeners(0);
|
|
9
|
+
|
|
10
|
+
// Map of active test workers, used in receiveMessages() to get a reference to
|
|
11
|
+
// the TestWorker instance, and relevant release functions.
|
|
12
|
+
const activeTestWorkers = new Map();
|
|
13
|
+
|
|
14
|
+
class TestWorker {
|
|
15
|
+
constructor(id, file) {
|
|
16
|
+
this.id = id;
|
|
17
|
+
this.file = file;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
teardown(fn) {
|
|
21
|
+
let done = false;
|
|
22
|
+
const teardownFn = async () => {
|
|
23
|
+
if (done) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
done = true;
|
|
28
|
+
if (activeTestWorkers.has(this.id)) {
|
|
29
|
+
activeTestWorkers.get(this.id).teardownFns.delete(teardownFn);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await fn();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
activeTestWorkers.get(this.id).teardownFns.add(teardownFn);
|
|
36
|
+
|
|
37
|
+
return teardownFn;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
publish(data) {
|
|
41
|
+
return publishMessage(this, data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async * subscribe() {
|
|
45
|
+
yield * receiveMessages(this);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class ReceivedMessage {
|
|
50
|
+
constructor(testWorker, id, serializedData) {
|
|
51
|
+
this.testWorker = testWorker;
|
|
52
|
+
this.id = id;
|
|
53
|
+
this.data = v8.deserialize(new Uint8Array(serializedData));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
reply(data) {
|
|
57
|
+
return publishMessage(this.testWorker, data, this.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure that, no matter how often it's received, we have a stable message
|
|
62
|
+
// object.
|
|
63
|
+
const messageCache = new WeakMap();
|
|
64
|
+
|
|
65
|
+
async function * receiveMessages(fromTestWorker, replyTo) {
|
|
66
|
+
for await (const [message] of on(events, 'message')) {
|
|
67
|
+
if (fromTestWorker !== undefined) {
|
|
68
|
+
if (message.type === 'deregister-test-worker' && message.id === fromTestWorker.id) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (message.type === 'message' && message.testWorkerId !== fromTestWorker.id) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (message.type !== 'message') {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (replyTo === undefined && message.replyTo !== undefined) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (replyTo !== undefined && message.replyTo !== replyTo) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const active = activeTestWorkers.get(message.testWorkerId);
|
|
90
|
+
// It is possible for a message to have been buffering for so long — perhaps
|
|
91
|
+
// due to the caller waiting before iterating to the next message — that the
|
|
92
|
+
// test worker has been deregistered. Ignore such messages.
|
|
93
|
+
//
|
|
94
|
+
// (This is really hard to write a test for, however!)
|
|
95
|
+
if (active === undefined) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let received = messageCache.get(message);
|
|
100
|
+
if (received === undefined) {
|
|
101
|
+
received = new ReceivedMessage(active.instance, message.messageId, message.serializedData);
|
|
102
|
+
messageCache.set(message, received);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
yield received;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let messageCounter = 0;
|
|
110
|
+
const messageIdPrefix = `${workerData.id}/message`;
|
|
111
|
+
const nextMessageId = () => `${messageIdPrefix}/${++messageCounter}`;
|
|
112
|
+
|
|
113
|
+
function publishMessage(testWorker, data, replyTo) {
|
|
114
|
+
const id = nextMessageId();
|
|
115
|
+
parentPort.postMessage({
|
|
116
|
+
type: 'message',
|
|
117
|
+
messageId: id,
|
|
118
|
+
testWorkerId: testWorker.id,
|
|
119
|
+
serializedData: [...v8.serialize(data)],
|
|
120
|
+
replyTo
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
async * replies() {
|
|
126
|
+
yield * receiveMessages(testWorker, id);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function broadcastMessage(data) {
|
|
132
|
+
const id = nextMessageId();
|
|
133
|
+
parentPort.postMessage({
|
|
134
|
+
type: 'broadcast',
|
|
135
|
+
messageId: id,
|
|
136
|
+
serializedData: [...v8.serialize(data)]
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
id,
|
|
141
|
+
async * replies() {
|
|
142
|
+
yield * receiveMessages(undefined, id);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function loadFactory() {
|
|
148
|
+
try {
|
|
149
|
+
const mod = require(workerData.filename);
|
|
150
|
+
if (typeof mod === 'function') {
|
|
151
|
+
return mod;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return mod.default;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error && (error.code === 'ERR_REQUIRE_ESM' || (error.code === 'MODULE_NOT_FOUND' && workerData.filename.startsWith('file://')))) {
|
|
157
|
+
const {default: factory} = await import(workerData.filename); // eslint-disable-line node/no-unsupported-features/es-syntax
|
|
158
|
+
return factory;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let signalAvailable = () => {
|
|
166
|
+
parentPort.postMessage({type: 'available'});
|
|
167
|
+
signalAvailable = () => {};
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let fatal;
|
|
171
|
+
loadFactory(workerData.filename).then(factory => {
|
|
172
|
+
if (typeof factory !== 'function') {
|
|
173
|
+
throw new TypeError(`Missing default factory function export for shared worker plugin at ${workerData.filename}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
factory({
|
|
177
|
+
negotiateProtocol(supported) {
|
|
178
|
+
if (!supported.includes('experimental')) {
|
|
179
|
+
fatal = new Error(`This version of AVA (${pkg.version}) is not compatible with shared worker plugin at ${workerData.filename}`);
|
|
180
|
+
throw fatal;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const produceTestWorker = instance => events.emit('testWorker', instance);
|
|
184
|
+
|
|
185
|
+
parentPort.on('message', async message => {
|
|
186
|
+
if (message.type === 'register-test-worker') {
|
|
187
|
+
const {id, file} = message;
|
|
188
|
+
const instance = new TestWorker(id, file);
|
|
189
|
+
|
|
190
|
+
activeTestWorkers.set(id, {instance, teardownFns: new Set()});
|
|
191
|
+
|
|
192
|
+
produceTestWorker(instance);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (message.type === 'deregister-test-worker') {
|
|
196
|
+
const {id} = message;
|
|
197
|
+
const {teardownFns} = activeTestWorkers.get(id);
|
|
198
|
+
activeTestWorkers.delete(id);
|
|
199
|
+
|
|
200
|
+
// Run possibly asynchronous release functions serially, in reverse
|
|
201
|
+
// order. Any error will crash the worker.
|
|
202
|
+
for await (const fn of [...teardownFns].reverse()) {
|
|
203
|
+
await fn();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
parentPort.postMessage({
|
|
207
|
+
type: 'deregistered-test-worker',
|
|
208
|
+
id
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Wait for a turn of the event loop, to allow new subscriptions to be
|
|
213
|
+
// set up in response to the previous message.
|
|
214
|
+
setImmediate(() => events.emit('message', message));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
initialData: workerData.initialData,
|
|
219
|
+
protocol: 'experimental',
|
|
220
|
+
|
|
221
|
+
ready() {
|
|
222
|
+
signalAvailable();
|
|
223
|
+
return this;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
broadcast(data) {
|
|
227
|
+
return broadcastMessage(data);
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async * subscribe() {
|
|
231
|
+
yield * receiveMessages();
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async * testWorkers() {
|
|
235
|
+
for await (const [worker] of on(events, 'testWorker')) {
|
|
236
|
+
yield worker;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}).catch(error => {
|
|
243
|
+
if (fatal === undefined) {
|
|
244
|
+
fatal = error;
|
|
245
|
+
}
|
|
246
|
+
}).finally(() => {
|
|
247
|
+
if (fatal !== undefined) {
|
|
248
|
+
process.nextTick(() => {
|
|
249
|
+
throw fatal;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const events = require('events');
|
|
2
|
+
const serializeError = require('../serialize-error');
|
|
3
|
+
|
|
4
|
+
let Worker;
|
|
5
|
+
try {
|
|
6
|
+
({Worker} = require('worker_threads')); // eslint-disable-line node/no-unsupported-features/node-builtins
|
|
7
|
+
} catch {}
|
|
8
|
+
|
|
9
|
+
const LOADER = require.resolve('./shared-worker-loader');
|
|
10
|
+
|
|
11
|
+
let sharedWorkerCounter = 0;
|
|
12
|
+
const launchedWorkers = new Map();
|
|
13
|
+
|
|
14
|
+
const waitForAvailable = async worker => {
|
|
15
|
+
for await (const [message] of events.on(worker, 'message')) {
|
|
16
|
+
if (message.type === 'available') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function launchWorker({filename, initialData}) {
|
|
23
|
+
if (launchedWorkers.has(filename)) {
|
|
24
|
+
return launchedWorkers.get(filename);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const id = `shared-worker/${++sharedWorkerCounter}`;
|
|
28
|
+
const worker = new Worker(LOADER, {
|
|
29
|
+
// Ensure the worker crashes for unhandled rejections, rather than allowing undefined behavior.
|
|
30
|
+
execArgv: ['--unhandled-rejections=strict'],
|
|
31
|
+
workerData: {
|
|
32
|
+
filename,
|
|
33
|
+
id,
|
|
34
|
+
initialData
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
const launched = {
|
|
38
|
+
statePromises: {
|
|
39
|
+
available: waitForAvailable(worker),
|
|
40
|
+
error: events.once(worker, 'error').then(([error]) => error) // eslint-disable-line promise/prefer-await-to-then
|
|
41
|
+
},
|
|
42
|
+
exited: false,
|
|
43
|
+
worker
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
launchedWorkers.set(filename, launched);
|
|
47
|
+
worker.once('exit', () => {
|
|
48
|
+
launched.exited = true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return launched;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function observeWorkerProcess(fork, runStatus) {
|
|
55
|
+
let registrationCount = 0;
|
|
56
|
+
let signalDeregistered;
|
|
57
|
+
const deregistered = new Promise(resolve => {
|
|
58
|
+
signalDeregistered = resolve;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
fork.promise.finally(() => {
|
|
62
|
+
if (registrationCount === 0) {
|
|
63
|
+
signalDeregistered();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
fork.onConnectSharedWorker(async channel => {
|
|
68
|
+
const launched = launchWorker(channel);
|
|
69
|
+
|
|
70
|
+
const handleChannelMessage = ({messageId, replyTo, serializedData}) => {
|
|
71
|
+
launched.worker.postMessage({
|
|
72
|
+
type: 'message',
|
|
73
|
+
testWorkerId: fork.forkId,
|
|
74
|
+
messageId,
|
|
75
|
+
replyTo,
|
|
76
|
+
serializedData
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleWorkerMessage = async message => {
|
|
81
|
+
if (message.type === 'broadcast' || (message.type === 'message' && message.testWorkerId === fork.forkId)) {
|
|
82
|
+
const {messageId, replyTo, serializedData} = message;
|
|
83
|
+
channel.forwardMessageToFork({messageId, replyTo, serializedData});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (message.type === 'deregistered-test-worker' && message.id === fork.forkId) {
|
|
87
|
+
launched.worker.off('message', handleWorkerMessage);
|
|
88
|
+
|
|
89
|
+
registrationCount--;
|
|
90
|
+
if (registrationCount === 0) {
|
|
91
|
+
signalDeregistered();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
launched.statePromises.error.then(error => { // eslint-disable-line promise/prefer-await-to-then
|
|
97
|
+
signalDeregistered();
|
|
98
|
+
launched.worker.off('message', handleWorkerMessage);
|
|
99
|
+
runStatus.emitStateChange({type: 'shared-worker-error', err: serializeError('Shared worker error', true, error)});
|
|
100
|
+
channel.signalError();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await launched.statePromises.available;
|
|
105
|
+
|
|
106
|
+
registrationCount++;
|
|
107
|
+
launched.worker.postMessage({
|
|
108
|
+
type: 'register-test-worker',
|
|
109
|
+
id: fork.forkId,
|
|
110
|
+
file: fork.file
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
fork.promise.finally(() => {
|
|
114
|
+
launched.worker.postMessage({
|
|
115
|
+
type: 'deregister-test-worker',
|
|
116
|
+
id: fork.forkId
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
channel.off('message', handleChannelMessage);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
launched.worker.on('message', handleWorkerMessage);
|
|
123
|
+
channel.on('message', handleChannelMessage);
|
|
124
|
+
channel.signalReady();
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
} finally {
|
|
128
|
+
// Attaching listeners has the side-effect of referencing the worker.
|
|
129
|
+
// Explicitly unreference it now so it does not prevent the main process
|
|
130
|
+
// from exiting.
|
|
131
|
+
launched.worker.unref();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return deregistered;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exports.observeWorkerProcess = observeWorkerProcess;
|