debugger-reject-after-fulfill.js (5487B)
1 // Throwing error after resolving async function's promise should not 2 // overwrite the promise's state or value/reason. 3 // This situation can happen either with debugger interaction or OOM. 4 5 // This testcase relies on the fact that there's a breakpoint after resolving 6 // the async function's promise, before leaving the async function's frame. 7 // This function searches for the last breakpoint before leaving the frame. 8 // 9 // - `declCode` should declare an async function `f`, and the function should 10 // set global variable `returning` to `true` just before return 11 // - `callCode` should call the function `f` and make sure the function's 12 // execution reaches the last breakpoint 13 function searchLastBreakpointBeforeReturn(declCode, callCode) { 14 const g = newGlobal({ newCompartment: true }); 15 const dbg = new Debugger(g); 16 g.eval(declCode); 17 18 let hit = false; 19 let offset = 0; 20 dbg.onEnterFrame = function(frame) { 21 if (frame.callee && frame.callee.name == "f") { 22 frame.onStep = () => { 23 if (!g.returning) { 24 return undefined; 25 } 26 27 offset = frame.offset; 28 return undefined; 29 }; 30 } 31 }; 32 33 g.eval(callCode); 34 35 drainJobQueue(); 36 37 assertEq(offset != 0, true); 38 39 return offset; 40 } 41 42 function testWithoutAwait() { 43 const declCode = ` 44 var returning = false; 45 async function f() { 46 return (returning = true, "expected"); 47 }; 48 `; 49 50 const callCode = ` 51 var p = f(); 52 `; 53 54 const offset = searchLastBreakpointBeforeReturn(declCode, callCode); 55 56 const g = newGlobal({ newCompartment: true }); 57 const dbg = new Debugger(g); 58 g.eval(declCode); 59 60 let onPromiseSettledCount = 0; 61 dbg.onPromiseSettled = function(promise) { 62 onPromiseSettledCount++; 63 // No promise should be rejected. 64 assertEq(promise.promiseState, "fulfilled"); 65 66 // Async function's promise should have expected value. 67 if (onPromiseSettledCount == 1) { 68 assertEq(promise.promiseValue, "expected"); 69 } 70 }; 71 72 let hitBreakpoint = false; 73 dbg.onEnterFrame = function(frame) { 74 if (frame.callee && frame.callee.name == "f") { 75 dbg.onEnterFrame = undefined; 76 frame.script.setBreakpoint(offset, { 77 hit() { 78 hitBreakpoint = true; 79 return { throw: "unexpected" }; 80 } 81 }); 82 } 83 }; 84 85 enableLastWarning(); 86 87 g.eval(` 88 var fulfilledValue; 89 var rejected = false; 90 `); 91 92 g.eval(callCode); 93 94 // The execution reaches to the last breakpoint without running job queue. 95 assertEq(hitBreakpoint, true); 96 97 const warn = getLastWarning(); 98 assertEq(warn.message, 99 "unhandlable error after resolving async function's promise"); 100 clearLastWarning(); 101 102 // Add reaction handler after resolution. 103 // This handler's job will be enqueued immediately. 104 g.eval(` 105 p.then(x => { 106 fulfilledValue = x; 107 }, e => { 108 rejected = true; 109 }); 110 `); 111 112 // Run the above handler. 113 drainJobQueue(); 114 115 assertEq(g.fulfilledValue, "expected"); 116 assertEq(onPromiseSettledCount >= 1, true); 117 } 118 119 function testWithAwait() { 120 const declCode = ` 121 var resolve; 122 var p = new Promise(r => { resolve = r }); 123 var returning = false; 124 async function f() { 125 await p; 126 return (returning = true, "expected"); 127 }; 128 `; 129 130 const callCode = ` 131 var p = f(); 132 `; 133 134 const resolveCode = ` 135 resolve("resolve"); 136 `; 137 138 const offset = searchLastBreakpointBeforeReturn(declCode, 139 callCode + resolveCode); 140 141 const g = newGlobal({newCompartment: true}); 142 const dbg = new Debugger(g); 143 g.eval(declCode); 144 145 let onPromiseSettledCount = 0; 146 dbg.onPromiseSettled = function(promise) { 147 onPromiseSettledCount++; 148 149 // No promise should be rejected. 150 assertEq(promise.promiseState, "fulfilled"); 151 152 // Async function's promise should have expected value. 153 if (onPromiseSettledCount == 2) { 154 assertEq(promise.promiseValue, "expected"); 155 } 156 }; 157 158 let hitBreakpoint = false; 159 dbg.onEnterFrame = function(frame) { 160 if (frame.callee && frame.callee.name == "f") { 161 dbg.onEnterFrame = undefined; 162 frame.script.setBreakpoint(offset, { 163 hit() { 164 hitBreakpoint = true; 165 return { throw: "unexpected" }; 166 } 167 }); 168 } 169 }; 170 171 enableLastWarning(); 172 173 g.eval(` 174 var fulfilledValue1; 175 var fulfilledValue2; 176 var rejected = false; 177 `); 178 179 g.eval(callCode); 180 181 assertEq(getLastWarning(), null); 182 183 // Add reaction handler before resolution. 184 // This handler's job will be enqueued when `p` is resolved. 185 g.eval(` 186 p.then(x => { 187 fulfilledValue1 = x; 188 }, e => { 189 rejected = true; 190 }); 191 `); 192 193 g.eval(resolveCode); 194 195 // Run the remaining part of async function, and the above handler. 196 drainJobQueue(); 197 198 // The execution reaches to the last breakpoint after running job queue for 199 // resolving `p`. 200 assertEq(hitBreakpoint, true); 201 202 const warn = getLastWarning(); 203 assertEq(warn.message, 204 "unhandlable error after resolving async function's promise"); 205 clearLastWarning(); 206 207 assertEq(g.fulfilledValue1, "expected"); 208 assertEq(g.rejected, false); 209 210 // Add reaction handler after resolution. 211 // This handler's job will be enqueued immediately. 212 g.eval(` 213 p.then(x => { 214 fulfilledValue2 = x; 215 }, e => { 216 rejected = true; 217 }); 218 `); 219 220 // Run the above handler. 221 drainJobQueue(); 222 223 assertEq(g.fulfilledValue2, "expected"); 224 assertEq(g.rejected, false); 225 assertEq(onPromiseSettledCount >= 3, true); 226 } 227 228 testWithoutAwait(); 229 testWithAwait();