Skip to content

Commit

Permalink
[New] add .teardown() on t instances
Browse files Browse the repository at this point in the history
Fixes #531.

Co-authored-by: Matteo Collina <hello@matteocollina.com>
Co-authored-by: Jordan Harband <ljharb@gmail.com>
  • Loading branch information
ljharb and mcollina committed Feb 13, 2021
1 parent 3c05a87 commit d3fc2ff
Show file tree
Hide file tree
Showing 5 changed files with 331 additions and 9 deletions.
59 changes: 50 additions & 9 deletions lib/test.js
Expand Up @@ -69,6 +69,7 @@ function Test(name_, opts_, cb_) {
this._plan = undefined;
this._cb = args.cb;
this._progeny = [];
this._teardown = [];
this._ok = true;
var depthEnvVar = process.env.NODE_TAPE_OBJECT_PRINT_DEPTH;
if (args.opts.objectPrintDepth) {
Expand Down Expand Up @@ -190,6 +191,14 @@ Test.prototype.end = function (err) {
this._end();
};

Test.prototype.teardown = function (fn) {
if (typeof fn !== 'function') {
this.fail('teardown: ' + inspect(fn) + ' is not a function');
} else {
this._teardown.push(fn);
}
};

Test.prototype._end = function (err) {
var self = this;

Expand All @@ -202,16 +211,48 @@ Test.prototype._end = function (err) {
return;
}

if (!this.ended) this.emit('end');
var pendingAsserts = this._pendingAsserts();
if (!this._planError && this._plan !== undefined && pendingAsserts) {
this._planError = true;
this.fail('plan != count', {
expected: this._plan,
actual: this.assertCount
});

function next(i) {
if (i === self._teardown.length) {
completeEnd();
return;
}
var fn = self._teardown[i];
var res;
try {
res = fn();
} catch (e) {
self.fail(e);
}
if (res && typeof res.then === 'function') {
res.then(function () {
next(++i);
}, function (_err) {
err = err || _err;
});
} else {
next(++i);
}
}

if (this._teardown.length > 0) {
next(0);
} else {
completeEnd();
}

function completeEnd() {
if (!self.ended) self.emit('end');
var pendingAsserts = self._pendingAsserts();
if (!self._planError && self._plan !== undefined && pendingAsserts) {
self._planError = true;
self.fail('plan != count', {
expected: self._plan,
actual: self.assertCount
});
}
self.ended = true;
}
this.ended = true;
};

Test.prototype._exit = function () {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -41,10 +41,12 @@
"through": "^2.3.8"
},
"devDependencies": {
"array.prototype.flatmap": "^1.2.4",
"aud": "^1.1.4",
"concat-stream": "^1.6.2",
"eclint": "^2.8.1",
"ecstatic": "^4.1.4",
"es-value-fixtures": "^1.2.1",
"eslint": "^7.20.0",
"falafel": "^2.2.4",
"js-yaml": "^3.14.0",
Expand Down
4 changes: 4 additions & 0 deletions readme.markdown
Expand Up @@ -170,6 +170,10 @@ If `cb` returns a Promise, it will be implicitly awaited. If that promise reject

Generate a new test that will be skipped over.

## test.teardown(cb)

Register a callback to run after the individual test has completed. Multiple registered teardown callbacks will run in order. Useful for undoing side effects, closing network connections, etc.

## test.onFinish(fn)

The onFinish hook will get invoked when ALL `tape` tests have finished right before `tape` is about to print the test summary.
Expand Down
8 changes: 8 additions & 0 deletions test/common.js
Expand Up @@ -69,6 +69,14 @@ module.exports.stripFullStack = function (output) {
// Handle stack trace variation in Node v0.8
/at(:?) Test\.(?:module\.exports|tap\.test\.err\.code)/g,
'at$1 Test.<anonymous>'
).replace(
// Handle stack trace variation in Node v0.8
/at(:?) (Test\.)?tap\.test\.test\.skip/g,
'at$1 $2<anonymous>'
).replace(
// Handle stack trace variation in Node v0.8
/(\[\.\.\. stack stripped \.\.\.\]\n *at) <anonymous> \(([^)]+)\)/g,
'$1 $2'
).split('\n');
};

Expand Down
267 changes: 267 additions & 0 deletions test/teardown.js
@@ -0,0 +1,267 @@
'use strict';

var tape = require('../');
var tap = require('tap');
var concat = require('concat-stream');
var forEach = require('for-each');
var v = require('es-value-fixtures');
var inspect = require('object-inspect');
var flatMap = require('array.prototype.flatmap');

var stripFullStack = require('./common').stripFullStack;

tap.test('teardowns', function (tt) {
tt.plan(1);

var test = tape.createHarness();
test.createStream().pipe(concat(function (body) {
tt.same(stripFullStack(body.toString('utf8')), [].concat(
'TAP version 13',
'# success',
'ok 1 should be truthy',
'# success teardown',
'# success teardown 2',
'# success (async)',
'ok 2 should be truthy',
'# success (async) teardown',
'# success (async) teardown 2',
'# nested teardowns',
'# nested success',
'ok 3 should be truthy',
'# nested teardown (nested success level)',
'# nested teardown (nested success level) 2',
'# nested failure',
'not ok 4 nested failure!',
' ---',
' operator: fail',
' at: Test.<anonymous> ($TEST/teardown.js:$LINE:$COL)',
' stack: |-',
' Error: nested failure!',
' [... stack stripped ...]',
' at Test.<anonymous> ($TEST/teardown.js:$LINE:$COL)',
' [... stack stripped ...]',
' ...',
'# nested teardown (nested fail level)',
'# nested teardown (nested fail level) 2',
'# nested teardown (top level)',
'# nested teardown (top level) 2',
'# fail',
'not ok 5 failure!',
' ---',
' operator: fail',
' at: Test.<anonymous> ($TEST/teardown.js:$LINE:$COL)',
' stack: |-',
' Error: failure!',
' [... stack stripped ...]',
' at Test.<anonymous> ($TEST/teardown.js:$LINE:$COL)',
' [... stack stripped ...]',
' ...',
'# failure teardown',
'# failure teardown 2',
'# teardown errors do not stop the next teardown fn from running',
'ok 6 should be truthy',
'not ok 7 SyntaxError: teardown error!',
' ---',
' operator: fail',
' stack: |-',
' Error: SyntaxError: teardown error!',
' [... stack stripped ...]',
' ...',
'not ok 8 plan != count',
' ---',
' operator: fail',
' expected: 1',
' actual: 2',
' stack: |-',
' Error: plan != count',
' [... stack stripped ...]',
' ...',
'# teardown runs after teardown error',
'# teardown given non-function fails the test',
'ok 9 should be truthy',
flatMap(v.nonFunctions, function (nonFunction, i) {
var offset = 10;
return [].concat(
'not ok ' + (offset + (i > 0 ? i + 1 : i)) + ' teardown: ' + inspect(nonFunction) + ' is not a function',
' ---',
' operator: fail',
' at: <anonymous> ($TEST/teardown.js:$LINE:$COL)',
' stack: |-',
' Error: teardown: ' + inspect(nonFunction) + ' is not a function',
' [... stack stripped ...]',
' at $TEST/teardown.js:$LINE:$COL',
' [... stack stripped ...]',
' at Test.<anonymous> ($TEST/teardown.js:$LINE:$COL)',
' [... stack stripped ...]',
' ...',
i > 0 ? [] : [
'not ok '+ (offset + 1) +' plan != count',
' ---',
' operator: fail',
' expected: 1',
' actual: 2',
' at: <anonymous> ($TEST/teardown.js:$LINE:$COL)',
' stack: |-',
' Error: plan != count',
' [... stack stripped ...]',
' at $TEST/teardown.js:$LINE:$COL',
' [... stack stripped ...]',
' at Test.<anonymous> ($TEST/teardown.js:$LINE:$COL)',
' [... stack stripped ...]',
' ...'
]
);
}),
typeof Promise === 'function' ? [
'# success (promise)',
'ok ' + (11 + v.nonFunctions.length) + ' should be truthy',
'# success (promise) teardown: 1',
'# success (promise) teardown: 2',
'# success (promise) teardown: 3'
] : [
'# SKIP success (promise)'
], [
'',
'1..' + ((typeof Promise === 'function' ? 1 : 0) + 10 + v.nonFunctions.length),
'# tests ' + ((typeof Promise === 'function' ? 1 : 0) + 10 + v.nonFunctions.length),
'# pass ' + ((typeof Promise === 'function' ? 1 : 0) + 5),
'# fail ' + (5 + v.nonFunctions.length),
''
]));
}));

test('success', function (t) {
t.plan(1);
t.teardown(function () {
t.comment('success teardown');
});
t.teardown(function () {
t.comment('success teardown 2');
});
t.ok('success!');
});

test('success (async)', function (t) {
t.plan(1);
t.teardown(function () {
t.comment('success (async) teardown');
});
t.teardown(function () {
t.comment('success (async) teardown 2');
});
setTimeout(function () {
t.ok('success!');
}, 10);
});

test('nested teardowns', function (t) {
t.plan(2);

t.teardown(function () {
t.comment('nested teardown (top level)');
});
t.teardown(function () {
t.comment('nested teardown (top level) 2');
});

t.test('nested success', function (st) {
st.teardown(function () {
st.comment('nested teardown (nested success level)');
});
st.teardown(function () {
st.comment('nested teardown (nested success level) 2');
});

st.ok('nested success!');
st.end();
});

t.test('nested failure', function (st) {
st.plan(1);

st.teardown(function () {
st.comment('nested teardown (nested fail level)');
});
st.teardown(function () {
st.comment('nested teardown (nested fail level) 2');
});

st.fail('nested failure!');
});
});

test('fail', function (t) {
t.plan(1);

t.teardown(function () {
t.comment('failure teardown');
});
t.teardown(function () {
t.comment('failure teardown 2');
});

t.fail('failure!');
});

test('teardown errors do not stop the next teardown fn from running', function (t) {
t.plan(1);

t.ok('teardown error test');

t.teardown(function () {
throw new SyntaxError('teardown error!');
});
t.teardown(function () {
t.comment('teardown runs after teardown error');
});
});

test('teardown given non-function fails the test', function (t) {
t.plan(1);

t.ok('non-function test');

forEach(v.nonFunctions, function (nonFunction) {
t.teardown(nonFunction);
});
});

test('success (promise)', { skip: typeof Promise !== 'function' }, function (t) {
t.plan(1);

t.teardown(function () {
return new Promise(function (resolve) {
t.comment('success (promise) teardown: 1');
setTimeout(resolve, 10);
}).then(function () {
t.comment('success (promise) teardown: 2');
});
});
t.teardown(function () {
t.comment('success (promise) teardown: 3');
});

setTimeout(function () {
t.ok('success!');
}, 10);
});
});

tap.test('teardown with promise', { skip: typeof Promise !== 'function', timeout: 1e3 }, function (tt) {
tt.plan(2);
tape('dummy test', function (t) {
var resolved = false;
t.teardown(function () {
tt.pass('tape teardown');
var p = Promise.resolve();
p.then(function () {
resolved = true;
});
return p;
});
t.on('end', function () {
tt.is(resolved, true);
});
t.end();
});
});

0 comments on commit d3fc2ff

Please sign in to comment.