Source code

Revision control

Copy as Markdown

Other Tools

// |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);
}