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.
@@ -0,0 +1,138 @@
1
+ const events = require('events');
2
+ const serializeError = require('../serialize-error');
3
+
4
+ let Worker;
5
+ try {
6
+ ({Worker} = require('worker_threads')); // eslint-disable-line node/no-unsupported-features/node-builtins
7
+ } catch {}
8
+
9
+ const LOADER = require.resolve('./shared-worker-loader');
10
+
11
+ let sharedWorkerCounter = 0;
12
+ const launchedWorkers = new Map();
13
+
14
+ const waitForAvailable = async worker => {
15
+ for await (const [message] of events.on(worker, 'message')) {
16
+ if (message.type === 'available') {
17
+ return;
18
+ }
19
+ }
20
+ };
21
+
22
+ function launchWorker({filename, initialData}) {
23
+ if (launchedWorkers.has(filename)) {
24
+ return launchedWorkers.get(filename);
25
+ }
26
+
27
+ const id = `shared-worker/${++sharedWorkerCounter}`;
28
+ const worker = new Worker(LOADER, {
29
+ // Ensure the worker crashes for unhandled rejections, rather than allowing undefined behavior.
30
+ execArgv: ['--unhandled-rejections=strict'],
31
+ workerData: {
32
+ filename,
33
+ id,
34
+ initialData
35
+ }
36
+ });
37
+ const launched = {
38
+ statePromises: {
39
+ available: waitForAvailable(worker),
40
+ error: events.once(worker, 'error').then(([error]) => error) // eslint-disable-line promise/prefer-await-to-then
41
+ },
42
+ exited: false,
43
+ worker
44
+ };
45
+
46
+ launchedWorkers.set(filename, launched);
47
+ worker.once('exit', () => {
48
+ launched.exited = true;
49
+ });
50
+
51
+ return launched;
52
+ }
53
+
54
+ async function observeWorkerProcess(fork, runStatus) {
55
+ let registrationCount = 0;
56
+ let signalDeregistered;
57
+ const deregistered = new Promise(resolve => {
58
+ signalDeregistered = resolve;
59
+ });
60
+
61
+ fork.promise.finally(() => {
62
+ if (registrationCount === 0) {
63
+ signalDeregistered();
64
+ }
65
+ });
66
+
67
+ fork.onConnectSharedWorker(async channel => {
68
+ const launched = launchWorker(channel);
69
+
70
+ const handleChannelMessage = ({messageId, replyTo, serializedData}) => {
71
+ launched.worker.postMessage({
72
+ type: 'message',
73
+ testWorkerId: fork.forkId,
74
+ messageId,
75
+ replyTo,
76
+ serializedData
77
+ });
78
+ };
79
+
80
+ const handleWorkerMessage = async message => {
81
+ if (message.type === 'broadcast' || (message.type === 'message' && message.testWorkerId === fork.forkId)) {
82
+ const {messageId, replyTo, serializedData} = message;
83
+ channel.forwardMessageToFork({messageId, replyTo, serializedData});
84
+ }
85
+
86
+ if (message.type === 'deregistered-test-worker' && message.id === fork.forkId) {
87
+ launched.worker.off('message', handleWorkerMessage);
88
+
89
+ registrationCount--;
90
+ if (registrationCount === 0) {
91
+ signalDeregistered();
92
+ }
93
+ }
94
+ };
95
+
96
+ launched.statePromises.error.then(error => { // eslint-disable-line promise/prefer-await-to-then
97
+ signalDeregistered();
98
+ launched.worker.off('message', handleWorkerMessage);
99
+ runStatus.emitStateChange({type: 'shared-worker-error', err: serializeError('Shared worker error', true, error)});
100
+ channel.signalError();
101
+ });
102
+
103
+ try {
104
+ await launched.statePromises.available;
105
+
106
+ registrationCount++;
107
+ launched.worker.postMessage({
108
+ type: 'register-test-worker',
109
+ id: fork.forkId,
110
+ file: fork.file
111
+ });
112
+
113
+ fork.promise.finally(() => {
114
+ launched.worker.postMessage({
115
+ type: 'deregister-test-worker',
116
+ id: fork.forkId
117
+ });
118
+
119
+ channel.off('message', handleChannelMessage);
120
+ });
121
+
122
+ launched.worker.on('message', handleWorkerMessage);
123
+ channel.on('message', handleChannelMessage);
124
+ channel.signalReady();
125
+ } catch {
126
+ return;
127
+ } finally {
128
+ // Attaching listeners has the side-effect of referencing the worker.
129
+ // Explicitly unreference it now so it does not prevent the main process
130
+ // from exiting.
131
+ launched.worker.unref();
132
+ }
133
+ });
134
+
135
+ return deregistered;
136
+ }
137
+
138
+ exports.observeWorkerProcess = observeWorkerProcess;
@@ -21,7 +21,7 @@ function load(providerModule, projectDir) {
21
21
  let level;
22
22
  const provider = makeProvider({
23
23
  negotiateProtocol(identifiers, {version}) {
24
- const [identifier] = identifiers.filter(identifier => Reflect.has(levelsByProtocol, identifier));
24
+ const identifier = identifiers.find(identifier => Reflect.has(levelsByProtocol, identifier));
25
25
 
26
26
  if (identifier === undefined) {
27
27
  fatal = new Error(`This version of AVA (${ava.version}) is not compatible with ${providerModule}@${version}`);
@@ -18,7 +18,6 @@ const colors = require('./colors');
18
18
  const formatSerializedError = require('./format-serialized-error');
19
19
  const improperUsageMessages = require('./improper-usage-messages');
20
20
  const prefixTitle = require('./prefix-title');
21
- const whileCorked = require('./while-corked');
22
21
 
23
22
  const nodeInternals = require('stack-utils').nodeInternals();
24
23
 
@@ -97,6 +96,48 @@ class LineWriterWithSpinner extends LineWriter {
97
96
  }
98
97
  }
99
98
 
99
+ function manageCorking(stream) {
100
+ let corked = false;
101
+ const cork = () => {
102
+ corked = true;
103
+ stream.cork();
104
+ };
105
+
106
+ const uncork = () => {
107
+ corked = false;
108
+ stream.uncork();
109
+ };
110
+
111
+ return {
112
+ decorateFlushingWriter(fn) {
113
+ return function (...args) {
114
+ if (corked) {
115
+ stream.uncork();
116
+ }
117
+
118
+ try {
119
+ return fn.apply(this, args);
120
+ } finally {
121
+ if (corked) {
122
+ stream.cork();
123
+ }
124
+ }
125
+ };
126
+ },
127
+
128
+ decorateWriter(fn) {
129
+ return function (...args) {
130
+ cork();
131
+ try {
132
+ return fn.apply(this, args);
133
+ } finally {
134
+ uncork();
135
+ }
136
+ };
137
+ }
138
+ };
139
+ }
140
+
100
141
  class Reporter {
101
142
  constructor({
102
143
  verbose,
@@ -112,13 +153,16 @@ class Reporter {
112
153
  this.stdStream = stdStream;
113
154
  this.watching = watching;
114
155
  this.relativeFile = file => path.relative(projectDir, file);
115
- this.consumeStateChange = whileCorked(this.reportStream, this.consumeStateChange);
156
+
157
+ const {decorateWriter, decorateFlushingWriter} = manageCorking(this.reportStream);
158
+ this.consumeStateChange = decorateWriter(this.consumeStateChange);
159
+ this.endRun = decorateWriter(this.endRun);
116
160
 
117
161
  if (this.verbose) {
118
162
  this.durationThreshold = durationThreshold || 100;
119
163
  this.spinner = null;
164
+ this.clearSpinner = () => {};
120
165
  this.lineWriter = new LineWriter(this.reportStream);
121
- this.endRun = whileCorked(this.reportStream, this.endRun);
122
166
  } else {
123
167
  this.spinner = ora({
124
168
  isEnabled: true,
@@ -128,8 +172,8 @@ class Reporter {
128
172
  spinner: spinner || (process.platform === 'win32' ? 'line' : 'dots'),
129
173
  stream: reportStream
130
174
  });
175
+ this.clearSpinner = decorateFlushingWriter(this.spinner.clear.bind(this.spinner));
131
176
  this.lineWriter = new LineWriterWithSpinner(this.reportStream, this.spinner);
132
- this.endRun = whileCorked(this.reportStream, whileCorked(this.lineWriter, this.endRun));
133
177
  }
134
178
 
135
179
  this.reset();
@@ -151,6 +195,7 @@ class Reporter {
151
195
  this.internalErrors = [];
152
196
  this.knownFailures = [];
153
197
  this.lineNumberErrors = [];
198
+ this.sharedWorkerErrors = [];
154
199
  this.uncaughtExceptions = [];
155
200
  this.unhandledRejections = [];
156
201
  this.unsavedSnapshots = [];
@@ -296,6 +341,19 @@ class Reporter {
296
341
  break;
297
342
  }
298
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
+
299
357
  case 'snapshot-error':
300
358
  this.unsavedSnapshots.push(event);
301
359
  break;
@@ -362,9 +420,7 @@ class Reporter {
362
420
 
363
421
  case 'worker-stderr': {
364
422
  // Forcibly clear the spinner, writing the chunk corrupts the TTY.
365
- if (this.spinner !== null) {
366
- this.spinner.clear();
367
- }
423
+ this.clearSpinner();
368
424
 
369
425
  this.stdStream.write(event.chunk);
370
426
  // If the chunk does not end with a linebreak, *forcibly* write one to
@@ -386,9 +442,7 @@ class Reporter {
386
442
 
387
443
  case 'worker-stdout': {
388
444
  // Forcibly clear the spinner, writing the chunk corrupts the TTY.
389
- if (this.spinner !== null) {
390
- this.spinner.clear();
391
- }
445
+ this.clearSpinner();
392
446
 
393
447
  this.stdStream.write(event.chunk);
394
448
  // If the chunk does not end with a linebreak, *forcibly* write one to
@@ -670,7 +724,7 @@ class Reporter {
670
724
  }
671
725
 
672
726
  if (this.failures.length > 0) {
673
- 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;
674
728
 
675
729
  const lastFailure = this.failures[this.failures.length - 1];
676
730
  for (const event of this.failures) {
@@ -694,7 +748,7 @@ class Reporter {
694
748
 
695
749
  if (!this.verbose) {
696
750
  if (this.internalErrors.length > 0) {
697
- 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;
698
752
 
699
753
  const last = this.internalErrors[this.internalErrors.length - 1];
700
754
  for (const event of this.internalErrors) {
@@ -716,6 +770,23 @@ class Reporter {
716
770
  }
717
771
  }
718
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
+
719
790
  if (this.uncaughtExceptions.length > 0) {
720
791
  const writeTrailingLines = this.unhandledRejections.length > 0;
721
792
 
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
@@ -42,6 +42,7 @@ class Runner extends Emittery {
42
42
  serial: [],
43
43
  todo: []
44
44
  };
45
+ this.waitForReady = [];
45
46
 
46
47
  const uniqueTestTitles = new Set();
47
48
  this.registerUniqueTitle = title => {
@@ -444,6 +445,8 @@ class Runner extends Emittery {
444
445
  });
445
446
  }
446
447
 
448
+ await Promise.all(this.waitForReady);
449
+
447
450
  if (concurrentTests.length === 0 && serialTests.length === 0) {
448
451
  this.emit('finish');
449
452
  // Don't run any hooks if there are no tests to run.
package/lib/test.js CHANGED
@@ -187,7 +187,8 @@ class ExecutionContext extends assert.Assertions {
187
187
  }
188
188
 
189
189
  get passed() {
190
- return testMap.get(this).testPassed;
190
+ const test = testMap.get(this);
191
+ return test.isHook ? test.testPassed : !test.assertError;
191
192
  }
192
193
 
193
194
  _throwsArgStart(assertion, file, line) {
package/lib/worker/ipc.js CHANGED
@@ -1,50 +1,42 @@
1
1
  'use strict';
2
- const Emittery = require('emittery');
2
+ const events = require('events');
3
+ const pEvent = require('p-event');
4
+ const {controlFlow} = require('../ipc-flow-control');
5
+ const {get: getOptions} = require('./options');
3
6
 
4
- const emitter = new Emittery();
5
- process.on('message', message => {
6
- if (!message.ava) {
7
- return;
8
- }
9
-
10
- switch (message.ava.type) {
11
- case 'options':
12
- emitter.emit('options', message.ava.options);
13
- break;
14
- case 'peer-failed':
15
- emitter.emit('peerFailed');
16
- break;
17
- case 'pong':
18
- emitter.emit('pong');
19
- break;
20
- default:
21
- break;
22
- }
23
- });
7
+ const selectAvaMessage = type => message => message.ava && message.ava.type === type;
24
8
 
25
- exports.options = emitter.once('options');
26
- exports.peerFailed = emitter.once('peerFailed');
9
+ exports.options = pEvent(process, 'message', selectAvaMessage('options')).then(message => message.ava.options);
10
+ exports.peerFailed = pEvent(process, 'message', selectAvaMessage('peer-failed'));
27
11
 
12
+ const bufferedSend = controlFlow(process);
28
13
  function send(evt) {
29
- if (process.connected) {
30
- process.send({ava: evt});
31
- }
14
+ bufferedSend({ava: evt});
32
15
  }
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
- process.channel.unref();
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
- process.channel.ref();
36
+ ref();
45
37
  const promise = pendingPings.then(async () => { // eslint-disable-line promise/prefer-await-to-then
46
38
  send({type: 'ping'});
47
- await emitter.once('pong');
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
+