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/load-config.js
CHANGED
|
@@ -1,27 +1,48 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const url = require('url');
|
|
4
5
|
const vm = require('vm');
|
|
5
|
-
const isPlainObject = require('is-plain-object');
|
|
6
|
+
const {isPlainObject} = require('is-plain-object');
|
|
6
7
|
const pkgConf = require('pkg-conf');
|
|
7
8
|
|
|
8
9
|
const NO_SUCH_FILE = Symbol('no ava.config.js file');
|
|
9
10
|
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
|
|
10
|
-
const EXPERIMENTS = new Set([
|
|
11
|
+
const EXPERIMENTS = new Set([
|
|
12
|
+
'configurableModuleFormat',
|
|
13
|
+
'disableNullExpectations',
|
|
14
|
+
'disableSnapshotsInHooks',
|
|
15
|
+
'nextGenConfig',
|
|
16
|
+
'reverseTeardowns',
|
|
17
|
+
'sharedWorkers'
|
|
18
|
+
]);
|
|
11
19
|
|
|
12
20
|
// *Very* rudimentary support for loading ava.config.js files containing an `export default` statement.
|
|
13
|
-
const evaluateJsConfig = configFile => {
|
|
14
|
-
const
|
|
15
|
-
const script = new vm.Script(`'use strict';(()=>{let __export__;\n${contents.replace(/export default/g, '__export__ =')};return __export__;})()`, {
|
|
21
|
+
const evaluateJsConfig = (contents, configFile) => {
|
|
22
|
+
const script = new vm.Script(`'use strict';(()=>{let __export__;\n${contents.toString('utf8').replace(/export default/g, '__export__ =')};return __export__;})()`, {
|
|
16
23
|
filename: configFile,
|
|
17
24
|
lineOffset: -1
|
|
18
25
|
});
|
|
19
|
-
return
|
|
20
|
-
default: script.runInThisContext()
|
|
21
|
-
};
|
|
26
|
+
return script.runInThisContext();
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
const
|
|
29
|
+
const importConfig = async ({configFile, fileForErrorMessage}) => {
|
|
30
|
+
let module;
|
|
31
|
+
try {
|
|
32
|
+
module = await import(url.pathToFileURL(configFile)); // eslint-disable-line node/no-unsupported-features/es-syntax
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw Object.assign(new Error(`Error loading ${fileForErrorMessage}: ${error.message}`), {parent: error});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {default: config = MISSING_DEFAULT_EXPORT} = module;
|
|
38
|
+
if (config === MISSING_DEFAULT_EXPORT) {
|
|
39
|
+
throw new Error(`${fileForErrorMessage} must have a default export`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return config;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const loadJsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.config.js')}, useImport = false) => {
|
|
25
46
|
if (!configFile.endsWith('.js')) {
|
|
26
47
|
return null;
|
|
27
48
|
}
|
|
@@ -30,7 +51,10 @@ const loadJsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.confi
|
|
|
30
51
|
|
|
31
52
|
let config;
|
|
32
53
|
try {
|
|
33
|
-
|
|
54
|
+
const contents = fs.readFileSync(configFile);
|
|
55
|
+
config = useImport && contents.includes('nonSemVerExperiments') && contents.includes('nextGenConfig') ?
|
|
56
|
+
importConfig({configFile, fileForErrorMessage}) :
|
|
57
|
+
evaluateJsConfig(contents, configFile) || MISSING_DEFAULT_EXPORT;
|
|
34
58
|
} catch (error) {
|
|
35
59
|
if (error.code === 'ENOENT') {
|
|
36
60
|
return null;
|
|
@@ -63,14 +87,17 @@ const loadCjsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.conf
|
|
|
63
87
|
}
|
|
64
88
|
};
|
|
65
89
|
|
|
66
|
-
const loadMjsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.config.mjs')}) => {
|
|
90
|
+
const loadMjsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.config.mjs')}, experimentally = false) => {
|
|
67
91
|
if (!configFile.endsWith('.mjs')) {
|
|
68
92
|
return null;
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
const fileForErrorMessage = path.relative(projectDir, configFile);
|
|
72
96
|
try {
|
|
73
|
-
fs.readFileSync(configFile);
|
|
97
|
+
const contents = fs.readFileSync(configFile);
|
|
98
|
+
if (experimentally && contents.includes('nonSemVerExperiments') && contents.includes('nextGenConfig')) {
|
|
99
|
+
return {config: importConfig({configFile, fileForErrorMessage}), fileForErrorMessage};
|
|
100
|
+
}
|
|
74
101
|
} catch (error) {
|
|
75
102
|
if (error.code === 'ENOENT') {
|
|
76
103
|
return null;
|
|
@@ -82,11 +109,7 @@ const loadMjsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.conf
|
|
|
82
109
|
throw new Error(`AVA cannot yet load ${fileForErrorMessage} files`);
|
|
83
110
|
};
|
|
84
111
|
|
|
85
|
-
function
|
|
86
|
-
let packageConf = pkgConf.sync('ava', {cwd: resolveFrom});
|
|
87
|
-
const filepath = pkgConf.filepath(packageConf);
|
|
88
|
-
const projectDir = filepath === null ? resolveFrom : path.dirname(filepath);
|
|
89
|
-
|
|
112
|
+
function resolveConfigFile(projectDir, configFile) {
|
|
90
113
|
if (configFile) {
|
|
91
114
|
configFile = path.resolve(configFile); // Relative to CWD
|
|
92
115
|
if (path.basename(configFile) !== path.relative(projectDir, configFile)) {
|
|
@@ -98,6 +121,15 @@ function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {
|
|
|
98
121
|
}
|
|
99
122
|
}
|
|
100
123
|
|
|
124
|
+
return configFile;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function loadConfigSync({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) {
|
|
128
|
+
let packageConf = pkgConf.sync('ava', {cwd: resolveFrom});
|
|
129
|
+
const filepath = pkgConf.filepath(packageConf);
|
|
130
|
+
const projectDir = filepath === null ? resolveFrom : path.dirname(filepath);
|
|
131
|
+
|
|
132
|
+
configFile = resolveConfigFile(projectDir, configFile);
|
|
101
133
|
const allowConflictWithPackageJson = Boolean(configFile);
|
|
102
134
|
|
|
103
135
|
let [{config: fileConf, fileForErrorMessage} = {config: NO_SUCH_FILE, fileForErrorMessage: undefined}, ...conflicting] = [
|
|
@@ -157,4 +189,79 @@ function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {
|
|
|
157
189
|
return config;
|
|
158
190
|
}
|
|
159
191
|
|
|
160
|
-
|
|
192
|
+
exports.loadConfigSync = loadConfigSync;
|
|
193
|
+
|
|
194
|
+
async function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) {
|
|
195
|
+
let packageConf = await pkgConf('ava', {cwd: resolveFrom});
|
|
196
|
+
const filepath = pkgConf.filepath(packageConf);
|
|
197
|
+
const projectDir = filepath === null ? resolveFrom : path.dirname(filepath);
|
|
198
|
+
|
|
199
|
+
configFile = resolveConfigFile(projectDir, configFile);
|
|
200
|
+
const allowConflictWithPackageJson = Boolean(configFile);
|
|
201
|
+
|
|
202
|
+
// TODO: Refactor resolution logic to implement https://github.com/avajs/ava/issues/2285.
|
|
203
|
+
let [{config: fileConf, fileForErrorMessage} = {config: NO_SUCH_FILE, fileForErrorMessage: undefined}, ...conflicting] = [
|
|
204
|
+
loadJsConfig({projectDir, configFile}, true),
|
|
205
|
+
loadCjsConfig({projectDir, configFile}),
|
|
206
|
+
loadMjsConfig({projectDir, configFile}, true)
|
|
207
|
+
].filter(result => result !== null);
|
|
208
|
+
|
|
209
|
+
if (conflicting.length > 0) {
|
|
210
|
+
throw new Error(`Conflicting configuration in ${fileForErrorMessage} and ${conflicting.map(({fileForErrorMessage}) => fileForErrorMessage).join(' & ')}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let sawPromise = false;
|
|
214
|
+
if (fileConf !== NO_SUCH_FILE) {
|
|
215
|
+
if (allowConflictWithPackageJson) {
|
|
216
|
+
packageConf = {};
|
|
217
|
+
} else if (Object.keys(packageConf).length > 0) {
|
|
218
|
+
throw new Error(`Conflicting configuration in ${fileForErrorMessage} and package.json`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then
|
|
222
|
+
sawPromise = true;
|
|
223
|
+
fileConf = await fileConf;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isPlainObject(fileConf) && typeof fileConf !== 'function') {
|
|
227
|
+
throw new TypeError(`${fileForErrorMessage} must export a plain object or factory function`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof fileConf === 'function') {
|
|
231
|
+
fileConf = fileConf({projectDir});
|
|
232
|
+
if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then
|
|
233
|
+
sawPromise = true;
|
|
234
|
+
fileConf = await fileConf;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!isPlainObject(fileConf)) {
|
|
238
|
+
throw new TypeError(`Factory method exported by ${fileForErrorMessage} must return a plain object`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ('ava' in fileConf) {
|
|
243
|
+
throw new Error(`Encountered ’ava’ property in ${fileForErrorMessage}; avoid wrapping the configuration`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const config = {...defaults, nonSemVerExperiments: {}, ...fileConf, ...packageConf, projectDir};
|
|
248
|
+
|
|
249
|
+
const {nonSemVerExperiments: experiments} = config;
|
|
250
|
+
if (!isPlainObject(experiments)) {
|
|
251
|
+
throw new Error(`nonSemVerExperiments from ${fileForErrorMessage} must be an object`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const key of Object.keys(experiments)) {
|
|
255
|
+
if (!EXPERIMENTS.has(key)) {
|
|
256
|
+
throw new Error(`nonSemVerExperiments.${key} from ${fileForErrorMessage} is not a supported experiment`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (sawPromise && experiments.nextGenConfig !== true) {
|
|
261
|
+
throw new Error(`${fileForErrorMessage} exported a promise or an asynchronous factory function. You must enable the ’asyncConfigurationLoading’ experiment for this to work.`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return config;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
exports.loadConfig = loadConfig;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const {EventEmitter, on} = require('events');
|
|
2
|
+
const v8 = require('v8');
|
|
3
|
+
const {workerData, parentPort} = require('worker_threads');
|
|
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,140 @@
|
|
|
1
|
+
const events = require('events');
|
|
2
|
+
const serializeError = require('../serialize-error');
|
|
3
|
+
|
|
4
|
+
let Worker;
|
|
5
|
+
try {
|
|
6
|
+
({Worker} = require('worker_threads'));
|
|
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
|
+
worker.setMaxListeners(0);
|
|
38
|
+
|
|
39
|
+
const launched = {
|
|
40
|
+
statePromises: {
|
|
41
|
+
available: waitForAvailable(worker),
|
|
42
|
+
error: events.once(worker, 'error').then(([error]) => error) // eslint-disable-line promise/prefer-await-to-then
|
|
43
|
+
},
|
|
44
|
+
exited: false,
|
|
45
|
+
worker
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
launchedWorkers.set(filename, launched);
|
|
49
|
+
worker.once('exit', () => {
|
|
50
|
+
launched.exited = true;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return launched;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function observeWorkerProcess(fork, runStatus) {
|
|
57
|
+
let registrationCount = 0;
|
|
58
|
+
let signalDeregistered;
|
|
59
|
+
const deregistered = new Promise(resolve => {
|
|
60
|
+
signalDeregistered = resolve;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
fork.promise.finally(() => {
|
|
64
|
+
if (registrationCount === 0) {
|
|
65
|
+
signalDeregistered();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
fork.onConnectSharedWorker(async channel => {
|
|
70
|
+
const launched = launchWorker(channel);
|
|
71
|
+
|
|
72
|
+
const handleChannelMessage = ({messageId, replyTo, serializedData}) => {
|
|
73
|
+
launched.worker.postMessage({
|
|
74
|
+
type: 'message',
|
|
75
|
+
testWorkerId: fork.forkId,
|
|
76
|
+
messageId,
|
|
77
|
+
replyTo,
|
|
78
|
+
serializedData
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleWorkerMessage = async message => {
|
|
83
|
+
if (message.type === 'broadcast' || (message.type === 'message' && message.testWorkerId === fork.forkId)) {
|
|
84
|
+
const {messageId, replyTo, serializedData} = message;
|
|
85
|
+
channel.forwardMessageToFork({messageId, replyTo, serializedData});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (message.type === 'deregistered-test-worker' && message.id === fork.forkId) {
|
|
89
|
+
launched.worker.off('message', handleWorkerMessage);
|
|
90
|
+
|
|
91
|
+
registrationCount--;
|
|
92
|
+
if (registrationCount === 0) {
|
|
93
|
+
signalDeregistered();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
launched.statePromises.error.then(error => { // eslint-disable-line promise/prefer-await-to-then
|
|
99
|
+
signalDeregistered();
|
|
100
|
+
launched.worker.off('message', handleWorkerMessage);
|
|
101
|
+
runStatus.emitStateChange({type: 'shared-worker-error', err: serializeError('Shared worker error', true, error)});
|
|
102
|
+
channel.signalError();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await launched.statePromises.available;
|
|
107
|
+
|
|
108
|
+
registrationCount++;
|
|
109
|
+
launched.worker.postMessage({
|
|
110
|
+
type: 'register-test-worker',
|
|
111
|
+
id: fork.forkId,
|
|
112
|
+
file: fork.file
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
fork.promise.finally(() => {
|
|
116
|
+
launched.worker.postMessage({
|
|
117
|
+
type: 'deregister-test-worker',
|
|
118
|
+
id: fork.forkId
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
channel.off('message', handleChannelMessage);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
launched.worker.on('message', handleWorkerMessage);
|
|
125
|
+
channel.on('message', handleChannelMessage);
|
|
126
|
+
channel.signalReady();
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
} finally {
|
|
130
|
+
// Attaching listeners has the side-effect of referencing the worker.
|
|
131
|
+
// Explicitly unreference it now so it does not prevent the main process
|
|
132
|
+
// from exiting.
|
|
133
|
+
launched.worker.unref();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return deregistered;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
exports.observeWorkerProcess = observeWorkerProcess;
|
package/lib/reporters/default.js
CHANGED
|
@@ -195,6 +195,7 @@ class Reporter {
|
|
|
195
195
|
this.internalErrors = [];
|
|
196
196
|
this.knownFailures = [];
|
|
197
197
|
this.lineNumberErrors = [];
|
|
198
|
+
this.sharedWorkerErrors = [];
|
|
198
199
|
this.uncaughtExceptions = [];
|
|
199
200
|
this.unhandledRejections = [];
|
|
200
201
|
this.unsavedSnapshots = [];
|
|
@@ -340,6 +341,19 @@ class Reporter {
|
|
|
340
341
|
break;
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
case 'shared-worker-error': {
|
|
345
|
+
this.sharedWorkerErrors.push(event);
|
|
346
|
+
|
|
347
|
+
if (this.verbose) {
|
|
348
|
+
this.lineWriter.ensureEmptyLine();
|
|
349
|
+
this.lineWriter.writeLine(colors.error(`${figures.cross} Error in shared worker`));
|
|
350
|
+
this.lineWriter.writeLine();
|
|
351
|
+
this.writeErr(event);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
|
|
343
357
|
case 'snapshot-error':
|
|
344
358
|
this.unsavedSnapshots.push(event);
|
|
345
359
|
break;
|
|
@@ -710,7 +724,7 @@ class Reporter {
|
|
|
710
724
|
}
|
|
711
725
|
|
|
712
726
|
if (this.failures.length > 0) {
|
|
713
|
-
const writeTrailingLines = this.internalErrors.length > 0 || this.uncaughtExceptions.length > 0 || this.unhandledRejections.length > 0;
|
|
727
|
+
const writeTrailingLines = this.internalErrors.length > 0 || this.sharedWorkerErrors.length > 0 || this.uncaughtExceptions.length > 0 || this.unhandledRejections.length > 0;
|
|
714
728
|
|
|
715
729
|
const lastFailure = this.failures[this.failures.length - 1];
|
|
716
730
|
for (const event of this.failures) {
|
|
@@ -734,7 +748,7 @@ class Reporter {
|
|
|
734
748
|
|
|
735
749
|
if (!this.verbose) {
|
|
736
750
|
if (this.internalErrors.length > 0) {
|
|
737
|
-
const writeTrailingLines = this.uncaughtExceptions.length > 0 || this.unhandledRejections.length > 0;
|
|
751
|
+
const writeTrailingLines = this.sharedWorkerErrors.length > 0 || this.uncaughtExceptions.length > 0 || this.unhandledRejections.length > 0;
|
|
738
752
|
|
|
739
753
|
const last = this.internalErrors[this.internalErrors.length - 1];
|
|
740
754
|
for (const event of this.internalErrors) {
|
|
@@ -756,6 +770,23 @@ class Reporter {
|
|
|
756
770
|
}
|
|
757
771
|
}
|
|
758
772
|
|
|
773
|
+
if (this.sharedWorkerErrors.length > 0) {
|
|
774
|
+
const writeTrailingLines = this.uncaughtExceptions.length > 0 || this.unhandledRejections.length > 0;
|
|
775
|
+
|
|
776
|
+
const last = this.sharedWorkerErrors[this.sharedWorkerErrors.length - 1];
|
|
777
|
+
for (const evt of this.sharedWorkerErrors) {
|
|
778
|
+
this.lineWriter.writeLine(colors.error(`${figures.cross} Error in shared worker`));
|
|
779
|
+
this.lineWriter.writeLine();
|
|
780
|
+
this.writeErr(evt.err);
|
|
781
|
+
if (evt !== last || writeTrailingLines) {
|
|
782
|
+
this.lineWriter.writeLine();
|
|
783
|
+
this.lineWriter.writeLine();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
wroteSomething = true;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
759
790
|
if (this.uncaughtExceptions.length > 0) {
|
|
760
791
|
const writeTrailingLines = this.unhandledRejections.length > 0;
|
|
761
792
|
|
package/lib/reporters/tap.js
CHANGED
|
@@ -125,12 +125,22 @@ class TapReporter {
|
|
|
125
125
|
this.reportStream.write(`# ${stripAnsi(title)}${os.EOL}`);
|
|
126
126
|
if (evt.logs) {
|
|
127
127
|
for (const log of evt.logs) {
|
|
128
|
-
const logLines = indentString(log, 4).replace(/^ {4}
|
|
128
|
+
const logLines = indentString(log, 4).replace(/^ {4}/gm, '# ');
|
|
129
129
|
this.reportStream.write(`${logLines}${os.EOL}`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
writeTimeout(evt) {
|
|
135
|
+
const err = new Error(`Exited because no new tests completed within the last ${evt.period}ms of inactivity`);
|
|
136
|
+
|
|
137
|
+
for (const [testFile, tests] of evt.pendingTests) {
|
|
138
|
+
for (const title of tests) {
|
|
139
|
+
this.writeTest({testFile, title, err}, {passed: false, todo: false, skip: false});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
consumeStateChange(evt) { // eslint-disable-line complexity
|
|
135
145
|
const fileStats = this.stats && evt.testFile ? this.stats.byFile.get(evt.testFile) : null;
|
|
136
146
|
|
|
@@ -172,7 +182,7 @@ class TapReporter {
|
|
|
172
182
|
this.writeTest(evt, {passed: true, todo: false, skip: false});
|
|
173
183
|
break;
|
|
174
184
|
case 'timeout':
|
|
175
|
-
this.
|
|
185
|
+
this.writeTimeout(evt);
|
|
176
186
|
break;
|
|
177
187
|
case 'uncaught-exception':
|
|
178
188
|
this.writeCrash(evt);
|