Skip to content

Commit 99aee51

Browse files
committed
Core: Fix infinite loop crash when mixing test.only() with module.only()
I noticed that the first branch of `addOnlyTest()` was uncovered in test coverage reports. In expanding the `only-module-then-test.js` fixture to cover this (by adding a late test.only call), which was originally added in #1658. I decided to also add an early test.only call to see what would happen. The outcome is irrelevant/ambigious, but it seemed worth observing. To my surprise, it created an infinite loop. This further convinces me that we should make "early errors" show up in the HTML Reporter (TODO added in 37f6e4b), and subsequently get rid of this re-entry hack. The TapReporter and QUnit CLI are already able to render early errors. The HtmlReporter not yet. It's not generally a priority imho, but, because "No tests" is a common scenario, if we utilize an early error to report it, that makes it a priority to support in HtmlReporter as otherwise there'd be no visible feedback for it. It's okay to have to pop open devtools when causing an early syntax error or something like that in your own code, but when using the UI to match no tests, there should at least be some kind of visible feedback and not a blank page.
1 parent de3a37d commit 99aee51

File tree

6 files changed

+105
-2
lines changed

6 files changed

+105
-2
lines changed

src/module.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ module.only = function (...args) {
144144
if (!focused) {
145145
// Upon the first module.only() call,
146146
// delete any and all previously registered modules and tests.
147+
//
148+
// TODO: This does not clear SuiteReport, which means the empty modules are
149+
// left behind and wrongly reported as "skipped" (skipped == total), and
150+
// deleted tests wrongly count toward runEnd.testCounts as passing test
151+
// (this.getFailedAssertions().length === 0).
152+
// This is why /test/cli/fixtures/only-test-only-module-mix.tap.txt reports
153+
// 1..3 instead of 1..1.
147154
config.modules.length = 0;
148155
config.queue.length = 0;
149156

src/reports/suite.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export default class SuiteReport {
2929
tests: this.tests.map(test => test.start()),
3030
childSuites: this.childSuites.map(suite => suite.start()),
3131
testCounts: {
32+
// TODO: Due to code reuse, this ends up computing getStatus()
33+
// for tests that obviously haven't run yet. It's harmless but
34+
// quite inefficient recursion, that we repeat many times over
35+
// also via test.start() and suite.start() above.
3236
total: this.getTestCounts().total
3337
}
3438
};

src/test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,16 @@ function checkPollution () {
911911
let focused = false; // indicates that the "only" filter was used
912912

913913
function addTest (settings) {
914-
if (focused || config.currentModule.ignored) {
914+
if (
915+
// Never ignore the internal "No tests" error, as this would infinite loop.
916+
// See /test/cli/fixtures/only-test-only-module-mix.tap.txt
917+
//
918+
// TODO: If we improve HtmlReporter to buffer and replay early errors,
919+
// we can change "No tests" to be a global error, and thus get rid of
920+
// the "validTest" concept and the ProcessQueue re-entry hack.
921+
!(settings.callback && settings.callback.validTest) &&
922+
(focused || config.currentModule.ignored)
923+
) {
915924
return;
916925
}
917926

test/cli/fixtures/only-module-then-test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ QUnit.module('module A', function () {
1010
});
1111
});
1212

13-
QUnit.test('test A2', function (assert) {
13+
QUnit.test.only('test A2', function (assert) {
14+
assert.true(false, 'not run');
15+
});
16+
17+
QUnit.test('test A3', function (assert) {
1418
assert.true(false, 'not run');
1519
});
1620
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// This currently runs 0 tests because stuff cancels out.
2+
// The intent is ambigious and outcome doesn't really matter.
3+
// It's covered as regression test against a past crash:
4+
//
5+
// * module A:
6+
// - start module scope A
7+
// * test A1:
8+
// - queue A1
9+
// * test.only A2:
10+
// - start "test" focus mode.
11+
// - clear queue (delete A1).
12+
// - queue A2
13+
// * module.only B:
14+
// - start "module" focus mode.
15+
// - clear queue (delete A2).
16+
// - modify parent A to become "ignored".
17+
// - start child-module scope B
18+
// * test B1:
19+
// - ignored, because it's a non-only test and
20+
// we are in "focus" mode due to the earlier test.only.
21+
// thus the only test we would run is a "test.only" test,
22+
// inside a "module.only" block, which this file doesn't have any of.
23+
// * test A4:
24+
// - ignored
25+
// * ProcessingQueue.end:
26+
// - create new Error('No tests were run.')
27+
// - emit it via a dynamically created test
28+
// - despite forcing validTest=true, the added test (as of QUnit 2.21.0) is ignored
29+
// because it is a non-only test and we're in "focus" mode.
30+
// This causes an infinite loop between ProcessingQueue.end and ProcessingQueue.advance
31+
// because it thinks it ads a test, but doesn't.
32+
// This scenerio is surprising because the usual way to active "focused" test mode,
33+
// is by defining a test.only() case, in which case the queue by definition isn't
34+
// empty. Even if the test.only() case was skipped by a filter (or config.moduleId/testId)
35+
// this is taken care of by forcing validTest=true.
36+
37+
QUnit.module('module A', function () {
38+
// ... start module scope A
39+
40+
// ... queue A1
41+
QUnit.test('test A1', function (assert) {
42+
assert.true(false, 'not run');
43+
});
44+
45+
QUnit.test.only('test A2', function (assert) {
46+
assert.true(false, 'not run');
47+
});
48+
49+
QUnit.module.only('module B', function () {
50+
QUnit.test('test B1', function (assert) {
51+
assert.true(true, 'run');
52+
});
53+
});
54+
55+
QUnit.test('test A4', function (assert) {
56+
assert.true(false, 'not run');
57+
});
58+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# command: ["qunit", "only-test-only-module-mix.js"]
2+
3+
TAP version 13
4+
not ok 1 global failure
5+
---
6+
message: No tests were run.
7+
severity: failed
8+
actual : undefined
9+
expected: undefined
10+
stack: |
11+
Error: No tests were run.
12+
at qunit.js
13+
at internal
14+
...
15+
1..3
16+
# pass 2
17+
# skip 0
18+
# todo 0
19+
# fail 1
20+
21+
# exit code: 1

0 commit comments

Comments
 (0)