ava 3.12.0 → 3.15.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/eslint-plugin-helper.js +134 -29
- package/lib/api.js +14 -13
- package/lib/assert.js +11 -3
- package/lib/cli.js +19 -21
- package/lib/concordance-options.js +1 -1
- package/lib/fork.js +79 -23
- package/lib/globs.js +1 -5
- package/lib/ipc-flow-control.js +22 -25
- package/lib/load-config.js +125 -18
- package/lib/plugin-support/shared-worker-loader.js +252 -0
- package/lib/plugin-support/shared-workers.js +140 -0
- package/lib/reporters/default.js +33 -2
- package/lib/reporters/tap.js +12 -2
- package/lib/run-status.js +5 -0
- package/lib/runner.js +53 -9
- package/lib/snapshot-manager.js +67 -9
- package/lib/test.js +20 -12
- package/lib/worker/ipc.js +171 -26
- package/lib/worker/plugin.js +121 -0
- package/lib/worker/subprocess.js +24 -1
- package/package.json +35 -31
- package/plugin.d.ts +79 -0
- package/plugin.js +9 -0
- package/readme.md +2 -7
package/lib/run-status.js
CHANGED
|
@@ -27,6 +27,7 @@ class RunStatus extends Emittery {
|
|
|
27
27
|
passedKnownFailingTests: 0,
|
|
28
28
|
passedTests: 0,
|
|
29
29
|
selectedTests: 0,
|
|
30
|
+
sharedWorkerErrors: 0,
|
|
30
31
|
skippedTests: 0,
|
|
31
32
|
timeouts: 0,
|
|
32
33
|
todoTests: 0,
|
|
@@ -93,6 +94,9 @@ class RunStatus extends Emittery {
|
|
|
93
94
|
this.addPendingTest(event);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
break;
|
|
98
|
+
case 'shared-worker-error':
|
|
99
|
+
stats.sharedWorkerErrors++;
|
|
96
100
|
break;
|
|
97
101
|
case 'test-failed':
|
|
98
102
|
stats.failedTests++;
|
|
@@ -164,6 +168,7 @@ class RunStatus extends Emittery {
|
|
|
164
168
|
this.stats.failedHooks > 0 ||
|
|
165
169
|
this.stats.failedTests > 0 ||
|
|
166
170
|
this.stats.failedWorkers > 0 ||
|
|
171
|
+
this.stats.sharedWorkerErrors > 0 ||
|
|
167
172
|
this.stats.timeouts > 0 ||
|
|
168
173
|
this.stats.uncaughtExceptions > 0 ||
|
|
169
174
|
this.stats.unhandledRejections > 0
|
package/lib/runner.js
CHANGED
|
@@ -29,8 +29,11 @@ class Runner extends Emittery {
|
|
|
29
29
|
|
|
30
30
|
this.activeRunnables = new Set();
|
|
31
31
|
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
|
|
32
|
+
this.skippedSnapshots = false;
|
|
33
|
+
this.boundSkipSnapshot = this.skipSnapshot.bind(this);
|
|
32
34
|
this.interrupted = false;
|
|
33
35
|
this.snapshots = null;
|
|
36
|
+
this.nextTaskIndex = 0;
|
|
34
37
|
this.tasks = {
|
|
35
38
|
after: [],
|
|
36
39
|
afterAlways: [],
|
|
@@ -42,6 +45,7 @@ class Runner extends Emittery {
|
|
|
42
45
|
serial: [],
|
|
43
46
|
todo: []
|
|
44
47
|
};
|
|
48
|
+
this.waitForReady = [];
|
|
45
49
|
|
|
46
50
|
const uniqueTestTitles = new Set();
|
|
47
51
|
this.registerUniqueTitle = title => {
|
|
@@ -75,6 +79,8 @@ class Runner extends Emittery {
|
|
|
75
79
|
});
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
metadata.taskIndex = this.nextTaskIndex++;
|
|
83
|
+
|
|
78
84
|
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
|
|
79
85
|
|
|
80
86
|
if (this.checkSelectedByLineNumbers) {
|
|
@@ -195,8 +201,19 @@ class Runner extends Emittery {
|
|
|
195
201
|
return this.snapshots.compare(options);
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
skipSnapshot() {
|
|
205
|
+
this.skippedSnapshots = true;
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
saveSnapshotState() {
|
|
199
|
-
if (
|
|
209
|
+
if (
|
|
210
|
+
this.updateSnapshots &&
|
|
211
|
+
(
|
|
212
|
+
this.runOnlyExclusive ||
|
|
213
|
+
this.skippingTests ||
|
|
214
|
+
this.skippedSnapshots
|
|
215
|
+
)
|
|
216
|
+
) {
|
|
200
217
|
return {cannotSave: true};
|
|
201
218
|
}
|
|
202
219
|
|
|
@@ -205,9 +222,11 @@ class Runner extends Emittery {
|
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
if (this.updateSnapshots) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
return {touchedFiles: snapshotManager.cleanSnapshots({
|
|
226
|
+
file: this.file,
|
|
227
|
+
fixedLocation: this.snapshotDir,
|
|
228
|
+
projectDir: this.projectDir
|
|
229
|
+
})};
|
|
211
230
|
}
|
|
212
231
|
|
|
213
232
|
return {};
|
|
@@ -284,7 +303,7 @@ class Runner extends Emittery {
|
|
|
284
303
|
return result;
|
|
285
304
|
}
|
|
286
305
|
|
|
287
|
-
async runHooks(tasks, contextRef, titleSuffix, testPassed) {
|
|
306
|
+
async runHooks(tasks, contextRef, {titleSuffix, testPassed, associatedTaskIndex} = {}) {
|
|
288
307
|
const hooks = tasks.map(task => new Runnable({
|
|
289
308
|
contextRef,
|
|
290
309
|
experiments: this.experiments,
|
|
@@ -293,8 +312,9 @@ class Runner extends Emittery {
|
|
|
293
312
|
task.implementation :
|
|
294
313
|
t => task.implementation.apply(null, [t].concat(task.args)),
|
|
295
314
|
compareTestSnapshot: this.boundCompareTestSnapshot,
|
|
315
|
+
skipSnapshot: this.boundSkipSnapshot,
|
|
296
316
|
updateSnapshots: this.updateSnapshots,
|
|
297
|
-
metadata: task.metadata,
|
|
317
|
+
metadata: {...task.metadata, associatedTaskIndex},
|
|
298
318
|
powerAssert: this.powerAssert,
|
|
299
319
|
title: `${task.title}${titleSuffix || ''}`,
|
|
300
320
|
isHook: true,
|
|
@@ -325,7 +345,14 @@ class Runner extends Emittery {
|
|
|
325
345
|
|
|
326
346
|
async runTest(task, contextRef) {
|
|
327
347
|
const hookSuffix = ` for ${task.title}`;
|
|
328
|
-
let hooksOk = await this.runHooks(
|
|
348
|
+
let hooksOk = await this.runHooks(
|
|
349
|
+
this.tasks.beforeEach,
|
|
350
|
+
contextRef,
|
|
351
|
+
{
|
|
352
|
+
titleSuffix: hookSuffix,
|
|
353
|
+
associatedTaskIndex: task.metadata.taskIndex
|
|
354
|
+
}
|
|
355
|
+
);
|
|
329
356
|
|
|
330
357
|
let testOk = false;
|
|
331
358
|
if (hooksOk) {
|
|
@@ -338,6 +365,7 @@ class Runner extends Emittery {
|
|
|
338
365
|
task.implementation :
|
|
339
366
|
t => task.implementation.apply(null, [t].concat(task.args)),
|
|
340
367
|
compareTestSnapshot: this.boundCompareTestSnapshot,
|
|
368
|
+
skipSnapshot: this.boundSkipSnapshot,
|
|
341
369
|
updateSnapshots: this.updateSnapshots,
|
|
342
370
|
metadata: task.metadata,
|
|
343
371
|
powerAssert: this.powerAssert,
|
|
@@ -357,7 +385,14 @@ class Runner extends Emittery {
|
|
|
357
385
|
logs: result.logs
|
|
358
386
|
});
|
|
359
387
|
|
|
360
|
-
hooksOk = await this.runHooks(
|
|
388
|
+
hooksOk = await this.runHooks(
|
|
389
|
+
this.tasks.afterEach,
|
|
390
|
+
contextRef,
|
|
391
|
+
{
|
|
392
|
+
titleSuffix: hookSuffix,
|
|
393
|
+
testPassed: testOk,
|
|
394
|
+
associatedTaskIndex: task.metadata.taskIndex
|
|
395
|
+
});
|
|
361
396
|
} else {
|
|
362
397
|
this.emit('stateChange', {
|
|
363
398
|
type: 'test-failed',
|
|
@@ -371,7 +406,14 @@ class Runner extends Emittery {
|
|
|
371
406
|
}
|
|
372
407
|
}
|
|
373
408
|
|
|
374
|
-
const alwaysOk = await this.runHooks(
|
|
409
|
+
const alwaysOk = await this.runHooks(
|
|
410
|
+
this.tasks.afterEachAlways,
|
|
411
|
+
contextRef,
|
|
412
|
+
{
|
|
413
|
+
titleSuffix: hookSuffix,
|
|
414
|
+
testPassed: testOk,
|
|
415
|
+
associatedTaskIndex: task.metadata.taskIndex
|
|
416
|
+
});
|
|
375
417
|
return alwaysOk && hooksOk && testOk;
|
|
376
418
|
}
|
|
377
419
|
|
|
@@ -444,6 +486,8 @@ class Runner extends Emittery {
|
|
|
444
486
|
});
|
|
445
487
|
}
|
|
446
488
|
|
|
489
|
+
await Promise.all(this.waitForReady);
|
|
490
|
+
|
|
447
491
|
if (concurrentTests.length === 0 && serialTests.length === 0) {
|
|
448
492
|
this.emit('finish');
|
|
449
493
|
// Don't run any hooks if there are no tests to run.
|
package/lib/snapshot-manager.js
CHANGED
|
@@ -104,13 +104,32 @@ function combineEntries(entries) {
|
|
|
104
104
|
const buffers = [];
|
|
105
105
|
let byteLength = 0;
|
|
106
106
|
|
|
107
|
-
const sortedKeys = [...entries.keys()].sort()
|
|
107
|
+
const sortedKeys = [...entries.keys()].sort((keyA, keyB) => {
|
|
108
|
+
const [a, b] = [entries.get(keyA), entries.get(keyB)];
|
|
109
|
+
const taskDifference = a.taskIndex - b.taskIndex;
|
|
110
|
+
|
|
111
|
+
if (taskDifference !== 0) {
|
|
112
|
+
return taskDifference;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const [assocA, assocB] = [a.associatedTaskIndex, b.associatedTaskIndex];
|
|
116
|
+
if (assocA !== undefined && assocB !== undefined) {
|
|
117
|
+
const assocDifference = assocA - assocB;
|
|
118
|
+
|
|
119
|
+
if (assocDifference !== 0) {
|
|
120
|
+
return assocDifference;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return a.snapIndex - b.snapIndex;
|
|
125
|
+
});
|
|
126
|
+
|
|
108
127
|
for (const key of sortedKeys) {
|
|
109
128
|
const keyBuffer = Buffer.from(`\n\n## ${key}\n\n`, 'utf8');
|
|
110
129
|
buffers.push(keyBuffer);
|
|
111
130
|
byteLength += keyBuffer.byteLength;
|
|
112
131
|
|
|
113
|
-
const formattedEntries = entries.get(key);
|
|
132
|
+
const formattedEntries = entries.get(key).buffers;
|
|
114
133
|
const last = formattedEntries[formattedEntries.length - 1];
|
|
115
134
|
for (const entry of formattedEntries) {
|
|
116
135
|
buffers.push(entry);
|
|
@@ -176,10 +195,11 @@ function encodeSnapshots(buffersByHash) {
|
|
|
176
195
|
byteOffset += 2;
|
|
177
196
|
|
|
178
197
|
const entries = [];
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
198
|
+
// Maps can't have duplicate keys, so all items in [...buffersByHash.keys()]
|
|
199
|
+
// are unique, so sortedHashes should be deterministic.
|
|
200
|
+
const sortedHashes = [...buffersByHash.keys()].sort();
|
|
201
|
+
const sortedBuffersByHash = [...sortedHashes.map(hash => [hash, buffersByHash.get(hash)])];
|
|
202
|
+
for (const [hash, snapshotBuffers] of sortedBuffersByHash) {
|
|
183
203
|
buffers.push(Buffer.from(hash, 'hex'));
|
|
184
204
|
byteOffset += MD5_HASH_LENGTH;
|
|
185
205
|
|
|
@@ -332,6 +352,7 @@ class Manager {
|
|
|
332
352
|
const descriptor = concordance.describe(options.expected, concordanceOptions);
|
|
333
353
|
const snapshot = concordance.serialize(descriptor);
|
|
334
354
|
const entry = formatEntry(options.label, descriptor);
|
|
355
|
+
const {taskIndex, snapIndex, associatedTaskIndex} = options;
|
|
335
356
|
|
|
336
357
|
return () => { // Must be called in order!
|
|
337
358
|
this.hasChanges = true;
|
|
@@ -353,9 +374,9 @@ class Manager {
|
|
|
353
374
|
snapshots.push(snapshot);
|
|
354
375
|
|
|
355
376
|
if (this.reportEntries.has(options.belongsTo)) {
|
|
356
|
-
this.reportEntries.get(options.belongsTo).push(entry);
|
|
377
|
+
this.reportEntries.get(options.belongsTo).buffers.push(entry);
|
|
357
378
|
} else {
|
|
358
|
-
this.reportEntries.set(options.belongsTo, [entry]);
|
|
379
|
+
this.reportEntries.set(options.belongsTo, {buffers: [entry], taskIndex, snapIndex, associatedTaskIndex});
|
|
359
380
|
}
|
|
360
381
|
};
|
|
361
382
|
}
|
|
@@ -428,12 +449,49 @@ const determineSnapshotDir = mem(({file, fixedLocation, projectDir}) => {
|
|
|
428
449
|
|
|
429
450
|
exports.determineSnapshotDir = determineSnapshotDir;
|
|
430
451
|
|
|
431
|
-
function
|
|
452
|
+
function determineSnapshotPaths({file, fixedLocation, projectDir}) {
|
|
432
453
|
const dir = determineSnapshotDir({file, fixedLocation, projectDir});
|
|
433
454
|
const relFile = path.relative(projectDir, resolveSourceFile(file));
|
|
434
455
|
const name = path.basename(relFile);
|
|
435
456
|
const reportFile = `${name}.md`;
|
|
436
457
|
const snapFile = `${name}.snap`;
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
dir,
|
|
461
|
+
relFile,
|
|
462
|
+
snapFile,
|
|
463
|
+
reportFile
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function cleanFile(file) {
|
|
468
|
+
try {
|
|
469
|
+
fs.unlinkSync(file);
|
|
470
|
+
return [file];
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (error.code === 'ENOENT') {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Remove snapshot and report if they exist. Returns an array containing the
|
|
481
|
+
// paths of the touched files.
|
|
482
|
+
function cleanSnapshots({file, fixedLocation, projectDir}) {
|
|
483
|
+
const {dir, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});
|
|
484
|
+
|
|
485
|
+
return [
|
|
486
|
+
...cleanFile(path.join(dir, snapFile)),
|
|
487
|
+
...cleanFile(path.join(dir, reportFile))
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
exports.cleanSnapshots = cleanSnapshots;
|
|
492
|
+
|
|
493
|
+
function load({file, fixedLocation, projectDir, recordNewSnapshots, updating}) {
|
|
494
|
+
const {dir, relFile, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});
|
|
437
495
|
const snapPath = path.join(dir, snapFile);
|
|
438
496
|
|
|
439
497
|
let appendOnly = !updating;
|
package/lib/test.js
CHANGED
|
@@ -187,7 +187,8 @@ class ExecutionContext extends assert.Assertions {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
get passed() {
|
|
190
|
-
|
|
190
|
+
const test = testMap.get(this);
|
|
191
|
+
return test.isHook ? test.testPassed : !test.assertError;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
_throwsArgStart(assertion, file, line) {
|
|
@@ -229,7 +230,17 @@ class Test {
|
|
|
229
230
|
const index = id ? 0 : this.nextSnapshotIndex++;
|
|
230
231
|
const label = id ? '' : message || `Snapshot ${index + 1}`; // Human-readable labels start counting at 1.
|
|
231
232
|
|
|
232
|
-
const {
|
|
233
|
+
const {taskIndex, associatedTaskIndex} = this.metadata;
|
|
234
|
+
const {record, ...result} = options.compareTestSnapshot({
|
|
235
|
+
belongsTo,
|
|
236
|
+
deferRecording,
|
|
237
|
+
expected,
|
|
238
|
+
index,
|
|
239
|
+
label,
|
|
240
|
+
taskIndex,
|
|
241
|
+
snapIndex: this.snapshotCount,
|
|
242
|
+
associatedTaskIndex
|
|
243
|
+
});
|
|
233
244
|
if (record) {
|
|
234
245
|
this.deferredSnapshotRecordings.push(record);
|
|
235
246
|
}
|
|
@@ -238,6 +249,10 @@ class Test {
|
|
|
238
249
|
};
|
|
239
250
|
|
|
240
251
|
this.skipSnapshot = () => {
|
|
252
|
+
if (typeof options.skipSnapshot === 'function') {
|
|
253
|
+
options.skipSnapshot();
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
if (options.updateSnapshots) {
|
|
242
257
|
this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots'));
|
|
243
258
|
} else {
|
|
@@ -297,11 +312,8 @@ class Test {
|
|
|
297
312
|
};
|
|
298
313
|
}
|
|
299
314
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} else {
|
|
303
|
-
throw new Error('`t.end()` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`');
|
|
304
|
-
}
|
|
315
|
+
const error_ = this.metadata.inline ? new Error('`t.end()` is not supported inside `t.try()`') : new Error('`t.end()` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`');
|
|
316
|
+
throw error_;
|
|
305
317
|
}
|
|
306
318
|
|
|
307
319
|
endCallback(error, savedError) {
|
|
@@ -735,11 +747,7 @@ class Test {
|
|
|
735
747
|
if (this.metadata.failing) {
|
|
736
748
|
passed = !passed;
|
|
737
749
|
|
|
738
|
-
|
|
739
|
-
error = null;
|
|
740
|
-
} else {
|
|
741
|
-
error = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
|
|
742
|
-
}
|
|
750
|
+
error = passed ? null : new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
|
|
743
751
|
}
|
|
744
752
|
|
|
745
753
|
return {
|
package/lib/worker/ipc.js
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
const
|
|
2
|
+
const events = require('events');
|
|
3
|
+
const pEvent = require('p-event');
|
|
3
4
|
const {controlFlow} = require('../ipc-flow-control');
|
|
5
|
+
const {get: getOptions} = require('./options');
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
process.on('message', message => {
|
|
7
|
-
if (!message.ava) {
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
switch (message.ava.type) {
|
|
12
|
-
case 'options':
|
|
13
|
-
emitter.emit('options', message.ava.options);
|
|
14
|
-
break;
|
|
15
|
-
case 'peer-failed':
|
|
16
|
-
emitter.emit('peerFailed');
|
|
17
|
-
break;
|
|
18
|
-
case 'pong':
|
|
19
|
-
emitter.emit('pong');
|
|
20
|
-
break;
|
|
21
|
-
default:
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
7
|
+
const selectAvaMessage = type => message => message.ava && message.ava.type === type;
|
|
25
8
|
|
|
26
|
-
exports.options =
|
|
27
|
-
exports.peerFailed =
|
|
9
|
+
exports.options = pEvent(process, 'message', selectAvaMessage('options')).then(message => message.ava.options);
|
|
10
|
+
exports.peerFailed = pEvent(process, 'message', selectAvaMessage('peer-failed'));
|
|
28
11
|
|
|
29
12
|
const bufferedSend = controlFlow(process);
|
|
30
13
|
function send(evt) {
|
|
@@ -33,18 +16,27 @@ function send(evt) {
|
|
|
33
16
|
|
|
34
17
|
exports.send = send;
|
|
35
18
|
|
|
19
|
+
let refs = 1;
|
|
20
|
+
function ref() {
|
|
21
|
+
if (++refs === 1) {
|
|
22
|
+
process.channel.ref();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
36
26
|
function unref() {
|
|
37
|
-
|
|
27
|
+
if (refs > 0 && --refs === 0) {
|
|
28
|
+
process.channel.unref();
|
|
29
|
+
}
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
exports.unref = unref;
|
|
41
33
|
|
|
42
34
|
let pendingPings = Promise.resolve();
|
|
43
35
|
async function flush() {
|
|
44
|
-
|
|
36
|
+
ref();
|
|
45
37
|
const promise = pendingPings.then(async () => { // eslint-disable-line promise/prefer-await-to-then
|
|
46
38
|
send({type: 'ping'});
|
|
47
|
-
await
|
|
39
|
+
await pEvent(process, 'message', selectAvaMessage('pong'));
|
|
48
40
|
if (promise === pendingPings) {
|
|
49
41
|
unref();
|
|
50
42
|
}
|
|
@@ -54,3 +46,156 @@ async function flush() {
|
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
exports.flush = flush;
|
|
49
|
+
|
|
50
|
+
let channelCounter = 0;
|
|
51
|
+
let messageCounter = 0;
|
|
52
|
+
|
|
53
|
+
const channelEmitters = new Map();
|
|
54
|
+
function createChannelEmitter(channelId) {
|
|
55
|
+
if (channelEmitters.size === 0) {
|
|
56
|
+
process.on('message', message => {
|
|
57
|
+
if (!message.ava) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const {channelId, type, ...payload} = message.ava;
|
|
62
|
+
if (
|
|
63
|
+
type === 'shared-worker-error' ||
|
|
64
|
+
type === 'shared-worker-message' ||
|
|
65
|
+
type === 'shared-worker-ready'
|
|
66
|
+
) {
|
|
67
|
+
const emitter = channelEmitters.get(channelId);
|
|
68
|
+
if (emitter !== undefined) {
|
|
69
|
+
emitter.emit(type, payload);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const emitter = new events.EventEmitter();
|
|
76
|
+
channelEmitters.set(channelId, emitter);
|
|
77
|
+
return [emitter, () => channelEmitters.delete(channelId)];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function registerSharedWorker(filename, initialData) {
|
|
81
|
+
const channelId = `${getOptions().forkId}/channel/${++channelCounter}`;
|
|
82
|
+
const [channelEmitter, unsubscribe] = createChannelEmitter(channelId);
|
|
83
|
+
|
|
84
|
+
let forcedUnref = false;
|
|
85
|
+
let refs = 0;
|
|
86
|
+
const forceUnref = () => {
|
|
87
|
+
if (forcedUnref) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
forcedUnref = true;
|
|
92
|
+
if (refs > 0) {
|
|
93
|
+
unref();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const refChannel = () => {
|
|
98
|
+
if (!forcedUnref && ++refs === 1) {
|
|
99
|
+
ref();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const unrefChannel = () => {
|
|
104
|
+
if (!forcedUnref && refs > 0 && --refs === 0) {
|
|
105
|
+
unref();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
send({
|
|
110
|
+
type: 'shared-worker-connect',
|
|
111
|
+
channelId,
|
|
112
|
+
filename,
|
|
113
|
+
initialData
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let currentlyAvailable = false;
|
|
117
|
+
let error = null;
|
|
118
|
+
|
|
119
|
+
refChannel();
|
|
120
|
+
const ready = pEvent(channelEmitter, 'shared-worker-ready').then(() => { // eslint-disable-line promise/prefer-await-to-then
|
|
121
|
+
currentlyAvailable = error === null;
|
|
122
|
+
}).finally(unrefChannel);
|
|
123
|
+
|
|
124
|
+
const messageEmitters = new Set();
|
|
125
|
+
const handleMessage = message => {
|
|
126
|
+
// Wait for a turn of the event loop, to allow new subscriptions to be set
|
|
127
|
+
// up in response to the previous message.
|
|
128
|
+
setImmediate(() => {
|
|
129
|
+
for (const emitter of messageEmitters) {
|
|
130
|
+
emitter.emit('message', message);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
channelEmitter.on('shared-worker-message', handleMessage);
|
|
136
|
+
|
|
137
|
+
pEvent(channelEmitter, 'shared-worker-error').then(() => { // eslint-disable-line promise/prefer-await-to-then
|
|
138
|
+
unsubscribe();
|
|
139
|
+
forceUnref();
|
|
140
|
+
|
|
141
|
+
error = new Error('The shared worker is no longer available');
|
|
142
|
+
currentlyAvailable = false;
|
|
143
|
+
for (const emitter of messageEmitters) {
|
|
144
|
+
emitter.emit('error', error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
forceUnref,
|
|
150
|
+
ready,
|
|
151
|
+
channel: {
|
|
152
|
+
available: ready,
|
|
153
|
+
|
|
154
|
+
get currentlyAvailable() {
|
|
155
|
+
return currentlyAvailable;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async * receive() {
|
|
159
|
+
if (error !== null) {
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const emitter = new events.EventEmitter();
|
|
164
|
+
messageEmitters.add(emitter);
|
|
165
|
+
try {
|
|
166
|
+
refChannel();
|
|
167
|
+
for await (const [message] of events.on(emitter, 'message')) {
|
|
168
|
+
yield message;
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
unrefChannel();
|
|
172
|
+
messageEmitters.delete(emitter);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
post(serializedData, replyTo) {
|
|
177
|
+
if (error !== null) {
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!currentlyAvailable) {
|
|
182
|
+
throw new Error('Shared worker is not yet available');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const messageId = `${channelId}/message/${++messageCounter}`;
|
|
186
|
+
send({
|
|
187
|
+
type: 'shared-worker-message',
|
|
188
|
+
channelId,
|
|
189
|
+
messageId,
|
|
190
|
+
replyTo,
|
|
191
|
+
serializedData
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return messageId;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
exports.registerSharedWorker = registerSharedWorker;
|
|
201
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const v8 = require('v8');
|
|
2
|
+
const pkg = require('../../package.json');
|
|
3
|
+
const subprocess = require('./subprocess');
|
|
4
|
+
const options = require('./options');
|
|
5
|
+
|
|
6
|
+
const workers = new Map();
|
|
7
|
+
const workerTeardownFns = new WeakMap();
|
|
8
|
+
|
|
9
|
+
function createSharedWorker(filename, initialData, teardown) {
|
|
10
|
+
const channel = subprocess.registerSharedWorker(filename, initialData, teardown);
|
|
11
|
+
|
|
12
|
+
class ReceivedMessage {
|
|
13
|
+
constructor(id, serializedData) {
|
|
14
|
+
this.id = id;
|
|
15
|
+
this.data = v8.deserialize(new Uint8Array(serializedData));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
reply(data) {
|
|
19
|
+
return publishMessage(data, this.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Ensure that, no matter how often it's received, we have a stable message
|
|
24
|
+
// object.
|
|
25
|
+
const messageCache = new WeakMap();
|
|
26
|
+
async function * receiveMessages(replyTo) {
|
|
27
|
+
for await (const evt of channel.receive()) {
|
|
28
|
+
if (replyTo === undefined && evt.replyTo !== undefined) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (replyTo !== undefined && evt.replyTo !== replyTo) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let message = messageCache.get(evt);
|
|
37
|
+
if (message === undefined) {
|
|
38
|
+
message = new ReceivedMessage(evt.messageId, evt.serializedData);
|
|
39
|
+
messageCache.set(evt, message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
yield message;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function publishMessage(data, replyTo) {
|
|
47
|
+
const id = channel.post([...v8.serialize(data)], replyTo);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
async * replies() {
|
|
52
|
+
yield * receiveMessages(id);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
available: channel.available,
|
|
59
|
+
protocol: 'experimental',
|
|
60
|
+
|
|
61
|
+
get currentlyAvailable() {
|
|
62
|
+
return channel.currentlyAvailable;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
publish(data) {
|
|
66
|
+
return publishMessage(data);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async * subscribe() {
|
|
70
|
+
yield * receiveMessages();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const supportsSharedWorkers = process.versions.node >= '12.17.0';
|
|
76
|
+
|
|
77
|
+
function registerSharedWorker({
|
|
78
|
+
filename,
|
|
79
|
+
initialData,
|
|
80
|
+
supportedProtocols,
|
|
81
|
+
teardown
|
|
82
|
+
}) {
|
|
83
|
+
if (!options.get().experiments.sharedWorkers) {
|
|
84
|
+
throw new Error('Shared workers are experimental. Opt in to them in your AVA configuration');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!supportsSharedWorkers) {
|
|
88
|
+
throw new Error('Shared workers require Node.js 12.17 or newer');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!supportedProtocols.includes('experimental')) {
|
|
92
|
+
throw new Error(`This version of AVA (${pkg.version}) does not support any of the desired shared worker protocols: ${supportedProtocols.join()}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let worker = workers.get(filename);
|
|
96
|
+
if (worker === undefined) {
|
|
97
|
+
worker = createSharedWorker(filename, initialData, async () => {
|
|
98
|
+
// Run possibly asynchronous teardown functions serially, in reverse
|
|
99
|
+
// order. Any error will crash the worker.
|
|
100
|
+
const teardownFns = workerTeardownFns.get(worker);
|
|
101
|
+
if (teardownFns !== undefined) {
|
|
102
|
+
for await (const fn of [...teardownFns].reverse()) {
|
|
103
|
+
await fn();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
workers.set(filename, worker);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (teardown !== undefined) {
|
|
111
|
+
if (workerTeardownFns.has(worker)) {
|
|
112
|
+
workerTeardownFns.get(worker).push(teardown);
|
|
113
|
+
} else {
|
|
114
|
+
workerTeardownFns.set(worker, [teardown]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return worker;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
exports.registerSharedWorker = registerSharedWorker;
|