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.
@@ -1,26 +1,25 @@
1
1
  'use strict';
2
- const normalizeExtensions = require('./lib/extensions');
2
+ let isMainThread = true;
3
+ let supportsWorkers = false;
4
+ try {
5
+ ({isMainThread} = require('worker_threads'));
6
+ supportsWorkers = true;
7
+ } catch {}
8
+
3
9
  const {classify, hasExtension, isHelperish, matches, normalizeFileForMatching, normalizeGlobs, normalizePatterns} = require('./lib/globs');
4
- const loadConfig = require('./lib/load-config');
5
- const providerManager = require('./lib/provider-manager');
6
10
 
7
- const configCache = new Map();
8
- const helperCache = new Map();
11
+ let resolveGlobs;
12
+ let resolveGlobsSync;
9
13
 
10
- function load(projectDir, overrides) {
11
- const cacheKey = `${JSON.stringify(overrides)}\n${projectDir}`;
12
- if (helperCache.has(cacheKey)) {
13
- return helperCache.get(cacheKey);
14
- }
14
+ if (!supportsWorkers || !isMainThread) {
15
+ const normalizeExtensions = require('./lib/extensions');
16
+ const {loadConfig, loadConfigSync} = require('./lib/load-config');
17
+ const providerManager = require('./lib/provider-manager');
15
18
 
16
- let conf;
17
- let providers;
18
- if (configCache.has(projectDir)) {
19
- ({conf, providers} = configCache.get(projectDir));
20
- } else {
21
- conf = loadConfig({resolveFrom: projectDir});
19
+ const configCache = new Map();
22
20
 
23
- providers = [];
21
+ const collectProviders = ({conf, projectDir}) => {
22
+ const providers = [];
24
23
  if (Reflect.has(conf, 'babel')) {
25
24
  const {level, main} = providerManager.babel(projectDir);
26
25
  providers.push({
@@ -39,12 +38,125 @@ function load(projectDir, overrides) {
39
38
  });
40
39
  }
41
40
 
42
- configCache.set(projectDir, {conf, providers});
41
+ return providers;
42
+ };
43
+
44
+ const buildGlobs = ({conf, providers, projectDir, overrideExtensions, overrideFiles}) => {
45
+ const extensions = overrideExtensions ?
46
+ normalizeExtensions(overrideExtensions) :
47
+ normalizeExtensions(conf.extensions, providers);
48
+
49
+ return {
50
+ cwd: projectDir,
51
+ ...normalizeGlobs({
52
+ extensions,
53
+ files: overrideFiles ? overrideFiles : conf.files,
54
+ providers
55
+ })
56
+ };
57
+ };
58
+
59
+ resolveGlobsSync = (projectDir, overrideExtensions, overrideFiles) => {
60
+ if (!configCache.has(projectDir)) {
61
+ const conf = loadConfigSync({resolveFrom: projectDir});
62
+ const providers = collectProviders({conf, projectDir});
63
+ configCache.set(projectDir, {conf, providers});
64
+ }
65
+
66
+ const {conf, providers} = configCache.get(projectDir);
67
+ return buildGlobs({conf, providers, projectDir, overrideExtensions, overrideFiles});
68
+ };
69
+
70
+ resolveGlobs = async (projectDir, overrideExtensions, overrideFiles) => {
71
+ if (!configCache.has(projectDir)) {
72
+ configCache.set(projectDir, loadConfig({resolveFrom: projectDir}).then(conf => { // eslint-disable-line promise/prefer-await-to-then
73
+ const providers = collectProviders({conf, projectDir});
74
+ return {conf, providers};
75
+ }));
76
+ }
77
+
78
+ const {conf, providers} = await configCache.get(projectDir);
79
+ return buildGlobs({conf, providers, projectDir, overrideExtensions, overrideFiles});
80
+ };
81
+ }
82
+
83
+ if (supportsWorkers) {
84
+ const v8 = require('v8');
85
+
86
+ const MAX_DATA_LENGTH_EXCLUSIVE = 100 * 1024; // Allocate 100 KiB to exchange globs.
87
+
88
+ if (isMainThread) {
89
+ const {Worker} = require('worker_threads');
90
+ let data;
91
+ let sync;
92
+ let worker;
93
+
94
+ resolveGlobsSync = (projectDir, overrideExtensions, overrideFiles) => {
95
+ if (worker === undefined) {
96
+ const dataBuffer = new SharedArrayBuffer(MAX_DATA_LENGTH_EXCLUSIVE);
97
+ data = new Uint8Array(dataBuffer);
98
+
99
+ const syncBuffer = new SharedArrayBuffer(4);
100
+ sync = new Int32Array(syncBuffer);
101
+
102
+ worker = new Worker(__filename, {
103
+ workerData: {
104
+ dataBuffer,
105
+ syncBuffer,
106
+ firstMessage: {projectDir, overrideExtensions, overrideFiles}
107
+ }
108
+ });
109
+ worker.unref();
110
+ } else {
111
+ worker.postMessage({projectDir, overrideExtensions, overrideFiles});
112
+ }
113
+
114
+ Atomics.wait(sync, 0, 0);
115
+
116
+ const byteLength = Atomics.exchange(sync, 0, 0);
117
+ if (byteLength === MAX_DATA_LENGTH_EXCLUSIVE) {
118
+ throw new Error('Globs are over 100 KiB and cannot be resolved');
119
+ }
120
+
121
+ const globsOrError = v8.deserialize(data.slice(0, byteLength));
122
+ if (globsOrError instanceof Error) {
123
+ throw globsOrError;
124
+ }
125
+
126
+ return globsOrError;
127
+ };
128
+ } else {
129
+ const {parentPort, workerData} = require('worker_threads');
130
+ const data = new Uint8Array(workerData.dataBuffer);
131
+ const sync = new Int32Array(workerData.syncBuffer);
132
+
133
+ const handleMessage = async ({projectDir, overrideExtensions, overrideFiles}) => {
134
+ let encoded;
135
+ try {
136
+ const globs = await resolveGlobs(projectDir, overrideExtensions, overrideFiles);
137
+ encoded = v8.serialize(globs);
138
+ } catch (error) {
139
+ encoded = v8.serialize(error);
140
+ }
141
+
142
+ const byteLength = encoded.length < MAX_DATA_LENGTH_EXCLUSIVE ? encoded.copy(data) : MAX_DATA_LENGTH_EXCLUSIVE;
143
+ Atomics.store(sync, 0, byteLength);
144
+ Atomics.notify(sync, 0);
145
+ };
146
+
147
+ parentPort.on('message', handleMessage);
148
+ handleMessage(workerData.firstMessage);
149
+ delete workerData.firstMessage;
43
150
  }
151
+ }
152
+
153
+ const helperCache = new Map();
44
154
 
45
- const extensions = overrides && overrides.extensions ?
46
- normalizeExtensions(overrides.extensions) :
47
- normalizeExtensions(conf.extensions, providers);
155
+ function load(projectDir, overrides) {
156
+ const cacheKey = `${JSON.stringify(overrides)}\n${projectDir}`;
157
+ if (helperCache.has(cacheKey)) {
158
+ return helperCache.get(cacheKey);
159
+ }
48
160
 
49
161
  let helperPatterns = [];
50
162
  if (overrides && overrides.helpers !== undefined) {
@@ -55,14 +167,7 @@ function load(projectDir, overrides) {
55
167
  helperPatterns = normalizePatterns(overrides.helpers);
56
168
  }
57
169
 
58
- const globs = {
59
- cwd: projectDir,
60
- ...normalizeGlobs({
61
- extensions,
62
- files: overrides && overrides.files ? overrides.files : conf.files,
63
- providers
64
- })
65
- };
170
+ const globs = resolveGlobsSync(projectDir, overrides && overrides.extensions, overrides && overrides.files);
66
171
 
67
172
  const classifyForESLint = file => {
68
173
  const {isTest} = classify(file, globs);
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 => {
@@ -110,21 +111,15 @@ class Api extends Emittery {
110
111
  }
111
112
  };
112
113
 
113
- let cacheDir;
114
114
  let testFiles;
115
115
  try {
116
- cacheDir = this._createCacheDir();
117
116
  testFiles = await globs.findTests({cwd: this.options.projectDir, ...apiOptions.globs});
118
117
  if (selectedFiles.length === 0) {
119
- if (filter.length === 0) {
120
- selectedFiles = testFiles;
121
- } else {
122
- selectedFiles = globs.applyTestFileFilter({
123
- cwd: this.options.projectDir,
124
- filter: filter.map(({pattern}) => pattern),
125
- testFiles
126
- });
127
- }
118
+ selectedFiles = filter.length === 0 ? testFiles : globs.applyTestFileFilter({
119
+ cwd: this.options.projectDir,
120
+ filter: filter.map(({pattern}) => pattern),
121
+ testFiles
122
+ });
128
123
  }
129
124
  } catch (error) {
130
125
  selectedFiles = [];
@@ -192,7 +187,7 @@ class Api extends Emittery {
192
187
 
193
188
  const {providers = []} = this.options;
194
189
  const providerStates = (await Promise.all(providers.map(async ({type, main}) => {
195
- const state = await main.compile({cacheDir, files: testFiles});
190
+ const state = await main.compile({cacheDir: this._createCacheDir(), files: testFiles});
196
191
  return state === null ? null : {type, state};
197
192
  }))).filter(state => state !== null);
198
193
 
@@ -206,6 +201,8 @@ class Api extends Emittery {
206
201
  concurrency = 1;
207
202
  }
208
203
 
204
+ const deregisteredSharedWorkers = [];
205
+
209
206
  // Try and run each file, limited by `concurrency`.
210
207
  await pMap(selectedFiles, async file => {
211
208
  // No new files should be run once a test has timed out or failed,
@@ -231,6 +228,7 @@ class Api extends Emittery {
231
228
 
232
229
  const worker = fork(file, options, apiOptions.nodeArguments);
233
230
  runStatus.observeWorker(worker, file, {selectingLines: lineNumbers.length > 0});
231
+ deregisteredSharedWorkers.push(sharedWorkers.observeWorkerProcess(worker, runStatus));
234
232
 
235
233
  pendingWorkers.add(worker);
236
234
  worker.promise.then(() => {
@@ -238,8 +236,11 @@ class Api extends Emittery {
238
236
  });
239
237
  restartTimer();
240
238
 
241
- return worker.promise;
239
+ await worker.promise;
242
240
  }, {concurrency, stopOnError: false});
241
+
242
+ // Allow shared workers to clean up before the run ends.
243
+ await Promise.all(deregisteredSharedWorkers);
243
244
  } catch (error) {
244
245
  if (error && error.name === 'AggregateError') {
245
246
  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
@@ -7,7 +7,7 @@ const arrify = require('arrify');
7
7
  const yargs = require('yargs');
8
8
  const readPkg = require('read-pkg');
9
9
  const isCi = require('./is-ci');
10
- const loadConfig = require('./load-config');
10
+ const {loadConfig} = require('./load-config');
11
11
 
12
12
  function exit(message) {
13
13
  console.error(`\n ${require('./chalk').get().red(figures.cross)} ${message}`);
@@ -83,7 +83,7 @@ exports.run = async () => { // eslint-disable-line complexity
83
83
  let confError = null;
84
84
  try {
85
85
  const {argv: {config: configFile}} = yargs.help(false);
86
- conf = loadConfig({configFile});
86
+ conf = await loadConfig({configFile});
87
87
  } catch (error) {
88
88
  confError = error;
89
89
  }
@@ -409,35 +409,33 @@ exports.run = async () => { // eslint-disable-line complexity
409
409
  workerArgv: argv['--']
410
410
  });
411
411
 
412
- let reporter;
413
- if (combined.tap && !combined.watch && debug === null) {
414
- reporter = new TapReporter({
415
- projectDir,
416
- reportStream: process.stdout,
417
- stdStream: process.stderr
418
- });
419
- } else {
420
- reporter = new DefaultReporter({
421
- projectDir,
422
- reportStream: process.stdout,
423
- stdStream: process.stderr,
424
- watching: combined.watch,
425
- verbose: debug !== null || combined.verbose || isCi || !process.stdout.isTTY
426
- });
427
- }
412
+ const reporter = combined.tap && !combined.watch && debug === null ? new TapReporter({
413
+ projectDir,
414
+ reportStream: process.stdout,
415
+ stdStream: process.stderr
416
+ }) : new DefaultReporter({
417
+ projectDir,
418
+ reportStream: process.stdout,
419
+ stdStream: process.stderr,
420
+ watching: combined.watch,
421
+ verbose: debug !== null || combined.verbose || isCi || !process.stdout.isTTY
422
+ });
428
423
 
429
424
  api.on('run', plan => {
430
425
  reporter.startRun(plan);
431
426
 
432
427
  if (process.env.AVA_EMIT_RUN_STATUS_OVER_IPC === 'I\'ll find a payphone baby / Take some time to talk to you') {
433
- if (process.versions.node >= '12.17.0') {
428
+ const {controlFlow} = require('./ipc-flow-control');
429
+ const bufferedSend = controlFlow(process);
430
+
431
+ if (process.versions.node >= '12.16.0') {
434
432
  plan.status.on('stateChange', evt => {
435
- process.send(evt);
433
+ bufferedSend(evt);
436
434
  });
437
435
  } else {
438
436
  const v8 = require('v8');
439
437
  plan.status.on('stateChange', evt => {
440
- process.send([...v8.serialize(evt)]);
438
+ bufferedSend([...v8.serialize(evt)]);
441
439
  });
442
440
  }
443
441
  }
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- const util = require('util');
2
+ const util = require('util'); // eslint-disable-line unicorn/import-style
3
3
  const ansiStyles = require('ansi-styles');
4
4
  const stripAnsi = require('strip-ansi');
5
5
  const cloneDeepWith = require('lodash/cloneDeepWith');
package/lib/fork.js CHANGED
@@ -12,16 +12,57 @@ if (fs.realpathSync(__filename) !== __filename) {
12
12
  // In case the test file imports a different AVA install,
13
13
  // the presence of this variable allows it to require this one instead
14
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
+ }
15
40
 
16
- const workerPath = require.resolve('./worker/subprocess');
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
+ }
17
59
 
18
- const useAdvanced = process.versions.node >= '12.17.0';
19
- // FIXME: Fix this in api.js or cli.js.
20
- const serializeOptions = useAdvanced ?
21
- options => JSON.parse(JSON.stringify(options)) : // Use JSON serialization to remove non-clonable values.
22
- options => options;
60
+ let forkCounter = 0;
23
61
 
24
62
  module.exports = (file, options, execArgv = process.execArgv) => {
63
+ const forkId = `fork/${++forkCounter}`;
64
+ const sharedWorkerChannels = new Map();
65
+
25
66
  let finished = false;
26
67
 
27
68
  const emitter = new Emittery();
@@ -32,17 +73,17 @@ module.exports = (file, options, execArgv = process.execArgv) => {
32
73
  };
33
74
 
34
75
  options = {
35
- file,
36
76
  baseDir: process.cwd(),
77
+ file,
78
+ forkId,
37
79
  ...options
38
80
  };
39
81
 
40
- const subprocess = childProcess.fork(workerPath, options.workerArgv, {
82
+ const subprocess = childProcess.fork(WORKER_PATH, options.workerArgv, {
41
83
  cwd: options.projectDir,
42
84
  silent: true,
43
85
  env: {NODE_ENV: 'test', ...process.env, ...options.environmentVariables, AVA_PATH},
44
- execArgv,
45
- serialization: useAdvanced ? 'advanced' : 'json'
86
+ execArgv
46
87
  });
47
88
 
48
89
  subprocess.stdout.on('data', chunk => {
@@ -73,15 +114,25 @@ module.exports = (file, options, execArgv = process.execArgv) => {
73
114
  return;
74
115
  }
75
116
 
76
- if (message.ava.type === 'ready-for-options') {
77
- send({type: 'options', options: serializeOptions(options)});
78
- return;
79
- }
80
-
81
- if (message.ava.type === 'ping') {
82
- send({type: 'pong'});
83
- } else {
84
- emitStateChange(message.ava);
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);
85
136
  }
86
137
  });
87
138
 
@@ -106,6 +157,10 @@ module.exports = (file, options, execArgv = process.execArgv) => {
106
157
  });
107
158
 
108
159
  return {
160
+ file,
161
+ forkId,
162
+ promise,
163
+
109
164
  exit() {
110
165
  forcedExit = true;
111
166
  subprocess.kill();
@@ -115,11 +170,12 @@ module.exports = (file, options, execArgv = process.execArgv) => {
115
170
  send({type: 'peer-failed'});
116
171
  },
117
172
 
118
- onStateChange(listener) {
119
- return emitter.on('stateChange', listener);
173
+ onConnectSharedWorker(listener) {
174
+ return emitter.on('connectSharedWorker', listener);
120
175
  },
121
176
 
122
- file,
123
- promise
177
+ onStateChange(listener) {
178
+ return emitter.on('stateChange', listener);
179
+ }
124
180
  };
125
181
  };
package/lib/globs.js CHANGED
@@ -82,11 +82,7 @@ function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: igno
82
82
  filePatterns = defaultTestPatterns;
83
83
  }
84
84
 
85
- if (ignoredByWatcherPatterns) {
86
- ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns, ...normalizePatterns(ignoredByWatcherPatterns)];
87
- } else {
88
- ignoredByWatcherPatterns = [...defaultIgnoredByWatcherPatterns];
89
- }
85
+ ignoredByWatcherPatterns = ignoredByWatcherPatterns ? [...defaultIgnoredByWatcherPatterns, ...normalizePatterns(ignoredByWatcherPatterns)] : [...defaultIgnoredByWatcherPatterns];
90
86
 
91
87
  for (const {level, main} of providers) {
92
88
  if (level >= providerManager.levels.pathRewrites) {
@@ -1,40 +1,37 @@
1
- // Manage how quickly messages are delivered to the channel. In theory, we
2
- // should be able to call `send()` until it returns `false` but this leads to
3
- // crashes with advanced serialization, see
4
- // <https://github.com/nodejs/node/issues/34797>.
5
- //
6
- // Even if that's fixed (and the Node.js versions with the fixes are the
7
- // minimally supported versions) we need flow control based on `send()`'s return
8
- // value.
9
-
10
- const nowAndTimers = require('./now-and-timers');
11
-
12
1
  function controlFlow(channel) {
13
- let sending = false;
2
+ let errored = false;
3
+ let deliverImmediately = true;
14
4
 
15
- const buffer = [];
16
- const deliverNext = () => {
17
- if (!channel.connected) {
18
- buffer.length = 0;
5
+ const backlog = [];
6
+ const deliverNext = error => {
7
+ if (error !== null) {
8
+ errored = true;
19
9
  }
20
10
 
21
- if (buffer.length === 0) {
22
- sending = false;
23
- return;
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);
24
19
  }
25
20
 
26
- channel.send(buffer.shift(), deliverNext);
21
+ // Re-enable immediate delivery if there is no backpressure and the backlog
22
+ // has been cleared.
23
+ deliverImmediately = ok && backlog.length === 0;
27
24
  };
28
25
 
29
26
  return message => {
30
- if (!channel.connected) {
27
+ if (errored || !channel.connected) {
31
28
  return;
32
29
  }
33
30
 
34
- buffer.push(message);
35
- if (!sending) {
36
- sending = true;
37
- nowAndTimers.setImmediate(deliverNext);
31
+ if (deliverImmediately) {
32
+ deliverImmediately = channel.send(message, deliverNext);
33
+ } else {
34
+ backlog.push(message);
38
35
  }
39
36
  };
40
37
  }