Skip to content

Commit f2cfd21

Browse files
Lightning00BladeDevtools-frontend LUCI CQ
authored andcommitted
[unittest] Fix TrackAsyncOperations loop
For some reason we ended up in a loop where the `original` was actually a stubbed instance, and in that case we blocked the Main tread as we when into an infinite loop. Instead of spending a day or two trying to fix the old code, I chose to rewrite it to keep track of original and stubs separately. Additionally every stub now needs to be explicitly added to the AsyncTrackedFunctions. Bug: none Change-Id: I04b1913d4c63be603b931957d00df8d349d0afe0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6925580 Reviewed-by: Ergün Erdoğmuş <[email protected]> Commit-Queue: Nikolay Vitkov <[email protected]>
1 parent a433003 commit f2cfd21

File tree

1 file changed

+63
-48
lines changed

1 file changed

+63
-48
lines changed

front_end/testing/TrackAsyncOperations.ts

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,48 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
type TrackedAsyncOperation = 'Promise'|'requestAnimationFrame'|'setTimeout'|'setInterval'|'requestIdleCallback'|
6+
'cancelIdleCallback'|'cancelAnimationFrame'|'clearTimeout'|'clearInterval';
7+
8+
type OriginalTrackedAsyncOperations = {
9+
[K in TrackedAsyncOperation]: typeof window[K];
10+
};
11+
/**
12+
* Capture the original at point in creation of the module
13+
* Unless something before this is loaded
14+
* This should always be the original
15+
*/
16+
const originals: Readonly<OriginalTrackedAsyncOperations> = {
17+
Promise,
18+
requestAnimationFrame: requestAnimationFrame.bind(window),
19+
requestIdleCallback: requestIdleCallback.bind(window),
20+
setInterval: setInterval.bind(window),
21+
setTimeout: setTimeout.bind(window),
22+
cancelAnimationFrame: cancelAnimationFrame.bind(window),
23+
clearInterval: clearInterval.bind(window),
24+
clearTimeout: clearTimeout.bind(window),
25+
cancelIdleCallback: cancelIdleCallback.bind(window)
26+
};
27+
28+
// We can't use Sinon for stubbing as 1) we need to double wrap sometimes
29+
interface Stub<TKey extends TrackedAsyncOperation> {
30+
name: TKey;
31+
stubWith: (typeof window)[TKey];
32+
}
33+
const stubs: Array<Stub<TrackedAsyncOperation>> = [];
34+
function stub<T extends TrackedAsyncOperation>(name: T, stubWith: (typeof window)[T]) {
35+
window[name] = stubWith;
36+
stubs.push({name, stubWith});
37+
}
38+
39+
function restoreAll() {
40+
for (const {name} of stubs) {
41+
(window[name] as unknown) = originals[name];
42+
}
43+
stubs.length = 0;
44+
}
545
interface AsyncActivity {
6-
type: 'promise'|'requestAnimationFrame'|'setTimeout'|'setInterval'|'requestIdleCallback';
46+
type: TrackedAsyncOperation;
747
pending: boolean;
848
cancelDelayed?: () => void;
949
id?: string;
@@ -41,13 +81,15 @@ export async function checkForPendingActivity(testName = '') {
4181
const pendingCount = asyncActivity.filter(a => a.pending).length;
4282
const totalCount = asyncActivity.length;
4383
try {
84+
const PromiseConstructor = originals.Promise;
4485
// First we wait for the pending async activity to finish normally
45-
await original(Promise).all(asyncActivity.filter(a => a.pending).map(a => original(Promise).race([
86+
await PromiseConstructor.all(asyncActivity.filter(a => a.pending).map(a => PromiseConstructor.race([
4687
a.promise,
47-
new (original(Promise))(
48-
(_, reject) => original(setTimeout)(
88+
new PromiseConstructor<void>(
89+
(resolve, reject) => originals.setTimeout(
4990
() => {
5091
if (!a.pending) {
92+
resolve();
5193
return;
5294
}
5395
// If something is still pending after some time, we try to
@@ -96,16 +138,16 @@ export function stopTrackingAsyncActivity() {
96138
function trackingRequestAnimationFrame(fn: FrameRequestCallback) {
97139
const activity: AsyncActivity = {type: 'requestAnimationFrame', pending: true, stack: getStack(new Error())};
98140
let id = 0;
99-
activity.promise = new (original(Promise<void>))(resolve => {
141+
activity.promise = new originals.Promise<void>(resolve => {
100142
activity.runImmediate = () => {
101143
fn(performance.now());
102144
activity.pending = false;
103145
resolve();
104146
};
105-
id = original(requestAnimationFrame)(activity.runImmediate);
147+
id = originals.requestAnimationFrame(activity.runImmediate);
106148
activity.id = 'a' + id;
107149
activity.cancelDelayed = () => {
108-
original(cancelAnimationFrame)(id);
150+
originals.cancelAnimationFrame(id);
109151
activity.pending = false;
110152
resolve();
111153
};
@@ -117,16 +159,16 @@ function trackingRequestAnimationFrame(fn: FrameRequestCallback) {
117159
function trackingRequestIdleCallback(fn: IdleRequestCallback, opts?: IdleRequestOptions): number {
118160
const activity: AsyncActivity = {type: 'requestIdleCallback', pending: true, stack: getStack(new Error())};
119161
let id = 0;
120-
activity.promise = new (original(Promise<void>))(resolve => {
162+
activity.promise = new originals.Promise<void>(resolve => {
121163
activity.runImmediate = (idleDeadline?: IdleDeadline) => {
122164
fn(idleDeadline ?? {didTimeout: true, timeRemaining: () => 0} as IdleDeadline);
123165
activity.pending = false;
124166
resolve();
125167
};
126-
id = original(requestIdleCallback)(activity.runImmediate, opts);
168+
id = originals.requestIdleCallback(activity.runImmediate, opts);
127169
activity.id = 'd' + id;
128170
activity.cancelDelayed = () => {
129-
original(cancelIdleCallback)(id);
171+
originals.cancelIdleCallback(id);
130172
activity.pending = false;
131173
resolve();
132174
};
@@ -137,9 +179,10 @@ function trackingRequestIdleCallback(fn: IdleRequestCallback, opts?: IdleRequest
137179

138180
function trackingSetTimeout(arg: TimerHandler, time?: number, ...params: unknown[]) {
139181
const activity: AsyncActivity = {type: 'setTimeout', pending: true, stack: getStack(new Error())};
140-
let id: ReturnType<typeof setTimeout>|undefined;
141-
activity.promise = new (original(Promise<void>))(resolve => {
182+
let id: number|undefined;
183+
activity.promise = new originals.Promise<void>(resolve => {
142184
activity.runImmediate = () => {
185+
originals.clearTimeout(id);
143186
if (typeof (arg) === 'function') {
144187
arg(...params);
145188
} else {
@@ -148,10 +191,10 @@ function trackingSetTimeout(arg: TimerHandler, time?: number, ...params: unknown
148191
activity.pending = false;
149192
resolve();
150193
};
151-
id = original(setTimeout)(activity.runImmediate, time);
194+
id = originals.setTimeout(activity.runImmediate, time);
152195
activity.id = 't' + id;
153196
activity.cancelDelayed = () => {
154-
original(clearTimeout)(id);
197+
originals.clearTimeout(id);
155198
activity.pending = false;
156199
resolve();
157200
};
@@ -167,11 +210,11 @@ function trackingSetInterval(arg: TimerHandler, time?: number, ...params: unknow
167210
stack: getStack(new Error()),
168211
};
169212
let id = 0;
170-
activity.promise = new (original(Promise<void>))(resolve => {
171-
id = original(setInterval)(arg, time, ...params);
213+
activity.promise = new originals.Promise<void>(resolve => {
214+
id = originals.setInterval(arg, time, ...params);
172215
activity.id = 'i' + id;
173216
activity.cancelDelayed = () => {
174-
original(clearInterval)(id);
217+
originals.clearInterval(id);
175218
activity.pending = false;
176219
resolve();
177220
};
@@ -210,10 +253,9 @@ const BasePromise: Omit<PromiseConstructor, UntrackedPromiseMethod> = {
210253
// which never settles.
211254
const TrackingPromise: PromiseConstructor = Object.assign(
212255
function<T>(arg: (resolve: (value: T|PromiseLike<T>) => void, reject: (reason?: unknown) => void) => void) {
213-
const originalPromiseType = original(Promise);
214-
const promise = new (originalPromiseType)(arg);
256+
const promise = new originals.Promise(arg);
215257
const activity: AsyncActivity = {
216-
type: 'promise',
258+
type: 'Promise',
217259
promise,
218260
stack: getStack(new Error()),
219261
pending: false,
@@ -223,7 +265,7 @@ const TrackingPromise: PromiseConstructor = Object.assign(
223265
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>)|undefined|
224266
null): Promise<TResult1|TResult2> {
225267
activity.pending = true;
226-
return originalPromiseType.prototype.then.apply(this, [
268+
return originals.Promise.prototype.then.apply(this, [
227269
result => {
228270
if (!onFulfilled) {
229271
return this;
@@ -250,30 +292,3 @@ const TrackingPromise: PromiseConstructor = Object.assign(
250292
function getStack(error: Error): string {
251293
return (error.stack ?? 'No stack').split('\n').slice(2).join('\n');
252294
}
253-
254-
// We can't use Sinon for stubbing as 1) we need to double wrap sometimes and 2)
255-
// we need to access original values.
256-
interface Stub<TKey extends keyof typeof window> {
257-
name: TKey;
258-
original: (typeof window)[TKey];
259-
stubWith: (typeof window)[TKey];
260-
}
261-
262-
const stubs: Array<Stub<keyof typeof window>> = [];
263-
264-
function stub<T extends keyof typeof window>(name: T, stubWith: (typeof window)[T]) {
265-
const original = window[name];
266-
window[name] = stubWith;
267-
stubs.push({name, original, stubWith});
268-
}
269-
270-
function original<T>(stubWith: T): T {
271-
return stubs.find(s => s.stubWith === stubWith)?.original;
272-
}
273-
274-
function restoreAll() {
275-
for (const {name, original} of stubs) {
276-
(window[name] as typeof original) = original;
277-
}
278-
stubs.length = 0;
279-
}

0 commit comments

Comments
 (0)