diff -r 000000000000 -r 6474c204b198 js/src/jsapi-tests/testDebugger.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/js/src/jsapi-tests/testDebugger.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,274 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "jscntxt.h" + +#include "js/OldDebugAPI.h" +#include "jsapi-tests/tests.h" + +using namespace js; + +static int callCounts[2] = {0, 0}; + +static void * +callCountHook(JSContext *cx, JSAbstractFramePtr frame, bool isConstructing, bool before, + bool *ok, void *closure) +{ + callCounts[before]++; + + JS::RootedValue thisv(cx); + frame.getThisValue(cx, &thisv); // assert if fp is incomplete + + return cx; // any non-null value causes the hook to be called again after +} + +BEGIN_TEST(testDebugger_bug519719) +{ + CHECK(JS_SetDebugMode(cx, true)); + JS_SetCallHook(rt, callCountHook, nullptr); + EXEC("function call(fn) { fn(0); }\n" + "function f(g) { for (var i = 0; i < 9; i++) call(g); }\n" + "f(Math.sin);\n" // record loop, starting in f + "f(Math.cos);\n"); // side exit in f -> call + CHECK_EQUAL(callCounts[0], 20); + CHECK_EQUAL(callCounts[1], 20); + return true; +} +END_TEST(testDebugger_bug519719) + +static void * +nonStrictThisHook(JSContext *cx, JSAbstractFramePtr frame, bool isConstructing, bool before, + bool *ok, void *closure) +{ + if (before) { + bool *allWrapped = (bool *) closure; + JS::RootedValue thisv(cx); + frame.getThisValue(cx, &thisv); + *allWrapped = *allWrapped && !JSVAL_IS_PRIMITIVE(thisv); + } + return nullptr; +} + +BEGIN_TEST(testDebugger_getThisNonStrict) +{ + bool allWrapped = true; + CHECK(JS_SetDebugMode(cx, true)); + JS_SetCallHook(rt, nonStrictThisHook, (void *) &allWrapped); + EXEC("function nonstrict() { }\n" + "Boolean.prototype.nonstrict = nonstrict;\n" + "String.prototype.nonstrict = nonstrict;\n" + "Number.prototype.nonstrict = nonstrict;\n" + "Object.prototype.nonstrict = nonstrict;\n" + "nonstrict.call(true);\n" + "true.nonstrict();\n" + "nonstrict.call('');\n" + "''.nonstrict();\n" + "nonstrict.call(42);\n" + "(42).nonstrict();\n" + // The below don't really get 'wrapped', but it's okay. + "nonstrict.call(undefined);\n" + "nonstrict.call(null);\n" + "nonstrict.call({});\n" + "({}).nonstrict();\n"); + CHECK(allWrapped); + return true; +} +END_TEST(testDebugger_getThisNonStrict) + +static void * +strictThisHook(JSContext *cx, JSAbstractFramePtr frame, bool isConstructing, bool before, + bool *ok, void *closure) +{ + if (before) { + bool *anyWrapped = (bool *) closure; + JS::RootedValue thisv(cx); + frame.getThisValue(cx, &thisv); + *anyWrapped = *anyWrapped || !JSVAL_IS_PRIMITIVE(thisv); + } + return nullptr; +} + +BEGIN_TEST(testDebugger_getThisStrict) +{ + bool anyWrapped = false; + CHECK(JS_SetDebugMode(cx, true)); + JS_SetCallHook(rt, strictThisHook, (void *) &anyWrapped); + EXEC("function strict() { 'use strict'; }\n" + "Boolean.prototype.strict = strict;\n" + "String.prototype.strict = strict;\n" + "Number.prototype.strict = strict;\n" + "strict.call(true);\n" + "true.strict();\n" + "strict.call('');\n" + "''.strict();\n" + "strict.call(42);\n" + "(42).strict();\n" + "strict.call(undefined);\n" + "strict.call(null);\n"); + CHECK(!anyWrapped); + return true; +} +END_TEST(testDebugger_getThisStrict) + +static bool calledThrowHook = false; + +static JSTrapStatus +ThrowHook(JSContext *cx, JSScript *, jsbytecode *, jsval *rval, void *closure) +{ + JS_ASSERT(!closure); + calledThrowHook = true; + + JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + + char text[] = "new Error()"; + JS::RootedValue _(cx); + JS_EvaluateScript(cx, global, text, strlen(text), "", 0, &_); + + return JSTRAP_CONTINUE; +} + +BEGIN_TEST(testDebugger_throwHook) +{ + CHECK(JS_SetDebugMode(cx, true)); + CHECK(JS_SetThrowHook(rt, ThrowHook, nullptr)); + EXEC("function foo() { throw 3 };\n" + "for (var i = 0; i < 10; ++i) { \n" + " var x = {}\n" + " try {\n" + " foo(); \n" + " } catch(e) {}\n" + "}\n"); + CHECK(calledThrowHook); + CHECK(JS_SetThrowHook(rt, nullptr, nullptr)); + return true; +} +END_TEST(testDebugger_throwHook) + +BEGIN_TEST(testDebugger_debuggerObjectVsDebugMode) +{ + CHECK(JS_DefineDebuggerObject(cx, global)); + JS::RootedObject debuggee(cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, JS::FireOnNewGlobalHook)); + CHECK(debuggee); + + { + JSAutoCompartment ae(cx, debuggee); + CHECK(JS_SetDebugMode(cx, true)); + CHECK(JS_InitStandardClasses(cx, debuggee)); + } + + JS::RootedObject debuggeeWrapper(cx, debuggee); + CHECK(JS_WrapObject(cx, &debuggeeWrapper)); + JS::RootedValue v(cx, JS::ObjectValue(*debuggeeWrapper)); + CHECK(JS_SetProperty(cx, global, "debuggee", v)); + + EVAL("var dbg = new Debugger(debuggee);\n" + "var hits = 0;\n" + "dbg.onDebuggerStatement = function () { hits++; };\n" + "debuggee.eval('debugger;');\n" + "hits;\n", + &v); + CHECK_SAME(v, JSVAL_ONE); + + { + JSAutoCompartment ae(cx, debuggee); + CHECK(JS_SetDebugMode(cx, false)); + } + + EVAL("debuggee.eval('debugger; debugger; debugger;');\n" + "hits;\n", + &v); + CHECK_SAME(v, INT_TO_JSVAL(4)); + + return true; +} +END_TEST(testDebugger_debuggerObjectVsDebugMode) + +BEGIN_TEST(testDebugger_newScriptHook) +{ + // Test that top-level indirect eval fires the newScript hook. + CHECK(JS_DefineDebuggerObject(cx, global)); + JS::RootedObject g(cx, JS_NewGlobalObject(cx, getGlobalClass(), nullptr, JS::FireOnNewGlobalHook)); + CHECK(g); + { + JSAutoCompartment ae(cx, g); + CHECK(JS_InitStandardClasses(cx, g)); + } + + JS::RootedObject gWrapper(cx, g); + CHECK(JS_WrapObject(cx, &gWrapper)); + JS::RootedValue v(cx, JS::ObjectValue(*gWrapper)); + CHECK(JS_SetProperty(cx, global, "g", v)); + + EXEC("var dbg = Debugger(g);\n" + "var hits = 0;\n" + "dbg.onNewScript = function (s) {\n" + " hits += Number(s instanceof Debugger.Script);\n" + "};\n"); + + // Since g is a debuggee, g.eval should trigger newScript, regardless of + // what scope object we use to enter the compartment. + // + // Scripts are associated with the global where they're compiled, so we + // deliver them only to debuggers that are watching that particular global. + // + return testIndirectEval(g, "Math.abs(0)"); +} + +bool testIndirectEval(JS::HandleObject scope, const char *code) +{ + EXEC("hits = 0;"); + + { + JSAutoCompartment ae(cx, scope); + JSString *codestr = JS_NewStringCopyZ(cx, code); + CHECK(codestr); + JS::RootedValue arg(cx, JS::StringValue(codestr)); + JS::RootedValue v(cx); + CHECK(JS_CallFunctionName(cx, scope, "eval", arg, &v)); + } + + JS::RootedValue hitsv(cx); + EVAL("hits", &hitsv); + CHECK_SAME(hitsv, INT_TO_JSVAL(1)); + return true; +} +END_TEST(testDebugger_newScriptHook) + +BEGIN_TEST(testDebugger_singleStepThrow) + { + CHECK(JS_SetDebugModeForCompartment(cx, cx->compartment(), true)); + CHECK(JS_SetInterrupt(rt, onStep, nullptr)); + + CHECK(JS_DefineFunction(cx, global, "setStepMode", setStepMode, 0, 0)); + EXEC("var e;\n" + "setStepMode();\n" + "function f() { throw 0; }\n" + "try { f(); }\n" + "catch (x) { e = x; }\n"); + return true; + } + + static bool + setStepMode(JSContext *cx, unsigned argc, jsval *vp) + { + CallArgs args = CallArgsFromVp(argc, vp); + + NonBuiltinScriptFrameIter iter(cx); + JS::RootedScript script(cx, iter.script()); + if (!JS_SetSingleStepMode(cx, script, true)) + return false; + + args.rval().set(UndefinedValue()); + return true; + } + + static JSTrapStatus + onStep(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void *closure) + { + return JSTRAP_CONTINUE; + } +END_TEST(testDebugger_singleStepThrow)