ava 2.3.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/index.d.ts +71 -0
- package/lib/api.js +2 -1
- package/lib/cli.js +1 -0
- package/lib/load-config.js +15 -1
- package/lib/parse-test-args.js +15 -0
- package/lib/reporters/mini.js +5 -0
- package/lib/reporters/verbose.js +3 -0
- package/lib/runner.js +29 -27
- package/lib/serialize-error.js +2 -1
- package/lib/snapshot-manager.js +39 -20
- package/lib/test.js +221 -15
- package/lib/worker/subprocess.js +1 -0
- package/package.json +12 -13
- package/profile.js +1 -0
- package/readme.md +1 -0
package/index.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export type ThrowsExpectation = {
|
|
|
25
25
|
name?: string;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
export type CommitDiscardOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* Whether the logs should be included in those of the parent test.
|
|
31
|
+
*/
|
|
32
|
+
retainLogs?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
/** Options that can be passed to the `t.snapshot()` assertion. */
|
|
29
36
|
export type SnapshotOptions = {
|
|
30
37
|
/** If provided and not an empty string, used to select the snapshot to compare the `expected` value against. */
|
|
@@ -363,6 +370,7 @@ export interface ExecutionContext<Context = unknown> extends Assertions {
|
|
|
363
370
|
log: LogFn;
|
|
364
371
|
plan: PlanFn;
|
|
365
372
|
timeout: TimeoutFn;
|
|
373
|
+
try: TryFn<Context>;
|
|
366
374
|
}
|
|
367
375
|
|
|
368
376
|
export interface LogFn {
|
|
@@ -392,6 +400,69 @@ export interface TimeoutFn {
|
|
|
392
400
|
(ms: number): void;
|
|
393
401
|
}
|
|
394
402
|
|
|
403
|
+
export interface TryFn<Context = unknown> {
|
|
404
|
+
/**
|
|
405
|
+
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
|
|
406
|
+
* the test will fail. A macro may be provided. The title may help distinguish attempts from
|
|
407
|
+
* one another.
|
|
408
|
+
*/
|
|
409
|
+
<Args extends any[]>(title: string, fn: EitherMacro<Args, Context>, ...args: Args): Promise<TryResult>;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
|
|
413
|
+
* the test will fail. A macro may be provided. The title may help distinguish attempts from
|
|
414
|
+
* one another.
|
|
415
|
+
*/
|
|
416
|
+
<Args extends any[]>(title: string, fn: [EitherMacro<Args, Context>, ...EitherMacro<Args, Context>[]], ...args: Args): Promise<TryResult[]>;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
|
|
420
|
+
* the test will fail. A macro may be provided.
|
|
421
|
+
*/
|
|
422
|
+
<Args extends any[]>(fn: EitherMacro<Args, Context>, ...args: Args): Promise<TryResult>;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
|
|
426
|
+
* the test will fail. A macro may be provided.
|
|
427
|
+
*/
|
|
428
|
+
<Args extends any[]>(fn: [EitherMacro<Args, Context>, ...EitherMacro<Args, Context>[]], ...args: Args): Promise<TryResult[]>;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface AssertionError extends Error {}
|
|
432
|
+
|
|
433
|
+
export interface TryResult {
|
|
434
|
+
/**
|
|
435
|
+
* Title of the attempt, helping you tell attempts aparts.
|
|
436
|
+
*/
|
|
437
|
+
title: string;
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Indicates whether all assertions passed, or at least one failed.
|
|
441
|
+
*/
|
|
442
|
+
passed: boolean;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Errors raised for each failed assertion.
|
|
446
|
+
*/
|
|
447
|
+
errors: AssertionError[];
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Logs created during the attempt using `t.log()`. Contains formatted values.
|
|
451
|
+
*/
|
|
452
|
+
logs: string[];
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Commit the attempt. Counts as one assertion for the plan count. If the
|
|
456
|
+
* attempt failed, calling this will also cause your test to fail.
|
|
457
|
+
*/
|
|
458
|
+
commit(options?: CommitDiscardOptions): void;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Discard the attempt.
|
|
462
|
+
*/
|
|
463
|
+
discard(options?: CommitDiscardOptions): void;
|
|
464
|
+
}
|
|
465
|
+
|
|
395
466
|
/** The `t` value passed to implementations for tests & hooks declared with the `.cb` modifier. */
|
|
396
467
|
export interface CbExecutionContext<Context = unknown> extends ExecutionContext<Context> {
|
|
397
468
|
/**
|
package/lib/api.js
CHANGED
|
@@ -149,6 +149,7 @@ class Api extends Emittery {
|
|
|
149
149
|
|
|
150
150
|
await this.emit('run', {
|
|
151
151
|
clearLogOnNextRun: runtimeOptions.clearLogOnNextRun === true,
|
|
152
|
+
experiments: Object.keys(apiOptions.experiments),
|
|
152
153
|
failFastEnabled: failFast,
|
|
153
154
|
filePathPrefix: commonPathPrefix(files),
|
|
154
155
|
files,
|
|
@@ -323,7 +324,7 @@ class Api extends Emittery {
|
|
|
323
324
|
const forkExecArgv = execArgv.slice();
|
|
324
325
|
let flagName = '--inspect';
|
|
325
326
|
const oldValue = forkExecArgv[inspectArgIndex];
|
|
326
|
-
if (oldValue.
|
|
327
|
+
if (oldValue.includes('brk')) {
|
|
327
328
|
flagName += '-brk';
|
|
328
329
|
}
|
|
329
330
|
|
package/lib/cli.js
CHANGED
|
@@ -238,6 +238,7 @@ exports.run = async () => { // eslint-disable-line complexity
|
|
|
238
238
|
color: conf.color,
|
|
239
239
|
compileEnhancements: conf.compileEnhancements !== false,
|
|
240
240
|
concurrency: conf.concurrency ? parseInt(conf.concurrency, 10) : 0,
|
|
241
|
+
experiments: conf.nonSemVerExperiments,
|
|
241
242
|
extensions,
|
|
242
243
|
failFast: conf.failFast,
|
|
243
244
|
failWithoutAssertions: conf.failWithoutAssertions !== false,
|
package/lib/load-config.js
CHANGED
|
@@ -6,6 +6,7 @@ const pkgConf = require('pkg-conf');
|
|
|
6
6
|
|
|
7
7
|
const NO_SUCH_FILE = Symbol('no ava.config.js file');
|
|
8
8
|
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
|
|
9
|
+
const EXPERIMENTS = new Set(['tryAssertion']);
|
|
9
10
|
|
|
10
11
|
function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) { // eslint-disable-line complexity
|
|
11
12
|
let packageConf = pkgConf.sync('ava', {cwd: resolveFrom});
|
|
@@ -82,7 +83,20 @@ function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
const config = {...defaults, nonSemVerExperiments: {}, ...fileConf, ...packageConf, projectDir};
|
|
87
|
+
|
|
88
|
+
const {nonSemVerExperiments: experiments} = config;
|
|
89
|
+
if (!isPlainObject(experiments)) {
|
|
90
|
+
throw new Error(`nonSemVerExperiments from ${fileForErrorMessage} must be an object`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const key of Object.keys(experiments)) {
|
|
94
|
+
if (!EXPERIMENTS.has(key)) {
|
|
95
|
+
throw new Error(`nonSemVerExperiments.${key} from ${fileForErrorMessage} is not a supported experiment`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return config;
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
module.exports = loadConfig;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
function parseTestArgs(args) {
|
|
3
|
+
const rawTitle = typeof args[0] === 'string' ? args.shift() : undefined;
|
|
4
|
+
const receivedImplementationArray = Array.isArray(args[0]);
|
|
5
|
+
const implementations = receivedImplementationArray ? args.shift() : args.splice(0, 1);
|
|
6
|
+
|
|
7
|
+
const buildTitle = implementation => {
|
|
8
|
+
const title = implementation.title ? implementation.title(rawTitle, ...args) : rawTitle;
|
|
9
|
+
return {title, isSet: typeof title !== 'undefined', isValid: typeof title === 'string', isEmpty: !title};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return {args, buildTitle, implementations, rawTitle, receivedImplementationArray};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = parseTestArgs;
|
package/lib/reporters/mini.js
CHANGED
|
@@ -135,6 +135,11 @@ class MiniReporter {
|
|
|
135
135
|
|
|
136
136
|
cliCursor.hide(this.reportStream);
|
|
137
137
|
this.lineWriter.writeLine();
|
|
138
|
+
|
|
139
|
+
if (plan.experiments.length > 0) {
|
|
140
|
+
this.lineWriter.writeLine(colors.information(`${figures.warning} Experiments are enabled. These are unsupported and may change or be be removed at any time.`));
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
this.spinner.start();
|
|
139
144
|
}
|
|
140
145
|
|
package/lib/reporters/verbose.js
CHANGED
|
@@ -97,6 +97,9 @@ class VerboseReporter {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
this.lineWriter.writeLine();
|
|
100
|
+
if (plan.experiments.length > 0) {
|
|
101
|
+
this.lineWriter.writeLine(colors.information(`${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.${os.EOL}`));
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
consumeStateChange(evt) { // eslint-disable-line complexity
|
package/lib/runner.js
CHANGED
|
@@ -3,6 +3,7 @@ const Emittery = require('emittery');
|
|
|
3
3
|
const matcher = require('matcher');
|
|
4
4
|
const ContextRef = require('./context-ref');
|
|
5
5
|
const createChain = require('./create-chain');
|
|
6
|
+
const parseTestArgs = require('./parse-test-args');
|
|
6
7
|
const snapshotManager = require('./snapshot-manager');
|
|
7
8
|
const serializeError = require('./serialize-error');
|
|
8
9
|
const Runnable = require('./test');
|
|
@@ -11,6 +12,7 @@ class Runner extends Emittery {
|
|
|
11
12
|
constructor(options = {}) {
|
|
12
13
|
super();
|
|
13
14
|
|
|
15
|
+
this.experiments = options.experiments || {};
|
|
14
16
|
this.failFast = options.failFast === true;
|
|
15
17
|
this.failWithoutAssertions = options.failWithoutAssertions !== false;
|
|
16
18
|
this.file = options.file;
|
|
@@ -39,12 +41,21 @@ class Runner extends Emittery {
|
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
const uniqueTestTitles = new Set();
|
|
44
|
+
this.registerUniqueTitle = title => {
|
|
45
|
+
if (uniqueTestTitles.has(title)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
uniqueTestTitles.add(title);
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
|
|
42
53
|
let hasStarted = false;
|
|
43
54
|
let scheduledStart = false;
|
|
44
55
|
const meta = Object.freeze({
|
|
45
56
|
file: options.file
|
|
46
57
|
});
|
|
47
|
-
this.chain = createChain((metadata,
|
|
58
|
+
this.chain = createChain((metadata, testArgs) => { // eslint-disable-line complexity
|
|
48
59
|
if (hasStarted) {
|
|
49
60
|
throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.');
|
|
50
61
|
}
|
|
@@ -57,40 +68,33 @@ class Runner extends Emittery {
|
|
|
57
68
|
});
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
const
|
|
61
|
-
args.shift() :
|
|
62
|
-
undefined;
|
|
63
|
-
const implementations = Array.isArray(args[0]) ?
|
|
64
|
-
args.shift() :
|
|
65
|
-
args.splice(0, 1);
|
|
71
|
+
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
|
|
66
72
|
|
|
67
73
|
if (metadata.todo) {
|
|
68
74
|
if (implementations.length > 0) {
|
|
69
75
|
throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
if (
|
|
78
|
+
if (!rawTitle) { // Either undefined or a string.
|
|
73
79
|
throw new TypeError('`todo` tests require a title');
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
if (
|
|
77
|
-
throw new Error(`Duplicate test title: ${
|
|
78
|
-
} else {
|
|
79
|
-
uniqueTestTitles.add(specifiedTitle);
|
|
82
|
+
if (!this.registerUniqueTitle(rawTitle)) {
|
|
83
|
+
throw new Error(`Duplicate test title: ${rawTitle}`);
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
if (this.match.length > 0) {
|
|
83
87
|
// --match selects TODO tests.
|
|
84
|
-
if (matcher([
|
|
88
|
+
if (matcher([rawTitle], this.match).length === 1) {
|
|
85
89
|
metadata.exclusive = true;
|
|
86
90
|
this.runOnlyExclusive = true;
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
this.tasks.todo.push({title:
|
|
94
|
+
this.tasks.todo.push({title: rawTitle, metadata});
|
|
91
95
|
this.emit('stateChange', {
|
|
92
96
|
type: 'declared-test',
|
|
93
|
-
title:
|
|
97
|
+
title: rawTitle,
|
|
94
98
|
knownFailing: false,
|
|
95
99
|
todo: true
|
|
96
100
|
});
|
|
@@ -100,15 +104,13 @@ class Runner extends Emittery {
|
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
for (const implementation of implementations) {
|
|
103
|
-
let title = implementation
|
|
104
|
-
implementation.title(specifiedTitle, ...args) :
|
|
105
|
-
specifiedTitle;
|
|
107
|
+
let {title, isSet, isValid, isEmpty} = buildTitle(implementation);
|
|
106
108
|
|
|
107
|
-
if (
|
|
109
|
+
if (isSet && !isValid) {
|
|
108
110
|
throw new TypeError('Test & hook titles must be strings');
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
if (
|
|
113
|
+
if (isEmpty) {
|
|
112
114
|
if (metadata.type === 'test') {
|
|
113
115
|
throw new TypeError('Tests must have a title');
|
|
114
116
|
} else if (metadata.always) {
|
|
@@ -118,12 +120,8 @@ class Runner extends Emittery {
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
if (metadata.type === 'test') {
|
|
122
|
-
|
|
123
|
-
throw new Error(`Duplicate test title: ${title}`);
|
|
124
|
-
} else {
|
|
125
|
-
uniqueTestTitles.add(title);
|
|
126
|
-
}
|
|
123
|
+
if (metadata.type === 'test' && !this.registerUniqueTitle(title)) {
|
|
124
|
+
throw new Error(`Duplicate test title: ${title}`);
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
const task = {
|
|
@@ -162,6 +160,7 @@ class Runner extends Emittery {
|
|
|
162
160
|
todo: false,
|
|
163
161
|
failing: false,
|
|
164
162
|
callback: false,
|
|
163
|
+
inline: false, // Set for attempt metadata created by `t.try()`
|
|
165
164
|
always: false
|
|
166
165
|
}, meta);
|
|
167
166
|
}
|
|
@@ -269,6 +268,7 @@ class Runner extends Emittery {
|
|
|
269
268
|
async runHooks(tasks, contextRef, titleSuffix) {
|
|
270
269
|
const hooks = tasks.map(task => new Runnable({
|
|
271
270
|
contextRef,
|
|
271
|
+
experiments: this.experiments,
|
|
272
272
|
failWithoutAssertions: false,
|
|
273
273
|
fn: task.args.length === 0 ?
|
|
274
274
|
task.implementation :
|
|
@@ -309,6 +309,7 @@ class Runner extends Emittery {
|
|
|
309
309
|
// Only run the test if all `beforeEach` hooks passed.
|
|
310
310
|
const test = new Runnable({
|
|
311
311
|
contextRef,
|
|
312
|
+
experiments: this.experiments,
|
|
312
313
|
failWithoutAssertions: this.failWithoutAssertions,
|
|
313
314
|
fn: task.args.length === 0 ?
|
|
314
315
|
task.implementation :
|
|
@@ -316,7 +317,8 @@ class Runner extends Emittery {
|
|
|
316
317
|
compareTestSnapshot: this.boundCompareTestSnapshot,
|
|
317
318
|
updateSnapshots: this.updateSnapshots,
|
|
318
319
|
metadata: task.metadata,
|
|
319
|
-
title: task.title
|
|
320
|
+
title: task.title,
|
|
321
|
+
registerUniqueTitle: this.registerUniqueTitle
|
|
320
322
|
});
|
|
321
323
|
|
|
322
324
|
const result = await this.runSingle(test);
|
package/lib/serialize-error.js
CHANGED
|
@@ -39,7 +39,8 @@ function buildSource(source) {
|
|
|
39
39
|
const file = path.resolve(projectDir, source.file.trim());
|
|
40
40
|
const rel = path.relative(projectDir, file);
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const [segment] = rel.split(path.sep);
|
|
43
|
+
const isWithinProject = segment !== '..' && (process.platform !== 'win32' || !segment.includes(':'));
|
|
43
44
|
const isDependency = isWithinProject && path.dirname(rel).split(path.sep).includes('node_modules');
|
|
44
45
|
|
|
45
46
|
return {
|
package/lib/snapshot-manager.js
CHANGED
|
@@ -305,45 +305,64 @@ class Manager {
|
|
|
305
305
|
compare(options) {
|
|
306
306
|
const hash = md5Hex(options.belongsTo);
|
|
307
307
|
const entries = this.snapshotsByHash.get(hash) || [];
|
|
308
|
-
|
|
309
|
-
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, exceeds expected index of ${entries.length}`);
|
|
310
|
-
}
|
|
308
|
+
const snapshotBuffer = entries[options.index];
|
|
311
309
|
|
|
312
|
-
if (
|
|
310
|
+
if (!snapshotBuffer) {
|
|
313
311
|
if (!this.recordNewSnapshots) {
|
|
314
312
|
return {pass: false};
|
|
315
313
|
}
|
|
316
314
|
|
|
315
|
+
if (options.deferRecording) {
|
|
316
|
+
const record = this.deferRecord(hash, options);
|
|
317
|
+
return {pass: true, record};
|
|
318
|
+
}
|
|
319
|
+
|
|
317
320
|
this.record(hash, options);
|
|
318
321
|
return {pass: true};
|
|
319
322
|
}
|
|
320
323
|
|
|
321
|
-
const snapshotBuffer = entries[options.index];
|
|
322
324
|
const actual = concordance.deserialize(snapshotBuffer, concordanceOptions);
|
|
323
|
-
|
|
324
325
|
const expected = concordance.describe(options.expected, concordanceOptions);
|
|
325
326
|
const pass = concordance.compareDescriptors(actual, expected);
|
|
326
327
|
|
|
327
328
|
return {actual, expected, pass};
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
|
|
331
|
+
deferRecord(hash, options) {
|
|
331
332
|
const descriptor = concordance.describe(options.expected, concordanceOptions);
|
|
332
|
-
|
|
333
|
-
this.hasChanges = true;
|
|
334
333
|
const snapshot = concordance.serialize(descriptor);
|
|
335
|
-
if (this.snapshotsByHash.has(hash)) {
|
|
336
|
-
this.snapshotsByHash.get(hash).push(snapshot);
|
|
337
|
-
} else {
|
|
338
|
-
this.snapshotsByHash.set(hash, [snapshot]);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
334
|
const entry = formatEntry(options.label, descriptor);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
335
|
+
|
|
336
|
+
return () => { // Must be called in order!
|
|
337
|
+
this.hasChanges = true;
|
|
338
|
+
|
|
339
|
+
let snapshots = this.snapshotsByHash.get(hash);
|
|
340
|
+
if (!snapshots) {
|
|
341
|
+
snapshots = [];
|
|
342
|
+
this.snapshotsByHash.set(hash, snapshots);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (options.index > snapshots.length) {
|
|
346
|
+
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, exceeds expected index of ${snapshots.length}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (options.index < snapshots.length) {
|
|
350
|
+
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, already exists`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
snapshots.push(snapshot);
|
|
354
|
+
|
|
355
|
+
if (this.reportEntries.has(options.belongsTo)) {
|
|
356
|
+
this.reportEntries.get(options.belongsTo).push(entry);
|
|
357
|
+
} else {
|
|
358
|
+
this.reportEntries.set(options.belongsTo, [entry]);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
record(hash, options) {
|
|
364
|
+
const record = this.deferRecord(hash, options);
|
|
365
|
+
record();
|
|
347
366
|
}
|
|
348
367
|
|
|
349
368
|
save() {
|
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) {
|
|
@@ -67,6 +68,95 @@ class ExecutionContext extends assert.Assertions {
|
|
|
67
68
|
this.timeout = ms => {
|
|
68
69
|
test.timeout(ms);
|
|
69
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
|
+
};
|
|
70
160
|
}
|
|
71
161
|
|
|
72
162
|
get end() {
|
|
@@ -99,32 +189,74 @@ class ExecutionContext extends assert.Assertions {
|
|
|
99
189
|
class Test {
|
|
100
190
|
constructor(options) {
|
|
101
191
|
this.contextRef = options.contextRef;
|
|
192
|
+
this.experiments = options.experiments || {};
|
|
102
193
|
this.failWithoutAssertions = options.failWithoutAssertions;
|
|
103
194
|
this.fn = options.fn;
|
|
104
195
|
this.metadata = options.metadata;
|
|
105
196
|
this.title = options.title;
|
|
197
|
+
this.registerUniqueTitle = options.registerUniqueTitle;
|
|
106
198
|
this.logs = [];
|
|
107
199
|
|
|
108
|
-
this.
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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;
|
|
115
221
|
};
|
|
116
222
|
|
|
117
223
|
this.skipSnapshot = () => {
|
|
118
224
|
if (options.updateSnapshots) {
|
|
119
225
|
this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots'));
|
|
120
226
|
} else {
|
|
121
|
-
this.
|
|
227
|
+
this.nextSnapshotIndex++;
|
|
228
|
+
this.snapshotCount++;
|
|
122
229
|
this.countPassedAssertion();
|
|
123
230
|
}
|
|
124
231
|
};
|
|
125
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
|
+
|
|
126
257
|
this.assertCount = 0;
|
|
127
258
|
this.assertError = undefined;
|
|
259
|
+
this.attemptCount = 0;
|
|
128
260
|
this.calledEnd = false;
|
|
129
261
|
this.duration = null;
|
|
130
262
|
this.endCallbackFinisher = null;
|
|
@@ -133,11 +265,12 @@ class Test {
|
|
|
133
265
|
this.finishDueToTimeout = null;
|
|
134
266
|
this.finishing = false;
|
|
135
267
|
this.pendingAssertionCount = 0;
|
|
268
|
+
this.pendingAttemptCount = 0;
|
|
136
269
|
this.pendingThrowsAssertion = null;
|
|
137
270
|
this.planCount = null;
|
|
138
271
|
this.startedAt = 0;
|
|
139
|
-
this.timeoutTimer = null;
|
|
140
272
|
this.timeoutMs = 0;
|
|
273
|
+
this.timeoutTimer = null;
|
|
141
274
|
}
|
|
142
275
|
|
|
143
276
|
bindEndCallback() {
|
|
@@ -147,7 +280,11 @@ class Test {
|
|
|
147
280
|
};
|
|
148
281
|
}
|
|
149
282
|
|
|
150
|
-
|
|
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
|
+
}
|
|
151
288
|
}
|
|
152
289
|
|
|
153
290
|
endCallback(error, savedError) {
|
|
@@ -181,6 +318,10 @@ class Test {
|
|
|
181
318
|
this.saveFirstError(new Error('Assertion passed, but test has already finished'));
|
|
182
319
|
}
|
|
183
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
|
+
|
|
184
325
|
this.assertCount++;
|
|
185
326
|
this.refreshTimeout();
|
|
186
327
|
}
|
|
@@ -191,7 +332,11 @@ class Test {
|
|
|
191
332
|
|
|
192
333
|
addPendingAssertion(promise) {
|
|
193
334
|
if (this.finishing) {
|
|
194
|
-
this.saveFirstError(new Error('Assertion
|
|
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'));
|
|
195
340
|
}
|
|
196
341
|
|
|
197
342
|
this.assertCount++;
|
|
@@ -211,11 +356,53 @@ class Test {
|
|
|
211
356
|
this.saveFirstError(new Error('Assertion failed, but test has already finished'));
|
|
212
357
|
}
|
|
213
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
|
+
|
|
214
363
|
this.assertCount++;
|
|
215
364
|
this.refreshTimeout();
|
|
216
365
|
this.saveFirstError(error);
|
|
217
366
|
}
|
|
218
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
|
+
|
|
219
406
|
saveFirstError(error) {
|
|
220
407
|
if (!this.assertError) {
|
|
221
408
|
this.assertError = error;
|
|
@@ -279,11 +466,27 @@ class Test {
|
|
|
279
466
|
}
|
|
280
467
|
|
|
281
468
|
verifyAssertions() {
|
|
282
|
-
if (
|
|
283
|
-
|
|
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) {
|
|
284
489
|
this.saveFirstError(new Error('Test finished without running any assertions'));
|
|
285
|
-
} else if (this.pendingAssertionCount > 0) {
|
|
286
|
-
this.saveFirstError(new Error('Test finished, but an assertion is still pending'));
|
|
287
490
|
}
|
|
288
491
|
}
|
|
289
492
|
}
|
|
@@ -476,11 +679,14 @@ class Test {
|
|
|
476
679
|
}
|
|
477
680
|
|
|
478
681
|
return {
|
|
682
|
+
deferredSnapshotRecordings: this.deferredSnapshotRecordings,
|
|
479
683
|
duration: this.duration,
|
|
480
684
|
error,
|
|
481
685
|
logs: this.logs,
|
|
482
686
|
metadata: this.metadata,
|
|
483
687
|
passed,
|
|
688
|
+
snapshotCount: this.snapshotCount,
|
|
689
|
+
assertCount: this.assertCount,
|
|
484
690
|
title: this.title
|
|
485
691
|
};
|
|
486
692
|
}
|
package/lib/worker/subprocess.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ava",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Testing can be a drag. AVA helps you get it done.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "avajs/ava",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"lint": "xo",
|
|
14
|
-
"test:tap": "tap
|
|
14
|
+
"test:tap": "tap",
|
|
15
15
|
"test:typescript": "tsc --noEmit -p test/ts-types",
|
|
16
16
|
"test": "npm run lint && npm run test:typescript && npm run test:tap"
|
|
17
17
|
},
|
|
@@ -59,11 +59,11 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@ava/babel-preset-stage-4": "^4.0.0",
|
|
61
61
|
"@ava/babel-preset-transform-test-files": "^6.0.0",
|
|
62
|
-
"@babel/core": "^7.
|
|
63
|
-
"@babel/generator": "^7.
|
|
62
|
+
"@babel/core": "^7.6.0",
|
|
63
|
+
"@babel/generator": "^7.6.0",
|
|
64
64
|
"@concordance/react": "^2.0.0",
|
|
65
65
|
"ansi-escapes": "^4.2.1",
|
|
66
|
-
"ansi-styles": "^4.
|
|
66
|
+
"ansi-styles": "^4.1.0",
|
|
67
67
|
"arr-flatten": "^1.1.0",
|
|
68
68
|
"array-union": "^2.1.0",
|
|
69
69
|
"array-uniq": "^2.1.0",
|
|
@@ -132,27 +132,26 @@
|
|
|
132
132
|
"write-file-atomic": "^3.0.0"
|
|
133
133
|
},
|
|
134
134
|
"devDependencies": {
|
|
135
|
-
"@types/node": "^10.14.
|
|
135
|
+
"@types/node": "^10.14.18",
|
|
136
136
|
"cli-table3": "^0.5.1",
|
|
137
|
-
"codecov": "^3.5.0",
|
|
138
137
|
"delay": "^4.3.0",
|
|
139
|
-
"execa": "^2.0.
|
|
138
|
+
"execa": "^2.0.4",
|
|
140
139
|
"get-stream": "^5.1.0",
|
|
141
140
|
"git-branch": "^2.0.1",
|
|
142
|
-
"has-ansi": "^
|
|
141
|
+
"has-ansi": "^4.0.0",
|
|
143
142
|
"lolex": "^4.2.0",
|
|
144
|
-
"proxyquire": "^2.1.
|
|
143
|
+
"proxyquire": "^2.1.3",
|
|
145
144
|
"react": "^16.9.0",
|
|
146
145
|
"react-test-renderer": "^16.9.0",
|
|
147
146
|
"replace-string": "^3.0.0",
|
|
148
147
|
"signal-exit": "^3.0.0",
|
|
149
|
-
"sinon": "^7.4.
|
|
148
|
+
"sinon": "^7.4.2",
|
|
150
149
|
"source-map-fixtures": "^2.1.0",
|
|
151
|
-
"tap": "^14.6.
|
|
150
|
+
"tap": "^14.6.4",
|
|
152
151
|
"temp-write": "^4.0.0",
|
|
153
152
|
"touch": "^3.1.0",
|
|
154
153
|
"ts-node": "^8.3.0",
|
|
155
|
-
"typescript": "^3.
|
|
154
|
+
"typescript": "^3.6.3",
|
|
156
155
|
"xo": "^0.24.0",
|
|
157
156
|
"zen-observable": "^0.8.14"
|
|
158
157
|
},
|
package/profile.js
CHANGED
package/readme.md
CHANGED
|
@@ -216,6 +216,7 @@ It's the [Andromeda galaxy](https://simple.wikipedia.org/wiki/Andromeda_galaxy).
|
|
|
216
216
|
- [AVA stickers, t-shirts, etc](https://www.redbubble.com/people/sindresorhus/works/30330590-ava-logo)
|
|
217
217
|
- [Awesome list](https://github.com/avajs/awesome-ava)
|
|
218
218
|
- [AVA Casts](http://avacasts.com)
|
|
219
|
+
- [Do you like AVA? Donate here!](https://opencollective.com/ava)
|
|
219
220
|
- [More…](https://github.com/avajs/awesome-ava)
|
|
220
221
|
|
|
221
222
|
## Team
|