ava 2.0.0 → 2.4.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/lib/test.js CHANGED
@@ -6,6 +6,7 @@ const isObservable = require('is-observable');
6
6
  const plur = require('plur');
7
7
  const assert = require('./assert');
8
8
  const nowAndTimers = require('./now-and-timers');
9
+ const parseTestArgs = require('./parse-test-args');
9
10
  const concordanceOptions = require('./concordance-options').default;
10
11
 
11
12
  function formatErrorValue(label, error) {
@@ -13,13 +14,12 @@ function formatErrorValue(label, error) {
13
14
  return {label, formatted};
14
15
  }
15
16
 
16
- const captureStack = start => {
17
+ const captureSavedError = () => {
17
18
  const limitBefore = Error.stackTraceLimit;
18
19
  Error.stackTraceLimit = 1;
19
- const obj = {};
20
- Error.captureStackTrace(obj, start);
20
+ const err = new Error();
21
21
  Error.stackTraceLimit = limitBefore;
22
- return obj.stack;
22
+ return err;
23
23
  };
24
24
 
25
25
  const testMap = new WeakMap();
@@ -60,7 +60,7 @@ class ExecutionContext extends assert.Assertions {
60
60
  };
61
61
 
62
62
  this.plan = count => {
63
- test.plan(count, captureStack(test.plan));
63
+ test.plan(count, captureSavedError());
64
64
  };
65
65
 
66
66
  this.plan.skip = () => {};
@@ -68,11 +68,100 @@ class ExecutionContext extends assert.Assertions {
68
68
  this.timeout = ms => {
69
69
  test.timeout(ms);
70
70
  };
71
+
72
+ this.try = async (...attemptArgs) => {
73
+ if (test.experiments.tryAssertion !== true) {
74
+ throw new Error('t.try() is currently an experiment. Opt in by setting `nonSemVerExperiments.tryAssertion` to `true`.');
75
+ }
76
+
77
+ const {args, buildTitle, implementations, receivedImplementationArray} = parseTestArgs(attemptArgs);
78
+
79
+ if (implementations.length === 0) {
80
+ throw new TypeError('Expected an implementation.');
81
+ }
82
+
83
+ const attemptPromises = implementations.map(implementation => {
84
+ let {title, isSet, isValid, isEmpty} = buildTitle(implementation);
85
+
86
+ if (!isSet || isEmpty) {
87
+ title = `${test.title} (attempt ${test.attemptCount + 1})`;
88
+ } else if (!isValid) {
89
+ throw new TypeError('`t.try()` titles must be strings'); // Throw synchronously!
90
+ }
91
+
92
+ if (!test.registerUniqueTitle(title)) {
93
+ throw new Error(`Duplicate test title: ${title}`);
94
+ }
95
+
96
+ return {implementation, title};
97
+ }).map(async ({implementation, title}) => {
98
+ let committed = false;
99
+ let discarded = false;
100
+
101
+ const {assertCount, deferredSnapshotRecordings, errors, logs, passed, snapshotCount, startingSnapshotCount} = await test.runAttempt(title, t => implementation(t, ...args));
102
+
103
+ return {
104
+ errors,
105
+ logs: [...logs], // Don't allow modification of logs.
106
+ passed,
107
+ title,
108
+ commit: ({retainLogs = true} = {}) => {
109
+ if (committed) {
110
+ return;
111
+ }
112
+
113
+ if (discarded) {
114
+ test.saveFirstError(new Error('Can\'t commit a result that was previously discarded'));
115
+ return;
116
+ }
117
+
118
+ committed = true;
119
+ test.finishAttempt({
120
+ assertCount,
121
+ commit: true,
122
+ deferredSnapshotRecordings,
123
+ errors,
124
+ logs,
125
+ passed,
126
+ retainLogs,
127
+ snapshotCount,
128
+ startingSnapshotCount
129
+ });
130
+ },
131
+ discard: ({retainLogs = false} = {}) => {
132
+ if (committed) {
133
+ test.saveFirstError(new Error('Can\'t discard a result that was previously committed'));
134
+ return;
135
+ }
136
+
137
+ if (discarded) {
138
+ return;
139
+ }
140
+
141
+ discarded = true;
142
+ test.finishAttempt({
143
+ assertCount: 0,
144
+ commit: false,
145
+ deferredSnapshotRecordings,
146
+ errors,
147
+ logs,
148
+ passed,
149
+ retainLogs,
150
+ snapshotCount,
151
+ startingSnapshotCount
152
+ });
153
+ }
154
+ };
155
+ });
156
+
157
+ const results = await Promise.all(attemptPromises);
158
+ return receivedImplementationArray ? results : results[0];
159
+ };
71
160
  }
72
161
 
73
162
  get end() {
74
163
  const end = testMap.get(this).bindEndCallback();
75
- const endFn = error => end(error, captureStack(endFn));
164
+ const endFn = error => end(error, captureSavedError());
76
165
  return endFn;
77
166
  }
78
167
 
@@ -100,32 +189,74 @@ class ExecutionContext extends assert.Assertions {
100
189
  class Test {
101
190
  constructor(options) {
102
191
  this.contextRef = options.contextRef;
192
+ this.experiments = options.experiments || {};
103
193
  this.failWithoutAssertions = options.failWithoutAssertions;
104
194
  this.fn = options.fn;
105
195
  this.metadata = options.metadata;
106
196
  this.title = options.title;
197
+ this.registerUniqueTitle = options.registerUniqueTitle;
107
198
  this.logs = [];
108
199
 
109
- this.snapshotInvocationCount = 0;
110
- this.compareWithSnapshot = assertionOptions => {
111
- const belongsTo = assertionOptions.id || this.title;
112
- const {expected} = assertionOptions;
113
- const index = assertionOptions.id ? 0 : this.snapshotInvocationCount++;
114
- const label = assertionOptions.id ? '' : assertionOptions.message || `Snapshot ${this.snapshotInvocationCount}`;
115
- return options.compareTestSnapshot({belongsTo, expected, index, label});
200
+ const {snapshotBelongsTo = this.title, nextSnapshotIndex = 0} = options;
201
+ this.snapshotBelongsTo = snapshotBelongsTo;
202
+ this.nextSnapshotIndex = nextSnapshotIndex;
203
+ this.snapshotCount = 0;
204
+
205
+ const deferRecording = this.metadata.inline;
206
+ this.deferredSnapshotRecordings = [];
207
+ this.compareWithSnapshot = ({expected, id, message}) => {
208
+ this.snapshotCount++;
209
+
210
+ // TODO: In a breaking change, reject non-undefined, falsy IDs and messages.
211
+ const belongsTo = id || snapshotBelongsTo;
212
+ const index = id ? 0 : this.nextSnapshotIndex++;
213
+ const label = id ? '' : message || `Snapshot ${index + 1}`; // Human-readable labels start counting at 1.
214
+
215
+ const {record, ...result} = options.compareTestSnapshot({belongsTo, deferRecording, expected, index, label});
216
+ if (record) {
217
+ this.deferredSnapshotRecordings.push(record);
218
+ }
219
+
220
+ return result;
116
221
  };
117
222
 
118
223
  this.skipSnapshot = () => {
119
224
  if (options.updateSnapshots) {
120
225
  this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots'));
121
226
  } else {
122
- this.snapshotInvocationCount++;
227
+ this.nextSnapshotIndex++;
228
+ this.snapshotCount++;
123
229
  this.countPassedAssertion();
124
230
  }
125
231
  };
126
232
 
233
+ this.runAttempt = async (title, fn) => {
234
+ if (this.finishing) {
235
+ this.saveFirstError(new Error('Running a `t.try()`, but the test has already finished'));
236
+ }
237
+
238
+ this.attemptCount++;
239
+ this.pendingAttemptCount++;
240
+
241
+ const {contextRef, snapshotBelongsTo, nextSnapshotIndex, snapshotCount: startingSnapshotCount} = this;
242
+ const attempt = new Test({
243
+ ...options,
244
+ fn,
245
+ metadata: {...options.metadata, callback: false, failing: false, inline: true},
246
+ contextRef: contextRef.copy(),
247
+ snapshotBelongsTo,
248
+ nextSnapshotIndex,
249
+ title
250
+ });
251
+
252
+ const {deferredSnapshotRecordings, error, logs, passed, assertCount, snapshotCount} = await attempt.run();
253
+ const errors = error ? [error] : [];
254
+ return {assertCount, deferredSnapshotRecordings, errors, logs, passed, snapshotCount, startingSnapshotCount};
255
+ };
256
+
127
257
  this.assertCount = 0;
128
258
  this.assertError = undefined;
259
+ this.attemptCount = 0;
129
260
  this.calledEnd = false;
130
261
  this.duration = null;
131
262
  this.endCallbackFinisher = null;
@@ -134,24 +265,29 @@ class Test {
134
265
  this.finishDueToTimeout = null;
135
266
  this.finishing = false;
136
267
  this.pendingAssertionCount = 0;
268
+ this.pendingAttemptCount = 0;
137
269
  this.pendingThrowsAssertion = null;
138
270
  this.planCount = null;
139
271
  this.startedAt = 0;
140
- this.timeoutTimer = null;
141
272
  this.timeoutMs = 0;
273
+ this.timeoutTimer = null;
142
274
  }
143
275
 
144
276
  bindEndCallback() {
145
277
  if (this.metadata.callback) {
146
- return (error, stack) => {
147
- this.endCallback(error, stack);
278
+ return (error, savedError) => {
279
+ this.endCallback(error, savedError);
148
280
  };
149
281
  }
150
282
 
151
- 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)`');
283
+ if (this.metadata.inline) {
284
+ throw new Error('`t.end()` is not supported inside `t.try()`');
285
+ } else {
286
+ 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)`');
287
+ }
152
288
  }
153
289
 
154
- endCallback(error, stack) {
290
+ endCallback(error, savedError) {
155
291
  if (this.calledEnd) {
156
292
  this.saveFirstError(new Error('`t.end()` called more than once'));
157
293
  return;
@@ -163,7 +299,7 @@ class Test {
163
299
  this.saveFirstError(new assert.AssertionError({
164
300
  actual: error,
165
301
  message: 'Callback called with an error',
166
- stack,
302
+ savedError,
167
303
  values: [formatErrorValue('Callback called with an error:', error)]
168
304
  }));
169
305
  }
@@ -182,6 +318,10 @@ class Test {
182
318
  this.saveFirstError(new Error('Assertion passed, but test has already finished'));
183
319
  }
184
320
 
321
+ if (this.pendingAttemptCount > 0) {
322
+ this.saveFirstError(new Error('Assertion passed, but an attempt is pending. Use the attempt’s assertions instead'));
323
+ }
324
+
185
325
  this.assertCount++;
186
326
  this.refreshTimeout();
187
327
  }
@@ -192,7 +332,11 @@ class Test {
192
332
 
193
333
  addPendingAssertion(promise) {
194
334
  if (this.finishing) {
195
- this.saveFirstError(new Error('Assertion passed, but test has already finished'));
335
+ this.saveFirstError(new Error('Assertion started, but test has already finished'));
336
+ }
337
+
338
+ if (this.pendingAttemptCount > 0) {
339
+ this.saveFirstError(new Error('Assertion started, but an attempt is pending. Use the attempt’s assertions instead'));
196
340
  }
197
341
 
198
342
  this.assertCount++;
@@ -212,18 +356,60 @@ class Test {
212
356
  this.saveFirstError(new Error('Assertion failed, but test has already finished'));
213
357
  }
214
358
 
359
+ if (this.pendingAttemptCount > 0) {
360
+ this.saveFirstError(new Error('Assertion failed, but an attempt is pending. Use the attempt’s assertions instead'));
361
+ }
362
+
215
363
  this.assertCount++;
216
364
  this.refreshTimeout();
217
365
  this.saveFirstError(error);
218
366
  }
219
367
 
368
+ finishAttempt({commit, deferredSnapshotRecordings, errors, logs, passed, retainLogs, snapshotCount, startingSnapshotCount}) {
369
+ if (this.finishing) {
370
+ if (commit) {
371
+ this.saveFirstError(new Error('`t.try()` result was committed, but the test has already finished'));
372
+ } else {
373
+ this.saveFirstError(new Error('`t.try()` result was discarded, but the test has already finished'));
374
+ }
375
+ }
376
+
377
+ if (commit) {
378
+ this.assertCount++;
379
+
380
+ if (startingSnapshotCount === this.snapshotCount) {
381
+ this.snapshotCount += snapshotCount;
382
+ this.nextSnapshotIndex += snapshotCount;
383
+ for (const record of deferredSnapshotRecordings) {
384
+ record();
385
+ }
386
+ } else {
387
+ this.saveFirstError(new Error('Cannot commit `t.try()` result. Do not run concurrent snapshot assertions when using `t.try()`'));
388
+ }
389
+ }
390
+
391
+ this.pendingAttemptCount--;
392
+
393
+ if (commit && !passed) {
394
+ this.saveFirstError(errors[0]);
395
+ }
396
+
397
+ if (retainLogs) {
398
+ for (const log of logs) {
399
+ this.addLog(log);
400
+ }
401
+ }
402
+
403
+ this.refreshTimeout();
404
+ }
405
+
220
406
  saveFirstError(error) {
221
407
  if (!this.assertError) {
222
408
  this.assertError = error;
223
409
  }
224
410
  }
225
411
 
226
- plan(count, planStack) {
412
+ plan(count, planError) {
227
413
  if (typeof count !== 'number') {
228
414
  throw new TypeError('Expected a number');
229
415
  }
@@ -232,7 +418,7 @@ class Test {
232
418
 
233
419
  // In case the `planCount` doesn't match `assertCount, we need the stack of
234
420
  // this function to throw with a useful stack.
235
- this.planStack = planStack;
421
+ this.planError = planError;
236
422
  }
237
423
 
238
424
  timeout(ms) {
@@ -274,17 +460,33 @@ class Test {
274
460
  assertion: 'plan',
275
461
  message: `Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertCount}.`,
276
462
  operator: '===',
277
- stack: this.planStack
463
+ savedError: this.planError
278
464
  }));
279
465
  }
280
466
  }
281
467
 
282
468
  verifyAssertions() {
283
- if (!this.assertError) {
284
- if (this.failWithoutAssertions && !this.calledEnd && this.planCount === null && this.assertCount === 0) {
469
+ if (this.assertError) {
470
+ return;
471
+ }
472
+
473
+ if (this.pendingAttemptCount > 0) {
474
+ this.saveFirstError(new Error('Test finished, but not all attempts were committed or discarded'));
475
+ return;
476
+ }
477
+
478
+ if (this.pendingAssertionCount > 0) {
479
+ this.saveFirstError(new Error('Test finished, but an assertion is still pending'));
480
+ return;
481
+ }
482
+
483
+ if (this.failWithoutAssertions) {
484
+ if (this.planCount !== null) {
485
+ return; // `verifyPlan()` will report an error already.
486
+ }
487
+
488
+ if (this.assertCount === 0 && !this.calledEnd) {
285
489
  this.saveFirstError(new Error('Test finished without running any assertions'));
286
- } else if (this.pendingAssertionCount > 0) {
287
- this.saveFirstError(new Error('Test finished, but an assertion is still pending'));
288
490
  }
289
491
  }
290
492
  }
@@ -311,7 +513,7 @@ class Test {
311
513
  fixedSource: {file: pending.file, line: pending.line},
312
514
  improperUsage: true,
313
515
  message: `Improper usage of \`t.${pending.assertion}()\` detected`,
314
- stack: error instanceof Error && error.stack,
516
+ savedError: error instanceof Error && error,
315
517
  values
316
518
  }));
317
519
  return true;
@@ -365,7 +567,7 @@ class Test {
365
567
  if (!this.detectImproperThrows(result.error)) {
366
568
  this.saveFirstError(new assert.AssertionError({
367
569
  message: 'Error thrown in test',
368
- stack: result.error instanceof Error && result.error.stack,
570
+ savedError: result.error instanceof Error && result.error,
369
571
  values: [formatErrorValue('Error thrown in test:', result.error)]
370
572
  }));
371
573
  }
@@ -438,7 +640,7 @@ class Test {
438
640
  if (!this.detectImproperThrows(error)) {
439
641
  this.saveFirstError(new assert.AssertionError({
440
642
  message: 'Rejected promise returned by test',
441
- stack: error instanceof Error && error.stack,
643
+ savedError: error instanceof Error && error,
442
644
  values: [formatErrorValue('Rejected promise returned by test. Reason:', error)]
443
645
  }));
444
646
  }
@@ -477,11 +679,14 @@ class Test {
477
679
  }
478
680
 
479
681
  return {
682
+ deferredSnapshotRecordings: this.deferredSnapshotRecordings,
480
683
  duration: this.duration,
481
684
  error,
482
685
  logs: this.logs,
483
686
  metadata: this.metadata,
484
687
  passed,
688
+ snapshotCount: this.snapshotCount,
689
+ assertCount: this.assertCount,
485
690
  title: this.title
486
691
  };
487
692
  }
package/lib/watcher.js CHANGED
@@ -162,7 +162,7 @@ class Watcher {
162
162
  }).on('all', (event, path) => {
163
163
  if (event === 'add' || event === 'change' || event === 'unlink') {
164
164
  debug('Detected %s of %s', event, path);
165
- this.dirtyStates[path] = event;
165
+ this.dirtyStates[nodePath.join(this.globs.cwd, path)] = event;
166
166
  this.debouncer.debounce();
167
167
  }
168
168
  });
@@ -180,7 +180,7 @@ class Watcher {
180
180
  const dependencies = evt.dependencies.map(x => relative(x)).filter(filePath => {
181
181
  const {isHelper, isSource} = globs.classify(filePath, this.globs);
182
182
  return isHelper || isSource;
183
- });
183
+ }).map(x => nodePath.join(this.globs.cwd, x));
184
184
  this.updateTestDependencies(evt.testFile, dependencies);
185
185
  });
186
186
  });
@@ -215,7 +215,7 @@ class Watcher {
215
215
  }
216
216
 
217
217
  for (const file of evt.files) {
218
- this.touchedFiles.add(nodePath.relative(process.cwd(), file));
218
+ this.touchedFiles.add(file);
219
219
  }
220
220
  });
221
221
  });
@@ -229,7 +229,8 @@ class Watcher {
229
229
  }
230
230
 
231
231
  const fileStats = plan.status.stats.byFile.get(evt.testFile);
232
- this.updateExclusivity(evt.testFile, fileStats.declaredTests > fileStats.selectedTests);
232
+ const ranExclusiveTests = fileStats.selectedTests > 0 && fileStats.declaredTests > fileStats.selectedTests;
233
+ this.updateExclusivity(evt.testFile, ranExclusiveTests);
233
234
  });
234
235
  });
235
236
  }
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+ const tty = require('tty');
3
+
4
+ // Call original method to ensure the correct errors are thrown.
5
+ const assertHasColorsArguments = count => {
6
+ tty.WriteStream.prototype.hasColors(count);
7
+ };
8
+
9
+ const makeHasColors = colorDepth => (count = 16, env) => {
10
+ // `count` is optional too, so make sure it's not an env object.
11
+ if (env === undefined && typeof count === 'object' && count !== null) {
12
+ count = 16;
13
+ }
14
+
15
+ assertHasColorsArguments(count);
16
+ return count <= 2 ** colorDepth;
17
+ };
18
+
19
+ module.exports = makeHasColors;
@@ -2,39 +2,85 @@
2
2
  const tty = require('tty');
3
3
  const ansiEscapes = require('ansi-escapes');
4
4
  const options = require('./options').get();
5
+ const makeHasColors = require('./fake-tty-has-colors');
5
6
 
6
7
  const fakeTTYs = new Set();
7
8
 
8
9
  const {isatty} = tty;
9
10
  tty.isatty = fd => fakeTTYs.has(fd) || isatty(fd);
10
11
 
11
- const simulateTTY = (stream, {colorDepth, columns, rows}) => {
12
- Object.assign(stream, {isTTY: true, columns, rows});
12
+ const takesCallbackAndReturnWriteResult = tty.WriteStream.prototype.clearLine.length === 1;
13
+
14
+ const assertCallback = callback => {
15
+ // FIXME: Better replicate Node.js' internal errors.
16
+ if (callback !== undefined && typeof callback !== 'function') {
17
+ const error = new TypeError('Callback must be a function');
18
+ error.code = 'ERR_INVALID_CALLBACK';
19
+ throw error;
20
+ }
21
+ };
22
+
23
+ const fakeWriters = {
24
+ clearLine(dir, callback) {
25
+ assertCallback(callback);
13
26
 
14
- stream.clearLine = dir => {
15
27
  switch (dir) {
16
28
  case -1:
17
- stream.write(ansiEscapes.eraseStartLine);
18
- break;
29
+ return this.write(ansiEscapes.eraseStartLine, callback);
19
30
  case 1:
20
- stream.write(ansiEscapes.eraseEndLine);
21
- break;
31
+ return this.write(ansiEscapes.eraseEndLine, callback);
22
32
  default:
23
- stream.write(ansiEscapes.eraseLine);
33
+ return this.write(ansiEscapes.eraseLine, callback);
24
34
  }
25
- };
35
+ },
26
36
 
27
- stream.clearScreenDown = () => stream.write(ansiEscapes.eraseDown);
37
+ clearScreenDown(callback) {
38
+ assertCallback(callback);
28
39
 
29
- stream.cursorTo = (x, y) => stream.write(ansiEscapes.cursorTo(x, y));
40
+ return this.write(ansiEscapes.eraseDown, callback);
41
+ },
30
42
 
31
- stream.getWindowSize = () => [80, 24];
43
+ cursorTo(x, y, callback) {
44
+ assertCallback(callback);
45
+ return this.write(ansiEscapes.cursorTo(x, y), callback);
46
+ },
32
47
 
33
- stream.moveCursor = (x, y) => stream.write(ansiEscapes.cursorMove(x, y));
48
+ moveCursor(x, y, callback) {
49
+ assertCallback(callback);
50
+ return this.write(ansiEscapes.cursorMove(x, y), callback);
51
+ }
52
+ };
34
53
 
54
+ const simulateTTY = (stream, {colorDepth, hasColors, columns, rows}) => {
55
+ Object.assign(stream, {isTTY: true, columns, rows});
56
+
57
+ if (takesCallbackAndReturnWriteResult) {
58
+ Object.assign(stream, fakeWriters);
59
+ } else {
60
+ Object.assign(stream, {
61
+ clearLine(dir) {
62
+ fakeWriters.clearLine.call(this, dir);
63
+ },
64
+ clearScreenDown() {
65
+ fakeWriters.clearScreenDown.call(this);
66
+ },
67
+ cursorTo(x, y) {
68
+ fakeWriters.cursorTo.call(this, x, y);
69
+ },
70
+ moveCursor(x, y) {
71
+ fakeWriters.moveCursor.call(this, x, y);
72
+ }
73
+ });
74
+ }
75
+
76
+ stream.getWindowSize = () => [80, 24];
35
77
  if (colorDepth !== undefined) {
36
78
  stream.getColorDepth = () => colorDepth;
37
79
  }
80
+
81
+ if (hasColors) {
82
+ stream.hasColors = makeHasColors(colorDepth);
83
+ }
38
84
  };
39
85
 
40
86
  if (options.tty.stderr) {
@@ -31,6 +31,7 @@ ipc.options.then(options => {
31
31
  }
32
32
 
33
33
  const runner = new Runner({
34
+ experiments: options.experiments,
34
35
  failFast: options.failFast,
35
36
  failWithoutAssertions: options.failWithoutAssertions,
36
37
  file: options.file,