Skip to content

Commit

Permalink
test_runner: add before/after/each hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Jul 21, 2022
1 parent 2fd4c01 commit 4f983d5
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 15 deletions.
11 changes: 11 additions & 0 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,19 @@ function runInParentContext(Factory) {
return cb;
}

function hook(hook) {
return (fn, options) => {
const parent = testResources.get(executionAsyncId()) || setup(root);
parent.createHook(hook, fn, options);
};
}

module.exports = {
test: FunctionPrototypeBind(test, root),
describe: runInParentContext(Suite),
it: runInParentContext(ItTest),
before: hook('before'),
after: hook('after'),
beforeEach: hook('beforeEach'),
afterEach: hook('afterEach'),
};
99 changes: 88 additions & 11 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeReduce,
ArrayPrototypeShift,
ArrayPrototypeSlice,
ArrayPrototypeUnshift,
FunctionPrototype,
Number,
ObjectSeal,
PromisePrototypeThen,
PromiseResolve,
ReflectApply,
Expand All @@ -31,7 +34,7 @@ const {
kEmptyObject,
} = require('internal/util');
const { isPromise } = require('internal/util/types');
const { isUint32, validateAbortSignal } = require('internal/validators');
const { isUint32, validateAbortSignal, validateOneOf } = require('internal/validators');
const { setTimeout } = require('timers/promises');
const { cpus } = require('os');
const { bigint: hrtime } = process.hrtime;
Expand All @@ -50,6 +53,8 @@ const testOnlyFlag = !isTestRunner && getOptionValue('--test-only');
const rootConcurrency = isTestRunner ? cpus().length : 1;

const kShouldAbort = Symbol('kShouldAbort');
const kRunHook = Symbol('kRunHook');
const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']);


function stopTest(timeout, signal) {
Expand All @@ -75,6 +80,10 @@ class TestContext {
return this.#test.signal;
}

get name() {
return this.#test.name;
}

diagnostic(message) {
this.#test.diagnostic(message);
}
Expand All @@ -97,6 +106,14 @@ class TestContext {

return subtest.start();
}

beforeEach(fn, options) {
this.#test.createHook('beforeEach', fn, options);
}

afterEach(fn, options) {
this.#test.createHook('afterEach', fn, options);
}
}

class Test extends AsyncResource {
Expand Down Expand Up @@ -185,6 +202,12 @@ class Test extends AsyncResource {
this.pendingSubtests = [];
this.readySubtests = new SafeMap();
this.subtests = [];
this.hooks = {
before: [],
after: [],
beforeEach: [],
afterEach: [],
};
this.waitingOn = 0;
this.finished = false;
}
Expand Down Expand Up @@ -303,10 +326,19 @@ class Test extends AsyncResource {
kCancelledByParent
)
);
this.startTime = this.startTime || this.endTime; // If a test was canceled before it was started, e.g inside a hook
this.cancelled = true;
this.#abortController.abort();
}

createHook(name, fn, options) {
validateOneOf(name, 'hook name', kHookNames);
// eslint-disable-next-line no-use-before-define
const hook = new TestHook(fn, options);
ArrayPrototypePush(this.hooks[name], hook);
return hook;
}

fail(err) {
if (this.error !== null) {
return;
Expand Down Expand Up @@ -370,26 +402,41 @@ class Test extends AsyncResource {
return { ctx, args: [ctx] };
}

async [kRunHook](hook, args) {
validateOneOf(hook, 'hook name', kHookNames);
await ArrayPrototypeReduce(this.hooks[hook], async (prev, hook) => {
await prev;
await hook.run(args);
}, PromiseResolve());
}

async run() {
this.parent.activeSubtests++;
if (this.parent !== null) {
this.parent.activeSubtests++;
}
this.startTime = hrtime();

if (this[kShouldAbort]()) {
this.postRun();
return;
}

const { args, ctx } = this.getRunArgs();
if (this.parent?.hooks.beforeEach.length > 0) {
await this.parent[kRunHook]('beforeEach', { args, ctx });
}

try {
const stopPromise = stopTest(this.timeout, this.signal);
const { args, ctx } = this.getRunArgs();
ArrayPrototypeUnshift(args, this.fn, ctx); // Note that if it's not OK to mutate args, we need to first clone it.
const runArgs = ArrayPrototypeSlice(args);
ArrayPrototypeUnshift(runArgs, this.fn, ctx);

if (this.fn.length === args.length - 1) {
if (this.fn.length === runArgs.length - 1) {
// This test is using legacy Node.js error first callbacks.
const { promise, cb } = createDeferredCallback();

ArrayPrototypePush(args, cb);
const ret = ReflectApply(this.runInAsyncScope, this, args);
ArrayPrototypePush(runArgs, cb);
const ret = ReflectApply(this.runInAsyncScope, this, runArgs);

if (isPromise(ret)) {
this.fail(new ERR_TEST_FAILURE(
Expand All @@ -402,7 +449,7 @@ class Test extends AsyncResource {
}
} else {
// This test is synchronous or using Promises.
const promise = ReflectApply(this.runInAsyncScope, this, args);
const promise = ReflectApply(this.runInAsyncScope, this, runArgs);
await SafePromiseRace([PromiseResolve(promise), stopPromise]);
}

Expand All @@ -420,6 +467,10 @@ class Test extends AsyncResource {
}
}

if (this.parent?.hooks.afterEach.length > 0) {
await this.parent[kRunHook]('afterEach', { args, ctx });
}

// Clean up the test. Then, try to report the results and execute any
// tests that were pending due to available concurrency.
this.postRun();
Expand Down Expand Up @@ -523,26 +574,47 @@ class Test extends AsyncResource {
}
}

class TestHook extends Test {
#args;
constructor(fn, options) {
if (options === null || typeof options !== 'object') {
options = kEmptyObject;
}
super({ __proto__: null, fn, ...options });
}
run(args) {
this.#args = args;
return super.run();
}
getRunArgs() {
return this.#args;
}
}

class ItTest extends Test {
constructor(opt) { super(opt); } // eslint-disable-line no-useless-constructor
getRunArgs() {
return { ctx: { signal: this.signal }, args: [] };
return { ctx: { signal: this.signal, name: this.name }, args: [] };
}
}
class Suite extends Test {
constructor(options) {
super(options);

try {
const context = { signal: this.signal };
this.buildSuite = this.runInAsyncScope(this.fn, context, [context]);
const { ctx, args } = this.getRunArgs();
this.buildSuite = this.runInAsyncScope(this.fn, ctx, args);
} catch (err) {
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
}
this.fn = () => {};
this.buildPhaseFinished = true;
}

getRunArgs() {
return { ctx: { signal: this.signal, name: this.name }, args: [] };
}

start() {
return this.run();
}
Expand All @@ -562,11 +634,16 @@ class Suite extends Test {
return;
}


const hookArgs = this.getRunArgs();
await this[kRunHook]('before', hookArgs);
const stopPromise = stopTest(this.timeout, this.signal);
const subtests = this.skipped || this.error ? [] : this.subtests;
const promise = SafePromiseAll(subtests, (subtests) => subtests.start());

await SafePromiseRace([promise, stopPromise]);
await this[kRunHook]('after', hookArgs);

this.pass();
this.postRun();
}
Expand Down
6 changes: 5 additions & 1 deletion lib/test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
const { test, describe, it } = require('internal/test_runner/harness');
const { test, describe, it, before, after, beforeEach, afterEach } = require('internal/test_runner/harness');
const { emitExperimentalWarning } = require('internal/util');

emitExperimentalWarning('The test runner');
Expand All @@ -8,3 +8,7 @@ module.exports = test;
module.exports.test = test;
module.exports.describe = describe;
module.exports.it = it;
module.exports.before = before;
module.exports.after = after;
module.exports.beforeEach = beforeEach;
module.exports.afterEach = afterEach;
6 changes: 3 additions & 3 deletions test/message/test_runner_describe_it.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,15 @@ it('callback fail', (done) => {
});

it('sync t is this in test', function() {
assert.deepStrictEqual(this, { signal: this.signal });
assert.deepStrictEqual(this, { signal: this.signal, name: this.name });
});

it('async t is this in test', async function() {
assert.deepStrictEqual(this, { signal: this.signal });
assert.deepStrictEqual(this, { signal: this.signal, name: this.name });
});

it('callback t is this in test', function(done) {
assert.deepStrictEqual(this, { signal: this.signal });
assert.deepStrictEqual(this, { signal: this.signal, name: this.name });
done();
});

Expand Down
52 changes: 52 additions & 0 deletions test/parallel/test-runner-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { describe, it, before, after, beforeEach, afterEach, test } = require('node:test');

const testArr = [];
const afterTest = common.mustCall(() => {
assert.deepStrictEqual(testArr, [
'beforeEach 1', '1', 'afterEach 1',
'beforeEach 2', '2', 'afterEach 2',
'beforeEach 3', '3', 'afterEach 3',
]);
});
test('test', async (t) => {
t.beforeEach((t) => testArr.push('beforeEach ' + t.name));
t.afterEach((t) => testArr.push('afterEach ' + t.name));

await t.test('1', () => testArr.push('1'));
await t.test('2', () => testArr.push('2'));
await t.test('3', () => testArr.push('3'));
}).then(afterTest);


const describeArr = [];
const afterDescribe = common.mustCall(() => {
assert.deepStrictEqual(describeArr, [
'before describe hooks',
'beforeEach 1', '1', 'afterEach 1',
'beforeEach 2', '2', 'afterEach 2',
'beforeEach 3', '3', 'afterEach 3',
'after describe hooks',
]);
});
describe('describe hooks', () => {
before(function() {
describeArr.push('before ' + this.name);
});
after(function() {
describeArr.push('after ' + this.name);
afterDescribe();
});
beforeEach(function() {
describeArr.push('beforeEach ' + this.name);
});
afterEach(function() {
describeArr.push('afterEach ' + this.name);
});

it('1', () => describeArr.push('1'));
it('2', () => describeArr.push('2'));
it('3', () => describeArr.push('3'));
});

0 comments on commit 4f983d5

Please sign in to comment.