ava 3.11.0 → 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/index.d.ts +1 -1
- package/lib/api.js +8 -1
- package/lib/assert.js +11 -3
- package/lib/cli.js +13 -8
- package/lib/extensions.js +4 -1
- package/lib/fork.js +84 -20
- package/lib/ipc-flow-control.js +39 -0
- package/lib/load-config.js +8 -2
- package/lib/module-types.js +75 -0
- package/lib/plugin-support/shared-worker-loader.js +252 -0
- package/lib/plugin-support/shared-workers.js +138 -0
- package/lib/provider-manager.js +1 -1
- package/lib/reporters/default.js +83 -12
- 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 +24 -22
- package/plugin.d.ts +81 -0
- package/plugin.js +9 -0
- package/readme.md +4 -3
- package/lib/reporters/while-corked.js +0 -13
package/index.d.ts
CHANGED
|
@@ -433,7 +433,7 @@ export interface CbExecutionContext<Context = unknown> extends ExecutionContext<
|
|
|
433
433
|
end(error?: any): void;
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
export type ImplementationResult = PromiseLike<void> | Subscribable | void;
|
|
436
|
+
export type ImplementationResult = PromiseLike<void> | Subscribable | void;
|
|
437
437
|
export type Implementation<Context = unknown> = (t: ExecutionContext<Context>) => ImplementationResult;
|
|
438
438
|
export type CbImplementation<Context = unknown> = (t: CbExecutionContext<Context>) => ImplementationResult;
|
|
439
439
|
|
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
|
@@ -284,6 +284,7 @@ exports.run = async () => { // eslint-disable-line complexity
|
|
|
284
284
|
const TapReporter = require('./reporters/tap');
|
|
285
285
|
const Watcher = require('./watcher');
|
|
286
286
|
const normalizeExtensions = require('./extensions');
|
|
287
|
+
const normalizeModuleTypes = require('./module-types');
|
|
287
288
|
const {normalizeGlobs, normalizePattern} = require('./globs');
|
|
288
289
|
const normalizeNodeArguments = require('./node-arguments');
|
|
289
290
|
const validateEnvironmentVariables = require('./environment-variables');
|
|
@@ -301,12 +302,6 @@ exports.run = async () => { // eslint-disable-line complexity
|
|
|
301
302
|
|
|
302
303
|
const {type: defaultModuleType = 'commonjs'} = pkg || {};
|
|
303
304
|
|
|
304
|
-
const moduleTypes = {
|
|
305
|
-
cjs: 'commonjs',
|
|
306
|
-
mjs: 'module',
|
|
307
|
-
js: defaultModuleType
|
|
308
|
-
};
|
|
309
|
-
|
|
310
305
|
const providers = [];
|
|
311
306
|
if (Reflect.has(conf, 'babel')) {
|
|
312
307
|
try {
|
|
@@ -348,6 +343,13 @@ exports.run = async () => { // eslint-disable-line complexity
|
|
|
348
343
|
exit(error.message);
|
|
349
344
|
}
|
|
350
345
|
|
|
346
|
+
let moduleTypes;
|
|
347
|
+
try {
|
|
348
|
+
moduleTypes = normalizeModuleTypes(conf.extensions, defaultModuleType, experiments);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
exit(error.message);
|
|
351
|
+
}
|
|
352
|
+
|
|
351
353
|
let globs;
|
|
352
354
|
try {
|
|
353
355
|
globs = normalizeGlobs({files: conf.files, ignoredByWatcher: conf.ignoredByWatcher, extensions, providers});
|
|
@@ -428,14 +430,17 @@ exports.run = async () => { // eslint-disable-line complexity
|
|
|
428
430
|
reporter.startRun(plan);
|
|
429
431
|
|
|
430
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
|
+
|
|
431
436
|
if (process.versions.node >= '12.16.0') {
|
|
432
437
|
plan.status.on('stateChange', evt => {
|
|
433
|
-
|
|
438
|
+
bufferedSend(evt);
|
|
434
439
|
});
|
|
435
440
|
} else {
|
|
436
441
|
const v8 = require('v8');
|
|
437
442
|
plan.status.on('stateChange', evt => {
|
|
438
|
-
|
|
443
|
+
bufferedSend([...v8.serialize(evt)]);
|
|
439
444
|
});
|
|
440
445
|
}
|
|
441
446
|
}
|
package/lib/extensions.js
CHANGED
|
@@ -2,8 +2,11 @@ module.exports = (configuredExtensions, providers = []) => {
|
|
|
2
2
|
// Combine all extensions possible for testing. Remove duplicate extensions.
|
|
3
3
|
const duplicates = new Set();
|
|
4
4
|
const seen = new Set();
|
|
5
|
+
|
|
6
|
+
const normalize = extensions => Array.isArray(extensions) ? extensions : Object.keys(extensions);
|
|
7
|
+
|
|
5
8
|
const combine = extensions => {
|
|
6
|
-
for (const ext of extensions) {
|
|
9
|
+
for (const ext of normalize(extensions)) {
|
|
7
10
|
if (seen.has(ext)) {
|
|
8
11
|
duplicates.add(ext);
|
|
9
12
|
} else {
|
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,75 @@
|
|
|
1
|
+
const requireTrueValue = value => {
|
|
2
|
+
if (value !== true) {
|
|
3
|
+
throw new TypeError('When specifying module types, use `true` for ’cjs’, ’mjs’ and ’js’ extensions');
|
|
4
|
+
}
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const normalize = (extension, type, defaultModuleType) => {
|
|
8
|
+
switch (extension) {
|
|
9
|
+
case 'cjs':
|
|
10
|
+
requireTrueValue(type);
|
|
11
|
+
return 'commonjs';
|
|
12
|
+
case 'mjs':
|
|
13
|
+
requireTrueValue(type);
|
|
14
|
+
return 'module';
|
|
15
|
+
case 'js':
|
|
16
|
+
requireTrueValue(type);
|
|
17
|
+
return defaultModuleType;
|
|
18
|
+
default:
|
|
19
|
+
if (type !== 'commonjs' && type !== 'module') {
|
|
20
|
+
throw new TypeError(`Module type for ’${extension}’ must be ’commonjs’ or ’module’`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return type;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const deriveFromObject = (extensionsObject, defaultModuleType) => {
|
|
28
|
+
const moduleTypes = {};
|
|
29
|
+
for (const [extension, type] of Object.entries(extensionsObject)) {
|
|
30
|
+
moduleTypes[extension] = normalize(extension, type, defaultModuleType);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return moduleTypes;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const deriveFromArray = (extensions, defaultModuleType) => {
|
|
37
|
+
const moduleTypes = {};
|
|
38
|
+
for (const extension of extensions) {
|
|
39
|
+
switch (extension) {
|
|
40
|
+
case 'cjs':
|
|
41
|
+
moduleTypes.cjs = 'commonjs';
|
|
42
|
+
break;
|
|
43
|
+
case 'mjs':
|
|
44
|
+
moduleTypes.mjs = 'module';
|
|
45
|
+
break;
|
|
46
|
+
case 'js':
|
|
47
|
+
moduleTypes.js = defaultModuleType;
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
moduleTypes[extension] = 'commonjs';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return moduleTypes;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = (configuredExtensions, defaultModuleType, experiments) => {
|
|
58
|
+
if (configuredExtensions === undefined) {
|
|
59
|
+
return {
|
|
60
|
+
cjs: 'commonjs',
|
|
61
|
+
mjs: 'module',
|
|
62
|
+
js: defaultModuleType
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(configuredExtensions)) {
|
|
67
|
+
return deriveFromArray(configuredExtensions, defaultModuleType);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!experiments.configurableModuleFormat) {
|
|
71
|
+
throw new Error('You must enable the `configurableModuleFormat` experiment in order to specify module types');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return deriveFromObject(configuredExtensions, defaultModuleType);
|
|
75
|
+
};
|
|
@@ -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
|
+
});
|