summaryrefslogtreecommitdiffstats
path: root/devtools/server/tests/xpcshell/test_promises_run_to_completion.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/tests/xpcshell/test_promises_run_to_completion.js')
-rw-r--r--devtools/server/tests/xpcshell/test_promises_run_to_completion.js132
1 files changed, 132 insertions, 0 deletions
diff --git a/devtools/server/tests/xpcshell/test_promises_run_to_completion.js b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js
new file mode 100644
index 0000000000..4d1e8745fe
--- /dev/null
+++ b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js
@@ -0,0 +1,132 @@
+// Bug 1145201: Promise then-handlers can still be executed while the debugger is paused.
+//
+// When a promise is resolved, for each of its callbacks, a microtask is queued
+// to run the callback. At various points, the HTML spec says the browser must
+// "perform a microtask checkpoint", which means to draw microtasks from the
+// queue and run them, until the queue is empty.
+//
+// The HTML spec is careful to perform a microtask checkpoint directly after
+// each invocation of an event handler or DOM callback, so that code using
+// promises can trust that its promise callbacks run promptly, in a
+// deterministic order, without DOM events or other outside influences
+// intervening.
+//
+// When the JavaScript debugger interrupts the execution of debuggee content
+// code, it naturally must process events for its own user interface and promise
+// callbacks. However, it must not run any debuggee microtasks. The debuggee has
+// been interrupted in the midst of executing some other code, and the
+// JavaScript spec promises developers: "Once execution of a Job is initiated,
+// the Job always executes to completion. No other Job may be initiated until
+// the currently running Job completes." [1] This promise would be broken if the
+// debugger's own event processing ran debuggee microtasks during the
+// interruption.
+//
+// Looking at things from the other side, a microtask checkpoint must be
+// performed before returning from a debugger callback, rather than being put
+// off until the debuggee performs its next microtask checkpoint, so that
+// debugger microtasks are not interleaved with debuggee microtasks. A debuggee
+// microtask could hit a breakpoint or otherwise re-enter the debugger, which
+// might be quite surprised to see a new debugger callback begin before its
+// previous promise callbacks could finish.
+//
+// [1]: https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues
+
+"use strict";
+
+const Debugger = require("Debugger");
+
+function test_promises_run_to_completion() {
+ const g = createTestGlobal(
+ "test global for test_promises_run_to_completion.js"
+ );
+ const dbg = new Debugger(g);
+ g.Assert = Assert;
+ const log = [""];
+ g.log = log;
+
+ dbg.onDebuggerStatement = function handleDebuggerStatement(frame) {
+ dbg.onDebuggerStatement = undefined;
+
+ // Exercise the promise machinery: resolve a promise and perform a microtask
+ // queue. When called from a debugger hook, the debuggee's microtasks should not
+ // run.
+ log[0] += "debug-handler(";
+ Promise.resolve(42).then(v => {
+ Assert.equal(
+ v,
+ 42,
+ "debugger callback promise handler got the right value"
+ );
+ log[0] += "debug-react";
+ });
+ log[0] += "(";
+ force_microtask_checkpoint();
+ log[0] += ")";
+
+ Promise.resolve(42).then(v => {
+ // The microtask running this callback should be handled as we leave the
+ // onDebuggerStatement Debugger callback, and should not be interleaved
+ // with debuggee microtasks.
+ log[0] += "(trailing)";
+ });
+
+ log[0] += ")";
+ };
+
+ // Evaluate some debuggee code that resolves a promise, and then enters the debugger.
+ Cu.evalInSandbox(
+ `
+ log[0] += "eval(";
+ Promise.resolve(42).then(function debuggeePromiseCallback(v) {
+ Assert.equal(v, 42, "debuggee promise handler got the right value");
+ // Debugger microtask checkpoints must not run debuggee microtasks, so
+ // this callback should run at the next microtask checkpoint *not*
+ // performed by the debugger.
+ log[0] += "eval-react";
+ });
+
+ log[0] += "debugger(";
+ debugger;
+ log[0] += "))";
+ `,
+ g
+ );
+
+ // Let other microtasks run. This should run the debuggee's promise callback.
+ log[0] += "final(";
+ force_microtask_checkpoint();
+ log[0] += ")";
+
+ Assert.equal(
+ log[0],
+ `\
+eval(\
+debugger(\
+debug-handler(\
+(debug-react)\
+)\
+(trailing)\
+))\
+final(\
+eval-react\
+)`,
+ "microtasks ran as expected"
+ );
+
+ run_next_test();
+}
+
+function force_microtask_checkpoint() {
+ // Services.tm.spinEventLoopUntilEmpty only performs a microtask checkpoint if
+ // there is actually an event to run. So make one up.
+ let ran = false;
+ Services.tm.dispatchToMainThread(() => {
+ ran = true;
+ });
+ Services.tm.spinEventLoopUntil(
+ "Test(test_promises_run_to_completion.js:force_microtask_checkpoint)",
+ () => ran
+ );
+}
+
+add_test(test_promises_run_to_completion);