Skip to content

Commit d84bd67

Browse files
committed
Adding debug info to test isolation validation
1 parent 4fd8c53 commit d84bd67

11 files changed

+588
-35
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { run } from '@ember/runloop';
2+
3+
export default function getDebugInfoAvailable() {
4+
return typeof run.backburner.getDebugInfo === 'function';
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
const PENDING_REQUESTS = 'Pending AJAX requests';
2+
const PENDING_TEST_WAITERS = 'Pending test waiters: YES';
3+
const PENDING_TIMERS = 'Pending timers';
4+
const PENDING_SCHEDULED_ITEMS = 'Pending scheduled items';
5+
const ACTIVE_RUNLOOPS = 'Active runloops: YES';
6+
7+
export function getSummary(testCounts, leakCounts, testNames) {
8+
let leakInfo =
9+
leakCounts.length > 0
10+
? `We found the following information that may help you identify code that violated test isolation: \n
11+
${leakCounts.join('\n')}
12+
\n`
13+
: '';
14+
let summary = `TESTS ARE NOT ISOLATED\n
15+
The following ${testCounts} test(s) have one or more issues that are resulting in non-isolation (async execution is extending beyond the duration of the test):\n
16+
${testNames.join('\n')}
17+
\n
18+
${leakInfo}
19+
More information has been printed to the console. Please use that information to help in debugging.
20+
`;
21+
22+
return summary;
23+
}
24+
25+
export default class TestDebugInfoSummary {
26+
constructor() {
27+
this.reset();
28+
}
29+
30+
add(testDebugInfo) {
31+
let summary = testDebugInfo.summary;
32+
33+
this._testDebugInfos.push(testDebugInfo);
34+
35+
this.fullTestNames.push(summary.fullTestName);
36+
if (summary.hasPendingRequests) {
37+
this.hasPendingRequests = true;
38+
}
39+
if (summary.hasPendingWaiters) {
40+
this.hasPendingWaiters = true;
41+
}
42+
if (summary.hasRunLoop) {
43+
this.hasRunLoop = true;
44+
}
45+
if (summary.hasPendingTimers) {
46+
this.hasPendingTimers = true;
47+
}
48+
if (summary.pendingScheduledQueueItemCount > 0) {
49+
this.hasPendingScheduledQueueItems = true;
50+
}
51+
52+
this.totalPendingRequestCount += summary.pendingRequestCount;
53+
this.totalPendingTimersCount += summary.pendingTimersCount;
54+
this.totalPendingScheduledQueueItemCount += summary.pendingScheduledQueueItemCount;
55+
}
56+
57+
get hasDebugInfo() {
58+
return this._testDebugInfos.length > 0;
59+
}
60+
61+
reset() {
62+
this._testDebugInfos = [];
63+
this.fullTestNames = [];
64+
this.hasPendingRequests = false;
65+
this.hasPendingWaiters = false;
66+
this.hasRunLoop = false;
67+
this.hasPendingTimers = false;
68+
this.hasPendingScheduledQueueItems = false;
69+
this.totalPendingRequestCount = 0;
70+
this.totalPendingTimersCount = 0;
71+
this.totalPendingScheduledQueueItemCount = 0;
72+
}
73+
74+
printToConsole(_console = console) {
75+
_console.group('Tests not isolated');
76+
77+
this._testDebugInfos.forEach(testDebugInfo => {
78+
let summary = testDebugInfo.summary;
79+
80+
_console.group(summary.fullTestName);
81+
82+
if (summary.hasPendingRequests) {
83+
_console.log(this.formatCount(PENDING_REQUESTS, summary.pendingRequestCount));
84+
}
85+
86+
if (summary.hasPendingWaiters) {
87+
_console.log(PENDING_TEST_WAITERS);
88+
}
89+
90+
if (summary.hasPendingTimers) {
91+
_console.group(this.formatCount(PENDING_TIMERS, summary.pendingTimersCount));
92+
summary.pendingTimersStackTraces.forEach(stackTrace => {
93+
_console.log(stackTrace);
94+
});
95+
_console.groupEnd();
96+
}
97+
98+
if (summary.hasPendingScheduledQueueItems) {
99+
_console.group(
100+
this.formatCount(PENDING_SCHEDULED_ITEMS, summary.pendingScheduledQueueItemCount)
101+
);
102+
summary.pendingScheduledQueueItemStackTraces.forEach(stackTrace => {
103+
_console.log(stackTrace);
104+
});
105+
_console.groupEnd();
106+
}
107+
108+
if (summary.hasRunLoop) {
109+
_console.log(ACTIVE_RUNLOOPS);
110+
}
111+
112+
_console.groupEnd();
113+
});
114+
115+
_console.groupEnd();
116+
}
117+
118+
formatForBrowser() {
119+
let leakCounts = [];
120+
121+
if (this.hasPendingRequests) {
122+
leakCounts.push(this.formatCount(PENDING_REQUESTS, this.totalPendingRequestCount));
123+
}
124+
125+
if (this.hasPendingWaiters) {
126+
leakCounts.push(PENDING_TEST_WAITERS);
127+
}
128+
129+
if (this.hasPendingTimers) {
130+
leakCounts.push(this.formatCount(PENDING_TIMERS, this.totalPendingTimersCount));
131+
}
132+
133+
if (this.hasPendingScheduledQueueItems) {
134+
leakCounts.push(
135+
this.formatCount(PENDING_SCHEDULED_ITEMS, this.totalPendingScheduledQueueItemCount)
136+
);
137+
}
138+
139+
if (this.hasRunLoop) {
140+
leakCounts.push(ACTIVE_RUNLOOPS);
141+
}
142+
143+
return getSummary(this._testDebugInfos.length, leakCounts, this.fullTestNames);
144+
}
145+
146+
formatCount(title, count) {
147+
return `${title}: ${count}`;
148+
}
149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Encapsulates debug information for an individual test. Aggregates information
3+
* from:
4+
* - the test info provided by qunit (module & name)
5+
* - info provided by @ember/test-helper's getSettledState function
6+
* - hasPendingTimers
7+
* - hasRunLoop
8+
* - hasPendingWaiters
9+
* - hasPendingRequests
10+
* - pendingRequestCount
11+
* - info provided by backburner's getDebugInfo method (timers, schedules, and stack trace info)
12+
*/
13+
export default class TestDebugInfo {
14+
constructor(module, name, settledState, debugInfo) {
15+
this.module = module;
16+
this.name = name;
17+
this.settledState = settledState;
18+
this.debugInfo = debugInfo;
19+
}
20+
21+
get fullTestName() {
22+
return `${this.module}: ${this.name}`;
23+
}
24+
25+
get summary() {
26+
if (!this._summaryInfo) {
27+
this._summaryInfo = Object.assign(
28+
{
29+
fullTestName: this.fullTestName,
30+
},
31+
this.settledState
32+
);
33+
34+
if (this.debugInfo) {
35+
this._summaryInfo.pendingTimersCount = this.debugInfo.timers.length;
36+
this._summaryInfo.pendingTimersStackTraces = this.debugInfo.timers.map(
37+
timer => timer.stack
38+
);
39+
this._summaryInfo.pendingScheduledQueueItemCount = this.debugInfo.instanceStack
40+
.filter(q => q)
41+
.reduce((total, item) => {
42+
Object.keys(item).forEach(queueName => {
43+
total += item[queueName].length;
44+
});
45+
46+
return total;
47+
}, 0);
48+
this._summaryInfo.pendingScheduledQueueItemStackTraces = this.debugInfo.instanceStack
49+
.filter(q => q)
50+
.reduce((stacks, deferredActionQueues) => {
51+
Object.keys(deferredActionQueues).forEach(queue => {
52+
deferredActionQueues[queue].forEach(
53+
queueItem => queueItem.stack && stacks.push(queueItem.stack)
54+
);
55+
});
56+
return stacks;
57+
}, []);
58+
}
59+
}
60+
61+
return this._summaryInfo;
62+
}
63+
}

addon-test-support/ember-qunit/test-isolation-validation.js

+20-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { run } from '@ember/runloop';
2-
import { isSettled } from '@ember/test-helpers';
2+
import { isSettled, getSettledState } from '@ember/test-helpers';
3+
import TestDebugInfo from './-internal/test-debug-info';
4+
import TestDebugInfoSummary from './-internal/test-debug-info-summary';
5+
import getDebugInfoAvailable from './-internal/get-debug-info-available';
36

4-
const TESTS_NOT_ISOLATED = [];
7+
const nonIsolatedTests = new TestDebugInfoSummary();
8+
const { backburner } = run;
59

610
/**
711
* Detects if a specific test isn't isolated. A test is considered
@@ -19,7 +23,16 @@ const TESTS_NOT_ISOLATED = [];
1923
*/
2024
export function detectIfTestNotIsolated({ module, name }) {
2125
if (!isSettled()) {
22-
TESTS_NOT_ISOLATED.push(`${module}: ${name}`);
26+
let testDebugInfo;
27+
let backburnerDebugInfo;
28+
29+
if (getDebugInfoAvailable()) {
30+
backburnerDebugInfo = backburner.getDebugInfo();
31+
}
32+
33+
testDebugInfo = new TestDebugInfo(module, name, getSettledState(), backburnerDebugInfo);
34+
35+
nonIsolatedTests.add(testDebugInfo);
2336
run.cancelTimers();
2437
}
2538
}
@@ -32,17 +45,10 @@ export function detectIfTestNotIsolated({ module, name }) {
3245
* @throws Error if tests are not isolated
3346
*/
3447
export function reportIfTestNotIsolated() {
35-
if (TESTS_NOT_ISOLATED.length > 0) {
36-
let leakyTests = TESTS_NOT_ISOLATED.slice();
37-
TESTS_NOT_ISOLATED.length = 0;
48+
if (nonIsolatedTests.hasDebugInfo) {
49+
nonIsolatedTests.printToConsole();
50+
nonIsolatedTests.reset();
3851

39-
throw new Error(getMessage(leakyTests.length, leakyTests.join('\n')));
52+
throw new Error(nonIsolatedTests.formatForBrowser());
4053
}
4154
}
42-
43-
export function getMessage(testCount, testsToReport) {
44-
return `TESTS ARE NOT ISOLATED
45-
The following (${testCount}) tests have one or more of pending timers, pending AJAX requests, pending test waiters, or are still in a runloop: \n
46-
${testsToReport}
47-
`;
48-
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"ember-disable-prototype-extensions": "^1.1.2",
4848
"ember-load-initializers": "^1.0.0",
4949
"ember-resolver": "^5.0.1",
50-
"ember-source": "~3.5.0",
50+
"ember-source": "3.4.5",
5151
"ember-source-channel-url": "^1.0.1",
5252
"ember-try": "^1.1.0",
5353
"eslint": "^5.7.0",

0 commit comments

Comments
 (0)