Skip to content

Commit

Permalink
Refactor how tests are actually run
Browse files Browse the repository at this point in the history
Fixes #1684. Fixes #1416. Refs #1158.

Properly support serial hooks. Hooks are divided into the following
categories:

* before
* beforeEach
* afterEach
* afterEach.always
* after
* after.always

For each category all hooks are run in the order they're declared in.
This is different from tests, where serial tests are run before
concurrent ones.

By default hooks run concurrently. However a serial hook is not run
before all preceding concurrent hooks have completed. Serial hooks are
never run concurrently.

Always hooks are now always run, even if --fail-fast is enabled.

Internally, TestCollection, Sequence and Concurrent have been removed.
This has led to a major refactor of Runner, and some smaller breaking
changes and bug fixes:

* Unnecessary validations have been removed
* Macros can be used with hooks
* A macro is recognized even if no additional arguments are given, so it
can modify the test (or hook) title
* --match now also matches todo tests
* Skipped and todo tests are shown first in the output
* --fail-fast prevents subsequent tests from running as long as the
failure occurs in a serial test
  • Loading branch information
novemberborn committed Feb 11, 2018
1 parent 8de2630 commit 963f5cf
Show file tree
Hide file tree
Showing 24 changed files with 1,456 additions and 3,627 deletions.
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ export interface SerialInterface<Context = {}> {
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;

after: AfterInterface<Context>;
afterEach: AfterInterface<Context>;
before: BeforeInterface<Context>;
beforeEach: BeforeInterface<Context>;
cb: CbInterface<Context>;
failing: FailingInterface<Context>;
only: OnlyInterface<Context>;
Expand Down
4 changes: 4 additions & 0 deletions index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ export interface SerialInterface<Context = {}> {
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;

after: AfterInterface<Context>;
afterEach: AfterInterface<Context>;
before: BeforeInterface<Context>;
beforeEach: BeforeInterface<Context>;
cb: CbInterface<Context>;
failing: FailingInterface<Context>;
only: OnlyInterface<Context>;
Expand Down
64 changes: 0 additions & 64 deletions lib/concurrent.js

This file was deleted.

41 changes: 41 additions & 0 deletions lib/context-ref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';
const clone = require('lodash.clone');

class ContextRef {
constructor() {
this.value = {};
}

get() {
return this.value;
}

set(newValue) {
this.value = newValue;
}

copy() {
return new LateBinding(this); // eslint-disable-line no-use-before-define
}
}
module.exports = ContextRef;

class LateBinding extends ContextRef {
constructor(ref) {
super();
this.ref = ref;
this.bound = false;
}

get() {
if (!this.bound) {
this.set(clone(this.ref.get()));
}
return super.get();
}

set(newValue) {
this.bound = true;
super.set(newValue);
}
}
108 changes: 108 additions & 0 deletions lib/create-chain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use strict';
const chainRegistry = new WeakMap();

function startChain(name, call, defaults) {
const fn = function () {
call(Object.assign({}, defaults), Array.from(arguments));
};
Object.defineProperty(fn, 'name', {value: name});
chainRegistry.set(fn, {call, defaults, fullName: name});
return fn;
}

function extendChain(prev, name, flag) {
if (!flag) {
flag = name;
}

const fn = function () {
callWithFlag(prev, flag, Array.from(arguments));
};
const fullName = `${chainRegistry.get(prev).fullName}.${name}`;
Object.defineProperty(fn, 'name', {value: fullName});
prev[name] = fn;

chainRegistry.set(fn, {flag, fullName, prev});
return fn;
}

function callWithFlag(prev, flag, args) {
const combinedFlags = {[flag]: true};
do {
const step = chainRegistry.get(prev);
if (step.call) {
step.call(Object.assign({}, step.defaults, combinedFlags), args);
prev = null;
} else {
combinedFlags[step.flag] = true;
prev = step.prev;
}
} while (prev);
}

function createHookChain(hook, isAfterHook) {
// Hook chaining rules:
// * `always` comes immediately after "after hooks"
// * `skip` must come at the end
// * no `only`
// * no repeating
extendChain(hook, 'cb', 'callback');
extendChain(hook, 'skip', 'skipped');
extendChain(hook.cb, 'skip', 'skipped');
if (isAfterHook) {
extendChain(hook, 'always');
extendChain(hook.always, 'cb', 'callback');
extendChain(hook.always, 'skip', 'skipped');
extendChain(hook.always.cb, 'skip', 'skipped');
}
return hook;
}

function createChain(fn, defaults) {
// Test chaining rules:
// * `serial` must come at the start
// * `only` and `skip` must come at the end
// * `failing` must come at the end, but can be followed by `only` and `skip`
// * `only` and `skip` cannot be chained together
// * no repeating
const root = startChain('test', fn, Object.assign({}, defaults, {type: 'test'}));
extendChain(root, 'cb', 'callback');
extendChain(root, 'failing');
extendChain(root, 'only', 'exclusive');
extendChain(root, 'serial');
extendChain(root, 'skip', 'skipped');
extendChain(root.cb, 'failing');
extendChain(root.cb, 'only', 'exclusive');
extendChain(root.cb, 'skip', 'skipped');
extendChain(root.cb.failing, 'only', 'exclusive');
extendChain(root.cb.failing, 'skip', 'skipped');
extendChain(root.failing, 'only', 'exclusive');
extendChain(root.failing, 'skip', 'skipped');
extendChain(root.serial, 'cb', 'callback');
extendChain(root.serial, 'failing');
extendChain(root.serial, 'only', 'exclusive');
extendChain(root.serial, 'skip', 'skipped');
extendChain(root.serial.cb, 'failing');
extendChain(root.serial.cb, 'only', 'exclusive');
extendChain(root.serial.cb, 'skip', 'skipped');
extendChain(root.serial.cb.failing, 'only', 'exclusive');
extendChain(root.serial.cb.failing, 'skip', 'skipped');

root.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {type: 'after'})), true);
root.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {type: 'afterEach'})), true);
root.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {type: 'before'})), false);
root.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {type: 'beforeEach'})), false);

root.serial.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {serial: true, type: 'after'})), true);
root.serial.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {serial: true, type: 'afterEach'})), true);
root.serial.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {serial: true, type: 'before'})), false);
root.serial.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {serial: true, type: 'beforeEach'})), false);

// "todo" tests cannot be chained. Allow todo tests to be flagged as needing
// to be serial.
root.todo = startChain('test.todo', fn, Object.assign({}, defaults, {type: 'test', todo: true}));
root.serial.todo = startChain('test.serial.todo', fn, Object.assign({}, defaults, {serial: true, type: 'test', todo: true}));

return root;
}
module.exports = createChain;
6 changes: 3 additions & 3 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ const Runner = require('./runner');
const opts = require('./worker-options').get();

const runner = new Runner({
bail: opts.failFast,
failFast: opts.failFast,
failWithoutAssertions: opts.failWithoutAssertions,
file: opts.file,
match: opts.match,
projectDir: opts.projectDir,
runOnlyExclusive: opts.runOnlyExclusive,
serial: opts.serial,
updateSnapshots: opts.updateSnapshots,
snapshotDir: opts.snapshotDir,
runOnlyExclusive: opts.runOnlyExclusive
updateSnapshots: opts.updateSnapshots
});

worker.setRunner(runner);
Expand Down
Loading

0 comments on commit 963f5cf

Please sign in to comment.