Skip to content

Commit

Permalink
Refactor so that make() and fork() can be implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
novemberborn committed Apr 13, 2020
1 parent e5e736d commit b0aef86
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 98 deletions.
40 changes: 23 additions & 17 deletions lib/create-chain.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
'use strict';
const chainRegistry = new WeakMap();

function startChain(name, declare, annotations) {
function startChain(name, {annotations, declare, type}) {
const fn = (...args) => {
declare(annotations, args);
declare(type, annotations, args);
};

Object.defineProperty(fn, 'name', {value: name});
chainRegistry.set(fn, {annotations, declare, fullName: name});
chainRegistry.set(fn, {
declare(flags, args) {
declare(type, {...annotations, ...flags}, args);
},
fullName: name
});
return fn;
}

Expand All @@ -32,7 +37,7 @@ function declareWithFlag(previous, flag, args) {
combinedFlags[step.flag] = true;
previous = step.previous;
} else {
step.declare({...step.annotations, ...combinedFlags}, args);
step.declare(combinedFlags, args);
break;
}
} while (previous);
Expand Down Expand Up @@ -78,11 +83,12 @@ function createChain({
allowMultipleImplementations
};

const declare = (declaredAnnotations, args) => {
const declare = (type, declaredAnnotations, args) => {
declareWithOptions({
annotations: {...annotations, ...declaredAnnotations},
args,
options
options,
type
});
};

Expand Down Expand Up @@ -111,7 +117,7 @@ function createChain({
// * `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', declare, {type: 'test'});
const root = startChain('test', {declare, type: 'test'});
extendChain(root, 'failing');
extendChain(root, 'only', 'exclusive');
extendChain(root, 'serial');
Expand Down Expand Up @@ -139,21 +145,21 @@ function createChain({
extendChain(root.serial.cb.failing, 'skip', 'skipped');
}

root.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', declare, {type: 'after'}));
root.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', declare, {type: 'afterEach'}));
root.before = createHookChain({allowCallbacks}, startChain('test.before', declare, {type: 'before'}));
root.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', declare, {type: 'beforeEach'}));
root.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', {declare, type: 'after'}));
root.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', {declare, type: 'afterEach'}));
root.before = createHookChain({allowCallbacks}, startChain('test.before', {declare, type: 'before'}));
root.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', {declare, type: 'beforeEach'}));

root.serial.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', declare, {serial: true, type: 'after'}));
root.serial.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', declare, {serial: true, type: 'afterEach'}));
root.serial.before = createHookChain({allowCallbacks}, startChain('test.before', declare, {serial: true, type: 'before'}));
root.serial.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', declare, {serial: true, type: 'beforeEach'}));
root.serial.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', {annotations: {serial: true}, declare, type: 'after'}));
root.serial.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', {annotations: {serial: true}, declare, type: 'afterEach'}));
root.serial.before = createHookChain({allowCallbacks}, startChain('test.before', {annotations: {serial: true}, declare, type: 'before'}));
root.serial.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', {annotations: {serial: true}, declare, type: 'beforeEach'}));
root.serial.macro = macro;

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

root.macro = macro;
root.meta = meta;
Expand Down
138 changes: 57 additions & 81 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const parseTestArgs = require('./parse-test-args');
const snapshotManager = require('./snapshot-manager');
const serializeError = require('./serialize-error');
const Runnable = require('./test');
const {Task, TaskList} = require('./task-list');

class Runner extends Emittery {
constructor(options = {}) {
Expand All @@ -29,17 +30,7 @@ class Runner extends Emittery {
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
this.interrupted = false;
this.snapshots = null;
this.tasks = {
after: [],
afterAlways: [],
afterEach: [],
afterEachAlways: [],
before: [],
beforeEach: [],
concurrent: [],
serial: [],
todo: []
};
this.tasks = new TaskList();

const uniqueTestTitles = new Set();
this.registerUniqueTitle = title => {
Expand All @@ -61,8 +52,7 @@ class Runner extends Emittery {
failing: false,
inline: false, // Default value; only attempts created by `t.try()` have this annotation set to `true`.
serial: false,
skipped: false,
todo: false
skipped: false
},
meta: Object.freeze({
file: options.file,
Expand All @@ -74,11 +64,9 @@ class Runner extends Emittery {
declare: ({ // eslint-disable-line complexity
annotations,
args: declarationArguments,
options: {
allowExperimentalMacros,
allowImplementationTitleFns,
allowMultipleImplementations
}}) => {
options,
type
}) => {
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 @@ -91,13 +79,9 @@ class Runner extends Emittery {
});
}

const {args, buildTitle, implementations, rawTitle} = parseTestArgs(declarationArguments, {
allowExperimentalMacros,
allowImplementationTitleFns,
allowMultipleImplementations
});
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(declarationArguments, options);

if (annotations.todo) {
if (type === '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.');
}
Expand All @@ -118,7 +102,7 @@ class Runner extends Emittery {
}
}

this.tasks.todo.push({title: rawTitle, annotations});
this.tasks.add(Task.todo({annotations, title: rawTitle}));
this.emit('stateChange', {
type: 'declared-test',
title: rawTitle,
Expand All @@ -138,48 +122,51 @@ class Runner extends Emittery {
}

if (isEmpty) {
if (annotations.type === 'test') {
if (type === 'test') {
throw new TypeError('Tests must have a title');
} else if (annotations.always) {
title = `${annotations.type}.always hook`;
title = `${type}.always hook`;
} else {
title = `${annotations.type} hook`;
title = `${type} hook`;
}
}

if (annotations.type === 'test' && !this.registerUniqueTitle(title)) {
if (type === 'test' && !this.registerUniqueTitle(title)) {
throw new Error(`Duplicate test title: ${title}`);
}

const task = {
allowExperimentalMacros,
allowImplementationTitleFns,
allowMultipleImplementations,
annotations: {...annotations},
args,
implementation,
title
};

if (annotations.type === 'test') {
if (type === 'test') {
let {exclusive} = annotations;
if (this.match.length > 0) {
// --match overrides .only()
task.annotations.exclusive = matcher([title], this.match).length === 1;
exclusive = matcher([title], this.match).length === 1;
}

if (task.annotations.exclusive) {
if (exclusive) {
this.runOnlyExclusive = true;
}

this.tasks[annotations.serial ? 'serial' : 'concurrent'].push(task);
this.tasks.add(Task.test({
annotations: {...annotations, exclusive},
args,
implementation,
options,
title
}));
this.emit('stateChange', {
type: 'declared-test',
title,
knownFailing: annotations.failing,
todo: false
});
} else if (!annotations.skipped) {
this.tasks[annotations.type + (annotations.always ? 'Always' : '')].push(task);
} else {
this.tasks.add(Task[type]({
annotations,
args,
implementation,
options,
title
}));
}
}
}
Expand Down Expand Up @@ -305,11 +292,9 @@ class Runner extends Emittery {
return result;
}

async runHooks(tasks, contextRef, titleSuffix, testPassed) {
const hooks = tasks.map(task => new Runnable({
allowExperimentalMacros: task.allowExperimentalMacros,
allowImplementationTitleFns: task.allowImplementationTitleFns,
allowMultipleImplementations: task.allowMultipleImplementations,
async runHooks(type, contextRef, titleSuffix, testPassed) {
const hooks = [...this.tasks.select(type)].map(task => new Runnable({
...task.options,
annotations: task.annotations,
contextRef,
experiments: this.experiments,
Expand Down Expand Up @@ -348,7 +333,7 @@ class Runner extends Emittery {

async runTest(task, contextRef) {
const hookSuffix = ` for ${task.title}`;
let hooksOk = await this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix);
let hooksOk = await this.runHooks('beforeEach', contextRef, hookSuffix);

let testOk = false;
if (hooksOk) {
Expand Down Expand Up @@ -383,7 +368,7 @@ class Runner extends Emittery {
logs: result.logs
});

hooksOk = await this.runHooks(this.tasks.afterEach, contextRef, hookSuffix, testOk);
hooksOk = await this.runHooks('afterEach', contextRef, hookSuffix, testOk);
} else {
this.emit('stateChange', {
type: 'test-failed',
Expand All @@ -397,14 +382,14 @@ class Runner extends Emittery {
}
}

const alwaysOk = await this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix, testOk);
const alwaysOk = await this.runHooks('afterEachAlways', contextRef, hookSuffix, testOk);
return alwaysOk && hooksOk && testOk;
}

async start() {
const concurrentTests = [];
const serialTests = [];
for (const task of this.tasks.serial) {
let concurrentTests = [];
let serialTests = [];
for (const task of this.tasks.select('test')) {
if (this.runOnlyExclusive && !task.annotations.exclusive) {
continue;
}
Expand All @@ -417,34 +402,25 @@ class Runner extends Emittery {
todo: false
});

if (!task.annotations.skipped) {
serialTests.push(task);
}
}

for (const task of this.tasks.concurrent) {
if (this.runOnlyExclusive && !task.annotations.exclusive) {
if (task.annotations.skipped) {
continue;
}

this.emit('stateChange', {
type: 'selected-test',
title: task.title,
knownFailing: task.annotations.failing,
skip: task.annotations.skipped,
todo: false
});

if (!task.annotations.skipped) {
if (this.serial) {
serialTests.push(task);
} else {
concurrentTests.push(task);
}
if (task.annotations.serial) {
serialTests.push(task);
} else {
concurrentTests.push(task);
}
}

for (const task of this.tasks.todo) {
// Reassign the concurrent tasks, but always run them after the explicitly
// serial ones.
if (this.serial) {
serialTests = [...serialTests, ...concurrentTests];
concurrentTests = [];
}

for (const task of this.tasks.select('todo')) {
if (this.runOnlyExclusive && !task.annotations.exclusive) {
continue;
}
Expand All @@ -467,7 +443,7 @@ class Runner extends Emittery {
const contextRef = new ContextRef();

// Note that the hooks and tests always begin running asynchronously.
const beforePromise = this.runHooks(this.tasks.before, contextRef);
const beforePromise = this.runHooks('before', contextRef);
const serialPromise = beforePromise.then(beforeHooksOk => { // eslint-disable-line promise/prefer-await-to-then
// Don't run tests if a `before` hook failed.
if (!beforeHooksOk) {
Expand Down Expand Up @@ -517,11 +493,11 @@ class Runner extends Emittery {
const ok = await concurrentPromise;
// Only run `after` hooks if all hooks and tests passed.
if (ok) {
await this.runHooks(this.tasks.after, contextRef);
await this.runHooks('after', contextRef);
}

// Always run `after.always` hooks.
await this.runHooks(this.tasks.afterAlways, contextRef);
await this.runHooks('afterAlways', contextRef);
process.removeListener('beforeExit', beforeExitHandler);
await this.emit('finish');
} catch (error) {
Expand Down
Loading

0 comments on commit b0aef86

Please sign in to comment.