Skip to content

Commit 62ca15a

Browse files
authored
Core: Convert "No tests were run." from fake test to error
* Remove hacky re-entrance from ProcessingQueue.done() -> test() + advance() -> done(), existed only for this purpose. This also removes the need for the 99aee51 workaround, which avoided a crash by infinite loop. * Remove unused internal `test` injection to ProcessingQueue, existed only for this purpose. * Remove "omit stack trace" logic in test.js, existed only for this purpose. To keep output for the "No tests" error similarly clean and distraction-free, the TAP reporter treats error stack traces with a similar cleaner since #1789. * Remove unused internal `validTest` mechanism existed only for this purpose. This was originally impossible to trigger externally because it required setting `validTest` to a private symbol. In QUnit 1.16 this was simplified as part of commit 3f08a1a, to take any boolean true value to ease some implementation details, however it remained internal in purpose. A search today for `/validTest:/` and `/validTest = /` over public GitHub-hosted repositories, shows that (fortunatley) nobody has started relying on this. I found only copies of QUnit itself. As a nice side-effect, fixtures like async-module-error.tap.txt now no longer display a useless "No tests" after an uncaught error that already bailed the test run. This happened previously because errors are not tests, and so technically there was no test. No that "No tests" is itself considered a bail out, TAP absorbs this after the first error, just like it alreayd does for other cascading errors. Closes #1790.
1 parent 9fed286 commit 62ca15a

16 files changed

+57
-102
lines changed

demos/grunt-contrib-qunit.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ QUnit.test.each('failing tests', {
2121
>> Actual: false
2222
>> Expected: true
2323
>> at `],
24-
'no-tests': ['fail-no-tests', `Testing fail-no-tests.html F
25-
>> global failure
26-
>> Message: No tests were run.
27-
>> Actual: undefined
28-
>> Expected: undefined
24+
'no-tests': ['fail-no-tests', `Testing fail-no-tests.html
2925
>> Error: No tests were run.
3026
>> at `],
3127
uncaught: ['fail-uncaught', `Testing fail-uncaught.html \n\

demos/grunt-contrib-qunit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"private": true,
33
"devDependencies": {
44
"grunt": "1.6.1",
5-
"grunt-contrib-qunit": "10.0.0"
5+
"grunt-contrib-qunit": "10.1.1"
66
},
77
"scripts": {
88
"test": "grunt"

src/core/core.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { createStartFunction } from './start.js';
2626
// rather than partly in config.js and partly here.
2727
config.currentModule.suiteReport = runSuite;
2828

29-
config._pq = new ProcessingQueue(test);
29+
config._pq = new ProcessingQueue();
3030

3131
const QUnit = {
3232

src/core/processing-queue.js

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import config from './config.js';
2-
import { extend, generateHash, performance } from './utilities.js';
2+
import { generateHash, performance } from './utilities.js';
33
import { runLoggingCallbacks } from './callbacks.js';
44
import Promise from './promise.js';
55
import { runSuite } from './module.js';
6+
import onUncaughtException from './on-uncaught-exception.js';
67
import { emit } from './events.js';
78
import { setTimeout } from './globals.js';
89

@@ -28,11 +29,7 @@ function unitSamplerGenerator (seed) {
2829
}
2930

3031
class ProcessingQueue {
31-
/**
32-
* @param {Function} test Reference to the QUnit.test() method
33-
*/
34-
constructor (test) {
35-
this.test = test;
32+
constructor () {
3633
this.priorityCount = 0;
3734
this.unitSampler = null;
3835

@@ -168,7 +165,7 @@ class ProcessingQueue {
168165
done () {
169166
// We have reached the end of the processing queue and are about to emit the
170167
// "runEnd" event after which reporters typically stop listening and exit
171-
// the process. First, check if we need to emit one final test.
168+
// the process. First, check if we need to emit an error event.
172169
if (config.stats.testCount === 0 && config.failOnZeroTests === true) {
173170
let error;
174171
if (config.filter && config.filter.length) {
@@ -183,19 +180,7 @@ class ProcessingQueue {
183180
error = new Error('No tests were run.');
184181
}
185182

186-
this.test('global failure', extend(function (assert) {
187-
assert.pushResult({
188-
result: false,
189-
message: error.message,
190-
source: error.stack
191-
});
192-
}, { validTest: true }));
193-
194-
// We do need to call `advance()` in order to resume the processing queue.
195-
// Once this new test is finished processing, we'll reach `done` again, and
196-
// that time the above condition will evaluate to false.
197-
this.advance();
198-
return;
183+
onUncaughtException(error);
199184
}
200185

201186
const storage = config.storage;

src/core/reporters/TapReporter.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export default class TapReporter {
179179
this.log = options.log || console.log.bind(console);
180180

181181
this.testCount = 0;
182+
this.started = false;
182183
this.ended = false;
183184
this.bailed = false;
184185

@@ -192,8 +193,11 @@ export default class TapReporter {
192193
return new TapReporter(runner, options);
193194
}
194195

195-
onRunStart (_runSuite) {
196-
this.log('TAP version 13');
196+
onRunStart () {
197+
if (!this.started) {
198+
this.log('TAP version 13');
199+
this.started = true;
200+
}
197201
}
198202

199203
onError (error) {
@@ -206,6 +210,7 @@ export default class TapReporter {
206210
// Imitate onTestEnd
207211
// Skip this if we're past "runEnd" as it would look odd
208212
if (!this.ended) {
213+
this.onRunStart();
209214
this.testCount = this.testCount + 1;
210215
this.log(kleur.red(`not ok ${this.testCount} global failure`));
211216
this.logError(error);

src/core/test.js

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,6 @@ export default function Test (settings) {
8787

8888
++Test.count;
8989
this.errorForStack = new Error();
90-
if (this.callback && this.callback.validTest) {
91-
// Omit the test-level trace for the internal "No tests" test failure,
92-
// There is already an assertion-level trace, and that's noisy enough
93-
// as it is.
94-
this.errorForStack.stack = undefined;
95-
}
9690
this.testReport = new TestReport(this.testName, this.module.suiteReport, {
9791
todo: this.todo,
9892
skip: this.skip,
@@ -782,11 +776,6 @@ Test.prototype = {
782776
},
783777

784778
valid: function () {
785-
// Internally-generated tests are always valid
786-
if (this.callback && this.callback.validTest) {
787-
return true;
788-
}
789-
790779
function moduleChainIdMatch (testModule, selectedId) {
791780
return (
792781
// undefined or empty array
@@ -909,16 +898,7 @@ function checkPollution () {
909898
let focused = false; // indicates that the "only" filter was used
910899

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

test/cli/cli-main.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,12 @@ ReferenceError: varIsNotDefined is not defined
202202
assert.equal(execution.snapshot, `TAP version 13
203203
not ok 1 global failure
204204
---
205-
message: "No tests matched the filter \\"no matches\\"."
205+
message: "Error: No tests matched the filter \\"no matches\\"."
206206
severity: failed
207-
actual : undefined
208-
expected: undefined
209207
stack: |
210208
Error: No tests matched the filter "no matches".
211-
at qunit.js
212-
at internal
213209
...
210+
Bail out! Error: No tests matched the filter "no matches".
214211
1..1
215212
# pass 0
216213
# skip 0

test/cli/fixtures/async-module-error-promise.tap.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# name: module() with promise return value
22
# command: ["qunit","async-module-error-promise.js"]
33

4+
TAP version 13
45
not ok 1 global failure
56
---
67
message: |+
@@ -13,7 +14,6 @@ not ok 1 global failure
1314
at internal
1415
...
1516
Bail out! Error: Failed to load file async-module-error-promise.js
16-
TAP version 13
1717
ok 2 module manually returning a promise > has a test
1818
1..2
1919
# pass 1

test/cli/fixtures/async-module-error-thenable.tap.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# name: module() with promise return value
22
# command: ["qunit","async-module-error-thenable.js"]
33

4+
TAP version 13
45
not ok 1 global failure
56
---
67
message: |+
@@ -13,7 +14,6 @@ not ok 1 global failure
1314
at internal
1415
...
1516
Bail out! Error: Failed to load file async-module-error-thenable.js
16-
TAP version 13
1717
ok 2 module manually returning a thenable > has a test
1818
1..2
1919
# pass 1

test/cli/fixtures/async-module-error.tap.txt

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# name: module() with async function
22
# command: ["qunit","async-module-error.js"]
33

4+
TAP version 13
45
not ok 1 global failure
56
---
67
message: |+
@@ -13,18 +14,6 @@ not ok 1 global failure
1314
at internal
1415
...
1516
Bail out! Error: Failed to load file async-module-error.js
16-
TAP version 13
17-
not ok 2 global failure
18-
---
19-
message: No tests were run.
20-
severity: failed
21-
actual : undefined
22-
expected: undefined
23-
stack: |
24-
Error: No tests were run.
25-
at qunit.js
26-
at internal
27-
...
2817
1..2
2918
# pass 0
3019
# skip 0

test/cli/fixtures/no-tests.tap.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44
TAP version 13
55
not ok 1 global failure
66
---
7-
message: No tests were run.
7+
message: Error: No tests were run.
88
severity: failed
9-
actual : undefined
10-
expected: undefined
119
stack: |
1210
Error: No tests were run.
13-
at qunit.js
14-
at internal
1511
...
12+
Bail out! Error: No tests were run.
1613
1..1
1714
# pass 0
1815
# skip 0

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,29 @@
2424
// - ignored
2525
// * ProcessingQueue.end:
2626
// - 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.
27+
//
28+
// - Past problem 1: The error is hidden.
29+
//
30+
// Up until QUnit 2.21.0, this error was emitted via a
31+
// dynamically created QUnit.test(), with `validTest=true` set to bypass
32+
// filters like config.filter, config.module, and config.testId.
33+
// However, because it is a regular test (not an "only" test) it is
34+
// ignored because this test file defines one or more "QUnit.test.only" cases,
35+
// which puts us in "focus" mode and thus never even reaches Test.valid().
36+
//
37+
// - Past problem 2: Crash by infinite loop.
38+
//
39+
// The first problem causes an infinite loop between ProcessingQueue.end
40+
// and ProcessingQueue.advance, because end() thinks it adds a fake test, but
41+
// the definition is ignored due to "focused" test by a previous test.only().
42+
// Normally, that would by definition mean the queue can't be empty and so
43+
// the "No tests" code would never be reached. Even if the test.only() case
44+
// was skipped by a filter (or config.moduleId/testId) this was accounted
45+
// for by setting validTest=true.
46+
// Fixed in QUnit 3 (99aee51a8a) by exempting validTest from "focused" mode.
47+
//
48+
// - QUnit 3 emits "No tests" as "error" instead of a fake test, which obsoletes
49+
// the above workarounds.
3650

3751
QUnit.module('module A', function () {
3852
// ... start module scope A

test/cli/fixtures/only-test-only-module-mix.tap.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33
TAP version 13
44
not ok 1 global failure
55
---
6-
message: No tests were run.
6+
message: Error: No tests were run.
77
severity: failed
8-
actual : undefined
9-
expected: undefined
108
stack: |
119
Error: No tests were run.
12-
at qunit.js
13-
at internal
1410
...
11+
Bail out! Error: No tests were run.
1512
1..3
1613
# pass 2
1714
# skip 0

test/cli/fixtures/syntax-error.tap.txt

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# name: load file with syntax error
22
# command: ["qunit", "syntax-error.js"]
33

4+
TAP version 13
45
not ok 1 global failure
56
---
67
message: |+
@@ -13,18 +14,6 @@ not ok 1 global failure
1314
at internal
1415
...
1516
Bail out! Error: Failed to load file syntax-error.js
16-
TAP version 13
17-
not ok 2 global failure
18-
---
19-
message: No tests were run.
20-
severity: failed
21-
actual : undefined
22-
expected: undefined
23-
stack: |
24-
Error: No tests were run.
25-
at qunit.js
26-
at internal
27-
...
2817
1..2
2918
# pass 0
3019
# skip 0

test/cli/fixtures/unhandled-rejection.tap.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# name: unhandled rejection
22
# command: ["qunit","unhandled-rejection.js"]
33

4+
TAP version 13
45
not ok 1 global failure
56
---
67
message: Error: outside of a test context
@@ -13,7 +14,6 @@ not ok 1 global failure
1314
at internal
1415
...
1516
Bail out! Error: outside of a test context
16-
TAP version 13
1717
not ok 2 Unhandled Rejections > test passes just fine, but has a rejected promise
1818
---
1919
message: global failure: Error: Error thrown in non-returned promise!
@@ -23,7 +23,6 @@ not ok 2 Unhandled Rejections > test passes just fine, but has a rejected promis
2323
stack: |
2424
Error: Error thrown in non-returned promise!
2525
at /qunit/test/cli/fixtures/unhandled-rejection.js:10:13
26-
at internal
2726
...
2827
1..2
2928
# pass 0

test/main/TapReporter.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ QUnit.module('TapReporter', function (hooks) {
5050
},
5151
emit: function (event, data) {
5252
this.on[event](data);
53+
},
54+
clear: function () {
55+
buffer = '';
5356
}
5457
};
5558
last = undefined;
@@ -161,6 +164,8 @@ QUnit.module('TapReporter', function (hooks) {
161164
});
162165

163166
QUnit.test('output global failure (string)', function (assert) {
167+
emitter.emit('runStart');
168+
emitter.clear();
164169
emitter.emit('error', 'Boo');
165170

166171
assert.strictEqual(buffer, kleur.red('not ok 1 global failure') + '\n' +
@@ -175,6 +180,8 @@ QUnit.module('TapReporter', function (hooks) {
175180
QUnit.test('output global failure (Error)', function (assert) {
176181
var err = new ReferenceError('Boo is not defined');
177182
mockStack(err);
183+
emitter.emit('runStart');
184+
emitter.clear();
178185
emitter.emit('error', err);
179186

180187
assert.strictEqual(buffer, kleur.red('not ok 1 global failure') + '\n' +

0 commit comments

Comments
 (0)