test_runner: count nested tests

PR-URL: https://github.com/nodejs/node/pull/47094
Fixes: https://github.com/nodejs/node/issues/46762
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
Moshe Atlow authored and GitHub committed 2023-03-21 08:16:38 +00:00
1 parent 965ea9d586
commit d1eaded0d1
21 files changed
+163 -93

No files matched your search

+11
View File
@@ -168,6 +168,17 @@ function setup(root) {
__proto__: null,
bootstrapComplete: false,
coverage: null,
counters: {
__proto__: null,
all: 0,
failed: 0,
passed: 0,
cancelled: 0,
skipped: 0,
todo: 0,
planned: 0,
suites: 0,
},
};
root.startTime = hrtime();
return root;
+1
View File
@@ -113,6 +113,7 @@ function reportDetails(nesting, data = kEmptyObject) {
let details = `${_indent} ---\n`;
details += jsToYaml(_indent, 'duration_ms', duration_ms);
details += jsToYaml(_indent, 'type', data.type);
details += jsToYaml(_indent, null, error);
details += `${_indent} ...\n`;
return details;
+16 -17
View File
@@ -10,10 +10,8 @@ const {
ArrayPrototypeSome,
ArrayPrototypeSort,
ArrayPrototypeSplice,
FunctionPrototypeCall,
Number,
ObjectAssign,
ObjectKeys,
PromisePrototypeThen,
SafePromiseAll,
SafePromiseAllReturnVoid,
@@ -55,8 +53,9 @@ const { YAMLToJs } = require('internal/test_runner/yaml_to_js');
const { TokenKind } = require('internal/test_runner/tap_lexer');
const {
isSupportedFileType,
countCompletedTest,
doesPathMatchFilter,
isSupportedFileType,
} = require('internal/test_runner/utils');
const { basename, join, resolve } = require('path');
const { once } = require('events');
@@ -67,7 +66,7 @@ const {
const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch'];
const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
const kDiagnosticsFilterArgs = ['tests', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];
const kCanceledTests = new SafeSet()
.add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure);
@@ -151,10 +150,10 @@ function getRunArgs({ path, inspectPort }) {
class FileTest extends Test {
#buffer = [];
#counters = { __proto__: null, all: 0, failed: 0, passed: 0, cancelled: 0, skipped: 0, todo: 0, totalFailed: 0 };
#reportedChildren = 0;
failedSubtests = false;
#skipReporting() {
return this.#counters.all > 0 && (!this.error || this.error.failureType === kSubtestsFailed);
return this.#reportedChildren > 0 && (!this.error || this.error.failureType === kSubtestsFailed);
}
#checkNestedComment({ comment }) {
const firstSpaceIndex = StringPrototypeIndexOf(comment, ' ');
@@ -204,11 +203,19 @@ class FileTest extends Test {
const method = pass ? 'ok' : 'fail';
this.reporter[method](nesting, this.name, testNumber, node.description, diagnostics, directive);
if (nesting === 0) {
FunctionPrototypeCall(super.countSubtest,
{ finished: true, skipped: skip, isTodo: todo, passed: pass, cancelled },
this.#counters);
this.failedSubtests ||= !pass;
}
this.#reportedChildren++;
countCompletedTest({
name: node.description,
finished: true,
skipped: skip,
isTodo: todo,
passed: pass,
cancelled,
nesting,
reportedType: diagnostics.type,
}, this.root.harness);
break;
}
@@ -233,14 +240,6 @@ class FileTest extends Test {
this.reportStarted();
this.#handleReportItem(ast);
}
countSubtest(counters) {
if (this.#counters.all === 0) {
return super.countSubtest(counters);
}
ArrayPrototypeForEach(ObjectKeys(counters), (key) => {
counters[key] += this.#counters[key];
});
}
reportStarted() {}
report() {
const skipReporting = this.#skipReporting();
+24 -35
View File
@@ -34,6 +34,7 @@ const { MockTracker } = require('internal/test_runner/mock');
const { TestsStream } = require('internal/test_runner/tests_stream');
const {
createDeferredCallback,
countCompletedTest,
isTestFailureError,
parseCommandLine,
} = require('internal/test_runner/utils');
@@ -186,6 +187,7 @@ class Test extends AsyncResource {
this.runOnlySubtests = this.only;
this.testNumber = 0;
this.timeout = kDefaultTimeout;
this.root = this;
} else {
const nesting = parent.parent === null ? parent.nesting :
parent.nesting + 1;
@@ -197,6 +199,7 @@ class Test extends AsyncResource {
this.runOnlySubtests = !this.only;
this.testNumber = parent.subtests.length + 1;
this.timeout = parent.timeout;
this.root = parent.root;
}
switch (typeof concurrency) {
@@ -575,31 +578,7 @@ class Test extends AsyncResource {
this.postRun();
}
countSubtest(counters) {
// Check SKIP and TODO tests first, as those should not be counted as
// failures.
if (this.skipped) {
counters.skipped++;
} else if (this.isTodo) {
counters.todo++;
} else if (this.cancelled) {
counters.cancelled++;
} else if (!this.passed) {
counters.failed++;
} else {
counters.passed++;
}
if (!this.passed) {
counters.totalFailed++;
}
counters.all++;
}
postRun(pendingSubtestsError) {
const counters = {
__proto__: null, all: 0, failed: 0, passed: 0, cancelled: 0, skipped: 0, todo: 0, totalFailed: 0,
};
// If the test was failed before it even started, then the end time will
// be earlier than the start time. Correct that here.
if (this.endTime < this.startTime) {
@@ -610,6 +589,7 @@ class Test extends AsyncResource {
// The test has run, so recursively cancel any outstanding subtests and
// mark this test as failed if any subtests failed.
this.pendingSubtests = [];
let failed = 0;
for (let i = 0; i < this.subtests.length; i++) {
const subtest = this.subtests[i];
@@ -617,12 +597,14 @@ class Test extends AsyncResource {
subtest.#cancel(pendingSubtestsError);
subtest.postRun(pendingSubtestsError);
}
subtest.countSubtest(counters);
if (!subtest.passed) {
failed++;
}
}
if ((this.passed || this.parent === null) && counters.totalFailed > 0) {
const subtestString = `subtest${counters.totalFailed > 1 ? 's' : ''}`;
const msg = `${counters.totalFailed} ${subtestString} failed`;
if ((this.passed || this.parent === null) && failed > 0) {
const subtestString = `subtest${failed > 1 ? 's' : ''}`;
const msg = `${failed} ${subtestString} failed`;
this.fail(new ERR_TEST_FAILURE(msg, kSubtestsFailed));
}
@@ -637,18 +619,19 @@ class Test extends AsyncResource {
this.parent.processPendingSubtests();
} else if (!this.reported) {
this.reported = true;
this.reporter.plan(this.nesting, kFilename, counters.all);
this.reporter.plan(this.nesting, kFilename, this.root.harness.counters.planned);
for (let i = 0; i < this.diagnostics.length; i++) {
this.reporter.diagnostic(this.nesting, kFilename, this.diagnostics[i]);
}
this.reporter.diagnostic(this.nesting, kFilename, `tests ${counters.all}`);
this.reporter.diagnostic(this.nesting, kFilename, `pass ${counters.passed}`);
this.reporter.diagnostic(this.nesting, kFilename, `fail ${counters.failed}`);
this.reporter.diagnostic(this.nesting, kFilename, `cancelled ${counters.cancelled}`);
this.reporter.diagnostic(this.nesting, kFilename, `skipped ${counters.skipped}`);
this.reporter.diagnostic(this.nesting, kFilename, `todo ${counters.todo}`);
this.reporter.diagnostic(this.nesting, kFilename, `tests ${this.root.harness.counters.all}`);
this.reporter.diagnostic(this.nesting, kFilename, `suites ${this.root.harness.counters.suites}`);
this.reporter.diagnostic(this.nesting, kFilename, `pass ${this.root.harness.counters.passed}`);
this.reporter.diagnostic(this.nesting, kFilename, `fail ${this.root.harness.counters.failed}`);
this.reporter.diagnostic(this.nesting, kFilename, `cancelled ${this.root.harness.counters.cancelled}`);
this.reporter.diagnostic(this.nesting, kFilename, `skipped ${this.root.harness.counters.skipped}`);
this.reporter.diagnostic(this.nesting, kFilename, `todo ${this.root.harness.counters.todo}`);
this.reporter.diagnostic(this.nesting, kFilename, `duration_ms ${this.#duration()}`);
if (this.harness?.coverage) {
@@ -689,6 +672,7 @@ class Test extends AsyncResource {
}
report() {
countCompletedTest(this);
if (this.subtests.length > 0) {
this.reporter.plan(this.subtests[0].nesting, kFilename, this.subtests.length);
} else {
@@ -703,6 +687,10 @@ class Test extends AsyncResource {
directive = this.reporter.getTodo(this.message);
}
if (this.reportedType) {
details.type = this.reportedType;
}
if (this.passed) {
this.reporter.ok(this.nesting, kFilename, this.testNumber, this.name, details, directive);
} else {
@@ -746,6 +734,7 @@ class TestHook extends Test {
}
class Suite extends Test {
reportedType = 'suite';
constructor(options) {
super(options);
+25
View File
@@ -222,8 +222,33 @@ function parseCommandLine() {
return globalTestOptions;
}
function countCompletedTest(test, harness = test.root.harness) {
if (test.nesting === 0) {
harness.counters.planned++;
}
if (test.reportedType === 'suite') {
harness.counters.suites++;
return;
}
// Check SKIP and TODO tests first, as those should not be counted as
// failures.
if (test.skipped) {
harness.counters.skipped++;
} else if (test.isTodo) {
harness.counters.todo++;
} else if (test.cancelled) {
harness.counters.cancelled++;
} else if (!test.passed) {
harness.counters.failed++;
} else {
harness.counters.passed++;
}
harness.counters.all++;
}
module.exports = {
convertStringToRegExp,
countCompletedTest,
createDeferredCallback,
doesPathMatchFilter,
isSupportedFileType,
+4 -3
View File
@@ -260,10 +260,11 @@ not ok 4 - callback abort signal
*
...
1..4
# tests 4
# pass 0
# tests 22
# suites 0
# pass 8
# fail 0
# cancelled 4
# cancelled 14
# skipped 0
# todo 0
# duration_ms *
+6 -3
View File
@@ -64,6 +64,7 @@ TAP version 13
not ok 1 - describe timeout signal
---
duration_ms: *
type: 'suite'
failureType: 'testAborted'
error: 'The operation was aborted due to timeout'
code: 23
@@ -78,6 +79,7 @@ not ok 1 - describe timeout signal
not ok 2 - describe abort signal
---
duration_ms: *
type: 'suite'
failureType: 'testAborted'
error: 'This operation was aborted'
code: 20
@@ -94,10 +96,11 @@ not ok 2 - describe abort signal
*
...
1..2
# tests 2
# pass 0
# tests 9
# suites 2
# pass 4
# fail 0
# cancelled 2
# cancelled 5
# skipped 0
# todo 0
# duration_ms *
+14 -4
View File
@@ -210,6 +210,7 @@ ok 21 - immediate resolve pass
not ok 22 - subtest sync throw fail
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '1 subtest failed'
code: 'ERR_TEST_FAILURE'
@@ -247,11 +248,13 @@ not ok 23 - sync throw non-error fail
ok 24 - level 0a
---
duration_ms: *
type: 'suite'
...
# Subtest: invalid subtest - pass but subtest fails
ok 25 - invalid subtest - pass but subtest fails
---
duration_ms: *
type: 'suite'
...
# Subtest: sync skip option
ok 26 - sync skip option # SKIP
@@ -494,6 +497,7 @@ not ok 53 - custom inspect symbol that throws fail
not ok 54 - subtest sync throw fails
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '2 subtests failed'
code: 'ERR_TEST_FAILURE'
@@ -511,6 +515,7 @@ not ok 54 - subtest sync throw fails
not ok 55 - describe sync throw fails
---
duration_ms: *
type: 'suite'
failureType: 'testCodeFailure'
error: 'thrown from describe'
code: 'ERR_TEST_FAILURE'
@@ -539,6 +544,7 @@ not ok 55 - describe sync throw fails
not ok 56 - describe async throw fails
---
duration_ms: *
type: 'suite'
failureType: 'testCodeFailure'
error: 'thrown from describe'
code: 'ERR_TEST_FAILURE'
@@ -587,6 +593,7 @@ not ok 56 - describe async throw fails
not ok 57 - timeouts
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '2 subtests failed'
code: 'ERR_TEST_FAILURE'
@@ -612,6 +619,7 @@ not ok 57 - timeouts
not ok 58 - successful thenable
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '1 subtest failed'
code: 'ERR_TEST_FAILURE'
@@ -620,6 +628,7 @@ not ok 58 - successful thenable
not ok 59 - rejected thenable
---
duration_ms: *
type: 'suite'
failureType: 'testCodeFailure'
error: 'custom error'
code: 'ERR_TEST_FAILURE'
@@ -643,10 +652,11 @@ not ok 60 - invalid subtest fail
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
# tests 60
# pass 23
# fail 22
# cancelled 0
# tests 67
# suites 9
# pass 29
# fail 19
# cancelled 4
# skipped 10
# todo 5
# duration_ms *
@@ -10,14 +10,17 @@ TAP version 13
ok 1 - nested
---
duration_ms: *
type: 'suite'
...
1..1
ok 1 - nested - no tests
---
duration_ms: *
type: 'suite'
...
1..1
# tests 1
# suites 2
# pass 1
# fail 0
# cancelled 0
+13 -4
View File
@@ -25,11 +25,13 @@ TAP version 13
ok 3 - nested
---
duration_ms: *
type: 'suite'
...
1..3
ok 1 - describe hooks
---
duration_ms: *
type: 'suite'
...
# Subtest: before throws
# Subtest: 1
@@ -52,6 +54,7 @@ ok 1 - describe hooks
not ok 2 - before throws
---
duration_ms: *
type: 'suite'
failureType: 'hookFailed'
error: 'failed running before hook'
code: 'ERR_TEST_FAILURE'
@@ -80,6 +83,7 @@ not ok 2 - before throws
not ok 3 - after throws
---
duration_ms: *
type: 'suite'
failureType: 'hookFailed'
error: 'failed running after hook'
code: 'ERR_TEST_FAILURE'
@@ -136,6 +140,7 @@ not ok 3 - after throws
not ok 4 - beforeEach throws
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '2 subtests failed'
code: 'ERR_TEST_FAILURE'
@@ -183,6 +188,7 @@ not ok 4 - beforeEach throws
not ok 5 - afterEach throws
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '2 subtests failed'
code: 'ERR_TEST_FAILURE'
@@ -216,6 +222,7 @@ not ok 5 - afterEach throws
not ok 6 - afterEach when test fails
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '1 subtest failed'
code: 'ERR_TEST_FAILURE'
@@ -263,6 +270,7 @@ not ok 6 - afterEach when test fails
not ok 7 - afterEach throws and test fails
---
duration_ms: *
type: 'suite'
failureType: 'subtestsFailed'
error: '2 subtests failed'
code: 'ERR_TEST_FAILURE'
@@ -491,10 +499,11 @@ not ok 13 - t.after() is called if test body throws
...
# - after() called
1..13
# tests 13
# pass 2
# fail 11
# cancelled 0
# tests 35
# suites 8
# pass 14
# fail 19
# cancelled 2
# skipped 0
# todo 0
# duration_ms *
+3 -2
View File
@@ -21,10 +21,11 @@ not ok 1 - does not keep event loop alive
*
...
1..1
# tests 1
# tests 2
# suites 0
# pass 0
# fail 0
# cancelled 1
# cancelled 2
# skipped 0
# todo 0
# duration_ms *
+7 -3
View File
@@ -131,6 +131,7 @@ ok 11 - only = true, with subtests
ok 12 - describe only = true, with subtests
---
duration_ms: *
type: 'suite'
...
# Subtest: describe only = true, with a mixture of subtests
# Subtest: `it` subtest 1
@@ -167,6 +168,7 @@ ok 12 - describe only = true, with subtests
ok 13 - describe only = true, with a mixture of subtests
---
duration_ms: *
type: 'suite'
...
# Subtest: describe only = true, with subtests
# Subtest: subtest should run
@@ -188,12 +190,14 @@ ok 13 - describe only = true, with a mixture of subtests
ok 14 - describe only = true, with subtests
---
duration_ms: *
type: 'suite'
...
1..14
# tests 14
# pass 4
# tests 34
# suites 3
# pass 14
# fail 0
# cancelled 0
# skipped 10
# skipped 20
# todo 0
# duration_ms *
+5 -4
View File
@@ -642,10 +642,11 @@ not ok 65 - invalid subtest fail
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
# tests 65
# pass 27
# fail 21
# cancelled 2
# tests 79
# suites 0
# pass 37
# fail 24
# cancelled 3
# skipped 10
# todo 5
# duration_ms *
+5 -4
View File
@@ -642,10 +642,11 @@ not ok 65 - invalid subtest fail
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
1..65
# tests 65
# pass 27
# fail 21
# cancelled 2
# tests 79
# suites 0
# pass 37
# fail 24
# cancelled 3
# skipped 10
# todo 5
# duration_ms *
@@ -275,10 +275,11 @@
Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
tests 65
pass 27
fail 21
cancelled 2
tests 79
suites 0
pass 37
fail 24
cancelled 3
skipped 10
todo 5
duration_ms *
@@ -38,16 +38,19 @@ ok 7 - top level skipped it enabled # SKIP
ok 8 - top level describe disabled # SKIP test name does not match pattern
---
duration_ms: *
type: 'suite'
...
# Subtest: top level skipped describe disabled
ok 9 - top level skipped describe disabled # SKIP test name does not match pattern
---
duration_ms: *
type: 'suite'
...
# Subtest: top level skipped describe enabled
ok 10 - top level skipped describe enabled # SKIP
---
duration_ms: *
type: 'suite'
...
# Subtest: top level runs because name includes PaTtErN
ok 11 - top level runs because name includes PaTtErN
@@ -80,6 +83,7 @@ ok 12 - top level test enabled
ok 3 - nested describe disabled # SKIP test name does not match pattern
---
duration_ms: *
type: 'suite'
...
# Subtest: nested describe enabled
# Subtest: is enabled
@@ -91,17 +95,20 @@ ok 12 - top level test enabled
ok 4 - nested describe enabled
---
duration_ms: *
type: 'suite'
...
1..4
ok 13 - top level describe enabled
---
duration_ms: *
type: 'suite'
...
1..13
# tests 13
# pass 4
# suites 6
# pass 6
# fail 0
# cancelled 0
# skipped 9
# skipped 7
# todo 0
# duration_ms *
@@ -31,10 +31,11 @@ ok 4 - not only and does not match pattern # SKIP 'only' option not set
duration_ms: *
...
1..4
# tests 4
# pass 1
# tests 6
# suites 0
# pass 2
# fail 0
# cancelled 0
# skipped 3
# skipped 4
# todo 0
# duration_ms *
@@ -26,6 +26,7 @@ not ok 3 - fail
...
1..3
# tests 3
# suites 0
# pass 1
# fail 0
# cancelled 2
+4 -3
View File
@@ -155,9 +155,10 @@ const testFixtures = fixtures.path('test-runner');
assert.match(stdout, /# Subtest: level 0b/);
assert.match(stdout, /not ok 4 - level 0b/);
assert.match(stdout, / {2}error: 'level 0b error'/);
assert.match(stdout, /# tests 4/);
assert.match(stdout, /# pass 2/);
assert.match(stdout, /# fail 2/);
assert.match(stdout, /# tests 8/);
assert.match(stdout, /# pass 4/);
assert.match(stdout, /# fail 3/);
assert.match(stdout, /# skipped 1/);
}
{
@@ -12,7 +12,7 @@ const { spawnSync } = require('child_process');
const stdout = child.stdout.toString();
assert.match(stdout, /^# Warning: Test "extraneous async activity test" generated asynchronous activity after the test ended/m);
assert.match(stdout, /^# pass 1/m);
assert.match(stdout, /^# fail 0$/m);
assert.match(stdout, /^# fail 1$/m);
assert.match(stdout, /^# cancelled 0$/m);
assert.strictEqual(child.status, 1);
assert.strictEqual(child.signal, null);
@@ -26,7 +26,7 @@ const { spawnSync } = require('child_process');
const stdout = child.stdout.toString();
assert.match(stdout, /^# Warning: Test "extraneous async activity test" generated asynchronous activity after the test ended/m);
assert.match(stdout, /^# pass 1$/m);
assert.match(stdout, /^# fail 0$/m);
assert.match(stdout, /^# fail 1$/m);
assert.match(stdout, /^# cancelled 0$/m);
assert.strictEqual(child.status, 1);
assert.strictEqual(child.signal, null);
@@ -11,6 +11,7 @@
**
[90m* should skip [90m(*ms)[39m # SKIP[39m
[34m* tests 3[39m
[34m* suites 0[39m
[34m* pass 1[39m
[34m* fail 1[39m
[34m* cancelled 0[39m