// |jit-test| --setpref=experimental.error_capture_stack_trace; load(libdir + "asserts.js"); if ('captureStackTrace' in Error) { assertEq(Error.captureStackTrace.length, 2); let x = Error.captureStackTrace({}); assertEq(x, undefined); assertThrowsInstanceOf(() => Error.captureStackTrace(), TypeError); assertThrowsInstanceOf(() => Error.captureStackTrace(2), TypeError); Error.captureStackTrace({}, 2); Error.captureStackTrace({}, null); Error.captureStackTrace({}, {}); function caller(f) { return f(); } function fill() { let x = {} Error.captureStackTrace(x, caller); return x; } function test_elision() { let x = caller(fill); let { stack } = x; assertEq(stack.includes("caller"), false); assertEq(stack.includes("fill"), false); ({ stack } = caller(() => caller(fill))) print(stack); assertEq(stack.includes("caller"), true); // Only elide the first caller! assertEq(stack.includes("fill"), false); } test_elision(); function nestedLambda(f) { (() => { (() => { (() => { (() => { f(); })(); })(); })(); })(); } // If we never see a matching frame when requesting a truncated // stack we should return the empty string function test_no_match() { let obj = {}; // test_elision chosen arbitrarily as a function object which // doesn't exist in the call stack here. let capture = () => Error.captureStackTrace(obj, test_elision); nestedLambda(capture); assertEq(obj.stack, ""); } test_no_match() function count_frames(str) { return str.split("\n").length } function test_nofilter() { let obj = {}; let capture = () => Error.captureStackTrace(obj); nestedLambda(capture); assertEq(count_frames(obj.stack), 9); } test_nofilter(); function test_in_eval() { let obj = eval(` let obj = {}; let capture = () => Error.captureStackTrace(obj); nestedLambda(capture); obj `) // Same as above, with an eval frame added! assertEq(count_frames(obj.stack), 10); } test_in_eval(); // // [[ErrorData]] // const stackGetter = Object.getOwnPropertyDescriptor(Error.prototype, 'stack').get; const getStack = function (obj) { return stackGetter.call(obj); }; function test_uncensored() { let err = undefined; function create_err() { err = new Error; Error.captureStackTrace(err, test_uncensored); } nestedLambda(create_err); // Calling Error.captureStackTrace doesn't mess with the internal // [[ErrorData]] slot assertEq(count_frames(err.stack), 2); assertEq(count_frames(getStack(err)), 9) } test_uncensored() // In general, the stacks a non-caller version of Error.captureStackStrace // should match what Error gives you function compare_stacks() { function censor_column(str) { return str.replace(/:(\d+):\d+\n/g, ":$1:censored\n") } let obj = {}; let err = (Error.captureStackTrace(obj), new Error) assertEq(censor_column(err.stack), censor_column(obj.stack)); } compare_stacks(); nestedLambda(compare_stacks) // New global function test_in_global(global) { global.evaluate(caller.toString()); global.evaluate(fill.toString()); global.evaluate(test_elision.toString()); global.evaluate("test_elision()"); global.evaluate(nestedLambda.toString()) global.evaluate(test_no_match.toString()); global.evaluate("test_no_match()"); global.evaluate(compare_stacks.toString()); global.evaluate(` compare_stacks(); nestedLambda(compare_stacks) `) } let global = newGlobal(); test_in_global(global); let global2 = newGlobal({ principal: 0 }); test_in_global(global2) let global3 = newGlobal({ principal: 0xfffff }); test_in_global(global3) // What if the caller is a proxy? const caller_proxy = new Proxy(caller, { apply: function (target, thisArg, arguments) { return target(...arguments); } }); function fill_proxy() { let x = {} Error.captureStackTrace(x, caller_proxy); return x; } // Proxies don't count for elision. function test_proxy_elision() { let x = caller_proxy(fill_proxy); let { stack } = x; assertEq(stack.includes("caller"), true); assertEq(stack.includes("fill_proxy"), true); } test_proxy_elision(); const trivial_proxy = new Proxy(caller, {}); function fill_trivial() { let x = {} Error.captureStackTrace(x, trivial_proxy); return x; } // Elision doesn't work even on forwarding proxy function test_trivial_elision() { let x = caller(fill_trivial); let { stack } = x; assertEq(stack.includes("caller"), true); assertEq(stack.includes("fill"), true); } test_trivial_elision(); // Elision happens through bind function test_bind_elision() { let b = caller.bind(undefined, fill); let { stack } = b(); assertEq(stack.includes("caller"), false); assertEq(stack.includes("fill"), false); } test_bind_elision(); // Cross Realm testing let nr = newGlobal({ newCompartment: true }) nr.eval(`globalThis.x = {}`); Error.captureStackTrace(nr.x); // Test strict definition function test_strict_definition() { "use strict"; assertThrowsInstanceOf(() => Error.captureStackTrace(Object.freeze({ stack: null })), TypeError); } test_strict_definition(); function test_property_descriptor() { let o = {}; Error.captureStackTrace(o); let desc = Object.getOwnPropertyDescriptor(o, "stack"); assertEq(desc.configurable, true) assertEq(desc.writable, true) assertEq(desc.enumerable, false) } test_property_descriptor(); function test_delete() { let o = {}; Error.captureStackTrace(o); delete o.stack assertEq("stack" in o, false) } test_delete(); // Principal testing: This is basic/shell-principals.js extended to support // and compare Error.captureStackTrace. // // Reminder: // > In the shell, a principal is simply a 32-bit mask: P subsumes Q if the // > set bits in P are a superset of those in Q. Thus, the principal 0 is // > subsumed by everything, and the principal ~0 subsumes everything. // Given a string of letters |expected|, say "abc", assert that the stack // contains calls to a series of functions named by the next letter from // the string, say a, b, and then c. Younger frames appear earlier in // |expected| than older frames. let count = 0; function check(expected, stack) { print("check(" + JSON.stringify(expected) + ") against:\n" + stack); count++; // Extract only the function names from the stack trace. Omit the frames // for the top-level evaluation, if it is present. var split = stack.split(/(.)?@.*\n/).slice(0, -1); if (split[split.length - 1] === undefined) split = split.slice(0, -2); print(JSON.stringify(split)); // Check the function names against the expected sequence. assertEq(split.length, expected.length * 2); for (var i = 0; i < expected.length; i++) assertEq(split[i * 2 + 1], expected[i]); } var low = newGlobal({ principal: 0 }); var mid = newGlobal({ principal: 0xffff }); var high = newGlobal({ principal: 0xfffff }); eval('function a() { let o = {}; Error.captureStackTrace(o); check("a", o.stack); b(); }'); low.eval('function b() { let o = {}; Error.captureStackTrace(o); check("b", o.stack); c(); }'); mid.eval('function c() { let o = {}; Error.captureStackTrace(o); check("cba", o.stack); d(); }'); high.eval('function d() { let o = {}; Error.captureStackTrace(o); check("dcba", o.stack); e(); }'); // Globals created with no explicit principals get 0xffff. eval('function e() { let o = {}; Error.captureStackTrace(o); check("ecba", o.stack); f(); }'); low.eval('function f() { let o = {}; Error.captureStackTrace(o); check("fb", o.stack); g(); }'); mid.eval('function g() { let o = {}; Error.captureStackTrace(o); check("gfecba", o.stack); h(); }'); high.eval('function h() { let o = {}; Error.captureStackTrace(o); check("hgfedcba", o.stack); }'); // Make everyone's functions visible to each other, as needed. b = low.b; low.c = mid.c; mid.d = high.d; high.e = e; f = low.f; low.g = mid.g; mid.h = high.h; low.check = mid.check = high.check = check; // Kick the whole process off. a(); assertEq(count, 8); // Ensure filtering is based on caller realm not on target object. low.eval("low_target = {}"); mid.eval("mid_target = {}"); high.eval("high_target = {}"); high.low_target = mid.low_target = low.low_target; high.mid_target = low.mid_target = mid.mid_target; mid.high_target = low.high_target = high.high_target; high.low_cst = mid.low_cst = low.low_cst = low.Error.captureStackTrace; high.mid_cst = low.mid_cst = mid.mid_cst = mid.Error.captureStackTrace; mid.high_cst = low.high_cst = high.high_cst = high.Error.captureStackTrace; for (let g of [low, mid, high]) { assertEq("low_target" in g, true); assertEq("mid_target" in g, true); assertEq("high_target" in g, true); assertEq("low_cst" in g, true); assertEq("mid_cst" in g, true); assertEq("high_cst" in g, true); // install caller function z -- single letter name for // check compat. g.eval("function z(f) { f() }") } low.eval("function q() { Error.captureStackTrace(low_target); }") high.q = low.q; // Caller function z is from high, but using low Error.captureStackTrace, so // z should be elided. high.eval("z(q)"); check("q", low.low_target.stack); low.eval("function r() { high_cst(low_target) }") high.r = low.r; // Can see everything here using high cst and low target. high.eval("function t() { z(r) }"); high.t(); check("rzt", low.low_target.stack); }