Skip to content

Commit 4bd46ba

Browse files
authored
feat(testurnner): allow multiple hooks isntances and per-test hooks (#1571)
1 parent 6503c83 commit 4bd46ba

File tree

2 files changed

+118
-55
lines changed

2 files changed

+118
-55
lines changed

utils/testrunner/TestRunner.js

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ function isTestFailure(testResult) {
7272
return testResult === TestResult.Failed || testResult === TestResult.TimedOut || testResult === TestResult.Crashed;
7373
}
7474

75+
function createHook(callback, name) {
76+
const location = getCallerLocation(__filename);
77+
return { name, body: callback, location };
78+
}
79+
7580
class Test {
7681
constructor(suite, name, callback, location) {
7782
this._suite = suite;
@@ -83,6 +88,7 @@ class Test {
8388
this._location = location;
8489
this._timeout = INFINITE_TIMEOUT;
8590
this._repeat = 1;
91+
this._hooks = [];
8692

8793
// Test results. TODO: make these private.
8894
this.result = null;
@@ -100,6 +106,7 @@ class Test {
100106
test._timeout = this._timeout;
101107
test._mode = this._mode;
102108
test._expectation = this._expectation;
109+
test._hooks = this._hooks.slice();
103110
return test;
104111
}
105112

@@ -155,6 +162,18 @@ class Test {
155162
setRepeat(repeat) {
156163
this._repeat = repeat;
157164
}
165+
166+
before(callback) {
167+
this._hooks.push(createHook(callback, 'before'));
168+
}
169+
170+
after(callback) {
171+
this._hooks.push(createHook(callback, 'after'));
172+
}
173+
174+
hooks(name) {
175+
return this._hooks.filter(hook => !name || hook.name === name);
176+
}
158177
}
159178

160179
class Suite {
@@ -166,12 +185,7 @@ class Suite {
166185
this._expectation = TestExpectation.Ok;
167186
this._location = location;
168187
this._repeat = 1;
169-
170-
// TODO: make these private.
171-
this.beforeAll = null;
172-
this.beforeEach = null;
173-
this.afterAll = null;
174-
this.afterEach = null;
188+
this._hooks = [];
175189

176190
this.Modes = { ...TestMode };
177191
this.Expectations = { ...TestExpectation };
@@ -225,6 +239,26 @@ class Suite {
225239
setRepeat(repeat) {
226240
this._repeat = repeat;
227241
}
242+
243+
beforeEach(callback) {
244+
this._hooks.push(createHook(callback, 'beforeEach'));
245+
}
246+
247+
afterEach(callback) {
248+
this._hooks.push(createHook(callback, 'afterEach'));
249+
}
250+
251+
beforeAll(callback) {
252+
this._hooks.push(createHook(callback, 'beforeAll'));
253+
}
254+
255+
afterAll(callback) {
256+
this._hooks.push(createHook(callback, 'afterAll'));
257+
}
258+
259+
hooks(name) {
260+
return this._hooks.filter(hook => !name || hook.name === name);
261+
}
228262
}
229263

230264
class Result {
@@ -330,16 +364,20 @@ class TestWorker {
330364
if (this._markTerminated(test))
331365
return;
332366
const suite = this._suiteStack.pop();
333-
if (!await this._runHook(test, suite, 'afterAll'))
334-
return;
367+
for (const hook of suite.hooks('afterAll')) {
368+
if (!await this._runHook(test, hook, suite.fullName()))
369+
return;
370+
}
335371
}
336372
while (this._suiteStack.length < suiteStack.length) {
337373
if (this._markTerminated(test))
338374
return;
339375
const suite = suiteStack[this._suiteStack.length];
340376
this._suiteStack.push(suite);
341-
if (!await this._runHook(test, suite, 'beforeAll'))
342-
return;
377+
for (const hook of suite.hooks('beforeAll')) {
378+
if (!await this._runHook(test, hook, suite.fullName()))
379+
return;
380+
}
343381
}
344382

345383
if (this._markTerminated(test))
@@ -349,8 +387,12 @@ class TestWorker {
349387
// no matter what happens.
350388

351389
await this._testPass._willStartTest(this, test);
352-
for (let i = 0; i < this._suiteStack.length; i++)
353-
await this._runHook(test, this._suiteStack[i], 'beforeEach');
390+
for (const suite of this._suiteStack) {
391+
for (const hook of suite.hooks('beforeEach'))
392+
await this._runHook(test, hook, suite.fullName(), true);
393+
}
394+
for (const hook of test.hooks('before'))
395+
await this._runHook(test, hook, test.fullName(), true);
354396

355397
if (!test.error && !this._markTerminated(test)) {
356398
await this._testPass._willStartTestBody(this, test);
@@ -371,19 +413,19 @@ class TestWorker {
371413
await this._testPass._didFinishTestBody(this, test);
372414
}
373415

374-
for (let i = this._suiteStack.length - 1; i >= 0; i--)
375-
await this._runHook(test, this._suiteStack[i], 'afterEach');
416+
for (const hook of test.hooks('after'))
417+
await this._runHook(test, hook, test.fullName(), true);
418+
for (const suite of this._suiteStack.slice().reverse()) {
419+
for (const hook of suite.hooks('afterEach'))
420+
await this._runHook(test, hook, suite.fullName(), true);
421+
}
376422
await this._testPass._didFinishTest(this, test);
377423
}
378424

379-
async _runHook(test, suite, hookName) {
380-
const hook = suite[hookName];
381-
if (!hook)
382-
return true;
383-
384-
await this._testPass._willStartHook(this, suite, hook.location, hookName);
425+
async _runHook(test, hook, fullName, passTest = false) {
426+
await this._testPass._willStartHook(this, hook, fullName);
385427
const timeout = this._testPass._runner._timeout;
386-
const { promise, terminate } = runUserCallback(hook.body, timeout, [this._state, test]);
428+
const { promise, terminate } = runUserCallback(hook.body, timeout, passTest ? [this._state, test] : [this._state]);
387429
this._runningHookTerminate = terminate;
388430
let error = await promise;
389431
this._runningHookTerminate = null;
@@ -396,7 +438,7 @@ class TestWorker {
396438
}
397439
let message;
398440
if (error === TimeoutError) {
399-
message = `${locationString} - Timeout Exceeded ${timeout}ms while running "${hookName}" in suite "${suite.fullName()}"`;
441+
message = `${locationString} - Timeout Exceeded ${timeout}ms while running "${hook.name}" in "${fullName}"`;
400442
error = null;
401443
} else if (error === TerminatedError) {
402444
// Do not report termination details - it's just noise.
@@ -405,21 +447,22 @@ class TestWorker {
405447
} else {
406448
if (error.stack)
407449
await this._testPass._runner._sourceMapSupport.rewriteStackTraceWithSourceMaps(error);
408-
message = `${locationString} - FAILED while running "${hookName}" in suite "${suite.fullName()}": `;
450+
message = `${locationString} - FAILED while running "${hook.name}" in suite "${fullName}": `;
409451
}
410-
await this._testPass._didFailHook(this, suite, hook.location, hookName, message, error);
452+
await this._testPass._didFailHook(this, hook, fullName, message, error);
411453
test.error = error;
412454
return false;
413455
}
414456

415-
await this._testPass._didCompleteHook(this, suite, hook.location, hookName);
457+
await this._testPass._didCompleteHook(this, hook, fullName);
416458
return true;
417459
}
418460

419461
async shutdown() {
420462
while (this._suiteStack.length > 0) {
421463
const suite = this._suiteStack.pop();
422-
await this._runHook({}, suite, 'afterAll');
464+
for (const hook of suite.hooks('afterAll'))
465+
await this._runHook({}, hook, suite.fullName());
423466
}
424467
}
425468
}
@@ -542,19 +585,19 @@ class TestPass {
542585
debug('testrunner:test')(`[${worker._workerId}] ${test.result.toUpperCase()} "${test.fullName()}" (${test.location().fileName + ':' + test.location().lineNumber})`);
543586
}
544587

545-
async _willStartHook(worker, suite, location, hookName) {
546-
debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" started for "${suite.fullName()}" (${location.fileName + ':' + location.lineNumber})`);
588+
async _willStartHook(worker, hook, fullName) {
589+
debug('testrunner:hook')(`[${worker._workerId}] "${hook.name}" started for "${fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`);
547590
}
548591

549-
async _didFailHook(worker, suite, location, hookName, message, error) {
550-
debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" FAILED for "${suite.fullName()}" (${location.fileName + ':' + location.lineNumber})`);
592+
async _didFailHook(worker, hook, fullName, message, error) {
593+
debug('testrunner:hook')(`[${worker._workerId}] "${hook.name}" FAILED for "${fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`);
551594
if (message)
552595
this._result.addError(message, error, worker);
553596
this._result.setResult(TestResult.Crashed, message);
554597
}
555598

556-
async _didCompleteHook(worker, suite, location, hookName) {
557-
debug('testrunner:hook')(`[${worker._workerId}] "${hookName}" OK for "${suite.fullName()}" (${location.fileName + ':' + location.lineNumber})`);
599+
async _didCompleteHook(worker, hook, fullName) {
600+
debug('testrunner:hook')(`[${worker._workerId}] "${hook.name}" OK for "${fullName}" (${hook.location.fileName + ':' + hook.location.lineNumber})`);
558601
}
559602
}
560603

@@ -593,10 +636,10 @@ class TestRunner extends EventEmitter {
593636

594637
this._debuggerLogBreakpointLines = new Multimap();
595638

596-
this.beforeAll = this._addHook.bind(this, 'beforeAll');
597-
this.beforeEach = this._addHook.bind(this, 'beforeEach');
598-
this.afterAll = this._addHook.bind(this, 'afterAll');
599-
this.afterEach = this._addHook.bind(this, 'afterEach');
639+
this.beforeAll = (callback) => this._currentSuite.beforeAll(callback);
640+
this.beforeEach = (callback) => this._currentSuite.beforeEach(callback);
641+
this.afterAll = (callback) => this._currentSuite.afterAll(callback);
642+
this.afterEach = (callback) => this._currentSuite.afterEach(callback);
600643

601644
this.describe = this._suiteBuilder([]);
602645
this.it = this._testBuilder([]);
@@ -694,12 +737,6 @@ class TestRunner extends EventEmitter {
694737
this.describe.skip(true)('', module.xdescribe, ...args);
695738
}
696739

697-
_addHook(hookName, callback) {
698-
assert(this._currentSuite[hookName] === null, `Only one ${hookName} hook available per suite`);
699-
const location = getCallerLocation(__filename);
700-
this._currentSuite[hookName] = { body: callback, location };
701-
}
702-
703740
async run(options = {}) {
704741
const { totalTimeout = 0 } = options;
705742
let session = this._debuggerLogBreakpointLines.size ? await setLogBreakpoints(this._debuggerLogBreakpointLines) : null;

utils/testrunner/test/testrunner.spec.js

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -214,47 +214,73 @@ module.exports.addTests = function({testRunner, expect}) {
214214
const log = [];
215215
const t = newTestRunner();
216216
t.beforeAll(() => log.push('root:beforeAll'));
217-
t.beforeEach(() => log.push('root:beforeEach'));
217+
t.beforeEach(() => log.push('root:beforeEach1'));
218+
t.beforeEach(() => log.push('root:beforeEach2'));
218219
t.it('uno', () => log.push('test #1'));
219220
t.describe('suite1', () => {
220-
t.beforeAll(() => log.push('suite:beforeAll'));
221-
t.beforeEach(() => log.push('suite:beforeEach'));
221+
t.beforeAll(() => log.push('suite:beforeAll1'));
222+
t.beforeAll(() => log.push('suite:beforeAll2'));
223+
t.beforeEach((state, test) => {
224+
log.push('suite:beforeEach');
225+
test.before(() => log.push('test:before1'));
226+
test.before(() => log.push('test:before2'));
227+
test.after(() => log.push('test:after1'));
228+
test.after(() => log.push('test:after2'));
229+
});
222230
t.it('dos', () => log.push('test #2'));
223231
t.it('tres', () => log.push('test #3'));
224-
t.afterEach(() => log.push('suite:afterEach'));
232+
t.afterEach(() => log.push('suite:afterEach1'));
233+
t.afterEach(() => log.push('suite:afterEach2'));
225234
t.afterAll(() => log.push('suite:afterAll'));
226235
});
227236
t.it('cuatro', () => log.push('test #4'));
228237
t.afterEach(() => log.push('root:afterEach'));
229-
t.afterAll(() => log.push('root:afterAll'));
238+
t.afterAll(() => log.push('root:afterAll1'));
239+
t.afterAll(() => log.push('root:afterAll2'));
230240
await t.run();
231241
expect(log).toEqual([
232242
'root:beforeAll',
233-
'root:beforeEach',
243+
'root:beforeEach1',
244+
'root:beforeEach2',
234245
'test #1',
235246
'root:afterEach',
236247

237-
'suite:beforeAll',
248+
'suite:beforeAll1',
249+
'suite:beforeAll2',
238250

239-
'root:beforeEach',
251+
'root:beforeEach1',
252+
'root:beforeEach2',
240253
'suite:beforeEach',
254+
'test:before1',
255+
'test:before2',
241256
'test #2',
242-
'suite:afterEach',
257+
'test:after1',
258+
'test:after2',
259+
'suite:afterEach1',
260+
'suite:afterEach2',
243261
'root:afterEach',
244262

245-
'root:beforeEach',
263+
'root:beforeEach1',
264+
'root:beforeEach2',
246265
'suite:beforeEach',
266+
'test:before1',
267+
'test:before2',
247268
'test #3',
248-
'suite:afterEach',
269+
'test:after1',
270+
'test:after2',
271+
'suite:afterEach1',
272+
'suite:afterEach2',
249273
'root:afterEach',
250274

251275
'suite:afterAll',
252276

253-
'root:beforeEach',
277+
'root:beforeEach1',
278+
'root:beforeEach2',
254279
'test #4',
255280
'root:afterEach',
256281

257-
'root:afterAll',
282+
'root:afterAll1',
283+
'root:afterAll2',
258284
]);
259285
});
260286
it('should have the same state object in hooks and test', async() => {

0 commit comments

Comments
 (0)