test_promises_run_to_completion.js (4438B)
1 // Bug 1145201: Promise then-handlers can still be executed while the debugger is paused. 2 // 3 // When a promise is resolved, for each of its callbacks, a microtask is queued 4 // to run the callback. At various points, the HTML spec says the browser must 5 // "perform a microtask checkpoint", which means to draw microtasks from the 6 // queue and run them, until the queue is empty. 7 // 8 // The HTML spec is careful to perform a microtask checkpoint directly after 9 // each invocation of an event handler or DOM callback, so that code using 10 // promises can trust that its promise callbacks run promptly, in a 11 // deterministic order, without DOM events or other outside influences 12 // intervening. 13 // 14 // When the JavaScript debugger interrupts the execution of debuggee content 15 // code, it naturally must process events for its own user interface and promise 16 // callbacks. However, it must not run any debuggee microtasks. The debuggee has 17 // been interrupted in the midst of executing some other code, and the 18 // JavaScript spec promises developers: "Once execution of a Job is initiated, 19 // the Job always executes to completion. No other Job may be initiated until 20 // the currently running Job completes." [1] This promise would be broken if the 21 // debugger's own event processing ran debuggee microtasks during the 22 // interruption. 23 // 24 // Looking at things from the other side, a microtask checkpoint must be 25 // performed before returning from a debugger callback, rather than being put 26 // off until the debuggee performs its next microtask checkpoint, so that 27 // debugger microtasks are not interleaved with debuggee microtasks. A debuggee 28 // microtask could hit a breakpoint or otherwise re-enter the debugger, which 29 // might be quite surprised to see a new debugger callback begin before its 30 // previous promise callbacks could finish. 31 // 32 // [1]: https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues 33 34 "use strict"; 35 36 const Debugger = require("Debugger"); 37 38 function test_promises_run_to_completion() { 39 const g = createTestGlobal( 40 "test global for test_promises_run_to_completion.js" 41 ); 42 const dbg = new Debugger(g); 43 g.Assert = Assert; 44 const log = [""]; 45 g.log = log; 46 47 dbg.onDebuggerStatement = function handleDebuggerStatement() { 48 dbg.onDebuggerStatement = undefined; 49 50 // Exercise the promise machinery: resolve a promise and perform a microtask 51 // queue. When called from a debugger hook, the debuggee's microtasks should not 52 // run. 53 log[0] += "debug-handler("; 54 Promise.resolve(42).then(v => { 55 Assert.equal( 56 v, 57 42, 58 "debugger callback promise handler got the right value" 59 ); 60 log[0] += "debug-react"; 61 }); 62 log[0] += "("; 63 force_microtask_checkpoint(); 64 log[0] += ")"; 65 66 Promise.resolve(42).then(() => { 67 // The microtask running this callback should be handled as we leave the 68 // onDebuggerStatement Debugger callback, and should not be interleaved 69 // with debuggee microtasks. 70 log[0] += "(trailing)"; 71 }); 72 73 log[0] += ")"; 74 }; 75 76 // Evaluate some debuggee code that resolves a promise, and then enters the debugger. 77 Cu.evalInSandbox( 78 ` 79 log[0] += "eval("; 80 Promise.resolve(42).then(function debuggeePromiseCallback(v) { 81 Assert.equal(v, 42, "debuggee promise handler got the right value"); 82 // Debugger microtask checkpoints must not run debuggee microtasks, so 83 // this callback should run at the next microtask checkpoint *not* 84 // performed by the debugger. 85 log[0] += "eval-react"; 86 }); 87 88 log[0] += "debugger("; 89 debugger; 90 log[0] += "))"; 91 `, 92 g 93 ); 94 95 // Let other microtasks run. This should run the debuggee's promise callback. 96 log[0] += "final("; 97 force_microtask_checkpoint(); 98 log[0] += ")"; 99 100 Assert.equal( 101 log[0], 102 `\ 103 eval(\ 104 debugger(\ 105 debug-handler(\ 106 (debug-react)\ 107 )\ 108 (trailing)\ 109 ))\ 110 final(\ 111 eval-react\ 112 )`, 113 "microtasks ran as expected" 114 ); 115 116 run_next_test(); 117 } 118 119 function force_microtask_checkpoint() { 120 // Services.tm.spinEventLoopUntilEmpty only performs a microtask checkpoint if 121 // there is actually an event to run. So make one up. 122 let ran = false; 123 Services.tm.dispatchToMainThread(() => { 124 ran = true; 125 }); 126 Services.tm.spinEventLoopUntil( 127 "Test(test_promises_run_to_completion.js:force_microtask_checkpoint)", 128 () => ran 129 ); 130 } 131 132 add_test(test_promises_run_to_completion);