diff options
Diffstat (limited to 'js/src/jit-test/tests/debug')
1029 files changed, 31966 insertions, 0 deletions
diff --git a/js/src/jit-test/tests/debug/DebuggeeWouldRun-01.js b/js/src/jit-test/tests/debug/DebuggeeWouldRun-01.js new file mode 100644 index 0000000000..315e3dcf4f --- /dev/null +++ b/js/src/jit-test/tests/debug/DebuggeeWouldRun-01.js @@ -0,0 +1,7 @@ +// Bug 1250190: Shouldn't crash. |jit-test| exitstatus: 3 + +g = newGlobal(); +var dbg = Debugger(g) +dbg.onNewPromise = () => g.makeFakePromise(); +g.makeFakePromise(); + diff --git a/js/src/jit-test/tests/debug/DebuggeeWouldRun-02.js b/js/src/jit-test/tests/debug/DebuggeeWouldRun-02.js new file mode 100644 index 0000000000..290effcb98 --- /dev/null +++ b/js/src/jit-test/tests/debug/DebuggeeWouldRun-02.js @@ -0,0 +1,7 @@ +// Bug 1250190: Shouldn't crash. |jit-test| exitstatus: 3 + +var g = newGlobal(); +var dbg = Debugger(g) +dbg.onNewGlobalObject = () => g.newGlobal(); +g.newGlobal(); +print("yo"); diff --git a/js/src/jit-test/tests/debug/DebuggeeWouldRun-03.js b/js/src/jit-test/tests/debug/DebuggeeWouldRun-03.js new file mode 100644 index 0000000000..e6c522c485 --- /dev/null +++ b/js/src/jit-test/tests/debug/DebuggeeWouldRun-03.js @@ -0,0 +1,9 @@ +// Bug 1250190: Shouldn't crash. |jit-test| error: yadda + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onNewGlobalObject = function () { + dbg.onNewGlobalObject = function () { throw "yadda"; }; + newGlobal({newCompartment: true}); +} +newGlobal({newCompartment: true}); diff --git a/js/src/jit-test/tests/debug/DebuggeeWouldRun-04.js b/js/src/jit-test/tests/debug/DebuggeeWouldRun-04.js new file mode 100644 index 0000000000..684f3f8e7e --- /dev/null +++ b/js/src/jit-test/tests/debug/DebuggeeWouldRun-04.js @@ -0,0 +1,9 @@ +// Bug 1250190: Shouldn't crash. |jit-test| error: yadda + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onNewScript = function () { + dbg.onNewScript = function () { throw "yadda"; }; + g.Function("noodles;"); +} +g.Function("poodles;"); diff --git a/js/src/jit-test/tests/debug/Debugger-add-Debugger-prototype.js b/js/src/jit-test/tests/debug/Debugger-add-Debugger-prototype.js new file mode 100644 index 0000000000..4af5eee474 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-add-Debugger-prototype.js @@ -0,0 +1,6 @@ +load(libdir + "asserts.js"); + +assertThrowsInstanceOf(function () { + var dbg = new Debugger(); + dbg.addDebuggee(Debugger.Object.prototype); +}, TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-adoptDebuggeeValue.js b/js/src/jit-test/tests/debug/Debugger-adoptDebuggeeValue.js new file mode 100644 index 0000000000..4d10062acb --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-adoptDebuggeeValue.js @@ -0,0 +1,39 @@ +// simplest possible test of Debugger.adoptDebuggeeValue + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); + +var dbg1 = new Debugger(); +var gDO1 = dbg1.addDebuggee(g); +var obj1 = gDO1.executeInGlobal("({})").return; + +var dbg2 = Debugger(g); +var gDO2 = dbg2.addDebuggee(g); +var obj2 = gDO2.executeInGlobal("({})").return; + +assertThrowsInstanceOf(function () { + obj1.defineProperty("foo", { + configurable: true, + enumerable: true, + value: obj2, + writable: true + }); +}, Error); + +let obj3 = dbg1.adoptDebuggeeValue(obj2); + +obj1.defineProperty("foo", { + configurable: true, + enumerable: true, + value: obj3, + writable: true +}); + +assertThrowsInstanceOf(function () { + dbg1.adoptDebuggeeValue({}); +}, TypeError); + +assertThrowsInstanceOf(function () { + dbg1.adoptDebuggeeValue(Debugger.Object.prototype); +}, TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-adoptFrame.js b/js/src/jit-test/tests/debug/Debugger-adoptFrame.js new file mode 100644 index 0000000000..f5a911fe6c --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-adoptFrame.js @@ -0,0 +1,81 @@ +// validate the common behavior of of Debugger.adoptFrame + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); + +const dbg1 = new Debugger(); +const gDO1 = dbg1.addDebuggee(g); + +let suspendedFrame; +dbg1.onDebuggerStatement = function() { + // Test working with an onStack frame. + const frame1 = dbg1.getNewestFrame(); + + const dbg = new Debugger(); + assertErrorMessage( + () => dbg.adoptFrame(frame1), + Error, + "Debugger.Frame's global is not a debuggee" + ); + + dbg.addDebuggee(g); + + const frame2 = dbg.adoptFrame(frame1); + + assertMatchingFrame(frame1, frame2); + + suspendedFrame = frame1; +}; +const generator = g.eval(` +function* fn() { + debugger; + yield; +} +fn(); +`); +generator.next(); + +(function() { + // Test working with a suspended generator frame. + const dbg = new Debugger(); + assertErrorMessage( + () => dbg.adoptFrame(suspendedFrame), + Error, + "Debugger.Frame's global is not a debuggee" + ); + + dbg.addDebuggee(g); + + const frame2 = dbg.adoptFrame(suspendedFrame); + + assertMatchingFrame(frame2, suspendedFrame); +})(); + +generator.next(); +const deadFrame = suspendedFrame; + +(function() { + // Test working with a dead frame. + const dbg = new Debugger(); + + // This doesn't throw because the dead frame doesn't have any + // debuggee-specific data associated with it anymore. + dbg.adoptFrame(deadFrame); + + dbg.addDebuggee(g); + + const frame2 = dbg.adoptFrame(deadFrame); + + assertMatchingFrame(frame2, deadFrame); +})(); + +function assertMatchingFrame(frame1, frame2) { + assertEq(frame2.onStack, frame1.onStack); + assertEq(frame2.terminated, frame1.terminated); + + if (!frame2.terminated) { + assertEq(frame2.type, frame1.type); + assertEq(frame2.offset, frame1.offset); + } +} diff --git a/js/src/jit-test/tests/debug/Debugger-allowUnobservedAsmJS-01.js b/js/src/jit-test/tests/debug/Debugger-allowUnobservedAsmJS-01.js new file mode 100644 index 0000000000..51f9d033d5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-allowUnobservedAsmJS-01.js @@ -0,0 +1,26 @@ +load(libdir + "asm.js"); + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("dbg = new Debugger(parent);"); + +// Initial state is to inhibit asm.js. +assertEq(g.dbg.allowUnobservedAsmJS, false); + +var asmFunStr = USE_ASM + 'function f() {} return f'; + +// With asm.js inhibited, asm.js should fail with a type error about the +// debugger being on. +assertAsmTypeFail(asmFunStr); + +// With asm.js uninhibited, asm.js linking should work. +g.dbg.allowUnobservedAsmJS = true; +assertEq(asmLink(asmCompile(asmFunStr))(), undefined); + +// Toggling back should inhibit again. +g.dbg.allowUnobservedAsmJS = false; +assertAsmTypeFail(asmFunStr); + +// Removing the global should lift the inhibition. +g.dbg.removeDebuggee(this); +assertEq(asmLink(asmCompile(asmFunStr))(), undefined); diff --git a/js/src/jit-test/tests/debug/Debugger-allowUnobservedAsmJS-02.js b/js/src/jit-test/tests/debug/Debugger-allowUnobservedAsmJS-02.js new file mode 100644 index 0000000000..324de24004 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-allowUnobservedAsmJS-02.js @@ -0,0 +1,25 @@ +// |jit-test| skip-if: helperThreadCount() === 0 + +// Debugger.allowUnobservedAsmJS with off-thread parsing. + +load(libdir + "asm.js"); + + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("dbg = new Debugger(parent);"); + +assertEq(g.dbg.allowUnobservedAsmJS, false); + +enableLastWarning(); + +var asmFunStr = USE_ASM + 'function f() {} return f'; +offThreadCompileScript("(function() {" + asmFunStr + "})"); +runOffThreadScript(); + +var msg = getLastWarning().message; +assertEq(msg === "asm.js type error: Disabled by debugger" || + msg === "asm.js type error: Disabled by lack of a JIT compiler" || + msg === "asm.js type error: Disabled by 'asmjs' runtime option" || + msg === "asm.js type error: Disabled by lack of compiler support", + true); diff --git a/js/src/jit-test/tests/debug/Debugger-clearAllBreakpoints-01.js b/js/src/jit-test/tests/debug/Debugger-clearAllBreakpoints-01.js new file mode 100644 index 0000000000..ea5489a699 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-clearAllBreakpoints-01.js @@ -0,0 +1,29 @@ +// clearAllBreakpoints clears breakpoints for the current Debugger object only. + +var g = newGlobal({newCompartment: true}); + +var hits = 0; +function attach(i) { + var dbg = Debugger(g); + var handler = { + hit: function (frame) { + hits++; + dbg.clearAllBreakpoints(); + } + }; + + dbg.onDebuggerStatement = function (frame) { + var s = frame.script; + var offs = s.getLineOffsets(g.line0 + 3); + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], handler); + }; +} +for (var i = 0; i < 4; i++) + attach(i); + +g.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "for (var i = 0; i < 7; i++)\n" + // line0 + 2 + " Math.sin(0);\n"); // line0 + 3 +assertEq(hits, 4); diff --git a/js/src/jit-test/tests/debug/Debugger-clearAllBreakpoints-02.js b/js/src/jit-test/tests/debug/Debugger-clearAllBreakpoints-02.js new file mode 100644 index 0000000000..a5394a633e --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-clearAllBreakpoints-02.js @@ -0,0 +1,31 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() +// clearAllBreakpoints should clear breakpoints for WASM scripts. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval(` + var wasm = wasmTextToBinary( + '(module (func (nop) (nop)) (export "test" (func 0)))'); + var m = new WebAssembly.Instance(new WebAssembly.Module(wasm)); + var offsets = wasmCodeOffsets(wasm); +`); +var wasmScript = dbg.findScripts().filter(s => s.format == 'wasm')[0]; + +let count = 0; +wasmScript.setBreakpoint(g.offsets[0], { + hit: () => { + count++; + }, +}); + +g.m.exports.test(); +assertEq(count, 1); + +g.m.exports.test(); +assertEq(count, 2); + +dbg.clearAllBreakpoints(); + +g.m.exports.test(); +assertEq(count, 2); diff --git a/js/src/jit-test/tests/debug/Debugger-ctor-01.js b/js/src/jit-test/tests/debug/Debugger-ctor-01.js new file mode 100644 index 0000000000..72a5409bb0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-ctor-01.js @@ -0,0 +1,21 @@ +load(libdir + 'asserts.js'); + +// Debugger rejects arguments that aren't cross-compartment wrappers. +assertThrowsInstanceOf(function () { Debugger(null); }, TypeError); +assertThrowsInstanceOf(function () { Debugger(true); }, TypeError); +assertThrowsInstanceOf(function () { Debugger(42); }, TypeError); +assertThrowsInstanceOf(function () { Debugger("bad"); }, TypeError); +assertThrowsInstanceOf(function () { Debugger(function () {}); }, TypeError); +assertThrowsInstanceOf(function () { Debugger(this); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(null); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(true); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(42); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger("bad"); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(function () {}); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(this); }, TypeError); + +// From the main compartment, creating a Debugger on a sandbox compartment. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +assertEq(dbg instanceof Debugger, true); +assertEq(Object.getPrototypeOf(dbg), Debugger.prototype); diff --git a/js/src/jit-test/tests/debug/Debugger-ctor-02.js b/js/src/jit-test/tests/debug/Debugger-ctor-02.js new file mode 100644 index 0000000000..f1e4afdba9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-ctor-02.js @@ -0,0 +1,13 @@ +// Test creating a Debugger in a sandbox, debugging the initial global. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("var dbg = new Debugger(debuggeeGlobal);"); +assertEq(g.eval("dbg instanceof Debugger"), true); + +// The Debugger constructor from this compartment will not accept as its argument +// an Object from this compartment. Shenanigans won't fool the membrane. +g.parent = this; +assertThrowsInstanceOf(function () { g.eval("parent.Debugger(parent.Object())"); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-ctor-03.js b/js/src/jit-test/tests/debug/Debugger-ctor-03.js new file mode 100644 index 0000000000..f2245e7c9a --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-ctor-03.js @@ -0,0 +1,19 @@ +// If the debuggee cannot be put into debug mode, throw. + +// Run this test only if this compartment can't be put into debug mode. +var canEnable = true; +if (typeof setDebugMode === 'function') { + try { + setDebugMode(true); + } catch (exc) { + canEnable = false; + } +} + +if (!canEnable) { + var g = newGlobal(); + g.libdir = libdir; + g.eval("load(libdir + 'asserts.js');"); + g.parent = this; + g.eval("assertThrowsInstanceOf(function () { new Debugger(parent); }, Error);"); +} diff --git a/js/src/jit-test/tests/debug/Debugger-ctor-04.js b/js/src/jit-test/tests/debug/Debugger-ctor-04.js new file mode 100644 index 0000000000..4ecc619bd4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-ctor-04.js @@ -0,0 +1,5 @@ +// Repeated Debugger() arguments are ignored. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g, g, g); +assertEq(dbg.getDebuggees().length, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-ctor-05.js b/js/src/jit-test/tests/debug/Debugger-ctor-05.js new file mode 100644 index 0000000000..3b5e3127c7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-ctor-05.js @@ -0,0 +1,8 @@ +// Redundant non-global Debugger() arguments are ignored. + +var g = newGlobal({newCompartment: true}); +g.eval("var a = {}, b = {};"); +var dbg = Debugger(g.a, g.b); +var arr = dbg.getDebuggees(); +assertEq(arr.length, 1); +assertEq(arr[0], dbg.addDebuggee(g)); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-01.js b/js/src/jit-test/tests/debug/Debugger-debuggees-01.js new file mode 100644 index 0000000000..b8a22c029c --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-01.js @@ -0,0 +1,5 @@ +// A Debugger object created with no argument initially has no debuggees. +var dbg = new Debugger; +var debuggees = dbg.getDebuggees(); +assertEq(Array.isArray(debuggees), true); +assertEq(debuggees.length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-02.js b/js/src/jit-test/tests/debug/Debugger-debuggees-02.js new file mode 100644 index 0000000000..cc220edba3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-02.js @@ -0,0 +1,10 @@ +// The array returned by getDebuggees is just a snapshot, not live. +var dbg = new Debugger; +var a1 = dbg.getDebuggees(); +var g = newGlobal({newCompartment: true}); +var gw = dbg.addDebuggee(g); +assertEq(gw instanceof Debugger.Object, true); +var a2 = dbg.getDebuggees(); +assertEq(a2.length, 1); +assertEq(a2[0], gw); +assertEq(a1.length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-03.js b/js/src/jit-test/tests/debug/Debugger-debuggees-03.js new file mode 100644 index 0000000000..a792eed138 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-03.js @@ -0,0 +1,34 @@ +// Debugger hooks fire based on debuggees. + +var g1 = newGlobal({newCompartment: true}); +g1.eval("var g2 = newGlobal('same-compartment')"); +var g2 = g1.g2; +g1.eval("function f() { debugger; g2.g(); }"); +g2.eval("function g() { debugger; }"); + +var log; +var dbg = new Debugger; +dbg.onDebuggerStatement = function (frame) { log += frame.callee.name; }; + +// No debuggees: onDebuggerStatement is not called. +log = ''; +g1.f(); +assertEq(log, ''); + +// Add a debuggee and check that the handler is called. +var g1w = dbg.addDebuggee(g1); +log = ''; +g1.f(); +assertEq(log, 'f'); + +// Two debuggees, two onDebuggerStatement calls. +dbg.addDebuggee(g2); +log = ''; +g1.f(); +assertEq(log, 'fg'); + +// After a debuggee is removed, it no longer calls hooks. +assertEq(dbg.removeDebuggee(g1w), undefined); +log = ''; +g1.f(); +assertEq(log, 'g'); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-04.js b/js/src/jit-test/tests/debug/Debugger-debuggees-04.js new file mode 100644 index 0000000000..9f4714f86e --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-04.js @@ -0,0 +1,26 @@ +// hasDebuggee tests. + +var g1 = newGlobal({newCompartment: true}), g1w; +g1.eval("var g2 = newGlobal('same-compartment')"); +var g2 = g1.g2; +var g1w, g2w; + +var dbg = new Debugger; +function checkHas(hasg1, hasg2) { + assertEq(dbg.hasDebuggee(g1), hasg1); + if (typeof g1w === 'object') + assertEq(dbg.hasDebuggee(g1w), hasg1); + assertEq(dbg.hasDebuggee(g2), hasg2); + if (typeof g2w === 'object') + assertEq(dbg.hasDebuggee(g2w), hasg2); +} + +checkHas(false, false); +g1w = dbg.addDebuggee(g1); +checkHas(true, false); +g2w = dbg.addDebuggee(g2); +checkHas(true, true); +dbg.removeDebuggee(g1w); +checkHas(false, true); +dbg.removeDebuggee(g2); +checkHas(false, false); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-05.js b/js/src/jit-test/tests/debug/Debugger-debuggees-05.js new file mode 100644 index 0000000000..14ed9337f3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-05.js @@ -0,0 +1,8 @@ +// addDebuggee returns different Debugger.Object wrappers for different Debugger objects. + +var g = newGlobal({newCompartment: true}); +var dbg1 = new Debugger; +var gw1 = dbg1.addDebuggee(g); +var dbg2 = new Debugger; +var gw2 = dbg2.addDebuggee(g); +assertEq(gw1 !== gw2, true); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-06.js b/js/src/jit-test/tests/debug/Debugger-debuggees-06.js new file mode 100644 index 0000000000..264f32c980 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-06.js @@ -0,0 +1,27 @@ +// {has,add,remove}Debuggee throw a TypeError if the argument is invalid. + +load(libdir + "asserts.js"); + +var dbg = new Debugger; + +function check(val) { + assertThrowsInstanceOf(function () { dbg.hasDebuggee(val); }, TypeError); + assertThrowsInstanceOf(function () { dbg.addDebuggee(val); }, TypeError); + assertThrowsInstanceOf(function () { dbg.removeDebuggee(val); }, TypeError); +} + +// Primitive values are invalid. +check(undefined); +check(null); +check(false); +check(1); +check(NaN); +check("ok"); +check(Symbol("ok")); + +// A Debugger.Object that belongs to a different Debugger object is invalid. +var g = newGlobal({newCompartment: true}); +var dbg2 = new Debugger; +var w = dbg2.addDebuggee(g); +assertEq(w instanceof Debugger.Object, true); +check(w); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-08.js b/js/src/jit-test/tests/debug/Debugger-debuggees-08.js new file mode 100644 index 0000000000..4793e49eaf --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-08.js @@ -0,0 +1,25 @@ +// Adding a debuggee more than once is redundant. + +var dbg = new Debugger; +var g = newGlobal({newCompartment: true}); +var w = dbg.addDebuggee(g); +assertEq(w instanceof Debugger.Object, true); + +function usual() { + assertEq(dbg.hasDebuggee(g), true); + assertEq(dbg.hasDebuggee(w), true); + var arr = dbg.getDebuggees(); + assertEq(arr.length, 1); + assertEq(arr[0], w); +} + +usual(); +assertEq(dbg.addDebuggee(g), w); +usual(); +assertEq(dbg.addDebuggee(w), w); +usual(); + +// Removing the debuggee once is enough. +assertEq(dbg.removeDebuggee(g), undefined); +assertEq(dbg.hasDebuggee(g), false); +assertEq(dbg.getDebuggees().length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-09.js b/js/src/jit-test/tests/debug/Debugger-debuggees-09.js new file mode 100644 index 0000000000..bb7fb046a7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-09.js @@ -0,0 +1,21 @@ +// If hasDebuggee(x) is false, removeDebuggee(x) does nothing. + +var dbg = new Debugger; + +function check(obj) { + // If obj is something we could never debug, hasDebuggee(obj) is false. + assertEq(dbg.hasDebuggee(obj), false); + + // If hasDebuggee(x) is false, removeDebuggee(x) does nothing. + assertEq(dbg.removeDebuggee(obj), undefined); +} + +// global objects which happen not to be debuggees at the moment +var g1 = newGlobal('same-compartment'); +check(g1); + +// objects in a compartment that is already debugging us +var g2 = newGlobal({newCompartment: true}); +g2.parent = this; +g2.eval("var dbg = new Debugger(parent);"); +check(g2); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-10.js b/js/src/jit-test/tests/debug/Debugger-debuggees-10.js new file mode 100644 index 0000000000..7e5c369bc0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-10.js @@ -0,0 +1,18 @@ +// Allow diamonds in the graph of the compartment "debugs" relation. +var program = newGlobal({newCompartment: true}); +var d1 = newGlobal({newCompartment: true}); +d1.top = this; +var d2 = newGlobal({newCompartment: true}); +d2.top = this; +var dbg = new Debugger(d1, d2); +d1.eval("var dbg = new Debugger(top.program)"); +d2.eval("var dbg = new Debugger(top.program)"); + +// mess with the edges a little bit -- all this should be fine, no cycles +d1.dbg.removeDebuggee(program); +d1.dbg.addDebuggee(program); +dbg.addDebuggee(program); +d1.dbg.addDebuggee(d2); +dbg.removeDebuggee(d2); +dbg.addDebuggee(d2); + diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-11.js b/js/src/jit-test/tests/debug/Debugger-debuggees-11.js new file mode 100644 index 0000000000..d1ca587e2b --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-11.js @@ -0,0 +1,22 @@ +// Don't allow cycles in the graph of the compartment "debugs" relation. + +load(libdir + "asserts.js"); + +// trivial cycles +var dbg = new Debugger; +assertThrowsInstanceOf(function () { dbg.addDebuggee(this); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(this); }, TypeError); + +// cycles of length 2 +var d1 = newGlobal({newCompartment: true}); +d1.top = this; +d1.eval("var dbg = new Debugger(top)"); +assertThrowsInstanceOf(function () { dbg.addDebuggee(d1); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(d1); }, TypeError); + +// cycles of length 3 +var d2 = newGlobal({newCompartment: true}); +d2.top = this; +d2.eval("var dbg = new Debugger(top.d1)"); +assertThrowsInstanceOf(function () { dbg.addDebuggee(d2); }, TypeError); +assertThrowsInstanceOf(function () { new Debugger(d2); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-12.js b/js/src/jit-test/tests/debug/Debugger-debuggees-12.js new file mode 100644 index 0000000000..b77e9cff59 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-12.js @@ -0,0 +1,10 @@ +// Events in a non-debuggee are ignored, even if a debuggee is in the same compartment. +var g1 = newGlobal({newCompartment: true}); +var g2 = g1.eval("newGlobal('same-compartment')"); +var dbg = new Debugger(g1); +var hits = 0; +dbg.onDebuggerStatement = function () { hits++; }; +g1.eval("debugger;"); +assertEq(hits, 1); +g2.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-13.js b/js/src/jit-test/tests/debug/Debugger-debuggees-13.js new file mode 100644 index 0000000000..6030bda3cc --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-13.js @@ -0,0 +1,9 @@ +// Removing a debuggee does not detach the debugger from a compartment if another debuggee is in it. +var g1 = newGlobal({newCompartment: true}); +var g2 = g1.eval("newGlobal('same-compartment')"); +var dbg = new Debugger(g1, g2); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { hits++; }; +dbg.removeDebuggee(g1); +g2.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-14.js b/js/src/jit-test/tests/debug/Debugger-debuggees-14.js new file mode 100644 index 0000000000..491a116c09 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-14.js @@ -0,0 +1,8 @@ +// Adding a debuggee in a compartment that is already in debug mode works +// even if a script from that compartment is on the stack. + +var g = newGlobal({newCompartment: true}); +var dbg1 = Debugger(g); +var dbg2 = Debugger(); +g.parent = this; +g.eval("parent.dbg2.addDebuggee(this);"); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-15.js b/js/src/jit-test/tests/debug/Debugger-debuggees-15.js new file mode 100644 index 0000000000..d20972e1ce --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-15.js @@ -0,0 +1,7 @@ +// Debugger mode can be disabled for a compartment even if it has scripts running. +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +g.parent = this; +var n = 2; +g.eval("parent.dbg.removeDebuggee(this); parent.n += 2"); +assertEq(n, 4); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-16.js b/js/src/jit-test/tests/debug/Debugger-debuggees-16.js new file mode 100644 index 0000000000..eebe99ba96 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-16.js @@ -0,0 +1,30 @@ +// GC can turn off debug mode in a compartment. + +var dbgs = []; +var nonDebugGlobals = []; +var f = gc; +for (var i = 0; i < 4; i++) { + // Create two globals, one debuggee. + var g1 = newGlobal({newCompartment: true}); + var g2 = g1.eval("newGlobal('same-compartment')"); + var dbg = Debugger(g1); + dbg.onDebuggerStatement = function () {}; + + // Thread a chain of functions through the non-debuggee globals. + g2.eval("function f() { return g() + 1; }"); + g2.g = f; + f = g2.f; + + // Root the Debugger objects and non-debuggee globals. + dbgs[i] = dbg; + nonDebugGlobals[i] = g2; +} + +// Call the chain of functions. At the end of the chain is gc. This will +// collect (some or all of) the debuggee globals, leaving non-debuggee +// globals. It should disable debug mode in those debuggee compartments. +nonDebugGlobals[nonDebugGlobals.length - 1].f(); + +gc(); +nonDebugGlobals[0].g = function () { return 0; } +assertEq(nonDebugGlobals[nonDebugGlobals.length - 1].f(), nonDebugGlobals.length); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-17.js b/js/src/jit-test/tests/debug/Debugger-debuggees-17.js new file mode 100644 index 0000000000..bb1ec664e5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-17.js @@ -0,0 +1,26 @@ +// addDebuggee, hasDebuggee, and removeDebuggee all throw if presented with +// objects that are not valid global object designators. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger; + +function check(bad) { + print("check(" + JSON.stringify(bad) + ")"); + assertThrowsInstanceOf(function () { dbg.addDebuggee(bad); }, TypeError); + assertEq(dbg.getDebuggees().length, 0); + assertThrowsInstanceOf(function () { dbg.hasDebuggee(bad); }, TypeError); + assertThrowsInstanceOf(function () { dbg.removeDebuggee(bad); }, TypeError); +} + +var g = newGlobal({newCompartment: true}); +check(g.Object()); +check(g.Object); +check(g.Function("")); + +// A Debugger.Object belonging to a different Debugger is not a valid way +// to designate a global, even if its referent is a global. +var g2 = newGlobal({newCompartment: true}); +var dbg2 = new Debugger; +var d2g2 = dbg2.addDebuggee(g2); +check(d2g2); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-18.js b/js/src/jit-test/tests/debug/Debugger-debuggees-18.js new file mode 100644 index 0000000000..a4aa94ed54 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-18.js @@ -0,0 +1,114 @@ +// Debugger.prototype.{addDebuggee,hasDebuggee,removeDebuggee} recognize globals +// regardless of how they are specified. + +var dbg = new Debugger; + +// Assert that dbg's debuggees are exactly the set passed as arguments. +// The arguments are assumed to be Debugger.Object instances referring to +// globals without wrappers --- which is the sort returned by addDebuggee. +function assertDebuggees(...expected) { + print("assertDebuggees([" + expected.map((g) => g.toSource()) + "])"); + var debuggees = dbg.getDebuggees(); + assertEq(expected.length, debuggees.length); + for (let g of expected) + assertEq(debuggees.indexOf(g) != -1, true); +} + +var g1 = newGlobal({newCompartment: true}); g1.toSource = function () { return "[global g1]"; }; +var g2 = newGlobal({newCompartment: true}); g2.toSource = function () { return "[global g2]"; }; + +assertDebuggees(); + +// Produce every possible way to designate g1, for us to play with. +// Globals can be designated by any of the following: +// +// - "CCW": a Cross-Compartment Wrapper (CCW) of a global object +// - "D.O": a Debugger.Object whose referent is a global object +// - "D.O of CCW": a Debugger.Object whose referent is a CCW of a +// global object, where the CCW can be securely unwrapped +// +// There's no direct "G", since globals are always in their own +// compartments, never the debugger's; if we ever viewed them directly, +// that would be a compartment violation. + +// "dg1" means "Debugger.Object referring (directly) to g1". +var dg1 = dbg.addDebuggee(g1); +dg1.toSource = function() { return "[Debugger.Object for global g1]"; }; +assertEq(dg1.unwrap(), dg1); +assertDebuggees(dg1); + +// We need to add g2 as a debuggee; that's the only way to get a D.O referring +// to it without a wrapper. +var dg2 = dbg.addDebuggee(g2); +dg2.toSource = function() { return "[Debugger.Object for global g2]"; }; +assertEq(dg2.unwrap(), dg2); +assertDebuggees(dg1, dg2); + +// "dwg1" means "Debugger.Object referring to CCW of g1". +var dwg1 = dg2.makeDebuggeeValue(g1); +assertEq(dwg1.unwrap(), dg1.makeDebuggeeValue(g1)); +dwg1.toSource = function() { return "[Debugger.Object for CCW of WindowProxy of g1]"; }; + +assertDebuggees(dg1, dg2); +assertEq(dbg.removeDebuggee(g1), undefined); +assertEq(dbg.removeDebuggee(g2), undefined); +assertDebuggees(); + +// Systematically cover all the single-global possibilities: +// +// | added as | designated as | addDebuggee | hasDebuggee | removeDebuggee | +// |-------------+---------------+-------------+-------------+----------------| +// | (not added) | CCW | X | X | X | +// | | D.O | X | X | X | +// | | D.O of CCW | X | X | X | +// |-------------+---------------+-------------+-------------+----------------| +// | CCW | CCW | X | X | X | +// | | D.O | X | X | X | +// | | D.O of CCW | X | X | X | +// |-------------+---------------+-------------+-------------+----------------| +// | D.O | CCW | X | X | X | +// | | D.O | X | X | X | +// | | D.O of CCW | X | X | X | +// |-------------+---------------+-------------+-------------+----------------| +// | D.O of CCW | CCW | X | X | X | +// | | D.O | X | X | X | +// | | D.O of CCW | X | X | X | + +// Cover the "(not added)" section of the table, other than "addDebuggee": +assertEq(dbg.hasDebuggee(g1), false); +assertEq(dbg.hasDebuggee(dg1), false); +assertEq(dbg.hasDebuggee(dwg1), false); + +assertEq(dbg.removeDebuggee(g1), undefined); assertDebuggees(); +assertEq(dbg.removeDebuggee(dg1), undefined); assertDebuggees(); +assertEq(dbg.removeDebuggee(dwg1), undefined); assertDebuggees(); + +// Try all operations adding the debuggee using |addAs|, and operating on it +// using |designateAs|, thereby covering one row of the table (outside the '(not +// added)' section), and one case in the '(not added)', 'designated as' section. +// +// |Direct| should be the Debugger.Object referring directly to the debuggee +// global, for checking the results from addDebuggee and getDebuggees. +function combo(addAs, designateAs, direct) { + print("combo(" + JSON.stringify(addAs) + ", " + JSON.stringify(designateAs) + ")"); + assertDebuggees(); + assertEq(dbg.addDebuggee(addAs), direct); + assertDebuggees(direct); + assertEq(dbg.addDebuggee(designateAs), direct); + assertDebuggees(direct); + assertEq(dbg.hasDebuggee(designateAs), true); + assertEq(dbg.removeDebuggee(designateAs), undefined); + assertDebuggees(); +} + +combo(g1, g1, dg1); +combo(dg1, g1, dg1); +combo(dwg1, g1, dg1); + +combo(g1, dg1, dg1); +combo(dg1, dg1, dg1); +combo(dwg1, dg1, dg1); + +combo(g1, dwg1, dg1); +combo(dg1, dwg1, dg1); +combo(dwg1, dwg1, dg1); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-19.js b/js/src/jit-test/tests/debug/Debugger-debuggees-19.js new file mode 100644 index 0000000000..7883a8a076 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-19.js @@ -0,0 +1,49 @@ +// removeAllDebuggees removes all the debuggees. + +var dbg = new Debugger; + +// If we eval in a debuggee, log which debuggee it was. +var log; +dbg.onEnterFrame = function (frame) { + log += 'e'; + // frame.environment in all evals below is the global lexical env. + log += frame.environment.parent.object.label; +}; + +var g1 = newGlobal({newCompartment: true}); +log = ''; +g1.eval('Math'); +assertEq(log, ''); // not yet a debuggee + +var g1w = dbg.addDebuggee(g1); +assertEq(g1w instanceof Debugger.Object, true); +g1w.label = 'g1'; +log = ''; +g1.eval('Math'); // now a debuggee +assertEq(log, 'eg1'); + +var g2 = newGlobal({newCompartment: true}); +log = ''; +g1.eval('Math'); // debuggee +g2.eval('Math'); // not a debuggee +assertEq(log, 'eg1'); + +var g2w = dbg.addDebuggee(g2); +assertEq(g2w instanceof Debugger.Object, true); +g2w.label = 'g2'; +log = ''; +g1.eval('Math'); // debuggee +g2.eval('this'); // debuggee +assertEq(log, 'eg1eg2'); + +var a1 = dbg.getDebuggees(); +assertEq(a1.length, 2); + +assertEq(dbg.removeAllDebuggees(), undefined); +var a2 = dbg.getDebuggees(); +assertEq(a2.length, 0); + +log = ''; +g1.eval('Math'); // no longer a debuggee +g2.eval('this'); // no longer a debuggee +assertEq(log, ''); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-20.js b/js/src/jit-test/tests/debug/Debugger-debuggees-20.js new file mode 100644 index 0000000000..8cc27d0fca --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-20.js @@ -0,0 +1,30 @@ +// addAllGlobalsAsDebuggees adds all the globals as debuggees. + +var g1 = newGlobal({newCompartment: true}); // Created before the Debugger; debuggee. +var g2 = newGlobal({newCompartment: true}); // Created before the Debugger; not debuggee. + +var dbg = new Debugger; + +var g3 = newGlobal({newCompartment: true}); // Created after the Debugger; debuggee. +var g4 = newGlobal({newCompartment: true}); // Created after the Debugger; not debuggee. + +var g1w = dbg.addDebuggee(g1); +var g3w = dbg.addDebuggee(g3); +assertEq(dbg.addAllGlobalsAsDebuggees(), undefined); + +// Get Debugger.Objects viewing the WindowProxies from their own compartments. +assertEq(g1w.makeDebuggeeValue(g1), g3w.makeDebuggeeValue(g1).unwrap()); +assertEq(g3w.makeDebuggeeValue(g3), g1w.makeDebuggeeValue(g3).unwrap()); + +var g2w = g1w.makeDebuggeeValue(g2).unwrap(); +var g4w = g1w.makeDebuggeeValue(g4).unwrap(); + +var thisw = g1w.makeDebuggeeValue(this).unwrap(); + +// Check that they're all there. +assertEq(dbg.hasDebuggee(g1w), true); +assertEq(dbg.hasDebuggee(g2w), true); +assertEq(dbg.hasDebuggee(g3w), true); +assertEq(dbg.hasDebuggee(g4w), true); +// The debugger's global is not a debuggee. +assertEq(dbg.hasDebuggee(thisw), false); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-21.js b/js/src/jit-test/tests/debug/Debugger-debuggees-21.js new file mode 100644 index 0000000000..fb5f1b849d --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-21.js @@ -0,0 +1,12 @@ +// Errors adding globals in addAllGlobalsAsDebuggees should be reported. + +// The exception that might be thrown in this test reflects our inability +// to change compartments to debug mode while they have frames on the +// stack. If we run this test with --debugjit, it won't throw an error at +// all, since all compartments are already in debug mode. So, pass if the +// script completes normally, or throws an appropriate exception. +try { + newGlobal().eval("(new Debugger).addAllGlobalsAsDebuggees();"); +} catch (ex) { + assertEq(!!(''+ex).match(/can't start debugging: a debuggee script is on the stack/), true); +} diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-22.js b/js/src/jit-test/tests/debug/Debugger-debuggees-22.js new file mode 100644 index 0000000000..4191fb0e61 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-22.js @@ -0,0 +1,24 @@ +// Adding a debuggee allowed with scripts on stack. + +var g = newGlobal({newCompartment: true}); +g.dbg = new Debugger; + +g.eval("" + function f(d) { + g(d); + if (d) + assertEq(dbg.hasDebuggee(this), true); +}); + +g.eval("" + function g(d) { + if (!d) + return; + + dbg.addDebuggee(this); +}); + +g.eval("(" + function test() { + f(false); + f(false); + f(true); + f(true); +} + ")();"); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-23.js b/js/src/jit-test/tests/debug/Debugger-debuggees-23.js new file mode 100644 index 0000000000..fabdc3761e --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-23.js @@ -0,0 +1,107 @@ +// Adding a debuggee allowed with scripts on stack from stranger places. + +// Test CCW. +(function testCCW() { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + g.dbg = dbg; + g.GLOBAL = g; + + g.turnOnDebugger = function () { + dbg.addDebuggee(g); + }; + + g.eval("" + function f(d) { + turnOnDebugger(); + assertEq(dbg.hasDebuggee(GLOBAL), true); + }); + + g.eval("(" + function test() { + f(false); + f(false); + f(true); + f(true); + } + ")();"); +})(); + +// Test getter. +(function testGetter() { + var g = newGlobal({newCompartment: true}); + g.dbg = new Debugger; + g.GLOBAL = g; + + g.eval("" + function f(obj) { + obj.foo; + assertEq(dbg.hasDebuggee(GLOBAL), true); + }); + + g.eval("(" + function test() { + f({ get foo() { dbg.addDebuggee(GLOBAL); } }); + } + ")();"); +})(); + +// Test setter. +(function testSetter() { + var g = newGlobal({newCompartment: true}); + g.dbg = new Debugger; + g.GLOBAL = g; + + g.eval("" + function f(obj) { + obj.foo = 42; + assertEq(dbg.hasDebuggee(GLOBAL), true); + }); + + g.eval("(" + function test() { + f({ set foo(v) { dbg.addDebuggee(GLOBAL); } }); + } + ")();"); +})(); + +// Test toString. +(function testToString() { + var g = newGlobal({newCompartment: true}); + g.dbg = new Debugger; + g.GLOBAL = g; + + g.eval("" + function f(obj) { + obj + ""; + assertEq(dbg.hasDebuggee(GLOBAL), true); + }); + + g.eval("(" + function test() { + f({ toString: function () { dbg.addDebuggee(GLOBAL); }}); + } + ")();"); +})(); + +// Test valueOf. +(function testValueOf() { + var g = newGlobal({newCompartment: true}); + g.dbg = new Debugger; + g.GLOBAL = g; + + g.eval("" + function f(obj) { + obj + ""; + assertEq(dbg.hasDebuggee(GLOBAL), true); + }); + + g.eval("(" + function test() { + f({ valueOf: function () { dbg.addDebuggee(GLOBAL); }}); + } + ")();"); +})(); + +// Test proxy trap. +(function testProxyTrap() { + var g = newGlobal({newCompartment: true}); + g.dbg = new Debugger; + g.GLOBAL = g; + + g.eval("" + function f(proxy) { + proxy["foo"]; + assertEq(dbg.hasDebuggee(GLOBAL), true); + }); + + g.eval("(" + function test() { + var handler = { get: function () { dbg.addDebuggee(GLOBAL); } }; + var proxy = new Proxy({}, handler); + f(proxy); + } + ")();"); +})(); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-24.js b/js/src/jit-test/tests/debug/Debugger-debuggees-24.js new file mode 100644 index 0000000000..9243c247d1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-24.js @@ -0,0 +1,55 @@ +// Turning debugger on for a particular global with on-stack scripts shouldn't +// make other globals' scripts observable. + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var g3 = newGlobal({newCompartment: true}); + +g1.eval("" + function f() { + var name = "f"; + g(); + return name; +}); +g2.eval("" + function g() { + var name = "g"; + h(); + return name; +}); +g3.eval("" + function h() { + var name = "h"; + toggle(); + return name; +}); + +g1.g = g2.g; +g2.h = g3.h; + +function name(f) { + return f.environment.getVariable("name"); +} + +var dbg = new Debugger; +g3.toggle = function () { + var frame; + + // Only f should be visible. + dbg.addDebuggee(g1); + frame = dbg.getNewestFrame(); + assertEq(name(frame), "f"); + + // Now h should also be visible. + dbg.addDebuggee(g3); + frame = dbg.getNewestFrame(); + assertEq(name(frame), "h"); + assertEq(name(frame.older), "f"); + + // Finally everything should be visible. + dbg.addDebuggee(g2); + frame = dbg.getNewestFrame(); + assertEq(name(frame), "h"); + assertEq(name(frame.older), "g"); + assertEq(name(frame.older.older), "f"); +}; + +g1.eval("(" + function () { f(); } + ")();"); + diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-25.js b/js/src/jit-test/tests/debug/Debugger-debuggees-25.js new file mode 100644 index 0000000000..f066e5a3ef --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-25.js @@ -0,0 +1,48 @@ +// Turning debugger off global at a time. + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var g3 = newGlobal({newCompartment: true}); + +g1.eval("" + function f() { + var name = "f"; + g(); + return name; +}); +g2.eval("" + function g() { + var name = "g"; + h(); + return name; +}); +g3.eval("" + function h() { + var name = "h"; + toggle(); + return name; +}); + +g1.g = g2.g; +g2.h = g3.h; + +function name(f) { + return f.environment.getVariable("name"); +} + +var dbg = new Debugger; +g3.toggle = function () { + var frame; + + // Add all globals. + dbg.addDebuggee(g1); + dbg.addDebuggee(g3); + dbg.addDebuggee(g2); + + // Remove one at a time. + dbg.removeDebuggee(g3); + assertEq(name(dbg.getNewestFrame()), "g"); + dbg.removeDebuggee(g2); + assertEq(name(dbg.getNewestFrame()), "f"); + dbg.removeDebuggee(g1); +}; + +g1.eval("(" + function () { f(); } + ")();"); + diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-26.js b/js/src/jit-test/tests/debug/Debugger-debuggees-26.js new file mode 100644 index 0000000000..d2e7f610b1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-26.js @@ -0,0 +1,34 @@ +// Ion can bail in-place when throwing exceptions with debug mode toggled on. + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.toggle = function toggle(x, d) { + if (d) { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame().older; + assertEq(frame.callee.name, "f"); + assertEq(frame.implementation, "ion"); + throw 42; + } + }; + + g.eval("" + function f(x, d) { g(x, d); }); + g.eval("" + function g(x, d) { toggle(x, d); }); + + try { + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(42, false); + f(42, true); + } + ")();"); + } catch (exc) { + assertEq(exc, 42); + } +}); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-27.js b/js/src/jit-test/tests/debug/Debugger-debuggees-27.js new file mode 100644 index 0000000000..7046b68385 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-27.js @@ -0,0 +1,19 @@ +// Test that we can OSR with the same script on the stack multiple times. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; + +g.toggle = function toggle() { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame(); +} + +g.eval("" + function f(x) { + if (x == 0) { + toggle(); + return; + } + f(x - 1); +}); + +g.eval("f(3);"); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-28.js b/js/src/jit-test/tests/debug/Debugger-debuggees-28.js new file mode 100644 index 0000000000..a5c11201cb --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-28.js @@ -0,0 +1,109 @@ +// Test that on->off->on and off->on->off toggles don't crash. + +function addRemove(dbg, g) { + dbg.addDebuggee(g); + var f = dbg.getNewestFrame(); + while (f) + f = f.older; + dbg.removeDebuggee(g); +} + +function removeAdd(dbg, g) { + dbg.removeDebuggee(g); + dbg.addDebuggee(g); + var f = dbg.getNewestFrame(); + while (f) + f = f.older; +} + +function newGlobalDebuggerPair(toggleSeq) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + if (toggleSeq == removeAdd) + dbg.addDebuggee(g); + + g.eval("" + function f() { return g(); }); + g.eval("" + function g() { return h(); }); + g.eval("line0 = Error().lineNumber;"); + g.eval("" + function h() { + for (var i = 0; i < 100; i++) + interruptIf(i == 95); + debugger; + return i; + }); + + setInterruptCallback(function () { return true; }); + + return [g, dbg]; +} + +function testInterrupt(toggleSeq) { + var [g, dbg] = newGlobalDebuggerPair(toggleSeq); + + setInterruptCallback(function () { + toggleSeq(dbg, g); + return true; + }); + + assertEq(g.f(), 100); +} + +function testPrologue(toggleSeq) { + var [g, dbg] = newGlobalDebuggerPair(toggleSeq); + + dbg.onEnterFrame = function (f) { + if (f.callee && f.callee.name == "h") + toggleSeq(dbg, g); + }; + + assertEq(g.f(), 100); +} + +function testEpilogue(toggleSeq) { + var [g, dbg] = newGlobalDebuggerPair(toggleSeq); + + dbg.onEnterFrame = function (f) { + if (f.callee && f.callee.name == "h") { + f.onPop = function () { + toggleSeq(dbg, g); + }; + } + }; + + assertEq(g.f(), 100); +} + +function testTrap(toggleSeq) { + var [g, dbg] = newGlobalDebuggerPair(toggleSeq); + + dbg.onEnterFrame = function (f) { + if (f.callee && f.callee.name == "h") { + var offs = f.script.getLineOffsets(g.line0 + 2); + assertEq(offs.length > 0, true); + f.script.setBreakpoint(offs[0], { hit: function () { + toggleSeq(dbg, g); + }}); + } + }; + + assertEq(g.f(), 100); +} + +function testDebugger(toggleSeq) { + var [g, dbg] = newGlobalDebuggerPair(toggleSeq); + + dbg.onDebuggerStatement = function () { + toggleSeq(dbg, g); + }; + + assertEq(g.f(), 100); +} + +testInterrupt(addRemove); +testInterrupt(removeAdd); + +testPrologue(removeAdd); +testEpilogue(removeAdd); +testTrap(removeAdd); +testDebugger(removeAdd); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-29.js b/js/src/jit-test/tests/debug/Debugger-debuggees-29.js new file mode 100644 index 0000000000..fb05edc884 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-29.js @@ -0,0 +1,6 @@ +// Debugger.prototype.addDebuggee should not accept invisible-to-debugger globals. +load(libdir + 'asserts.js'); + +var g = newGlobal({ newCompartment: true, invisibleToDebugger: true }); + +assertThrowsInstanceOf(() => { new Debugger(g); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-30.js b/js/src/jit-test/tests/debug/Debugger-debuggees-30.js new file mode 100644 index 0000000000..2287c38d8b --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-30.js @@ -0,0 +1,32 @@ +// Debugger and debuggees must be in different compartments. + +load(libdir + "asserts.js"); + +function testConstructor() { + var g = newGlobal({sameCompartmentAs: this}); + assertTypeErrorMessage(() => new Debugger(g), + "Debugger: argument must be an object from a different compartment"); +} +testConstructor(); + +function testAddDebuggee() { + var g = newGlobal({sameCompartmentAs: this}); + var dbg = new Debugger(); + assertTypeErrorMessage(() => dbg.addDebuggee(this), + "debugger and debuggee must be in different compartments"); +} +testAddDebuggee(); + +function testAddAllGlobalsAsDebuggees() { + var g1 = newGlobal({sameCompartmentAs: this}); + var g2 = newGlobal({newCompartment: true}); + var g3 = newGlobal({sameCompartmentAs: g2}); + var g4 = newGlobal({newCompartment: true, sameZoneAs: this}); + var dbg = new Debugger(); + dbg.addAllGlobalsAsDebuggees(); + assertEq(dbg.hasDebuggee(g1), false); + assertEq(dbg.hasDebuggee(g2), true); + assertEq(dbg.hasDebuggee(g3), true); + assertEq(dbg.hasDebuggee(g4), true); +} +testAddAllGlobalsAsDebuggees(); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-31.js b/js/src/jit-test/tests/debug/Debugger-debuggees-31.js new file mode 100644 index 0000000000..0eb7f65212 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-31.js @@ -0,0 +1,30 @@ +// |jit-test| skip-if: !hasFunction["gczeal"] +// Removing and adding debuggees during an incremental sweep should not confuse +// the generatorFrames map. + +// Let any ongoing incremental GC finish, and then clear any ambient zeal +// settings established by the harness (the JS_GC_ZEAL env var, shell arguments, +// etc.) +gczeal(0); + +let g = newGlobal({newCompartment: true}); +g.eval('function* f() { yield 123; }'); + +let dbg = Debugger(g); +dbg.onEnterFrame = frame => { + dbg.removeDebuggee(g); + dbg.addDebuggee(g); +}; + +// Select GCZeal mode 10 (IncrementalMultipleSlices: Incremental GC in many +// slices) and use long slices, to make sure that the debuggee removal occurs +// during a slice. +gczeal(10, 0); +gcslice(1); +while (gcstate() !== "NotAcctive" && gcstate() !== "Sweep") { + gcslice(1000); +} + +let genObj = g.f(); +genObj.return(); +assertEq(gcstate(), "Sweep"); diff --git a/js/src/jit-test/tests/debug/Debugger-debuggees-32.js b/js/src/jit-test/tests/debug/Debugger-debuggees-32.js new file mode 100644 index 0000000000..4d5bab9e1a --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-debuggees-32.js @@ -0,0 +1,13 @@ +let g = newGlobal({newCompartment: true}); +g.eval('function* f() { yield 123; }'); + +let dbg = Debugger(g); +let genObj = g.f(); + +dbg.onEnterFrame = frame => { + frame.onStep = () => {}; + dbg.removeDebuggee(g); + dbg.addDebuggee(g); +}; + +genObj.return(); diff --git a/js/src/jit-test/tests/debug/Debugger-findAllGlobals-01.js b/js/src/jit-test/tests/debug/Debugger-findAllGlobals-01.js new file mode 100644 index 0000000000..7400ef9ea1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findAllGlobals-01.js @@ -0,0 +1,24 @@ +// Debugger.prototype.findAllGlobals surface. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger; +var d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(dbg), 'findAllGlobals'); +assertEq(d.configurable, true); +assertEq(d.enumerable, false); +assertEq(d.writable, true); +assertEq(typeof d.value, 'function'); +assertEq(dbg.findAllGlobals.length, 0); +assertEq(dbg.findAllGlobals.name, 'findAllGlobals'); + +// findAllGlobals can only be applied to real Debugger instances. +assertThrowsInstanceOf(function() { + Debugger.prototype.findAllGlobals.call(Debugger.prototype); + }, + TypeError); +var a = dbg.findAllGlobals(); +assertEq(a instanceof Array, true); +assertEq(a.length > 0, true); +for (g of a) { + assertEq(g instanceof Debugger.Object, true); +} diff --git a/js/src/jit-test/tests/debug/Debugger-findAllGlobals-02.js b/js/src/jit-test/tests/debug/Debugger-findAllGlobals-02.js new file mode 100644 index 0000000000..a44516128a --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findAllGlobals-02.js @@ -0,0 +1,26 @@ +// Debugger.prototype.findAllGlobals finds ALL the globals! + +var g1 = newGlobal({newCompartment: true}); // Created before the Debugger; debuggee. +var g2 = newGlobal({newCompartment: true}); // Created before the Debugger; not debuggee. + +var dbg = new Debugger; + +var g3 = newGlobal({newCompartment: true}); // Created after the Debugger; debuggee. +var g4 = newGlobal({newCompartment: true}); // Created after the Debugger; not debuggee. + +var g1w = dbg.addDebuggee(g1); +var g3w = dbg.addDebuggee(g3); + +var a = dbg.findAllGlobals(); + +// Get Debugger.Objects around global objects +var g2w = dbg.makeGlobalObjectReference(g2); +var g4w = dbg.makeGlobalObjectReference(g4); +var thisw = dbg.makeGlobalObjectReference(this); + +// Check that they're all there. +assertEq(a.indexOf(g1w) != -1, true); +assertEq(a.indexOf(g2w) != -1, true); +assertEq(a.indexOf(g3w) != -1, true); +assertEq(a.indexOf(g4w) != -1, true); +assertEq(a.indexOf(thisw) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-01.js b/js/src/jit-test/tests/debug/Debugger-findObjects-01.js new file mode 100644 index 0000000000..4ed43310a9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-01.js @@ -0,0 +1,4 @@ +// In a debugger with no debuggees, findObjects should return no objects. + +var dbg = new Debugger; +assertEq(dbg.findObjects().length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-02.js b/js/src/jit-test/tests/debug/Debugger-findObjects-02.js new file mode 100644 index 0000000000..b3bdbbf519 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-02.js @@ -0,0 +1,18 @@ +// In a debuggee with live objects, findObjects finds those objects. + +var g = newGlobal({newCompartment: true}); + +let defObject = v => g.eval(`this.${v} = { toString: () => "[object ${v}]" }`); +defObject("a"); +defObject("b"); +defObject("c"); + +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var aw = gw.makeDebuggeeValue(g.a); +var bw = gw.makeDebuggeeValue(g.b); +var cw = gw.makeDebuggeeValue(g.c); + +assertEq(dbg.findObjects().indexOf(aw) != -1, true); +assertEq(dbg.findObjects().indexOf(bw) != -1, true); +assertEq(dbg.findObjects().indexOf(cw) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-03.js b/js/src/jit-test/tests/debug/Debugger-findObjects-03.js new file mode 100644 index 0000000000..0daf850754 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-03.js @@ -0,0 +1,12 @@ +// findObjects' result includes objects referenced by other objects. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval('this.a = { b: {} };'); + +var bw = gw.makeDebuggeeValue(g.a.b); + +var objects = dbg.findObjects(); +assertEq(objects.indexOf(bw) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-04.js b/js/src/jit-test/tests/debug/Debugger-findObjects-04.js new file mode 100644 index 0000000000..ac62575637 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-04.js @@ -0,0 +1,16 @@ +// findObjects' result includes objects captured by closures. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` + this.f = (function () { + let a = { foo: () => {} }; + return () => a; + }()); +`); + +let objects = dbg.findObjects(); +let aw = gw.makeDebuggeeValue(g.f()); +assertEq(objects.indexOf(aw) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-05.js b/js/src/jit-test/tests/debug/Debugger-findObjects-05.js new file mode 100644 index 0000000000..7d581ca98c --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-05.js @@ -0,0 +1,10 @@ +// findObjects' result doesn't include any duplicates. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +dbg.addDebuggee(g); + +let objects = dbg.findObjects(); +let set = new Set(objects); + +assertEq(objects.length, set.size); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-06.js b/js/src/jit-test/tests/debug/Debugger-findObjects-06.js new file mode 100644 index 0000000000..2f8d5a11f6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-06.js @@ -0,0 +1,14 @@ +// In a debugger with multiple debuggees, findObjects finds objects from all debuggees. + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var g1w = dbg.addDebuggee(g1); +var g2w = dbg.addDebuggee(g2); + +g1.eval('this.a = {};'); +g2.eval('this.b = {};'); + +var objects = dbg.findObjects(); +assertEq(objects.indexOf(g1w.makeDebuggeeValue(g1.a)) != -1, true); +assertEq(objects.indexOf(g2w.makeDebuggeeValue(g2.b)) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-07.js b/js/src/jit-test/tests/debug/Debugger-findObjects-07.js new file mode 100644 index 0000000000..5d4549cc0e --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-07.js @@ -0,0 +1,22 @@ +// findObjects can filter objects by class name. + +var g = newGlobal({newCompartment: true}); + +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval('this.re = /foo/;'); +g.eval('this.d = new Date();'); + +var rew = gw.makeDebuggeeValue(g.re); +var dw = gw.makeDebuggeeValue(g.d); + +var objects; + +objects = dbg.findObjects({ class: "RegExp" }); +assertEq(objects.indexOf(rew) != -1, true); +assertEq(objects.indexOf(dw) == -1, true); + +objects = dbg.findObjects({ class: "Date" }); +assertEq(objects.indexOf(dw) != -1, true); +assertEq(objects.indexOf(rew) == -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-08.js b/js/src/jit-test/tests/debug/Debugger-findObjects-08.js new file mode 100644 index 0000000000..fd6ceb1031 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-08.js @@ -0,0 +1,12 @@ +// Passing bad query properties to Debugger.prototype.findScripts throws. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger(); +var g = newGlobal(); + +assertThrowsInstanceOf(() => dbg.findObjects({ class: null }), TypeError); +assertThrowsInstanceOf(() => dbg.findObjects({ class: true }), TypeError); +assertThrowsInstanceOf(() => dbg.findObjects({ class: 1337 }), TypeError); +assertThrowsInstanceOf(() => dbg.findObjects({ class: /re/ }), TypeError); +assertThrowsInstanceOf(() => dbg.findObjects({ class: {} }), TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-09.js b/js/src/jit-test/tests/debug/Debugger-findObjects-09.js new file mode 100644 index 0000000000..4d95d3abf9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-09.js @@ -0,0 +1,9 @@ +// We don't return objects where our query's class name is the prefix of the +// object's class name and vice versa. + +var dbg = new Debugger(); +var g = newGlobal({newCompartment: true}); +var gw = dbg.addDebuggee(g); + +assertEq(dbg.findObjects({ class: "Objec" }).length, 0); +assertEq(dbg.findObjects({ class: "Objectttttt" }).length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-10.js b/js/src/jit-test/tests/debug/Debugger-findObjects-10.js new file mode 100644 index 0000000000..d490dac26c --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-10.js @@ -0,0 +1,5 @@ +// Debugger.prototype.findObjects should not expose internal JSFunction objects. + +var g = newGlobal({newCompartment: true}); +g.eval(`function f() { return function() {}; }`); +new Debugger(g).findObjects(); diff --git a/js/src/jit-test/tests/debug/Debugger-findObjects-11.js b/js/src/jit-test/tests/debug/Debugger-findObjects-11.js new file mode 100644 index 0000000000..8c50a95ed6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findObjects-11.js @@ -0,0 +1,7 @@ +// This shouldn't segfault. + +var g = newGlobal({newCompartment: true}); +g.eval(`function f() { return function() { + function g() {} +}; }`); +new Debugger(g).findObjects(); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-01.js b/js/src/jit-test/tests/debug/Debugger-findScripts-01.js new file mode 100644 index 0000000000..e39691605b --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-01.js @@ -0,0 +1,4 @@ +// In a debugger with no debuggees, findScripts should return no scripts. + +var dbg = new Debugger; +assertEq(dbg.findScripts().length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-02.js b/js/src/jit-test/tests/debug/Debugger-findScripts-02.js new file mode 100644 index 0000000000..5a2b7cbfbf --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-02.js @@ -0,0 +1,16 @@ +// In a debuggee with functions, findScripts finds those functions' scripts. + +var g = newGlobal({newCompartment: true}); +g.eval('function f(){}'); +g.eval('function g(){}'); +g.eval('function h(){}'); + +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var fw = gw.makeDebuggeeValue(g.f); +var gw = gw.makeDebuggeeValue(g.g); +var hw = gw.makeDebuggeeValue(g.h); + +assertEq(dbg.findScripts().indexOf(fw.script) != -1, true); +assertEq(dbg.findScripts().indexOf(gw.script) != -1, true); +assertEq(dbg.findScripts().indexOf(hw.script) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-03.js b/js/src/jit-test/tests/debug/Debugger-findScripts-03.js new file mode 100644 index 0000000000..2bc9d84de8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-03.js @@ -0,0 +1,16 @@ +// While eval code is running, findScripts returns its script. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +g.check = function () { + log += 'c'; + var frame = dbg.getNewestFrame(); + assertEq(frame.type, "eval"); + assertEq(dbg.findScripts().indexOf(frame.script) != -1, true); +}; + +log = ''; +g.eval('check()'); +assertEq(log, 'c'); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-04.js b/js/src/jit-test/tests/debug/Debugger-findScripts-04.js new file mode 100644 index 0000000000..8d81d5174e --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-04.js @@ -0,0 +1,27 @@ +// Within a series of evals and calls, all their frames' scripts appear in findScripts' result. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +g.check = function () { + log += 'c'; + var scripts = dbg.findScripts(); + + var innerEvalFrame = dbg.getNewestFrame(); + assertEq(innerEvalFrame.type, "eval"); + assertEq(scripts.indexOf(innerEvalFrame.script) != -1, true); + + var callFrame = innerEvalFrame.older; + assertEq(callFrame.type, "call"); + assertEq(scripts.indexOf(callFrame.script) != -1, true); + + var outerEvalFrame = callFrame.older; + assertEq(outerEvalFrame.type, "eval"); + assertEq(scripts.indexOf(outerEvalFrame.script) != -1, true); + assertEq(innerEvalFrame != outerEvalFrame, true); +}; + +g.eval('function f() { eval("check();") }'); +log = ''; +g.eval('f();'); +assertEq(log, 'c'); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-05.js b/js/src/jit-test/tests/debug/Debugger-findScripts-05.js new file mode 100644 index 0000000000..b82dfda34b --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-05.js @@ -0,0 +1,18 @@ +// findScripts' result includes scripts for nested functions. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var log; + +g.eval('function f() { return function g() { return function h() { return "Squee!"; } } }'); +var fw = gw.makeDebuggeeValue(g.f); +var gw = gw.makeDebuggeeValue(g.f()); +var hw = gw.makeDebuggeeValue(g.f()()); + +assertEq(fw.script != gw.script, true); +assertEq(fw.script != hw.script, true); + +var scripts = dbg.findScripts(); +assertEq(scripts.indexOf(fw.script) != -1, true); +assertEq(scripts.indexOf(gw.script) != -1, true); +assertEq(scripts.indexOf(hw.script) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-06.js b/js/src/jit-test/tests/debug/Debugger-findScripts-06.js new file mode 100644 index 0000000000..129aa48423 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-06.js @@ -0,0 +1,13 @@ +// In a debugger with multiple debuggees, findScripts finds scripts across all debuggees. +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var g1w = dbg.addDebuggee(g1); +var g2w = dbg.addDebuggee(g2); + +g1.eval('function f() {}'); +g2.eval('function g() {}'); + +var scripts = dbg.findScripts(); +assertEq(scripts.indexOf(g1w.makeDebuggeeValue(g1.f).script) != -1, true); +assertEq(scripts.indexOf(g2w.makeDebuggeeValue(g2.g).script) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-07.js b/js/src/jit-test/tests/debug/Debugger-findScripts-07.js new file mode 100644 index 0000000000..47132b1f13 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-07.js @@ -0,0 +1,33 @@ +// findScripts can filter scripts by global. +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var g3 = newGlobal({newCompartment: true}); + +var dbg = new Debugger(); +var g1w = dbg.addDebuggee(g1); +var g2w = dbg.addDebuggee(g2); + +g1.eval('function f() {}'); +g2.eval('function g() {}'); +g2.eval('function h() {}'); +var g1fw = g1w.makeDebuggeeValue(g1.f); +var g2gw = g2w.makeDebuggeeValue(g2.g); + +var scripts; + +scripts = dbg.findScripts({}); +assertEq(scripts.indexOf(g1fw.script) != -1, true); +assertEq(scripts.indexOf(g2gw.script) != -1, true); + +scripts = dbg.findScripts({global: g1}); +assertEq(scripts.indexOf(g1fw.script) != -1, true); +assertEq(scripts.indexOf(g2gw.script) != -1, false); + +scripts = dbg.findScripts({global: g2}); +assertEq(scripts.indexOf(g1fw.script) != -1, false); +assertEq(scripts.indexOf(g2gw.script) != -1, true); + +scripts = dbg.findScripts({global: g3}); +// findScripts should only return debuggee scripts, and g3 isn't a +// debuggee, so this should be completely empty. +assertEq(scripts.length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-08-script2 b/js/src/jit-test/tests/debug/Debugger-findScripts-08-script2 new file mode 100644 index 0000000000..40b3aafff5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-08-script2 @@ -0,0 +1,3 @@ +// -*- mode: js2 -*- +g1.eval('function g1g() {}'); +g2.eval('function g2g() {}'); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-08.js b/js/src/jit-test/tests/debug/Debugger-findScripts-08.js new file mode 100644 index 0000000000..08b75b0216 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-08.js @@ -0,0 +1,81 @@ +// Debugger.prototype.findScripts can filter scripts by URL. +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var g3 = newGlobal({newCompartment: true}); + +// Define some functions whose url will be this test file. +g1.eval('function g1f() {}'); +g2.eval('function g2f() {}'); + +// Define some functions whose url will be a different file. +url2 = scriptdir + "Debugger-findScripts-08-script2"; +load(url2); + +var dbg = new Debugger(); +var g1w = dbg.addDebuggee(g1); +var g2w = dbg.addDebuggee(g2); +var g3w = dbg.addDebuggee(g3); + +var g1fw = g1w.makeDebuggeeValue(g1.g1f); +var g1gw = g1w.makeDebuggeeValue(g1.g1g); +var g2fw = g2w.makeDebuggeeValue(g2.g2f); +var g2gw = g2w.makeDebuggeeValue(g2.g2g); + +// Find the url of this file. +url = g1fw.script.url; + +var scripts; + +scripts = dbg.findScripts({}); +assertEq(scripts.indexOf(g1fw.script) != -1, true); +assertEq(scripts.indexOf(g1gw.script) != -1, true); +assertEq(scripts.indexOf(g2fw.script) != -1, true); +assertEq(scripts.indexOf(g2gw.script) != -1, true); + +scripts = dbg.findScripts({url:url}); +assertEq(scripts.indexOf(g1fw.script) != -1, true); +assertEq(scripts.indexOf(g1gw.script) != -1, false); +assertEq(scripts.indexOf(g2fw.script) != -1, true); +assertEq(scripts.indexOf(g2gw.script) != -1, false); + +scripts = dbg.findScripts({url:url2}); +assertEq(scripts.indexOf(g1fw.script) != -1, false); +assertEq(scripts.indexOf(g1gw.script) != -1, true); +assertEq(scripts.indexOf(g2fw.script) != -1, false); +assertEq(scripts.indexOf(g2gw.script) != -1, true); + +scripts = dbg.findScripts({url:url, global:g1}); +assertEq(scripts.indexOf(g1fw.script) != -1, true); +assertEq(scripts.indexOf(g1gw.script) != -1, false); +assertEq(scripts.indexOf(g2fw.script) != -1, false); +assertEq(scripts.indexOf(g2gw.script) != -1, false); + +scripts = dbg.findScripts({url:url2, global:g1}); +assertEq(scripts.indexOf(g1fw.script) != -1, false); +assertEq(scripts.indexOf(g1gw.script) != -1, true); +assertEq(scripts.indexOf(g2fw.script) != -1, false); +assertEq(scripts.indexOf(g2gw.script) != -1, false); + +scripts = dbg.findScripts({url:url, global:g2}); +assertEq(scripts.indexOf(g1fw.script) != -1, false); +assertEq(scripts.indexOf(g1gw.script) != -1, false); +assertEq(scripts.indexOf(g2fw.script) != -1, true); +assertEq(scripts.indexOf(g2gw.script) != -1, false); + +scripts = dbg.findScripts({url:url2, global:g2}); +assertEq(scripts.indexOf(g1fw.script) != -1, false); +assertEq(scripts.indexOf(g1gw.script) != -1, false); +assertEq(scripts.indexOf(g2fw.script) != -1, false); +assertEq(scripts.indexOf(g2gw.script) != -1, true); + +scripts = dbg.findScripts({url:"xlerb"}); // "XLERB"??? +assertEq(scripts.indexOf(g1fw.script) != -1, false); +assertEq(scripts.indexOf(g1gw.script) != -1, false); +assertEq(scripts.indexOf(g2fw.script) != -1, false); +assertEq(scripts.indexOf(g2gw.script) != -1, false); + +scripts = dbg.findScripts({url:url, global:g3}); +assertEq(scripts.indexOf(g1fw.script) != -1, false); +assertEq(scripts.indexOf(g1gw.script) != -1, false); +assertEq(scripts.indexOf(g2fw.script) != -1, false); +assertEq(scripts.indexOf(g2gw.script) != -1, false); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-09.js b/js/src/jit-test/tests/debug/Debugger-findScripts-09.js new file mode 100644 index 0000000000..b485415e9f --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-09.js @@ -0,0 +1,45 @@ +// Passing bad query properties to Debugger.prototype.findScripts throws. +load(libdir + 'asserts.js'); + +var dbg = new Debugger(); +var g = newGlobal(); +assertEq(dbg.findScripts().length, 0); +assertEq(dbg.findScripts({}).length, 0); + +assertEq(dbg.findScripts({global:g}).length, 0); +assertThrowsInstanceOf(function () { dbg.findScripts({global:null}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({global:true}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({global:4}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({global:"I must have fruit!"}); }, TypeError); + +assertEq(dbg.findScripts({url:""}).length, 0); +assertThrowsInstanceOf(function () { dbg.findScripts({url:null}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:true}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:4}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:{}}); }, TypeError); + +assertEq(dbg.findScripts({url:"", line:1}).length, 0); +assertEq(dbg.findScripts({url:"", line:numberToDouble(2)}).length, 0); + +// A 'line' property without a 'url' property is verboten. +assertThrowsInstanceOf(function () { dbg.findScripts({line:1}); }, TypeError); + +assertThrowsInstanceOf(function () { dbg.findScripts({url:"",line:null}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:"",line:{}}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:"",line:true}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:"",line:""}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:"",line:0}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:"",line:-1}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({url:"",line:1.5}); }, TypeError); + +// Values of any type for 'innermost' are accepted. +assertEq(dbg.findScripts({url:"", line:1, innermost:true}).length, 0); +assertEq(dbg.findScripts({url:"", line:1, innermost:1}).length, 0); +assertEq(dbg.findScripts({url:"", line:1, innermost:"yes"}).length, 0); +assertEq(dbg.findScripts({url:"", line:1, innermost:{}}).length, 0); +assertEq(dbg.findScripts({url:"", line:1, innermost:[]}).length, 0); + +// An 'innermost' property without 'url' and 'line' properties is verboten. +assertThrowsInstanceOf(function () { dbg.findScripts({innermost:true}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({innermost:true, line:1}); }, TypeError); +assertThrowsInstanceOf(function () { dbg.findScripts({innermost:true, url:"foo"}); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-10.js b/js/src/jit-test/tests/debug/Debugger-findScripts-10.js new file mode 100644 index 0000000000..f8ab68dda7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-10.js @@ -0,0 +1,13 @@ +// Specifying a non-debuggee global in a Debugger.prototype.findScripts query should +// cause the query to return no scripts. + +var g1 = newGlobal({newCompartment: true}); +g1.eval('function f(){}'); + +var g2 = newGlobal({newCompartment: true}); +g2.eval('function g(){}'); + +var dbg = new Debugger(g1); +assertEq(dbg.findScripts({global:g1}).length > 0, true); +assertEq(dbg.findScripts({global:g2}).length, 0); +assertEq(dbg.findScripts({global:this}).length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-11-script2 b/js/src/jit-test/tests/debug/Debugger-findScripts-11-script2 new file mode 100644 index 0000000000..7a170645e8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-11-script2 @@ -0,0 +1,18 @@ +// -*- mode: js2 -*- +// Line numbers in this file are checked in Debugger-findScripts-11.js. + +// line 3 + +var x = ""; +function f() { + x += "the map"; // line 8 + return function g() { + return "to me what you have stolen"; // line 10 + }; +} + +function h(x, y) { + if (x == 0) return y+1; // line 15 + if (y == 0) return h(x-1, 1); + return h(x-1, h(x, y-1)); +} diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-11.js b/js/src/jit-test/tests/debug/Debugger-findScripts-11.js new file mode 100644 index 0000000000..4cf9a54a88 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-11.js @@ -0,0 +1,36 @@ +// Debugger.prototype.findScripts can filter scripts by line number. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +var scriptname = scriptdir + 'Debugger-findScripts-11-script2'; +g.load(scriptname); + +var gfw = gw.makeDebuggeeValue(g.f); +var ggw = gw.makeDebuggeeValue(g.f()); +var ghw = gw.makeDebuggeeValue(g.h); + +// Specifying a line outside of all functions screens out all function scripts. +assertEq(dbg.findScripts({url:scriptname, line:3}).indexOf(gfw.script) != -1, false); +assertEq(dbg.findScripts({url:scriptname, line:3}).indexOf(ggw.script) != -1, false); +assertEq(dbg.findScripts({url:scriptname, line:3}).indexOf(ghw.script) != -1, false); + +// Specifying a different url screens out scripts, even when global and line match. +assertEq(dbg.findScripts({url:"xlerb", line:8}).indexOf(gfw.script) != -1, false); +assertEq(dbg.findScripts({url:"xlerb", line:8}).indexOf(ggw.script) != -1, false); +assertEq(dbg.findScripts({url:"xlerb", line:8}).indexOf(ghw.script) != -1, false); + +// A line number within a function selects that function's script. +assertEq(dbg.findScripts({url:scriptname, line:8}).indexOf(gfw.script) != -1, true); +assertEq(dbg.findScripts({url:scriptname, line:8}).indexOf(ggw.script) != -1, false); +assertEq(dbg.findScripts({url:scriptname, line:8}).indexOf(ghw.script) != -1, false); + +// A line number within a nested function selects all enclosing functions' scripts. +assertEq(dbg.findScripts({url:scriptname, line:10}).indexOf(gfw.script) != -1, true); +assertEq(dbg.findScripts({url:scriptname, line:10}).indexOf(ggw.script) != -1, true); +assertEq(dbg.findScripts({url:scriptname, line:10}).indexOf(ghw.script) != -1, false); + +// A line number in a non-nested function selects that function. +assertEq(dbg.findScripts({url:scriptname, line:15}).indexOf(gfw.script) != -1, false); +assertEq(dbg.findScripts({url:scriptname, line:15}).indexOf(ggw.script) != -1, false); +assertEq(dbg.findScripts({url:scriptname, line:15}).indexOf(ghw.script) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-12-script1 b/js/src/jit-test/tests/debug/Debugger-findScripts-12-script1 new file mode 100644 index 0000000000..f9f484e970 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-12-script1 @@ -0,0 +1,19 @@ +// -*- mode: js2 -*- +// Script for Debugger-findScripts-12.js to load. +// Line numbers in this script are cited in the test. + +function f() { + // line 6 + function ff() { + return "my wuv, I want you always beside me"; // line 8 + }; + ff.global = this; + return ff; +}; + +function g() { + return "to Oz"; // line 15 +} + +f.global = this; +g.global = this; diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-12-script2 b/js/src/jit-test/tests/debug/Debugger-findScripts-12-script2 new file mode 100644 index 0000000000..3350c8ed29 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-12-script2 @@ -0,0 +1,19 @@ +// -*- mode: js2 -*- +// Script for Debugger-findScripts-12.js to load. +// Line numbers in this script are cited in the test, and must align with ...-script1. + +function h() { + // line 6 + function hh() { + return "on investment"; // line 8 + }; + hh.global = this; + return hh; +}; + +function i() { + return "to innocence"; // line 15 +} + +h.global = this; +i.global = this; diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-12.js b/js/src/jit-test/tests/debug/Debugger-findScripts-12.js new file mode 100644 index 0000000000..912e3d3301 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-12.js @@ -0,0 +1,129 @@ +// Debugger.prototype.findScripts can filter by global, url, and line number. + +// Two scripts, with different functions at the same line numbers. +var url1 = scriptdir + 'Debugger-findScripts-12-script1'; +var url2 = scriptdir + 'Debugger-findScripts-12-script2'; + +// Three globals: two with code, one with nothing. +var g1 = newGlobal({newCompartment: true}); +g1.toSource = () => "[global g1]"; +g1.load(url1); +g1.load(url2); +var g2 = newGlobal({newCompartment: true}); +g2.toSource = () => "[global g2]"; +g2.load(url1); +g2.load(url2); +var g3 = newGlobal({newCompartment: true}); + +var dbg = new Debugger(g1, g2, g3); + +function script(func) { + var gw = dbg.addDebuggee(func.global); + var script = gw.makeDebuggeeValue(func).script; + script.toString = function () { + return "[Debugger.Script for " + func.name + " in " + JSON.stringify(func.global) + "]"; + }; + return script; +} + +// The function scripts we know of. There may be random eval scripts involved, but +// we don't care about those. +var allScripts = ([g1.f, g1.f(), g1.g, g1.h, g1.h(), g1.i, + g2.f, g2.f(), g2.g, g2.h, g2.h(), g2.i].map(script)); + +// Search for scripts using |query|, expecting no members of allScripts +// except those given in |expected| in the result. If |expected| is +// omitted, expect no members of allScripts at all. +function queryExpectOnly(query, expected) { + print(); + print("queryExpectOnly(" + JSON.stringify(query) + ")"); + var scripts = dbg.findScripts(query); + var present = allScripts.filter(function (s) { return scripts.indexOf(s) != -1; }); + if (expected) { + expected = expected.map(script); + expected.forEach(function (s) { + if (present.indexOf(s) == -1) + assertEq(s + " not present", "is present"); + }); + present.forEach(function (s) { + if (expected.indexOf(s) == -1) + assertEq(s + " is present", "not present"); + }); + } else { + assertEq(present.length, 0); + } +} + +// We have twelve functions: two globals, each with two urls, each +// defining three functions. Show that all the different combinations of +// query parameters select what they should. + +// There are gaps in the pattern: +// - You can only filter by line if you're also filtering by url. +// - You can't ask for only the innermost scripts unless you're filtering by line. + +// Filtering by global, url, and line produces one function, or two +// where they are nested. +queryExpectOnly({ global:g1, url:url1, line: 6 }, [g1.f ]); +queryExpectOnly({ global:g1, url:url1, line: 8 }, [g1.f, g1.f()]); +queryExpectOnly({ global:g1, url:url1, line: 15 }, [g1.g ]); +queryExpectOnly({ global:g1, url:url2, line: 6 }, [g1.h ]); +queryExpectOnly({ global:g1, url:url2, line: 8 }, [g1.h, g1.h()]); +queryExpectOnly({ global:g1, url:url2, line: 15 }, [g1.i ]); +queryExpectOnly({ global:g2, url:url1, line: 6 }, [g2.f ]); +queryExpectOnly({ global:g2, url:url1, line: 8 }, [g2.f, g2.f()]); +queryExpectOnly({ global:g2, url:url1, line: 15 }, [g2.g ]); +queryExpectOnly({ global:g2, url:url2, line: 6 }, [g2.h ]); +queryExpectOnly({ global:g2, url:url2, line: 8 }, [g2.h, g2.h()]); +queryExpectOnly({ global:g2, url:url2, line: 15 }, [g2.i ]); + +// Filtering by global, url, and line, and requesting only the innermost +// function at each point, should produce only one function. +queryExpectOnly({ global:g1, url:url1, line: 6, innermost: true }, [g1.f ]); +queryExpectOnly({ global:g1, url:url1, line: 8, innermost: true }, [g1.f()]); +queryExpectOnly({ global:g1, url:url1, line: 15, innermost: true }, [g1.g ]); +queryExpectOnly({ global:g1, url:url2, line: 6, innermost: true }, [g1.h ]); +queryExpectOnly({ global:g1, url:url2, line: 8, innermost: true }, [g1.h()]); +queryExpectOnly({ global:g1, url:url2, line: 15, innermost: true }, [g1.i ]); +queryExpectOnly({ global:g2, url:url1, line: 6, innermost: true }, [g2.f ]); +queryExpectOnly({ global:g2, url:url1, line: 8, innermost: true }, [g2.f()]); +queryExpectOnly({ global:g2, url:url1, line: 15, innermost: true }, [g2.g ]); +queryExpectOnly({ global:g2, url:url2, line: 6, innermost: true }, [g2.h ]); +queryExpectOnly({ global:g2, url:url2, line: 8, innermost: true }, [g2.h()]); +queryExpectOnly({ global:g2, url:url2, line: 15, innermost: true }, [g2.i ]); + +// Filtering by url and global should produce sets of three scripts. +queryExpectOnly({ global:g1, url:url1 }, [g1.f, g1.f(), g1.g]); +queryExpectOnly({ global:g1, url:url2 }, [g1.h, g1.h(), g1.i]); +queryExpectOnly({ global:g2, url:url1 }, [g2.f, g2.f(), g2.g]); +queryExpectOnly({ global:g2, url:url2 }, [g2.h, g2.h(), g2.i]); + +// Filtering by url and line, innermost-only, should produce sets of two scripts, +// or four where there are nested functions. +queryExpectOnly({ url:url1, line: 6 }, [g1.f, g2.f ]); +queryExpectOnly({ url:url1, line: 8 }, [g1.f, g1.f(), g2.f, g2.f()]); +queryExpectOnly({ url:url1, line:15 }, [g1.g, g2.g ]); +queryExpectOnly({ url:url2, line: 6 }, [g1.h, g2.h ]); +queryExpectOnly({ url:url2, line: 8 }, [g1.h, g1.h(), g2.h, g2.h()]); +queryExpectOnly({ url:url2, line:15 }, [g1.i, g2.i ]); + +// Filtering by url and line, and requesting only the innermost scripts, +// should always produce pairs of scripts. +queryExpectOnly({ url:url1, line: 6, innermost: true }, [g1.f, g2.f ]); +queryExpectOnly({ url:url1, line: 8, innermost: true }, [g1.f(), g2.f()]); +queryExpectOnly({ url:url1, line:15, innermost: true }, [g1.g, g2.g ]); +queryExpectOnly({ url:url2, line: 6, innermost: true }, [g1.h, g2.h ]); +queryExpectOnly({ url:url2, line: 8, innermost: true }, [g1.h(), g2.h()]); +queryExpectOnly({ url:url2, line:15, innermost: true }, [g1.i, g2.i ]); + +// Filtering by global only should produce sets of six scripts. +queryExpectOnly({ global:g1 }, [g1.f, g1.f(), g1.g, g1.h, g1.h(), g1.i]); +queryExpectOnly({ global:g2 }, [g2.f, g2.f(), g2.g, g2.h, g2.h(), g2.i]); + +// Filtering by url should produce sets of six scripts. +queryExpectOnly({ url:url1 }, [g1.f, g1.f(), g1.g, g2.f, g2.f(), g2.g]); +queryExpectOnly({ url:url2 }, [g1.h, g1.h(), g1.i, g2.h, g2.h(), g2.i]); + +// Filtering by no axes should produce all twelve scripts. +queryExpectOnly({}, [g1.f, g1.f(), g1.g, g1.h, g1.h(), g1.i, + g2.f, g2.f(), g2.g, g2.h, g2.h(), g2.i]); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-14.js b/js/src/jit-test/tests/debug/Debugger-findScripts-14.js new file mode 100644 index 0000000000..f02a1b4e11 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-14.js @@ -0,0 +1,30 @@ +// Debugger.prototype.findScripts can find the innermost script at a given +// source location. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +function script(f) { + return gw.makeDebuggeeValue(f).script; +} + +function arrayIsOnly(array, element) { + return array.length == 1 && array[0] === element; +} + +url = scriptdir + 'Debugger-findScripts-14.script1'; +g.load(url); + +var scripts; + +// When we're doing 'innermost' queries, we don't have to worry about finding +// random eval scripts: we should get exactly one script, for the function +// covering that line. +scripts = dbg.findScripts({url:url, line:4, innermost:true}); +assertEq(arrayIsOnly(scripts, script(g.f)), true); + +scripts = dbg.findScripts({url:url, line:6, innermost:true}); +assertEq(arrayIsOnly(scripts, script(g.f())), true); + +scripts = dbg.findScripts({url:url, line:8, innermost:true}); +assertEq(arrayIsOnly(scripts, script(g.f()())), true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-14.script1 b/js/src/jit-test/tests/debug/Debugger-findScripts-14.script1 new file mode 100644 index 0000000000..00da459fc1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-14.script1 @@ -0,0 +1,12 @@ +// -*- mode:js2 -*- + +function f() { + var x = 1; // line 4 + return function g() { + var y = 2; // line 6 + return function h() { + var z = 3; // line 8 + return x+y+z; + }; + }; +} diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-15.js b/js/src/jit-test/tests/debug/Debugger-findScripts-15.js new file mode 100644 index 0000000000..d9cd8fb966 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-15.js @@ -0,0 +1,9 @@ +// findScripts finds non-compile-and-go scripts + +var g = newGlobal({newCompartment: true}); +g.evaluate("function f(x) { return x + 1; }"); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var s = dbg.findScripts(); +var fw = gw.getOwnPropertyDescriptor("f").value; +assertEq(s.indexOf(fw.script) !== -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-16.js b/js/src/jit-test/tests/debug/Debugger-findScripts-16.js new file mode 100644 index 0000000000..9d735d9186 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-16.js @@ -0,0 +1,12 @@ +// Bug 744731 - findScripts() finds active debugger executeInGlobal scripts. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(dbg.findScripts().indexOf(dbg.getNewestFrame().script) !== -1, true); +}; +gw.executeInGlobal("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-17.js b/js/src/jit-test/tests/debug/Debugger-findScripts-17.js new file mode 100644 index 0000000000..3c1fc56937 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-17.js @@ -0,0 +1,15 @@ +// Bug 744731 - findScripts() finds active debugger frame.eval scripts. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(dbg.findScripts().indexOf(dbg.getNewestFrame().script) !== -1, true); + }; + frame.eval("debugger;"); +}; +g.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-18.js b/js/src/jit-test/tests/debug/Debugger-findScripts-18.js new file mode 100644 index 0000000000..ea5ee0d318 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-18.js @@ -0,0 +1,46 @@ +// In a debuggee with multiple scripts with varying displayURLs (aka //# +// sourceURL), findScripts can filter by displayURL. + +var g = newGlobal({newCompartment: true}); + +g.eval("function f(){} //# sourceURL=f.js"); +g.eval("function g(){} //# sourceURL=g.js"); +g.eval("function h(){}"); + +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var fw = gw.makeDebuggeeValue(g.f); +var ggw = gw.makeDebuggeeValue(g.g); +var hw = gw.makeDebuggeeValue(g.h); + +var fScripts = dbg.findScripts({ displayURL: "f.js" }); +assertEq(fScripts.indexOf(fw.script) != -1, true); +assertEq(fScripts.indexOf(ggw.script), -1); +assertEq(fScripts.indexOf(hw.script), -1); + + +fScripts = dbg.findScripts({ displayURL: "f.js", + line: 1 }); +assertEq(fScripts.indexOf(fw.script) != -1, true); +assertEq(fScripts.indexOf(ggw.script), -1); +assertEq(fScripts.indexOf(hw.script), -1); + +var gScripts = dbg.findScripts({ displayURL: "g.js" }); +assertEq(gScripts.indexOf(ggw.script) != -1, true); +assertEq(gScripts.indexOf(fw.script), -1); +assertEq(gScripts.indexOf(hw.script), -1); + +var allScripts = dbg.findScripts(); +assertEq(allScripts.indexOf(fw.script) != -1, true); +assertEq(allScripts.indexOf(ggw.script) != -1, true); +assertEq(allScripts.indexOf(hw.script) != -1, true); + +try { + dbg.findScripts({ displayURL: 3 }); + // Should never get here because the above line should throw + // JSMSG_UNEXPECTED_TYPE. + assertEq(true, false); +} catch(e) { + assertEq(e.name, "TypeError"); + assertEq(e.message.includes("displayURL"), true); +} diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-19.js b/js/src/jit-test/tests/debug/Debugger-findScripts-19.js new file mode 100644 index 0000000000..46021a0bbc --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-19.js @@ -0,0 +1,5 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +try { g.eval('function drag(ev) {'); } catch (ex) { } +for (s of dbg.findScripts()) + s.lineCount; diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-20.js b/js/src/jit-test/tests/debug/Debugger-findScripts-20.js new file mode 100644 index 0000000000..04c73c0972 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-20.js @@ -0,0 +1,20 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval('function f(){}'); + +var o = gw.makeDebuggeeValue(g.f); + +var allScripts = dbg.findScripts(); +var scripts = dbg.findScripts({ + source: o.script.source +}); +assertEq(scripts.length, allScripts.length); +assertEq(scripts.indexOf(o.script) !== -1, true); + +scripts = dbg.findScripts({ + source: o.script.source, + line: 1 +}); +assertEq(scripts.indexOf(o.script) !== -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-22.js b/js/src/jit-test/tests/debug/Debugger-findScripts-22.js new file mode 100644 index 0000000000..9f2884a325 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-22.js @@ -0,0 +1,8 @@ +// Bug 1239813: Don't let compartments get GC'd while findScripts is holding +// them in its ScriptQuery's hash set. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +dbg.addDebuggee(g); +g = newGlobal({sameZoneAs: g.Math}); +dbg.findScripts({get source() { gc(); return undefined; }}); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-23.js b/js/src/jit-test/tests/debug/Debugger-findScripts-23.js new file mode 100644 index 0000000000..bb45365a69 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-23.js @@ -0,0 +1,21 @@ +// |jit-test| skip-if: isLcovEnabled() + +// If a script is (re)lazified, findScripts should not delazify it. + +var dbg = new Debugger(); + +var g = newGlobal({newCompartment: true}); +g.eval('function f(){}'); +assertEq(g.eval('isLazyFunction(f)'), true); + +dbg.addDebuggee(g); +dbg.findScripts(); +assertEq(g.eval('isLazyFunction(f)'), true); + +dbg.removeAllDebuggees(); +relazifyFunctions(); +assertEq(g.eval('isLazyFunction(f)'), true); + +dbg.addDebuggee(g); +var scripts = dbg.findScripts(); +assertEq(g.eval('isLazyFunction(f)'), true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-24.js b/js/src/jit-test/tests/debug/Debugger-findScripts-24.js new file mode 100644 index 0000000000..54b37d811f --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-24.js @@ -0,0 +1,35 @@ +// findScripts should reject Debugger.Source objects from other Debuggers. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +g.evaluate(`function f() { print("earth/heart/hater"); }`, + { lineNumber: 1800 }); + +var dbg1 = new Debugger; +var gDO1 = dbg1.addDebuggee(g); +var fDO1 = gDO1.getOwnPropertyDescriptor('f').value; +assertEq(fDO1.script.source instanceof Debugger.Source, true); + +var dbg2 = new Debugger; +var gDO2 = dbg2.addDebuggee(g); +var fDO2 = gDO2.getOwnPropertyDescriptor('f').value; +assertEq(fDO2.script.source instanceof Debugger.Source, true); + +assertEq(fDO1.script.source !== fDO2.script.source, true); + +// findScripts should reject Debugger.Source objects that don't belong to the +// Debugger it's being invoked on. +assertThrowsInstanceOf(() => dbg1.findScripts({ source: fDO2.script.source }), + TypeError); +assertThrowsInstanceOf(() => dbg2.findScripts({ source: fDO1.script.source }), + TypeError); + +// findScripts should reject Debugger.Source.prototype. +assertThrowsInstanceOf(() => dbg1.findScripts({ source: Debugger.Source.prototype }), + TypeError); + +// These should not throw, and should return something, but we're not going to +// be picky about exactly what, given findScript's sensitivity to GC. +dbg1.findScripts({ source: fDO1.script.source }); +dbg2.findScripts({ source: fDO2.script.source }); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-25.js b/js/src/jit-test/tests/debug/Debugger-findScripts-25.js new file mode 100644 index 0000000000..3985b78c53 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-25.js @@ -0,0 +1,14 @@ +// In a debugger with multiple debuggees, findScripts finds scripts across all debuggees. +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var g1w = dbg.addDebuggee(g1); +var g2w = dbg.addDebuggee(g2); + +// Add eval() to make functions non-relazifiable. +g1.eval('function f() { eval(""); }'); +g2.eval('function g() { eval(""); }'); + +var scripts = dbg.findScripts(); +assertEq(scripts.indexOf(g1w.makeDebuggeeValue(g1.f).script) != -1, true); +assertEq(scripts.indexOf(g2w.makeDebuggeeValue(g2.g).script) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-26.js b/js/src/jit-test/tests/debug/Debugger-findScripts-26.js new file mode 100644 index 0000000000..1c1510aa4f --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-26.js @@ -0,0 +1,18 @@ +// |jit-test| skip-if: !('oomTest' in this) +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` +function f() { + function inner() { + var x; + } +} +`); + +let url = thisFilename(); +let line = 4; + +oomTest(() => { dbg.findScripts({url, line}) }); +oomTest(() => { dbg.findScripts({url, line, innermost: true}) }); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-27.js b/js/src/jit-test/tests/debug/Debugger-findScripts-27.js new file mode 100644 index 0000000000..2a6513011b --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-27.js @@ -0,0 +1,22 @@ +// |jit-test| skip-if: isLcovEnabled() +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` +function f() { + function inner() { + var x; + } +} +`); + +// GC will collect top-level script of the eval +gc(); +gc(); + +let url = thisFilename(); +let line = 4; + +// Function `f` and `inner` should still match +assertEq(dbg.findScripts({url, line}).length, 2); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-28.js b/js/src/jit-test/tests/debug/Debugger-findScripts-28.js new file mode 100644 index 0000000000..30097bac3d --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-28.js @@ -0,0 +1,26 @@ +// |jit-test| skip-if: isLcovEnabled() +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` +function f() { + function inner() { + var x; + } + function asm_mod() { + "use asm"; + function mtd() {} + return { mtd: mtd } + } +} +`); + +gc(); +gc(); + +let url = thisFilename(); +let line = 4; + +// Function `f` and `inner` should still match +assertEq(dbg.findScripts({url, line}).length, 2); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-29.js b/js/src/jit-test/tests/debug/Debugger-findScripts-29.js new file mode 100644 index 0000000000..c0a4d5e604 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-29.js @@ -0,0 +1,21 @@ +// |jit-test| skip-if: isLcovEnabled() + +// If the specified line is the next line after the function, +// the function shouldn't match. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` +function f() { +} +`); + +gc(); +gc(); + +let url = thisFilename(); + +assertEq(dbg.findScripts({url, line: 3}).length, 1); +assertEq(dbg.findScripts({url, line: 4}).length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-30.js b/js/src/jit-test/tests/debug/Debugger-findScripts-30.js new file mode 100644 index 0000000000..01b7a624c2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-30.js @@ -0,0 +1,21 @@ +// |jit-test| skip-if: isLcovEnabled() + +// If the specified line is the next line after the function, +// the function shouldn't match. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` +var a = x => x + +1; +`); + +gc(); +gc(); + +let url = thisFilename(); + +assertEq(dbg.findScripts({url, line: 3}).length, 1); +assertEq(dbg.findScripts({url, line: 4}).length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-31.js b/js/src/jit-test/tests/debug/Debugger-findScripts-31.js new file mode 100644 index 0000000000..6be8a2d2fd --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-31.js @@ -0,0 +1,21 @@ +// |jit-test| skip-if: isLcovEnabled() + +// If the specified line is the next line after the function, +// the function shouldn't match. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` +var a = x => (x + 1 +); +`); + +gc(); +gc(); + +let url = thisFilename(); + +assertEq(dbg.findScripts({url, line: 3}).length, 1); +assertEq(dbg.findScripts({url, line: 4}).length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-delazify.js b/js/src/jit-test/tests/debug/Debugger-findScripts-delazify.js new file mode 100644 index 0000000000..554b5dd2c8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-delazify.js @@ -0,0 +1,485 @@ +// |jit-test| skip-if: isLcovEnabled() + +// findScript should try to avoid delazifying unnecessarily. + +function newTestcase(code) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger(); + var gw = dbg.addDebuggee(g); + + var lines = code.split('\n'); + // Returns the line number of the line with "<= line". + // + 1 for 1-origin. + var line = lines.findIndex(x => x.includes("<= line")) + 1; + + g.eval(code); + + // Functions are relazified, and the top-level script is dropped. + relazifyFunctions(); + + return [dbg, g, line]; +} + +var url = thisFilename(); +var dbg, g, line, scripts; + +// If the specified line is inside the function body, only the function should +// be delazified. +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +// <= line +} +function f3() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 1); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f2"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), true); + +// If the functions starts at the specified line, the function shouldn't be +// the first function to delazify. +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +function f3() { // <= line +} +function f4() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 1); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f3"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +// f2 is delazified because f3 cannot be the first function to delazify. +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +// Multiple functions in the specified line, and one of them starts before +// the specified line. +// All functions should be returned, and others shouldn't be delazified. +[dbg, g, line] = newTestcase(` +function f1() {} +function f2() { +} function f3() {} function f4() {} function f5() { // <= line +} +function f6() {} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); +assertEq(g.eval(`isLazyFunction(f5)`), true); +assertEq(g.eval(`isLazyFunction(f6)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 4); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f2,f3,f4,f5"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), false); +assertEq(g.eval(`isLazyFunction(f5)`), false); +assertEq(g.eval(`isLazyFunction(f6)`), true); + +// The same rule should apply to inner functions. +[dbg, g, line] = newTestcase(` +function f1() {} +function f2() { + function g1() { + } + function g2() { + function h1() {} + function h2() { + } function h3() {} function h4() {} function h5() { // <= line + } + function h6() {} + + return [h1, h2, h3, h4, h5, h6]; + } + function g3() { + } + + return [g1, g2, g3]; +} +function f3() {} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 6); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f2,g2,h2,h3,h4,h5"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), true); +g.eval(`var [g1, g2, g3] = f2();`); +assertEq(g.eval(`isLazyFunction(g1)`), true); +assertEq(g.eval(`isLazyFunction(g2)`), false); +assertEq(g.eval(`isLazyFunction(g3)`), true); +g.eval(`var [h1, h2, h3, h4, h5, h6] = g2();`); +assertEq(g.eval(`isLazyFunction(h1)`), true); +assertEq(g.eval(`isLazyFunction(h2)`), false); +assertEq(g.eval(`isLazyFunction(h3)`), false); +assertEq(g.eval(`isLazyFunction(h4)`), false); +assertEq(g.eval(`isLazyFunction(h5)`), false); +assertEq(g.eval(`isLazyFunction(h6)`), true); + +// The same rule should apply to functions inside parameter expression. +[dbg, g, line] = newTestcase(` +function f1( + a = function g1() {}, + b = function g2() { + }, c = function g3() {}, d = function g4() { // <= line + }, + e = function g5() {}, +) { + return [a, b, c, d, e]; +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 4); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f1,g2,g3,g4"); + +assertEq(g.eval(`isLazyFunction(f1)`), false); + +g.eval(`var [g1, g2, g3, g4, g5] = f1();`); + +assertEq(g.eval(`isLazyFunction(g1)`), true); +assertEq(g.eval(`isLazyFunction(g2)`), false); +assertEq(g.eval(`isLazyFunction(g3)`), false); +assertEq(g.eval(`isLazyFunction(g4)`), false); +assertEq(g.eval(`isLazyFunction(g5)`), true); + +// The same should apply to function inside method with computed property. +[dbg, g, line] = newTestcase(` +var f1, f2, f3; +var O = { + [(f1 = () => 0, "m1")]() { + }, + [(f2 = () => 0, "m2")](p2) { // <= line + }, + [(f3 = () => 0, "m3")]() { + }, +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(O.m2)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(O.m1)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(O.m3)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 2); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f2,"); +// Use parameterNames because displayName isn't set for computed property. +assertEq(scripts.map(s => `${s.displayName}(${s.parameterNames.join(",")})`) + .sort().join(","), "f2(),undefined(p2)"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +// m1 is delazified because f2 cannot be the first function to delazify. +assertEq(g.eval(`isLazyFunction(O.m1)`), false); +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(O.m2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(O.m3)`), true); + +[dbg, g, line] = newTestcase(` +var f1, f2, f3; +var O = { + [(f1 = () => 0, "m1")]() { + }, + [(f2 = () => 0 + + 1, "m2")](p2) { // <= line + }, + [(f3 = () => 0, "m3")]() { + }, +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(O.m1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(O.m2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(O.m3)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 2); +assertEq(scripts.map(s => `${s.displayName}(${s.parameterNames.join(",")})`) + .sort().join(","), "f2(),undefined(p2)"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(O.m1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(O.m2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(O.m3)`), true); + +// Class constructor shouldn't be delazified even if methods match. +[dbg, g, line] = newTestcase(` +// Use variable to access across eval. +var C = class { + constructor() { + } + m1() {} + m2() { + } m3() {} m4() { // <= line + } + m5() {} +} +`); + +assertEq(g.eval(`isLazyFunction(C)`), true); +assertEq(g.eval(`isLazyFunction(C.prototype.m1)`), true); +assertEq(g.eval(`isLazyFunction(C.prototype.m2)`), true); +assertEq(g.eval(`isLazyFunction(C.prototype.m3)`), true); +assertEq(g.eval(`isLazyFunction(C.prototype.m4)`), true); +assertEq(g.eval(`isLazyFunction(C.prototype.m5)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 3); +assertEq(scripts.map(s => s.displayName).sort().join(","), "m2,m3,m4"); + +assertEq(g.eval(`isLazyFunction(C)`), true); +assertEq(g.eval(`isLazyFunction(C.prototype.m1)`), true); +assertEq(g.eval(`isLazyFunction(C.prototype.m2)`), false); +assertEq(g.eval(`isLazyFunction(C.prototype.m3)`), false); +assertEq(g.eval(`isLazyFunction(C.prototype.m4)`), false); +assertEq(g.eval(`isLazyFunction(C.prototype.m5)`), true); + +// If the line is placed before sourceStart, the function shouldn't match, +// and the function shouldn't be delazified. +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +function +// <= line +f3() { +} +function f4() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 0); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +// f2 is delazified because it's the first function before the specified line. +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +function f3 +// <= line +() { +} +function f4() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 0); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +// f2 is delazified because it's the first function before the specified line. +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +function f3 +( // <= line +) { +} +function f4() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 1); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f3"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +// f2 is delazified because it's the first function _before_ the specified line. +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +function f3 +( +// <= line +) { +} +function f4() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 1); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f3"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +// If the specified line is the next line after the function ends, +// nothing should match, but the function should be delazified. +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +// <= line +function f3() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 0); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), false); +assertEq(g.eval(`isLazyFunction(f3)`), true); + +// The matching non-lazy script should prevent the previous function's +// delazification. +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +function f3() { + // <= line +} +function f4() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +// Delazify f3. +g.eval(`f3()`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 1); +assertEq(scripts.map(s => s.displayName).sort().join(","), "f3"); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +// The non-matching non-lazy script should prevent the previous function's +// delazification. +[dbg, g, line] = newTestcase(` +function f1() { +} +function f2() { +} +function f3() { +} +// <= line +function f4() { +} +`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), true); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +// Delazify f3. +g.eval(`f3()`); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), true); + +scripts = dbg.findScripts({url, line}); +assertEq(scripts.length, 0); + +assertEq(g.eval(`isLazyFunction(f1)`), true); +assertEq(g.eval(`isLazyFunction(f2)`), true); +assertEq(g.eval(`isLazyFunction(f3)`), false); +assertEq(g.eval(`isLazyFunction(f4)`), true); diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-optimized-out.js b/js/src/jit-test/tests/debug/Debugger-findScripts-optimized-out.js new file mode 100644 index 0000000000..1c38bbbf4e --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-optimized-out.js @@ -0,0 +1,31 @@ +// Accessing Debugger.Script's properties which triggers delazification can +// fail if the function for the script is optimized out. +// It shouldn't crash but just throw an error. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +g.eval(` +function enclosing() { + (function g1() {}); + (function* g2() {}); + (async function g3() {}); + (async function* g4() {}); + () => {}; + async () => {}; +} +`); + +for (const s of dbg.findScripts()) { + if (!s.displayName) { + continue; + } + + try { + s.lineCount; // don't assert + } catch (exc) { + // If that didn't throw, it's fine. If it did, check the message. + assertEq(exc.message, "function is optimized out"); + } +} diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-uncompleted-01.js b/js/src/jit-test/tests/debug/Debugger-findScripts-uncompleted-01.js new file mode 100644 index 0000000000..4a7e9139ce --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-uncompleted-01.js @@ -0,0 +1,54 @@ +// |jit-test| skip-if: isLcovEnabled() + +// Uncompleted scripts and their inner scripts shouldn't be found in +// findScripts. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +let message = ""; +try { + g.eval(` +(function nonLazyOuter() { + (function nonLazyInner() { + function lazy1() { + function lazy2() { + } + } + })(); +})(); + +(function uncompletedNonLazy() { + function lazyInUncompleted1() { + function lazyInUncompleted2() { + } + } + // LazyScripts for above 2 functions are created when parsing, + // and JSScript for uncompletedNonLazy is created at the beginning of + // compiling it, but it doesn't get code() since the following switch-case + // throws error while emitting bytecode. + switch (1) { + ${"case 1:".repeat(2**16+1)} + } +})(); +`); +} catch (e) { + message = e.message; +} + +assertEq(message.includes("too many switch cases"), true, + "Error for switch-case should be thrown," + + "in order to test the case that uncompleted script is created"); + +for (var script of dbg.findScripts()) { + // Since all of above scripts can be GCed, we cannot check the set of + // found scripts. + if (script.displayName) { + assertEq(script.displayName != "uncompletedNonLazy", true, + "Uncompiled script shouldn't be found"); + assertEq(script.displayName != "lazyInUncompleted1", true, + "Scripts inside uncompiled script shouldn't be found"); + assertEq(script.displayName != "lazyInUncompleted2", true, + "Scripts inside uncompiled script shouldn't be found"); + } +} diff --git a/js/src/jit-test/tests/debug/Debugger-findScripts-uncompleted-02.js b/js/src/jit-test/tests/debug/Debugger-findScripts-uncompleted-02.js new file mode 100644 index 0000000000..c1201a5c6f --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findScripts-uncompleted-02.js @@ -0,0 +1,49 @@ +// Uncompleted scripts shouldn't be found in findScripts. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +let message = ""; +try { + g.eval(` +(function nonLazyOuter() { + (function nonLazyInner() { + function lazy1() { + function lazy2() { + } + } + })(); +})(); + +(function uncompletedNonLazy() { + (function completedNonLazy() { + function lazyInCompleted1() { + function lazyInCompleted2() { + } + } + })(); + // completedNonLazy and its inner scripts can be exposed to debugger since + // the compilation for completedNonLazy finishes, even if the enclosing + // uncompletedNonLazy fails to compile. + switch (1) { + ${"case 1:".repeat(2**16+1)} + } +})(); +`); +} catch (e) { + message = e.message; +} + +assertEq(message.includes("too many switch cases"), true, + "Error for switch-case should be thrown," + + "in order to test the case that uncompleted script is created"); + +let actualNames = []; +for (var script of dbg.findScripts()) { + // Since all of above scripts can be GCed, we cannot check the set of + // found scripts. + if (script.displayName) { + assertEq(script.displayName != "uncompletedNonLazy", true, + "Uncompiled script shouldn't be found"); + } +} diff --git a/js/src/jit-test/tests/debug/Debugger-findSourceURLs.js b/js/src/jit-test/tests/debug/Debugger-findSourceURLs.js new file mode 100644 index 0000000000..d00ade6325 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findSourceURLs.js @@ -0,0 +1,18 @@ +// findSourceURLs should return all URLs compiled in debuggee realms, +// except when a shrinking GC has occurred. + +let g = newGlobal({newCompartment: true}); +let startNumber = gcparam("gcNumber"); +g.evaluate("function foo() {}", { fileName: "foo.js" }); +g.evaluate("function bar() {}", { fileName: "bar.js" }); +g.evaluate("function baz() {}", { fileName: "baz.js" }); + +let dbg = new Debugger(g); +let urls = dbg.findSourceURLs(); + +let endNumber = gcparam("gcNumber"); +if (startNumber == endNumber) { + assertEq(urls.includes("foo.js"), true); + assertEq(urls.includes("bar.js"), true); + assertEq(urls.includes("baz.js"), true); +} diff --git a/js/src/jit-test/tests/debug/Debugger-findSources-01.js b/js/src/jit-test/tests/debug/Debugger-findSources-01.js new file mode 100644 index 0000000000..4cac711e52 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findSources-01.js @@ -0,0 +1,4 @@ +// In a debugger with no debuggees, findSources should return no scripts. + +const dbg = new Debugger; +assertEq(dbg.findSources().length, 0); diff --git a/js/src/jit-test/tests/debug/Debugger-findSources-02.js b/js/src/jit-test/tests/debug/Debugger-findSources-02.js new file mode 100644 index 0000000000..532d39da12 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findSources-02.js @@ -0,0 +1,15 @@ +// In a debugger with scripts, findSources finds the script source. + +const g = newGlobal({newCompartment: true}); +// Declare a function in order to keep the script source alive across GC. +g.evaluate(`function fa() {}`, { fileName: "a.js" }); +g.evaluate(`function fb() {}`, { fileName: "b.js" }); +g.evaluate(`function fc() {}`, { fileName: "c.js" }); + +const dbg = new Debugger(); +const gw = dbg.addDebuggee(g); + +const sources = dbg.findSources(); +assertEq(sources.filter(s => s.url == "a.js").length, 1); +assertEq(sources.filter(s => s.url == "b.js").length, 1); +assertEq(sources.filter(s => s.url == "c.js").length, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-findSources-03.js b/js/src/jit-test/tests/debug/Debugger-findSources-03.js new file mode 100644 index 0000000000..9cda3a432a --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-findSources-03.js @@ -0,0 +1,19 @@ +// In a debugger with multiple debuggees, findSources finds script sources across all debuggees. + +const g1 = newGlobal({newCompartment: true}); +const g2 = newGlobal({newCompartment: true}); +// Declare a function in order to keep the script source alive across GC. +g1.evaluate(`function fa() {}`, { fileName: "a.js" }); +g1.evaluate(`function fb() {}`, { fileName: "b.js" }); +g2.evaluate(`function fc() {}`, { fileName: "c.js" }); +g2.evaluate(`function fd() {}`, { fileName: "d.js" }); + +const dbg = new Debugger(); +const g1w = dbg.addDebuggee(g1); +const g2w = dbg.addDebuggee(g2); + +const sources = dbg.findSources(); +assertEq(dbg.findSources().filter(s => s.url == "a.js").length, 1); +assertEq(dbg.findSources().filter(s => s.url == "b.js").length, 1); +assertEq(dbg.findSources().filter(s => s.url == "c.js").length, 1); +assertEq(dbg.findSources().filter(s => s.url == "d.js").length, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-getNewestFrame-01.js b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-01.js new file mode 100644 index 0000000000..b1cd194a53 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-01.js @@ -0,0 +1,20 @@ +// getNewestFrame basics. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +assertEq(dbg.getNewestFrame(), null); + +var global = this; +var frame; +function f() { + frame = dbg.getNewestFrame(); + assertEq(frame instanceof Debugger.Frame, true); + assertEq(frame.type, "eval"); + assertEq(frame.older, null); +} +g.h = this; +g.eval("h.f()"); +assertEq(frame.onStack, false); +assertThrowsInstanceOf(function () { frame.older; }, Error); diff --git a/js/src/jit-test/tests/debug/Debugger-getNewestFrame-02.js b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-02.js new file mode 100644 index 0000000000..799159af32 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-02.js @@ -0,0 +1,20 @@ +// Hooks and Debugger.prototype.getNewestFrame produce the same Frame object. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +var savedFrame, savedCallee; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame, savedFrame); + assertEq(frame.onStack, true); + assertEq(frame.callee, savedCallee); + hits++; +}; +g.h = function () { + savedFrame = dbg.getNewestFrame(); + savedCallee = savedFrame.callee; + assertEq(savedCallee.name, "f"); +}; +g.eval("function f() { h(); debugger; }"); +g.f(); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-getNewestFrame-03.js b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-03.js new file mode 100644 index 0000000000..b9396e2f92 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-03.js @@ -0,0 +1,9 @@ +// Debugger.prototype.getNewestFrame() ignores dummy frames. +// See bug 678086. + +var g = newGlobal({newCompartment: true}); +g.f = function () { return dbg.getNewestFrame(); }; +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var fw = gw.getOwnPropertyDescriptor("f").value; +assertEq(fw.call().return, null); diff --git a/js/src/jit-test/tests/debug/Debugger-getNewestFrame-generators-01.js b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-generators-01.js new file mode 100644 index 0000000000..908bd4f2f3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-generators-01.js @@ -0,0 +1,49 @@ +// Generator/async frames can be created and revived by calling Debugger.getNewestFrame(). +// +// Modified copy of Frame-older-generators-01.js. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* gen() { + f(); + yield 1; + f(); + } + function* genDefaults(x=f()) { + f(); + } + async function af() { + f(); + await 1; + f(); + } + async function afDefaults(x=f()) { + await 1; + f(); + } +`); + +function test(expected, code) { + let dbg = Debugger(g); + let hits = 0; + let genFrame = null; + g.f = () => { + hits++; + let frame = dbg.getNewestFrame(); + if (genFrame === null) { + genFrame = frame; + } else { + assertEq(frame, genFrame); + } + assertEq(genFrame.callee.name, expected); + } + + g.eval(code); + assertEq(hits, 2); + dbg.removeDebuggee(g); +} + +test("gen", "for (var x of gen()) {}"); +test("genDefaults", "for (var x of genDefaults()) {}"); +test("af", "af(); drainJobQueue();"); +test("afDefaults", "afDefaults(); drainJobQueue();") diff --git a/js/src/jit-test/tests/debug/Debugger-isCompilableUnit.js b/js/src/jit-test/tests/debug/Debugger-isCompilableUnit.js new file mode 100644 index 0000000000..dbfde0ef46 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-isCompilableUnit.js @@ -0,0 +1,58 @@ +load(libdir + "asserts.js"); + +const bad_types = [ + 2112, + {geddy: "lee"}, + () => 1, + [], + Array +] + +// We only accept strings around here! +for (var badType of bad_types) { + assertThrowsInstanceOf(() => { + Debugger.isCompilableUnit(badType); + }, TypeError); +} + +const compilable_units = [ + "wubba-lubba-dub-dub", + "'Get Schwifty!'", + "1 + 2", + "function f(x) {}", + "function x(...f,) {", // statements with bad syntax are always compilable + "let x = 100", + ";;;;;;;;", + "", + " ", + "\n", + "let x", +] + +const non_compilable_units = [ + "function f(x) {", + "(...d) =>", + "{geddy:", + "{", + "[1, 2", + "[", + "1 +", + "let x =", + "3 ==", +] + +for (var code of compilable_units) { + assertEq(Debugger.isCompilableUnit(code), true); +} + +for (var code of non_compilable_units) { + assertEq(Debugger.isCompilableUnit(code), false); +} + +// Supplying no arguments should throw a type error +assertThrowsInstanceOf(() => { + Debugger.isCompilableUnit(); +}, TypeError); + +// Supplying extra arguments should be fine +assertEq(Debugger.isCompilableUnit("", 1, 2, 3, 4, {}, []), true); diff --git a/js/src/jit-test/tests/debug/Debugger-multi-01.js b/js/src/jit-test/tests/debug/Debugger-multi-01.js new file mode 100644 index 0000000000..c986accdbb --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-multi-01.js @@ -0,0 +1,31 @@ +// When there are multiple debuggers, their hooks are called in order. + +var g = newGlobal({newCompartment: true}); +var log; +var arr = []; + +function addDebug(msg) { + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function (stack) { log += msg; }; + arr.push(dbg); +} + +addDebug('a'); +addDebug('b'); +addDebug('c'); + +log = ''; +assertEq(g.eval("debugger; 0;"), 0); +assertEq(log, 'abc'); + +// Calling debugger hooks continues, even if one returns a resumption value +// other than undefined. + +arr[0].onDebuggerStatement = function (stack) { + log += 'a'; + return {return: 1}; +}; + +log = ''; +assertEq(g.eval("debugger; 0;"), 1); +assertEq(log, 'abc'); diff --git a/js/src/jit-test/tests/debug/Debugger-multi-02.js b/js/src/jit-test/tests/debug/Debugger-multi-02.js new file mode 100644 index 0000000000..bff152b0df --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-multi-02.js @@ -0,0 +1,32 @@ +// Test adding hooks during dispatch. The behavior is deterministic and "nice", +// but mainly what we are checking here is that we do not crash due to +// modifying a data structure while we're iterating over it. + +var g = newGlobal({newCompartment: true}); +var n = 0; +var hits; + +function addDebugger() { + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function (stack) { + hits++; + addDebugger(); + }; +} + +addDebugger(); // now there is one enabled Debugger +hits = 0; +g.eval("debugger;"); // after this there are two +assertEq(hits, 1); + +hits = 0; +g.eval("debugger;"); // after this there are four +assertEq(hits, 2); + +hits = 0; +g.eval("debugger;"); // after this there are eight +assertEq(hits, 4); + +hits = 0; +g.eval("debugger;"); +assertEq(hits, 8); diff --git a/js/src/jit-test/tests/debug/Debugger-multi-03.js b/js/src/jit-test/tests/debug/Debugger-multi-03.js new file mode 100644 index 0000000000..cd89fdead1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-multi-03.js @@ -0,0 +1,21 @@ +// Q: But who shall debug the debuggers? A: jimb + +var log = ''; + +function addDebug(g, id) { + var debuggerGlobal = newGlobal({newCompartment: true}); + debuggerGlobal.debuggee = g; + debuggerGlobal.id = id; + debuggerGlobal.print = function (s) { log += s; }; + debuggerGlobal.eval( + 'var dbg = new Debugger(debuggee);\n' + + 'dbg.onDebuggerStatement = function () { print(id); debugger; print(id); };\n'); + return debuggerGlobal; +} + +var base = newGlobal({newCompartment: true}); +var top = base; +for (var i = 0; i < 8; i++) // why have 2 debuggers when you can have 8 + top = addDebug(top, i); +base.eval("debugger;"); +assertEq(log, '0123456776543210'); diff --git a/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-01.js b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-01.js new file mode 100644 index 0000000000..b2f0b779cf --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-01.js @@ -0,0 +1,45 @@ +// If debugger.onEnterFrame returns {return:val}, the frame returns. + +var g = newGlobal({newCompartment: true}); +g.set = false; +g.eval("function f() {\n" + + " set = true;\n" + + " return 'fail';\n" + + "}\n"); +g.eval("function g() { return 'g ' + f(); }"); +g.eval("function h() { return 'h ' + g(); }"); + +var dbg = Debugger(g); +var savedFrame; +dbg.onEnterFrame = function (frame) { + savedFrame = frame; + return {return: "pass"}; +}; + +// Call g.f as a function. +savedFrame = undefined; +assertEq(g.f(), "pass"); +assertEq(savedFrame.onStack, false); +assertEq(g.set, false); + +// Call g.f as a constructor. +savedFrame = undefined; +var r = new g.f; +assertEq(typeof r, "object"); +assertEq(savedFrame.onStack, false); +assertEq(g.set, false); + +var count = 0; +dbg.onEnterFrame = function (frame) { + count++; + if (count == 3) { + savedFrame = frame; + return {return: "pass"}; + } + return undefined; +}; +g.set = false; +savedFrame = undefined; +assertEq(g.h(), "h g pass"); +assertEq(savedFrame.onStack, false); +assertEq(g.set, false); diff --git a/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-02.js b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-02.js new file mode 100644 index 0000000000..9a8778b9cc --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-02.js @@ -0,0 +1,28 @@ +// If debugger.onEnterFrame returns {throw:val}, an exception is thrown in the debuggee. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.set = false; +g.eval("function f() {\n" + + " set = true;\n" + + " return 'fail';\n" + + "}\n"); + +var dbg = Debugger(g); +var savedFrame; +dbg.onEnterFrame = function (frame) { + savedFrame = frame; + return {throw: "pass"}; +}; + +savedFrame = undefined; +assertThrowsValue(g.f, "pass"); +assertEq(savedFrame.onStack, false); +assertEq(g.set, false); + +savedFrame = undefined; +assertThrowsValue(function () { new g.f; }, "pass"); +assertEq(savedFrame.onStack, false); +assertEq(g.set, false); + diff --git a/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-03.js b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-03.js new file mode 100644 index 0000000000..0da20925b0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-03.js @@ -0,0 +1,26 @@ +// If debugger.onEnterFrame returns null, the debuggee is terminated immediately. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.set = false; + +var dbg = Debugger(g); +var savedFrame; +dbg.onDebuggerStatement = function (frame) { + var innerSavedFrame; + dbg.onEnterFrame = function (frame) { + innerSavedFrame = frame; + return null; + }; + // Using frame.eval lets us catch termination. + assertEq(frame.eval("set = true;"), null); + assertEq(innerSavedFrame.onStack, false); + savedFrame = frame; + return { return: "pass" }; +}; + +savedFrame = undefined; +assertEq(g.eval("debugger;"), "pass"); +assertEq(savedFrame.onStack, false); +assertEq(g.set, false); diff --git a/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-04.js b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-04.js new file mode 100644 index 0000000000..fae18adff7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-04.js @@ -0,0 +1,16 @@ +// If debugger.onEnterFrame returns undefined, the frame should continue execution. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +var savedFrame; +dbg.onEnterFrame = function (frame) { + hits++; + savedFrame = frame; + return undefined; +}; + +savedFrame = undefined; +assertEq(g.eval("'pass';"), "pass"); +assertEq(savedFrame.onStack, false); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-05.js b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-05.js new file mode 100644 index 0000000000..8885aa0d80 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-05.js @@ -0,0 +1,98 @@ +// Exercise the call to ScriptDebugPrologue in js_InternalInterpret. + +// This may change, but as of this writing, inline caches (ICs) are +// disabled in debug mode, and those are the only users of the out-of-line entry +// points for JIT code (arityCheckEntry, argsCheckEntry, fastEntry); debug +// mode uses only invokeEntry. This means most of the bytecode tails in +// js_InternalInterpret that might call ScriptPrologue or ScriptEpilogue are +// unreachable in debug mode: they're only called from the out-of-line entry +// points. +// +// The exception is REJOIN_THIS_PROTOTYPE, which can be reached reliably if you +// add a JS_GC call to stubs::GetPropNoCache. JIT code calls that stub to +// retrieve the 'prototype' property of a function called as a constructor, if +// TI can't establish the exact identity of that prototype's value at compile +// time. Thus the preoccupation with constructors here. + +load(libdir + "asserts.js"); + +var debuggee = newGlobal({newCompartment: true}); +var dbg = Debugger(debuggee); +var hits, savedFrame; + +// Allow the constructor to return normally. +dbg.onEnterFrame = function (frame) { + hits++; + if (frame.constructing) { + savedFrame = frame; + assertEq(savedFrame.onStack, true); + return undefined; + } + return undefined; +}; +hits = 0; +debuggee.hits = 0; +savedFrame = undefined; +assertEq(typeof debuggee.eval("function f(){ hits++; } f.prototype = {}; new f;"), "object"); +assertEq(hits, 2); +assertEq(savedFrame.onStack, false); +assertEq(debuggee.hits, 1); + +// Force an early return from the constructor. +dbg.onEnterFrame = function (frame) { + hits++; + if (frame.constructing) { + savedFrame = frame; + assertEq(savedFrame.onStack, true); + return { return: "pass" }; + } + return undefined; +}; +hits = 0; +debuggee.hits = 0; +savedFrame = undefined; +assertEq(typeof debuggee.eval("function f(){ hits++; } f.prototype = {}; new f;"), "object"); +assertEq(hits, 2); +assertEq(savedFrame.onStack, false); +assertEq(debuggee.hits, 0); + +// Force the constructor to throw an exception. +dbg.onEnterFrame = function (frame) { + hits++; + if (frame.constructing) { + savedFrame = frame; + assertEq(savedFrame.onStack, true); + return { throw: "pass" }; + } + return undefined; +}; +hits = 0; +debuggee.hits = 0; +savedFrame = undefined; +assertThrowsValue(function () { + debuggee.eval("function f(){ hits++ } f.prototype = {}; new f;"); + }, "pass"); +assertEq(hits, 2); +assertEq(savedFrame.onStack, false); +assertEq(debuggee.hits, 0); + +// Ensure that forcing an early return only returns from one JS call. +debuggee.eval("function g() { var result = new f; g_hits++; return result; }"); +dbg.onEnterFrame = function (frame) { + hits++; + if (frame.constructing) { + savedFrame = frame; + assertEq(savedFrame.onStack, true); + return { return: "pass" }; + } + return undefined; +}; +hits = 0; +debuggee.hits = 0; +debuggee.g_hits = 0; +savedFrame = undefined; +assertEq(typeof debuggee.eval("g();"), "object"); +assertEq(hits, 3); +assertEq(savedFrame.onStack, false); +assertEq(debuggee.hits, 0); +assertEq(debuggee.g_hits, 1); diff --git a/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-06.js b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-06.js new file mode 100644 index 0000000000..c515475e9d --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onEnterFrame-resumption-06.js @@ -0,0 +1,31 @@ +// |jit-test| error:all-jobs-completed-successfully +// Verifiy that onEnterFrame's force-return queues the promise microtask +// to run in the debuggee's job queue, not the debugger's +// AutoDebuggerJobQueueInterruption. + +let g = newGlobal({ newCompartment: true }); +g.eval(` + async function asyncFn(x) { + await Promise.resolve(); + } + function enterDebuggee(){} +`); +const dbg = new Debugger(g); + +(async function() { + let it = g.asyncFn(); + + // Force-return when the await resumes. + dbg.onEnterFrame = () => { + dbg.onEnterFrame = undefined; + return { return: "exit" }; + }; + + const result = await it; + assertEq(result, "exit"); + // If execution here is resumed from the debugger's queue, this call will + // trigger DebuggeeWouldRun exception. + g.enterDebuggee(); + + throw "all-jobs-completed-successfully"; +})(); diff --git a/js/src/jit-test/tests/debug/Debugger-onNativeCall-01.js b/js/src/jit-test/tests/debug/Debugger-onNativeCall-01.js new file mode 100644 index 0000000000..4ee9c6f387 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNativeCall-01.js @@ -0,0 +1,64 @@ +// Test that the onNativeCall hook is called when expected. + +load(libdir + 'eqArrayHelper.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +g.eval(` +const x = []; +Object.defineProperty(x, "a", { + get: print, + set: print, +}); +function f() { + x.a++; + x.push(4); +} +`); + +for (let i = 0; i < 5; i++) { + g.f(); +} + +const rv = []; +dbg.onNativeCall = (callee, reason) => { rv.push(callee.name, reason); }; + +var dbg2 = Debugger(g); +var gdbg2 = dbg2.addDebuggee(g); + +const fscript = gdbg.getOwnPropertyDescriptor('f').value.script; + +for (let i = 0; i < 5; i++) { + // The onNativeCall hook is called when doing global evaluations. + rv.length = 0; + gdbg.executeInGlobal(`f()`); + assertEqArray(rv, ["print", "get", "print", "set", "push", "call"]); + + // The onNativeCall hook is called when doing frame evaluations. + let handlerCalled = false; + const handler = { + hit(frame) { + fscript.clearBreakpoint(handler); + rv.length = 0; + frame.eval(`f()`); + assertEqArray(rv, ["print", "get", "print", "set", "push", "call"]); + handlerCalled = true; + }, + }; + fscript.setBreakpoint(fscript.mainOffset, handler); + g.f(); + assertEq(handlerCalled, true); + + // The onNativeCall hook is *not* called when not in a debugger evaluation. + rv.length = 0; + g.f(); + assertEqArray(rv, []); + + // The onNativeCall hook is *not* called when in a debugger evaluation + // associated with a different debugger. + rv.length = 0; + gdbg2.executeInGlobal(`f()`); + assertEqArray(rv, []); +} diff --git a/js/src/jit-test/tests/debug/Debugger-onNativeCall-02.js b/js/src/jit-test/tests/debug/Debugger-onNativeCall-02.js new file mode 100644 index 0000000000..9e372d179a --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNativeCall-02.js @@ -0,0 +1,61 @@ +// Test that the onNativeCall hook can control the call's behavior. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +g.eval(` +var x = []; +Object.defineProperty(x, "a", { + get: print, + set: print, +}); +var rv; +function f() { + x.a++; + try { + rv = x.push(4); + } catch (e) { + throw "rethrowing"; + } +} +`); + +for (let i = 0; i < 5; i++) { + g.f(); +} + +for (let i = 0; i < 5; i++) { + // Test terminating execution. + dbg.onNativeCall = (callee, reason) => { + return null; + }; + const len = g.x.length; + let v = gdbg.executeInGlobal(`f()`); + assertEq(v, null); + assertEq(g.x.length, len); + + // Test throwing an exception. + dbg.onNativeCall = (callee, reason) => { + return { throw: "throwing" }; + }; + v = gdbg.executeInGlobal(`f()`); + assertEq(v.throw, "throwing"); + + // Test throwing an exception #2. + dbg.onNativeCall = (callee, reason) => { + if (callee.name == "push") { + return { throw: "throwing" }; + } + }; + v = gdbg.executeInGlobal(`f()`); + assertEq(v.throw, "rethrowing"); + + // Test returning a different value from the native. + dbg.onNativeCall = (callee, reason) => { + return { return: "value" }; + }; + v = gdbg.executeInGlobal(`f()`); + assertEq(v.return, undefined); + assertEq(g.rv, "value"); +} diff --git a/js/src/jit-test/tests/debug/Debugger-onNativeCall-03.js b/js/src/jit-test/tests/debug/Debugger-onNativeCall-03.js new file mode 100644 index 0000000000..265edb556c --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNativeCall-03.js @@ -0,0 +1,24 @@ +// Test onNativeCall's behavior when used with self-hosted functions. + +load(libdir + 'eqArrayHelper.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +const rv = []; + +dbg.onEnterFrame = f => { + rv.push("EnterFrame"); +}; + +dbg.onNativeCall = f => { + rv.push(f.displayName); +}; + +gdbg.executeInGlobal(` + var x = [1,3,2]; + x.sort((a, b) => {print(a)}); +`); + +assertEqArray(rv, ["EnterFrame", "sort", "EnterFrame", "print", "EnterFrame", "print"]); diff --git a/js/src/jit-test/tests/debug/Debugger-onNativeCall-04.js b/js/src/jit-test/tests/debug/Debugger-onNativeCall-04.js new file mode 100644 index 0000000000..d3ee1377bf --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNativeCall-04.js @@ -0,0 +1,26 @@ +// Test that onNativeCall behaves correctly when a debugger eval might enter the +// JIT via OSR. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +g.eval(` +const x = []; +function f() { + for (let i = 0; i < 5; i++) { + x.push(i); + } +} +`); + +let numCalls = 0; +dbg.onNativeCall = callee => { assertEq(callee.name, "push"); numCalls++; }; + +var dbg2 = Debugger(g); + +for (let i = 0; i < 5; i++) { + numCalls = 0; + gdbg.executeInGlobal(`f()`); + assertEq(numCalls, 5); +} diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-01.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-01.js new file mode 100644 index 0000000000..736d29ad03 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-01.js @@ -0,0 +1,64 @@ +// Debugger.prototype.onNewGlobalObject surfaces. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger; + +function f() { } +function g() { } + +assertEq(Object.getOwnPropertyDescriptor(dbg, 'onNewGlobalObject'), undefined); + +var d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(dbg), 'onNewGlobalObject'); +assertEq(d.enumerable, false); +assertEq(d.configurable, true); +assertEq(typeof d.get, "function"); +assertEq(typeof d.set, "function"); + +assertEq(dbg.onNewGlobalObject, undefined); + +assertThrowsInstanceOf(function () { dbg.onNewGlobalObject = ''; }, TypeError); +assertEq(dbg.onNewGlobalObject, undefined); + +assertThrowsInstanceOf(function () { dbg.onNewGlobalObject = false; }, TypeError); +assertEq(dbg.onNewGlobalObject, undefined); + +assertThrowsInstanceOf(function () { dbg.onNewGlobalObject = 0; }, TypeError); +assertEq(dbg.onNewGlobalObject, undefined); + +assertThrowsInstanceOf(function () { dbg.onNewGlobalObject = Math.PI; }, TypeError); +assertEq(dbg.onNewGlobalObject, undefined); + +assertThrowsInstanceOf(function () { dbg.onNewGlobalObject = null; }, TypeError); +assertEq(dbg.onNewGlobalObject, undefined); + +assertThrowsInstanceOf(function () { dbg.onNewGlobalObject = {}; }, TypeError); +assertEq(dbg.onNewGlobalObject, undefined); + +// But any function, even a useless one, is okay. How fair is that? +dbg.onNewGlobalObject = f; +assertEq(dbg.onNewGlobalObject, f); + +dbg.onNewGlobalObject = undefined; +assertEq(dbg.onNewGlobalObject, undefined); + +var dbg2 = new Debugger; +assertEq(dbg.onNewGlobalObject, undefined); +assertEq(dbg2.onNewGlobalObject, undefined); + +dbg.onNewGlobalObject = f; +assertEq(dbg.onNewGlobalObject, f); +assertEq(dbg2.onNewGlobalObject, undefined); + +dbg2.onNewGlobalObject = g; +assertEq(dbg.onNewGlobalObject, f); +assertEq(dbg2.onNewGlobalObject, g); + +dbg.onNewGlobalObject = undefined; +assertEq(dbg.onNewGlobalObject, undefined); +assertEq(dbg2.onNewGlobalObject, g); + +// You shouldn't be able to apply the accessor to the prototype. +assertThrowsInstanceOf(function () { + Debugger.prototype.onNewGlobalObject = function () { }; + }, TypeError); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-02.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-02.js new file mode 100644 index 0000000000..c8c3d74add --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-02.js @@ -0,0 +1,23 @@ +// onNewGlobalObject handlers fire, until they are removed. + +var dbg = new Debugger; +var log; + +log = ''; +newGlobal(); +assertEq(log, ''); + +dbg.onNewGlobalObject = function (global) { + log += 'n'; + assertEq(global.seen, undefined); + global.seen = true; +}; + +log = ''; +newGlobal(); +assertEq(log, 'n'); + +log = ''; +dbg.onNewGlobalObject = undefined; +newGlobal(); +assertEq(log, ''); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-03.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-03.js new file mode 100644 index 0000000000..76a3649a2f --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-03.js @@ -0,0 +1,40 @@ +// onNewGlobalObject handlers on different Debugger instances are independent. + +var dbg1 = new Debugger; +var log1; +function h1(global) { + log1 += 'n'; + assertEq(global.seen, undefined); + global.seen = true; +} + +var dbg2 = new Debugger; +var log2; +function h2(global) { + log2 += 'n'; + assertEq(global.seen, undefined); + global.seen = true; +} + +log1 = log2 = ''; +newGlobal(); +assertEq(log1, ''); +assertEq(log2, ''); + +log1 = log2 = ''; +dbg1.onNewGlobalObject = h1; +newGlobal(); +assertEq(log1, 'n'); +assertEq(log2, ''); + +log1 = log2 = ''; +dbg2.onNewGlobalObject = h2; +newGlobal(); +assertEq(log1, 'n'); +assertEq(log2, 'n'); + +log1 = log2 = ''; +dbg1.onNewGlobalObject = undefined; +newGlobal(); +assertEq(log1, ''); +assertEq(log2, 'n'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-04.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-04.js new file mode 100644 index 0000000000..6e02d19bdc --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-04.js @@ -0,0 +1,14 @@ +// onNewGlobalObject handlers only fire on enabled Debuggers. + +var dbg = new Debugger; +var log; + +dbg.onNewGlobalObject = function (global) { + log += 'n'; + assertEq(global.seen, undefined); + global.seen = true; +}; + +log = ''; +newGlobal(); +assertEq(log, 'n'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-05.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-05.js new file mode 100644 index 0000000000..4ec3edc198 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-05.js @@ -0,0 +1,13 @@ +// An onNewGlobalObject handler can disable itself. + +var dbg = new Debugger; +var log; + +dbg.onNewGlobalObject = function (global) { + log += 'n'; + dbg.onNewGlobalObject = undefined; +}; + +log = ''; +newGlobal(); +assertEq(log, 'n'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-06.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-06.js new file mode 100644 index 0000000000..f58f4e7ee1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-06.js @@ -0,0 +1,20 @@ +// One Debugger's onNewGlobalObject handler can disable another Debugger's handler. + +var dbg1 = new Debugger; +var dbg2 = new Debugger; +var dbg3 = new Debugger; +var log; +var hit; + +function handler(global) { + hit++; + log += hit; + if (hit == 2) + dbg1.onNewGlobalObject = dbg2.onNewGlobalObject = dbg3.onNewGlobalObject = undefined; +}; + +log = ''; +hit = 0; +dbg1.onNewGlobalObject = dbg2.onNewGlobalObject = dbg3.onNewGlobalObject = handler; +newGlobal(); +assertEq(log, '12'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-07.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-07.js new file mode 100644 index 0000000000..eb79ea4fcc --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-07.js @@ -0,0 +1,18 @@ +// One Debugger's onNewGlobalObject handler can disable other Debuggers. + +var dbg1 = new Debugger; +var dbg2 = new Debugger; +var dbg3 = new Debugger; +var log; +var hit; + +function handler(global) { + hit++; + log += hit; +}; + +log = ''; +hit = 0; +dbg1.onNewGlobalObject = dbg2.onNewGlobalObject = dbg3.onNewGlobalObject = handler; +newGlobal(); +assertEq(log, '123'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-08.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-08.js new file mode 100644 index 0000000000..f981e8ae06 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-08.js @@ -0,0 +1,26 @@ +// Creating a global within an onNewGlobalObject handler causes a recursive handler invocation. +// +// This isn't really desirable behavior, as presumably a global created while a +// handler is running is one the debugger is creating for its own purposes and +// should not be observed, but if this behavior changes, we sure want to know. + +var dbg = new Debugger; +var log; +var depth; + +dbg.onNewGlobalObject = function (global) { + log += '('; depth++; + + assertEq(global.seen, undefined); + global.seen = true; + + if (depth < 3) + newGlobal(); + + log += ')'; depth--; +}; + +log = ''; +depth = 0; +newGlobal(); +assertEq(log, '((()))'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-09.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-09.js new file mode 100644 index 0000000000..1c4f029720 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-09.js @@ -0,0 +1,34 @@ +// Resumption values from onNewGlobalObject handlers are disallowed. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger; +var log; + +dbg.onNewGlobalObject = function (g) { log += 'n'; return undefined; }; +log = ''; +assertEq(typeof newGlobal(), "object"); +assertEq(log, 'n'); + +dbg.uncaughtExceptionHook = function (ex) { assertEq(/disallowed/.test(ex), true); log += 'u'; } +dbg.onNewGlobalObject = function (g) { log += 'n'; return { return: "snoo" }; }; +log = ''; +assertEq(typeof newGlobal(), "object"); +assertEq(log, 'nu'); + +dbg.onNewGlobalObject = function (g) { log += 'n'; return { throw: "snoo" }; }; +log = ''; +assertEq(typeof newGlobal(), "object"); +assertEq(log, 'nu'); + +dbg.onNewGlobalObject = function (g) { log += 'n'; return null; }; +log = ''; +assertEq(typeof newGlobal(), "object"); +assertEq(log, 'nu'); + +dbg.uncaughtExceptionHook = function (ex) { assertEq(/foopy/.test(ex), true); log += 'u'; } +dbg.onNewGlobalObject = function (g) { log += 'n'; throw "foopy"; }; +log = ''; +assertEq(typeof newGlobal(), "object"); +assertEq(log, 'nu'); + diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-10.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-10.js new file mode 100644 index 0000000000..ce90ed541a --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-10.js @@ -0,0 +1,27 @@ +// An earlier onNewGlobalObject handler returning a 'throw' resumption +// value causes later handlers not to run. + +load(libdir + 'asserts.js'); + +var dbg1 = new Debugger; +var dbg2 = new Debugger; +var dbg3 = new Debugger; +var log; +var count; + +dbg1.onNewGlobalObject = dbg2.onNewGlobalObject = dbg3.onNewGlobalObject = function (global) { + count++; + log += count; + if (count == 2) + return { throw: "snoo" }; + return undefined; +}; +dbg2.uncaughtExceptionHook = function (exn) { + assertEq(/disallowed/.test(exn), true); + log += 'u'; +}; + +log = ''; +count = 0; +assertEq(typeof newGlobal(), "object"); +assertEq(log, '12u3'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-11.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-11.js new file mode 100644 index 0000000000..33345980dd --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-11.js @@ -0,0 +1,31 @@ +// Resumption values other than |undefined| from uncaughtExceptionHook from +// onNewGlobalObject handlers are ignored (other than cancelling further hooks). + +load(libdir + 'asserts.js'); + +var dbg = new Debugger; +var log; + +dbg.onNewGlobalObject = function () { + log += 'n'; + throw 'party'; +}; + +dbg.uncaughtExceptionHook = function (ex) { + log += 'u'; + assertEq(ex, 'party'); + return { throw: 'fit' }; +}; + +log = ''; +assertEq(typeof newGlobal(), 'object'); +assertEq(log, 'nu'); + +dbg.uncaughtExceptionHook = function (ex) { + log += 'u'; + assertEq(ex, 'party'); +}; + +log = ''; +assertEq(typeof newGlobal(), 'object'); +assertEq(log, 'nu'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-12.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-12.js new file mode 100644 index 0000000000..591d28f64d --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-12.js @@ -0,0 +1,25 @@ +// Resumption values from uncaughtExceptionHook from onNewGlobalObject +// handlers do not affect the dispatch of the event to other Debugger instances. + +load(libdir + 'asserts.js'); + +var dbg1 = new Debugger; +var dbg2 = new Debugger; +var dbg3 = new Debugger; +var log; + +dbg1.onNewGlobalObject = dbg2.onNewGlobalObject = dbg3.onNewGlobalObject = function () { + log += 'n'; + throw 'party'; +}; + +dbg1.uncaughtExceptionHook = dbg2.uncaughtExceptionHook = dbg3.uncaughtExceptionHook = +function (ex) { + log += 'u'; + assertEq(ex, 'party'); + return { throw: 'fit' }; +}; + +log = ''; +assertEq(typeof newGlobal(), 'object'); +assertEq(log, 'nununu'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-13.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-13.js new file mode 100644 index 0000000000..86cb252c8c --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-13.js @@ -0,0 +1,18 @@ +// onNewGlobalObject handlers receive the correct Debugger.Object instances. + +var dbg = new Debugger; + +var gw = null; +dbg.onNewGlobalObject = function (global) { + assertEq(arguments.length, 1); + assertEq(this, dbg); + gw = global; +}; +var g = newGlobal({newCompartment: true}); +assertEq(typeof gw, 'object'); +assertEq(dbg.addDebuggee(g), gw); + +// The Debugger.Objects passed to onNewGlobalObject are the global itself +// without any cross-compartment wrappers. +// NOTE: They also ignore any WindowProxy that may be associated with global. +assertEq(gw.unwrap(), gw); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-14.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-14.js new file mode 100644 index 0000000000..fb61fab526 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-14.js @@ -0,0 +1,17 @@ +// Globals passed to onNewGlobalObject handers are ready for use immediately. + +var dbg = new Debugger; +var log = ''; +dbg.onNewGlobalObject = function (global) { + log += 'n'; + var gw = dbg.addDebuggee(global); + gw.defineProperty('x', { value: -1 }); + // Check that the global's magic lazy properties are working. + assertEq(gw.executeInGlobalWithBindings('Math.atan2(y,x)', { y: 0 }).return, Math.PI); + // Check that the global's prototype is hooked up. + assertEq(gw.executeInGlobalWithBindings('x.toString()', { x: gw }).return, "[object global]"); +}; + +newGlobal({newCompartment: true}); + +assertEq(log, 'n'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-15.js b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-15.js new file mode 100644 index 0000000000..e6035dd8e3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-15.js @@ -0,0 +1,25 @@ +// Globals marked as invisibleToDebugger behave appropriately. + +load(libdir + "asserts.js"); + +var dbg = new Debugger; +var log = ''; +dbg.onNewGlobalObject = function (global) { + log += 'n'; +} + +assertEq(typeof newGlobal(), "object"); +assertEq(typeof newGlobal({invisibleToDebugger: false}), "object"); +assertEq(log, 'nn'); + +log = ''; +assertEq(typeof newGlobal({newCompartment: true, invisibleToDebugger: true}), "object"); +assertEq(log, ''); + +assertThrowsInstanceOf(() => dbg.addDebuggee(newGlobal({newCompartment: true, invisibleToDebugger: true})), + Error); + +var glob = newGlobal({newCompartment: true, invisibleToDebugger: true}); +dbg.addAllGlobalsAsDebuggees(); +dbg.onDebuggerStatement = function (frame) { assertEq(true, false); }; +glob.eval('debugger'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewPromise-01.js b/js/src/jit-test/tests/debug/Debugger-onNewPromise-01.js new file mode 100644 index 0000000000..9788e027c9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewPromise-01.js @@ -0,0 +1,13 @@ +// Test that the onNewPromise hook gets called when promises are allocated in +// the scope of debuggee globals. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + + +let promisesFound = []; +dbg.onNewPromise = p => { promisesFound.push(p); }; + +let p1 = new g.Promise(function (){}); +assertEq(promisesFound.indexOf(gw.makeDebuggeeValue(p1)) != -1, true); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewPromise-02.js b/js/src/jit-test/tests/debug/Debugger-onNewPromise-02.js new file mode 100644 index 0000000000..61cb8f03ef --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewPromise-02.js @@ -0,0 +1,24 @@ +// onNewPromise handlers fire, until they are removed. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +log = ''; +new g.Promise(function (){}); +assertEq(log, ''); + +dbg.onNewPromise = function (promise) { + log += 'n'; + assertEq(promise.seen, undefined); + promise.seen = true; +}; + +log = ''; +new g.Promise(function (){}); +assertEq(log, 'n'); + +log = ''; +dbg.onNewPromise = undefined; +new g.Promise(function (){}); +assertEq(log, ''); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewPromise-03.js b/js/src/jit-test/tests/debug/Debugger-onNewPromise-03.js new file mode 100644 index 0000000000..38890ae957 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewPromise-03.js @@ -0,0 +1,41 @@ +// onNewPromise handlers on different Debugger instances are independent. + +var g = newGlobal({newCompartment: true}); +var dbg1 = new Debugger(g); +var log1; +function h1(promise) { + log1 += 'n'; + assertEq(promise.seen, undefined); + promise.seen = true; +} + +var dbg2 = new Debugger(g); +var log2; +function h2(promise) { + log2 += 'n'; + assertEq(promise.seen, undefined); + promise.seen = true; +} + +log1 = log2 = ''; +new g.Promise(function (){}); +assertEq(log1, ''); +assertEq(log2, ''); + +log1 = log2 = ''; +dbg1.onNewPromise = h1; +new g.Promise(function (){}); +assertEq(log1, 'n'); +assertEq(log2, ''); + +log1 = log2 = ''; +dbg2.onNewPromise = h2; +new g.Promise(function (){}); +assertEq(log1, 'n'); +assertEq(log2, 'n'); + +log1 = log2 = ''; +dbg1.onNewPromise = undefined; +new g.Promise(function (){}); +assertEq(log1, ''); +assertEq(log2, 'n'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewPromise-04.js b/js/src/jit-test/tests/debug/Debugger-onNewPromise-04.js new file mode 100644 index 0000000000..298ccedd80 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewPromise-04.js @@ -0,0 +1,14 @@ +// An onNewPromise handler can disable itself. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onNewPromise = function (promise) { + log += 'n'; + dbg.onNewPromise = undefined; +}; + +log = ''; +new g.Promise(function (){}); +new g.Promise(function (){}); +assertEq(log, 'n'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewPromise-05.js b/js/src/jit-test/tests/debug/Debugger-onNewPromise-05.js new file mode 100644 index 0000000000..08762de687 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewPromise-05.js @@ -0,0 +1,24 @@ +// Creating a promise within an onNewPromise handler causes a recursive handler +// invocation. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var log; +var depth; + +dbg.onNewPromise = function (promise) { + log += '('; depth++; + + assertEq(promise.seen, undefined); + promise.seen = true; + + if (depth < 3) + gw.executeInGlobal(`new Promise(_=>{})`); + + log += ')'; depth--; +}; + +log = ''; +depth = 0; +new g.Promise(function (){}); +assertEq(log, '((()))'); diff --git a/js/src/jit-test/tests/debug/Debugger-onNewPromise-06.js b/js/src/jit-test/tests/debug/Debugger-onNewPromise-06.js new file mode 100644 index 0000000000..0e24cdf372 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewPromise-06.js @@ -0,0 +1,35 @@ +// Resumption values from onNewPromise handlers are disallowed. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onNewPromise = function (g) { log += 'n'; return undefined; }; +log = ''; +assertEq(typeof new g.Promise(function (){}), "object"); +assertEq(log, 'n'); + +dbg.uncaughtExceptionHook = function (ex) { assertEq(/disallowed/.test(ex), true); log += 'u'; } +dbg.onNewPromise = function (g) { log += 'n'; return { return: "snoo" }; }; +log = ''; +assertEq(typeof new g.Promise(function (){}), "object"); +assertEq(log, 'nu'); + +dbg.onNewPromise = function (g) { log += 'n'; return { throw: "snoo" }; }; +log = ''; +assertEq(typeof new g.Promise(function (){}), "object"); +assertEq(log, 'nu'); + +dbg.onNewPromise = function (g) { log += 'n'; return null; }; +log = ''; +assertEq(typeof new g.Promise(function (){}), "object"); +assertEq(log, 'nu'); + +dbg.uncaughtExceptionHook = function (ex) { assertEq(/foopy/.test(ex), true); log += 'u'; } +dbg.onNewPromise = function (g) { log += 'n'; throw "foopy"; }; +log = ''; +assertEq(typeof new g.Promise(function (){}), "object"); +assertEq(log, 'nu'); + diff --git a/js/src/jit-test/tests/debug/Debugger-onNewPromise-07.js b/js/src/jit-test/tests/debug/Debugger-onNewPromise-07.js new file mode 100644 index 0000000000..4ef41cc67c --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onNewPromise-07.js @@ -0,0 +1,13 @@ +// Errors in onNewPromise handlers are reported correctly, and don't mess up the +// promise creation. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +let e; +dbg.uncaughtExceptionHook = ee => { e = ee; }; +dbg.onNewPromise = () => { throw new Error("woops!"); }; + +assertEq(typeof new g.Promise(function (){}), "object"); +assertEq(!!e, true); +assertEq(!!e.message.match(/woops/), true); diff --git a/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-01.js b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-01.js new file mode 100644 index 0000000000..84f8323997 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-01.js @@ -0,0 +1,18 @@ +// Test that the onPromiseSettled hook gets called when a promise settles. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +let log = ""; +let pw; +dbg.onPromiseSettled = pw_ => { + pw = pw_; + log += "s"; +}; + +let p = new g.Promise(function (){}); +g.settlePromiseNow(p); + +assertEq(log, "s"); +assertEq(pw, gw.makeDebuggeeValue(p)); diff --git a/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-02.js b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-02.js new file mode 100644 index 0000000000..b78cbccf11 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-02.js @@ -0,0 +1,24 @@ +// onPromiseSettled handlers fire, until they are removed. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, ''); + +dbg.onPromiseSettled = function (promise) { + log += 's'; + assertEq(promise.seen, undefined); + promise.seen = true; +}; + +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, 's'); + +log = ''; +dbg.onPromiseSettled = undefined; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, ''); diff --git a/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-03.js b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-03.js new file mode 100644 index 0000000000..2f11252480 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-03.js @@ -0,0 +1,40 @@ +// onPromiseSettled handlers on different Debugger instances are independent. +var g = newGlobal({newCompartment: true}); +var dbg1 = new Debugger(g); +var log1; +function h1(promise) { + log1 += 's'; + assertEq(promise.seen, undefined); + promise.seen = true; +} + +var dbg2 = new Debugger(g); +var log2; +function h2(promise) { + log2 += 's'; + assertEq(promise.seen, undefined); + promise.seen = true; +} + +log1 = log2 = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log1, ''); +assertEq(log2, ''); + +log1 = log2 = ''; +dbg1.onPromiseSettled = h1; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log1, 's'); +assertEq(log2, ''); + +log1 = log2 = ''; +dbg2.onPromiseSettled = h2; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log1, 's'); +assertEq(log2, 's'); + +log1 = log2 = ''; +dbg1.onPromiseSettled = undefined; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log1, ''); +assertEq(log2, 's'); diff --git a/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-04.js b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-04.js new file mode 100644 index 0000000000..6a5297223a --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-04.js @@ -0,0 +1,14 @@ +// An onPromiseSettled handler can disable itself. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onPromiseSettled = function (promise) { + log += 's'; + dbg.onPromiseSettled = undefined; +}; + +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, 's'); diff --git a/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-05.js b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-05.js new file mode 100644 index 0000000000..8c99684619 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-05.js @@ -0,0 +1,25 @@ +// Settling a promise within an onPromiseSettled handler causes a recursive +// handler invocation. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var log; +var depth; + +dbg.onPromiseSettled = function (promise) { + log += '('; depth++; + + assertEq(promise.seen, undefined); + promise.seen = true; + + if (depth < 3) { + gw.executeInGlobal(`settlePromiseNow(new Promise(_=>{}));`); + } + log += ')'; depth--; +}; + +log = ''; +depth = 0; +g.settlePromiseNow(new g.Promise(_=>{})); +assertEq(log, '((()))'); diff --git a/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-06.js b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-06.js new file mode 100644 index 0000000000..4ed8947ab8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-onPromiseSettled-06.js @@ -0,0 +1,35 @@ +// Resumption values from onPromiseSettled handlers are disallowed. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onPromiseSettled = function (g) { log += 's'; return undefined; }; +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, 's'); + +dbg.uncaughtExceptionHook = function (ex) { assertEq(/disallowed/.test(ex), true); log += 'u'; } +dbg.onPromiseSettled = function (g) { log += 's'; return { return: "snoo" }; }; +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, 'su'); + +dbg.onPromiseSettled = function (g) { log += 's'; return { throw: "snoo" }; }; +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, 'su'); + +dbg.onPromiseSettled = function (g) { log += 's'; return null; }; +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, 'su'); + +dbg.uncaughtExceptionHook = function (ex) { assertEq(/foopy/.test(ex), true); log += 'u'; } +dbg.onPromiseSettled = function (g) { log += 's'; throw "foopy"; }; +log = ''; +g.settlePromiseNow(new g.Promise(function (){})); +assertEq(log, 'su'); + diff --git a/js/src/jit-test/tests/debug/Debugger-setInstrumentation-01.js b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-01.js new file mode 100644 index 0000000000..44378e26df --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-01.js @@ -0,0 +1,196 @@ +// Test breakpoint site instrumentation functionality with a variety of control +// flow constructs. + +load(libdir + 'eqArrayHelper.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +var allScripts = []; + +function setScriptId(script) { + script.setInstrumentationId(allScripts.length); + allScripts.push(script); + + script.getChildScripts().forEach(setScriptId); +} + +dbg.onNewScript = setScriptId; + +function getOffsetLine(scriptId, offset) { + const script = allScripts[scriptId]; + return script.getOffsetMetadata(offset).lineNumber; +} + +const executedLines = []; +gdbg.setInstrumentation( + gdbg.makeDebuggeeValue((kind, script, offset) => { + executedLines.push(getOffsetLine(script, offset)); + }), + ["breakpoint"] +); + +function testFunction(fn, expected) { + for (var i = 0; i < 3; i++) { + fn(); + } + + assertEq(executedLines.length, 0); + gdbg.setInstrumentationActive(true); + + for (var i = 0; i < 5; i++) { + executedLines.length = 0; + fn(); + assertEqArray(executedLines, expected); + } + + executedLines.length = 0; + gdbg.setInstrumentationActive(false); +} + +g.eval(` +function basic() { + var a = 0; + a++; +} +`); + +testFunction(() => g.basic(), + [3, 4, 5]); + +g.eval(` +function cforloop() { + for (var i = 0; + i < 5; + i++) { + } +} +`); + +testFunction(() => g.cforloop(), + [3, 4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 7]); + +g.eval(` +function ifstatement(n) { + if (n) { + return 1; + } + return 0; +} +`); + +testFunction(() => g.ifstatement(true), [3, 4]); +testFunction(() => g.ifstatement(false), [3, 6]); + +g.eval(` +function whileloop(n) { + while (n < 3) { + n++; + } +} +`); + +testFunction(() => g.whileloop(1), + [3, 4, 3, 4, 3, 6]); + +g.eval(` +function dowhileloop(n) { + do { + if (++n == 3) { + break; + } + } while (true); +} +`); + +testFunction(() => g.dowhileloop(1), + [4, 7, 4, 5, 8]); + +g.eval(` +function forinloop(v) { + for (let i in v) { + var a = i; + } +} +`); + +testFunction(() => g.forinloop([1,2,3]), + [3, 4, 4, 4, 6]); + +g.eval(` +function forofloop(v) { + for (const i of v) { + var a = i; + } +} +`); + +testFunction(() => g.forofloop([1,2,3]), + [3, 4, 4, 4, 6]); + +g.eval(` +function normalswitch(v) { + switch (v) { + case 3: + return 0; + case "three": + return 1; + default: + return 2; + } +} +`); + +testFunction(() => g.normalswitch(3), [3, 5]); +testFunction(() => g.normalswitch("three"), [3, 7]); +testFunction(() => g.normalswitch(2), [3, 9]); + +g.eval(` +function tableswitch(v) { + switch (v) { + case 0: + case 1: + case 2: + break; + case 3: + return 1; + default: + return 2; + } +} +`); + +testFunction(() => g.tableswitch(0), [3, 7, 13]); +testFunction(() => g.tableswitch(3), [3, 9]); +testFunction(() => g.tableswitch(5), [3, 11]); + +g.eval(` +function trycatch(f) { + try { + f(); + } catch (e) { + return e; + } +} +`); + +testFunction(() => g.trycatch(() => { throw 3 }), [4, 6]); +testFunction(() => g.trycatch(() => {}), [4, 8]); + +g.eval(` +function tryfinally(f) { + var a; + try { + f(); + } finally { + a = 0; + } + a = 1; +} +`); + +testFunction(() => { + try { g.tryfinally(() => { throw 3 }); } catch (e) {} +}, [5, 7]); +testFunction(() => g.tryfinally(() => {}), [5, 7, 9, 10]); diff --git a/js/src/jit-test/tests/debug/Debugger-setInstrumentation-02.js b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-02.js new file mode 100644 index 0000000000..68111b192e --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-02.js @@ -0,0 +1,130 @@ +// Test main/entry/exit point instrumentation. + +load(libdir + 'eqArrayHelper.js'); + +ignoreUnhandledRejections(); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +var allScripts = []; + +function setScriptId(script) { + script.setInstrumentationId(allScripts.length); + allScripts.push(script); + + script.getChildScripts().forEach(setScriptId); +} + +dbg.onNewScript = setScriptId; + +const executedPoints = []; +function hitPoint(kind, scriptId) { + const name = allScripts[scriptId].displayName; + executedPoints.push(`${name}:${kind}`); +} + +gdbg.setInstrumentation( + gdbg.makeDebuggeeValue(hitPoint), + ["main", "entry", "exit"] +); + +function testFunction(fn, expected) { + for (var i = 0; i < 3; i++) { + try { fn(); } catch (e) {} + drainJobQueue(); + } + + assertEq(executedPoints.length, 0); + gdbg.setInstrumentationActive(true); + + for (var i = 0; i < 5; i++) { + executedPoints.length = 0; + try { fn(); } catch (e) {} + drainJobQueue(); + assertEqArray(executedPoints, expected); + } + + executedPoints.length = 0; + gdbg.setInstrumentationActive(false); +} + +g.eval(` +function basic() { + var a = 0; + a++; +} +`); + +testFunction(() => g.basic(), ["basic:main", "basic:exit"]); + +g.eval(` +function thrower(v) { + if (v) { + throw new Error(); + } +} +`); + +testFunction(() => g.thrower(0), ["thrower:main", "thrower:exit"]); +testFunction(() => g.thrower(1), ["thrower:main"]); + +g.eval(` +function* yielder() { yield 1; } +function iterator() { + for (var n of yielder()) {} +} +`); + +testFunction(() => g.iterator(), [ + "iterator:main", + "yielder:main", "yielder:exit", + "yielder:entry", "yielder:exit", + "yielder:entry", "yielder:exit", + "iterator:exit" +]); + +g.eval(` +function promiser(n) { + return new Promise(function promiseInternal(resolve, reject) { + if (n) { + reject(new Error()); + } else { + resolve(); + } + }); +} +function f1() {} +async function asyncer(n) { await promiser(n) } +async function asyncerCatch(n) { try { await promiser(n) } catch (e) { f1() } } +async function asyncerFinally(n) { try { await promiser(n) } finally { f1() } } +`); + +testFunction(() => g.asyncer(0), [ + "asyncer:main", + "promiser:main", "promiseInternal:main", "promiseInternal:exit", "promiser:exit", + "asyncer:exit", + "asyncer:entry", "asyncer:exit" +]); +testFunction(() => g.asyncer(1), [ + "asyncer:main", + "promiser:main", "promiseInternal:main", "promiseInternal:exit", "promiser:exit", + "asyncer:exit", + "asyncer:entry", // Note: no exit, GeneratorThrowOrReturn is called. + "asyncer:entry", "asyncer:exit" +]); +testFunction(() => g.asyncerCatch(1), [ + "asyncerCatch:main", + "promiser:main", "promiseInternal:main", "promiseInternal:exit", "promiser:exit", + "asyncerCatch:exit", + "asyncerCatch:entry", // Note: no exit, GeneratorThrowOrReturn is called. + "asyncerCatch:entry", "f1:main", "f1:exit", "asyncerCatch:exit" +]); +testFunction(() => g.asyncerFinally(1), [ + "asyncerFinally:main", + "promiser:main", "promiseInternal:main", "promiseInternal:exit", "promiser:exit", + "asyncerFinally:exit", + "asyncerFinally:entry", // Note: no exit, GeneratorThrowOrReturn is called. + "asyncerFinally:entry", "f1:main", "f1:exit", "asyncerFinally:entry", "asyncerFinally:exit" +]); diff --git a/js/src/jit-test/tests/debug/Debugger-setInstrumentation-03.js b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-03.js new file mode 100644 index 0000000000..aed1888718 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-03.js @@ -0,0 +1,72 @@ +// Test instrumentation functionality when using generators and async functions. + +load(libdir + 'eqArrayHelper.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +var allScripts = []; + +function setScriptId(script) { + script.setInstrumentationId(allScripts.length); + allScripts.push(script); + + script.getChildScripts().forEach(setScriptId); +} + +dbg.onNewScript = setScriptId; + +function getOffsetLine(scriptId, offset) { + const script = allScripts[scriptId]; + return script.getOffsetMetadata(offset).lineNumber; +} + +const executedLines = []; +gdbg.setInstrumentation( + gdbg.makeDebuggeeValue((kind, script, offset) => { + executedLines.push(getOffsetLine(script, offset)); + }), + ["breakpoint"] +); + +function testFunction(fn, expected) { + gdbg.setInstrumentationActive(true); + + for (var i = 0; i < 5; i++) { + executedLines.length = 0; + fn(); + assertEqArray(executedLines, expected); + } + + gdbg.setInstrumentationActive(false); +} + +g.eval(` +async function asyncfun() { + await Promise.resolve(0); + await Promise.resolve(1); + var a = 0; + await Promise.resolve(2); + a++; +} +`); + +testFunction(() => { + async function f() { await g.asyncfun(); } + f(); + drainJobQueue(); +}, [3, 3, 4, 4, 5, 6, 6, 7]); + +g.eval(` +function *generator() { + yield 1; + var a = 0; + yield 2; + a++; +} +`); + +testFunction(() => { + for (const i of g.generator()) {} +}, [3, 4, 5, 6]); diff --git a/js/src/jit-test/tests/debug/Debugger-setInstrumentation-04.js b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-04.js new file mode 100644 index 0000000000..0e86eb9096 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-04.js @@ -0,0 +1,34 @@ +// Test that instrumenting a function does not interfere with script +// disassembly when reporting errors. + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +function setScriptId(script) { + script.setInstrumentationId(0); + script.getChildScripts().forEach(setScriptId); +} + +dbg.onNewScript = setScriptId; + +const executedLines = []; +gdbg.setInstrumentation( + gdbg.makeDebuggeeValue(() => {}), + ["breakpoint"] +); + +g.eval(` +function foo(v) { + v.f().g(); +} +`); + +var gotException = false; +try { + g.foo({ f: () => { return { g: 0 } } }); +} catch (e) { + gotException = true; + assertEq(e.toString().includes("v.f().g is not a function"), true); +} +assertEq(gotException, true); diff --git a/js/src/jit-test/tests/debug/Debugger-setInstrumentation-05.js b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-05.js new file mode 100644 index 0000000000..4a138c8480 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-05.js @@ -0,0 +1,43 @@ +// Test some inputs that should fail instrumentation validation. + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +function assertThrows(fn, text) { + try { + fn(); + assertEq(true, false); + } catch (e) { + if (!e.toString().includes(text)) { + print(`Expected ${text}, got ${e}`); + assertEq(true, false); + } + } +} + +assertThrows(() => gdbg.setInstrumentation(undefined, []), + "Instrumentation callback must be an object"); +assertThrows(() => gdbg.setInstrumentation(gdbg.makeDebuggeeValue({}), ["foo"]), + "Unknown instrumentation kind"); + +let lastScript = null; +dbg.onNewScript = script => lastScript = script; + +assertThrows(() => { + g.eval(`x = 3`); + lastScript.setInstrumentationId("twelve"); +}, "Script instrumentation ID must be a number"); + +assertThrows(() => { + g.eval(`x = 3`); + lastScript.setInstrumentationId(10); + lastScript.setInstrumentationId(11); +}, "Script instrumentation ID is already set"); + +dbg.onNewScript = undefined; +gdbg.setInstrumentation(gdbg.makeDebuggeeValue({}), ["main"]); +gdbg.setInstrumentationActive(true); + +assertThrows(() => { g.eval(`x = 3`) }, + "Instrumentation ID not set for script"); diff --git a/js/src/jit-test/tests/debug/Debugger-setInstrumentation-06.js b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-06.js new file mode 100644 index 0000000000..14b97e5f81 --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-06.js @@ -0,0 +1,105 @@ +// Test instrumentation on property and element accesses. + +load(libdir + 'eqArrayHelper.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +function setScriptId(script) { + script.setInstrumentationId(0); + script.getChildScripts().forEach(setScriptId); +} + +dbg.onNewScript = setScriptId; + +function getOffsetLine(scriptId, offset) { + const script = allScripts[scriptId]; + return script.getOffsetMetadata(offset).lineNumber; +} + +const executedOperations = []; + +function propertyRead(script, offset, obj, prop) { + executedOperations.push(`read:${obj.label}:${prop}`); +} + +function propertyWrite(script, offset, obj, prop, rhs) { + executedOperations.push(`write:${obj.label}:${prop}:${typeof rhs}`); +} + +function callback(kind, ...args) { + switch (kind) { + case "getProperty": + case "getElement": + propertyRead(...args); + break; + case "setProperty": + case "setElement": + propertyWrite(...args); + } +} + +gdbg.setInstrumentation( + gdbg.makeDebuggeeValue(callback), + ["getProperty", "getElement", "setProperty", "setElement"] +); + +function testFunction(fn, expected) { + gdbg.setInstrumentationActive(true); + for (var i = 0; i < 5; i++) { + executedOperations.length = 0; + fn(); + assertEqArray(executedOperations, expected); + } + gdbg.setInstrumentationActive(false); +} + +g.eval(` +function basic(o, i) { + o.x = o.x + 1; + o[i] = o[i] + 1; +} +`); + +testFunction(() => g.basic({ label: 0}, 3), [ + "read:0:x", + "write:0:x:number", + "read:0:3", + "write:0:3:number", +]); + +g.eval(` +function incdec(o, i) { + o.x++; + o[i]++; +} +`); + +testFunction(() => g.incdec({ label: 0}, 3), [ + "read:0:x", + "write:0:x:number", + "read:0:3", + "write:0:3:number", +]); + +g.eval(` +function destructure({ f, g }) { + const { h, i } = f; +} +`); + +testFunction(() => g.destructure({ label: 0, f: { label: 1 }}, 3), [ + "read:0:f", + "read:0:g", + "read:1:h", + "read:1:i", +]); + +g.eval(` +function destructureArray([a, b]) {} +`); + +// Array destructuring uses iterators instead of getElement accesses, +// and no property accesses will be captured by instrumentation. +testFunction(() => g.destructureArray([1, 2]), []); diff --git a/js/src/jit-test/tests/debug/Debugger-setInstrumentation-bug1571169.js b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-bug1571169.js new file mode 100644 index 0000000000..6359d96cad --- /dev/null +++ b/js/src/jit-test/tests/debug/Debugger-setInstrumentation-bug1571169.js @@ -0,0 +1,18 @@ +// |jit-test| error: Error + +var lfOffThreadGlobal = newGlobal(); +evaluate(` + nukeAllCCWs(); + var g92 = newGlobal({ newCompartment: true }); + var dbg = Debugger(g92); + var gdbg = dbg.addDebuggee(g92); + gdbg.setInstrumentation( + gdbg.makeDebuggeeValue((kind, script, offset) => {}), + ["breakpoint"] + ); + gdbg.setInstrumentationActive(true); + g92.eval(\` + function basic() {} + \`); +`); + diff --git a/js/src/jit-test/tests/debug/Environment-01.js b/js/src/jit-test/tests/debug/Environment-01.js new file mode 100644 index 0000000000..40f3b03570 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-01.js @@ -0,0 +1,23 @@ +// A live Environment can observe the new variables introduced by ES5 non-strict direct eval. + +var g = newGlobal({newCompartment: true}); +g.eval("var x = 'global'; function f(s) { h(); eval(s); h(); }"); +g.eval("function h() { debugger; }"); +var dbg = Debugger(g); +var env = undefined; +var hits = 0; +dbg.onDebuggerStatement = function (hframe) { + if (env === undefined) { + // First debugger statement. + env = hframe.older.environment; + assertEq(env.find("x") !== env, true); + assertEq(env.names().indexOf("x"), -1); + } else { + // Second debugger statement, post-eval. + assertEq(env.find("x"), env); + assertEq(env.names().indexOf("x") >= 0, true); + } + hits++; +}; +g.f("var x = 'local';"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-02.js b/js/src/jit-test/tests/debug/Environment-02.js new file mode 100644 index 0000000000..77af721a29 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-02.js @@ -0,0 +1,20 @@ +// The last Environment on the environment chain always has .type == "object" and .object === the global object. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.eval("function h() { debugger; }"); +var hits = 0; +dbg.onDebuggerStatement = function (hframe) { + var env = hframe.older.environment; + while (env.parent) + env = env.parent; + assertEq(env.type, "object"); + assertEq(env.object, gw); + hits++; +}; + +g.eval("h();"); +g.eval("(function () { h(); return []; })();"); +g.eval("with (Math) { h(-2 * PI); }"); +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/Environment-03.js b/js/src/jit-test/tests/debug/Environment-03.js new file mode 100644 index 0000000000..812ee21cbb --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-03.js @@ -0,0 +1,10 @@ +// Test that getting a function's environment can unlazify scripts. + +var g = newGlobal({newCompartment: true}); +g.eval('function f() { }'); +var dbg = new Debugger; +var gw = dbg.makeGlobalObjectReference(g); +var fw = gw.getOwnPropertyDescriptor('f').value; +gc(); +dbg.addDebuggee(g); +var fenv = fw.environment; diff --git a/js/src/jit-test/tests/debug/Environment-Function-prototype.js b/js/src/jit-test/tests/debug/Environment-Function-prototype.js new file mode 100644 index 0000000000..97e54f62fc --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-Function-prototype.js @@ -0,0 +1,7 @@ +// Don't crash when getting the Debugger.Environment of a frame inside +// Function.prototype. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onEnterFrame = function (frame) { frame.environment; }; +g.Function.prototype(); diff --git a/js/src/jit-test/tests/debug/Environment-bug-1431461.js b/js/src/jit-test/tests/debug/Environment-bug-1431461.js new file mode 100644 index 0000000000..188f3ee5a7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-bug-1431461.js @@ -0,0 +1,26 @@ +// Check that duplicate bindings are not created for let/const variables. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +g.eval(` +function f(x, y=x) { + let z = "Z"; + debugger; + return x + y + z; +} +`); + +let hits = 0; +let names = []; + +dbg.onDebuggerStatement = frame => { + hits++; + for (let env = frame.environment; env.type !== "object"; env = env.parent) { + names.push(...env.names()); + } +}; + +assertEq(g.f("X", "Y"), "XYZ"); +assertEq(hits, 1); +assertEq(names.sort().join(", "), "arguments, x, y, z"); diff --git a/js/src/jit-test/tests/debug/Environment-calleeScript-01.js b/js/src/jit-test/tests/debug/Environment-calleeScript-01.js new file mode 100644 index 0000000000..a5417b9070 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-calleeScript-01.js @@ -0,0 +1,48 @@ +// Debugger.Environment.prototype.calleeScript reveals the script of function +// environments. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +function check(code, expectedType, expectedCallee) { + print("check(" + JSON.stringify(code) + ")"); + var hits; + dbg.onDebuggerStatement = function (frame) { + hits++; + var env = frame.environment; + assertEq(env.type, expectedType); + assertEq(env.calleeScript, expectedCallee ? expectedCallee.script : null); + }; + hits = 0; + g.eval(code); + assertEq(hits, 1); +} + +check('debugger;', 'declarative', null); +check('with({}) { debugger; };', 'with', null); +check('{ let x=1; debugger; };', 'declarative', null); + +g.eval('function f() { debugger; }'); +check('f()', 'declarative', gw.makeDebuggeeValue(g.f)); + +g.eval('function g() { h(); }'); +g.eval('function h() { debugger; }'); +check('g()', 'declarative', gw.makeDebuggeeValue(g.h)); + +// All evals get a lexical scope. +check('"use strict"; eval("debugger");', 'declarative', null); +g.eval('function j() { "use strict"; eval("debugger;"); }'); +check('j()', 'declarative', null); + +// All evals get a lexical scope. +check('eval("debugger");', 'declarative', null); + +g.eval('function* m() { debugger; yield true; }'); +check('m().next();', 'declarative', gw.makeDebuggeeValue(g.m)); + +g.eval('function n() { { let x = 1; debugger; } }'); +check('n()', 'declarative', null); + +g.eval('function* o() { debugger; yield true; }'); +check('o().next();', 'declarative', gw.makeDebuggeeValue(g.o)); diff --git a/js/src/jit-test/tests/debug/Environment-calleeScript-02.js b/js/src/jit-test/tests/debug/Environment-calleeScript-02.js new file mode 100644 index 0000000000..d518c4b8a0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-calleeScript-02.js @@ -0,0 +1,25 @@ +// Debugger.Environment.prototype.calleeScript gets the right script. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +g.eval('function f(x) { return function (y) { eval(""); debugger; return x + y; } }'); +g.eval('var g = f(2);'); +g.eval('var h = f(3);'); + +function check(fun, label) { + print("check(" + label + ")"); + var hits; + dbg.onDebuggerStatement = function (frame) { + hits++; + var env = frame.environment; + assertEq(env.calleeScript, gw.makeDebuggeeValue(fun).script); + }; + hits = 0; + fun(); + assertEq(hits, 1); +} + +check(g.g, 'g.g'); +check(g.h, 'g.h'); diff --git a/js/src/jit-test/tests/debug/Environment-calleeScript-03.js b/js/src/jit-test/tests/debug/Environment-calleeScript-03.js new file mode 100644 index 0000000000..f21522b4cc --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-calleeScript-03.js @@ -0,0 +1,25 @@ +// Environments of different instances of the same generator have the same +// calleeScript. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +function check(gen, label) { + print("check(" + label + ")"); + var hits; + dbg.onDebuggerStatement = function (frame) { + hits++; + var env = frame.environment; + assertEq(env.calleeScript, gw.makeDebuggeeValue(g.f).script); + }; + hits = 0; + gen.next(); + assertEq(hits, 1); +} + +g.eval('function* f(x) { debugger; yield x; }'); +g.eval('var g = f(2);'); +g.eval('var h = f(3);'); +check(g.g, 'g.g'); +check(g.h, 'g.h'); diff --git a/js/src/jit-test/tests/debug/Environment-find-01.js b/js/src/jit-test/tests/debug/Environment-find-01.js new file mode 100644 index 0000000000..abce77df16 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-01.js @@ -0,0 +1,19 @@ +// find sees that vars are hoisted out of with statements. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.environment.find("x").type, "with"); + hits++; +}; + +assertEq(g.eval("(function () {\n" + + " function g() { x = 1; }\n" + + " with ({x: 2}) {\n" + + " var x;\n" + + " debugger;\n" + + " return x;\n" + + " }\n" + + "})();"), 2); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-find-02.js b/js/src/jit-test/tests/debug/Environment-find-02.js new file mode 100644 index 0000000000..c8ab45b786 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-02.js @@ -0,0 +1,18 @@ +// env.find() finds nonenumerable names in the global environment. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var env = dbg.getNewestFrame().environment; + var last = env; + while (last.parent) + last = last.parent; + + assertEq(env.find("Array"), last); + hits++; +}; + +g.eval("h();"); +g.eval("(function () { h(); })();"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-find-03.js b/js/src/jit-test/tests/debug/Environment-find-03.js new file mode 100644 index 0000000000..3800e247d8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-03.js @@ -0,0 +1,20 @@ +// env.find() finds nonenumerable properties in with statements. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var frame = dbg.getNewestFrame(); + var target = frame.eval("obj").return; + var env = frame.environment.find("PI"); + assertEq(env.object, target); + hits++; +}; + +g.obj = g.Math; +g.eval("with (obj) h();"); +g.eval("with (Math) { let x = 12; h(); }"); +g.eval("obj = {};\n" + + "Object.defineProperty(obj, 'PI', {enumerable: false, value: 'Marlowe'});\n" + + "with (obj) h();\n"); +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/Environment-find-04.js b/js/src/jit-test/tests/debug/Environment-find-04.js new file mode 100644 index 0000000000..24a714e252 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-04.js @@ -0,0 +1,21 @@ +// env.find throws a TypeError if the argument is not an identifier. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var env = dbg.getNewestFrame().environment; + assertThrowsInstanceOf(function () { env.find(); }, TypeError); + assertThrowsInstanceOf(function () { env.find(""); }, TypeError); + assertThrowsInstanceOf(function () { env.find(" "); }, TypeError); + assertThrowsInstanceOf(function () { env.find(0); }, TypeError); + assertThrowsInstanceOf(function () { env.find("0"); }, TypeError); + assertThrowsInstanceOf(function () { env.find("0xc"); }, TypeError); + assertThrowsInstanceOf(function () { env.find("Anna Karenina"); }, TypeError); + hits++; +}; +g.eval("h();"); +g.eval("with ([1]) h();"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-find-05.js b/js/src/jit-test/tests/debug/Environment-find-05.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-05.js diff --git a/js/src/jit-test/tests/debug/Environment-find-06.js b/js/src/jit-test/tests/debug/Environment-find-06.js new file mode 100644 index 0000000000..b34321647e --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-06.js @@ -0,0 +1,47 @@ +// Environment.prototype.find finds bindings that are function arguments, 'let' +// bindings, or FunctionExpression names. + +var g = newGlobal({newCompartment: true}); +g.eval("function h() { debugger; }"); + +var dbg = new Debugger(g); + +function test1(code) { + var hits = 0; + dbg.onDebuggerStatement = function (frame) { + var env = frame.older.environment.find('X'); + assertEq(env.names().indexOf('X') !== -1, true); + assertEq(env.type, 'declarative'); + assertEq(env.parent !== null, true); + hits++; + }; + g.eval(code); + assertEq(hits, 1); +} + +var manyNames = ''; +for (var i = 0; i < 2048; i++) + manyNames += 'x' + i + ', '; +manyNames += 'X'; + +function test2(code) { + print(code + " : one"); + test1(code.replace('@@', 'X')); + print(code + " : many"); + test1(code.replace('@@', manyNames)); +} + +test2('function f(@@) { h(); } f(1);'); +test2('function f(@@) { h(); } f();'); +test2('function f(@@) { return function g() { h(X); }; } f(1)();'); +test2('function f(@@) { return function g() { h(X); }; } f()();'); + +test2(' { let @@ = 0; h(); }'); +test2('function f(a, b, c) { let @@ = 0; h(); } f(1, 2, 3);'); +test2(' { let @@ = 0; { let y = 0; h(); } }'); +test2('function f() { let @@ = 0; { let y = 0; h(); } } f();'); +test2(' { for (let @@ = 0; X < 1; X++) h(); }'); +test2('function f() { for (let @@ = 0; X < 1; X++) h(); } f();'); + +test1('(function X() { h(); })();'); +test1('(function X(a, b, c) { h(); })(1, 2, 3);'); diff --git a/js/src/jit-test/tests/debug/Environment-find-07.js b/js/src/jit-test/tests/debug/Environment-find-07.js new file mode 100644 index 0000000000..a6b2def550 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-07.js @@ -0,0 +1,22 @@ +// We can find into and from optimized out scopes. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +dbg.addDebuggee(g); + +g.eval("" + function f() { + var x = 42; + function g() { } + g(); +}); + +dbg.onEnterFrame = function (f) { + if (f.callee && (f.callee.name === "g")) { + genv = f.environment.parent; + assertEq(genv.optimizedOut, true); + assertEq(genv.find("f").type, "object"); + assertEq(f.environment.find("x"), genv); + } +} + +g.f(); diff --git a/js/src/jit-test/tests/debug/Environment-gc-01.js b/js/src/jit-test/tests/debug/Environment-gc-01.js new file mode 100644 index 0000000000..ec6804cacb --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-gc-01.js @@ -0,0 +1,19 @@ +// An Environment keeps its referent alive. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(x) { return 2 * x; }"); +var dbg = Debugger(g); +var env; +dbg.onEnterFrame = function (frame) { env = frame.environment; }; +assertEq(g.f(22), 44); +dbg.onEnterFrame = undefined; + +assertEq(env.find("x"), env); +assertEq(env.names().join(","), "arguments,x"); + +gc(); +g.gc(g); +gc(env); + +assertEq(env.find("x"), env); +assertEq(env.names().join(","), "arguments,x"); diff --git a/js/src/jit-test/tests/debug/Environment-gc-02.js b/js/src/jit-test/tests/debug/Environment-gc-02.js new file mode 100644 index 0000000000..0ed5a0d103 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-gc-02.js @@ -0,0 +1,28 @@ +// A closure's .environment keeps the lexical environment alive even if the closure is destroyed. + +var N = 4; +var g = newGlobal({newCompartment: true}); +g.eval("function add(a) { return function (b) { return eval('a + b'); }; }"); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var aw = gw.getOwnPropertyDescriptor("add").value; + +// Create N closures and collect environments. +var arr = []; +for (var i = 0; i < N; i++) + arr[i] = aw.call(null, i).return.environment; + +// Test that they work now. +function check() { + for (var i = 0; i < N; i++) { + assertEq(arr[i].find("b"), null); + assertEq(arr[i].find("a"), arr[i]); + } +} +check(); + +// Test that they work after gc. +gc(); +gc({}); +g.gc(g); +check(); diff --git a/js/src/jit-test/tests/debug/Environment-gc-03.js b/js/src/jit-test/tests/debug/Environment-gc-03.js new file mode 100644 index 0000000000..3243ec65ba --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-gc-03.js @@ -0,0 +1,21 @@ +// Test that block scopes cannot be resurrected by onStep. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function(frame) { + frame.onStep = (function() { + frame.environment; + }); +}; + +g.eval("debugger; for (let i = 0; i < 1; i++) (function(){});"); + +// If the last freshened block scope was incorrectly resurrected by onStep +// above, GCing will assert. +gc(); + +g.eval("debugger; { let i = 0; (function(){ i = 42; }); }"); +gc(); + +g.eval("debugger; try { throw 42; } catch (e) { };"); +gc(); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-01.js b/js/src/jit-test/tests/debug/Environment-getVariable-01.js new file mode 100644 index 0000000000..9b3bbd9e1c --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-01.js @@ -0,0 +1,14 @@ +// Environment.prototype.getVariable does not see variables bound in enclosing scopes. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.environment.getVariable("x"), 13); + assertEq(frame.environment.getVariable("k"), undefined); + assertEq(frame.environment.find("k").getVariable("k"), 3); + hits++; +}; +g.eval("var k = 3; function f(x) { debugger; }"); +g.f(13); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-02.js b/js/src/jit-test/tests/debug/Environment-getVariable-02.js new file mode 100644 index 0000000000..9d76a03957 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-02.js @@ -0,0 +1,18 @@ +// getVariable works in function scopes. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var lexicalEnv = frame.environment; + var varEnv = lexicalEnv.parent; + assertEq(varEnv.getVariable("a"), 1); + assertEq(varEnv.getVariable("b"), 2); + assertEq(varEnv.getVariable("c"), 3); + assertEq(varEnv.getVariable("d"), 7); + assertEq(lexicalEnv.getVariable("e"), 8); + hits++; +}; +g.eval("function f(a, [b, c]) { var d = a + b + c + 1; let e = d + 1; debugger; }"); +g.f(1, [2, 3]); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-03.js b/js/src/jit-test/tests/debug/Environment-getVariable-03.js new file mode 100644 index 0000000000..9475d1cce3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-03.js @@ -0,0 +1,21 @@ +// getVariable sees bindings in let-block scopes. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += frame.environment.getVariable("x"); +}; +g.eval("function f() {\n" + + " let x = 'a';\n" + + " debugger;\n" + + " for (let x = 0; x < 2; x++)\n" + + " if (x === 0)\n" + + " debugger;\n" + + " else {\n" + + " let x = 'b'; debugger;\n" + + " }\n" + + "}\n"); +g.f(); +g.eval("{ let x = 'd'; debugger; }"); +assertEq(log, 'a0bd'); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-04.js b/js/src/jit-test/tests/debug/Environment-getVariable-04.js new file mode 100644 index 0000000000..850446f577 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-04.js @@ -0,0 +1,12 @@ +// getVariable sees variables in function scopes added by non-strict direct eval. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var v; +dbg.onDebuggerStatement = function (frame) { + v = frame.environment.getVariable("x"); +}; + +g.eval("function f(s) { eval(s); debugger; }"); +g.f("var x = 'Q';"); +assertEq(v, 'Q'); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-05.js b/js/src/jit-test/tests/debug/Environment-getVariable-05.js new file mode 100644 index 0000000000..cfed91d86c --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-05.js @@ -0,0 +1,10 @@ +// getVariable sees global variables. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += frame.environment.parent.getVariable("x") + frame.environment.parent.getVariable("y"); +}; +g.eval("var x = 'a'; this.y = 'b'; debugger;"); +assertEq(log, 'ab'); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-06.js b/js/src/jit-test/tests/debug/Environment-getVariable-06.js new file mode 100644 index 0000000000..898d0df333 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-06.js @@ -0,0 +1,12 @@ +// getVariable sees properties inherited from the global object's prototype chain. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += frame.environment.parent.getVariable("x") + frame.environment.parent.getVariable("y"); +}; +g.eval("Object.getPrototypeOf(this).x = 'a';\n" + + "Object.prototype.y = 'b';\n" + + "debugger;\n"); +assertEq(log, 'ab'); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-07.js b/js/src/jit-test/tests/debug/Environment-getVariable-07.js new file mode 100644 index 0000000000..9f48d3208e --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-07.js @@ -0,0 +1,10 @@ +// getVariable can get properties from with-block scopes. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var v; +dbg.onDebuggerStatement = function (frame) { + v = frame.environment.getVariable("x"); +}; +g.eval("var x = 1; { let x = 2; with ({x: 3}) { debugger; } }"); +assertEq(v, 3); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-08.js b/js/src/jit-test/tests/debug/Environment-getVariable-08.js new file mode 100644 index 0000000000..ca77d582a1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-08.js @@ -0,0 +1,10 @@ +// getVariable sees properties inherited from a with-object's prototype chain. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var v; +dbg.onDebuggerStatement = function (frame) { + v = frame.environment.getVariable("x"); +}; +g.eval("var x = 1; { let x = 2; with (Object.create({x: 3})) { debugger; } }"); +assertEq(v, 3); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-09.js b/js/src/jit-test/tests/debug/Environment-getVariable-09.js new file mode 100644 index 0000000000..5e2136b9d7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-09.js @@ -0,0 +1,13 @@ +// getVariable works on ancestors of frame.environment. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + for (var env = frame.environment; env; env = env.parent) { + if (env.find("x") === env) + log += env.getVariable("x"); + } +}; +g.eval("var x = 1; { let x = 2; with (Object.create({x: 3})) { debugger; } }"); +assertEq(log, "321"); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-10.js b/js/src/jit-test/tests/debug/Environment-getVariable-10.js new file mode 100644 index 0000000000..57273cc628 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-10.js @@ -0,0 +1,27 @@ +// getVariable works on a heavyweight environment after control leaves its scope. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var envs = []; +dbg.onDebuggerStatement = function (frame) { + envs.push(frame.environment); +}; +g.eval("var f;\n" + + "for (var x = 0; x < 3; x++) {\n" + + " (function (x) {\n" + + " for (var y = 0; y < 3; y++) {\n" + + " (function (z) {\n" + + " eval(z); // force heavyweight\n" + + " debugger;\n" + + " })(x + y);\n" + + " }\n" + + " })(x);\n" + + "}"); + +var i = 0; +for (var x = 0; x < 3; x++) { + for (var y = 0; y < 3; y++) { + var e = envs[i++]; + assertEq(e.getVariable("z"), x + y); + } +} diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-11.js b/js/src/jit-test/tests/debug/Environment-getVariable-11.js new file mode 100644 index 0000000000..23dcdc5fe8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-11.js @@ -0,0 +1,15 @@ +// The value returned by getVariable can be a Debugger.Object. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var a = frame.environment.parent.getVariable('Math'); + assertEq(a instanceof Debugger.Object, true); + var b = gw.getOwnPropertyDescriptor('Math').value; + assertEq(a, b); + hits++; +}; +g.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-12.js b/js/src/jit-test/tests/debug/Environment-getVariable-12.js new file mode 100644 index 0000000000..d31bd3b1b4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-12.js @@ -0,0 +1,61 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(frame.environment.parent.getVariable('y'), true); +}; + +g.eval("var g;" + + "function f(x) {" + + " { let y = x; " + + " if (x)" + + " g = function() { eval('debugger') };" + + " else" + + " g();" + + " }" + + "}" + + "f(true);" + + "f(false);"); +assertEq(hits, 1); + +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(frame.environment.parent.getVariable('y'), 1); + assertEq(frame.environment.parent.names().indexOf('z'), -1); +}; + +g.eval("var g;" + + "{ let y = 1; " + + " g = function () { eval(''); debugger; };" + + " { let z = 2; " + + " g();" + + " }"+ + "}"); +assertEq(hits, 1); + +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + var e = frame.older.environment.parent; + assertEq(e.getVariable('z'), true); + e = e.parent; + assertEq(e.getVariable('y'), true); +}; + +g.eval("var g;" + + "function h() { eval(''); debugger; };" + + "for (var x of [true, false]) {" + + " { let y = x; " + + " { let z = x; " + + " if (x)" + + " g = function () { print(z); h() };" + + " else" + + " g();" + + " }" + + " }" + + "}"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-13.js b/js/src/jit-test/tests/debug/Environment-getVariable-13.js new file mode 100644 index 0000000000..37d5e90afc --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-13.js @@ -0,0 +1,50 @@ +// Tests that we can use debug scopes with Ion frames. +// +// Unfortunately these tests are brittle. They depend on opaque JIT heuristics +// kicking in. + +load(libdir + "jitopts.js"); + +// GCs can invalidate JIT code and cause this test to fail. +gczeal(0); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(0); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + // Note that this *depends* on CCW scripted functions being opaque to Ion + // optimization and not deoptimizing the frames below the call to toggle. + g.toggle = function toggle(d) { + if (d) { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame(); + assertEq(frame.implementation, "ion"); + // g is heavyweight but its call object is optimized out, because its + // arguments and locals are unaliased. + // + // Calling frame.environment here should make a fake debug scope that + // gets things directly from the frame. Calling frame.arguments doesn't + // go through the scope object and reads directly off the frame. Assert + // that the two are equal. + assertEq(frame.environment.getVariable("x"), frame.arguments[1]); + } + }; + + g.eval("" + function f(d, x) { g(d, x); }); + g.eval("" + function g(d, x) { + for (var i = 0; i < 100; i++); + function inner() { i = 42; }; + toggle(d); + // Use x so it doesn't get optimized out. + x++; + }); + + g.eval("(" + function test() { + for (i = 0; i < 15; i++) + f(false, 42); + f(true, 42); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-14.js b/js/src/jit-test/tests/debug/Environment-getVariable-14.js new file mode 100644 index 0000000000..2eff1690ff --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-14.js @@ -0,0 +1,18 @@ +// Debugger.Environment can reflect optimized out function scopes + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +dbg.addDebuggee(g); + +g.eval("" + function f() { + var x = 42; + function g() { } + g(); +}); + +dbg.onEnterFrame = function (f) { + if (f.callee && (f.callee.name === "g")) + assertEq(f.environment.parent.getVariable("x").optimizedOut, true); +} + +g.f(); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-15.js b/js/src/jit-test/tests/debug/Environment-getVariable-15.js new file mode 100644 index 0000000000..f0495cda17 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-15.js @@ -0,0 +1,31 @@ +// Don't hand out internal function objects via Debugger.Environment.prototype.getVariable. + +// When the real scope chain object holding the binding for 'f' in 'function f() +// { ... }' is optimized out because it's never used, we whip up fake scope +// chain objects for Debugger to use, if it looks. However, the value of the +// variable f will be an internal function object, not a live function object, +// since the latter was not recorded. Internal function objects should not be +// exposed via Debugger. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +dbg.onDebuggerStatement = function (frame) { + var g_call_env = frame.older.environment; // g's locals + var g_decl_env = g_call_env.parent; // 'function g' binding + var f_call_env = g_decl_env.parent; // f's locals + var f_decl_env = f_call_env.parent; // 'function f' binding + assertEq(f_decl_env.getVariable('f').optimizedOut, true); +} + +g.evaluate(` + + function h() { debugger; } + (function f() { + return function g() { + h(); + return 1; + } + })()(); + + `); diff --git a/js/src/jit-test/tests/debug/Environment-getVariable-WouldRun.js b/js/src/jit-test/tests/debug/Environment-getVariable-WouldRun.js new file mode 100644 index 0000000000..c6bf3a8cb7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-getVariable-WouldRun.js @@ -0,0 +1,17 @@ +// getVariable that would trigger a getter does not crash or explode. +// It should throw WouldRunDebuggee, but that isn't implemented yet. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertThrowsInstanceOf(function () { + frame.environment.parent.parent.getVariable("x"); + }, Error); + hits++; +}; +g.eval("Object.defineProperty(this, 'x', {get: function () { throw new Error('fail'); }});\n" + + "debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-identity-01.js b/js/src/jit-test/tests/debug/Environment-identity-01.js new file mode 100644 index 0000000000..7d0c5b134d --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-01.js @@ -0,0 +1,40 @@ +// The value of frame.environment is the same Environment object at different +// times within a single visit to a scope. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +g.eval("function h() { debugger; }"); +var hits, env; +dbg.onDebuggerStatement = function (hframe) { + var frame = hframe.older; + var e = frame.environment; + + // frame.environment is at least cached from one moment to the next. + assertEq(e, frame.environment); + + // frame.environment is cached from statement to statement within a call frame. + if (env === undefined) + env = e; + else + assertEq(e, env); + + hits++; +}; + +hits = 0; +env = undefined; +g.eval("function f() { (function () { var i = 0; h(); var j = 2; h(); })(); }"); +g.f(); +assertEq(hits, 2); + +hits = 0; +env = undefined; +g.eval("function f2() { { let i = 0; h(); let j = 2; h(); } }"); +g.f2(); +assertEq(hits, 2); + +hits = 0; +env = undefined; +g.eval("function f3() { { let i; for (i = 0; i < 2; i++) h(); } }"); +g.f3(); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-identity-02.js b/js/src/jit-test/tests/debug/Environment-identity-02.js new file mode 100644 index 0000000000..a48f8b93bf --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-02.js @@ -0,0 +1,29 @@ +// frame.environment is different for different activations of a scope. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +g.eval("function h() { debugger; }"); +var arr; +dbg.onDebuggerStatement = function (hframe) { + var e = hframe.older.environment; + assertEq(arr.indexOf(e), -1); + arr.push(e); +}; + +function test(code, expectedHits) { + arr = []; + g.eval(code); + assertEq(arr.length, expectedHits); +} + +// two separate calls to a function +test("(function () { var f = function (a) { h(); return a; }; f(1); f(2); })();", 2); + +// recursive calls to a function +test("(function f(n) { h(); return n < 2 ? 1 : n * f(n - 1); })(3);", 3); + +// separate visits to a block in the same call frame +test("(function () { for (var i = 0; i < 3; i++) { let j = i * 4; h(); }})();", 3); + +// two strict direct eval calls in the same function scope +test("(function () { 'use strict'; for (var i = 0; i < 3; i++) eval('h();'); })();", 3); diff --git a/js/src/jit-test/tests/debug/Environment-identity-03.js b/js/src/jit-test/tests/debug/Environment-identity-03.js new file mode 100644 index 0000000000..a088f1cc53 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-03.js @@ -0,0 +1,106 @@ +// Two Environments nested in the same runtime scope share the correct tail of their parent chains. + +// The compiler must be allowed to elide empty scopes and so forth, so this +// test does not check the number of unshared Environments. Instead, each test +// case identifies the expected innermost shared scope by the name of a +// variable in it. + +var g = newGlobal({newCompartment: true}); +g.eval("function h() { debugger; }"); +var dbg = Debugger(g); +var hits, name, shared, unshared; +dbg.onDebuggerStatement = function (hframe) { + var frame = hframe.older; + + // Find name in frame.environment. + var env, child = null; + for (env = frame.environment; env !== null; env = env.parent) { + if (env.names().indexOf(name) != -1) + break; + child = env; + } + assertEq(env !== null, true, "expected '" + name + "' to be in scope"); + assertEq(env, frame.environment.find(name), + "env.find should find the same frame as the written out search"); + + if (hits === 0) { + // First hit. + shared = env; + unshared = child; + } else { + // Subsequent hit. + assertEq(env, shared, "the environment containing '" + name + "' should be shared"); + assertEq(child === null || unshared === null || unshared !== child, true, + "environments nested within the one containing '" + name + "' should not be shared"); + } + hits++; +}; + +function test(sharedName, expectedHits, code) { + hits = 0; + name = sharedName; + shared = unshared = undefined; + g.eval(code); + assertEq(hits, expectedHits); +} + +// Basic test cases. +// +// (The stray "a = b" assignments in these tests are to inhibit the flat closure +// optimization, which Environments expose. There's nothing really wrong with +// the optimization or with the debugger exposing it, but that's not what we +// want to test here.) + +test("q", 2, "let q = function (a) { h(); }; q(1); q(2);"); +test("a", 2, "q = function (a) { (function (b) { h(); a = b; })(2); h(); }; q(1);"); +test("a", 2, "q = function (a) { h(); return function (b) { h(); a = b; }; }; q(1)(2);"); +test("n", 3, "q = function (n) { for (var i = 0; i < n; i++) { { let j = i; h(); } } }; q(3);"); + +// A function with long dynamic and static chains. +var N = 80; + +var code = "function f" + N + "(a" + N + ") {\neval('a0 + a1'); h();\n}\n"; +for (var i = N; --i >= 0;) { + var call = "f" + (i + 1) + "(a" + i + " - 1);\n"; + code = ("function f" + i + "(a" + i + ") {\n" + + code + + call + + "if (a" + i + " === 0) " + call + + "}\n"); +} + +g.eval(code); +test("a0", 2, "f0(0);"); +test("a17", 2, "f0(17);"); +test("a" + (N-2), 2, "f0(" + (N-2) + ");"); +test("a" + (N-1), 2, "f0(" + (N-1) + ");"); + +// A function with a short dynamic chain and a long static chain. +N = 60; + +function DeepStaticShallowDynamic(i, n) { + var code = "function f" + i + "(a" + i + ") {\n"; + if (i >= n) + code += "eval('a1 + a2'); h();\n"; + else + code += "return " + DeepStaticShallowDynamic(i+1, n) + ";\n"; + code += "}"; + return code; +} +g.eval(DeepStaticShallowDynamic(1, N)); + +function* range(start, stop) { + for (var i = start; i < stop; i++) + yield i; +} + +function DSSDsplit(s) { + return ("var mid = f1" + [...range(0, s)].map((i) => "(" + i + ")").join("") + ";\n" + + "mid" + [...range(s, N)].map((i) => "(" + i + ")").join("") + ";\n" + + "mid" + [...range(s, N)].map((i) => "(" + i + ")").join("") + ";\n"); +} + +test("a1", 2, DSSDsplit(1)); +test("a17", 2, DSSDsplit(17)); +test("a" + (N-2), 2, DSSDsplit(N-2)); +test("a" + (N-1), 2, DSSDsplit(N-1)); diff --git a/js/src/jit-test/tests/debug/Environment-identity-04.js b/js/src/jit-test/tests/debug/Environment-identity-04.js new file mode 100644 index 0000000000..607b871817 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-04.js @@ -0,0 +1,19 @@ +// Observably different visits to the same with-statement produce distinct Environments. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(a, obj) { with (obj) return function () { return a; }; }"); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + // Even though the two visits to the with-statement have the same target + // object, Math, the environments are observably different. + var f1 = frame.eval("f(1, Math);").return; + var f2 = frame.eval("f(2, Math);").return; + assertEq(f1.environment !== f2.environment, true); + assertEq(f1.object, f2.object); + assertEq(f1.call().return, 1); + assertEq(f2.call().return, 2); + hits++; +}; +g.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-identity-05.js b/js/src/jit-test/tests/debug/Environment-identity-05.js new file mode 100644 index 0000000000..d72b5e76f3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-05.js @@ -0,0 +1,19 @@ +// Tests that freshened blocks behave correctly in Debugger. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +var oldEnv = null; +dbg.onDebuggerStatement = function (frame) { + if (!oldEnv) { + oldEnv = frame.environment; + } else { + // Block has been freshened by |for (let ...)|, should be different + // identity. + log += (oldEnv === frame.environment); + } + log += frame.environment.getVariable("x"); +}; +g.eval("for (let x = 0; x < 2; x++) { eval(\"\"); debugger; }"); +gc(); +assertEq(log, "0false1"); diff --git a/js/src/jit-test/tests/debug/Environment-inspectable-01.js b/js/src/jit-test/tests/debug/Environment-inspectable-01.js new file mode 100644 index 0000000000..7ced83aa41 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-inspectable-01.js @@ -0,0 +1,80 @@ +// Environments are only inspectable while their globals are debuggees. + +load(libdir + 'asserts.js'); + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +g2.g1 = g1; +g1.g2 = g2; + +g1.eval('function f(xf) { return function h(xh) { eval(""); debugger; } }'); +g1.eval('var h = f("value of xf");'); + +// To ensure that xk gets located on the heap, and thus outlives its stack frame, we +// store a function that captures it here. Kind of a kludge. +g2.eval('var capture;'); +g2.eval('function k(xk) { capture = function () { return xk; }; g1.h("value of xh"); }'); + +var dbg = new Debugger; +dbg.addDebuggee(g1); +dbg.addDebuggee(g2); + +dbg.onDebuggerStatement = debuggerHandler; + +var log = ''; + +g1.eval('g2.k("value of xk");'); + +var he, ke, ee; + +function debuggerHandler(frame) { + log += 'd'; + + assertEq(frame.type, 'call'); + he = frame.environment; + + assertEq(frame.older.type, 'call'); + ke = frame.older.environment; + + assertEq(frame.older.older.type, 'eval'); + ee = frame.older.older.environment; + + assertEq(he.inspectable, true); + assertEq(he.getVariable('xh'), 'value of xh'); + assertEq(he.parent.parent.getVariable('xf'), 'value of xf'); + assertEq(ke.inspectable, true); + assertEq(ke.getVariable('xk'), 'value of xk'); + assertEq(ee.inspectable, true); + assertEq(ee.type, 'declarative'); + assertEq(ee.parent.type, 'object'); + + dbg.removeDebuggee(g2); + + assertEq(he.inspectable, true); + assertEq(he.type, 'declarative'); + assertEq(ke.inspectable, false); + assertThrowsInstanceOf(() => ke.getVariable('xk'), Error); + assertEq(ee.inspectable, true); + assertEq(ee.type, 'declarative'); + assertEq(ee.parent.type, 'object'); + + dbg.removeDebuggee(g1); + + assertEq(he.inspectable, false); + assertThrowsInstanceOf(() => he.getVariable('xh'), Error); + assertEq(ke.inspectable, false); + assertThrowsInstanceOf(() => ke.getVariable('xk'), Error); + assertEq(ee.inspectable, false); + assertThrowsInstanceOf(() => ee.type, Error); +} + +assertEq(log, 'd'); + +dbg.addDebuggee(g2); + +assertEq(he.inspectable, false); +assertThrowsInstanceOf(() => he.getVariable('xh'), Error); +assertEq(ke.inspectable, true); +assertEq(ke.getVariable('xk'), 'value of xk'); +assertEq(ee.inspectable, false); +assertThrowsInstanceOf(() => ee.type, Error); diff --git a/js/src/jit-test/tests/debug/Environment-module-01.js b/js/src/jit-test/tests/debug/Environment-module-01.js new file mode 100644 index 0000000000..728c5f64bd --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-module-01.js @@ -0,0 +1,26 @@ +// Debug environments for module environments should include variables that are +// are not closed over or exported. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onEnterFrame = function (frame) { + if (!frame.older) { + return; + } + const env = frame.older.environment; + assertEq(env.names().join(','), "foo,y,x,z"); + assertEq(env.getVariable('x'), 0); + assertEq(env.getVariable('y'), 1); + assertEq(env.getVariable('z'), 2); + env.setVariable('x', 3); + assertEq(env.getVariable('x'), 3); +}; +const m = g.parseModule(` + var x = 0; + export var y = 1; + const z = 2; + foo(); + function foo() {} +`); +m.declarationInstantiation(); +m.evaluation(); diff --git a/js/src/jit-test/tests/debug/Environment-module-02.js b/js/src/jit-test/tests/debug/Environment-module-02.js new file mode 100644 index 0000000000..442116c0a9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-module-02.js @@ -0,0 +1,30 @@ +// Debug environments for module environments should be able to access closed +// over variables after the module script has executed. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +const m = g.parseModule(` + var x = 42; + export function foo() { return x; } + foo(); +`); +m.declarationInstantiation(); + +let fooFunction; +dbg.onEnterFrame = function (frame) { + fooFunction = frame.callee; +}; + +m.evaluation(); +assertEq(fooFunction instanceof Debugger.Object, true); + +dbg.onEnterFrame = function (frame) { + const env = frame.environment.parent; + assertEq(env.names().join(','), "foo,x"); + assertEq(env.getVariable('x'), 42); + env.setVariable('x', 3); + assertEq(env.getVariable('x'), 3); +}; + +fooFunction.call(); diff --git a/js/src/jit-test/tests/debug/Environment-names-01.js b/js/src/jit-test/tests/debug/Environment-names-01.js new file mode 100644 index 0000000000..be053fe6e0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-names-01.js @@ -0,0 +1,19 @@ +// env.names() lists nonenumerable names in with-statement environments. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var env = dbg.getNewestFrame().environment; + var names = env.names(); + assertEq(names.indexOf("a") !== -1, true); + + // FIXME: Bug 748592 - proxies don't correctly propagate JSITER_HIDDEN + //assertEq(names.indexOf("b") !== -1, true); + //assertEq(names.indexOf("isPrototypeOf") !== -1, true); + hits++; +}; +g.eval("var obj = {a: 1};\n" + + "Object.defineProperty(obj, 'b', {value: 2});\n" + + "with (obj) h();"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-names-02.js b/js/src/jit-test/tests/debug/Environment-names-02.js new file mode 100644 index 0000000000..9dd87f49a3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-names-02.js @@ -0,0 +1,34 @@ +// env.names() on object environments ignores property names that are not identifiers. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var withNames, globalNames; +g.h = function () { + var env = dbg.getNewestFrame().environment; + withNames = env.names(); + while (env.parent !== null) + env = env.parent; + globalNames = env.names(); +}; + +g.eval("" + + function fill(obj) { + obj.sanityCheck = 1; + obj["0xcafe"] = 2; + obj[" "] = 3; + obj[""] = 4; + obj[0] = 5; + obj[Symbol.for("moon")] = 6; + return obj; + }) +g.eval("fill(this);\n" + + "with (fill({})) h();"); + +for (var names of [withNames, globalNames]) { + assertEq(names.indexOf("sanityCheck") !== -1, true); + assertEq(names.indexOf("0xcafe"), -1); + assertEq(names.indexOf(" "), -1); + assertEq(names.indexOf(""), -1); + assertEq(names.indexOf("0"), -1); + assertEq(names.indexOf(Symbol.for("moon")), -1); +} diff --git a/js/src/jit-test/tests/debug/Environment-names-03.js b/js/src/jit-test/tests/debug/Environment-names-03.js new file mode 100644 index 0000000000..cffc7cb461 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-names-03.js @@ -0,0 +1,22 @@ +// Optimized out scopes should have working names(). + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +dbg.addDebuggee(g); + +g.eval("" + function f() { + var x = 42; + function g() { } + g(); +}); + +dbg.onEnterFrame = function (f) { + if (f.callee && (f.callee.name === "g")) { + var names = f.environment.parent.names(); + assertEq(names.indexOf("x") !== -1, true); + assertEq(names.indexOf("g") !== -1, true); + assertEq(names.length, 3); // x,g,arguments + } +} + +g.f(); diff --git a/js/src/jit-test/tests/debug/Environment-nondebuggee.js b/js/src/jit-test/tests/debug/Environment-nondebuggee.js new file mode 100644 index 0000000000..32c36a8b84 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-nondebuggee.js @@ -0,0 +1,40 @@ +// Using an environment that is not a debuggee should throw. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +let gw = dbg.addDebuggee(g); +var log; + +function check(env) { + assertEq(env.inspectable, false); + assertThrowsInstanceOf(() => env.type, Error); + assertThrowsInstanceOf(() => env.object, Error); + assertThrowsInstanceOf(() => env.parent, Error); + assertThrowsInstanceOf(() => env.calleeScript, Error); + + assertThrowsInstanceOf(() => env.names(), Error); + assertThrowsInstanceOf(() => env.find('x'), Error); + assertThrowsInstanceOf(() => env.getVariable('x'), Error); + assertThrowsInstanceOf(() => env.setVariable('x'), Error); +} + +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + + let env = frame.environment; + dbg.removeDebuggee(g); + check(env); +} + +log = ''; +g.eval('let x = 42; debugger;'); +assertEq(log, 'd'); + +dbg.addDebuggee(g); +g.eval('function f() { }'); +let env = gw.getOwnPropertyDescriptor('f').value.environment; +assertEq(env.type, 'declarative'); +dbg.removeDebuggee(g); +check(env); diff --git a/js/src/jit-test/tests/debug/Environment-object-01.js b/js/src/jit-test/tests/debug/Environment-object-01.js new file mode 100644 index 0000000000..c8f434bbb1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-object-01.js @@ -0,0 +1,9 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +dbg.onDebuggerStatement = (frame) => { + assertEq(frame.environment.parent.type, "with"); + assertEq(frame.environment.parent.parent.type, "object"); + assertEq(frame.environment.parent.parent.object.getOwnPropertyDescriptor("x").value, 42); +} +g.evalReturningScope("x = 42; debugger;"); diff --git a/js/src/jit-test/tests/debug/Environment-optimizedOut-01.js b/js/src/jit-test/tests/debug/Environment-optimizedOut-01.js new file mode 100644 index 0000000000..964148af9a --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-optimizedOut-01.js @@ -0,0 +1,44 @@ +// Optimized out scopes should be considered optimizedOut. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +dbg.addDebuggee(g); + +g.eval("" + function f() { + var x = 42; + { + let y = 43; + (function () { })(); + } +}); + +dbg.onEnterFrame = function (f) { + if (f.callee && (f.callee.name === undefined)) { + blockenv = f.environment.parent; + assertEq(blockenv.optimizedOut, true); + assertEq(blockenv.inspectable, true); + assertEq(blockenv.type, "declarative"); + assertEq(blockenv.calleeScript, null); + assertEq(blockenv.names().indexOf("y") !== -1, true); + + funenv = blockenv.parent; + assertEq(funenv.optimizedOut, true); + assertEq(funenv.inspectable, true); + assertEq(funenv.type, "declarative"); + assertEq(funenv.calleeScript, f.older.script); + assertEq(funenv.names().indexOf("x") !== -1, true); + + globalenv = funenv.parent.parent; + assertEq(globalenv.optimizedOut, false); + assertEq(globalenv.inspectable, true); + assertEq(globalenv.type, "object"); + assertEq(globalenv.calleeScript, null); + + dbg.removeDebuggee(g); + + assertEq(blockenv.inspectable, false); + assertEq(funenv.inspectable, false); + } +} + +g.f(); diff --git a/js/src/jit-test/tests/debug/Environment-parent-01.js b/js/src/jit-test/tests/debug/Environment-parent-01.js new file mode 100644 index 0000000000..e50f0cc82d --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-parent-01.js @@ -0,0 +1,18 @@ +// The objects on the environment chain are all Debugger.Environment objects. +// The environment chain ends in null. + +var g = newGlobal({newCompartment: true}) +g.eval("function f(a) { return function (b) { return function (c) { h(); return a + b + c; }; }; }"); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var n = 0; + for (var env = dbg.getNewestFrame().environment; env !== null; env = env.parent) { + n++; + assertEq(env instanceof Debugger.Environment, true); + } + assertEq(n >= 4, true); + hits++; +}; +assertEq(g.f(5)(7)(9), 21); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-scopeKind-01.js b/js/src/jit-test/tests/debug/Environment-scopeKind-01.js new file mode 100644 index 0000000000..6f44cc51e2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-scopeKind-01.js @@ -0,0 +1,26 @@ +// Environment.prototype.scopeKind produces expected values. + +load(libdir + 'eqArrayHelper.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +function getScopeKinds(text) { + const kinds = []; + dbg.onDebuggerStatement = frame => { + let env = frame.environment; + while (env) { + kinds.push(env.scopeKind); + env = env.parent; + } + }; + g.eval(text); + return kinds; +} + +assertEqArray(getScopeKinds("function f(x) { debugger; }; f()"), + ["function", null, null]); +assertEqArray(getScopeKinds("function f(x) { let y = 0; debugger; }; f()"), + ["function lexical", "function", null, null]); +assertEqArray(getScopeKinds("function f(x) { let y = 0; with(x) { debugger; } } f({})"), + [null, "function lexical", "function", null, null]); diff --git a/js/src/jit-test/tests/debug/Environment-selfhosted-builtins.js b/js/src/jit-test/tests/debug/Environment-selfhosted-builtins.js new file mode 100644 index 0000000000..1026a486f4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-selfhosted-builtins.js @@ -0,0 +1,15 @@ +// The environment of self-hosted builtins is not exposed to the debugger and +// instead is reported as |undefined| just like native builtins. + +let g = newGlobal({newCompartment: true}); + +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +// Array is a known native builtin function. +let nativeBuiltin = gw.makeDebuggeeValue(g.Array); +assertEq(nativeBuiltin.environment, undefined); + +// Array.prototype[@@iterator] is a known self-hosted builtin function. +let selfhostedBuiltin = gw.makeDebuggeeValue(g.Array.prototype[Symbol.iterator]); +assertEq(selfhostedBuiltin.environment, undefined); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-01.js b/js/src/jit-test/tests/debug/Environment-setVariable-01.js new file mode 100644 index 0000000000..ed67774f79 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-01.js @@ -0,0 +1,9 @@ +// Environment.prototype.setVariable can set global variables. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.environment.parent.setVariable("x", 2); +}; +g.eval("var x = 1; debugger;"); +assertEq(g.x, 2); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-02.js b/js/src/jit-test/tests/debug/Environment-setVariable-02.js new file mode 100644 index 0000000000..7fb7f5e02e --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-02.js @@ -0,0 +1,10 @@ +// The argument to setVariable can be a Debugger.Object. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +dbg.onDebuggerStatement = function (frame) { + frame.environment.parent.setVariable("x", gw); +}; +g.eval("var x = 1; debugger;"); +assertEq(g.x, g); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-03.js b/js/src/jit-test/tests/debug/Environment-setVariable-03.js new file mode 100644 index 0000000000..3fe0dee4d8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-03.js @@ -0,0 +1,16 @@ +// setVariable cannot create new global variables. +// (Other kinds of environment are tested in Environment-variables.js.) + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertThrowsInstanceOf(function () { frame.environment.setVariable("x", 7); }, TypeError); + hits++; +}; +g.eval("debugger"); +assertEq("x" in g, false); +assertEq(hits, 1); + diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-04.js b/js/src/jit-test/tests/debug/Environment-setVariable-04.js new file mode 100644 index 0000000000..d4b2347c79 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-04.js @@ -0,0 +1,10 @@ +// setVariable can set variables and arguments in functions. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.environment.setVariable("a", 100); + frame.environment.setVariable("b", 200); +}; +g.eval("function f(a) { var b = a + 1; debugger; return a + b; }"); +assertEq(g.f(1), 300); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-05.js b/js/src/jit-test/tests/debug/Environment-setVariable-05.js new file mode 100644 index 0000000000..d78edad3bf --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-05.js @@ -0,0 +1,14 @@ +// setVariable can change the types of variables and arguments in functions. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(a) { var b = a + 1; debugger; return a + b; }"); +for (var i = 0; i < 20; i++) + assertEq(g.f(i), 2 * i + 1); + +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.environment.setVariable("a", "xyz"); + frame.environment.setVariable("b", "zy"); +}; +for (var i = 0; i < 10; i++) + assertEq(g.f(i), "xyzzy"); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-06.js b/js/src/jit-test/tests/debug/Environment-setVariable-06.js new file mode 100644 index 0000000000..9b235dfe7a --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-06.js @@ -0,0 +1,9 @@ +// setVariable on an argument works as expected with non-strict 'arguments'. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(a) { debugger; return arguments[0]; }"); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.environment.setVariable("a", 2); +}; +assertEq(g.f(1), 2); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-07.js b/js/src/jit-test/tests/debug/Environment-setVariable-07.js new file mode 100644 index 0000000000..2b58885335 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-07.js @@ -0,0 +1,14 @@ +// setVariable works on let-bindings. + +var g = newGlobal({newCompartment: true}); +function test(code, val) { + g.eval("function f() { " + code + " }"); + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function (frame) { + frame.environment.setVariable("a", val); + }; + assertEq(g.f(), val); +} + +test("let a = 1; debugger; return a;", "xyzzy"); +test("{ let a = 1; debugger; return a; }", "plugh"); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-08.js b/js/src/jit-test/tests/debug/Environment-setVariable-08.js new file mode 100644 index 0000000000..35de70975b --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-08.js @@ -0,0 +1,29 @@ +// setVariable throws if no binding exists. + +load(libdir + "asserts.js"); + +function test(code) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger(g); + var hits = 0; + dbg.onDebuggerStatement = function (frame) { + var env = frame.older.environment; + assertThrowsInstanceOf(function () { env.setVariable("y", 2); }, Error); + hits++; + }; + g.eval("var y = 0; function d() { debugger; }"); + + assertEq(g.eval(code), 0); + + assertEq(g.y, 0); + assertEq(hits, 1); +} + +// local scope of non-heavyweight function +test("function f() { var x = 1; d(); return y; } f();"); + +// block scope +test("function h(x) { if (x) { let x = 1; d(); return y; } } h(3);"); + +// strict eval scope +test("'use strict'; eval('d(); y;');"); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-10.js b/js/src/jit-test/tests/debug/Environment-setVariable-10.js new file mode 100644 index 0000000000..2ba46b06b3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-10.js @@ -0,0 +1,32 @@ +// setVariable works on non-innermost environments. + +// (The debuggee code here is a bit convoluted to defeat optimizations that +// could make obj.b a null closure or obj.i a flat closure--that is, a function +// that gets a frozen copy of i instead of a reference to the runtime +// environment that contains it. setVariable does not currently detect this +// flat closure case.) + +var g = newGlobal({newCompartment: true}); +g.eval("function d() { debugger; }\n" + + "var i = 'FAIL';\n" + + "function a() {\n" + + " var obj = {b: function (i) { d(obj); return i; },\n" + + " i: function () { return i; }};\n" + + " var i = 'FAIL2';\n" + + " return obj;\n" + + "}\n"); + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + var x = 0; + for (var env = frame.older.environment; env; env = env.parent) { + if (env.getVariable("i") !== undefined) + env.setVariable("i", x++); + } +}; + +var obj = g.a(); +var r = obj.b('FAIL3'); +assertEq(r, 0); +assertEq(obj.i(), 1); +assertEq(g.i, 2); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-11.js b/js/src/jit-test/tests/debug/Environment-setVariable-11.js new file mode 100644 index 0000000000..d2030159d1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-11.js @@ -0,0 +1,16 @@ +// setVariable cannot modify the binding for a FunctionExpression's name. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var env = frame.environment.find("f"); + assertEq(env.getVariable("f"), frame.callee); + assertThrowsInstanceOf(function () { env.setVariable("f", 0) }, TypeError); + assertThrowsInstanceOf(function () { env.setVariable("f", frame.callee) }, TypeError); + hits++; +}; +g.eval("(function f() { eval(\"\"); debugger; })();"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-12.js b/js/src/jit-test/tests/debug/Environment-setVariable-12.js new file mode 100644 index 0000000000..6806ad9196 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-12.js @@ -0,0 +1,21 @@ +// setVariable can create a new property on a with block's bindings object, if +// it is shadowing an existing property on the prototype chain. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + var env = frame.environment.find("x"); + env.setVariable("x", 2); +}; +g.eval("var obj1 = {x: 1}, obj2 = Object.create(obj1), z; with (obj2) { debugger; z = x; }"); +assertEq(g.obj1.x, 1); +assertEq(g.obj2.x, 2); +assertEq(g.z, 2); + +// The property created by setVariable is like the one created by ordinary +// assignment in a with-block. +var desc = Object.getOwnPropertyDescriptor(g.obj2, "x"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, true); +assertEq(desc.writable, true); +assertEq(desc.value, 2); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-13.js b/js/src/jit-test/tests/debug/Environment-setVariable-13.js new file mode 100644 index 0000000000..9e9f1653b6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-13.js @@ -0,0 +1,20 @@ +// Debugger.Environment should throw trying to setVariable on optimized out scope. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +dbg.addDebuggee(g); + +g.eval("" + function f() { + var x = 42; + function g() { } + g(); +}); + +dbg.onEnterFrame = function (f) { + if (f.callee && (f.callee.name === "g")) + assertThrowsInstanceOf(function () { f.environment.parent.setVariable("x", 43) }, ReferenceError); +} + +g.f(); diff --git a/js/src/jit-test/tests/debug/Environment-setVariable-WouldRun.js b/js/src/jit-test/tests/debug/Environment-setVariable-WouldRun.js new file mode 100644 index 0000000000..a9d97dfacc --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-setVariable-WouldRun.js @@ -0,0 +1,23 @@ +// setVariable triggering a setter throws WouldRunDebuggee. + +load(libdir + "asserts.js"); + +function test(code) { + var g = newGlobal({newCompartment: true}); + g.eval("function d() { debugger; }"); + var dbg = Debugger(g); + var hits = 0; + dbg.onDebuggerStatement = function (frame) { + var env = frame.older.environment.find("x"); + assertThrowsInstanceOf( + () => env.setVariable("x", 0), + Debugger.DebuggeeWouldRun); + hits++; + }; + g.eval(code); + assertEq(hits, 1); +} + +test("Object.defineProperty(this, 'x', {set: function (v) {}}); d();"); +test("Object.defineProperty(Object.prototype, 'x', {set: function (v) {}}); d();"); +test("with ({set x(v) {}}) eval(d());"); diff --git a/js/src/jit-test/tests/debug/Environment-type-01.js b/js/src/jit-test/tests/debug/Environment-type-01.js new file mode 100644 index 0000000000..39b339cb39 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-type-01.js @@ -0,0 +1,29 @@ +// env.type is 'object' in global environments and with-blocks, and 'declarative' otherwise. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +function test(code, expected) { + var actual = ''; + g.h = function () { actual += dbg.getNewestFrame().environment.type; } + g.eval(code); + assertEq(actual, expected); +} + +test("h();", 'declarative'); +test("(function (s) { eval(s); })('var v = h();')", 'declarative'); +test("(function (s) { h(); })();", 'declarative'); +test("{let x = 1, y = 2; h();}", 'declarative'); +test("with({x: 1, y: 2}) h();", 'with'); +test("(function (s) { with ({x: 1, y: 2}) h(); })();", 'with'); +test("{ let x = 1; h(); }", 'declarative'); +test("for (let x = 0; x < 1; x++) h();", 'declarative'); +test("for (let x in h()) ;", 'declarative'); +test("for (let x in {a:1}) h();", 'declarative'); +test("try { throw new Error; } catch (x) { h(x) }", 'declarative'); +test("'use strict'; eval('var z = 1; h();');", 'declarative'); + +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.eval("h(), 2 + 2;").return, 4); +} +test("debugger;", 'declarative'); +test("(function f() { debugger; })();", 'declarative'); diff --git a/js/src/jit-test/tests/debug/Environment-unscopables.js b/js/src/jit-test/tests/debug/Environment-unscopables.js new file mode 100644 index 0000000000..d738fc11b3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-unscopables.js @@ -0,0 +1,37 @@ +// An Environment for a `with` statement does not observe bindings ruled out by @@unscopables. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(` + let x = 'global'; + function f() { + let obj = { + x: 'obj', + y: 'obj', + [Symbol.unscopables]: {x: 1}, + }; + with (obj) + debugger; + } +`); +let dbg = Debugger(g); +let hits = 0; +dbg.onDebuggerStatement = function (frame) { + let env = frame.environment; + + assertEq(env.find("x") !== env, true); + assertEq(env.names().indexOf("x"), -1); + assertEq(env.getVariable("x"), undefined); + assertThrowsInstanceOf(() => env.setVariable("x", 7), TypeError); + + assertEq(env.find("y") === env, true); + assertEq(env.getVariable("y"), "obj"); + env.setVariable("y", 8); + + assertEq(frame.eval("x").return, "global"); + assertEq(frame.eval("y").return, 8); + hits++; +}; +g.f(); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-variables.js b/js/src/jit-test/tests/debug/Environment-variables.js new file mode 100644 index 0000000000..7a9b7072c4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-variables.js @@ -0,0 +1,85 @@ +// Comprehensive test of get/setVariable on many kinds of environments and +// bindings. + +load(libdir + "asserts.js"); + +var cases = [ + // global bindings and bindings on the global prototype chain + "x = VAL; @@", + "var x = VAL; @@", + "Object.prototype.x = VAL; @@", + + // let and catch bindings + "let x = VAL; @@", + "{ let x = VAL; @@ }", + "try { throw VAL; } catch (x) { @@ }", + "try { throw VAL; } catch (x) { @@ }", + "for (let x of [VAL]) { @@ }", + "switch (0) { default: let x = VAL; @@ }", + + // arguments + "function f(x) { @@ } f(VAL);", + "function f([w, x]) { @@ } f([0, VAL]);", + "function f({v: x}) { @@ } f({v: VAL});", + "function f([w, {v: x}]) { @@ } f([0, {v: VAL}]);", + + // bindings in functions + "function f() { var x = VAL; @@ } f();", + "function f() { let x = VAL; @@ } f();", + "function f() { function x() {} x = VAL; @@ } f();", + + // dynamic bindings + "function f(s) { eval(s); @@ } f('var x = VAL');", + "var x = VAL; function f(s) { eval('var x = 0;'); eval(s); @@ } f('delete x;');", + "function f(obj) { with (obj) { @@ } } f({x: VAL});", + "function f(obj) { with (obj) { @@ } } f(Object.create({x: VAL}));", + "function f(b) { if (b) { function x(){} } x = VAL; @@ } f(1);", +]; + +var nextval = 1000; + +function test(code, debugStmts, followupStmts) { + var val = nextval++; + var hits = 0; + + var g = newGlobal({newCompartment: true}); + g.eval("function debugMe() { var x = 'wrong-x'; eval(\"\"); debugger; }"); + g.capture = null; + + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + if (frame.callee !== null && frame.callee.name == 'debugMe') + frame = frame.older; + var env = frame.environment.find("x"); + assertEq(env.getVariable("x"), val) + assertEq(env.setVariable("x", 'ok'), undefined); + assertEq(env.getVariable("x"), 'ok'); + + // setVariable cannot create new variables. + assertThrowsInstanceOf(function () { env.setVariable("newVar", 0); }, TypeError); + hits++; + }; + + code = code.replace("@@", debugStmts); + if (followupStmts !== undefined) + code += " " + followupStmts; + code = code.replace(/VAL/g, String(val)); + g.eval(code); + assertEq(hits, 1); +} + +for (var s of cases) { + // Test triggering the debugger right in the scope in which x is bound. + test(s, "eval(\"\"); debugger; assertEq(x, 'ok');"); + + // Test calling a function that triggers the debugger. + test(s, "debugMe(); assertEq(x, 'ok');"); + + // Test triggering the debugger from a scope nested in x's scope. + test(s, "{ let y = 'irrelevant'; (function (z) { { let zz = y; eval(\"\"); debugger; } })(); } assertEq(x, 'ok');"), + + // Test closing over the variable and triggering the debugger later, after + // leaving the variable's scope. + test(s, "capture = {dbg: function () { eval(\"\"); debugger; }, get x() { return x; }};", + "assertEq(capture.x, VAL); capture.dbg(); assertEq(capture.x, 'ok');"); +} diff --git a/js/src/jit-test/tests/debug/Frame-01.js b/js/src/jit-test/tests/debug/Frame-01.js new file mode 100644 index 0000000000..9619fab8f0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-01.js @@ -0,0 +1,34 @@ +// Test .type fields of topmost stack frame passed to onDebuggerStatement. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var expected, hits; +dbg.onDebuggerStatement = function (f) { + assertEq(Object.getPrototypeOf(f), Debugger.Frame.prototype); + assertEq(f.type, expected.type); + assertEq(f.script.isGeneratorFunction, expected.generator); + assertEq(f.constructing, expected.constructing); + hits++; +}; + +function test(code, expectobj, expectedHits) { + expected = expectobj; + hits = 0; + g.evaluate(code); + assertEq(hits, arguments.length < 3 ? 1 : expectedHits); +} + +test("debugger;", {type: "global", generator: false, constructing: false}); +test("(function () { debugger; })();", {type: "call", generator: false, constructing: false}); +test("new function() { debugger; };", {type: "call", generator: false, constructing: true}); +test("new function () { (function() { debugger; })(); }", {type: "call", generator: false, constructing: false}); +test("eval('debugger;');", {type: "eval", generator: false, constructing: false}); +test("this.eval('debugger;'); // indirect eval", {type: "eval", generator: false, constructing: false}); +test("(function () { eval('debugger;'); })();", {type: "eval", generator: false, constructing: false}); +test("new function () { eval('debugger'); }", {type: "eval", generator: false, constructing: false}); +test("function* gen() { debugger; yield 1; debugger; }\n" + + "for (var x of gen()) {}\n", + {type: "call", generator: true, constructing: false}, 2); +test("var iter = (function* stargen() { debugger; yield 1; debugger; })();\n" + + "iter.next(); iter.next();", + {type: "call", generator: true, constructing: false}, 2); diff --git a/js/src/jit-test/tests/debug/Frame-02.js b/js/src/jit-test/tests/debug/Frame-02.js new file mode 100644 index 0000000000..dddb1e7024 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-02.js @@ -0,0 +1,24 @@ +// When the debugger is triggered twice from the same stack frame, the same +// Debugger.Frame object is passed to the hook both times. + +var g = newGlobal({newCompartment: true}); +var hits, frame; +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (f) { + if (hits++ == 0) + frame = f; + else + assertEq(f, frame); +}; + +hits = 0; +g.evaluate("debugger; debugger;"); +assertEq(hits, 2); + +hits = 0; +g.evaluate("function f() { debugger; debugger; } f();"); +assertEq(hits, 2); + +hits = 0; +g.evaluate("eval('debugger; debugger;');"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-03.js b/js/src/jit-test/tests/debug/Frame-03.js new file mode 100644 index 0000000000..19a300084a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-03.js @@ -0,0 +1,19 @@ +// When the debugger is triggered from different stack frames that happen to +// occupy the same memory, it delivers different Debugger.Frame objects. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits; +var a = []; +dbg.onDebuggerStatement = function (frame) { + for (var i = 0; i < a.length; i++) + assertEq(a[i] === frame, false); + a.push(frame); + hits++; +}; + +g.eval("function f() { debugger; }"); +g.eval("function h() { debugger; f(); }"); +hits = 0; +g.eval("for (var i = 0; i < 4; i++) h();"); +assertEq(hits, 8); diff --git a/js/src/jit-test/tests/debug/Frame-arguments-01.js b/js/src/jit-test/tests/debug/Frame-arguments-01.js new file mode 100644 index 0000000000..29a68b7cf4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-arguments-01.js @@ -0,0 +1,41 @@ +// Frame.prototype.arguments with primitive values + +var g = newGlobal({newCompartment: true}); +g.args = null; +var dbg = new Debugger(g); +var hits; +var v; +dbg.onDebuggerStatement = function (frame) { + hits++; + var args = frame.arguments; + assertEq(args instanceof Array, true); + assertEq(Array.isArray(args), false); + assertEq(args, frame.arguments); + assertEq(args.length, g.args.length); + for (var i = 0; i < args.length; i++) + assertEq(args[i], g.args[i]); +}; + +// no formal parameters +g.eval("function f() { debugger; }"); + +hits = 0; +g.eval("args = []; f();"); +g.eval("this.f();"); +g.eval("var world = Symbol('world'); " + + "args = ['hello', world, 3.14, true, false, null, undefined]; " + + "f('hello', world, 3.14, true, false, null, undefined);"); +g.eval("f.apply(undefined, args);"); +g.eval("args = [-0, NaN, -1/0]; this.f(-0, NaN, -1/0);"); +assertEq(hits, 5); + +// with formal parameters +g.eval("function f(a, b) { debugger; }"); + +hits = 0; +g.eval("args = []; f();"); +g.eval("this.f();"); +g.eval("args = ['a', 'b']; f('a', 'b');"); +g.eval("this.f('a', 'b');"); +g.eval("f.bind(null, 'a')('b');"); +assertEq(hits, 5); diff --git a/js/src/jit-test/tests/debug/Frame-arguments-02.js b/js/src/jit-test/tests/debug/Frame-arguments-02.js new file mode 100644 index 0000000000..145ec824d8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-arguments-02.js @@ -0,0 +1,19 @@ +// Object arguments. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var args = frame.arguments; + assertEq(args, frame.arguments); + assertEq(args instanceof Array, true); + assertEq(args.length, 2); + assertEq(args[0] instanceof Debugger.Object, true); + assertEq(args[0].class, args[1]); + hits++; +}; + +g.eval("function f(obj, cls) { debugger; }"); +g.eval("f({}, 'Object');"); +g.eval("f(Date, 'Function');"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-arguments-03.js b/js/src/jit-test/tests/debug/Frame-arguments-03.js new file mode 100644 index 0000000000..b2d36eb0b5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-arguments-03.js @@ -0,0 +1,34 @@ +// Destructuring arguments. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var args = frame.arguments; + assertEq(args[0], 1); + assertEq(args.length, 4); + + assertEq(args[1] instanceof Debugger.Object, true); + assertEq(args[1].class, "Array"); + var getprop = frame.eval("(function (p) { return this[p]; })").return; + assertEq(getprop instanceof Debugger.Object, true); + assertEq(getprop.apply(args[1], ["length"]).return, 2); + assertEq(getprop.apply(args[1], [0]).return, 2); + assertEq(getprop.apply(args[1], [1]).return, 3); + + assertEq(args[2] instanceof Debugger.Object, true); + assertEq(args[2].class, "Object"); + var x = getprop.apply(args[2], ["x"]).return; + assertEq(x.class, "Array"); + assertEq(getprop.apply(x, ["0"]).return, 4); + assertEq(getprop.apply(args[2], ["z"]).return, 5); + + assertEq(args[3] instanceof Debugger.Object, true); + assertEq(args[3].class, "Object"); + assertEq(getprop.apply(args[3], ["q"]).return, 6); + hits++; +}; + +g.eval("function f(a, [b, c], {x: [y], z: w}, {q}) { debugger; }"); +g.eval("f(1, [2, 3], {x: [4], z: 5}, {q: 6});"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-arguments-04.js b/js/src/jit-test/tests/debug/Frame-arguments-04.js new file mode 100644 index 0000000000..21db3560aa --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-arguments-04.js @@ -0,0 +1,18 @@ +// frame.arguments works for all live frames + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + for (var i = 0; i <= 4; i++) { + assertEq(frame.arguments.length, 1); + assertEq(frame.arguments[0], i); + frame = frame.older; + } + assertEq(frame, null); + hits++; +}; + +g.eval("function f(n) { if (n == 0) debugger; else f(n - 1); }"); +g.f(4); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-arguments-05.js b/js/src/jit-test/tests/debug/Frame-arguments-05.js new file mode 100644 index 0000000000..bc34809b11 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-arguments-05.js @@ -0,0 +1,19 @@ +// frame.arguments is "live" (it reflects assignments to arguments). + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log = ''; +var args; +dbg.onDebuggerStatement = function (frame) { + if (args === undefined) + args = frame.arguments; + else + assertEq(frame.arguments, args); + log += args[0]; + assertEq(frame.eval("x = '0';").return, '0'); + log += args[0]; +}; + +g.eval("function f(x) { x = '2'; debugger; x = '3'; debugger; }"); +g.f("1"); +assertEq(log, "2030"); diff --git a/js/src/jit-test/tests/debug/Frame-arguments-06.js b/js/src/jit-test/tests/debug/Frame-arguments-06.js new file mode 100644 index 0000000000..ade1b6ac8f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-arguments-06.js @@ -0,0 +1,38 @@ +// Test extracting frame.arguments element getters and calling them in +// various awkward ways. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +var fframe, farguments, fgetter; +dbg.onDebuggerStatement = function (frame) { + if (hits === 0) { + fframe = frame; + farguments = frame.arguments; + fgetter = Object.getOwnPropertyDescriptor(farguments, "0").get; + assertEq(fgetter instanceof Function, true); + + // Calling the getter without an appropriate this-object + // fails, but shouldn't assert or crash. + assertThrowsInstanceOf(function () { fgetter.call(Math); }, TypeError); + } else { + // Since fframe is still on the stack, fgetter can be applied to it. + assertEq(fframe.onStack, true); + assertEq(fgetter.call(farguments), 100); + + // Since h was called without arguments, there is no argument 0. + assertEq(fgetter.call(frame.arguments), undefined); + } + hits++; +}; + +g.eval("function h() { debugger; }"); +g.eval("function f(x) { debugger; h(); }"); +g.f(100); +assertEq(hits, 2); + +// Now that fframe is no longer live, trying to get its arguments should throw. +assertEq(fframe.onStack, false); +assertThrowsInstanceOf(function () { fgetter.call(farguments); }, Error); diff --git a/js/src/jit-test/tests/debug/Frame-arguments-07.js b/js/src/jit-test/tests/debug/Frame-arguments-07.js new file mode 100644 index 0000000000..a73e4e97ed --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-arguments-07.js @@ -0,0 +1,24 @@ +// When argument[x] is assigned, where x > callee.length, frame.arguments reflects the change. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(a, b) {\n" + + " for (var i = 0; i < arguments.length; i++)\n" + + " arguments[i] = i;\n" + + " eval(\"\");\n" + + " debugger;\n" + + "}\n"); + +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var argc = frame.eval("arguments.length").return; + var args = frame.arguments; + assertEq(args.length, argc); + for (var i = 0; i < argc; i++) + assertEq(args[i], i); + hits++; +} + +g.f(9); +g.f(9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-asyncPromise-01.js b/js/src/jit-test/tests/debug/Frame-asyncPromise-01.js new file mode 100644 index 0000000000..d8ac80a6ce --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-asyncPromise-01.js @@ -0,0 +1,72 @@ +// Testing the promise property on async function frames. + +load(libdir + 'asserts.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +g.eval(` +async function f() { + debugger; + await Promise.resolve(); + debugger; +} +`); + +let frame; +const promises = []; +dbg.onEnterFrame = function(f) { + frame = f; + promises.push(["start", frame.asyncPromise]); + + dbg.onEnterFrame = function(f) { + assertEq(f, frame); + promises.push(["resume", frame.asyncPromise]); + }; + + frame.onPop = function() { + promises.push(["suspend", frame.asyncPromise]); + + frame.onPop = function() { + promises.push(["finish", frame.asyncPromise]); + }; + }; + + dbg.onDebuggerStatement = function(f) { + assertEq(f, frame); + promises.push(["dbg", frame.asyncPromise]); + }; +}; + +(async () => { + const resultPromise = g.f(); + + const framePromise = frame.asyncPromise; + assertEq(framePromise instanceof Debugger.Object, true); + assertEq(framePromise.unsafeDereference(), resultPromise); + + assertEq(promises.length, 3); + assertEq(promises[0][0], "start"); + assertEq(promises[0][1], null); + + assertEq(promises[1][0], "dbg"); + assertEq(promises[1][1], framePromise); + + assertEq(promises[2][0], "suspend"); + assertEq(promises[2][1], framePromise); + + await resultPromise; + + // Frame has terminated, so accessing the property throws. + assertThrowsInstanceOf(() => frame.asyncPromise, Error); + + assertEq(promises.length, 6); + + assertEq(promises[3][0], "resume"); + assertEq(promises[3][1], framePromise); + + assertEq(promises[4][0], "dbg"); + assertEq(promises[4][1], framePromise); + + assertEq(promises[5][0], "finish"); + assertEq(promises[5][1], framePromise); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-asyncPromise-02.js b/js/src/jit-test/tests/debug/Frame-asyncPromise-02.js new file mode 100644 index 0000000000..c65d13f9c5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-asyncPromise-02.js @@ -0,0 +1,26 @@ +// Test the promise property on normal function frames. + +load(libdir + 'asserts.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +g.eval(` +function f() { + debugger; +} +`); + +let frame; +const promises = []; +dbg.onDebuggerStatement = function(f) { + frame = f; + promises.push(frame.asyncPromise); +}; + +const resultPromise = g.f(); + +// Frame has terminated, so accessing the property throws. +assertThrowsInstanceOf(() => frame.asyncPromise, Error); + +assertEq(promises.length, 1); +assertEq(promises[0], undefined); diff --git a/js/src/jit-test/tests/debug/Frame-asyncPromise-03.js b/js/src/jit-test/tests/debug/Frame-asyncPromise-03.js new file mode 100644 index 0000000000..8dee273c28 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-asyncPromise-03.js @@ -0,0 +1,39 @@ +// Test the promise property on generator function frames. + +load(libdir + 'asserts.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +g.eval(` +function* f() { + debugger; + yield; + debugger; +} +`); + +let frame; +const promises = []; +dbg.onDebuggerStatement = function(f) { + frame = f; + promises.push(frame.asyncPromise); +}; + +const it = g.f(); + +assertEq(promises.length, 0); + +it.next(); + +assertEq(frame.asyncPromise, undefined); + +assertEq(promises.length, 1); +assertEq(promises[0], undefined); + +it.next(); + +// Frame has terminated, so accessing the property throws. +assertThrowsInstanceOf(() => frame.asyncPromise, Error); + +assertEq(promises.length, 2); +assertEq(promises[1], undefined); diff --git a/js/src/jit-test/tests/debug/Frame-asyncPromise-04.js b/js/src/jit-test/tests/debug/Frame-asyncPromise-04.js new file mode 100644 index 0000000000..1d2a6bf55f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-asyncPromise-04.js @@ -0,0 +1,104 @@ +// Test the promise property on async generator function frames. + +load(libdir + 'asserts.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); +g.eval(` +async function* f() { + debugger; + yield 42; + debugger; + return 50 +} +`); + +let frame; +const promises = []; +dbg.onEnterFrame = function(f) { + frame = f; + promises.push(["enter", frame.asyncPromise]); + + frame.onPop = function() { + promises.push(["leave", frame.asyncPromise]); + }; + + dbg.onDebuggerStatement = function(f) { + assertEq(f, frame); + promises.push(["dbg", frame.asyncPromise]); + }; +}; + + +(async () => { + const it = g.f(); + + // Promise is null until initial iteration. + assertEq(frame.asyncPromise, null); + + assertEq(promises.length, 2); + assertEq(promises[0][0], "enter"); + assertEq(promises[0][1], null); + + assertEq(promises[1][0], "leave"); + assertEq(promises[1][1], null); + + + const result1Promise = it.next(); + + const frameIt1Promise = frame.asyncPromise; + assertEq(frameIt1Promise instanceof Debugger.Object, true); + assertEq(frameIt1Promise.unsafeDereference(), result1Promise); + + assertEq(promises.length, 5); + assertEq(promises[2][0], "enter"); + assertEq(promises[2][1], frameIt1Promise); + + assertEq(promises[3][0], "dbg"); + assertEq(promises[3][1], frameIt1Promise); + + assertEq(promises[4][0], "leave"); + assertEq(promises[4][1], frameIt1Promise); + + await result1Promise; + + // Iteration step has fully completed, so promise state has been cleared. + assertEq(frame.asyncPromise, null); + + assertEq(promises.length, 7); + assertEq(promises[5][0], "enter"); + assertEq(promises[5][1], frameIt1Promise); + + assertEq(promises[6][0], "leave"); + assertEq(promises[6][1], frameIt1Promise); + + + const result2Promise = it.next(); + + const frameIt2Promise = frame.asyncPromise; + assertEq(frameIt2Promise instanceof Debugger.Object, true); + assertEq(frameIt2Promise.unsafeDereference(), result2Promise); + assertEq(frameIt1Promise !== frameIt2Promise, true); + + assertEq(promises.length, 10); + assertEq(promises[7][0], "enter"); + assertEq(promises[7][1], frameIt2Promise); + + assertEq(promises[8][0], "dbg"); + assertEq(promises[8][1], frameIt2Promise); + + assertEq(promises[9][0], "leave"); + assertEq(promises[9][1], frameIt2Promise); + + await result2Promise; + + // Frame has terminated, so accessing the property throws. + assertThrowsInstanceOf(() => frame.asyncPromise, Error); + + assertEq(promises.length, 12); + assertEq(promises[10][0], "enter"); + assertEq(promises[10][1], frameIt2Promise); + + assertEq(promises[11][0], "leave"); + assertEq(promises[11][1], frameIt2Promise); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-callee-01.js b/js/src/jit-test/tests/debug/Frame-callee-01.js new file mode 100644 index 0000000000..05daf85784 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-callee-01.js @@ -0,0 +1,26 @@ +// Frame.prototype.callee for a normal function frame. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +function f() {} +`); + +const fObj = dbg.makeGlobalObjectReference(g).makeDebuggeeValue(g.f); +let frame; +let callee; +dbg.onEnterFrame = function(f) { + frame = f; + callee = frame.callee; +}; + +g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(callee instanceof Debugger.Object, true); +assertEq(callee, fObj); + +// The frame has finished evaluating, so the callee is no longer accessible. +assertThrowsInstanceOf(() => frame.callee, Error); diff --git a/js/src/jit-test/tests/debug/Frame-callee-02.js b/js/src/jit-test/tests/debug/Frame-callee-02.js new file mode 100644 index 0000000000..ca1c979cca --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-callee-02.js @@ -0,0 +1,37 @@ +// Frame.prototype.callee for a generator function frame. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +function* f() {} +`); + +const fObj = dbg.makeGlobalObjectReference(g).makeDebuggeeValue(g.f); +let frame; +let callee; +dbg.onEnterFrame = function(f) { + frame = f; + callee = frame.callee; +}; + +const it = g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(callee instanceof Debugger.Object, true); +assertEq(callee, fObj); +assertEq(frame.callee, callee); + +const lastFrame = frame; +const lastCallee = callee; +frame = null; +callee = null; + +it.next(); + +assertEq(frame, lastFrame); +assertEq(callee, lastCallee); + +// The frame has finished evaluating, so the callee is no longer accessible. +assertThrowsInstanceOf(() => frame.callee, Error); diff --git a/js/src/jit-test/tests/debug/Frame-callee-03.js b/js/src/jit-test/tests/debug/Frame-callee-03.js new file mode 100644 index 0000000000..fc0f6f51eb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-callee-03.js @@ -0,0 +1,37 @@ +// Frame.prototype.callee for an async function frame. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +async function f() { await Promise.resolve(); } +`); + +const fObj = dbg.makeGlobalObjectReference(g).makeDebuggeeValue(g.f); +let frame; +let callee; +dbg.onEnterFrame = function(f) { + frame = f; + callee = frame.callee; +}; + +const promise = g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(callee instanceof Debugger.Object, true); +assertEq(callee, fObj); +assertEq(frame.callee, callee); + +const lastFrame = frame; +const lastCallee = callee; +frame = null; +callee = null; + +promise.then(() => { + assertEq(frame, lastFrame); + assertEq(callee, lastCallee); + + // The frame has finished evaluating, so the callee is no longer accessible. + assertThrowsInstanceOf(() => frame.callee, Error); +}); diff --git a/js/src/jit-test/tests/debug/Frame-callee-04.js b/js/src/jit-test/tests/debug/Frame-callee-04.js new file mode 100644 index 0000000000..a260b7d9e6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-callee-04.js @@ -0,0 +1,47 @@ +// Frame.prototype.callee for an async generator function frame. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +async function* f() { await Promise.resolve(); } +`); + +const fObj = dbg.makeGlobalObjectReference(g).makeDebuggeeValue(g.f); +let frame; +let callee; +dbg.onEnterFrame = function(f) { + frame = f; + callee = frame.callee; +}; + +const it = g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(callee instanceof Debugger.Object, true); +assertEq(callee, fObj); +assertEq(frame.callee, callee); + +let lastFrame = frame; +let lastCallee = callee; +frame = null; +callee = null; + +const promise = it.next(); + +assertEq(callee, fObj); +assertEq(frame.callee, callee); + +lastFrame = frame; +lastCallee = callee; +frame = null; +callee = null; + +promise.then(() => { + assertEq(frame, lastFrame); + assertEq(callee, lastCallee); + + // The frame has finished evaluating, so the callee is no longer accessible. + assertThrowsInstanceOf(() => frame.callee, Error); +}); diff --git a/js/src/jit-test/tests/debug/Frame-constructing-01.js b/js/src/jit-test/tests/debug/Frame-constructing-01.js new file mode 100644 index 0000000000..c17a454512 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-constructing-01.js @@ -0,0 +1,27 @@ +// Debugger.Frame.prototype.constructing on a generator. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); + +g.eval(` +function* f() {} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.constructing, false); +}; + +const it = g.f(); + +assertEq(frame.constructing, false); +frame = null; + +it.next(); + +assertEq(!!frame, true); +// Throws because the frame has terminated. +assertThrowsInstanceOf(() => frame.constructing, Error); diff --git a/js/src/jit-test/tests/debug/Frame-constructing-02.js b/js/src/jit-test/tests/debug/Frame-constructing-02.js new file mode 100644 index 0000000000..b1c0c846a7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-constructing-02.js @@ -0,0 +1,31 @@ +// Debugger.Frame.prototype.constructing on an async function. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); + +g.eval(` +async function f() { + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.constructing, false); +}; + +(async () => { + const promise = g.f(); + + assertEq(frame.constructing, false); + frame = null; + + await promise; + + assertEq(!!frame, true); + // Throws because the frame has terminated. + assertThrowsInstanceOf(() => frame.constructing, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-constructing-03.js b/js/src/jit-test/tests/debug/Frame-constructing-03.js new file mode 100644 index 0000000000..4569314131 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-constructing-03.js @@ -0,0 +1,35 @@ +// Debugger.Frame.prototype.constructing on an async generator. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); + +g.eval(` +async function* f() { + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.constructing, false); +}; + +(async () => { + const it = g.f(); + + assertEq(frame.constructing, false); + frame = null; + + const promise = it.next(); + + assertEq(!!frame, true); + assertEq(frame.constructing, false); + + await promise; + + // Throws because the frame has terminated. + assertThrowsInstanceOf(() => frame.constructing, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-environment-01.js b/js/src/jit-test/tests/debug/Frame-environment-01.js new file mode 100644 index 0000000000..1168732e29 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-01.js @@ -0,0 +1,13 @@ +// frame.environment is a Debugger.Environment object + +var g = newGlobal({newCompartment: true}) +var dbg = Debugger(g); +g.h = function () { + assertEq(dbg.getNewestFrame().environment instanceof Debugger.Environment, true); +}; + +g.eval("h()"); +g.evaluate("h()"); +g.eval("eval('h()')"); +g.eval("function f() { h(); }"); +g.f(); diff --git a/js/src/jit-test/tests/debug/Frame-environment-02.js b/js/src/jit-test/tests/debug/Frame-environment-02.js new file mode 100644 index 0000000000..76283711be --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-02.js @@ -0,0 +1,12 @@ +// dbg.getNewestFrame().environment works. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.h = function () { + var env = dbg.getNewestFrame().environment.parent; + assertEq(env instanceof Debugger.Environment, true); + assertEq(env.object, gw); + assertEq(env.parent, null); +}; +g.eval("h()"); diff --git a/js/src/jit-test/tests/debug/Frame-environment-03.js b/js/src/jit-test/tests/debug/Frame-environment-03.js new file mode 100644 index 0000000000..71f176260c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-03.js @@ -0,0 +1,11 @@ +// If !frame.onStack, frame.environment throws. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var frame; +g.h = function () { frame = dbg.getNewestFrame(); }; +g.eval("h();"); +assertEq(frame.onStack, false); +assertThrowsInstanceOf(function () { frame.environment; }, Error); diff --git a/js/src/jit-test/tests/debug/Frame-environment-04.js b/js/src/jit-test/tests/debug/Frame-environment-04.js new file mode 100644 index 0000000000..280b8a7c49 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-04.js @@ -0,0 +1,12 @@ +// frame.environment can be called from the onEnterFrame hook. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(x) { return 2 * x; }"); +var dbg = Debugger(g); +var hits = 0; +dbg.onEnterFrame = function (frame) { + assertEq(frame.environment.names().join(","), "arguments,x"); + hits++; +}; +assertEq(g.f(22), 44); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-environment-05.js b/js/src/jit-test/tests/debug/Frame-environment-05.js new file mode 100644 index 0000000000..e39140b438 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-05.js @@ -0,0 +1,9 @@ +// Test that Debugger.Frame.prototype.environment works at all pcs of a script +// with an aliased block scope. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.onStep = (function () { frame.environment; }); +}; +g.eval("debugger; for (let i of [1,2,3]) print(i);"); diff --git a/js/src/jit-test/tests/debug/Frame-environment-06.js b/js/src/jit-test/tests/debug/Frame-environment-06.js new file mode 100644 index 0000000000..6faea6ad52 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-06.js @@ -0,0 +1,105 @@ +// |jit-test| error:all-jobs-completed-successfully +// Test that Debugger.Frame.prototype.environment works on suspended +// async function. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +var resolveTop; +var resolveBlock; +var resolveLoop; +var resolveCatch; + +async function f() { + var promises = { + top: new Promise(r => { resolveTop = r; }), + block: new Promise(r => { resolveBlock = r; }), + loop: new Promise(r => { resolveLoop = r; }), + catch: new Promise(r => { resolveCatch = r; }), + }; + + var value = 42; + Promise.resolve().then(resolveTop); + await promises.top; + { + let block = "block"; + Promise.resolve().then(resolveBlock); + await promises.block; + } + for (let loop of ["loop"]) { + Promise.resolve().then(resolveLoop); + await promises.loop; + } + try { + throw "err"; + } catch (err) { + Promise.resolve().then(resolveCatch); + await promises.catch; + } + return value; +} +`); + +const waitForOnPop = frame => new Promise(r => { + assertEq(frame.onPop, undefined); + frame.onPop = () => { + frame.onPop = undefined; + r(); + }; +}); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + dbg.onEnterFrame = undefined; +}; + +(async () => { + const promise = g.f(); + + assertEq(!!frame, true); + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["arguments", "promises", "value"]) + ); + assertEq(frame.environment.getVariable("value"), 42); + + frame.environment.setVariable("value", 43); + + assertEq(frame.environment.getVariable("value"), 43); + + await waitForOnPop(frame); + + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["block"]) + ); + assertEq(frame.environment.getVariable("block"), "block"); + + await waitForOnPop(frame); + + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["loop"]) + ); + assertEq(frame.environment.getVariable("loop"), "loop"); + + await waitForOnPop(frame); + + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["err"]) + ); + assertEq(frame.environment.getVariable("err"), "err"); + + const result = await promise; + + assertEq(result, 43); + + assertThrowsInstanceOf(() => frame.environment, Error); + + throw "all-jobs-completed-successfully"; +})(); diff --git a/js/src/jit-test/tests/debug/Frame-environment-07.js b/js/src/jit-test/tests/debug/Frame-environment-07.js new file mode 100644 index 0000000000..5e51ba79af --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-07.js @@ -0,0 +1,83 @@ +// Test that Debugger.Frame.prototype.environment works on suspended generators. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +function* f() { + var value = 42; + yield; + { + let block = "block"; + yield; + } + for (let loop of ["loop"]) { + yield; + } + try { + throw "err"; + } catch (err) { + yield; + } + return value; +} +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + dbg.onEnterFrame = undefined; +}; + +const it = g.f(); + +assertEq(!!frame, true); + +let result = it.next(); + +assertEq(result.done, false); +assertEq(result.value, undefined); + +assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["arguments", "value"]) +); + +assertEq(frame.environment.getVariable("value"), 42); + +frame.environment.setVariable("value", 43); + +assertEq(frame.environment.getVariable("value"), 43); + +result = it.next(); + +assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["block"]) +); +assertEq(frame.environment.getVariable("block"), "block"); + +result = it.next(); + +assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["loop"]) +); +assertEq(frame.environment.getVariable("loop"), "loop"); + +result = it.next(); + +assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["err"]) +); +assertEq(frame.environment.getVariable("err"), "err"); + +result = it.next(); + +assertEq(result.done, true); +assertEq(result.value, 43); + +assertThrowsInstanceOf(() => frame.environment, Error); diff --git a/js/src/jit-test/tests/debug/Frame-environment-08.js b/js/src/jit-test/tests/debug/Frame-environment-08.js new file mode 100644 index 0000000000..f4d10d0547 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-08.js @@ -0,0 +1,108 @@ +// |jit-test| error:all-jobs-completed-successfully +// Test that Debugger.Frame.prototype.environment works on suspended +// async generators. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +var resolveTop; +var resolveBlock; +var resolveLoop; +var resolveCatch; + +async function* f() { + var promises = { + top: new Promise(r => { resolveTop = r; }), + block: new Promise(r => { resolveBlock = r; }), + loop: new Promise(r => { resolveLoop = r; }), + catch: new Promise(r => { resolveCatch = r; }), + }; + + var value = 42; + await promises.top; + { + let block = "block"; + await promises.block; + } + for (let loop of ["loop"]) { + await promises.loop; + } + try { + throw "err"; + } catch (err) { + await promises.catch; + } + return value; +} +`); + +const waitForOnPop = frame => new Promise(r => { + assertEq(frame.onPop, undefined); + frame.onPop = () => { + frame.onPop = undefined; + r(); + }; +}); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + dbg.onEnterFrame = undefined; +}; + +(async () => { + const it = g.f(); + + assertEq(!!frame, true); + + let promise = it.next(); + + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["arguments", "promises", "value"]) + ); + + //FIXME assertEq(frame.environment.getVariable("value"), 42); + + frame.environment.setVariable("value", 43); + + g.resolveTop(); + await waitForOnPop(frame); + + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["block"]) + ); + assertEq(frame.environment.getVariable("block"), "block"); + + g.resolveBlock(); + await waitForOnPop(frame); + + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["loop"]) + ); + assertEq(frame.environment.getVariable("loop"), "loop"); + + g.resolveLoop(); + await waitForOnPop(frame); + + assertEq( + JSON.stringify(frame.environment.names()), + JSON.stringify(["err"]) + ); + assertEq(frame.environment.getVariable("err"), "err"); + + g.resolveCatch(); + const result = await promise; + + assertEq(result.done, true); + //FIXME assertEq(result.value, 43); + + assertThrowsInstanceOf(() => frame.environment, Error); + + throw "all-jobs-completed-successfully"; +})(); diff --git a/js/src/jit-test/tests/debug/Frame-eval-01.js b/js/src/jit-test/tests/debug/Frame-eval-01.js new file mode 100644 index 0000000000..ae1a63cdb7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-01.js @@ -0,0 +1,8 @@ +// simplest possible test of Debugger.Frame.prototype.eval + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var c; +dbg.onDebuggerStatement = function (frame) { c = frame.eval("2 + 2"); }; +g.eval("debugger;"); +assertEq(c.return, 4); diff --git a/js/src/jit-test/tests/debug/Frame-eval-02.js b/js/src/jit-test/tests/debug/Frame-eval-02.js new file mode 100644 index 0000000000..79b5ff8a15 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-02.js @@ -0,0 +1,10 @@ +// frame.eval() throws if frame is not live + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var f; +dbg.onDebuggerStatement = function (frame) { f = frame; }; +g.eval("debugger;"); +assertThrowsInstanceOf(function () { f.eval("2 + 2"); }, Error); diff --git a/js/src/jit-test/tests/debug/Frame-eval-03.js b/js/src/jit-test/tests/debug/Frame-eval-03.js new file mode 100644 index 0000000000..9b640b7c13 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-03.js @@ -0,0 +1,19 @@ +// Test eval-ing names in a topmost script frame + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.eval("a").return, 2); + assertEq(frame.eval("c").return, 4); + var exc = frame.eval("d").throw; + assertEq(exc instanceof Debugger.Object, true); + assertEq(exc.proto, frame.eval("ReferenceError.prototype").return); + hits++; +}; +g.eval("function f(a, b) { var c = a + b; debugger; eval('debugger;'); }"); +g.eval("f(2, 2);"); +g.eval("var a = 2, b = 2, c = a + b; debugger;"); +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/Frame-eval-04.js b/js/src/jit-test/tests/debug/Frame-eval-04.js new file mode 100644 index 0000000000..83ec7945eb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-04.js @@ -0,0 +1,11 @@ +// frame.eval SyntaxErrors are reflected, not thrown + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var exc, SEp; +dbg.onDebuggerStatement = function (frame) { + exc = frame.eval("#$@!").throw; + SEp = frame.eval("SyntaxError.prototype").return; +}; +g.eval("debugger;"); +assertEq(exc.proto, SEp); diff --git a/js/src/jit-test/tests/debug/Frame-eval-05.js b/js/src/jit-test/tests/debug/Frame-eval-05.js new file mode 100644 index 0000000000..44fdf77701 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-05.js @@ -0,0 +1,14 @@ +// var declarations in strict frame.eval do not modify the frame + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var cv; +dbg.onDebuggerStatement = function (frame) { + cv = frame.eval("'use strict'; var a = 2; h();"); +}; +g.a = 1; +g.eval("function f(s) { function h() { return a; } eval(s); debugger; } "); +g.eval("f('0');"); +assertEq(cv.return, 1); +g.eval("f('var a = 3;');"); +assertEq(cv.return, 3); diff --git a/js/src/jit-test/tests/debug/Frame-eval-06.js b/js/src/jit-test/tests/debug/Frame-eval-06.js new file mode 100644 index 0000000000..811ea8a926 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-06.js @@ -0,0 +1,19 @@ +// frame.eval throws if frame is a generator frame that isn't currently on the stack + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.eval("function* gen(a) { debugger; yield a; }"); +g.eval("function test() { debugger; }"); +var dbg = new Debugger(g); +var genframe; +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + if (frame.callee.name == 'gen') + genframe = frame; + else + assertThrowsInstanceOf(function () { genframe.eval("a"); }, Error); + hits++; +}; +g.eval("var it = gen(42); assertEq(it.next().value, 42); test();"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-eval-07.js b/js/src/jit-test/tests/debug/Frame-eval-07.js new file mode 100644 index 0000000000..6770a40f32 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-07.js @@ -0,0 +1,31 @@ +// test frame.eval in non-top frames + +var g = newGlobal({newCompartment: true}); +var N = g.N = 12; // must be even +assertEq(N % 2, 0); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var n = frame.eval("n").return; + if (n === 0) { + for (var i = 0; i <= N; i++) { + assertEq(frame.type, 'call'); + assertEq(frame.callee.name, i % 2 === 0 ? 'even' : 'odd'); + assertEq(frame.eval("n").return, i); + frame = frame.older; + } + assertEq(frame.type, 'call'); + assertEq(frame.callee.name, undefined); + frame = frame.older; + assertEq(frame.type, 'eval'); + hits++; + } +}; + +var result = g.eval("(" + function () { + function odd(n) { return n > 0 && !even(n - 1); } + function even(n) { debugger; return n == 0 || !odd(n - 1); } + return even(N); + } + ")();"); +assertEq(result, true); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-eval-08.js b/js/src/jit-test/tests/debug/Frame-eval-08.js new file mode 100644 index 0000000000..a6a12a5375 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-08.js @@ -0,0 +1,23 @@ +// The arguments can escape from a function via a debugging hook. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// capture arguments object and test function +var args, testfn; +dbg.onDebuggerStatement = function (frame) { + args = frame.eval("arguments").return; + testfn = frame.eval("test").return; +}; +g.eval("function f() { debugger; }"); +g.eval("var test = " + function test(args) { + assertEq(args.length, 3); + assertEq(args[0], this); + assertEq(args[1], f); + assertEq(args[2].toString(), "[object Object]"); + return 42; + } + ";"); +g.eval("f(this, f, {});"); + +var cv = testfn.apply(null, [args]); +assertEq(cv.return, 42); diff --git a/js/src/jit-test/tests/debug/Frame-eval-09.js b/js/src/jit-test/tests/debug/Frame-eval-09.js new file mode 100644 index 0000000000..cb7c0b8ca7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-09.js @@ -0,0 +1,21 @@ +// assigning to local variables in frame.eval code + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.eval("outerarg = 1; outervar = 2; innerarg = 3; innervar = 4;"); +}; + +var result = g.eval("(" + function outer(outerarg) { + var outervar = 200; + function inner(innerarg) { + var innervar = 400; + eval(""); + debugger; + return innerarg + innervar; + } + var innersum = inner(300); + return outerarg + outervar + innersum; + } + ")(100)"); + +assertEq(result, 10); diff --git a/js/src/jit-test/tests/debug/Frame-eval-10.js b/js/src/jit-test/tests/debug/Frame-eval-10.js new file mode 100644 index 0000000000..7605aefd58 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-10.js @@ -0,0 +1,13 @@ +// frame.eval returns null if the eval code fails with an uncatchable error. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + if (hits++ === 0) + assertEq(frame.eval("debugger;"), null); + else + return null; +}; +assertEq(g.eval("debugger; 'ok';"), "ok"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-eval-11.js b/js/src/jit-test/tests/debug/Frame-eval-11.js new file mode 100644 index 0000000000..4e76a1a5b9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-11.js @@ -0,0 +1,15 @@ +// The arguments can escape from a function via a debugging hook. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// capture arguments object and test function +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.older.eval('arguments[0]').return, 'ponies'); + hits++; +}; +g.eval("function g() { debugger; }"); +g.eval("function f() { g(); }"); +g.eval("f('ponies')"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-eval-12.js b/js/src/jit-test/tests/debug/Frame-eval-12.js new file mode 100644 index 0000000000..498e7a07a2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-12.js @@ -0,0 +1,13 @@ +// The arguments can escape from a function via a debugging hook. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// capture arguments object and test function +dbg.onDebuggerStatement = function (frame) { + var args = frame.older.environment.parent.getVariable('arguments'); + assertEq(args.missingArguments, true); +}; +g.eval("function h() { debugger; }"); +g.eval("function f() { var x = 0; return function() { x++; h() } }"); +g.eval("f('ponies')()"); diff --git a/js/src/jit-test/tests/debug/Frame-eval-13.js b/js/src/jit-test/tests/debug/Frame-eval-13.js new file mode 100644 index 0000000000..8aebc4f1cf --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-13.js @@ -0,0 +1,13 @@ +// The debugger may add new bindings into existing scopes + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function(frame) { + assertEq(frame.eval("var x = 3; x").return, 3); + hits++; +} +var hits = 0; +g.eval("(function() { debugger; })()"); +assertEq(hits, 1); +g.eval("(function() { var x = 4; debugger; })()"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-eval-14.js b/js/src/jit-test/tests/debug/Frame-eval-14.js new file mode 100644 index 0000000000..7a0cfc2aa3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-14.js @@ -0,0 +1,26 @@ +// Test the corner case of accessing an unaliased variable of a block +// while the block is not live. + +var g = newGlobal({newCompartment: true}); +g.eval("function h() { debugger }"); +g.eval("function f() { { let x = 1, y; (function() { y = 0 })(); h() } }"); +g.eval("var surprise = null"); + +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +dbg.onDebuggerStatement = function(hFrame) { + var fFrame = hFrame.older; + assertEq(fFrame.environment.getVariable('x'), 1); + assertEq(fFrame.environment.getVariable('y'), 0); + fFrame.eval("surprise = function() { return ++x }"); + assertEq(gw.executeInGlobal("surprise()").return, 2); +} +g.f(); +assertEq(g.surprise !== null, true); + +// Either succeed or throw an error about 'x' not being live +try { + assertEq(g.surprise(), 3); +} catch (e) { + assertEq(e+'', 'Error: x is not live'); +} diff --git a/js/src/jit-test/tests/debug/Frame-eval-15.js b/js/src/jit-test/tests/debug/Frame-eval-15.js new file mode 100644 index 0000000000..3a452305cc --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-15.js @@ -0,0 +1,13 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval("function h() { debugger }"); +g.eval("function f() { h() }"); +g.blah = 42; +dbg.onDebuggerStatement = function(frame) { + frame.older.eval("var blah = 43"); + frame.older.eval("blah = 44"); + assertEq(frame.older.environment.getVariable("blah"), 44); +} +g.f(); +assertEq(g.blah, 42); diff --git a/js/src/jit-test/tests/debug/Frame-eval-16.js b/js/src/jit-test/tests/debug/Frame-eval-16.js new file mode 100644 index 0000000000..f47ccfca76 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-16.js @@ -0,0 +1,26 @@ +// eval correctly handles optional custom url option +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var count = 0; + +function testUrl (options, expected) { + count++; + dbg.onDebuggerStatement = function (frame) { + dbg.onNewScript = function (script) { + dbg.onNewScript = undefined; + assertEq(script.url, expected); + count--; + }; + frame.eval("", options); + }; + g.eval("debugger;"); +} + + +testUrl(undefined, "debugger eval code"); +testUrl(null, "debugger eval code"); +testUrl({ url: undefined }, "debugger eval code"); +testUrl({ url: null }, "null"); +testUrl({ url: 5 }, "5"); +testUrl({ url: "test" }, "test"); +assertEq(count, 0); diff --git a/js/src/jit-test/tests/debug/Frame-eval-17.js b/js/src/jit-test/tests/debug/Frame-eval-17.js new file mode 100644 index 0000000000..22fb095505 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-17.js @@ -0,0 +1,24 @@ +// eval correctly handles optional lineNumber option +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var count = 0; + +function testLineNumber (options, expected) { + count++; + dbg.onDebuggerStatement = function (frame) { + dbg.onNewScript = function (script) { + dbg.onNewScript = undefined; + assertEq(script.startLine, expected); + count--; + }; + frame.eval("", options); + }; + g.eval("debugger;"); +} + + +testLineNumber(undefined, 1); +testLineNumber({}, 1); +testLineNumber({ lineNumber: undefined }, 1); +testLineNumber({ lineNumber: 5 }, 5); +assertEq(count, 0); diff --git a/js/src/jit-test/tests/debug/Frame-eval-18.js b/js/src/jit-test/tests/debug/Frame-eval-18.js new file mode 100644 index 0000000000..3cde24eb28 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-18.js @@ -0,0 +1,12 @@ +// yield is not allowed in eval in a star generator. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +dbg.onDebuggerStatement = function (frame) { + assertThrowsInstanceOf(function() { frame.eval('yield 10;') }, SyntaxError); +}; + +g.eval("(function*g(){ debugger; })()"); diff --git a/js/src/jit-test/tests/debug/Frame-eval-19.js b/js/src/jit-test/tests/debug/Frame-eval-19.js new file mode 100644 index 0000000000..950113f8c0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-19.js @@ -0,0 +1,34 @@ +// Eval-in-frame of optimized frames to break out of an infinite loop. + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_IonEagerNoOffthreadCompilation)) + quit(0); + +withJitOptions(Opts_IonEagerNoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.eval("" + function f(d) { g(d); }); + g.eval("" + function g(d) { h(d); }); + g.eval("" + function h(d) { + var i = 0; + while (d) + interruptIf(d && i++ == 4000); + }); + + setInterruptCallback(function () { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame(); + if (frame.callee.name != "h" || frame.implementation != "ion") + return true; + frame.eval("d = false;"); + return true; + }); + + g.eval("(" + function () { + for (i = 0; i < 5; i++) + f(false); + f(true); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Frame-eval-20.js b/js/src/jit-test/tests/debug/Frame-eval-20.js new file mode 100644 index 0000000000..dde786f8ef --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-20.js @@ -0,0 +1,48 @@ +// |jit-test| --ion-osr=off + +// Eval-in-frame with different type on non-youngest Ion frame. + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(0); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + function test(shadow) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + // Note that we depend on CCW scripted functions being opaque to Ion + // optimization for this test. + g.h = function h(d) { + if (d) { + dbg.addDebuggee(g); + var f = dbg.getNewestFrame().older; + assertEq(f.implementation, "ion"); + assertEq(f.environment.getVariable("foo"), 42); + + // EIF of a different type too. + f.eval((shadow ? "var " : "") + "foo = 'string of 42'"); + g.expected = shadow ? 42 : "string of 42"; + } + } + + g.eval("" + function f(d) { + var foo = 42; + g(d); + return foo; + }); + g.eval("" + function g(d) { + h(d); + }); + + g.eval("(" + function () { + for (i = 0; i < 5; i++) + f(false); + assertEq(f(true), "string of 42"); + } + ")();"); + } + + test(false); + test(true); +}); diff --git a/js/src/jit-test/tests/debug/Frame-eval-21.js b/js/src/jit-test/tests/debug/Frame-eval-21.js new file mode 100644 index 0000000000..a1fdc8b810 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-21.js @@ -0,0 +1,33 @@ +// Eval-in-frame with different type on baseline frame with let-scoping + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_BaselineEager)) + quit(0); + +withJitOptions(Opts_BaselineEager, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.h = function h(d) { + if (d) { + dbg.addDebuggee(g); + var f = dbg.getNewestFrame().older; + assertEq(f.implementation, "baseline"); + assertEq(f.environment.getVariable("foo"), 42); + f.eval("foo = 'string of 42'"); + } + } + + g.eval("" + function f(d) { + if (d) { + let foo = 42; + g(d); + return foo; + } + }); + + g.eval("" + function g(d) { h(d); }); + + g.eval("(" + function () { assertEq(f(true), "string of 42"); } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Frame-eval-22.js b/js/src/jit-test/tests/debug/Frame-eval-22.js new file mode 100644 index 0000000000..2a9f081feb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-22.js @@ -0,0 +1,32 @@ +// Debugger.Frame preserves Ion frame identity + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg1 = new Debugger; + var dbg2 = new Debugger; + + g.toggle = function toggle(x, d) { + if (d) { + dbg1.addDebuggee(g); + dbg2.addDebuggee(g); + var frame1 = dbg1.getNewestFrame(); + assertEq(frame1.environment.getVariable("x"), x); + assertEq(frame1.implementation, "ion"); + frame1.environment.setVariable("x", "not 42"); + assertEq(dbg2.getNewestFrame().environment.getVariable("x"), "not 42"); + } + }; + + g.eval("" + function f(x, d) { toggle(x, d); }); + + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(42, false); + f(42, true); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Frame-eval-23.js b/js/src/jit-test/tests/debug/Frame-eval-23.js new file mode 100644 index 0000000000..ae30791809 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-23.js @@ -0,0 +1,37 @@ +// Debugger.Frame preserves Ion frame mutations after removing debuggee. + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.toggle = function toggle(x, d) { + if (d) { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame().older; + assertEq(frame.callee.name, "f"); + assertEq(frame.environment.getVariable("x"), x); + assertEq(frame.implementation, "ion"); + frame.environment.setVariable("x", "not 42"); + dbg.removeDebuggee(g); + } + }; + + g.eval("" + function f(x, d) { + g(x, d); + if (d) + assertEq(x, "not 42"); + }); + + g.eval("" + function g(x, d) { toggle(x, d); }); + + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(42, false); + f(42, true); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Frame-eval-24.js b/js/src/jit-test/tests/debug/Frame-eval-24.js new file mode 100644 index 0000000000..e3a9096d10 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-24.js @@ -0,0 +1,25 @@ +// Make sure the getVariable/setVariable/eval functions work correctly with +// unaliased locals. +var g = newGlobal({newCompartment: true}); +g.eval(` +function g() { debugger; }; +function f(arg) { + var y = arg - 3; + var a1 = 1; + var a2 = 1; + var b = arg + 9; + var z = function() { return a1 + a2; }; + g(); + return y * b; // To prevent the JIT from optimizing out y and b. +};`); + +var dbg = new Debugger(g); + +dbg.onDebuggerStatement = function handleDebugger(frame) { + assertEq(frame.older.eval("y + b").return, 26); + assertEq(frame.older.environment.getVariable("y"), 7); + frame.older.environment.setVariable("b", 4); + assertEq(frame.older.eval("y + b").return, 11); +}; + +g.f(10); diff --git a/js/src/jit-test/tests/debug/Frame-eval-25.js b/js/src/jit-test/tests/debug/Frame-eval-25.js new file mode 100644 index 0000000000..cc91b28587 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-25.js @@ -0,0 +1,25 @@ +// Make sure we can recover missing arguments even when it gets assigned to +// another slot. + +load(libdir + "asserts.js"); +load(libdir + "evalInFrame.js"); + +function h() { + evalInFrame(1, "a.push(0)"); +} + +function f() { + var a = arguments; + h(); +} + +assertThrowsInstanceOf(f, TypeError); + +function g() { + { + let a = arguments; + h(); + } +} + +assertThrowsInstanceOf(g, TypeError); diff --git a/js/src/jit-test/tests/debug/Frame-eval-26.js b/js/src/jit-test/tests/debug/Frame-eval-26.js new file mode 100644 index 0000000000..5a15c305e8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-26.js @@ -0,0 +1,16 @@ +// Bug 1026477: Defining functions with D.F.p.eval works, even if there's +// already a non-aliased var binding for the identifier. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.older.eval('function f() { }'); +}; + +// When the compiler sees the 'debugger' statement, it marks all variables as +// aliased, but we want to test the case where f is in a stack frame slot, so we +// put the 'debugger' statement in a separate function, and use frame.older to +// get back to the anonymous function's frame. +g.eval('function q() { debugger; }'); +assertEq(typeof g.eval('(function () { var f = 42; q(); return f; })();'), + "function"); diff --git a/js/src/jit-test/tests/debug/Frame-eval-27.js b/js/src/jit-test/tests/debug/Frame-eval-27.js new file mode 100644 index 0000000000..38553c1967 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-27.js @@ -0,0 +1,13 @@ +// Bug 1026477: Defining functions with D.F.p.eval works, even if there's +// already a var binding for the identifier. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.eval('function f() { }'); +}; + +// When the compiler sees the 'debugger' statement, it marks all variables as +// aliased, so f will live in a Call object. +assertEq(typeof g.eval('(function () { var f = 42; debugger; return f;})();'), + "function"); diff --git a/js/src/jit-test/tests/debug/Frame-eval-28.js b/js/src/jit-test/tests/debug/Frame-eval-28.js new file mode 100644 index 0000000000..ec11acb53f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-28.js @@ -0,0 +1,12 @@ +// Test that strict Debugger.Frame.eval has a correct static scope. +options('strict_mode'); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onEnterFrame = function(f) { + hits++; + if (hits > 2) + return; + f.eval("42"); +}; +g.eval("42"); diff --git a/js/src/jit-test/tests/debug/Frame-eval-29.js b/js/src/jit-test/tests/debug/Frame-eval-29.js new file mode 100644 index 0000000000..73a3e735e0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-29.js @@ -0,0 +1,59 @@ +// Test reading and setting values on "hollow" debug scopes. In testGet and +// testSet below, f and g *must* be called from a non-heavyweight lambda to +// trigger the creation of the "hollow" debug scopes for the missing scopes. +// +// The reason is that a direct call to f or g that accesses a in testGet or +// testSet's frame is actually recoverable. The Debugger can synthesize a scope +// based on the frame. By contorting through a lambda, it becomes unsound to +// synthesize a scope based on the lambda function's frame. Since f and g are +// accessing a, which is itself free inside the lambda, the Debugger has no way +// to tell if the on-stack testGet or testSet frame is the frame that *would +// have* allocated a scope for the lambda, *had the lambda been heavyweight*. +// +// More concretely, if the inner lambda were returned from testGet and testSet, +// then called from a different invocation of testGet or testSet, it becomes +// obvious that it is incorrect to synthesize a scope based on the frame of +// that different invocation. + +load(libdir + "evalInFrame.js"); + +function f() { + // Eval one frame up. Nothing aliases a. + evalInFrame(1, "print(a)"); +} + +function g() { + evalInFrame(1, "a = 43"); +} + +function testGet() { + { + let a = 42; + (function () { f(); })(); + } +} + +function testSet() { + { + let a = 42; + (function () { g(); })(); + } +} + +var log = ""; + +try { + testGet(); +} catch (e) { + // Throws due to a having been optimized out. + log += "g"; +} + +try { + testSet(); +} catch (e) { + // Throws due to a having been optimized out. + log += "s"; +} + +assertEq(log, "gs"); diff --git a/js/src/jit-test/tests/debug/Frame-eval-30.js b/js/src/jit-test/tests/debug/Frame-eval-30.js new file mode 100644 index 0000000000..f99912c452 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-30.js @@ -0,0 +1,19 @@ +// Test that Debugger.Frame.eval correctly throws on redeclaration. + +load(libdir + "evalInFrame.js"); + +let x; + +function f() { + evalInFrame(1, "var x;"); +} + +var log = ""; + +try { + f(); +} catch (e) { + log += "e"; +} + +assertEq(log, "e"); diff --git a/js/src/jit-test/tests/debug/Frame-eval-31.js b/js/src/jit-test/tests/debug/Frame-eval-31.js new file mode 100644 index 0000000000..bef94a046a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-31.js @@ -0,0 +1,9 @@ +// evalInFrame with non-syntactic scopes. + +load(libdir + "asserts.js"); +load(libdir + "evalInFrame.js"); + +evalReturningScope(` + var x = 42; + assertEq(evalInFrame(0, "x"), 42); +`); diff --git a/js/src/jit-test/tests/debug/Frame-eval-32.js b/js/src/jit-test/tests/debug/Frame-eval-32.js new file mode 100644 index 0000000000..9a5309d6aa --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-32.js @@ -0,0 +1,8 @@ +// |jit-test| error: ReferenceError + +// Test the TDZ works for glbao lexicals through Debugger environments in +// compound assignments. +load(libdir + "evalInFrame.js"); + +evalInFrame(0, "x |= 0"); +let x; diff --git a/js/src/jit-test/tests/debug/Frame-eval-33.js b/js/src/jit-test/tests/debug/Frame-eval-33.js new file mode 100644 index 0000000000..d5d3f94804 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-33.js @@ -0,0 +1,39 @@ +load(libdir + "evalInFrame.js"); + +// Test that computing the implicit 'this' in calls for D.F.eval is as if it +// were a pasted-in eval. + +var G = this; + +function globalFun(check, expectedThis) { + if (check) + assertEq(this, expectedThis); + return this; +} +var expectedGlobalFunThis = globalFun(false); +evalInFrame(0, "globalFun(true, expectedGlobalFunThis)"); + +(function testInnerFun() { + function innerFun(check, expectedThis) { + if (check) + assertEq(this, expectedThis); + return this; + } + var expectedInnerFunThis = innerFun(false); + evalInFrame(0, "innerFun(true, expectedInnerFunThis)"); + return [innerFun, expectedInnerFunThis]; // To prevent the JIT from optimizing out vars. +})(); + +(function testWith() { + var o = { + withFun: function withFun(check, expectedThis) { + if (check) + assertEq(this, expectedThis); + return this; + } + }; + with (o) { + var expectedWithFunThis = withFun(false); + evalInFrame(0, "withFun(true, expectedWithFunThis)"); + } +})(); diff --git a/js/src/jit-test/tests/debug/Frame-eval-stack.js b/js/src/jit-test/tests/debug/Frame-eval-stack.js new file mode 100644 index 0000000000..71f7c74789 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-eval-stack.js @@ -0,0 +1,19 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval("function h() { debugger; }"); +g.eval("function g() { h() }"); +g.eval("function f() { var blah = 333; g() }"); + +dbg.onDebuggerStatement = function(frame) { + frame = frame.older; + g.trace = frame.older.eval("(new Error()).stack;").return; +} +g.f(); + +assertEq(typeof g.trace, "string"); + +var frames = g.trace.split("\n"); +assertEq(frames[0].includes("eval code"), true); +assertEq(frames[1].startsWith("f@"), true); +assertEq(frames[2].startsWith("@"), true); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js new file mode 100644 index 0000000000..792d4b1867 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js @@ -0,0 +1,35 @@ +// evalWithBindings basics + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.evalWithBindings("x", {x: 2}).return, 2); + assertEq(frame.evalWithBindings("x + y", {x: 2}).return, 5); + hits++; +}; + +// in global code +g.y = 3; +g.eval("debugger;"); + +// in function code +g.y = "fail"; +g.eval("function f(y) { debugger; }"); +g.f(3); + +// in direct eval code +g.eval("function f() { var y = 3; eval('debugger;'); }"); +g.f(); + +// in strict eval code with var +g.eval("function f() { 'use strict'; eval('var y = 3; debugger;'); }"); +g.f(); + +// in a with block +g.eval("with ({y: 3}) { debugger; }"); + +// shadowing +g.eval("{ let x = 50, y = 3; debugger; }"); + +assertEq(hits, 6); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js new file mode 100644 index 0000000000..debb901b4d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js @@ -0,0 +1,21 @@ +// evalWithBindings to call a method of a debuggee value + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var global = dbg.addDebuggee(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var obj = frame.arguments[0]; + var expected = frame.arguments[1]; + assertEq(frame.evalWithBindings("obj.toString()", {obj: obj}).return, expected); + hits++; +}; + +g.eval("function f(obj, expected) { debugger; }"); + +g.eval("f(new Number(-0), '0');"); +g.eval("f(new String('ok'), 'ok');"); +g.eval("f(Symbol('still ok'), 'Symbol(still ok)');"); +g.eval("f(Object(Symbol('still ok')), 'Symbol(still ok)');"); +g.eval("f({toString: function () { return f; }}, f);"); +assertEq(hits, 5); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js new file mode 100644 index 0000000000..91ebbf5a13 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js @@ -0,0 +1,16 @@ +// arguments works in evalWithBindings (it does not interpose a function scope) +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var global = dbg.addDebuggee(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var argc = frame.arguments.length; + assertEq(argc, 8); + assertEq(frame.evalWithBindings("arguments[prop]", {prop: "length"}).return, argc); + for (var i = 0; i < argc; i++) + assertEq(frame.evalWithBindings("arguments[i]", {i: i}).return, frame.arguments[i]); + hits++; +}; +g.eval("function f() { debugger; }"); +g.eval("f(undefined, -0, NaN, '\uffff', Symbol('alpha'), Array.prototype, Math, f);"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js new file mode 100644 index 0000000000..0de428c563 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js @@ -0,0 +1,17 @@ +// evalWithBindings works on non-top frames. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var f1; +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.older.evalWithBindings("q + r", {r: 3}).return, 5); + + // frame.older.older is in the same function as frame, but a different activation of it + assertEq(frame.older.older.evalWithBindings("q + r", {r: 3}).return, 6); + hits++; +}; + +g.eval("function f1(q) { if (q == 1) debugger; else f2(2); }"); +g.eval("function f2(arg) { var q = arg; f1(1); }"); +g.f1(3); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js new file mode 100644 index 0000000000..7f9466fc33 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js @@ -0,0 +1,12 @@ +// evalWithBindings code can assign to the bindings. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.evalWithBindings("for (i = 0; i < 5; i++) {} i;", {i: 10}).return, 5); + hits++; +}; + +g.eval("debugger;"); +assertEq("i" in g, false); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js new file mode 100644 index 0000000000..42f1a42b5e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js @@ -0,0 +1,9 @@ +// In evalWithBindings code, assignment to any name not in the bindings works just as in eval. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.evalWithBindings("y = z; x = w;", {z: 2, w: 3}).return, 3); +}; +g.eval("function f(x) { debugger; assertEq(x, 3); }"); +g.eval("var y = 0; f(0);"); +assertEq(g.y, 2); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js new file mode 100644 index 0000000000..a98b7650cb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js @@ -0,0 +1,16 @@ +// var statements in strict evalWithBindings code behave like strict eval. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.evalWithBindings("var i = a*a + b*b; i === 25;", {a: 3, b: 4}).return, true); + hits++; +}; +g.eval("'use strict'; debugger;"); +assertEq(hits, 1); +assertEq("i" in g, false); + +g.eval("function die() { throw fit; }"); +g.eval("Object.defineProperty(this, 'i', {get: die, set: die});"); +g.eval("'use strict'; debugger;"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js new file mode 100644 index 0000000000..3785d5a2a1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js @@ -0,0 +1,13 @@ +// evalWithBindings ignores non-enumerable and non-own properties. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.evalWithBindings("toString + constructor + length", []).return, 112233); + var obj = Object.create({constructor: "FAIL"}, {length: {value: "fail"}}); + assertEq(frame.evalWithBindings("toString + constructor + length", obj).return, 112233); + hits++; +}; +g.eval("function f() { var toString = 111111, constructor = 1111, length = 11; debugger; }"); +g.f(); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js new file mode 100644 index 0000000000..c697903193 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js @@ -0,0 +1,27 @@ +// evalWithBindings code is debuggee code, so it can trip the debugger. It nests! +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var f1; +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + f1 = frame; + + // This trips the onExceptionUnwind hook. + var x = frame.evalWithBindings("wrongSpeling", {rightSpelling: 2}).throw; + + assertEq(frame.evalWithBindings("exc.name", {exc: x}).return, "ReferenceError"); + hits++; +}; +dbg.onExceptionUnwind = function (frame, exc) { + assertEq(frame !== f1, true); + + // f1's environment does not contain the binding for the first evalWithBindings call. + assertEq(f1.eval("rightSpelling").return, "dependent"); + assertEq(f1.evalWithBindings("n + rightSpelling", {n: "in"}).return, "independent"); + + // frame's environment does contain the binding. + assertEq(frame.eval("rightSpelling").return, 2); + assertEq(frame.evalWithBindings("rightSpelling + three", {three: 3}).return, 5); + hits++; +}; +g.eval("(function () { var rightSpelling = 'dependent'; debugger; })();"); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js new file mode 100644 index 0000000000..005b5bf48b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js @@ -0,0 +1,16 @@ +// Direct eval code under evalWithBindings sees both the bindings and the enclosing scope. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var code = + "assertEq(a, 1234);\n" + + "assertEq(b, null);\n" + + "assertEq(c, 'ok');\n"; + assertEq(frame.evalWithBindings("eval(s)", {s: code, a: 1234}).return, undefined); + hits++; +}; +g.eval("function f(b) { var c = 'ok'; debugger; }"); +g.f(null); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-11.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-11.js new file mode 100644 index 0000000000..4d17304053 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-11.js @@ -0,0 +1,18 @@ +// var statements in non-strict evalWithBindings code behave like non-strict direct eval. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.evalWithBindings("var i = v; 42;", { v: 'inner' }).return, 42); +}; + +g.i = 'outer'; +log = ''; +assertEq(g.eval('debugger; i;'), 'inner'); +assertEq(log, 'd'); + +g.j = 'outer'; +log = ''; +assertEq(g.eval('debugger; j;'), 'outer'); +assertEq(log, 'd'); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-12.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-12.js new file mode 100644 index 0000000000..23e42ec0fe --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-12.js @@ -0,0 +1,26 @@ +// evalWithBindings correctly handles optional custom url option +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var count = 0; + +function testUrl (options, expected) { + count++; + dbg.onDebuggerStatement = function (frame) { + dbg.onNewScript = function (script) { + dbg.onNewScript = undefined; + assertEq(script.url, expected); + count--; + }; + frame.evalWithBindings("", {}, options); + }; + g.eval("debugger;"); +} + + +testUrl(undefined, "debugger eval code"); +testUrl(null, "debugger eval code"); +testUrl({ url: undefined }, "debugger eval code"); +testUrl({ url: null }, "null"); +testUrl({ url: 5 }, "5"); +testUrl({ url: "test" }, "test"); +assertEq(count, 0); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-13.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-13.js new file mode 100644 index 0000000000..37cf30026f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-13.js @@ -0,0 +1,24 @@ +// evalWithBindings correctly handles optional lineNumber option +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var count = 0; + +function testLineNumber (options, expected) { + count++; + dbg.onDebuggerStatement = function (frame) { + dbg.onNewScript = function (script) { + dbg.onNewScript = undefined; + assertEq(script.startLine, expected); + count--; + }; + frame.evalWithBindings("", {}, options); + }; + g.eval("debugger;"); +} + + +testLineNumber(undefined, 1); +testLineNumber({}, 1); +testLineNumber({ lineNumber: undefined }, 1); +testLineNumber({ lineNumber: 5 }, 5); +assertEq(count, 0); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-14.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-14.js new file mode 100644 index 0000000000..2875cd63b0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-14.js @@ -0,0 +1,20 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +// We're going to need to eval with a thrown exception from inside +// onExceptionUnwind, so guard against infinite recursion. +var exceptionCount = 0; +dbg.onDebuggerStatement = function (frame) { + var x = frame.evalWithBindings("throw 'haha'", { rightSpelling: 4 }).throw; + assertEq(x, "haha"); +}; +dbg.onExceptionUnwind = function (frame, exc) { + ++exceptionCount; + if (exceptionCount == 1) { + var y = frame.evalWithBindings("throw 'haha2'", { rightSpelling: 2 }).throw; + assertEq(y, "haha2"); + } else { + assertEq(frame.evalWithBindings("rightSpelling + three", { three : 3 }).return, 5); + } +}; +g.eval("(function () { var rightSpelling = 7; debugger; })();"); +assertEq(exceptionCount, 2); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-15.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-15.js new file mode 100644 index 0000000000..98aba48d14 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-15.js @@ -0,0 +1,15 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +dbg.onDebuggerStatement = function (frame) { + // The bindings object is unused but adds another environment on the + // environment chain. Make sure 'this' computes the right value in light of + // this. + assertEq(frame.evalWithBindings(`this === foo;`, { bar: 42 }).return, true); + assertEq(frame.evalWithBindings(`eval('this') === foo;`, { bar: 42 }).return, true); +}; + +g.eval(` +var foo = { bar: function() { debugger; } }; +foo.bar(); +`); diff --git a/js/src/jit-test/tests/debug/Frame-identity-01.js b/js/src/jit-test/tests/debug/Frame-identity-01.js new file mode 100644 index 0000000000..de16e680be --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-identity-01.js @@ -0,0 +1,19 @@ +// Check that {return:} resumption kills the current stack frame. + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function () { + var dbg = new Debugger(debuggeeGlobal); + var prev = null; + dbg.onDebuggerStatement = function (frame) { + assertEq(frame === prev, false); + if (prev) + assertEq(prev.onStack, false); + prev = frame; + return {return: frame.arguments[0]}; + }; + } + ")();"); + +function f(i) { debugger; } +for (var i = 0; i < 10; i++) + assertEq(f(i), i); diff --git a/js/src/jit-test/tests/debug/Frame-identity-02.js b/js/src/jit-test/tests/debug/Frame-identity-02.js new file mode 100644 index 0000000000..d3f1b94b9c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-identity-02.js @@ -0,0 +1,21 @@ +// Check that {throw:} resumption kills the current stack frame. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function () { + var dbg = new Debugger(debuggeeGlobal); + var prev = null; + dbg.onDebuggerStatement = function (frame) { + assertEq(frame === prev, false); + if (prev) + assertEq(prev.onStack, false); + prev = frame; + return {throw: debuggeeGlobal.i}; + }; + } + ")();"); + +function f() { debugger; } +for (var i = 0; i < 10; i++) + assertThrowsValue(f, i); diff --git a/js/src/jit-test/tests/debug/Frame-identity-03.js b/js/src/jit-test/tests/debug/Frame-identity-03.js new file mode 100644 index 0000000000..fad88e01c4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-identity-03.js @@ -0,0 +1,49 @@ +// Test that we create new Debugger.Frames and reuse old ones correctly with recursion. + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function () { + function id(f) { + return ("id" in f) ? f.id : (f.id = nextid++); + } + + var dbg = new Debugger(debuggeeGlobal); + dbg.onDebuggerStatement = function (frame) { + var a = []; + for (; frame; frame = frame.older) + a.push(frame); + var s = ''; + while (a.length) + s += id(a.pop()); + results.push(s); + }; + } + ")();"); + +function cons(a, b) { + debugger; + return [a, b]; +} + +function tree(n) { + if (n < 2) + return n; + return cons(tree(n - 1), tree(n - 2)); +} + +g.eval("results = []; nextid = 0;"); +debugger; +assertEq(g.results.join(","), "0"); +assertEq(g.nextid, 1); + +g.eval("results = [];"); +tree(2); +assertEq(g.results.join(","), "012"); // 0=global, 1=tree, 2=cons + +g.eval("results = []; nextid = 1;"); +tree(3); +assertEq(g.results.join(","), "0123,014"); //0=global, 1=tree(3), 2=tree(2), 3=cons, 4=cons + +g.eval("results = []; nextid = 1;"); +tree(4); +// 0=global, 1=tree(4), 2=tree(3), 3=tree(2), 4=cons, tree(1), 5=cons, 6=tree(2), 7=cons, 8=cons +assertEq(g.results.join(","), "01234,0125,0167,018"); diff --git a/js/src/jit-test/tests/debug/Frame-identity-04.js b/js/src/jit-test/tests/debug/Frame-identity-04.js new file mode 100644 index 0000000000..79d0a9f24c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-identity-04.js @@ -0,0 +1,20 @@ +// Test that on-stack Debugger.Frames are not GC'd even if they are only reachable +// from the js::Debugger::frames table. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(n) { if (n) f(n - 1); debugger; }"); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + if (hits === 0) { + for (; frame; frame = frame.older) + frame.seen = true; + } else { + for (; frame; frame = frame.older) + assertEq(frame.seen, true); + } + gc(); + hits++; +}; +g.f(20); +assertEq(hits, 21); diff --git a/js/src/jit-test/tests/debug/Frame-identity-05.js b/js/src/jit-test/tests/debug/Frame-identity-05.js new file mode 100644 index 0000000000..ee92cf9c41 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-identity-05.js @@ -0,0 +1,20 @@ +// Suspended generators keep their associated Debugger.Frames gc-alive. + +var g = newGlobal({newCompartment: true}); +g.eval("function* f() { debugger; yield 1; debugger; }"); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + if (hits === 0) + frame.seen = true; + else + assertEq(frame.seen, true); + gc(); + hits++; +}; +var it = g.f(); +gc(); +assertEq(it.next().value, 1); +gc(); +assertEq(it.next().done, true); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-identity-06.js b/js/src/jit-test/tests/debug/Frame-identity-06.js new file mode 100644 index 0000000000..04eb16b386 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-identity-06.js @@ -0,0 +1,45 @@ +// Debugger.Frames for async functions are not GC'd while they're suspended. +// The awaited promise keeps the generator alive, via its reaction lists. + +var g = newGlobal({newCompartment: true}); +g.eval(` + // Create a few promises. + var promises = [], resolvers = []; + for (let i = 0; i < 3; i++) + promises.push(new Promise(r => { resolvers.push(r); })); + + async function f() { + debugger; + for (let p of promises) { + await p; + debugger; + } + } +`); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + if (hits === 0) + frame.seen = true; + else + assertEq(frame.seen, true); + hits++; +}; + +let done = false; +g.f().then(_ => { done = true; }); +gc(); +drainJobQueue(); +gc(); + +// Resolve the promises one by one. +for (let [i, resolve] of g.resolvers.entries()) { + assertEq(hits, 1 + i); + assertEq(done, false); + resolve("x"); + gc(); + drainJobQueue(); + gc(); +} +assertEq(hits, 1 + g.resolvers.length); +assertEq(done, true); diff --git a/js/src/jit-test/tests/debug/Frame-identity-07.js b/js/src/jit-test/tests/debug/Frame-identity-07.js new file mode 100644 index 0000000000..fa7a835992 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-identity-07.js @@ -0,0 +1,52 @@ +// Distinct generator calls result in distinct Debugger.Frames. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* count(n) { + if (n > 0) { + for (let x of count(n - 1)) + yield x; + yield n; + } + } +`); + +let log = ""; +let dbg = Debugger(g); +let nextId = 0; +function mark(frame) { + if (frame.id === undefined) + frame.id = nextId++; +} +dbg.onEnterFrame = frame => { + mark(frame); + log += frame.id + "["; + frame.onPop = completion => { + mark(frame); + log += "]" + frame.id; + }; +}; + + +let j = 0; +for (let k of g.count(5)) { + assertEq(k, ++j); + log += " "; +} + +assertEq(log, + // Calling a generator function just returns a generator object + // without running the body at all; hence "0[]0". However, this call + // does evaluate default argument values, if any, so we do report an + // onEnterFrame / onPop for it. + "0[]0" + + // Demanding the first value from the top generator forces + // SpiderMonkey to create all five generator objects (the empty "n[]n" + // pairs) and then demand a value from them (the longer "n[...]n" + // substrings). + "0[1[]11[2[]22[3[]33[4[]44[5[]55[]5]4]3]2]1]0 " + + "0[1[2[3[4[]4]3]2]1]0 " + + "0[1[2[3[]3]2]1]0 " + + "0[1[2[]2]1]0 " + + "0[1[]1]0 " + + "0[]0"); diff --git a/js/src/jit-test/tests/debug/Frame-implementation-01.js b/js/src/jit-test/tests/debug/Frame-implementation-01.js new file mode 100644 index 0000000000..5eaeea97fb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-implementation-01.js @@ -0,0 +1,45 @@ +// Debugger.Frames of all implementations. + +load(libdir + "jitopts.js"); + +function testFrameImpl(jitopts, assertFrameImpl) { + if (!jitTogglesMatch(jitopts)) + return; + + withJitOptions(jitopts, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.toggle = function toggle(d) { + if (d) { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame(); + // We only care about the f and g frames. + for (var i = 0; i < 2; i++) { + assertFrameImpl(frame); + frame = frame.older; + } + } + }; + + g.eval("" + function f(d) { g(d); }); + g.eval("" + function g(d) { toggle(d); }); + + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(false); + f(true); + } + ")();"); + }); +} + +[[Opts_BaselineEager, + function (f) { assertEq(f.implementation, "baseline"); }], + // Note that the Ion case *depends* on CCW scripted functions being opaque to + // Ion optimization and not deoptimizing the frames below the call to toggle. + [Opts_Ion2NoOffthreadCompilation, + function (f) { assertEq(f.implementation, "ion"); }], + [Opts_NoJits, + function (f) { assertEq(f.implementation, "interpreter"); }]].forEach(function ([opts, fn]) { + testFrameImpl(opts, fn); + }); diff --git a/js/src/jit-test/tests/debug/Frame-implementation-02.js b/js/src/jit-test/tests/debug/Frame-implementation-02.js new file mode 100644 index 0000000000..ebcd65835f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-implementation-02.js @@ -0,0 +1,51 @@ +// Test that Ion frames are invalidated by turning on Debugger. Invalidation +// is unobservable, but we know that Ion scripts cannot handle Debugger +// handlers, so we test for the handlers being called. + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + var onPopExecuted = false; + var breakpointHit = false; + + g.toggle = function toggle(d) { + if (d) { + dbg.addDebuggee(g); + + var frame1 = dbg.getNewestFrame(); + assertEq(frame1.implementation, "ion"); + frame1.onPop = function () { + onPopExecuted = true; + }; + + var frame2 = frame1.older; + assertEq(frame2.implementation, "ion"); + // Offset of |print(42 + 42)| + var offset = frame2.script.getLineOffsets(3)[0]; + frame2.script.setBreakpoint(offset, { hit: function (fr) { + assertEq(fr.implementation != "ion", true); + breakpointHit = true; + }}); + } + }; + + g.eval("" + function f(d) { + g(d); + print(42 + 42); + }); + g.eval("" + function g(d) { toggle(d); }); + + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(false); + f(true); + } + ")();"); + + assertEq(onPopExecuted, true); + assertEq(breakpointHit, true); +}); diff --git a/js/src/jit-test/tests/debug/Frame-newTargetEval-01.js b/js/src/jit-test/tests/debug/Frame-newTargetEval-01.js new file mode 100644 index 0000000000..69978dcb8b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-newTargetEval-01.js @@ -0,0 +1,40 @@ +// Test that new.target is acceptably usable in RematerializedFrames. + +gczeal(0); + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.toggle = function toggle(d, expected) { + if (d) { + dbg.addDebuggee(g); + + var frame = dbg.getNewestFrame(); + assertEq(frame.implementation, "ion"); + assertEq(frame.constructing, true); + + // CONGRATS IF THIS FAILS! You, proud saviour, have made new.target parse + // in debug frame evals (presumably by hooking up static scope walks). + // Uncomment the assert below for efaust's undying gratitude. + // Note that we use .name here because of CCW nonsense. + assertEq(frame.eval('new.target').throw.unsafeDereference().name, "SyntaxError"); + // assertEq(frame.eval('new.target').value.unsafeDereference(), expected); + } + }; + + g.eval("" + function f(d) { new g(d, g, 15); }); + + g.eval("" + function g(d, expected) { toggle(d, expected); }); + + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(false); + f(true); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Frame-newTargetEval-02.js b/js/src/jit-test/tests/debug/Frame-newTargetEval-02.js new file mode 100644 index 0000000000..89a20d4e39 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-newTargetEval-02.js @@ -0,0 +1,43 @@ +// Test that new.target is acceptably usable in RematerializedFrames. + +gczeal(0); + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.toggle = function toggle(d, expected) { + if (d) { + dbg.addDebuggee(g); + + var frame = dbg.getNewestFrame(); + assertEq(frame.implementation, "ion"); + + // the arrow function will not be constructing, even though it has a + // new.target value. + assertEq(frame.constructing, false); + + // CONGRATS IF THIS FAILS! You, proud saviour, have made new.target parse + // in debug frame evals (presumably by hooking up static scope walks). + // Uncomment the assert below for efaust's undying gratitude. + // Note that we use .name here because of CCW nonsense. + assertEq(frame.eval('new.target').throw.unsafeDereference().name, "SyntaxError"); + // assertEq(frame.eval('new.target').return.unsafeDereference(), expected); + } + }; + + g.eval("" + function f(d) { new g(d, g, 15); }); + + g.eval("" + function g(d, expected) { (() => toggle(d, expected))(); }); + + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(false); + f(true); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Frame-newTargetOverflow-01.js b/js/src/jit-test/tests/debug/Frame-newTargetOverflow-01.js new file mode 100644 index 0000000000..2e2b48f10c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-newTargetOverflow-01.js @@ -0,0 +1,41 @@ +// Test that Ion frames are invalidated by turning on Debugger. Invalidation +// is unobservable, but we know that Ion scripts cannot handle Debugger +// handlers, so we test for the handlers being called. + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(); + +// GCs can invalidate JIT code and cause this test to fail. +if ('gczeal' in this) + gczeal(0); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + g.toggle = function toggle(d) { + if (d) { + dbg.addDebuggee(g); + + var frame = dbg.getNewestFrame(); + assertEq(frame.implementation, "ion"); + assertEq(frame.constructing, true); + + // overflow args are read from the parent's frame + // ensure we have the right offset to read from those. + assertEq(frame.arguments[1], 15); + } + }; + + g.eval("" + function f(d) { new g(d, 15); }); + + g.eval("" + function g(d) { toggle(d); }); + + g.eval("(" + function test() { + for (var i = 0; i < 5; i++) + f(false); + f(true); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/Frame-offset-01.js b/js/src/jit-test/tests/debug/Frame-offset-01.js new file mode 100644 index 0000000000..69f7f3ae5a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-offset-01.js @@ -0,0 +1,11 @@ +// frame.offset throws if !frame.onStack. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var f; +dbg.onDebuggerStatement = function (frame) { f = frame; }; +g.eval("debugger;"); +assertEq(f.onStack, false); +assertThrowsInstanceOf(function () { f.offset; }, Error); diff --git a/js/src/jit-test/tests/debug/Frame-offset-02.js b/js/src/jit-test/tests/debug/Frame-offset-02.js new file mode 100644 index 0000000000..d11945cc5c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-offset-02.js @@ -0,0 +1,16 @@ +// frame.offset gives different values at different points in a script. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var s = undefined, a = [] +dbg.onDebuggerStatement = function (frame) { + if (s === undefined) + s = frame.script; + else + assertEq(s, frame.script); + assertEq(frame.offset !== undefined, true); + assertEq(a.indexOf(frame.offset), -1); + a.push(frame.offset); +}; +g.eval("debugger; debugger; debugger;"); +assertEq(a.length, 3); diff --git a/js/src/jit-test/tests/debug/Frame-offset-03.js b/js/src/jit-test/tests/debug/Frame-offset-03.js new file mode 100644 index 0000000000..76384541f0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-offset-03.js @@ -0,0 +1,71 @@ +// frame.offset works in generators. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +function* f() { + yield 11; + yield 21; + yield 31; +} +`); + +const ranges = []; +let script; +let frame; +dbg.onEnterFrame = function(f) { + if (frame) { + assertEq(f, frame); + assertEq(f.script, script); + } else { + frame = f; + script = f.script; + assertEq(frame instanceof Debugger.Frame, true); + assertEq(script instanceof Debugger.Script, true); + } + + const range = [script.getOffsetMetadata(frame.offset).lineNumber, null]; + ranges.push(range); + f.onPop = () => { + range[1] = script.getOffsetMetadata(frame.offset).lineNumber; + }; +}; + +const it = g.f(); + +assertEq(ranges.length, 1); +assertEq(ranges[0][0], 2); +assertEq(ranges[0][1], 2); +assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 2); + +it.next(); + +assertEq(ranges.length, 2); +assertEq(ranges[1][0], 2); +assertEq(ranges[1][1], 3); +assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 3); + +it.next(); + +assertEq(ranges.length, 3); +assertEq(ranges[2][0], 3); +assertEq(ranges[2][1], 4); +assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 4); + +it.next(); + +assertEq(ranges.length, 4); +assertEq(ranges[3][0], 4); +assertEq(ranges[3][1], 5); +assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 5); + +it.next(); + +assertEq(ranges.length, 5); +assertEq(ranges[4][0], 5); +assertEq(ranges[4][1], 5); + +// The frame has finished evaluating, so the callee is no longer accessible. +assertThrowsInstanceOf(() => frame.offset, Error); diff --git a/js/src/jit-test/tests/debug/Frame-offset-04.js b/js/src/jit-test/tests/debug/Frame-offset-04.js new file mode 100644 index 0000000000..563dad4d84 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-offset-04.js @@ -0,0 +1,50 @@ +// frame.offset works in async fns. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +async function f() { + await Promise.resolve(); +} +`); + +const ranges = []; +let script; +let frame; +dbg.onEnterFrame = function(f) { + if (frame) { + assertEq(f, frame); + assertEq(f.script, script); + } else { + frame = f; + script = f.script; + assertEq(frame instanceof Debugger.Frame, true); + assertEq(script instanceof Debugger.Script, true); + } + + const range = [script.getOffsetMetadata(frame.offset).lineNumber, null]; + ranges.push(range); + f.onPop = () => { + range[1] = script.getOffsetMetadata(frame.offset).lineNumber; + }; +}; + +(async () => { + const promise = g.f(); + + assertEq(ranges.length, 1); + assertEq(ranges[0][0], 2); + assertEq(ranges[0][1], 3); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 3); + + await promise; + + assertEq(ranges.length, 2); + assertEq(ranges[1][0], 3); + assertEq(ranges[1][1], 3); + + // The frame has finished evaluating, so the callee is no longer accessible. + assertThrowsInstanceOf(() => frame.offset, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-offset-05.js b/js/src/jit-test/tests/debug/Frame-offset-05.js new file mode 100644 index 0000000000..e5b2242409 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-offset-05.js @@ -0,0 +1,94 @@ +// frame.offset works in async generator fns. +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +async function* f() { + yield 11; + yield 21; + yield 31; +} +`); + +const ranges = []; +let script; +let frame; +dbg.onEnterFrame = function(f) { + if (frame) { + assertEq(f, frame); + assertEq(f.script, script); + } else { + frame = f; + script = f.script; + assertEq(frame instanceof Debugger.Frame, true); + assertEq(script instanceof Debugger.Script, true); + } + + const range = [script.getOffsetMetadata(frame.offset).lineNumber, null]; + ranges.push(range); + f.onPop = () => { + range[1] = script.getOffsetMetadata(frame.offset).lineNumber; + }; +}; + +(async () => { + let promise; + const it = g.f(); + + assertEq(ranges.length, 1); + assertEq(ranges[0][0], 2); + assertEq(ranges[0][1], 2); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 2); + + promise = it.next(); + + assertEq(ranges.length, 2); + assertEq(ranges[1][0], 2); + assertEq(ranges[1][1], 3); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 3); + + await promise; + + assertEq(ranges.length, 3); + assertEq(ranges[2][0], 3); + assertEq(ranges[2][1], 3); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 3); + + promise = it.next(); + + assertEq(ranges.length, 4); + assertEq(ranges[3][0], 3); + assertEq(ranges[3][1], 4); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 4); + + await promise; + + assertEq(ranges.length, 5); + assertEq(ranges[4][0], 4); + assertEq(ranges[4][1], 4); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 4); + + promise = it.next(); + + assertEq(ranges.length, 6); + assertEq(ranges[5][0], 4); + assertEq(ranges[5][1], 5); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 5); + + await promise; + + assertEq(ranges.length, 7); + assertEq(ranges[6][0], 5); + assertEq(ranges[6][1], 5); + assertEq(script.getOffsetMetadata(frame.offset).lineNumber, 5); + + promise = it.next(); + + assertEq(ranges.length, 8); + assertEq(ranges[7][0], 5); + assertEq(ranges[7][1], 5); + + // The frame has finished evaluating, so the callee is no longer accessible. + assertThrowsInstanceOf(() => frame.offset, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-older-01.js b/js/src/jit-test/tests/debug/Frame-older-01.js new file mode 100644 index 0000000000..750c6614b8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-older-01.js @@ -0,0 +1,19 @@ +// Basic call chain. + +var g = newGlobal({newCompartment: true}); +var result = null; +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + var a = []; + assertEq(frame === frame.older, false); + for (; frame; frame = frame.older) + a.push(frame.type === 'call' ? frame.callee.name : frame.type); + a.reverse(); + result = a.join(", "); +}; + +g.eval("function first() { return second(); }"); +g.eval("function second() { return eval('third()'); }"); +g.eval("function third() { debugger; }"); +g.evaluate("first();"); +assertEq(result, "global, first, second, eval, third"); diff --git a/js/src/jit-test/tests/debug/Frame-older-02.js b/js/src/jit-test/tests/debug/Frame-older-02.js new file mode 100644 index 0000000000..c5f772e46c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-older-02.js @@ -0,0 +1,34 @@ +// An explicit async stack should interrupt a Debugger.Frame chain. + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +let done = false; +dbg.onDebuggerStatement = function (frame) { + // The frame has no "older" frame because the explicit async stack + // attached to the async function takes priority over the real + // parent frame that is tracked in the frame iterator. + assertEq(!!frame.older, false); + + done = true; +}; + +g.eval(` +let draining = false; +async function run() { + await Promise.resolve(); + + // Make sure that the test is running within "drainJobQueue()". + assertEq(draining, true); + debugger; +} + +(function main() { + run(); + + // Force resumption of the "run" function. + draining = true; + drainJobQueue(); + draining = false; +})(); +`); +assertEq(done, true); diff --git a/js/src/jit-test/tests/debug/Frame-older-generators-01.js b/js/src/jit-test/tests/debug/Frame-older-generators-01.js new file mode 100644 index 0000000000..f79f485faf --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-older-generators-01.js @@ -0,0 +1,53 @@ +// Generator/async frames can be created by following .older. +// +// The goal here is to get some test coverage creating generator Frame objects +// at some time other than when firing onEnterFrame. Here they're created after +// the initial yield. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function f() { + debugger; + } + function* gen() { + f(); + yield 1; + f(); + } + function* genDefaults(x=f()) { + f(); + } + async function af() { + f(); + await 1; + f(); + } + async function afDefaults(x=f()) { + await 1; + f(); + } +`); + +function test(expected, code) { + let dbg = Debugger(g); + let hits = 0; + let genFrame = null; + dbg.onDebuggerStatement = frame => { + hits++; + assertEq(frame.callee.name, "f"); + if (genFrame === null) { + genFrame = frame.older; + } else { + assertEq(frame.older, genFrame); + } + assertEq(genFrame.callee.name, expected); + }; + g.eval(code); + assertEq(hits, 2); + dbg.removeDebuggee(g); +} + +test("gen", "for (var x of gen()) {}"); +test("genDefaults", "for (var x of genDefaults()) {}"); +test("af", "af(); drainJobQueue();"); +test("afDefaults", "afDefaults(); drainJobQueue();") diff --git a/js/src/jit-test/tests/debug/Frame-older-generators-02.js b/js/src/jit-test/tests/debug/Frame-older-generators-02.js new file mode 100644 index 0000000000..f677bf64a8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-older-generators-02.js @@ -0,0 +1,50 @@ +// Like Frame-older-generators-01.js, but attach the debugger on the fly. +// +// (That is, check that it works even if the debugger never received +// onNewGenerator for the generator, because it wasn't attached at the time.) + +let g = newGlobal({newCompartment: true}); +g.eval(` + function f() { + attach(); + debugger; + } + function* gen() { + f(); + yield 1; + f(); + } + async function af() { + f(); + await 1; + f(); + } +`); + +function test(expected, code) { + let dbg; + let hits = 0; + let genFrame = null; + + g.attach = () => { + if (dbg === undefined) { + dbg = Debugger(g); + dbg.onDebuggerStatement = frame => { + hits++; + assertEq(frame.callee.name, "f"); + if (genFrame === null) { + genFrame = frame.older; + } else { + assertEq(frame.older, genFrame); + } + assertEq(genFrame.callee.name, expected); + }; + } + }; + g.eval(code); + assertEq(hits, 2); + dbg.removeDebuggee(g); +} + +test("gen", "for (var x of gen()) {}"); +test("af", "af(); drainJobQueue();"); diff --git a/js/src/jit-test/tests/debug/Frame-older-generators-03.js b/js/src/jit-test/tests/debug/Frame-older-generators-03.js new file mode 100644 index 0000000000..128898b206 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-older-generators-03.js @@ -0,0 +1,24 @@ +// Test that Debugger.Frame.prototype.older works on suspended generators. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +function* f() {} +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + dbg.onEnterFrame = undefined; +}; + +const it = g.f(); + +assertEq(frame.older, null); + +it.next(); + +assertThrowsInstanceOf(() => frame.older, Error); diff --git a/js/src/jit-test/tests/debug/Frame-older-generators-04.js b/js/src/jit-test/tests/debug/Frame-older-generators-04.js new file mode 100644 index 0000000000..405e08bed5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-older-generators-04.js @@ -0,0 +1,28 @@ +// Test that Debugger.Frame.prototype.older works on suspended async functions. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +async function f() { + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + dbg.onEnterFrame = undefined; +}; + +(async () => { + const promise = g.f(); + + assertEq(frame.older, null); + + await promise; + + assertThrowsInstanceOf(() => frame.older, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-older-generators-05.js b/js/src/jit-test/tests/debug/Frame-older-generators-05.js new file mode 100644 index 0000000000..61267a8443 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-older-generators-05.js @@ -0,0 +1,32 @@ +// Test that Debugger.Frame.prototype.older works on suspended async generators. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +async function* f() { + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + dbg.onEnterFrame = undefined; +}; + +(async () => { + const it = g.f(); + + assertEq(frame.older, null); + + const promise = it.next(); + + assertEq(frame.older, null); + + await promise; + + assertThrowsInstanceOf(() => frame.older, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-olderSavedFrame-01.js b/js/src/jit-test/tests/debug/Frame-olderSavedFrame-01.js new file mode 100644 index 0000000000..018e6130ea --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-olderSavedFrame-01.js @@ -0,0 +1,40 @@ +// An explicit async stack should be available via olderSavedFrame. +// This test makes sure that "main" shows up as an async saved frame +// instead of "older", even though "drainJobQueue()" is running inside of +// "main()" directly. + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +let done = false; +dbg.onDebuggerStatement = function (frame) { + // This frame has an "olderSavedFrame" because "run()" is an async function + // and those have explicit async stacks attached to them. + const parent = frame.olderSavedFrame; + assertEq(typeof parent.source, "string"); + assertEq(parent.line, 12); + assertEq(parent.column, 3); + assertEq(parent.asyncCause, "async"); + assertEq(parent.functionDisplayName, "main"); + done = true; +}; + +g.eval(` +let draining = false; +async function run() { + await Promise.resolve(); + + // Make sure that the test is running within "drainJobQueue()". + assertEq(draining, true); + debugger; +} + +(function main() { + run(); + + // Force resumption of the "run" function. + draining = true; + drainJobQueue(); + draining = false; +})(); +`); +assertEq(done, true); diff --git a/js/src/jit-test/tests/debug/Frame-olderSavedFrame-02.js b/js/src/jit-test/tests/debug/Frame-olderSavedFrame-02.js new file mode 100644 index 0000000000..ae774b8784 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-olderSavedFrame-02.js @@ -0,0 +1,32 @@ +// An explicit async stack should be available via olderSavedFrame. +// This test makes sure that "main" shows up as an async saved frame properly +// when the promise is resumed normally outside "main()". + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +let done = false; +dbg.onDebuggerStatement = function (frame) { + // This frame has an "olderSavedFrame" because "run()" is an async function + // and those have explicit async stacks attached to them. + const parent = frame.olderSavedFrame; + assertEq(typeof parent.source, "string"); + assertEq(parent.line, 9); + assertEq(parent.column, 3); + assertEq(parent.asyncCause, "async"); + assertEq(parent.functionDisplayName, "main"); + done = true; +}; + +g.eval(` +let draining = false; +async function run() { + await Promise.resolve(); + debugger; +} + +(function main() { + run(); +})(); +drainJobQueue(); +`); +assertEq(done, true); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-01.js b/js/src/jit-test/tests/debug/Frame-onPop-01.js new file mode 100644 index 0000000000..2b9f227e3d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-01.js @@ -0,0 +1,29 @@ +// When multiple frames have onPop handlers, they are called in the correct order. +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +g.eval("function g() { f(); }"); +g.eval("function h() { g(); }"); +g.eval("function i() { h(); }"); + +var dbg = new Debugger(g); +var log; +function logger(frame, mark) { + return function (completion) { + assertEq(this, frame); + assertEq('return' in completion, true); + log += mark; + }; +} +dbg.onEnterFrame = function handleEnter(f) { + log += "(" + f.callee.name; + // Note that this establishes a distinct function object as each + // frame's onPop handler. Thus, a pass proves that each frame is + // remembering its handler separately. + f.onPop = logger(f, f.callee.name + ")"); +}; +dbg.onDebuggerStatement = function handleDebugger(f) { + log += 'd'; +}; +log = ''; +g.i(); +assertEq(log, "(i(h(g(fdf)g)h)i)"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-02.js b/js/src/jit-test/tests/debug/Frame-onPop-02.js new file mode 100644 index 0000000000..11179f64b9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-02.js @@ -0,0 +1,20 @@ +// Clearing a frame's onPop handler works. +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +var dbg = new Debugger(g); + +var log; +dbg.onEnterFrame = function handleEnter(f) { + log += "("; + f.onPop = function handlePop() { + assertEq("handlePop was called", "handlePop should never be called"); + }; +}; +dbg.onDebuggerStatement = function handleDebugger(f) { + log += "d"; + assertEq(typeof f.onPop, "function"); + f.onPop = undefined; +}; +log = ''; +g.f(); +assertEq(log, "(d"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-03.js b/js/src/jit-test/tests/debug/Frame-onPop-03.js new file mode 100644 index 0000000000..e2a2034921 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-03.js @@ -0,0 +1,32 @@ +// When an exception is propagated out of multiple frames, their onPop +// and onExceptionUnwind handlers are called in the correct order. +var g = newGlobal({newCompartment: true}); +g.eval("function f() { throw 'mud'; }"); +g.eval("function g() { f(); }"); +g.eval("function h() { g(); }"); +g.eval("function i() { h(); }"); + +var dbg = new Debugger(g); +var log; +function makePopHandler(label) { + return function handlePop(completion) { + log += label; + assertEq(completion.throw, "mud"); + }; +} +dbg.onEnterFrame = function handleEnter(f) { + log += "(" + f.callee.name; + f.onPop = makePopHandler(")" + f.callee.name); +}; +dbg.onExceptionUnwind = function handleExceptionUnwind(f, x) { + assertEq(x, 'mud'); + log += "u" + f.callee.name; +}; +log = ''; +try { + g.i(); +} catch (x) { + log += 'c'; + assertEq(x, "mud"); +} +assertEq(log, "(i(h(g(fuf)fug)guh)hui)ic"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-04.js b/js/src/jit-test/tests/debug/Frame-onPop-04.js new file mode 100644 index 0000000000..3915fce278 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-04.js @@ -0,0 +1,30 @@ +// When a termination is propagated out of multiple frames, their onPop +// handlers are called in the correct order, and no onExceptionUnwind +// handlers are called. +var g = newGlobal({newCompartment: true}); +g.eval("function f() { terminate(); }"); +g.eval("function g() { f(); }"); +g.eval("function h() { g(); }"); +g.eval("function i() { h(); }"); + +var dbg = new Debugger(g); +var log; +var count = 0; +function makePopHandler(label, resumption) { + return function handlePop(completion) { + log += label; + assertEq(completion, null); + return resumption; + }; +} +dbg.onEnterFrame = function handleEnter(f) { + log += "(" + f.callee.name; + f.onPop = makePopHandler(f.callee.name + ")", + count++ == 0 ? { return: 'king' } : undefined); +}; +dbg.onExceptionUnwind = function handleExceptionUnwind(f, x) { + log += 'u'; +}; +log = ''; +assertEq(g.i(), 'king'); +assertEq(log, "(i(h(g(ff)g)h)i)"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-05.js b/js/src/jit-test/tests/debug/Frame-onPop-05.js new file mode 100644 index 0000000000..6e73c0e1ce --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-05.js @@ -0,0 +1,25 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +g.debuggerGlobal = this; +var log; + +dbg.onEnterFrame = function handleEnter(f) { + log += '('; + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.throw, "election"); + }; +}; +dbg.onExceptionUnwind = function handleExceptionUnwind(f, x) { + log += 'u'; + assertEq(x, "election"); +}; + +log = ''; +try { + g.eval("try { throw 'election'; } finally { debuggerGlobal.log += 'f'; }"); +} catch (x) { + log += 'c'; + assertEq(x, 'election'); +} +assertEq(log, '(ufu)c'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-06.js b/js/src/jit-test/tests/debug/Frame-onPop-06.js new file mode 100644 index 0000000000..05e8474564 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-06.js @@ -0,0 +1,20 @@ +// dbg.getNewestFrame in an onPop handler returns the frame being popped. + +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +g.eval("function g() { f(); }"); +g.eval("function h() { g(); }"); +g.eval("function i() { h(); }"); + +var dbg = new Debugger(g); +var log; +dbg.onEnterFrame = function handleEnter(f) { + log += "(" + f.callee.name; + f.onPop = function handlePop(c) { + log += ")" + f.callee.name; + assertEq(dbg.getNewestFrame(), this); + }; +}; +log = ''; +g.i(); +assertEq(log, "(i(h(g(f)f)g)h)i"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-08.js b/js/src/jit-test/tests/debug/Frame-onPop-08.js new file mode 100644 index 0000000000..2d7f63576d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-08.js @@ -0,0 +1,16 @@ +// Setting onPop handlers from a 'debugger' statement handler works. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onDebuggerStatement = function handleDebugger(frame) { + assertEq(frame.type, "eval"); + log += 'd'; + frame.onPop = function handlePop(c) { + log += ')'; + }; +}; + +log = ''; +g.eval('debugger;'); +assertEq(log, 'd)'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-09.js b/js/src/jit-test/tests/debug/Frame-onPop-09.js new file mode 100644 index 0000000000..2147a76905 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-09.js @@ -0,0 +1,23 @@ +// Setting onPop handlers from an onExceptionUnwind handler works. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onExceptionUnwind = function handleUnwind(frame) { + log += 'u'; + assertEq(frame.type, "eval"); + frame.onPop = function handleCallPop(c) { + log += ')'; + assertEq(c.throw, 'up'); + }; +}; + +log = ""; +try { + g.eval("throw 'up';"); + log += '-'; +} catch (x) { + log += 'c'; + assertEq(x, 'up'); +} +assertEq(log, 'u)c'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-10.js b/js/src/jit-test/tests/debug/Frame-onPop-10.js new file mode 100644 index 0000000000..091bf076e4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-10.js @@ -0,0 +1,22 @@ +// Setting onPop handlers from an onStep handler works. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onDebuggerStatement = function handleDebugger(frame) { + log += 'd'; + assertEq(frame.type, "eval"); + frame.onStep = function handleStep() { + log += 's'; + this.onStep = undefined; + this.onPop = function handlePop() { + log += ')'; + }; + }; +}; + +log = ""; +g.flag = false; +g.eval('debugger; flag = true'); +assertEq(log, 'ds)'); +assertEq(g.flag, true); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-11.js b/js/src/jit-test/tests/debug/Frame-onPop-11.js new file mode 100644 index 0000000000..36b58cd6dc --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-11.js @@ -0,0 +1,23 @@ +// Setting onPop handlers from breakpoint handlers works. +var g = newGlobal({newCompartment: true}); +g.eval("function f(){ return 'to normalcy'; }"); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var log; + +// Set a breakpoint at the start of g.f +var gf = gw.makeDebuggeeValue(g.f); +var fStartOffset = gf.script.getLineOffsets(gf.script.startLine)[0]; +gf.script.setBreakpoint(fStartOffset, { + hit: function handleHit(frame) { + log += 'b'; + frame.onPop = function handlePop(c) { + log += ')'; + assertEq(c.return, "to normalcy"); + }; + } +}); + +log = ""; +assertEq(g.f(), "to normalcy"); +assertEq(log, "b)"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-12.js b/js/src/jit-test/tests/debug/Frame-onPop-12.js new file mode 100644 index 0000000000..6fef64d635 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-12.js @@ -0,0 +1,21 @@ +// Setting an onPop handler from an onPop handler doesn't throw, but the +// new handler doesn't fire. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onDebuggerStatement = function handleDebugger(frame) { + log += 'd'; + assertEq(frame.type, "eval"); + frame.onPop = function firstHandlePop(c) { + log +=')'; + assertEq(c.return, 'on investment'); + this.onPop = function secondHandlePop(c) { + assertEq("secondHandlePop was called", "secondHandlePop should never be called"); + }; + }; +}; + +log = ""; +g.eval("debugger; 'on investment';"); +assertEq(log, 'd)'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-13.js b/js/src/jit-test/tests/debug/Frame-onPop-13.js new file mode 100644 index 0000000000..2fe0f79ddf --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-13.js @@ -0,0 +1,37 @@ +// One can set onPop handlers on some frames but not others. +var g = newGlobal({newCompartment: true}); +g.eval("function f(n) { if (n > 0) f(n-1); else debugger; }"); +var dbg = new Debugger(g); +var log; + +Debugger.Frame.prototype.nthOlder = function nthOlder(n) { + var f = this; + while (n-- > 0) + f = f.older; + return f; +}; + +dbg.onEnterFrame = function handleEnter(f) { + log += "(" + f.arguments[0]; +}; + +function makePopHandler(n) { + return function handlePop(c) { + log += ")" + this.arguments[0]; + assertEq(this.arguments[0], n); + }; +} + +dbg.onDebuggerStatement = function handleDebugger(f) { + // Set onPop handers on some frames, and leave others alone. Vary the + // spacing. + f.nthOlder(2).onPop = makePopHandler(2); + f.nthOlder(3).onPop = makePopHandler(3); + f.nthOlder(5).onPop = makePopHandler(5); + f.nthOlder(8).onPop = makePopHandler(8); + f.nthOlder(13).onPop = makePopHandler(13); +}; + +log = ''; +g.f(20); +assertEq(log, "(20(19(18(17(16(15(14(13(12(11(10(9(8(7(6(5(4(3(2(1(0)2)3)5)8)13"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-14.js b/js/src/jit-test/tests/debug/Frame-onPop-14.js new file mode 100644 index 0000000000..671f63d4bb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-14.js @@ -0,0 +1,25 @@ +// A frame's onPop handler is called only once, even if it is for a function +// called from a loop. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +var count; +dbg.onDebuggerStatement = function handleDebug(frame) { + log += 'd'; + assertEq(frame.type, "call"); + count++; + if (count == 10) { + frame.onPop = function handlePop(c) { + log += ')' + this.arguments[0]; + assertEq(c.return, "snifter"); + }; + } +}; + +g.eval("function f(n) { debugger; return 'snifter'; }"); +log = ''; +count = 0; +g.eval("for (i = 0; i < 20; i++) f(i);"); +assertEq(count, 20); +assertEq(log, "dddddddddd)9dddddddddd"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-15.js b/js/src/jit-test/tests/debug/Frame-onPop-15.js new file mode 100644 index 0000000000..6b76723cb2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-15.js @@ -0,0 +1,31 @@ +// Each resumption of a generator gets the same Frame; its onPop handler +// fires each time the generator yields. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +var seenFrame = null; +dbg.onDebuggerStatement = function handleDebugger(frame) { + log += 'd'; + assertEq(frame.type, "call"); + + if (seenFrame === null) { + seenFrame = frame; + } else { + assertEq(seenFrame, frame); + } + + let i = frame.eval('i').return; + if (i % 3 == 0) { + frame.onPop = function handlePop(c) { + assertEq(this, seenFrame); + log += ')' + i; + }; + } +}; + +g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }"); +log =''; +assertEq(g.eval("var t = 0; for (j of g()) t += j; t;"), 45); +assertEq(log, "d)0d)0d)0d)3d)3d)3d)6d)6d)6d)9)9"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-16.js b/js/src/jit-test/tests/debug/Frame-onPop-16.js new file mode 100644 index 0000000000..dfb765d1e7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-16.js @@ -0,0 +1,18 @@ +// onPop handlers fire even on frames that make tail calls. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +g.eval('function f(n) { if (n > 0) f(n-1); else debugger; }'); + +dbg.onEnterFrame = function handleEnter(frame) { + log += '('; + frame.onPop = function handlePop(c) { + log += ')'; + assertEq(typeof c == "object" && 'return' in c, true); + }; +}; + +log = ''; +g.f(10); +assertEq(log, "((((((((((()))))))))))"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-17.js b/js/src/jit-test/tests/debug/Frame-onPop-17.js new file mode 100644 index 0000000000..f37bcb3cd5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-17.js @@ -0,0 +1,41 @@ +// onPop surfaces. +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// Assigning a bogus value to Debugger.Frame.prototype.onPop raises a TypeError. +function test(badValue) { + print("store " + JSON.stringify(badValue) + " in Debugger.Frame.prototype.onPop"); + + var log; + dbg.onDebuggerStatement = function handleDebugger(frame) { + log += "d"; + assertThrowsInstanceOf(function () { frame.onPop = badValue; }, TypeError); + }; + + log = ""; + g.eval("debugger"); + assertEq(log, "d"); +} + +test(null); +test(false); +test(1); +test("stringy"); +test(Symbol("symbolic")); +test({}); +test([]); + +// Getting and setting the prototype's onPop is an error. +assertThrowsInstanceOf(function () { Debugger.Frame.prototype.onPop; }, TypeError); +assertThrowsInstanceOf( + function () { Debugger.Frame.prototype.onPop = function () {}; }, + TypeError); + +// The getters and setters correctly check the type of their 'this' argument. +var descriptor = Object.getOwnPropertyDescriptor(Debugger.Frame.prototype, 'onPop'); +assertEq(descriptor.configurable, true); +assertEq(descriptor.enumerable, false); +assertThrowsInstanceOf(function () { descriptor.get.call({}); }, TypeError); +assertThrowsInstanceOf(function () { descriptor.set.call({}, function() {}); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-18.js b/js/src/jit-test/tests/debug/Frame-onPop-18.js new file mode 100644 index 0000000000..2fd37c27bb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-18.js @@ -0,0 +1,22 @@ +// A garbage collection in the debugger compartment does not disturb onPop +// handlers. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onEnterFrame = function handleEnter(frame) { + log += '('; + frame.onPop = function handlePop(completion) { + log += ')'; + }; +}; + +dbg.onDebuggerStatement = function handleDebugger (frame) { + log += 'd'; + // GC in the debugger's compartment only. + gc(dbg); +}; + +log = ''; +assertEq(g.eval('debugger; 42;'), 42); +assertEq(log, '(d)'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-19.js b/js/src/jit-test/tests/debug/Frame-onPop-19.js new file mode 100644 index 0000000000..27c8a9b131 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-19.js @@ -0,0 +1,16 @@ +// A garbage collection in the debuggee compartment does not disturb onPop +// handlers. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onEnterFrame = function handleEnter(frame) { + log += '('; + frame.onPop = function handlePop(completion) { + log += ')'; + }; +}; + +log = ''; +assertEq(g.eval('gc(this); 42;'), 42); +assertEq(log, '()'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-20.js b/js/src/jit-test/tests/debug/Frame-onPop-20.js new file mode 100644 index 0000000000..2702c48048 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-20.js @@ -0,0 +1,15 @@ +// A global garbage collection does not disturb onPop handlers. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; + +dbg.onEnterFrame = function handleEnter(frame) { + log += '('; + frame.onPop = function handlePop(completion) { + log += ')'; + }; +}; + +log = ''; +assertEq(g.eval('gc(); 42;'), 42); +assertEq(log, '()'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-21.js b/js/src/jit-test/tests/debug/Frame-onPop-21.js new file mode 100644 index 0000000000..c7d52f974c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-21.js @@ -0,0 +1,30 @@ +// frame.eval works from an onPop handler. +var g = newGlobal({newCompartment: true}); +g.eval('function f(a,b) { var x = "entablature", y; debugger; return x+y+a+b; }'); + +var dbg = new Debugger(g); +var log; + +dbg.onDebuggerStatement = function handleDebugger(frame) { + log += 'd'; + frame.onPop = handlePop; +}; + +function handlePop(c) { + log += ')'; + + // Arguments must be live. + assertEq(this.eval('a').return, 'frieze'); + assertEq(this.eval('b = "architrave"').return, 'architrave'); + assertEq(this.eval('arguments[1]').return, 'architrave'); + assertEq(this.eval('b').return, 'architrave'); + + // function-scope variables must be live. + assertEq(this.eval('x').return, 'entablature'); + assertEq(this.eval('y = "cornice"').return, 'cornice'); + assertEq(this.eval('y').return, 'cornice'); +} + +log = ''; +g.eval('f("frieze", "stylobate")'); +assertEq(log, 'd)'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-23.js b/js/src/jit-test/tests/debug/Frame-onPop-23.js new file mode 100644 index 0000000000..894250ef5b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-23.js @@ -0,0 +1,34 @@ +// Check that the line number reported at an onPop stop makes sense, +// even when it happens on an "artificial" instruction. + +var g = newGlobal({newCompartment: true}); + +// This bit of code arranges for the line number of the "artificial" +// instruction to be something nonsensical -- the middle of a loop +// which cannot be entered. +g.eval(`function f() { + debugger; // +0 + if(false) { // +1 + for(var b=0; b<0; b++) { // +2 + c = 2; // +3 + } // +4 + } // +5 +} // +6 +`); + +var dbg = Debugger(g); + +let debugLine; +let foundLine; + +dbg.onDebuggerStatement = function(frame) { + debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber; + frame.onPop = function(c) { + foundLine = this.script.getOffsetLocation(this.offset).lineNumber; + }; +}; + +g.eval("f();\n"); + +// The stop should happen on the closing brace of the function. +assertEq(foundLine == debugLine + 6, true); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-after-debugger-return.js b/js/src/jit-test/tests/debug/Frame-onPop-after-debugger-return.js new file mode 100644 index 0000000000..ddd117bf83 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-after-debugger-return.js @@ -0,0 +1,11 @@ +// Bug 744730. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (f) { return {return: 1234}; }; +var hit = false; +dbg.onEnterFrame = function (f) { + f.onPop = function () { hit = true}; +}; +g.eval("debugger;"); +assertEq(hit, true); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-assign-function.js b/js/src/jit-test/tests/debug/Frame-onPop-assign-function.js new file mode 100644 index 0000000000..1994248d90 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-assign-function.js @@ -0,0 +1,48 @@ +// Changing onPop while the function is dead is allowed. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +let steps = new Set(); +dbg.onDebuggerStatement = function(frame) { + // Setting 'onPop' while alive is allowed. + steps.add("debugger 1"); + assertEq(frame.onStack, true); + frame.onPop = function() { + steps.add("onpop 1"); + }; + + dbg.onDebuggerStatement = function() { + // Clear the 'onPop' while dead. + steps.add("debugger 2"); + assertEq(frame.onStack, false); + + // Clearing 'onPop' while dead is allowed. + frame.onPop = undefined; + + // Setting 'onPop' while dead is allowed. + frame.onPop = function() { + steps.add("onpop 2"); + }; + }; +}; + +g.eval( + ` + function myGen() { + debugger; + } + + const g = myGen(); + + debugger; + ` +); + +assertDeepEq(Array.from(steps), [ + "debugger 1", + "onpop 1", + "debugger 2", +]); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-assign-generator.js b/js/src/jit-test/tests/debug/Frame-onPop-assign-generator.js new file mode 100644 index 0000000000..14a7a2a162 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-assign-generator.js @@ -0,0 +1,68 @@ +// Changing onPop while the generator is suspended/dead is allowed. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +let steps = new Set(); +dbg.onDebuggerStatement = function(frame) { + // Setting 'onStep' while alive is allowed. + steps.add("debugger 1"); + assertEq(frame.onStack, true); + frame.onPop = function() { + steps.add("onpop 1"); + }; + + dbg.onDebuggerStatement = function() { + // Clear the 'onPop' while suspended. + steps.add("debugger 2"); + assertEq(frame.onStack, false); + + // Clearing 'onPop' while suspended is allowed. + frame.onPop = undefined; + + // Setting 'onPop' while suspended is allowed. + frame.onPop = function() { + steps.add("onpop 2"); + }; + + dbg.onDebuggerStatement = function() { + steps.add("debugger 3"); + assertEq(frame.onStack, false); + + // Clearing 'onPop' while dead is allowed. + frame.onPop = undefined; + + // Setting 'onPop' while dead is allowed. + frame.onPop = function() { + steps.add("onpop 3"); + }; + }; + }; +}; + +g.eval( + ` + function* myGen() { + debugger; + yield; + } + + const g = myGen(); + g.next(); + + debugger; + g.next(); + + debugger; + ` +); + +assertDeepEq(Array.from(steps), [ + "debugger 1", + "onpop 1", + "debugger 2", + "onpop 2", + "debugger 3", +]); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-async-01.js b/js/src/jit-test/tests/debug/Frame-onPop-async-01.js new file mode 100644 index 0000000000..8d491f3f7f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-async-01.js @@ -0,0 +1,39 @@ +// When an async function awaits, if Frame.onPop processes microtasks, +// the async function itself will not run. It'll run later. +// +// This is a reentrancy test, like Frame-onPop-generators-03. + +let g = newGlobal({newCompartment: true}); +g.log = ""; +g.eval(` + async function f() { + log += "1"; + debugger; + log += "2"; + await Promise.resolve(3); + log += "3"; + return "ok"; + } +`); + +let dbg = Debugger(g); +dbg.onDebuggerStatement = frame => { + frame.onPop = completion => { + // What we are really testing is that when onPop is called, we have not + // yet thrown this async function activation back into the hopper. + g.log += 'A'; + drainJobQueue(); + g.log += 'B'; + + frame.onPop = completion => { + g.log += 'C'; + }; + }; +}; + +let status = "FAIL - g.f() did not resolve"; +g.f().then(value => { status = value; }); +assertEq(g.log, "12AB"); +drainJobQueue(); +assertEq(g.log, "12AB3C"); +assertEq(status, "ok"); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-async-02.js b/js/src/jit-test/tests/debug/Frame-onPop-async-02.js new file mode 100644 index 0000000000..8e579b588b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-async-02.js @@ -0,0 +1,36 @@ +// |jit-test| error:all-jobs-completed-successfully + +load(libdir + 'match.js'); +load(libdir + 'match-debugger.js'); +const { Pattern } = Match; +const { OBJECT_WITH_EXACTLY: EXACT } = Pattern; + +let g = newGlobal({newCompartment: true}); +let dbg = Debugger(g); +const log = []; +g.capture = function () { + dbg.getNewestFrame().onPop = completion => { + log.push(completion); + }; +}; + +g.eval(` + async function f() { + capture(); + await Promise.resolve(3); + return "ok"; + } +`); + + +const promise = g.f(); +promise.then(value => { + assertEq(value, "ok"); + + Pattern([ + EXACT({ return: new DebuggerObjectPattern("Promise"), await:true }), + EXACT({ return: new DebuggerObjectPattern("Promise") }), + ]).assert(log); + + throw "all-jobs-completed-successfully"; +}); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-async-generators-01.js b/js/src/jit-test/tests/debug/Frame-onPop-async-generators-01.js new file mode 100644 index 0000000000..0c2cc27f8c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-async-generators-01.js @@ -0,0 +1,53 @@ +// |jit-test| error:all-jobs-completed-successfully + +load(libdir + "asserts.js"); +load(libdir + 'match.js'); +load(libdir + 'match-debugger.js'); +const { Pattern } = Match; +const { OBJECT_WITH_EXACTLY: EXACT } = Pattern; + +let g = newGlobal({newCompartment: true}); +let dbg = Debugger(g); +const log = []; +g.capture = function () { + dbg.getNewestFrame().onPop = completion => { + log.push(completion); + }; +}; + +g.eval(` + async function* asyncgen() { + capture(); + await Promise.resolve(1); + yield 2; + await Promise.resolve(3); + yield 4; + return "ok"; + } +`); + +async function collect() { + let items = []; + for await (let item of g.asyncgen()) { + items.push(item); + } + return items; +} + +collect().then(value => { + assertDeepEq(value, [2, 4]); + + Pattern([ + EXACT({ return: new DebuggerObjectPattern("Promise"), await: true }), + EXACT({ return: 2, await: true }), + EXACT({ return: 2, yield: true }), + EXACT({ return: new DebuggerObjectPattern("Promise"), await: true }), + EXACT({ return: 4, await: true }), + EXACT({ return: 4, yield: true }), + EXACT({ return: "ok", await: true }), + EXACT({ return: "ok" }), + ]).assert(log); + + throw "all-jobs-completed-successfully"; +}); + diff --git a/js/src/jit-test/tests/debug/Frame-onPop-dead-frame.js b/js/src/jit-test/tests/debug/Frame-onPop-dead-frame.js new file mode 100644 index 0000000000..52c6f2ab79 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-dead-frame.js @@ -0,0 +1,28 @@ +// Don't crash trying to fire a dead frame's onPop handler. + +var g = newGlobal({newCompartment: true}); +g.eval('function f() { debugger; }'); + +var log = ''; + +// Create two Debuggers debugging the same global `g`. Both will put onPop +// handlers on the same frame. +var dbg1 = Debugger(g); +dbg1.onDebuggerStatement = frame1 => { + frame1.onPop = completion => { + log += 'A'; + dbg2.removeDebuggee(g); // kills frame2, so frame2.onPop should not fire + log += 'B'; + }; +}; + +var dbg2 = Debugger(g); +dbg2.onDebuggerStatement = frame2 => { + frame2.onPop = completion => { + log += 'C'; + }; +}; + +g.f(); + +assertEq(log, 'AB'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-error-error.js b/js/src/jit-test/tests/debug/Frame-onPop-error-error.js new file mode 100644 index 0000000000..fc6c18b6d4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-error-error.js @@ -0,0 +1,60 @@ +// |jit-test| error: TestComplete +// onPop can request a termination when stopped for a termination + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// We use Debugger.Frame.prototype.eval and ignore the outer 'eval' frame so we +// can catch the termination. + +function test(type, provocation) { + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + var log; + dbg.onEnterFrame = function handleFirstFrame(f) { + log += 'f'; + dbg.onDebuggerStatement = function handleDebugger(f) { + log += 'd'; + return null; + }; + + dbg.onEnterFrame = function handleSecondFrame(f) { + log += 'e'; + assertEq(f.type, 'eval'); + + dbg.onEnterFrame = function handleThirdFrame(f) { + log += '('; + assertEq(f.type, type); + + dbg.onEnterFrame = function handleExtraFrames(f) { + // This should never be called. + assertEq(false, true); + }; + + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c, null); + return null; + }; + }; + }; + + assertEq(f.eval(provocation), null); + }; + + log = ''; + // This causes handleFirstFrame to be called. + assertEq(typeof g.eval('eval'), 'function'); + assertEq(log, 'fe(d)'); + + print(); +} + +g.eval('function f() { debugger; return \'termination fail\'; }'); +test('call', 'f();'); +test('call', 'new f;'); +test('eval', 'eval(\'debugger; \\\'termination fail\\\';\');'); +test('global', 'evaluate(\'debugger; \\\'termination fail\\\';\');'); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-error-return.js b/js/src/jit-test/tests/debug/Frame-onPop-error-return.js new file mode 100644 index 0000000000..d8abdebd70 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-error-return.js @@ -0,0 +1,47 @@ +// |jit-test| error: TestComplete +// onPop can change a termination into a normal return. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + var wasConstructing; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + return null; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + wasConstructing = f.constructing; + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c, null); + return { return: 'favor' }; + }; + }; + + log = ''; + var result = provocation(); + if (wasConstructing) + assertEq(typeof result, "object"); + else + assertEq(result, 'favor'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; return 'termination fail'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; \'termination fail\';"); }); +test("global", function () { return g.evaluate("debugger; \'termination fail\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-error-scope-unwind-01.js b/js/src/jit-test/tests/debug/Frame-onPop-error-scope-unwind-01.js new file mode 100644 index 0000000000..c4c6ee0f73 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-error-scope-unwind-01.js @@ -0,0 +1,33 @@ +// Tests that exception handling works with block scopes. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var correct; +dbg.onEnterFrame = function (f) { + if (f.callee && f.callee.name == "f") { + f.onPop = function() { + // The scope at the point of onPop is at the point of popping (the + // noSuchFn call). + correct = (f.environment.getVariable("e") === 42 && + f.environment.getVariable("outer") === undefined); + }; + } +}; +g.eval("" + function f() { + var outer = 43; + try { + eval(""); + throw 42; + } catch (e) { + noSuchFn(e); + } +}); + + +try { + g.eval("f();"); +} catch (e) { + // The above is expected to throw. +} + +assertEq(correct, true); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-error-scope-unwind-02.js b/js/src/jit-test/tests/debug/Frame-onPop-error-scope-unwind-02.js new file mode 100644 index 0000000000..935c4173a0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-error-scope-unwind-02.js @@ -0,0 +1,36 @@ +// Tests that exception handling works with block scopes. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var correct; +dbg.onEnterFrame = function (f) { + if (f.callee && f.callee.name == "f") { + f.onPop = function() { + // The scope at the point of onPop is at the point of popping (the + // noSuchFn call). + correct = (f.environment.getVariable("e") === 42 && + f.environment.getVariable("outer") === undefined); + }; + } +}; +g.eval("" + function f() { + var outer = 43; + // Surround with a loop to insert a loop trynote. + for (;;) { + try { + eval(""); + throw 42; + } catch (e) { + noSuchFn(e); + } + } +}); + + +try { + g.eval("f();"); +} catch (e) { + // The above is expected to throw. +} + +assertEq(correct, true); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-error-throw.js b/js/src/jit-test/tests/debug/Frame-onPop-error-throw.js new file mode 100644 index 0000000000..0f96865ead --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-error-throw.js @@ -0,0 +1,42 @@ +// |jit-test| error: TestComplete +// onPop can change a termination into a throw. + +load(libdir + "asserts.js"); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + return null; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c, null); + return { throw: 'snow' }; + }; + }; + + log = ''; + assertThrowsValue(provocation, 'snow'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; return 'termination fail'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; \'termination fail\';"); }); +test("global", function () { return g.evaluate("debugger; \'termination fail\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-error.js b/js/src/jit-test/tests/debug/Frame-onPop-error.js new file mode 100644 index 0000000000..777bf70c97 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-error.js @@ -0,0 +1,59 @@ +// |jit-test| error: TestComplete +// onPop fires when frames are terminated. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// We use Debugger.Frame.prototype.eval and ignore the outer 'eval' frame so we +// can catch the termination. + +function test(type, provocation) { + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + var log; + dbg.onEnterFrame = function handleFirstFrame(f) { + log += 'f'; + dbg.onDebuggerStatement = function handleDebugger(f) { + log += 'd'; + return null; + }; + + dbg.onEnterFrame = function handleSecondFrame(f) { + log += 'e'; + assertEq(f.type, 'eval'); + + dbg.onEnterFrame = function handleThirdFrame(f) { + log += '('; + assertEq(f.type, type); + + dbg.onEnterFrame = function handleExtraFrames(f) { + // This should never be called. + assertEq(false, true); + }; + + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c, null); + }; + }; + }; + + assertEq(f.eval(provocation), null); + }; + + log = ''; + // This causes handleFirstFrame to be called. + assertEq(typeof g.eval('eval'), 'function'); + assertEq(log, 'fe(d)'); + + print(); +} + +g.eval('function f() { debugger; return \'termination fail\'; }'); +test('call', 'f();'); +test('call', 'new f;'); +test('eval', 'eval(\'debugger; \\\'termination fail\\\';\');'); +test('global', 'evaluate(\'debugger; \\\'termination fail\\\';\');'); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generator-resumption-01.js b/js/src/jit-test/tests/debug/Frame-onPop-generator-resumption-01.js new file mode 100644 index 0000000000..617baceb7b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generator-resumption-01.js @@ -0,0 +1,14 @@ +// A generator is left closed after frame.onPop returns a {return:} resumption value. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +dbg.onDebuggerStatement = frame => { + frame.onPop = completion => ({return: "ok"}); +}; +g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }"); +var it = g.g(); +var result = it.next(); +assertEq(result.value, "ok"); +assertEq(result.done, true); +assertEq(it.next().value, undefined); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-01.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-01.js new file mode 100644 index 0000000000..6f64bdd6a4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-01.js @@ -0,0 +1,20 @@ +// Returning {throw:} from an onPop handler when yielding works. +// It closes the generator-iterator. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +dbg.onDebuggerStatement = function handleDebugger(frame) { + frame.onPop = function (c) { + return {throw: "fit"}; + }; +}; +g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }"); +g.eval("var it = g();"); +var rv = gw.executeInGlobal("it.next();"); +assertEq(rv.throw, "fit"); + +dbg.enabled = false; +assertEq(g.it.next().value, undefined); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-02.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-02.js new file mode 100644 index 0000000000..8b1ea96e60 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-02.js @@ -0,0 +1,19 @@ +// |jit-test| error: fit + +// Throwing an exception from an onPop handler when yielding terminates the debuggee +// but does not close the generator-iterator. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +dbg.onDebuggerStatement = function handleDebugger(frame) { + frame.onPop = function (c) { + throw "fit"; + }; +}; +g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }"); +g.eval("var it = g();"); +assertEq(gw.executeInGlobal("it.next();"), null); + +dbg.enabled = false; +assertEq(g.it.next().value, 1); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-03.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-03.js new file mode 100644 index 0000000000..cb7b43a08a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-03.js @@ -0,0 +1,42 @@ +// onPop fires while the [[GeneratorState]] is still "executing". +// +// This test checks that Debugger doesn't accidentally make it possible to +// reenter a generator frame that's on the stack. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval('function* f() { debugger; yield 1; debugger; yield 2; debugger; }'); +let dbg = Debugger(g); +let genObj = g.f(); + +let hits = 0; +dbg.onDebuggerStatement = frame => { + frame.onPop = completion => { + dbg.removeDebuggee(g); // avoid the DebuggeeWouldRun exception + hits++; + if (hits < 3) { + // We're yielding. Calling .return(), .next(), or .throw() on a + // generator that's currently on the stack fails with a TypeError. + assertThrowsInstanceOf(() => genObj.next(), g.TypeError); + assertThrowsInstanceOf(() => genObj.throw("fit"), g.TypeError); + assertThrowsInstanceOf(() => genObj.return(), g.TypeError); + } else { + // This time we're returning. The generator has already been + // closed, so its methods work but are basically no-ops. + let result = genObj.next(); + assertEq(result.done, true); + assertEq(result.value, undefined); + + assertThrowsValue(() => genObj.throw("fit"), "fit"); + + result = genObj.return(); + assertEq(result.done, true); + assertEq(result.value, undefined); + } + dbg.addDebuggee(g); + }; +}; + +for (let x of genObj) {} +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-04.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-04.js new file mode 100644 index 0000000000..8e4ea689e6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-04.js @@ -0,0 +1,26 @@ +// Terminating a generator from the onPop callback for its initial yield +// leaves the Frame in a sane but inactive state. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval("function* f(x) { yield x; }"); +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +let genFrame = null; +dbg.onDebuggerStatement = frame => { + dbg.onEnterFrame = frame => { + if (frame.callee == gw.getOwnPropertyDescriptor("f").value) { + genFrame = frame; + frame.onPop = completion => null; + } + }; + assertEq(frame.eval("f(0);"), null); +}; + +g.eval("debugger;"); + +assertEq(genFrame instanceof Debugger.Frame, true); +assertEq(genFrame.onStack, false); +assertThrowsInstanceOf(() => genFrame.callee, Error); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-05.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-05.js new file mode 100644 index 0000000000..09d66d7c84 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-05.js @@ -0,0 +1,29 @@ +// In .onPop for the "initial yield" of a generator, while the generator frame +// is on the stack, the generator object's .next() method throws. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* f() { + return "ok"; + } +`); + +let hits = 0; +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); +dbg.onEnterFrame = frame => { + dbg.onEnterFrame = undefined; // Trigger only once. + frame.onPop = completion => { + // Initial yield. + let genObj = completion.return; + assertEq(genObj.class, "Generator"); + let result = frame.evalWithBindings("genObj.next()", {genObj}); + assertEq(result.throw.class, "TypeError"); + assertEq(result.throw.getProperty("message").return, + "already executing generator"); + hits++; + }; +}; + +g.f(); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-06.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-06.js new file mode 100644 index 0000000000..993e2e11bb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-06.js @@ -0,0 +1,28 @@ +load(libdir + "asserts.js"); +load(libdir + 'match.js'); +load(libdir + 'match-debugger.js'); +const { Pattern } = Match; +const { OBJECT_WITH_EXACTLY: EXACT } = Pattern; + +let g = newGlobal({newCompartment: true}); +let dbg = Debugger(g); +const log = []; +g.capture = function () { + dbg.getNewestFrame().onPop = completion => { + log.push(completion); + }; +}; + +g.eval(` + function* f() { + capture(); + yield 3; + return "ok"; + } +`); + +assertDeepEq([... g.f()], [3]); +Pattern([ + EXACT({ return: new DebuggerObjectPattern("Object", { value: 3, done: false }), yield: true }), + EXACT({ return: new DebuggerObjectPattern("Object", { value: "ok", done: true }) }), +]).assert(log); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-07.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-07.js new file mode 100644 index 0000000000..7872b345e6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-07.js @@ -0,0 +1,40 @@ +// Completion values for generators report yields and initial yields. + +load(libdir + "asserts.js"); +load(libdir + 'match.js'); +load(libdir + 'match-debugger.js'); +const { Pattern } = Match; +const { OBJECT_WITH_EXACTLY: X } = Pattern; + +const g = newGlobal({ newCompartment: true }); +g.eval(` + function* f() { + yield "yielding"; + return "returning"; + } +`); + +const dbg = new Debugger(g); +const completions = []; +dbg.onEnterFrame = frame => { + frame.onPop = completion => { + completions.push(completion); + }; +}; + +assertDeepEq([... g.f()], ["yielding"]); +print(JSON.stringify(completions)); +Pattern([ + X({ + return: new DebuggerObjectPattern("Generator", {}), + yield: true, + initial: true + }), + X({ + return: new DebuggerObjectPattern("Object", { value: "yielding", done: false }), + yield: true + }), + X({ + return: new DebuggerObjectPattern("Object", { value: "returning", done: true }) + }), +]).assert(completions); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-08.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-08.js new file mode 100644 index 0000000000..75238ee17c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-08.js @@ -0,0 +1,16 @@ +// Creating a new generator frame after the generator is closed. + +var g = newGlobal({ newCompartment: true }); +g.eval("function* gen(x) { debugger; }"); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = frame => { + frame.onPop = completion => { + assertEq(frame.callee.name, "gen"); + assertEq(frame.eval("x").return, 3); + var f2 = (new Debugger(g)).getNewestFrame(); + assertEq(f2.callee.name, "gen"); + assertEq(f2.eval("x").return, 3); + }; +}; +g.gen(3).next(); + diff --git a/js/src/jit-test/tests/debug/Frame-onPop-multiple-01.js b/js/src/jit-test/tests/debug/Frame-onPop-multiple-01.js new file mode 100644 index 0000000000..e5280bae6b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-multiple-01.js @@ -0,0 +1,106 @@ +// Multiple debuggers all get their onPop handlers called. + +function completionsEqual(c1, c2) { + if (c1 && c2) { + if (c1.throw) + return c1.throw === c2.throw; + else + return c1.return === c2.return; + } + return c1 === c2; +} + +function completionString(c) { + if (c == null) + return 'x'; + if (c.return) + return 'r' + c.return; + if (c.throw) + return 't' + c.throw; + return '?'; +} + +var g = newGlobal({newCompartment: true}); // poor thing +g.eval('function f() { debugger; return "1"; }'); + +// A list of the debuggers' Debugger.Frame instances. When it's all over, +// we test that they are all marked as no longer live. +var frames = []; + +// We start off the test via Debugger.Frame.prototype.eval, so if we end +// with a termination, we still catch it, instead of aborting the whole +// test. (Debugger.Object.prototype.executeInGlobal would simplify this...) +var dbg0 = new Debugger(g); +dbg0.onEnterFrame = function handleOriginalEnter(frame) { + dbg0.log += '('; + dbg0.onEnterFrame = undefined; + + assertEq(frame.onStack, true); + frames.push(frame); + + var dbgs = []; + var log; + + // Create a separate debugger to carry out each item in sequence. + for (let i = 0; i < 9; i++) { + // Each debugger's handlers close over a distinct 'dbg', but + // that's the only distinction between them. Otherwise, they're + // driven entirely by global data, so the order in which events are + // dispatched to them shouldn't matter. + let dbg = new Debugger(g); + dbgs.push(dbg); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + assertEq(f.onStack, true); + frames.push(f); + }; + + // First expect the 'eval'... + dbg.onEnterFrame = function handleEnterEval(f) { + log += 'e'; + assertEq(f.type, 'eval'); + assertEq(f.onStack, true); + frames.push(f); + + // Then expect the call. + dbg.onEnterFrame = function handleEnterCall(f) { + log += '('; + assertEq(f.type, 'call'); + assertEq(f.onStack, true); + frames.push(f); + + // Don't expect any further frames. + dbg.onEnterFrame = function handleExtraEnter(f) { + log += 'z'; + }; + + f.onPop = function handlePop(c) { + log += ')'; + assertEq(this.onStack, true); + assertEq(completionsEqual(c, { return: '1' }), true); + frames.push(this); + + // Check that this debugger is in the list, and then remove it. + var i = dbgs.indexOf(dbg); + assertEq(i != -1, true); + dbgs.splice(i,1); + }; + }; + }; + } + + log = ''; + assertEq(completionsEqual(frame.eval('f()'), { return: '1' }), true); + assertEq(log, "eeeeeeeee(((((((((ddddddddd)))))))))"); + + dbg0.log += '.'; +}; + +dbg0.log = ''; +g.eval('eval'); +assertEq(dbg0.log, '(.'); + +// Check that all Debugger.Frame instances we ran into are now marked as dead. +for (var i = 0; i < frames.length; i++) + assertEq(frames[i].onStack, false); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-multiple-02.js b/js/src/jit-test/tests/debug/Frame-onPop-multiple-02.js new file mode 100644 index 0000000000..6ab1283c9f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-multiple-02.js @@ -0,0 +1,36 @@ +// One Debugger's onPop handler can remove another Debugger's onPop handler. +var g = newGlobal({newCompartment: true}); +var dbg1 = new Debugger(g); +var dbg2 = new Debugger(g); + +var log; +var frames = []; +var firstPop = true; + +function handleEnter(frame) { + log += '('; + frames.push(frame); + frame.onPop = function handlePop(completion) { + log += ')'; + assertEq(completion.return, 42); + if (firstPop) { + // We can't say which frame's onPop handler will get called first. + if (this == frames[0]) + frames[1].onPop = undefined; + else + frames[0].onPop = undefined; + gc(); + } else { + assertEq("second pop handler was called", + "second pop handler should not be called"); + } + firstPop = false; + }; +}; + +dbg1.onEnterFrame = handleEnter; +dbg2.onEnterFrame = handleEnter; + +log = ''; +assertEq(g.eval('40 + 2'), 42); +assertEq(log, '(()'); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-multiple-04.js b/js/src/jit-test/tests/debug/Frame-onPop-multiple-04.js new file mode 100644 index 0000000000..451359ce7e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-multiple-04.js @@ -0,0 +1,27 @@ +// If one Debugger's onPop handler causes another Debugger to create a +// Debugger.Frame instance referring to the same frame, that frame still +// gets marked as not live after all the onPop handlers have run. +var g = newGlobal({newCompartment: true}); +var dbg1 = new Debugger(g); +var dbg2 = new Debugger(g); + +var log; +var frame2; + +dbg1.onEnterFrame = function handleEnter(frame) { + log += '('; + frame.onPop = function handlerPop1(c) { + log += ')'; + frame2 = dbg2.getNewestFrame(); + assertEq(frame2.onStack, true); + frame2.onPop = function handlePop2(c) { + assertEq("late frame's onPop handler ran", + "late frame's onPop handler should not run"); + }; + }; +}; + +log = ''; +assertEq(g.eval('40 + 2'), 42); +assertEq(log, '()'); +assertEq(frame2.onStack, false); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-return-error.js b/js/src/jit-test/tests/debug/Frame-onPop-return-error.js new file mode 100644 index 0000000000..d01134f63e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-return-error.js @@ -0,0 +1,59 @@ +// |jit-test| error: TestComplete +// onPop can change a normal return into a termination. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// We use Debugger.Frame.prototype.eval and ignore the outer 'eval' frame so we +// can catch the termination. + +function test(type, provocation) { + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + var log; + dbg.onEnterFrame = function handleFirstFrame(f) { + log += 'f'; + dbg.onDebuggerStatement = function handleDebugger(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleSecondFrame(f) { + log += 'e'; + assertEq(f.type, 'eval'); + + dbg.onEnterFrame = function handleThirdFrame(f) { + log += '('; + assertEq(f.type, type); + + dbg.onEnterFrame = function handleExtraFrames(f) { + // This should never be called. + assertEq(false, true); + }; + + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.return, 'compliment'); + return null; + }; + }; + }; + + assertEq(f.eval(provocation), null); + }; + + log = ''; + // This causes handleFirstFrame to be called. + assertEq(typeof g.eval('eval'), 'function'); + assertEq(log, 'fe(d)'); + + print(); +} + +g.eval('function f() { debugger; return \'compliment\'; }'); +test('call', 'f();'); +test('call', 'new f;'); +test('eval', 'eval(\'debugger; \\\'compliment\\\';\');'); +test('global', 'evaluate(\'debugger; \\\'compliment\\\';\');'); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-return-return.js b/js/src/jit-test/tests/debug/Frame-onPop-return-return.js new file mode 100644 index 0000000000..f8c8240b88 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-return-return.js @@ -0,0 +1,46 @@ +// |jit-test| error: TestComplete +// onPop can change a normal return into a normal return of a different value. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + var wasConstructing; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + wasConstructing = f.constructing; + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.return, 'compliment'); + return { return: 'favor' }; + }; + }; + + log = ''; + var result = provocation(); + if (wasConstructing) + assertEq(typeof result, "object"); + else + assertEq(result, 'favor'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; return 'compliment'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; \'compliment\';"); }); +test("global", function () { return g.evaluate("debugger; \'compliment\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-return-throw.js b/js/src/jit-test/tests/debug/Frame-onPop-return-throw.js new file mode 100644 index 0000000000..369f316119 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-return-throw.js @@ -0,0 +1,41 @@ +// |jit-test| error: TestComplete +// onPop can change a normal return into a throw. + +load(libdir + "asserts.js"); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.return, 'compliment'); + return { throw: 'snow' }; + }; + }; + + log = ''; + assertThrowsValue(provocation, 'snow'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; return 'compliment'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; \'compliment\';"); }); +test("global", function () { return g.evaluate("debugger; \'compliment\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-return.js b/js/src/jit-test/tests/debug/Frame-onPop-return.js new file mode 100644 index 0000000000..e1288d8d3a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-return.js @@ -0,0 +1,45 @@ +// |jit-test| error: TestComplete +// onPop fires when frames return normally. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + var wasConstructing; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + wasConstructing = f.constructing; + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.return, 'compliment'); + }; + }; + + log = ''; + var result = provocation(); + if (wasConstructing) + assertEq(typeof result, "object"); + else + assertEq(result, 'compliment'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; return 'compliment'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; \'compliment\';"); }); +test("global", function () { return g.evaluate("debugger; \'compliment\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-throw-error.js b/js/src/jit-test/tests/debug/Frame-onPop-throw-error.js new file mode 100644 index 0000000000..83dee69686 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-throw-error.js @@ -0,0 +1,59 @@ +// |jit-test| error: TestComplete +// onPop can change a throw into a termination. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// We use Debugger.Frame.prototype.eval and ignore the outer 'eval' frame so we +// can catch the termination. + +function test(type, provocation) { + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + var log; + dbg.onEnterFrame = function handleFirstFrame(f) { + log += 'f'; + dbg.onDebuggerStatement = function handleDebugger(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleSecondFrame(f) { + log += 'e'; + assertEq(f.type, 'eval'); + + dbg.onEnterFrame = function handleThirdFrame(f) { + log += '('; + assertEq(f.type, type); + + dbg.onEnterFrame = function handleExtraFrames(f) { + // This should never be called. + assertEq(false, true); + }; + + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.throw, 'mud'); + return null; + }; + }; + }; + + assertEq(f.eval(provocation), null); + }; + + log = ''; + // This causes handleFirstFrame to be called. + assertEq(typeof g.eval('eval'), 'function'); + assertEq(log, 'fe(d)'); + + print(); +} + +g.eval('function f() { debugger; throw \'mud\'; }'); +test('call', 'f();'); +test('call', 'new f;'); +test('eval', 'eval(\'debugger; throw \\\'mud\\\';\');'); +test('global', 'evaluate(\'debugger; throw \\\'mud\\\';\');'); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-throw-return.js b/js/src/jit-test/tests/debug/Frame-onPop-throw-return.js new file mode 100644 index 0000000000..a7c323d5f2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-throw-return.js @@ -0,0 +1,46 @@ +// |jit-test| error: TestComplete +// onPop can change a throw into a normal return. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + var wasConstructing; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + wasConstructing = f.constructing; + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.throw, 'mud'); + return { return: 'favor' }; + }; + }; + + log = ''; + var result = provocation(); + if (wasConstructing) + assertEq(typeof result, "object"); + else + assertEq(result, 'favor'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; throw 'mud'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; throw \'mud\';"); }); +test("global", function () { return g.evaluate("debugger; throw \'mud\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-throw-throw.js b/js/src/jit-test/tests/debug/Frame-onPop-throw-throw.js new file mode 100644 index 0000000000..5562b1f44c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-throw-throw.js @@ -0,0 +1,41 @@ +// |jit-test| error: TestComplete +// onPop can change a throw into a throw of a different value. + +load(libdir + "asserts.js"); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.throw, 'mud'); + return { throw: 'snow' }; + }; + }; + + log = ''; + assertThrowsValue(provocation, 'snow'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; throw 'mud'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; throw \'mud\';"); }); +test("global", function () { return g.evaluate("debugger; throw \'mud\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onPop-throw.js b/js/src/jit-test/tests/debug/Frame-onPop-throw.js new file mode 100644 index 0000000000..8e481a8736 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-throw.js @@ -0,0 +1,40 @@ +// |jit-test| error: TestComplete +// onPop fires when frames throw an exception. + +load(libdir + "asserts.js"); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +function test(type, provocation) { + var log; + + // Help people figure out which 'test' call failed. + print("type: " + JSON.stringify(type)); + print("provocation: " + JSON.stringify(provocation)); + + dbg.onDebuggerStatement = function handleDebuggerStatement(f) { + log += 'd'; + }; + + dbg.onEnterFrame = function handleEnterFrame(f) { + log += '('; + assertEq(f.type, type); + f.onPop = function handlePop(c) { + log += ')'; + assertEq(c.throw, 'mud'); + }; + }; + + log = ''; + assertThrowsValue(provocation, 'mud'); + assertEq(log, "(d)"); + + print(); +} + +g.eval("function f() { debugger; throw 'mud'; }"); +test("call", g.f); +test("call", function () { return new g.f; }); +test("eval", function () { return g.eval("debugger; throw \'mud\';"); }); +test("global", function () { return g.evaluate("debugger; throw \'mud\';"); }); +throw 'TestComplete'; diff --git a/js/src/jit-test/tests/debug/Frame-onStack-01.js b/js/src/jit-test/tests/debug/Frame-onStack-01.js new file mode 100644 index 0000000000..3e69a0a8b3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStack-01.js @@ -0,0 +1,41 @@ +// Debugger.Frame.prototype.onStack is true for frames on the stack and false for +// frames that have returned + +var desc = Object.getOwnPropertyDescriptor(Debugger.Frame.prototype, "live"); +assertEq(typeof desc.get, "function"); +assertEq(desc.set, undefined); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, false); + +var loc; + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("var hits = 0;"); +g.eval("(" + function () { + var a = []; + var dbg = Debugger(debuggeeGlobal); + dbg.onDebuggerStatement = function (frame) { + var loc = debuggeeGlobal.loc; + a[loc] = frame; + for (var i = 0; i < a.length; i++) { + assertEq(a[i] === frame, i === loc); + assertEq(!!(a[i] && a[i].onStack), i >= loc); + } + hits++; + }; + } + ")()"); + +function f(n) { + loc = n; debugger; + if (n !== 0) { + f(n - 1); + loc = n; debugger; + eval("f(n - 1);"); + loc = n; debugger; + } +} + +f(4); +assertEq(g.hits, 16 + 8*3 + 4*3 + 2*3 + 1*3); + diff --git a/js/src/jit-test/tests/debug/Frame-onStack-02.js b/js/src/jit-test/tests/debug/Frame-onStack-02.js new file mode 100644 index 0000000000..be21f1e18a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStack-02.js @@ -0,0 +1,32 @@ +// Debugger.Frame.prototype.onStack is false for frames that have thrown or been thrown through + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("var finalCheck;"); +g.eval("(" + function () { + var a = []; + var dbg = Debugger(debuggeeGlobal); + dbg.onDebuggerStatement = function (frame) { + a.push(frame); + for (var i = 0; i < a.length; i++) + assertEq(a[i].onStack, true); + }; + finalCheck = function (n) { + assertEq(a.length, n); + for (var i = 0; i < n; i++) + assertEq(a[i].onStack, false); + }; + } + ")()"); + +function f(n) { + debugger; + if (--n > 0) + f(n); + else + throw "fit"; +} + +assertThrowsValue(function () { f(10); }, "fit"); +g.finalCheck(10); diff --git a/js/src/jit-test/tests/debug/Frame-onStack-03.js b/js/src/jit-test/tests/debug/Frame-onStack-03.js new file mode 100644 index 0000000000..af872782d5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStack-03.js @@ -0,0 +1,25 @@ +// frame properties throw if !frame.onStack + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var f; +Debugger(g).onDebuggerStatement = function (frame) { + assertEq(frame.onStack, true); + assertEq(frame.type, "call"); + assertEq(frame.this instanceof Object, true); + assertEq(frame.older instanceof Debugger.Frame, true); + assertEq(frame.callee instanceof Debugger.Object, true); + assertEq(frame.constructing, false); + assertEq(frame.arguments.length, 0); + f = frame; +}; + +g.eval("(function () { debugger; }).call({});"); +assertEq(f.onStack, false); +assertThrowsInstanceOf(function () { f.type; }, Error); +assertThrowsInstanceOf(function () { f.this; }, Error); +assertThrowsInstanceOf(function () { f.older; }, Error); +assertThrowsInstanceOf(function () { f.callee; }, Error); +assertThrowsInstanceOf(function () { f.constructing; }, Error); +assertThrowsInstanceOf(function () { f.arguments; }, Error); diff --git a/js/src/jit-test/tests/debug/Frame-onStack-04.js b/js/src/jit-test/tests/debug/Frame-onStack-04.js new file mode 100644 index 0000000000..cc9a132091 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStack-04.js @@ -0,0 +1,31 @@ +// frame.onStack is false for frames discarded during uncatchable error unwinding. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +var snapshot; +dbg.onDebuggerStatement = function (frame) { + var stack = []; + for (var f = frame; f; f = f.older) { + if (f.type === "call" && f.script !== null) + stack.push(f); + } + snapshot = stack; + if (hits++ === 0) + assertEq(frame.eval("x();"), null); + else + return null; +}; + +g.eval("function z() { debugger; }"); +g.eval("function y() { z(); }"); +g.eval("function x() { y(); }"); +assertEq(g.eval("debugger; 'ok';"), "ok"); +assertEq(hits, 2); +assertEq(snapshot.length, 3); +for (var i = 0; i < snapshot.length; i++) { + assertEq(snapshot[i].onStack, false); + assertThrowsInstanceOf(() => frame.script, Error); +} diff --git a/js/src/jit-test/tests/debug/Frame-onStack-05.js b/js/src/jit-test/tests/debug/Frame-onStack-05.js new file mode 100644 index 0000000000..7e8ce8c68d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStack-05.js @@ -0,0 +1,33 @@ +// frame.onStack is false for frames removed after their compartments stopped being debuggees. + +load(libdir + 'asserts.js'); + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = Debugger(g1, g2); +var hits = 0; +var snapshot = []; +dbg.onDebuggerStatement = function (frame) { + if (hits++ === 0) { + assertEq(frame.eval("x();"), null); + } else { + for (var f = frame; f; f = f.older) { + if (f.type === "call" && f.script !== null) + snapshot.push(f); + } + dbg.removeDebuggee(g2); + return null; + } +}; + +g1.eval("function z() { debugger; }"); +g2.z = g1.z; +g2.eval("function y() { z(); }"); +g2.eval("function x() { y(); }"); +assertEq(g2.eval("debugger; 'ok';"), "ok"); +assertEq(hits, 2); +assertEq(snapshot.length, 3); +for (var i = 0; i < snapshot.length; i++) { + assertEq(snapshot[i].onStack, false); + assertThrowsInstanceOf(() => frame.script, Error); +} diff --git a/js/src/jit-test/tests/debug/Frame-onStack-06.js b/js/src/jit-test/tests/debug/Frame-onStack-06.js new file mode 100644 index 0000000000..8316da9b9e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStack-06.js @@ -0,0 +1,26 @@ +// frame.onStack is false for generator frames after they return. + +let g = newGlobal({newCompartment: true}); +g.eval("function* f() { debugger; }"); + +let dbg = Debugger(g); +let savedFrame; + +dbg.onDebuggerStatement = frame => { + savedFrame = frame; + assertEq(frame.callee.name, "f"); + assertEq(frame.onStack, true); + frame.onPop = function() { + assertEq(frame.onStack, true); + }; +}; +g.f().next(); + +assertEq(savedFrame.onStack, false); +try { + savedFrame.older; + throw new Error("expected exception, none thrown"); +} catch (exc) { + assertEq(exc.message, "Debugger.Frame is not on stack or suspended"); +} + diff --git a/js/src/jit-test/tests/debug/Frame-onStack-07.js b/js/src/jit-test/tests/debug/Frame-onStack-07.js new file mode 100644 index 0000000000..055a7d41b2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStack-07.js @@ -0,0 +1,56 @@ +// frame.onStack is false for generator frames popped due to exception or termination. + +load(libdir + "/asserts.js"); + +function test(when, what) { + let g = newGlobal({newCompartment: true}); + g.eval("function* f(x) { yield x; }"); + + let dbg = new Debugger; + let gw = dbg.addDebuggee(g); + let fw = gw.getOwnPropertyDescriptor("f").value; + + let t = 0; + let poppedFrame; + + function tick(frame) { + if (frame.callee == fw) { + if (t == when) { + poppedFrame = frame; + dbg.onEnterFrame = undefined; + frame.onPop = undefined; + return what; + } + t++; + } + return undefined; + } + + dbg.onDebuggerStatement = frame => { + dbg.onEnterFrame = frame => { + frame.onPop = function() { + return tick(this); + }; + return tick(frame); + }; + let result = frame.eval("for (let _ of f(0)) {}"); + if (result && "stack" in result) { + result.stack = true; + } + assertDeepEq(result, what); + }; + g.eval("debugger;"); + + assertEq(t, when); + assertEq(poppedFrame.onStack, false); + assertErrorMessage(() => poppedFrame.older, + Error, + "Debugger.Frame is not on stack or suspended"); +} + +for (let when = 0; when < 6; when++) { + for (let what of [null, {throw: "fit", stack: true}]) { + test(when, what); + } +} + diff --git a/js/src/jit-test/tests/debug/Frame-onStep-01.js b/js/src/jit-test/tests/debug/Frame-onStep-01.js new file mode 100644 index 0000000000..dcab3c6add --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-01.js @@ -0,0 +1,24 @@ +// Simple Debugger.Frame.prototype.onStep test. +// Test that onStep fires often enough to see all four values of a. + +var g = newGlobal({newCompartment: true}); +g.a = 0; +g.eval("function f() {\n" + + " a += 2;\n" + + " a += 2;\n" + + " a += 2;\n" + + " return a;\n" + + "}\n"); + +var dbg = Debugger(g); +var seen = [0, 0, 0, 0, 0, 0, 0]; +dbg.onEnterFrame = function (frame) { + frame.onStep = function () { + assertEq(arguments.length, 0); + assertEq(this, frame); + seen[g.a] = 1; + }; +} + +g.f(); +assertEq(seen.join(""), "1010101"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-02.js b/js/src/jit-test/tests/debug/Frame-onStep-02.js new file mode 100644 index 0000000000..ed30f041a0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-02.js @@ -0,0 +1,27 @@ +// Setting frame.onStep to undefined turns off single-stepping. + +var g = newGlobal({newCompartment: true}); +g.a = 0; +g.eval("function f() {\n" + + " a++;\n" + + " a++;\n" + + " a++;\n" + + " a++;\n" + + " return a;\n" + + "}\n"); + +var dbg = Debugger(g); +var seen = [0, 0, 0, 0, 0]; +dbg.onEnterFrame = function (frame) { + seen[g.a] = 1; + frame.onStep = function () { + seen[g.a] = 1; + if (g.a === 2) { + frame.onStep = undefined; + assertEq(frame.onStep, undefined); + } + }; +} + +g.f(); +assertEq(seen.join(","), "1,1,1,0,0"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-03.js b/js/src/jit-test/tests/debug/Frame-onStep-03.js new file mode 100644 index 0000000000..0451604abd --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-03.js @@ -0,0 +1,28 @@ +// Setting onStep does not affect later calls to the same function. +// (onStep is per-frame, not per-function.) + +var g = newGlobal({newCompartment: true}); +g.a = 1; +g.eval("function f(a) {\n" + + " var x = 2 * a;\n" + + " return x * x;\n" + + "}\n"); + +var dbg = Debugger(g); +var log = ''; +dbg.onEnterFrame = function (frame) { + log += '+'; + frame.onStep = function () { + if (log.charAt(log.length - 1) != 's') + log += 's'; + }; +}; + +g.f(1); +log += '|'; +g.f(2); +log += '|'; +dbg.onEnterFrame = undefined; +g.f(3); + +assertEq(log, '+s|+s|'); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-04.js b/js/src/jit-test/tests/debug/Frame-onStep-04.js new file mode 100644 index 0000000000..fb2d6f49e4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-04.js @@ -0,0 +1,34 @@ +// When a recursive function has many frames on the stack, onStep may be set or +// not independently on each frame. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(x) {\n" + + " if (x > 0)\n" + + " f(x - 1);\n" + + " else\n" + + " debugger;\n" + + " return x;\n" + + "}"); + +var dbg = Debugger(g); +var seen = [0, 0, 0, 0, 0, 0, 0, 0]; +function step() { + seen[this.arguments[0]] = 1; +} +dbg.onEnterFrame = function (frame) { + // Turn on stepping for even-numbered frames. + var x = frame.arguments[0]; + if (x % 2 === 0) + frame.onStep = step; +}; +dbg.onDebuggerStatement = function (frame) { + // This is called with 8 call frames on the stack, 7 down to 0. + // At this point we should have seen all the even-numbered frames. + assertEq(seen.join(""), "10101010"); + + // Now reset seen to see which frames fire onStep on the way out. + seen = [0, 0, 0, 0, 0, 0, 0, 0]; +}; + +g.f(7); +assertEq(seen.join(""), "10101010"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-05.js b/js/src/jit-test/tests/debug/Frame-onStep-05.js new file mode 100644 index 0000000000..c5e5b146f1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-05.js @@ -0,0 +1,14 @@ +// Upon returning to a frame with an onStep hook, the hook is called before the +// next line. + +var g = newGlobal({newCompartment: true}); +g.log = ''; +g.eval("function f() { debugger; }"); + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.older.onStep = function () { g.log += 's'; }; +}; +g.eval("f();\n" + + "log += 'x';\n"); +assertEq(g.log.charAt(0), 's'); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-06.js b/js/src/jit-test/tests/debug/Frame-onStep-06.js new file mode 100644 index 0000000000..ba5d7ea773 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-06.js @@ -0,0 +1,58 @@ +// After returning from an implicit toString call, the calling frame's onStep +// hook fires. + +var g = newGlobal({newCompartment: true}); +g.eval("var originalX = {toString: function () { debugger; log += 'x'; return 1; }};\n"); + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + g.log += 'd'; + frame.older.onStep = function () { + if (!g.log.match(/[sy]$/)) + g.log += 's'; + }; +}; + +// expr is an expression that will trigger an implicit toString call. +function check(expr) { + g.log = ''; + g.x = g.originalX; + g.eval(expr + ";\n" + + "log += 'y';\n"); + assertEq(g.log, 'dxsy'); +} + +check("'' + x"); +check("0 + x"); +check("0 - x"); +check("0 * x"); +check("0 / x"); +check("0 % x"); +check("+x"); +check("x in {}"); +check("x++"); +check("++x"); +check("x--"); +check("--x"); +check("x < 0"); +check("x > 0"); +check("x >= 0"); +check("x <= 0"); +check("x == 0"); +check("x != 0"); +check("x & 1"); +check("x | 1"); +check("x ^ 1"); +check("~x"); +check("x << 1"); +check("x >> 1"); +check("x >>> 1"); + +g.eval("var getter = { get x() { debugger; return log += 'x'; } }"); +check("getter.x"); + +g.eval("var setter = { set x(v) { debugger; return log += 'x'; } }"); +check("setter.x = 1"); + +g.eval("Object.defineProperty(this, 'thisgetter', { get: function() { debugger; log += 'x'; }});"); +check("thisgetter"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-07.js b/js/src/jit-test/tests/debug/Frame-onStep-07.js new file mode 100644 index 0000000000..de8ce4e7c0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-07.js @@ -0,0 +1,23 @@ +// The tracejit does not interfere with frame.onStep. +// +// The function f() writes 'L' to the log in a loop. If we enable stepping and +// write an 's' each time frame.onStep is called, any two Ls should have at +// least one 's' between them. + +var g = newGlobal({newCompartment: true}); +g.N = 11; +g.log = ''; +g.eval("function f() {\n" + + " for (var i = 0; i <= N; i++)\n" + + " log += 'L';\n" + + "}\n"); +g.f(); +assertEq(/LL/.exec(g.log) !== null, true); + +var dbg = Debugger(g); +dbg.onEnterFrame = function (frame) { + frame.onStep = function () { g.log += 's'; }; +}; +g.log = ''; +g.f(); +assertEq(/LL/.exec(g.log), null); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-08.js b/js/src/jit-test/tests/debug/Frame-onStep-08.js new file mode 100644 index 0000000000..857c94a50e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-08.js @@ -0,0 +1,29 @@ +// frame.onStep can coexist with breakpoints. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onEnterFrame = function (frame) { + var handler = {hit: function () { log += 'B'; }}; + var lines = frame.script.getAllOffsets(); + for (var line in lines) { + line = Number(line); + var offs = lines[line]; + for (var i = 0; i < offs.length; i++) + frame.script.setBreakpoint(offs[i], handler); + } + + frame.onStep = function () { log += 's'; }; +}; + +g.eval("one = 1;\n" + + "two = 2;\n" + + "three = 3;\n" + + "four = 4;\n"); +assertEq(g.four, 4); + +// Breakpoints hit on all four lines, plus the final line. +assertEq(log.replace(/[^B]/g, ''), 'BBBBB'); + +// onStep was called between each pair of breakpoints. +assertEq(/BB/.exec(log), null); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-09.js b/js/src/jit-test/tests/debug/Frame-onStep-09.js new file mode 100644 index 0000000000..1204ac21a9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-09.js @@ -0,0 +1,24 @@ +// After an implicit toString call throws an exception, the calling frame's +// onStep hook fires. + +var g = newGlobal({newCompartment: true}); +g.eval("var x = {toString: function () { debugger; log += 'x'; throw 'mud'; }};"); + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + g.log += 'd'; + frame.older.onStep = function () { + if (!g.log.match(/[sy]$/)) + g.log += 's'; + }; +}; + +g.log = ''; +g.eval("try { x + ''; } catch (x) { }\n" + + "log += 'y';\n"); +assertEq(g.log, "dxsy"); + +g.log = ''; +g.eval("try { '' + x; } catch (x) { }\n" + + "log += 'y';\n"); +assertEq(g.log, "dxsy"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-10.js b/js/src/jit-test/tests/debug/Frame-onStep-10.js new file mode 100644 index 0000000000..758f64f1a3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-10.js @@ -0,0 +1,28 @@ +// Throwing and catching an error in an onStep handler shouldn't interfere +// with throwing and catching in the debuggee. + +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; throw 'mud'; }"); + +var dbg = Debugger(g); +var stepped = false; +dbg.onDebuggerStatement = function (frame) { + frame.older.onStep = function () { + stepped = true; + try { + throw 'snow'; + } catch (x) { + assertEq(x, 'snow'); + } + }; +}; + +stepped = false; +g.eval("var caught;\n" + + "try {\n" + + " f();\n" + + "} catch (x) {\n" + + " caught = x;\n" + + "}\n"); +assertEq(stepped, true); +assertEq(g.caught, 'mud'); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-11.js b/js/src/jit-test/tests/debug/Frame-onStep-11.js new file mode 100644 index 0000000000..15dd847379 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-11.js @@ -0,0 +1,36 @@ +// Stepping out of a finally should not appear to +// step backward to some earlier statement. + +var g = newGlobal({newCompartment: true}); +g.eval(`function f() { + debugger; // +0 + var x = 0; // +1 + try { // +2 + x = 1; // +3 + throw 'something'; // +4 + } catch (e) { // +5 + x = 2; // +6 + } finally { // +7 + x = 3; // +8 + } // +9 + x = 4; // +10 + }`); // +11 + +var dbg = Debugger(g); + +let foundLines = ''; + +dbg.onDebuggerStatement = function(frame) { + let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber; + frame.onStep = function() { + // Only record a stop when the offset is an entry point. + let foundLine = this.script.getOffsetLocation(this.offset).lineNumber; + if (foundLine != debugLine && this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) { + foundLines += "," + (foundLine - debugLine); + } + }; +}; + +g.f(); + +assertEq(foundLines, ",1,2,3,4,5,6,7,8,10,11"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-12.js b/js/src/jit-test/tests/debug/Frame-onStep-12.js new file mode 100644 index 0000000000..c751c278a5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-12.js @@ -0,0 +1,118 @@ +// Check that stepping doesn't make it look like unreachable code is running. + +// Because our script source notes record only those bytecode offsets +// at which source positions change, the default behavior in the +// absence of a source note is to attribute a bytecode instruction to +// the same source location as the preceding instruction. When control +// flows from the preceding bytecode to the one we're emitting, that's +// usually plausible. But successors in the bytecode stream are not +// necessarily successors in the control flow graph. If the preceding +// bytecode was a back edge of a loop, or the jump at the end of a +// 'then' clause, its source position can be completely unrelated to +// that of its successor. +// +// We try to avoid showing such nonsense offsets to the user by +// requiring breakpoints and single-stepping to stop only at a line's +// entry points, as reported by Debugger.Script.prototype.getLineOffsets; +// and then ensuring that those entry points are all offsets mentioned +// explicitly in the source notes, and hence deliberately attributed +// to the given bytecode. +// +// This bit of JavaScript compiles to bytecode ending in a branch +// instruction whose source position is the body of an unreachable +// loop. The first instruction of the bytecode we emit following it +// will inherit this nonsense position, if we have not explicitly +// emitted a source note for said instruction. +// +// This test steps across such code and verifies that control never +// appears to enter the unreachable loop. + +var bitOfCode = `debugger; // +0 + if(false) { // +1 + for(var b=0; b<0; b++) { // +2 + c = 2 // +3 + } // +4 + }`; // +5 + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +g.eval("function nothing() { }\n"); + +var log = ''; +dbg.onDebuggerStatement = function(frame) { + let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber; + frame.onStep = function() { + let foundLine = this.script.getOffsetLocation(this.offset).lineNumber; + if (this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) { + log += (foundLine - debugLine).toString(16); + } + }; +}; + +function testOne(name, body, expected) { + print(name); + log = ''; + g.eval(`function ${name} () { ${body} }`); + g.eval(`${name}();`); + assertEq(log, expected); +} + + + +// Test the instructions at the end of a "try". +testOne("testTryFinally", + `try { + ${bitOfCode} + } finally { // +6 + } // +7 + nothing(); // +8 + `, "1689"); + +// The same but without a finally clause. +testOne("testTryCatch", + `try { + ${bitOfCode} + } catch (e) { // +6 + } // +7 + nothing(); // +8 + `, "189"); + +// Test the instructions at the end of a "catch". +testOne("testCatchFinally", + `try { + throw new TypeError(); + } catch (e) { + ${bitOfCode} + } finally { // +6 + } // +7 + nothing(); // +8 + `, "1689"); + +// Test the instruction at the end of a "finally" clause. +testOne("testFinally", + `try { + } finally { + ${bitOfCode} + } // +6 + nothing(); // +7 + `, "178"); + +// Test the instruction at the end of a "then" clause. +testOne("testThen", + `if (1 === 1) { + ${bitOfCode} + } else { // +6 + } // +7 + nothing(); // +8 + `, "189"); + +// Test the instructions leaving a switch block. +testOne("testSwitch", + `var x = 5; + switch (x) { + case 5: + ${bitOfCode} + } // +6 + nothing(); // +7 + `, "178"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-13.js b/js/src/jit-test/tests/debug/Frame-onStep-13.js new file mode 100644 index 0000000000..b2d4a3c1b1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-13.js @@ -0,0 +1,29 @@ +// Stepping over a not-taken "if" that is at the end of the function +// should move to the end of the function, not somewhere in the body +// of the "if". + +var g = newGlobal({newCompartment: true}); +g.eval(`function f() { // 1 + var a,c; // 2 + debugger; // 3 + if(false) { // 4 + for(var b=0; b<0; b++) { // 5 + c = 2; // 6 + } // 7 + } // 8 +} // 9 +`); + +var dbg = Debugger(g); +var badStep = false; + +dbg.onDebuggerStatement = function(frame) { + let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber; + assertEq(debugLine, 3); + frame.onStep = function() { + let foundLine = this.script.getOffsetLocation(this.offset).lineNumber; + assertEq(foundLine <= 4 || foundLine >= 8, true); + }; +}; + +g.eval("f();\n"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-14.js b/js/src/jit-test/tests/debug/Frame-onStep-14.js new file mode 100644 index 0000000000..a7139adb09 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-14.js @@ -0,0 +1,46 @@ +// Test how stepping interacts with switch statements. + +var g = newGlobal({newCompartment: true}); + +g.eval('function bob() { return "bob"; }'); + +// Stepping into a sparse switch should not stop on literal cases. +evaluate(`function lit(x) { // 1 + debugger; // 2 + switch(x) { // 3 + case "nope": // 4 + break; // 5 + case "bob": // 6 + break; // 7 + } // 8 +}`, {lineNumber: 1, global: g}); + +// Stepping into a sparse switch should stop on non-literal cases. +evaluate(`function nonlit(x) { // 1 + debugger; // 2 + switch(x) { // 3 + case bob(): // 4 + break; // 5 + } // 6 +}`, {lineNumber: 1, global: g}); + +var dbg = Debugger(g); +var badStep = false; + +function test(s, okLine) { + dbg.onDebuggerStatement = function(frame) { + frame.onStep = function() { + let thisLine = this.script.getOffsetLocation(this.offset).lineNumber; + // The stop at line 3 is the switch. + if (thisLine > 3) { + assertEq(thisLine, okLine) + frame.onStep = undefined; + } + }; + }; + g.eval(s); +} + +test("lit('bob');", 7); + +test("nonlit('bob');", 4); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-15.js b/js/src/jit-test/tests/debug/Frame-onStep-15.js new file mode 100644 index 0000000000..819b0e4b6c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-15.js @@ -0,0 +1,43 @@ +// Test how stepping interacts with for(;;) statements. + +let g = newGlobal({newCompartment: true}); + +// We want a for(;;) loop whose body is evaluated at least once, to +// see whether the loop head is hit a second time. +g.eval(`function f() { + let x = 0; + debugger; // +0 + for(;;) { // +1 + if (x++ == 1) break; // +2 + } // +3 + debugger; // +4 +}`); + +let dbg = Debugger(g); + +function test(s, expected) { + let result = ''; + + dbg.onDebuggerStatement = function(frame) { + // On the second debugger statement, we're done. + dbg.onDebuggerStatement = function(frame) { + frame.onStep = undefined; + }; + + let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber; + frame.onStep = function() { + // Only examine stops at entry points for the line. + let lineNo = this.script.getOffsetLocation(this.offset).lineNumber; + if (this.script.getLineOffsets(lineNo).indexOf(this.offset) < 0) { + return undefined; + } + + let delta = this.script.getOffsetLocation(this.offset).lineNumber - debugLine; + result += delta; + }; + }; + g.eval(s); + assertEq(result, expected); +} + +test('f()', '2124'); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-16.js b/js/src/jit-test/tests/debug/Frame-onStep-16.js new file mode 100644 index 0000000000..2acf76951d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-16.js @@ -0,0 +1,34 @@ +// Stepping through a function with a return statement should pause on +// the closing brace of the function. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log; + +function test(fnStr) { + log = ''; + g.eval(fnStr); + + dbg.onDebuggerStatement = function(frame) { + frame.onStep = function() { + let {lineNumber, isEntryPoint} = frame.script.getOffsetLocation(frame.offset); + if (isEntryPoint) { + log += lineNumber + ' '; + } + }; + }; + + g.eval("f(23);"); +} + +test("function f(x) {\n" + // 1 + " debugger;\n" + // 2 + " return 23 + x;\n" + // 3 + "}\n"); // 4 +assertEq(log, '3 4 '); + +test("function f(x) {\n" + // 1 + " debugger;\n" + // 2 + " return;\n" + // 3 + "}\n"); // 4 +assertEq(log, '3 4 '); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-17.js b/js/src/jit-test/tests/debug/Frame-onStep-17.js new file mode 100644 index 0000000000..a73bd2187c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-17.js @@ -0,0 +1,35 @@ +// Test how stepping interacts with for-in/of statements. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var log; +var previous; + +dbg.onDebuggerStatement = function (frame) { + let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber; + log = ''; + previous = ''; + frame.onStep = function() { + let foundLine = this.script.getOffsetLocation(this.offset).lineNumber; + if (this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) { + let thisline = (foundLine - debugLine).toString(16); + if (thisline !== previous) { + log += thisline; + previous = thisline; + } + } + }; +}; + +function testOne(decl, loopKind) { + let body = "var array = [2, 4, 6];\ndebugger;\nfor (" + decl + " iter " + + loopKind + " array) {\n print(iter);\n}\n"; + g.eval(body); + assertEq(log, "12121214"); +} + +for (let decl of ["", "var", "let"]) { + testOne(decl, "in"); + testOne(decl, "of"); +} diff --git a/js/src/jit-test/tests/debug/Frame-onStep-18.js b/js/src/jit-test/tests/debug/Frame-onStep-18.js new file mode 100644 index 0000000000..0484a45cf7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-18.js @@ -0,0 +1,22 @@ +// Regression test for bug 1370648. + +let g = newGlobal({newCompartment: true}); + +let dbg = Debugger(g); +let lines = [0, 0, 0, 0, 0]; +dbg.onDebuggerStatement = function (frame) { + let dLine = frame.script.getOffsetLocation(frame.offset).lineNumber; + lines[0] = 1; + frame.onStep = function () { + lines[frame.script.getOffsetLocation(this.offset).lineNumber - dLine] = 1; + }; +} + +let s = ` + debugger; // 0 + if (1 !== 1) { // 1 + print("dead code!?"); // 2 + } // 3 +`; +g.eval(s); +assertEq(lines.join(""), "11001"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-19.js b/js/src/jit-test/tests/debug/Frame-onStep-19.js new file mode 100644 index 0000000000..019c026a8e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-19.js @@ -0,0 +1,41 @@ +// Stepping should ignore nested function declarations. + +// Nested functions are hoisted to the top of the function body, +// so technically the first thing that happens when you call the outer function +// is that each inner function is created and bound to a local variable. +// But users don't actually want to see that happen when they're stepping. +// It's super confusing. + +load(libdir + "stepping.js"); + +testStepping( + `\ + (function() { // line 1 + let x = 1; // line 2 + funcb("funcb"); // line 3 + function funcb(msg) { // line 4 + console.log(msg) + } + }) // line 7 + `, + [1, 2, 3, 7]); + +// Stopping at the ClassDeclaration on line 8 is fine. For that matter, +// stopping on line 5 wouldn't be so bad if we did it after line 3 and before +// line 8; alas, the actual order of execution is 5, 2, 3, 8... which is too +// confusing. +testStepping( + `\ + function f() { // 1 + var x = 0; // 2 + a(); // 3 + + function a() { // 5 + x += 1; // 6 + } // 7 + class Car {} // 8 + return x; // 9 + } // 10 + f + `, + [1, 2, 3, 8, 9, 10]); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-20.js b/js/src/jit-test/tests/debug/Frame-onStep-20.js new file mode 100644 index 0000000000..e070037c36 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-20.js @@ -0,0 +1,41 @@ +// Stepping should always pause in a frame between two function calls. + +let g = newGlobal({newCompartment: true}); +g.evaluate(` + class X { + constructor() { this._p = 0; } + m() { return this; } + get p() { return this._p; } + set p(value) { this._p = value; } + } + let x = new X; + + function f() { return 1; } + function inc(x) { return x + 1; } +`); + +let dbg = Debugger(g); + +// `code` is a snippet of JS that performs two JS calls. +function test(code) { + let hits = 0; + let log = ""; + dbg.onEnterFrame = frame => { + if (hits++ === 0) + frame.onStep = () => { log += "s"; }; + else + log += "E"; + }; + + g.eval(code); + assertEq(log.includes("EE"), false, "should have received onStep between onEnterFrame events"); + assertEq(log.match(/^s+Es+Es*$/) !== null, true, + "should get two calls, with steps before, between, and possibly after"); +} + +test("f(); f();"); +test("f() + f()"); +test("inc(f())"); +test("x.m().m()"); +test("new X().m()"); +test("x.p = x.p"); // getter, then setter diff --git a/js/src/jit-test/tests/debug/Frame-onStep-assign-function.js b/js/src/jit-test/tests/debug/Frame-onStep-assign-function.js new file mode 100644 index 0000000000..5125359292 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-assign-function.js @@ -0,0 +1,49 @@ +// Changing onStep while the function is dead is allowed. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +let steps = new Set(); +dbg.onDebuggerStatement = function(frame) { + // Setting 'onStep' while alive is allowed. + steps.add("debugger 1"); + assertEq(frame.onStack, true); + frame.onStep = function() { + steps.add("onstep 1"); + }; + + dbg.onDebuggerStatement = function() { + // Clear the 'onStep' while dead. + steps.add("debugger 2"); + assertEq(frame.onStack, false); + + // Clearing 'onStep' while dead is allowed. + frame.onStep = undefined; + + // Setting 'onStep' while dead is allowed. + frame.onStep = function() { + steps.add("onstep 2"); + }; + }; +}; + +g.eval( + ` + function myGen() { + debugger; + print("make sure we have a place to step to inside the frame"); + } + + const g = myGen(); + + debugger; + ` +); + +assertDeepEq(Array.from(steps), [ + "debugger 1", + "onstep 1", + "debugger 2", +]); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-assign-generator.js b/js/src/jit-test/tests/debug/Frame-onStep-assign-generator.js new file mode 100644 index 0000000000..2a733f3a83 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-assign-generator.js @@ -0,0 +1,68 @@ +// Changing onStep while the generator is suspended/dead is allowed. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +let steps = new Set(); +dbg.onDebuggerStatement = function(frame) { + // Setting 'onStep' while alive is allowed. + steps.add("debugger 1"); + assertEq(frame.onStack, true); + frame.onStep = function() { + steps.add("onstep 1"); + }; + + dbg.onDebuggerStatement = function() { + // Clear the 'onStep' while suspended. + steps.add("debugger 2"); + assertEq(frame.onStack, false); + + // Clearing 'onStep' while suspended is allowed. + frame.onStep = undefined; + + // Setting 'onStep' while suspended is allowed. + frame.onStep = function() { + steps.add("onstep 2"); + }; + + dbg.onDebuggerStatement = function() { + steps.add("debugger 3"); + assertEq(frame.onStack, false); + + // Clearing 'onStep' while dead is allowed. + frame.onStep = undefined; + + // Setting 'onStep' while dead is allowed. + frame.onStep = function() { + steps.add("onstep 3"); + }; + }; + }; +}; + +g.eval( + ` + function* myGen() { + debugger; + yield; + } + + const g = myGen(); + g.next(); + + debugger; + g.next(); + + debugger; + ` +); + +assertDeepEq(Array.from(steps), [ + "debugger 1", + "onstep 1", + "debugger 2", + "onstep 2", + "debugger 3", +]); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-async-01.js b/js/src/jit-test/tests/debug/Frame-onStep-async-01.js new file mode 100644 index 0000000000..4490524f2c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-async-01.js @@ -0,0 +1,35 @@ +// Stepping works across `await` in async functions. + +// Set up debuggee. +var g = newGlobal({newCompartment: true}); +g.log = ""; +g.eval(` // line 1 +async function aloop() { // 2 + for (let i = 0; i < 3; i++) { // 3 + await i; // 4 + log += " "; // 5 + } // 6 + log += "^"; // 7 +} +`); + +// Set up debugger. +let previousLine = -1; +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + frame.onStep = function () { + assertEq(this, frame); + let line = frame.script.getOffsetLocation(frame.offset).lineNumber; + if (previousLine != line) { + g.log += line; // We stepped to a new line. + previousLine = line; + } + }; + dbg.onEnterFrame = undefined; +}; + +// Run. +g.aloop(); +drainJobQueue(); + +assertEq(g.log, "2345 345 345 37^"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-async-02.js b/js/src/jit-test/tests/debug/Frame-onStep-async-02.js new file mode 100644 index 0000000000..33dcdf9ba0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-async-02.js @@ -0,0 +1,87 @@ +// With enough hackery, stepping in and out of async functions can be made to +// work as users expect. +// +// This test exercises the common case when we have syntactically `await +// $ASYNC_FN($ARGS)` so that the calls nest as if they were synchronous +// calls. It works, but there's a problem. +// +// onStep fires in extra places that end users would find very confusing--see +// the comment marked (!) below. As a result, Debugger API consumers must do +// some extra work to skip pausing there. This test is a proof of concept that +// shows what sort of effort is needed. It maintains a single `asyncStack` and +// skips the onStep hook if we're not running the function at top of the async +// stack. Real debuggers would have to maintain multiple async stacks. + +// Set up debuggee. +var g = newGlobal({newCompartment: true}); +g.eval(`\ +async function outer() { // line 1 + return (await inner()) + (await inner()) + "!"; // 2 +} // 3 +async function inner() { // 4 + return (await leaf()) + (await leaf()); // 5 +} // 6 +async function leaf() { // 7 + return (await Promise.resolve("m")); // 8 +} // 9 +`); + +// Set up debugger. +let previousLine = -1; +let dbg = new Debugger(g); +let log = ""; +let asyncStack = []; + +dbg.onEnterFrame = frame => { + assertEq(frame.type, "call"); + + // If we're entering this frame for the first time, push it to the async + // stack. + if (!frame.seen) { + frame.seen = true; + asyncStack.push(frame); + log += "("; + } + + frame.onStep = () => { + // When stepping, we sometimes pause at opcodes in older frames (!) + // where all that's happening is async function administrivia. + // + // For example, the first time `leaf()` yields, `inner()` and + // `outer()` are still on the stack; they haven't awaited yet because + // control has not returned from `leaf()` to them yet. So stepping will + // hop from line 8 to line 5 to line 2 as we unwind the stack, then + // resume on line 8. + // + // Anyway: skip that noise. + if (frame !== asyncStack[asyncStack.length - 1]) + return; + + let line = frame.script.getOffsetLocation(frame.offset).lineNumber; + if (previousLine != line) { + log += line; // We stepped to a new line. + previousLine = line; + } + }; + + frame.onPop = completion => { + // Popping the frame. But async function frames are popped multiple + // times: for the "initial suspend", at each await, and on return. The + // debugger offers no easy way to distinguish them (bug 1470558). + // For now there's an "await" property, but bug 1470558 may come up + // with a different solution, so don't rely on it! + if (!completion.await) { + // Returning (not awaiting or at initial suspend). + assertEq(asyncStack.pop(), frame); + log += ")"; + } + }; +}; + +// Run. +let result; +g.outer().then(v => { result = v; }); +drainJobQueue(); + +assertEq(result, "mmmm!"); +assertEq(log, "(12(45(789)5(789)56)2(45(789)5(789)56)23)"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-async-03.js b/js/src/jit-test/tests/debug/Frame-onStep-async-03.js new file mode 100644 index 0000000000..ecbd0ddf2c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-async-03.js @@ -0,0 +1,18 @@ +// Bug 1501666: assertions about the script's step mode count must take +// suspended calls into account. This should not crash. + +var g = newGlobal({ newCompartment: true }); +g.eval(` + async function f(y) { + await true; + await true; + }; +`); + +g.f(); +g.f(); + +var dbg = Debugger(g); +dbg.onEnterFrame = function(frame) { + frame.onStep = function() {} +} diff --git a/js/src/jit-test/tests/debug/Frame-onStep-async-gc-01.js b/js/src/jit-test/tests/debug/Frame-onStep-async-gc-01.js new file mode 100644 index 0000000000..6ff59d6ce8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-async-gc-01.js @@ -0,0 +1,28 @@ +// An onStep handler on a suspended async function frame keeps a Debugger alive. + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function f() { + debugger; + await Promise.resolve(0); + return 'ok'; + } +`); + +let dbg = Debugger(g); +let hit = false; +dbg.onDebuggerStatement = frame => { + frame.onPop = completion => { + frame.onStep = () => { hit = true; }; + frame.onPop = undefined; + }; + dbg.onDebuggerStatement = undefined; + dbg = null; +}; + +g.f(); +assertEq(dbg, null); +gc(); +assertEq(hit, false); +drainJobQueue(); +assertEq(hit, true); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-01.js b/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-01.js new file mode 100644 index 0000000000..ad61ccd9e4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-01.js @@ -0,0 +1,42 @@ +// The debugger can't force return from a generator before the initial yield. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* f() { + yield 1; + } +`); + +let dbg = new Debugger(g); +let steps = 0; +let uncaughtErrorsReported = 0; +dbg.onEnterFrame = frame => { + assertEq(frame.callee.name, "f"); + dbg.onEnterFrame = undefined; + frame.onStep = () => { + steps++; + + // This test case never resumes the generator after the initial + // yield. Therefore the initial yield has not happened yet. So this + // force-return will be an error. + return {return: "ponies"}; + }; + + // Having an onPop hook exercises some assertions that don't happen + // otherwise. + frame.onPop = completion => {}; +}; + +dbg.uncaughtExceptionHook = (reason) => { + // When onEnterFrame returns an invalid resumption value, + // the error is reported here. + assertEq(reason instanceof TypeError, true); + uncaughtErrorsReported++; + return undefined; // Cancel the force-return. Let the debuggee continue. +}; + +let result = g.f(); +assertEq(result instanceof g.f, true); + +assertEq(steps > 0, true); +assertEq(uncaughtErrorsReported, steps); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-02.js b/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-02.js new file mode 100644 index 0000000000..25f9186c77 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-02.js @@ -0,0 +1,78 @@ +// Like Frame-onStep-generator-resumption-01.js, but bail out by throwing. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* f() { + yield 1; + } +`); + +// Try force-returning from one of the instructions in `f` before the initial +// yield. In detail: +// +// * This test calls `g.f()` under the Debugger. +// * It uses the Debugger to step `ttl` times. +// If we reach the initial yield before stepping the `ttl`th time, we're done. +// * Otherwise, the test tries to force-return from `f`. +// * That's an error, so the uncaughtExceptionHook is called. +// * The uncaughtExceptionHook returns a `throw` completion value. +// +// Returns `true` if we reached the initial yield, false otherwise. +// +// Note that this function is called in a loop so that every possible relevant +// value of `ttl` is tried once. +function test(ttl) { + let dbg = new Debugger(g); + let exiting = false; // we ran out of time-to-live and have forced return + let done = false; // we reached the initial yield without forced return + let reported = false; // a TypeError was reported. + + dbg.onEnterFrame = frame => { + assertEq(frame.callee.name, "f"); + dbg.onEnterFrame = undefined; + frame.onStep = () => { + if (ttl == 0) { + exiting = true; + // This test case never resumes the generator after the initial + // yield. Therefore the initial yield has not happened yet. So this + // force-return will be an error. + return {return: "ponies"}; + } + ttl--; + }; + frame.onPop = completion => { + if (!exiting) + done = true; + }; + }; + + dbg.uncaughtExceptionHook = (exc) => { + // When onStep returns an invalid resumption value, + // the error is reported here. + assertEq(exc instanceof TypeError, true); + reported = true; + return {throw: "FAIL"}; // Bail out of the test. + }; + + let result; + let caught = undefined; + try { + result = g.f(); + } catch (exc) { + caught = exc; + } + + if (done) { + assertEq(reported, false); + assertEq(result instanceof g.f, true); + assertEq(caught, undefined); + } else { + assertEq(reported, true); + assertEq(caught, "FAIL"); + } + + dbg.enabled = false; + return done; +} + +for (let ttl = 0; !test(ttl); ttl++) {} diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-03.js b/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-03.js new file mode 100644 index 0000000000..3b2e8d2b3a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generator-resumption-03.js @@ -0,0 +1,49 @@ +// Don't crash on {return:} from onStep in a generator, before the initial suspend. + +// This test tries to force-return from each bytecode instruction in a +// generator, up to the initial suspend. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.values = [1, 2, 3]; +g.eval(`function* f(arr=values) { yield* arr; }`); + +let dbg = Debugger(g); + +function test(ttl) { + let hits = 0; + dbg.onEnterFrame = frame => { + assertEq(frame.callee.name, "f"); + frame.onStep = () => { + if (--ttl === 0) + return {return: 123}; + }; + }; + dbg.uncaughtExceptionHook = exc => { + return {throw: "debugger error: " + exc}; + }; + + let val = undefined; + let caught = undefined; + try { + val = g.f(); + } catch (exc) { + caught = exc; + } + + if (val === undefined) { + // Tried to force-return before the initial suspend. + assertEq(caught, "debugger error: TypeError: can't force return from a generator before the initial yield"); + assertEq(ttl, 0); + return "pass"; + } else { + // Reached the initial suspend without forcing a return. + assertEq(typeof val, "object"); + assertEq(val instanceof g.f, true); + assertEq(ttl, 1); + return "done"; + } +} + +for (let i = 1; test(i) === "pass"; i++) {} diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generators-01.js b/js/src/jit-test/tests/debug/Frame-onStep-generators-01.js new file mode 100644 index 0000000000..7fbf26a94d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-01.js @@ -0,0 +1,31 @@ +// Stepping into the `.next()` method of a generator works as expected. + +let g = newGlobal({newCompartment: true}); +g.eval(`\ +function* nums() { // line 1 + yield 1; // 2 + yield 2; // 3 +} // 4 +function f() { // 5 + let gen = nums(); // 6 + gen.next(); // 7 + gen.next(); // 8 + gen.next(); // 9 +} // 10 +`); + +let log = []; +let previousLine = -1; +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + frame.onStep = () => { + let line = frame.script.getOffsetLocation(frame.offset).lineNumber; + if (previousLine != line) { // We stepped to a new line. + log.push(line); + previousLine = line; + } + }; +}; + +g.f(); +assertEq(log.join(" "), "5 6 1 6 7 1 2 7 8 2 3 8 9 3 9 10"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generators-02.js b/js/src/jit-test/tests/debug/Frame-onStep-generators-02.js new file mode 100644 index 0000000000..3e1fa6a433 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-02.js @@ -0,0 +1,44 @@ +// Stepping into the `.throw()` method of a generator with no relevant catch block. +// +// The debugger fires onEnterFrame and then frame.onPop for the generator frame when +// `gen.throw()` is called. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(`\ +function* z() { // line 1 + yield 1; // 2 + yield 2; // 3 +} // 4 +function f() { // 5 + let gen = z(); // 6 + gen.next(); // 7 + gen.throw("fit"); // 8 +} // 9 +`); + +let log = ""; +let previousLine = -1; +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + log += frame.callee.name + "{"; + frame.onStep = () => { + let line = frame.script.getOffsetLocation(frame.offset).lineNumber; + if (previousLine != line) { // We stepped to a new line. + log += line; + previousLine = line; + } + }; + frame.onPop = completion => { + if ("throw" in completion) + log += "!"; + log += "}"; + } +}; + +assertThrowsValue(() => g.f(), "fit"); +// z{1} is the initial generator setup. +// z{12} is the first .next() call, running to `yield 1` on line 2 +// The final `z{2!}` is for the .throw() call. +assertEq(log, "f{56z{1}67z{12}78z{2!}!}"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generators-03.js b/js/src/jit-test/tests/debug/Frame-onStep-generators-03.js new file mode 100644 index 0000000000..b9f6f5e5fd --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-03.js @@ -0,0 +1,45 @@ +// Stepping into the `.throw()` method of a generator with a relevant catch block. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(`\ +function* z() { // line 1 + try { // 2 + yield 1; // 3 + } catch (exc) { // 4 + yield 2; // 5 + } // 6 +} // 7 +function f() { // 8 + let gen = z(); // 9 + gen.next(); // 10 + gen.throw("fit"); // 11 +} // 12 +`); + +let log = []; +let previousLine = -1; +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + log.push(frame.callee.name + " in"); + frame.onStep = () => { + let line = frame.script.getOffsetLocation(frame.offset).lineNumber; + if (previousLine != line) { // We stepped to a new line. + log.push(line); + previousLine = line; + } + }; + frame.onPop = completion => { + log.push(frame.callee.name + " out"); + }; +}; + +g.f(); +assertEq( + log.join(", "), + "f in, 8, 9, z in, 1, z out, " + + "9, 10, z in, 1, 2, 3, z out, " + + "10, 11, z in, 3, 2, 4, 5, z out, " + // not sure why we hit line 2 here, source notes bug maybe + "11, 12, f out" +); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generators-04.js b/js/src/jit-test/tests/debug/Frame-onStep-generators-04.js new file mode 100644 index 0000000000..ef33571a6b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-04.js @@ -0,0 +1,36 @@ +// Stepping works across `yield` in generators. + +// Set up debuggee. +var g = newGlobal({newCompartment: true}); +g.log = ""; +g.eval(` +function* range(stop) { // line 2 + for (let i = 0; i < stop; i++) { // 3 + yield i; // 4 + log += " "; // 5 + } // 6 + log += "^"; // 7 +} +`); + +// Set up debugger. +let previousLine = -1; +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + frame.onStep = function () { + assertEq(this, frame); + let line = frame.script.getOffsetLocation(frame.offset).lineNumber; + if (previousLine != line) { + g.log += line; // We stepped to a new line. + previousLine = line; + } + }; + dbg.onEnterFrame = undefined; +}; + +// Run. +for (let value of g.range(3)) { + g.log += "*"; +} + +assertEq(g.log, "234*5 34*5 34*5 37^"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generators-05.js b/js/src/jit-test/tests/debug/Frame-onStep-generators-05.js new file mode 100644 index 0000000000..b4996ab217 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-05.js @@ -0,0 +1,14 @@ +// GC'ing a Debugger.Frame and its AbstractGeneratorObject adjusts the script's +// stepper count. + +let g = newGlobal({newCompartment: true}); +g.eval(`function* f() {}`); +for (let i = 0; i < 2; i++) { + let dbg = new Debugger(g); + dbg.onEnterFrame = frame => { + frame.onStep = () => {}; + }; + g.f(); + dbg.onEnterFrame = undefined; + gc(); +} diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generators-defaults.js b/js/src/jit-test/tests/debug/Frame-onStep-generators-defaults.js new file mode 100644 index 0000000000..ea32cc0094 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-defaults.js @@ -0,0 +1,35 @@ +// onStep works during the evaluation of default parameter values in generators. +// +// (They're evaluated at a weird time in the generator life cycle, before the +// generator object is created.) + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(`\ + function f1() {} // line 1 + function f2() {} // 2 + function f3() {} // 3 + // 4 + function* gen( // 5 + name, // 6 + schema = f1(), // 7 + timeToLive = f2(), // 8 + lucidity = f3() // 9 + ) { // 10 + } // 11 +`); + +let dbg = Debugger(g); +let log = []; +dbg.onEnterFrame = frame => { + frame.onStep = () => { + let line = frame.script.getOffsetLocation(frame.offset).lineNumber; + if (log.length == 0 || line != log[log.length - 1]) { + log.push(line); + } + }; +}; + +g.gen(0); +assertDeepEq(log, [5, 7, 1, 8, 2, 9, 3, 10]); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-generators-gc-01.js b/js/src/jit-test/tests/debug/Frame-onStep-generators-gc-01.js new file mode 100644 index 0000000000..54a77260f3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-gc-01.js @@ -0,0 +1,84 @@ +// onStep hooks on suspended Frames can keep Debuggers alive, even chaining them. + +// The path through the heap we're building and testing here is: +// gen0 (generator object) -> frame1 (suspended Frame with .onStep) -> dbg1 (Debugger object) +// -> gen1 -> frame2 -> dbg2 +// where everything after `gen1` is otherwise unreachable, and the edges +// `frame1 -> dbg1` and `frames2 -> dbg2` are due to the .onStep handlers, not +// strong refrences. +// +// There is no easy way to thread an event through this whole path; when we +// call gen0.next(), it will fire frame1.onStep(), but from there, making sure +// gen1.next() is called requires some minor heroics (see the WeakMap below). + +var gen0; + +var hits2 = 0; +var resuming2 = false; + +function onStep2() { + if (resuming2) { + hits2++; + resuming2 = false; + } +} + +function setup() { + let g1 = newGlobal({newCompartment: true}); + g1.eval(` + function* gf1() { + debugger; + yield 1; + return 'done'; + } + `); + gen0 = g1.gf1(); + + let g2 = newGlobal({newCompartment: true}); + g2.eval(` + function* gf2() { debugger; yield 1; return 'done'; } + + var resuming1 = false; + + function makeOnStepHook1(dbg1) { + // We use this WeakMap as a weak reference from frame1.onStep to dbg1. + var weak = new WeakMap(); + weak.set(dbg1, {}); + return () => { + if (resuming1) { + var dbg1Arr = nondeterministicGetWeakMapKeys(weak); + assertEq(dbg1Arr.length, 1); + dbg1Arr[0].gen1.next(); + resuming1 = false; + } + }; + } + + function test(g1, gen0) { + let dbg1 = Debugger(g1); + dbg1.onDebuggerStatement = frame1 => { + frame1.onStep = makeOnStepHook1(dbg1); + dbg1.onDebuggerStatement = undefined; + }; + gen0.next(); // run to yield point, creating frame1 and setting its onStep hook + resuming1 = true; + dbg1.gen1 = gf2(); + return dbg1.gen1; + } + `); + + let dbg2 = Debugger(g2); + dbg2.onDebuggerStatement = frame2 => { + frame2.onStep = onStep2; + dbg2.onDebuggerStatement = undefined; + }; + var gen1 = g2.test(g1, gen0); + gen1.next(); // run to yield point, creating frame2 and setting its onStep hook + resuming2 = true; +} + +setup(); +gc(); +assertEq(hits2, 0); +gen0.next(); // fires frame1.onStep, which calls gen1.next(), which fires frame2.onStep +assertEq(hits2, 1); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-iterators.js b/js/src/jit-test/tests/debug/Frame-onStep-iterators.js new file mode 100644 index 0000000000..2437abaab1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-iterators.js @@ -0,0 +1,20 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var log; +var a = []; + +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + frame.onStep = function () { + // This handler must not wipe out the debuggee's value in JSContext::iterValue. + log += 's'; + // This will use JSContext::iterValue in the debugger. + for (let i of a) + log += 'i'; + }; +}; + +log = ''; +g.eval("debugger; for (let i of [1,2,3]) print(i);"); +assertEq(!!log.match(/^ds*$/), true); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-lines-01.js b/js/src/jit-test/tests/debug/Frame-onStep-lines-01.js new file mode 100644 index 0000000000..a32a0535da --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-lines-01.js @@ -0,0 +1,78 @@ +// Test that a frame's onStep handler gets called at least once on each line of a function. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// When we hit a 'debugger' statement, set offsets to the frame's script's +// table of line offsets --- a sparse array indexed by line number. Begin +// single-stepping the current frame; for each source line we hit, delete +// the line's entry in offsets. Thus, at the end, offsets is an array with +// an element for each line we did not reach. +var doSingleStep = true; +var offsets; +dbg.onDebuggerStatement = function (frame) { + var script = frame.script; + offsets = script.getAllOffsets(); + print("debugger line: " + script.getOffsetLocation(frame.offset).lineNumber); + print("original lines: " + JSON.stringify(Object.keys(offsets))); + if (doSingleStep) { + frame.onStep = function onStepHandler() { + var line = script.getOffsetLocation(this.offset).lineNumber; + delete offsets[line]; + }; + } +}; + +g.eval( + 'function t(a, b, c) { \n' + + ' debugger; \n' + + ' var x = a; \n' + + ' x += b; \n' + + ' if (x < 10) \n' + + ' x -= c; \n' + + ' return x; \n' + + '} \n' + ); + +// This should stop at every line but the first of the function. +g.eval('t(1,2,3)'); +assertEq(Object.keys(offsets).length, 1); + +// This should stop at every line but the first of the function, and the +// body of the 'if'. +g.eval('t(10,20,30)'); +assertEq(Object.keys(offsets).length, 2); + +// This shouldn't stop at all. It's the frame that's in single-step mode, +// not the script, so the prior execution of t in single-step mode should +// have no effect on this one. +doSingleStep = false; +g.eval('t(0, 0, 0)'); +assertEq(Object.keys(offsets).length, 7); +doSingleStep = true; + +// Single-step in an eval frame. This should reach every line but the +// first. +g.eval( + 'debugger; \n' + + 'var a=1, b=2, c=3; \n' + + 'var x = a; \n' + + 'x += b; \n' + + 'if (x < 10) \n' + + ' x -= c; \n' + ); +print("final lines: " + JSON.stringify(Object.keys(offsets))); +assertEq(Object.keys(offsets).length, 1); + +// Single-step in a global code frame. This should reach every line but the +// first. +g.evaluate( + 'debugger; \n' + + 'var a=1, b=2, c=3; \n' + + 'var x = a; \n' + + 'x += b; \n' + + 'if (x < 10) \n' + + ' x -= c; \n' + ); +print("final lines: " + JSON.stringify(Object.keys(offsets))); +assertEq(Object.keys(offsets).length, 1); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-resumption-01.js b/js/src/jit-test/tests/debug/Frame-onStep-resumption-01.js new file mode 100644 index 0000000000..9fe94fb21f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-01.js @@ -0,0 +1,14 @@ +// If frame.onStep returns {return:val}, the frame returns. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(x) {\n" + + " var a = x * x;\n" + + " return a;\n" + + "}\n"); + +var dbg = Debugger(g); +dbg.onEnterFrame = function (frame) { + frame.onStep = function () { return {return: "pass"}; }; +}; + +assertEq(g.f(4), "pass"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-resumption-02.js b/js/src/jit-test/tests/debug/Frame-onStep-resumption-02.js new file mode 100644 index 0000000000..852160b03c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-02.js @@ -0,0 +1,17 @@ +// If frame.onStep returns {throw:}, an exception is thrown in the debuggee. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.eval("function h() { debugger; }\n" + + "function f() {\n" + + " h();\n" + + " return 'fail';\n" + + "}\n"); + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + frame.older.onStep = function () { return {throw: "pass"}; }; +}; + +assertThrowsValue(g.f, "pass"); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-resumption-03.js b/js/src/jit-test/tests/debug/Frame-onStep-resumption-03.js new file mode 100644 index 0000000000..ab3ecdd855 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-03.js @@ -0,0 +1,19 @@ +// If frame.onStep returns null, the debuggee terminates. + +var g = newGlobal({newCompartment: true}); +g.eval("function h() { debugger; }"); + +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + if (hits == 1) { + var rv = frame.eval("h();\n" + + "throw 'fail';\n"); + assertEq(rv, null); + } else { + frame.older.onStep = function () { return null; }; + } +}; +g.h(); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-resumption-04.js b/js/src/jit-test/tests/debug/Frame-onStep-resumption-04.js new file mode 100644 index 0000000000..d557152a14 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-04.js @@ -0,0 +1,31 @@ +// If frame.onStep returns null, debuggee catch and finally blocks are skipped. + +var g = newGlobal({newCompartment: true}); +g.eval("function h() { debugger; }"); + +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + if (hits == 1) { + var rv = frame.eval("try {\n" + + " h();\n" + + " throw 'fail';\n" + + "} catch (exc) {\n" + + " caught = exc;\n" + + "} finally {\n" + + " finallyHit = true;\n" + + "}\n"); + assertEq(rv, null); + } else { + frame.older.onStep = function () { + this.onStep = undefined; + return null; + }; + } +}; + +g.h(); +assertEq(hits, 2); +assertEq("caught" in g, false); +assertEq("finallyHit" in g, false); diff --git a/js/src/jit-test/tests/debug/Frame-onStep-resumption-05.js b/js/src/jit-test/tests/debug/Frame-onStep-resumption-05.js new file mode 100644 index 0000000000..f49e68051f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-05.js @@ -0,0 +1,55 @@ +// Test that invoking the interrupt callback counts as a step. + +function testResumptionVal(resumptionVal, turnOffDebugMode) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + g.log = ""; + g.resumptionVal = resumptionVal; + + setInterruptCallback(function () { + g.log += "i"; + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame(); + frame.onStep = function () { + g.log += "s"; + frame.onStep = undefined; + + if (turnOffDebugMode) + dbg.removeDebuggee(g); + + return resumptionVal; + }; + return true; + }); + + try { + return g.eval("(" + function f() { + log += "f"; + invokeInterruptCallback(function (interruptRv) { + log += "r"; + assertEq(interruptRv, resumptionVal == undefined); + }); + log += "a"; + return 42; + } + ")();"); + } finally { + assertEq(g.log, resumptionVal == undefined ? "fisra" : "fisr"); + } +} + +assertEq(testResumptionVal(undefined), 42); +assertEq(testResumptionVal({ return: "not 42" }), "not 42"); +try { + testResumptionVal({ throw: "thrown 42" }); +} catch (e) { + assertEq(e, "thrown 42"); +} + +assertEq(testResumptionVal(undefined, true), 42); +assertEq(testResumptionVal({ return: "not 42" }, true), "not 42"); + +try { + testResumptionVal({ throw: "thrown 42" }, true); +} catch (e) { + assertEq(e, "thrown 42"); +} diff --git a/js/src/jit-test/tests/debug/Frame-onStep-resumption-06.js b/js/src/jit-test/tests/debug/Frame-onStep-resumption-06.js new file mode 100644 index 0000000000..3170c0319c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-06.js @@ -0,0 +1,31 @@ +// |jit-test| error:all-jobs-completed-successfully +// Verifiy that onStep's force-return queues the promise microtask to run in +// the debuggee's job queue, not the debugger's +// AutoDebuggerJobQueueInterruption. + +let g = newGlobal({ newCompartment: true }); +g.eval(` + async function asyncFn(x) { + await Promise.resolve(); + } + function enterDebuggee(){} +`); +const dbg = new Debugger(g); + +(async function() { + let it = g.asyncFn(); + + // Force-return when the await resumes and steps. + dbg.onEnterFrame = frame => { + dbg.onEnterFrame = undefined; + frame.onStep = () => ({ return: "exit" }); + }; + + const result = await it; + assertEq(result, "exit"); + // If execution here is resumed from the debugger's queue, this call will + // trigger DebuggeeWouldRun exception. + g.enterDebuggee(); + + throw "all-jobs-completed-successfully"; +})(); diff --git a/js/src/jit-test/tests/debug/Frame-script-01.js b/js/src/jit-test/tests/debug/Frame-script-01.js new file mode 100644 index 0000000000..9246f42d69 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-script-01.js @@ -0,0 +1,25 @@ +// Frame.prototype.script for eval frames. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// Apply |f| to each frame that is |skip| frames up from each frame that +// executes a 'debugger' statement when evaluating |code| in the global g. +function ApplyToFrameScript(code, skip, f) { + dbg.onDebuggerStatement = function (frame) { + while (skip-- > 0) + frame = frame.older; + assertEq(frame.type, "eval"); + f(frame.script); + }; + g.eval(code); +} + +ApplyToFrameScript('debugger;', 0, + function (script) { + assertEq(script instanceof Debugger.Script, true); + }); +ApplyToFrameScript("(function () { eval('debugger;'); })();", 0, + function (script) { + assertEq(script instanceof Debugger.Script, true); + }); diff --git a/js/src/jit-test/tests/debug/Frame-script-02.js b/js/src/jit-test/tests/debug/Frame-script-02.js new file mode 100644 index 0000000000..3b5bf12c63 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-script-02.js @@ -0,0 +1,27 @@ +// Frame.prototype.script for call frames. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// Apply |f| to each frame that is |skip| frames up from each frame that +// executes a 'debugger' statement when evaluating |code| in the global g. +function ApplyToFrameScript(code, skip, f) { + dbg.onDebuggerStatement = function (frame) { + while (skip-- > 0) + frame = frame.older; + assertEq(frame.type, "call"); + f(frame.script); + }; + g.eval(code); +} + +ApplyToFrameScript('(function () { debugger; })();', 0, + function (script) { + assertEq(script instanceof Debugger.Script, true); + }); + +// This would be nice, once we can get host call frames: +// ApplyToFrameScript("(function () { debugger; }).call(null);", 1, +// function (script) { +// assertEq(script, null); +// }); diff --git a/js/src/jit-test/tests/debug/Frame-script-03.js b/js/src/jit-test/tests/debug/Frame-script-03.js new file mode 100644 index 0000000000..c29de59e25 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-script-03.js @@ -0,0 +1,8 @@ +// frame.script can create a Debugger.Script for a JS_Evaluate* script. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var s; +dbg.onDebuggerStatement = function (frame) { s = frame.script; }; +g.evaluate("debugger;"); +assertEq(s instanceof Debugger.Script, true); diff --git a/js/src/jit-test/tests/debug/Frame-script-04.js b/js/src/jit-test/tests/debug/Frame-script-04.js new file mode 100644 index 0000000000..8f0f953b1d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-script-04.js @@ -0,0 +1,35 @@ +// Frame.prototype.script for generator frames. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +function* f() {} +`); + +let frame; +let script; +dbg.onEnterFrame = function(f) { + frame = f; + script = frame.script; +}; + +const it = g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(script instanceof Debugger.Script, true); +assertEq(frame.script, script); + +const lastFrame = frame; +const lastScript = script; +frame = null; +script = null; + +it.next(); + +assertEq(frame, lastFrame); +assertEq(script, lastScript); + +// The frame has finished evaluating, so the script is no longer accessible. +assertThrowsInstanceOf(() => frame.script, Error); diff --git a/js/src/jit-test/tests/debug/Frame-script-05.js b/js/src/jit-test/tests/debug/Frame-script-05.js new file mode 100644 index 0000000000..7c1094f01d --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-script-05.js @@ -0,0 +1,37 @@ +// Frame.prototype.script for async function frames. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +async function f() { + await Promise.resolve() +} +`); + +let frame; +let script; +dbg.onEnterFrame = function(f) { + frame = f; + script = frame.script; +}; + +const promise = g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(script instanceof Debugger.Script, true); +assertEq(frame.script, script); + +const lastFrame = frame; +const lastScript = script; +frame = null; +script = null; + +promise.then(() => { + assertEq(frame, lastFrame); + assertEq(script, lastScript); + + // The frame has finished evaluating, so the script is no longer accessible. + assertThrowsInstanceOf(() => frame.script, Error); +}); diff --git a/js/src/jit-test/tests/debug/Frame-script-06.js b/js/src/jit-test/tests/debug/Frame-script-06.js new file mode 100644 index 0000000000..a01000347a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-script-06.js @@ -0,0 +1,48 @@ +// Frame.prototype.script for async generator frames. + +load(libdir + "asserts.js"); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +g.eval(` +async function* f() { + await Promise.resolve(); +} +`); + +let frame; +let script; +dbg.onEnterFrame = function(f) { + frame = f; + script = frame.script; +}; + +const it = g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(script instanceof Debugger.Script, true); +assertEq(frame.script, script); + +let lastFrame = frame; +let lastScript = script; +frame = null; +script = null; + +let promise = it.next(); + +assertEq(frame, lastFrame); +assertEq(script, lastScript); +assertEq(frame.script, script); + +lastFrame = frame; +lastScript = script; +frame = null; +script = null; + +promise.then(() => { + assertEq(frame, lastFrame); + assertEq(script, lastScript); + + // The frame has finished evaluating, so the script is no longer accessible. + assertThrowsInstanceOf(() => frame.script, Error); +}); diff --git a/js/src/jit-test/tests/debug/Frame-script-environment-nondebuggee.js b/js/src/jit-test/tests/debug/Frame-script-environment-nondebuggee.js new file mode 100644 index 0000000000..2d11b00c3b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-script-environment-nondebuggee.js @@ -0,0 +1,32 @@ +// The script and environment of a non-debuggee frame are inaccessible. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; + +var log; +dbg.onDebuggerStatement = function (frame) { + log += frame.type; + // Initially, 'frame' is a debuggee frame, and we should be able to see its script and environment. + assertEq(frame.script instanceof Debugger.Script, true); + assertEq(frame.environment instanceof Debugger.Environment, true); + + // If we make g no longer a debuggee, then trying to touch the frame at + // all should throw. + dbg.removeDebuggee(g); + assertThrowsInstanceOf(() => frame.script, Error); + assertThrowsInstanceOf(() => frame.environment, Error); +} + +g.eval('function f() { debugger; }'); + +log = ''; +dbg.addDebuggee(g); +g.f(); +assertEq(log, 'call'); + +log = ''; +dbg.addDebuggee(g); +g.eval('debugger;'); +assertEq(log, 'eval'); diff --git a/js/src/jit-test/tests/debug/Frame-terminated-01.js b/js/src/jit-test/tests/debug/Frame-terminated-01.js new file mode 100644 index 0000000000..d1b3de79e0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-terminated-01.js @@ -0,0 +1,19 @@ +// Check `.terminated` functionality for normal functions. + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +function f(){} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.terminated, false); +}; + +g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(frame.terminated, true); diff --git a/js/src/jit-test/tests/debug/Frame-terminated-02.js b/js/src/jit-test/tests/debug/Frame-terminated-02.js new file mode 100644 index 0000000000..86323db5cb --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-terminated-02.js @@ -0,0 +1,27 @@ +// Check `.terminated` functionality for async functions. + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +async function f(){ + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.terminated, false); +}; + +(async () => { + const promise = g.f(); + + assertEq(frame instanceof Debugger.Frame, true); + assertEq(frame.terminated, false); + + await promise; + + assertEq(frame.terminated, true); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-terminated-03.js b/js/src/jit-test/tests/debug/Frame-terminated-03.js new file mode 100644 index 0000000000..c5ca4febda --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-terminated-03.js @@ -0,0 +1,23 @@ +// Check `.terminated` functionality for generator functions. + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +function* f(){} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.terminated, false); +}; + +const it = g.f(); + +assertEq(frame instanceof Debugger.Frame, true); +assertEq(frame.terminated, false); + +it.next(); + +assertEq(frame.terminated, true); diff --git a/js/src/jit-test/tests/debug/Frame-terminated-04.js b/js/src/jit-test/tests/debug/Frame-terminated-04.js new file mode 100644 index 0000000000..74d00c8a5b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-terminated-04.js @@ -0,0 +1,31 @@ +// Check `.terminated` functionality for async generator functions. + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +g.eval(` +async function* f(){ + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.terminated, false); +}; + +(async () => { + const it = g.f(); + + assertEq(frame instanceof Debugger.Frame, true); + assertEq(frame.terminated, false); + + const promise = it.next(); + + assertEq(frame.terminated, false); + + await promise; + + assertEq(frame.terminated, true); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-this-01.js b/js/src/jit-test/tests/debug/Frame-this-01.js new file mode 100644 index 0000000000..147276624e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-01.js @@ -0,0 +1,24 @@ +// Frame.prototype.this in strict-mode functions, with primitive values + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(frame.this, g.v); +}; + +g.eval("function f() { 'use strict'; debugger; }"); + +g.eval("Boolean.prototype.f = f; v = true; v.f();"); +g.eval("f.call(v);"); +g.eval("Number.prototype.f = f; v = 3.14; v.f();"); +g.eval("f.call(v);"); +g.eval("String.prototype.f = f; v = 'hello'; v.f();"); +g.eval("f.call(v);"); +g.eval("Symbol.prototype.f = f; v = Symbol('world'); v.f();"); +g.eval("f.call(v);"); +g.eval("v = undefined; f.call(v);"); +g.eval("v = null; f.call(v);"); + +assertEq(hits, 10); diff --git a/js/src/jit-test/tests/debug/Frame-this-02.js b/js/src/jit-test/tests/debug/Frame-this-02.js new file mode 100644 index 0000000000..a94ba5cc69 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-02.js @@ -0,0 +1,17 @@ +// Frame.prototype.this in strict direct eval frames + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(frame.this, g.v); +}; + +g.eval("function f() { 'use strict'; eval('debugger;'); }"); + +g.eval("Boolean.prototype.f = f; v = true; v.f();"); +g.eval("f.call(v);"); +g.eval("v = null; f.call(v);"); + +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/Frame-this-03.js b/js/src/jit-test/tests/debug/Frame-this-03.js new file mode 100644 index 0000000000..18f8663479 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-03.js @@ -0,0 +1,29 @@ +// Frame.prototype.this in non-strict-mode functions, with primitive values + +function classOf(obj) { + return Object.prototype.toString.call(obj).match(/^\[object (.*)\]$/)[1]; +} + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(frame.this instanceof Debugger.Object, true); + assertEq(frame.this.class, g.v == null ? classOf(g) : classOf(Object(g.v))); +}; + +g.eval("function f() { debugger; }"); + +g.eval("Boolean.prototype.f = f; v = true; v.f();"); +g.eval("f.call(v);"); +g.eval("Number.prototype.f = f; v = 3.14; v.f();"); +g.eval("f.call(v);"); +g.eval("String.prototype.f = f; v = 'hello'; v.f();"); +g.eval("f.call(v);"); +g.eval("Symbol.prototype.f = f; v = Symbol('world'); v.f();"); +g.eval("f.call(v);"); +g.eval("v = undefined; f.call(v);"); +g.eval("v = null; f.call(v);"); + +assertEq(hits, 10); diff --git a/js/src/jit-test/tests/debug/Frame-this-04.js b/js/src/jit-test/tests/debug/Frame-this-04.js new file mode 100644 index 0000000000..9111ac13ff --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-04.js @@ -0,0 +1,25 @@ +// Debugger.Frame.prototype.this in functions, with object values + +function classOf(obj) { + return Object.prototype.toString.call(obj).match(/^\[object (.*)\]$/)[1]; +} + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(frame.this instanceof Debugger.Object, true); + assertEq(frame.this.class, classOf(Object(g.v))); +}; + +g.eval("function f() { debugger; }"); + +g.eval("v = {}; f.call(v);"); +g.eval("v.f = f; v.f();"); +g.eval("v = new Date; f.call(v);"); +g.eval("v.f = f; v.f();"); +g.eval("v = []; f.call(v);"); +g.eval("Object.prototype.f = f; v.f();"); +g.eval("v = this; f();"); +assertEq(hits, 7); diff --git a/js/src/jit-test/tests/debug/Frame-this-05.js b/js/src/jit-test/tests/debug/Frame-this-05.js new file mode 100644 index 0000000000..3c38f10bca --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-05.js @@ -0,0 +1,23 @@ +// Frame.this and evalInFrame in the global scope. +var g = newGlobal({newCompartment: true}); +g.eval("x = 4; this['.this'] = 222;"); +var dbg = new Debugger(g); +var res; +dbg.onDebuggerStatement = function (frame) { + res = frame.eval("this.x").return; + res += frame.this.unsafeDereference().x; +}; +g.eval("debugger;"); +assertEq(res, 8); + +// And inside eval. +g.eval("x = 3; eval('debugger')"); +assertEq(res, 6); +g.eval("x = 2; eval('eval(\\'debugger\\')')"); +assertEq(res, 4); + +// And inside arrow functions. +g.eval("x = 1; (() => { debugger; })()"); +assertEq(res, 2); +g.eval("x = 5; (() => { eval('debugger'); })()"); +assertEq(res, 10); diff --git a/js/src/jit-test/tests/debug/Frame-this-06.js b/js/src/jit-test/tests/debug/Frame-this-06.js new file mode 100644 index 0000000000..d38398645f --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-06.js @@ -0,0 +1,22 @@ +// Frame.this and evalInFrame with missing this, strict and non-strict. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var evalThis, frameThis; +dbg.onEnterFrame = function (frame) { + if (frame.type === "eval") + return; + assertEq(frame.type, "call"); + evalThis = frame.eval("this"); + frameThis = frame.this; +}; + +// Strict, this is primitive. +g.eval("var foo = function() { 'use strict'; }; foo.call(33);"); +assertEq(evalThis.return, 33); +assertEq(frameThis, 33); + +// Non-strict, this has to be boxed. +g.eval("var bar = function() { }; bar.call(22);"); +assertEq(typeof evalThis.return, "object"); +assertEq(evalThis.return.unsafeDereference().valueOf(), 22); +assertEq(frameThis.unsafeDereference().valueOf(), 22); diff --git a/js/src/jit-test/tests/debug/Frame-this-07.js b/js/src/jit-test/tests/debug/Frame-this-07.js new file mode 100644 index 0000000000..50d938f46c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-07.js @@ -0,0 +1,19 @@ +// Frame.this can be marked as optimized-out in some cases. Here we call an +// arrow function but its enclosing function is no longer live, so it's +// impossible to recover its missing 'this' binding. +var g = newGlobal({newCompartment: true}); +g.eval("x = 4"); +g.eval("var foo = function() { return () => 1; }; var arrow = foo.call(3);"); +var dbg = new Debugger(g); +var log = ""; +dbg.onEnterFrame = function (frame) { + if (frame.type === "eval") + return; + assertEq(frame.type, "call"); + assertEq(frame.this.optimizedOut, true); + frame.eval("try { print(this.x); } catch(e) { exc = e; }"); + assertEq(typeof g.exc, "object"); + log += "d"; +}; +g.eval("arrow();"); +assertEq(log, "d"); diff --git a/js/src/jit-test/tests/debug/Frame-this-08.js b/js/src/jit-test/tests/debug/Frame-this-08.js new file mode 100644 index 0000000000..4346115865 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-08.js @@ -0,0 +1,16 @@ +// Frame.this and evalInFrame in arrow function that uses 'this'. +var g = newGlobal({newCompartment: true}); +g.eval("x = 4"); +g.eval("var foo = function() { 'use strict'; return () => this; }; var arrow = foo.call(3);"); +var dbg = new Debugger(g); +var hits = 0; +dbg.onEnterFrame = function (frame) { + if (frame.type === "eval") + return; + hits++; + assertEq(frame.type, "call"); + assertEq(frame.this, 3); + assertEq(frame.eval("this + 1").return, 4); +}; +g.eval("arrow();"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-this-09.js b/js/src/jit-test/tests/debug/Frame-this-09.js new file mode 100644 index 0000000000..3ff6cc2a96 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-09.js @@ -0,0 +1,45 @@ +// Ensure |Frame.this| returns the right value even if we're still in the +// script's prologue, before JSOP_FUNCTIONTHIS. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onEnterFrame = function (frame) { + if (frame.type === 'eval') + return; + hits++; + + var frameThis = frame.this; + if (frameThis !== null && typeof frameThis === "object") + g.gotThis = frameThis.unsafeDereference(); + else + g.gotThis = frameThis; + + assertEq(frame.this, frameThis, "getter should not return a different object"); +}; + +// Strict mode, function uses |this|. +g.eval("function strictfun() { 'use strict'; return this; }"); +g.eval("strictfun.call(Math); assertEq(gotThis, Math);"); +g.eval("strictfun.call(true); assertEq(gotThis, true);"); +g.eval("strictfun.call(); assertEq(gotThis, undefined);"); + +// Strict mode, function doesn't use |this|. +g.eval("function strictfunNoThis() { 'use strict'; }"); +g.eval("strictfunNoThis.call(Math); assertEq(gotThis, Math);"); +g.eval("strictfunNoThis.call(true); assertEq(gotThis, true);"); +g.eval("strictfunNoThis.call(null); assertEq(gotThis, null);"); + +// Non-strict mode (primitive |this| is boxed), function uses |this|. +g.eval("function nonstrictfun() { return this; }"); +g.eval("nonstrictfun.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfun.call(null); assertEq(gotThis, this);"); +g.eval("nonstrictfun.call(); assertEq(gotThis, this);"); + +// Non-strict mode (primitive |this| is boxed), function doesn't use |this|. +g.eval("function nonstrictfunNoThis() {}"); +g.eval("nonstrictfunNoThis.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfunNoThis.call(null); assertEq(gotThis, this);"); +g.eval("nonstrictfunNoThis.call(); assertEq(gotThis, this);"); + +assertEq(hits, 12); diff --git a/js/src/jit-test/tests/debug/Frame-this-10.js b/js/src/jit-test/tests/debug/Frame-this-10.js new file mode 100644 index 0000000000..5ca65a0c93 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-10.js @@ -0,0 +1,42 @@ +// Check the Frame.this getter always returns the same object for a given frame. +// Primitive this-values should not be boxed multiple times. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var framesEntered = 0; +var framesPopped = 0; +var numSteps = 0; +dbg.onEnterFrame = function (frame) { + if (frame.type === 'eval') + return; + framesEntered++; + + var frameThis = frame.this; + assertEq(frame.this, frameThis); + + frame.onPop = function() { + framesPopped++; + assertEq(frame.this, frameThis); + }; + + frame.onStep = function() { + numSteps++; + assertEq(frame.this, frameThis); + } + + g.gotThis = frameThis.unsafeDereference(); +}; + +g.eval("function nonstrictfun() { return this; }"); +g.eval("nonstrictfun.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfun.call(true); assertEq(gotThis.valueOf(), true);"); +g.eval("nonstrictfun.call(); assertEq(gotThis, this);"); + +g.eval("function nonstrictfunNoThis() { return 1; }"); +g.eval("nonstrictfunNoThis.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfunNoThis.call(true); assertEq(gotThis.valueOf(), true);"); +g.eval("nonstrictfunNoThis.call(); assertEq(gotThis, this);"); + +assertEq(framesEntered, 6); +assertEq(framesPopped, 6); +assertEq(numSteps > 15, true); diff --git a/js/src/jit-test/tests/debug/Frame-this-11.js b/js/src/jit-test/tests/debug/Frame-this-11.js new file mode 100644 index 0000000000..9e137dba87 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-11.js @@ -0,0 +1,46 @@ +// Ensure evalInFrame("this") returns the right value even if we're still in the +// script's prologue, before JSOP_FUNCTIONTHIS. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onEnterFrame = function (frame) { + if (frame.type === 'eval') + return; + hits++; + + var frameThis = frame.eval('this').return; + if (frameThis !== null && typeof frameThis === "object") + g.gotThis = frameThis.unsafeDereference(); + else + g.gotThis = frameThis; + + assertEq(frame.this, frameThis); + assertEq(frame.eval('this').return, frameThis); +}; + +// Strict mode, function uses |this|. +g.eval("function strictfun() { 'use strict'; return this; }"); +g.eval("strictfun.call(Math); assertEq(gotThis, Math);"); +g.eval("strictfun.call(true); assertEq(gotThis, true);"); +g.eval("strictfun.call(); assertEq(gotThis, undefined);"); + +// Strict mode, function doesn't use |this|. +g.eval("function strictfunNoThis() { 'use strict'; }"); +g.eval("strictfunNoThis.call(Math); assertEq(gotThis, Math);"); +g.eval("strictfunNoThis.call(true); assertEq(gotThis, true);"); +g.eval("strictfunNoThis.call(null); assertEq(gotThis, null);"); + +// Non-strict mode (primitive |this| is boxed), function uses |this|. +g.eval("function nonstrictfun() { return this; }"); +g.eval("nonstrictfun.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfun.call(null); assertEq(gotThis, this);"); +g.eval("nonstrictfun.call(); assertEq(gotThis, this);"); + +// Non-strict mode (primitive |this| is boxed), function doesn't use |this|. +g.eval("function nonstrictfunNoThis() {}"); +g.eval("nonstrictfunNoThis.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfunNoThis.call(null); assertEq(gotThis, this);"); +g.eval("nonstrictfunNoThis.call(); assertEq(gotThis, this);"); + +assertEq(hits, 12); diff --git a/js/src/jit-test/tests/debug/Frame-this-12.js b/js/src/jit-test/tests/debug/Frame-this-12.js new file mode 100644 index 0000000000..3db0c4a8d6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-12.js @@ -0,0 +1,42 @@ +// Check evalInFrame("this") always returns the same object for a given frame. +// Primitive this-values should not be boxed multiple times. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var framesEntered = 0; +var framesPopped = 0; +var numSteps = 0; +dbg.onEnterFrame = function (frame) { + if (frame.type === 'eval') + return; + framesEntered++; + + var frameThis = frame.eval('this').return; + + frame.onPop = function() { + framesPopped++; + assertEq(frame.eval('this').return, frameThis); + }; + + frame.onStep = function() { + numSteps++; + assertEq(frame.eval('this').return, frameThis); + } + + g.gotThis = frameThis.unsafeDereference(); + assertEq(frame.this, frameThis); +}; + +g.eval("function nonstrictfun() { return this; }"); +g.eval("nonstrictfun.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfun.call(true); assertEq(gotThis.valueOf(), true);"); +g.eval("nonstrictfun.call(); assertEq(gotThis, this);"); + +g.eval("function nonstrictfunNoThis() { return 1; }"); +g.eval("nonstrictfunNoThis.call(Math); assertEq(gotThis, Math);"); +g.eval("nonstrictfunNoThis.call(true); assertEq(gotThis.valueOf(), true);"); +g.eval("nonstrictfunNoThis.call(); assertEq(gotThis, this);"); + +assertEq(framesEntered, 6); +assertEq(framesPopped, 6); +assertEq(numSteps > 15, true); diff --git a/js/src/jit-test/tests/debug/Frame-this-13.js b/js/src/jit-test/tests/debug/Frame-this-13.js new file mode 100644 index 0000000000..4200cecc45 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-13.js @@ -0,0 +1,26 @@ +// Test that Debugger.Frame.prototype.this works on normal functions. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(); +const gDO = dbg.addDebuggee(g); + +g.eval(` +var context = {}; +var f = function() { + return this; +}.bind(context); +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + dbg.onEnterFrame = undefined; +}; + +g.f(); + +assertEq(!!frame, true); +assertThrowsInstanceOf(() => frame.this, Error); diff --git a/js/src/jit-test/tests/debug/Frame-this-14.js b/js/src/jit-test/tests/debug/Frame-this-14.js new file mode 100644 index 0000000000..a47b203ea1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-14.js @@ -0,0 +1,31 @@ +// Test that Debugger.Frame.prototype.this works on a suspended generator +// function. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(); +const gDO = dbg.addDebuggee(g); + +g.eval(` +var context = {}; +var f = function*() { + return this; +}.bind(context); +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + dbg.onEnterFrame = undefined; +}; + +const it = g.f(); + +assertEq(!!frame, true); +assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + +it.next(); + +assertThrowsInstanceOf(() => frame.this, Error); diff --git a/js/src/jit-test/tests/debug/Frame-this-15.js b/js/src/jit-test/tests/debug/Frame-this-15.js new file mode 100644 index 0000000000..350fa508a9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-15.js @@ -0,0 +1,34 @@ +// Test that Debugger.Frame.prototype.this works on a suspended async +// function. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(); +const gDO = dbg.addDebuggee(g); + +g.eval(` +var context = {}; +var f = async function() { + await Promise.resolve(); + return this; +}.bind(context); +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + dbg.onEnterFrame = undefined; +}; + +(async () => { + const promise = g.f(); + + assertEq(!!frame, true); + assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + + await promise; + + assertThrowsInstanceOf(() => frame.this, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-this-16.js b/js/src/jit-test/tests/debug/Frame-this-16.js new file mode 100644 index 0000000000..43aec1f2c5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-this-16.js @@ -0,0 +1,38 @@ +// Test that Debugger.Frame.prototype.this works on a suspended async +// generator function. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(); +const gDO = dbg.addDebuggee(g); + +g.eval(` +var context = {}; +var f = async function*() { + await Promise.resolve(); + return this; +}.bind(context); +`); + +let frame; +dbg.onEnterFrame = f => { + frame = f; + assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + dbg.onEnterFrame = undefined; +}; + +(async () => { + const it = g.f(); + + assertEq(!!frame, true); + assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + + const promise = it.next(); + + assertEq(frame.this, gDO.makeDebuggeeValue(g.context)); + + await promise; + + assertThrowsInstanceOf(() => frame.this, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-type-01.js b/js/src/jit-test/tests/debug/Frame-type-01.js new file mode 100644 index 0000000000..b17338874c --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-type-01.js @@ -0,0 +1,22 @@ +// Debugger.Frame.prototype.type on a normal function. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); + +g.eval(` +function f() {} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.type, "call"); +}; + +g.f(); + +assertEq(!!frame, true); +// Throws because the frame has terminated. +assertThrowsInstanceOf(() => frame.type, Error); diff --git a/js/src/jit-test/tests/debug/Frame-type-02.js b/js/src/jit-test/tests/debug/Frame-type-02.js new file mode 100644 index 0000000000..07e32ae29a --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-type-02.js @@ -0,0 +1,27 @@ +// Debugger.Frame.prototype.type on a generator. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); + +g.eval(` +function* f() {} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.type, "call"); +}; + +const it = g.f(); + +assertEq(frame.type, "call"); +frame = null; + +it.next(); + +assertEq(!!frame, true); +// Throws because the frame has terminated. +assertThrowsInstanceOf(() => frame.type, Error); diff --git a/js/src/jit-test/tests/debug/Frame-type-03.js b/js/src/jit-test/tests/debug/Frame-type-03.js new file mode 100644 index 0000000000..38dc37d1c3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-type-03.js @@ -0,0 +1,31 @@ +// Debugger.Frame.prototype.type on an async function. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); + +g.eval(` +async function f() { + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.type, "call"); +}; + +(async () => { + const promise = g.f(); + + assertEq(frame.type, "call"); + frame = null; + + await promise; + + assertEq(!!frame, true); + // Throws because the frame has terminated. + assertThrowsInstanceOf(() => frame.type, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Frame-type-04.js b/js/src/jit-test/tests/debug/Frame-type-04.js new file mode 100644 index 0000000000..4c4a65b5e9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-type-04.js @@ -0,0 +1,35 @@ +// Debugger.Frame.prototype.type on an async generator. + +load(libdir + "asserts.js"); + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); + +g.eval(` +async function* f() { + await Promise.resolve(); +} +`); + +let frame; +dbg.onEnterFrame = function(f) { + frame = f; + assertEq(frame.type, "call"); +}; + +(async () => { + const it = g.f(); + + assertEq(frame.type, "call"); + frame = null; + + const promise = it.next(); + + assertEq(!!frame, true); + assertEq(frame.type, "call"); + + await promise; + + // Throws because the frame has terminated. + assertThrowsInstanceOf(() => frame.type, Error); +})(); diff --git a/js/src/jit-test/tests/debug/Memory-01.js b/js/src/jit-test/tests/debug/Memory-01.js new file mode 100644 index 0000000000..7dc00b9ee8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-01.js @@ -0,0 +1,6 @@ +assertEq(typeof Debugger.Memory, "function"); +let dbg = new Debugger; +assertEq(dbg.memory instanceof Debugger.Memory, true); + +load(libdir + "asserts.js"); +assertThrowsInstanceOf(() => new Debugger.Memory, TypeError); diff --git a/js/src/jit-test/tests/debug/Memory-allocationSamplingProbability-01.js b/js/src/jit-test/tests/debug/Memory-allocationSamplingProbability-01.js new file mode 100644 index 0000000000..3dbd7be9d9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-allocationSamplingProbability-01.js @@ -0,0 +1,45 @@ +// Test that setting Debugger.Memory.prototype.allocationSamplingProbability to +// a bad number throws. + +load(libdir + "asserts.js"); + +const root = newGlobal({newCompartment: true}); + +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root); + +var mem = dbg.memory; + +// Out of range, negative +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = -Number.MAX_VALUE, + TypeError); +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = -1, + TypeError); +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = -Number.MIN_VALUE, + TypeError); + +// In range +mem.allocationSamplingProbability = -0.0; +mem.allocationSamplingProbability = 0.0; +mem.allocationSamplingProbability = Number.MIN_VALUE; +mem.allocationSamplingProbability = 1 / 3; +mem.allocationSamplingProbability = .5; +mem.allocationSamplingProbability = 2 / 3; +mem.allocationSamplingProbability = 1 - Math.pow(2, -53); +mem.allocationSamplingProbability = 1; + +// Out of range, positive +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = 1 + Number.EPSILON, + TypeError); +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = 2, + TypeError); +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = Number.MAX_VALUE, + TypeError); + +// Out of range, non-finite +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = -Infinity, + TypeError); +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = Infinity, + TypeError); +assertThrowsInstanceOf(() => mem.allocationSamplingProbability = NaN, + TypeError); diff --git a/js/src/jit-test/tests/debug/Memory-allocationSamplingProbability-02.js b/js/src/jit-test/tests/debug/Memory-allocationSamplingProbability-02.js new file mode 100644 index 0000000000..bbd2582a68 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-allocationSamplingProbability-02.js @@ -0,0 +1,36 @@ +// Test that we only sample about allocationSamplingProbability * 100 percent of +// allocations. + +const root = newGlobal({newCompartment: true}); + +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root); + +root.eval(` + objs = []; + objs.push(new Object); +`); + +root.eval("" + function makeSomeAllocations() { + for (var i = 0; i < 100; i++) { + objs.push(new Object); + } +}); + +function measure(P, expected) { + root.setSavedStacksRNGState(Number.MAX_SAFE_INTEGER - 1); + dbg.memory.allocationSamplingProbability = P; + root.makeSomeAllocations(); + assertEq(dbg.memory.drainAllocationsLog().length, expected); +} + +dbg.memory.trackingAllocationSites = true; + +// These are the sample counts that were correct when this test was last +// updated; changes to SpiderMonkey may occasionally cause changes +// here. Anything that is within a plausible range for the given sampling +// probability is fine. +measure(0.0, 0); +measure(1.0, 100); +measure(0.1, 11); +measure(0.5, 49); diff --git a/js/src/jit-test/tests/debug/Memory-allocationsLogOverflowed-01.js b/js/src/jit-test/tests/debug/Memory-allocationsLogOverflowed-01.js new file mode 100644 index 0000000000..5e7176db34 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-allocationsLogOverflowed-01.js @@ -0,0 +1,24 @@ +// Test basic usage of `Debugger.Memory.prototype.allocationsLogOverflowed`. + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(root); +dbg.memory.trackingAllocationSites = true; +dbg.memory.maxAllocationsLogLength = 1; + +root.eval("(" + function immediate() { + // Allocate more than the max log length. + this.objs = [{}, {}, {}, {}]; +} + "());"); + +// The log should have overflowed. +assertEq(dbg.memory.allocationsLogOverflowed, true); + +// Once drained, the flag should be reset. +const allocs = dbg.memory.drainAllocationsLog(); +assertEq(dbg.memory.allocationsLogOverflowed, false); + +// If we keep allocations under the max log length, then we shouldn't have +// overflowed. +dbg.memory.maxAllocationsLogLength = 10000; +root.eval("this.objs = [{}, {}, {}, {}];"); +assertEq(dbg.memory.allocationsLogOverflowed, false); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-01.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-01.js new file mode 100644 index 0000000000..8b7599c73d --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-01.js @@ -0,0 +1,31 @@ +// Test basic usage of drainAllocationsLog() + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root) +dbg.memory.trackingAllocationSites = true; + +root.eval("(" + function immediate() { + this.tests = [ + {x: 1}, + [], + /(two|2)\s*problems/, + new function Ctor(){}, + new Object(), + new Array(), + new Date(), + ]; +} + "());"); + +const allocs = dbg.memory.drainAllocationsLog(); +print(allocs.join("\n--------------------------------------------------------------------------\n")); +print("Total number of allocations logged: " + allocs.length); + +let idx = -1; +for (let object of root.tests) { + let wrappedObject = wrappedRoot.makeDebuggeeValue(object); + let allocSite = wrappedObject.allocationSite; + let newIdx = allocs.map(x => x.frame).indexOf(allocSite); + assertEq(newIdx > idx, true); + idx = newIdx; +} diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-02.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-02.js new file mode 100644 index 0000000000..e9d56bf103 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-02.js @@ -0,0 +1,14 @@ +// Test that drainAllocationsLog fails when we aren't trackingAllocationSites. + +load(libdir + 'asserts.js'); + +const root = newGlobal(); +const dbg = new Debugger(); + +dbg.memory.trackingAllocationSites = true; +root.eval("this.alloc1 = {}"); +dbg.memory.trackingAllocationSites = false; +root.eval("this.alloc2 = {};"); + +assertThrowsInstanceOf(() => dbg.memory.drainAllocationsLog(), + Error); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-03.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-03.js new file mode 100644 index 0000000000..fb0ba5e0d5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-03.js @@ -0,0 +1,24 @@ +// Test when there are more allocations than the maximum log length. + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +dbg.addDebuggee(root) + +dbg.memory.maxAllocationsLogLength = 3; +dbg.memory.trackingAllocationSites = true; + +root.eval([ + "this.alloc1 = {};", // line 1 + "this.alloc2 = {};", // line 2 + "this.alloc3 = {};", // line 3 + "this.alloc4 = {};", // line 4 +].join("\n")); + +const allocs = dbg.memory.drainAllocationsLog(); + +// Should have stayed at the maximum length. +assertEq(allocs.length, 3); +// Should have kept the most recent allocation. +assertEq(allocs[2].frame.line, 4); +// Should have thrown away the oldest allocation. +assertEq(allocs.map(x => x.frame.line).indexOf(1), -1); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-04.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-04.js new file mode 100644 index 0000000000..1a0c55f276 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-04.js @@ -0,0 +1,21 @@ +// Test that when we shorten the maximum log length, we won't get a longer log +// than that new maximum afterwards. + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +dbg.addDebuggee(root) + +dbg.memory.trackingAllocationSites = true; + +root.eval([ + "this.alloc1 = {};", // line 1 + "this.alloc2 = {};", // line 2 + "this.alloc3 = {};", // line 3 + "this.alloc4 = {};", // line 4 +].join("\n")); + +dbg.memory.maxAllocationsLogLength = 1; +const allocs = dbg.memory.drainAllocationsLog(); + +// Should have trimmed down to the new maximum length. +assertEq(allocs.length, 1); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-05.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-05.js new file mode 100644 index 0000000000..59f9094d9b --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-05.js @@ -0,0 +1,9 @@ +// Test an empty allocation log. + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +dbg.addDebuggee(root) + +dbg.memory.trackingAllocationSites = true; +const allocs = dbg.memory.drainAllocationsLog(); +assertEq(allocs.length, 0); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-06.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-06.js new file mode 100644 index 0000000000..e112cd8c0e --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-06.js @@ -0,0 +1,23 @@ +// Test doing a GC while we have a non-empty log. + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +dbg.addDebuggee(root) +dbg.memory.trackingAllocationSites = true; + +root.eval("(" + function immediate() { + this.tests = [ + ({}), + [], + /(two|2)\s*problems/, + new function Ctor(){}, + new Object(), + new Array(), + new Date(), + ]; +} + "());"); + +gc(); + +const allocs = dbg.memory.drainAllocationsLog(); +assertEq(allocs.length >= root.tests.length, true); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-07.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-07.js new file mode 100644 index 0000000000..21ffb3aaf6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-07.js @@ -0,0 +1,10 @@ +// Test retrieving the log when allocation tracking hasn't ever been enabled. + +load(libdir + 'asserts.js'); + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +dbg.addDebuggee(root) + +assertThrowsInstanceOf(() => dbg.memory.drainAllocationsLog(), + Error); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-08.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-08.js new file mode 100644 index 0000000000..78803b9aa7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-08.js @@ -0,0 +1,30 @@ +// Test retrieving the log multiple times. + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +dbg.addDebuggee(root) + +root.eval([ + "this.allocs = [];", + "this.doFirstAlloc = " + function () { + this.allocs.push({}); this.firstAllocLine = Error().lineNumber; + }, + "this.doSecondAlloc = " + function () { + this.allocs.push(new Object()); this.secondAllocLine = Error().lineNumber; + } +].join("\n")); + +dbg.memory.trackingAllocationSites = true; + +root.doFirstAlloc(); +let allocs1 = dbg.memory.drainAllocationsLog(); +root.doSecondAlloc(); +let allocs2 = dbg.memory.drainAllocationsLog(); + +let allocs1Lines = allocs1.filter(x => !!x.frame).map(x => x.frame.line); +assertEq(allocs1Lines.indexOf(root.firstAllocLine) != -1, true); +assertEq(allocs1Lines.indexOf(root.secondAllocLine) == -1, true); + +let allocs2Lines = allocs2.filter(x => !!x.frame).map(x => x.frame.line); +assertEq(allocs2Lines.indexOf(root.secondAllocLine) != -1, true); +assertEq(allocs2Lines.indexOf(root.firstAllocLine) == -1, true); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-09.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-09.js new file mode 100644 index 0000000000..62834fcc1b --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-09.js @@ -0,0 +1,20 @@ +// Test logs that contain allocations from many debuggee compartments. + +const dbg = new Debugger(); + +const root1 = newGlobal({newCompartment: true}); +const root2 = newGlobal({newCompartment: true}); +const root3 = newGlobal({newCompartment: true}); + +dbg.addDebuggee(root1); +dbg.addDebuggee(root2); +dbg.addDebuggee(root3); + +dbg.memory.trackingAllocationSites = true; + +root1.eval("this.alloc = {}"); +root2.eval("this.alloc = {}"); +root3.eval("this.alloc = {}"); + +const allocs = dbg.memory.drainAllocationsLog(); +assertEq(allocs.length >= 3, true); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-10.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-10.js new file mode 100644 index 0000000000..0bcad27a33 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-10.js @@ -0,0 +1,21 @@ +// Test logs that contain allocations from debuggee compartments added as we are +// logging. + +const dbg = new Debugger(); + +dbg.memory.trackingAllocationSites = true; + +const root1 = newGlobal({newCompartment: true}); +dbg.addDebuggee(root1); +root1.eval("this.alloc = {}"); + +const root2 = newGlobal({newCompartment: true}); +dbg.addDebuggee(root2); +root2.eval("this.alloc = {}"); + +const root3 = newGlobal({newCompartment: true}); +dbg.addDebuggee(root3); +root3.eval("this.alloc = {}"); + +const allocs = dbg.memory.drainAllocationsLog(); +assertEq(allocs.length >= 3, true); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-11.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-11.js new file mode 100644 index 0000000000..7e5d567a81 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-11.js @@ -0,0 +1,25 @@ +// Test logs that shouldn't contain allocations from debuggee compartments +// removed as we are logging. + +const dbg = new Debugger(); + +const root1 = newGlobal({newCompartment: true}); +dbg.addDebuggee(root1); +const root2 = newGlobal({newCompartment: true}); +dbg.addDebuggee(root2); +const root3 = newGlobal({newCompartment: true}); +dbg.addDebuggee(root3); + +dbg.memory.trackingAllocationSites = true; + +dbg.removeDebuggee(root1); +root1.eval("this.alloc = {}"); + +dbg.removeDebuggee(root2); +root2.eval("this.alloc = {}"); + +dbg.removeDebuggee(root3); +root3.eval("this.alloc = {}"); + +const allocs = dbg.memory.drainAllocationsLog(); +assertEq(allocs.length, 0); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-13.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-13.js new file mode 100644 index 0000000000..df294bce91 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-13.js @@ -0,0 +1,16 @@ +// |jit-test| skip-if: helperThreadCount() === 0 + +// Test that we don't crash while logging allocations and there is +// off-main-thread compilation. OMT compilation will allocate functions and +// regexps, but we just punt on measuring that accurately. + +const root = newGlobal({newCompartment: true}); +root.eval("this.dbg = new Debugger()"); +root.dbg.addDebuggee(this); +root.dbg.memory.trackingAllocationSites = true; + +offThreadCompileScript( + "function foo() {\n" + + " print('hello world');\n" + + "}" +); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-14.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-14.js new file mode 100644 index 0000000000..39b2674c31 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-14.js @@ -0,0 +1,47 @@ +// Test that drainAllocationsLog returns some timestamps. + +load(libdir + 'asserts.js'); + +var allocTimes = []; + +allocTimes.push(timeSinceCreation()); + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(root); + +dbg.memory.trackingAllocationSites = true; +root.eval("this.alloc1 = {}"); +allocTimes.push(timeSinceCreation()); +root.eval("this.alloc2 = {}"); +allocTimes.push(timeSinceCreation()); +root.eval("this.alloc3 = {}"); +allocTimes.push(timeSinceCreation()); +root.eval("this.alloc4 = {}"); +allocTimes.push(timeSinceCreation()); + +allocs = dbg.memory.drainAllocationsLog(); +assertEq(allocs.length >= 4, true); +assertEq(allocs[0].timestamp >= allocTimes[0], true); +var seenAlloc = 0; +var lastIndexSeenAllocIncremented = 0; +for (i = 1; i < allocs.length; ++i) { + assertEq(allocs[i].timestamp >= allocs[i - 1].timestamp, true); + // It isn't possible to exactly correlate the entries in the + // allocs array with the entries in allocTimes, because we can't + // control exactly how many allocations are done during the course + // of a given eval. However, we can assume that there is some + // allocation recorded after each entry in allocTimes. So, we + // track the allocTimes entry we've passed, and then after the + // loop assert that we've seen them all. We also assert that a + // non-zero number of allocations has happened since the last seen + // increment. + while (seenAlloc < allocTimes.length + && allocs[i].timestamp >= allocTimes[seenAlloc]) { + assertEq(i - lastIndexSeenAllocIncremented > 0, true); + lastIndexSeenAllocIncremented = i; + ++seenAlloc; + } +} +// There should be one entry left in allocTimes, because we recorded a +// time after the last possible allocation in the array. +assertEq(seenAlloc, allocTimes.length -1); diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-15.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-15.js new file mode 100644 index 0000000000..d8af3c9319 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-15.js @@ -0,0 +1,30 @@ +// Test drainAllocationsLog() and [[Class]] names. +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root) + +root.eval( + ` + this.tests = [ + { expected: "Object", test: () => new Object }, + { expected: "Array", test: () => [] }, + { expected: "Date", test: () => new Date }, + { expected: "RegExp", test: () => /problems/ }, + { expected: "Int8Array", test: () => new Int8Array }, + { expected: "Promise", test: () => new Promise(function (){})}, + ]; + ` +); + + +for (let { expected, test } of root.tests) { + print(expected); + + dbg.memory.trackingAllocationSites = true; + test(); + let allocs = dbg.memory.drainAllocationsLog(); + dbg.memory.trackingAllocationSites = false; + + assertEq(allocs.some(a => a.class === expected), true); +} + diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-17.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-17.js new file mode 100644 index 0000000000..b0faf83641 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-17.js @@ -0,0 +1,55 @@ +// Test drainAllocationsLog() and byte sizes. + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root); + +root.eval( + ` + function AsmModule(stdlib, foreign, heap) { + "use asm"; + + function test() { + return 5|0; + } + + return { test: test }; + } + const buf = new ArrayBuffer(1024*8); + + function Ctor() {} + this.tests = [ + { name: "new UInt8Array(256)", fn: () => new Uint8Array(256) }, + { name: "arguments", fn: function () { return arguments; } }, + { name: "asm.js module", fn: () => AsmModule(this, {}, buf) }, + { name: "/2manyproblemz/g", fn: () => /2manyproblemz/g }, + { name: "iterator", fn: () => [1,2,3][Symbol.iterator]() }, + { name: "Error()", fn: () => Error() }, + { name: "new Ctor", fn: () => new Ctor }, + { name: "{}", fn: () => ({}) }, + { name: "new Date", fn: () => new Date }, + { name: "[1,2,3]", fn: () => [1,2,3] }, + ]; + ` +); + +for (let { name, fn } of root.tests) { + print("Test: " + name); + + dbg.memory.trackingAllocationSites = true; + + fn(); + + let entries = dbg.memory.drainAllocationsLog(); + + for (let {size} of entries) { + print(" " + size + " bytes"); + // We should get some kind of byte size. We aren't testing that in depth + // here, it is tested pretty thoroughly in + // js/src/jit-test/tests/heap-analysis/byteSize-of-object.js. + assertEq(typeof size, "number"); + assertEq(size > 0, true); + } + + dbg.memory.trackingAllocationSites = false; +} diff --git a/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-18.js b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-18.js new file mode 100644 index 0000000000..6ab17714e6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-drainAllocationsLog-18.js @@ -0,0 +1,31 @@ +// |jit-test| skip-if: !('gczeal' in this) + +// Test drainAllocationsLog() entries' inNursery flag. + +gczeal(0); + +const root = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root); + +dbg.memory.trackingAllocationSites = true; + +root.eval( + ` + for (let i = 0; i < 10; i++) + allocationMarker({ nursery: true }); + + for (let i = 0; i < 10; i++) + allocationMarker({ nursery: false }); + ` +); + +let entries = dbg.memory.drainAllocationsLog().filter(e => e.class == "AllocationMarker"); + +assertEq(entries.length, 20); + +for (let i = 0; i < 10; i++) + assertEq(entries[i].inNursery, true); + +for (let i = 10; i < 20; i++) + assertEq(entries[i].inNursery, false); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-01.js b/js/src/jit-test/tests/debug/Memory-takeCensus-01.js new file mode 100644 index 0000000000..d5972a5ec6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-01.js @@ -0,0 +1,23 @@ +// Debugger.Memory.prototype.takeCensus returns a value of an appropriate shape. + +var dbg = new Debugger; + +function checkProperties(census) { + assertEq(typeof census, 'object'); + for (prop of Object.getOwnPropertyNames(census)) { + var desc = Object.getOwnPropertyDescriptor(census, prop); + assertEq(desc.enumerable, true); + assertEq(desc.configurable, true); + assertEq(desc.writable, true); + if (typeof desc.value === 'object') + checkProperties(desc.value); + else + assertEq(typeof desc.value, 'number'); + } +} + +checkProperties(dbg.memory.takeCensus()); + +var g = newGlobal({newCompartment: true}); +dbg.addDebuggee(g); +checkProperties(dbg.memory.takeCensus()); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-02.js b/js/src/jit-test/tests/debug/Memory-takeCensus-02.js new file mode 100644 index 0000000000..0ab49cb2de --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-02.js @@ -0,0 +1,49 @@ +// Debugger.Memory.prototype.takeCensus behaves plausibly as we allocate objects. + +// Exact object counts vary in ways we can't predict. For example, +// BaselineScripts can hold onto "template objects", which exist only to hold +// the shape and type for newly created objects. When BaselineScripts are +// discarded, these template objects go with them. +// +// So instead of expecting precise counts, we expect counts that are at least as +// many as we would expect given the object graph we've built. + +load(libdir + 'census.js'); + +// A Debugger with no debuggees had better not find anything. +var dbg = new Debugger; +var census0 = dbg.memory.takeCensus(); +Census.walkCensus(census0, "census0", Census.assertAllZeros); + +function newGlobalWithDefs() { + var g = newGlobal({newCompartment: true}); + g.eval(` + function times(n, fn) { + var a=[]; + for (var i = 0; i<n; i++) + a.push(fn()); + return a; + }`); + return g; +} + +// Allocate a large number of various types of objects, and check that census +// finds them. +var g = newGlobalWithDefs(); +dbg.addDebuggee(g); + +g.eval('var objs = times(100, () => ({}));'); +g.eval('var rxs = times(200, () => /foo/);'); +g.eval('var ars = times(400, () => []);'); +g.eval('var fns = times(800, () => () => {});'); + +var census1 = dbg.memory.takeCensus(); +Census.walkCensus(census1, "census1", + Census.assertAllNotLessThan( + { 'objects': + { 'Object': { count: 100 }, + 'RegExp': { count: 200 }, + 'Array': { count: 400 }, + 'Function': { count: 800 } + } + })); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-03.js b/js/src/jit-test/tests/debug/Memory-takeCensus-03.js new file mode 100644 index 0000000000..4284f642d8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-03.js @@ -0,0 +1,26 @@ +// Debugger.Memory.prototype.takeCensus behaves plausibly as we add and remove debuggees. + +load(libdir + 'census.js'); + +var dbg = new Debugger; + +var census0 = dbg.memory.takeCensus(); +Census.walkCensus(census0, "census0", Census.assertAllZeros); + +var g1 = newGlobal({newCompartment: true}); +dbg.addDebuggee(g1); +var census1 = dbg.memory.takeCensus(); +Census.walkCensus(census1, "census1", Census.assertAllNotLessThan(census0)); + +var g2 = newGlobal({newCompartment: true}); +dbg.addDebuggee(g2); +var census2 = dbg.memory.takeCensus(); +Census.walkCensus(census2, "census2", Census.assertAllNotLessThan(census1), new Set(["bytes"])); + +dbg.removeDebuggee(g2); +var census3 = dbg.memory.takeCensus(); +Census.walkCensus(census3, "census3", Census.assertAllEqual(census1), new Set(["bytes"])); + +dbg.removeDebuggee(g1); +var census4 = dbg.memory.takeCensus(); +Census.walkCensus(census4, "census4", Census.assertAllEqual(census0)); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-04.js b/js/src/jit-test/tests/debug/Memory-takeCensus-04.js new file mode 100644 index 0000000000..33fbeaa163 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-04.js @@ -0,0 +1,27 @@ +// Test that Debugger.Memory.prototype.takeCensus finds GC roots that are on the +// stack. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval(` + function withAllocationMarkerOnStack(f) { + (function () { + var onStack = allocationMarker(); + f(); + return onStack; // To prevent the JIT from optimizing out onStack. + }()) + } +`); + +assertEq("AllocationMarker" in dbg.memory.takeCensus().objects, false, + "There shouldn't exist any allocation markers in the census."); + +var allocationMarkerCount; +g.withAllocationMarkerOnStack(() => { + allocationMarkerCount = dbg.memory.takeCensus().objects.AllocationMarker.count; +}); + +assertEq(allocationMarkerCount, 1, + "Should have one allocation marker in the census, because there " + + "was one on the stack."); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-05.js b/js/src/jit-test/tests/debug/Memory-takeCensus-05.js new file mode 100644 index 0000000000..4cd99158c1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-05.js @@ -0,0 +1,14 @@ +// Test that Debugger.Memory.prototype.takeCensus finds cross compartment +// wrapper GC roots. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +assertEq("AllocationMarker" in dbg.memory.takeCensus().objects, false, + "There shouldn't exist any allocation markers in the census."); + +this.ccw = g.allocationMarker(); + +assertEq(dbg.memory.takeCensus().objects.AllocationMarker.count, 1, + "Should have one allocation marker in the census, because there " + + "is one cross-compartment wrapper referring to it."); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-06.js b/js/src/jit-test/tests/debug/Memory-takeCensus-06.js new file mode 100644 index 0000000000..51b46d7e14 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-06.js @@ -0,0 +1,115 @@ +// Check Debugger.Memory.prototype.takeCensus handling of 'breakdown' argument. + +load(libdir + 'match.js'); +var Pattern = Match.Pattern; + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +Pattern({ count: Pattern.NATURAL, + bytes: Pattern.NATURAL }) + .assert(dbg.memory.takeCensus({ breakdown: { by: 'count' } })); + +let census = dbg.memory.takeCensus({ breakdown: { by: 'count', count: false, bytes: false } }); +assertEq('count' in census, false); +assertEq('bytes' in census, false); + +census = dbg.memory.takeCensus({ breakdown: { by: 'count', count: true, bytes: false } }); +assertEq('count' in census, true); +assertEq('bytes' in census, false); + +census = dbg.memory.takeCensus({ breakdown: { by: 'count', count: false, bytes: true } }); +assertEq('count' in census, false); +assertEq('bytes' in census, true); + +census = dbg.memory.takeCensus({ breakdown: { by: 'count', count: true, bytes: true } }); +assertEq('count' in census, true); +assertEq('bytes' in census, true); + + +// Pattern doesn't mind objects with extra properties, so we'll restrict this +// list to the object classes we're pretty sure are going to stick around for +// the forseeable future. +Pattern({ + Function: { count: Pattern.NATURAL }, + Object: { count: Pattern.NATURAL }, + Debugger: { count: Pattern.NATURAL }, + global: { count: Pattern.NATURAL }, + + // The below are all Debugger prototype objects. + Source: { count: Pattern.NATURAL }, + Environment: { count: Pattern.NATURAL }, + Script: { count: Pattern.NATURAL }, + Memory: { count: Pattern.NATURAL }, + Frame: { count: Pattern.NATURAL } + }) + .assert(dbg.memory.takeCensus({ breakdown: { by: 'objectClass' } })); + +Pattern({ + objects: { count: Pattern.NATURAL }, + scripts: { count: Pattern.NATURAL }, + strings: { count: Pattern.NATURAL }, + other: { count: Pattern.NATURAL } + }) + .assert(dbg.memory.takeCensus({ breakdown: { by: 'coarseType' } })); + +// As for { by: 'objectClass' }, restrict our pattern to the types +// we predict will stick around for a long time. +Pattern({ + JSString: { count: Pattern.NATURAL }, + 'js::Shape': { count: Pattern.NATURAL }, + JSObject: { count: Pattern.NATURAL }, + }) + .assert(dbg.memory.takeCensus({ breakdown: { by: 'internalType' } })); + + +// Nested breakdowns. + +let coarse_type_pattern = { + objects: { count: Pattern.NATURAL }, + scripts: { count: Pattern.NATURAL }, + strings: { count: Pattern.NATURAL }, + other: { count: Pattern.NATURAL } +}; + +Pattern({ + JSString: coarse_type_pattern, + 'js::Shape': coarse_type_pattern, + JSObject: coarse_type_pattern, + }) + .assert(dbg.memory.takeCensus({ + breakdown: { by: 'internalType', + then: { by: 'coarseType' } + } + })); + +Pattern({ + Function: { count: Pattern.NATURAL }, + Object: { count: Pattern.NATURAL }, + Debugger: { count: Pattern.NATURAL }, + global: { count: Pattern.NATURAL }, + other: coarse_type_pattern + }) + .assert(dbg.memory.takeCensus({ + breakdown: { + by: 'objectClass', + then: { by: 'count' }, + other: { by: 'coarseType' } + } + })); + +Pattern({ + objects: { count: Pattern.NATURAL, label: "object" }, + scripts: { count: Pattern.NATURAL, label: "scripts" }, + strings: { count: Pattern.NATURAL, label: "strings" }, + other: { count: Pattern.NATURAL, label: "other" } + }) + .assert(dbg.memory.takeCensus({ + breakdown: { + by: 'coarseType', + objects: { by: 'count', label: 'object' }, + scripts: { by: 'count', label: 'scripts' }, + strings: { by: 'count', label: 'strings' }, + other: { by: 'count', label: 'other' } + } + })); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-07.js b/js/src/jit-test/tests/debug/Memory-takeCensus-07.js new file mode 100644 index 0000000000..985a47c96e --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-07.js @@ -0,0 +1,75 @@ +// Debugger.Memory.prototype.takeCensus breakdown: check error handling on +// property gets. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { get by() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + + + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'count', get count() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'count', get bytes() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + + + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'objectClass', get then() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'objectClass', get other() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + + + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'coarseType', get objects() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'coarseType', get scripts() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'coarseType', get strings() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'coarseType', get other() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); + + + +assertThrowsValue(() => { + dbg.memory.takeCensus({ + breakdown: { by: 'internalType', get then() { throw "ಠ_ಠ" } } + }); +}, "ಠ_ಠ"); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-08.js b/js/src/jit-test/tests/debug/Memory-takeCensus-08.js new file mode 100644 index 0000000000..8b8e21f81c --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-08.js @@ -0,0 +1,73 @@ +// Debugger.Memory.prototype.takeCensus: test by: 'count' breakdown + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +g.eval(` + var stuff = []; + function add(n, c) { + for (let i = 0; i < n; i++) + stuff.push(c()); + } + + let count = 0; + + function obj() { return { count: count++ }; } + obj.factor = 1; + + // This creates a closure (a function JSObject) that has captured + // a Call object. So each call creates two items. + function fun() { let v = count; return () => { return v; } } + fun.factor = 2; + + function str() { return 'perambulator' + count++; } + str.factor = 1; + + // Eval a fresh text each time, allocating: + // - a fresh ScriptSourceObject + // - a new JSScripts, not an eval cache hits + // - a fresh prototype object + // - a fresh Call object, since the eval makes 'ev' heavyweight + // - the new function itself + function ev() { + return eval(\`(function () { return \${ count++ } })\`); + } + ev.factor = 5; + + // A new object (1) with a new shape (2) with a new atom (3) + function shape() { return { [ 'theobroma' + count++ ]: count }; } + shape.factor = 3; + `); + +let baseline = 0; +function countIncreasedByAtLeast(n) { + let oldBaseline = baseline; + + // Since a census counts only reachable objects, one might assume that calling + // GC here would have no effect on the census results. But GC also throws away + // JIT code and any objects it might be holding (template objects, say); + // takeCensus reaches those. Shake everything loose that we can, to make the + // census approximate reachability a bit more closely, and make our results a + // bit more predictable. + gc(g, 'shrinking'); + + baseline = dbg.memory.takeCensus({ breakdown: { by: 'count' } }).count; + return baseline >= oldBaseline + n; +} + +countIncreasedByAtLeast(0); + +g.add(100, g.obj); +assertEq(countIncreasedByAtLeast(g.obj.factor * 100), true); + +g.add(100, g.fun); +assertEq(countIncreasedByAtLeast(g.fun.factor * 100), true); + +g.add(100, g.str); +assertEq(countIncreasedByAtLeast(g.str.factor * 100), true); + +g.add(100, g.ev); +assertEq(countIncreasedByAtLeast(g.ev.factor * 100), true); + +g.add(100, g.shape); +assertEq(countIncreasedByAtLeast(g.shape.factor * 100), true); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-09.js b/js/src/jit-test/tests/debug/Memory-takeCensus-09.js new file mode 100644 index 0000000000..ff4823ee7a --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-09.js @@ -0,0 +1,74 @@ +// Debugger.Memory.prototype.takeCensus: by: allocationStack breakdown + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.evaluate(` + var log = []; + function f() { log.push(allocationMarker()); } + function g() { f(); } + function h() { f(); } + `, + { fileName: "Rockford", lineNumber: 1000 }); + +// Create one allocationMarker with tracking turned off, +// so it will have no associated stack. +g.f(); + +dbg.memory.allocationSamplingProbability = 1; +dbg.memory.trackingAllocationSites = true; + +for ([f, n] of [[g.f, 20], [g.g, 10], [g.h, 5]]) + for (let i = 0; i < n; i++) + f(); // all allocations of allocationMarker occur with this line as the + // oldest stack frame. + +let census = dbg.memory.takeCensus({ breakdown: { by: 'objectClass', + then: { by: 'allocationStack', + then: { by: 'count', + label: 'haz stack' + }, + noStack: { by: 'count', + label: 'no haz stack' + } + } + } + }); + +let map = census.AllocationMarker; +assertEq(map instanceof Map, true); + +// Gather the stacks we are expecting to appear as keys, and +// check that there are no unexpected keys. +let stacks = { }; + +map.forEach((v, k) => { + if (k === 'noStack') { + // No need to save this key. + } else if (k.functionDisplayName === 'f' && + k.parent.functionDisplayName === null) { + stacks.f = k; + } else if (k.functionDisplayName === 'f' && + k.parent.functionDisplayName === 'g' && + k.parent.parent.functionDisplayName === null) { + stacks.fg = k; + } else if (k.functionDisplayName === 'f' && + k.parent.functionDisplayName === 'h' && + k.parent.parent.functionDisplayName === null) { + stacks.fh = k; + } else { + assertEq(true, false); + } +}); + +assertEq(map.get('noStack').label, 'no haz stack'); +assertEq(map.get('noStack').count, 1); + +assertEq(map.get(stacks.f).label, 'haz stack'); +assertEq(map.get(stacks.f).count, 20); + +assertEq(map.get(stacks.fg).label, 'haz stack'); +assertEq(map.get(stacks.fg).count, 10); + +assertEq(map.get(stacks.fh).label, 'haz stack'); +assertEq(map.get(stacks.fh).count, 5); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-10.js b/js/src/jit-test/tests/debug/Memory-takeCensus-10.js new file mode 100644 index 0000000000..abe01d56d4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-10.js @@ -0,0 +1,57 @@ +// Check byte counts produced by takeCensus. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +let sizeOfAM = byteSize(allocationMarker()); + +// Allocate a single allocation marker, and check that we can find it. +g.eval('var hold = allocationMarker();'); +let census = dbg.memory.takeCensus({ breakdown: { by: 'objectClass' } }); +assertEq(census.AllocationMarker.count, 1); +assertEq(census.AllocationMarker.bytes, sizeOfAM); + +g.evaluate(` + var objs = []; + function fnerd() { + objs.push(allocationMarker()); + for (let i = 0; i < 10; i++) + objs.push(allocationMarker()); + } + `, + { fileName: 'J. Edgar Hoover', lineNumber: 2000 }); + +dbg.memory.allocationSamplingProbability = 1; +dbg.memory.trackingAllocationSites = true; + +g.hold = null; +g.fnerd(); + +census = dbg.memory.takeCensus({ + breakdown: { by: 'objectClass', + then: { by: 'allocationStack' } + } +}); + +let seen = 0; +census.AllocationMarker.forEach((v, k) => { + assertEq(k.functionDisplayName, 'fnerd'); + assertEq(k.source, 'J. Edgar Hoover'); + switch (k.line) { + case 2003: + assertEq(v.count, 1); + assertEq(v.bytes, sizeOfAM); + seen++; + break; + + case 2005: + assertEq(v.count, 10); + assertEq(v.bytes, 10 * sizeOfAM); + seen++; + break; + + default: assertEq(true, false); + } +}); + +assertEq(seen, 2); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-11.js b/js/src/jit-test/tests/debug/Memory-takeCensus-11.js new file mode 100644 index 0000000000..06accbe74d --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-11.js @@ -0,0 +1,49 @@ +// |jit-test| skip-if: isLcovEnabled() + +// NOTE: Code coverage keeps top-level script alive even if normally it would be +// GC'd. Skip this test in that case. + +// Check byte counts produced by takeCensus. + +const g = newGlobal({newCompartment: true, disableLazyParsing: true }); +g.eval("setJitCompilerOption('ion.warmup.trigger', 1000)"); + +const dbg = new Debugger(g); + +g.evaluate("function one() {}", { fileName: "one.js" }); +g.evaluate(`function two1() {} + function two2() {}`, + { fileName: "two.js" }); +g.evaluate(`function three1() {} + function three2() {} + function three3() {}`, + { fileName: "three.js" }); + +const report = dbg.memory.takeCensus({ + breakdown: { + by: "coarseType", + scripts: { + by: "filename", + then: { by: "count", count: true, bytes: false }, + noFilename: { + by: "internalType", + then: { by: "count", count: true, bytes: false } + } + }, + + // Not really interested in these, but they're here for completeness. + objects: { by: "count", count: true, byte: false }, + strings: { by: "count", count: true, byte: false }, + other: { by: "count", count: true, byte: false }, + } +}); + +print(JSON.stringify(report, null, 4)); + +assertEq(report.scripts["one.js"].count, 1); +assertEq(report.scripts["two.js"].count, 2); +assertEq(report.scripts["three.js"].count, 3); + +const noFilename = report.scripts.noFilename; +assertEq(!!noFilename, true); +assertEq(typeof noFilename, "object"); diff --git a/js/src/jit-test/tests/debug/Memory-takeCensus-12.js b/js/src/jit-test/tests/debug/Memory-takeCensus-12.js new file mode 100644 index 0000000000..cb4f64ebc5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-takeCensus-12.js @@ -0,0 +1,61 @@ +// Sanity test that we can accumulate matching individuals in a bucket. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var bucket = { by: "bucket" }; +var count = { by: "count", count: true, bytes: false }; + +var all = dbg.memory.takeCensus({ breakdown: bucket }); +var allCount = dbg.memory.takeCensus({ breakdown: count }).count; + +var coarse = dbg.memory.takeCensus({ + breakdown: { + by: "coarseType", + objects: bucket, + strings: bucket, + scripts: bucket, + other: bucket + } +}); +var coarseCount = dbg.memory.takeCensus({ + breakdown: { + by: "coarseType", + objects: count, + strings: count, + scripts: count, + other: count + } +}); + +assertEq(all.length > 0, true); +assertEq(all.length, allCount); + +assertEq(coarse.objects.length > 0, true); +assertEq(coarseCount.objects.count, coarse.objects.length); + +assertEq(coarse.strings.length > 0, true); +assertEq(coarseCount.strings.count, coarse.strings.length); + +assertEq(coarse.scripts.length > 0, true); +assertEq(coarseCount.scripts.count, coarse.scripts.length); + +assertEq(coarse.other.length > 0, true); +assertEq(coarseCount.other.count, coarse.other.length); + +assertEq(all.length >= coarse.objects.length, true); +assertEq(all.length >= coarse.strings.length, true); +assertEq(all.length >= coarse.scripts.length, true); +assertEq(all.length >= coarse.other.length, true); + +function assertIsIdentifier(id) { + assertEq(id, Math.floor(id)); + assertEq(id > 0, true); + assertEq(id <= Math.pow(2, 48), true); +} + +all.forEach(assertIsIdentifier); +coarse.objects.forEach(assertIsIdentifier); +coarse.strings.forEach(assertIsIdentifier); +coarse.scripts.forEach(assertIsIdentifier); +coarse.other.forEach(assertIsIdentifier); diff --git a/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-01.js b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-01.js new file mode 100644 index 0000000000..46c0e1c34b --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-01.js @@ -0,0 +1,37 @@ +// Test that we can track allocation sites by setting +// Debugger.Memory.prototype.trackingAllocationSites to true and then get the +// allocation site via Debugger.Object.prototype.allocationSite. + +const root = newGlobal({newCompartment: true}); + +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root); + +assertEq(dbg.memory.trackingAllocationSites, false); +dbg.memory.trackingAllocationSites = true; +assertEq(dbg.memory.trackingAllocationSites, true); + +root.eval("(" + function immediate() { + this.tests = [ + { name: "object literal", object: ({}), line: Error().lineNumber }, + { name: "array literal", object: [], line: Error().lineNumber }, + { name: "regexp literal", object: /(two|2)\s*problems/, line: Error().lineNumber }, + { name: "new constructor", object: new function Ctor(){}, line: Error().lineNumber }, + { name: "new Object", object: new Object(), line: Error().lineNumber }, + { name: "new Array", object: new Array(), line: Error().lineNumber }, + { name: "new Date", object: new Date(), line: Error().lineNumber } + ]; +} + "());"); + +dbg.memory.trackingAllocationSites = false; +assertEq(dbg.memory.trackingAllocationSites, false); + +for (let { name, object, line } of root.tests) { + print("Entering test: " + name); + + let wrappedObject = wrappedRoot.makeDebuggeeValue(object); + let allocationSite = wrappedObject.allocationSite; + print("Allocation site: " + allocationSite); + + assertEq(allocationSite.line, line); +} diff --git a/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-02.js b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-02.js new file mode 100644 index 0000000000..5fa91f002a --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-02.js @@ -0,0 +1,19 @@ +// Test that we don't get allocation sites when nobody has asked for them. + +const root = newGlobal({newCompartment: true}); + +const dbg = new Debugger(); +const wrappedRoot = dbg.addDebuggee(root); + +dbg.memory.trackingAllocationSites = true; +root.eval("this.obj = {};"); +dbg.memory.trackingAllocationSites = false; +root.eval("this.obj2 = {};"); + +let wrappedObj = wrappedRoot.makeDebuggeeValue(root.obj); +let allocationSite = wrappedObj.allocationSite; +assertEq(allocationSite != null && typeof allocationSite == "object", true); + +let wrappedObj2 = wrappedRoot.makeDebuggeeValue(root.obj2); +let allocationSite2 = wrappedObj2.allocationSite; +assertEq(allocationSite2, null); diff --git a/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js new file mode 100644 index 0000000000..62205b8249 --- /dev/null +++ b/js/src/jit-test/tests/debug/Memory-trackingAllocationSites-03.js @@ -0,0 +1,76 @@ +// Test that multiple Debuggers behave reasonably. + +load(libdir + "asserts.js"); + +let dbg1, dbg2, root1, root2; + +function isTrackingAllocations(global, dbgObj) { + const site = dbgObj.makeDebuggeeValue(global.eval("({})")).allocationSite; + if (site) { + assertEq(typeof site, "object"); + } + return !!site; +} + +function test(name, fn) { + print(); + print(name); + + // Reset state. + root1 = newGlobal({newCompartment: true}); + root2 = newGlobal({newCompartment: true}); + dbg1 = new Debugger; + dbg2 = new Debugger; + + // Run the test. + fn(); + + print(" OK"); +} + +test("Can track allocations even if a different debugger is already tracking " + + "them.", + () => { + let d1r1 = dbg1.addDebuggee(root1); + let d2r1 = dbg2.addDebuggee(root1); + dbg1.memory.trackingAllocationSites = true; + dbg2.memory.trackingAllocationSites = true; + assertEq(isTrackingAllocations(root1, d1r1), true); + assertEq(isTrackingAllocations(root1, d2r1), true); + }); + +test("Removing root1 as a debuggee from all debuggers should disable the " + + "allocation hook.", + () => { + dbg1.memory.trackingAllocationSites = true; + let d1r1 = dbg1.addDebuggee(root1); + dbg1.removeAllDebuggees(); + assertEq(isTrackingAllocations(root1, d1r1), false); + }); + +test("Adding a new debuggee to a debugger that is tracking allocations should " + + "enable the hook for the new debuggee.", + () => { + dbg1.memory.trackingAllocationSites = true; + let d1r1 = dbg1.addDebuggee(root1); + assertEq(isTrackingAllocations(root1, d1r1), true); + }); + +test("Setting trackingAllocationSites to true should throw if the debugger " + + "cannot install the allocation hooks for *every* debuggee.", + () => { + let d1r1 = dbg1.addDebuggee(root1); + let d1r2 = dbg1.addDebuggee(root2); + + // Can't install allocation hooks for root2 with this set. + root2.enableShellAllocationMetadataBuilder(); + + assertThrowsInstanceOf(() => dbg1.memory.trackingAllocationSites = true, + Error); + + // And after it throws, its trackingAllocationSites accessor should reflect that + // allocation site tracking is still disabled in that Debugger. + assertEq(dbg1.memory.trackingAllocationSites, false); + assertEq(isTrackingAllocations(root1, d1r1), false); + assertEq(isTrackingAllocations(root2, d1r2), false); + }); diff --git a/js/src/jit-test/tests/debug/Object-01.js b/js/src/jit-test/tests/debug/Object-01.js new file mode 100644 index 0000000000..fa28890c1c --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-01.js @@ -0,0 +1,17 @@ +// Debugger.Object basics + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.arguments[0], frame.callee); + assertEq(Object.getPrototypeOf(frame.arguments[0]), Debugger.Object.prototype); + assertEq(frame.arguments[0] instanceof Debugger.Object, true); + assertEq(frame.arguments[0] !== frame.arguments[1], true); + assertEq(Object.getPrototypeOf(frame.arguments[1]), Debugger.Object.prototype); + assertEq(frame.arguments[1] instanceof Debugger.Object, true); + hits++; +}; + +g.eval("var obj = {}; function f(a, b) { debugger; } f(f, obj);"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Object-02.js b/js/src/jit-test/tests/debug/Object-02.js new file mode 100644 index 0000000000..b5111cb8bd --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-02.js @@ -0,0 +1,13 @@ +// Debugger.Object referents can be transparent wrappers of objects in the debugger compartment. + +var g = newGlobal({newCompartment: true}); +g.f = function (a, b) { return a + "/" + b; }; +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var f = frame.eval("f").return; + assertEq(f.call(null, "a", "b").return, "a/b"); + hits++; +}; +g.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Object-apply-01.js b/js/src/jit-test/tests/debug/Object-apply-01.js new file mode 100644 index 0000000000..4abc32bfe1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-apply-01.js @@ -0,0 +1,59 @@ +// tests calling script functions via Debugger.Object.prototype.apply/call + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +var dbg = new Debugger(g); + +var hits = 0; +function test(usingApply) { + dbg.onDebuggerStatement = function (frame) { + var fn = frame.arguments[0]; + var cv = usingApply ? fn.apply(null, [9, 16]) : fn.call(null, 9, 16); + assertEq(Object.keys(cv).join(","), "return"); + assertEq(Object.getPrototypeOf(cv), Object.prototype); + assertEq(cv.return, 25); + + cv = usingApply ? fn.apply(null, ["hello ", "world"]) : fn.call(null, "hello ", "world"); + assertEq(Object.keys(cv).join(","), "return"); + assertEq(cv.return, "hello world"); + + // Handle more or less arguments. + assertEq((usingApply ? fn.apply(null, [1, 5, 100]) : fn.call(null, 1, 5, 100)).return, 6); + assertEq((usingApply ? fn.apply(null, []) : fn.call(null)).return, NaN); + assertEq((usingApply ? fn.apply() : fn.call()).return, NaN); + + // Throw if a this-value or argument is an object but not a Debugger.Object. + assertThrowsInstanceOf(function () { usingApply ? fn.apply({}, []) : fn.call({}); }, + TypeError); + assertThrowsInstanceOf(function () { usingApply ? fn.apply(null, [{}]) : fn.call(null, {}); }, + TypeError); + hits++; + }; + g.eval("f(function (a, b) { return a + b; });"); + + // The callee receives the right arguments even if more arguments are provided + // than the callee's .length. + dbg.onDebuggerStatement = function (frame) { + assertEq((usingApply ? frame.arguments[0].apply(null, ['one', 'two']) + : frame.arguments[0].call(null, 'one', 'two')).return, + 2); + hits++; + }; + g.eval("f(function () { return arguments.length; });"); + + // Exceptions are reported as {throw,stack} completion values. + dbg.onDebuggerStatement = function (frame) { + var lose = frame.arguments[0]; + var cv = usingApply ? lose.apply(null, []) : lose.call(null); + assertEq(Object.keys(cv).join(","), "throw,stack"); + assertEq(cv.throw, frame.callee); + hits++; + }; + g.eval("f(function lose() { throw f; });"); +} + +test(true); +test(false); +assertEq(hits, 6); diff --git a/js/src/jit-test/tests/debug/Object-apply-02.js b/js/src/jit-test/tests/debug/Object-apply-02.js new file mode 100644 index 0000000000..18ce4468a2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-apply-02.js @@ -0,0 +1,58 @@ +// tests calling native functions via Debugger.Object.prototype.apply/call + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +var dbg = new Debugger(g); + +function test(usingApply) { + dbg.onDebuggerStatement = function (frame) { + var max = frame.arguments[0]; + var cv = usingApply ? max.apply(null, [9, 16]) : max.call(null, 9, 16); + assertEq(cv.return, 16); + + cv = usingApply ? max.apply() : max.call(); + assertEq(cv.return, -1/0); + + cv = usingApply ? max.apply(null, [2, 5, 3, 8, 1, 9, 4, 6, 7]) + : max.call(null, 2, 5, 3, 8, 1, 9, 4, 6, 7); + assertEq(cv.return, 9); + + // second argument to apply must be an array + assertThrowsInstanceOf(function () { max.apply(null, 12); }, TypeError); + }; + g.eval("f(Math.max);"); + + dbg.onDebuggerStatement = function (frame) { + var push = frame.arguments[0]; + var arr = frame.arguments[1]; + var cv; + + cv = usingApply ? push.apply(arr, [0, 1, 2]) : push.call(arr, 0, 1, 2); + assertEq(cv.return, 3); + + cv = usingApply ? push.apply(arr, [arr]) : push.call(arr, arr); + assertEq(cv.return, 4); + + cv = usingApply ? push.apply(arr) : push.call(arr); + assertEq(cv.return, 4); + + // You can apply Array.prototype.push to a string; it does ToObject on + // it. But as the length property on String objects is non-writable, + // attempting to increase the length will throw a TypeError. + cv = usingApply + ? push.apply("hello", ["world"]) + : push.call("hello", "world"); + assertEq("throw" in cv, true); + var ex = cv.throw; + assertEq(frame.evalWithBindings("ex instanceof TypeError", { ex: ex }).return, true); + }; + g.eval("var a = []; f(Array.prototype.push, a);"); + assertEq(g.a.length, 4); + assertEq(g.a.slice(0, 3).join(","), "0,1,2"); + assertEq(g.a[3], g.a); +} + +test(true); +test(false); diff --git a/js/src/jit-test/tests/debug/Object-apply-03.js b/js/src/jit-test/tests/debug/Object-apply-03.js new file mode 100644 index 0000000000..f23cc46518 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-apply-03.js @@ -0,0 +1,21 @@ +// reentering the debugger several times via onDebuggerStatement and apply/call on a single stack + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +function test(usingApply) { + dbg.onDebuggerStatement = function (frame) { + var n = frame.arguments[0]; + if (n > 1) { + var result = usingApply ? frame.callee.apply(null, [n - 1]) + : frame.callee.call(null, n - 1); + result.return *= n; + return result; + } + }; + g.eval("function fac(n) { debugger; return 1; }"); + assertEq(g.fac(5), 5 * 4 * 3 * 2 * 1); +} + +test(true); +test(false); diff --git a/js/src/jit-test/tests/debug/Object-apply-04.js b/js/src/jit-test/tests/debug/Object-apply-04.js new file mode 100644 index 0000000000..04115344b2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-apply-04.js @@ -0,0 +1,15 @@ +// Debugger.Object.prototype.apply/call works with function proxies + +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var proxy = frame.arguments[0]; + assertEq(proxy.name, undefined); + assertEq(proxy.apply(null, [33]).return, 34); + assertEq(proxy.call(null, 33).return, 34); + hits++; +}; +g.eval("f(new Proxy(function (arg) { return arg + 1; }, {}));"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Object-asEnvironment-01.js b/js/src/jit-test/tests/debug/Object-asEnvironment-01.js new file mode 100644 index 0000000000..39bfe4dabf --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-asEnvironment-01.js @@ -0,0 +1,15 @@ +// Tests D.O.asEnvironment() returning the global lexical scope. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +g.evaluate(` + var x = 42; + let y = "foo" +`); + +var globalLexical = gw.asEnvironment(); +assertEq(globalLexical.names().length, 1); +assertEq(globalLexical.getVariable("y"), "foo"); +assertEq(globalLexical.parent.getVariable("x"), 42); diff --git a/js/src/jit-test/tests/debug/Object-boundTargetFunction-01.js b/js/src/jit-test/tests/debug/Object-boundTargetFunction-01.js new file mode 100644 index 0000000000..f87a0abc13 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-boundTargetFunction-01.js @@ -0,0 +1,26 @@ +// Smoke tests for bound function things. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var arrw = gw.executeInGlobal("var arr = []; arr;").return; +var pushw = gw.executeInGlobal("var push = arr.push.bind(arr); push;").return; +assertEq(pushw.isBoundFunction, true); +assertEq(pushw.boundThis, arrw); +assertEq(pushw.boundArguments.length, 0); + +var arr2 = gw.executeInGlobal("var arr2 = []; arr2").return; +assertEq(pushw.call(arr2, "tuesday").return, 1); +g.eval("assertEq(arr.length, 1);"); +g.eval("assertEq(arr[0], 'tuesday');"); +g.eval("assertEq(arr2.length, 0);"); + +g.eval("push = arr.push.bind(arr, 1, 'seven', {x: 'q'});"); +pushw = gw.getOwnPropertyDescriptor("push").value; +assertEq(pushw.isBoundFunction, true); +var args = pushw.boundArguments; +assertEq(args.length, 3); +assertEq(args[0], 1); +assertEq(args[1], 'seven'); +assertEq(args[2] instanceof Debugger.Object, true); +assertEq(args[2].getOwnPropertyDescriptor("x").value, "q"); diff --git a/js/src/jit-test/tests/debug/Object-boundTargetFunction-02.js b/js/src/jit-test/tests/debug/Object-boundTargetFunction-02.js new file mode 100644 index 0000000000..74676c435e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-boundTargetFunction-02.js @@ -0,0 +1,25 @@ +// Test that bound function accessors work on: +// - an ordinary non-bound function; +// - a native function; +// - and an object that isn't a function at all. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var fw = gw.executeInGlobal("function f() {}; f").return; +assertEq(fw.isBoundFunction, false); +assertEq(fw.boundThis, undefined); +assertEq(fw.boundArguments, undefined); +assertEq(fw.boundTargetFunction, undefined); + +var nw = gw.executeInGlobal("var n = Math.max; n").return; +assertEq(nw.isBoundFunction, false); +assertEq(nw.boundThis, undefined); +assertEq(fw.boundArguments, undefined); +assertEq(nw.boundTargetFunction, undefined); + +var ow = gw.executeInGlobal("var o = {}; o").return; +assertEq(ow.isBoundFunction, undefined); +assertEq(ow.boundThis, undefined); +assertEq(fw.boundArguments, undefined); +assertEq(ow.boundTargetFunction, undefined); diff --git a/js/src/jit-test/tests/debug/Object-boundTargetFunction-03.js b/js/src/jit-test/tests/debug/Object-boundTargetFunction-03.js new file mode 100644 index 0000000000..2fb45f3822 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-boundTargetFunction-03.js @@ -0,0 +1,20 @@ +// Test that inspecting a bound function that was bound again does the right +// thing. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var expr = "function f() { return this; }; var bf = f.bind(1, 2).bind(3, 4); bf"; +var bfw = gw.executeInGlobal(expr).return; + +assertEq(bfw.isBoundFunction, true); +assertEq(bfw.boundThis, 3); +var args = bfw.boundArguments; +assertEq(args.length, 1); +assertEq(args[0], 4); + +assertEq(bfw.boundTargetFunction.isBoundFunction, true); +assertEq(bfw.boundTargetFunction.boundThis, 1); +args = bfw.boundTargetFunction.boundArguments; +assertEq(args.length, 1); +assertEq(args[0], 2); diff --git a/js/src/jit-test/tests/debug/Object-callable.js b/js/src/jit-test/tests/debug/Object-callable.js new file mode 100644 index 0000000000..31130b0175 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-callable.js @@ -0,0 +1,18 @@ +// Test Debugger.Object.prototype.callable. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.arguments[0].callable, frame.arguments[1]); + hits++; +}; + +g.eval("function f(obj, iscallable) { debugger; }"); + +g.eval("f({}, false);"); +g.eval("f(Function.prototype, true);"); +g.eval("f(f, true);"); +g.eval("f(new Proxy({}, {}), false);"); +g.eval("f(new Proxy(f, {}), true);"); +assertEq(hits, 5); diff --git a/js/src/jit-test/tests/debug/Object-class.js b/js/src/jit-test/tests/debug/Object-class.js new file mode 100644 index 0000000000..ee64998190 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-class.js @@ -0,0 +1,26 @@ +// Basic tests for Debugger.Object.prototype.class. +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +g.eval('function f() { debugger; }'); + +dbg.onDebuggerStatement = function (frame) { + var arr = frame.arguments; + assertEq(arr[0].class, "Object"); + assertEq(arr[1].class, "Array"); + assertEq(arr[2].class, "Function"); + assertEq(arr[3].class, "Date"); + assertEq(arr[4].class, "Object"); + assertEq(arr[5].class, "Function"); + assertEq(arr[6].class, "Object"); + hits++; +}; +g.f(Object.prototype, [], eval, new Date, + new Proxy({}, {}), new Proxy(eval, {}), new Proxy(new Date, {})); +assertEq(hits, 1); + +// Debugger.Object.prototype.class should see through cross-compartment +// wrappers. +g.eval('f(Object.prototype, [], eval, new Date,\ + new Proxy({}, {}), new Proxy(f, {}), new Proxy(new Date, {}));'); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Object-createSource.js b/js/src/jit-test/tests/debug/Object-createSource.js new file mode 100644 index 0000000000..e71bd702e1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-createSource.js @@ -0,0 +1,18 @@ +// createSource creates new sources. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); +let gdbg = dbg.addDebuggee(g); + +let source = gdbg.createSource({ + text: "x = 3", + url: "foo.js", + startLine: 3, + sourceMapURL: "sourceMapURL", + isScriptElement: true, +}); +assertEq(source.text, "x = 3"); +assertEq(source.url, "foo.js"); +assertEq(source.startLine, 3); +assertEq(source.sourceMapURL, "sourceMapURL"); +assertEq(source.introductionType, "inlineScript"); diff --git a/js/src/jit-test/tests/debug/Object-defineProperties-01.js b/js/src/jit-test/tests/debug/Object-defineProperties-01.js new file mode 100644 index 0000000000..36bf55c7a7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperties-01.js @@ -0,0 +1,46 @@ +// Debug.Object.prototype.defineProperties. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +var descProps = ['configurable', 'enumerable', 'writable', 'value', 'get', 'set']; +function test(objexpr, descs) { + g.eval("obj = (" + objexpr + ");"); + var gobjw = gw.getOwnPropertyDescriptor("obj").value; + gobjw.defineProperties(descs); + + var indirectEval = eval; + var obj = indirectEval("(" + objexpr + ");"); + Object.defineProperties(obj, descs); + + var ids = Object.keys(descs); + for (var i = 0; i < ids.length; i++) { + var actual = gobjw.getOwnPropertyDescriptor(ids[i]); + var expected = Object.getOwnPropertyDescriptor(obj, ids[i]); + assertEq(Object.getPrototypeOf(actual), Object.prototype); + assertEq(actual.configurable, expected.configurable); + assertEq(actual.enumerable, expected.enumerable); + for (var j = 0; j < descProps; j++) { + var prop = descProps[j]; + assertEq(prop in actual, prop in expected); + assertEq(actual[prop], expected[prop]); + } + } +} + +test("{}", {}); +test("/abc/", {}); + +g.eval("var aglobal = newGlobal('same-compartment');"); +var aglobal = newGlobal('same-compartment'); +test("aglobal", {}); + +var adescs = {a: {enumerable: true, writable: true, value: 0}}; +test("{}", adescs); +test("{a: 1}", adescs); + +var arrdescs = [{value: 'a'}, {value: 'b'}, , {value: 'd'}]; +test("{}", arrdescs); +test("[]", arrdescs); +test("[0, 1, 2, 3]", arrdescs); diff --git a/js/src/jit-test/tests/debug/Object-defineProperties-02.js b/js/src/jit-test/tests/debug/Object-defineProperties-02.js new file mode 100644 index 0000000000..1383781004 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperties-02.js @@ -0,0 +1,33 @@ +// Exceptions thrown by obj.defineProperties are copied into the debugger compartment. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +function test(objexpr, descs) { + var exca, excb; + + g.eval("obj = (" + objexpr + ");"); + var gobjw = gw.getOwnPropertyDescriptor("obj").value; + try { + gobjw.defineProperties(descs); + } catch (exc) { + exca = exc; + } + + var indirectEval = eval; + var obj = indirectEval("(" + objexpr + ");"); + try { + Object.defineProperties(obj, descs); + } catch (exc) { + excb = exc; + } + + assertEq(Object.getPrototypeOf(exca), Object.getPrototypeOf(excb)); + assertEq(exca.message, excb.message); + assertEq(typeof exca.fileName, "string"); + assertEq(typeof exca.stack, "string"); +} + +test("Object.create(null, {p: {value: 1}})", {p: {value: 2}}); +test("({})", {x: {get: 'bad'}}); diff --git a/js/src/jit-test/tests/debug/Object-defineProperties-03.js b/js/src/jit-test/tests/debug/Object-defineProperties-03.js new file mode 100644 index 0000000000..ad5b5c0f3f --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperties-03.js @@ -0,0 +1,20 @@ +// obj.defineProperties can define accessor properties. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.value = undefined; +g.eval("function gf() { return 12; }\n" + + "function sf(v) { value = v; }\n"); +var gfw = gw.getOwnPropertyDescriptor("gf").value; +var sfw = gw.getOwnPropertyDescriptor("sf").value; +gw.defineProperties({x: {configurable: true, get: gfw, set: sfw}}); +assertEq(g.x, 12); +g.x = 'ok'; +assertEq(g.value, 'ok'); + +var desc = g.Object.getOwnPropertyDescriptor(g, "x"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, false); +assertEq(desc.get, g.gf); +assertEq(desc.set, g.sf); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-01.js b/js/src/jit-test/tests/debug/Object-defineProperty-01.js new file mode 100644 index 0000000000..91330694b9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-01.js @@ -0,0 +1,12 @@ +// obj.defineProperty can define simple data properties. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gobj = dbg.addDebuggee(g); +gobj.defineProperty("x", {configurable: true, enumerable: true, writable: true, value: 'ok'}); +assertEq(g.x, 'ok'); + +var desc = g.Object.getOwnPropertyDescriptor(g, "x"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, true); +assertEq(desc.writable, true); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-02.js b/js/src/jit-test/tests/debug/Object-defineProperty-02.js new file mode 100644 index 0000000000..e72fd7f2bb --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-02.js @@ -0,0 +1,10 @@ +// obj.defineProperty can define a data property with object value. + +var g = newGlobal({newCompartment: true}); +g.eval("var a = {};"); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var desc = gw.getOwnPropertyDescriptor("a"); +assertEq(desc.value instanceof Debugger.Object, true); +gw.defineProperty("b", desc); +assertEq(g.a, g.b); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-03.js b/js/src/jit-test/tests/debug/Object-defineProperty-03.js new file mode 100644 index 0000000000..04a2aa8174 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-03.js @@ -0,0 +1,21 @@ +// defineProperty can set array elements + +var g = newGlobal({newCompartment: true}); +g.a = g.Array(0, 1, 2); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var aw = gw.getOwnPropertyDescriptor("a").value; + +aw.defineProperty(0, {value: 'ok0'}); // by number +assertEq(g.a[0], 'ok0'); +var desc = g.Object.getOwnPropertyDescriptor(g.a, "0"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, true); +assertEq(desc.writable, true); + +aw.defineProperty("1", {value: 'ok1'}); // by string +assertEq(g.a[1], 'ok1'); +desc = g.Object.getOwnPropertyDescriptor(g.a, "1"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, true); +assertEq(desc.writable, true); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-04.js b/js/src/jit-test/tests/debug/Object-defineProperty-04.js new file mode 100644 index 0000000000..023a0cbf83 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-04.js @@ -0,0 +1,9 @@ +// defineProperty can add array elements, bumping length + +var g = newGlobal({newCompartment: true}); +g.a = g.Array(0, 1, 2); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var aw = gw.getOwnPropertyDescriptor("a").value; +aw.defineProperty(3, {configurable: true, enumerable: true, writable: true, value: 3}); +assertEq(g.a.length, 4); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-05.js b/js/src/jit-test/tests/debug/Object-defineProperty-05.js new file mode 100644 index 0000000000..be494faaf5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-05.js @@ -0,0 +1,20 @@ +// defineProperty can define accessor properties. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.value = undefined; +g.eval("function gf() { return 12; }\n" + + "function sf(v) { value = v; }\n"); +var gfw = gw.getOwnPropertyDescriptor("gf").value; +var sfw = gw.getOwnPropertyDescriptor("sf").value; +gw.defineProperty("x", {configurable: true, get: gfw, set: sfw}); +assertEq(g.x, 12); +g.x = 'ok'; +assertEq(g.value, 'ok'); + +var desc = g.Object.getOwnPropertyDescriptor(g, "x"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, false); +assertEq(desc.get, g.gf); +assertEq(desc.set, g.sf); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-06.js b/js/src/jit-test/tests/debug/Object-defineProperty-06.js new file mode 100644 index 0000000000..0739746bd7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-06.js @@ -0,0 +1,21 @@ +// obj.defineProperty with vague descriptors works like Object.defineProperty + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +gw.defineProperty("p", {configurable: true, enumerable: true}); +assertEq(g.p, undefined); +var desc = g.Object.getOwnPropertyDescriptor(g, "p"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, true); +assertEq(desc.value, undefined); +assertEq(desc.writable, false); + +gw.defineProperty("q", {}); +assertEq(g.q, undefined); +var desc = g.Object.getOwnPropertyDescriptor(g, "q"); +assertEq(desc.configurable, false); +assertEq(desc.enumerable, false); +assertEq(desc.value, undefined); +assertEq(desc.writable, false); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-07.js b/js/src/jit-test/tests/debug/Object-defineProperty-07.js new file mode 100644 index 0000000000..8565c47357 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-07.js @@ -0,0 +1,10 @@ +// obj.defineProperty throws if a value, getter, or setter is not a debuggee value. + +load(libdir + "asserts.js"); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gobj = dbg.addDebuggee(g); +assertThrowsInstanceOf(function () { gobj.defineProperty('x', {value: {}}); }, TypeError); +assertThrowsInstanceOf(function () { gobj.defineProperty('x', {get: Number}); }, TypeError); +assertThrowsInstanceOf(function () { gobj.defineProperty('x', {get: gobj, set: Number}) }, + TypeError); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-08.js b/js/src/jit-test/tests/debug/Object-defineProperty-08.js new file mode 100644 index 0000000000..fb694719e3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-08.js @@ -0,0 +1,10 @@ +// obj.defineProperty throws if a value, getter, or setter is in a different compartment than obj + +load(libdir + "asserts.js"); +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var g1w = dbg.addDebuggee(g1); +var g2w = dbg.addDebuggee(g2); +assertThrowsInstanceOf(function () { g1w.defineProperty('x', {value: g2w}); }, TypeError); +assertThrowsInstanceOf(function () { g1w.defineProperty('x', {get: g1w, set: g2w}); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-09.js b/js/src/jit-test/tests/debug/Object-defineProperty-09.js new file mode 100644 index 0000000000..dc69e4fde1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-09.js @@ -0,0 +1,24 @@ +// defineProperty can't re-define non-configurable properties. +// Also: when defineProperty throws, the exception is native to the debugger +// compartment, not a wrapper. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +gw.defineProperty("p", {value: 1}); +g.p = 4; +assertEq(g.p, 1); + +var threw; +try { + gw.defineProperty("p", {value: 2}); + threw = false; +} catch (exc) { + threw = true; + assertEq(exc instanceof TypeError, true); + assertEq(typeof exc.message, "string"); + assertEq(typeof exc.stack, "string"); +} +assertEq(threw, true); + +assertEq(g.p, 1); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-10.js b/js/src/jit-test/tests/debug/Object-defineProperty-10.js new file mode 100644 index 0000000000..3e4f9930c5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-10.js @@ -0,0 +1,10 @@ +// defineProperty can make a non-configurable writable property non-writable + +load(libdir + "asserts.js"); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +gw.defineProperty("p", {writable: true, value: 1}); +gw.defineProperty("p", {writable: false}); +g.p = 4; +assertEq(g.p, 1); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-11.js b/js/src/jit-test/tests/debug/Object-defineProperty-11.js new file mode 100644 index 0000000000..03ed7d651e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-11.js @@ -0,0 +1,16 @@ +// obj.defineProperty works when obj's referent is a wrapper. + +var x = {}; +var g = newGlobal({newCompartment: true}); +g.x = x; +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var xw = gw.getOwnPropertyDescriptor("x").value; +xw.defineProperty("p", {configurable: true, enumerable: true, writable: true, value: gw}); +assertEq(x.p, g); + +var desc = Object.getOwnPropertyDescriptor(x, "p"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, true); +assertEq(desc.writable, true); +assertEq(desc.value, g); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-12.js b/js/src/jit-test/tests/debug/Object-defineProperty-12.js new file mode 100644 index 0000000000..821617711d --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-12.js @@ -0,0 +1,18 @@ +// obj.defineProperty redefining an existing property leaves unspecified attributes unchanged. + +var g = newGlobal({newCompartment: true}); +g.p = 1; +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +gw.defineProperty("p", {value: 2}); +assertEq(g.p, 2); + +var desc = Object.getOwnPropertyDescriptor(g, "p"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, true); +assertEq(desc.writable, true); +assertEq(desc.value, 2); + +g.p = 3; +assertEq(g.p, 3); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-13.js b/js/src/jit-test/tests/debug/Object-defineProperty-13.js new file mode 100644 index 0000000000..3bfd554c14 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-13.js @@ -0,0 +1,16 @@ +// defineProperty throws if a getter or setter is neither undefined nor callable. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +for (let v of [null, false, 'bad', 0, 2.76, {}]) { + assertThrowsInstanceOf(function () { + gw.defineProperty("p", {configurable: true, get: v}); + }, TypeError); + assertThrowsInstanceOf(function () { + gw.defineProperty("p", {configurable: true, set: v}); + }, TypeError); +} diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-14.js b/js/src/jit-test/tests/debug/Object-defineProperty-14.js new file mode 100644 index 0000000000..5a6deda121 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-14.js @@ -0,0 +1,15 @@ +// defineProperty accepts undefined for desc.get/set. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +gw.defineProperty("p", {get: undefined, set: undefined}); + +var desc = g.eval("Object.getOwnPropertyDescriptor(this, 'p')"); +assertEq("get" in desc, true); +assertEq("set" in desc, true); +assertEq(desc.get, undefined); +assertEq(desc.set, undefined); diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-non-primitive-key.js b/js/src/jit-test/tests/debug/Object-defineProperty-non-primitive-key.js new file mode 100644 index 0000000000..c3c503968f --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-non-primitive-key.js @@ -0,0 +1,41 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var gw = dbg.addDebuggee(g); + +g.eval(` + var obj = { + p: 0, + [Symbol.iterator]: 0, + }; +`); + +// Return |key| as an object. +function toObject(key) { + return { + [Symbol.toPrimitive]() { + return key; + } + }; +} + +let obj = gw.getProperty("obj").return; + +for (let key of obj.getOwnPropertyNames()) { + let keyObject = toObject(key); + + obj.defineProperty(key, {value: 1}); + assertEq(g.obj[key], 1); + + obj.defineProperty(keyObject, {value: 1}); + assertEq(g.obj[key], 1); +} + +for (let key of obj.getOwnPropertySymbols()) { + let keyObject = toObject(key); + + obj.defineProperty(key, {value: 1}); + assertEq(g.obj[key], 1); + + obj.defineProperty(keyObject, {value: 1}); + assertEq(g.obj[key], 1); +} diff --git a/js/src/jit-test/tests/debug/Object-defineProperty-surfaces-01.js b/js/src/jit-test/tests/debug/Object-defineProperty-surfaces-01.js new file mode 100644 index 0000000000..8cf9d85200 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-defineProperty-surfaces-01.js @@ -0,0 +1,8 @@ +// Debugger.Object.prototype.defineProperty with too few arguments throws. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +assertThrowsInstanceOf(function () { gw.defineProperty("x"); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Object-deleteProperty-01.js b/js/src/jit-test/tests/debug/Object-deleteProperty-01.js new file mode 100644 index 0000000000..65a465b75e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-deleteProperty-01.js @@ -0,0 +1,17 @@ +// Basic deleteProperty tests. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +assertEq(gw.deleteProperty("no such property"), true); + +g.Object.defineProperty(g, "p", {configurable: true, value: 0}); +assertEq(gw.deleteProperty("p"), true); + +g[0] = 0; +assertEq(gw.deleteProperty(0), true); +assertEq("0" in g, false); + +assertEq(gw.deleteProperty(), false); // can't delete g.undefined +assertEq(g.undefined, undefined); diff --git a/js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js b/js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js new file mode 100644 index 0000000000..38827c908d --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js @@ -0,0 +1,16 @@ +// Don't crash when a scripted proxy handler throws Error.prototype. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + try { + frame.arguments[0].deleteProperty("x"); + } catch (exc) { + return; + } + throw new Error("deleteProperty should throw"); +}; + +g.eval("function h(obj) { debugger; }"); +g.eval("h(new Proxy({}, { deleteProperty() { throw Error.prototype; }}));"); + diff --git a/js/src/jit-test/tests/debug/Object-deleteProperty-error-02.js b/js/src/jit-test/tests/debug/Object-deleteProperty-error-02.js new file mode 100644 index 0000000000..e685b93f46 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-deleteProperty-error-02.js @@ -0,0 +1,19 @@ +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + try { + frame.arguments[0].deleteProperty("x"); + } catch (exc) { + assertEq(exc instanceof Debugger.DebuggeeWouldRun, true); + return; + } + throw new Error("deleteProperty should throw"); +}; + +g.evaluate("function h(obj) { debugger; } \n" + + "h(new Proxy({}, \n" + + " { deleteProperty: function () { \n" + + " var e = new ReferenceError('diaf', 'fail'); \n" + + " throw e; \n" + + " } \n" + + " }));"); diff --git a/js/src/jit-test/tests/debug/Object-deleteProperty-non-primitive-key.js b/js/src/jit-test/tests/debug/Object-deleteProperty-non-primitive-key.js new file mode 100644 index 0000000000..83a10b89eb --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-deleteProperty-non-primitive-key.js @@ -0,0 +1,49 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var gw = dbg.addDebuggee(g); + +g.eval(` + var obj = { + p: 0, + [Symbol.iterator]: 0, + }; +`); + +// Return |key| as an object. +function toObject(key) { + return { + [Symbol.toPrimitive]() { + return key; + } + }; +} + +let obj = gw.getProperty("obj").return; + +for (let key of obj.getOwnPropertyNames()) { + let keyObject = toObject(key); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.deleteProperty(key), true); + assertEq(g.obj[key], undefined); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.deleteProperty(keyObject), true); + assertEq(g.obj[key], undefined); +} + +for (let key of obj.getOwnPropertySymbols()) { + let keyObject = toObject(key); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.deleteProperty(key), true); + assertEq(g.obj[key], undefined); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.deleteProperty(keyObject), true); + assertEq(g.obj[key], undefined); +} diff --git a/js/src/jit-test/tests/debug/Object-displayName-01.js b/js/src/jit-test/tests/debug/Object-displayName-01.js new file mode 100644 index 0000000000..2a316f9d38 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-displayName-01.js @@ -0,0 +1,21 @@ +// Debugger.Object.prototype.displayName + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var name; +dbg.onDebuggerStatement = function (frame) { name = frame.callee.displayName; }; + +g.eval("(function f() { debugger; })();"); +assertEq(name, "f"); +g.eval("(function () { debugger; })();"); +assertEq(name, undefined); +g.eval("Function('debugger;')();"); +assertEq(name, "anonymous"); +g.eval("var f = function() { debugger; }; f()"); +assertEq(name, "f"); +g.eval("var a = {}; a.f = function() { debugger; }; a.f()"); +assertEq(name, "a.f"); +g.eval("(async function grondo() { debugger; })();"); +assertEq(name, "grondo"); +g.eval("(async function* estux() { debugger; })().next();"); +assertEq(name, "estux"); diff --git a/js/src/jit-test/tests/debug/Object-environment-01.js b/js/src/jit-test/tests/debug/Object-environment-01.js new file mode 100644 index 0000000000..ea74761708 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-environment-01.js @@ -0,0 +1,17 @@ +// obj.environment is undefined when the referent is not a JS function. + +var g = newGlobal({newCompartment: true}) +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +assertEq(gw.environment, undefined); + +g.eval("var r = /x/;"); +var rw = gw.getOwnPropertyDescriptor("r").value; +assertEq(rw.class, "RegExp"); +assertEq(rw.environment, undefined); + +// Native function. +var fw = gw.getOwnPropertyDescriptor("parseInt").value; +assertEq(fw.class, "Function"); +assertEq(fw.environment, undefined); + diff --git a/js/src/jit-test/tests/debug/Object-environment-02.js b/js/src/jit-test/tests/debug/Object-environment-02.js new file mode 100644 index 0000000000..d499e1ca2c --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-environment-02.js @@ -0,0 +1,22 @@ +// The .environment of a function Debugger.Object is an Environment object. + +var g = newGlobal({newCompartment: true}) +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); + +function check(expr) { + print("checking " + JSON.stringify(expr)); + let completion = gDO.executeInGlobal(expr); + if (completion.throw) + throw completion.throw.unsafeDereference(); + assertEq(completion.return.environment instanceof Debugger.Environment, true); +} + +g.eval('function j(a) { }'); + +check('j'); +check('(() => { })'); +check('(function f() { })'); +check('(function* g() { })'); +check('(async function m() { })'); +check('(async function* n() { })'); diff --git a/js/src/jit-test/tests/debug/Object-errorLineNumber-errorColumnNumber.js b/js/src/jit-test/tests/debug/Object-errorLineNumber-errorColumnNumber.js new file mode 100644 index 0000000000..a581f93b6e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-errorLineNumber-errorColumnNumber.js @@ -0,0 +1,55 @@ +// Debugger.Object.prototype.{errorLineNumber,errorColumnNumber} return the +// line number and column number associated with some error object. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +var syntaxError = gw.executeInGlobal("\nlet a, a;").throw; +assertEq(syntaxError.errorLineNumber, 2); +assertEq(syntaxError.errorColumnNumber, 7); + +var typeError = gw.executeInGlobal("\n1 + f();").throw; +assertEq(typeError.errorLineNumber, 2); +assertEq(typeError.errorColumnNumber, 1); + +// Custom errors have no line/column numbers . +var customError = gw.executeInGlobal("\nthrow 1;").throw; +assertEq(customError.errorLineNumber, undefined); +assertEq(customError.errorColumnNumber, undefined); + +customError = gw.executeInGlobal("\nthrow { errorLineNumber: 10, errorColumnNumber: 20 };").throw; +assertEq(customError.errorLineNumber, undefined); +assertEq(customError.errorColumnNumber, undefined); + +customError = gw.executeInGlobal("\nthrow { lineNumber: 10, columnNumber: 20 };").throw; +assertEq(customError.errorLineNumber, undefined); +assertEq(customError.errorColumnNumber, undefined); + +// Ensure that the method works across globals. +g.eval(`var g = newGlobal({newCompartment: true}); + g.eval('var err; \\n' + + 'try {\\n' + + ' f();\\n' + + '} catch (e) {\\n' + + ' err = e;\\n' + + '}'); + var err2 = g.err;`); +var otherGlobalError = gw.executeInGlobal("throw err2").throw; +assertEq(otherGlobalError.errorLineNumber, 3); +assertEq(otherGlobalError.errorColumnNumber, 3); + +// Ensure that non-error objects return undefined. +const Args = [ + "1", + "'blah'", + "({})", + "[]", + "() => 1" +] + +for (let arg of Args) { + let nonError = gw.executeInGlobal(`${arg}`).return; + assertEq(nonError.errorLineNumber, undefined); + assertEq(nonError.errorColumnNumber, undefined); +} diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-01.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-01.js new file mode 100644 index 0000000000..af2fa3600f --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-01.js @@ -0,0 +1,13 @@ +// Debugger.Object.prototype.executeInGlobal basics + +var g = newGlobal({newCompartment: true}); +var h = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var hw = dbg.addDebuggee(h); + +g.y = "Bitte Orca"; +h.y = "Visiter"; +var y = "W H O K I L L"; +assertEq(gw.executeInGlobal('y').return, "Bitte Orca"); +assertEq(hw.executeInGlobal('y').return, "Visiter"); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-02.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-02.js new file mode 100644 index 0000000000..8a2592837f --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-02.js @@ -0,0 +1,20 @@ +// Debugger.Object.prototype.executeInGlobal argument validation + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var gobj = gw.makeDebuggeeValue(g.eval("({})")); + +assertThrowsInstanceOf(function () { gw.executeInGlobal(); }, TypeError); +assertThrowsInstanceOf(function () { gw.executeInGlobal(10); }, TypeError); +assertThrowsInstanceOf(function () { gobj.executeInGlobal('42'); }, TypeError); +assertEq(gw.executeInGlobal('42').return, 42); + +assertThrowsInstanceOf(function () { gw.executeInGlobalWithBindings(); }, TypeError); +assertThrowsInstanceOf(function () { gw.executeInGlobalWithBindings('42'); }, TypeError); +assertThrowsInstanceOf(function () { gw.executeInGlobalWithBindings(10, 1729); }, TypeError); +assertThrowsInstanceOf(function () { gw.executeInGlobalWithBindings('42', 1729); }, TypeError); +assertThrowsInstanceOf(function () { gobj.executeInGlobalWithBindings('42', {}); }, TypeError); +assertEq(gw.executeInGlobalWithBindings('42', {}).return, 42); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-03.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-03.js new file mode 100644 index 0000000000..1d2edd4ccc --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-03.js @@ -0,0 +1,19 @@ +// Debugger.Object.prototype.executeInGlobal: closures capturing the global + +var g = newGlobal({newCompartment: true}); +var h = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var hw = dbg.addDebuggee(h); + +g.x = "W H O K I L L"; +h.x = "No Color"; +var c1 = gw.executeInGlobal('(function () { return x; })').return; +var c2 = hw.executeInGlobal('(function () { return x; })').return; +var c3 = gw.executeInGlobalWithBindings('(function () { return x + y; })', { y:" In Rainbows" }).return; +var c4 = hw.executeInGlobalWithBindings('(function () { return x + y; })', { y:" In Rainbows" }).return; + +assertEq(c1.call(null).return, "W H O K I L L"); +assertEq(c2.call(null).return, "No Color"); +assertEq(c3.call(null).return, "W H O K I L L In Rainbows"); +assertEq(c4.call(null).return, "No Color In Rainbows"); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-04.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-04.js new file mode 100644 index 0000000000..cf53e8a67f --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-04.js @@ -0,0 +1,55 @@ +// Debugger.Object.prototype.executeInGlobal: nested evals + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +assertEq(gw.executeInGlobal("eval('\"Awake\"');").return, "Awake"); + +// Evaluating non-strict-mode code uses the given global as its variable +// environment. +g.x = "Swing Lo Magellan"; +g.y = "The Milk-Eyed Mender"; +assertEq(gw.executeInGlobal("eval('var x = \"A Brief History of Love\"');\n" + + "var y = 'Merriweather Post Pavilion';" + + "x;").return, + "A Brief History of Love"); +assertEq(g.x, "A Brief History of Love"); +assertEq(g.y, "Merriweather Post Pavilion"); + +// As above, but notice that we still create bindings on the global, even +// when we've interposed a new environment via 'withBindings'. +g.x = "Swing Lo Magellan"; +g.y = "The Milk-Eyed Mender"; +assertEq(gw.executeInGlobalWithBindings("eval('var x = d1;'); var y = d2; x;", + { d1: "A Brief History of Love", + d2: "Merriweather Post Pavilion" }).return, + "A Brief History of Love"); +assertEq(g.x, "A Brief History of Love"); +assertEq(g.y, "Merriweather Post Pavilion"); + + +// Strict mode code variants of the above: + +// Strict mode still lets us create bindings on the global as this is +// equivalent to executing statements at the global level. But note that +// setting strict mode means that nested evals get their own call objects. +g.x = "Swing Lo Magellan"; +g.y = "The Milk-Eyed Mender"; +assertEq(gw.executeInGlobal("\'use strict\';\n" + + "eval('var x = \"A Brief History of Love\"');\n" + + "var y = \"Merriweather Post Pavilion\";" + + "x;").return, + "Swing Lo Magellan"); +assertEq(g.x, "Swing Lo Magellan"); +assertEq(g.y, "Merriweather Post Pavilion"); + +// Introducing a bindings object shouldn't change this behavior. +g.x = "Swing Lo Magellan"; +g.y = "The Milk-Eyed Mender"; +assertEq(gw.executeInGlobalWithBindings("'use strict'; eval('var x = d1;'); var y = d2; x;", + { d1: "A Brief History of Love", + d2: "Merriweather Post Pavilion" }).return, + "Swing Lo Magellan"); +assertEq(g.x, "Swing Lo Magellan"); +assertEq(g.y, "Merriweather Post Pavilion"); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-05.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-05.js new file mode 100644 index 0000000000..899a6cf1eb --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-05.js @@ -0,0 +1,21 @@ +// Debugger.Object.prototype.executeInGlobal throws when asked to evaluate in a CCW of a global. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger(); + +var g1 = newGlobal({newCompartment: true}); +var dg1 = dbg.addDebuggee(g1); + +var g2 = newGlobal({newCompartment: true}); +var dg2 = dbg.addDebuggee(g2); + +// Generate a Debugger.Object viewing g2 from g1's compartment. +var dg1wg2 = dg1.makeDebuggeeValue(g2); +assertEq(dg1wg2.unwrap(), dg2.makeDebuggeeValue(g2)); +assertThrowsInstanceOf(function () { dg1wg2.executeInGlobal('1'); }, TypeError); +assertThrowsInstanceOf(function () { dg1wg2.executeInGlobalWithBindings('x', { x: 1 }); }, TypeError); + +// These, however, should not throw. +assertEq(dg1.executeInGlobal('1729').return, 1729); +assertEq(dg1.executeInGlobalWithBindings('x', { x: 1729 }).return, 1729); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-06.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-06.js new file mode 100644 index 0000000000..37fdf4db5c --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-06.js @@ -0,0 +1,8 @@ +// Debugger.Object.prototype.executeInGlobal sets 'this' to the global. + +var dbg = new Debugger; +var g1 = newGlobal({newCompartment: true}); +var g1w = dbg.addDebuggee(g1); + +assertEq(g1w.executeInGlobal('this').return, g1w.makeDebuggeeValue(g1)); +assertEq(g1w.executeInGlobalWithBindings('this', { x:42 }).return, g1w.makeDebuggeeValue(g1)); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-07.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-07.js new file mode 100644 index 0000000000..09f5d7430f --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-07.js @@ -0,0 +1,24 @@ +// executeInGlobal correctly handles optional custom url option +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var debuggee = dbg.getDebuggees()[0]; +var count = 0; + +function testUrl (options, expected) { + count++; + dbg.onNewScript = function(script){ + dbg.onNewScript = undefined; + assertEq(script.url, expected); + count--; + }; + debuggee.executeInGlobal("", options); +} + + +testUrl(undefined, "debugger eval code"); +testUrl(null, "debugger eval code"); +testUrl({ url: undefined }, "debugger eval code"); +testUrl({ url: null }, "null"); +testUrl({ url: 5 }, "5"); +testUrl({ url: "test" }, "test"); +assertEq(count, 0); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-08.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-08.js new file mode 100644 index 0000000000..3c3819f2d3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-08.js @@ -0,0 +1,22 @@ +// executeInGlobal correctly handles optional lineNumber option +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var debuggee = dbg.getDebuggees()[0]; +var count = 0; + +function testLineNumber (options, expected) { + count++; + dbg.onNewScript = function(script){ + dbg.onNewScript = undefined; + assertEq(script.startLine, expected); + count--; + }; + debuggee.executeInGlobal("", options); +} + + +testLineNumber(undefined, 1); +testLineNumber({}, 1); +testLineNumber({ lineNumber: undefined }, 1); +testLineNumber({ lineNumber: 5 }, 5); +assertEq(count, 0); diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-09.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-09.js new file mode 100644 index 0000000000..a4df6d5517 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-09.js @@ -0,0 +1,9 @@ +// The frame created for executeInGlobal is never marked as a 'FUNCTION' frame. + +(function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + var gw = dbg.addDebuggee(g); + gw.executeInGlobalWithBindings("eval('Math')",{}).return +})(); + diff --git a/js/src/jit-test/tests/debug/Object-executeInGlobal-10.js b/js/src/jit-test/tests/debug/Object-executeInGlobal-10.js new file mode 100644 index 0000000000..0120bcc1dd --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-executeInGlobal-10.js @@ -0,0 +1,13 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +// executeInGlobal should be able to introduce and persist lexical bindings. +assertEq(gw.executeInGlobal(`let x = 42; x;`).return, 42); +assertEq(gw.executeInGlobal(`x;`).return, 42); + +// By contrast, Debugger.Frame.eval is like direct eval, and shouldn't be able +// to introduce new lexical bindings. +dbg.onDebuggerStatement = function (frame) { frame.eval(`let y = 84;`); }; +g.eval(`debugger;`); +assertEq(!!gw.executeInGlobal(`y;`).throw, true); diff --git a/js/src/jit-test/tests/debug/Object-forceLexicalInitializationByName.js b/js/src/jit-test/tests/debug/Object-forceLexicalInitializationByName.js new file mode 100644 index 0000000000..7550d15ef0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-forceLexicalInitializationByName.js @@ -0,0 +1,61 @@ +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +let errorOne, errorTwo; + +function evalErrorStr(global, evalString) { + try { + global.evaluate(evalString); + return undefined; + } catch (e) { + return e.toString(); + } +} + + +assertEq(evalErrorStr(g, "let y = IDONTEXIST;"), "ReferenceError: IDONTEXIST is not defined"); +assertEq(evalErrorStr(g, "y = 1;"), + "ReferenceError: can't access lexical declaration 'y' before initialization"); + +// Here we flip the uninitialized binding to undfined. +assertEq(gw.forceLexicalInitializationByName("y"), true); +assertEq(g.evaluate("y"), undefined); +g.evaluate("y = 1;"); +assertEq(g.evaluate("y"), 1); + +// Ensure that bogus bindings return false, but otherwise trigger no error or +// side effect. +assertEq(gw.forceLexicalInitializationByName("idontexist"), false); +assertEq(evalErrorStr(g, "idontexist"), "ReferenceError: idontexist is not defined"); + +// Ensure that ropes (non-atoms) behave properly +assertEq(gw.forceLexicalInitializationByName(("foo" + "bar" + "bop" + "zopple" + 2 + 3).slice(1)), + false); +assertEq(evalErrorStr(g, "let oobarbopzopple23 = IDONTEXIST;"), "ReferenceError: IDONTEXIST is not defined"); +assertEq(gw.forceLexicalInitializationByName(("foo" + "bar" + "bop" + "zopple" + 2 + 3).slice(1)), + true); +assertEq(g.evaluate("oobarbopzopple23"), undefined); + +// Ensure that only strings are accepted by forceLexicalInitializationByName +const bad_types = [ + 2112, + {geddy: "lee"}, + () => 1, + [], + Array, + "'1'", // non-identifier +] + +for (var badType of bad_types) { + assertThrowsInstanceOf(() => { + gw.forceLexicalInitializationByName(badType); + }, TypeError); +} + +// Finally, supplying no arguments should throw a type error +assertThrowsInstanceOf(() => { + Debugger.isCompilableUnit(); +}, TypeError); diff --git a/js/src/jit-test/tests/debug/Object-gc-01.js b/js/src/jit-test/tests/debug/Object-gc-01.js new file mode 100644 index 0000000000..5dbb01057c --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-gc-01.js @@ -0,0 +1,14 @@ +// Debugger.Objects keep their referents alive. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var arr = []; +dbg.onDebuggerStatement = function (frame) { arr.push(frame.eval("[]").return); }; +g.eval("for (var i = 0; i < 10; i++) debugger;"); +assertEq(arr.length, 10); + +gc(); + +for (var i = 0; i < arr.length; i++) + assertEq(arr[i].class, "Array"); + diff --git a/js/src/jit-test/tests/debug/Object-getErrorMessageName.js b/js/src/jit-test/tests/debug/Object-getErrorMessageName.js new file mode 100644 index 0000000000..11c98d93e5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getErrorMessageName.js @@ -0,0 +1,29 @@ +// Debugger.Object.prototype.getErrorMessageName returns the error message name +// associated with some error object. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +assertEq(gw.executeInGlobal("(42).toString(0)").throw.errorMessageName, "JSMSG_BAD_RADIX"); + +// Custom errors have no error message name. +assertEq(gw.executeInGlobal("throw new Error()").throw.errorMessageName, undefined); + +// Ensure that the method works across globals. +g.eval(`var g = newGlobal({newCompartment: true}); + g.eval('var err; try { (42).toString(0); } catch (e) { err = e; }'); + var err2 = g.err;`); +assertEq(gw.executeInGlobal("throw err2").throw.errorMessageName, "JSMSG_BAD_RADIX"); + +// Ensure that non-error objects return undefined. +const Args = [ + "1", + "'blah'", + "({})", + "[]", + "() => 1" +] + +for (let arg of Args) + assertEq(gw.executeInGlobal(`${arg}`).return.errorMessageName, undefined); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-01.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-01.js new file mode 100644 index 0000000000..a39faeb2f2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-01.js @@ -0,0 +1,59 @@ +// getOwnPropertyDescriptor works with simple data properties. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits; +var expected; +dbg.onDebuggerStatement = function (frame) { + var args = frame.arguments; + var obj = args[0], id = args[1]; + var desc = obj.getOwnPropertyDescriptor(id); + if (expected === undefined) { + assertEq(desc, undefined); + } else { + assertEq(desc instanceof Object, true); + assertEq(desc.enumerable, expected.enumerable); + assertEq(desc.configurable, expected.configurable); + assertEq(desc.hasOwnProperty("value"), true); + assertEq(desc.value, expected.value); + assertEq(desc.writable, expected.writable === undefined ? true : expected.writable); + assertEq("get" in desc, false); + assertEq("set" in desc, false); + } + hits++; +}; + +g.eval("function f(obj, id) { debugger; }"); + +function test(obj, id, desc) { + expected = desc; + hits = 0; + g.f(obj, id); + assertEq(hits, 1); +} + +var obj = g.eval("({a: 1, ' ': undefined, '0': 0})"); +test(obj, "a", {value: 1, enumerable: true, configurable: true}); +test(obj, " ", {value: undefined, enumerable: true, configurable: true}); +test(obj, "b", undefined); +test(obj, "0", {value: 0, enumerable: true, configurable: true}); +test(obj, 0, {value: 0, enumerable: true, configurable: true}); + +var arr = g.eval("[7,,]"); +test(arr, 'length', {value: 2, enumerable: false, configurable: false}); +test(arr, 0, {value: 7, enumerable: true, configurable: true}); +test(arr, "0", {value: 7, enumerable: true, configurable: true}); +test(arr, 1, undefined); +test(arr, "1", undefined); +test(arr, 2, undefined); +test(arr, "2", undefined); +test(arr, "argelfraster", undefined); + +var re = g.eval("/erwe/"); +test(re, 'lastIndex', {value: 0, enumerable: false, configurable: false}); + +// String objects have a well-behaved resolve hook. +var str = g.eval("new String('hello world')"); +test(str, 'length', {value: 11, enumerable: false, configurable: false, writable: false}); +test(str, 0, {value: 'h', enumerable: true, configurable: false, writable: false}); +test(str, "0", {value: 'h', enumerable: true, configurable: false, writable: false}); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-02.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-02.js new file mode 100644 index 0000000000..7f5368d263 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-02.js @@ -0,0 +1,8 @@ +// Property values that are objects are reflected as Debugger.Objects. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(); +var gobj = dbg.addDebuggee(g); +g.self = g; +var desc = gobj.getOwnPropertyDescriptor("self"); +assertEq(desc.value, gobj.makeDebuggeeValue(g)); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-03.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-03.js new file mode 100644 index 0000000000..9a30d63d17 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-03.js @@ -0,0 +1,22 @@ +// obj.getOwnPropertyDescriptor works on global objects. + +var g = newGlobal({newCompartment: true}); +g.eval("var v;"); +this.eval("var v;"); + +var dbg = Debugger(); +var obj = dbg.addDebuggee(g); + +function test(name) { + var desc = obj.getOwnPropertyDescriptor(name); + assertEq(desc instanceof Object, true); + var expected = Object.getOwnPropertyDescriptor(this, name); + assertEq(Object.prototype.toString.call(desc), Object.prototype.toString.call(expected)); + assertEq(desc.enumerable, expected.enumerable); + assertEq(desc.configurable, expected.configurable); + assertEq(desc.writable, expected.writable); + assertEq(desc.value, expected.value); +} + +test("Infinity"); +test("v"); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-04.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-04.js new file mode 100644 index 0000000000..a24528bec6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-04.js @@ -0,0 +1,18 @@ +// obj.getOwnPropertyDescriptor works on accessor properties. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gdo = dbg.addDebuggee(g); + +g.called = false; +g.eval("var a = {get x() { called = true; }};"); + +var desc = gdo.getOwnPropertyDescriptor("a").value.getOwnPropertyDescriptor("x"); +assertEq(g.called, false); +assertEq(desc.enumerable, true); +assertEq(desc.configurable, true); +assertEq("value" in desc, false); +assertEq("writable" in desc, false); +assertEq(desc.get instanceof Debugger.Object, true); +assertEq(desc.get.class, "Function"); +assertEq(desc.set, undefined); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-05.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-05.js new file mode 100644 index 0000000000..b2223076e1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-05.js @@ -0,0 +1,17 @@ +// obj.getOwnPropertyDescriptor presents getters and setters as Debugger.Object objects. + +var g = newGlobal({newCompartment: true}); +g.S = function foreignFunction(v) {}; +g.eval("var a = {};\n" + + "function G() {}\n" + + "Object.defineProperty(a, 'p', {get: G, set: S})"); + +var dbg = new Debugger; +var gdo = dbg.addDebuggee(g); +var desc = gdo.getOwnPropertyDescriptor("a").value.getOwnPropertyDescriptor("p"); +assertEq(desc.enumerable, false); +assertEq(desc.configurable, false); +assertEq("value" in desc, false); +assertEq("writable" in desc, false); +assertEq(desc.get, gdo.getOwnPropertyDescriptor("G").value); +assertEq(desc.set, gdo.getOwnPropertyDescriptor("S").value); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-06.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-06.js new file mode 100644 index 0000000000..b0d9facb7e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-06.js @@ -0,0 +1,29 @@ +// obj.getOwnPropertyDescriptor works when obj is a transparent cross-compartment wrapper. + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +g1.next = g2; + +// This test is a little hard to follow, especially the !== assertions. +// +// Bottom line: the value of a property of g1 can only be an object in g1's +// compartment, so any Debugger.Objects obtained by calling +// g1obj.getOwnPropertyDescriptor should all have referents in g1's +// compartment. + +var dbg = new Debugger; +var g1obj = dbg.addDebuggee(g1); +var g2obj = dbg.addDebuggee(g2); +var wobj = g1obj.getOwnPropertyDescriptor("next").value; +assertEq(wobj instanceof Debugger.Object, true); +assertEq(wobj !== g2obj, true); // referents are in two different compartments + +g2.x = "ok"; +assertEq(wobj.getOwnPropertyDescriptor("x").value, "ok"); + +g1.g2min = g2.min = g2.Math.min; +g2.eval("Object.defineProperty(this, 'y', {get: min});"); +assertEq(g2.y, Infinity); +var wmin = wobj.getOwnPropertyDescriptor("y").get; +assertEq(wmin !== g2obj.getOwnPropertyDescriptor("min").value, true); // as above +assertEq(wmin, g1obj.getOwnPropertyDescriptor("g2min").value); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-non-primitive-key.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-non-primitive-key.js new file mode 100644 index 0000000000..71b177aa63 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-non-primitive-key.js @@ -0,0 +1,37 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var gw = dbg.addDebuggee(g); + +g.eval(` + var obj = { + p: 1, + [Symbol.iterator]: 2, + }; +`); + +// Return |key| as an object. +function toObject(key) { + return { + [Symbol.toPrimitive]() { + return key; + } + }; +} + +let obj = gw.getProperty("obj").return; + +for (let key of obj.getOwnPropertyNames()) { + let keyObject = toObject(key); + let value = g.obj[key]; + + assertEq(obj.getOwnPropertyDescriptor(key).value, value); + assertEq(obj.getOwnPropertyDescriptor(keyObject).value, value); +} + +for (let key of obj.getOwnPropertySymbols()) { + let keyObject = toObject(key); + let value = g.obj[key]; + + assertEq(obj.getOwnPropertyDescriptor(key).value, value); + assertEq(obj.getOwnPropertyDescriptor(keyObject).value, value); +} diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-surfaces-01.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-surfaces-01.js new file mode 100644 index 0000000000..b0b89f7623 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-surfaces-01.js @@ -0,0 +1,14 @@ +// The argument to Debugger.Object.prototype.getOwnPropertyDescriptor can be omitted. + +var g = newGlobal({newCompartment: true}); +g.eval("var obj = {};"); + +var dbg = Debugger(g); +var obj; +dbg.onDebuggerStatement = function (frame) { obj = frame.eval("obj").return; }; +g.eval("debugger;"); + +assertEq(obj.getOwnPropertyDescriptor(), undefined); +g.obj.undefined = 17; +var desc = obj.getOwnPropertyDescriptor(); +assertEq(desc.value, 17); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-surfaces-02.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-surfaces-02.js new file mode 100644 index 0000000000..00ff44ea4f --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyDescriptor-surfaces-02.js @@ -0,0 +1,14 @@ +// The argument to Debugger.Object.prototype.getOwnPropertyDescriptor can be an object. +var g = newGlobal({newCompartment: true}); +g.eval("var obj = {};"); + +var dbg = Debugger(g); +var obj; +dbg.onDebuggerStatement = function (frame) { obj = frame.eval("obj").return; }; +g.eval("debugger;"); + +var nameobj = {toString: function () { return 'x'; }}; +assertEq(obj.getOwnPropertyDescriptor(nameobj), undefined); +g.obj.x = 17; +var desc = obj.getOwnPropertyDescriptor(nameobj); +assertEq(desc.value, 17); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyNames-01.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyNames-01.js new file mode 100644 index 0000000000..b4ceb76f14 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyNames-01.js @@ -0,0 +1,33 @@ +// Basic getOwnPropertyNames tests. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(); +var gobj = dbg.addDebuggee(g); + +function test(code) { + code = "(" + code + ");"; + var expected = Object.getOwnPropertyNames(eval(code)); + g.eval("obj = " + code); + var actual = gobj.getOwnPropertyDescriptor("obj").value.getOwnPropertyNames(); + assertEq(JSON.stringify(actual.sort()), JSON.stringify(expected.sort())); +} + +test("{}"); +test("{a: 0, b: 1}"); +test("{'name with space': 0}"); +test("{get x() {}, set y(v) {}}"); +test("{get x() { throw 'fit'; }}"); +test("Object.create({a: 1})"); +test("Object.create({get a() {}, set a(v) {}})"); +test("(function () { var x = {a: 0, b: 1}; delete a; return x; })()"); +test("Object.create(null, {x: {value: 0}})"); +test("[]"); +test("[0, 1, 2]"); +test("[,,,,,]"); +test("/a*a/"); +test("function () {}"); +test("(function () {\n" + + " var x = {};\n" + + " x[Symbol()] = 1; x[Symbol.for('moon')] = 2; x[Symbol.iterator] = 3;\n" + + " return x;\n" + + "})()"); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertyNames-02.js b/js/src/jit-test/tests/debug/Object-getOwnPropertyNames-02.js new file mode 100644 index 0000000000..df06f55760 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertyNames-02.js @@ -0,0 +1,11 @@ +// obj.getOwnPropertyNames() works when obj's referent is itself a cross-compartment wrapper. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(); +var gobj = dbg.addDebuggee(g); +g.p = {xyzzy: 8}; // makes a cross-compartment wrapper +g.p[Symbol.for("plugh")] = 9; +var wp = gobj.getOwnPropertyDescriptor("p").value; +var names = wp.getOwnPropertyNames(); +assertEq(names.length, 1); +assertEq(names[0], "xyzzy"); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertySymbols-01.js b/js/src/jit-test/tests/debug/Object-getOwnPropertySymbols-01.js new file mode 100644 index 0000000000..3e16f3e2e3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertySymbols-01.js @@ -0,0 +1,33 @@ +// Basic getOwnPropertSymbols tests. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(); +var gobj = dbg.addDebuggee(g); + +function test(code) { + code = "(" + code + ");"; + var expected = Object.getOwnPropertySymbols(eval(code)); + g.eval("obj = " + code); + var actual = gobj.getOwnPropertyDescriptor("obj").value.getOwnPropertySymbols(); + + assertEq(JSON.stringify(actual.map((x) => x.toString()).sort()), + JSON.stringify(expected.map((x) => x.toString()).sort())); +} + +test("{}"); +test("Array.prototype"); // Symbol.iterator +test("Object.create(null)"); +test("(function() {let x = Symbol(); let o = {}; o[x] = 1; return o;})()"); +test("(function() {let x = Symbol('foo'); let o = {}; o[x] = 1; return o;})()"); +test("(function() {let x = Symbol('foo'); let y = Symbol('bar'); \ + let o = {}; o[x] = 1; o[y] = 2; return o;})()"); +test("(function() {let x = Symbol('foo with spaces'); \ + let o = {}; o[x] = 1; return o;})()"); +test("(function() {let x = Symbol('foo'); \ + let o = function(){}; o[x] = 1; return o;})()"); +test("(function() {let x = Symbol('foo'); \ + let o = Object.create(null); o[x] = 1; return o;})()"); +test("(function() {let x = Symbol('foo'); \ + let o = new Array(); o[x] = 1; return o;})()"); +test("(function() {let x = Symbol('foo'); \ + let o = {}; o[x] = 1; delete o[x]; return o;})()"); diff --git a/js/src/jit-test/tests/debug/Object-getOwnPropertySymbols-02.js b/js/src/jit-test/tests/debug/Object-getOwnPropertySymbols-02.js new file mode 100644 index 0000000000..7f85caa941 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getOwnPropertySymbols-02.js @@ -0,0 +1,12 @@ +// obj.getOwnPropertySymbols() works when obj's referent is itself a cross-compartment wrapper. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(); +var gobj = dbg.addDebuggee(g); +g.p = {xyzzy: 8}; // makes a cross-compartment wrapper +var sym = Symbol("plugh"); +g.p[sym] = 9; +var wp = gobj.getOwnPropertyDescriptor("p").value; +var names = wp.getOwnPropertySymbols(); +assertEq(names.length, 1); +assertEq(names[0], sym); diff --git a/js/src/jit-test/tests/debug/Object-getPromiseReactions-01.js b/js/src/jit-test/tests/debug/Object-getPromiseReactions-01.js new file mode 100644 index 0000000000..ab56984960 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getPromiseReactions-01.js @@ -0,0 +1,25 @@ +// Debugger.Object.prototype.getPromiseReactions throws on non-promises, but +// works on wrappers of promises. + +load(libdir + 'asserts.js'); +load(libdir + 'array-compare.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger; +var DOg = dbg.addDebuggee(g); + +assertThrowsInstanceOf(() => DOg.getPromiseReactions(), TypeError); + +// Try retrieving an empty reaction list from an actual promise. +g.eval(`var p = Promise.resolve();`); +var DOgp = DOg.makeDebuggeeValue(g.p); +assertEq(true, arraysEqual(DOgp.getPromiseReactions(), [])); + +// Try a Debugger.Object of a cross-compartment wrapper of a promise. This +// should still work: the promise accessors generally do checked unwraps of +// their arguments. +var g2 = newGlobal({ newCompartment: true }); +DOg2 = dbg.addDebuggee(g2); +var DOg2gp = DOg2.makeDebuggeeValue(g.p); +assertEq(DOgp, DOg2gp.unwrap()); +assertEq(true, arraysEqual(DOg2gp.getPromiseReactions(), [])); diff --git a/js/src/jit-test/tests/debug/Object-getPromiseReactions-02.js b/js/src/jit-test/tests/debug/Object-getPromiseReactions-02.js new file mode 100644 index 0000000000..9922dd3389 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getPromiseReactions-02.js @@ -0,0 +1,49 @@ +// Debugger.Object.protoype.getPromiseReactions reports directly resolved promises. + +load(libdir + 'array-compare.js'); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger; +var DOg = dbg.addDebuggee(g); + +g.eval(` + var pResolve, pReject; + var p = new Promise((resolve, reject) => { pResolve = resolve; pReject = reject }); + var p2 = new Promise((resolve, reject) => { resolve(p); }); + var p3 = new Promise((resolve, reject) => { resolve(p); }); + var p4 = new Promise((resolve, reject) => { resolve(p2); }); +`); + +var [DOp, DOp2, DOp3, DOp4] = [g.p, g.p2, g.p3, g.p4].map(p => DOg.makeDebuggeeValue(p)); + +// Since the standard resolving functions enqueue a job to do the `then` call, +// none of the reactions should be visible yet. +assertEq(true, arraysEqual(DOp.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp2.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp3.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp4.getPromiseReactions(), [])); + +// This should let them all appear in place. +drainJobQueue(); + +assertEq(true, arraysEqual(DOp.getPromiseReactions(), [DOp2, DOp3])); +assertEq(true, arraysEqual(DOp2.getPromiseReactions(), [DOp4])); +assertEq(true, arraysEqual(DOp3.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp4.getPromiseReactions(), [])); + +// Resolving the initial promise should kick off its reactions, but propagating +// that the rest of the way requires microtasks. +g.pResolve(42); + +assertEq(true, arraysEqual(DOp.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp2.getPromiseReactions(), [DOp4])); +assertEq(true, arraysEqual(DOp3.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp4.getPromiseReactions(), [])); + +// Let the propagation complete. +drainJobQueue(); + +assertEq(true, arraysEqual(DOp.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp2.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp3.getPromiseReactions(), [])); +assertEq(true, arraysEqual(DOp4.getPromiseReactions(), [])); diff --git a/js/src/jit-test/tests/debug/Object-getPromiseReactions-03.js b/js/src/jit-test/tests/debug/Object-getPromiseReactions-03.js new file mode 100644 index 0000000000..81d639efb7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getPromiseReactions-03.js @@ -0,0 +1,41 @@ +// Debugger.Object.prototype.getPromiseReactions reports reaction records +// created with `then` and `catch`. + +load(libdir + 'match.js'); +load(libdir + 'match-debugger.js'); + +const { Pattern } = Match; +const { OBJECT_WITH_EXACTLY: EXACT } = Pattern; +function EQ(v) { + return new DebuggerIdentical(v); +} + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger; +var DOg = dbg.addDebuggee(g); + +g.eval(` + function identity(v) { return v; } + function thrower(e) { throw e; } + function fib(n) { if (n <= 1) return 1; else return fib(n-1) + fib(n-2); } + function triangle(n) { return (n+1) * n / 2; } + + var pResolve, pReject; + var p = new Promise((resolve, reject) => { pResolve = resolve; pReject = reject }); + var p2 = p.then(identity, thrower); + var p3 = p.then(fib); + var p4 = p.catch(triangle); +`); + +var [DOidentity, DOthrower, DOfib, DOtriangle, DOp, DOp2, DOp3, DOp4] = + [g.identity, g.thrower, g.fib, g.triangle, g.p, g.p2, g.p3, g.p4].map(p => DOg.makeDebuggeeValue(p)); + +Match.Pattern([ + EXACT({ resolve: EQ(DOidentity), reject: EQ(DOthrower), result: EQ(DOp2) }), + EXACT({ resolve: EQ(DOfib), result: EQ(DOp3) }), + EXACT({ reject: EQ(DOtriangle), result: EQ(DOp4) }) +]).assert(DOp.getPromiseReactions(), + "promiseReactions doesn't return expected reaction list"); + +g.pResolve(3); +assertEq(DOp.getPromiseReactions().length, 0); diff --git a/js/src/jit-test/tests/debug/Object-getPromiseReactions-04.js b/js/src/jit-test/tests/debug/Object-getPromiseReactions-04.js new file mode 100644 index 0000000000..47278c7d6e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getPromiseReactions-04.js @@ -0,0 +1,48 @@ +// Debugger.Object.prototype.getPromiseReactions reports reaction records +// created by `await` expressions in async functions. + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger; +const DOg = dbg.addDebuggee(g); + +g.eval(` + var pResolve, pReject; + var p0 = new Promise((resolve, reject) => { pResolve = resolve; pReject = reject }); + + // In this case, promiseReactions encounters a Debugger.Frame we had already + // associated with the generator, when we hit the debugger statement. + async function f1() { debugger; await p0; } + + // In this case, promiseReactions must construct the Debugger.Frame itself, + // since it is the first to encounter the generator. + async function f2() { await p0; debugger; } +`); + +let DFf1, DFf2; +dbg.onDebuggerStatement = (frame) => { + DFf1 = frame; + dbg.onDebuggerStatement = (frame) => { + DFf2 = frame; + dbg.onDebuggerStatement = () => { throw "Shouldn't fire twice"; }; + }; +}; + +g.eval(`var p1 = f1();`); +assertEq(DFf1.callee.name, "f1"); + +g.eval(`var p2 = f2();`); +assertEq(DFf2, undefined); + +const [DOp0, DOp1, DOp2] = + [g.p0, g.p1, g.p2].map(p => DOg.makeDebuggeeValue(p)); + +const reactions = DOp0.getPromiseReactions(); +assertEq(reactions.length, 2); +assertEq(reactions[0], DFf1); +assertEq(true, reactions[1] instanceof Debugger.Frame); + +// Let f2 run until it hits its debugger statement. +g.pResolve(42); +drainJobQueue(); +assertEq(DFf2.terminated, true); +assertEq(reactions[1], DFf2); diff --git a/js/src/jit-test/tests/debug/Object-getPromiseReactions-05.js b/js/src/jit-test/tests/debug/Object-getPromiseReactions-05.js new file mode 100644 index 0000000000..89fc1fe70b --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getPromiseReactions-05.js @@ -0,0 +1,48 @@ +// Debugger.Object.prototype.getPromiseReactions reports reaction records +// created by `await` expressions in async generators. + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger; +const DOg = dbg.addDebuggee(g); + +g.eval(` + var pResolve, pReject; + var p0 = new Promise((resolve, reject) => { pResolve = resolve; pReject = reject }); + + // In this case, promiseReactions encounters a Debugger.Frame we had already + // associated with the generator, when we hit the debugger statement. + async function* f1() { debugger; await p0; } + + // In this case, promiseReactions must construct the Debugger.Frame itself, + // since it is the first to encounter the generator. + async function* f2() { await p0; debugger; } +`); + +let DFf1, DFf2; +dbg.onDebuggerStatement = (frame) => { + DFf1 = frame; + dbg.onDebuggerStatement = (frame) => { + DFf2 = frame; + dbg.onDebuggerStatement = () => { throw "Shouldn't fire twice"; }; + }; +}; + +g.eval(`var p1 = f1().next();`); +assertEq(DFf1.callee.name, "f1"); + +g.eval(`var p2 = f2().next();`); +assertEq(DFf2, undefined); + +const [DOp0, DOp1, DOp2] = + [g.p0, g.p1, g.p2].map(p => DOg.makeDebuggeeValue(p)); + +const reactions = DOp0.getPromiseReactions(); +assertEq(reactions.length, 2); +assertEq(reactions[0], DFf1); +assertEq(true, reactions[1] instanceof Debugger.Frame); + +// Let f2 run until it hits its debugger statement. +g.pResolve(42); +drainJobQueue(); +assertEq(DFf2.terminated, true); +assertEq(reactions[1], DFf2); diff --git a/js/src/jit-test/tests/debug/Object-getPromiseReactions-06.js b/js/src/jit-test/tests/debug/Object-getPromiseReactions-06.js new file mode 100644 index 0000000000..99d0bba872 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getPromiseReactions-06.js @@ -0,0 +1,43 @@ +// Debugger.Object.prototype.getPromiseReactions should handle reaction records +// that are cross-compartment with their promises. + +var dbg = new Debugger; + +var g1 = newGlobal({ newCompartment: true }); +var DOg1 = dbg.addDebuggee(g1); + +var g2 = newGlobal({ newCompartment: true }); +var DOg2 = dbg.addDebuggee(g2); + +g1.eval(` + var pResolve, pReject; + var p = new Promise((resolve, reject) => { pResolve = resolve; pReject = reject }); +`); + +g2.p = g1.p; +g2.eval(` + var p2 = new Promise((resolve, reject) => { resolve(p); }); +`); + +const DOp = DOg1.makeDebuggeeValue(g1.p); +const DOp2 = DOg2.makeDebuggeeValue(g2.p2); + +// Since the standard resolving functions enqueue a job to do the `then` call, +// we need to drain the queue before p2 will appear on p1's reaction list. +drainJobQueue(); + +const reactions = DOp.getPromiseReactions(); +assertEq(true, Array.isArray(reactions)); +assertEq(reactions.length, 1); +assertEq(typeof reactions[0], "object"); +assertEq(true, reactions[0].resolve instanceof Debugger.Object); +assertEq(true, reactions[0].resolve.callable); +assertEq(true, reactions[0].reject instanceof Debugger.Object); +assertEq(true, reactions[0].reject.callable); + +// Unfortunately, this is not p2; it's the promise returned by the internal call +// to `then` that attached the reaction record to p. See bug 1603575 for ideas +// about how to actually retrieve p2. +assertEq(true, reactions[0].result instanceof Debugger.Object); +assertEq(reactions[0].result.class, "Promise"); +assertEq(true, reactions[0].result !== DOp2); diff --git a/js/src/jit-test/tests/debug/Object-getProperty-01.js b/js/src/jit-test/tests/debug/Object-getProperty-01.js new file mode 100644 index 0000000000..378611b7e2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getProperty-01.js @@ -0,0 +1,122 @@ +// tests calling script functions via Debugger.Object.prototype.getProperty +"use strict"; + +var global = newGlobal({newCompartment: true}); +var dbg = new Debugger(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +global.eval(` +const normalObj = { }; +const abruptObj = { }; +const sym = Symbol("a symbol key"); + +const arr = [1, 2, 3]; +const obj = { + get stringNormal(){ + return "a value"; + }, + get stringAbrupt() { + throw "a value"; + }, + get objectNormal() { + return normalObj; + }, + get objectAbrupt() { + throw abruptObj; + }, + get context() { + return this; + }, + + 1234: "number key", + [sym]: "symbol key", + stringProp: "a value", + objectProp: {}, + method() { + return "a value"; + }, + undefined: "undefined value", +}; +const propObj = obj.objectProp; +const methodObj = obj.method; + +const objChild = Object.create(obj); + +const proxyChild = new Proxy(obj, {}); + +debugger; +`); + +function onDebuggerStatement(frame) { + const { environment } = frame; + const arr = environment.getVariable("arr"); + const obj = environment.getVariable("obj"); + const objChild = environment.getVariable("objChild"); + const proxyChild = environment.getVariable("proxyChild"); + + const sym = environment.getVariable("sym"); + const normalObj = environment.getVariable("normalObj"); + const abruptObj = environment.getVariable("abruptObj"); + const propObj = environment.getVariable("propObj"); + const methodObj = environment.getVariable("methodObj"); + + assertEq(arr.getProperty(1).return, 2); + assertEq(arr.getProperty("1").return, 2); + + assertEq(obj.getProperty().return, "undefined value"); + + assertEq(obj.getProperty("missing").return, undefined); + + assertEq(obj.getProperty("stringNormal").return, "a value"); + assertEq(obj.getProperty("stringAbrupt").throw, "a value"); + + assertEq(obj.getProperty("objectNormal").return, normalObj); + assertEq(obj.getProperty("objectAbrupt").throw, abruptObj); + + assertEq(obj.getProperty("context").return, obj); + + assertEq(obj.getProperty(1234).return, "number key"); + assertEq(obj.getProperty(sym).return, "symbol key"); + assertEq(obj.getProperty("stringProp").return, "a value"); + assertEq(obj.getProperty("objectProp").return, propObj); + + assertEq(obj.getProperty("method").return, methodObj); + + assertEq(objChild.getProperty().return, "undefined value"); + + assertEq(objChild.getProperty("missing").return, undefined); + + assertEq(objChild.getProperty("stringNormal").return, "a value"); + assertEq(objChild.getProperty("stringAbrupt").throw, "a value"); + + assertEq(objChild.getProperty("objectNormal").return, normalObj); + assertEq(objChild.getProperty("objectAbrupt").throw, abruptObj); + + assertEq(objChild.getProperty("context").return, objChild); + + assertEq(objChild.getProperty(1234).return, "number key"); + assertEq(objChild.getProperty(sym).return, "symbol key"); + assertEq(objChild.getProperty("stringProp").return, "a value"); + assertEq(objChild.getProperty("objectProp").return, propObj); + + assertEq(objChild.getProperty("method").return, methodObj); + + assertEq(proxyChild.getProperty().return, "undefined value"); + + assertEq(proxyChild.getProperty("missing").return, undefined); + + assertEq(proxyChild.getProperty("stringNormal").return, "a value"); + assertEq(proxyChild.getProperty("stringAbrupt").throw, "a value"); + + assertEq(proxyChild.getProperty("objectNormal").return, normalObj); + assertEq(proxyChild.getProperty("objectAbrupt").throw, abruptObj); + + assertEq(proxyChild.getProperty("context").return, proxyChild); + + assertEq(proxyChild.getProperty(1234).return, "number key"); + assertEq(proxyChild.getProperty(sym).return, "symbol key"); + assertEq(proxyChild.getProperty("stringProp").return, "a value"); + assertEq(proxyChild.getProperty("objectProp").return, propObj); + + assertEq(proxyChild.getProperty("method").return, methodObj); +}; diff --git a/js/src/jit-test/tests/debug/Object-getProperty-02.js b/js/src/jit-test/tests/debug/Object-getProperty-02.js new file mode 100644 index 0000000000..cb38cb5a26 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getProperty-02.js @@ -0,0 +1,35 @@ +// tests calling script functions via Debugger.Object.prototype.getProperty +// to see if they trigger debugger traps +"use strict"; + +var global = newGlobal({newCompartment: true}); +var dbg = new Debugger(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +let obj; +global.eval(` +const obj = { + get prop() { + debugger; + return 42; + } +}; + +debugger; +`); + + +function onDebuggerStatement(frame) { + dbg.onDebuggerStatement = onDebuggerStatementGetter; + + obj = frame.environment.getVariable("obj"); +} + +let debuggerRan = false; + +assertEq(obj.getProperty("prop").return, 42); +assertEq(debuggerRan, true); + +function onDebuggerStatementGetter(frame) { + debuggerRan = true; +} diff --git a/js/src/jit-test/tests/debug/Object-getProperty-03.js b/js/src/jit-test/tests/debug/Object-getProperty-03.js new file mode 100644 index 0000000000..a70b7f6e47 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getProperty-03.js @@ -0,0 +1,46 @@ +// tests calling script functions via Debugger.Object.prototype.getProperty +// with different receiver objects. +"use strict"; +load(libdir + "/asserts.js"); + +var global = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var globalDO = dbg.addDebuggee(global); +var windowProxyDO = globalDO.makeDebuggeeValue(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +global.eval(` +const sloppy = { + get getter() { return this; }, +}; +const strict = { + get getter() { "use strict"; return this; }, +}; +debugger; +`); + +function onDebuggerStatement(frame) { + const { environment } = frame; + const sloppy = environment.getVariable("sloppy"); + const strict = environment.getVariable("strict"); + + assertEq(sloppy.getProperty("getter").return, sloppy); + assertEq(sloppy.getProperty("getter", sloppy).return, sloppy); + assertEq(sloppy.getProperty("getter", strict).return, strict); + assertEq(sloppy.getProperty("getter", 1).return.class, "Number"); + assertEq(sloppy.getProperty("getter", true).return.class, "Boolean"); + assertEq(sloppy.getProperty("getter", null).return, windowProxyDO); + assertEq(sloppy.getProperty("getter", undefined).return, windowProxyDO); + assertErrorMessage(() => sloppy.getProperty("getter", {}), TypeError, + "Debugger: expected Debugger.Object, got Object"); + + assertEq(strict.getProperty("getter").return, strict); + assertEq(strict.getProperty("getter", sloppy).return, sloppy); + assertEq(strict.getProperty("getter", strict).return, strict); + assertEq(strict.getProperty("getter", 1).return, 1); + assertEq(strict.getProperty("getter", true).return, true); + assertEq(strict.getProperty("getter", null).return, null); + assertEq(strict.getProperty("getter", undefined).return, undefined); + assertErrorMessage(() => strict.getProperty("getter", {}), TypeError, + "Debugger: expected Debugger.Object, got Object"); +}; diff --git a/js/src/jit-test/tests/debug/Object-getProperty-non-primitive-key.js b/js/src/jit-test/tests/debug/Object-getProperty-non-primitive-key.js new file mode 100644 index 0000000000..649764322e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-getProperty-non-primitive-key.js @@ -0,0 +1,37 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var gw = dbg.addDebuggee(g); + +g.eval(` + var obj = { + p: 1, + [Symbol.iterator]: 2, + }; +`); + +// Return |key| as an object. +function toObject(key) { + return { + [Symbol.toPrimitive]() { + return key; + } + }; +} + +let obj = gw.getProperty("obj").return; + +for (let key of obj.getOwnPropertyNames()) { + let keyObject = toObject(key); + let value = g.obj[key]; + + assertEq(obj.getProperty(key).return, value); + assertEq(obj.getProperty(keyObject).return, value); +} + +for (let key of obj.getOwnPropertySymbols()) { + let keyObject = toObject(key); + let value = g.obj[key]; + + assertEq(obj.getProperty(key).return, value); + assertEq(obj.getProperty(keyObject).return, value); +} diff --git a/js/src/jit-test/tests/debug/Object-identity-01.js b/js/src/jit-test/tests/debug/Object-identity-01.js new file mode 100644 index 0000000000..2eaaa8ab12 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-identity-01.js @@ -0,0 +1,10 @@ +// Two references to the same object get the same Debugger.Object wrapper. +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.arguments[0], frame.arguments[1]); + hits++; +}; +g.eval("var obj = {}; function f(a, b) { debugger; } f(obj, obj);"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Object-identity-02.js b/js/src/jit-test/tests/debug/Object-identity-02.js new file mode 100644 index 0000000000..f488c25b3e --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-identity-02.js @@ -0,0 +1,10 @@ +// Different objects get different Debugger.Object wrappers. +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.arguments[0] === frame.arguments[1], false); + hits++; +}; +g.eval("function f(a, b) { debugger; } f({}, {});"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Object-identity-03.js b/js/src/jit-test/tests/debug/Object-identity-03.js new file mode 100644 index 0000000000..0e2af5220b --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-identity-03.js @@ -0,0 +1,25 @@ +// The same object gets the same Debugger.Object wrapper at different times, if the difference would be observable. + +var N = 12; + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var wrappers = []; + +dbg.onDebuggerStatement = function (frame) { wrappers.push(frame.arguments[0]); }; +g.eval("var originals = []; function f(x) { originals.push(x); debugger; }"); +for (var i = 0; i < N; i++) + g.eval("f({});"); +assertEq(wrappers.length, N); + +for (var i = 0; i < N; i++) + for (var j = i + 1; j < N; j++) + assertEq(wrappers[i] === wrappers[j], false); + +gc(); + +dbg.onDebuggerStatement = function (frame) { assertEq(frame.arguments[0], wrappers.pop()); }; +g.eval("function h(x) { debugger; }"); +for (var i = 0; i < N; i++) + g.eval("h(originals.pop());"); +assertEq(wrappers.length, 0); diff --git a/js/src/jit-test/tests/debug/Object-isArrowFunction.js b/js/src/jit-test/tests/debug/Object-isArrowFunction.js new file mode 100644 index 0000000000..6d7bd531b6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-isArrowFunction.js @@ -0,0 +1,24 @@ +// Debugger.Object.prototype.isArrowFunction recognizes arrow functions. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); +var hits = 0; + +function checkIsArrow(shouldBe, expr) { + print(expr); + assertEq(gDO.executeInGlobal(expr).return.isArrowFunction, shouldBe); +} + +checkIsArrow(true, '() => { }'); +checkIsArrow(true, '(a) => { bleh; }'); +checkIsArrow(false, 'Object.getPrototypeOf(() => { })'); +checkIsArrow(false, '(function () { })'); +checkIsArrow(false, 'function f() { } f'); +checkIsArrow((void 0), '({})'); +checkIsArrow(false, 'Math.atan2'); +checkIsArrow(false, 'Function.prototype'); +checkIsArrow(false, 'Function("")'); +checkIsArrow(false, 'new Function("")'); +checkIsArrow(false, '(async function f () {})'); +checkIsArrow(true, '(async () => { })'); diff --git a/js/src/jit-test/tests/debug/Object-isClassConstructor.js b/js/src/jit-test/tests/debug/Object-isClassConstructor.js new file mode 100644 index 0000000000..342e9d1a52 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-isClassConstructor.js @@ -0,0 +1,21 @@ +// Debugger.Object.prototype.isClassConstructor recognizes ES6 classes. + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(); +var gDO = dbg.addDebuggee(g); +var hits = 0; + +function checkIsClassConstructor(shouldBe, expr) { + print(expr); + assertEq(gDO.executeInGlobal(expr).return.isClassConstructor, shouldBe); +} + +checkIsClassConstructor(true, "class MyClass{}; MyClass;"); +checkIsClassConstructor(false, "class MyClass2{}; MyClass2.constructor;"); +checkIsClassConstructor( + false, + "class MyClass3{}; Object.getPrototypeOf(MyClass3)" +); +checkIsClassConstructor(false, "(a) => { bleh; }"); +checkIsClassConstructor(false, "(async function f () {})"); +checkIsClassConstructor(void 0, "({})"); diff --git a/js/src/jit-test/tests/debug/Object-isSameNative-01.js b/js/src/jit-test/tests/debug/Object-isSameNative-01.js new file mode 100644 index 0000000000..9359062cd6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-isSameNative-01.js @@ -0,0 +1,9 @@ +// Test that isSameNative will accept a CCW as an argument. + +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(); +var gdbg = dbg.addDebuggee(g); + +var g2 = newGlobal({ newCompartment: true }); + +assertEq(gdbg.getProperty("print").return.isSameNative(g2.print), true); diff --git a/js/src/jit-test/tests/debug/Object-isSameNative.js b/js/src/jit-test/tests/debug/Object-isSameNative.js new file mode 100644 index 0000000000..b01b6b4611 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-isSameNative.js @@ -0,0 +1,46 @@ +// Test that the onNativeCall hook is called when expected. + +load(libdir + 'eqArrayHelper.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var gdbg = dbg.addDebuggee(g); + +assertEq(gdbg.getProperty("print").return.isSameNative(print), true); +assertEq(gdbg.getProperty("print").return.isSameNative(newGlobal), false); + +g.eval(` +const x = []; +Object.defineProperty(x, "a", { + get: print, + set: print, +}); +function f() { + x.a++; + x.length = 0; + x.push(4, 5, 6); + x.sort(print); +} +`); + +const comparisons = [ + print, + Array.prototype.push, + Array.prototype.sort, // Note: self-hosted + newGlobal +]; + +const rv = []; +dbg.onNativeCall = (callee, reason) => { + for (const fn of comparisons) { + if (callee.isSameNative(fn)) { + rv.push(fn.name); + } + } +} + +for (let i = 0; i < 5; i++) { + rv.length = 0; + gdbg.executeInGlobal(`f()`); + assertEqArray(rv, ["print", "print", "push", "sort"]); +} diff --git a/js/src/jit-test/tests/debug/Object-makeDebuggeeNativeFunction-01.js b/js/src/jit-test/tests/debug/Object-makeDebuggeeNativeFunction-01.js new file mode 100644 index 0000000000..996ad7cffc --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-makeDebuggeeNativeFunction-01.js @@ -0,0 +1,28 @@ +// Debugger.Object.prototype.makeDebuggeeNativeFunction does what it is +// supposed to do. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +// It would be nice if we could test that this call doesn't produce a CCW, +// and that calling makeDebuggeeValue instead does, but +// Debugger.Object.isProxy only returns true for scripted proxies. +const push = gw.makeDebuggeeNativeFunction(Array.prototype.push); + +gw.setProperty("push", push); +g.eval("var x = []; push.call(x, 2); x.push(3)"); +assertEq(g.x[0], 2); +assertEq(g.x[1], 3); + +// Interpreted functions should throw. +assertThrowsInstanceOf(() => { + gw.makeDebuggeeNativeFunction(() => {}); +}, Error); + +// Native functions which have extended slots should throw. +let f; +new Promise(resolve => { f = resolve; }) +assertThrowsInstanceOf(() => gw.makeDebuggeeNativeFunction(f), Error); diff --git a/js/src/jit-test/tests/debug/Object-makeDebuggeeValue-01.js b/js/src/jit-test/tests/debug/Object-makeDebuggeeValue-01.js new file mode 100644 index 0000000000..4b281421d4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-makeDebuggeeValue-01.js @@ -0,0 +1,42 @@ +// Debugger.Object.prototype.makeDebuggeeValue creates only one +// Debugger.Object instance for each debuggee object. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval("var x = { 'now playing': 'Joy Division' };"); +g.eval("var y = { 'mood': 'bleak' };"); + +wx = gw.makeDebuggeeValue(g.x); +assertEq(wx, gw.makeDebuggeeValue(g.x)); +assertEq(wx === g.x, false); +assertEq("now playing" in wx, false); +assertEq(wx.getOwnPropertyNames().indexOf("now playing"), 0); +wx.commentary = "deconstruction"; +assertEq("deconstruction" in g.x, false); + +wy = gw.makeDebuggeeValue(g.y); +assertEq(wy === wx, false); +wy.commentary = "reconstruction"; +assertEq(wx.commentary, "deconstruction"); + +// Separate debuggers get separate Debugger.Object instances, but both +// instances' referents are the same underlying object. +var dbg2 = new Debugger(); +var gw2 = dbg2.addDebuggee(g); +w2x = gw2.makeDebuggeeValue(g.x); +assertEq(wx === w2x, false); +w2x.defineProperty("breadcrumb", { value: "pumpernickel" }); +assertEq(wx.getOwnPropertyDescriptor("breadcrumb").value, "pumpernickel"); + +// Non-objects are already debuggee values. +assertEq(gw.makeDebuggeeValue("foonting turlingdromes"), "foonting turlingdromes"); +assertEq(gw.makeDebuggeeValue(true), true); +assertEq(gw.makeDebuggeeValue(false), false); +assertEq(gw.makeDebuggeeValue(null), null); +assertEq(gw.makeDebuggeeValue(1729), 1729); +assertEq(gw.makeDebuggeeValue(Math.PI), Math.PI); +assertEq(gw.makeDebuggeeValue(undefined), undefined); +var s = g.eval("Symbol('Stavromula Beta')"); +assertEq(gw.makeDebuggeeValue(s), s); diff --git a/js/src/jit-test/tests/debug/Object-makeDebuggeeValue-02.js b/js/src/jit-test/tests/debug/Object-makeDebuggeeValue-02.js new file mode 100644 index 0000000000..23c493ea43 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-makeDebuggeeValue-02.js @@ -0,0 +1,12 @@ +// Debugger.Object.prototype.makeDebuggeeValue returns the object wrapped +// the same way as Debugger.Frame.prototype.eval, etc. +var g = newGlobal({newCompartment: true}); +g.eval("function f() { debugger; }"); +var dbg = Debugger(); +var gw = dbg.addDebuggee(g); +var jsonw; +dbg.onDebuggerStatement = function (frame) { + jsonw = frame.eval("JSON").return; +}; +g.eval("debugger;"); +assertEq(gw.makeDebuggeeValue(g.JSON), jsonw); diff --git a/js/src/jit-test/tests/debug/Object-name-01.js b/js/src/jit-test/tests/debug/Object-name-01.js new file mode 100644 index 0000000000..5159720de9 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-name-01.js @@ -0,0 +1,17 @@ +// Debugger.Object.prototype.name + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var name; +dbg.onDebuggerStatement = function (frame) { name = frame.callee.name; }; + +g.eval("(function f() { debugger; })();"); +assertEq(name, "f"); +g.eval("(function () { debugger; })();"); +assertEq(name, undefined); +g.eval("Function('debugger;')();"); +assertEq(name, "anonymous"); +g.eval("(async function grondo() { debugger; })();"); +assertEq(name, "grondo"); +g.eval("(async function* estux() { debugger; })().next();"); +assertEq(name, "estux"); diff --git a/js/src/jit-test/tests/debug/Object-name-02.js b/js/src/jit-test/tests/debug/Object-name-02.js new file mode 100644 index 0000000000..b9ae25ca88 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-name-02.js @@ -0,0 +1,16 @@ +// The .name of a non-function object is undefined. + +var g = newGlobal({newCompartment: true}); +var hits = 0; +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.arguments[0].name, undefined); + hits++; +}; +g.eval("function f(nonfunction) { debugger; }"); + +g.eval("f({});"); +g.eval("f(/a*/);"); +g.eval("f({name: 'bad'});"); +g.eval("f(new Proxy({}, {}));"); +assertEq(hits, 4); diff --git a/js/src/jit-test/tests/debug/Object-parameterNames.js b/js/src/jit-test/tests/debug/Object-parameterNames.js new file mode 100644 index 0000000000..3e3c504714 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-parameterNames.js @@ -0,0 +1,32 @@ +load(libdir + 'array-compare.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); +var hits = 0; + +function check(expr, expected) { + print("checking " + JSON.stringify(expr)); + + let completion = gDO.executeInGlobal(expr); + if (completion.throw) + throw completion.throw.unsafeDereference(); + + let fn = completion.return; + if (expected === undefined) + assertEq(fn.parameterNames, undefined); + else + assertEq(arraysEqual(fn.parameterNames, expected), true); +} + +check('(function () {})', []); +check('(function (x) {})', ["x"]); +check('(function (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z) {})', + ["a","b","c","d","e","f","g","h","i","j","k","l","m", + "n","o","p","q","r","s","t","u","v","w","x","y","z"]); +check('(function (a, [b, c], {d, e:f}) { })', + ["a", undefined, undefined]); +check('({a:1})', undefined); +check('Math.atan2', [undefined, undefined]); +check('(async function (a, b, c) {})', ["a", "b", "c"]); +check('(async function* (d, e, f) {})', ["d", "e", "f"]); diff --git a/js/src/jit-test/tests/debug/Object-preventExtensions-01.js b/js/src/jit-test/tests/debug/Object-preventExtensions-01.js new file mode 100644 index 0000000000..d5339e2d3d --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-preventExtensions-01.js @@ -0,0 +1,17 @@ +// Basic preventExtensions test. + +var g = newGlobal({newCompartment: true}); +var obj = g.eval("({x: 1})"); +assertEq(g.Object.isExtensible(obj), true); + +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var objw = gw.makeDebuggeeValue(obj); +assertEq(objw.isExtensible(), true); + +assertEq(objw.preventExtensions(), undefined); +assertEq(g.Object.isExtensible(obj), false); +assertEq(objw.isExtensible(), false); + +// Calling preventExtensions again has no effect. +assertEq(objw.preventExtensions(), undefined); diff --git a/js/src/jit-test/tests/debug/Object-promiseDependentPromises-realms.js b/js/src/jit-test/tests/debug/Object-promiseDependentPromises-realms.js new file mode 100644 index 0000000000..537421f6a0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-promiseDependentPromises-realms.js @@ -0,0 +1,17 @@ +// Bug 1475669 - Cross-compartment dependent promises. + +// Create a promise in realm 1. +let g1 = newGlobal({newCompartment: true}); +let p1 = new g1.Promise((_resolve, _reject) => {}); + +// Add a dependent promise in realm 2. +let g2 = newGlobal({newCompartment: true}); +let p2 = g2.Promise.prototype.then.call(p1, g2.eval(`value => {}`)); + +// Use debugger to find p2 from p1. +let dbg = new Debugger; +let g1w = dbg.addDebuggee(g1); +let g2w = dbg.addDebuggee(g2); +let dependents = g1w.makeDebuggeeValue(p1).promiseDependentPromises; +assertEq(dependents.length, 1); +assertEq(dependents[0], g2w.makeDebuggeeValue(p2)); diff --git a/js/src/jit-test/tests/debug/Object-proto.js b/js/src/jit-test/tests/debug/Object-proto.js new file mode 100644 index 0000000000..065810a0b4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-proto.js @@ -0,0 +1,23 @@ +// Debugger.Object.prototype.proto +var g = newGlobal({newCompartment: true}); +var dbgeval = function () { + var dbg = new Debugger(g); + var hits = 0; + g.eval("function f() { debugger; }"); + var lastval; + dbg.onDebuggerStatement = function (frame) { lastval = frame.arguments[0]; }; + return function dbgeval(s) { + g.eval("f(" + s + ");"); + return lastval; + }; + }(); + +var Op = dbgeval("Object.prototype"); +assertEq(Op.proto, null); +assertEq(dbgeval("({})").proto, Op); + +var Ap = dbgeval("[]").proto; +assertEq(Ap, dbgeval("Array.prototype")); +assertEq(Ap.proto, Op); + +assertEq(dbgeval("Object").proto, dbgeval("Function.prototype")); diff --git a/js/src/jit-test/tests/debug/Object-proxy.js b/js/src/jit-test/tests/debug/Object-proxy.js new file mode 100644 index 0000000000..c32233c9c7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-proxy.js @@ -0,0 +1,44 @@ +// Debugger.Object.prototype.isProxy recognizes (scripted) proxies. +// Debugger.Object.prototype.proxyTarget exposes the [[Proxytarget]] of a proxy. +// Debugger.Object.prototype.proxyHandler exposes the [[ProxyHandler]] of a proxy. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); + +g.eval('var target = [1,2,3];'); +g.eval('var handler = {has: ()=>false};'); +g.eval('var proxy = new Proxy(target, handler);'); +g.eval('var proxyProxy = new Proxy(proxy, proxy);'); +g.eval('var revoker = Proxy.revocable(target, handler);'); +g.eval('var revocable = revoker.proxy;'); + +var target = gDO.getOwnPropertyDescriptor('target').value; +var handler = gDO.getOwnPropertyDescriptor('handler').value; +var proxy = gDO.getOwnPropertyDescriptor('proxy').value; +var proxyProxy = gDO.getOwnPropertyDescriptor('proxyProxy').value; +var revocable = gDO.getOwnPropertyDescriptor('revocable').value; + +assertEq(target.isProxy, false); +assertEq(target.proxyTarget, undefined); +assertEq(target.proxyHandler, undefined); + +assertEq(handler.isProxy, false); +assertEq(handler.proxyTarget, undefined); +assertEq(handler.proxyHandler, undefined); + +assertEq(proxy.isProxy, true); +assertEq(proxy.proxyTarget, target); +assertEq(proxy.proxyHandler, handler); + +assertEq(proxyProxy.isProxy, true); +assertEq(proxyProxy.proxyTarget, proxy); +assertEq(proxyProxy.proxyHandler, proxy); + +assertEq(revocable.isProxy, true); +assertEq(revocable.proxyTarget, target); +assertEq(revocable.proxyHandler, handler); +g.eval('revoker.revoke();'); +assertEq(revocable.isProxy, true); +assertEq(revocable.proxyTarget, null); +assertEq(revocable.proxyHandler, null); diff --git a/js/src/jit-test/tests/debug/Object-script-AsmJSNative.js b/js/src/jit-test/tests/debug/Object-script-AsmJSNative.js new file mode 100644 index 0000000000..3854f7f03d --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-script-AsmJSNative.js @@ -0,0 +1,15 @@ +function test(stdlib, foreign) { + "use asm" + function f(y) { + y = +y; + } + return f; +}; +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval(` + var dbg = new Debugger(); + var parentw = dbg.addDebuggee(parent); + var testw = parentw.makeDebuggeeValue(parent.test); + var scriptw = testw.script; +`); diff --git a/js/src/jit-test/tests/debug/Object-script-environment-nondebuggee.js b/js/src/jit-test/tests/debug/Object-script-environment-nondebuggee.js new file mode 100644 index 0000000000..3e77bc89d5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-script-environment-nondebuggee.js @@ -0,0 +1,24 @@ +// The script and environment of a non-debuggee function are null. + +var g = newGlobal({newCompartment: true}); +g.eval('function f() { return "from f"; }'); + +var dbg = new Debugger; +var gw = dbg.makeGlobalObjectReference(g); +var fw = gw.getOwnPropertyDescriptor('f').value; + +// g is not a debuggee, so we can't fetch f's script or environment. +assertEq(fw.script, null); +assertEq(fw.environment, null); + +// If we add g as a debuggee, we can fetch f's script and environment. +dbg.addDebuggee(g); +var fscript = fw.script; +var fenv = fw.environment; +assertEq(fscript instanceof Debugger.Script, true); +assertEq(fenv instanceof Debugger.Environment, true); + +// Removing g as a debuggee makes the script and environment inaccessible again. +dbg.removeDebuggee(g); +assertEq(fw.script, null); +assertEq(fw.environment, null); diff --git a/js/src/jit-test/tests/debug/Object-script-lazy.js b/js/src/jit-test/tests/debug/Object-script-lazy.js new file mode 100644 index 0000000000..a0fd9ef7ff --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-script-lazy.js @@ -0,0 +1,57 @@ +// Asking for the script of a Debugger.Object should work, even when lazy +// functions are involved. + +// Note that we never hand out the scripts of non-debuggee functions, and +// putting a compartment in debug mode automatically de-lazifies all its +// functions. (This ensures that findScripts works, too.) +// +// So here we create a reference to a non-debuggee function, verify that we +// can't access its interesting properties, make it a debuggee, and verify +// that everything becomes available. + +// Create functions f, g in a non-debuggee compartment. +var g1 = newGlobal({newCompartment: true}); +g1.eval('function f() { return "from f"; }'); +g1.eval('function g() { return "from g"; }'); +g1.eval('this.h = f.bind(g, 42, "foo");'); + +// Create a debuggee compartment with CCWs referring to f and g. +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var g2w = dbg.addDebuggee(g2); +g2.f = g1.f; +g2.g = g1.g; +g2.h = g1.h; + +// At this point, g1.f should still be a lazy function. Unwrapping a D.O +// referring to g2's CCW of f should yield a D.O referring to f directly. +// Asking for that second D.O's script should yield null, because it's not +// a debuggee. +var fDO = g2w.getOwnPropertyDescriptor('f').value; +assertEq(fDO.unwrap().class, "Function"); +assertEq(fDO.unwrap().script, null); + +// Similarly for g1.g, and asking for its parameter names. +var gDO = g2w.getOwnPropertyDescriptor('g').value; +assertEq(gDO.unwrap().parameterNames, undefined); + +// Similarly for g1.h, and asking for its bound function properties. +var hDO = g2w.getOwnPropertyDescriptor('h').value; +assertEq(hDO.unwrap().class, "Function"); +assertEq(hDO.unwrap().isBoundFunction, undefined); +assertEq(hDO.unwrap().isArrowFunction, undefined); +assertEq(hDO.unwrap().boundTargetFunction, undefined); +assertEq(hDO.unwrap().boundThis, undefined); +assertEq(hDO.unwrap().boundArguments, undefined); + +// Add g1 as a debuggee, and verify that we can get everything. +dbg.addDebuggee(g1); +assertEq(fDO.unwrap().script instanceof Debugger.Script, true); +assertEq(gDO.unwrap().parameterNames instanceof Array, true); +assertEq(hDO.unwrap().isBoundFunction, true); +assertEq(hDO.unwrap().isArrowFunction, false); +assertEq(hDO.unwrap().boundTargetFunction, fDO.unwrap()); +assertEq(hDO.unwrap().boundThis, gDO.unwrap()); +assertEq(hDO.unwrap().boundArguments.length, 2); +assertEq(hDO.unwrap().boundArguments[0], 42); +assertEq(hDO.unwrap().boundArguments[1], "foo"); diff --git a/js/src/jit-test/tests/debug/Object-script.js b/js/src/jit-test/tests/debug/Object-script.js new file mode 100644 index 0000000000..fbaa0232fa --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-script.js @@ -0,0 +1,25 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); + +function check(expr, expected) { + print("checking " + JSON.stringify(expr) + ", expecting " + + (expected ? "script" : "no script")); + + let completion = gDO.executeInGlobal(expr); + if (completion.throw) + throw completion.throw.unsafeDereference(); + + let val = completion.return; + if (expected) + assertEq(val.script instanceof Debugger.Script, true); + else + assertEq(val.script, undefined); +} + +check('(function g(){})', true); +check('(function* h() {})', true); +check('(async function j() {})', true); +check('(async function* k() {})', true); +check('({})', false); +check('Math.atan2', false); diff --git a/js/src/jit-test/tests/debug/Object-seal-01.js b/js/src/jit-test/tests/debug/Object-seal-01.js new file mode 100644 index 0000000000..f8190f4ecc --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-seal-01.js @@ -0,0 +1,63 @@ +// Basic tests for obj.{seal,freeze,preventExtensions,isSealed,isFrozen,isExtensible}. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +g.eval("function compareObjects() {\n" + + " assertEq(Object.isExtensible(x), Object.isExtensible(y));\n" + + " var xnames = Object.getOwnPropertyNames(x).sort();\n" + + " var ynames = Object.getOwnPropertyNames(y).sort();\n" + + " assertEq(xnames.length, ynames.length);\n" + + " for (var i = 0; i < xnames.length; i++) {\n" + + " assertEq(xnames[i], ynames[i]);\n" + + " var name = xnames[i];\n" + + " var xd = Object.getOwnPropertyDescriptor(x, name);\n" + + " var yd = Object.getOwnPropertyDescriptor(y, name);\n" + + " assertEq(xd.configurable, yd.configurable, code + '.' + name + ' .configurable');\n" + + " assertEq(xd.enumerable, yd.enumerable, code + '.' + name + ' .enumerable');\n" + + " assertEq(xd.writable, yd.writable, code + '.' + name + ' .writable');\n" + + " }\n" + + "}\n"); + +function test(code) { + g.code = code; + g.eval("x = (" + code + ");"); + g.eval("y = (" + code + ");"); + var xw = gw.getOwnPropertyDescriptor("x").value; + + function check() { + // The Debugger.Object seal/freeze/preventExtensions methods + // had the same effect as the corresponding ES5 Object methods. + g.compareObjects(); + + // The Debugger.Object introspection methods agree with the ES5 Object methods. + assertEq(xw.isExtensible(), g.Object.isExtensible(g.x), code + ' isExtensible'); + assertEq(xw.isSealed(), g.Object.isSealed(g.x), code + ' isSealed'); + assertEq(xw.isFrozen(), g.Object.isFrozen(g.x), code + ' isFrozen'); + } + + check(); + + xw.preventExtensions(); + assertEq(g.Object.isExtensible(g.x), false, code + ' preventExtensions'); + g.Object.preventExtensions(g.y); + check(); + + xw.seal(); + assertEq(g.Object.isSealed(g.x), true, code + ' seal'); + g.Object.seal(g.y); + check(); + + xw.freeze(); + assertEq(g.Object.isFrozen(g.x), true, code + ' freeze'); + g.Object.freeze(g.y); + check(); +} + +test("{}"); +test("{a: [1], get b() { return -1; }}"); +test("Object.create(null, {x: {value: 3}, y: {get: Math.min}})"); +test("[]"); +test("[,,,,,]"); +test("[0, 1, 2]"); diff --git a/js/src/jit-test/tests/debug/Object-setProperty-01.js b/js/src/jit-test/tests/debug/Object-setProperty-01.js new file mode 100644 index 0000000000..cde5f68e54 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-setProperty-01.js @@ -0,0 +1,185 @@ +// tests calling script functions via Debugger.Object.prototype.setProperty +"use strict"; + +var global = newGlobal({newCompartment: true}); +var dbg = new Debugger(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +global.eval(` +const sym = Symbol("a symbol key"); + +const arr = []; +const obj = { + set stringNormal(value) { + this._stringNormal = value; + }, + set stringAbrupt(value) { + throw value; + }, + + set objectNormal(value) { + this._objectNormal = value; + }, + set objectAbrupt(value) { + throw value; + }, + + set context(value) { + this._context = this; + }, + + set 1234(value) { + this._1234 = value; + }, + + set [sym](value) { + this._sym = value; + }, + + get getterOnly() {}, + readOnly: "", + + stringProp: "", + objProp: {}, +}; +Object.defineProperty(obj, "readOnly", { writable: false }); + +const objChild = Object.create(obj); +const proxyChild = new Proxy(obj, {}); + +const testObj = { }; +const testChildObj = { }; +const testProxyObj = { }; + +debugger; +`); + +function onDebuggerStatement(frame) { + const { environment } = frame; + const sym = environment.getVariable("sym"); + const arr = environment.getVariable("arr"); + const obj = environment.getVariable("obj"); + const objChild = environment.getVariable("objChild"); + const proxyChild = environment.getVariable("proxyChild"); + + assertEq(arr.setProperty(1, "index 1").return, true); + assertEq(arr.getProperty(1).return, "index 1"); + assertEq(arr.getProperty("1").return, "index 1"); + assertEq(arr.getProperty(0).return, undefined); + assertEq(arr.getProperty("length").return, 2); + + assertEq(arr.setProperty("2", "index 2").return, true); + assertEq(arr.getProperty(2).return, "index 2"); + assertEq(arr.getProperty("2").return, "index 2"); + assertEq(arr.getProperty(0).return, undefined); + assertEq(arr.getProperty("length").return, 3); + + const testObj = environment.getVariable("testObj"); + assertEq(obj.setProperty("undefined", 123).return, true); + assertEq(obj.getProperty("undefined").return, 123); + assertEq(obj.setProperty().return, true); + assertEq(obj.getProperty("undefined").return, undefined); + + assertEq(obj.setProperty("missing", 42).return, true); + assertEq(obj.getProperty("missing").return, 42); + + assertEq(obj.setProperty("stringNormal", "a normal value").return, true); + assertEq(obj.getProperty("_stringNormal").return, "a normal value"); + assertEq(obj.setProperty("stringAbrupt", "an abrupt value").throw, "an abrupt value"); + + assertEq(obj.setProperty("objectNormal", testObj).return, true); + assertEq(obj.getProperty("_objectNormal").return, testObj); + assertEq(obj.setProperty("objectAbrupt", testObj).throw, testObj); + + assertEq(obj.setProperty("context", "a value").return, true); + assertEq(obj.getProperty("_context").return, obj); + + assertEq(obj.setProperty(1234, "number value").return, true); + assertEq(obj.getProperty("_1234").return, "number value"); + + assertEq(obj.setProperty(sym, "symbol value").return, true); + assertEq(obj.getProperty("_sym").return, "symbol value"); + + assertEq(obj.setProperty("getterOnly", "a value").return, false); + assertEq(obj.setProperty("readOnly", "a value").return, false); + + assertEq(obj.setProperty("stringProp", "a normal value").return, true); + assertEq(obj.getProperty("stringProp").return, "a normal value"); + + assertEq(obj.setProperty("objectProp", testObj).return, true); + assertEq(obj.getProperty("objectProp").return, testObj); + + const testChildObj = environment.getVariable("testChildObj"); + assertEq(objChild.setProperty("undefined", 123).return, true); + assertEq(objChild.getProperty("undefined").return, 123); + assertEq(objChild.setProperty().return, true); + assertEq(objChild.getProperty("undefined").return, undefined); + + assertEq(objChild.setProperty("missing", 42).return, true); + assertEq(objChild.getProperty("missing").return, 42); + + assertEq(objChild.setProperty("stringNormal", "a normal child value").return, true); + assertEq(objChild.getProperty("_stringNormal").return, "a normal child value"); + + assertEq(objChild.setProperty("stringAbrupt", "an abrupt child value").throw, "an abrupt child value"); + + assertEq(objChild.setProperty("objectNormal", testChildObj).return, true); + assertEq(objChild.getProperty("_objectNormal").return, testChildObj); + + assertEq(objChild.setProperty("objectAbrupt", testChildObj).throw, testChildObj); + + assertEq(objChild.setProperty("context", "a value").return, true); + assertEq(objChild.getProperty("_context").return, objChild); + + assertEq(objChild.setProperty(1234, "number value").return, true); + assertEq(objChild.getProperty("_1234").return, "number value"); + + assertEq(objChild.setProperty(sym, "symbol value").return, true); + assertEq(objChild.getProperty("_sym").return, "symbol value"); + + assertEq(objChild.setProperty("getterOnly", "a value").return, false); + assertEq(objChild.setProperty("readOnly", "a value").return, false); + + assertEq(objChild.setProperty("stringProp", "a normal child value").return, true); + assertEq(objChild.getProperty("stringProp").return, "a normal child value"); + + assertEq(objChild.setProperty("objectProp", testChildObj).return, true); + assertEq(objChild.getProperty("objectProp").return, testChildObj); + + const testProxyObj = environment.getVariable("testProxyObj"); + assertEq(proxyChild.setProperty("undefined", 123).return, true); + assertEq(proxyChild.getProperty("undefined").return, 123); + assertEq(proxyChild.setProperty().return, true); + assertEq(proxyChild.getProperty("undefined").return, undefined); + + assertEq(proxyChild.setProperty("missing", 42).return, true); + assertEq(proxyChild.getProperty("missing").return, 42); + + assertEq(proxyChild.setProperty("stringNormal", "a normal child value").return, true); + assertEq(proxyChild.getProperty("_stringNormal").return, "a normal child value"); + + assertEq(proxyChild.setProperty("stringAbrupt", "an abrupt child value").throw, "an abrupt child value"); + + assertEq(proxyChild.setProperty("objectNormal", testProxyObj).return, true); + assertEq(proxyChild.getProperty("_objectNormal").return, testProxyObj); + + assertEq(proxyChild.setProperty("objectAbrupt", testProxyObj).throw, testProxyObj); + + assertEq(proxyChild.setProperty("context", "a value").return, true); + assertEq(proxyChild.getProperty("_context").return, proxyChild); + + assertEq(proxyChild.setProperty(1234, "number value").return, true); + assertEq(proxyChild.getProperty("_1234").return, "number value"); + + assertEq(proxyChild.setProperty(sym, "symbol value").return, true); + assertEq(proxyChild.getProperty("_sym").return, "symbol value"); + + assertEq(proxyChild.setProperty("getterOnly", "a value").return, false); + assertEq(proxyChild.setProperty("readOnly", "a value").return, false); + + assertEq(proxyChild.setProperty("stringProp", "a normal child value").return, true); + assertEq(proxyChild.getProperty("stringProp").return, "a normal child value"); + + assertEq(proxyChild.setProperty("objectProp", testProxyObj).return, true); + assertEq(proxyChild.getProperty("objectProp").return, testProxyObj); +}; diff --git a/js/src/jit-test/tests/debug/Object-setProperty-02.js b/js/src/jit-test/tests/debug/Object-setProperty-02.js new file mode 100644 index 0000000000..42b61f8190 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-setProperty-02.js @@ -0,0 +1,36 @@ +// tests calling script functions via Debugger.Object.prototype.setProperty +// to see if they trigger debugger traps +"use strict"; + +var global = newGlobal({newCompartment: true}); +var dbg = new Debugger(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +let obj; +global.eval(` +const obj = { + set prop(val) { + debugger; + this._prop = val; + } +}; + +debugger; +`); + + +function onDebuggerStatement(frame) { + dbg.onDebuggerStatement = onDebuggerStatementGetter; + + obj = frame.environment.getVariable("obj"); +} + +let debuggerRan = false; + +assertEq(obj.setProperty("prop", 42).return, true); +assertEq(obj.getProperty("_prop").return, 42); +assertEq(debuggerRan, true); + +function onDebuggerStatementGetter(frame) { + debuggerRan = true; +} diff --git a/js/src/jit-test/tests/debug/Object-setProperty-03.js b/js/src/jit-test/tests/debug/Object-setProperty-03.js new file mode 100644 index 0000000000..002553c477 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-setProperty-03.js @@ -0,0 +1,67 @@ +// tests calling script functions via Debugger.Object.prototype.setProperty +// with different receiver objects. +"use strict"; +load(libdir + "/asserts.js"); + +var global = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var globalDO = dbg.addDebuggee(global); +var windowProxyDO = globalDO.makeDebuggeeValue(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +global.eval(` +let receiver; +function check(value, thisVal) { + receiver = thisVal; + if (value !== "value") throw "Unexpected value"; +} +const sloppy = { + set setter(value) { check(value, this); }, +}; +const strict = { + set setter(value) { "use strict"; check(value, this); }, +}; +debugger; +`); + +function onDebuggerStatement(frame) { + const { environment } = frame; + const sloppy = environment.getVariable("sloppy"); + const strict = environment.getVariable("strict"); + const receiver = () => environment.getVariable("receiver"); + const value = "value"; + + assertEq(sloppy.setProperty("setter", value).return, true); + assertEq(receiver(), sloppy); + assertEq(sloppy.setProperty("setter", value, sloppy).return, true); + assertEq(receiver(), sloppy); + assertEq(sloppy.setProperty("setter", value, strict).return, true); + assertEq(receiver(), strict); + assertEq(sloppy.setProperty("setter", value, 1).return, true); + assertEq(receiver().class, "Number"); + assertEq(sloppy.setProperty("setter", value, true).return, true); + assertEq(receiver().class, "Boolean"); + assertEq(sloppy.setProperty("setter", value, null).return, true); + assertEq(receiver(), windowProxyDO); + assertEq(sloppy.setProperty("setter", value, undefined).return, true); + assertEq(receiver(), windowProxyDO); + assertErrorMessage(() => sloppy.setProperty("setter", value, {}), TypeError, + "Debugger: expected Debugger.Object, got Object"); + + assertEq(strict.setProperty("setter", value).return, true); + assertEq(receiver(), strict); + assertEq(strict.setProperty("setter", value, sloppy).return, true); + assertEq(receiver(), sloppy); + assertEq(strict.setProperty("setter", value, strict).return, true); + assertEq(receiver(), strict); + assertEq(strict.setProperty("setter", value, 1).return, true); + assertEq(receiver(), 1); + assertEq(strict.setProperty("setter", value, true).return, true); + assertEq(receiver(), true); + assertEq(strict.setProperty("setter", value, null).return, true); + assertEq(receiver(), null); + assertEq(strict.setProperty("setter", value, undefined).return, true); + assertEq(receiver(), undefined); + assertErrorMessage(() => strict.setProperty("setter", value, {}), TypeError, + "Debugger: expected Debugger.Object, got Object"); +}; diff --git a/js/src/jit-test/tests/debug/Object-setProperty-non-primitive-key.js b/js/src/jit-test/tests/debug/Object-setProperty-non-primitive-key.js new file mode 100644 index 0000000000..e6aaa138a4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-setProperty-non-primitive-key.js @@ -0,0 +1,49 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var gw = dbg.addDebuggee(g); + +g.eval(` + var obj = { + p: 0, + [Symbol.iterator]: 0, + }; +`); + +// Return |key| as an object. +function toObject(key) { + return { + [Symbol.toPrimitive]() { + return key; + } + }; +} + +let obj = gw.getProperty("obj").return; + +for (let key of obj.getOwnPropertyNames()) { + let keyObject = toObject(key); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.setProperty(key, 2).return, true); + assertEq(g.obj[key], 2); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.setProperty(keyObject, 2).return, true); + assertEq(g.obj[key], 2); +} + +for (let key of obj.getOwnPropertySymbols()) { + let keyObject = toObject(key); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.setProperty(key, 2).return, true); + assertEq(g.obj[key], 2); + + g.obj[key] = 1; + assertEq(g.obj[key], 1); + assertEq(obj.setProperty(keyObject, 2).return, true); + assertEq(g.obj[key], 2); +} diff --git a/js/src/jit-test/tests/debug/Object-unsafeDereference-01.js b/js/src/jit-test/tests/debug/Object-unsafeDereference-01.js new file mode 100644 index 0000000000..daac3555e2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-unsafeDereference-01.js @@ -0,0 +1,10 @@ +// Debugger.Object.prototype.unsafeDereference returns the referent directly. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +assertEq(gw.getOwnPropertyDescriptor('Math').value.unsafeDereference(), g.Math); + +g.eval('var obj = {}'); +assertEq(gw.getOwnPropertyDescriptor('obj').value.unsafeDereference(), g.obj); diff --git a/js/src/jit-test/tests/debug/Object-unwrap-01.js b/js/src/jit-test/tests/debug/Object-unwrap-01.js new file mode 100644 index 0000000000..345723e0e8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-unwrap-01.js @@ -0,0 +1,23 @@ +// Check Debugger.Object.prototype.unwrap surfaces. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger(); +var g = newGlobal({newCompartment: true}); +var gw = dbg.addDebuggee(g); + +assertEq(Object.getOwnPropertyDescriptor(gw, 'unwrap'), undefined); +var d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(gw), 'unwrap'); +assertEq(d.enumerable, false); +assertEq(d.configurable, true); +assertEq(d.writable, true); + +assertEq(typeof gw.unwrap, "function"); +assertEq(gw.unwrap.length, 0); +assertEq(gw.unwrap.name, "unwrap"); + +// It can be called. +gw.unwrap(); + +// You shouldn't be able to apply the accessor to the prototype. +assertThrowsInstanceOf(function () { Debugger.Object.prototype.unwrap(); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Object-unwrap-02.js b/js/src/jit-test/tests/debug/Object-unwrap-02.js new file mode 100644 index 0000000000..3d65db2fe7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-unwrap-02.js @@ -0,0 +1,21 @@ +// Debugger.Object.prototype.unwrap unwraps Debugger.Objects referring to +// cross-compartment wrappers. + +var dbg = new Debugger(); + +var g1 = newGlobal({newCompartment: true}); +var dg1 = dbg.addDebuggee(g1); +assertEq(dg1.unwrap(), dg1); + +var g2 = newGlobal({newCompartment: true}); +var dg2 = dbg.addDebuggee(g2); + +var dg1g2 = dg1.makeDebuggeeValue(g2); +assertEq(dg1g2.unwrap(), dg2.makeDebuggeeValue(g2)); + +// Try an ordinary object, not a global. +var g2o = g2.Object(); +var dg2o = dg2.makeDebuggeeValue(g2o); +var dg1g2o = dg1.makeDebuggeeValue(g2o); +assertEq(dg1g2o.unwrap(), dg2o); +assertEq(dg1g2o.unwrap().unwrap(), dg2o); diff --git a/js/src/jit-test/tests/debug/Object-unwrap-03.js b/js/src/jit-test/tests/debug/Object-unwrap-03.js new file mode 100644 index 0000000000..d6c9180be6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-unwrap-03.js @@ -0,0 +1,15 @@ +// Debugger.Object.prototype.unwrap should not let us see things in +// invisible-to-Debugger compartments. + +load(libdir + 'asserts.js'); + +var g = newGlobal({ newCompartment: true, invisibleToDebugger: true }); + +var dbg = new Debugger; + +// Create a wrapper in our compartment for the global. +// Note that makeGlobalObjectReference won't do: it tries to dereference as far +// as it can go. +var /* yo */ DOwg = dbg.makeGlobalObjectReference(this).makeDebuggeeValue(g); + +assertThrowsInstanceOf(() => DOwg.unwrap(), Error); diff --git a/js/src/jit-test/tests/debug/Promise-race-dependent-promises.js b/js/src/jit-test/tests/debug/Promise-race-dependent-promises.js new file mode 100644 index 0000000000..196d29aeae --- /dev/null +++ b/js/src/jit-test/tests/debug/Promise-race-dependent-promises.js @@ -0,0 +1,46 @@ +// Promise.race(...) may add a dummy PromiseReaction which is only used for the +// debugger. +// +// See BlockOnPromise when called from PerformPromiseRace for when this dummy +// reaction is created. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +function test(withFastPath) { + g.eval(` + function newPromiseCapability() { + var resolve, reject, promise = new Promise(function(r1, r2) { + resolve = r1; + reject = r2; + }); + return {promise, resolve, reject}; + } + + var {promise: alwaysPending} = newPromiseCapability(); + + if (!${withFastPath}) { + // Disable the BlockOnPromise fast path by giving |alwaysPending| a + // non-default "then" function property. This will ensure the dummy + // reaction is created. + alwaysPending.then = function() {}; + } + + var result = Promise.race([alwaysPending]); + `); + + var alwaysPending = gw.makeDebuggeeValue(g.alwaysPending); + var result = gw.makeDebuggeeValue(g.result); + + assertEq(alwaysPending.promiseDependentPromises.length, 1); + assertEq(alwaysPending.promiseDependentPromises[0], result); + + assertEq(result.promiseDependentPromises.length, 0); +} + +// No dummy reaction created when the fast path is taken. +test(true); + +// Dummy reaction is created when we can't take the fast path. +test(false); diff --git a/js/src/jit-test/tests/debug/RematerializedFrame-retval.js b/js/src/jit-test/tests/debug/RematerializedFrame-retval.js new file mode 100644 index 0000000000..18e7778f27 --- /dev/null +++ b/js/src/jit-test/tests/debug/RematerializedFrame-retval.js @@ -0,0 +1,39 @@ +// |jit-test| error: InternalError; --baseline-eager; --ion-eager +// Make sure that return values we store in RematerializedFrames via resumption +// values get propagated to the BaselineFrames we build from them. +// +// Test case from bug 1285939; there's another in bug 1282518, but this one +// takes less time to run, when it doesn't crash. + +var lfLogBuffer = ` +function testResumptionVal(resumptionVal, turnOffDebugMode) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + setInterruptCallback(function () { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame(); + frame.onStep = function () { + frame.onStep = undefined; + return resumptionVal; + }; + return true; + }); + try { + return g.eval("(" + function f() { + invokeInterruptCallback(function (interruptRv) { + f({ valueOf: function () { dbg.g(dbg); }}); + }); + } + ")();"); + } finally { } +} +assertEq(testResumptionVal({ return: "not 42" }), "not 42"); +`; +loadFile(lfLogBuffer); +function loadFile(lfVarx) { + try { + let m = parseModule(lfVarx); + m.declarationInstantiation(); + m.evaluation(); + } catch (lfVare) {} +} + diff --git a/js/src/jit-test/tests/debug/Script-01.js b/js/src/jit-test/tests/debug/Script-01.js new file mode 100644 index 0000000000..c88d6bc3e2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-01.js @@ -0,0 +1,70 @@ +// We get the same Debugger.Script object instance each time we ask. + +var global = newGlobal({newCompartment: true}); +global.eval('function f() { debugger; }'); +global.eval('function g() { debugger; }'); + +var debug = new Debugger(global); + +function evalAndNoteScripts(prog) { + var scripts = {}; + debug.onDebuggerStatement = function(frame) { + if (frame.type == "call") + assertEq(frame.script, frame.callee.script); + scripts.frame = frame.script; + if (frame.arguments[0]) + scripts.argument = frame.arguments[0].script; + }; + global.eval(prog); + return scripts; +} + +// If we create a frame for a function and pass it as a value, those should +// both yield the same Debugger.Script instance. +var scripts = evalAndNoteScripts('f(f)'); +assertEq(scripts.frame, scripts.argument); +var fScript = scripts.argument; + +// If we call a second time, we should still get the same instance. +scripts = evalAndNoteScripts('f(f)'); +assertEq(scripts.frame, fScript); +assertEq(scripts.argument, fScript); + +// If we call with a different argument, we should get a different Debugger.Script. +scripts = evalAndNoteScripts('f(g)'); +assertEq(scripts.frame !== scripts.argument, true); +assertEq(scripts.frame, fScript); +var gScript = scripts.argument; + +// See if we can get g via the frame. +scripts = evalAndNoteScripts('g(f)'); +assertEq(scripts.frame !== scripts.argument, true); +assertEq(scripts.frame, gScript); +assertEq(scripts.argument, fScript); + +// Different closures made from the same 'function' expression should yield +// the same script. +global.eval('function gen1(x) { return function clo(y) { return x+y; }; }'); +global.eval('var clo1 = gen1(42);'); +global.eval('var clo2 = gen1("smoot");'); +var scripts1 = evalAndNoteScripts('f(clo1)'); +var scripts2 = evalAndNoteScripts('f(clo2)'); +assertEq(scripts1.argument, scripts2.argument); + +// Different closures made from the same 'function' declaration should yield +// the same script. +global.eval('function gen2(x) { function clo(y) { return x+y; }; return clo; }'); +global.eval('var clo1 = gen2(42);'); +global.eval('var clo2 = gen2("smoot");'); +var scripts1 = evalAndNoteScripts('f(clo1)'); +var scripts2 = evalAndNoteScripts('f(clo2)'); +assertEq(scripts1.argument, scripts2.argument); + +// Different closures made from the same 'function' statement should yield +// the same script. +global.eval('function gen3(x) { if (true) { function clo(y) { return x+y; }; return clo; } }'); +global.eval('var clo1 = gen3(42);'); +global.eval('var clo2 = gen3("smoot");'); +var scripts1 = evalAndNoteScripts('f(clo1)'); +var scripts2 = evalAndNoteScripts('f(clo2)'); +assertEq(scripts1.argument, scripts2.argument); diff --git a/js/src/jit-test/tests/debug/Script-02.js b/js/src/jit-test/tests/debug/Script-02.js new file mode 100644 index 0000000000..071dc465ae --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-02.js @@ -0,0 +1,6 @@ +// Debugger.Script throws when applied as a constructor. + +load(libdir + 'asserts.js'); + +assertThrowsInstanceOf(function() { Debugger.Script(); }, TypeError); +assertThrowsInstanceOf(function() { new Debugger.Script(); }, TypeError); diff --git a/js/src/jit-test/tests/debug/Script-clearBreakpoint-01.js b/js/src/jit-test/tests/debug/Script-clearBreakpoint-01.js new file mode 100644 index 0000000000..631e9b0a29 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-01.js @@ -0,0 +1,19 @@ +// A breakpoint handler may clear itself. + +var g = newGlobal({newCompartment: true}); +var bphits = 0; +var handler = {hit: function (frame) { frame.script.clearBreakpoint(this); bphits++; }}; +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var offs = frame.script.getLineOffsets(g.line0 + 3); + for (var i = 0; i < offs.length; i++) + frame.script.setBreakpoint(offs[i], handler); + hits++; +}; +g.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "for (var i = 0; i < 4; i++)\n" + // line0 + 2 + " result = 'ok';\n"); // line0 + 3 +assertEq(hits, 1); +assertEq(bphits, 1); diff --git a/js/src/jit-test/tests/debug/Script-clearBreakpoint-02.js b/js/src/jit-test/tests/debug/Script-clearBreakpoint-02.js new file mode 100644 index 0000000000..2de4eb210e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-02.js @@ -0,0 +1,26 @@ +// A breakpoint cleared during dispatch does not fire. +// (Breakpoint dispatch is well-behaved even when breakpoint handlers clear other breakpoints.) + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + var s = frame.script; + function handler(i) { + if (i === 1) + return function () { log += i; s.clearBreakpoint(h[1]); s.clearBreakpoint(h[2]); }; + return function () { log += i; }; + } + var offs = s.getLineOffsets(g.line0 + 2); + var h = []; + for (var i = 0; i < 4; i++) { + h[i] = {hit: handler(i)}; + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], h[i]); + } +}; + +g.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "result = 'ok';\n"); // line0 + 2 +assertEq(log, '013'); diff --git a/js/src/jit-test/tests/debug/Script-clearBreakpoint-03.js b/js/src/jit-test/tests/debug/Script-clearBreakpoint-03.js new file mode 100644 index 0000000000..c80939786e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-03.js @@ -0,0 +1,25 @@ +// Clearing a breakpoint by handler can clear multiple breakpoints. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var s; +dbg.onDebuggerStatement = function (frame) { + s = frame.eval("f").return.script; +}; +g.eval("var line0 = Error().lineNumber;\n" + + "function f(a, b) {\n" + // line0 + 1 + " return a + b;\n" + // line0 + 2 + "}\n" + + "debugger;\n"); + +var hits = 0; +var handler = {hit: function (frame) { hits++; s.clearBreakpoint(handler); }}; +var offs = s.getLineOffsets(g.line0 + 2); +for (var i = 0; i < 4; i++) { + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], handler); +} + +assertEq(g.f(2, 2), 4); +assertEq(hits, 1); +assertEq(s.getBreakpoints().length, 0); diff --git a/js/src/jit-test/tests/debug/Script-clearBreakpoint-04.js b/js/src/jit-test/tests/debug/Script-clearBreakpoint-04.js new file mode 100644 index 0000000000..b1da7b4095 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-clearBreakpoint-04.js @@ -0,0 +1,28 @@ +// clearBreakpoint clears breakpoints for the current Debugger object only. + +var g = newGlobal({newCompartment: true}); + +var hits = 0; +var handler = { + hit: function (frame) { + hits++; + frame.script.clearBreakpoint(handler); + } +}; + +function attach(i) { + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + var s = frame.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], handler); + }; +} +for (var i = 0; i < 4; i++) + attach(i); + +g.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "Math.sin(0);\n"); // line0 + 2 +assertEq(hits, 4); diff --git a/js/src/jit-test/tests/debug/Script-displayName-01.js b/js/src/jit-test/tests/debug/Script-displayName-01.js new file mode 100644 index 0000000000..02056f7542 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-displayName-01.js @@ -0,0 +1,17 @@ +// Debugger.Script.prototype.displayName + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var name; +dbg.onDebuggerStatement = f => { name = f.callee.script.displayName; }; + +g.eval("(function f() { debugger; })();"); +assertEq(name, "f"); +g.eval("(function () { debugger; })();"); +assertEq(name, undefined); +g.eval("Function('debugger;')();"); +assertEq(name, "anonymous"); +g.eval("var f = function() { debugger; }; f()"); +assertEq(name, "f"); +g.eval("var a = {}; a.f = function() { debugger; }; a.f()"); +assertEq(name, "a.f"); diff --git a/js/src/jit-test/tests/debug/Script-format-01.js b/js/src/jit-test/tests/debug/Script-format-01.js new file mode 100644 index 0000000000..24db07dc72 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-format-01.js @@ -0,0 +1,18 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +// Tests that JavaScript scripts have a "js" format and wasm scripts have a +// "wasm" format. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var gotScript; +dbg.onNewScript = (script) => { + gotScript = script; +}; + +g.eval(`(() => {})()`); +assertEq(gotScript.format, "js"); + +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); +assertEq(gotScript.format, "wasm"); diff --git a/js/src/jit-test/tests/debug/Script-gc-01.js b/js/src/jit-test/tests/debug/Script-gc-01.js new file mode 100644 index 0000000000..f328008b15 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-gc-01.js @@ -0,0 +1,26 @@ +// Debugger.Script instances with live referents stay alive. + +var N = 4; +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var i; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.script instanceof Debugger.Script, true); + frame.script.id = i; +}; + +g.eval('var arr = [];') +for (i = 0; i < N; i++) // loop to defeat conservative GC + g.eval("arr.push(function () { debugger }); arr[arr.length - 1]();"); + +gc(); + +var hits; +dbg.onDebuggerStatement = function (frame) { + hits++; + assertEq(frame.script.id, i); +}; +hits = 0; +for (i = 0; i < N; i++) + g.arr[i](); +assertEq(hits, N); diff --git a/js/src/jit-test/tests/debug/Script-gc-02.js b/js/src/jit-test/tests/debug/Script-gc-02.js new file mode 100644 index 0000000000..c805e2c66e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-gc-02.js @@ -0,0 +1,14 @@ +// Debugger.Scripts keep their referents alive. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var arr = []; +dbg.onDebuggerStatement = function (frame) { arr.push(frame.script); }; +g.eval("for (var i = 0; i < 10; i++) Function('debugger;')();"); +assertEq(arr.length, 10); + +gc(); + +for (var i = 0; i < arr.length; i++) + assertEq(arr[i].lineCount, 4); + diff --git a/js/src/jit-test/tests/debug/Script-gc-03.js b/js/src/jit-test/tests/debug/Script-gc-03.js new file mode 100644 index 0000000000..ab53203239 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-gc-03.js @@ -0,0 +1,15 @@ +// Referents of Debugger.Scripts in other compartments always survive per-compartment GC. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var arr = []; +dbg.onDebuggerStatement = function (frame) { arr.push(frame.script); }; +g.eval("for (var i = 0; i < 100; i++) Function('debugger;')();"); +assertEq(arr.length, 100); + +gc(g); + +for (var i = 0; i < arr.length; i++) + assertEq(arr[i].lineCount, 4); + +gc(); diff --git a/js/src/jit-test/tests/debug/Script-getAllColumnOffsets.js b/js/src/jit-test/tests/debug/Script-getAllColumnOffsets.js new file mode 100644 index 0000000000..1661ca2ad2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getAllColumnOffsets.js @@ -0,0 +1,147 @@ +load(libdir + "assert-offset-columns.js"); + +// getColumnOffsets correctly places the various parts of a ForStatement. +assertOffsetColumns( + "function f(n) { for (var i = 0; i < n; ++i) hits.push('.'); hits.push('!'); }", + " ^ ^ ^ ^ ^ ^ ^ ^", + "0 1 3 4 . 2 1 3 4 . 2 1 3 4 . 2 1 5 6 ! 7" +); + +// getColumnOffsets correctly places multiple variable declarations. +assertOffsetColumns( + "function f(n){var w0,x1=3,y2=4,z3=9}", + " ^ ^ ^^" +); + +// getColumnOffsets correctly places comma separated expressions. +assertOffsetColumns( + "function f(n){print(n),print(n),print(n)}", + " ^ ^ ^ ^ ^" +); + +// getColumnOffsets correctly places object properties. +assertOffsetColumns( + // Should hit each property in the object. + "function f(n){var o={a:1,b:2,c:3}}", + " ^^ ^ ^ ^" +); + +// getColumnOffsets correctly places array properties. +assertOffsetColumns( + // Should hit each item in the array. + "function f(n){var a=[1,2,n]}", + " ^^ ^ ^ ^" +); + +// getColumnOffsets correctly places function calls. +assertOffsetColumns( + "function ppppp() { return 1; }\n" + + "function f(){ 1 && ppppp(ppppp()) && new Error() }", + " ^ ^ ^ ^ ^", + "0 2 1 3 4" +); + +// getColumnOffsets correctly places the various parts of a SwitchStatement. +assertOffsetColumns( + "function f(n) { switch(n) { default: print(n); } }", + " ^ ^ ^ ^" +); + +// getColumnOffsets correctly places the various parts of a BreakStatement. +assertOffsetColumns( + "function f(n) { do { print(n); if (n === 3) { break; } } while(false); }", + " ^ ^ ^ ^ ^ ^ ^", + "0 1 2 3 4 6" +); + +// If the loop condition is unreachable, we currently don't report its offset. +assertOffsetColumns( + "function f(n) { do { print(n); break; } while(false); }", + " ^ ^ ^ ^ ^", +); + +// getColumnOffsets correctly places the various parts of a ContinueStatement. +assertOffsetColumns( + "function f(n) { do { print(n); continue; } while(false); }", + " ^ ^ ^ ^ ^ ^" +); + +// getColumnOffsets correctly places the various parts of a WithStatement. +assertOffsetColumns( + "function f(n) { with({}) { print(n); } }", + " ^ ^ ^ ^" +); + +// getColumnOffsets correctly places the various parts of a IfStatement. +assertOffsetColumns( + "function f(n) { if (n == 3) print(n); }", + " ^ ^ ^ ^" +); + +// getColumnOffsets correctly places the various parts of a IfStatement +// with an if/else +assertOffsetColumns( + "function f(n) { if (n == 2); else if (n === 3) print(n); }", + " ^ ^ ^ ^ ^" +); + +// getColumnOffsets correctly places the various parts of a DoWhileStatement. +assertOffsetColumns( + "function f(n) { do { print(n); } while(false); }", + " ^ ^ ^ ^ ^" +); + +// getColumnOffsets correctly places the part of normal ::Dot node with identifier root. +assertOffsetColumns( + "var args = [];\n" + + "var obj = { base: { a(){ return { b(){} }; } } };\n" + + "function f(n) { obj.base.a().b(...args); }", + " ^ ^ ^ ^ ^", + "0 1 3 2 4" +); + +// getColumnOffsets correctly places the part of normal ::Dot node with "this" root. +assertOffsetColumns( + "var args = [];\n" + + "var obj = { base: { a(){ return { b(){} }; } } };\n" + + "var f = function() { this.base.a().b(...args); }.bind(obj);", + " ^ ^ ^ ^ ^", + "0 1 3 2 4" +); + +// getColumnOffsets correctly places the part of normal ::Dot node with "super" base. +assertOffsetColumns( + "var args = [];\n" + + "var obj = { base: { a(){ return { b(){} }; } } };\n" + + "var f = { __proto__: obj, f(n) { super.base.a().b(...args); } }.f;", + " ^ ^ ^ ^ ^", + "0 1 3 2 4" +); + +// getColumnOffsets correctly places the part of normal ::Dot node with other base. +assertOffsetColumns( + "var args = [];\n" + + "var obj = { base: { a(){ return { b(){} }; } } };\n" + + "function f(n) { (0, obj).base.a().b(...args); }", + " ^ ^ ^ ^ ^ ^", + "0 1 2 4 3 5" +); + +// getColumnOffsets correctly places the part of folded ::Elem node. +assertOffsetColumns( + "var args = [];\n" + + "var obj = { base: { a(){ return { b(){} }; } } };\n" + + // Constant folding makes the static string behave like a dot access. + "function f(n) { obj.base['a']()['b'](...args); }", + " ^ ^ ^ ^ ^", + "0 1 3 2 4" +); + +// getColumnOffsets correctly places the part of computed ::Elem node. +assertOffsetColumns( + "var args = [], a = 'a', b = 'b';\n" + + "var obj = { base: { a(){ return { b(){} }; } } };\n" + + "function f(n) { obj.base[a]()[b](...args); }", + " ^ ^ ^^ ^", + "0 1 3 2 4" +); diff --git a/js/src/jit-test/tests/debug/Script-getBreakpoints-01.js b/js/src/jit-test/tests/debug/Script-getBreakpoints-01.js new file mode 100644 index 0000000000..ac143588fb --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getBreakpoints-01.js @@ -0,0 +1,40 @@ +// Basic Script.prototype.getBreakpoints tests. + +var g = newGlobal({newCompartment: true}); +g.eval("var line0 = Error().lineNumber;\n" + + "function f(x) {\n" + // line0 + 1 + " if (x < 0)\n" + // line0 + 2 + " return -x;\n" + // line0 + 3 + " return x;\n" + + "}"); + +var s; +var offsets = []; +var handlers = []; +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + s = frame.eval("f").return.script; + var off; + + for (var i = 0; i < 3; i++) { + var off = s.getLineOffsets(g.line0 + 2 + i)[0]; + assertEq(typeof off, 'number'); + handlers[i] = {}; + s.setBreakpoint(off, handlers[i]); + offsets[i] = off; + } +}; +g.eval("debugger;"); + +// getBreakpoints without an offset gets all breakpoints in the script. +var bps = s.getBreakpoints(); +assertEq(bps.length, handlers.length); +for (var i = 0; i < bps.length; i++) + assertEq(bps.indexOf(handlers[i]) !== -1, true); + +// getBreakpoints with an offset finds only breakpoints at that offset. +for (var i = 0; i < offsets.length; i++) { + var bps = s.getBreakpoints(offsets[i]); + assertEq(bps.length, 1); + assertEq(bps[0], handlers[i]); +} diff --git a/js/src/jit-test/tests/debug/Script-getBreakpoints-02.js b/js/src/jit-test/tests/debug/Script-getBreakpoints-02.js new file mode 100644 index 0000000000..ead429889e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getBreakpoints-02.js @@ -0,0 +1,42 @@ +// Script.prototype.getBreakpoints returns breakpoints for the current Debugger object only. + +var g = newGlobal({newCompartment: true}); + +var debuggers = []; +var hits = 0; +function attach(g, i) { + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + var s = frame.script; + var offs = s.getLineOffsets(g.line0 + 2); + var hitAny = false; + var handler = { + hit: function (frame) { + assertEq(this, handler); + assertEq(frame.script, s); + var bps = s.getBreakpoints(); + assertEq(bps.length, offs.length); + for (var i = 0; i < bps.length; i++) + assertEq(bps[i], handler); + + if (!hitAny) { + hitAny = true; + hits++; + } + } + }; + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], handler); + hits++; + }; + debuggers[i] = dbg; +} + +for (var i = 0; i < 3; i++) + attach(g, i); +g.done = false; +g.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "done = true;\n"); // line0 + 2 +assertEq(hits, 6); +assertEq(g.done, true); diff --git a/js/src/jit-test/tests/debug/Script-getChildScripts-01.js b/js/src/jit-test/tests/debug/Script-getChildScripts-01.js new file mode 100644 index 0000000000..99f9cf5b36 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getChildScripts-01.js @@ -0,0 +1,40 @@ +// Basic getChildScripts tests. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; +function note(s) { + assertEq(s instanceof Debugger.Script, true); + log += 'S'; + var c = s.getChildScripts(); + if (c.length > 0) { + log += '['; + for (var i = 0; i < c.length; i++) + note(c[i]); + log += ']'; + } +} +dbg.onDebuggerStatement = function (frame) { note(frame.script); }; + +function test(code, expected) { + log = ''; + g.eval(code); + assertEq(log, expected); +} + +test("debugger;", + "S"); +test("function f() {} debugger;", + "S[S]"); +test("function g() { function h() { function k() {} return k; } return h; } debugger;", + "S[S[S[S]]]"); +test("function q() {} function qq() {} debugger;", + "S[SS]"); +test("[0].map(function id(a) { return a; }); debugger;", + "S[S]"); +test("Function('return 2+2;')(); debugger;", + "S"); +test("var obj = {get x() { return 0; }, set x(v) {}}; debugger;", + "S[SS]"); +test("function* qux(n) { for (var i = 0; i < n; i++) yield i; } debugger;", + "S[S]"); diff --git a/js/src/jit-test/tests/debug/Script-getChildScripts-02.js b/js/src/jit-test/tests/debug/Script-getChildScripts-02.js new file mode 100644 index 0000000000..4a8982aeea --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getChildScripts-02.js @@ -0,0 +1,20 @@ +// getChildScripts returns scripts in source order. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var scripts = []; +var cs; +dbg.onDebuggerStatement = function (frame) { + scripts.push(frame.script); + if (scripts.length === 1) + cs = frame.script.getChildScripts(); +}; + +g.eval("function f() { debugger; }\n" + + "var g = function () { debugger; }\n" + + "debugger; f(); g();"); + +assertEq(scripts.length, 3); +assertEq(cs.length, 2); +assertEq(cs[0], scripts[1]); +assertEq(cs[1], scripts[2]); diff --git a/js/src/jit-test/tests/debug/Script-getChildScripts-03.js b/js/src/jit-test/tests/debug/Script-getChildScripts-03.js new file mode 100644 index 0000000000..09e5cba32c --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getChildScripts-03.js @@ -0,0 +1,16 @@ +// getChildScripts on a direct eval script returns the right scripts. +// (A bug had it also returning the script for the calling function.) + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var arr = frame.script.getChildScripts(); + assertEq(arr.length, 1); + assertEq(arr[0], frame.eval("h").return.script); + hits++; +}; + +g.eval("function f(s) { eval(s); }"); +g.f("debugger; function h(a) { return a + 1; }"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Script-getChildScripts-04.js b/js/src/jit-test/tests/debug/Script-getChildScripts-04.js new file mode 100644 index 0000000000..fc47247137 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getChildScripts-04.js @@ -0,0 +1,15 @@ +// script.getChildScripts() works correctly during the newScript hook. +// (A bug had it including the script for the calling function.) + +var g = newGlobal({newCompartment: true}); +g.eval("function h(a) { eval(a); }"); + +var dbg = Debugger(g); +var arr, kscript; +dbg.onNewScript = function (script) { arr = script.getChildScripts(); }; +dbg.onDebuggerStatement = function (frame) { kscript = frame.callee.script; }; + +g.h("function k(a) { debugger; return a + 1; } k(-1);"); +assertEq(kscript instanceof Debugger.Script, true); +assertEq(arr.length, 1); +assertEq(arr[0], kscript); diff --git a/js/src/jit-test/tests/debug/Script-getChildScripts-05.js b/js/src/jit-test/tests/debug/Script-getChildScripts-05.js new file mode 100644 index 0000000000..594e8db435 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getChildScripts-05.js @@ -0,0 +1,16 @@ +// Test that lazy inner functions inside eval are tagged properly so we don't +// incorrectly do NAME -> GNAME optimization. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onNewScript = function delazify(script, global) { + // Force delazification of inner functions. + script.getChildScripts(); +}; + +g.eval("" + function f() { + var $; + eval('var obj={foo:1}; $=function() { assertEq(obj.foo, 1); }'); + return $; +}); +g.eval("f()();"); diff --git a/js/src/jit-test/tests/debug/Script-getEffectfulOffsets.js b/js/src/jit-test/tests/debug/Script-getEffectfulOffsets.js new file mode 100644 index 0000000000..49289a946f --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getEffectfulOffsets.js @@ -0,0 +1,33 @@ +// Check that Script.getEffectfulOffsets behaves sensibly. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var numEffectfulOperations; + +function onNewScript(script) { + script.getChildScripts().forEach(onNewScript); + numEffectfulOperations += script.getEffectfulOffsets().length; +} +dbg.onNewScript = onNewScript; + +function test(code, expected) { + numEffectfulOperations = 0; + g.eval(` +function f(a, b, c) { +${code} +} +`); + assertEq(numEffectfulOperations, expected); +} + +test("a.f = 0", 2); +test("a.f++", 2); +test("return a.f", 1); +test("a[b] = 0", 2); +test("a[b]++", 2); +test("return a[b]", 1); +test("a = 0", 1); +test("d = 0", 2); +test("with (a) { b = 0; }", 4); +test("let o = {}; ({x: o.x} = { x: 10 })", 2); +test("var x", 1); diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-01.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-01.js new file mode 100644 index 0000000000..4bd1c47eaa --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-01.js @@ -0,0 +1,13 @@ +// getLineOffsets on a line that is definitely outside a script returns an empty array. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var offs = frame.script.getLineOffsets(g.line0 + 2); + assertEq(Array.isArray(offs), true); + assertEq(offs.length, 0); + hits++; +}; +g.eval("var line0 = Error().lineNumber; debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-02.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-02.js new file mode 100644 index 0000000000..7fab0c9fb8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-02.js @@ -0,0 +1,33 @@ +// getLineOffsets correctly places the various parts of a ForStatement. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + function handler(line) { + return {hit: function (frame) { g.log += "" + line; }}; + } + + var s = frame.eval("f").return.script; + for (var line = 2; line <= 6; line++) { + var offs = s.getLineOffsets(g.line0 + line); + var h = handler(line); + for (var i = 0; i < offs.length; i++) { + assertEq(s.getOffsetLocation(offs[i]).lineNumber, g.line0 + line); + s.setBreakpoint(offs[i], h); + } + } +}; + +g.log = ''; +g.eval("var line0 = Error().lineNumber;\n" + + "function f(n) {\n" + // line0 + 1 + " for (var i = 0;\n" + // line0 + 2 + " i < n;\n" + // line0 + 3 + " i++)\n" + // line0 + 4 + " log += '.';\n" + // line0 + 5 + " log += '!';\n" + // line0 + 6 + "}\n" + + "debugger;\n"); +assertEq(g.log, ""); +g.f(3); +assertEq(g.log, "235.435.435.436!"); diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-03.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-03.js new file mode 100644 index 0000000000..62302f5a56 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-03.js @@ -0,0 +1,36 @@ +// getLineOffsets treats one-line compound statements as having only one entry-point. +// (A breakpoint on a line that only executes once will only hit once.) + +var g = newGlobal({newCompartment: true}); +g.line0 = null; +var dbg = Debugger(g); +var log; +dbg.onDebuggerStatement = function (frame) { + var s = frame.script; + var lineno = g.line0 + 2; + var offs = s.getLineOffsets(lineno); + for (var i = 0; i < offs.length; i++) { + assertEq(s.getOffsetLocation(offs[i]).lineNumber, lineno); + s.setBreakpoint(offs[i], {hit: function () { log += 'B'; }}); + } + log += 'A'; +}; + +function test(s) { + log = ''; + g.eval("line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + s); // line0 + 2 + assertEq(log, 'AB'); +} + +g.i = 0; +g.j = 0; +test("{i++; i--; i++; i--; }"); +test("if (i === 0) i++; else i--;"); +test("while (i < 5) i++;"); +test("do --i; while (i > 0);"); +test("for (i = 0; i < 5; i++) j++;"); +test("for (var x in [0, 1, 2]) j++;"); +test("switch (i) { case 0: j = 0; case 1: j = 1; case 2: j = 2; default: j = i; }"); +test("switch (i) { case 'A': j = 0; case 'B': j = 1; case 'C': j = 2; default: j = i; }"); diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-04.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-04.js new file mode 100644 index 0000000000..9e828b8af6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-04.js @@ -0,0 +1,53 @@ +// getLineOffsets works with instructions reachable only by breaking out of a loop or switch. + +var g = newGlobal({newCompartment: true}); +g.line0 = null; +var dbg = Debugger(g); +var where; +dbg.onDebuggerStatement = function (frame) { + var s = frame.eval("f").return.script; + var lineno = g.line0 + where; + var offs = s.getLineOffsets(lineno); + for (var i = 0; i < offs.length; i++) { + assertEq(s.getOffsetLocation(offs[i]).lineNumber, lineno); + s.setBreakpoint(offs[i], {hit: function () { g.log += 'B'; }}); + } + g.log += 'A'; +}; + +function test(s) { + var count = (s.split(/\n/).length - 1); // number of newlines in s + g.log = ''; + where = 3 + count + 1; + g.eval("line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "function f(i) {\n" + // line0 + 2 + s + // line0 + 3 ... line0 + where - 2 + " log += '?';\n" + // line0 + where - 1 + " log += '!';\n" + // line0 + where + "}\n"); + g.f(0); + assertEq(g.log, 'A?B!'); +} + +test("i = 128;\n" + + "for (;;) {\n" + + " var x = i - 10;;\n" + + " if (x < 0)\n" + + " break;\n" + + " i >>= 2;\n" + + "}\n"); + +test("while (true)\n" + + " if (++i === 2) break;\n"); + +test("do {\n" + + " if (++i === 2) break;\n" + + "} while (true);\n"); + +test("switch (i) {\n" + + " case 2: return 7;\n" + + " case 1: return 8;\n" + + " case 0: break;\n" + + " default: return -i;\n" + + "}\n"); diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-05.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-05.js new file mode 100644 index 0000000000..0a030ad87e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-05.js @@ -0,0 +1,65 @@ +// getLineOffsets identifies multiple ways to land on a line. + +var g = newGlobal({newCompartment: true}); +g.line0 = null; +var dbg = Debugger(g); +var where; +dbg.onDebuggerStatement = function (frame) { + var s = frame.script, lineno, offs; + + lineno = g.line0 + where; + offs = s.getLineOffsets(lineno); + for (var i = 0; i < offs.length; i++) { + assertEq(s.getOffsetLocation(offs[i]).lineNumber, lineno); + s.setBreakpoint(offs[i], {hit: function () { g.log += 'B'; }}); + } + + lineno++; + offs = s.getLineOffsets(lineno); + for (var i = 0; i < offs.length; i++) { + assertEq(s.getOffsetLocation(offs[i]).lineNumber, lineno); + s.setBreakpoint(offs[i], {hit: function () { g.log += 'C'; }}); + } + + g.log += 'A'; +}; + +function test(s) { + assertEq(s.charAt(s.length - 1), '\n'); + var count = (s.split(/\n/).length - 1); // number of lines in s + g.log = ''; + where = 1 + count; + g.eval("line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + s + // line0 + 2 ... line0 + where + "log += 'D';\n"); + assertEq(g.log, 'AB!CD'); +} + +// if-statement with yes and no paths on a single line +g.i = 0; +test("if (i === 0)\n" + + " log += '!'; else log += 'X';\n"); +test("if (i === 2)\n" + + " log += 'X'; else log += '!';\n"); + +// break to a line that has code inside and outside the loop +g.i = 2; +test("while (1) {\n" + + " if (i === 2) break;\n" + + " log += 'X'; } log += '!';\n"); + +// leaving a while loop by failing the test, when the last line has stuff both inside and outside the loop +g.i = 0; +test("while (i > 0) {\n" + + " if (i === 70) log += 'X';\n" + + " --i; } log += '!';\n"); + +// multiple case-labels on the same line +g.i = 0; +test("switch (i) {\n" + + " case 0: case 1: log += '!'; break; }\n"); +test("switch ('' + i) {\n" + + " case '0': case '1': log += '!'; break; }\n"); +test("switch (i) {\n" + + " case 'ok' + i: case i - i: log += '!'; break; }\n"); diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-06.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-06.js new file mode 100644 index 0000000000..f5fccaed56 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-06.js @@ -0,0 +1,99 @@ +// getLineOffsets works with extended instructions, such as JSOP_GOTOX. + +var g = newGlobal({newCompartment: true}); +g.line0 = null; +var dbg = Debugger(g); +var where; +dbg.onDebuggerStatement = function (frame) { + var s = frame.script; + var offs; + var lineno = g.line0 + where; + offs = s.getLineOffsets(lineno); + for (var i = 0; i < offs.length; i++) { + assertEq(s.getOffsetLocation(offs[i]).lineNumber, lineno); + s.setBreakpoint(offs[i], {hit: function (frame) { g.log += 'B'; }}); + } + g.log += 'A'; +}; + +function test(s) { + assertEq(s.charAt(s.length - 1) !== '\n', true); + var count = s.split(/\n/).length; // number of lines in s + g.i = 0; + g.log = ''; + where = 1 + count; + g.eval("line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + s + // line0 + 2 ... line0 + where + " log += 'C';\n"); + assertEq(g.log, 'ABC'); +} + +function repeat(s) { + return Array((1 << 14) + 1).join(s); // 16K copies of s +} +var long_expr = "i" + repeat(" + i"); +var long_throw_stmt = "throw " + long_expr + ";\n"; + +// long break (JSOP_GOTOX) +test("for (;;) {\n" + + " if (i === 0)\n" + + " break;\n" + + " " + long_throw_stmt + + "}"); + +// long continue (JSOP_GOTOX) +test("do {\n" + + " if (i === 0)\n" + + " continue;\n" + + " " + long_throw_stmt + + "} while (i !== 0);"); + +// long if consequent (JSOP_IFEQX) +test("if (i === 2) {\n" + + " " + long_throw_stmt + + "}"); + +// long catch-block with finally (JSOP_GOSUBX) +test("try {\n" + + " i = 0;\n" + + "} catch (exc) {\n" + + " throw " + long_expr + ";\n" + + "} finally {\n" + + " i = 1;\n" + + "}"); + +// long case (JSOP_TABLESWITCHX) +test("switch (i) {\n" + + " default:\n" + + " case 1: " + long_throw_stmt + + " case 0: i++; }"); + +test("switch (i) {\n" + + " case 1: case 2: case 3: " + long_throw_stmt + + " default: i++; }"); + +// long case (JSOP_LOOKUPSWITCHX) +test("switch ('' + i) {\n" + + " default:\n" + + " case '1': " + long_throw_stmt + + " case '0': i++; }"); + +test("switch (i) {\n" + + " case '1': case '2': case '3': " + long_throw_stmt + + " default: i++; }"); + +// long case or case-expression (JSOP_CASEX) +test("switch (i) {\n" + + " case i + 1 - i:\n" + + " default:\n" + + " " + long_throw_stmt + + " case i + i:\n" + + " i++; break; }"); + +// long case when JSOP_CASE is used (JSOP_DEFAULTX) +test("switch (i) {\n" + + " case i + 1 - i:\n" + + " " + long_throw_stmt + + " default:\n" + + " i++; break; }"); diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-07.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-07.js new file mode 100644 index 0000000000..5ecf311615 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-07.js @@ -0,0 +1,19 @@ +// Lazy scripts should correctly report line offsets + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); + +g.eval("// Header comment\n" + // <- line 6 in this file + "\n" + + "\n" + + "function f(n) {\n" + // <- line 9 in this file + " var foo = '!';\n" + + "}"); + +dbg.addDebuggee(g); +var scripts = dbg.findScripts(); +for (var i = 0; i < scripts.length; i++) { + // Nothing should have offsets for the deffun on line 9 if lazy scripts + // correctly update the position. + assertEq(scripts[i].getLineOffsets(9).length, 0); +} diff --git a/js/src/jit-test/tests/debug/Script-getLineOffsets-08.js b/js/src/jit-test/tests/debug/Script-getLineOffsets-08.js new file mode 100644 index 0000000000..b51b0e09f8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getLineOffsets-08.js @@ -0,0 +1,25 @@ +// A "while" or a "for" loop should have a single entry point. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +dbg.onDebuggerStatement = function(frame) { + var s = frame.eval('f').return.script; + + // There should be just a single entry point for the first line of + // the function. See below to understand the "+2". + assertEq(s.getLineOffsets(g.line0 + 2).length, 1); +}; + + +function test(code) { + g.eval('var line0 = Error().lineNumber;\n' + + 'function f() {\n' + // line0 + 1 + code + '\n' + // line0 + 2 -- see above + '}\n' + + 'debugger;'); +} + +test('while (false)\n;'); +test('for (;false;)\n;'); +test('for (;;) break;\n;'); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetLine-01.js b/js/src/jit-test/tests/debug/Script-getOffsetLine-01.js new file mode 100644 index 0000000000..65a1f50c53 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetLine-01.js @@ -0,0 +1,25 @@ +// Basic getOffsetLocation test, using Error.lineNumber as the gold standard. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits; +dbg.onDebuggerStatement = function (frame) { + var knownLine = frame.eval("line").return; + assertEq(frame.script.getOffsetLocation(frame.offset).lineNumber, knownLine); + hits++; +}; + +hits = 0; +g.eval("var line = new Error().lineNumber; debugger;"); +assertEq(hits, 1); + +hits = 0; +g.eval("var s = 2 + 2;\n" + + "s += 2;\n" + + "line = new Error().lineNumber; debugger;\n" + + "s += 2;\n" + + "s += 2;\n" + + "line = new Error().lineNumber; debugger;\n" + + "s += 2;\n" + + "assertEq(s, 12);\n"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetLine-02.js b/js/src/jit-test/tests/debug/Script-getOffsetLine-02.js new file mode 100644 index 0000000000..d118e0e2db --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetLine-02.js @@ -0,0 +1,19 @@ +// Frame.script/offset and Script.getOffsetLocation work in non-top frames. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + var a = []; + for (; frame.type == "call"; frame = frame.older) + a.push(frame.script.getOffsetLocation(frame.offset).lineNumber - g.line0); + assertEq(a.join(","), "1,2,3,4"); + hits++; +}; +g.eval("var line0 = Error().lineNumber;\n" + + "function f0() { debugger; }\n" + + "function f1() { f0(); }\n" + + "function f2() { f1(); }\n" + + "function f3() { f2(); }\n" + + "f3();\n"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetLocation.js b/js/src/jit-test/tests/debug/Script-getOffsetLocation.js new file mode 100644 index 0000000000..3df02f6fc1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetLocation.js @@ -0,0 +1,37 @@ +// getOffsetLocation agrees with getAllColumnOffsets + +var global = newGlobal({newCompartment: true}); +Debugger(global).onDebuggerStatement = function (frame) { + var script = frame.script; + var byOffset = []; + script.getAllColumnOffsets().forEach(function (entry) { + var {lineNumber, columnNumber, offset} = entry; + byOffset[offset] = {lineNumber, columnNumber}; + }); + + frame.onStep = function() { + var offset = frame.offset; + var location = script.getOffsetLocation(offset); + if (location.isEntryPoint) { + assertEq(location.lineNumber, byOffset[offset].lineNumber); + assertEq(location.columnNumber, byOffset[offset].columnNumber); + } else { + assertEq(byOffset[offset], undefined); + } + }; +}; + +function test(body) { + print("Test: " + body); + global.eval(`function f(n) { debugger; ${body} }`); + global.f(3); +} + +test("for (var i = 0; i < n; ++i) ;"); +test("var w0,x1=3,y2=4,z3=9"); +test("print(n),print(n),print(n)"); +test("var o={a:1,b:2,c:3}"); +test("var a=[1,2,n]"); + +global.eval("function ppppp() { return 1; }"); +test("1 && ppppp(ppppp()) && new Error()"); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetMetadata.js b/js/src/jit-test/tests/debug/Script-getOffsetMetadata.js new file mode 100644 index 0000000000..0f265ff0dc --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetMetadata.js @@ -0,0 +1,36 @@ +var global = newGlobal({newCompartment: true}); +var dbg = Debugger(global); +dbg.onDebuggerStatement = function(frame) { + const bps = frame.script.getPossibleBreakpoints(); + + const stepBps = []; + frame.onStep = function() { + assertOffset(this); + }; + + assertOffset(frame); + + function assertOffset(frame) { + const meta = frame.script.getOffsetMetadata(frame.offset); + + if (meta.isBreakpoint) { + assertEq(frame.offset, bps[0].offset); + const expectedData = bps.shift(); + + assertEq(meta.lineNumber, expectedData.lineNumber); + assertEq(meta.columnNumber, expectedData.columnNumber); + assertEq(meta.isStepStart, expectedData.isStepStart); + } else { + assertEq(meta.isStepStart, false); + } + }; +}; + +global.eval(` + function a() { return "str"; } + debugger; + + console.log("42" + a()); + a(); + a() + a(); +`); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-01.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-01.js new file mode 100644 index 0000000000..7757d4fbf4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-01.js @@ -0,0 +1,501 @@ +// Currently the Jit integration has a few issues, let's keep this test +// case deterministic. +// +// - Baseline OSR increments the loop header twice. +// - Ion is not updating any counter yet. +// +if (getJitCompilerOptions()["ion.warmup.trigger"] != 30) + setJitCompilerOption("ion.warmup.trigger", 30); +if (getJitCompilerOptions()["baseline.warmup.trigger"] != 10) + setJitCompilerOption("baseline.warmup.trigger", 10); + +/* + * These test cases are annotated with the output produced by LCOV [1]. Comment + * starting with //<key> without any spaces are used as a reference for the code + * coverage output. Any "$" in these line comments are replaced by the current + * line number, and any "%" are replaced with the current function name (defined + * by the FN key). + * + * [1] http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php + */ +function checkGetOffsetsCoverage(fun) { + var keys = [ "TN", "SF", "FN", "FNDA", "FNF", "FNH", "BRDA", "BRF", "BRH", "DA", "LF", "LH" ]; + function startsWithKey(s) { + for (k of keys) { + if (s.startsWith(k)) + return true; + } + return false; + }; + + // Extract the body of the function, as the code to be executed. + var source = fun.toString(); + source = source.slice(source.indexOf('{') + 1, source.lastIndexOf('}')); + + // Extract comment starting with the previous keys, as a reference. + var lcovRef = []; + var currLine = 0; + var currFun = [{name: "top-level", braces: 1}]; + for (var line of source.split('\n')) { + currLine++; + + for (var comment of line.split("//").slice(1)) { + if (!startsWithKey(comment)) + continue; + comment = comment.trim(); + if (comment.startsWith("FN:")) + currFun.push({ name: comment.split(',')[1], braces: 0 }); + var name = currFun[currFun.length - 1].name; + if (!comment.startsWith("DA:")) + continue; + comment = { + offset: null, + lineNumber: currLine, + columnNumber: null, + count: comment.split(",")[1] | 0, + script: (name == "top-level" ? undefined : name) + }; + lcovRef.push(comment); + } + + var deltaBraces = line.split('{').length - line.split('}').length; + currFun[currFun.length - 1].braces += deltaBraces; + if (currFun[currFun.length - 1].braces == 0) + currFun.pop(); + } + + // Create a new global and instrument it with a debugger, to find all scripts, + // created in the current global. + var g = newGlobal({newCompartment: true}); + var dbg = Debugger(g); + dbg.collectCoverageInfo = true; + + var topLevel = null; + dbg.onNewScript = function (s) { + topLevel = s; + dbg.onNewScript = function () {}; + }; + + // Evaluate the code, and collect the hit counts for each scripts / lines. + g.eval(source); + + var coverageRes = []; + function collectCoverage(s) { + var res = s.getOffsetsCoverage(); + if (res == null) + res = [{ + offset: null, + lineNumber: null, + columnNumber: null, + script: s.displayName, + count: 0 + }]; + else { + res.map(function (e) { + e.script = s.displayName; + return e; + }); + } + coverageRes.push(res); + s.getChildScripts().forEach(collectCoverage); + }; + collectCoverage(topLevel); + coverageRes = [].concat(...coverageRes); + + // Check that all the lines are present the result. + function match(ref) { + return function (entry) { + return ref.lineNumber == entry.lineNumber && ref.script == entry.script; + } + } + function ppObj(entry) { + var str = "{"; + for (var k in entry) { + if (entry[k] != null) + str += " '" + k + "': " + entry[k] + ","; + } + str += "}"; + return str; + } + for (ref of lcovRef) { + var res = coverageRes.find(match(ref)); + if (!res) { + // getOffsetsCoverage returns null if we have no result for the + // script. We added a fake entry with an undefined lineNumber, which is + // used to match against the modified reference. + var missRef = Object.create(ref); + missRef.lineNumber = null; + res = coverageRes.find(match(missRef)); + } + + if (!res || res.count != ref.count) { + print("Cannot find `" + ppObj(ref) + "` in the following results:\n", coverageRes.map(ppObj).join("\n")); + print("In the following source:\n", source); + assertEq(true, false); + } + } +} + +checkGetOffsetsCoverage(function () { //FN:$,top-level + ",".split(','); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + function f() { //FN:$,f + ",".split(','); //DA:$,0 + } + ",".split(','); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + function f() { //FN:$,f + ",".split(','); //DA:$,1 + } + f(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { ','.split(','); //FN:$,top-level //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { function f() { ','.split(','); } //FN:$,top-level //FN:$,f //DA:$,1 + f(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + if (l.length == 3) //DA:$,1 + l.push(''); //DA:$,0 + l.pop(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + if (l.length == 2) //DA:$,1 + l.push(''); //DA:$,1 + l.pop(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + if (l.length == 3) //DA:$,1 + l.push(''); //DA:$,0 + else + l.pop(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + if (l.length == 2) //DA:$,1 + l.push(''); //DA:$,1 + else + l.pop(); //DA:$,0 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + if (l.length == 2) //DA:$,1 + l.push(''); //DA:$,1 + else { + if (l.length == 1) //DA:$,0 + l.pop(); //DA:$,0 + } +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + function f(i) { //FN:$,f + var x = 0; //DA:$,2 + while (i--) { // Currently OSR wrongly count the loop header twice. + // So instead of DA:$,12 , we have DA:$,13 . + x += i; //DA:$,10 + x = x / 2; //DA:$,10 + } + return x; //DA:$,2 + } + + f(5); //DA:$,1 + f(5); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + try { //DA:$,1 + var l = ",".split(','); //DA:$,1 + if (l.length == 2) { //DA:$,1 + l.push(''); //DA:$,1 + throw l; //DA:$,1 + } + l.pop(); //DA:$,0 + } catch (x) { //DA:$,1 + x.pop(); //DA:$,1 + } +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + try { //DA:$,1 + try { //DA:$,1 + if (l.length == 2) { //DA:$,1 + l.push(''); //DA:$,1 + throw l; //DA:$,1 + } + l.pop(); //DA:$,0 + } finally { //DA:$,1 + l.pop(); //DA:$,1 + } + } catch (x) { //DA:$,1 + } +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + function f() { //FN:$,f + throw 1; //DA:$,1 + f(); //DA:$,0 + } + var l = ",".split(','); //DA:$,1 + try { //DA:$,1 + f(); //DA:$,1 + f(); //DA:$,0 + } catch (x) { //DA:$,1 + } +}); + + +// Test TableSwitch opcode +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 + case 0: + l.push('0'); //DA:$,0 + break; + case 1: + l.push('1'); //DA:$,0 + break; + case 2: + l.push('2'); //DA:$,1 + break; + case 3: + l.push('3'); //DA:$,0 + break; + } + l.pop(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 + case 0: + l.push('0'); //DA:$,0 + case 1: + l.push('1'); //DA:$,0 + case 2: + l.push('2'); //DA:$,1 + case 3: + l.push('3'); //DA:$,1 + } + l.pop(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 + case 5: + l.push('5'); //DA:$,0 + case 4: + l.push('4'); //DA:$,0 + case 3: + l.push('3'); //DA:$,0 + case 2: + l.push('2'); //DA:$,1 + } + l.pop(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 + case 2: + l.push('2'); //DA:$,1 + case 5: + l.push('5'); //DA:$,1 + } + l.pop(); //DA:$,1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 + case 3: + l.push('1'); //DA:$,0 + case 5: + l.push('5'); //DA:$,0 + } + l.pop(); //DA:$,1 +}); + +// Unfortunately the differences between switch implementations leaks in the +// code coverage reports. +checkGetOffsetsCoverage(function () { //FN:$,top-level + function f(a) { //FN:$,f + return a; //DA:$,2 + } + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 + case f(-42): //DA:$,1 + l.push('1'); //DA:$,0 + case f(51): //DA:$,1 + l.push('5'); //DA:$,0 + } + l.pop(); //DA:$,1 +}); + + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 //BRDA:$,0,0,0 //BRDA:$,0,1,1 //BRDA:$,0,2,0 //BRDA:$,0,3,0 + case 0: + case 1: + l.push('0'); //DA:$,0 + l.push('1'); //DA:$,0 + case 2: + l.push('2'); //DA:$,1 + case 3: + l.push('3'); //DA:$,1 + } + l.pop(); //DA:$,1 + //FNF:1 + //FNH:1 + //LF:7 + //LH:5 + //BRF:4 + //BRH:1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 //BRDA:$,0,0,0 //BRDA:$,0,1,0 //BRDA:$,0,2,1 //BRDA:$,0,3,0 + case 0: + l.push('0'); //DA:$,0 + case 1: + l.push('1'); //DA:$,0 + case 2: + case 3: + l.push('2'); //DA:$,1 + l.push('3'); //DA:$,1 + } + l.pop(); //DA:$,1 + //FNF:1 + //FNH:1 + //LF:7 + //LH:5 + //BRF:4 + //BRH:1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 //BRDA:$,0,0,0 //BRDA:$,0,1,0 //BRDA:$,0,2,1 //BRDA:$,0,3,0 + case 0: + l.push('0'); //DA:$,0 + case 1: + default: + l.push('1'); //DA:$,0 + case 2: + l.push('2'); //DA:$,1 + case 3: + l.push('3'); //DA:$,1 + } + l.pop(); //DA:$,1 + //FNF:1 + //FNH:1 + //LF:7 + //LH:5 + //BRF:4 + //BRH:1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 //BRDA:$,0,0,0 //BRDA:$,0,1,0 //BRDA:$,0,2,1 //BRDA:$,0,3,0 + case 0: + l.push('0'); //DA:$,0 + case 1: + l.push('1'); //DA:$,0 + default: + case 2: + l.push('2'); //DA:$,1 + case 3: + l.push('3'); //DA:$,1 + } + l.pop(); //DA:$,1 + //FNF:1 + //FNH:1 + //LF:7 + //LH:5 + //BRF:4 + //BRH:1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 //BRDA:$,0,0,0 //BRDA:$,0,1,0 //BRDA:$,0,2,1 //BRDA:$,0,3,0 //BRDA:$,0,4,0 + case 0: + l.push('0'); //DA:$,0 + case 1: + l.push('1'); //DA:$,0 + default: + l.push('default'); //DA:$,0 + case 2: + l.push('2'); //DA:$,1 + case 3: + l.push('3'); //DA:$,1 + } + l.pop(); //DA:$,1 + //FNF:1 + //FNH:1 + //LF:8 + //LH:5 + //BRF:5 + //BRH:1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ",".split(','); //DA:$,1 + switch (l.length) { //DA:$,1 //BRDA:$,0,0,0 //BRDA:$,0,1,0 //BRDA:$,0,2,0 //BRDA:$,0,3,1 + case 0: + l.push('0'); //DA:$,0 + case 1: + l.push('1'); //DA:$,0 + default: + l.push('2'); //DA:$,1 + case 3: + l.push('3'); //DA:$,1 + } + l.pop(); //DA:$,1 + //FNF:1 + //FNH:1 + //LF:7 + //LH:5 + //BRF:4 + //BRH:1 +}); + +checkGetOffsetsCoverage(function () { //FN:$,top-level //FNDA:1,% + var l = ','.split(','); //DA:$,1 + if (l.length === 45) { //DA:$,1 //BRDA:$,0,0,1 //BRDA:$,0,1,0 + switch (l[0]) { //DA:$,0 //BRDA:$,1,0,- //BRDA:$,1,1,- + case ',': + l.push('0'); //DA:$,0 + default: + l.push('1'); //DA:$,0 + } + } + l.pop(); //DA:$,1 + //FNF:1 + //FNH:1 + //LF:6 + //LH:3 + //BRF:4 + //BRH:1 +}); + +// If you add a test case here, do the same in +// jit-test/tests/coverage/simple.js diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-02.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-02.js new file mode 100644 index 0000000000..dc1420e026 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-02.js @@ -0,0 +1,41 @@ +// |jit-test| --ion-pgo=off; skip-if: isLcovEnabled() + +// This script check that when we enable / disable the code coverage collection, +// then we have different results for the getOffsetsCoverage methods. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var coverageInfo = []; +var num = 20; +function loop(i) { + var n = 0; + for (n = 0; n < i; n++) + debugger; +} +g.eval(loop.toString()); + +dbg.onDebuggerStatement = function (f) { + // Collect coverage info each time we hit a debugger statement. + coverageInfo.push(f.callee.script.getOffsetsCoverage()); +}; + +coverageInfo = []; +dbg.collectCoverageInfo = false; +g.eval("loop(" + num + ");"); +assertEq(coverageInfo.length, num); +assertEq(coverageInfo[0], null); +assertEq(coverageInfo[num - 1], null); + +coverageInfo = []; +dbg.collectCoverageInfo = true; +g.eval("loop(" + num + ");"); +assertEq(coverageInfo.length, num); +assertEq(!coverageInfo[0], false); +assertEq(!coverageInfo[num - 1], false); + +coverageInfo = []; +dbg.collectCoverageInfo = false; +g.eval("loop(" + num + ");"); +assertEq(coverageInfo.length, num); +assertEq(coverageInfo[0], null); +assertEq(coverageInfo[num - 1], null); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-03.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-03.js new file mode 100644 index 0000000000..8bbafb9838 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-03.js @@ -0,0 +1,21 @@ +// |jit-test| error: Error: can't start debugging: a debuggee script is on the stack + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +function loop(i) { + var n = 0; + for (n = 0; n < i; n++) + debugger; +} +g.eval(loop.toString()); + +var countDown = 20; +dbg.onDebuggerStatement = function (f) { + // Should throw an error. + if (countDown > 0 && --countDown == 0) { + dbg.collectCoverageInfo = !dbg.collectCoverageInfo; + } +}; + +dbg.collectCoverageInfo = false; +g.eval("loop("+ (2 * countDown) +");"); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-04.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-04.js new file mode 100644 index 0000000000..22bff85eab --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-04.js @@ -0,0 +1,22 @@ +// |jit-test| error: Error: can't start debugging: a debuggee script is on the stack + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +function loop(i) { + var n = 0; + for (n = 0; n < i; n++) + debugger; +} +g.eval(loop.toString()); + +var countDown = 20; +dbg.onDebuggerStatement = function (f) { + // Should throw an error. + if (countDown > 0 && --countDown == 0) { + dbg.collectCoverageInfo = !dbg.collectCoverageInfo; + } +}; + +dbg.collectCoverageInfo = true; +g.eval("loop("+ (2 * countDown) +");"); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-05.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-05.js new file mode 100644 index 0000000000..3072fbc00e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-05.js @@ -0,0 +1,24 @@ +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +function f(x) { + while (x) { + interruptIf(true); + x -= 1; + } +} +g.eval(f.toString()); + +// Toogle the debugger while the function f is running. +setInterruptCallback(toogleDebugger); +function toogleDebugger() { + dbg.enabled = !dbg.enabled; + return true; +} + +dbg.collectCoverageInfo = false; +dbg.enabled = false; +g.eval("f(10);"); + +dbg.collectCoverageInfo = true; +dbg.enabled = false; +g.eval("f(10);"); diff --git a/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-bug1233178.js b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-bug1233178.js new file mode 100644 index 0000000000..70753c8533 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getOffsetsCoverage-bug1233178.js @@ -0,0 +1,13 @@ +gczeal(2); +g = newGlobal({newCompartment: true}); +dbg = Debugger(g); +function loop() { + for (var i = 0; i < 10; i++) + debugger; +} +g.eval(loop.toString()); +dbg.onDebuggerStatement = function(f) { + f.script.getOffsetsCoverage(); +} +dbg.collectCoverageInfo = true; +g.eval("loop")(); diff --git a/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints-02.js b/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints-02.js new file mode 100644 index 0000000000..56ea6620d3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints-02.js @@ -0,0 +1,83 @@ + +var global = newGlobal({newCompartment: true}); +var dbg = Debugger(global); +dbg.onDebuggerStatement = onDebuggerStatement; + +global.eval(` + debugger; + function f() { + var o = {}; // 4 + + o.a; o.a; o.a; o.a; // 6 + o.a; o.a; // 7 + o.a; o.a; o.a; // 8 + o.a; // 9 + } // 10 +`); + +function onDebuggerStatement(frame) { + const fScript = frame.script.getChildScripts()[0]; + + const allBreakpoints = fScript.getPossibleBreakpoints(); + assertEq(allBreakpoints.length, 12); + + assertBPCount({ line: 4 }, 1); + assertBPCount({ line: 5 }, 0); + assertBPCount({ line: 6 }, 4); + assertBPCount({ line: 7 }, 2); + assertBPCount({ line: 8 }, 3); + assertBPCount({ line: 9 }, 1); + assertBPCount({ line: 10 }, 1); + + assertBPCount({ line: 6, minColumn: 7 }, 3); + assertBPCount({ line: 6, maxColumn: 16 }, 3); + assertBPCount({ line: 6, minColumn: 7, maxColumn: 16 }, 2); + assertBPError({ line: 1, minLine: 1 }, "line", "not allowed alongside 'minLine'/'maxLine'"); + assertBPError({ line: 1, maxLine: 1 }, "line", "not allowed alongside 'minLine'/'maxLine'"); + assertBPError({ line: "1" }, "line", "not an integer"); + + assertBPCount({ minLine: 9 }, 2); + assertBPCount({ minLine: 9, minColumn: 0 }, 2); + assertBPCount({ minLine: 9, minColumn: 8 }, 1); + assertBPError({ minLine: "1" }, "minLine", "not an integer"); + assertBPError({ minColumn: 1 }, "minColumn", "not allowed without 'line' or 'minLine'"); + assertBPError({ minLine: 1, minColumn: "1" }, "minColumn", "not an integer"); + + assertBPCount({ maxLine: 7 }, 5); + assertBPCount({ maxLine: 7, maxColumn: 0 }, 5); + assertBPCount({ maxLine: 7, maxColumn: 8 }, 6); + assertBPError({ maxLine: "1" }, "maxLine", "not an integer"); + assertBPError({ maxColumn: 1 }, "maxColumn", "not allowed without 'line' or 'maxLine'"); + assertBPError({ maxLine: 1, maxColumn: "1" }, "maxColumn", "not an integer"); + + assertBPCount({ minLine: 6, maxLine: 8 }, 6); + assertBPCount({ minLine: 6, minColumn: 8, maxLine: 8 }, 5); + assertBPCount({ minLine: 6, maxLine: 8, maxColumn: 8 }, 7); + assertBPCount({ minLine: 6, minColumn: 8, maxLine: 8, maxColumn: 8 }, 6); + + assertBPCount({ + minOffset: fScript.getPossibleBreakpoints({ line: 6 })[3].offset, + }, 8); + assertBPError({ minOffset: "1" }, "minOffset", "not an integer"); + assertBPCount({ + maxOffset: fScript.getPossibleBreakpoints({ line: 6 })[3].offset, + }, 4); + assertBPError({ maxOffset: "1" }, "maxOffset", "not an integer"); + assertBPCount({ + minOffset: fScript.getPossibleBreakpoints({ line: 6 })[2].offset, + maxOffset: fScript.getPossibleBreakpoints({ line: 7 })[1].offset, + }, 3); + + function assertBPError(query, field, message) { + try { + fScript.getPossibleBreakpoints(query); + assertEq(false, true); + } catch (err) { + assertEq(err.message, `getPossibleBreakpoints' '${field}' is ${message}`); + } + } + + function assertBPCount(query, count) { + assertEq(fScript.getPossibleBreakpoints(query).length, count); + } +}; diff --git a/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints.js b/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints.js new file mode 100644 index 0000000000..5c52f52de3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-getPossibleBreakpoints.js @@ -0,0 +1,384 @@ +// simple ExpressionStatement +assertBreakpoints(` + /*S*/a; + /*S*/obj.prop; +`); + +// ExpressionStatement with calls +assertBreakpoints(` + /*S*/a(); + /*S*/obj./*B*/prop(); +`); + +// calls with many args +assertBreakpoints(` + /*S*/a/*B*/(1); + /*S*/a/*B*/(1,2); + /*S*/a/*B*/(1,2,3); +`); + + +// ExpressionStatement with nested expression calls. +assertBreakpoints(` + "45"; + /*S*/"45" + /*B*/a(); + /*S*/b() + "45"; + + /*S*/"45" + o./*B*/a(); + /*S*/o./*B*/b() + "45"; + /*S*/"45" + o./*B*/a() + o./*B*/b(); + /*S*/o./*B*/b() + "45" + o./*B*/a(); + /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// var VariableStatement initializers +assertBreakpoints(` + var foo1 = /*S*/"" + o.a + "" + /*B*/b(), + foo2 = /*S*/"45", + foo3 = /*S*/"45" + /*B*/a(), + foo4 = /*S*/b() + "45", + foo5 = /*S*/"45" + /*B*/a() + /*B*/b(), + foo6 = /*S*/b() + "45" + /*B*/a(), + foo7 = /*S*/b() + /*B*/a() + "45", + foo8 = /*S*/"45" + o./*B*/a(), + foo9 = /*S*/o./*B*/b() + "45", + foo10 = /*S*/"45" + o./*B*/a() + o./*B*/b(), + foo11 = /*S*/o./*B*/b() + "45" + o./*B*/a(), + foo12 = /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// let VariableStatement initializers +assertBreakpoints(` + let foo1 = /*S*/"" + o.a + "" + /*B*/b(), + foo2 = /*S*/"45", + foo3 = /*S*/"45" + /*B*/a(), + foo4 = /*S*/b() + "45", + foo5 = /*S*/"45" + /*B*/a() + /*B*/b(), + foo6 = /*S*/b() + "45" + /*B*/a(), + foo7 = /*S*/b() + /*B*/a() + "45", + foo8 = /*S*/"45" + o./*B*/a(), + foo9 = /*S*/o./*B*/b() + "45", + foo10 = /*S*/"45" + o./*B*/a() + o./*B*/b(), + foo11 = /*S*/o./*B*/b() + "45" + o./*B*/a(), + foo12 = /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// const VariableStatement initializers +assertBreakpoints(` + const foo1 = /*S*/"" + o.a + "" + /*B*/b(), + foo2 = /*S*/"45", + foo3 = /*S*/"45" + /*B*/a(), + foo4 = /*S*/b() + "45", + foo5 = /*S*/"45" + /*B*/a() + /*B*/b(), + foo6 = /*S*/b() + "45" + /*B*/a(), + foo7 = /*S*/b() + /*B*/a() + "45", + foo8 = /*S*/"45" + o./*B*/a(), + foo9 = /*S*/o./*B*/b() + "45", + foo10 = /*S*/"45" + o./*B*/a() + o./*B*/b(), + foo11 = /*S*/o./*B*/b() + "45" + o./*B*/a(), + foo12 = /*S*/o./*B*/b() + o./*B*/a() + "45"; +`); + +// EmptyStatement +assertBreakpoints(` + ; + ; + ; + /*S*/a(); +`); + +// IfStatement +assertBreakpoints(` + if (/*S*/a) {} + if (/*S*/a()) {} + if (/*S*/obj.prop) {} + if (/*S*/obj./*B*/prop()) {} + if (/*S*/"42" + a) {} + if (/*S*/"42" + /*B*/a()) {} + if (/*S*/"42" + obj.prop) {} + if (/*S*/"42" + obj./*B*/prop()) {} +`); + +// DoWhile +assertBreakpoints(` + do { + /*S*/fn(); + } while(/*S*/a) + do { + /*S*/fn(); + } while(/*S*/"42" + /*B*/a()); +`); + +// While +assertBreakpoints(` + while(/*S*/a) { + /*S*/fn(); + } + while(/*S*/"42" + /*B*/a()) { + /*S*/fn(); + } +`); + +// ForExpr +assertBreakpoints(` + for (/*S*/b = 42; /*S*/c; /*S*/d) /*S*/fn(); + for (var b = /*S*/42; /*S*/c; /*S*/d) /*S*/fn(); + for (let b = /*S*/42; /*S*/c; /*S*/d) /*S*/fn(); + for (const b = /*S*/42; /*S*/c; /*S*/d) /*S*/fn(); + for (b in /*S*/d) /*S*/fn(); + for (var b in /*S*/d) /*S*/fn(); + for (let b in /*S*/d) /*S*/fn(); + for (const b in /*S*/d) /*S*/fn(); + for (b of /*S*/d) /*S*/fn(); + for (var b of /*S*/d) /*S*/fn(); + for (let b of /*S*/d) /*S*/fn(); + for (const b of /*S*/d) /*S*/fn(); +`); + +// SwitchStatement +assertBreakpoints(` + switch (/*S*/d) { + case 42: + /*S*/fn(); + } +`); + +// ContinueStatement +assertBreakpoints(` + while (/*S*/a) { + /*S*/continue; + } +`); + +// BreakStatement +assertBreakpoints(` + while (/*S*/a) { + /*S*/break; + } +`); + +// ReturnStatement +assertBreakpoints(` + /*S*/return a + /*B*/b(); +`); + +// WithStatement +assertBreakpoints(` + with (/*S*/a) { + /*S*/fn(); + } +`); + +// ThrowStatement +assertBreakpoints(` + /*S*/throw /*B*/fn(); + /*S*/throw "42" + /*B*/fn(); +`); + +// DebuggerStatement +assertBreakpoints(` + /*S*/debugger; + /*S*/debugger; +`); + +// BlockStatent wrapper +assertBreakpoints(` + { + /*S*/a(); + } +`); + +// ClassDeclaration +assertBreakpoints(` + class Foo2 {} + /*S*/class Foo extends ("" + o.a + /*B*/a() + /*B*/b()) { } +`); + +// Misc examples +assertBreakpoints(` + /*S*/void /*B*/a(); +`); +assertBreakpoints(` + /*S*/a() + /*B*/b(); +`); +assertBreakpoints(` + for ( + var i = /*S*/0; + /*S*/i < n; // 4 + /*S*/++i + ) { + /*S*/console./*B*/log("omg"); + } +`); +assertBreakpoints(` + function * gen(){ + var foo = ( + (/*S*/console./*B*/log('before', /*B*/a())), + (yield console./*B*/log('mid', /*B*/b())), + (console./*B*/log('after', /*B*/a())) + ); + var foo2 = /*S*/a() + /*B*/b(); + /*S*/console./*B*/log(foo); + /*B*/} + var i = /*S*/0; + for (var foo of /*S*/gen()) { + /*S*/console./*B*/log(i++); + } +`); +assertBreakpoints(` + var fn = /*S*/() => { + /*S*/console./*B*/log("fn"); + /*S*/return /*B*/new Proxy({ prop: 42 }, { + deleteProperty() { + /*S*/console./*B*/log("delete"); + /*B*/} + }); + /*B*/}; +`); +assertBreakpoints(` + var fn = /*S*/async (arg) => { + /*S*/console./*B*/log("fn"); + /*B*/}; +`); +assertBreakpoints(` + var fn = /*S*/arg => { + /*S*/console./*B*/log("fn"); + /*B*/}; +`); +assertBreakpoints(` + var fn = /*S*/async arg => { + /*S*/console./*B*/log("fn"); + /*B*/}; +`); +assertBreakpoints(` + var fn = /*S*/(arg) => /*S*/console./*B*/log("fn"); + var fn = /*S*/async (arg) => /*S*/console./*B*/log("fn"); + var fn = /*S*/arg => /*S*/console./*B*/log("fn"); + var fn = /*S*/async arg => /*S*/console./*B*/log("fn"); +`); +assertBreakpoints(` + if ((/*S*/delete /*B*/fn().prop) + /*B*/b()) { + /*S*/console./*B*/log("foo"); + } +`); +assertBreakpoints(` + for (var j = /*S*/0; (/*S*/o.a) < 3; (/*S*/j++, /*B*/a(), /*B*/b())) { + /*S*/console./*B*/log(i); + } +`); +assertBreakpoints(` + function fn2( + [a, b] = (/*B*/a(), /*B*/b()) + ) { + /*S*/a(); + /*S*/b(); + /*B*/} + + ({ a, b } = (/*S*/a(), /*B*/b())); +`); +assertBreakpoints(` + /*S*/o.a + "42" + /*B*/a() + /*B*/b(); +`); +assertBreakpoints(` + /*S*/a(); + /*S*/o./*B*/a(/*B*/b()); +`); +assertBreakpoints(` + (/*S*/{}[obj.a] = 42 + /*B*/a()); +`); +assertBreakpoints(` + var { + foo = o.a + } = /*S*/{}; +`); +assertBreakpoints(` + var ack = /*S*/[ + o.a, + o.b, + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + /*B*/a(), + ]; +`); + +function assertBreakpoints(expected) { + const input = expected.replace(/\/\*[BS]\*\//g, ""); + + var global = newGlobal({ newCompartment: true }); + var dbg = Debugger(global); + dbg.onDebuggerStatement = function(frame) { + const fScript = frame.environment.parent.getVariable("f").script; + + let positions = []; + (function recurse(script) { + const bps = script.getPossibleBreakpoints(); + const offsets = script.getPossibleBreakpointOffsets(); + + assertEq(offsets.length, bps.length); + for (let i = 0; i < bps.length; i++) { + assertEq(offsets[i], bps[i].offset); + } + + positions = positions.concat(bps); + script.getChildScripts().forEach(recurse); + })(fScript); + + const result = annotateOffsets(input, positions); + assertEq(result, expected + "/*B*/"); + }; + + global.eval(`function f(){${input}} debugger;`); +} + +function annotateOffsets(code, positions) { + const offsetLookup = createOffsetLookup(code); + + positions = positions.slice(); + positions.sort((a, b) => { + const lineDiff = a.lineNumber - b.lineNumber; + return lineDiff === 0 ? a.columnNumber - b.columnNumber : lineDiff; + }); + positions.reverse(); + + let output = ""; + let last = code.length; + for (const { lineNumber, columnNumber, isStepStart } of positions) { + const offset = offsetLookup(lineNumber, columnNumber); + + output = + "/*" + + (isStepStart ? "S" : "B") + + "*/" + + code.slice(offset, last) + + output; + last = offset; + } + return code.slice(0, last) + output; +} + +function createOffsetLookup(code) { + const lines = code.split(/(\r?\n|\r|\u2028|\u2029)/g); + const lineOffsets = []; + + let count = 0; + for (const [i, str] of lines.entries()) { + if (i % 2 === 0) { + lineOffsets[i / 2] = count; + } + count += str.length; + } + + return function(line, column) { + // Lines from getAllColumnOffsets are 1-based. + line = line - 1; + + if (!lineOffsets.hasOwnProperty(line)) { + throw new Error("Unknown line " + line + " column " + column); + } + return lineOffsets[line] + column; + }; +} diff --git a/js/src/jit-test/tests/debug/Script-global-01.js b/js/src/jit-test/tests/debug/Script-global-01.js new file mode 100644 index 0000000000..fb2d131e35 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-global-01.js @@ -0,0 +1,18 @@ +// Debugger.Script.prototype.script returns the global the script runs in. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.global, gw); +} + +g.eval('debugger;'); +assertEq(log, 'd'); + +g.eval('function f() { debugger; }'); +g.f(); +assertEq(log, 'dd'); diff --git a/js/src/jit-test/tests/debug/Script-global-02.js b/js/src/jit-test/tests/debug/Script-global-02.js new file mode 100644 index 0000000000..9d5e1fda32 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-global-02.js @@ -0,0 +1,36 @@ +// Debugger.Script.prototype.script returns the global the script runs in. +// Multi-global version. + +var dbg = new Debugger; + +var g1 = newGlobal({newCompartment: true}); +var g1w = dbg.addDebuggee(g1); + +var g2 = newGlobal({newCompartment: true}); +var g2w = dbg.addDebuggee(g2); + +var g3 = newGlobal({newCompartment: true}); +var g3w = dbg.addDebuggee(g3); + +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.global, g1w); + assertEq(frame.older.script.global, g2w); + assertEq(frame.older.older.script.global, g3w); + assertEq(frame.older.older.older.script.global, g1w); +} + +g1.eval('function f() { debugger; }'); + +g2.g1 = g1; +g2.eval('function g() { g1.f(); }'); + +g3.g2 = g2; +g3.eval('function h() { g2.g(); }'); + +g1.g3 = g3; +g1.eval('function i() { g3.h(); }'); + +g1.i(); +assertEq(log, 'd'); diff --git a/js/src/jit-test/tests/debug/Script-isFunction.js b/js/src/jit-test/tests/debug/Script-isFunction.js new file mode 100644 index 0000000000..5f68d54b19 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-isFunction.js @@ -0,0 +1,25 @@ +// Script.isFunction should do what it is supposed to do. + +const g = newGlobal({newCompartment: true}); +const dbg = new Debugger; +dbg.addDebuggee(g); + +let gNumFunctionScripts = 0; +function countFunctionScripts(script) { + if (script.isFunction) { + gNumFunctionScripts++; + } + return script.getChildScripts().forEach(countFunctionScripts); +} + +dbg.onNewScript = countFunctionScripts; + +g.eval(` +function f() { + function f2() {} +} +async function g() {} +function* h() {} +`); + +assertEq(gNumFunctionScripts, 4); diff --git a/js/src/jit-test/tests/debug/Script-isInCatchScope.js b/js/src/jit-test/tests/debug/Script-isInCatchScope.js new file mode 100644 index 0000000000..c5d3cfe12e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-isInCatchScope.js @@ -0,0 +1,68 @@ +// Test if isInCatchScope properly detects catch blocks. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +function test(string, mustBeCaught) { + let index = 0; + dbg.onExceptionUnwind = function (frame) { + let willBeCaught = false; + do { + if (frame.script.isInCatchScope(frame.offset)) { + willBeCaught = true; + break; + } + frame = frame.older; + } while (frame != null); + assertEq(willBeCaught, mustBeCaught[index++]); + }; + + try { + g.eval(string); + } catch (ex) {} + assertEq(index, mustBeCaught.length); +} + +// Should correctly detect catch blocks +test("throw new Error();", [false]); +test("try { throw new Error(); } catch (e) {}", [true]); +test("try { throw new Error(); } finally {}", [false, false]); +test("try { throw new Error(); } catch (e) {} finally {}", [true]); + +// Source of the exception shouldn't matter +test("(null)();", [false]); +test("try { (null)(); } catch (e) {}", [true]); +test("try { (null)(); } finally {}", [false, false]); +test("try { (null)(); } catch (e) {} finally {}", [true]); + +// Should correctly detect catch blocks in functions +test("function f() { throw new Error(); } f();", [false, false]); +test("function f() { try { throw new Error(); } catch (e) {} } f();", [true]); +test("function f() { try { throw new Error(); } finally {} } f();", [false, false, false]); +test("function f() { try { throw new Error(); } catch (e) {} finally {} } f();", [true]); + +// Should correctly detect catch blocks in evals +test("eval('throw new Error();')", [false, false]); +test("eval('try { throw new Error(); } catch (e) {}');", [true]); +test("eval('try { throw new Error(); } finally {}');", [false, false, false]); +test("eval('try { throw new Error(); } catch (e) {} finally {}');", [true]); + +// Should correctly detect rethrows +test("try { throw new Error(); } catch (e) { throw e; }", [true, false]); +test("try { try { throw new Error(); } catch (e) { throw e; } } catch (e) {}", [true, true]); +test("try { try { throw new Error(); } finally {} } catch (e) {}", [true, true]); +test("function f() { try { throw new Error(); } catch (e) { throw e; } } f();", [true, false, false]); +test("function f() { try { try { throw new Error(); } catch (e) { throw e; } } catch (e) {} } f();", [true, true]); +test("function f() { try { try { throw new Error(); } finally {} } catch (e) {} } f();", [true, true]); +test("eval('try { throw new Error(); } catch (e) { throw e; }')", [true, false, false]); +test("eval('try { try { throw new Error(); } catch (e) { throw e; } } catch (e) {}')", [true, true]); + +// Should correctly detect catch blocks across frame boundaries +test("function f() { throw new Error(); } try { f(); } catch (e) {}", [true, true]); +test("function f() { throw new Error(); } try { f(); } catch (e) { throw e; }", [true, true, false]); +test("try { eval('throw new Error()'); } catch (e) {}", [true, true]); +test("try { eval('throw new Error()'); } catch (e) { throw e; }", [true, true, false]); + +// Should correctly detect catch blocks just before and just after throws +test("throw new Error; try {} catch (e) {}", [false]); +test("try {} catch (e) {} throw new Error();", [false]); diff --git a/js/src/jit-test/tests/debug/Script-isModule-01.js b/js/src/jit-test/tests/debug/Script-isModule-01.js new file mode 100644 index 0000000000..36e1c9301e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-isModule-01.js @@ -0,0 +1,13 @@ +// Debugger.Object.prototype.isModule + +const g = newGlobal({newCompartment: true}); +const dbg = Debugger(g); +let count = 0; +dbg.onNewScript = function (script) { + count += 1; + assertEq(script.isModule, true); +}; +const m = g.parseModule(""); +m.declarationInstantiation(); +m.evaluation(); +assertEq(count, 1); diff --git a/js/src/jit-test/tests/debug/Script-isModule-02.js b/js/src/jit-test/tests/debug/Script-isModule-02.js new file mode 100644 index 0000000000..c469f11ec8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-isModule-02.js @@ -0,0 +1,11 @@ +// Debugger.Object.prototype.isModule + +const g = newGlobal({newCompartment: true}); +const dbg = Debugger(g); +let count = 0; +dbg.onNewScript = function (script) { + count += 1; + assertEq(script.isModule, false); +}; +const m = g.eval(""); +assertEq(count, 1); diff --git a/js/src/jit-test/tests/debug/Script-isModule-03.js b/js/src/jit-test/tests/debug/Script-isModule-03.js new file mode 100644 index 0000000000..0b13718dcf --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-isModule-03.js @@ -0,0 +1,18 @@ +// Debugger.Object.prototype.isModule + +const g = newGlobal({newCompartment: true}); +const dbg = Debugger(g); +let count = 0; +dbg.onNewScript = function (script) { + count += 1; + assertEq(script.isModule, true); + + dbg.onNewScript = function (script) { + count += 1; + assertEq(script.isModule, false); + }; +}; +const m = g.parseModule("eval('')"); +m.declarationInstantiation(); +m.evaluation(); +assertEq(count, 2); diff --git a/js/src/jit-test/tests/debug/Script-isModule-04.js b/js/src/jit-test/tests/debug/Script-isModule-04.js new file mode 100644 index 0000000000..8d22ca680a --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-isModule-04.js @@ -0,0 +1,19 @@ +// Debugger.Object.prototype.isModule + +const g = newGlobal({newCompartment: true}); +const dbg = Debugger(g); +let count = 0; +dbg.onNewScript = function (script) { + count += 1; + assertEq(script.isModule, true); +}; +dbg.onDebuggerStatement = function (frame) { + const { script } = frame; + + assertEq(script.isModule, false); +}; +const m = g.parseModule("(function(){ debugger; })()"); +m.declarationInstantiation(); +m.evaluation(); + +assertEq(count, 1); diff --git a/js/src/jit-test/tests/debug/Script-lineCount.js b/js/src/jit-test/tests/debug/Script-lineCount.js new file mode 100644 index 0000000000..1eeb28013d --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-lineCount.js @@ -0,0 +1,23 @@ +// Test Script.lineCount. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +function test(scriptText, expectedLineCount) { + let found = false; + + dbg.onNewScript = function(script, global) { + assertEq(script.lineCount, expectedLineCount); + found = true; + }; + + g.evaluate(scriptText); + assertEq(found, true); +} + +src = 'var a = (function(){\n' + // 0 + 'var b = 9;\n' + // 1 + 'console.log("x", b);\n'+ // 2 + 'return b;\n' + // 3 + '})();'; // 4 +test(src, 5); diff --git a/js/src/jit-test/tests/debug/Script-mainOffset-01.js b/js/src/jit-test/tests/debug/Script-mainOffset-01.js new file mode 100644 index 0000000000..ae958a523b --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-mainOffset-01.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ +// The main offset of a script should be hit before it performs any actions. + +var g = newGlobal({newCompartment: true}); +g.eval("var n = 0; function foo() { n = 1; }"); +var dbg = Debugger(g); + +var hits = 0; +function breakpointHit(frame) { + hits++; + assertEq(frame.eval("n").return, 0); +} + +dbg.onDebuggerStatement = function (frame) { + var script = frame.eval("foo").return.script; + script.setBreakpoint(script.mainOffset, { hit: breakpointHit }); +}; +g.eval("debugger; foo()"); +assertEq(g.eval("n"), 1); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Script-parameterNames.js b/js/src/jit-test/tests/debug/Script-parameterNames.js new file mode 100644 index 0000000000..1557a97e22 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-parameterNames.js @@ -0,0 +1,36 @@ +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); + +function check(expr, expected) { + let completion = gDO.executeInGlobal(expr); + if (completion.throw) + throw completion.throw.unsafeDereference(); + + let fn = completion.return; + if (expected === undefined) + assertEq(fn.script.parameterNames, undefined); + else + assertDeepEq(fn.script.parameterNames, expected); +} + +check('(function () {})', []); +check('(function (x) {})', ["x"]); +check('(function (x = 1) {})', ["x"]); +check('(function (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z) {})', + ["a","b","c","d","e","f","g","h","i","j","k","l","m", + "n","o","p","q","r","s","t","u","v","w","x","y","z"]); +check('(function (a, [b, c], {d, e:f}) { })', + ["a", undefined, undefined]); +check('(async function (a, b, c) {})', ["a", "b", "c"]); +check('(async function* (d, e, f) {})', ["d", "e", "f"]); + +// Non-function scripts have |undefined| parameterNames. +var log = []; +dbg.onEnterFrame = function(frame) { + log.push(frame.script.parameterNames); +}; +g.evaluate("function foo(a, b) { return eval('1'); }; foo();"); +assertDeepEq(log, [undefined, ["a", "b"], undefined]); // global, function, eval diff --git a/js/src/jit-test/tests/debug/Script-selfhosted-builtins.js b/js/src/jit-test/tests/debug/Script-selfhosted-builtins.js new file mode 100644 index 0000000000..ff981f5400 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-selfhosted-builtins.js @@ -0,0 +1,15 @@ +// The script of self-hosted builtins is not exposed to the debugger and +// instead is reported as |undefined| just like native builtins. + +let g = newGlobal({newCompartment: true}); + +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +// Array is a known native builtin function. +let nativeBuiltin = gw.makeDebuggeeValue(g.Array); +assertEq(nativeBuiltin.script, undefined); + +// Array.prototype[@@iterator] is a known self-hosted builtin function. +let selfhostedBuiltin = gw.makeDebuggeeValue(g.Array.prototype[Symbol.iterator]); +assertEq(selfhostedBuiltin.script, undefined); diff --git a/js/src/jit-test/tests/debug/Script-source-01.js b/js/src/jit-test/tests/debug/Script-source-01.js new file mode 100644 index 0000000000..d50d53e583 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-source-01.js @@ -0,0 +1,26 @@ +/* + * Script.prototype.source should be an object. Moreover, it should be the + * same object for each child script within the same debugger. + */ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +let count = 0; +dbg.onNewScript = function (script) { + assertEq(typeof script.source, "object"); + function traverse(script) { + ++count; + script.getChildScripts().forEach(function (child) { + assertEq(child.source, script.source); + traverse(child); + }); + } + traverse(script); +} + +g.eval("2 * 3"); +g.eval("function f() {}"); +g.eval("function f() { function g() {} }"); +g.eval("eval('2 * 3')"); +g.eval("new Function('2 * 3')"); +assertEq(count, 10); diff --git a/js/src/jit-test/tests/debug/Script-source-02.js b/js/src/jit-test/tests/debug/Script-source-02.js new file mode 100644 index 0000000000..43bb375d22 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-source-02.js @@ -0,0 +1,16 @@ +/* + * Script.prototype.source should be the same object for both the top-level + * script and the script of functions accessed as debuggee values on the global + */ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +let count = 0; +dbg.onDebuggerStatement = function (frame) { + ++count; + assertEq(frame.script.source, gw.makeDebuggeeValue(g.f).script.source); +} + +g.eval("function f() {}; debugger;"); +assertEq(count, 1); diff --git a/js/src/jit-test/tests/debug/Script-source-03.js b/js/src/jit-test/tests/debug/Script-source-03.js new file mode 100644 index 0000000000..ed42967d7c --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-source-03.js @@ -0,0 +1,22 @@ +/* + * Script.prototype.source should be a different object for the same script + * within different debuggers. + */ +let g = newGlobal({newCompartment: true}); +let dbg1 = new Debugger(g); +let dbg2 = new Debugger(g); + +var count = 0; +var source; +function test(script) { + ++count; + if (!source) + source = script.source; + else + assertEq(script.source != source, true); +}; +dbg1.onNewScript = test; +dbg2.onNewScript = test; + +g.eval("2 * 3"); +assertEq(count, 2); diff --git a/js/src/jit-test/tests/debug/Script-sourceStart-01.js b/js/src/jit-test/tests/debug/Script-sourceStart-01.js new file mode 100644 index 0000000000..d5399a2b7d --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-sourceStart-01.js @@ -0,0 +1,22 @@ +/* + * Script.prototype.sourceStart and Script.prototype.sourceLength should both be + * a number. + */ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +var count = 0; +function test(string, range) { + dbg.onNewScript = function (script) { + ++count; + assertEq(script.sourceStart, range[0]); + assertEq(script.sourceLength, range[1]); + }; + + g.eval(string); +}; + +test("", [0, 0]); +test("2 * 3", [0, 5]); +test("2\n*\n3", [0, 5]); +assertEq(count, 3); diff --git a/js/src/jit-test/tests/debug/Script-sourceStart-02.js b/js/src/jit-test/tests/debug/Script-sourceStart-02.js new file mode 100644 index 0000000000..784388b1ce --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-sourceStart-02.js @@ -0,0 +1,32 @@ +/* + * For function statements, Script.prototype.sourceStart and + * Script.prototype.sourceLength should comprise both the opening '(' and the + * closing '}'. + */ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +function test(string, ranges) { + var index = 0; + dbg.onNewScript = function (script) { + function traverse(script) { + script.getChildScripts().forEach(function (script) { + assertEq(script.sourceStart, ranges[index][0]); + assertEq(script.sourceLength, ranges[index][1]); + ++index; + traverse(script); + }); + } + traverse(script); + }; + + g.eval(string); + assertEq(index, ranges.length); +}; + +test("function f() {}", [[10, 5]]); +test("function f() { function g() {} }", [[10, 22], [25, 5]]); +test("function f() { function g() { function h() {} } }", [[10, 39], [25, 22], [40, 5]]); +test("function f() { if (true) function g() {} }", [[10, 32], [35, 5]]); +test("var o = { get p () {} }", [[16, 5]]); +test("var o = { set p (x) {} }", [[16, 6]]); diff --git a/js/src/jit-test/tests/debug/Script-sourceStart-03.js b/js/src/jit-test/tests/debug/Script-sourceStart-03.js new file mode 100644 index 0000000000..073c31f2db --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-sourceStart-03.js @@ -0,0 +1,35 @@ +/* + * For arrow functions, Script.prototype.sourceStart and + * Script.prototype.sourceLength should comprise the entire function expression + * (including arguments) + */ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +function test(string, ranges) { + var index = 0; + dbg.onNewScript = function (script) { + function traverse(script) { + script.getChildScripts().forEach(function (script) { + assertEq(script.sourceStart, ranges[index][0]); + assertEq(script.sourceLength, ranges[index][1]); + ++index; + traverse(script); + }); + } + traverse(script); + }; + + g.eval(string); + + /* + * In some configurations certain child scripts are listed twice, so we + * cannot rely on index always having the exact same value + */ + assertEq(0 < index && index <= ranges.length, true); +}; + +test("() => {}", [[0, 8]]); +test("(x, y) => { x * y }", [[0, 19]]); +test("x => x * x", [[0, 10]]); +test("x => x => x * x", [[0, 15], [5, 10], [5, 10]]); diff --git a/js/src/jit-test/tests/debug/Script-sourceStart-04.js b/js/src/jit-test/tests/debug/Script-sourceStart-04.js new file mode 100644 index 0000000000..de03a2f582 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-sourceStart-04.js @@ -0,0 +1,25 @@ +/* + * For eval and Function constructors, Script.prototype.sourceStart and + * Script.prototype.sourceLength should comprise the entire script (excluding + * arguments in the case of Function constructors) + */ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +var count = 0; +function test(string, range) { + dbg.onNewScript = function (script) { + ++count; + if (count % 2 == 0) { + assertEq(script.sourceStart, range[0]); + assertEq(script.sourceLength, range[1]); + } + } + + g.eval(string); +} + +test("eval('2 * 3')", [0, 5]); +test("new Function('2 * 3')", [0, 31]); +test("new Function('x', 'x * x')", [0, 32]); +assertEq(count, 6); diff --git a/js/src/jit-test/tests/debug/Script-startColumn.js b/js/src/jit-test/tests/debug/Script-startColumn.js new file mode 100644 index 0000000000..5f8f0c5d4e --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-startColumn.js @@ -0,0 +1,99 @@ +// Script.prototype.startColumn returns the correct column for all scripts. + +const g = newGlobal({newCompartment: true, useWindowProxy: true}); +const dbg = Debugger(g); +const gw = dbg.addDebuggee(g); + +function test(f, expected) { + const fw = gw.makeDebuggeeValue(f); + assertEq(fw.callable, true); + assertEq(fw.script.startColumn, expected); +} + +g.eval(` +function f1() { } +`); +test(g.f1, 11); + +g.eval(` +var f2 = function({ a, b, c }, d, e, ...more) { }; +`); +test(g.f2, 17); + +g.eval(` +var f3 = function *() { }; +`); +test(g.f3, 19); + +g.eval(` +var f4 = async function + () { }; +`); +test(g.f4, 2); + +g.eval(` +var f5 = (a, b) => a + b; +`); +test(g.f5, 9); + +g.eval(` +var f6 = a => a + 1; +`); +test(g.f6, 9); + +g.eval(` +var MyClass = class { + method() { } +}; +var myInstance = new MyClass(); +`); +test(g.myInstance.method, 10); +test(g.myInstance.constructor, 14); + +const gEager = newGlobal({newCompartment: true, useWindowProxy: true, disableLazyParsing: true}); +const eagerDbg = Debugger(gEager); +const gEagerWrapped = eagerDbg.addDebuggee(gEager); +gEager.eval(` +function f7() { } +`); +const f7w = gEagerWrapped.makeDebuggeeValue(gEager.f7); +assertEq(f7w.callable, true); +assertEq(f7w.script.startColumn, 11); + +g.eval(` +function f8() { + return function f8Inner() { } +} +`); +test(g.f8, 11); +test(g.f8(), 27); + +g.eval(` +var f9 = new Function(\"\"); +`); +test(g.f9, 0); + +let hit = 0; +let column; +dbg.onDebuggerStatement = function (frame) { + column = frame.script.startColumn; + hit += 1; +}; + +g.eval(` debugger;`); +assertEq(column, 0); +assertEq(hit, 1); + +const location = { fileName: "column.js", lineNumber: 1, columnNumber: 1 }; +hit = 0; +g.evaluate(` debugger;`, location); +assertEq(column, 1); +assertEq(hit, 1); + +g.evaluate(`var f10 = function () { };`, location); +test(g.f10, 20); + +g.evaluate(` +var f11 = function () { }; +`, location); +test(g.f11, 19); diff --git a/js/src/jit-test/tests/debug/Script-startLine.js b/js/src/jit-test/tests/debug/Script-startLine.js new file mode 100644 index 0000000000..baf27427e1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-startLine.js @@ -0,0 +1,63 @@ +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var start, count; +dbg.onDebuggerStatement = function (frame) { + assertEq(start, undefined); + start = frame.script.startLine; + count = frame.script.lineCount; + assertEq(typeof frame.script.url, 'string'); +}; + +function test(f, manualCount) { + start = count = g.first = g.last = undefined; + f(); + if (manualCount) + g.last = g.first + manualCount - 1; + assertEq(start, g.first); + assertEq(count, g.last + 1 - g.first); + print(start, count); +} + +test(function () { + g.eval("first = Error().lineNumber;\n" + + "debugger;\n" + + "last = Error().lineNumber;"); +}); + +test(function () { + g.evaluate("first = Error().lineNumber;\n" + + "debugger;\n" + + Array(17000).join("\n") + + "last = Error().lineNumber;"); +}); + +test(function () { + g.eval("function f1() { first = Error().lineNumber\n" + + " debugger;\n" + + " last = Error().lineNumber; }\n" + + "f1();"); +}); + +g.eval("function f2() {\n" + + " eval('first = Error().lineNumber\\n\\ndebugger;\\n\\nlast = Error().lineNumber;');\n" + + "}\n"); +test(g.f2); +test(g.f2); + +// Having a last = Error().lineNumber forces a setline srcnote, so test a +// function that ends with newline srcnotes. +g.eval("/* Any copyright is dedicated to the Public Domain.\n" + + " http://creativecommons.org/publicdomain/zero/1.0/ */\n" + + "\n" + + "function secondCall() { first = Error().lineNumber;\n" + + " debugger;\n" + + " // Comment\n" + + " eval(\"42;\");\n" + + " function foo() {}\n" + + " if (true) {\n" + + " foo();\n" + + // The "missing" newline here is a trick to make a newline + // source note come at the end. A real newline between the two + // closing braces causes a setline note instead. + " } }"); +test(g.secondCall, 8); diff --git a/js/src/jit-test/tests/debug/Script-url.js b/js/src/jit-test/tests/debug/Script-url.js new file mode 100644 index 0000000000..0bfe398963 --- /dev/null +++ b/js/src/jit-test/tests/debug/Script-url.js @@ -0,0 +1,10 @@ +// Script.prototype.url can be a string or null. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +for (var fileName of ['file:///var/foo.js', null]) { + g.evaluate("function f(x) { return 2*x; }", {fileName: fileName}); + var fw = gw.getOwnPropertyDescriptor('f').value; + assertEq(fw.script.url, fileName); +} diff --git a/js/src/jit-test/tests/debug/Source-displayURL-deprecated.js b/js/src/jit-test/tests/debug/Source-displayURL-deprecated.js new file mode 100644 index 0000000000..efa92eddd8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-displayURL-deprecated.js @@ -0,0 +1,26 @@ +/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */ +// Source.prototype.displayURL can be a string or null. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +function getDisplayURL() { + let fw = gw.makeDebuggeeValue(g.f); + return fw.script.source.displayURL; +} + +// Comment pragmas +g.evaluate('function f() {}\n' + + '//@ sourceURL=file:///var/quux.js'); +assertEq(getDisplayURL(), 'file:///var/quux.js'); + +g.evaluate('function f() {}\n' + + '/*//@ sourceURL=file:///var/quux.js*/'); +assertEq(getDisplayURL(), 'file:///var/quux.js'); + +g.evaluate('function f() {}\n' + + '/*\n' + + '//@ sourceURL=file:///var/quux.js\n' + + '*/'); +assertEq(getDisplayURL(), 'file:///var/quux.js'); diff --git a/js/src/jit-test/tests/debug/Source-displayURL-disable.js b/js/src/jit-test/tests/debug/Source-displayURL-disable.js new file mode 100644 index 0000000000..3ac2dd3fff --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-displayURL-disable.js @@ -0,0 +1,14 @@ +// |jit-test| --no-source-pragmas + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); +let source; +dbg.onDebuggerStatement = function (frame) { + source = frame.script.source; +}; + +g.eval(` + debugger; + //# sourceURL=file.js +`); +assertEq(source.displayURL, null); diff --git a/js/src/jit-test/tests/debug/Source-displayURL.js b/js/src/jit-test/tests/debug/Source-displayURL.js new file mode 100644 index 0000000000..c182631da0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-displayURL.js @@ -0,0 +1,91 @@ +/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */ +// Source.prototype.displayURL can be a string or null. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +function getDisplayURL() { + let fw = gw.makeDebuggeeValue(g.f); + return fw.script.source.displayURL; +} + +// Without a source url +g.evaluate("function f(x) { return 2*x; }"); +assertEq(getDisplayURL(), null); + +// With a source url +g.evaluate("function f(x) { return 2*x; }", {displayURL: 'file:///var/foo.js'}); +assertEq(getDisplayURL(), 'file:///var/foo.js'); + +// Nested functions +let fired = false; +dbg.onDebuggerStatement = function (frame) { + fired = true; + assertEq(frame.script.source.displayURL, 'file:///var/bar.js'); +}; +g.evaluate('(function () { (function () { debugger; })(); })();', + {displayURL: 'file:///var/bar.js'}); +assertEq(fired, true); + +// Comment pragmas +g.evaluate('function f() {}\n' + + '//# sourceURL=file:///var/quux.js'); +assertEq(getDisplayURL(), 'file:///var/quux.js'); + +g.evaluate('function f() {}\n' + + '/*//# sourceURL=file:///var/quux.js*/'); +assertEq(getDisplayURL(), 'file:///var/quux.js'); + +g.evaluate('function f() {}\n' + + '/*\n' + + '//# sourceURL=file:///var/quux.js\n' + + '*/'); +assertEq(getDisplayURL(), 'file:///var/quux.js'); + +// Spaces are disallowed by the URL spec (they should have been +// percent-encoded). +g.evaluate('function f() {}\n' + + '//# sourceURL=http://example.com/has illegal spaces'); +assertEq(getDisplayURL(), 'http://example.com/has'); + +// When the URL is missing, we don't set the sourceMapURL and we don't skip the +// next line of input. +g.evaluate('function f() {}\n' + + '//# sourceURL=\n' + + 'function z() {}'); +assertEq(getDisplayURL(), null); +assertEq('z' in g, true); + +// The last comment pragma we see should be the one which sets the displayURL. +g.evaluate('function f() {}\n' + + '//# sourceURL=http://example.com/foo.js\n' + + '//# sourceURL=http://example.com/bar.js'); +assertEq(getDisplayURL(), 'http://example.com/bar.js'); + +// With both a comment and the evaluate option. +g.evaluate('function f() {}\n' + + '//# sourceURL=http://example.com/foo.js', + {displayURL: 'http://example.com/bar.js'}); +assertEq(getDisplayURL(), 'http://example.com/foo.js'); + + +// Bug 981987 reported that we hadn't set sourceURL yet when firing onNewScript +// from the Function constructor. +var capturedScript; +var capturedDisplayURL; +var capturedSourceMapURL; +dbg.onNewScript = function (script) { + capturedScript = script; + capturedDisplayURL = script.source.displayURL; + capturedSourceMapURL = script.source.sourceMapURL; + dbg.onNewScript = undefined; +}; +var fun = gw.makeDebuggeeValue(g.Function('//# sourceURL=munge.js\n//# sourceMappingURL=grunge.map\n')); +assertEq(capturedScript, fun.script); + +assertEq(capturedDisplayURL, fun.script.source.displayURL); +assertEq(capturedDisplayURL, 'munge.js'); + +assertEq(capturedSourceMapURL, fun.script.source.sourceMapURL); +assertEq(capturedSourceMapURL, 'grunge.map'); diff --git a/js/src/jit-test/tests/debug/Source-element-01.js b/js/src/jit-test/tests/debug/Source-element-01.js new file mode 100644 index 0000000000..c674d2774c --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-element-01.js @@ -0,0 +1,13 @@ +// Source.prototype.element can be an object or undefined. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.evaluate("function f(x) { return 2*x; }", {element: { foo: "bar" }}); +var fw = gw.getOwnPropertyDescriptor('f').value; +assertEq(typeof fw.script.source.element, "object"); +assertEq(fw.script.source.element instanceof Debugger.Object, true); +assertEq(fw.script.source.element.getOwnPropertyDescriptor("foo").value, "bar"); +g.evaluate("function f(x) { return 2*x; }"); +var fw = gw.getOwnPropertyDescriptor('f').value; +assertEq(typeof fw.script.source.element, "undefined"); diff --git a/js/src/jit-test/tests/debug/Source-element-02.js b/js/src/jit-test/tests/debug/Source-element-02.js new file mode 100644 index 0000000000..38c92280b4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-element-02.js @@ -0,0 +1,6 @@ +// Specifying an owning element in a cross-global evaluation shouldn't crash. +// That is, when 'evaluate' switches compartments, it should properly wrap +// the CompileOptions members that will become cross-compartment +// references. + +evaluate('42 + 1729', { global: newGlobal(), element: {} }); diff --git a/js/src/jit-test/tests/debug/Source-element-03.js b/js/src/jit-test/tests/debug/Source-element-03.js new file mode 100644 index 0000000000..ad719849af --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-element-03.js @@ -0,0 +1,26 @@ +// |jit-test| skip-if: helperThreadCount() === 0 + +// Owning elements and attribute names are attached to scripts compiled +// off-thread. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); + +var elt = new g.Object; +var eltDO = gDO.makeDebuggeeValue(elt); + +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + var source = frame.script.source; + assertEq(source.element, eltDO); + assertEq(source.elementAttributeName, 'mass'); +}; + +g.offThreadCompileScript('debugger;', + { element: elt, + elementAttributeName: 'mass' }); +log += 'o'; +g.runOffThreadScript(); +assertEq(log, 'od'); diff --git a/js/src/jit-test/tests/debug/Source-element-04.js b/js/src/jit-test/tests/debug/Source-element-04.js new file mode 100644 index 0000000000..8aa0cfef3f --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-element-04.js @@ -0,0 +1,13 @@ +// source.element is undefined if the script was introduced using eval() or Function(). + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +g.eval("function f(x) { return 2*x; }"); +var fw = gw.getOwnPropertyDescriptor('f').value; +assertEq(fw.script.source.element, undefined); + +g.x = g.Function("return 13;"); +var xw = gw.getOwnPropertyDescriptor('x').value; +assertEq(xw.script.source.element, undefined); diff --git a/js/src/jit-test/tests/debug/Source-element-05.js b/js/src/jit-test/tests/debug/Source-element-05.js new file mode 100644 index 0000000000..702a6ef712 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-element-05.js @@ -0,0 +1,11 @@ +// source.element is undefined when bad values are passed to evaluate(). + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +for (let nonObject of [32, "[object Object]", null, undefined]) { + g.evaluate("function f(x) { return 2*x; }", {element: nonObject}); + var fw = gw.getOwnPropertyDescriptor('f').value; + assertEq(fw.script.source.element, undefined); +} diff --git a/js/src/jit-test/tests/debug/Source-elementAttributeName.js b/js/src/jit-test/tests/debug/Source-elementAttributeName.js new file mode 100644 index 0000000000..5e14a6857e --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-elementAttributeName.js @@ -0,0 +1,11 @@ +// Source.prototype.elementAttributeName can be a string or undefined. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.evaluate("function f(x) { return 2*x; }", {elementAttributeName: "src"}); +var fw = gw.getOwnPropertyDescriptor('f').value; +assertEq(fw.script.source.elementAttributeName, "src"); +g.evaluate("function f(x) { return 2*x; }"); +var fw = gw.getOwnPropertyDescriptor('f').value; +assertEq(fw.script.source.elementAttributeName, undefined); diff --git a/js/src/jit-test/tests/debug/Source-introductionScript-01.js b/js/src/jit-test/tests/debug/Source-introductionScript-01.js new file mode 100644 index 0000000000..29dfeb0c83 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-introductionScript-01.js @@ -0,0 +1,118 @@ +// Dynamically generated sources should have their introduction script and +// offset set correctly. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); +var log; + +// Direct eval, while the frame is live. +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + var source = frame.script.source; + var introducer = frame.older; + assertEq(source.introductionScript, introducer.script); + assertEq(source.introductionOffset, introducer.offset); +}; +log = ''; +g.eval('\n\neval("\\n\\ndebugger;");'); +assertEq(log, 'd'); + +// Direct eval, after the frame has been popped. +var introducer, introduced; +dbg.onDebuggerStatement = function (frame) { + log += 'de1'; + introducer = frame.script; + dbg.onDebuggerStatement = function (frame) { + log += 'de2'; + introduced = frame.script.source; + }; +}; +log = ''; +g.evaluate('debugger; eval("\\n\\ndebugger;");', { lineNumber: 1812 }); +assertEq(log, 'de1de2'); +assertEq(introduced.introductionScript, introducer); +assertEq(introducer.getOffsetLocation(introduced.introductionOffset).lineNumber, 1812); + +// Indirect eval, while the frame is live. +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + var source = frame.script.source; + var introducer = frame.older; + assertEq(source.introductionScript, introducer.script); + assertEq(source.introductionOffset, introducer.offset); +}; +log = ''; +g.eval('\n\n(0,eval)("\\n\\ndebugger;");'); +assertEq(log, 'd'); + +// Indirect eval, after the frame has been popped. +var introducer, introduced; +dbg.onDebuggerStatement = function (frame) { + log += 'de1'; + introducer = frame.script; + dbg.onDebuggerStatement = function (frame) { + log += 'de2'; + introduced = frame.script.source; + }; +}; +log = ''; +g.evaluate('debugger; (0,eval)("\\n\\ndebugger;");', { lineNumber: 1066 }); +assertEq(log, 'de1de2'); +assertEq(introduced.introductionScript, introducer); +assertEq(introducer.getOffsetLocation(introduced.introductionOffset).lineNumber, 1066); + +// Function constructor. +dbg.onDebuggerStatement = function (frame) { + log += 'o'; + var outerScript = frame.script; + var outerOffset = frame.offset; + dbg.onDebuggerStatement = function (frame) { + log += 'i'; + var source = frame.script.source; + assertEq(source.introductionScript, outerScript); + assertEq(outerScript.getOffsetLocation(source.introductionOffset).lineNumber, + outerScript.getOffsetLocation(outerOffset).lineNumber); + }; +}; +log = ''; +g.eval('\n\n\ndebugger; Function("debugger;")()'); +assertEq(log, 'oi'); + +// Function constructor, after the the introduction call's frame has been +// popped. +var introducer; +dbg.onDebuggerStatement = function (frame) { + log += 'F2'; + introducer = frame.script; +}; +log = ''; +var fDO = gDO.executeInGlobal('debugger; Function("origami;")', { lineNumber: 1685 }).return; +var source = fDO.script.source; +assertEq(log, 'F2'); +assertEq(source.introductionScript, introducer); +assertEq(introducer.getOffsetLocation(source.introductionOffset).lineNumber, 1685); + +// If the introduction script is in a different global from the script it +// introduced, we don't record it. +dbg.onDebuggerStatement = function (frame) { + log += 'x'; + var source = frame.script.source; + assertEq(source.introductionScript, undefined); + assertEq(source.introductionOffset, undefined); +}; +log = ''; +g.eval('debugger;'); // introduction script is this top-level script +assertEq(log, 'x'); + +// If the code is introduced by a function that doesn't provide +// introduction information, that shouldn't be a problem. +dbg.onDebuggerStatement = function (frame) { + log += 'x'; + var source = frame.script.source; + assertEq(source.introductionScript, undefined); + assertEq(source.introductionOffset, undefined); +}; +log = ''; +g.eval('evaluate("debugger;", { lineNumber: 1729 });'); +assertEq(log, 'x'); diff --git a/js/src/jit-test/tests/debug/Source-introductionScript-02.js b/js/src/jit-test/tests/debug/Source-introductionScript-02.js new file mode 100644 index 0000000000..45b0f61cb7 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-introductionScript-02.js @@ -0,0 +1,44 @@ +// Calls to 'eval', etc. by JS primitives get attributed to the point of +// the primitive's call. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); +var log = ''; + +function outerHandler(frame) { + log += 'o'; + var outerScript = frame.script; + + dbg.onDebuggerStatement = function (frame) { + log += 'i'; + var source = frame.script.source; + var introScript = source.introductionScript; + assertEq(introScript, outerScript); + assertEq(introScript.getOffsetLocation(source.introductionOffset).lineNumber, 1234); + }; +}; + +log = ''; +dbg.onDebuggerStatement = outerHandler; +g.evaluate('debugger; ["debugger;"].map(eval)', { lineNumber: 1234 }); +assertEq(log, 'oi'); + +log = ''; +dbg.onDebuggerStatement = outerHandler; +g.evaluate('debugger; "debugger;".replace(/.*/, eval);', + { lineNumber: 1234 }); +assertEq(log, 'oi'); + + +// If the call takes place in another global, however, we don't record the +// introduction script. +log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionScript, undefined); + assertEq(frame.script.source.introductionOffset, undefined); +}; +["debugger;"].map(g.eval); +"debugger;".replace(/.*/, g.eval); +assertEq(log, 'dd'); diff --git a/js/src/jit-test/tests/debug/Source-introductionScript-03.js b/js/src/jit-test/tests/debug/Source-introductionScript-03.js new file mode 100644 index 0000000000..7488309df1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-introductionScript-03.js @@ -0,0 +1,32 @@ +// We don't record introduction scripts in a different global from the +// introduced script, even if they're both debuggees. + +var dbg = new Debugger; + +var g1 = newGlobal({newCompartment: true}); +g1.g1 = g1; +var g1DO = dbg.addDebuggee(g1); + +var g2 = newGlobal({newCompartment: true}); +g2.g1 = g1; + +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionScript, undefined); + assertEq(frame.script.source.introductionOffset, undefined); +}; + +g2.eval('g1.eval("debugger;");'); +assertEq(log, 'd'); + +// Just for sanity: when it's not cross-global, we do note the introducer. +log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionScript instanceof Debugger.Script, true); + assertEq(typeof frame.script.source.introductionOffset, "number"); +}; +// Exactly as above, but with g1 instead of g2. +g1.eval('g1.eval("debugger;");'); +assertEq(log, 'd'); diff --git a/js/src/jit-test/tests/debug/Source-introductionType-data b/js/src/jit-test/tests/debug/Source-introductionType-data new file mode 100644 index 0000000000..eab7469213 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-introductionType-data @@ -0,0 +1 @@ +debugger; diff --git a/js/src/jit-test/tests/debug/Source-introductionType.js b/js/src/jit-test/tests/debug/Source-introductionType.js new file mode 100644 index 0000000000..36faf268cd --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-introductionType.js @@ -0,0 +1,119 @@ +// |jit-test| skip-if: helperThreadCount() === 0 + +// Check that scripts' introduction types are properly marked. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gDO = dbg.addDebuggee(g); +var log; + +// (Indirect) eval. +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, 'eval'); +}; +log = ''; +g.eval('debugger;'); +assertEq(log, 'd'); + +// Function constructor. +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, 'Function'); +}; +log = ''; +g.Function('debugger;')(); +assertEq(log, 'd'); + +// GeneratorFunction constructor. +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, 'GeneratorFunction'); +}; +log = ''; +g.eval('(function*() {})').constructor('debugger;')().next(); +assertEq(log, 'd'); + +// Shell 'evaluate' function +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, "js shell evaluate"); +}; +log = ''; +g.evaluate('debugger;'); +assertEq(log, 'd'); + +// Shell 'load' function +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, "js shell load"); +}; +log = ''; +g.load(scriptdir + 'Source-introductionType-data'); +assertEq(log, 'd'); + +// Shell 'run' function +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, "js shell run"); +}; +log = ''; +g.run(scriptdir + 'Source-introductionType-data'); +assertEq(log, 'd'); + +// Shell 'offThreadCompileScript' function. +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, "js shell offThreadCompileScript"); +}; +log = ''; +g.offThreadCompileScript('debugger;'); +g.runOffThreadScript(); +assertEq(log, 'd'); + +// Debugger.Frame.prototype.eval +dbg.onDebuggerStatement = function (frame) { + log += 'o'; + dbg.onDebuggerStatement = innerHandler; + frame.eval('debugger'); + function innerHandler(frame) { + log += 'i'; + assertEq(frame.script.source.introductionType, "debugger eval"); + } +}; +log = ''; +g.eval('debugger;'); +assertEq(log, 'oi'); + +// Debugger.Frame.prototype.evalWithBindings +dbg.onDebuggerStatement = function (frame) { + log += 'o'; + dbg.onDebuggerStatement = innerHandler; + frame.evalWithBindings('debugger', { x: 42 }); + function innerHandler(frame) { + log += 'i'; + assertEq(frame.script.source.introductionType, "debugger eval"); + } +}; +log = ''; +g.eval('debugger;'); +assertEq(log, 'oi'); + +// Debugger.Object.executeInGlobal +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, "debugger eval"); +}; +log = ''; +gDO.executeInGlobal('debugger;'); +assertEq(log, 'd'); + +// Debugger.Object.executeInGlobalWithBindings +dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.introductionType, "debugger eval"); +}; +log = ''; +gDO.executeInGlobalWithBindings('debugger;', { x: 42 }); +assertEq(log, 'd'); + diff --git a/js/src/jit-test/tests/debug/Source-invisible.js b/js/src/jit-test/tests/debug/Source-invisible.js new file mode 100644 index 0000000000..420b6faec8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-invisible.js @@ -0,0 +1,10 @@ +// Looking at ScriptSourceObjects in invisible-to-debugger compartments is okay. + +var gi = newGlobal({ newCompartment: true, invisibleToDebugger: true }); + +var gv = newGlobal({newCompartment: true}); +gi.cloneAndExecuteScript('function f() {}', gv); + +var dbg = new Debugger; +var gvw = dbg.addDebuggee(gv); +gvw.getOwnPropertyDescriptor('f').value.script.source; diff --git a/js/src/jit-test/tests/debug/Source-reparse.js b/js/src/jit-test/tests/debug/Source-reparse.js new file mode 100644 index 0000000000..3667001f3e --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-reparse.js @@ -0,0 +1,42 @@ +// reparsing a source should produce equivalent scripts and avoid invoking the +// onNewScript hook. + +const g = newGlobal({newCompartment: true}); +const dbg = new Debugger; +dbg.addDebuggee(g); + +let globalScript; +dbg.onNewScript = script => { globalScript = script }; + +g.evaluate(` +function f() { + for (var i = 0; i < 10; i++) { + g(); + } +} + +function g() { + return 3; +} + +f(); +`, { fileName: "foobar.js", lineNumber: 3 }); + +let onNewScriptCalls = 0; +dbg.onNewScript = script => { onNewScriptCalls++; }; + +const reparsedScript = globalScript.source.reparse(); + +assertEq(onNewScriptCalls, 0); + +assertEq(reparsedScript.url, "foobar.js"); +assertEq(reparsedScript.startLine, 3); + +// Test for the same breakpoint positions in the original and reparsed script. +function getBreakpointPositions(script) { + const offsets = script.getPossibleBreakpoints(); + const str = offsets.map(({ lineNumber, columnNumber }) => `${lineNumber}:${columnNumber}`).toString(); + const childPositions = script.getChildScripts().map(getBreakpointPositions); + return str + childPositions.toString(); +} +assertEq(getBreakpointPositions(globalScript), getBreakpointPositions(reparsedScript)); diff --git a/js/src/jit-test/tests/debug/Source-sourceMapURL-deprecated.js b/js/src/jit-test/tests/debug/Source-sourceMapURL-deprecated.js new file mode 100644 index 0000000000..f25f652a04 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-sourceMapURL-deprecated.js @@ -0,0 +1,82 @@ +// Source.prototype.sourceMapURL can be a string or null. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +function getSourceMapURL() { + let fw = gw.makeDebuggeeValue(g.f); + return fw.script.source.sourceMapURL; +} + +function setSourceMapURL(url) { + let fw = gw.makeDebuggeeValue(g.f); + fw.script.source.sourceMapURL = url; +} + +// Without a source map +g.evaluate("function f(x) { return 2*x; }"); +assertEq(getSourceMapURL(), null); + +// With a source map +g.evaluate("function f(x) { return 2*x; }", {sourceMapURL: 'file:///var/foo.js.map'}); +assertEq(getSourceMapURL(), 'file:///var/foo.js.map'); + +// Nested functions +let fired = false; +dbg.onDebuggerStatement = function (frame) { + fired = true; + assertEq(frame.script.source.sourceMapURL, 'file:///var/bar.js.map'); +}; +g.evaluate('(function () { (function () { debugger; })(); })();', + {sourceMapURL: 'file:///var/bar.js.map'}); +assertEq(fired, true); + +// Comment pragmas +g.evaluate('function f() {}\n' + + '//@ sourceMappingURL=file:///var/quux.js.map'); +assertEq(getSourceMapURL(), 'file:///var/quux.js.map'); + +g.evaluate('function f() {}\n' + + '/*//@ sourceMappingURL=file:///var/quux.js.map*/'); +assertEq(getSourceMapURL(), 'file:///var/quux.js.map'); + +g.evaluate('function f() {}\n' + + '/*\n' + + '//@ sourceMappingURL=file:///var/quux.js.map\n' + + '*/'); +assertEq(getSourceMapURL(), 'file:///var/quux.js.map'); + +// Spaces are disallowed by the URL spec (they should have been +// percent-encoded). +g.evaluate('function f() {}\n' + + '//@ sourceMappingURL=http://example.com/has illegal spaces.map'); +assertEq(getSourceMapURL(), 'http://example.com/has'); + +// When the URL is missing, we don't set the sourceMapURL and we don't skip the +// next line of input. +g.evaluate('function f() {}\n' + + '//@ sourceMappingURL=\n' + + 'function z() {}'); +assertEq(getSourceMapURL(), null); +assertEq('z' in g, true); + +// The last comment pragma we see should be the one which sets the source map's +// URL. +g.evaluate('function f() {}\n' + + '//@ sourceMappingURL=http://example.com/foo.js.map\n' + + '//@ sourceMappingURL=http://example.com/bar.js.map'); +assertEq(getSourceMapURL(), 'http://example.com/bar.js.map'); + +// With both a comment and the evaluate option. +g.evaluate('function f() {}\n' + + '//@ sourceMappingURL=http://example.com/foo.js.map', + {sourceMapURL: 'http://example.com/bar.js.map'}); +assertEq(getSourceMapURL(), 'http://example.com/foo.js.map'); + +// Make sure setting the sourceMapURL manually works +setSourceMapURL('baz.js.map'); +assertEq(getSourceMapURL(), 'baz.js.map'); + +setSourceMapURL(''); +assertEq(getSourceMapURL(), 'baz.js.map'); diff --git a/js/src/jit-test/tests/debug/Source-sourceMapURL-disable.js b/js/src/jit-test/tests/debug/Source-sourceMapURL-disable.js new file mode 100644 index 0000000000..0e103d7df3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-sourceMapURL-disable.js @@ -0,0 +1,14 @@ +// |jit-test| --no-source-pragmas + +const g = newGlobal({ newCompartment: true }); +const dbg = Debugger(g); +let source; +dbg.onDebuggerStatement = function (frame) { + source = frame.script.source; +}; + +g.eval(` + debugger; + //# sourceMappingURL=file.js +`); +assertEq(source.sourceMapURL, null); diff --git a/js/src/jit-test/tests/debug/Source-sourceMapURL.js b/js/src/jit-test/tests/debug/Source-sourceMapURL.js new file mode 100644 index 0000000000..449b44cebd --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-sourceMapURL.js @@ -0,0 +1,82 @@ +// Source.prototype.sourceMapURL can be a string or null. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +function getSourceMapURL() { + let fw = gw.makeDebuggeeValue(g.f); + return fw.script.source.sourceMapURL; +} + +function setSourceMapURL(url) { + let fw = gw.makeDebuggeeValue(g.f); + fw.script.source.sourceMapURL = url; +} + +// Without a source map +g.evaluate("function f(x) { return 2*x; }"); +assertEq(getSourceMapURL(), null); + +// With a source map +g.evaluate("function f(x) { return 2*x; }", {sourceMapURL: 'file:///var/foo.js.map'}); +assertEq(getSourceMapURL(), 'file:///var/foo.js.map'); + +// Nested functions +let fired = false; +dbg.onDebuggerStatement = function (frame) { + fired = true; + assertEq(frame.script.source.sourceMapURL, 'file:///var/bar.js.map'); +}; +g.evaluate('(function () { (function () { debugger; })(); })();', + {sourceMapURL: 'file:///var/bar.js.map'}); +assertEq(fired, true); + +// Comment pragmas +g.evaluate('function f() {}\n' + + '//# sourceMappingURL=file:///var/quux.js.map'); +assertEq(getSourceMapURL(), 'file:///var/quux.js.map'); + +g.evaluate('function f() {}\n' + + '/*//# sourceMappingURL=file:///var/quux.js.map*/'); +assertEq(getSourceMapURL(), 'file:///var/quux.js.map'); + +g.evaluate('function f() {}\n' + + '/*\n' + + '//# sourceMappingURL=file:///var/quux.js.map\n' + + '*/'); +assertEq(getSourceMapURL(), 'file:///var/quux.js.map'); + +// Spaces are disallowed by the URL spec (they should have been +// percent-encoded). +g.evaluate('function f() {}\n' + + '//# sourceMappingURL=http://example.com/has illegal spaces.map'); +assertEq(getSourceMapURL(), 'http://example.com/has'); + +// When the URL is missing, we don't set the sourceMapURL and we don't skip the +// next line of input. +g.evaluate('function f() {}\n' + + '//# sourceMappingURL=\n' + + 'function z() {}'); +assertEq(getSourceMapURL(), null); +assertEq('z' in g, true); + +// The last comment pragma we see should be the one which sets the source map's +// URL. +g.evaluate('function f() {}\n' + + '//# sourceMappingURL=http://example.com/foo.js.map\n' + + '//# sourceMappingURL=http://example.com/bar.js.map'); +assertEq(getSourceMapURL(), 'http://example.com/bar.js.map'); + +// With both a comment and the evaluate option. +g.evaluate('function f() {}\n' + + '//# sourceMappingURL=http://example.com/foo.js.map', + {sourceMapURL: 'http://example.com/bar.js.map'}); +assertEq(getSourceMapURL(), 'http://example.com/foo.js.map'); + +// Make sure setting the sourceMapURL manually works +setSourceMapURL('baz.js.map'); +assertEq(getSourceMapURL(), 'baz.js.map'); + +setSourceMapURL(''); +assertEq(getSourceMapURL(), 'baz.js.map'); diff --git a/js/src/jit-test/tests/debug/Source-startLine.js b/js/src/jit-test/tests/debug/Source-startLine.js new file mode 100644 index 0000000000..5f748f2cf2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-startLine.js @@ -0,0 +1,11 @@ +// Source.prototype.startLine reflects the start line supplied when parsing. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.evaluate("function f(x) {}"); +var fw = gw.getOwnPropertyDescriptor('f').value; +assertEq(fw.script.source.startLine, 1); +g.evaluate("function g(x) {}", {lineNumber: 10}); +var gw = gw.getOwnPropertyDescriptor('g').value; +assertEq(gw.script.source.startLine, 10); diff --git a/js/src/jit-test/tests/debug/Source-surfaces.js b/js/src/jit-test/tests/debug/Source-surfaces.js new file mode 100644 index 0000000000..f2b3e81d9d --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-surfaces.js @@ -0,0 +1,33 @@ +// Debugger.Source.prototype + +load(libdir + 'asserts.js'); + +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.text.call(42) +}, TypeError); +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.text.call({}) +}, TypeError); +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.text.call(Debugger.Source.prototype) +}, TypeError); + +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.element.call(42) +}, TypeError); +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.element.call({}) +}, TypeError); +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.element.call(Debugger.Source.prototype) +}, TypeError); + +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.elementAttributeName.call(42) +}, TypeError); +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.elementAttributeName.call({}) +}, TypeError); +assertThrowsInstanceOf(function () { + Debugger.Source.prototype.elementAttributeName.call(Debugger.Source.prototype) +}, TypeError); diff --git a/js/src/jit-test/tests/debug/Source-text-01.js b/js/src/jit-test/tests/debug/Source-text-01.js new file mode 100644 index 0000000000..799ccf36e0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-text-01.js @@ -0,0 +1,26 @@ +/* + * Debugger.Source.prototype.text should return a string. Moreover, it + * should be the same string for each child script sharing that + * Debugger.Source. + */ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +var count = 0; +dbg.onNewScript = function (script) { + var text = script.source.text; + assertEq(typeof text, "string"); + function traverse(script) { + ++count; + script.getChildScripts().forEach(function (script) { + assertEq(script.source.text, text); + traverse(script); + }); + }; + traverse(script); +} + +g.eval("2 * 3"); +g.eval("function f() {}"); +g.eval("function f() { function g() {} }"); +assertEq(count, 6); diff --git a/js/src/jit-test/tests/debug/Source-text-02.js b/js/src/jit-test/tests/debug/Source-text-02.js new file mode 100644 index 0000000000..45f0b9660b --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-text-02.js @@ -0,0 +1,20 @@ +// Nested compilation units (say, an eval with in an eval) should have the +// correct sources attributed to them. +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +var text; +var count = 0; +dbg.onNewScript = function (script) { + ++count; + if (count % 2 == 0) + assertEq(script.source.text, text); +} + +g.eval("eval('" + (text = "") + "')"); +g.eval("eval('" + (text = "2 * 3") + "')"); +g.eval("new Function('" + (text = "") + "')"); +g.eval("new Function('" + (text = "2 * 3") + "')"); +evaluate("", { global: g }); +evaluate("2 * 3", { global: g }); +assertEq(count, 10); diff --git a/js/src/jit-test/tests/debug/Source-text-lazy.js b/js/src/jit-test/tests/debug/Source-text-lazy.js new file mode 100644 index 0000000000..b6bf3e2423 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-text-lazy.js @@ -0,0 +1,38 @@ +// |jit-test| skip-if: typeof withSourceHook !== 'function' +// withSourceHook isn't defined if you pass the shell the --fuzzing-safe +// option. Skip this test silently, to avoid spurious failures. + +/* + * Debugger.Source.prototype.text should correctly retrieve the source for + * code compiled with CompileOptions::LAZY_SOURCE. + */ + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +function test(source) { + // To ensure that we're getting the value the source hook returns, make + // it differ from the actual source. + let frobbed = source.replace(/debugger/, 'reggubed'); + let log = ''; + + withSourceHook(function (url) { + log += 's'; + assertEq(url, "BanalBivalve.jsm"); + return frobbed; + }, () => { + dbg.onDebuggerStatement = function (frame) { + log += 'd'; + assertEq(frame.script.source.text, frobbed); + } + + g.evaluate(source, { fileName: "BanalBivalve.jsm", + sourceIsLazy: true }); + }); + + assertEq(log, 'ds'); +} + +test("debugger; // Ignominious Iguana"); +test("(function () { debugger; /* Meretricious Marmoset */})();"); +test("(() => { debugger; })(); // Gaunt Gibbon"); diff --git a/js/src/jit-test/tests/debug/Source-url-01.js b/js/src/jit-test/tests/debug/Source-url-01.js new file mode 100644 index 0000000000..4a020aa756 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-url-01.js @@ -0,0 +1,10 @@ +// Source.prototype.url returns a synthesized URL for eval code. + +var g = newGlobal({newCompartment: true}); +g.eval('function double() { return 2*x }'); + +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +var fw = gw.getOwnPropertyDescriptor('double').value; +assertEq(!!fw.script.source.url.match(/Source-url-01.js line [0-9]+ > eval/), true); diff --git a/js/src/jit-test/tests/debug/Source-url-02.js b/js/src/jit-test/tests/debug/Source-url-02.js new file mode 100644 index 0000000000..9d20155b59 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-url-02.js @@ -0,0 +1,11 @@ +// Source.prototype.url returns a synthesized URL for Function code. + +var g = newGlobal({newCompartment: true}); +g.eval('var double = Function("x", "return 2*x;");'); + +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +var fw = gw.getOwnPropertyDescriptor('double').value; +print(fw.script.source.url); +assertEq(!!fw.script.source.url.match(/Source-url-02.js .* > Function/), true); diff --git a/js/src/jit-test/tests/debug/Source-url.js b/js/src/jit-test/tests/debug/Source-url.js new file mode 100644 index 0000000000..b37f609cf6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Source-url.js @@ -0,0 +1,10 @@ +// Source.prototype.url can be a string or null. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +for (var fileName of ['file:///var/foo.js', null]) { + g.evaluate("function f(x) { return 2*x; }", {fileName: fileName}); + var fw = gw.getOwnPropertyDescriptor('f').value; + assertEq(fw.script.source.url, fileName); +} diff --git a/js/src/jit-test/tests/debug/breakpoint-01.js b/js/src/jit-test/tests/debug/breakpoint-01.js new file mode 100644 index 0000000000..3adde6b977 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-01.js @@ -0,0 +1,22 @@ +// Basic breakpoint test. + +var g = newGlobal({newCompartment: true}); +g.s = ''; +var handler = { + hit: function (frame) { + assertEq(this, handler); + g.s += '1'; + } +}; +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + g.s += '0'; + var line0 = frame.script.getOffsetLocation(frame.offset).lineNumber; + var offs = frame.script.getLineOffsets(line0 + 2); + for (var i = 0; i < offs.length; i++) + frame.script.setBreakpoint(offs[i], handler); +}; +g.eval("debugger;\n" + + "s += 'a';\n" + // line0 + 1 + "s += 'b';\n"); // line0 + 2 +assertEq(g.s, "0a1b"); diff --git a/js/src/jit-test/tests/debug/breakpoint-02.js b/js/src/jit-test/tests/debug/breakpoint-02.js new file mode 100644 index 0000000000..45dd420515 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-02.js @@ -0,0 +1,15 @@ +// Setting a breakpoint in a non-debuggee Script is an error. + +load(libdir + "asserts.js"); + +var g1 = newGlobal({newCompartment: true}); +var g2 = g1.eval("newGlobal('same-compartment')"); +g2.eval("function f() { return 2; }"); +g1.f = g2.f; + +var dbg = Debugger(g1); +var s; +dbg.onDebuggerStatement = function (frame) { s = frame.eval("f").return.script; }; +g1.eval("debugger;"); + +assertThrowsInstanceOf(function () { s.setBreakpoint(0, {}); }, Error); diff --git a/js/src/jit-test/tests/debug/breakpoint-03.js b/js/src/jit-test/tests/debug/breakpoint-03.js new file mode 100644 index 0000000000..ce582cb214 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-03.js @@ -0,0 +1,16 @@ +// Setting a breakpoint in a script we are no longer debugging is an error. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(); +var gobj = dbg.addDebuggee(g); +g.eval("function f() { return 2; }"); + +var s; +dbg.onDebuggerStatement = function (frame) { s = frame.eval("f").return.script; }; +g.eval("debugger;"); +s.setBreakpoint(0, {}); // ok + +dbg.removeDebuggee(gobj); +assertThrowsInstanceOf(function () { s.setBreakpoint(0, {}); }, Error); diff --git a/js/src/jit-test/tests/debug/breakpoint-04.js b/js/src/jit-test/tests/debug/breakpoint-04.js new file mode 100644 index 0000000000..f5c0c957b3 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-04.js @@ -0,0 +1,30 @@ +// Hitting a breakpoint with no hit method does nothing. + +var g = newGlobal({newCompartment: true}); +g.s = ''; +g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " debugger;\n" + // line0 + 2 + " s += 'x';\n" + // line0 + 3 + "}\n") +var dbg = Debugger(g); +var bp = []; +dbg.onDebuggerStatement = function (frame) { + g.s += 'D'; + var arr = frame.script.getLineOffsets(g.line0 + 3); + for (var i = 0; i < arr.length; i++) { + var obj = {}; + bp[i] = obj; + frame.script.setBreakpoint(arr[i], obj); + } +}; + +g.f(); +assertEq(g.s, "Dx"); + +dbg.onDebuggerStatement = undefined; + +for (var i = 0; i < bp.length; i++) + bp[i].hit = function () { g.s += 'B'; }; +g.f(); +assertEq(g.s, "DxBx"); diff --git a/js/src/jit-test/tests/debug/breakpoint-05.js b/js/src/jit-test/tests/debug/breakpoint-05.js new file mode 100644 index 0000000000..61ae53eb4e --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-05.js @@ -0,0 +1,19 @@ +// If the offset parameter to setBreakpoint is invalid, throw an error. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + // We assume at least one offset between 0 and frame.offset is invalid. + assertThrowsInstanceOf( + function () { + for (var i = 0; i < frame.offset; i++) + frame.script.setBreakpoint(i, {}); + }, + Error); + hits++; +}; +g.eval("x = 256; debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/breakpoint-06.js b/js/src/jit-test/tests/debug/breakpoint-06.js new file mode 100644 index 0000000000..897220d40a --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-06.js @@ -0,0 +1,20 @@ +// The argument to a breakpoint hit method is a frame. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame1) { + function hit(frame2) { + assertEq(frame2, frame1); + hits++; + } + var s = frame1.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], {hit: hit}); +}; +g.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "x = 1;\n"); // line0 + 2 +assertEq(hits, 1); +assertEq(g.x, 1); diff --git a/js/src/jit-test/tests/debug/breakpoint-07.js b/js/src/jit-test/tests/debug/breakpoint-07.js new file mode 100644 index 0000000000..01ff27a339 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-07.js @@ -0,0 +1,30 @@ +// Code runs fine if do-nothing breakpoints are set on every line. + +var g = newGlobal({newCompartment: true}); +var src = ("var line0 = Error().lineNumber;\n" + + "function gcd(a, b) {\n" + // line0 + 1 + " if (a > b)\n" + // line0 + 2 + " return gcd(b, a);\n" + // line0 + 3 + " var c = b % a;\n" + // line0 + 4 + " if (c === 0)\n" + // line0 + 5 + " return a;\n" + // line0 + 6 + " return gcd(c, a);\n" + // line0 + 7 + "}\n"); // line0 + 8 +g.eval(src); + +var dbg = Debugger(g); +var hits = 0 ; +dbg.onDebuggerStatement = function (frame) { + var s = frame.eval("gcd").return.script; + var offs; + for (var lineno = g.line0 + 2; (offs = s.getLineOffsets(lineno)).length > 0; lineno++) { + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], {hit: function (f) { hits++; }}); + } + assertEq(lineno > g.line0 + 7, true); + assertEq(lineno <= g.line0 + 9, true); +}; + +g.eval("debugger;"); +assertEq(g.gcd(31 * 7 * 5 * 3 * 2, 11 * 3 * 3 * 2), 6); +assertEq(hits >= 18, true); diff --git a/js/src/jit-test/tests/debug/breakpoint-08.js b/js/src/jit-test/tests/debug/breakpoint-08.js new file mode 100644 index 0000000000..ae0e905503 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-08.js @@ -0,0 +1,31 @@ +// Breakpoints are dropped from eval scripts when they finish executing. +// (The eval cache doesn't cache breakpoints.) + +var g = newGlobal({newCompartment: true}); + +g.line0 = undefined; +g.eval("function f() {\n" + + " return eval(s);\n" + + "}\n"); +g.s = ("line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "result = 'ok';\n"); // line0 + 2 + +var dbg = Debugger(g); +var hits = 0, bphits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.type, 'eval'); + assertEq(frame.script.getBreakpoints().length, 0); + var h = {hit: function (frame) { bphits++; }}; + var offs = frame.script.getLineOffsets(g.line0 + 2); + for (var i = 0; i < offs.length; i++) + frame.script.setBreakpoint(offs[i], h); + hits++; +}; + +for (var i = 0; i < 3; i++) { + assertEq(g.f(), 'ok'); + assertEq(g.result, 'ok'); +} +assertEq(hits, 3); +assertEq(bphits, 3); diff --git a/js/src/jit-test/tests/debug/breakpoint-09.js b/js/src/jit-test/tests/debug/breakpoint-09.js new file mode 100644 index 0000000000..45aee69985 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-09.js @@ -0,0 +1,13 @@ +// Setting a breakpoint in an eval script that is not on the stack. Bug 746973. +// We don't assert that the breakpoint actually hits because that depends on +// the eval cache, an implementation detail. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +g.eval("function f() { return eval('2+2'); }"); +var s; +dbg.onNewScript = function (script) { s = script; }; +g.f(); +for (var offset of s.getLineOffsets(s.startLine)) + s.setBreakpoint(offset, {hit: function () {}}); +assertEq(g.f(), 4); diff --git a/js/src/jit-test/tests/debug/breakpoint-10.js b/js/src/jit-test/tests/debug/breakpoint-10.js new file mode 100644 index 0000000000..65665ae173 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-10.js @@ -0,0 +1,19 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var fscript = null; +dbg.onNewScript = function(script) { + dbg.onNewScript = undefined; + fscript = script.getChildScripts()[0]; +} + +g.eval("function f(x) { arguments[0] = 3; return x }"); +assertEq(fscript !== null, true); + +fscript.setBreakpoint(0, {hit:function(frame) { + assertEq(frame.eval('x').return, 1); + assertEq(frame.arguments[0], 1); + return {return:42}; +}}); + +assertEq(g.f(1), 42); diff --git a/js/src/jit-test/tests/debug/breakpoint-11.js b/js/src/jit-test/tests/debug/breakpoint-11.js new file mode 100644 index 0000000000..4dce2e95b4 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-11.js @@ -0,0 +1,38 @@ +// Setting a breakpoint in a generator function works, and we can +// traverse the stack and evaluate expressions in the context of older +// generator frames. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + function hit(frame) { + frame.older.eval("q += 16"); + } + + var s = frame.script; + var offs = s.getLineOffsets(g.line0 + 9); + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], {hit: hit}); +}; + +g.eval("line0 = Error().lineNumber;\n" + + "function* g(x) {\n" + // + 1 + " var q = 10;\n" + // + 2 + " yield* x;\n" + // + 3 + " return q;\n" + // + 4 + "}\n" + // + 5 + "function* range(n) {\n" + // + 6 + " debugger;\n" + // + 7 + " for (var i = 0; i < n; i++)\n" + // + 8 + " yield i;\n" + // + 9 <-- breakpoint + " return;\n" + // so that line 9 only has the yield + "}"); + +g.eval("var iter = g(range(2))"); +g.eval("var first = iter.next().value"); +g.eval("var second = iter.next().value"); +g.eval("var third = iter.next().value"); + +assertEq(g.first, 0); +assertEq(g.second, 1); +assertEq(g.third, 42); diff --git a/js/src/jit-test/tests/debug/breakpoint-12.js b/js/src/jit-test/tests/debug/breakpoint-12.js new file mode 100644 index 0000000000..96770c8f26 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-12.js @@ -0,0 +1,78 @@ +// Removing a global as a debuggee forgets all its breakpoints. + + +var dbgA = new Debugger; +var logA = ''; + +var dbgB = new Debugger; +var logB = ''; + +var g1 = newGlobal({newCompartment: true}); +g1.eval('function g1f() { print("Weltuntergang"); }'); +var DOAg1 = dbgA.addDebuggee(g1); +var DOAg1f = DOAg1.getOwnPropertyDescriptor('g1f').value; +DOAg1f.script.setBreakpoint(0, { hit: () => { logA += '1'; } }); + +var DOBg1 = dbgB.addDebuggee(g1); +var DOBg1f = DOBg1.getOwnPropertyDescriptor('g1f').value; +DOBg1f.script.setBreakpoint(0, { hit: () => { logB += '1'; } }); + + +var g2 = newGlobal({newCompartment: true}); +g2.eval('function g2f() { print("Mokushi"); }'); + +var DOAg2 = dbgA.addDebuggee(g2); +var DOAg2f = DOAg2.getOwnPropertyDescriptor('g2f').value; +DOAg2f.script.setBreakpoint(0, { hit: () => { logA += '2'; } }); + +var DOBg2 = dbgB.addDebuggee(g2); +var DOBg2f = DOBg2.getOwnPropertyDescriptor('g2f').value; +DOBg2f.script.setBreakpoint(0, { hit: () => { logB += '2'; } }); + +assertEq(logA, ''); +assertEq(logB, ''); +g1.g1f(); +g2.g2f(); +assertEq(logA, '12'); +assertEq(logB, '12'); +logA = logB = ''; + +// Removing a global as a debuggee should make its breakpoint not hit. +dbgA.removeDebuggee(g2); +dbgB.removeDebuggee(g1); +assertEq(logA, ''); +assertEq(logB, ''); +g1.g1f(); +g2.g2f(); +assertEq(logA, '1'); +assertEq(logB, '2'); +logA = logB = ''; + +// Adding the global back as a debuggee should not resurrect its breakpoints. +dbgA.addDebuggee(g2); +dbgB.addDebuggee(g1); +assertEq(logA, ''); +assertEq(logB, ''); +g1.g1f(); +g2.g2f(); +assertEq(logA, '1'); +assertEq(logB, '2'); +logA = logB = ''; + +// But, we can set them again, and it all works. +DOAg2f.script.setBreakpoint(0, { hit: () => { logA += '2'; } }); +assertEq(logA, ''); +assertEq(logB, ''); +g2.g2f(); +g1.g1f(); +assertEq(logA, '21'); +assertEq(logB, '2'); +logA = logB = ''; + +DOBg1f.script.setBreakpoint(0, { hit: () => { logB += '1'; } }); +assertEq(logA, ''); +assertEq(logB, ''); +g2.g2f(); +g1.g1f(); +assertEq(logA, '21'); +assertEq(logB, '21'); diff --git a/js/src/jit-test/tests/debug/breakpoint-13.js b/js/src/jit-test/tests/debug/breakpoint-13.js new file mode 100644 index 0000000000..d839195ce4 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-13.js @@ -0,0 +1,13 @@ +// Breakpoints should be hit on scripts gotten not via Debugger.Frame. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(x) { return x + 1; }"); +// Warm up so f gets OSRed into the jits. +g.eval("for (var i = 0; i < 10000; i++) f(i);"); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var fw = gw.getOwnPropertyDescriptor("f").value; +var hits = 0; +fw.script.setBreakpoint(0, { hit: function(frame) { hits++; } }); +g.eval("f(42);"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/breakpoint-14.js b/js/src/jit-test/tests/debug/breakpoint-14.js new file mode 100644 index 0000000000..48ee53594d --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-14.js @@ -0,0 +1,14 @@ +// Breakpoints should be hit on scripts gotten not via Debugger.Frame. + +var g = newGlobal({newCompartment: true}); +g.eval("function f(x) { return x + 1; }"); +g.eval("function g(x) { f(x); }"); +// Warm up so f gets OSRed into the jits and g inlines f. +g.eval("for (var i = 0; i < 10000; i++) g(i);"); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var fw = gw.getOwnPropertyDescriptor("f").value; +var hits = 0; +fw.script.setBreakpoint(0, { hit: function(frame) { hits++; } }); +g.eval("g(42);"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/breakpoint-gc-01.js b/js/src/jit-test/tests/debug/breakpoint-gc-01.js new file mode 100644 index 0000000000..2cbfe66fb4 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-gc-01.js @@ -0,0 +1,25 @@ +// Handlers for breakpoints in an eval script are live as long as the script is on the stack. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + function handler(i) { + return {hit: function () { log += '' + i; }}; + } + + var s = frame.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var i = 0; i < 7; i++) { + var h = handler(i); + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], h); + } + gc(); +}; + + +g.eval("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "x = 1;\n"); // line0 + 2 +assertEq(log, '0123456'); diff --git a/js/src/jit-test/tests/debug/breakpoint-gc-02.js b/js/src/jit-test/tests/debug/breakpoint-gc-02.js new file mode 100644 index 0000000000..89c447ae67 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-gc-02.js @@ -0,0 +1,28 @@ +// A Debugger with live breakpoints is live. + +var g = newGlobal({newCompartment: true}); +g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " return 2;\n" + // line0 + 2 + "}\n"); + +var N = 4; +var hits = 0; +for (var i = 0; i < N; i++) { + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + var handler = {hit: function () { hits++; }}; + var s = frame.eval("f").return.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], handler); + }; + g.eval('debugger;'); + dbg.onDebuggerStatement = undefined; + dbg = null; +} + +gc(); + +assertEq(g.f(), 2); +assertEq(hits, N); diff --git a/js/src/jit-test/tests/debug/breakpoint-gc-04.js b/js/src/jit-test/tests/debug/breakpoint-gc-04.js new file mode 100644 index 0000000000..38c7d14975 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-gc-04.js @@ -0,0 +1,23 @@ +// Enabled debuggers keep breakpoint handlers alive. + +var g = newGlobal({newCompartment: true}); +g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " return 2;\n" + // line0 + 2 + "}\n"); +var N = 4; +var hits = 0; +for (var i = 0; i < N; i++) { + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + var handler = {hit: function () { hits++; }}; + var s = frame.eval("f").return.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], handler); + }; +} +g.eval('debugger;'); +gc({}); +assertEq(g.f(), 2); +assertEq(hits, N); diff --git a/js/src/jit-test/tests/debug/breakpoint-gc-05.js b/js/src/jit-test/tests/debug/breakpoint-gc-05.js new file mode 100644 index 0000000000..41e4e2aaaf --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-gc-05.js @@ -0,0 +1,25 @@ +// Disabled debuggers keep breakpoint handlers alive. + +var g = newGlobal({newCompartment: true}); +g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " return 2;\n" + // line0 + 2 + "}\n"); +var N = 4; +var hits = 0; +for (var i = 0; i < N; i++) { + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + var handler = {hit: function () { hits++; }}; + var s = frame.eval("f").return.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], handler); + }; +} +g.eval('debugger;'); +dbg.enabled = false; +gc({}); +dbg.enabled = true; +assertEq(g.f(), 2); +assertEq(hits, N); diff --git a/js/src/jit-test/tests/debug/breakpoint-multi-01.js b/js/src/jit-test/tests/debug/breakpoint-multi-01.js new file mode 100644 index 0000000000..88f52aedf5 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-multi-01.js @@ -0,0 +1,28 @@ +// A single Debugger object can set multiple breakpoints at an instruction. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'D'; + function handler(i) { + return {hit: function (frame) { log += '' + i; }}; + } + var f = frame.eval("f").return; + var s = f.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var i = 0; i < 10; i++) { + var bp = handler(i); + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], bp); + } + assertEq(f.call().return, 42); + log += 'X'; +}; + +g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " return 42;\n" + // line0 + 2 + "}\n" + + "debugger;\n"); +assertEq(log, 'D0123456789X'); diff --git a/js/src/jit-test/tests/debug/breakpoint-multi-02.js b/js/src/jit-test/tests/debug/breakpoint-multi-02.js new file mode 100644 index 0000000000..b8d9b51d95 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-multi-02.js @@ -0,0 +1,42 @@ +// After clearing one breakpoint, another breakpoint at the same instruction still works. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var script = null; +var handlers = []; +dbg.onDebuggerStatement = function (frame) { + function handler(i) { + return {hit: function (frame) { g.log += '' + i; }}; + } + var f = frame.eval("f").return; + var s = f.script; + if (script === null) + script = s; + else + assertEq(s, script); + + var offs = s.getLineOffsets(g.line0 + 3); + for (var i = 0; i < 3; i++) { + handlers[i] = handler(i); + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], handlers[i]); + } +}; + +g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " log += 'x';\n" + // line0 + 2 + " log += 'y';\n" + // line0 + 3 + "}\n" + + "debugger;\n"); +assertEq(handlers.length, 3); + +g.log = ''; +g.f(); +assertEq(g.log, 'x012y'); + +script.clearBreakpoint(handlers[0]); + +g.log = ''; +g.f(); +assertEq(g.log, 'x12y'); diff --git a/js/src/jit-test/tests/debug/breakpoint-multi-03.js b/js/src/jit-test/tests/debug/breakpoint-multi-03.js new file mode 100644 index 0000000000..930123bbc6 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-multi-03.js @@ -0,0 +1,27 @@ +// Multiple Debugger objects can set breakpoints at the same instruction. + +var g = newGlobal({newCompartment: true}); +function attach(g, i) { + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + var s = frame.eval("f").return.script; + var offs = s.getLineOffsets(g.line0 + 3); + for (var j = 0; j < offs.length; j++) + s.setBreakpoint(offs[j], {hit: function () { g.log += "" + i; }}); + }; +} + +g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " log += 'a';\n" + // line0 + 2 + " log += 'b';\n" + // line0 + 3 + "}\n"); + +for (var i = 0; i < 3; i++) + attach(g, i); + +g.log = ''; +g.eval('debugger;'); +g.log += 'x'; +g.f(); +assertEq(g.log, 'xa012b'); diff --git a/js/src/jit-test/tests/debug/breakpoint-multi-04.js b/js/src/jit-test/tests/debug/breakpoint-multi-04.js new file mode 100644 index 0000000000..fa407a0e17 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-multi-04.js @@ -0,0 +1,48 @@ +// After clearing one breakpoint, another breakpoint from a different Debugger object at the same instruction still works. + +function test(which) { + var g = newGlobal({newCompartment: true}); + g.eval("var line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " return " + which + ";\n" + // line0 + 2 + "}\n"); + + var log; + var scripts = []; + var handlers = []; + function addDebugger(g, i) { + var dbg = Debugger(g); + dbg.onDebuggerStatement = function (frame) { + var s = frame.eval("f").return.script; + scripts[i] = s; + var offs = s.getLineOffsets(g.line0 + 2); + var handler = {hit: function (frame) { log += '' + i; } }; + s.setBreakpoint(0, handler); + handlers[i] = handler; + }; + } + + var expected = ''; + for (var i = 0; i < 3; i++) { + addDebugger(g, i); + if (i !== which) + expected += '' + i; + } + g.eval('debugger;'); + + for (var i = 0; i < 3; i++) + assertEq(scripts[i].getBreakpoints()[0], handlers[i]); + + log = ''; + g.f(); + assertEq(log, '012'); + + scripts[which].clearAllBreakpoints(); + + log = ''; + g.f(); + assertEq(log, expected); +} + +for (var j = 0; j < 3; j++) + test(j); diff --git a/js/src/jit-test/tests/debug/breakpoint-noncng.js b/js/src/jit-test/tests/debug/breakpoint-noncng.js new file mode 100644 index 0000000000..2ce779cbd7 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-noncng.js @@ -0,0 +1,20 @@ +// Breakpoints work in non-compile-and-go code. Bug 738479. + +var g = newGlobal({newCompartment: true}); +g.s = ''; +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.evaluate( + "function f() {\n" + // fscript.startLine + " s += 'a';\n" + // fscript.startLine + 1 + " s += 'b';\n" + // fscript.startLine + 2 + "}\n"); + +var fscript = gw.makeDebuggeeValue(g.f).script; +var handler = {hit: function (frame) { g.s += '1'; }}; +for (var pc of fscript.getLineOffsets(fscript.startLine + 2)) + fscript.setBreakpoint(pc, handler); + +g.f(); + +assertEq(g.s, "a1b"); diff --git a/js/src/jit-test/tests/debug/breakpoint-oom-01.js b/js/src/jit-test/tests/debug/breakpoint-oom-01.js new file mode 100644 index 0000000000..4fd709ccd0 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-oom-01.js @@ -0,0 +1,41 @@ +// |jit-test| skip-if: !('oomTest' in this) + +// Test for OOM hitting a breakpoint in a generator. +// +// (The purpose is to test OOM-handling in the code that creates the +// Debugger.Frame object and associates it with the generator object.) + +let g = newGlobal({newCompartment: true}); +g.eval(`\ + function* gen(x) { // line 1 + x++; // 2 + yield x; // 3 + } // 4 +`); + +let dbg = new Debugger; + +// On OOM in the debugger, propagate it to the debuggee. +dbg.uncaughtExceptionHook = exc => exc === "out of memory" ? {throw: exc} : null; + +let gw = dbg.addDebuggee(g); +let script = gw.makeDebuggeeValue(g.gen).script; +let hits = 0; +let handler = { + hit(frame) { + hits++; + print("x=", frame.environment.getVariable("x")); + } +}; +for (let offset of script.getLineOffsets(2)) + script.setBreakpoint(offset, handler); + +let result; +oomTest(() => { + hits = 0; + result = g.gen(1).next(); +}, false); +assertEq(hits, 1); +assertEq(result.done, false); +assertEq(result.value, 2); + diff --git a/js/src/jit-test/tests/debug/breakpoint-resume-01.js b/js/src/jit-test/tests/debug/breakpoint-resume-01.js new file mode 100644 index 0000000000..ce73b53810 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-resume-01.js @@ -0,0 +1,25 @@ +// A breakpoint handler hit method can return {throw: exc} to throw an exception. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + function hit(frame) { + return {throw: frame.eval("new Error('PASS')").return}; + } + + var s = frame.script; + var offs = s.getLineOffsets(g.line0 + 3); + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], {hit: hit}); +}; + +g.s = null; +g.eval("line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "try {\n" + // line0 + 2 + " s = 'FAIL';\n" + // line0 + 3 + "} catch (exc) {\n" + + " assertEq(exc.constructor, Error);\n" + + " s = exc.message;\n" + + "}\n"); +assertEq(g.s, 'PASS'); diff --git a/js/src/jit-test/tests/debug/breakpoint-resume-02.js b/js/src/jit-test/tests/debug/breakpoint-resume-02.js new file mode 100644 index 0000000000..966851fa3d --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-resume-02.js @@ -0,0 +1,34 @@ +// A breakpoint handler hit method can return null to raise an uncatchable error. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + g.log += 'D'; + + function hit(frame) { + g.log += 'H'; + return null; + } + + var f = frame.eval("f").return; + var s = f.script; + var offs = s.getLineOffsets(g.line0 + 3); + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], {hit: hit}); + + var rv = f.call(); + assertEq(rv, null); + g.log += 'X'; +}; + +g.log = ''; +g.eval("line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " try {\n" + // line0 + 2 + " log += '3';\n" + // line0 + 3 + " } catch (exc) {\n" + + " log += '5';\n" + + " }\n" + + "}\n" + + "debugger;\n"); +assertEq(g.log, 'DHX'); diff --git a/js/src/jit-test/tests/debug/breakpoint-resume-03.js b/js/src/jit-test/tests/debug/breakpoint-resume-03.js new file mode 100644 index 0000000000..84222bf198 --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-resume-03.js @@ -0,0 +1,30 @@ +// A breakpoint handler hit method can return {return: val} to force the frame to return. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (frame) { + g.log += 'D'; + + function hit(frame) { + g.log += 'H'; + return {return: 'ok'}; + } + + var f = frame.eval("f").return; + var s = f.script; + var offs = s.getLineOffsets(g.line0 + 2); + for (var i = 0; i < offs.length; i++) + s.setBreakpoint(offs[i], {hit: hit}); + + var rv = f.call(); + assertEq(rv.return, 'ok'); + g.log += 'X'; +}; + +g.log = ''; +g.eval("line0 = Error().lineNumber;\n" + + "function f() {\n" + // line0 + 1 + " log += '2';\n" + // line0 + 2 + "}\n" + + "debugger;\n"); +assertEq(g.log, 'DHX'); diff --git a/js/src/jit-test/tests/debug/breakpoint-resume-04.js b/js/src/jit-test/tests/debug/breakpoint-resume-04.js new file mode 100644 index 0000000000..4fed89170f --- /dev/null +++ b/js/src/jit-test/tests/debug/breakpoint-resume-04.js @@ -0,0 +1,37 @@ +// |jit-test| error:all-jobs-completed-successfully +// Verifiy that a breakpoints force-return queues the promise microtask to run +// in the debuggee's job queue, not the debugger's +// AutoDebuggerJobQueueInterruption. + +let g = newGlobal({ newCompartment: true }); +g.eval(` + async function asyncFn(x) { + await Promise.resolve(); + debugger; + } + function enterDebuggee(){} +`); +const dbg = new Debugger(g); + +(async function() { + let it = g.asyncFn(); + + // Force-return when the await resumes and steps. + dbg.onEnterFrame = frame => { + dbg.onEnterFrame = undefined; + const bps = frame.script.getPossibleBreakpoints({ line: 4 }); + assertEq(bps.length, 1); + frame.script.setBreakpoint(bps[0].offset, { + hit: () => ({ return: "exit" }) + }); + }; + + const result = await it; + assertEq(result, "exit"); + // If execution here is resumed from the debugger's queue, this call will + // trigger DebuggeeWouldRun exception. + g.enterDebuggee(); + + + throw "all-jobs-completed-successfully"; +})(); diff --git a/js/src/jit-test/tests/debug/bug-1102549.js b/js/src/jit-test/tests/debug/bug-1102549.js new file mode 100644 index 0000000000..fbe38d9ae4 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1102549.js @@ -0,0 +1,9 @@ +// |jit-test| error: log is not defined + +if (!this.Promise) + throw new Error('log is not defined'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onPromiseSettled = function (g) { log += 's'; throw "foopy"; }; +g.settlePromiseNow(new g.Promise(function (){})); diff --git a/js/src/jit-test/tests/debug/bug-1103386.js b/js/src/jit-test/tests/debug/bug-1103386.js new file mode 100644 index 0000000000..3db8e10a7b --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1103386.js @@ -0,0 +1,10 @@ +load(libdir + "immutable-prototype.js"); + +// Random chosen test: js/src/jit-test/tests/auto-regress/bug700295.js +if (globalPrototypeChainIsMutable()) { + __proto__ = null; + Object.prototype.__proto__ = this; +} + +// Random chosen test: js/src/jit-test/tests/debug/Memory-takeCensus-05.js +Debugger(newGlobal({newCompartment: true})).memory.takeCensus(); diff --git a/js/src/jit-test/tests/debug/bug-1103813.js b/js/src/jit-test/tests/debug/bug-1103813.js new file mode 100644 index 0000000000..0e060b32d8 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1103813.js @@ -0,0 +1,7 @@ +// Random chosen test: js/src/jit-test/tests/debug/Source-invisible.js +newGlobal({ + newCompartment: true, + invisibleToDebugger: true +}) +// Random chosen test: js/src/jit-test/tests/debug/Debugger-findObjects-05.js +x = (new Debugger).findObjects() diff --git a/js/src/jit-test/tests/debug/bug-1103817.js b/js/src/jit-test/tests/debug/bug-1103817.js new file mode 100644 index 0000000000..f571b6f603 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1103817.js @@ -0,0 +1,5 @@ +// Random chosen test: js/src/jit-test/tests/debug/Source-introductionScript-04.js +x = (new Debugger).addDebuggee(newGlobal({newCompartment: true})); +print(x.getOwnPropertyDescriptor('Function').value.proto.script); +// Random chosen test: js/src/jit-test/tests/debug/Memory-takeCensus-03.js +(new Debugger).memory.takeCensus(); diff --git a/js/src/jit-test/tests/debug/bug-1110327.js b/js/src/jit-test/tests/debug/bug-1110327.js new file mode 100644 index 0000000000..e45d88884f --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1110327.js @@ -0,0 +1,5 @@ +// Randomly chosen test: js/src/jit-test/tests/debug/Debugger-debuggees-10.js +x = newGlobal({newCompartment: true}) +x.t = this +// Randomly chosen test: js/src/jit-test/tests/debug/Debugger-findObjects-06.js +Debugger(x).findObjects() diff --git a/js/src/jit-test/tests/debug/bug-1136806.js b/js/src/jit-test/tests/debug/bug-1136806.js new file mode 100644 index 0000000000..104e79b1ee --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1136806.js @@ -0,0 +1,7 @@ +// |jit-test| allow-unhandlable-oom; allow-oom + +if (typeof oomAfterAllocations == "function") { + Debugger() + oomAfterAllocations(6) + Debugger() +} diff --git a/js/src/jit-test/tests/debug/bug-1192401.js b/js/src/jit-test/tests/debug/bug-1192401.js new file mode 100644 index 0000000000..d55d22b8e5 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1192401.js @@ -0,0 +1,6 @@ +// |jit-test| --more-compartments +const dbg = new Debugger(); +const g = evalcx("lazy"); +dbg.addDebuggee(g); +dbg.memory.trackingAllocationSites = true; +g.eval("this.alloc = {}"); diff --git a/js/src/jit-test/tests/debug/bug-1238610.js b/js/src/jit-test/tests/debug/bug-1238610.js new file mode 100644 index 0000000000..54d519f29d --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1238610.js @@ -0,0 +1,21 @@ +// |jit-test| allow-oom; skip-if: !('oomAfterAllocations' in this) || helperThreadCount() === 0 + +lfcode = new Array(); +dbg = Debugger(); +dbg.onEnterFrame = function() {}; +g = newGlobal(); +lfcode.push(` + oomAfterAllocations(100); + new Number(); + dbg.addDebuggee(g); +`) +file = lfcode.shift(); +loadFile(file); +function loadFile(lfVarx) { + lfGlobal = newGlobal() + for (lfLocal in this) + if (!(lfLocal in lfGlobal)) + lfGlobal[lfLocal] = this[lfLocal] + offThreadCompileScript(lfVarx) + lfGlobal.runOffThreadScript() +} diff --git a/js/src/jit-test/tests/debug/bug-1240090.js b/js/src/jit-test/tests/debug/bug-1240090.js new file mode 100644 index 0000000000..20be20adb4 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1240090.js @@ -0,0 +1,23 @@ +gczeal(2); +g = newGlobal({newCompartment: true, disableLazyParsing: true}); +dbg = Debugger(g); +dbg.onNewScript = function() { return function() { return this; } }; +schedulegc(10); +g.evaluate("function one() {}"); +g.evaluate(` + function target () {} + function two2() {} + `, {}); +g.evaluate(` + function three1() {} + function three2() {} + function three3() {} + `, {}); +dbg.memory.takeCensus({ + breakdown: { + by: "coarseType", + scripts: { + by: "filename" + } + } +}); diff --git a/js/src/jit-test/tests/debug/bug-1248162.js b/js/src/jit-test/tests/debug/bug-1248162.js new file mode 100644 index 0000000000..825b3376e4 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1248162.js @@ -0,0 +1,11 @@ +// |jit-test| allow-oom; skip-if: !('oomTest' in this) + +// Adapted from randomly chosen test: js/src/jit-test/tests/debug/Debugger-onNewGlobalObject-01.js +for (var i = 0; i < 9; ++i) { + var dbg = new Debugger; + dbg.onNewGlobalObject = function() {}; +} +// jsfunfuzz-generated +oomTest(function() { + newGlobal({sameZoneAs: this}); +}) diff --git a/js/src/jit-test/tests/debug/bug-1260725.js b/js/src/jit-test/tests/debug/bug-1260725.js new file mode 100644 index 0000000000..ce1c263f6a --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1260725.js @@ -0,0 +1,9 @@ +// |jit-test| skip-if: !('oomTest' in this) + +var dbg = new Debugger; +dbg.onNewGlobalObject = function(global) { + dbg.memory.takeCensus({}); +}; +oomTest(function() { + newGlobal({sameZoneAs: this}) +}); diff --git a/js/src/jit-test/tests/debug/bug-1260728.js b/js/src/jit-test/tests/debug/bug-1260728.js new file mode 100644 index 0000000000..fbccb448e7 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1260728.js @@ -0,0 +1,12 @@ +// |jit-test| error:Error + +var g = newGlobal(); +var dbg = new Debugger(g); +dbg.onNewScript = function(script) { + fscript = script.getChildScripts()[0]; +} +g.eval("function f(x) { arguments[0] = 3; return x }"); +fscript.setBreakpoint(0, {hit:function(frame) { + assertEq(frame.eval("assertEq(arguments, undefined)").return, 1); +}}); +assertEq(g.f(1), 42); diff --git a/js/src/jit-test/tests/debug/bug-1385844-2.js b/js/src/jit-test/tests/debug/bug-1385844-2.js new file mode 100644 index 0000000000..50d070b41e --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1385844-2.js @@ -0,0 +1,39 @@ +// Frame invalidatation should not follow 'debugger eval prev' links. +// This should not fail a 'frame.isDebuggee()' assertion. +// This version of the test requires only a single Debugger, and a single +// debuggee global. + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); + +g.eval(` + function f() { debugger; } +`); +g.observeAll = observeAll; + +dbg.onDebuggerStatement = first; +g.eval(`debugger;`); + +var saved; +function first(frame) { + saved = frame; + dbg.onDebuggerStatement = second; + saved.eval(`f()`); +} + +function second() { + saved.eval(`observeAll()`); +} + +function observeAll() { + // Setting this hook causes `Debugger::updateExecutionObservabilityOfFrames` + // to walk the stack looking for frames running in `g` and marking them as + // debuggees. It should ignore 'debugger eval prev' links; if it does not, the + // traversal will jump from the frame for `second`'s eval directly to `saved`, + // the frame for the `g.eval` call. In particular, it will not visit the frame + // for the eval in `first`, which was never marked as a debuggee. (Simply + // being created for a `Debugger.Frame.prototype.eval` call doesn't + // necessarily mark you as a debuggee, if your behavior doesn't need to be + // observed.) + dbg.onEnterFrame = function () { }; +} diff --git a/js/src/jit-test/tests/debug/bug-1385844.js b/js/src/jit-test/tests/debug/bug-1385844.js new file mode 100644 index 0000000000..3f40803a07 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1385844.js @@ -0,0 +1,24 @@ +// Frame invalidatation should not follow 'debugger eval prev' links. +// This should not fail a 'frame.isDebuggee()' assertion. + +var g1 = this; + +var h = newGlobal({ newCompartment: true }); +h.parent = g1; +h.eval(` + var hdbg = new Debugger(parent); + function j() { + hdbg.onEnterFrame = function(frame) {}; + } +`); + +var g2 = newGlobal({ newCompartment: true }); +g2.j = h.j; + +var dbg = new Debugger(g2); +var g2DO = dbg.addDebuggee(g2); + +dbg.onDebuggerStatement = function(f) { + f.eval('j()'); +}; +g2.eval('debugger'); diff --git a/js/src/jit-test/tests/debug/bug-1444604-reduced.js b/js/src/jit-test/tests/debug/bug-1444604-reduced.js new file mode 100644 index 0000000000..6e84fef194 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1444604-reduced.js @@ -0,0 +1,27 @@ +// LiveSavedFrameCache should not be confused by eval-in-frame-prev links. + +const g = newGlobal({newCompartment: true}); +const dbg = new Debugger(); +const gDO = dbg.addDebuggee(g); + +g.evaluate(` + function inner() { debugger; } + function outer() { inner(); } +`); + +dbg.onDebuggerStatement = function handler(frame) { + // Capture the JS stack, putting frames for handler, inner, and outer in the + // cache. Perform all captures in g's compartment, to avoid flushing the cache + // for compartment mismatches. + frame.eval('print(`in inner: ${saveStack()}`)'); + + // Carry out a capture in outer's context, following the eval-in-frame prev + // link and thus skipping over inner, removing it from the cache. + frame.older.eval('print(`in outer: ${saveStack()}`)'); + + // Capture again. inner's hasCachedSavedFrame bit should be set, but its entry + // has been flushed. + frame.eval('print(`in inner: ${saveStack()}`)'); +}; + +g.outer(); diff --git a/js/src/jit-test/tests/debug/bug-1444604.js b/js/src/jit-test/tests/debug/bug-1444604.js new file mode 100644 index 0000000000..d5906791db --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1444604.js @@ -0,0 +1,16 @@ +// Fuzz test: LiveSavedFrameCache should not be confused by eval-in-frame-prev links. +// See bug-144604-reduced.js for a more direct version. + +var evalInFrame = (function (global) { + var dbgGlobal = newGlobal({newCompartment: true}); + var dbg = new dbgGlobal.Debugger(); + return function evalInFrame(upCount, code) { + dbg.addDebuggee(global); + var frame = dbg.getNewestFrame().older; + for (var i = 0; i < upCount; i++) { + } + var completion = frame.eval(code); + }; +})(this); +enableTrackAllocations(); +evalInFrame(1, "print(a)"); diff --git a/js/src/jit-test/tests/debug/bug-1477084.js b/js/src/jit-test/tests/debug/bug-1477084.js new file mode 100644 index 0000000000..2e45a8cc05 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1477084.js @@ -0,0 +1,30 @@ +// Don't assert trying to force return before the initial yield of an async function. + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.parentExc = new Error("pants"); +g.eval(` + var dbg = new Debugger; + var pw = dbg.addDebuggee(parent); + var hits = 0; + dbg.onExceptionUnwind = function (frame) { + dbg.onExceptionUnwind = undefined; + return {return: undefined}; + }; + dbg.uncaughtExceptionHook = exc => { + hits++; + assertEq(exc instanceof TypeError, true); + assertEq(/force return.*before the initial yield/.test(exc.message), true); + return {throw: pw.makeDebuggeeValue(parentExc)}; + }; +`); + +async function* method({ x: callbackfn = unresolvableReference }) {} +try { + method(); +} catch (exc) { + g.dbg.enabled = false; + assertEq(exc, g.parentExc); +} + +assertEq(g.hits, 1); diff --git a/js/src/jit-test/tests/debug/bug-1564012.js b/js/src/jit-test/tests/debug/bug-1564012.js new file mode 100644 index 0000000000..b51f9e16fc --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1564012.js @@ -0,0 +1,8 @@ +fullcompartmentchecks(true); +var g = newGlobal({ + newCompartment: true +}); +g.eval("function*f(){debugger;yield}"); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function(frame) {}; +g.f().next(); diff --git a/js/src/jit-test/tests/debug/bug-1565275.js b/js/src/jit-test/tests/debug/bug-1565275.js new file mode 100644 index 0000000000..242bce3a85 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1565275.js @@ -0,0 +1,19 @@ +// |jit-test| skip-if: !('oomTest' in this) + +Object.defineProperty(this, "fuzzutils", { + value: { + evaluate: function() {}, + } +}); +var g = newGlobal({ + newCompartment: true +}); +g.parent = this; +g.eval("(" + function() { + var dbg = Debugger(parent); + dbg.onEnterFrame = function(frame) { + frame.onStep = function() {} + } +} + ")()"); +fuzzutils.evaluate(); +oomTest(new Function(), {expectExceptionOnFailure: false}); diff --git a/js/src/jit-test/tests/debug/bug-1572391.js b/js/src/jit-test/tests/debug/bug-1572391.js new file mode 100644 index 0000000000..816173a154 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1572391.js @@ -0,0 +1,15 @@ +g = newGlobal({ newCompartment: true }); +dbg = Debugger(g); +dbg.onDebuggerStatement = function(frame) { + frame.older +} + +g.eval(` + function* countdown(recur) { + if (recur) + yield* countdown(false); + debugger; + } +`); + +g.countdown(true).next() diff --git a/js/src/jit-test/tests/debug/bug-1576862-2.js b/js/src/jit-test/tests/debug/bug-1576862-2.js new file mode 100644 index 0000000000..6f7c31e98a --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1576862-2.js @@ -0,0 +1,23 @@ +// |jit-test| skip-if: !('stackTest' in this) +// Failure to rewrap an exception in Completion::fromJSResult should be propagated. + +var dbgGlobal = newGlobal({ newCompartment: true }); +var dbg = new dbgGlobal.Debugger(); +dbg.addDebuggee(this); + +function test() { + // Make this call's stack frame a debuggee, to ensure that + // slowPathOnLeaveFrame runs when this frame exits. That calls + // Completion::fromJSResult to capture this frame's completion value. + dbg.getNewestFrame(); + + // Throw from the non-debuggee compartment, to force Completion::fromJSResult + // to rewrap the exception. + dbgGlobal.assertEq(1,2); +} + +stackTest(test, { + // When the bug is fixed, the failure to rewrap the exception turns the throw + // into a termination, so we won't get an exception. + expectExceptionOnFailure: false +}); diff --git a/js/src/jit-test/tests/debug/bug-1584195.js b/js/src/jit-test/tests/debug/bug-1584195.js new file mode 100644 index 0000000000..93aca7291d --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-1584195.js @@ -0,0 +1,31 @@ +// |jit-test| skip-if: !('gczeal' in this) +// Bug 1584195: Debugger.Frame finalizer should't try to apply +// IsAboutToBeFinalized to cells of other alloc kinds, whose arenas may have +// been turned over to fresh allocations. + +try { + evaluate(` + var g9 = newGlobal({newCompartment: true}); + g9.parent = this; + g9.eval(\` + var dbg = new Debugger(parent); + dbg.onEnterFrame = frame => {}; + \`); + function lfPromise(x) { + return new Promise(resolve => {}); + } + async function lfAsync() { + await lfPromise(); + } lfAsync(); + `); + gczeal(20, 2); + evaluate(` + async function lfAsync() {} lfAsync(); + `); + gczeal(12, 7); + drainJobQueue(); + evaluate(` + new { ... v22 => { + `); +} catch (exc) {} +evaluate(""); diff --git a/js/src/jit-test/tests/debug/bug-725733.js b/js/src/jit-test/tests/debug/bug-725733.js new file mode 100644 index 0000000000..d518ceb74b --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-725733.js @@ -0,0 +1,9 @@ +// |jit-test| +// Adding a debuggee must leave its scripts in a safe state. + +var g = newGlobal({newCompartment: true}); +g.eval( + "function f(x) { return {q: x}; }\n" + + "var n = f('').q;\n"); +var dbg = new Debugger(g); +g.eval("f(0)"); diff --git a/js/src/jit-test/tests/debug/bug-800586.js b/js/src/jit-test/tests/debug/bug-800586.js new file mode 100644 index 0000000000..06cc33300a --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-800586.js @@ -0,0 +1,7 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +dbg.onDebuggerStatement = function (f) { + gw.executeInGlobal("eval('var x = \"A Brief History of Love\"');\n") +}; +g.eval('debugger'); diff --git a/js/src/jit-test/tests/debug/bug-826669.js b/js/src/jit-test/tests/debug/bug-826669.js new file mode 100644 index 0000000000..21b50441ff --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-826669.js @@ -0,0 +1,7 @@ +gczeal(9, 2) +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var g1w = dbg.addDebuggee(g1); +g1.eval('function f() {}'); +scripts = dbg.findScripts({}); diff --git a/js/src/jit-test/tests/debug/bug-858170.js b/js/src/jit-test/tests/debug/bug-858170.js new file mode 100644 index 0000000000..f3e1a334bc --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-858170.js @@ -0,0 +1,7 @@ +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onNewScript = function (s) { + throw new Error(); +}; +dbg.uncaughtExceptionHook = function () {} +g.eval("2 * 3"); diff --git a/js/src/jit-test/tests/debug/bug-876654.js b/js/src/jit-test/tests/debug/bug-876654.js new file mode 100644 index 0000000000..08514695a5 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug-876654.js @@ -0,0 +1,13 @@ +// |jit-test| +// Exercise finding a DebuggerSource cross compartment wrapper in +// Compartment::findOutgoingEdges() + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); +dbg.onNewScript = function (script) { + var text = script.source.text; +} +g.eval("function f() { function g() {} }"); +gczeal(9, 1) +var a = {} +var b = {} diff --git a/js/src/jit-test/tests/debug/bug1001372.js b/js/src/jit-test/tests/debug/bug1001372.js new file mode 100644 index 0000000000..77511754be --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1001372.js @@ -0,0 +1,21 @@ +var lfcode = new Array(); +lfcode.push = loadFile; +lfcode.push(""); +lfcode.push(""); +lfcode.push("3"); +lfcode.push(""); +lfcode.push(""); +lfcode.push(""); +lfcode.push(""); +lfcode.push("4"); +lfcode.push(""); +lfcode.push("0"); +lfcode.push("gczeal(2);"); +lfcode.push("\ + var g = newGlobal({newCompartment: true});\ + g.parent = this;\ + g.eval('new Debugger(parent).onExceptionUnwind = function() {};');\ + "); +function loadFile(lfVarx) { + evaluate(lfVarx); +} diff --git a/js/src/jit-test/tests/debug/bug1002797.js b/js/src/jit-test/tests/debug/bug1002797.js new file mode 100644 index 0000000000..f7cbcfccf7 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1002797.js @@ -0,0 +1,15 @@ +var lfcode = new Array(); +lfcode.push(""); +lfcode.push("0"); +lfcode.push("newGlobal(\"1\").eval(\"(new Debugger).addAllGlobalsAsDebuggees();\");\n"); +while (true) { + var file = lfcode.shift(); if (file == undefined) { break; } + loadFile(file) +} +function loadFile(lfVarx) { + try { + if (lfVarx.substr(-3) != ".js" && lfVarx.length != 1) { + evaluate(lfVarx); + } else if (!isNaN(lfVarx)) {} + } catch (lfVare) { } +} diff --git a/js/src/jit-test/tests/debug/bug1004447.js b/js/src/jit-test/tests/debug/bug1004447.js new file mode 100644 index 0000000000..18ee6757f5 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1004447.js @@ -0,0 +1,23 @@ +// Tests that earlier try notes don't interfere with later exception handling. + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function () { + dbg = new Debugger(debuggeeGlobal); +} + ")();"); +var myObj = { p1: 'a', } +try { + with(myObj) { + do { + throw value; + } while(false); + } +} catch(e) { + // The above is expected to throw. +} + +try { + if(!(p1 === 1)) { } +} catch (e) { + // The above is expected to throw. +} diff --git a/js/src/jit-test/tests/debug/bug1006205.js b/js/src/jit-test/tests/debug/bug1006205.js new file mode 100644 index 0000000000..dc899523a1 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1006205.js @@ -0,0 +1,20 @@ +var lfcode = new Array(); +lfcode.push = loadFile; +lfcode.push("\ +var g = newGlobal({newCompartment: true});\ +g.debuggeeGlobal = this;\ +g.eval(\"(\" + function () {\ + dbg = new Debugger(debuggeeGlobal);\ + } + \")();\");\ +"); +lfcode.push("gc();"); +lfcode.push("\ +var g = newGlobal({newCompartment: true});\ +g.debuggeeGlobal = this;\ +g.eval(\"(\" + function () {\ + dbg = new Debugger(debuggeeGlobal);\ +} + \")();\");\ +"); +function loadFile(lfVarx) { +function newFunc(x) { new Function(x)(); }; newFunc(lfVarx); +} diff --git a/js/src/jit-test/tests/debug/bug1006473.js b/js/src/jit-test/tests/debug/bug1006473.js new file mode 100644 index 0000000000..9eeefc2625 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1006473.js @@ -0,0 +1,19 @@ +// |jit-test| error: ReferenceError + +var lfcode = new Array(); +lfcode.push("gczeal(4);"); +lfcode.push("setJitCompilerOption('ion.warmup.trigger', 30);"); +lfcode.push("\ +var g = newGlobal({newCompartment: true});\ +g.parent = this;\ +g.eval('function f(frame, exc) { f2 = function () { return exc; }; exc = 123; }');\ +g.eval('new Debugger(parent).onExceptionUnwind = f;');\ +var obj = int8 ('oops');\ +"); +while (true) { + var file = lfcode.shift(); if (file == undefined) { break; } + loadFile(file) +} +function loadFile(lfVarx) { + evaluate(lfVarx); +} diff --git a/js/src/jit-test/tests/debug/bug1106164.js b/js/src/jit-test/tests/debug/bug1106164.js new file mode 100644 index 0000000000..7075134dda --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1106164.js @@ -0,0 +1,17 @@ +// |jit-test| error: ReferenceError +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () { };"); +evaluate("\ + var tokenCodes = {};\ + tokenCodes.continue = 0;\ + var arr = [\ + (0.E87 ), \ + ];\ + for(var reportCompare in tokenCodes) {\ + for(var p1 in arr) {\ + if(arr[j . arr ++ ] === p) {\ + }\ + }\ + }\ +", { noScriptRval : true, isRunOnce: true }); diff --git a/js/src/jit-test/tests/debug/bug1106719.js b/js/src/jit-test/tests/debug/bug1106719.js new file mode 100644 index 0000000000..4f42975cfb --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1106719.js @@ -0,0 +1,12 @@ +// |jit-test| allow-oom; allow-unhandlable-oom; allow-overrecursed; skip-if: getBuildConfiguration()['android'] +// Disabled on Android due to harness problems (Bug 1532654) + +g = newGlobal({newCompartment: true}) +g.parent = this +g.eval("Debugger(parent).onExceptionUnwind=(function(){})") +gcparam("maxBytes", gcparam("gcBytes")) +function f() { + f() + y(arguments) +} +f() diff --git a/js/src/jit-test/tests/debug/bug1107525.js b/js/src/jit-test/tests/debug/bug1107525.js new file mode 100644 index 0000000000..35a5eb466f --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1107525.js @@ -0,0 +1,9 @@ +// |jit-test| error: InternalError +enableGeckoProfiling(); +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () { hits++; };"); +function f() { + var x = f(); +} +f(); diff --git a/js/src/jit-test/tests/debug/bug1107913.js b/js/src/jit-test/tests/debug/bug1107913.js new file mode 100644 index 0000000000..b1ba03ba2a --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1107913.js @@ -0,0 +1,7 @@ +// |jit-test| error: TypeError + +var g = newGlobal(); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () {};"); +Object.preventExtensions(this); +evaluate("function testcase() { }", { noScriptRval : true, isRunOnce: true }); diff --git a/js/src/jit-test/tests/debug/bug1108159.js b/js/src/jit-test/tests/debug/bug1108159.js new file mode 100644 index 0000000000..4fff79de57 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1108159.js @@ -0,0 +1,12 @@ +// |jit-test| slow; skip-if: helperThreadCount() === 0 + +var s = ''; +for (var i = 0; i < 70000; i++) + { + s += 'function x' + i + '() { x' + i + '(); }\n'; +} +evaluate(s); +var g = newGlobal({newCompartment: true}); +(new Debugger).addDebuggee(g); +g.offThreadCompileScript('debugger;',{}); +g.runOffThreadScript(); diff --git a/js/src/jit-test/tests/debug/bug1108556.js b/js/src/jit-test/tests/debug/bug1108556.js new file mode 100644 index 0000000000..181dbc2f99 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1108556.js @@ -0,0 +1,10 @@ +// |jit-test| error: ReferenceError + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () { hits++; };"); +evaluate('\ +var fe="v";\ +for (i=0; String.fromCharCode(0x004E); i++)\ + fe += fe;\ +', { isRunOnce: true }); diff --git a/js/src/jit-test/tests/debug/bug1109328.js b/js/src/jit-test/tests/debug/bug1109328.js new file mode 100644 index 0000000000..8bbadace22 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1109328.js @@ -0,0 +1,8 @@ +try { + gcslice(0)(""()); +} catch (e) {} +g = newGlobal({newCompartment: true}) +g.parent = this +g.eval("Debugger(parent).onExceptionUnwind=(function(){})"); +gcparam("maxBytes", gcparam("maxBytes") - 8); +gc(); diff --git a/js/src/jit-test/tests/debug/bug1109915.js b/js/src/jit-test/tests/debug/bug1109915.js new file mode 100644 index 0000000000..be93d8cd68 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1109915.js @@ -0,0 +1,17 @@ +var evalInFrame = (function (global) { + var dbgGlobal = newGlobal({newCompartment: true}); + var dbg = new dbgGlobal.Debugger(); + return function evalInFrame(upCount, code) { + dbg.addDebuggee(global); + var frame = dbg.getNewestFrame().older; + var completion = frame.eval(code); + }; +})(this); +function g1(x, args) {} +function f1(x, y, o) { + for (var i=0; i<50; i++) { + o.apply(evalInFrame(0, "x"), x); + } +} +var o1 = {apply: g1}; +assertEq(f1(3, 5, o1), undefined); diff --git a/js/src/jit-test/tests/debug/bug1109964.js b/js/src/jit-test/tests/debug/bug1109964.js new file mode 100644 index 0000000000..82b2760680 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1109964.js @@ -0,0 +1,10 @@ +var dbgGlobal = newGlobal({newCompartment: true}); +var dbg = new dbgGlobal.Debugger(); +dbg.addDebuggee(this); +function f() { + var a = arguments; + a[1]; + dbg.getNewestFrame().eval("a"); +} +f(); + diff --git a/js/src/jit-test/tests/debug/bug1111199.js b/js/src/jit-test/tests/debug/bug1111199.js new file mode 100644 index 0000000000..b7aa78d04c --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1111199.js @@ -0,0 +1,17 @@ +g = newGlobal({newCompartment: true}) +g.parent = this +g.eval("Debugger(parent).onExceptionUnwind=(function(){})") +try { +function f(code) { + n = parseInt('', 0); + return g("try{}catch(e){}", n) +} +function g(s, n) { + s2 = s + s + d = (n - (function () { + return "" + this.id + eval.id; + } )().abstract) / 2 + m = g(s2, d) +} +f("switch(''){default:break;}") +} catch(exc1) {} diff --git a/js/src/jit-test/tests/debug/bug1114587.js b/js/src/jit-test/tests/debug/bug1114587.js new file mode 100644 index 0000000000..538419b038 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1114587.js @@ -0,0 +1,26 @@ +// |jit-test| error: TypeError +var lfcode = new Array(); +lfcode.push(""); +lfcode.push(""); +lfcode.push("\ +var g = newGlobal();\ +g.debuggeeGlobal = this;\ +g.eval('(' + function () {\ + dbg = new Debugger(debuggeeGlobal);\ + dbg.onExceptionUnwind = function (frame, exc) {\ + var s = '!';\ + for (var f = frame; f; f = f.older)\ + s += f.callee.name;\ + };\ + } + ')();');\ +Debugger(17) = this;\ +"); +while (true) { + var file = lfcode.shift(); if (file == undefined) { break; } + loadFile(file) +} +function loadFile(lfVarx) { + if (lfVarx.substr(-3) != ".js" && lfVarx.length != 1) { + function newFunc(x) { new Function(x)(); }; newFunc(lfVarx); + } +} diff --git a/js/src/jit-test/tests/debug/bug1116103.js b/js/src/jit-test/tests/debug/bug1116103.js new file mode 100644 index 0000000000..63703e77a2 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1116103.js @@ -0,0 +1,11 @@ +// |jit-test| error: ReferenceError + +evaluate(` + var g = newGlobal({newCompartment: true}); + g.parent = this; + g.eval('new Debugger(parent).onExceptionUnwind = function() {};'); +`) +{ + while (x && 0) {} + let x +} diff --git a/js/src/jit-test/tests/debug/bug1118878.js b/js/src/jit-test/tests/debug/bug1118878.js new file mode 100644 index 0000000000..26dcc68382 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1118878.js @@ -0,0 +1,11 @@ + +var evalInFrame = (function (global) { + var dbgGlobal = newGlobal({newCompartment: true}); + var dbg = new dbgGlobal.Debugger(); + return function evalInFrame(upCount, code) { + dbg.addDebuggee(global); + var frame = dbg.getNewestFrame().older; + var completion = frame.eval(code); + }; +})(this); +evaluate("for (var k in 'xxx') (function g() { Math.atan2(42); evalInFrame((0), (''), true); })();", { noScriptRval : true, isRunOnce : true }); diff --git a/js/src/jit-test/tests/debug/bug1121083.js b/js/src/jit-test/tests/debug/bug1121083.js new file mode 100644 index 0000000000..9879df8fa6 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1121083.js @@ -0,0 +1,15 @@ +// |jit-test| exitstatus: 6 + +g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("Debugger(parent).onExceptionUnwind = function () {};"); + +function f(x) { + if (x === 0) { + return; + } + f(x - 1); + f(x - 1); +} +timeout(0.00001); +f(100); diff --git a/js/src/jit-test/tests/debug/bug1130768.js b/js/src/jit-test/tests/debug/bug1130768.js new file mode 100644 index 0000000000..64f1a7c0cc --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1130768.js @@ -0,0 +1,12 @@ +// |jit-test| error:foo +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("(" + function() { + var dbg = new Debugger(parent); + count = 0; + dbg.onExceptionUnwind = function(frame) { + frame.onPop = function() { if (count++ < 30) frame.eval("x = 3"); }; + }; +} + ")()"); +Object.defineProperty(this, "x", {set: [].map}); +throw "foo"; diff --git a/js/src/jit-test/tests/debug/bug1133196.js b/js/src/jit-test/tests/debug/bug1133196.js new file mode 100644 index 0000000000..60d963a8bc --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1133196.js @@ -0,0 +1,16 @@ +// |jit-test| error: TypeError + +var g = newGlobal(); +g.parent = this; +g.eval("(" + function() { + var dbg = new Debugger(parent); + dbg.onExceptionUnwind = function(frame) { + frame.older.onStep = function() {} + }; +} + ")()"); +function f() { + (function inner(arr) { + inner(arr.map); + })([]); +} +f(); diff --git a/js/src/jit-test/tests/debug/bug1147939.js b/js/src/jit-test/tests/debug/bug1147939.js new file mode 100644 index 0000000000..a2bd2cf798 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1147939.js @@ -0,0 +1,8 @@ +// |jit-test| error: TypeError +var g = newGlobal(); +g.debuggeeGlobal = this; +g.eval("(" + function () { + dbg = new Debugger(debuggeeGlobal); + dbg.onExceptionUnwind = Map; +} + ")();"); +throw new Error("oops"); diff --git a/js/src/jit-test/tests/debug/bug1148917.js b/js/src/jit-test/tests/debug/bug1148917.js new file mode 100644 index 0000000000..2645f913f3 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1148917.js @@ -0,0 +1,14 @@ +// |jit-test| error: Error + +var g = newGlobal(); +g.eval('function f(a) { evaluate("f(" + " - 1);", {newContext: true}); }'); +var dbg = new Debugger(g); +var frames = []; +dbg.onEnterFrame = function (frame) { + if (frames.length == 3) + return; + frames.push(frame); + for (var f of frames) + f.eval('a').return +}; +g.f(); diff --git a/js/src/jit-test/tests/debug/bug1160182.js b/js/src/jit-test/tests/debug/bug1160182.js new file mode 100644 index 0000000000..d4471a42f2 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1160182.js @@ -0,0 +1,13 @@ +// |jit-test| skip-if: isLcovEnabled() + +var g = newGlobal({newCompartment: true}); +g.eval("(" + function() { + var o = {get x() {}}; + this.method = Object.getOwnPropertyDescriptor(o, "x").get; + assertEq(isLazyFunction(method), true); +} + ")()"); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +var scripts = dbg.findScripts(); +var methodv = gw.makeDebuggeeValue(g.method); +assertEq(scripts.indexOf(methodv.script) != -1, true); diff --git a/js/src/jit-test/tests/debug/bug1161332.js b/js/src/jit-test/tests/debug/bug1161332.js new file mode 100644 index 0000000000..2b8f371495 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1161332.js @@ -0,0 +1,16 @@ +// |jit-test| error: Error + +var g = newGlobal(); +g.eval('function f(a) { if (a == 1) debugger; evaluate("f(" + a + " - 1);"); }'); +var N = 2; +var dbg = new Debugger(g); +var frames = []; +dbg.onEnterFrame = function (frame) { + frames.push(frame); + frame.onPop = function () { assertEq(frame.onPop, frame.onPop); }; +}; +dbg.onDebuggerStatement = function (frame) { + for (var f of frames) + f.eval('a').return; +}; +evaluate("g.f(N);"); diff --git a/js/src/jit-test/tests/debug/bug1188334.js b/js/src/jit-test/tests/debug/bug1188334.js new file mode 100644 index 0000000000..546fe71aa6 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1188334.js @@ -0,0 +1,18 @@ +var evalInFrame = (function (global) { + var dbgGlobal = newGlobal({newCompartment: true}); + var dbg = new dbgGlobal.Debugger(); + return function evalInFrame(upCount, code) { + dbg.addDebuggee(global); + var frame = dbg.getNewestFrame().older; + var completion = frame.eval(code); + }; +})(this); +function* f() { + { + let {} = "xxx"; + yield evalInFrame(0, "x"); + } +} +var gen = f(); +gen.next() + diff --git a/js/src/jit-test/tests/debug/bug1191499.js b/js/src/jit-test/tests/debug/bug1191499.js new file mode 100644 index 0000000000..4cd59267f9 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1191499.js @@ -0,0 +1,17 @@ + +setJitCompilerOption('ion.warmup.trigger', 2); +setJitCompilerOption('offthread-compilation.enable', 0); +var g = newGlobal({newCompartment: true}); +var dbg2 = new Debugger; +g.toggle = function toggle(x, d) { + if (d) { + dbg2.addDebuggee(g); + dbg2.getNewestFrame().environment.getVariable("x"); + } +}; +g.eval("" + function f(x, d) { toggle(++arguments, d); }); +g.eval("(" + function test() { + for (var i = 0; i < 30; i++) + f(42, false); + f(42, true); +} + ")();"); diff --git a/js/src/jit-test/tests/debug/bug1216261.js b/js/src/jit-test/tests/debug/bug1216261.js new file mode 100644 index 0000000000..0d98327256 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1216261.js @@ -0,0 +1,12 @@ +// |jit-test| exitstatus: 3; skip-if: !('oomAfterAllocations' in this) + +var g = newGlobal(); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function(frame) { + oomAfterAllocations(5); + // OOMs here, and possibly again in the error reporter when trying to + // report the OOM, so the shell just exits with code 3. + frame.older.eval("escaped = function() { return y }"); +} +g.eval("function h() { debugger }"); +g.eval("(function () { var y = {p:42}; h(); yield })().next();"); diff --git a/js/src/jit-test/tests/debug/bug1219905.js b/js/src/jit-test/tests/debug/bug1219905.js new file mode 100644 index 0000000000..5ed51a2423 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1219905.js @@ -0,0 +1,11 @@ +// |jit-test| allow-oom; skip-if: !('oomTest' in this) + +// We need allow-oom here because the debugger reports an uncaught exception if +// it hits OOM calling the exception unwind hook. This causes the shell to exit +// with the OOM reason. + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function() {}"); +let finished = false; +oomTest(() => l, false); diff --git a/js/src/jit-test/tests/debug/bug1221378.js b/js/src/jit-test/tests/debug/bug1221378.js new file mode 100644 index 0000000000..d8e6781dff --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1221378.js @@ -0,0 +1,11 @@ +// Bug 1221378: allocating an array from within the object metadata callback +// shouldn't cause re-entrant resolution of lazy prototypes. + +// To test for this regression, we need some way to make the code that collects +// metadata about object allocations allocate an Array. Presently, +// enableShellObjectMetadataCallback installs a callback that does this, but if +// that hook gets removed (in production there's only ever one callback we +// actually use), then the ability to make whatever metadata collection code +// remains allocate an Array will cover this regression. For example, it could +// be a flag that one can only set in debug builds from TestingFunctions.cpp. +newGlobal().eval('enableShellAllocationMetadataBuilder(); Array'); diff --git a/js/src/jit-test/tests/debug/bug1232655.js b/js/src/jit-test/tests/debug/bug1232655.js new file mode 100644 index 0000000000..c849465b4e --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1232655.js @@ -0,0 +1,5 @@ +g = newGlobal({newCompartment: true}); +g.log = ""; +Debugger(g).onDebuggerStatement = frame => frame.eval("log += this.Math.toString();"); +g.eval("(function() { with ({}) debugger })()"); +assertEq(g.log, "[object Math]"); diff --git a/js/src/jit-test/tests/debug/bug1240546.js b/js/src/jit-test/tests/debug/bug1240546.js new file mode 100644 index 0000000000..6d548d8b92 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1240546.js @@ -0,0 +1,9 @@ +// |jit-test| allow-oom; skip-if: !('oomAfterAllocations' in this) + +var g = newGlobal(); +g.debuggeeGlobal = this; +g.eval("(" + function() { + oomAfterAllocations(100); + var dbg = Debugger(debuggeeGlobal); + dbg.onEnterFrame = function(frame) {} +} + ")()"); diff --git a/js/src/jit-test/tests/debug/bug1240803.js b/js/src/jit-test/tests/debug/bug1240803.js new file mode 100644 index 0000000000..ab1e0fb641 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1240803.js @@ -0,0 +1,20 @@ +// |jit-test| allow-oom; skip-if: !('oomAfterAllocations' in this) + +(function() { + g = newGlobal({newCompartment: true}) + dbg = new Debugger + g.toggle = function(d) { + if (d) { + dbg.addDebuggee(g); + dbg.getNewestFrame(); + oomAfterAllocations(2); + setBreakpoint; + } + } + g.eval("" + function f(d) { return toggle(d); }) + g.eval("(" + function() { + f(false); + f(true); + } + ")()") +})(); + diff --git a/js/src/jit-test/tests/debug/bug1242111.js b/js/src/jit-test/tests/debug/bug1242111.js new file mode 100644 index 0000000000..dae0efcdab --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1242111.js @@ -0,0 +1,8 @@ +// |jit-test| allow-oom; skip-if: !('oomAfterAllocations' in this) + +var g = newGlobal(); +g.debuggeeGlobal = []; +g.eval("(" + function() { + oomAfterAllocations(57); + Debugger(debuggeeGlobal).onEnterFrame = function() {} +} + ")()"); diff --git a/js/src/jit-test/tests/debug/bug1242798.js b/js/src/jit-test/tests/debug/bug1242798.js new file mode 100644 index 0000000000..38a4780a9d --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1242798.js @@ -0,0 +1,14 @@ + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +g.eval("" + function f(c) { + if (c == 0) + return; + if (c == 2) + debugger; + f(c-1); + for (var i = 0; i < 100; i++) + Debugger += newGlobal('#15: myObj.parseFloat !== parseFloat'); +}); +dbg.onDebuggerStatement = function (frame) {}; +g.eval("f(2)"); diff --git a/js/src/jit-test/tests/debug/bug1245862.js b/js/src/jit-test/tests/debug/bug1245862.js new file mode 100644 index 0000000000..274903bc40 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1245862.js @@ -0,0 +1,22 @@ +// |jit-test| allow-oom; skip-if: !('oomAfterAllocations' in this) + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +g.h = function h(d) { + if (d) { + dbg.addDebuggee(g); + var f = dbg.getNewestFrame().older; + f.st_p1((oomAfterAllocations(10)) + "foo = 'string of 42'"); + } +} +g.eval("" + function f(d) { + g(d); +}); +g.eval("" + function g(d) { + h(d); +}); +g.eval("(" + function () { + for (i = 0; i < 5; i++) + f(false); + assertEq(f(true), "string of 42"); +} + ")();"); diff --git a/js/src/jit-test/tests/debug/bug1246605.js b/js/src/jit-test/tests/debug/bug1246605.js new file mode 100644 index 0000000000..5bf873ee5f --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1246605.js @@ -0,0 +1,13 @@ +g = newGlobal({newCompartment: true}); +dbg = Debugger(g); +dbg.onNewScript = function(script) { fscript = script.getChildScripts()[0]; } +g.eval("function f() { arguments[0]; }"); +var hitBreakpoint = false; +fscript.setBreakpoint(0, { + hit: function() { + getBacktrace({ args: 1 }); + hitBreakpoint = true; + } +}); +g.f(""); +assertEq(hitBreakpoint, true); diff --git a/js/src/jit-test/tests/debug/bug1251919.js b/js/src/jit-test/tests/debug/bug1251919.js new file mode 100644 index 0000000000..79fd05f890 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1251919.js @@ -0,0 +1,10 @@ +// |jit-test| skip-if: !('oomTest' in this) + +// jsfunfuzz-generated +fullcompartmentchecks(true); +// Adapted from randomly chosen test: js/src/jit-test/tests/debug/bug-1248162.js +var dbg = new Debugger; +dbg.onNewGlobalObject = function() {}; +oomTest(function() { + newGlobal({sameZoneAs: this}); +}) diff --git a/js/src/jit-test/tests/debug/bug1252453.js b/js/src/jit-test/tests/debug/bug1252453.js new file mode 100644 index 0000000000..626d9be624 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1252453.js @@ -0,0 +1,21 @@ +// |jit-test| --no-threads + +lfcode = new Array(); +gczeal(8,2); +lfcode.push(` +const root = newGlobal(); +const dbg = new Debugger; +const wrappedRoot = dbg.addDebuggee(root); +dbg.memory.trackingAllocationSites = 1; +root.eval("(" + function immediate() { '_' << foo } + "())"); +`); +file = lfcode.shift(); +loadFile(file); +function loadFile(lfVarx) { + try { + function newFunc(x) { return Function(x)(); } + newFunc(lfVarx)(); + } catch (lfVare) { + print(lfVare) + } +} diff --git a/js/src/jit-test/tests/debug/bug1252464.js b/js/src/jit-test/tests/debug/bug1252464.js new file mode 100644 index 0000000000..e93e5ec32f --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1252464.js @@ -0,0 +1,15 @@ +// |jit-test| error: ReferenceError + +g = newGlobal({newCompartment: true}); +dbg = Debugger(g); +hits = 0; +dbg.onNewScript = function () { return hits++; }; +assertEq(g.eval("eval('2 + 3')"), 5); +this.gczeal(hits,1); +dbg = Debugger(g); +g.h = function () { + var env = dbg.getNewestFrame().environment; + dbg = 0; + assertThrowsInstanceOf; +} +g.eval("h()"); diff --git a/js/src/jit-test/tests/debug/bug1253246.js b/js/src/jit-test/tests/debug/bug1253246.js new file mode 100644 index 0000000000..c7ab843419 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1253246.js @@ -0,0 +1,5 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +dbg.onDebuggerStatement = (frame) => { frame.eval("c = 42;"); }; +g.evalReturningScope("'use strict'; debugger;"); diff --git a/js/src/jit-test/tests/debug/bug1254123.js b/js/src/jit-test/tests/debug/bug1254123.js new file mode 100644 index 0000000000..72f36ef3ec --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1254123.js @@ -0,0 +1,14 @@ +// |jit-test| skip-if: !('oomTest' in this) + +evaluate(` +function ERROR(msg) { + throw new Error("boom"); +} +for (var i = 0; i < 9; ++ i) { + var dbg = new Debugger; + dbg.onNewGlobalObject = ERROR; +} +oomTest(function() { + newGlobal({sameZoneAs: this}); +}) +`); diff --git a/js/src/jit-test/tests/debug/bug1254190.js b/js/src/jit-test/tests/debug/bug1254190.js new file mode 100644 index 0000000000..3d3572b469 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1254190.js @@ -0,0 +1,12 @@ +// |jit-test| slow; skip-if: !('oomTest' in this); allow-oom + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onNewScript = function (s) { + log += dbg.findScripts({ source: s.source }).length; +} +log = ""; +oomTest(() => { + var static = newGlobal({sameZoneAs: this}); + g.eval("(function() {})()"); +}); diff --git a/js/src/jit-test/tests/debug/bug1254578.js b/js/src/jit-test/tests/debug/bug1254578.js new file mode 100644 index 0000000000..f36bcef601 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1254578.js @@ -0,0 +1,19 @@ +// |jit-test| slow; skip-if: !('oomTest' in this) + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function() { + dbg = new Debugger(debuggeeGlobal); + dbg.onExceptionUnwind = function(frame, exc) { + var s = '!'; + for (var f = frame; f; f = f.older) + debuggeeGlobal.log += s; + }; +} + ")();"); +var dbg = new Debugger; +dbg.onNewGlobalObject = function(global) { + get.seen = true; +}; +oomTest(function() { + newGlobal({sameZoneAs: this}) +}); diff --git a/js/src/jit-test/tests/debug/bug1257045.js b/js/src/jit-test/tests/debug/bug1257045.js new file mode 100644 index 0000000000..15b33288c8 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1257045.js @@ -0,0 +1,10 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +fullcompartmentchecks(true); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onNewScript = (function(script) { + s = script; +}) +g.eval(`new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); +s.source; diff --git a/js/src/jit-test/tests/debug/bug1263899.js b/js/src/jit-test/tests/debug/bug1263899.js new file mode 100644 index 0000000000..5392b89603 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1263899.js @@ -0,0 +1,30 @@ +try { + evaluate(` + function runTestCase() $ERROR() + function $ERROR() { + throw Error + } + Object.defineProperty(this, "x", { value: 0 }); + setJitCompilerOption("baseline.warmup.trigger", 0); + setJitCompilerOption("ion.warmup.trigger", 0) + `) + evaluate(`function f() {} f(x)`) + runTestCase() +} catch (exc) {} +evaluate(` + g = newGlobal({newCompartment: true}) + g.parent = this + g.eval("(" + function() { + Debugger(parent).onExceptionUnwind = function(frame) { + frame.older + } + } + ")()") + try { $ERROR() } catch(e){} +`) +try { +evaluate(` + x ^= null; + if (x = 1) + $ERROR() +`); +} catch(e) {} diff --git a/js/src/jit-test/tests/debug/bug1264961.js b/js/src/jit-test/tests/debug/bug1264961.js new file mode 100644 index 0000000000..c43a29504d --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1264961.js @@ -0,0 +1,23 @@ +// |jit-test| slow; skip-if: !('oomTest' in this) + +loadFile(` + var o = {} + var global = this; + var p = new Proxy(o, { + "deleteProperty": function (await , key) { + var g = newGlobal({sameZoneAs: this}); + g.parent = global; + g.eval("var dbg = new Debugger(parent); dbg.onEnterFrame = function(frame) {};"); + } + }) + for (var i=0; i<100; i++); + assertEq(delete p.foo, true); +`); +function loadFile(lfVarx) { + var k = 0; + oomTest(function() { + // In practice a crash occurs before iteration 4000. + if (k++ <= 4000) + eval(lfVarx); + }, {expectExceptionOnFailure: false}); +} diff --git a/js/src/jit-test/tests/debug/bug1266434.js b/js/src/jit-test/tests/debug/bug1266434.js new file mode 100644 index 0000000000..34a59dac26 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1266434.js @@ -0,0 +1,8 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var g = newGlobal({newCompartment: true}); +g.evaluate("function f(x) { return x + 1; }"); +var gw = dbg.addDebuggee(g); +gczeal(2, 1); +var s = dbg.findScripts(); +gczeal(0); diff --git a/js/src/jit-test/tests/debug/bug1272908.js b/js/src/jit-test/tests/debug/bug1272908.js new file mode 100644 index 0000000000..b1e1c5aeaf --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1272908.js @@ -0,0 +1,19 @@ +// |jit-test| slow; skip-if: !('oomTest' in this) + +// Adapted from randomly chosen test: js/src/jit-test/tests/modules/bug-1233915.js +g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("(" + function() { + Debugger(parent).onExceptionUnwind = function(frame) { + frame.eval("") + } } + ")()"); +// Adapted from randomly chosen test: js/src/jit-test/tests/debug/bug1254123.js +function ERROR(msg) { + throw new Error("boom"); +} +var dbg = new Debugger; +dbg.onNewGlobalObject = ERROR; +oomTest(function() { + newGlobal({sameZoneAs: this}); +}) + diff --git a/js/src/jit-test/tests/debug/bug1275001.js b/js/src/jit-test/tests/debug/bug1275001.js new file mode 100644 index 0000000000..52e3a1fc0d --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1275001.js @@ -0,0 +1,30 @@ + +g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("(" + function() { + Debugger(parent).onExceptionUnwind = function(frame) { + frame.older + } +} + ")()") +function check_one(expected, f, err) { + try { + f() + } catch (ex) { + s = ex.message; + assertEq(s.includes("undefined"), true); + } +} +ieval = eval +function check(expr, expected = expr) { + var end, err + for ([end, err] of[[".random_prop", ` is undefined` ]]) + statement = "o = {};" + expr + end; + cases = [ + function() { return ieval("var undef;" + statement); }, + Function(statement) + ] + for (f of cases) + check_one(expected, f, err) +} +check("undef"); +check("o.b"); diff --git a/js/src/jit-test/tests/debug/bug1282741.js b/js/src/jit-test/tests/debug/bug1282741.js new file mode 100644 index 0000000000..9e75495f58 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1282741.js @@ -0,0 +1,28 @@ + +function removeAdd(dbg, g) { + dbg.removeDebuggee(g); + dbg.addDebuggee(g); + switch (dbg.removeDebuggee(g)) {} +} +function newGlobalDebuggerPair(toggleSeq) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + dbg.addDebuggee(g); + g.eval("" + function f() { + for (var i = 0; i < 100; i++) interruptIf(i == 95); + }); + setInterruptCallback(function() { + return true; + }); + return [g, dbg]; +} +function testEpilogue(toggleSeq) { + var [g, dbg] = newGlobalDebuggerPair(toggleSeq); + dbg.onEnterFrame = function(f) { + f.onPop = function() { + toggleSeq(dbg, g); + } + }; + g.f() +} +testEpilogue(removeAdd); diff --git a/js/src/jit-test/tests/debug/bug1299121.js b/js/src/jit-test/tests/debug/bug1299121.js new file mode 100644 index 0000000000..95df524cb0 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1299121.js @@ -0,0 +1,10 @@ +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("(" + function() { + var dbg = new Debugger(parent); + dbg.onExceptionUnwind = function(frame) { + frame.eval("h = 3"); + }; +} + ")()"); +g = function h() { } +g(); diff --git a/js/src/jit-test/tests/debug/bug1300517.js b/js/src/jit-test/tests/debug/bug1300517.js new file mode 100644 index 0000000000..7ea07a0c74 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1300517.js @@ -0,0 +1,12 @@ +// |jit-test| error: ReferenceError +g = newGlobal({newCompartment: true}); +g.log *= ""; +Debugger(g).onDebuggerStatement = frame => frame.eval("log += this.Math.toString();"); +let forceException = g.eval(` + (class extends class {} { + constructor() { + debugger; + } + }) +`); +new forceException; diff --git a/js/src/jit-test/tests/debug/bug1300528.js b/js/src/jit-test/tests/debug/bug1300528.js new file mode 100644 index 0000000000..239a21df21 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1300528.js @@ -0,0 +1,33 @@ +// |jit-test| skip-if: helperThreadCount() === 0 + +load(libdir + "asserts.js"); + +function BigInteger(a, b, c) {} +function montConvert(x) { + var r = new BigInteger(null); + return r; +} +var ba = new Array(); +a = new BigInteger(ba); +g = montConvert(a); +var lfGlobal = newGlobal({newCompartment: true}); +for (lfLocal in this) { + if (!(lfLocal in lfGlobal)) { + lfGlobal[lfLocal] = this[lfLocal]; + } +} +lfGlobal.offThreadCompileScript(` + var dbg = new Debugger(g); + dbg.onEnterFrame = function (frame) { + var frameThis = frame.this; + } +`); +lfGlobal.runOffThreadScript(); +assertThrowsInstanceOf(test, ReferenceError); +function test() { + function check(fun, msg, todo) { + success = fun(); + } + check(() => Object.getPrototypeOf(view) == Object.getPrototypeOf(simple)); + typeof this; +} diff --git a/js/src/jit-test/tests/debug/bug1302432.js b/js/src/jit-test/tests/debug/bug1302432.js new file mode 100644 index 0000000000..2cd5678993 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1302432.js @@ -0,0 +1,11 @@ +setJitCompilerOption("baseline.warmup.trigger", 0); +setJitCompilerOption('ion.warmup.trigger', 0); +gczeal(7, 1); +var dbgGlobal = newGlobal({newCompartment: true}); +var dbg = new dbgGlobal.Debugger(); +dbg.addDebuggee(this); +function f(x, await = () => Array.isArray(revocable.proxy), ...get) { + dbg.getNewestFrame().older.eval("print(a)"); +} +function a() {} +for (var i = 0; i < 10; i++) f(); diff --git a/js/src/jit-test/tests/debug/bug1304553.js b/js/src/jit-test/tests/debug/bug1304553.js new file mode 100644 index 0000000000..bf85d10c6e --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1304553.js @@ -0,0 +1,21 @@ +var dbgGlobal = newGlobal({newCompartment: true}); +var dbg = new dbgGlobal.Debugger(); +dbg.addDebuggee(this); +function evalInFrame() { + var frame = dbg.getNewestFrame().older.older; + frame.eval("1"); +}; +var actual = ''; +try { + function f() { + evalInFrame(); + } +} catch (e) {} +let m = parseModule(` + actual = ''; + for (var i = 0; i < 1; ++i) + f(i); + actual; + `); +m.declarationInstantiation(); +m.evaluation(); diff --git a/js/src/jit-test/tests/debug/bug1308578.js b/js/src/jit-test/tests/debug/bug1308578.js new file mode 100644 index 0000000000..20df304eac --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1308578.js @@ -0,0 +1,10 @@ +// |jit-test| error: ReferenceError + +g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () {}"); +a = new class extends Array { + constructor() { + for (;; ([] = p)) {} + } +} diff --git a/js/src/jit-test/tests/debug/bug1330339.js b/js/src/jit-test/tests/debug/bug1330339.js new file mode 100644 index 0000000000..4654a93fae --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1330339.js @@ -0,0 +1,36 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; error: TestComplete + +if (!wasmDebuggingEnabled()) + throw "TestComplete"; + +let module = new WebAssembly.Module(wasmTextToBinary(` + (module + (import "global" "func" (func)) + (func (export "test") + call 0 ;; calls the import, which is func #0 + ) + ) +`)); + +let imports = { + global: { + func: function () { + let g = newGlobal({newCompartment: true}); + let dbg = new Debugger(g); + dbg.onExceptionUnwind = function (frame) { + frame.older; + }; + g.eval("throw new Error();"); + } + } +}; +let instance = new WebAssembly.Instance(module, imports); + +try { + instance.exports.test(); + assertEq(false, true); +} catch (e) { + assertEq(e.constructor.name, 'Error'); +} + +throw "TestComplete"; diff --git a/js/src/jit-test/tests/debug/bug1330489-sps.js b/js/src/jit-test/tests/debug/bug1330489-sps.js new file mode 100644 index 0000000000..ffed0c6588 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1330489-sps.js @@ -0,0 +1,44 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; error: TestComplete + +load(libdir + "asserts.js"); + +if (!wasmDebuggingEnabled()) + throw "TestComplete"; + +// Single-step profiling currently only works in the ARM simulator +if (!getBuildConfiguration()["arm-simulator"]) + throw "TestComplete"; + +enableGeckoProfiling(); +enableSingleStepProfiling(); + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("Debugger(parent).onExceptionUnwind = function () {};"); + +let module = new WebAssembly.Module(wasmTextToBinary(` + (module + (import "a" "b" (func $imp (result i32))) + (memory 1 1) + (table 2 2 anyfunc) + (elem (i32.const 0) $imp $def) + (func $def (result i32) (i32.load (i32.const 0))) + (type $v2i (func (result i32))) + (func $call (param i32) (result i32) (call_indirect (type $v2i) (get_local 0))) + (export "call" (func $call)) + ) +`)); + +let instance = new WebAssembly.Instance(module, { + a: { b: function () { throw "test"; } } +}); + +try { + instance.exports.call(0); + assertEq(false, true); +} catch (e) { + assertEq(e, "test"); +} + +disableGeckoProfiling(); +throw "TestComplete"; diff --git a/js/src/jit-test/tests/debug/bug1330489.js b/js/src/jit-test/tests/debug/bug1330489.js new file mode 100644 index 0000000000..79017fa301 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1330489.js @@ -0,0 +1,36 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; error: TestComplete + +load(libdir + "asserts.js"); + +if (!wasmDebuggingEnabled()) + throw "TestComplete"; + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("Debugger(parent).onExceptionUnwind = function () {};"); + +let module = new WebAssembly.Module(wasmTextToBinary(` + (module + (import "a" "b" (func $imp (result i32))) + (memory 1 1) + (table 2 2 anyfunc) + (elem (i32.const 0) $imp $def) + (func $def (result i32) (i32.load (i32.const 0))) + (type $v2i (func (result i32))) + (func $call (param i32) (result i32) (call_indirect (type $v2i) (get_local 0))) + (export "call" (func $call)) + ) +`)); + +let instance = new WebAssembly.Instance(module, { + a: { b: function () { throw "test"; } } +}); + +try { + instance.exports.call(0); + assertEq(false, true); +} catch (e) { + assertEq(e, "test"); +} + +throw "TestComplete"; diff --git a/js/src/jit-test/tests/debug/bug1330491.js b/js/src/jit-test/tests/debug/bug1330491.js new file mode 100644 index 0000000000..7fff829ab0 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1330491.js @@ -0,0 +1,21 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; error: TestComplete + +if (!wasmDebuggingEnabled()) + throw "TestComplete"; + +(function createShortLivedDebugger() { + var g = newGlobal({newCompartment: true}); + g.debuggeeGlobal = this; + g.eval("(" + function () { + dbg = new Debugger(debuggeeGlobal); + } + ")();"); +})(); + +let module = new WebAssembly.Module(wasmTextToBinary('(module (func))')); +new WebAssembly.Instance(module); + +gcslice(1000000); + +new WebAssembly.Instance(module); + +throw "TestComplete"; diff --git a/js/src/jit-test/tests/debug/bug1331064.js b/js/src/jit-test/tests/debug/bug1331064.js new file mode 100644 index 0000000000..4ade2305b1 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1331064.js @@ -0,0 +1,19 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; exitstatus: 3; skip-if: !wasmDebuggingEnabled() + +load(libdir + "asserts.js"); + +var g = newGlobal(); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () { some_error; };"); + +var module = new WebAssembly.Module(wasmTextToBinary(` +(module + (import $imp "a" "b" (result i32)) + (func $call (result i32) (call 0)) + (export "call" $call) +)`)); + +var instance = new WebAssembly.Instance(module, { a: { b: () => { + some_other_error; +}}}); +instance.exports.call(); diff --git a/js/src/jit-test/tests/debug/bug1331592.js b/js/src/jit-test/tests/debug/bug1331592.js new file mode 100644 index 0000000000..6282083de2 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1331592.js @@ -0,0 +1,28 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; error: TestComplete + +if (!wasmDebuggingEnabled()) + throw "TestComplete"; + +var module = new WebAssembly.Module(wasmTextToBinary(` + (module + (import "global" "func" (func (result i32))) + (func (export "func_0") (result i32) + call 0 ;; calls the import, which is func #0 + ) + ) +`)); + +var dbg; +(function (global) { + var dbgGlobal = newGlobal({newCompartment: true}); + dbg = new dbgGlobal.Debugger(); + dbg.addDebuggee(global); +})(this); + +var instance = new WebAssembly.Instance(module, { global: { func: () => { + var frame = dbg.getNewestFrame().older; + frame.eval("some_error"); +}}}); +instance.exports.func_0(); + +throw "TestComplete"; diff --git a/js/src/jit-test/tests/debug/bug1332493.js b/js/src/jit-test/tests/debug/bug1332493.js new file mode 100644 index 0000000000..5b3ce5dda2 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1332493.js @@ -0,0 +1,14 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; exitstatus: 3; skip-if: !wasmDebuggingEnabled() +// Checking in debug frame is initialized properly during stack overflow. + +var dbg; +(function () { dbg = new (newGlobal().Debugger)(this); })(); + +var m = new WebAssembly.Module(wasmTextToBinary(`(module + (import "a" "b" (result f64)) + ;; function that allocated large space for locals on the stack + (func (export "f") ${Array(200).join("(param f64)")} (result f64) (call 0)) +)`)); +var f = () => i.exports.f(); +var i = new WebAssembly.Instance(m, {a: {b: f}}); +f(); diff --git a/js/src/jit-test/tests/debug/bug1343579.js b/js/src/jit-test/tests/debug/bug1343579.js new file mode 100644 index 0000000000..3cefa8b056 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1343579.js @@ -0,0 +1,26 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() +// Checking if Debugger.Script.isInCatchScope return false for wasm. + +load(libdir + "wasm.js"); + +var results; +wasmRunWithDebugger( + '(module (memory 1) ' + + '(func $func0 i32.const 1000000 i32.load drop) ' + + '(func (export "test") call $func0))', + undefined, + function ({dbg, wasmScript}) { + results = []; + dbg.onExceptionUnwind = function (frame, value) { + if (frame.type != 'wasmcall') return; + var result = wasmScript.isInCatchScope(frame.offset); + results.push(result); + }; + }, + function ({error}) { + assertEq(error !== undefined, true); + assertEq(results.length, 2); + assertEq(results[0], false); + assertEq(results[1], false); + } +); diff --git a/js/src/jit-test/tests/debug/bug1351059.js b/js/src/jit-test/tests/debug/bug1351059.js new file mode 100644 index 0000000000..c21595e6f3 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1351059.js @@ -0,0 +1,22 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +// Tests that onEnterFrame events are enabled when Debugger callbacks set +// before Instance creation. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.onEnterFrameCalled = false; +g.eval(` + var dbg = new Debugger(parent); + dbg.onEnterFrame = frame => { + onEnterFrameCalled = true; + }; +`); +var i = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary(` + (module (func (export "f"))) +`))); +i.exports.f(); + +assertEq(g.onEnterFrameCalled, true); diff --git a/js/src/jit-test/tests/debug/bug1353356.js b/js/src/jit-test/tests/debug/bug1353356.js new file mode 100644 index 0000000000..3641f835da --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1353356.js @@ -0,0 +1,65 @@ +// |jit-test| allow-oom; --fuzzing-safe + +var lfLogBuffer = ` +//corefuzz-dcd-endofdata +//corefuzz-dcd-endofdata +//corefuzz-dcd-endofdata + setJitCompilerOption("ion.warmup.trigger", 4); + var g = newGlobal(); + g.debuggeeGlobal = this; + g.eval("(" + function () { + dbg = new Debugger(debuggeeGlobal); + dbg.onExceptionUnwind = function (frame, exc) { + var s = '!'; + for (var f = frame; f; f = f.older) + debuggeeGlobal.log += s; + }; + } + ")();"); + j('Number.prototype.toString.call([])'); +//corefuzz-dcd-endofdata +//corefuzz-dcd-endofdata +//corefuzz-dcd-endofdata +//corefuzz-dcd-selectmode 4 +//corefuzz-dcd-endofdata +} +//corefuzz-dcd-endofdata +//corefuzz-dcd-selectmode 5 +//corefuzz-dcd-endofdata +oomTest(() => i({ + new : (true ), + thisprops: true +})); +`; +lfLogBuffer = lfLogBuffer.split('\n'); +var lfRunTypeId = -1; +var lfCodeBuffer = ""; +while (true) { + var line = lfLogBuffer.shift(); + if (line == null) { + break; + } else if (line == "//corefuzz-dcd-endofdata") { + loadFile(lfCodeBuffer); + lfCodeBuffer = ""; + loadFile(line); + } else { + lfCodeBuffer += line + "\n"; + } +} +if (lfCodeBuffer) loadFile(lfCodeBuffer); +function loadFile(lfVarx) { + try { + if (lfVarx.indexOf("//corefuzz-dcd-selectmode ") === 0) { + lfRunTypeId = parseInt(lfVarx.split(" ")[1]) % 6; + } else { + switch (lfRunTypeId) { + case 4: + oomTest(function() { + let m = parseModule(lfVarx); + }); + break; + default: + evaluate(lfVarx); + } + } + } catch (lfVare) {} +} diff --git a/js/src/jit-test/tests/debug/bug1363233.js b/js/src/jit-test/tests/debug/bug1363233.js new file mode 100644 index 0000000000..feacc65aff --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1363233.js @@ -0,0 +1,14 @@ +// |jit-test| error: SyntaxError; +g = newGlobal({newCompartment: true}); +dbg = new Debugger; +setInterruptCallback(function () { + dbg.addDebuggee(g); + dbg.getNewestFrame(); + return true; +}); +g.eval("" + function f() { + for (var i = 0; i < 1; evaluate("class h { constructor() {} }")) { + interruptIf(1); + } +}); +g.f(); diff --git a/js/src/jit-test/tests/debug/bug1368736.js b/js/src/jit-test/tests/debug/bug1368736.js new file mode 100644 index 0000000000..e61a202ccb --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1368736.js @@ -0,0 +1,19 @@ +g = newGlobal({newCompartment: true}); +hits = 0; +Debugger(g).onDebuggerStatement = function(frame) { + // Set a breakpoint at the JSOP_AFTERYIELD op. + frame.script.setBreakpoint(117, {hit: function() { hits++; }}); +} +g.eval(` +function* range() { + eval(""); + debugger; + for (var i = 0; i < 3; i++) { + yield i; + } +} +var iter = range(); +for (var i = 0; i < 3; i++) + assertEq(iter.next().value, i); +`); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/bug1370905.js b/js/src/jit-test/tests/debug/bug1370905.js new file mode 100644 index 0000000000..8f8143132e --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1370905.js @@ -0,0 +1,15 @@ +// |jit-test| allow-oom; skip-if: !('oomTest' in this) + +function x() { + var global = newGlobal({sameZoneAs: this}); + global.eval('function f() { debugger; }'); + var debug = new Debugger(global); + var foo; + debug.onDebuggerStatement = function(frame) { + foo = frame.arguments[0]; + return null; + }; + global.eval('f(0)'); +} + +oomTest(x, false); diff --git a/js/src/jit-test/tests/debug/bug1375447.js b/js/src/jit-test/tests/debug/bug1375447.js new file mode 100644 index 0000000000..5801a150ed --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1375447.js @@ -0,0 +1,18 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.eval(` +var line0 = Error().lineNumber; +function f() { + try { + throw 4; + } catch(e) {} +} +`); +var script = gw.getOwnPropertyDescriptor("f").value.script; +var handler = { + hit: function() {} +}; +var offs = script.getLineOffsets(g.line0 + 4); +for (var i = 0; i < offs.length; i++) script.setBreakpoint(offs[i], handler); +assertEq(g.f(), undefined); diff --git a/js/src/jit-test/tests/debug/bug1385843.js b/js/src/jit-test/tests/debug/bug1385843.js new file mode 100644 index 0000000000..cc34a54488 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1385843.js @@ -0,0 +1,21 @@ +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.count = 0; +g.eval("(" + function() { + var dbg = new Debugger(parent); + dbg.onEnterFrame = function(frame) { + if (count === 5) + dbg.onEnterFrame = undefined; + count++; + var ex = frame.eval("this").throw.unsafeDereference(); + assertEq(ex.message.includes("call super constructor"), true); + } +} + ")()"); +class Foo1 {}; +class Foo2 extends Foo1 { + constructor() { + super(); + } +}; +new Foo2(); +assertEq(g.count, 6); diff --git a/js/src/jit-test/tests/debug/bug1397049.js b/js/src/jit-test/tests/debug/bug1397049.js new file mode 100644 index 0000000000..484fc2a178 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1397049.js @@ -0,0 +1,40 @@ +// Run debugger in its own global +let g = newGlobal({newCompartment: true}); +g.target = this; +g.evaluate(` + let d = new Debugger; + let gw = d.addDebuggee(target); + + d.onDebuggerStatement = function(frame) + { + frame = frame.older; + + let res = frame.eval("this"); + assertEq(res.return, frame.this); + + res = frame.evalWithBindings("this", {x:42}); + assertEq(res.return, frame.this); + } +`); + +// Debugger statement affects parse so hide in another function +function brk() { debugger; } + +function f1() { + var temp = "string"; + brk(); +} + +function f2() { + let temp = "string"; + brk(); +} + +function f3() { + const temp = "string"; + brk(); +} + +f1.call({}); +f2.call({}); +f3.call({}); diff --git a/js/src/jit-test/tests/debug/bug1397385.js b/js/src/jit-test/tests/debug/bug1397385.js new file mode 100644 index 0000000000..eea2c0c01e --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1397385.js @@ -0,0 +1,16 @@ +var g = newGlobal({newCompartment: true}); + +g.evaluate(` + function testInnerFun(defaultArg = 1) { + function innerFun(expectedThis) { return this; } + h(); + return innerFun; // To prevent the JIT from optimizing out innerFun. + } +`); + +g.h = function () { + var res = (new Debugger(g)).getNewestFrame().eval('assertEq(innerFun(), this)'); + assertEq("return" in res, true); +} + +g.testInnerFun(); diff --git a/js/src/jit-test/tests/debug/bug1404710.js b/js/src/jit-test/tests/debug/bug1404710.js new file mode 100644 index 0000000000..78a8bbb5b8 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1404710.js @@ -0,0 +1,10 @@ +// |jit-test| skip-if: !('stackTest' in this) +stackTest(new Function(` + var g = newGlobal(); + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function (frame) { + frame.evalWithBindings("x", {x: 2}).return; + }; + g.eval("function f(y) { debugger; }"); + g.f(3); +`), false); diff --git a/js/src/jit-test/tests/debug/bug1406437.js b/js/src/jit-test/tests/debug/bug1406437.js new file mode 100644 index 0000000000..4085f80a2d --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1406437.js @@ -0,0 +1,5 @@ +var g = newGlobal({newCompartment: true}); +cloneAndExecuteScript('function f() {}', g); +var dbg = new Debugger; +var dg = dbg.addDebuggee(g); +dg.getOwnPropertyDescriptor('f').value.script.source; diff --git a/js/src/jit-test/tests/debug/bug1417961.js b/js/src/jit-test/tests/debug/bug1417961.js new file mode 100644 index 0000000000..a10f709f71 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1417961.js @@ -0,0 +1,31 @@ +var evalInFrame = (function evalInFrame(global) { + var dbgGlobal = newGlobal({newCompartment: true}); + var dbg = new dbgGlobal.Debugger(); + return function evalInFrame(upCount, code) { + dbg.addDebuggee(global); + var frame = dbg.getNewestFrame().older; + for (var i = 0; i < upCount; i++) + frame = frame.older; + var completion = frame.eval(code); + if (completion.throw) + throw 1; + }; +})(this); +function f() { + for (var i = 0; i < 10; - i) + g(); +} +function h() { + evalInFrame(0, "a.push(1)"); + evalInFrame(1, "a.push(2)"); +} +function g() { + h(); +} +try { + f(); +} catch(e) {} +var a = []; +for (var i = 0; i < 3; i++) + g(); +assertEq(a.length, 6); diff --git a/js/src/jit-test/tests/debug/bug1432764.js b/js/src/jit-test/tests/debug/bug1432764.js new file mode 100644 index 0000000000..e4f7a507fb --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1432764.js @@ -0,0 +1,16 @@ +// |jit-test| error: uncaught exception +g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval(` + Debugger(parent).onExceptionUnwind = function(frame) { frame.older }; +`); + +var handler = { + has: function(tgt, key) { if (key == 'throw') { throw null; } } +}; + +var proxy = new Proxy({}, handler); + +for (let k of ['foo', 'throw']) { + k in proxy; +} diff --git a/js/src/jit-test/tests/debug/bug1434391.js b/js/src/jit-test/tests/debug/bug1434391.js new file mode 100644 index 0000000000..86efe7970b --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1434391.js @@ -0,0 +1,8 @@ +// |jit-test| skip-if: !('oomTest' in this) + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); +oomTest(function () { + assertEq(gw.executeInGlobal("(42).toString(0)").throw.errorMessageName, "JSMSG_BAD_RADIX"); +}); diff --git a/js/src/jit-test/tests/debug/bug1437537.js b/js/src/jit-test/tests/debug/bug1437537.js new file mode 100644 index 0000000000..ecee9b120c --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1437537.js @@ -0,0 +1,18 @@ +// Don't assert when pausing for onStep at JSOP_EXCEPTION. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +let f = g.Function(`try { throw new Error; } catch (e) { return 'natural'; }`); + +let limit = -1; +dbg.onEnterFrame = function (frame) { + frame.onStep = function () { + if (this.offset > limit) { + limit = this.offset; + return { return: 'forced' }; + } + }; +}; + +while (f() === 'forced') { +} diff --git a/js/src/jit-test/tests/debug/bug1480390.js b/js/src/jit-test/tests/debug/bug1480390.js new file mode 100644 index 0000000000..7ad90c67cd --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1480390.js @@ -0,0 +1,35 @@ +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () {};"); + +function* wrapNoThrow() { + let iter = { + [Symbol.iterator]() { + return this; + }, + next() { + return { value: 10, done: false }; + }, + return() { return "invalid return value" } + }; + for (const i of iter) + yield i; +} + +function foo() { + for (var i of [1,2,3]) { + for (var j of [4,5,6]) { + try { + for (const i of wrapNoThrow()) break; + } catch (e) {} + } + for (var j of [7,8,9]) { + } + } +} + +for (var i = 0; i < 10; i++) { + try { + foo(); + } catch(e) {} +} diff --git a/js/src/jit-test/tests/debug/bug1488163.js b/js/src/jit-test/tests/debug/bug1488163.js new file mode 100644 index 0000000000..9fe9d783fb --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1488163.js @@ -0,0 +1,17 @@ +async function f() { + // Enough variables that SM will eventually decide to create a lookup table + // for this scope. + var q0, q1, q2, q3, q4, q5, q6, q7, q8, q9; +} + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval(` + var dbg = new Debugger(parent); + dbg.onEnterFrame = frame => {}; +`); + +// Fragile: Trigger Shape::hashify() for the shape of the environment in f +// under a call to GetGeneratorObjectForFrame from the Debugger. +for (let i = 0; i < 3; i++) + f(); diff --git a/js/src/jit-test/tests/debug/bug1516958.js b/js/src/jit-test/tests/debug/bug1516958.js new file mode 100644 index 0000000000..24ba0b50ef --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1516958.js @@ -0,0 +1,3 @@ +(new (newGlobal({newCompartment:true}).Debugger)(this)).memory.trackingAllocationSites = true; +nukeAllCCWs(); +new Date(); diff --git a/js/src/jit-test/tests/debug/bug1557343-2.js b/js/src/jit-test/tests/debug/bug1557343-2.js new file mode 100644 index 0000000000..7eb340ff6d --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1557343-2.js @@ -0,0 +1,30 @@ +// |jit-test| --no-ggc +// Don't crash when two Debugger.Frames refer to the same generator script, and +// then one returns. + +var g = newGlobal({ newCompartment: true }); +g.eval(` + function* gen() { + debugger; + yield 1; + } + + function use_gen() { + var gen1 = gen(); + var gen2 = gen(); + + gen1.next(); + gen2.next(); + + gen1.next(); + gen2.next(); + } +`); + +var dbg = new Debugger(g); +var frame; +dbg.onDebuggerStatement = f => { + frame = f; +}; + +g.use_gen(); diff --git a/js/src/jit-test/tests/debug/bug1557343.js b/js/src/jit-test/tests/debug/bug1557343.js new file mode 100644 index 0000000000..964a6d8f15 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1557343.js @@ -0,0 +1,25 @@ +// |jit-test| --no-ggc + +var g = newGlobal({ newCompartment: true }); +g.eval(` + function* g3() { + debugger; + } + + function next() { + g3().next(); + } + + function ggg() { + gc(gc); + } +`); + +var dbg = new Debugger(g); +var frame; +dbg.onDebuggerStatement = f => { + frame = f; +}; + +g.next(); +g.ggg(); diff --git a/js/src/jit-test/tests/debug/bug1563051.js b/js/src/jit-test/tests/debug/bug1563051.js new file mode 100644 index 0000000000..cf3b40019a --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1563051.js @@ -0,0 +1,11 @@ +// |jit-test| error:123 +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +dbg.addDebuggee(g); +dbg.onEnterFrame = function(frame) { + frame.onPop = function() { + dbg.removeDebuggee(g); + throw 123; + } +} +g.eval("(function() {})()"); diff --git a/js/src/jit-test/tests/debug/bug1586762.js b/js/src/jit-test/tests/debug/bug1586762.js new file mode 100644 index 0000000000..416073f756 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1586762.js @@ -0,0 +1,9 @@ +setJitCompilerOption("baseline.warmup.trigger", 0); +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +dbg.addDebuggee(g); +g.eval("" + function f() { return 7; }); +dbg.onEnterFrame = function() { + dbg.removeDebuggee(g); +} +assertEq(g.f(), 7); diff --git a/js/src/jit-test/tests/debug/bug1591342.js b/js/src/jit-test/tests/debug/bug1591342.js new file mode 100644 index 0000000000..37c7b9944b --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1591342.js @@ -0,0 +1,13 @@ +// |jit-test| error: Permission denied to access object + +// jsfunfuzz-generated +newGlobal({ sameCompartmentAs: this }); +nukeAllCCWs(); +// Adapted from randomly chosen testcase: js/src/jit-test/tests/debug/clear-old-analyses-02.js +var g = newGlobal({ + newCompartment: true +}); +var dbg = Debugger(); +gw = dbg.addDebuggee(g); +g.eval("" + function fib() {}); +gw.makeDebuggeeValue(g.fib).script.setBreakpoint(0, {}); diff --git a/js/src/jit-test/tests/debug/bug1602392.js b/js/src/jit-test/tests/debug/bug1602392.js new file mode 100644 index 0000000000..e3e71bfc63 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1602392.js @@ -0,0 +1,19 @@ +// |jit-test| error:ReferenceError: iter is not defined +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () {};"); +function* f1() { + for (const x of iter) { + yield x; + } +} +function f2() { + for (var i of [1, 2, 3]) { + for (var j of [4, 5, 6]) { + for (const k of f1()) { + break; + } + } + } +} +f2(); diff --git a/js/src/jit-test/tests/debug/bug1644699-terminated-generator.js b/js/src/jit-test/tests/debug/bug1644699-terminated-generator.js new file mode 100644 index 0000000000..dc87ecda8c --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1644699-terminated-generator.js @@ -0,0 +1,79 @@ +// |jit-test| exitstatus:6 +// Ensure that a frame terminated due to an interrupt in the generator +// builtin will properly be treated as terminated. + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); +g.eval(` +var done = false; +async function* f() { + await null; + await null; + await null; + await null; + done = true; +} +`); + +dbg.onEnterFrame = f => { + frame = f; + dbg.onEnterFrame = undefined; +}; + +setInterruptCallback(function() { + const stack = saveStack(); + + // We want to explicitly terminate inside the AsyncGeneratorNext builtin + // when it tries to resume execution at the 'await' in the async generator. + // Terminating inside AsyncGeneratorNext causes the generator to be closed, + // and for this test case we specifically need that to happen without + // entering the async generator frame because we're aiming to trigger the + // case where DebugAPI::onLeaveFrame does not having the opportunity to + // clean up the generator information associated with the Debugger.Frame. + if ( + stack.parent && + stack.parent.source === "self-hosted" && + stack.parent.functionDisplayName === "AsyncGeneratorNext" && + stack.parent.parent && + stack.parent.parent.source === stack.source && + stack.parent.parent.line === DRAIN_QUEUE_LINE + ) { + return false; + } + + // Keep interrupting until we find the right place. + interruptIf(true); + return true; +}); + +// Run the async generator and suspend at the first await. +const it = g.f(); +let promise = it.next(); + +// Queue the interrupt so that it will start trying to terminate inside the +// generator builtin. +interruptIf(true); + +// Explicitly drain the queue to run the async generator to completion. +const DRAIN_QUEUE_LINE = saveStack().line + 1; +drainJobQueue(); + +let threw = false; +try { + // In the original testcase for this bug, this call would cause + // an assertion failure because the generator was closed. + frame.environment; +} catch (err) { + threw = true; +} +assertEq(threw, true); + +// The frame here still has a GeneratorInfo datastructure because the +// termination interrupt will cause the generator to be closed without +// clearing that data. The frame must still be treated as terminated in +// this case in order for the Debugger API to behave consistently. +assertEq(frame.terminated, true); + +// We should never reach the end of the async generator because it will +// have been terminated. +assertEq(g.done, false); diff --git a/js/src/jit-test/tests/debug/bug1647309.js b/js/src/jit-test/tests/debug/bug1647309.js new file mode 100644 index 0000000000..e4ffcfc349 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1647309.js @@ -0,0 +1,18 @@ +// |jit-test| skip-if: !('oomTest' in this) + +const g = newGlobal({ newCompartment: true }); +const dbg = new Debugger(g); + +// Define async generator in debuggee compartment. +g.eval("async function* f() { }"); + +// Use onEnterFrame hook to create generatorFrames entry. +dbg.onEnterFrame = () => {}; + +// Trigger failure in AsyncGeneratorNext. +ignoreUnhandledRejections(); +oomTest(function() { g.f().next(); }); + +// Trigger DebugAPI::onSingleStep to check generatorFrames. +dbg.onDebuggerStatement = frame => { frame.onStep = () => {}; } +g.eval("debugger"); diff --git a/js/src/jit-test/tests/debug/bug1675755-forceLexicalInitializationByName.js b/js/src/jit-test/tests/debug/bug1675755-forceLexicalInitializationByName.js new file mode 100644 index 0000000000..14b1af27fc --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1675755-forceLexicalInitializationByName.js @@ -0,0 +1,38 @@ +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +let errorOne, errorTwo; + +function evalErrorStr(global, evalString) { + try { + global.evaluate(evalString); + return undefined; + } catch (e) { + return e.toString(); + } +} + + +assertEq(evalErrorStr(g, "let y = IDONTEXIST;"), "ReferenceError: IDONTEXIST is not defined"); +assertEq(evalErrorStr(g, "y = 1;"), + "ReferenceError: can't access lexical declaration 'y' before initialization"); + +const LINEAR_SEARCHES_MAX = 3; +const SHAPE_CACHE_MIN_ENTRIES = 3; + +// Give the lexical enough properties so that it isBigEnoughForAShapeTable(). +for (i in [...Array(SHAPE_CACHE_MIN_ENTRIES)]) + gw.executeInGlobal(`let x${i} = 1`); + +// Search for y just enough times to cause the next search to trigger +// Shape::cachify(). +for (i in [...Array(LINEAR_SEARCHES_MAX - 1)]) + gw.executeInGlobal("y"); + +// Here we flip the uninitialized binding to undefined. But in the process, we +// will do the lookup on y that will trigger Shape::cachify. Verify that it +// happens in the correct compartment. +assertEq(gw.forceLexicalInitializationByName("y"), true); diff --git a/js/src/jit-test/tests/debug/bug1684821.js b/js/src/jit-test/tests/debug/bug1684821.js new file mode 100644 index 0000000000..dfbefd9962 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug1684821.js @@ -0,0 +1,26 @@ +let g = newGlobal({newCompartment: true}); +let d = new Debugger(g); + +d.onDebuggerStatement = function (frame) { + frame.environment; +}; + +g.evaluate(` + function * foo() { + // Force CallObject + LexicalEnvironmentObject + let x; + let y = () => x; + + // Force DebuggerEnvironment + debugger; + + // Force suspend and frame snapshot + yield; + + // Popping this frame will trigger a second snapshot + } +`) + +let x = g.foo(); +x.next(); +x.next(); diff --git a/js/src/jit-test/tests/debug/bug911065.js b/js/src/jit-test/tests/debug/bug911065.js new file mode 100644 index 0000000000..bec87b9344 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug911065.js @@ -0,0 +1,34 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); + +g.eval(` // 1 +var line0 = Error().lineNumber; // 2 +function f() { // 3 + for (var x of [0]) { // 4 + if (true == false) // 5 + return false; // 6, aka line0 + 4 + } // 7 + return true; // 8 +} // 9 +`); // 10 + +if (g.dis) + g.dis(g.f); + +var script = gw.getOwnPropertyDescriptor("f").value.script; + +print("Debugger's view:"); +print("----------------"); +for (var i = script.startLine; i <= script.startLine + script.lineCount; i++) { + print("Line " + i + ": " + JSON.stringify(script.getLineOffsets(i))); +} + +var hits = 0; +var handler = {hit: function () { hits++; }}; +var offs = script.getLineOffsets(g.line0 + 4); +for (var i = 0; i < offs.length; i++) + script.setBreakpoint(offs[i], handler); + +assertEq(g.f(), true); +assertEq(hits, 0); diff --git a/js/src/jit-test/tests/debug/bug967039.js b/js/src/jit-test/tests/debug/bug967039.js new file mode 100644 index 0000000000..fc86902c64 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug967039.js @@ -0,0 +1,6 @@ +var g1 = newGlobal({newCompartment: true}); +var dbg = Debugger(g1); +g1.dbg = dbg; +g1.eval("function foo() { dbg.removeDebuggee(this); }"); +g1.eval("function f() { try { throw 3; } catch(e) { foo(); } }\n"); +g1.f(); diff --git a/js/src/jit-test/tests/debug/bug973566.js b/js/src/jit-test/tests/debug/bug973566.js new file mode 100644 index 0000000000..5417235d93 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug973566.js @@ -0,0 +1,7 @@ +Object.prototype[1] = 'peek'; +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onEnterFrame = function (frame) { + var lines = frame.script.getAllOffsets(); +}; +g.eval("1;"); diff --git a/js/src/jit-test/tests/debug/bug980585.js b/js/src/jit-test/tests/debug/bug980585.js new file mode 100644 index 0000000000..30d8c44e52 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug980585.js @@ -0,0 +1,10 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +try { + g.eval("function f() { [1].map(function () {}); const x = 42; x = 43; } f();"); +} catch (e) { + // Ignore the syntax error. +} + +dbg.findScripts(); diff --git a/js/src/jit-test/tests/debug/bug999655.js b/js/src/jit-test/tests/debug/bug999655.js new file mode 100644 index 0000000000..756ef8d0b3 --- /dev/null +++ b/js/src/jit-test/tests/debug/bug999655.js @@ -0,0 +1,11 @@ + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onNewScript = function(script) { + fscript = script.getChildScripts()[0]; +} +g.eval("function f(x) { arguments[0] = 3; return x }"); +fscript.setBreakpoint(0, {hit:function(frame) { + assertEq(frame.arguments[0], 1); +}}); +g.f(1); diff --git a/js/src/jit-test/tests/debug/class-01.js b/js/src/jit-test/tests/debug/class-01.js new file mode 100644 index 0000000000..66f390bd4e --- /dev/null +++ b/js/src/jit-test/tests/debug/class-01.js @@ -0,0 +1,20 @@ +// |jit-test| error: TypeError + +let g = newGlobal(); +let dbg = Debugger(g); + +let forceException = g.eval(` + (class extends class {} { + // Calling this will throw for using |this| uninitialized. + constructor() { } + }) +`); + +dbg.onExceptionUnwind = function() { + return { + // Force the return of an illegal value. + return: 1 + } +} + +new forceException; diff --git a/js/src/jit-test/tests/debug/class-02.js b/js/src/jit-test/tests/debug/class-02.js new file mode 100644 index 0000000000..3821eea26e --- /dev/null +++ b/js/src/jit-test/tests/debug/class-02.js @@ -0,0 +1,20 @@ +// |jit-test| error: TypeError + +let g = newGlobal(); +let dbg = Debugger(g); + +let forceException = g.eval(` + (class extends class {} { + // Calling this will return a primitive immediately. + constructor() { return {}; } + }) +`); + +dbg.onEnterFrame = function() { + return { + // Force the return of an illegal value. + return: 1 + } +} + +new forceException; diff --git a/js/src/jit-test/tests/debug/class-03.js b/js/src/jit-test/tests/debug/class-03.js new file mode 100644 index 0000000000..da1a4f4345 --- /dev/null +++ b/js/src/jit-test/tests/debug/class-03.js @@ -0,0 +1,23 @@ +// |jit-test| error: TypeError + +let g = newGlobal(); +let dbg = Debugger(g); + +let forceException = g.eval(` + (class extends class {} { + // Calling this will return a primitive immediately. + constructor() { + debugger; + return {}; + } + }) +`); + +dbg.onDebuggerStatement = function() { + return { + // Force the return of an illegal value. + return: 1 + } +} + +new forceException; diff --git a/js/src/jit-test/tests/debug/class-04.js b/js/src/jit-test/tests/debug/class-04.js new file mode 100644 index 0000000000..5f772e4d38 --- /dev/null +++ b/js/src/jit-test/tests/debug/class-04.js @@ -0,0 +1,22 @@ +// |jit-test| error: TypeError + +let g = newGlobal(); +let dbg = Debugger(g); + +let forceException = g.eval(` + (class extends class {} { + // Calling this will return a primitive on return. + constructor() { return {}; } + }) +`); + +dbg.onEnterFrame = function(f) { + f.onPop = function() { + return { + // Force the return of an illegal value. + return: 1 + } + } +} + +new forceException; diff --git a/js/src/jit-test/tests/debug/class-05.js b/js/src/jit-test/tests/debug/class-05.js new file mode 100644 index 0000000000..4af7c23f83 --- /dev/null +++ b/js/src/jit-test/tests/debug/class-05.js @@ -0,0 +1,31 @@ +// |jit-test| error: TypeError + +let g = newGlobal(); +let dbg = Debugger(g); + +let forceException = g.eval(` + (class extends class {} { + // Calling this will return a primitive immediately. + constructor() { + debugger; + return {}; + } + }) +`); + +let handler = { + hit() { + return { + // Force the return of an illegal value. + return: 1 + } + } +}; + +dbg.onDebuggerStatement = function(frame) { + var line0 = frame.script.getOffsetLocation(frame.offset).lineNumber; + var offs = frame.script.getLineOffsets(line0 + 1); + frame.script.setBreakpoint(offs[0], handler); +} + +new forceException; diff --git a/js/src/jit-test/tests/debug/class-06.js b/js/src/jit-test/tests/debug/class-06.js new file mode 100644 index 0000000000..fc6ebdbaa9 --- /dev/null +++ b/js/src/jit-test/tests/debug/class-06.js @@ -0,0 +1,22 @@ +// |jit-test| error: TypeError + +let g = newGlobal(); +let dbg = Debugger(g); + +let forceException = g.eval(` + (class extends class {} { + // Calling this will return a primitive immediately. + constructor() { return {}; } + }) +`); + +dbg.onEnterFrame = function(f) { + f.onStep = function() { + return { + // Force the return of an illegal value. + return: 1 + } + } +} + +new forceException; diff --git a/js/src/jit-test/tests/debug/class-07.js b/js/src/jit-test/tests/debug/class-07.js new file mode 100644 index 0000000000..a6a69cb79f --- /dev/null +++ b/js/src/jit-test/tests/debug/class-07.js @@ -0,0 +1,21 @@ +// |jit-test| error: ReferenceError + +let g = newGlobal({newCompartment: true}); +let dbg = Debugger(g); + +let forceException = g.eval(` + (class extends class {} { + // Calling this will return a primitive immediately. + constructor() { return {}; } + }) +`); + +dbg.onEnterFrame = function() { + return { + // Force the return undefined, which will throw for returning + // while |this| is still undefined. + return: undefined + } +} +print("break here"); +new forceException; diff --git a/js/src/jit-test/tests/debug/class-08.js b/js/src/jit-test/tests/debug/class-08.js new file mode 100644 index 0000000000..000f9ae67c --- /dev/null +++ b/js/src/jit-test/tests/debug/class-08.js @@ -0,0 +1,13 @@ +let g = newGlobal({newCompartment: true}); +let dbg = Debugger(g); +dbg.onDebuggerStatement = function() { + // Force the constructor to return undefined, which should be replaced with + // |this| if the latter has been initialized. + return { return: undefined }; +} + +assertEq(g.eval(` + new (class extends class {} { + constructor() { super(); this.foo = 42; debugger; } + }) +`).foo, 42); diff --git a/js/src/jit-test/tests/debug/class-default-constructor-01.js b/js/src/jit-test/tests/debug/class-default-constructor-01.js new file mode 100644 index 0000000000..0c12f29545 --- /dev/null +++ b/js/src/jit-test/tests/debug/class-default-constructor-01.js @@ -0,0 +1,34 @@ +// We should be able to retrieve the script of a class's default constructor. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); + +// Class definitions go in the global's lexical environment, so we can't use +// getOwnPropertyDescriptor or g.X to retrieve their constructor. +// +// Derived clasess use a different script from the self-hosted compartment, so +// check those too. +gDO.executeInGlobal(` // 1729 + class X {}; // 1730 + // 1731 + // 1732 + class Y extends X {}; // 1733 +`, { lineNumber: 1729 }); + +function check(name, text, startLine) { + print(`checking ${name}`); + var desc = gDO.executeInGlobal(name).return; + assertEq(desc.class, 'Function'); + assertEq(desc.name, name); + var script = desc.script; + assertEq(script instanceof Debugger.Script, true, + "default constructor's script should be available"); + assertEq(script.startLine, startLine, + "default constructor's starting line should be set"); + var source = script.source; + assertEq(source.text.substr(script.sourceStart, script.sourceLength), text); +} + +check('X', 'class X {}', 1730); +check('Y', 'class Y extends X {}', 1733); diff --git a/js/src/jit-test/tests/debug/clear-old-analyses-01.js b/js/src/jit-test/tests/debug/clear-old-analyses-01.js new file mode 100644 index 0000000000..4b580563ce --- /dev/null +++ b/js/src/jit-test/tests/debug/clear-old-analyses-01.js @@ -0,0 +1,38 @@ +// |jit-test| error:AllDone +// When we enter debug mode in a compartment, we must throw away all +// analyses in that compartment (debug mode affects the results of +// analysis, so they become out of date). This is true even when we would +// otherwise be retaining jit code and its related data structures for +// animation timing. + +if (typeof gcPreserveCode != "function") + throw('AllDone'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; + +g.eval("" + + function fib(n) { + var a = 0, b = 1; + while (n-- > 0) + b = b+a, a = b-a; + return b; + }); + +g.fib(20); // Cause g.fib to be jitted. This creates an analysis with + // debug mode off. + +gcPreserveCode(); // Tell the gc to preserve JIT code and analyses by + // default. A recent call to js::NotifyAnimationActivity + // could have a similar effect in real life. + +dbg.addDebuggee(g); // Put g in debug mode. This triggers a GC which must + // clear all analyses. In the original buggy code, we also + // release all of g's scripts' JIT code, leading to a + // recompilation the next time it was called. + +g.fib(20); // Run g.fib again, causing it to be re-jitted. If the + // original analysis is still present, JM will assert, + // because it is not in debug mode. + +throw('AllDone'); diff --git a/js/src/jit-test/tests/debug/clear-old-analyses-02.js b/js/src/jit-test/tests/debug/clear-old-analyses-02.js new file mode 100644 index 0000000000..4a963ec8a5 --- /dev/null +++ b/js/src/jit-test/tests/debug/clear-old-analyses-02.js @@ -0,0 +1,39 @@ +// |jit-test| error:AllDone +// When we leave debug mode in a compartment, we must throw away all +// analyses in that compartment (debug mode affects the results of +// analysis, so they become out of date). We cannot skip this step when +// there are debuggee frames on the stack. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval("" + + function fib(n) { + var a = 0, b = 1; + while (n-- > 0) + b = b+a, a = b-a; + return b; + }); + + +// Cause g.fib to be jitted. This creates an analysis with debug mode on. +g.fib(20); + +// Setting a breakpoint in g.f causes us to throw away the jit code, but +// not the analysis. +gw.makeDebuggeeValue(g.fib).script.setBreakpoint(0, { hit: function (f) { } }); + +// Take g out of debug mode, with debuggee code on the stack. In older +// code, this would not trigger a cleansing GC, so the script will +// retain its analysis. +dbg.onDebuggerStatement = function (f) { + dbg.removeDebuggee(g); +}; +g.eval('debugger'); + +// Run g.fib again, causing it to be re-jitted. If the original analysis is +// still present, JM will assert, because it is not in debug mode. +g.fib(20); + +throw('AllDone'); diff --git a/js/src/jit-test/tests/debug/dispatch-01.js b/js/src/jit-test/tests/debug/dispatch-01.js new file mode 100644 index 0000000000..a1f062c3f9 --- /dev/null +++ b/js/src/jit-test/tests/debug/dispatch-01.js @@ -0,0 +1,22 @@ +// Test removing hooks during dispatch. + +var g = newGlobal({newCompartment: true}); +var log = ''; + +function addDebug(n) { + for (var i = 0; i < n; i++) { + var dbg = new Debugger(g); + dbg.num = i; + dbg.onDebuggerStatement = function (stack) { + log += this.num + ', '; + this.enabled = false; + this.onDebuggerStatement = undefined; + gc(); + }; + } + dbg = null; +} + +addDebug(10); +g.eval("debugger;"); +assertEq(log, '0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '); diff --git a/js/src/jit-test/tests/debug/execution-observability-01.js b/js/src/jit-test/tests/debug/execution-observability-01.js new file mode 100644 index 0000000000..1f6dd13f04 --- /dev/null +++ b/js/src/jit-test/tests/debug/execution-observability-01.js @@ -0,0 +1,22 @@ +// For perf reasons we don't recompile all a debuggee global's scripts when +// Debugger no longer needs to observe all execution for that global. Test that +// things don't crash if we try to run a script with a BaselineScript that was +// compiled with debug instrumentation when the global is no longer a debuggee. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var counter = 0; +dbg.onDebuggerStatement = function (frame) { + counter++; + if (counter == 15) + dbg.onDebuggerStatement = undefined; +}; + +g.eval("" + function f() { + { + let inner = 42; + debugger; + inner++; + } +}); +g.eval("for (var i = 0; i < 20; i++) f()"); diff --git a/js/src/jit-test/tests/debug/execution-observability-02.js b/js/src/jit-test/tests/debug/execution-observability-02.js new file mode 100644 index 0000000000..61d943812f --- /dev/null +++ b/js/src/jit-test/tests/debug/execution-observability-02.js @@ -0,0 +1,15 @@ +// Test that baseline frames are marked as debuggee when resuming from +// throwing. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var hits = 0; +dbg.onEnterFrame = function (f) { hits++; }; + +try { + g.eval("for (c in (function*() { yield })()) h"); +} catch (e) { +} + +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/execution-observability-03.js b/js/src/jit-test/tests/debug/execution-observability-03.js new file mode 100644 index 0000000000..d330571ec9 --- /dev/null +++ b/js/src/jit-test/tests/debug/execution-observability-03.js @@ -0,0 +1,17 @@ +// Tests that bare callVMs (in the delprop below) are patched correctly. + +var o = {}; +var global = this; +var p = new Proxy(o, { + "deleteProperty": function (target, key) { + var g = newGlobal({newCompartment: true}); + g.parent = global; + g.eval("var dbg = new Debugger(parent); dbg.onEnterFrame = function(frame) {};"); + return true; + } +}); +function test() { + for (var i=0; i<100; i++) {} + assertEq(delete p.foo, true); +} +test(); diff --git a/js/src/jit-test/tests/debug/execution-observability-04.js b/js/src/jit-test/tests/debug/execution-observability-04.js new file mode 100644 index 0000000000..0e91e54371 --- /dev/null +++ b/js/src/jit-test/tests/debug/execution-observability-04.js @@ -0,0 +1,21 @@ +// Test that we can do debug mode OSR from the interrupt handler. + +var global = this; +var hits = 0; +setInterruptCallback(function() { + print("Interrupt!"); + hits++; + var g = newGlobal({newCompartment: true}); + g.parent = global; + g.eval("var dbg = new Debugger(parent); dbg.onEnterFrame = function(frame) {};"); + return true; +}); + +function f(x) { + if (x > 200) + return; + interruptIf(x == 100); + f(x + 1); +} +f(0); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/execution-observability-05.js b/js/src/jit-test/tests/debug/execution-observability-05.js new file mode 100644 index 0000000000..1ba39f54c1 --- /dev/null +++ b/js/src/jit-test/tests/debug/execution-observability-05.js @@ -0,0 +1,23 @@ +// Test that we can do debug mode OSR from the interrupt handler through an +// on->off->on cycle. + +var global = this; +var hits = 0; +setInterruptCallback(function() { + print("Interrupt!"); + hits++; + var g = newGlobal({newCompartment: true}); + g.parent = global; + g.eval("var dbg = new Debugger(parent); dbg.onEnterFrame = function(frame) {};"); + g.eval("dbg.removeDebuggee(parent);"); + return true; +}); + +function f(x) { + if (x > 200) + return; + interruptIf(x == 100); + f(x + 1); +} +f(0); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/execution-observability-06.js b/js/src/jit-test/tests/debug/execution-observability-06.js new file mode 100644 index 0000000000..fbc3913002 --- /dev/null +++ b/js/src/jit-test/tests/debug/execution-observability-06.js @@ -0,0 +1,24 @@ +// Test that OSR respect debuggeeness. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval("" + function f(c) { + if (c == 0) + return; + if (c == 2) + debugger; + f(c-1); + acc = 0; + for (var i = 0; i < 100; i++) + acc += i; +}); + +var log = ""; +dbg.onDebuggerStatement = function (frame) { + frame.onPop = function f() { log += "p"; } +}; + +g.eval("f(2)"); + +assertEq(log, "p"); diff --git a/js/src/jit-test/tests/debug/gc-01.js b/js/src/jit-test/tests/debug/gc-01.js new file mode 100644 index 0000000000..949c74b576 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-01.js @@ -0,0 +1,20 @@ +// Debuggers with enabled hooks should not be GC'd even if they are otherwise +// unreachable. + +var g = newGlobal({newCompartment: true}); +var actual = 0; +var expected = 0; + +function f() { + for (var i = 0; i < 20; i++) { + var dbg = new Debugger(g); + dbg.num = i; + dbg.onDebuggerStatement = function (stack) { actual += this.num; }; + expected += i; + } +} + +f(); +gc(); gc(); gc(); +g.eval("debugger;"); +assertEq(actual, expected); diff --git a/js/src/jit-test/tests/debug/gc-02.js b/js/src/jit-test/tests/debug/gc-02.js new file mode 100644 index 0000000000..0bc05f2d72 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-02.js @@ -0,0 +1,28 @@ +// Dispatching an event to a debugger must keep enough of it gc-alive to avoid +// crashing. + +var g = newGlobal({newCompartment: true}); +var hits; + +function addDebug() { + // The loop is here to defeat the conservative GC. :-\ + for (var i = 0; i < 4; i++) { + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function (stack) { + hits++; + this.enabled = false; + this.onDebuggerStatement = undefined; + gc(); + }; + if (i > 0) { + dbg.enabled = false; + dbg.onDebuggerStatement = undefined; + dbg = null; + } + } +} + +addDebug(); +hits = 0; +g.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/gc-03.js b/js/src/jit-test/tests/debug/gc-03.js new file mode 100644 index 0000000000..9c1b7c5f21 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-03.js @@ -0,0 +1,24 @@ +// Storing a property on a Debugger.Object protects it from GC as long as the +// referent is alive. + +var g = newGlobal({newCompartment: true}); +var N = g.N = 3; +var dbg = Debugger(g); + +var i = 0; +dbg.onDebuggerStatement = function (frame) { + frame.arguments[0].id = i++; +}; +g.eval("function f(x) { debugger; }"); +g.eval("var arr = [], j; for (j = 0; j < N; j++) arr[j] = {};"); +g.eval("for (j = 0; j < N; j++) f(arr[j]);"); +assertEq(i, N); + +gc(); gc(); + +i = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.arguments[0].id, i++) +} +g.eval("for (j = 0; j < N; j++) f(arr[j]);"); +assertEq(i, N); diff --git a/js/src/jit-test/tests/debug/gc-04.js b/js/src/jit-test/tests/debug/gc-04.js new file mode 100644 index 0000000000..206be25f47 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-04.js @@ -0,0 +1,25 @@ +// Storing a Debugger.Object as a key in a WeakMap protects it from GC as long as +// the referent is alive. + +var g = newGlobal({newCompartment: true}); +var N = g.N = 10; +var dbg = Debugger(g); +var cache = new WeakMap; + +var i = 0; +dbg.onDebuggerStatement = function (frame) { + cache.set(frame.arguments[0], i++); +}; +g.eval("function f(x) { debugger; }"); +g.eval("var arr = [], j; for (j = 0; j < N; j++) arr[j] = {};"); +g.eval("for (j = 0; j < N; j++) f(arr[j]);"); +assertEq(i, N); + +gc(); gc(); + +i = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(cache.get(frame.arguments[0]), i++) +}; +g.eval("for (j = 0; j < N; j++) f(arr[j]);"); +assertEq(i, N); diff --git a/js/src/jit-test/tests/debug/gc-05.js b/js/src/jit-test/tests/debug/gc-05.js new file mode 100644 index 0000000000..e42b2a7272 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-05.js @@ -0,0 +1,41 @@ +// If a Debugger survives its debuggee, its object cache must still be swept. + +var g2arr = []; // non-debuggee globals +var xarr = []; // debuggee objects + +var N = 4, M = 4; +for (var i = 0; i < N; i++) { + var g1 = newGlobal({newCompartment: true}); + g1.M = M; + var dbg = new Debugger(g1); + var g2 = g1.eval("newGlobal('same-compartment')"); + g1.x = g2.eval("x = {};"); + + dbg.onDebuggerStatement = function (frame) { xarr.push(frame.eval("x").return); }; + g1.eval("debugger;"); + g2arr.push(g2); + + g1 = null; + gc(); +} + +// At least some of the debuggees have probably been collected at this +// point. It is nondeterministic, though. +assertEq(g2arr.length, N); +assertEq(xarr.length, N); + +// Try to make g2arr[i].eval eventually allocate a new object in the same +// location as a previously gc'd object. If the object caches are not being +// swept, the pointer coincidence will cause a Debugger.Object to be erroneously +// reused. +for (var i = 0; i < N; i++) { + var obj = xarr[i]; + for (j = 0; j < M; j++) { + assertEq(obj instanceof Debugger.Object, true); + g2arr[i].eval("x = x.prop = {};"); + obj = obj.getOwnPropertyDescriptor("prop").value;; + assertEq("seen" in obj, false); + obj.seen = true; + gc(); + } +} diff --git a/js/src/jit-test/tests/debug/gc-06.js b/js/src/jit-test/tests/debug/gc-06.js new file mode 100644 index 0000000000..16aaf12bd0 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-06.js @@ -0,0 +1,6 @@ +// Debugger objects do not keep debuggee globals live. +var dbg = new Debugger; +for (var i = 0; i < 10; i++) + dbg.addDebuggee(newGlobal({newCompartment: true})); +gc(); +assertEq(dbg.getDebuggees().length < 10, true); diff --git a/js/src/jit-test/tests/debug/gc-07.js b/js/src/jit-test/tests/debug/gc-07.js new file mode 100644 index 0000000000..46000d82e1 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-07.js @@ -0,0 +1,9 @@ +// Don't assert with dead Debugger.Object and live cross-compartment wrapper of referent. +var g = newGlobal({newCompartment: true}); +for (var j = 0; j < 4; j++) { + var dbg = new Debugger; + dbg.addDebuggee(g); + dbg.enabled = false; + dbg = null; + gc(); gc(); +} diff --git a/js/src/jit-test/tests/debug/gc-08.js b/js/src/jit-test/tests/debug/gc-08.js new file mode 100644 index 0000000000..68e323c821 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-08.js @@ -0,0 +1,22 @@ +// Debuggers with enabled onExceptionUnwind hooks should not be GC'd even if +// they are otherwise unreachable. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var actual = 0; +var expected = 0; + +function f() { + for (var i = 0; i < 20; i++) { + var dbg = new Debugger(g); + dbg.num = i; + dbg.onExceptionUnwind = function (stack, exc) { actual += this.num; }; + expected += i; + } +} + +f(); +gc(); +assertThrowsValue(function () { g.eval("throw 'fit';"); }, "fit"); +assertEq(actual, expected); diff --git a/js/src/jit-test/tests/debug/gc-09.2.js b/js/src/jit-test/tests/debug/gc-09.2.js new file mode 100644 index 0000000000..0049e32db8 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-09.2.js @@ -0,0 +1,16 @@ +// Bug 717104 - Unreachable debuggee globals should not keep their debuggers +// alive. The loop is to defeat conservative stack scanning; if the same stack +// locations are used each time through the loop, at least three of the +// debuggers should be collected. +// +// This is a slight modification of gc-09.js, which contains a cycle. + +for (var i = 0; i < 4; i++) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function () { throw "FAIL"; }; + dbg.o = makeFinalizeObserver(); +} + +gc(); +assertEq(finalizeCount() > 0, true); diff --git a/js/src/jit-test/tests/debug/gc-09.js b/js/src/jit-test/tests/debug/gc-09.js new file mode 100644 index 0000000000..0357a07862 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-09.js @@ -0,0 +1,15 @@ +// Bug 717104 - Unreachable debuggee globals should not keep their debuggers +// alive. The loop is to defeat conservative stack scanning; if the same stack +// locations are used each time through the loop, at least three of the +// debuggers should be collected. + +for (var i = 0; i < 4; i++) { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function () { throw "FAIL"; }; + dbg.o = makeFinalizeObserver(); + dbg.loop = g; // make a cycle of strong references with dbg and g +} + +gc(); +assertEq(finalizeCount() > 0, true); diff --git a/js/src/jit-test/tests/debug/gc-10.js b/js/src/jit-test/tests/debug/gc-10.js new file mode 100644 index 0000000000..9a3fb00b3d --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-10.js @@ -0,0 +1,28 @@ +// Debugger.Frame objects should not be GC'd when doing so would have observable +// effects. + +var g = newGlobal({ newCompartment: true }); + +var log = ''; +var saved; + +new Debugger(g).onDebuggerStatement = function (frame) { + + // Having a live onDebuggerStatement hook will (correctly) cause a Debugger to + // be retained, even if it is otherwise unreachable. + this.onDebuggerStatement = undefined; + + // Give this Debugger.Frame an observable effect. It should not be GC'd. + frame.onPop = function () { + log += 'p'; + } +} + +g.parent = this; + +g.eval(` + debugger; + gc(); +`); + +assertEq(log, 'p'); diff --git a/js/src/jit-test/tests/debug/gc-compartment-01.js b/js/src/jit-test/tests/debug/gc-compartment-01.js new file mode 100644 index 0000000000..b5d7bc09ab --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-compartment-01.js @@ -0,0 +1,6 @@ +// A debugger can survive per-compartment GC. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +gc(g); +gc(this); diff --git a/js/src/jit-test/tests/debug/gc-compartment-02.js b/js/src/jit-test/tests/debug/gc-compartment-02.js new file mode 100644 index 0000000000..97cbc8c419 --- /dev/null +++ b/js/src/jit-test/tests/debug/gc-compartment-02.js @@ -0,0 +1,13 @@ +// Referents of Debugger.Objects in other compartments always survive per-compartment GC. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var arr = []; +dbg.onDebuggerStatement = function (frame) { arr.push(frame.eval("[]").return); }; +g.eval("for (var i = 0; i < 10; i++) debugger;"); +assertEq(arr.length, 10); + +gc(g); + +for (var i = 0; i < arr.length; i++) + assertEq(arr[i].class, "Array"); diff --git a/js/src/jit-test/tests/debug/initarrayelem-hole-value.js b/js/src/jit-test/tests/debug/initarrayelem-hole-value.js new file mode 100644 index 0000000000..65629e862c --- /dev/null +++ b/js/src/jit-test/tests/debug/initarrayelem-hole-value.js @@ -0,0 +1,12 @@ +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.evaluate(`var dbg = new Debugger(parent); dbg.onEnterFrame = function(){};`); + +function f() { + for (var i = 0; i < 10; i++) { + var arr = [1, 2, , i]; + assertEq(2 in arr, false); + assertEq(3 in arr, true); + } +} +f(); diff --git a/js/src/jit-test/tests/debug/inspect-wrapped-promise.js b/js/src/jit-test/tests/debug/inspect-wrapped-promise.js new file mode 100644 index 0000000000..2c7a862f2f --- /dev/null +++ b/js/src/jit-test/tests/debug/inspect-wrapped-promise.js @@ -0,0 +1,87 @@ +load(libdir + "asserts.js"); + +ignoreUnhandledRejections(); + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +g.promise1 = new Promise(() => {}); +g.promise2 = Promise.resolve(42); +g.promise3 = Promise.reject(42); +g.promise4 = new Object(); +g.promise5 = Promise.prototype; + +let promiseDO1 = gw.getOwnPropertyDescriptor('promise1').value; +let promiseDO2 = gw.getOwnPropertyDescriptor('promise2').value; +let promiseDO3 = gw.getOwnPropertyDescriptor('promise3').value; +let promiseDO4 = gw.getOwnPropertyDescriptor('promise4').value; +let promiseDO5 = gw.getOwnPropertyDescriptor('promise5').value; + +assertEq(promiseDO1.isPromise, true); +assertEq(promiseDO2.isPromise, true); +assertEq(promiseDO3.isPromise, true); +assertEq(promiseDO4.isPromise, false); +assertEq(promiseDO5.isPromise, false); + +assertEq(promiseDO1.promiseState, "pending"); +assertEq(promiseDO2.promiseState, "fulfilled"); +assertEq(promiseDO3.promiseState, "rejected"); +assertThrowsInstanceOf(function () { promiseDO4.promiseState }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseState }, TypeError); + +assertThrowsInstanceOf(function () { promiseDO1.promiseValue }, TypeError); +assertEq(promiseDO2.promiseValue, 42); +assertThrowsInstanceOf(function () { promiseDO3.promiseValue }, TypeError); +assertThrowsInstanceOf(function () { promiseDO4.promiseValue }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseValue }, TypeError); + +assertThrowsInstanceOf(function () { promiseDO1.promiseReason }, TypeError); +assertThrowsInstanceOf(function () { promiseDO2.promiseReason }, TypeError); +assertEq(promiseDO3.promiseReason, 42); +assertThrowsInstanceOf(function () { promiseDO4.promiseReason }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseReason }, TypeError); + +// Depending on whether async stacks are activated, this can be null, which +// has typeof null. +assertEq(typeof promiseDO1.promiseAllocationSite === "object", true); +assertEq(typeof promiseDO2.promiseAllocationSite === "object", true); +assertEq(typeof promiseDO3.promiseAllocationSite === "object", true); +assertThrowsInstanceOf(function () { promiseDO4.promiseAllocationSite }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseAllocationSite }, TypeError); + +// Depending on whether async stacks are activated, this can be null, which +// has typeof null. +assertThrowsInstanceOf(function () { promiseDO1.promiseResolutionSite }, TypeError); +assertEq(typeof promiseDO2.promiseResolutionSite === "object", true); +assertEq(typeof promiseDO3.promiseResolutionSite === "object", true); +assertThrowsInstanceOf(function () { promiseDO4.promiseResolutionSite }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseResolutionSite }, TypeError); + +assertEq(promiseDO1.promiseID, 1); +assertEq(promiseDO2.promiseID, 2); +assertEq(promiseDO3.promiseID, 3); +assertThrowsInstanceOf(function () { promiseDO4.promiseID }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseID }, TypeError); + +assertEq(typeof promiseDO1.promiseDependentPromises, "object"); +assertEq(typeof promiseDO2.promiseDependentPromises, "object"); +assertEq(typeof promiseDO3.promiseDependentPromises, "object"); +assertThrowsInstanceOf(function () { promiseDO4.promiseDependentPromises }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseDependentPromises }, TypeError); + +assertEq(promiseDO1.promiseDependentPromises.length, 0); +assertEq(promiseDO2.promiseDependentPromises.length, 0); +assertEq(promiseDO3.promiseDependentPromises.length, 0); + +assertEq(typeof promiseDO1.promiseLifetime, "number"); +assertEq(typeof promiseDO2.promiseLifetime, "number"); +assertEq(typeof promiseDO3.promiseLifetime, "number"); +assertThrowsInstanceOf(function () { promiseDO4.promiseLifetime }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseLifetime }, TypeError); + +assertThrowsInstanceOf(function () { promiseDO1.promiseTimeToResolution }, TypeError); +assertEq(typeof promiseDO2.promiseTimeToResolution, "number"); +assertEq(typeof promiseDO3.promiseTimeToResolution, "number"); +assertThrowsInstanceOf(function () { promiseDO4.promiseTimeToResolution }, TypeError); +assertThrowsInstanceOf(function () { promiseDO5.promiseTimeToResolution }, TypeError); diff --git a/js/src/jit-test/tests/debug/isAsyncFunction-isGeneratorFunction.js b/js/src/jit-test/tests/debug/isAsyncFunction-isGeneratorFunction.js new file mode 100644 index 0000000000..fd2df0277b --- /dev/null +++ b/js/src/jit-test/tests/debug/isAsyncFunction-isGeneratorFunction.js @@ -0,0 +1,57 @@ +// Debugger.Script.prototype.isAsyncFunction, Debugger.Object.prototype.isAsyncFunction, +// Debugger.Script.prototype.isGeneratorFunction, Debugger.Object.prototype.isGeneratorFunction + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gDO = dbg.addDebuggee(g); +g.non_debuggee = function non_debuggee () {} + +function checkExpr(expr, { isAsync, isGenerator }) +{ + print("Evaluating: " + JSON.stringify(expr)); + let completion = gDO.executeInGlobal(expr); + if (completion.throw) + throw completion.throw.unsafeDereference(); + + let fn = completion.return; + assertEq(fn.isAsyncFunction, isAsync); + assertEq(fn.isGeneratorFunction, isGenerator); + + // The Debugger.Object and its Debugger.Script should always agree. + if (fn.script) { + assertEq(fn.isAsyncFunction, fn.script.isAsyncFunction); + assertEq(fn.isGeneratorFunction, fn.script.isGeneratorFunction); + } +} + +checkExpr('({})', { isAsync: undefined, isGenerator: undefined }); +checkExpr('non_debuggee', { isAsync: undefined, isGenerator: undefined }); +checkExpr('(function(){})', { isAsync: false, isGenerator: false }); +checkExpr('(function*(){})', { isAsync: false, isGenerator: true }); +checkExpr('(async function snerf(){})', { isAsync: true, isGenerator: false }); +checkExpr('(async function* omlu(){})', { isAsync: true, isGenerator: true }); + +checkExpr('new Function("1+2")', { isAsync: false, isGenerator: false }); +checkExpr('Object.getPrototypeOf(function*(){}).constructor("1+2")', + { isAsync: false, isGenerator: true }); +checkExpr('Object.getPrototypeOf(async function(){}).constructor("1+2")', + { isAsync: true, isGenerator: false }); +checkExpr('Object.getPrototypeOf(async function*(){}).constructor("1+2")', + { isAsync: true, isGenerator: true }); + +// Check eval scripts. +function checkFrame(expr, type) +{ + var log = ''; + dbg.onDebuggerStatement = function(frame) { + log += 'd'; + assertEq(frame.type, type); + assertEq(frame.script.isAsyncFunction, false); + assertEq(frame.script.isGeneratorFunction, false); + } + gDO.executeInGlobal(expr); + assertEq(log, 'd'); +} + +checkFrame('debugger;', 'global'); +checkFrame('eval("debugger;")', 'eval'); diff --git a/js/src/jit-test/tests/debug/isError.js b/js/src/jit-test/tests/debug/isError.js new file mode 100644 index 0000000000..253d2c76d4 --- /dev/null +++ b/js/src/jit-test/tests/debug/isError.js @@ -0,0 +1,19 @@ +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +g.error1 = new Error() +g.error2 = new g.Error() +g.error3 = new g.TypeError(); + +let error1DO = gw.getOwnPropertyDescriptor('error1').value; +let error2DO = gw.getOwnPropertyDescriptor('error2').value; +let error3DO = gw.getOwnPropertyDescriptor('error3').value; + +assertEq(error1DO.isError, true); +assertEq(error2DO.isError, true); +assertEq(error3DO.isError, true); + +g.nonError = new Array(); +let nonErrorDO = gw.getOwnPropertyDescriptor('nonError').value; +assertEq(nonErrorDO.isError, false); diff --git a/js/src/jit-test/tests/debug/job-queue-01.js b/js/src/jit-test/tests/debug/job-queue-01.js new file mode 100644 index 0000000000..259d0c2baf --- /dev/null +++ b/js/src/jit-test/tests/debug/job-queue-01.js @@ -0,0 +1,122 @@ +// Debuggee promise reaction jobs should not run from debugger callbacks. +// This covers: +// - onDebuggerStatement +// - onStep +// - onEnterFrame +// - onPop +// - onExceptionUnwind +// - breakpoint handlers +// - uncaughtExceptionHook + +var g = newGlobal({ newCompartment: true }); +g.parent = this; +var dbg = new Debugger; +var gDO = dbg.addDebuggee(g); +var log = ''; + +// Exercise the promise machinery: resolve a promise and drain the job queue (or +// in HTML terms, perform a microtask checkpoint). When called from a debugger +// hook, the debuggee's microtasks should not run. +function exercise(name) { + log += `${name}-handler`; + Promise.resolve(42).then(v => { + assertEq(v, 42); + log += `${name}-react`; + }); + log += `(`; + drainJobQueue(); + log += `)`; + + // This should be run by the implicit microtask checkpoint after each Debugger + // hook call. + Promise.resolve(42).then(v => { + assertEq(v, 42); + log += `(${name}-tail)`; + }); +} + +dbg.onDebuggerStatement = function (frame) { + exercise('debugger'); + + frame.onStep = function () { + this.onStep = undefined; + exercise('step'); + }; + + dbg.onEnterFrame = function (frame) { + dbg.onEnterFrame = undefined; + frame.onPop = function(completion) { + assertEq(completion.return, 'recompense'); + exercise('pop'); + } + + exercise('enter'); + } + + dbg.onExceptionUnwind = function(frame, value) { + dbg.onExceptionUnwind = undefined; + assertEq(value, 'recidivism'); + exercise('exception'); + return { return: 'recompense' }; + }; + + // Set a breakpoint on entry to g.breakpoint_here. + const script = gDO.getOwnPropertyDescriptor('breakpoint_here').value.script; + const handler = { + hit(frame) { + script.clearAllBreakpoints(); + exercise('bp'); + } + }; + script.setBreakpoint(0, handler); + + dbg.uncaughtExceptionHook = function (ex) { + assertEq(ex, 'turncoat'); + exercise('uncaught'); + }; + + // Throw an uncaught exception from the Debugger handler. This should reach + // uncaughtExceptionHook, but shouldn't affect the debuggee. + throw 'turncoat'; +}; + +g.eval(` + function breakpoint_here() { + throw 'recidivism'; + } + + parent.log += 'eval('; + + // DebuggeeWouldRun detection may prevent this callback from running at all if + // bug 1145201 is present. SpiderMonkey will try to run the promise reaction + // job from the Debugger hook's microtask checkpoint, triggering + // DebuggeeWouldRun. This is a little difficult to observe, since the callback + // never even begins execution. But it should cause the 'then' promise to be + // rejected, which the shell will report (if the assertEq(log, ...) doesn't + // kill the test first). + + Promise.resolve(84).then(function(v) { + assertEq(v, 84); + parent.log += 'eval-react'; + }); + debugger; + parent.log += '...'; + breakpoint_here(); + parent.log += ')'; +`); + +log += 'main-drain(' +drainJobQueue(); +log += ')'; + +assertEq(log, `eval(\ +debugger-handler(debugger-react)\ +uncaught-handler((debugger-tail)uncaught-react)(uncaught-tail)\ +step-handler(step-react)(step-tail)\ +...\ +enter-handler(enter-react)(enter-tail)\ +bp-handler(bp-react)(bp-tail)\ +exception-handler(exception-react)(exception-tail)\ +pop-handler(pop-react)(pop-tail)\ +)\ +main-drain(eval-react)`); diff --git a/js/src/jit-test/tests/debug/job-queue-02.js b/js/src/jit-test/tests/debug/job-queue-02.js new file mode 100644 index 0000000000..5f09a818af --- /dev/null +++ b/js/src/jit-test/tests/debug/job-queue-02.js @@ -0,0 +1,82 @@ +// |jit-test| error: "async tests completed successfully" +// Test that the shell's job queue doesn't skip calls to JS::JobQueueMayNotBeEmpty. + +// For expressions like `await 1`, or `await P` for some already-resolved +// promise P, there's no need to suspend the async call to determine the +// expression's value. Suspension and resumption are expensive, so it would be +// nice if we could avoid them. +// +// But of course, even when the value is known, the act of suspension itself is +// visible: an async call's first suspension returns to its (synchronous) +// caller; subsequent suspensions let other jobs run. So in general, we can't +// short-circuit such `await` expressions. +// +// However, if an async call has been resumed from the job queue (that is, this +// isn't the initial execution, with a synchronous caller expecting a promise of +// the call's final return value), and there are no other jobs following that, +// then the `await`'s reaction job would run immediately following this job --- +// which *is* indistinguishable from skipping the suspension altogether. +// +// A JS::JobQueue implementation may call JS::JobQueueIsEmpty to indicate to the +// engine that the currently running job is the last job in the queue, so this +// optimization may be considered (there are further conditions that must be met +// as well). If the JobQueue calls JobQueueIsEmpty, then it must also call +// JS::JobQueueMayNotBeEmpty when jobs are enqueued, to indicate when the +// opportunity has passed. + +var log = ''; +async function f(label, k) { + log += label + '1'; + await 1; + log += label + '2'; + await 1; + log += label + '3'; + + return k(); +} + +// Call `f` with `label` and `k`. If `skippable` is true, exercise the path that +// skips the suspension and resumption; otherwise exercise the +// non-short-circuited path. +function test(skippable, label, k) { + var resolve; + (new Promise(r => { resolve = r; })) + .then(v => { log += v + 't'; }); + assertEq(log, ''); + f(label, k); + // job queue now: f(label)'s first await's continuation + assertEq(log, label + '1'); + + if (!skippable) { + resolve('p'); + assertEq(log, label + '1'); + // job queue now: f(label)'s first await's continuation, explicit promise's reaction + } + + // Resuming f(label) will reach the second await, which should skip suspension + // or not, depending on whether we resolved that promise. +} + +// SpiderMonkey's internal 'queue is empty' flag is initially false, even though +// the queue is initially empty, because we don't yet know whether the embedding +// is going to participate in the optimization by calling +// JS::JobQueueMayNotBeEmpty and JS::JobQueueIsEmpty. But since the shell uses +// SpiderMonkey's internal job queue implementation, this call to +// `drainJobQueue` calls `JS::JobQueueIsEmpty`, and we are ready to play. +Promise.resolve(42).then(v => assertEq(v, 42)); +drainJobQueue(); + +log = ''; +test(true, 'b', continuation1); + +function continuation1() { + assertEq(log, 'b1b2b3'); + + log = ''; + test(false, 'c', continuation2); +} + +function continuation2() { + assertEq(log, 'c1c2ptc3'); + throw "async tests completed successfully"; // proof that we actually finished +} diff --git a/js/src/jit-test/tests/debug/job-queue-03.js b/js/src/jit-test/tests/debug/job-queue-03.js new file mode 100644 index 0000000000..7e3ce27ea4 --- /dev/null +++ b/js/src/jit-test/tests/debug/job-queue-03.js @@ -0,0 +1,173 @@ +// Multiple debuggers get their job queues drained after each hook. +// This covers: +// - onDebuggerStatement +// - onStep +// - onEnterFrame +// - onPop +// - onExceptionUnwind +// - breakpoint handlers +// - uncaughtExceptionHook + +const g = newGlobal({ newCompartment: true }); +g.parent = this; + +var log = ''; +let expected_throws = 0; + +function setup(global, label) { + const dbg = new Debugger; + dbg.gDO = dbg.addDebuggee(global); + dbg.log = ''; + + dbg.onDebuggerStatement = function (frame) { + // Exercise the promise machinery: resolve a promise and perform a microtask + // checkpoint. When called from a debugger hook, the debuggee's microtasks + // should not run. + function exercise(name) { + dbg.log += name + ','; + log += `${label}-${name}-handler\n`; + Promise.resolve(42).then(v => { + assertEq(v, 42); + log += `${label}-${name}-tail\n`; + }); + } + + exercise('debugger'); + + frame.onStep = function () { + this.onStep = undefined; + exercise('step'); + }; + + dbg.onEnterFrame = function (frame) { + dbg.onEnterFrame = undefined; + frame.onPop = function(completion) { + assertEq(completion.return, 'escutcheon'); + exercise('pop'); + } + + exercise('enter'); + } + + expected_throws++; + dbg.onExceptionUnwind = function(frame, value) { + dbg.onExceptionUnwind = undefined; + assertEq(value, 'myrmidon'); + exercise('exception'); + if (--expected_throws > 0) { + return undefined; + } else { + return { return: 'escutcheon' }; + } + }; + + // Set a breakpoint on entry to g.breakpoint_here. + const script = dbg.gDO.getOwnPropertyDescriptor('breakpoint_here').value.script; + const handler = { + hit(frame) { + script.clearAllBreakpoints(); + exercise('bp'); + } + }; + script.setBreakpoint(0, handler); + + dbg.uncaughtExceptionHook = function (ex) { + assertEq(ex, 'turncoat'); + exercise('uncaught'); + }; + + // Throw an uncaught exception from the Debugger handler. This should reach + // uncaughtExceptionHook, but shouldn't affect the debuggee. + throw 'turncoat'; + }; + + return dbg; +} + +const dbg1 = setup(g, '1'); +const dbg2 = setup(g, '2'); +const dbg3 = setup(g, '3'); + +g.eval(` + function breakpoint_here() { + throw 'myrmidon'; + } + + parent.log += 'eval-start\\n'; + + // DebuggeeWouldRun detection may prevent this callback from running at all if + // bug 1145201 is present. SpiderMonkey will try to run the promise reaction + // job from the Debugger hook's microtask checkpoint, triggering + // DebuggeeWouldRun. This is a little difficult to observe, since the callback + // never even begins execution. But it should cause the 'then' promise to be + // rejected, which the shell will report (if the assertEq(log, ...) doesn't + // kill the test first). + + Promise.resolve(84).then(function(v) { + assertEq(v, 84); + parent.log += 'eval-react'; + }); + debugger; + parent.log += 'stuff to step over\\n'; + breakpoint_here(); + parent.log += 'eval-end\\n'; +`); + +log += 'main-drain\n' +drainJobQueue(); +log += 'main-drain-done\n'; + +const regex = new RegExp(`eval-start +.-debugger-handler +.-uncaught-handler +.-debugger-tail +.-uncaught-tail +.-debugger-handler +.-uncaught-handler +.-debugger-tail +.-uncaught-tail +.-debugger-handler +.-uncaught-handler +.-debugger-tail +.-uncaught-tail +.-step-handler +.-step-tail +.-step-handler +.-step-tail +.-step-handler +.-step-tail +stuff to step over +.-enter-handler +.-enter-tail +.-enter-handler +.-enter-tail +.-enter-handler +.-enter-tail +.-bp-handler +.-bp-tail +.-bp-handler +.-bp-tail +.-bp-handler +.-bp-tail +.-exception-handler +.-exception-tail +.-exception-handler +.-exception-tail +.-exception-handler +.-exception-tail +.-pop-handler +.-pop-tail +.-pop-handler +.-pop-tail +.-pop-handler +.-pop-tail +eval-end +main-drain +eval-reactmain-drain-done +`); + +assertEq(!!log.match(regex), true) + +assertEq(dbg1.log, 'debugger,uncaught,step,enter,bp,exception,pop,'); +assertEq(dbg2.log, 'debugger,uncaught,step,enter,bp,exception,pop,'); +assertEq(dbg3.log, 'debugger,uncaught,step,enter,bp,exception,pop,'); diff --git a/js/src/jit-test/tests/debug/job-queue-04.js b/js/src/jit-test/tests/debug/job-queue-04.js new file mode 100644 index 0000000000..76cf241e8d --- /dev/null +++ b/js/src/jit-test/tests/debug/job-queue-04.js @@ -0,0 +1,24 @@ +// |jit-test| skip-if: !('oomTest' in this) +// Bug 1527862: Don't assert that the Debugger drained its job queue unless we +// actually saved the debuggee's queue. + +// Put a job in the queue. +Promise.resolve(42).then(() => {}); + +var g = newGlobal({ newCompartment: true }); +var dbg = new Debugger(g); +dbg.onNewScript = script => {}; + +// Cause an OOM while initializing the AutoDebuggerJobQueueInterruption, so that +// the destructor is run on an uninitialized instance. +// +// A properly initialized AutoDebuggerJobQueueInterruption asserts that the +// debugger left its job queue entry, before restoring the debuggee's job queue +// that it saved when it was initialized. But if OOM interrupts initialization, +// the job queue left on the JSContext is still the debuggee's, which we have no +// reason to expect is empty, so we shouldn't make any assertions about its +// state. The assertion must be conditional on proper initialization (and use +// the correct condition). +oomTest(() => { + g.eval("(function() {})"); +}, {expectExceptionOnFailure: false}); diff --git a/js/src/jit-test/tests/debug/makeGlobalObjectReference-01.js b/js/src/jit-test/tests/debug/makeGlobalObjectReference-01.js new file mode 100644 index 0000000000..922b8c108c --- /dev/null +++ b/js/src/jit-test/tests/debug/makeGlobalObjectReference-01.js @@ -0,0 +1,26 @@ +// Debugger.prototype.makeGlobalObjectReference returns a D.O for a global +// without adding it as a debuggee. + +let g1 = newGlobal({newCompartment: true}); +let dbg = new Debugger; +assertEq(dbg.hasDebuggee(g1), false); + +let g1w = dbg.makeGlobalObjectReference(g1); +assertEq(dbg.hasDebuggee(g1), false); +assertEq(g1w.unsafeDereference(), g1); +assertEq(g1, g1w.makeDebuggeeValue(g1).unsafeDereference()); + +assertEq(dbg.addDebuggee(g1w), g1w); +assertEq(dbg.hasDebuggee(g1), true); +assertEq(dbg.hasDebuggee(g1w), true); +assertEq(g1w.unsafeDereference(), g1); +assertEq(g1, g1w.makeDebuggeeValue(g1).unsafeDereference()); + +// makeGlobalObjectReference dereferences CCWs. +let g2 = newGlobal({newCompartment: true}); +g2.g1 = g1; +let g2w = dbg.addDebuggee(g2); +let g2g1w = g2w.getOwnPropertyDescriptor('g1').value; +assertEq(g2g1w !== g1w, true); +assertEq(g2g1w.unwrap(), g1w.makeDebuggeeValue(g1)); +assertEq(dbg.makeGlobalObjectReference(g2g1w), g1w); diff --git a/js/src/jit-test/tests/debug/makeGlobalObjectReference-02.js b/js/src/jit-test/tests/debug/makeGlobalObjectReference-02.js new file mode 100644 index 0000000000..a3db8e45e4 --- /dev/null +++ b/js/src/jit-test/tests/debug/makeGlobalObjectReference-02.js @@ -0,0 +1,13 @@ +// Debugger.prototype.makeGlobalObjectReference only accepts actual global objects. + +load(libdir + 'asserts.js'); + +var dbg = new Debugger; + +assertThrowsInstanceOf(() => dbg.makeGlobalObjectReference(true), TypeError); +assertThrowsInstanceOf(() => dbg.makeGlobalObjectReference("foo"), TypeError); +assertThrowsInstanceOf(() => dbg.makeGlobalObjectReference(12), TypeError); +assertThrowsInstanceOf(() => dbg.makeGlobalObjectReference(undefined), TypeError); +assertThrowsInstanceOf(() => dbg.makeGlobalObjectReference(null), TypeError); +assertThrowsInstanceOf(() => dbg.makeGlobalObjectReference({ xlerb: "sbot" }), TypeError); +assertEq(dbg.makeGlobalObjectReference(this) instanceof Debugger.Object, true); diff --git a/js/src/jit-test/tests/debug/makeGlobalObjectReference-03.js b/js/src/jit-test/tests/debug/makeGlobalObjectReference-03.js new file mode 100644 index 0000000000..b20cadb8ec --- /dev/null +++ b/js/src/jit-test/tests/debug/makeGlobalObjectReference-03.js @@ -0,0 +1,8 @@ +// Debugger.prototype.makeGlobalObjectReference should not accept invisible-to-debugger globals. +load(libdir + 'asserts.js'); + +var g = newGlobal({ newCompartment: true, invisibleToDebugger: true }); + +assertThrowsInstanceOf(function () { + (new Debugger).makeGlobalObjectReference(g) +}, TypeError); diff --git a/js/src/jit-test/tests/debug/noExecute-01.js b/js/src/jit-test/tests/debug/noExecute-01.js new file mode 100644 index 0000000000..09f6ba41a7 --- /dev/null +++ b/js/src/jit-test/tests/debug/noExecute-01.js @@ -0,0 +1,29 @@ +// Tests that NX disallows debuggee execution for all the hooks. + +load(libdir + "asserts.js"); +load(libdir + "debuggerNXHelper.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// Attempts to call g.f without going through an invocation function should +// throw. +g.eval(` + function f() { } + var o = { + get p() { }, + set p(x) { } + }; + `); + +var handlers = [() => { g.f(); }, + () => { g.o.p } , + () => { g.o.p = 42; }]; + +function testHook(hookName) { + for (var h of handlers) { + assertThrowsInstanceOf(h, Debugger.DebuggeeWouldRun); + } +} + +testDebuggerHooksNX(dbg, g, testHook); diff --git a/js/src/jit-test/tests/debug/noExecute-02.js b/js/src/jit-test/tests/debug/noExecute-02.js new file mode 100644 index 0000000000..d1565b0c3f --- /dev/null +++ b/js/src/jit-test/tests/debug/noExecute-02.js @@ -0,0 +1,39 @@ +// Tests that invocation functions work. + +load(libdir + "asserts.js"); +load(libdir + "debuggerNXHelper.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` + function d() { debugger; } + function f() { return 42; } + var o = { + get p() { return 42; }, + set p(x) { } + }; + `); + +var strs = ["f();", "o.p", "o.p = 42"]; + +var fw; +dbg.onDebuggerStatement = (frame) => { + fw = frame.arguments[0]; +}; +gw.executeInGlobal("d(f)"); +dbg.onDebuggerStatement = undefined; + +function testHook(hookName) { + var newestFrame = dbg.getNewestFrame(); + for (var s of strs) { + if (newestFrame) { + assertEq(newestFrame.eval(s).return, 42); + } + assertEq(gw.executeInGlobal(s).return, 42); + assertEq(fw.apply(null).return, 42); + } +} + +testDebuggerHooksNX(dbg, g, testHook); diff --git a/js/src/jit-test/tests/debug/noExecute-03.js b/js/src/jit-test/tests/debug/noExecute-03.js new file mode 100644 index 0000000000..7b8f138503 --- /dev/null +++ b/js/src/jit-test/tests/debug/noExecute-03.js @@ -0,0 +1,28 @@ +// Tests that invocation functions work outside of Debugger code. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(); +var gw = dbg.addDebuggee(g); + +g.eval(` + function f() { debugger; return 42; } + function f2() { return 42; } + var o = { + get p() { return 42; }, + set p(x) { } + }; + `); + +var strs = ["f(f2);", "o.p", "o.p = 42"]; + +var f2w; +dbg.onDebuggerStatement = (frame) => { + f2w = frame.arguments[0]; +}; + +for (var s of strs) { + assertEq(gw.executeInGlobal(s).return, 42); +} +assertEq(f2w.apply(null).return, 42); diff --git a/js/src/jit-test/tests/debug/noExecute-04.js b/js/src/jit-test/tests/debug/noExecute-04.js new file mode 100644 index 0000000000..628a425bad --- /dev/null +++ b/js/src/jit-test/tests/debug/noExecute-04.js @@ -0,0 +1,39 @@ +// Tests that NX works through the enabled toggle and adding/removing the +// global. + +load(libdir + "asserts.js"); +load(libdir + "debuggerNXHelper.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval(` + function f() { } + var o = { + get p() { }, + set p(x) { } + }; + `); + +var handlers = [() => { g.f(); }, + () => { g.o.p } , + () => { g.o.p = 42; }]; + +function testHookEnabled(hookName, trigger) { + for (var h of handlers) { + assertThrowsInstanceOf(h, Debugger.DebuggeeWouldRun); + } +} + +function testHookRemoval(hookName, trigger) { + for (var h of handlers) { + assertThrowsInstanceOf(h, Debugger.DebuggeeWouldRun); + dbg.removeDebuggee(g); + h(); + dbg.addDebuggee(g); + assertThrowsInstanceOf(h, Debugger.DebuggeeWouldRun); + } +} + +testDebuggerHooksNX(dbg, g, testHookEnabled); +testDebuggerHooksNX(dbg, g, testHookRemoval); diff --git a/js/src/jit-test/tests/debug/noExecute-05.js b/js/src/jit-test/tests/debug/noExecute-05.js new file mode 100644 index 0000000000..3bbed22263 --- /dev/null +++ b/js/src/jit-test/tests/debug/noExecute-05.js @@ -0,0 +1,43 @@ +// Tests that NX disallows debuggee execution for all debuggees. + +load(libdir + "asserts.js"); +load(libdir + "debuggerNXHelper.js"); + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger; + +dbg.addDebuggee(g1); +dbg.addDebuggee(g2); + +g1.eval(` + function f() { } + var o = { + get p() { }, + set p(x) { } + }; + `); + +g2.eval(` + function f() { } + var o = { + get p() { }, + set p(x) { } + }; + `); + +var handlers = [() => { g1.f(); }, + () => { g1.o.p } , + () => { g1.o.p = 42; }, + () => { g2.f(); }, + () => { g2.o.p } , + () => { g2.o.p = 42; } ]; + +function testHook(hookName) { + for (var h of handlers) { + assertThrowsInstanceOf(h, Debugger.DebuggeeWouldRun); + } +} + +testDebuggerHooksNX(dbg, g1, testHook); +testDebuggerHooksNX(dbg, g2, testHook); diff --git a/js/src/jit-test/tests/debug/noExecute-06.js b/js/src/jit-test/tests/debug/noExecute-06.js new file mode 100644 index 0000000000..43186c9e2d --- /dev/null +++ b/js/src/jit-test/tests/debug/noExecute-06.js @@ -0,0 +1,81 @@ +// Tests that NX disallows debuggee execution for multiple debuggers and +// multiple debuggees. + +load(libdir + "asserts.js"); +load(libdir + "debuggerNXHelper.js"); + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg1 = new Debugger; +var dbg2 = new Debugger; + +g1w1 = dbg1.addDebuggee(g1); + +g1w2 = dbg2.addDebuggee(g1); +g2w = dbg2.addDebuggee(g2); + +g1.eval(` + function d(f) { debugger; return f; } + function f() { return 42; } + var o = { + get p() { return 42; }, + set p(x) { } + }; + `); + +g2.eval(` + function d(f) { debugger; return f; } + function f() { return 42; } + var o = { + get p() { return 42; }, + set p(x) { } + }; + `); + +var strs = ["f();", "o.p", "o.p = 42"]; + +var fw1; +dbg1.onDebuggerStatement = (frame) => { + fw1 = frame.arguments[0]; +} +g1.eval('d(f)'); +dbg1.onDebuggerStatement = undefined; +var fw2; +dbg2.onDebuggerStatement = (frame) => { + fw2 = frame.arguments[0]; +} +g2.eval('d(f)'); +dbg2.onDebuggerStatement = undefined; + +function testHook(hookName) { + var newestG1Frame = dbg1.getNewestFrame(); + if (hookName != 'onNewGlobalObject' && + hookName != 'onNewScript' && + hookName != 'onNewPromise' && + hookName != 'onPromiseSettled') + { + var newestG2Frame = dbg2.getNewestFrame(); + } + + for (var s of strs) { + // When this hook is called, g1 has been locked twice, so even invocation + // functions do not work. + assertEq(g1w1.executeInGlobal(s).throw.unsafeDereference() instanceof Debugger.DebuggeeWouldRun, true); + assertEq(g1w2.executeInGlobal(s).throw.unsafeDereference() instanceof Debugger.DebuggeeWouldRun, true); + if (newestG1Frame) { + assertEq(newestG1Frame.eval(s).throw.unsafeDereference() instanceof Debugger.DebuggeeWouldRun, true); + } + assertEq(fw1.apply(null).throw.unsafeDereference() instanceof Debugger.DebuggeeWouldRun, true); + + // But g2 has only been locked once and so should work. + assertEq(g2w.executeInGlobal(s).throw, undefined); + if (newestG2Frame) { + assertEq(newestG2Frame.eval(s).throw, undefined); + } + assertEq(fw2.apply(null).return, 42); + } +} + +testDebuggerHooksNX(dbg1, g1, () => { + testDebuggerHooksNX(dbg2, g2, testHook); +}); diff --git a/js/src/jit-test/tests/debug/noExecute-07.js b/js/src/jit-test/tests/debug/noExecute-07.js new file mode 100644 index 0000000000..a5b4247351 --- /dev/null +++ b/js/src/jit-test/tests/debug/noExecute-07.js @@ -0,0 +1,36 @@ +// Tests provenance of Debugger.DebuggeeWouldRun errors. + +load(libdir + "asserts.js"); +load(libdir + "debuggerNXHelper.js"); + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var g3 = newGlobal({newCompartment: true}); +var dbg = new Debugger(g1); + +g3.eval(`var dbg = new Debugger`); +var g1w = g3.dbg.addDebuggee(g1); +g3.dbg.addDebuggee(g2); + +g1.eval(`function f() {}`); + +function testHook(hookName) { + // The stack is like so: + // g1 -> dbg (locks g1) -> g2 -> g3.dbg (locks g1 and g2) + // + // The DebuggeeWouldRun error is always allocated in the topmost locked + // Debugger's compartment. + + // If we try to run script in g1 without going through one of g3.dbg's + // invocation functions, we should get an error allocated in + // g3.Debugger.DebuggeeWouldRun. + assertThrowsInstanceOf(() => { g1.eval(`f()`); }, g3.Debugger.DebuggeeWouldRun); + + // If we try to run script in g1 via one of g3.dbg's invocation functions, + // we should get an error allocated in Debugger.DebuggeeWouldRun. + assertEq(g1w.executeInGlobal(`f()`).throw.unsafeDereference() instanceof Debugger.DebuggeeWouldRun, true); +} + +testDebuggerHooksNX(dbg, g1, () => { + testDebuggerHooksNX(g3.dbg, g2, testHook); +}); diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-01.js b/js/src/jit-test/tests/debug/onDebuggerStatement-01.js new file mode 100644 index 0000000000..bc5b39f94d --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-01.js @@ -0,0 +1,7 @@ +var g = newGlobal({newCompartment: true}); +g.log = ''; + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (stack) { g.log += '!'; }; +assertEq(g.eval("log += '1'; debugger; log += '2'; 3;"), 3); +assertEq(g.log, '1!2'); diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-02.js b/js/src/jit-test/tests/debug/onDebuggerStatement-02.js new file mode 100644 index 0000000000..3ed64830c0 --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-02.js @@ -0,0 +1,22 @@ +// Activity in the debugger compartment should not trigger debug hooks. + +var g = newGlobal({newCompartment: true}); +var hit = false; + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (stack) { hit = true; }; + +debugger; +assertEq(hit, false, "raw debugger statement in debugger compartment should not hit"); + +g.f = function () { debugger; }; +g.eval("f();"); +assertEq(hit, false, "debugger statement in debugger compartment function should not hit"); + +g.outerEval = eval; +g.eval("outerEval('debugger;');"); +assertEq(hit, false, "debugger statement in debugger compartment eval code should not hit"); + +var g2 = newGlobal({newCompartment: true}); +g2.eval("debugger;"); +assertEq(hit, false, "debugger statement in unrelated non-debuggee compartment should not hit"); diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-03.js b/js/src/jit-test/tests/debug/onDebuggerStatement-03.js new file mode 100644 index 0000000000..5350952477 --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-03.js @@ -0,0 +1,13 @@ +// A debugger statement in an onDebuggerStatement hook should not reenter. + +var g = newGlobal({newCompartment: true}); +var calls = 0; + +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (stack) { + calls++; + debugger; +}; + +assertEq(g.eval("debugger; 7;"), 7); +assertEq(calls, 1); diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-04.js b/js/src/jit-test/tests/debug/onDebuggerStatement-04.js new file mode 100644 index 0000000000..306f7919e5 --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-04.js @@ -0,0 +1,10 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function (frame) { + var code = "assertEq(c, 'ok');\n"; + assertEq(frame.evalWithBindings("eval(s)", {s: code, a: 1234}).return, undefined); +}; +g.eval("function first() { return second(); }"); +g.eval("function second() { return eval('third()'); }"); +g.eval("function third() { debugger; }"); +g.evaluate("first();"); diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-05.js b/js/src/jit-test/tests/debug/onDebuggerStatement-05.js new file mode 100644 index 0000000000..1f6dd13f04 --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-05.js @@ -0,0 +1,22 @@ +// For perf reasons we don't recompile all a debuggee global's scripts when +// Debugger no longer needs to observe all execution for that global. Test that +// things don't crash if we try to run a script with a BaselineScript that was +// compiled with debug instrumentation when the global is no longer a debuggee. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var counter = 0; +dbg.onDebuggerStatement = function (frame) { + counter++; + if (counter == 15) + dbg.onDebuggerStatement = undefined; +}; + +g.eval("" + function f() { + { + let inner = 42; + debugger; + inner++; + } +}); +g.eval("for (var i = 0; i < 20; i++) f()"); diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-async-generator-resumption-01.js b/js/src/jit-test/tests/debug/onDebuggerStatement-async-generator-resumption-01.js new file mode 100644 index 0000000000..20a9ecf964 --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-async-generator-resumption-01.js @@ -0,0 +1,60 @@ +// A Debugger can {return:} from onDebuggerStatement in an async generator. +// A resolved promise for a {value: _, done: true} object is returned. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function* f(x) { + debugger; // when==0 to force return here + await x; + yield 1; + debugger; // when==1 to force return here + } +`); + +let exc = null; +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); +function test(when) { + let hits = 0; + let outcome = "FAIL"; + dbg.onDebuggerStatement = frame => { + if (hits++ == when) + return {return: "ponies"}; + }; + + let iter = g.f(0); + + // At the initial suspend. + assertEq(hits, 0); + iter.next().then(result => { + // At the yield point, unless we already force-returned from the first + // debugger statement. + assertEq(hits, 1); + if (when == 0) + return result; + assertEq(result.value, 1); + assertEq(result.done, false); + return iter.next(); + }).then(result => { + // After forced return. + assertEq(hits, when + 1); + assertEq(result.value, "ponies"); + assertEq(result.done, true); + outcome = "pass"; + }).catch(e => { + // An assertion failed. + exc = e; + }); + + assertEq(hits, 1); + drainJobQueue(); + if (exc !== null) + throw exc; + assertEq(outcome, "pass"); +} + +for (let i = 0; i < 2; i++) { + test(i); +} diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-async-resumption-01.js b/js/src/jit-test/tests/debug/onDebuggerStatement-async-resumption-01.js new file mode 100644 index 0000000000..e8607217f6 --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-async-resumption-01.js @@ -0,0 +1,34 @@ +// A Debugger can {return:} from onDebuggerStatement in an async function. +// The async function's promise is resolved with the returned value. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function f(x) { + debugger; // when==0 to force return here + await x; + debugger; // when==1 to force return here + } +`); + +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); +function test(when, what, expected) { + let hits = 0; + let result = "FAIL"; + dbg.onDebuggerStatement = frame => { + if (hits++ == when) + return {return: gw.makeDebuggeeValue(what)}; + }; + g.f(0).then(x => { result = x; }); + assertEq(hits, 1); + drainJobQueue(); + assertEq(hits, when + 1); + assertEq(result, expected); +} + +for (let i = 0; i < 2; i++) { + test(i, "ok", "ok"); + test(i, g.Promise.resolve(37), 37); +} diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-async-resumption-02.js b/js/src/jit-test/tests/debug/onDebuggerStatement-async-resumption-02.js new file mode 100644 index 0000000000..67ff8fdf65 --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-async-resumption-02.js @@ -0,0 +1,31 @@ +// |jit-test| error:all-jobs-completed-successfully +// Verifiy that onDebuggerStatement's force-return queues the promise +// microtask to run in the debuggee's job queue, not the debugger's +// AutoDebuggerJobQueueInterruption. + +let g = newGlobal({ newCompartment: true }); +g.eval(` + async function asyncFn(x) { + await Promise.resolve(); + debugger; + } + function enterDebuggee(){} +`); +const dbg = new Debugger(g); + +(async function() { + let it = g.asyncFn(); + + // Force-return when the debugger runs after await resume. + dbg.onDebuggerStatement = () => { + return { return: "exit" }; + }; + + const result = await it; + assertEq(result, "exit"); + // If execution here is resumed from the debugger's queue, this call will + // trigger DebuggeeWouldRun exception. + g.enterDebuggee(); + + throw "all-jobs-completed-successfully"; +})(); diff --git a/js/src/jit-test/tests/debug/onDebuggerStatement-generator-resumption-01.js b/js/src/jit-test/tests/debug/onDebuggerStatement-generator-resumption-01.js new file mode 100644 index 0000000000..c4e1ade96e --- /dev/null +++ b/js/src/jit-test/tests/debug/onDebuggerStatement-generator-resumption-01.js @@ -0,0 +1,32 @@ +// A Debugger can {return:} from onDebuggerStatement in a generator. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* f1() { + debugger; + yield 1; + } + + function* f2() { + yield 1; + debugger; + yield 2; + } +`); + +let dbg = Debugger(g); +dbg.onDebuggerStatement = frame => ({return: "Ronja"}); + +let genObj = g.f1(); +assertDeepEq(genObj.next(), {value: "Ronja", done: true}); +// Forced return closes the generator. Closed generators behave like this: +assertDeepEq(genObj.next(), {value: undefined, done: true}); + +// It works the same after resuming from a yield. +genObj = g.f2(); +assertDeepEq(genObj.next(), {value: 1, done: false}); +assertDeepEq(genObj.next(), {value: "Ronja", done: true}); +assertDeepEq(genObj.next(), {value: undefined, done: true}); // closed + diff --git a/js/src/jit-test/tests/debug/onEnterFrame-01.js b/js/src/jit-test/tests/debug/onEnterFrame-01.js new file mode 100644 index 0000000000..e9df9131e9 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-01.js @@ -0,0 +1,29 @@ +// Basic enterFrame hook tests. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var type; +dbg.onEnterFrame = function (frame) { + try { + assertEq(frame instanceof Debugger.Frame, true); + assertEq(frame.onStack, true); + type = frame.type; + } catch (exc) { + type = "Exception thrown: " + exc; + } +}; + +function test(f, expected) { + type = undefined; + f(); + assertEq(type, expected); +} + +// eval triggers the hook +test(function () { g.eval("function h() { return 1; }"); }, "eval"); + +// function calls trigger it +test(function () { assertEq(g.h(), 1); }, "call"); + +// global scripts trigger it +test(function () { g.evaluate("var x = 5;"); assertEq(g.x, 5); }, "global"); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-02.js b/js/src/jit-test/tests/debug/onEnterFrame-02.js new file mode 100644 index 0000000000..efb6cf8e9a --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-02.js @@ -0,0 +1,22 @@ +// enterFrame test with recursive debuggee function. + +var g = newGlobal({newCompartment: true}); +var N = g.N = 9; +g.eval("function f(i) { if (i < N) f(i + 1); }"); + +var dbg = Debugger(g); +var arr = []; +dbg.onEnterFrame = function (frame) { + var i; + for (i = 0; i < arr.length; i++) + assertEq(frame !== arr[i], true); + arr[i] = frame; + + // Check that the whole stack is as expected. + var j = i; + for (; frame; frame = frame.older) + assertEq(arr[j--], frame); +}; + +g.f(0); +assertEq(arr.length, N + 1); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-03.js b/js/src/jit-test/tests/debug/onEnterFrame-03.js new file mode 100644 index 0000000000..dcb78e971c --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-03.js @@ -0,0 +1,23 @@ +// frame.eval works in the enterFrame hook. +// It triggers the enterFrame hook again, recursively. (!) + +var g = newGlobal({newCompartment: true}); +g.a = "."; + +var dbg = Debugger(g); +var nestCount = 0, N = 9; +var log = ""; +dbg.onEnterFrame = function (frame) { + assertEq(frame.type, "eval"); + if (nestCount < N) { + log += '('; + nestCount++; + var a = frame.eval("a").return; + log += a; + nestCount--; + log += ')'; + } +}; + +assertEq(g.eval("a"), "."); +assertEq(log, Array(N + 1).join("(") + Array(N + 1).join(".)")); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-05.js b/js/src/jit-test/tests/debug/onEnterFrame-05.js new file mode 100644 index 0000000000..c78e92e2c3 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-05.js @@ -0,0 +1,15 @@ +// The tracejit does not prevent onEnterFrame from being called. + +var g = newGlobal({newCompartment: true}); +g.eval("function f() { return 1; }\n"); +var N = g.N = 11; +g.eval("function h() {\n" + + " for (var i = 0; i < N; i += f()) {}\n" + + "}"); +g.h(); // record loop + +var dbg = Debugger(g); +var log = ''; +dbg.onEnterFrame = function (frame) { log += frame.callee.name; }; +g.h(); +assertEq(log, 'h' + Array(N + 1).join('f')); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-06.js b/js/src/jit-test/tests/debug/onEnterFrame-06.js new file mode 100644 index 0000000000..de3b10799a --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-06.js @@ -0,0 +1,19 @@ +// The tracejit does not prevent onEnterFrame from being called after entering +// a debuggee compartment from a non-debuggee compartment. + +var g1 = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = Debugger(g1, g2); +dbg.removeDebuggee(g2); // turn off debug mode in g2 + +g1.eval("function f() { return 1; }\n"); +var N = g1.N = 11; +g1.eval("function h() {\n" + + " for (var i = 0; i < N; i += f()) {}\n" + + "}"); +g1.h(); // record loop + +var log = ''; +dbg.onEnterFrame = function (frame) { log += frame.callee.name; }; +g1.h(); +assertEq(log, 'h' + Array(N + 1).join('f')); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-07.js b/js/src/jit-test/tests/debug/onEnterFrame-07.js new file mode 100644 index 0000000000..52e95c3892 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-07.js @@ -0,0 +1,15 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var visibleFrames = 0; +dbg.onEnterFrame = function (frame) { + print("> " + frame.script.url); + visibleFrames++; +} + +g.eval("(" + function iife() { + [1].forEach(function noop() {}); + for (let x of [1]) {} +} + ")()"); + +// 1 for eval, 1 for iife(), 1 for noop() +assertEq(visibleFrames, 3); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-01.js b/js/src/jit-test/tests/debug/onEnterFrame-async-01.js new file mode 100644 index 0000000000..299516264e --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-01.js @@ -0,0 +1,32 @@ +// async functions fire onEnterFrame each time they resume, like generators + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function timeout(n) { + for (let i = 0; i < n; i++) { + await Promise.resolve(i); + } + } + async function job() { + let racer = timeout(5); + await timeout(3); + await racer; + } +`); + +let dbg = Debugger(g); +let log = ""; +let nicknames = ["job", "t5", "t3"]; +dbg.onEnterFrame = frame => { + if (!("nickname" in frame)) + frame.nickname = nicknames.shift() || "FAIL"; + log += "(" + frame.nickname; + frame.onPop = completion => { log += ")"; }; +}; + +g.job(); +drainJobQueue(); +assertEq(log, + "(job(t5)(t3))" + + "(t5)(t3)".repeat(3) + + "(t5)(job)(t5)(job)"); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-01.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-01.js new file mode 100644 index 0000000000..1f4612147a --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-01.js @@ -0,0 +1,35 @@ +// A Debugger can {return:} from the first onEnterFrame for an async function. +// (The exact behavior is undocumented; we're testing that it doesn't crash.) + +ignoreUnhandledRejections(); + +let g = newGlobal({newCompartment: true}); +g.hit2 = false; +g.eval(`async function f(x) { await x; return "ponies"; }`); + +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); +let hits = 0; +let resumption = undefined; +dbg.onEnterFrame = frame => { + if (frame.type == "call" && frame.callee.name === "f") { + frame.onPop = completion => { + assertEq(completion.return.isPromise, true); + hits++; + }; + + // If we force-return a generator object here, the caller will receive + // a promise object resolved with that generator. + resumption = frame.eval(`(function* f2() { hit2 = true; })()`); + assertEq(resumption.return.class, "Generator"); + return resumption; + } +}; + +let p = g.f(0); +assertEq(hits, 1); +assertEq(g.hit2, false); +let pw = gw.makeDebuggeeValue(p); +assertEq(pw.isPromise, true); +assertEq(pw.promiseState, "fulfilled"); +assertEq(pw.promiseValue, resumption.return); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-02.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-02.js new file mode 100644 index 0000000000..15986741be --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-02.js @@ -0,0 +1,35 @@ +// A Debugger can {throw:} from onEnterFrame in an async function. +// The resulting promise (if any) is rejected with the thrown error value. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function f() { await 1; } + var err = new TypeError("object too hairy"); +`); + +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); +let errw = gw.makeDebuggeeValue(g.err); + +// Repeat the test for each onEnterFrame event. +// It fires up to two times: +// - when the async function g.f is called; +// - when we resume after the await to run to the end. +for (let when = 0; when < 2; when++) { + let hits = 0; + dbg.onEnterFrame = frame => { + return hits++ < when ? undefined : {throw: errw}; + }; + + let result = undefined; + g.f() + .then(value => { result = {returned: value}; }) + .catch(err => { result = {threw: err}; }); + + drainJobQueue(); + + assertEq(hits, when + 1); + assertDeepEq(result, {threw: g.err}); +} diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-03.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-03.js new file mode 100644 index 0000000000..6567a61d67 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-03.js @@ -0,0 +1,29 @@ +// A Debugger can {return:} from onEnterFrame at any resume point in an async function. +// The async function's promise is resolved with the returned value. + +let g = newGlobal({newCompartment: true}); +g.eval(`async function f(x) { await x; }`); + +let dbg = new Debugger(g); +function test(when) { + let hits = 0; + dbg.onEnterFrame = frame => { + if (frame.type == "call" && frame.callee.name === "f") { + if (hits++ == when) { + return {return: "exit"}; + } + } + }; + + let result = undefined; + let finished = false; + g.f("hello").then(value => { result = value; finished = true; }); + drainJobQueue(); + assertEq(finished, true); + assertEq(hits, when + 1); + assertEq(result, "exit"); +} + +// onEnterFrame with hits==0 is not a resume point; {return:} behaves differently there +// (see onEnterFrame-async-resumption-02.js). +test(1); // force return from first resume point, immediately after the await instruction diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-04.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-04.js new file mode 100644 index 0000000000..9f67d1f286 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-04.js @@ -0,0 +1,36 @@ +// Returning {throw:} from onEnterFrame when resuming inside a try block in an +// async function causes control to jump to the catch block. + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function af() { + try { + return await Promise.resolve("fail"); + } catch (exc) { + assertEq(exc, "fit"); + return "ok"; + } + } +`) + +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + if (!("hits" in frame)) { + frame.hits = 1; + } else if (++frame.hits === 2) { + // First hit happens when g.af() is called; + // second hit is resuming at the `await` inside the try block. + return {throw: "fit"}; + } +}; + +let p = g.af(); +let hits = 0; +p.then(value => { + result = value; + hits++; +}); +drainJobQueue(); +assertEq(hits, 1); +assertEq(result, "ok"); + diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-05.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-05.js new file mode 100644 index 0000000000..a0d9887c80 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-05.js @@ -0,0 +1,41 @@ +// A Debugger can't force-return from the first onEnterFrame for an async generator. + +ignoreUnhandledRejections(); + +let g = newGlobal({newCompartment: true}); +g.eval(`async function* f(x) { await x; return "ponies"; }`); + +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); +let log = ""; +let completion = undefined; +let resumption = undefined; +dbg.uncaughtExceptionHook = exc => { + log += "2"; + assertEq(exc.message, "can't force return from a generator before the initial yield"); + assertEq(exc.constructor, TypeError); + return undefined; // Squelch the error and let the debuggee continue. +}; +dbg.onEnterFrame = frame => { + if (frame.type == "call" && frame.callee.name === "f") { + frame.onPop = c => { + // We get here after the uncaughtExcpetionHook fires + // and the debuggee frame has run to the first await. + completion = c; + assertEq(completion.return.class, "AsyncGenerator"); + assertEq(completion.return !== resumption.return, true); + log += "3"; + }; + + // Try force-returning an actual object of the expected type. + dbg.onEnterFrame = undefined; // don't recurse + resumption = frame.eval('f(0)'); + assertEq(resumption.return.class, "AsyncGenerator"); + log += "1"; + return resumption; + } +}; + +let it = g.f(0); +assertEq(log, "123"); +assertEq(gw.makeDebuggeeValue(it), completion.return); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-06.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-06.js new file mode 100644 index 0000000000..e7ecab1754 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-06.js @@ -0,0 +1,49 @@ +// A Debugger can {return:} from the first onEnterFrame for an async function. +// (The exact behavior is undocumented; we're testing that it doesn't crash.) + +ignoreUnhandledRejections(); + +let g = newGlobal({newCompartment: true}); +g.eval(`async function f(x) { await x; return "ponies"; }`); +g.eval(`async function f2(x) { await x; return "moar ponies"; }`); + +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); +let hits = 0; +let resumption = undefined; +let savedAsyncGen = undefined; +dbg.onEnterFrame = frame => { + if (frame.type == "call" && frame.callee.name === "f2") { + frame.onPop = completion => { + if (savedAsyncGen === undefined) { + savedAsyncGen = completion.return; + } + }; + } + if (frame.type == "call" && frame.callee.name === "f") { + frame.onPop = completion => { + hits++; + }; + + return {return: savedAsyncGen}; + } +}; + +let p2 = g.f2(0); +let p = g.f(0); + +assertEq(hits, 1); + +drainJobQueue(); + +assertEq(hits, 1); + +let pw2 = gw.makeDebuggeeValue(p2); +assertEq(pw2.isPromise, true); +assertEq(pw2.promiseState, "fulfilled"); +assertEq(pw2.promiseValue, "moar ponies"); + +let pw = gw.makeDebuggeeValue(p); +assertEq(pw.isPromise, true); +assertEq(pw.promiseState, "fulfilled"); +assertEq(pw.promiseValue, "moar ponies"); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-07.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-07.js new file mode 100644 index 0000000000..578d93ff99 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-07.js @@ -0,0 +1,62 @@ +// A Debugger can't force-return from the first onEnterFrame for an async generator. + +ignoreUnhandledRejections(); + +let g = newGlobal({newCompartment: true}); +g.eval(`async function* f(x) { await x; return "ponies"; }`); + +let dbg = new Debugger; +dbg.uncaughtExceptionHook = exc => {}; // ignore errors +let gw = dbg.addDebuggee(g); +let hits = 0; +let resumption = undefined; + +dbg.onEnterFrame = frame => { + dbg.onEnterFrame = undefined; + assertEq(frame.type, "call"); + assertEq(frame.callee.name, "f"); + frame.onPop = completion => { + hits++; + }; + + // Try to force-return. It's too early. This results in a call to the + // uncaughtExceptionHook but is otherwise ignored. + return {return: "rainbows"}; +}; +let it = g.f(0); // onPop #1: the initial yield +assertEq(hits, 1); +let p = it.next(); // onPop #2: await x +assertEq(hits, 2); +drainJobQueue(); // onPop #3: return "ponies", #4: the final yield +assertEq(hits, 4); + +let pw = gw.makeDebuggeeValue(p); +assertEq(pw.isPromise, true); +assertEq(pw.promiseState, "fulfilled"); +assertEq(pw.promiseValue.getProperty("value").return, "ponies"); +assertEq(pw.promiseValue.getProperty("done").return, true); + +// ---- + +g.eval(`async function* f2(x) { await x; return "moar ponies"; }`); + +let savedAsyncGen = undefined; +dbg.onEnterFrame = frame => { + dbg.onEnterFrame = undefined; + assertEq(frame.type, "call"); + assertEq(frame.callee.name, "f2"); + frame.onPop = completion => { + if (savedAsyncGen === undefined) { + savedAsyncGen = completion.return; + } + }; +}; +let it2 = g.f2(123); +let p2 = it2.next(); +drainJobQueue(); + +let pw2 = gw.makeDebuggeeValue(p2); +assertEq(pw2.isPromise, true); +assertEq(pw2.promiseState, "fulfilled"); +assertEq(pw2.promiseValue.getProperty("value").return, "moar ponies"); +assertEq(pw2.promiseValue.getProperty("done").return, true); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-08.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-08.js new file mode 100644 index 0000000000..f9760da7b7 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-08.js @@ -0,0 +1,53 @@ +// Terminate execution from within the onPop handler, the result promise will +// never be resolved. + +load(libdir + "array-compare.js"); + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(g); + +let log = []; +g.log = log; + +g.eval(` + async function f() { + log.push("START"); + await 0; + log.push("MIDDLE"); + await 0; + log.push("END"); + } +`); + +const expectedTick1 = ["START"]; +const expectedTickN = [ + "START", + "enter: f", + "MIDDLE", + "pop: f", +]; + +Promise.resolve(0) +.then(() => assertEq(arraysEqual(log, expectedTick1), true)) +.then(() => assertEq(arraysEqual(log, expectedTickN), true)) +.then(() => assertEq(arraysEqual(log, expectedTickN), true)) +.then(() => assertEq(arraysEqual(log, expectedTickN), true)) +.then(() => assertEq(arraysEqual(log, expectedTickN), true)) +.catch(e => { + // Quit with non-zero exit code to ensure a test suite error is shown, + // even when this function is called within promise handlers which normally + // swallow any exceptions. + print("Error: " + e + "\nstack:\n" + e.stack); + quit(1); +}); + +g.f(); + +dbg.onEnterFrame = frame => { + log.push(`enter: ${frame.callee.name}`); + + frame.onPop = () => { + log.push(`pop: ${frame.callee.name}`); + return null; // Terminate execution. + }; +}; diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-09.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-09.js new file mode 100644 index 0000000000..4a3bd52c3b --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-09.js @@ -0,0 +1,33 @@ +// Resume execution of async generator when initially yielding. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +g.eval(` + async function* f() { + await 123; + return "ponies"; + } +`); + +let counter = 0; +dbg.onEnterFrame = frame => { + frame.onPop = completion => { + if (counter++ === 0) { + let genObj = completion.return.unsafeDereference(); + + genObj.next().then(({value, done}) => { + assertEq(value, "ponies"); + assertEq(done, true); + }); + } + }; +}; + +let it = g.f(); + +it.next().then(({value, done}) => { + assertEq(value, undefined); + assertEq(done, true); +}); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-10.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-10.js new file mode 100644 index 0000000000..3dfeb112c2 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-10.js @@ -0,0 +1,24 @@ +// Resolve async function promise when initially awaiting. + +let g = newGlobal({newCompartment: true}); +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +g.eval(` + var resolve; + var p = new Promise(r => { + resolve = r; + }); + + async function f() { + await p; + } +`); + +dbg.onEnterFrame = frame => { + frame.onPop = completion => { + g.resolve(0); + drainJobQueue(); + }; +}; +g.f(); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-11.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-11.js new file mode 100644 index 0000000000..32adf64d85 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-11.js @@ -0,0 +1,28 @@ +// If the async function's promise is already resolved, any attempt to return +// a differerent return value gets ignored. + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function f() { + throw "ok"; + } +`); + +let dbg = new Debugger(g); + +let hits = 0; +dbg.onEnterFrame = frame => { + frame.onPop = () => { + hits += 1; + + // Normal functions can override the return value, but async functions + // have already resolved their promise, so this return request will get + // ignored. + return {return: "FAIL"}; + }; +}; + +g.f().catch(x => { + assertEq(hits, 1); + assertEq(x, "ok"); +}); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-12.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-12.js new file mode 100644 index 0000000000..28ad0c4c44 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-12.js @@ -0,0 +1,28 @@ +// If the async function's promise is already resolved, any attempt to return +// a differerent return value gets ignored. + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function f() { + return "ok"; + } +`); + +let dbg = new Debugger(g); + +let hits = 0; +dbg.onEnterFrame = frame => { + frame.onPop = () => { + hits += 1; + + // Normal functions can override the return value, but async functions + // have already resolved their promise, so this return request will get + // ignored. + return {return: "FAIL"}; + }; +}; + +g.f().then(x => { + assertEq(hits, 1); + assertEq(x, "ok"); +}); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-13.js b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-13.js new file mode 100644 index 0000000000..f9e856a74c --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-13.js @@ -0,0 +1,21 @@ +// An onPop handler can return its input argument for async generators. The +// debugger correctly adjusts the state of the async generator object. + +let g = newGlobal({newCompartment: true}); +g.eval(` + async function* f() {} +`); + +let hits = 0; +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + frame.onPop = completion => { + hits++; + return completion; + }; +}; + +let it = g.f(); +let p = it.next(); + +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-01.js b/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-01.js new file mode 100644 index 0000000000..84bc5b63f7 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-01.js @@ -0,0 +1,41 @@ +// CanSkipAwait with resolved Promises when attaching onEnterFrame +// after the initial call into the async function. + +load(libdir + "array-compare.js"); + +let g = newGlobal({ newCompartment: true }); +let dbg = new Debugger(g); + +let log = []; +g.log = log; + +g.eval(` + async function f() { + log.push("START"); + await Promise.resolve(0); + log.push("MIDDLE"); + await Promise.resolve(0); + log.push("END"); + } +`); + +function neverCalled(e) { + // Quit with non-zero exit code to ensure a test suite error is shown, + // even when this function is called within promise handlers which normally + // swallow any exceptions. + print("Error: " + e + "\nstack:\n" + e.stack); + quit(1); +} + +g.f().then(() => { + assertEq(arraysEqual(log, [ + "START", + "enter: f", + "MIDDLE", + "END", + ]), true); +}).catch(neverCalled); + +dbg.onEnterFrame = frame => { + log.push(`enter: ${frame.callee.name}`); +}; diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-02.js b/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-02.js new file mode 100644 index 0000000000..a99b96a480 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-02.js @@ -0,0 +1,41 @@ +// CanSkipAwait with primitive values when attaching onEnterFrame +// after the initial call into the async function. + +load(libdir + "array-compare.js"); + +let g = newGlobal({ newCompartment: true }); +let dbg = new Debugger(g); + +let log = []; +g.log = log; + +g.eval(` + async function f() { + log.push("START"); + await 0; + log.push("MIDDLE"); + await 0; + log.push("END"); + } +`); + +function neverCalled(e) { + // Quit with non-zero exit code to ensure a test suite error is shown, + // even when this function is called within promise handlers which normally + // swallow any exceptions. + print("Error: " + e + "\nstack:\n" + e.stack); + quit(1); +} + +g.f().then(() => { + assertEq(arraysEqual(log, [ + "START", + "enter: f", + "MIDDLE", + "END", + ]), true); +}).catch(neverCalled); + +dbg.onEnterFrame = frame => { + log.push(`enter: ${frame.callee.name}`); +}; diff --git a/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-03.js b/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-03.js new file mode 100644 index 0000000000..cff1edd115 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-async-tryskipawait-03.js @@ -0,0 +1,42 @@ +// CanSkipAwait with plain objects when attaching onEnterFrame +// after the initial call into the async function. + +load(libdir + "array-compare.js"); + +let g = newGlobal({ newCompartment: true }); +let dbg = new Debugger(g); + +let log = []; +g.log = log; + +g.eval(` + async function f() { + log.push("START"); + await {}; + log.push("MIDDLE"); + await {}; + log.push("END"); + } +`); + +function neverCalled(e) { + // Quit with non-zero exit code to ensure a test suite error is shown, + // even when this function is called within promise handlers which normally + // swallow any exceptions. + print("Error: " + e + "\nstack:\n" + e.stack); + quit(1); +} + +g.f().then(() => { + assertEq(arraysEqual(log, [ + "START", + "enter: f", + "MIDDLE", + "enter: f", // Await not optimised away in JSOP_TRYSKIPAWAIT! + "END", + ]), true); +}).catch(neverCalled); + +dbg.onEnterFrame = frame => { + log.push(`enter: ${frame.callee.name}`); +}; diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-01.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-01.js new file mode 100644 index 0000000000..6f94496eaf --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-01.js @@ -0,0 +1,82 @@ +// Frame properties and methods work in generator-resuming onEnterFrame events. +// Also tests onPop events, for good measure. + +let g = newGlobal({newCompartment: true}); +g.eval(`\ + function* gen(lo, hi) { + var a = 1/2; + yield a; + yield a * a; + } +`); +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +let hits = 0; +let savedScript = null; +let savedEnv = null; +let savedOffsets = new Set; + +function check(frame) { + assertEq(frame.type, "call"); + assertEq(frame.constructing, false); + assertEq(frame.callee, gw.makeDebuggeeValue(g.gen)); + + // `arguments` elements don't work in resumed generator frames, + // because generators don't keep the arguments around. + // However, some of this is initialized when the frame.arguments object is + // created, so if they are created during the first onEnterFrame or onPop + // event, the properties exist, and those events can also see the values. + assertEq(frame.arguments.length, args.length); + for (var i = 0; i < args.length; i++) { + assertEq(frame.arguments.hasOwnProperty(i), true); + + if (hits < 2) + assertEq(frame.arguments[i], gw.makeDebuggeeValue(args[i]), `arguments[${i}]`); + else + assertEq(frame.arguments[i], undefined); + } + + if (savedEnv === null) { + savedEnv = frame.environment; + assertEq(savedScript, null); + savedScript = frame.script; + } else { + assertEq(frame.environment, savedEnv); + assertEq(frame.script, savedScript); + } + let a_expected = hits < 3 ? undefined : 1/2; + assertEq(savedEnv.getVariable("a"), a_expected); + + assertEq(frame.onStack, true); + + let pc = frame.offset; + assertEq(savedOffsets.has(pc), false); + savedOffsets.add(pc); + + assertEq(frame.older, null); + assertEq(frame.this, gw.makeDebuggeeValue(g)); + assertEq(typeof frame.implementation, "string"); + + // And the moment of truth: + assertEq(frame.eval("2 + 2").return, 4); + assertEq(frame.eval("a").return, a_expected); + assertEq(frame.eval("if (a !== undefined) { assertEq(a < (lo + hi) / 2, true); } 7;").return, 7); +} + +dbg.onEnterFrame = frame => { + if (frame.type === "eval") + return; + check(frame); + hits++; + frame.onPop = completion => { + check(frame); + hits++; + }; +}; + +// g.gen ignores the arguments passed to it, but we use them to test +// frame.arguments. +let args = [0, 10, g, dbg]; +for (let v of g.gen(...args)) {} +assertEq(hits, 8); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-02.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-02.js new file mode 100644 index 0000000000..5aa49a580d --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-02.js @@ -0,0 +1,27 @@ +// onEnterFrame fires after the [[GeneratorState]] is set to "executing". +// +// This test checks that Debugger doesn't accidentally make it possible to +// reenter a generator frame that's already on the stack. (Also tests a fun +// corner case in baseline debug-mode OSR.) + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval('function* f() { yield 1; yield 2; }'); +let dbg = Debugger(g); +let genObj = null; +let hits = 0; +dbg.onEnterFrame = frame => { + // The first time onEnterFrame fires, there is no generator object, so + // there's nothing to test. The generator object doesn't exist until + // JSOP_GENERATOR is reached, right before the initial yield. + if (genObj !== null) { + dbg.removeDebuggee(g); // avoid the DebuggeeWouldRun exception + assertThrowsInstanceOf(() => genObj.next(), g.TypeError); + dbg.addDebuggee(g); + hits++; + } +}; +genObj = g.f(); +for (let x of genObj) {} +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-03.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-03.js new file mode 100644 index 0000000000..ac8adce373 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-03.js @@ -0,0 +1,25 @@ +// If onEnterFrame terminates a generator, the Frame is left in a sane but inactive state. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval("function* f(x) { yield x; }"); +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +let genFrame = null; +dbg.onDebuggerStatement = frame => { + dbg.onEnterFrame = frame => { + if (frame.callee == gw.getOwnPropertyDescriptor("f").value) { + genFrame = frame; + return null; + } + }; + assertEq(frame.eval("f(0);"), null); +}; + +g.eval("debugger;"); + +assertEq(genFrame instanceof Debugger.Frame, true); +assertEq(genFrame.onStack, false); +assertThrowsInstanceOf(() => genFrame.callee, Error); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-04.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-04.js new file mode 100644 index 0000000000..00f9d0f3b4 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-04.js @@ -0,0 +1,44 @@ +// When a generator frame is resumed, the onEnterFrame fires again. +// The same Frame object is passed. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* easyMode() {} + + function* f() { yield* "XYZ"; } + function* hardMode() { + for (let c1 of "AB") + for (let c2 of f()) + yield c1 + c2; + } +`); + +function test(mode, expected) { + let dbg = new Debugger(g); + let nextid = 1; + dbg.onEnterFrame = frame => { + if (frame.type == "call") { + if (!("id" in frame)) + frame.id = nextid++; + g.log += frame.id + "("; + + frame.onPop = rv => { + g.log += ")"; + }; + } + }; + + g.log = ""; + g.eval(` + for (let x of ${mode}()) + log += x; + `); + assertEq(g.log, expected); + dbg.removeDebuggee(g); +} + +// We fire onEnterFrame for the initial activation when a generator is first +// called, even though the initial yield happens before any body code. This is +// weird but at least it's consistent. +test("easyMode", "1()1()"); +test("hardMode", "1()1(2()2())AX1(2())AY1(2())AZ1(2()3()3())BX1(3())BY1(3())BZ1(3())"); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-05.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-05.js new file mode 100644 index 0000000000..cacb1b825b --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-05.js @@ -0,0 +1,34 @@ +// When resuming a generator triggers one Debugger's onEnterFrame handler, +// all Debuggers' Debugger.Frames become usable at once. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* f() { + yield 1; + } +`); +let dbg1 = new Debugger(g); +let dbg2 = new Debugger(g); + +let hits = 0; +let savedFrame1; +let savedFrame2; +dbg1.onEnterFrame = frame => { + if (savedFrame1 === undefined) { + savedFrame1 = frame; + savedFrame2 = dbg2.getNewestFrame(); + } else { + hits++; + assertEq(savedFrame1, frame); + for (let f of [savedFrame2, savedFrame1]) { + assertEq(f.type, "call"); + assertEq(f.callee.name, "f"); + } + } +}; + +let values = [...g.f()]; +assertEq(hits, 2); +assertDeepEq(values, [1]); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-06.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-06.js new file mode 100644 index 0000000000..a19fdf7787 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-06.js @@ -0,0 +1,24 @@ +// Bug 1561935. Test interaction between debug traps for step mode and +// DebugAfterYield. + +let g = newGlobal({newCompartment: true}); +g.eval('function* f() { yield 1; yield 2; }'); +let dbg = Debugger(g); +let genObj = null; +let hits = 0; +dbg.onEnterFrame = frame => { + // The first time onEnterFrame fires, there is no generator object, so + // there's nothing to test. The generator object doesn't exist until + // JSOP_GENERATOR is reached, right before the initial yield. + if (genObj === null) { + // Trigger step mode. + frame.onStep = function() {}; + } else { + dbg.removeDebuggee(g); + dbg.addDebuggee(g); + hits++; + } +}; +genObj = g.f(); +for (let x of genObj) {} +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-07.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-07.js new file mode 100644 index 0000000000..d6ac18d993 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-07.js @@ -0,0 +1,23 @@ +// Bug 1561935. Test interaction between debug traps for step mode and +// DebugAfterYield. + +let g = newGlobal({newCompartment: true}); +g.eval('function* f() { yield 1; yield 2; }'); +let dbg = Debugger(g); +let genObj = null; +let hits = 0; +dbg.onEnterFrame = frame => { + // The first time onEnterFrame fires, there is no generator object, so + // there's nothing to test. The generator object doesn't exist until + // JSOP_GENERATOR is reached, right before the initial yield. + if (genObj === null) { + // Trigger step mode. + frame.onStep = function() {}; + } else { + dbg.removeDebuggee(g); + hits++; + } +}; +genObj = g.f(); +for (let x of genObj) {} +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-08.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-08.js new file mode 100644 index 0000000000..493b718155 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-08.js @@ -0,0 +1,17 @@ +// Bug 1556033. Test behavior of onEnterFrame "return" completion +// values during explicit .throw() calls. + +let g = newGlobal({newCompartment: true}); +g.eval(`function* f(x) { }`); +let dbg = new Debugger(g); + +let it = g.f(); + +dbg.onEnterFrame = () => ({ return: "exit" }); +const result = it.throw(); +assertEq(result.value, "exit"); +assertEq(result.done, true); + +const result2 = it.next(); +assertEq(result2.value, undefined); +assertEq(result2.done, true); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-09.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-09.js new file mode 100644 index 0000000000..20bee8d35a --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-09.js @@ -0,0 +1,17 @@ +// Bug 1556033. Test behavior of onEnterFrame "return" completion +// values during explicit .return() calls. + +let g = newGlobal({newCompartment: true}); +g.eval(`function* f(x) { }`); +let dbg = new Debugger(g); + +let it = g.f(); + +dbg.onEnterFrame = () => ({ return: "exit" }); +const result = it.return(); +assertEq(result.value, "exit"); +assertEq(result.done, true); + +const result2 = it.next(); +assertEq(result2.value, undefined); +assertEq(result2.done, true); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-10.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-10.js new file mode 100644 index 0000000000..32546dc4d1 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-10.js @@ -0,0 +1,17 @@ +// Bug 1556033. Test behavior of onEnterFrame "return" completion +// values during explicit .next() calls. + +let g = newGlobal({newCompartment: true}); +g.eval(`function* f(x) { }`); +let dbg = new Debugger(g); + +let it = g.f(); + +dbg.onEnterFrame = () => ({ return: "exit" }); +const result = it.next(); +assertEq(result.value, "exit"); +assertEq(result.done, true); + +const result2 = it.next(); +assertEq(result2.value, undefined); +assertEq(result2.done, true); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-01.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-01.js new file mode 100644 index 0000000000..e768b91777 --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-01.js @@ -0,0 +1,36 @@ +// A debugger can {throw:} from onEnterFrame at any resume point in a generator. +// It closes the generator. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* f() { yield 1; } + var exn = new TypeError("object too hairy"); +`); + +let dbg = new Debugger; +let gw = dbg.addDebuggee(g); + +// Repeat the test for each onEnterFrame event. +// It fires up to three times: +// - when the generator g.f is called; +// - when we enter it to run to `yield 1`; +// - when we resume after the yield to run to the end. +for (let i = 0; i < 3; i++) { + let hits = 0; + dbg.onEnterFrame = frame => { + return hits++ < i ? undefined : {throw: gw.makeDebuggeeValue(g.exn)}; + }; + let genObj; + assertThrowsValue( + () => { + genObj = g.f(); + for (let x of genObj) {} + }, + g.exn + ); + assertEq(hits, i + 1); + if (hits > 1) + assertEq(genObj.next().done, true); +} diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-02.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-02.js new file mode 100644 index 0000000000..fa1a8f233b --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-02.js @@ -0,0 +1,39 @@ +// A Debugger can {return:} from onEnterFrame at any resume point in a generator. +// Force-returning closes the generator. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.values = [1, 2, 3]; +g.eval(`function* f() { yield* values; }`); + +let dbg = Debugger(g); + +// onEnterFrame will fire up to 5 times. +// - once for the initial call to g.f(); +// - four times at resume points: +// - initial resume at the top of the generator body +// - resume after yielding 1 +// - resume after yielding 2 +// - resume after yielding 3 (this resumption will run to the end). +// This test ignores the initial call and focuses on resume points. +for (let i = 1; i < 5; i++) { + let hits = 0; + dbg.onEnterFrame = frame => { + return hits++ < i ? undefined : {return: "we're done here"}; + }; + + let genObj = g.f(); + let actual = []; + while (true) { + let r = genObj.next(); + if (r.done) { + assertDeepEq(r, {value: "we're done here", done: true}); + break; + } + actual.push(r.value); + } + assertEq(hits, i + 1); + assertDeepEq(actual, g.values.slice(0, i - 1)); + assertDeepEq(genObj.next(), {value: undefined, done: true}); +} diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-03.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-03.js new file mode 100644 index 0000000000..b5461f2b8c --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-03.js @@ -0,0 +1,35 @@ +// Returning {throw:} from onEnterFrame when resuming inside a try block in a +// generator causes control to jump to the catch block. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* gen() { + try { + yield 0; + return "fail"; + } catch (exc) { + assertEq(exc, "fit"); + return "ok"; + } + } +`) + +let dbg = new Debugger(g); +let hits = 0; +dbg.onEnterFrame = frame => { + assertEq(frame.callee.name, "gen"); + if (++hits == 3) { + // First hit is when calling gen(); + // second hit is resuming at the implicit initial yield; + // third hit is resuming inside the try block. + return {throw: "fit"}; + } +}; + +let it = g.gen(); +let result = it.next(); +assertEq(result.done, false); +assertEq(result.value, 0); +result = it.next(); +assertEq(result.done, true); +assertEq(result.value, "ok"); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-04.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-04.js new file mode 100644 index 0000000000..969cb0c70b --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-04.js @@ -0,0 +1,34 @@ +// Returning {throw:} from onEnterFrame when resuming inside a try block in a +// generator causes control to jump to the finally block. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* gen() { + try { + yield 0; + return "fail"; + } finally { + return "ok"; // preempts exception unwinding + } + } +`) + +let dbg = new Debugger(g); +dbg.onEnterFrame = frame => { + if (!("hits" in frame)) { + frame.hits = 1; + } else if (++frame.hits == 3) { + // First hit is when calling gen(); + // second hit is resuming at the implicit initial yield; + // third hit is resuming inside the try block. + return {throw: "fit"}; + } +}; + +let it = g.gen(); +let result = it.next(); +assertEq(result.done, false); +assertEq(result.value, 0); +result = it.next(); +assertEq(result.done, true); +assertEq(result.value, "ok"); diff --git a/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-05.js b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-05.js new file mode 100644 index 0000000000..f1f5a3860a --- /dev/null +++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-05.js @@ -0,0 +1,22 @@ +// {return:} from the initial onEnterFrame for a generator is an error. + +load(libdir + "asserts.js"); + +let g = newGlobal({newCompartment: true}); +g.values = [1, 2, 3]; +g.eval(`function* f(arr=values) { yield* arr; }`); + +let dbg = Debugger(g); + +let hits = 0; +dbg.onEnterFrame = frame => { + assertEq(frame.callee.name, "f"); + hits++; + return {return: 123}; +}; +dbg.uncaughtExceptionHook = exc => { + assertEq(exc instanceof TypeError, true); + return {throw: "REJECTED"}; +} +assertThrowsValue(g.f, "REJECTED"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-01.js b/js/src/jit-test/tests/debug/onExceptionUnwind-01.js new file mode 100644 index 0000000000..ac2734c5e8 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-01.js @@ -0,0 +1,24 @@ +// Basic onExceptionUnwind hook test. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hit = false; +dbg.onExceptionUnwind = function (frame, exc) { + // onExceptionUnwind is called multiple times as the stack is unwound. + // Only check the first hit. + assertEq(arguments.length, 2); + assertEq(frame instanceof Debugger.Frame, true); + if (!hit) { + assertEq(exc, 101); + assertEq(frame.type, "call"); + assertEq(frame.callee.name, "f"); + assertEq(frame.older.type, "eval"); + hit = true; + } +}; + +g.eval("function f() { throw 101; }"); +assertThrowsValue(function () { g.eval("f();"); }, 101); +assertEq(hit, true); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-02.js b/js/src/jit-test/tests/debug/onExceptionUnwind-02.js new file mode 100644 index 0000000000..b66f32316c --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-02.js @@ -0,0 +1,47 @@ +// The onExceptionUnwind hook is called multiple times as the stack unwinds. + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.dbg = null; +g.eval("(" + function () { + dbg = new Debugger(debuggeeGlobal); + dbg.onExceptionUnwind = function (frame, exc) { + assertEq(frame instanceof Debugger.Frame, true); + assertEq(exc instanceof Debugger.Object, true); + var s = '!'; + for (var f = frame; f; f = f.older) + if (f.type === "call") + s += f.callee.name; + s += ', '; + debuggeeGlobal.log += s; + }; + } + ")();"); + +var log; + +function k() { + throw new Error("oops"); // hook call 1 +} + +function j() { + k(); // hook call 2 + log += 'j-unreached, '; +} + +function h() { + j(); // hook call 3 + log += 'h-unreached, '; +} + +function f() { + try { + h(); // hook call 4 + } catch (exc) { + log += 'f-catch'; + } +} + +log = ''; +f(); +g.dbg.enabled = false; +assertEq(log, '!kjhf, !jhf, !hf, !f, f-catch'); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-03.js b/js/src/jit-test/tests/debug/onExceptionUnwind-03.js new file mode 100644 index 0000000000..aa77b473fd --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-03.js @@ -0,0 +1,57 @@ +// The onExceptionUnwind hook is called multiple times as the stack unwinds. + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.dbg = null; +g.eval("(" + function () { + dbg = new Debugger(debuggeeGlobal); + dbg.onExceptionUnwind = function (frame, exc) { + assertEq(frame instanceof Debugger.Frame, true); + assertEq(exc instanceof Debugger.Object, true); + var s = '!'; + for (var f = frame; f; f = f.older) + if (f.type === "call") + s += f.callee.name; + s += ', '; + debuggeeGlobal.log += s; + }; + } + ")();"); + +var log; + +function k() { + try { + throw new Error("oops"); // hook call 1 + } finally { + log += 'k-finally, '; + } // hook call 2 +} + +function j() { + k(); // hook call 3 + log += 'j-unreached, '; +} + +function h() { + try { + j(); // hook call 4 + log += 'h-unreached, '; + } catch (exc) { + log += 'h-catch, '; + throw exc; // hook call 5 + } +} + +function f() { + try { + h(); // hook call 6 + } catch (exc) { + log += 'f-catch, '; + } + log += 'f-after, '; +} + +log = ''; +f(); +g.dbg.enabled = false; +assertEq(log, '!kjhf, k-finally, !kjhf, !jhf, !hf, h-catch, !hf, !f, f-catch, f-after, '); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-04.js b/js/src/jit-test/tests/debug/onExceptionUnwind-04.js new file mode 100644 index 0000000000..678d0bf4bb --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-04.js @@ -0,0 +1,17 @@ +// onExceptionUnwind is not called for exceptions thrown and handled in the debugger. +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +g.log = ''; +dbg.onDebuggerStatement = function (frame) { + try { + throw new Error("oops"); + } catch (exc) { + g.log += exc.message; + } +}; +dbg.onExceptionUnwind = function (frame) { + g.log += 'BAD'; +}; + +g.eval("debugger; log += ' ok';"); +assertEq(g.log, 'oops ok'); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-05.js b/js/src/jit-test/tests/debug/onExceptionUnwind-05.js new file mode 100644 index 0000000000..6138e89634 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-05.js @@ -0,0 +1,12 @@ +// onExceptionUnwind returning undefined does not affect the thrown exception. + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("new Debugger(parent).onExceptionUnwind = function () {};"); + +var obj = new Error("oops"); +try { + throw obj; +} catch (exc) { + assertEq(exc, obj); +} diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-06.js b/js/src/jit-test/tests/debug/onExceptionUnwind-06.js new file mode 100644 index 0000000000..19dd33d02a --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-06.js @@ -0,0 +1,13 @@ +// onExceptionUnwind assigning to argv[1] does not affect the thrown exception. + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("function f(frame, exc) { f2 = function () { return exc; }; exc = 123; }"); +g.eval("new Debugger(parent).onExceptionUnwind = f;"); + +var obj = new Error("oops"); +try { + throw obj; +} catch (exc) { + assertEq(exc, obj); +} diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-07.js b/js/src/jit-test/tests/debug/onExceptionUnwind-07.js new file mode 100644 index 0000000000..c34e687d35 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-07.js @@ -0,0 +1,15 @@ +// Unwinding due to uncatchable errors does not trigger onExceptionUnwind. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits = 0; +dbg.onExceptionUnwind = function (frame, value) { hits = 'BAD'; }; +dbg.onDebuggerStatement = function (frame) { + if (hits++ === 0) + assertEq(frame.eval("debugger;"), null); + else + return null; +} + +assertEq(g.eval("debugger; 2"), 2); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-08.js b/js/src/jit-test/tests/debug/onExceptionUnwind-08.js new file mode 100644 index 0000000000..de804c5e3f --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-08.js @@ -0,0 +1,18 @@ +// Ensure that uncaught exceptions thrown in onExceptionUnwind do not +// propagate outward into debuggee execution. +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var frame; +dbg.onExceptionUnwind = function (f, x) { + frame = f; + assertEq(frame.onStack, true); + throw 'unhandled'; +}; +dbg.onDebuggerStatement = function(f) { + assertEq(f.eval('throw 42').throw, 42); + assertEq(frame.onStack, false); +}; +g.eval('debugger'); + +// Don't fail just because we reported an uncaught exception. +quit(0); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-09.js b/js/src/jit-test/tests/debug/onExceptionUnwind-09.js new file mode 100644 index 0000000000..b220ce8c51 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-09.js @@ -0,0 +1,15 @@ +// Ensure that ScriptDebugEpilogue gets called when onExceptionUnwind +// terminates execution. +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var frame; +dbg.onExceptionUnwind = function (f, x) { + frame = f; + assertEq(frame.onStack, true); + return null; +}; +dbg.onDebuggerStatement = function(f) { + assertEq(f.eval('throw 42'), null); + assertEq(frame.onStack, false); +}; +g.eval('debugger'); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-10.js b/js/src/jit-test/tests/debug/onExceptionUnwind-10.js new file mode 100644 index 0000000000..18f23e57b5 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-10.js @@ -0,0 +1,16 @@ +// Ensure that ScriptDebugEpilogue gets called when onExceptionUnwind +// terminates execution. +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var frame; +dbg.onExceptionUnwind = function (f, x) { + frame = f; + assertEq(frame.type, 'eval'); + assertEq(frame.onStack, true); + terminate(); +}; +dbg.onDebuggerStatement = function(f) { + assertEq(f.eval('throw 42'), null); + assertEq(frame.onStack, false); +}; +g.eval('debugger'); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-12.js b/js/src/jit-test/tests/debug/onExceptionUnwind-12.js new file mode 100644 index 0000000000..c11bf74217 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-12.js @@ -0,0 +1,14 @@ +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.hits = 0; +g.eval("new Debugger(parent).onExceptionUnwind = function () { hits++; };"); +function f() { + var x = f(); +} +try { + f(); +} catch (e) { + assertEq(e instanceof InternalError, true); +} finally { + assertEq(g.hits, 0); +} diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-13.js b/js/src/jit-test/tests/debug/onExceptionUnwind-13.js new file mode 100644 index 0000000000..ec12fa2a18 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-13.js @@ -0,0 +1,16 @@ +// |jit-test| error: 4 +// +// Test that we can handle doing debug mode OSR from onExceptionUnwind when +// settling on a pc without a Baseline ICEntry. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onExceptionUnwind = function () {}; + +g.eval("" + function f(y) { + if (y > 0) { + throw 4; + } +}); +g.eval("f(0)"); +g.eval("f(1)"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-14.js b/js/src/jit-test/tests/debug/onExceptionUnwind-14.js new file mode 100644 index 0000000000..0322469283 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-14.js @@ -0,0 +1,23 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval("" + function f() { + throw 42; +}); + +g.eval("" + function g() { + throw new Error("42"); +}); + +// Call the functions once. This will compile them in Ion under --ion-eager. +g.eval("try { f(); } catch (e) { }"); +g.eval("try { g(); } catch (e) { }"); + +// Now set an onExceptionUnwind hook so that the Ion-compiled functions will +// try to bail out. The tail of the bytecode for f and g looks like 'throw; +// retrval', with 'retrval' being unreachable. Since 'throw' is resumeAfter, +// bailing out for debug mode will attempt to resume at 'retrval'. Test that +// this case is handled. +dbg.onExceptionUnwind = function f() { }; +g.eval("try { f(); } catch (e) { }"); +g.eval("try { g(); } catch (e) { }"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-15.js b/js/src/jit-test/tests/debug/onExceptionUnwind-15.js new file mode 100644 index 0000000000..4e4361abe8 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-15.js @@ -0,0 +1,25 @@ +// Test that Ion->Baseline in-place debug mode bailout can recover the iterator +// from the snapshot in a for-of loop. + +g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("Debugger(parent).onExceptionUnwind=(function() {})"); +function* throwInNext() { + yield 1; + yield 2; + yield 3; + throw 42; +} + +function f() { + for (var o of throwInNext()); +} + +var log = ""; +try { + f(); +} catch (e) { + log += e; +} + +assertEq(log, "42"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-generators-01.js b/js/src/jit-test/tests/debug/onExceptionUnwind-generators-01.js new file mode 100644 index 0000000000..ec6b846df1 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-generators-01.js @@ -0,0 +1,39 @@ +// Generator/async frames can be created and revived by the onExceptionUnwind hook. +// +// Modified copy of Frame-older-generators-01.js. + +let g = newGlobal({newCompartment: true}); +g.eval(` + function* gen() { + try { throw new Error("bad"); } catch { } + yield 1; + try { throw new Error("bad"); } catch { } + } + async function af() { + try { throw new Error("bad"); } catch { } + await 1; + try { throw new Error("bad"); } catch { } + } +`); + +function test(expected, code) { + let dbg = Debugger(g); + let hits = 0; + let genFrame = null; + dbg.onExceptionUnwind = frame => { + hits++; + if (genFrame === null) { + genFrame = frame; + } else { + assertEq(frame, genFrame); + } + assertEq(genFrame.callee.name, expected); + } + + g.eval(code); + assertEq(hits, 2); + dbg.removeDebuggee(g); +} + +test("gen", "for (var x of gen()) {}"); +test("af", "af(); drainJobQueue();"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-01.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-01.js new file mode 100644 index 0000000000..cccc444d79 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-01.js @@ -0,0 +1,9 @@ +// Check that an onExceptionUnwind hook can force a frame to return a value early. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onExceptionUnwind = function (frame, exc) { + return { return:"sproon" }; +}; +g.eval("function f() { throw 'ksnife'; }"); +assertEq(g.f(), "sproon"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-02.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-02.js new file mode 100644 index 0000000000..9faaa886c6 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-02.js @@ -0,0 +1,10 @@ +// Check that if an onExceptionUnwind hook forces a constructor frame to +// return a primitive value, it still gets wrapped up in an object. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onExceptionUnwind = function (frame, exc) { + return { return:"sproon" }; +}; +g.eval("function f() { throw 'ksnife'; }"); +assertEq(typeof new g.f, "object"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-03.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-03.js new file mode 100644 index 0000000000..f18290f15f --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-03.js @@ -0,0 +1,11 @@ +// Check that an onExceptionUnwind hook can force a frame to throw a different exception. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onExceptionUnwind = function (frame, exc) { + return { throw:"sproon" }; +}; +g.eval("function f() { throw 'ksnife'; }"); +assertThrowsValue(g.f, "sproon"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-04.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-04.js new file mode 100644 index 0000000000..3b5f777952 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-04.js @@ -0,0 +1,17 @@ +// Check that an onExceptionUnwind hook can force a frame to terminate. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +g.eval("function f() { throw 'ksnife'; }"); +var log = ''; +dbg.onDebuggerStatement = function (frame) { + log += 'd1'; + assertEq(frame.eval("f();"), null); + log += 'd2'; +}; +dbg.onExceptionUnwind = function (frame, exc) { + log += 'u'; + return null; +}; +g.eval("debugger;"); +assertEq(log, "d1ud2"); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-05.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-05.js new file mode 100644 index 0000000000..00fe2aae44 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-05.js @@ -0,0 +1,32 @@ +// Make sure that stacks in completion values are not lost when an exception +// unwind hook returns undefined. + +let g = newGlobal({ newCompartment: true }); +g.eval(` + function foo() { + bar(); + } + function bar() { + throw new Error(); + } +`); + +let dbg = Debugger(g); +let unwindHits = 0, popHits = 0; +dbg.onExceptionUnwind = frame => { + unwindHits++; + return undefined; +} +dbg.onEnterFrame = frame => { + frame.onPop = completion => { + assertEq(completion.stack.functionDisplayName, "bar"); + popHits++; + }; +}; + +try { + g.eval("foo()"); +} catch (e) {} +assertEq(unwindHits, 3); +assertEq(popHits, 3); +dbg.removeDebuggee(g); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-async-02.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-async-02.js new file mode 100644 index 0000000000..00a05d545a --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-async-02.js @@ -0,0 +1,31 @@ +// |jit-test| error:all-jobs-completed-successfully +// Verifiy that onExceptionUnwind's force-return queues the promise +// microtask to run in the debuggee's job queue, not the debugger's +// AutoDebuggerJobQueueInterruption. + +let g = newGlobal({ newCompartment: true }); +g.eval(` + async function asyncFn(x) { + await Promise.resolve(); + throw new Error(); + } + function enterDebuggee(){} +`); +const dbg = new Debugger(g); + +(async function() { + let it = g.asyncFn(); + + // Force-return when the exception throws after await resume. + dbg.onExceptionUnwind = () => { + return { return: "exit" }; + }; + + const result = await it; + assertEq(result, "exit"); + // If execution here is resumed from the debugger's queue, this call will + // trigger DebuggeeWouldRun exception. + g.enterDebuggee(); + + throw "all-jobs-completed-successfully"; +})(); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-async.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-async.js new file mode 100644 index 0000000000..1c74d56c89 --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-async.js @@ -0,0 +1,35 @@ +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +g.eval(` +async function f() { + return e; +} +`); + +// Just continue +dbg.onExceptionUnwind = function(frame) { + return undefined; +}; +g.eval(` +var E; +f().catch(e => { exc = e }); +drainJobQueue(); +assertEq(exc instanceof ReferenceError, true); +`); + +// Return with resumption value. +dbg.onExceptionUnwind = function(frame) { + return { + return: 10 + }; +}; +var val = g.eval(` +var val; +f().then(v => { val = v }); +drainJobQueue(); +val; +`); +assertEq(val, 10); diff --git a/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-generator.js b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-generator.js new file mode 100644 index 0000000000..5824016c6c --- /dev/null +++ b/js/src/jit-test/tests/debug/onExceptionUnwind-resumption-generator.js @@ -0,0 +1,52 @@ +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); + +g.eval(` +function* f() { + e; +} +`); + +// To continue testing after uncaught exception, remember the exception and +// return normal completion. +var currentFrame; +var uncaughtException; +dbg.uncaughtExceptionHook = function(e) { + uncaughtException = e; + return { + return: "uncaught" + }; +}; +function testUncaughtException() { + uncaughtException = undefined; + var obj = g.eval(`f().next()`); + assertEq(obj.done, true); + assertEq(obj.value, 'uncaught'); + assertEq(uncaughtException instanceof TypeError, true); +} + +// Just continue +dbg.onExceptionUnwind = function(frame) { + return undefined; +}; +assertThrowsInstanceOf(() => g.eval(`f().next();`), g.ReferenceError); + +// Forced early return +dbg.onExceptionUnwind = function(frame) { + currentFrame = frame; + return { + return: "foo" + }; +}; +var obj = g.eval(`f().next()`); +assertEq(obj.done, true); +assertEq(obj.value, "foo"); + +// Bad resumption value +dbg.onExceptionUnwind = function(frame) { + currentFrame = frame; + return {declaim: "gadzooks"}; +}; +testUncaughtException(); diff --git a/js/src/jit-test/tests/debug/onNewScript-01.js b/js/src/jit-test/tests/debug/onNewScript-01.js new file mode 100644 index 0000000000..7c820ae58b --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-01.js @@ -0,0 +1,44 @@ +// Basic newScript hook tests. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var seen = new WeakMap(); +var hits = 0; +dbg.onNewScript = function (s) { + // Exceptions thrown from onNewScript are swept under the rug, but they + // will at least prevent hits from being the expected number. + assertEq(s instanceof Debugger.Script, true); + assertEq(!seen.has(s), true); + seen.set(s, true); + hits++; +}; + +dbg.uncaughtExceptionHook = function () { hits = -999; }; + +// eval code +hits = 0; +assertEq(g.eval("2 + 2"), 4); +assertEq(hits, 1); + +hits = 0; +assertEq(g.eval("eval('2 + 3')"), 5); +assertEq(hits, 2); + +// global code +hits = 0; +g.evaluate("3 + 4"); +assertEq(hits, 1); + +// function code +hits = 0; +var fn = g.Function("a", "return 5 + a;"); +assertEq(hits, 1); +assertEq(fn(8), 13); +assertEq(hits, 1); + +// cloning functions across compartments +var g2 = newGlobal({newCompartment: true}); +dbg.addDebuggee(g2, dbg); +hits = 0; +cloneAndExecuteScript("(function(a) { return 5 + a; })", g2); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/onNewScript-02.js b/js/src/jit-test/tests/debug/onNewScript-02.js new file mode 100644 index 0000000000..1f284c48c3 --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-02.js @@ -0,0 +1,62 @@ +// Creating a new script with any number of subscripts triggers the newScript hook exactly once. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var seen = new WeakMap(); +var hits; +dbg.onNewScript = function (s) { + assertEq(s instanceof Debugger.Script, true); + assertEq(!seen.has(s), true); + seen.set(s, true); + hits++; +}; + +dbg.uncaughtExceptionHook = function () { hits = -999; }; + +function test(f) { + hits = 0; + f(); + assertEq(hits, 1); +} + +// eval declaring a function +test(function () { g.eval("function A(m, n) { return m===0?n+1:n===0?A(m-1,1):A(m-1,A(m,n-1)); }"); }); + +// evaluate declaring a function +test(function () { g.eval("function g(a, b) { return b===0?a:g(b,a%b); }"); }); + +// eval declaring multiple functions +test(function () { + g.eval("function e(i) { return i===0||o(i-1); }\n" + + "function o(i) { return i!==0&&e(i-1); }\n"); +}); + +// eval declaring nested functions +test(function () { g.eval("function plus(x) { return function plusx(y) { return x + y; }; }"); }); + +// eval with a function-expression +test(function () { g.eval("[3].map(function (i) { return -i; });"); }); + +// eval with getters and setters +test(function () { g.eval("var obj = {get x() { return 1; }, set x(v) { print(v); }};"); }); + +// Function with nested functions +test(function () { return g.Function("a", "b", "return b - a;"); }); + +// cloning a function with nested functions +test(function () { cloneAndExecuteScript("(function(x) { return x + 1; })", g); }); + +// eval declaring a star generator +test(function () { g.eval("function* sg(n) { for (var i=0;i<n;i++) yield i; }"); }); + +// eval creating several instances of a closure +test(function () { g.eval("for (var i = 0; i < 7; i++)\n" + + " obj = function () { return obj; };\n"); }); + +// non-strict-mode direct eval +g.eval("function e(s) { eval(s); }"); +test(function () { g.e("function f(x) { return -x; }"); }); + +// strict-mode direct eval +g.eval("function E(s) { 'use strict'; eval(s); }"); +test(function () { g.E("function g(x) { return -x; }"); }); diff --git a/js/src/jit-test/tests/debug/onNewScript-03.js b/js/src/jit-test/tests/debug/onNewScript-03.js new file mode 100644 index 0000000000..a725430395 --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-03.js @@ -0,0 +1,7 @@ +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onNewScript = function (s) { + eval(longScript); +} +const longScript = "var x = 1;\n" + new Array(5000).join("x + ") + "x"; +g.eval(longScript); diff --git a/js/src/jit-test/tests/debug/onNewScript-CloneAndExecuteScript.js b/js/src/jit-test/tests/debug/onNewScript-CloneAndExecuteScript.js new file mode 100644 index 0000000000..abc417c4a8 --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-CloneAndExecuteScript.js @@ -0,0 +1,28 @@ +// Debugger should be notified of scripts created with cloneAndExecuteScript. + +var g = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger(g, g2); +var log = ''; + +dbg.onNewScript = function (evalScript) { + log += 'e'; + + dbg.onNewScript = function (clonedScript) { + log += 'c'; + clonedScript.setBreakpoint(0, { + hit(frame) { + log += 'b'; + assertEq(frame.script, clonedScript); + } + }); + }; +}; + +dbg.onDebuggerStatement = function (frame) { + log += 'd'; +}; + +assertEq(log, ''); +g.cloneAndExecuteScript("debugger; // nee", g2); +assertEq(log, 'ecbd'); diff --git a/js/src/jit-test/tests/debug/onNewScript-ExecuteInGlobalAndReturnScope.js b/js/src/jit-test/tests/debug/onNewScript-ExecuteInGlobalAndReturnScope.js new file mode 100644 index 0000000000..96439ea2a0 --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-ExecuteInGlobalAndReturnScope.js @@ -0,0 +1,32 @@ +// Debugger should be notified of scripts created with ExecuteInGlobalAndReturnScope. + +var g = newGlobal({newCompartment: true}); +var g2 = newGlobal({newCompartment: true}); +var dbg = new Debugger(g, g2); +var log = ''; +var canary = 42; + +dbg.onNewScript = function (evalScript) { + log += 'e'; + + dbg.onNewScript = function (clonedScript) { + log += 'c'; + clonedScript.setBreakpoint(0, { + hit(frame) { + log += 'b'; + assertEq(frame.script, clonedScript); + } + }); + }; +}; + +dbg.onDebuggerStatement = function (frame) { + log += 'd'; +}; + +assertEq(log, ''); +var evalScopes = g.evalReturningScope("canary = 'dead'; let lex = 42; debugger; // nee", g2); +assertEq(log, 'ecbd'); +assertEq(canary, 42); +assertEq(evalScopes.vars.canary, 'dead'); +assertEq(evalScopes.lexicals.lex, 42); diff --git a/js/src/jit-test/tests/debug/onNewScript-off-main-thread-01.js b/js/src/jit-test/tests/debug/onNewScript-off-main-thread-01.js new file mode 100644 index 0000000000..c88e4e0402 --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-off-main-thread-01.js @@ -0,0 +1,17 @@ +// |jit-test| skip-if: helperThreadCount() === 0 + +// We still get onNewScript notifications for code compiled off the main thread. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var log; +dbg.onNewScript = function (s) { + log += 's'; + assertEq(s.source.text, '"t" + "wine"'); +} + +log = ''; +g.offThreadCompileScript('"t" + "wine"'); +assertEq(g.runOffThreadScript(), 'twine'); +assertEq(log, 's'); diff --git a/js/src/jit-test/tests/debug/onNewScript-off-main-thread-02.js b/js/src/jit-test/tests/debug/onNewScript-off-main-thread-02.js new file mode 100644 index 0000000000..25552f8bf4 --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-off-main-thread-02.js @@ -0,0 +1,12 @@ +// |jit-test| skip-if: helperThreadCount() === 0 + +var global = newGlobal({newCompartment: true}); +var dbg = new Debugger(global); + +dbg.onNewScript = function (s) { + if (s.url === "<string>") + assertEq(s.getChildScripts().length, 1); +}; + +global.eval('offThreadCompileScript("function inner() { \\\"use asm\\\"; function xxx() {} return xxx; }");'); +global.eval('runOffThreadScript();'); diff --git a/js/src/jit-test/tests/debug/onNewScript-wasm-01.js b/js/src/jit-test/tests/debug/onNewScript-wasm-01.js new file mode 100644 index 0000000000..d169da6285 --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-wasm-01.js @@ -0,0 +1,33 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() +// Draining the job queue from an onNewScript hook reporting a streamed wasm +// module should not deadlock. + +ignoreUnhandledRejections(); + +try { + WebAssembly.compileStreaming(); + // Avoid mixing the test's jobs with the debuggee's, so that + // automated checks can make sure AutoSaveJobQueue only + // suspends debuggee work. + drainJobQueue(); +} catch (err) { + assertEq(String(err).indexOf("not supported with --no-threads") !== -1, true); + quit(); +} + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); + +var source = new g.Uint8Array(wasmTextToBinary('(module (func unreachable))')); +g.source = source; + +var dbg = new Debugger(g); +dbg.allowWasmBinarySource = true; +dbg.onNewScript = function (s, g) { + drainJobQueue(); +}; + +g.eval('WebAssembly.instantiateStreaming(source);'); + +drainJobQueue(); diff --git a/js/src/jit-test/tests/debug/onNewScript-wasm-02.js b/js/src/jit-test/tests/debug/onNewScript-wasm-02.js new file mode 100644 index 0000000000..3586fe4c2e --- /dev/null +++ b/js/src/jit-test/tests/debug/onNewScript-wasm-02.js @@ -0,0 +1,41 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() +// Draining the job queue from an onNewScript hook reporting a streamed wasm +// module should not deadlock. + +ignoreUnhandledRejections(); + +try { + WebAssembly.compileStreaming(); + // Avoid mixing the test's jobs with the debuggee's, so that + // automated checks can make sure AutoSaveJobQueue only + // suspends debuggee work. + drainJobQueue(); +} catch (err) { + assertEq(String(err).indexOf("not supported with --no-threads") !== -1, true); + quit(); +} + +var g = newGlobal({newCompartment: true}); + +var source = new g.Uint8Array(wasmTextToBinary('(module (func unreachable))')); +g.source = source; + +var dbg = new Debugger(g); +dbg.allowWasmBinarySource = true; +dbg.onNewScript = function (s, g) { + drainJobQueue(); +}; + +// For the old code to deadlock, OffThreadPromiseRuntimeState::internalDrain +// needs to get two Dispatchables at once when it swaps queues. A call to +// instantiateStreaming enqueues a job that will kick off a helper thread, so to +// make sure that two helper threads have had time to complete and enqueue their +// Dispatchables, we put a delay in the job queue after the helper thread +// launches. +g.eval(` + WebAssembly.instantiateStreaming(source); + WebAssembly.instantiateStreaming(source); + Promise.resolve().then(() => sleep(0.1)); +`); + +drainJobQueue(); diff --git a/js/src/jit-test/tests/debug/optimized-out-01.js b/js/src/jit-test/tests/debug/optimized-out-01.js new file mode 100644 index 0000000000..cb0210dcbf --- /dev/null +++ b/js/src/jit-test/tests/debug/optimized-out-01.js @@ -0,0 +1,47 @@ +// Tests that we can reflect optimized out values. +// +// Unfortunately these tests are brittle. They depend on opaque JIT heuristics +// kicking in. + +// Use gczeal 0 to keep CGC from invalidating Ion code and causing test failures. +gczeal(0); + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_Ion2NoOffthreadCompilation)) + quit(0); + +withJitOptions(Opts_Ion2NoOffthreadCompilation, function () { + var g = newGlobal({newCompartment: true}); + var dbg = new Debugger; + + // Note that this *depends* on CCW scripted functions being opaque to Ion + // optimization and not deoptimizing the frames below the call to toggle. + g.toggle = function toggle(d) { + if (d) { + dbg.addDebuggee(g); + var frame = dbg.getNewestFrame(); + assertEq(frame.implementation, "ion"); + // x is unused and should be elided. + assertEq(frame.environment.getVariable("x").optimizedOut, true); + assertEq(frame.arguments[1].optimizedOut, true); + } + }; + + g.eval("" + function f(d, x) { + "use strict"; + eval("g(d, x)"); // `eval` to avoid inlining g. + }); + + g.eval("" + function g(d, x) { + "use strict"; + for (var i = 0; i < 100; i++); + toggle(d); + }); + + g.eval("(" + function test() { + for (i = 0; i < 15; i++) + f(false, 42); + f(true, 42); + } + ")();"); +}); diff --git a/js/src/jit-test/tests/debug/optimized-out-02.js b/js/src/jit-test/tests/debug/optimized-out-02.js new file mode 100644 index 0000000000..c7690cca7a --- /dev/null +++ b/js/src/jit-test/tests/debug/optimized-out-02.js @@ -0,0 +1,38 @@ +// Test that prevUpToDate on frames are cleared. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval(` +function outer(unaliasedArg) { + var unaliasedVar = unaliasedArg + 42; + var aliasedVar = unaliasedArg; + + inner(); + return unaliasedVar; // To prevent the JIT from optimizing out unaliasedVar. + + function inner() { + aliasedVar++; + } +} +`); + +var log = ""; +for (var script of dbg.findScripts()) { + if (script.displayName === "inner") { + script.setBreakpoint(0, { hit: function(frame) { + // Force updateLiveScopes. + var outerEnv = frame.environment; + + // Get the environment of outer's frame on the stack, so that we may + // recover unaliased bindings in the debug scope. + outerEnv = frame.older.environment; + log += outerEnv.getVariable('unaliasedArg'); // 42 + log += outerEnv.getVariable('unaliasedVar'); // 84 + log += outerEnv.getVariable('aliasedVar'); // 42 + }}); + } +} + +g.outer(42); +assertEq(log, "428442"); diff --git a/js/src/jit-test/tests/debug/optimized-out-03.js b/js/src/jit-test/tests/debug/optimized-out-03.js new file mode 100644 index 0000000000..aada963326 --- /dev/null +++ b/js/src/jit-test/tests/debug/optimized-out-03.js @@ -0,0 +1,31 @@ +// Test that eval-in-frame throws on accessing optimized out values. + +load(libdir + "jitopts.js"); + +if (!jitTogglesMatch(Opts_IonEagerNoOffthreadCompilation)) + quit(0); + +withJitOptions(Opts_IonEagerNoOffthreadCompilation, function() { + var dbgGlobal = newGlobal({newCompartment: true}); + var dbg = new dbgGlobal.Debugger(); + dbg.addDebuggee(this); + + function f() { + assertEq(dbg.getNewestFrame().older.eval("print(a)").throw.unsafeDereference().toString(), + "Error: variable 'a' has been optimized out"); + } + + // Test optimized out binding in function scope. + (function () { + var a = 1; + for (var i = 0; i < 1; i++) { f(); a = 2; } + })(); + + // Test optimized out binding in block scope. + (function () { + { + let a = 1; + for (var i = 0; i < 1; i++) { f(); a = 2; } + } + })(); +}); diff --git a/js/src/jit-test/tests/debug/optimized-out-arrow-this.js b/js/src/jit-test/tests/debug/optimized-out-arrow-this.js new file mode 100644 index 0000000000..adc1702fa1 --- /dev/null +++ b/js/src/jit-test/tests/debug/optimized-out-arrow-this.js @@ -0,0 +1,38 @@ +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +g.eval(` +function f() { + // |this| in arrow-functions refers to the |this| binding in outer functions. + // So when |frame.eval("this")| is executed, the outer |this| binding should + // be returned, unless it has been optimised out. + (() => {})(); + + // Ensure a |this| binding is created for |f|. + return this; +} +`); + +var errors = []; + +function enterFrame(frame) { + // Disable the handler so we don't call it recursively through |frame.eval|. + dbg.onEnterFrame = undefined; + + // Store the error when resolving |this| was unsuccessful. + var r = frame.eval("this"); + if (r.throw) { + errors.push(r.throw); + } + + // Re-enable the handler. + dbg.onEnterFrame = enterFrame; +}; + +dbg.onEnterFrame = enterFrame; + +g.f(); + +assertEq(errors.length, 1); +assertEq(errors[0].unsafeDereference().toString(), + "Error: variable 'this' has been optimized out"); diff --git a/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js b/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js new file mode 100644 index 0000000000..a8fd1d9583 --- /dev/null +++ b/js/src/jit-test/tests/debug/private-methods-eval-in-frame.js @@ -0,0 +1,25 @@ +// |jit-test| --enable-private-fields; --enable-private-methods; +load(libdir + 'asserts.js'); +load(libdir + 'evalInFrame.js'); + +class B { + #priv() { + return 12; + } + + #val = ''; + set #x(x) { + this.#val = x + ' haha'; + } + get #x() { + return this.#val; + } + + ef(str) { + return evalInFrame(0, str); + } +} + +var b = new B(); +assertEq(b.ef(`this.#priv();`), 12); +assertEq(b.ef(`this.#x = 'Hi'; this.#x`), 'Hi haha'); diff --git a/js/src/jit-test/tests/debug/prologueFailure-01.js b/js/src/jit-test/tests/debug/prologueFailure-01.js new file mode 100644 index 0000000000..369941e432 --- /dev/null +++ b/js/src/jit-test/tests/debug/prologueFailure-01.js @@ -0,0 +1,32 @@ +g = newGlobal({newCompartment: true}); +g.parent = this; + +function installHook() { + let calledTimes = 0; + function hook() { + calledTimes++; + + // Allow the new.target.prototype get to throw. + if (calledTimes === 1) + return undefined; + + return { + return: undefined + }; + } + + Debugger(parent).onExceptionUnwind = hook; +} + + +g.eval("(" + installHook + ")()"); + +var handler = { + get(t, p) { + throw new TypeError; + } +}; + + +var f = new Proxy(function(){}, handler); +new f(); diff --git a/js/src/jit-test/tests/debug/prologueFailure-02.js b/js/src/jit-test/tests/debug/prologueFailure-02.js new file mode 100644 index 0000000000..54bde11d05 --- /dev/null +++ b/js/src/jit-test/tests/debug/prologueFailure-02.js @@ -0,0 +1,49 @@ +g = newGlobal({newCompartment: true}); +g.parent = this; + +function installHook() { + let calledTimes = 0; + function hook(frame) { + calledTimes++; + switch (calledTimes) { + case 1: + // Proxy get trap + assertEq(frame.type, "call"); + assertEq(frame.script.displayName.includes("get"), true); + break; + case 2: + // wrapper function. There is no entry for notRun + assertEq(frame.type, "call"); + assertEq(frame.script.displayName.includes("wrapper"), true); + break; + case 3: + assertEq(frame.type, "global"); + // Force the top-level to return cleanly, so that we can tell + // assertion failures from the intended throwing. + return { return: undefined }; + + default: + // that's the whole chain. + assertEq(false, true); + } + } + + Debugger(parent).onExceptionUnwind = hook; +} + + +g.eval("(" + installHook + ")()"); + +var handler = { + get(t, p) { + throw new TypeError; + } +}; + +function notRun() {} + +function wrapper() { + var f = new Proxy(notRun, handler); + new f(); +} +wrapper(); diff --git a/js/src/jit-test/tests/debug/prologueFailure-03.js b/js/src/jit-test/tests/debug/prologueFailure-03.js new file mode 100644 index 0000000000..d404428c0a --- /dev/null +++ b/js/src/jit-test/tests/debug/prologueFailure-03.js @@ -0,0 +1,26 @@ +g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval("(" + function() { + let calledTimes = 0; + Debugger(parent).onExceptionUnwind = function(frame) { + switch (calledTimes++) { + case 0: + assertEq(frame.older.type, "global"); + break; + case 1: + // Force toplevel to return placidly so that we can tell assertions + // from the throwing in the test. + assertEq(frame.older, null); + return { return: undefined }; + default: + assertEq(false, true); + } + } +} + ")()"); + +var handler = { + get() { + r; + } +}; +new(new Proxy(function() {}, handler)); diff --git a/js/src/jit-test/tests/debug/relazify-debugee-script-01.js b/js/src/jit-test/tests/debug/relazify-debugee-script-01.js new file mode 100644 index 0000000000..1a16046b8b --- /dev/null +++ b/js/src/jit-test/tests/debug/relazify-debugee-script-01.js @@ -0,0 +1,14 @@ +var g = newGlobal({ newCompartment: true }); +var dbg = Debugger(g); + +dbg.collectCoverageInfo = true; + +g.eval(` + function fn() {} + fn(); +`); + +dbg = null; +gc(); + +relazifyFunctions(); diff --git a/js/src/jit-test/tests/debug/resumption-01.js b/js/src/jit-test/tests/debug/resumption-01.js new file mode 100644 index 0000000000..7fc9857ac7 --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-01.js @@ -0,0 +1,12 @@ +// Simple {throw:} resumption. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (stack) { return {throw: "oops"}; }; + +assertThrowsValue(function () { g.eval("debugger;"); }, "oops"); + +g.eval("function f() { debugger; }"); +assertThrowsValue(function () { g.f(); }, "oops"); diff --git a/js/src/jit-test/tests/debug/resumption-02.js b/js/src/jit-test/tests/debug/resumption-02.js new file mode 100644 index 0000000000..1762d61767 --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-02.js @@ -0,0 +1,9 @@ +// Simple {return:} resumption. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = function (stack) { return {return: 1234}; }; + +assertEq(g.eval("debugger; false;"), 1234); +g.eval("function f() { debugger; return 'bad'; }"); +assertEq(g.f(), 1234); diff --git a/js/src/jit-test/tests/debug/resumption-03.js b/js/src/jit-test/tests/debug/resumption-03.js new file mode 100644 index 0000000000..f48992ced6 --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-03.js @@ -0,0 +1,35 @@ +// Returning and throwing objects. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function () { + var how, what; + var dbg = new Debugger(debuggeeGlobal); + dbg.onDebuggerStatement = function (frame) { + if (frame.callee.name === "configure") { + how = frame.arguments[0]; + what = frame.arguments[1]; + } else { + var resume = {}; + resume[how] = what; + return resume; + } + }; + } + ")();"); + +function configure(how, what) { debugger; } +function fire() { debugger; } + +var d = new Date; +configure('return', d); +assertEq(fire(), d); +configure('return', Math); +assertEq(fire(), Math); + +var x = new Error('oh no what are you doing'); +configure('throw', x); +assertThrowsValue(fire, x); +configure('throw', parseInt); +assertThrowsValue(fire, parseInt); diff --git a/js/src/jit-test/tests/debug/resumption-05.js b/js/src/jit-test/tests/debug/resumption-05.js new file mode 100644 index 0000000000..9c3f7cd54a --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-05.js @@ -0,0 +1,35 @@ +// null resumption value means terminate the debuggee + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function () { + var dbg = new Debugger(debuggeeGlobal); + dbg.onDebuggerStatement = function (frame) { + if (frame.callee === null) { + // The first debugger statement below. + debuggeeGlobal.log += "1"; + var cv = frame.eval("f();"); + assertEq(cv, null); + debuggeeGlobal.log += "2"; + } else { + // The second debugger statement. + debuggeeGlobal.log += "3"; + assertEq(frame.callee.name, "f"); + return null; + } + }; + } + ")()"); + +var log = ""; +debugger; + +function f() { + log += "4"; + try { + debugger; // the debugger terminates us here + } finally { + log += "5"; // this should not execute + } +} + +assertEq(log, "1432"); diff --git a/js/src/jit-test/tests/debug/resumption-07.js b/js/src/jit-test/tests/debug/resumption-07.js new file mode 100644 index 0000000000..2ba239e81a --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-07.js @@ -0,0 +1,34 @@ +// Return resumption values to non-debuggee frames. + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; + +var log; + +function handlerWithResumption(resumption) { + return function (frame) { + log += 'd'; + dbg.removeDebuggee(g); + return resumption; + }; +} + +log = ''; +dbg.onDebuggerStatement = handlerWithResumption(undefined); +dbg.addDebuggee(g); +assertEq(g.eval('debugger; 42;'), 42); +assertEq(log, 'd'); + +log = ''; +dbg.onDebuggerStatement = handlerWithResumption({ return: 1729 }); +dbg.addDebuggee(g); +assertEq(g.eval('debugger; 42;'), 1729); +assertEq(log, 'd'); + +log = ''; +dbg.onDebuggerStatement = handlerWithResumption(null); +dbg.addDebuggee(g); +assertEq(g.evaluate('debugger; 42;', { catchTermination: true }), 'terminated'); +assertEq(log, 'd'); diff --git a/js/src/jit-test/tests/debug/resumption-08.js b/js/src/jit-test/tests/debug/resumption-08.js new file mode 100644 index 0000000000..044029584e --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-08.js @@ -0,0 +1,93 @@ +// Check whether we respect resumption values when toggling debug mode on->off +// from various points with live scripts on the stack. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger; + +function reset() { + dbg.onEnterFrame = undefined; + dbg.onDebuggerStatement = undefined; + dbg.addDebuggee(g); + g.eval("(" + function test() { + for (i = 0; i < 5; i++) + f(42); + } + ")();"); +} + +g.eval("" + function f(d) { + return g(d); +}); + +g.eval("" + function g(d) { + debugger; + return d; +}); + +function testResumptionValues(handlerSetter) { + // Test normal return. + reset(); + handlerSetter(undefined); + assertEq(g.eval("(" + function test() { return f(42); } + ")();"), 42); + + // Test forced return. + reset(); + handlerSetter({ return: "not 42" }); + assertEq(g.eval("(" + function test() { return f(42); } + ")();"), "not 42"); + + // Test throw. + reset(); + handlerSetter({ throw: "thrown 42" }); + try { + g.eval("(" + function test() { return f(42); } + ")();");; + } catch (e) { + assertEq(e, "thrown 42"); + } +} + +// Turn off from within the prologue. +testResumptionValues(function (resumptionVal) { + dbg.onEnterFrame = function (frame) { + if (frame.older) { + if (frame.older.older) { + dbg.removeDebuggee(g); + return resumptionVal; + } + } + }; +}); + +// Turn off from within the epilogue. +testResumptionValues(function (resumptionVal) { + dbg.onEnterFrame = function (frame) { + if (frame.older) { + if (frame.older.older) { + frame.onPop = function () { + dbg.removeDebuggee(g); + return resumptionVal; + }; + } + } + }; +}); + +// Turn off from within debugger statement handler. +testResumptionValues(function (resumptionVal) { + dbg.onDebuggerStatement = function (frame) { + dbg.removeDebuggee(g); + return resumptionVal; + }; +}); + +// Turn off from within debug trap handler. +testResumptionValues(function (resumptionVal) { + dbg.onEnterFrame = function (frame) { + if (frame.older) { + if (frame.older.older) { + frame.onStep = function () { + dbg.removeDebuggee(g); + return resumptionVal; + } + } + } + }; +}); diff --git a/js/src/jit-test/tests/debug/resumption-09.js b/js/src/jit-test/tests/debug/resumption-09.js new file mode 100644 index 0000000000..5a04a32aea --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-09.js @@ -0,0 +1,41 @@ +// Test exception stack behavior when reusing completion values as resumption +// values. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +g.eval(` + function foo() { + bar(); + } + function bar() { + debugger; + } + function baz() { + throw new Error(); + } +`); + +var dbg = Debugger(g); +dbg.onDebuggerStatement = frame => { + return frame.eval("baz()"); +}; + +let popHits = 0; +dbg.onEnterFrame = frame => { + frame.onPop = completion => { + popHits++; + // Resumption values ignore any 'stack' property, and the script location of + // the place where the hook was called will be used when throwing. + if (popHits <= 2) { + assertEq(completion.stack.functionDisplayName, "baz"); + } else { + assertEq(completion.stack.functionDisplayName, "bar"); + } + }; +}; + +try { + g.eval("foo()"); +} catch (e) {} +assertEq(popHits, 5); diff --git a/js/src/jit-test/tests/debug/resumption-error-01.js b/js/src/jit-test/tests/debug/resumption-error-01.js new file mode 100644 index 0000000000..65cf81abb5 --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-error-01.js @@ -0,0 +1,7 @@ +// A resumption value can't have both {return:} and {throw:} properties. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +dbg.onDebuggerStatement = stack => ({return: 1, throw: 2}); +dbg.uncaughtExceptionHook = exc => ({return: "corrected"}); +assertEq(g.eval("debugger; false;"), "corrected"); diff --git a/js/src/jit-test/tests/debug/resumption-error-02.js b/js/src/jit-test/tests/debug/resumption-error-02.js new file mode 100644 index 0000000000..0b943d900e --- /dev/null +++ b/js/src/jit-test/tests/debug/resumption-error-02.js @@ -0,0 +1,16 @@ +// Error handling if parsing a resumption value throws. + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var rv; +dbg.onDebuggerStatement = stack => rv; +dbg.uncaughtExceptionHook = function (exc) { + assertEq(exc, "BANG"); + return {return: "recovered"}; +}; + +rv = {get throw() { throw "BANG"; }}; +assertEq(g.eval("debugger; false;"), "recovered"); + +rv = new Proxy({}, {has() { throw "BANG"; }}); +assertEq(g.eval("debugger; false;"), "recovered"); diff --git a/js/src/jit-test/tests/debug/save-queue-resets-draining.js b/js/src/jit-test/tests/debug/save-queue-resets-draining.js new file mode 100644 index 0000000000..3d63c09bf8 --- /dev/null +++ b/js/src/jit-test/tests/debug/save-queue-resets-draining.js @@ -0,0 +1,18 @@ +// The draining state is reset when saving the job queue. + +let g = newGlobal({newCompartment: true}); + +let dbg = new Debugger(); +let gw = dbg.addDebuggee(g); + +dbg.onDebuggerStatement = frame => { + // Enqueue a new job from within the debugger while executing another job + // from outside of the debugger. + enqueueJob(function() {}); +}; + +g.eval(` + enqueueJob(function() { + debugger; + }); +`); diff --git a/js/src/jit-test/tests/debug/setter-argc.js b/js/src/jit-test/tests/debug/setter-argc.js new file mode 100644 index 0000000000..f56195d00d --- /dev/null +++ b/js/src/jit-test/tests/debug/setter-argc.js @@ -0,0 +1,52 @@ +// Check that setters throw TypeError when passed no arguments, instead of crashing. + +function check(obj) { + let proto = Object.getPrototypeOf(obj); + let props = Object.getOwnPropertyNames(proto); + for (let prop of props) { + let desc = Object.getOwnPropertyDescriptor(proto, prop); + if (desc.set) { + print("bleah: " + JSON.stringify(prop)); + assertEq(typeof desc.set, 'function'); + try { + desc.set.call(obj); + assertEq("should have thrown TypeError", false); + } catch (e) { + assertEq(e instanceof TypeError, true); + } + } + } +} + +var dbg = new Debugger; +var g = newGlobal({newCompartment: true}); +var gw = dbg.addDebuggee(g); + +// Debugger +check(dbg); + +// Debugger.Memory +check(dbg.memory); + +// Debugger.Object +g.eval('function f() { debugger; }'); +var fw = gw.getOwnPropertyDescriptor('f').value; +check(fw); + +// Debugger.Script +check(fw.script); + +// Debugger.Source +check(fw.script.source); + +// Debugger.Environment +check(fw.environment); + +// Debugger.Frame +var log = ''; +dbg.onDebuggerStatement = function(frame) { + log += 'd'; + check(frame); +} +g.eval('f()'); +assertEq(log, 'd'); diff --git a/js/src/jit-test/tests/debug/surfaces-01.js b/js/src/jit-test/tests/debug/surfaces-01.js new file mode 100644 index 0000000000..0907916a4d --- /dev/null +++ b/js/src/jit-test/tests/debug/surfaces-01.js @@ -0,0 +1,17 @@ +// Check superficial characteristics of functions and properties (not functionality). + +function checkFunction(obj, name, nargs) { + var desc = Object.getOwnPropertyDescriptor(obj, name); + assertEq(desc.configurable, true, name + " should be configurable"); + assertEq(desc.writable, true, name + " should be writable"); + assertEq(desc.enumerable, false, name + " should be non-enumerable"); + assertEq(desc.value, obj[name]); // well obviously + assertEq(typeof desc.value, 'function', name + " should be a function"); + assertEq(desc.value.length, nargs, name + " should have .length === " + nargs); +} + +checkFunction(this, "Debugger", 1); + +assertEq(Debugger.prototype.constructor, Debugger); +assertEq(Object.prototype.toString.call(Debugger.prototype), "[object Debugger]"); +assertEq(Object.getPrototypeOf(Debugger.prototype), Object.prototype); diff --git a/js/src/jit-test/tests/debug/surfaces-02.js b/js/src/jit-test/tests/debug/surfaces-02.js new file mode 100644 index 0000000000..66e95d0ec9 --- /dev/null +++ b/js/src/jit-test/tests/debug/surfaces-02.js @@ -0,0 +1,31 @@ +// Debugger.prototype.onDebuggerStatement + +load(libdir + 'asserts.js'); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +gc(); // don't assert marking debug hooks +assertEq(dbg.onDebuggerStatement, undefined); + +function f() {} + +assertThrowsInstanceOf(function () { dbg.onDebuggerStatement = null; }, TypeError); +assertThrowsInstanceOf(function () { dbg.onDebuggerStatement = "bad"; }, TypeError); +assertThrowsInstanceOf(function () { dbg.onDebuggerStatement = {}; }, TypeError); +dbg.onDebuggerStatement = f; +assertEq(dbg.onDebuggerStatement, f); + +assertEq(Object.getOwnPropertyNames(dbg).length, 0); +var desc = Object.getOwnPropertyDescriptor(Debugger.prototype, "onDebuggerStatement"); +assertEq(desc.configurable, true); +assertEq(desc.enumerable, false); + +assertThrowsInstanceOf(function () { desc.get(); }, TypeError); +assertThrowsInstanceOf(function () { desc.get.call(undefined); }, TypeError); +assertThrowsInstanceOf(function () { desc.get.call(Debugger.prototype); }, TypeError); +assertEq(desc.get.call(dbg), f); + +assertThrowsInstanceOf(function () { desc.set(); }, TypeError); +assertThrowsInstanceOf(function () { desc.set.call(dbg); }, TypeError); +assertThrowsInstanceOf(function () { desc.set.call({}, f); }, TypeError); +assertThrowsInstanceOf(function () { desc.set.call(Debugger.prototype, f); }, TypeError); diff --git a/js/src/jit-test/tests/debug/surfaces-03.js b/js/src/jit-test/tests/debug/surfaces-03.js new file mode 100644 index 0000000000..0c440c6836 --- /dev/null +++ b/js/src/jit-test/tests/debug/surfaces-03.js @@ -0,0 +1,19 @@ +// dumb basics of uncaughtExceptionHook + +load(libdir + 'asserts.js'); + +var desc = Object.getOwnPropertyDescriptor(Debugger.prototype, "uncaughtExceptionHook"); +assertEq(typeof desc.get, 'function'); +assertEq(typeof desc.set, 'function'); + +assertThrowsInstanceOf(function () { Debugger.prototype.uncaughtExceptionHook = null; }, TypeError); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +assertEq(desc.get.call(dbg), null); +assertThrowsInstanceOf(function () { dbg.uncaughtExceptionHook = []; }, TypeError); +assertThrowsInstanceOf(function () { dbg.uncaughtExceptionHook = 3; }, TypeError); +dbg.uncaughtExceptionHook = Math.sin; +assertEq(dbg.uncaughtExceptionHook, Math.sin); +dbg.uncaughtExceptionHook = null; +assertEq(dbg.uncaughtExceptionHook, null); diff --git a/js/src/jit-test/tests/debug/surfaces-offsets.js b/js/src/jit-test/tests/debug/surfaces-offsets.js new file mode 100644 index 0000000000..418361a924 --- /dev/null +++ b/js/src/jit-test/tests/debug/surfaces-offsets.js @@ -0,0 +1,37 @@ +// Invalid offsets result in exceptions, not bogus results. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = Debugger(g); +var hits; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.script.getOffsetLocation(frame.offset).lineNumber, g.line); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(String(frame.offset)).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(Object(frame.offset)).lineNumber; }, Error); + + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(-1).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(1000000).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(0.25).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(+Infinity).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(-Infinity).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(NaN).lineNumber; }, Error); + + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(false).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(true).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation(undefined).lineNumber; }, Error); + assertThrowsInstanceOf(function () { frame.script.getOffsetLocation().lineNumber; }, Error); + + // We assume that at least one whole number between 0 and frame.offset is invalid. + assertThrowsInstanceOf( + function () { + for (var i = 0; i < frame.offset; i++) + frame.script.getOffsetLocation(i).lineNumber; + }, + Error); + + hits++; +}; + +hits = 0; +g.eval("var line = new Error().lineNumber; debugger;"); diff --git a/js/src/jit-test/tests/debug/testEarlyReturnOnCall.js b/js/src/jit-test/tests/debug/testEarlyReturnOnCall.js new file mode 100644 index 0000000000..f783f5a1f0 --- /dev/null +++ b/js/src/jit-test/tests/debug/testEarlyReturnOnCall.js @@ -0,0 +1,24 @@ +var g = newGlobal({newCompartment: true}); +g.eval("var success = false"); +g.eval("function ponies() {}"); +g.eval("function foo() { ponies(); success = false }"); + +var dbg = new Debugger(g); +dbg.onEnterFrame = function(frame) { + // The goal here is force an early return on the 'call' instruction, + // which should be the 3rd step (callgname, undefined, call) + var step = 0; + frame.onStep = function() { + ++step; + if (step == 2) { + g.success = true; + return; + } + if (step == 3) + return { return: undefined } + } + frame.onPop = function() { new Error(); /* boom */ } +} + +g.foo(); +assertEq(g.success, true); diff --git a/js/src/jit-test/tests/debug/uncaughtExceptionHook-01.js b/js/src/jit-test/tests/debug/uncaughtExceptionHook-01.js new file mode 100644 index 0000000000..2303e2339f --- /dev/null +++ b/js/src/jit-test/tests/debug/uncaughtExceptionHook-01.js @@ -0,0 +1,19 @@ +// Uncaught exceptions in the debugger itself are delivered to the +// uncaughtExceptionHook. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log; +dbg.onDebuggerStatement = function () { + log += 'x'; + throw new TypeError("fail"); +}; +dbg.uncaughtExceptionHook = function (exc) { + assertEq(this, dbg); + assertEq(exc instanceof TypeError, true); + log += '!'; +}; + +log = ''; +g.eval("debugger"); +assertEq(log, 'x!'); diff --git a/js/src/jit-test/tests/debug/uncaughtExceptionHook-02.js b/js/src/jit-test/tests/debug/uncaughtExceptionHook-02.js new file mode 100644 index 0000000000..ee58b40182 --- /dev/null +++ b/js/src/jit-test/tests/debug/uncaughtExceptionHook-02.js @@ -0,0 +1,12 @@ +// Returning a bad resumption value causes an exception that is reported to the +// uncaughtExceptionHook. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +dbg.onDebuggerStatement = function () { return {oops: "bad resumption value"}; }; +dbg.uncaughtExceptionHook = function (exc) { + assertEq(exc instanceof TypeError, true); + return {return: "pass"}; +}; + +assertEq(g.eval("debugger"), "pass"); diff --git a/js/src/jit-test/tests/debug/uncaughtExceptionHook-03.js b/js/src/jit-test/tests/debug/uncaughtExceptionHook-03.js new file mode 100644 index 0000000000..0924c8f023 --- /dev/null +++ b/js/src/jit-test/tests/debug/uncaughtExceptionHook-03.js @@ -0,0 +1,34 @@ +// |jit-test| error: ReferenceError +// If uncaughtExceptionHook is absent, the debuggee is terminated. + +var g = newGlobal({newCompartment: true}); +g.debuggeeGlobal = this; +g.eval("(" + function () { + var dbg = Debugger(debuggeeGlobal); + dbg.onDebuggerStatement = function (frame) { + if (frame.callee === null) { + debuggeeGlobal.log += '1'; + var cv = frame.eval("f();"); + debuggeeGlobal.log += '2'; + assertEq(cv, null); + } else { + assertEq(frame.callee.name, "f"); + debuggeeGlobal.log += '3'; + throw new ReferenceError("oops"); + } + }; + } + ")();"); + +function onerror(msg) { +} + +var log = ''; +debugger; +function f() { + try { + debugger; + } finally { + log += 'x'; + } +} +assertEq(log, '132'); diff --git a/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-01.js b/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-01.js new file mode 100644 index 0000000000..5ef62b6f6d --- /dev/null +++ b/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-01.js @@ -0,0 +1,25 @@ +// uncaughtExceptionHook returns a resumption value. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var rv; +dbg.onDebuggerStatement = function () { throw 15; }; +dbg.uncaughtExceptionHook = function (exc) { + assertEq(exc, 15); + return rv; +}; + +// case 1: undefined +rv = undefined; +g.eval("debugger"); + +// case 2: throw +rv = {throw: 57}; +var result; +assertThrowsValue(function () { g.eval("debugger"); }, 57); + +// case 3: return +rv = {return: 42}; +assertEq(g.eval("debugger;"), 42); diff --git a/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-02.js b/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-02.js new file mode 100644 index 0000000000..3ba7070a7b --- /dev/null +++ b/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-02.js @@ -0,0 +1,25 @@ +// uncaughtExceptionHook resumption value other than undefined does not cause +// further hooks to be skipped. + +var g = newGlobal({newCompartment: true}); +var log; + +function makeDebug(g, name) { + var dbg = new Debugger(g); + dbg.onDebuggerStatement = function (frame) { + log += name; + throw new Error(name); + }; + dbg.uncaughtExceptionHook = function (exc) { + assertEq(exc.message, name); + return name == "2" ? {return: 42} : undefined; + }; +} + +var arr = []; +for (var i = 0; i < 6; i++) + arr[i] = makeDebug(g, "" + i); + +log = ''; +assertEq(g.eval("debugger;"), 42); +assertEq(log, "012345"); diff --git a/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-03.js b/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-03.js new file mode 100644 index 0000000000..7835271e3c --- /dev/null +++ b/js/src/jit-test/tests/debug/uncaughtExceptionHook-resumption-03.js @@ -0,0 +1,12 @@ +// After an onExceptionUnwind hook throws, if uncaughtExceptionHook returns +// undefined, the original exception continues to propagate. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +var log = ''; +dbg.onExceptionUnwind = function () { log += "1"; throw new Error("oops"); }; +dbg.uncaughtExceptionHook = function () { log += "2"; }; + +g.eval("var x = new Error('oops');"); +g.eval("try { throw x; } catch (exc) { assertEq(exc, x); }"); +assertEq(log, "12"); diff --git a/js/src/jit-test/tests/debug/wasm-01.js b/js/src/jit-test/tests/debug/wasm-01.js new file mode 100644 index 0000000000..1d9396aa1b --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-01.js @@ -0,0 +1,32 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts are available via findScripts. + +var g = newGlobal({newCompartment: true}); +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); + +function isWasm(script) { return script.format === "wasm"; } + +var dbg = new Debugger(g); +var foundScripts1 = dbg.findScripts().filter(isWasm); +assertEq(foundScripts1.length, 1); +var found = foundScripts1[0]; + +// Add another module, we should be able to find it via findScripts. +g.eval(`o2 = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "a" (func 0)))')));`); +var foundScripts2 = dbg.findScripts().filter(isWasm); +assertEq(foundScripts2.length, 2); + +// The first module should be in the list as wrapping the same wasm module +// twice gets the same Debugger.Script. +assertEq(foundScripts2.indexOf(found) !== -1, true); + +// The two modules are distinct. +assertEq(foundScripts2[0] !== foundScripts2[1], true); + +// We should be able to find the same script via its source. +for (var ws of foundScripts2) { + var scriptsFromSource = dbg.findScripts({ source: ws.source }); + assertEq(scriptsFromSource.length, 1); + assertEq(scriptsFromSource[0], ws); +} diff --git a/js/src/jit-test/tests/debug/wasm-02.js b/js/src/jit-test/tests/debug/wasm-02.js new file mode 100644 index 0000000000..c0f3c52f3a --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-02.js @@ -0,0 +1,21 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts are available via onNewScript. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var gotScript; +dbg.onNewScript = (script) => { + gotScript = script; +} + +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); +assertEq(gotScript.format, "wasm"); + +var gotScript2 = gotScript; +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "a" (func 0)))')));`); +assertEq(gotScript.format, "wasm"); + +// The two wasm Debugger.Scripts are distinct. +assertEq(gotScript !== gotScript2, true); diff --git a/js/src/jit-test/tests/debug/wasm-03.js b/js/src/jit-test/tests/debug/wasm-03.js new file mode 100644 index 0000000000..99f377f18b --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-03.js @@ -0,0 +1,35 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts have synthesized sources. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var s; +dbg.onNewScript = (script) => { + s = script; +} + +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); +assertEq(s.format, "wasm"); + +var source = s.source; + +assertEq(s.source, source); +assertEq(source.introductionType, "wasm"); +assertEq(source.introductionScript, s); +// Wasm sources shouldn't be considered source mapped. +assertEq(!source.sourceMapURL, true); +assertThrowsInstanceOf(() => source.sourceMapURL = 'foo', Error); +// We must have some text. +assertEq(!!source.text, true); + +// TODOshu: Wasm is moving very fast and what we return for these values is +// currently not interesting to test. Instead, test that they do not throw. +source.url; +source.element; +source.displayURL; +source.introductionOffset; +source.elementAttributeName; diff --git a/js/src/jit-test/tests/debug/wasm-04.js b/js/src/jit-test/tests/debug/wasm-04.js new file mode 100644 index 0000000000..3d18e6ce1b --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-04.js @@ -0,0 +1,27 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts throw for everything except text. + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var s; +dbg.onNewScript = (script) => { + s = script; +} + +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); +assertEq(s.format, "wasm"); + +assertThrowsInstanceOf(() => s.displayName, Error); +assertThrowsInstanceOf(() => s.parameterNames, Error); +assertThrowsInstanceOf(() => s.url, Error); +assertThrowsInstanceOf(() => s.sourceStart, Error); +assertThrowsInstanceOf(() => s.sourceLength, Error); +assertThrowsInstanceOf(() => s.global, Error); +assertThrowsInstanceOf(() => s.getChildScripts(), Error); +assertThrowsInstanceOf(() => s.getAllOffsets(), Error); +assertThrowsInstanceOf(() => s.getBreakpoint(0), Error); +assertThrowsInstanceOf(() => s.getOffsetsCoverage(), Error); diff --git a/js/src/jit-test/tests/debug/wasm-06-onEnterFrame-null.js b/js/src/jit-test/tests/debug/wasm-06-onEnterFrame-null.js new file mode 100644 index 0000000000..b4afd9753f --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-06-onEnterFrame-null.js @@ -0,0 +1,17 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; exitstatus: 3; skip-if: !wasmDebuggingEnabled() +// Checking resumption values for 'null' at onEnterFrame. + +load(libdir + "asserts.js"); + +var g = newGlobal(''); +var dbg = new Debugger(); +dbg.addDebuggee(g); +sandbox.eval(` +var wasm = wasmTextToBinary('(module (func (nop)) (export "test" 0))'); +var m = new WebAssembly.Instance(new WebAssembly.Module(wasm));`); +dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + return null; +}; +g.eval("m.exports.test()"); +assertEq(false, true); diff --git a/js/src/jit-test/tests/debug/wasm-06-onPop-null.js b/js/src/jit-test/tests/debug/wasm-06-onPop-null.js new file mode 100644 index 0000000000..76bfb146ef --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-06-onPop-null.js @@ -0,0 +1,19 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; exitstatus: 3; skip-if: !wasmDebuggingEnabled() +// Checking resumption values for 'null' at frame's onPop. + +load(libdir + "asserts.js"); + +var g = newGlobal(''); +var dbg = new Debugger(); +dbg.addDebuggee(g); +sandbox.eval(` +var wasm = wasmTextToBinary('(module (func (nop)) (export "test" 0))'); +var m = new WebAssembly.Instance(new WebAssembly.Module(wasm));`); +dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + frame.onPop = function () { + return null; + }; +}; +g.eval("m.exports.test()"); +assertEq(false, true); diff --git a/js/src/jit-test/tests/debug/wasm-06.js b/js/src/jit-test/tests/debug/wasm-06.js new file mode 100644 index 0000000000..d8a8aa8b18 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-06.js @@ -0,0 +1,332 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; error: TestComplete; skip-if: !wasmDebuggingEnabled() +// Tests that wasm module scripts raises onEnterFrame and onLeaveFrame events. + +load(libdir + "asserts.js"); + +function runWasmWithDebugger(wast, lib, init, done) { + let g = newGlobal({newCompartment: true}); + let dbg = new Debugger(g); + + g.eval(` +var wasm = wasmTextToBinary('${wast}'); +var lib = ${lib || 'undefined'}; +var m = new WebAssembly.Instance(new WebAssembly.Module(wasm), lib);`); + + init(dbg, g); + let result = undefined, error = undefined; + try { + result = g.eval("m.exports.test()"); + } catch (ex) { + error = ex; + } + done(dbg, result, error, g); +} + +// Checking if onEnterFrame is fired for wasm frames and verifying the content +// of the frame and environment properties. +var onEnterFrameCalled, onLeaveFrameCalled, onExceptionUnwindCalled, testComplete; +runWasmWithDebugger( + '(module (func (result i32) (i32.const 42)) (export "test" (func 0)))', undefined, + function (dbg) { + var wasmScript = dbg.findScripts().filter(s => s.format == 'wasm')[0]; + assertEq(!!wasmScript, true); + onEnterFrameCalled = 0; + onLeaveFrameCalled = 0; + testComplete = false; + var evalFrame; + dbg.onEnterFrame = function (frame) { + if (frame.type !== 'wasmcall') { + if (frame.type === 'eval') + evalFrame = frame; + return; + } + + onEnterFrameCalled++; + + assertEq(frame.script, wasmScript); + assertEq(frame.older, evalFrame); + assertEq(frame.type, 'wasmcall'); + + let env = frame.environment; + assertEq(env instanceof Object, true); + assertEq(env.inspectable, true); + assertEq(env.parent !== null, true); + assertEq(env.type, 'declarative'); + assertEq(env.calleeScript, null); + assertEq(Array.isArray(env.names()), true); + assertEq(env.names().length, 0); + + frame.onPop = function() { + onLeaveFrameCalled++; + testComplete = true; + }; + }; + }, + function (dbg, result, error) { + assertEq(testComplete, true); + assertEq(onEnterFrameCalled, 1); + assertEq(onLeaveFrameCalled, 1); + assertEq(result, 42); + assertEq(error, undefined); + } +); + +// Checking the dbg.getNewestFrame() and frame.older. +runWasmWithDebugger( + '(module (import "env" "ex" (func $fn1)) (func $fn2 (call $fn1)) (export "test" (func $fn2)))', + '{env: { ex: () => { }}}', + function (dbg) { + onEnterFrameCalled = 0; + onLeaveFrameCalled = 0; + testComplete = false; + var evalFrame, wasmFrame; + dbg.onEnterFrame = function (frame) { + onEnterFrameCalled++; + + assertEq(dbg.getNewestFrame(), frame); + + switch (frame.type) { + case 'eval': + evalFrame = frame; + break; + case 'wasmcall': + wasmFrame = frame; + break; + case 'call': + assertEq(frame.older, wasmFrame); + assertEq(frame.older.older, evalFrame); + assertEq(frame.older.older.older, null); + testComplete = true; + break; + } + + frame.onPop = function() { + onLeaveFrameCalled++; + }; + }; + }, + function (dbg, result, error) { + assertEq(testComplete, true); + assertEq(onEnterFrameCalled, 3); + assertEq(onLeaveFrameCalled, 3); + assertEq(error, undefined); + } +); + +// Checking if we can enumerate frames and find 'wasmcall' one. +runWasmWithDebugger( + '(module (import "env" "ex" (func $fn1)) (func $fn2 (call $fn1)) (export "test" (func $fn2)))', + '{env: { ex: () => { debugger; }}}', + function (dbg) { + testComplete = false; + dbg.onDebuggerStatement = function (frame) { + assertEq(frame.type, 'call'); + assertEq(frame.older.type, 'wasmcall'); + assertEq(frame.older.older.type, 'eval'); + assertEq(frame.older.older.older, null); + testComplete = true; + } + }, + function (dbg, result, error) { + assertEq(testComplete, true); + assertEq(error, undefined); + } +); + +// Checking if onPop works without onEnterFrame handler. +runWasmWithDebugger( + '(module (import "env" "ex" (func $fn1)) (func $fn2 (call $fn1)) (export "test" (func $fn2)))', + '{env: { ex: () => { debugger; }}}', + function (dbg) { + onLeaveFrameCalled = 0; + dbg.onDebuggerStatement = function (frame) { + if (!frame.older || frame.older.type != 'wasmcall') + return; + frame.older.onPop = function () { + onLeaveFrameCalled++; + }; + } + }, + function (dbg, result, error) { + assertEq(onLeaveFrameCalled, 1); + assertEq(error, undefined); + } +); + +// Checking if function return values are not changed. +runWasmWithDebugger( + '(module (func (result f64) (f64.const 0.42)) (export "test" (func 0)))', undefined, + function (dbg) { + dbg.onEnterFrame = function (frame) { + dbg.onPop = function () {}; + }; + }, + function (dbg, result, error) { + assertEq(result, 0.42); + assertEq(error, undefined); + } +); +runWasmWithDebugger( + '(module (func (result f32) (f32.const 4.25)) (export "test" (func 0)))', undefined, + function (dbg) { + dbg.onEnterFrame = function (frame) { + dbg.onPop = function () {}; + }; + }, + function (dbg, result, error) { + assertEq(result, 4.25); + assertEq(error, undefined); + } +); + +// Checking if onEnterFrame/onExceptionUnwind work during exceptions -- +// `unreachable` causes wasm to throw WebAssembly.RuntimeError exception. +runWasmWithDebugger( + '(module (func (unreachable)) (export "test" (func 0)))', undefined, + function (dbg) { + onEnterFrameCalled = 0; + onLeaveFrameCalled = 0; + onExceptionUnwindCalled = 0; + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + onEnterFrameCalled++; + frame.onPop = function() { + onLeaveFrameCalled++; + }; + }; + dbg.onExceptionUnwind = function (frame) { + if (frame.type !== "wasmcall") return; + onExceptionUnwindCalled++; + }; + }, + function (dbg, result, error, g) { + assertEq(onEnterFrameCalled, 1); + assertEq(onLeaveFrameCalled, 1); + assertEq(onExceptionUnwindCalled, 1); + assertEq(error instanceof g.WebAssembly.RuntimeError, true); + } +); + +// Checking if onEnterFrame/onExceptionUnwind work during exceptions +// originated in the JavaScript import call. +runWasmWithDebugger( + '(module (import "env" "ex" (func $fn1)) (func $fn2 (call $fn1)) (export "test" (func $fn2)))', + '{env: { ex: () => { throw new Error(); }}}', + function (dbg) { + onEnterFrameCalled = 0; + onLeaveFrameCalled = 0; + onExceptionUnwindCalled = 0; + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + onEnterFrameCalled++; + frame.onPop = function() { + onLeaveFrameCalled++; + }; + }; + dbg.onExceptionUnwind = function (frame) { + if (frame.type !== "wasmcall") return; + onExceptionUnwindCalled++; + }; + }, + function (dbg, result, error, g) { + assertEq(onEnterFrameCalled, 1); + assertEq(onLeaveFrameCalled, 1); + assertEq(onExceptionUnwindCalled, 1); + assertEq(error instanceof g.Error, true); + } +); + +// Checking throwing in the handler. +runWasmWithDebugger( + '(module (func (unreachable)) (export "test" (func 0)))', undefined, + function (dbg) { + dbg.uncaughtExceptionHook = function (value) { + assertEq(value instanceof Error, true); + return {throw: 'test'}; + }; + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + throw new Error(); + }; + }, + function (dbg, result, error) { + assertEq(error, 'test'); + } +); +runWasmWithDebugger( + '(module (func (unreachable)) (export "test" (func 0)))', undefined, + function (dbg) { + dbg.uncaughtExceptionHook = function (value) { + assertEq(value instanceof Error, true); + return {throw: 'test'}; + }; + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + frame.onPop = function () { + throw new Error(); + } + }; + }, + function (dbg, result, error) { + assertEq(error, 'test'); + } +); + +// Checking resumption values for JS_THROW. +runWasmWithDebugger( + '(module (func (nop)) (export "test" (func 0)))', undefined, + function (dbg, g) { + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + return {throw: 'test'}; + }; + }, + function (dbg, result, error, g) { + assertEq(error, 'test'); + } +); +runWasmWithDebugger( + '(module (func (nop)) (export "test" (func 0)))', undefined, + function (dbg, g) { + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + frame.onPop = function () { + return {throw: 'test'}; + } + }; + }, + function (dbg, result, error, g) { + assertEq(error, 'test'); + } +); + +// Checking resumption values for JS_RETURN (not implemented by wasm baseline). +runWasmWithDebugger( + '(module (func (unreachable)) (export "test" (func 0)))', undefined, + function (dbg) { + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + return {return: 2}; + }; + }, + function (dbg, result, error) { + assertEq(result, undefined, 'NYI: result == 2, if JS_RETURN is implemented'); + assertEq(error != undefined, true, 'NYI: error == undefined, if JS_RETURN is implemented'); + } +); +runWasmWithDebugger( + '(module (func (unreachable)) (export "test" (func 0)))', undefined, + function (dbg) { + dbg.onEnterFrame = function (frame) { + if (frame.type !== "wasmcall") return; + frame.onPop = function () { + return {return: 2}; + } + }; + }, + function (dbg, result, error) { + assertEq(result, undefined, 'NYI: result == 2, if JS_RETURN is implemented'); + assertEq(error != undefined, true, 'NYI: error == undefined, if JS_RETURN is implemented'); + } +); +throw "TestComplete"; diff --git a/js/src/jit-test/tests/debug/wasm-07.js b/js/src/jit-test/tests/debug/wasm-07.js new file mode 100644 index 0000000000..be32a9977a --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-07.js @@ -0,0 +1,39 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() + +// Checking existence of all frame.offset references during onEnterFrame, +// onLeaveFrame and onStep events in the source code, and that we can +// potentially resolve offset back to the line/column. + +load(libdir + "wasm.js"); + +var offsets; +wasmRunWithDebugger( + '(module (func (nop) (nop)) (export "test" (func 0)))', + undefined, + function ({dbg}) { + offsets = []; + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') { + return; + } + offsets.push(frame.offset); + frame.onStep = function () { + offsets.push(frame.offset); + }; + frame.onPop = function () { + offsets.push(frame.offset); + }; + }; + }, + function ({wasmScript, error}) { + assertEq(error, undefined); + assertEq(offsets.length, 5); + offsets.forEach(offset => { + var loc = wasmScript.getOffsetLocation(offset); + assertEq(loc.isEntryPoint, true); + assertEq(loc.lineNumber > 0, true); + assertEq(loc.columnNumber > 0, true); + assertEq(wasmScript.getLineOffsets(loc.lineNumber).length, 1); + }); + } +); diff --git a/js/src/jit-test/tests/debug/wasm-08.js b/js/src/jit-test/tests/debug/wasm-08.js new file mode 100644 index 0000000000..b0b61d54fd --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-08.js @@ -0,0 +1,121 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() +// Checking if we destroying work registers by breakpoint/step handler. + +load(libdir + "wasm.js"); + +// Running the following code compiled from C snippet: +// +// signed func0(signed n) { +// double a = 1; float b = 0; signed c = 1; long long d = 1; +// for (;n > 0; n--) { +// a *= c; b += c; c++; d <<= 1; +// } +// return (signed)a + (signed)b + c + (signed)d; +// } +// +var onStepCalled; +wasmRunWithDebugger( + `(module + (func $func0 (param $var0 i32) (result i32) + (local $var1 i32) (local $var2 i64) (local $var3 f64) (local $var4 f32) + (local $var5 f64) (local $var6 i32) (local $var7 i32) (local $var8 i32) + i32.const 1 + set_local $var1 + i32.const 0 + set_local $var7 + i32.const 1 + set_local $var6 + i32.const 1 + set_local $var8 + block $label0 + get_local $var0 + i32.const 1 + i32.lt_s + br_if $label0 + get_local $var0 + i32.const 1 + i32.add + set_local $var1 + f32.const 0 + set_local $var4 + i64.const 1 + set_local $var2 + f64.const 1 + set_local $var3 + i32.const 1 + set_local $var0 + f64.const 1 + set_local $var5 + block + loop $label1 + get_local $var2 + i64.const 1 + i64.shl + set_local $var2 + get_local $var5 + get_local $var3 + f64.mul + set_local $var5 + get_local $var4 + get_local $var0 + f32.convert_s/i32 + f32.add + set_local $var4 + get_local $var3 + f64.const 1 + f64.add + set_local $var3 + get_local $var0 + i32.const 1 + i32.add + tee_local $var6 + set_local $var0 + get_local $var1 + i32.const -1 + i32.add + tee_local $var1 + i32.const 1 + i32.gt_s + br_if $label1 + end $label1 + end + get_local $var2 + i32.wrap/i64 + set_local $var1 + get_local $var4 + i32.trunc_s/f32 + set_local $var7 + get_local $var5 + i32.trunc_s/f64 + set_local $var8 + end $label0 + get_local $var7 + get_local $var8 + i32.add + get_local $var6 + i32.add + get_local $var1 + i32.add + return + ) + (func (export "test") (result i32) (call $func0 (i32.const 4))) +)`.split('\n').join(' '), + undefined, + function ({dbg}) { + onStepCalled = []; + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') return; + frame.onStep = function () { + onStepCalled.push(frame.offset); + }; + }; + }, + function ({result, error}) { + assertEq(result, /* func0(4) = */ 55); + assertEq(error, undefined); + // The number below reflects amount of wasm operators executed during + // the run of the test function, which runs $func0(4). It can change + // when the code above and/or meaning of wasm operators will change. + assertEq(onStepCalled.length, 166); + } +); diff --git a/js/src/jit-test/tests/debug/wasm-09.js b/js/src/jit-test/tests/debug/wasm-09.js new file mode 100644 index 0000000000..6612a6434d --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-09.js @@ -0,0 +1,32 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() +// Tests debugEnabled state of wasm when allowUnobservedAsmJS == true. + +load(libdir + "asserts.js"); + +// Checking that there are no offsets are present in a wasm instance script for +// which debug mode was not enabled. +function getWasmScriptWithoutAllowUnobservedAsmJS(wast) { + var sandbox = newGlobal({newCompartment: true}); + var dbg = new Debugger(); + dbg.allowUnobservedAsmJS = true; + dbg.addDebuggee(sandbox); + sandbox.eval(` + var wasm = wasmTextToBinary('${wast}'); + var m = new WebAssembly.Instance(new WebAssembly.Module(wasm)); + `); + // Attaching after wasm instance is created. + var wasmScript = dbg.findScripts().filter(s => s.format == 'wasm')[0]; + return wasmScript; +} + +var wasmScript1 = getWasmScriptWithoutAllowUnobservedAsmJS('(module (func (nop)))'); +var wasmLines1 = wasmScript1.source.text.split('\n'); +assertEq(wasmScript1.startLine, 1); +assertEq(wasmScript1.lineCount, 0); +assertEq(wasmLines1.every((l, n) => wasmScript1.getLineOffsets(n + 1).length == 0), true); + +// Checking that we must not resolve any location for any offset in a wasm +// instance which debug mode was not enabled. +var wasmScript2 = getWasmScriptWithoutAllowUnobservedAsmJS('(module (func (nop)))'); +for (var i = wasmTextToBinary('(module (func (nop)))').length - 1; i >= 0; i--) + assertThrowsInstanceOf(() => wasmScript2.getOffsetLocation(i), Error); diff --git a/js/src/jit-test/tests/debug/wasm-10.js b/js/src/jit-test/tests/debug/wasm-10.js new file mode 100644 index 0000000000..6f43385bd3 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-10.js @@ -0,0 +1,77 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() +// Tests that wasm module scripts has inspectable locals. + +load(libdir + "wasm.js"); +load(libdir + 'eqArrayHelper.js'); + +function monitorLocalValues(wast, lib, expected) { + function setupFrame(frame) { + var locals = {}; + framesLocals.push(locals); + frame.environment.names().forEach(n => { + locals[n] = [frame.environment.getVariable(n)]; + }); + frame.onStep = function () { + frame.environment.names().forEach(n => { + var prevValues = locals[n]; + if (!prevValues) + locals[n] = prevValues = [void 0]; + var value = frame.environment.getVariable(n); + if (prevValues[prevValues.length - 1] !== value) + prevValues.push(value); + }); + } + } + var framesLocals = []; + wasmRunWithDebugger(wast, lib, + function ({dbg}) { + dbg.onEnterFrame = function(frame) { + if (frame.type == "wasmcall") + setupFrame(frame); + } + }, + function ({error}) { + assertEq(error, undefined); + } + ); + assertEq(framesLocals.length, expected.length); + for (var i = 0; i < framesLocals.length; i++) { + var frameLocals = framesLocals[i]; + var expectedLocals = expected[i]; + var localsNames = Object.keys(frameLocals); + assertEq(localsNames.length, Object.keys(expectedLocals).length); + localsNames.forEach(n => { + assertEqArray(frameLocals[n], expectedLocals[n]); + }); + } +} + +monitorLocalValues( + '(module (func (nop) (nop)) (export "test" (func 0)))', + undefined, + [{}] +); +monitorLocalValues( + '(module (func (export "test") (local i32) (i32.const 1) (set_local 0)))', + undefined, + [{var0: [0, 1]}] +); +monitorLocalValues( + '(module (func (export "test") (local f32) (f32.const 1.5) (set_local 0)))', + undefined, + [{var0: [0, 1.5]}] +); +monitorLocalValues( + '(module (func (export "test") (local f64) (f64.const 42.25) (set_local 0)))', + undefined, + [{var0: [0, 42.25]}] +); +monitorLocalValues( + `(module + (func (param i32) (result i32) (get_local 0) (i32.const 2) (i32.add)) + (func (param i32) (local i32) (get_local 0) (call 0) (set_local 1)) + (func (export "test") (i32.const 1) (call 1)) +)`.replace(/\n/g, " "), + undefined, + [{}, {var0: [1], var1: [0, 3]}, {var0: [1]}] +); diff --git a/js/src/jit-test/tests/debug/wasm-11.js b/js/src/jit-test/tests/debug/wasm-11.js new file mode 100644 index 0000000000..1e776c2008 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-11.js @@ -0,0 +1,33 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() + +// Test single-stepping where the TLS register can be evicted by a non-trivial +// function body. + +var g = newGlobal({newCompartment: true}); +g.parent = this; +g.eval(` + var dbg = new Debugger(parent); +`); + +var i = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary(` + (module + (func (export "f2") + i64.const 0 + i64.const 0 + i32.const 0 + select + drop + ) + ) +`))); + +g.eval(` + var calledOnStep = 0; + dbg.onEnterFrame = frame => { + if (frame.type === "wasmcall") + frame.onStep = () => { calledOnStep++ } + }; +`); + +i.exports.f2(); +assertEq(g.calledOnStep >= 5, true); diff --git a/js/src/jit-test/tests/debug/wasm-12.js b/js/src/jit-test/tests/debug/wasm-12.js new file mode 100644 index 0000000000..6c645f3f7f --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-12.js @@ -0,0 +1,25 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts have special URLs. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); +g.eval(` +function initWasm(s) { return new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary(s))); } +o1 = initWasm('(module (func) (export "" (func 0)))'); +o2 = initWasm('(module (func) (func) (export "" (func 1)))'); +`); + +function isWasm(script) { return script.format === "wasm"; } + +function isValidWasmURL(url) { + // The URLs will have the following format: + // wasm: [<uri-encoded-filename-of-host> ":"] <64-bit-hash> + return /^wasm:(?:[^:]*:)*?[0-9a-f]{16}$/.test(url); +} + +var foundScripts = dbg.findScripts().filter(isWasm); +assertEq(foundScripts.length, 2); +assertEq(isValidWasmURL(foundScripts[0].source.url), true); +assertEq(isValidWasmURL(foundScripts[1].source.url), true); +assertEq(foundScripts[0].source.url != foundScripts[1].source.url, true); diff --git a/js/src/jit-test/tests/debug/wasm-13.js b/js/src/jit-test/tests/debug/wasm-13.js new file mode 100644 index 0000000000..9c16fa8058 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-13.js @@ -0,0 +1,106 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() +// Tests that wasm module scripts has inspectable globals and memory. + +load(libdir + "wasm.js"); +load(libdir + 'eqArrayHelper.js'); + +function monitorGlobalValues(wast, lib, expected) { + function setupFrame(frame) { + var globals = {}; + framesGlobals.push(globals); + // Environment with globals follow function scope enviroment + var globalEnv = frame.environment.parent; + globalEnv.names().forEach(n => { + globals[n] = [globalEnv.getVariable(n)]; + }); + frame.onStep = function () { + var globalEnv = frame.environment.parent; + globalEnv.names().forEach(n => { + var prevValues = globals[n]; + if (!prevValues) + globals[n] = prevValues = [void 0]; + var value = globalEnv.getVariable(n); + if (prevValues[prevValues.length - 1] !== value) + prevValues.push(value); + }); + } + } + var framesGlobals = []; + wasmRunWithDebugger(wast, lib, + function ({dbg}) { + dbg.onEnterFrame = function(frame) { + if (frame.type == "wasmcall") + setupFrame(frame); + } + }, + function ({error}) { + assertEq(error, undefined); + } + ); + assertEq(framesGlobals.length, expected.length); + for (var i = 0; i < framesGlobals.length; i++) { + var frameGlobals = framesGlobals[i]; + var expectedGlobals = expected[i]; + var globalsNames = Object.keys(frameGlobals); + assertEq(globalsNames.length, Object.keys(expectedGlobals).length); + globalsNames.forEach(n => { + if (typeof expectedGlobals[n][0] === "function") { + // expectedGlobals are assert functions + expectedGlobals[n].forEach((assertFn, i) => { + assertFn(frameGlobals[n][i]); + }); + return; + } + assertEqArray(frameGlobals[n], expectedGlobals[n]); + }); + } +} + +monitorGlobalValues( + '(module (func (export "test") (nop)))', + undefined, + [{}] +); +monitorGlobalValues( + '(module (memory (export "memory") 1 1) (func (export "test") (nop) (nop)))', + undefined, + [{ + memory0: [ + function (actual) { + var bufferProp = actual.proto.getOwnPropertyDescriptor("buffer"); + assertEq(!!bufferProp, true, "wasm memory buffer property"); + var buffer = bufferProp.get.call(actual).return; + var bufferLengthProp = buffer.proto.getOwnPropertyDescriptor("byteLength"); + var bufferLength = bufferLengthProp.get.call(buffer).return; + assertEq(bufferLength, 65536, "wasm memory size"); + } + ] + }] +); +monitorGlobalValues( + `(module\ + (global i32 (i32.const 1))(global i64 (i64.const 2))(global f32 (f32.const 3.5))(global f64 (f64.const 42.25))\ + ${wasmReftypesEnabled() ? '(global externref (ref.null extern))' : ''}\ + (func (export "test") (nop)))`, + undefined, + [(function () { + let x = {global0: [1], global1: [2], global2: [3.5], global3: [42.25]}; + if (wasmReftypesEnabled()) x.global4 = [ function (x) { assertEq(x.optimizedOut, true); } ]; + return x; + })()] +); +monitorGlobalValues( + `(module (global (mut i32) (i32.const 1))(global (mut i64) (i64.const 2))\ + (global (mut f32) (f32.const 3.5))(global (mut f64) (f64.const 42.25))\ + ${wasmReftypesEnabled() ? '(global (mut externref) (ref.null extern))' : ''}\ + (func (export "test")\ + (i32.const 2)(global.set 0)(i64.const 1)(global.set 1)\ + (f32.const 42.25)(global.set 2)(f64.const 3.5)(global.set 3)\ + ${wasmReftypesEnabled() ? '(ref.null extern)(global.set 4)' : ''}))`, + undefined, + [(function () { + let x = {global0: [1, 2], global1: [2, 1], global2: [3.5, 42.25], global3: [42.25, 3.5]}; + if (wasmReftypesEnabled()) x.global4 = [ function (x) { assertEq(x.optimizedOut, true); } ]; + return x; + })()] +) diff --git a/js/src/jit-test/tests/debug/wasm-binary-sources.js b/js/src/jit-test/tests/debug/wasm-binary-sources.js new file mode 100644 index 0000000000..4ed5b9fb50 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-binary-sources.js @@ -0,0 +1,32 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts have access to binary sources. + +load(libdir + "asserts.js"); +load(libdir + "array-compare.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var s; +dbg.onNewScript = (script) => { + s = script; +} + +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); +assertEq(s.format, "wasm"); + +var source = s.source; + +// The text is never generated with the native Debugger API. +assertEq(source.text.includes('module'), false); + +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(wasmTextToBinary('(module (func) (export "" (func 0)))')));`); +assertEq(s.format, "wasm"); + +var source2 = s.source; + +// The text is predefined if wasm binary sources are enabled. +assertEq(source2.text, '[debugger missing wasm binary-to-text conversion]'); +// The binary contains Uint8Array which is equal to wasm bytecode; +arraysEqual(source2.binary, wasmTextToBinary('(module (func) (export "" (func 0)))')); diff --git a/js/src/jit-test/tests/debug/wasm-breakpoint.js b/js/src/jit-test/tests/debug/wasm-breakpoint.js new file mode 100644 index 0000000000..803ad4dde7 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-breakpoint.js @@ -0,0 +1,194 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() +// Tests that wasm module scripts handles basic breakpoint operations. + +load(libdir + "wasm.js"); + +function runTest(wast, initFunc, doneFunc) { + let g = newGlobal({newCompartment: true}); + let dbg = new Debugger(g); + + g.eval(` +var b = wasmTextToBinary('${wast}'); +var m = new WebAssembly.Instance(new WebAssembly.Module(b)); +var breakpoints = wasmCodeOffsets(b); +`); + + var { breakpoints } = g; + + var wasmScript = dbg.findScripts().filter(s => s.format == 'wasm')[0]; + + initFunc({ + dbg, + wasmScript, + g, + breakpoints + }); + + let result, error; + try { + result = g.eval("m.exports.test()"); + } catch (ex) { + error = ex; + } + + doneFunc({ + dbg, + result, + error, + wasmScript, + g + }); +} + + +var onBreakpointCalled; + +// Checking if we can stop at specified breakpoint. +runTest( + '(module (func (nop) (nop)) (export "test" (func 0)))', + function ({wasmScript, breakpoints}) { + print(`${JSON.stringify(breakpoints)}`); + assertEq(breakpoints.length, 3); + assertEq(breakpoints[0] > 0, true); + // Checking if breakpoints offsets are in ascending order. + assertEq(breakpoints[0] < breakpoints[1], true); + assertEq(breakpoints[1] < breakpoints[2], true); + onBreakpointCalled = 0; + breakpoints.forEach(function (offset) { + wasmScript.setBreakpoint(offset, { + hit: (frame) => { + assertEq(frame.offset, offset); + onBreakpointCalled++; + } + }); + }); + }, + function ({dbg, error}) { + assertEq(error, undefined); + assertEq(onBreakpointCalled, 3); + } +); + +// Checking if we can remove breakpoint one by one. +runTest( + '(module (func (nop) (nop)) (export "test" (func 0)))', + function ({wasmScript, breakpoints}) { + onBreakpointCalled = 0; + var handlers = []; + breakpoints.forEach(function (offset, i) { + wasmScript.setBreakpoint(offset, handlers[i] = { + hit: (frame) => { + assertEq(frame.offset, breakpoints[0]); + onBreakpointCalled++; + // Removing all handlers. + handlers.forEach(h => wasmScript.clearBreakpoint(h)); + } + }); + }); + }, + function ({error}) { + assertEq(error, undefined); + assertEq(onBreakpointCalled, 1); + } +); + +// Checking if we can remove breakpoint one by one from a breakpoint handler. +runTest( + '(module (func (nop) (nop)) (export "test" (func 0)))', + function ({wasmScript, breakpoints}) { + onBreakpointCalled = 0; + var handlers = []; + breakpoints.forEach(function (offset, i) { + wasmScript.setBreakpoint(offset, handlers[i] = { + hit: (frame) => { + assertEq(frame.offset, breakpoints[0]); + onBreakpointCalled++; + // Removing all handlers. + handlers.forEach(h => wasmScript.clearBreakpoint(h)); + } + }); + }); + }, + function ({error}) { + assertEq(error, undefined); + assertEq(onBreakpointCalled, 1); + } +); + +// Checking if we can remove breakpoint one by one from onEnterFrame, +// but onStep will still work. +var onStepCalled; +runTest( + '(module (func (nop) (nop)) (export "test" (func 0)))', + function ({dbg, wasmScript, breakpoints}) { + onBreakpointCalled = 0; + onStepCalled = []; + var handlers = []; + breakpoints.forEach(function (offset, i) { + wasmScript.setBreakpoint(offset, handlers[i] = { + hit: (frame) => { + assertEq(false, true); + onBreakpointCalled++; + } + }); + }); + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') return; + frame.onStep = function () { + onStepCalled.push(frame.offset); + }; + + // Removing all handlers. + handlers.forEach(h => wasmScript.clearBreakpoint(h)); + }; + }, + function ({error}) { + assertEq(error, undefined); + assertEq(onBreakpointCalled, 0); + assertEq(onStepCalled.length, 3); + } +); + +// Checking if we can remove all breakpoints. +runTest( + '(module (func (nop) (nop)) (export "test" (func 0)))', + function ({wasmScript, breakpoints}) { + onBreakpointCalled = 0; + breakpoints.forEach(function (offset, i) { + wasmScript.setBreakpoint(offset, { + hit: (frame) => { + assertEq(frame.offset, breakpoints[0]); + onBreakpointCalled++; + // Removing all handlers. + wasmScript.clearAllBreakpoints(); + } + }); + }); + }, + function ({error}) { + assertEq(error, undefined); + assertEq(onBreakpointCalled, 1); + } +); + +// Checking if breakpoints are removed after debugger has been detached. +runTest( + '(module (func (nop) (nop)) (export "test" (func 0)))', + function ({dbg, wasmScript, g, breakpoints}) { + onBreakpointCalled = 0; + breakpoints.forEach(function (offset, i) { + wasmScript.setBreakpoint(offset, { + hit: (frame) => { + onBreakpointCalled++; + } + }); + }); + dbg.onEnterFrame = function (frame) { + dbg.removeDebuggee(g); + }; + }, + function ({error}) { + assertEq(error, undefined); + assertEq(onBreakpointCalled, 0); + } +); diff --git a/js/src/jit-test/tests/debug/wasm-get-return.js b/js/src/jit-test/tests/debug/wasm-get-return.js new file mode 100644 index 0000000000..233d8cf5da --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-get-return.js @@ -0,0 +1,62 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() +// Tests that wasm frame opPop event can access function resumption value. + +load(libdir + "wasm.js"); +load(libdir + 'eqArrayHelper.js'); + +function monitorFrameOnPopReturns(wast, expected) { + var values = []; + wasmRunWithDebugger( + wast, + undefined, + function ({dbg}) { + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') return; + frame.onPop = function (value) { + values.push(value.return); + }; + }; + }, + function ({error}) { + assertEq(error, undefined); + } + ); + assertEqArray(values, expected); +} + +monitorFrameOnPopReturns( + `(module (func (export "test")))`, + [undefined]); +monitorFrameOnPopReturns( + `(module (func (export "test") (result i32) (i32.const 42)))`, + [42]); +monitorFrameOnPopReturns( + `(module (func (export "test") (result f32) (f32.const 0.5)))`, + [0.5]); +monitorFrameOnPopReturns( + `(module (func (export "test") (result f64) (f64.const -42.75)))`, + [-42.75]); +monitorFrameOnPopReturns( + `(module (func (result i64) (i64.const 2)) (func (export "test") (call 0) (drop)))`, + [2, undefined]); + +// Checking if throwing frame has right resumption value. +var throwCount = 0; +wasmRunWithDebugger( + '(module (func (unreachable)) (func (export "test") (result i32) (call 0) (i32.const 1)))', + undefined, + function ({dbg, g}) { + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') return; + frame.onPop = function (value) { + if ('throw' in value) + throwCount++; + }; + }; + }, + function ({error}) { + assertEq(error != undefined, true); + assertEq(throwCount, 2); + } +); + diff --git a/js/src/jit-test/tests/debug/wasm-getAllColumnOffsets.js b/js/src/jit-test/tests/debug/wasm-getAllColumnOffsets.js new file mode 100644 index 0000000000..58e85f1d84 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-getAllColumnOffsets.js @@ -0,0 +1,41 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts have column and line to bytecode offset +// information when source text is generated. + +load(libdir + "asserts.js"); + +// Checking if experimental format generates internal source map to binary file +// by querying debugger scripts getAllColumnOffsets. +function getAllOffsets(wast) { + var sandbox = newGlobal({newCompartment: true}); + var dbg = new Debugger(); + dbg.addDebuggee(sandbox); + dbg.allowWasmBinarySource = true; + sandbox.eval(` + var wasm = wasmTextToBinary('${wast}'); + var m = new WebAssembly.Instance(new WebAssembly.Module(wasm)); + `); + var wasmScript = dbg.findScripts().filter(s => s.format == 'wasm')[0]; + return wasmScript.getAllColumnOffsets(); +} + +var offsets1 = getAllOffsets('(module \ + (func (nop)) \ + (func (drop (f32.sqrt (f32.add (f32.const 1.0) (f32.const 2.0))))) \ +)'); + +// There shall be total 8 lines with single and unique offset per line. +var usedOffsets = Object.create(null), + usedLines = Object.create(null); +assertEq(offsets1.length, 8); + +offsets1.forEach(({offset, lineNumber, columnNumber}) => { + assertEq(offset > 0, true); + assertEq(lineNumber > 0, true); + assertEq(columnNumber > 0, true); + usedOffsets[offset] = true; + usedLines[lineNumber] = true; +}); +assertEq(Object.keys(usedOffsets).length, 8); +assertEq(Object.keys(usedLines).length, 8); diff --git a/js/src/jit-test/tests/debug/wasm-jseval.js b/js/src/jit-test/tests/debug/wasm-jseval.js new file mode 100644 index 0000000000..ef35c3c0b7 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-jseval.js @@ -0,0 +1,45 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() +// Tests that JS can be evaluated on wasm module scripts frames. + +load(libdir + "wasm.js"); + +wasmRunWithDebugger( + '(module (memory 1 1)\ + (global (mut f64) (f64.const 0.5))\ + (global f32 (f32.const 3.5))\ + (func (param i32) (local f64) (f64.const 1.0) (tee_local 1) (set_global 0) (nop))\ + (export "test" (func 0))\ + (data (i32.const 0) "Abc\\x2A"))', + undefined, + function ({dbg}) { + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') return; + + var memoryContent = frame.eval('new DataView(memory0.buffer).getUint8(3)').return; + assertEq(memoryContent, 42, 'valid memory content is expected (0x2A)'); + + var global1AndParamSum = frame.eval('global1 + var0').return; + assertEq(global1AndParamSum, 3.5); + + var stepNumber = 0; + frame.onStep = function () { + switch (stepNumber) { + case 1: // after i64.const 1.0 + assertEq(frame.eval('global0').return, 0.5); + assertEq(frame.eval('var1').return, 0.0); + break; + case 2: // after tee_local $var1 + assertEq(frame.eval('var1').return, 1.0); + break; + case 3: // after set_global $global0 + assertEq(frame.eval('global0').return, 1.0); + break; + } + stepNumber++; + }; + }; + }, + function ({error}) { + assertEq(error, undefined); + } +); diff --git a/js/src/jit-test/tests/debug/wasm-onExceptionUnwind-gc.js b/js/src/jit-test/tests/debug/wasm-onExceptionUnwind-gc.js new file mode 100644 index 0000000000..5f4b4942af --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-onExceptionUnwind-gc.js @@ -0,0 +1,48 @@ +// |jit-test| skip-if: !wasmDebuggingEnabled() + +var sandbox = newGlobal({newCompartment: true}); +var dbg = new Debugger(sandbox); +var counter = 0; +dbg.onExceptionUnwind = (frame, value) => { + if (frame.type !== "wasmcall") + return; + if (++counter != 2) + return; + gc(); +}; + +sandbox.innerCode = wasmTextToBinary(`(module + (import "imports" "tbl" (table 1 anyfunc)) + (import "imports" "setNull" (func $setNull)) + (func $trap + call $setNull + unreachable + ) + (elem (i32.const 0) $trap) +)`); +sandbox.outerCode = wasmTextToBinary(`(module + (import "imports" "tbl" (table 1 anyfunc)) + (type $v2v (func)) + (func (export "run") + i32.const 0 + call_indirect (type $v2v) + ) +)`); + +sandbox.eval(` +(function() { + +var tbl = new WebAssembly.Table({initial:1, element:"anyfunc"}); +function setNull() { tbl.set(0, null) } +new WebAssembly.Instance(new WebAssembly.Module(innerCode), {imports:{tbl,setNull}}); +var outer = new WebAssembly.Instance(new WebAssembly.Module(outerCode), {imports:{tbl}}); +var caught; +try { + outer.exports.run(); +} catch (e) { + caught = e; +} +assertEq(caught instanceof WebAssembly.RuntimeError, true); + +})(); +`); diff --git a/js/src/jit-test/tests/debug/wasm-responseurls.js b/js/src/jit-test/tests/debug/wasm-responseurls.js new file mode 100644 index 0000000000..bc00b8e605 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-responseurls.js @@ -0,0 +1,40 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() +// Tests that wasm module can accept URL and sourceMapURL from response +// when instantiateStreaming is used. + +ignoreUnhandledRejections(); + +try { + WebAssembly.compileStreaming(); + // Avoid mixing the test's jobs with the debuggee's, so that + // automated checks can make sure AutoSaveJobQueue only + // suspends debuggee work. + drainJobQueue(); +} catch (err) { + assertEq(String(err).indexOf("not supported with --no-threads") !== -1, true); + quit(); +} + +load(libdir + "asserts.js"); + +var g = newGlobal({newCompartment: true}); + +var source = new g.Uint8Array(wasmTextToBinary('(module (func unreachable))')); +source.url = "http://example.org/test.wasm"; +source.sourceMappingURL = "http://example.org/test.wasm.map"; +g.source = source; + +var gotUrl, gotSourceMapURL; +var dbg = new Debugger(g); +dbg.allowWasmBinarySource = true; +dbg.onNewScript = function (s, g) { + gotUrl = s.source.url; + gotSourceMapURL = s.source.sourceMapURL; +}; + +g.eval('WebAssembly.instantiateStreaming(source);'); + +drainJobQueue(); + +assertEq(gotUrl, "http://example.org/test.wasm"); +assertEq(gotSourceMapURL, "http://example.org/test.wasm.map"); diff --git a/js/src/jit-test/tests/debug/wasm-sourceMappingURL.js b/js/src/jit-test/tests/debug/wasm-sourceMappingURL.js new file mode 100644 index 0000000000..7f7f245faf --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-sourceMappingURL.js @@ -0,0 +1,72 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module sourceMappingURL section is parsed. + +load(libdir + "asserts.js"); +load(libdir + "wasm-binary.js"); + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +var gotScript; +dbg.allowWasmBinarySource = true; +dbg.onNewScript = (script) => { + gotScript = script; +} + +function toU8(array) { + for (let b of array) + assertEq(b < 256, true); + return Uint8Array.from(array); +} + +function varU32(u32) { + assertEq(u32 >= 0, true); + assertEq(u32 < Math.pow(2,32), true); + var bytes = []; + do { + var byte = u32 & 0x7f; + u32 >>>= 7; + if (u32 != 0) + byte |= 0x80; + bytes.push(byte); + } while (u32 != 0); + return bytes; +} + +function string(name) { + var nameBytes = name.split('').map(c => { + var code = c.charCodeAt(0); + assertEq(code < 128, true); // TODO + return code; + }); + return varU32(nameBytes.length).concat(nameBytes); +} + +function appendSourceMappingURL(wasmBytes, url) { + if (!url) + return wasmBytes; + var payload = [...string('sourceMappingURL'), ...string(url)]; + return Uint8Array.from([...wasmBytes, userDefinedId, payload.length, ...payload]); +} +g.toWasm = (wast, url) => appendSourceMappingURL(wasmTextToBinary(wast), url); + +// The sourceMappingURL section is not present +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(toWasm('(module (func) (export "" (func 0)))')));`); +assertEq(gotScript.format, "wasm"); +assertEq(gotScript.source.sourceMapURL, null); + +// The sourceMappingURL section is present +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(toWasm('(module (func) (export "a" (func 0)))', 'http://example.org/test')));`); +assertEq(gotScript.format, "wasm"); +assertEq(gotScript.source.sourceMapURL, 'http://example.org/test'); + +// The sourceMapURL is read-only for wasm +assertThrowsInstanceOf(() => gotScript.source.sourceMapURL = 'foo', Error); + +// The sourceMappingURL section is present, and is still available when wasm +// binary source is disabled. +dbg.allowWasmBinarySource = false; +g.eval(`o = new WebAssembly.Instance(new WebAssembly.Module(toWasm('(module (func) (export "a" (func 0)))', 'http://example.org/test2')));`); +assertEq(gotScript.format, "wasm"); +assertEq(gotScript.source.sourceMapURL, 'http://example.org/test2'); diff --git a/js/src/jit-test/tests/debug/wasm-step.js b/js/src/jit-test/tests/debug/wasm-step.js new file mode 100644 index 0000000000..a7509facc7 --- /dev/null +++ b/js/src/jit-test/tests/debug/wasm-step.js @@ -0,0 +1,65 @@ +// |jit-test| test-also=--wasm-compiler=optimizing; skip-if: !wasmDebuggingEnabled() + +// Tests that wasm module scripts raises onEnterFrame and onLeaveFrame events. + +load(libdir + "wasm.js"); + +// Checking if we stop at every wasm instruction during step. +var onEnterFrameCalled, onLeaveFrameCalled, onStepCalled; +wasmRunWithDebugger( + '(module (func (nop) (nop)) (export "test" (func 0)))', + undefined, + function ({dbg}) { + onEnterFrameCalled = 0; + onLeaveFrameCalled = 0; + onStepCalled = []; + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') return; + onEnterFrameCalled++; + frame.onStep = function () { + onStepCalled.push(frame.offset); + }; + frame.onPop = function () { + onLeaveFrameCalled++; + }; + }; + }, + function ({error}) { + assertEq(error, undefined); + assertEq(onEnterFrameCalled, 1); + assertEq(onLeaveFrameCalled, 1); + assertEq(onStepCalled.length, 3); + assertEq(onStepCalled[0] > 0, true); + // The onStepCalled offsets are in ascending order. + assertEq(onStepCalled[0] < onStepCalled[1], true); + assertEq(onStepCalled[1] < onStepCalled[2], true); + } +); + +// Checking if step mode was disabled after debugger has been detached. +wasmRunWithDebugger( + '(module (func (nop) (nop)) (export "test" (func 0)))', + undefined, + function ({dbg, g}) { + onEnterFrameCalled = 0; + onLeaveFrameCalled = 0; + onStepCalled = []; + dbg.onEnterFrame = function (frame) { + if (frame.type != 'wasmcall') return; + onEnterFrameCalled++; + frame.onStep = function () { + onStepCalled.push(frame.offset); + }; + frame.onPop = function () { + onLeaveFrameCalled++; + }; + dbg.removeDebuggee(g); + }; + }, + function ({error}) { + assertEq(error, undefined); + assertEq(onEnterFrameCalled, 1); + assertEq(onLeaveFrameCalled, 0); + assertEq(onStepCalled.length, 0); + } +); |