Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add try-commit to test assertions #1947

Merged
merged 113 commits into from
Sep 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
40fa08e
Add try-commit to test assertions
qlonik Oct 6, 2018
2521b31
Don't use rest symbol to support Node 6
qlonik Oct 7, 2018
8afddc2
Update TS typings with t.try()
qlonik Oct 8, 2018
d64a6fb
Refactor tryTest: pull toCommit() defn from promise
qlonik Oct 10, 2018
fccf41b
Make sure commit()/discard() only called once
qlonik Oct 10, 2018
d1d559f
No need to save return error of attempt twice
qlonik Oct 10, 2018
6be281f
Use rest and spread on arguments to try function
qlonik Jan 15, 2019
eeb861b
Count pending before executing passed fn
qlonik Jan 15, 2019
7153337
Make behaviour of try more implicit
qlonik Jan 15, 2019
62ffd9b
Return array of assertion errors from commit()
qlonik Jan 15, 2019
ba690b7
Fix tests of the test class
qlonik Jan 15, 2019
a4f0fee
Fix error message when test finished early
qlonik Jan 15, 2019
7750dfc
Save logs from discard if needed
qlonik Jan 20, 2019
476148c
Remove discard before promise resolved
qlonik Jan 20, 2019
42a7245
Move attempt counting into Test class
qlonik Jan 20, 2019
e71ee0b
Add optional title to attempt
qlonik Jan 20, 2019
a5f0772
Fix errors in strings returned to a user
qlonik Jan 28, 2019
eb49df2
Add documentation to AttemptReturnValue
qlonik Jan 28, 2019
a22e97c
Rename AttemptReturnValue into AttemptResult
qlonik Jan 28, 2019
46772a7
Test snapshots as part of try-commit
qlonik Jan 28, 2019
40ede92
Format with xo
qlonik Jan 28, 2019
fd7be32
Adjust TypeScript definition
novemberborn Feb 10, 2019
279057f
Remove (currently) unnecessary check for test results containing mult…
novemberborn Feb 10, 2019
ed5db80
Count number of assertions performed in subtest
qlonik Mar 17, 2019
51323c1
Fix error where failed attempt was not discarded
qlonik Mar 17, 2019
fe8ebf8
Adjust test counting number of assertions performed
qlonik Mar 17, 2019
c28152f
Always fail when there are some non committed/discarded attempts
qlonik Mar 17, 2019
7fa7cf5
Add support for macros passed to attempt
qlonik Mar 17, 2019
95c8585
Make sure try-commit follows timeout settings
qlonik Mar 17, 2019
b43626d
Get rid of runtime conditional types
qlonik Mar 17, 2019
226044c
Type try fn to accept macros
qlonik Mar 17, 2019
c9643f7
Add macro test with parameter inference
qlonik Mar 17, 2019
cc5d4ae
Test types of try fn
qlonik Mar 18, 2019
5384876
Add new metadata type: inline
qlonik Apr 1, 2019
786a446
Fix complicated if-else statement
qlonik Apr 1, 2019
6533c76
Fix test to check assertCount instead of attemptCount
qlonik Apr 1, 2019
42f4b7c
Add test checking that .end() is not allowed inside try-commit
qlonik Apr 1, 2019
6b4dc21
Move ava test initializer into helper file
qlonik Apr 1, 2019
92ff524
Move try-commit tests into separate file
qlonik Apr 1, 2019
19c8d88
Check t.try is properly bound
qlonik Apr 6, 2019
fa7204c
Make sure inline test cannot access context
qlonik Apr 8, 2019
d627b13
Merge branch 'master' into test-try-commit-assertion
qlonik Apr 23, 2019
23fe80c
Fix exported name of attempt function
qlonik Apr 23, 2019
dc3060f
Use async/await in try implementation
qlonik Apr 23, 2019
be19cb8
Use object spread instead of Object.assign
qlonik Apr 23, 2019
ea65141
Fix error where `this` was used instead of `test`
qlonik Apr 23, 2019
f20495e
Merge branch 'master' into test-try-commit-assertion
novemberborn May 31, 2019
87603f5
Remove TryFn#skip() from type definition
novemberborn May 31, 2019
38c5bee
Change AssertionError to an interface
novemberborn May 31, 2019
8a6de55
Tweak type definition & comments
novemberborn May 31, 2019
f8c6bb1
Document metadata.inline
novemberborn May 31, 2019
bffb7a7
Reuse test argument parsing logic
novemberborn May 31, 2019
aca1d64
Single quotes
novemberborn May 31, 2019
5c31c1e
Fix result typing when t.try() is called with an array of implementat…
novemberborn May 31, 2019
6385ea1
Merge branch 'master' into test-try-commit-assertion
novemberborn Jun 1, 2019
e3f9b7c
Copy the context ref when building the attempt test
novemberborn Jun 1, 2019
4cbf0e1
t.try() attempts can never be marked as failing. Override metadata fr…
novemberborn Jun 1, 2019
ded6e4b
t.try() attempts can never use callback mode. Override metedata from …
novemberborn Jun 1, 2019
a14459f
Tweak error thrown when using t.end() inside a t.try()
novemberborn Jun 1, 2019
e5229d1
Untangle logic in verifyAssertion(), add some comments
novemberborn Jun 1, 2019
a3b0f88
Reorder Test property initializations
novemberborn Jun 1, 2019
6408c10
Refactor test title building and validation
novemberborn Jun 1, 2019
d2b85da
Ensure `t.try()` titles are unique
novemberborn Jun 1, 2019
2d6cb65
Refactor t.try() implementation
novemberborn Jun 1, 2019
c68deca
Make Context optional in TryFn interface
novemberborn Jun 2, 2019
8767cf9
Change default type of Context to 'unknown'
novemberborn Jun 2, 2019
b9dbadb
Copy logs that are exposed as the attempt result
novemberborn Jun 3, 2019
b325cde
Rename AttemptResult to TryResult
novemberborn Jun 3, 2019
30e522a
Only record snapshots when attempt is committed
novemberborn Jun 3, 2019
fcbffb7
Fix detection of callback test that ended with pending assertions rem…
novemberborn Jun 3, 2019
34bc3d4
Merge branch 'master' into test-try-commit-assertion
novemberborn Jun 16, 2019
bc1c951
Just pass an array to parseTestArgs
novemberborn Jun 16, 2019
31be1f7
Fix regressions introduced by parseTestArgs extraction
novemberborn Jun 16, 2019
c5c5f00
Remove call to removed fn addPendingAttemptAssertion
qlonik Jul 1, 2019
a01d4e0
Store registerUniqueTitle() on test instance from options
qlonik Jul 1, 2019
5baf9ad
Add dummy impl of registerUniqueTitle to tests
qlonik Jul 1, 2019
589e332
Use real ContextRef in test instead of dummy
qlonik Jul 1, 2019
40765fc
Pass real ContextRef instance at call-site in test-try-commit
qlonik Jul 1, 2019
edaf7fe
Fix try result resolution when passing array of test fns
qlonik Jul 1, 2019
7f489b5
Return assertCount value from run()
qlonik Jul 1, 2019
b2e9e80
Adjust test titles in try-commit title check
qlonik Jul 1, 2019
10ec2f5
Remove redundant error message check
qlonik Jul 1, 2019
89f6a5a
Adjust test to access parent context since it is allowed now
qlonik Jul 1, 2019
64461ff
Extract context data into variable in test checking context
qlonik Jul 1, 2019
cdf3403
Remove unused variable in test-try-commit
qlonik Jul 1, 2019
506bddf
Allow creating new snapshots with the value of updating
qlonik Jul 2, 2019
4f42253
Pass real contextRef and dummy registerUniqueTitle()
qlonik Jul 2, 2019
a374981
Allow passing title of the test per test case
qlonik Jul 2, 2019
2b537bb
Create not equal amount of snapshots in each attempt
qlonik Jul 2, 2019
a6fe8d2
Refactor try-snapshot test to have one snapshot manager
qlonik Jul 2, 2019
a7e2a7f
Add missing nextSnapshotIndex update when committing to attempt
qlonik Jul 2, 2019
5e87468
Update expected snapshot result
qlonik Jul 2, 2019
6d72cb0
Remove unneeded logging of failed test
qlonik Jul 2, 2019
9b41685
Update expectation for test with concurrent attempts
qlonik Jul 2, 2019
e5913f8
Merge branch 'master' into test-try-commit-assertion
novemberborn Jul 14, 2019
aca26c7
Remove unnecessary presence test
qlonik Jul 14, 2019
8abeb18
Switch try-snapshot tests to use async-await
qlonik Jul 14, 2019
3b677ae
Inline attempt functions in try call
qlonik Jul 14, 2019
6a7114d
Clarify title for failing test with try-commit
qlonik Jul 14, 2019
7b3a698
Remove test which no longer applies
qlonik Jul 14, 2019
ec00c90
Convert tests to use async-await
qlonik Jul 14, 2019
c17d2ab
Rename tests as suggested by novemberborn
qlonik Jul 14, 2019
d64c8d4
Add test cases suggested by novemberborn
qlonik Jul 14, 2019
2e624e1
Check that assert within attempt does not refresh test timeout
qlonik Jul 14, 2019
9503cb6
Merge branch 'test-try-commit-assertion' of https://github.com/qlonik…
novemberborn Aug 18, 2019
1652c4e
Merge branch 'master' into test-try-commit-assertion
novemberborn Aug 18, 2019
d26575e
Report pending attempts or assertions before verifying whether there …
novemberborn Aug 18, 2019
1920792
Attempts count as a single assertion for parent tests
novemberborn Aug 18, 2019
16cc21e
Fix error when an asynchronous assertion is started after the test ha…
novemberborn Aug 18, 2019
bb458be
Assertions must not be run outside of an active attempt
novemberborn Aug 18, 2019
c302923
Mark t.try() as experimental in the type definition
novemberborn Aug 18, 2019
fa7812d
Merge branch 'master' into test-try-commit-assertion
novemberborn Sep 8, 2019
2b8ba3a
Require opt-in for t.try()
novemberborn Sep 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions docs/06-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ AVA has a minimum depth of `3`.

## Experiments

From time to time, AVA will implement experimental features. These may change or be removed at any time, not just when there's a new major version. You can opt-in to such a feature by enabling it in the `nonSemVerExperiments` configuration.
From time to time, AVA will implement experimental features. These may change or be removed at any time, not just when there's a new major version. You can opt in to such a feature by enabling it in the `nonSemVerExperiments` configuration.

`ava.config.js`:
```js
Expand All @@ -175,6 +175,15 @@ export default {
};
```

There are currently no such features available.
You can opt in to the new `t.try()` assertion by specifying `tryAssertion`:

`ava.config.js`:
```js
export default {
nonSemVerExperiments: {
tryAssertion: true
}
};
```

[CLI]: ./05-command-line.md
71 changes: 71 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export type ThrowsExpectation = {
name?: string;
};

export type CommitDiscardOptions = {
/**
* Whether the logs should be included in those of the parent test.
*/
retainLogs?: boolean
}

/** Options that can be passed to the `t.snapshot()` assertion. */
export type SnapshotOptions = {
/** If provided and not an empty string, used to select the snapshot to compare the `expected` value against. */
Expand Down Expand Up @@ -363,6 +370,7 @@ export interface ExecutionContext<Context = unknown> extends Assertions {
log: LogFn;
plan: PlanFn;
timeout: TimeoutFn;
try: TryFn<Context>;
}

export interface LogFn {
Expand Down Expand Up @@ -392,6 +400,69 @@ export interface TimeoutFn {
(ms: number): void;
}

export interface TryFn<Context = unknown> {
/**
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
* the test will fail. A macro may be provided. The title may help distinguish attempts from
* one another.
*/
<Args extends any[]>(title: string, fn: EitherMacro<Args, Context>, ...args: Args): Promise<TryResult>;

/**
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
* the test will fail. A macro may be provided. The title may help distinguish attempts from
* one another.
*/
<Args extends any[]>(title: string, fn: [EitherMacro<Args, Context>, ...EitherMacro<Args, Context>[]], ...args: Args): Promise<TryResult[]>;

/**
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
* the test will fail. A macro may be provided.
*/
<Args extends any[]>(fn: EitherMacro<Args, Context>, ...args: Args): Promise<TryResult>;

/**
* Requires opt-in. Attempt to run some assertions. The result must be explicitly committed or discarded or else
* the test will fail. A macro may be provided.
*/
<Args extends any[]>(fn: [EitherMacro<Args, Context>, ...EitherMacro<Args, Context>[]], ...args: Args): Promise<TryResult[]>;
}

export interface AssertionError extends Error {}

export interface TryResult {
/**
* Title of the attempt, helping you tell attempts aparts.
*/
title: string;

/**
* Indicates whether all assertions passed, or at least one failed.
*/
passed: boolean;

/**
* Errors raised for each failed assertion.
*/
errors: AssertionError[];

/**
* Logs created during the attempt using `t.log()`. Contains formatted values.
*/
logs: string[];

/**
* Commit the attempt. Counts as one assertion for the plan count. If the
* attempt failed, calling this will also cause your test to fail.
*/
commit(options?: CommitDiscardOptions): void;

/**
* Discard the attempt.
*/
discard(options?: CommitDiscardOptions): void;
}

/** The `t` value passed to implementations for tests & hooks declared with the `.cb` modifier. */
export interface CbExecutionContext<Context = unknown> extends ExecutionContext<Context> {
/**
Expand Down
2 changes: 1 addition & 1 deletion lib/load-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const pkgConf = require('pkg-conf');

const NO_SUCH_FILE = Symbol('no ava.config.js file');
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
const EXPERIMENTS = new Set([]);
const EXPERIMENTS = new Set(['tryAssertion']);

function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) { // eslint-disable-line complexity
let packageConf = pkgConf.sync('ava', {cwd: resolveFrom});
Expand Down
15 changes: 15 additions & 0 deletions lib/parse-test-args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';
function parseTestArgs(args) {
const rawTitle = typeof args[0] === 'string' ? args.shift() : undefined;
const receivedImplementationArray = Array.isArray(args[0]);
const implementations = receivedImplementationArray ? args.shift() : args.splice(0, 1);

const buildTitle = implementation => {
const title = implementation.title ? implementation.title(rawTitle, ...args) : rawTitle;
return {title, isSet: typeof title !== 'undefined', isValid: typeof title === 'string', isEmpty: !title};
};

return {args, buildTitle, implementations, rawTitle, receivedImplementationArray};
}

module.exports = parseTestArgs;
56 changes: 29 additions & 27 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Emittery = require('emittery');
const matcher = require('matcher');
const ContextRef = require('./context-ref');
const createChain = require('./create-chain');
const parseTestArgs = require('./parse-test-args');
const snapshotManager = require('./snapshot-manager');
const serializeError = require('./serialize-error');
const Runnable = require('./test');
Expand All @@ -11,6 +12,7 @@ class Runner extends Emittery {
constructor(options = {}) {
super();

this.experiments = options.experiments || {};
this.failFast = options.failFast === true;
this.failWithoutAssertions = options.failWithoutAssertions !== false;
this.file = options.file;
Expand Down Expand Up @@ -39,12 +41,21 @@ class Runner extends Emittery {
};

const uniqueTestTitles = new Set();
this.registerUniqueTitle = title => {
if (uniqueTestTitles.has(title)) {
return false;
}

uniqueTestTitles.add(title);
return true;
};

let hasStarted = false;
let scheduledStart = false;
const meta = Object.freeze({
file: options.file
});
this.chain = createChain((metadata, args) => { // eslint-disable-line complexity
this.chain = createChain((metadata, testArgs) => { // eslint-disable-line complexity
if (hasStarted) {
throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.');
}
Expand All @@ -57,40 +68,33 @@ class Runner extends Emittery {
});
}

const specifiedTitle = typeof args[0] === 'string' ?
args.shift() :
undefined;
const implementations = Array.isArray(args[0]) ?
args.shift() :
args.splice(0, 1);
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);

if (metadata.todo) {
if (implementations.length > 0) {
throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
}

if (specifiedTitle === undefined || specifiedTitle === '') {
if (!rawTitle) { // Either undefined or a string.
throw new TypeError('`todo` tests require a title');
}

if (uniqueTestTitles.has(specifiedTitle)) {
throw new Error(`Duplicate test title: ${specifiedTitle}`);
} else {
uniqueTestTitles.add(specifiedTitle);
if (!this.registerUniqueTitle(rawTitle)) {
throw new Error(`Duplicate test title: ${rawTitle}`);
}

if (this.match.length > 0) {
// --match selects TODO tests.
if (matcher([specifiedTitle], this.match).length === 1) {
if (matcher([rawTitle], this.match).length === 1) {
metadata.exclusive = true;
this.runOnlyExclusive = true;
}
}

this.tasks.todo.push({title: specifiedTitle, metadata});
this.tasks.todo.push({title: rawTitle, metadata});
this.emit('stateChange', {
type: 'declared-test',
title: specifiedTitle,
title: rawTitle,
knownFailing: false,
todo: true
});
Expand All @@ -100,15 +104,13 @@ class Runner extends Emittery {
}

for (const implementation of implementations) {
let title = implementation.title ?
implementation.title(specifiedTitle, ...args) :
specifiedTitle;
let {title, isSet, isValid, isEmpty} = buildTitle(implementation);

if (title !== undefined && typeof title !== 'string') {
if (isSet && !isValid) {
throw new TypeError('Test & hook titles must be strings');
}

if (title === undefined || title === '') {
if (isEmpty) {
if (metadata.type === 'test') {
throw new TypeError('Tests must have a title');
} else if (metadata.always) {
Expand All @@ -118,12 +120,8 @@ class Runner extends Emittery {
}
}

if (metadata.type === 'test') {
if (uniqueTestTitles.has(title)) {
throw new Error(`Duplicate test title: ${title}`);
} else {
uniqueTestTitles.add(title);
}
if (metadata.type === 'test' && !this.registerUniqueTitle(title)) {
throw new Error(`Duplicate test title: ${title}`);
}

const task = {
Expand Down Expand Up @@ -162,6 +160,7 @@ class Runner extends Emittery {
todo: false,
failing: false,
callback: false,
inline: false, // Set for attempt metadata created by `t.try()`
always: false
}, meta);
}
Expand Down Expand Up @@ -269,6 +268,7 @@ class Runner extends Emittery {
async runHooks(tasks, contextRef, titleSuffix) {
const hooks = tasks.map(task => new Runnable({
contextRef,
experiments: this.experiments,
failWithoutAssertions: false,
fn: task.args.length === 0 ?
task.implementation :
Expand Down Expand Up @@ -309,14 +309,16 @@ class Runner extends Emittery {
// Only run the test if all `beforeEach` hooks passed.
const test = new Runnable({
contextRef,
experiments: this.experiments,
failWithoutAssertions: this.failWithoutAssertions,
fn: task.args.length === 0 ?
task.implementation :
t => task.implementation.apply(null, [t].concat(task.args)),
compareTestSnapshot: this.boundCompareTestSnapshot,
updateSnapshots: this.updateSnapshots,
metadata: task.metadata,
title: task.title
title: task.title,
registerUniqueTitle: this.registerUniqueTitle
});

const result = await this.runSingle(test);
Expand Down
59 changes: 39 additions & 20 deletions lib/snapshot-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,45 +305,64 @@ class Manager {
compare(options) {
const hash = md5Hex(options.belongsTo);
const entries = this.snapshotsByHash.get(hash) || [];
if (options.index > entries.length) {
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, exceeds expected index of ${entries.length}`);
}
const snapshotBuffer = entries[options.index];

if (options.index === entries.length) {
if (!snapshotBuffer) {
if (!this.recordNewSnapshots) {
return {pass: false};
}

if (options.deferRecording) {
const record = this.deferRecord(hash, options);
return {pass: true, record};
}

this.record(hash, options);
return {pass: true};
}

const snapshotBuffer = entries[options.index];
const actual = concordance.deserialize(snapshotBuffer, concordanceOptions);

const expected = concordance.describe(options.expected, concordanceOptions);
const pass = concordance.compareDescriptors(actual, expected);

return {actual, expected, pass};
}

record(hash, options) {
deferRecord(hash, options) {
const descriptor = concordance.describe(options.expected, concordanceOptions);

this.hasChanges = true;
const snapshot = concordance.serialize(descriptor);
if (this.snapshotsByHash.has(hash)) {
this.snapshotsByHash.get(hash).push(snapshot);
} else {
this.snapshotsByHash.set(hash, [snapshot]);
}

const entry = formatEntry(options.label, descriptor);
if (this.reportEntries.has(options.belongsTo)) {
this.reportEntries.get(options.belongsTo).push(entry);
} else {
this.reportEntries.set(options.belongsTo, [entry]);
}

return () => { // Must be called in order!
this.hasChanges = true;

let snapshots = this.snapshotsByHash.get(hash);
if (!snapshots) {
snapshots = [];
this.snapshotsByHash.set(hash, snapshots);
}

if (options.index > snapshots.length) {
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, exceeds expected index of ${snapshots.length}`);
}

if (options.index < snapshots.length) {
throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, already exists`);
}

snapshots.push(snapshot);

if (this.reportEntries.has(options.belongsTo)) {
this.reportEntries.get(options.belongsTo).push(entry);
} else {
this.reportEntries.set(options.belongsTo, [entry]);
}
};
}

record(hash, options) {
const record = this.deferRecord(hash, options);
record();
}

save() {
Expand Down
Loading