test_monitor_uncaught.js (9203B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { setTimeout } = ChromeUtils.importESModule( 8 "resource://gre/modules/Timer.sys.mjs" 9 ); 10 const { PromiseTestUtils } = ChromeUtils.importESModule( 11 "resource://testing-common/PromiseTestUtils.sys.mjs" 12 ); 13 14 // Prevent test failures due to the unhandled rejections in this test file. 15 PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest(); 16 17 add_task(async function test_globals() { 18 Assert.notEqual( 19 PromiseDebugging, 20 undefined, 21 "PromiseDebugging is available." 22 ); 23 }); 24 25 add_task(async function test_promiseID() { 26 let p1 = new Promise(() => {}); 27 let p2 = new Promise(() => {}); 28 let p3 = p2.catch(null); 29 let promise = [p1, p2, p3]; 30 31 let identifiers = promise.map(PromiseDebugging.getPromiseID); 32 info("Identifiers: " + JSON.stringify(identifiers)); 33 let idSet = new Set(identifiers); 34 Assert.equal( 35 idSet.size, 36 identifiers.length, 37 "PromiseDebugging.getPromiseID returns a distinct id per promise" 38 ); 39 40 let identifiers2 = promise.map(PromiseDebugging.getPromiseID); 41 Assert.equal( 42 JSON.stringify(identifiers), 43 JSON.stringify(identifiers2), 44 "Successive calls to PromiseDebugging.getPromiseID return the same id for the same promise" 45 ); 46 }); 47 48 add_task(async function test_observe_uncaught() { 49 // The names of Promise instances 50 let names = new Map(); 51 52 // The results for UncaughtPromiseObserver callbacks. 53 let CallbackResults = function (name) { 54 this.name = name; 55 this.expected = new Set(); 56 this.observed = new Set(); 57 this.blocker = new Promise(resolve => (this.resolve = resolve)); 58 }; 59 CallbackResults.prototype = { 60 observe(promise) { 61 info(this.name + " observing Promise " + names.get(promise)); 62 Assert.equal( 63 PromiseDebugging.getState(promise).state, 64 "rejected", 65 this.name + " observed a rejected Promise" 66 ); 67 if (!this.expected.has(promise)) { 68 Assert.ok( 69 false, 70 this.name + 71 " observed a Promise that it expected to observe, " + 72 names.get(promise) + 73 " (" + 74 PromiseDebugging.getPromiseID(promise) + 75 ", " + 76 PromiseDebugging.getAllocationStack(promise) + 77 ")" 78 ); 79 } 80 Assert.ok( 81 this.expected.delete(promise), 82 this.name + 83 " observed a Promise that it expected to observe, " + 84 names.get(promise) + 85 " (" + 86 PromiseDebugging.getPromiseID(promise) + 87 ")" 88 ); 89 Assert.ok( 90 !this.observed.has(promise), 91 this.name + " observed a Promise that it has not observed yet" 92 ); 93 this.observed.add(promise); 94 if (this.expected.size == 0) { 95 this.resolve(); 96 } else { 97 info( 98 this.name + 99 " is still waiting for " + 100 this.expected.size + 101 " observations:" 102 ); 103 info( 104 JSON.stringify(Array.from(this.expected.values(), x => names.get(x))) 105 ); 106 } 107 }, 108 }; 109 110 let onLeftUncaught = new CallbackResults("onLeftUncaught"); 111 let onConsumed = new CallbackResults("onConsumed"); 112 113 let observer = { 114 onLeftUncaught(promise) { 115 onLeftUncaught.observe(promise); 116 }, 117 onConsumed(promise) { 118 onConsumed.observe(promise); 119 }, 120 }; 121 122 let resolveLater = function (delay = 20) { 123 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 124 return new Promise(resolve => setTimeout(resolve, delay)); 125 }; 126 let rejectLater = function (delay = 20) { 127 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 128 return new Promise((resolve, reject) => setTimeout(reject, delay)); 129 }; 130 let makeSamples = function* () { 131 yield { 132 promise: Promise.resolve(0), 133 name: "Promise.resolve", 134 }; 135 yield { 136 promise: Promise.resolve(resolve => resolve(0)), 137 name: "Resolution callback", 138 }; 139 yield { 140 promise: Promise.resolve(0).catch(null), 141 name: "`catch(null)`", 142 }; 143 yield { 144 promise: Promise.reject(0).catch(() => {}), 145 name: "Reject and catch immediately", 146 }; 147 yield { 148 promise: resolveLater(), 149 name: "Resolve later", 150 }; 151 yield { 152 promise: Promise.reject("Simple rejection"), 153 leftUncaught: true, 154 consumed: false, 155 name: "Promise.reject", 156 }; 157 158 // Reject a promise now, consume it later. 159 let p = Promise.reject("Reject now, consume later"); 160 161 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 162 setTimeout( 163 () => 164 p.catch(() => { 165 info("Consumed promise"); 166 }), 167 200 168 ); 169 yield { 170 promise: p, 171 leftUncaught: true, 172 consumed: true, 173 name: "Reject now, consume later", 174 }; 175 176 yield { 177 promise: Promise.all([Promise.resolve("Promise.all"), rejectLater()]), 178 leftUncaught: true, 179 name: "Rejecting through Promise.all", 180 }; 181 yield { 182 promise: Promise.race([resolveLater(500), Promise.reject()]), 183 leftUncaught: true, // The rejection wins the race. 184 name: "Rejecting through Promise.race", 185 }; 186 yield { 187 promise: Promise.race([Promise.resolve(), rejectLater(500)]), 188 leftUncaught: false, // The resolution wins the race. 189 name: "Resolving through Promise.race", 190 }; 191 192 let boom = new Error("`throw` in the constructor"); 193 yield { 194 promise: new Promise(() => { 195 throw boom; 196 }), 197 leftUncaught: true, 198 name: "Throwing in the constructor", 199 }; 200 201 let rejection = Promise.reject("`reject` during resolution"); 202 yield { 203 promise: rejection, 204 leftUncaught: false, 205 consumed: false, // `rejection` is consumed immediately (see below) 206 name: "Promise.reject, again", 207 }; 208 209 yield { 210 promise: new Promise(resolve => resolve(rejection)), 211 leftUncaught: true, 212 consumed: false, 213 name: "Resolving with a rejected promise", 214 }; 215 216 yield { 217 promise: Promise.resolve(0).then(() => rejection), 218 leftUncaught: true, 219 consumed: false, 220 name: "Returning a rejected promise from success handler", 221 }; 222 223 yield { 224 promise: Promise.resolve(0).then(() => { 225 throw new Error(); 226 }), 227 leftUncaught: true, 228 consumed: false, 229 name: "Throwing during the call to the success callback", 230 }; 231 }; 232 let samples = []; 233 for (let s of makeSamples()) { 234 samples.push(s); 235 info( 236 "Promise '" + 237 s.name + 238 "' has id " + 239 PromiseDebugging.getPromiseID(s.promise) 240 ); 241 } 242 243 PromiseDebugging.addUncaughtRejectionObserver(observer); 244 245 for (let s of samples) { 246 names.set(s.promise, s.name); 247 if (s.leftUncaught || false) { 248 onLeftUncaught.expected.add(s.promise); 249 } 250 if (s.consumed || false) { 251 onConsumed.expected.add(s.promise); 252 } 253 } 254 255 info("Test setup, waiting for callbacks."); 256 await onLeftUncaught.blocker; 257 258 info("All calls to onLeftUncaught are complete."); 259 if (onConsumed.expected.size != 0) { 260 info("onConsumed is still waiting for the following Promise:"); 261 info( 262 JSON.stringify( 263 Array.from(onConsumed.expected.values(), x => names.get(x)) 264 ) 265 ); 266 await onConsumed.blocker; 267 } 268 269 info("All calls to onConsumed are complete."); 270 let removed = PromiseDebugging.removeUncaughtRejectionObserver(observer); 271 Assert.ok(removed, "removeUncaughtRejectionObserver succeeded"); 272 removed = PromiseDebugging.removeUncaughtRejectionObserver(observer); 273 Assert.ok( 274 !removed, 275 "second call to removeUncaughtRejectionObserver didn't remove anything" 276 ); 277 }); 278 279 add_task(async function test_uninstall_observer() { 280 let Observer = function () { 281 this.blocker = new Promise(resolve => (this.resolve = resolve)); 282 this.active = true; 283 }; 284 Observer.prototype = { 285 set active(x) { 286 this._active = x; 287 if (x) { 288 PromiseDebugging.addUncaughtRejectionObserver(this); 289 } else { 290 PromiseDebugging.removeUncaughtRejectionObserver(this); 291 } 292 }, 293 onLeftUncaught() { 294 Assert.ok(this._active, "This observer is active."); 295 this.resolve(); 296 }, 297 onConsumed() { 298 Assert.ok(false, "We should not consume any Promise."); 299 }, 300 }; 301 302 info("Adding an observer."); 303 let deactivate = new Observer(); 304 Promise.reject("I am an uncaught rejection."); 305 await deactivate.blocker; 306 Assert.ok(true, "The observer has observed an uncaught Promise."); 307 deactivate.active = false; 308 info( 309 "Removing the observer, it should not observe any further uncaught Promise." 310 ); 311 312 info( 313 "Rejecting a Promise and waiting a little to give a chance to observers." 314 ); 315 let wait = new Observer(); 316 Promise.reject("I am another uncaught rejection."); 317 await wait.blocker; 318 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 319 await new Promise(resolve => setTimeout(resolve, 100)); 320 // Normally, `deactivate` should not be notified of the uncaught rejection. 321 wait.active = false; 322 });