Bug 916845: Add functions for testing lazily-retrieved sources. r=ejpbruel
authorJim Blandy <jimb@mozilla.com>
Mon, 30 Sep 2013 10:25:18 -0700
changeset 149356 41ad19b8579719af7a5887f07b842a8727ffeeef
parent 149355 f12083ad5943e638855d5c34d92d44205716677a
child 149357 da373e056514a9f542d30cb175ef6c4a39aaffab
push id25386
push useremorley@mozilla.com
push dateTue, 01 Oct 2013 09:29:22 +0000
treeherdermozilla-central@6856c45f3688 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersejpbruel
bugs916845
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 916845: Add functions for testing lazily-retrieved sources. r=ejpbruel
js/src/jit-test/tests/basic/withSourceHook.js
js/src/jit-test/tests/debug/Source-text-01.js
js/src/jit-test/tests/debug/Source-text-02.js
js/src/jit-test/tests/debug/Source-text-lazy.js
js/src/jsfriendapi.cpp
js/src/jsfriendapi.h
js/src/shell/js.cpp
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/withSourceHook.js
@@ -0,0 +1,56 @@
+// Check that withSourceHook passes URLs, propagates exceptions, and
+// properly restores the original source hooks.
+
+load(libdir + 'asserts.js');
+
+// withSourceHook isn't defined if you pass the shell the --fuzzing-safe
+// option. Skip this test silently, to avoid spurious failures.
+if (typeof withSourceHook != 'function')
+  quit(0);
+
+var log = '';
+
+// Establish an outermost source hook.
+withSourceHook(function (url) {
+  log += 'o';
+  assertEq(url, 'outer');
+  return '(function outer() { 3; })';
+}, function () {
+  log += 'O';
+  // Verify that withSourceHook propagates exceptions thrown by source hooks.
+  assertThrowsValue(function () {
+    // Establish a source hook that throws.
+    withSourceHook(function (url) {
+      log += 'm';
+      assertEq(url, 'middle');
+      throw 'borborygmus'; // middle
+    }, function () {
+      log += 'M';
+      // Establish an innermost source hook that does not throw,
+      // and verify that it is in force.
+      assertEq(withSourceHook(function (url) {
+                                log += 'i';
+                                assertEq(url, 'inner');
+                                return '(function inner() { 1; })';
+                              }, function () {
+                                log += 'I';
+                                return evaluate('(function inner() { 2; })',
+                                                { fileName: 'inner', sourcePolicy: 'LAZY_SOURCE' })
+                                       .toSource();
+                              }),
+               '(function inner() { 1; })');
+      // Verify that the source hook that throws has been reinstated.
+      evaluate('(function middle() { })',
+               { fileName: 'middle', sourcePolicy: 'LAZY_SOURCE' })
+      .toSource();
+    });
+  }, 'borborygmus');
+
+  // Verify that the outermost source hook has been restored.
+  assertEq(evaluate('(function outer() { 4; })',
+                    { fileName: 'outer', sourcePolicy: 'LAZY_SOURCE' })
+           .toSource(),
+           '(function outer() { 3; })');
+});
+
+assertEq(log, 'OMIimo');
--- a/js/src/jit-test/tests/debug/Source-text-01.js
+++ b/js/src/jit-test/tests/debug/Source-text-01.js
@@ -1,11 +1,12 @@
-/* 
- * Script.prototype.source should be a string. Moreover, it should be the
- * same string for each child script within the same debugger.
+/*
+ * 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();
 let dbg = new Debugger(g);
 
 var count = 0;
 dbg.onNewScript = function (script) {
     var text = script.source.text;
     assertEq(typeof text, "string");
--- a/js/src/jit-test/tests/debug/Source-text-02.js
+++ b/js/src/jit-test/tests/debug/Source-text-02.js
@@ -1,9 +1,10 @@
-// Source.prototype.text should be a string
+// Nested compilation units (say, an eval with in an eval) should have the
+// correct sources attributed to them.
 let g = newGlobal();
 let dbg = new Debugger(g);
 
 var count = 0;
 dbg.onNewScript = function (script) {
     ++count;
     if (count % 2 == 0)
         assertEq(script.source.text, text);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Source-text-lazy.js
@@ -0,0 +1,39 @@
+/*
+ * Debugger.Source.prototype.text should correctly retrieve the source for
+ * code compiled with CompileOptions::LAZY_SOURCE.
+ */
+
+// withSourceHook isn't defined if you pass the shell the --fuzzing-safe
+// option. Skip this test silently, to avoid spurious failures.
+if (typeof withSourceHook != 'function')
+  quit(0);
+
+let g = newGlobal();
+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",
+                         sourcePolicy: "LAZY_SOURCE"});
+  });
+
+  assertEq(log, 'ds');
+}
+
+test("debugger; // Ignominious Iguana");
+test("(function () { debugger; /* Meretricious Marmoset */})();");
+test("(() => { debugger; })(); // Gaunt Gibbon");
--- a/js/src/jsfriendapi.cpp
+++ b/js/src/jsfriendapi.cpp
@@ -46,16 +46,22 @@ PerThreadDataFriendFields::PerThreadData
 }
 
 JS_FRIEND_API(void)
 js::SetSourceHook(JSRuntime *rt, SourceHook *hook)
 {
     rt->sourceHook = hook;
 }
 
+JS_FRIEND_API(SourceHook *)
+js::ForgetSourceHook(JSRuntime *rt)
+{
+    return rt->sourceHook.forget();
+}
+
 JS_FRIEND_API(void)
 JS_SetGrayGCRootsTracer(JSRuntime *rt, JSTraceDataOp traceOp, void *data)
 {
     rt->gcGrayRootTracer.op = traceOp;
     rt->gcGrayRootTracer.data = data;
 }
 
 JS_FRIEND_API(JSString *)
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -215,27 +215,37 @@ namespace js {
  * we call the 'load' member function of the instance of this class that
  * has hopefully been registered with the runtime, passing the code's URL,
  * and hope that it will be able to find the source.
  */
 class SourceHook {
   public:
     virtual ~SourceHook() { }
 
-    /* Set |*src| and |*length| to refer to the source code for |filename|. */
+    /*
+     * Set |*src| and |*length| to refer to the source code for |filename|.
+     * On success, the caller owns the buffer to which |*src| points, and
+     * should use JS_free to free it.
+     */
     virtual bool load(JSContext *cx, const char *filename, jschar **src, size_t *length) = 0;
 };
 
 /*
- * Have |rt| use |hook| to retrieve LAZY_SOURCE source code.
- * See the comments for SourceHook.
+ * Have |rt| use |hook| to retrieve LAZY_SOURCE source code. See the
+ * comments for SourceHook. The runtime takes ownership of the hook, and
+ * will delete it when the runtime itself is deleted, or when a new hook is
+ * set.
  */
 extern JS_FRIEND_API(void)
 SetSourceHook(JSRuntime *rt, SourceHook *hook);
 
+/* Remove |rt|'s source hook, and return it. The caller now owns the hook. */
+extern JS_FRIEND_API(SourceHook *)
+ForgetSourceHook(JSRuntime *rt);
+
 inline JSRuntime *
 GetRuntime(const JSContext *cx)
 {
     return ContextFriendFields::get(cx)->runtime_;
 }
 
 inline JSCompartment *
 GetContextCompartment(const JSContext *cx)
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -2,16 +2,17 @@
  * 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/. */
 
 /* JS shell. */
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/GuardObjects.h"
+#include "mozilla/PodOperations.h"
 #include "mozilla/Util.h"
 
 #ifdef XP_WIN
 # include <direct.h>
 #endif
 #include <errno.h>
 #if defined(XP_OS2) || defined(XP_WIN)
 # include <io.h>     /* for isatty() */
@@ -74,16 +75,17 @@
 # include <libgen.h>
 #endif
 
 using namespace js;
 using namespace js::cli;
 
 using mozilla::ArrayLength;
 using mozilla::Maybe;
+using mozilla::PodCopy;
 
 enum JSShellExitCode {
     EXITCODE_RUNTIME_ERROR      = 3,
     EXITCODE_FILE_NOT_FOUND     = 4,
     EXITCODE_OUT_OF_MEMORY      = 5,
     EXITCODE_TIMEOUT            = 6
 };
 
@@ -907,16 +909,17 @@ Evaluate(JSContext *cx, unsigned argc, j
     JSAutoByteString fileNameBytes;
     RootedString sourceURL(cx);
     RootedString sourceMapURL(cx);
     unsigned lineNumber = 1;
     RootedObject global(cx, NULL);
     bool catchTermination = false;
     bool saveFrameChain = false;
     RootedObject callerGlobal(cx, cx->global());
+    CompileOptions::SourcePolicy sourcePolicy = CompileOptions::SAVE_SOURCE;
 
     global = JS_GetGlobalForObject(cx, &args.callee());
     if (!global)
         return false;
 
     if (args.length() == 2) {
         RootedObject opts(cx, &args[1].toObject());
         RootedValue v(cx);
@@ -1019,16 +1022,38 @@ Evaluate(JSContext *cx, unsigned argc, j
         if (!JS_GetProperty(cx, opts, "saveFrameChain", &v))
             return false;
         if (!JSVAL_IS_VOID(v)) {
             bool b;
             if (!JS_ValueToBoolean(cx, v, &b))
                 return false;
             saveFrameChain = b;
         }
+
+        if (!JS_GetProperty(cx, opts, "sourcePolicy", &v))
+            return false;
+        if (!JSVAL_IS_VOID(v)) {
+            JSString *s = JS_ValueToString(cx, v);
+            if (!s)
+                return false;
+            char *policy = JS_EncodeStringToUTF8(cx, s);
+            if (!policy)
+                return false;
+            if (strcmp(policy, "NO_SOURCE") == 0) {
+                sourcePolicy = CompileOptions::NO_SOURCE;
+            } else if (strcmp(policy, "LAZY_SOURCE") == 0) {
+                sourcePolicy = CompileOptions::LAZY_SOURCE;
+            } else if (strcmp(policy, "SAVE_SOURCE") == 0) {
+                sourcePolicy = CompileOptions::SAVE_SOURCE;
+            } else {
+                JS_ReportError(cx, "bad 'sourcePolicy' option passed to 'evaluate': '%s'",
+                               policy);
+                return false;
+            }
+        }
     }
 
     RootedString code(cx, args[0].toString());
 
     size_t codeLength;
     const jschar *codeChars = JS_GetStringCharsAndLength(cx, code, &codeLength);
     if (!codeChars)
         return false;
@@ -1052,16 +1077,17 @@ Evaluate(JSContext *cx, unsigned argc, j
             opts |= JSOPTION_COMPILE_N_GO;
         if (noScriptRval)
             opts |= JSOPTION_NO_SCRIPT_RVAL;
 
         JS_SetOptions(cx, opts);
         CompileOptions options(cx);
         options.setFileAndLine(fileName, lineNumber);
         options.setElement(element);
+        options.setSourcePolicy(sourcePolicy);
         RootedScript script(cx, JS::Compile(cx, global, options, codeChars, codeLength));
         JS_SetOptions(cx, oldopts);
         if (!script)
             return false;
 
         if (sourceURL && !script->scriptSource()->hasSourceURL()) {
             const jschar *surl = JS_GetStringCharsZ(cx, sourceURL);
             if (!surl)
@@ -3799,16 +3825,102 @@ GetSelfHostedValue(JSContext *cx, unsign
     }
     RootedAtom srcAtom(cx, ToAtom<CanGC>(cx, args[0]));
     if (!srcAtom)
         return false;
     RootedPropertyName srcName(cx, srcAtom->asPropertyName());
     return cx->runtime()->cloneSelfHostedValue(cx, srcName, args.rval());
 }
 
+class ShellSourceHook: public SourceHook {
+    // The runtime to which we attached a source hook.
+    JSRuntime *rt;
+
+    // The function we should call to lazily retrieve source code.
+    // The constructor and destructor take care of rooting this with the
+    // runtime.
+    JSObject *fun;
+
+  public:
+    ShellSourceHook() : rt(NULL), fun(NULL) { }
+    bool init(JSContext *cx, JSFunction &fun) {
+        JS_ASSERT(!this->rt);
+        JS_ASSERT(!this->fun);
+        this->rt = cx->runtime();
+        this->fun = &fun;
+        return JS_AddNamedObjectRoot(cx, &this->fun,
+                                     "lazy source callback, set with withSourceHook");
+    }
+
+    ~ShellSourceHook() {
+        if (fun)
+            JS_RemoveObjectRootRT(rt, &fun);
+    }
+
+    bool load(JSContext *cx, const char *filename, jschar **src, size_t *length) {
+        JS_ASSERT(fun);
+
+        RootedString str(cx, JS_NewStringCopyZ(cx, filename));
+        if (!str)
+            return false;
+        RootedValue filenameValue(cx, StringValue(str));
+
+        RootedValue result(cx);
+        if (!Call(cx, UndefinedValue(), &fun->as<JSFunction>(),
+                  1, filenameValue.address(), &result))
+            return false;
+
+        str = JS_ValueToString(cx, result);
+        if (!str)
+            return false;
+
+        *length = JS_GetStringLength(str);
+        *src = cx->pod_malloc<jschar>(*length);
+        if (!*src)
+            return false;
+
+        const jschar *chars = JS_GetStringCharsZ(cx, str);
+        if (!chars)
+            return false;
+
+        PodCopy(*src, chars, *length);
+        return true;
+    }
+};
+
+static bool
+WithSourceHook(JSContext *cx, unsigned argc, jsval *vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedObject callee(cx, &args.callee());
+
+    if (args.length() != 2) {
+        ReportUsageError(cx, callee, "Wrong number of arguments.");
+        return false;
+    }
+
+    if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()
+        || !args[1].isObject() || !args[1].toObject().is<JSFunction>()) {
+        ReportUsageError(cx, callee, "First and second arguments must be functions.");
+        return false;
+    }
+
+    ShellSourceHook *hook = new ShellSourceHook();
+    if (!hook->init(cx, args[0].toObject().as<JSFunction>())) {
+        delete hook;
+        return false;
+    }
+
+    SourceHook *savedHook = js::ForgetSourceHook(cx->runtime());
+    js::SetSourceHook(cx->runtime(), hook);
+    bool result = Call(cx, UndefinedValue(), &args[1].toObject(), 0, NULL, args.rval());
+    js::SetSourceHook(cx->runtime(), savedHook);
+    return result;
+}
+
 static const JSFunctionSpecWithHelp shell_functions[] = {
     JS_FN_HELP("version", Version, 0, 0,
 "version([number])",
 "  Get or force a script compilation version number."),
 
     JS_FN_HELP("options", Options, 0, 0,
 "options([option ...])",
 "  Get or toggle JavaScript options."),
@@ -3832,17 +3944,28 @@ static const JSFunctionSpecWithHelp shel
 "      fileName: filename for error messages and debug info\n"
 "      lineNumber: starting line number for error messages and debug info\n"
 "      global: global in which to execute the code\n"
 "      newContext: if true, create and use a new cx (default: false)\n"
 "      saveFrameChain: if true, save the frame chain before evaluating code\n"
 "         and restore it afterwards\n"
 "      catchTermination: if true, catch termination (failure without\n"
 "         an exception value, as for slow scripts or out-of-memory)\n"
-"          and return 'terminated'\n"),
+"         and return 'terminated'\n"
+"      element: if present with value |v|, convert |v| to an object |o| mark\n"
+"         the source as being attached to the DOM element |o|. If the\n"
+"         property is omitted or |v| is null, don't attribute the source to\n"
+"         any DOM element.\n"
+"      sourceMapURL: if present with value |v|, convert |v| to a string, and\n"
+"         provide that as the code's source map URL. If omitted, attach no\n"
+"         source map URL to the code (although the code may provide one itself,\n"
+"         via a //#sourceMappingURL comment).\n"
+"      sourcePolicy: if present, the value converted to a string must be either\n"
+"         'NO_SOURCE', 'LAZY_SOURCE', or 'SAVE_SOURCE'; use the given source\n"
+"         retention policy for this compilation.\n"),
 
     JS_FN_HELP("run", Run, 1, 0,
 "run('foo.js')",
 "  Run the file named by the first argument, returning the number of\n"
 "  of milliseconds spent compiling and executing it."),
 
     JS_FN_HELP("readline", ReadLine, 0, 0,
 "readline()",
@@ -4148,16 +4271,37 @@ static const JSFunctionSpecWithHelp fuzz
     JS_FN_HELP("assertFloat32", testingFunc_assertFloat32, 2, 0,
 "assertFloat32(value, isFloat32)",
 "  In IonMonkey only, asserts that value has (resp. hasn't) the MIRType_Float32 if isFloat32 is true (resp. false)."),
 
     JS_FN_HELP("untrap", Untrap, 2, 0,
 "untrap(fun[, pc])",
 "  Remove a trap."),
 
+    JS_FN_HELP("withSourceHook", WithSourceHook, 1, 0,
+"withSourceHook(hook, fun)",
+"  Set this JS runtime's lazy source retrieval hook (that is, the hook\n"
+"  used to find sources compiled with |CompileOptions::LAZY_SOURCE|) to\n"
+"  |hook|; call |fun| with no arguments; and then restore the runtime's\n"
+"  original hook. Return or throw whatever |fun| did. |hook| gets\n"
+"  passed the requested code's URL, and should return a string.\n"
+"\n"
+"  Notes:\n"
+"\n"
+"  1) SpiderMonkey may assert if the returned code isn't close enough\n"
+"  to the script's real code, so this function is not fuzzer-safe.\n"
+"\n"
+"  2) The runtime can have only one source retrieval hook active at a\n"
+"  time. If |fun| is not careful, |hook| could be asked to retrieve the\n"
+"  source code for compilations that occurred long before it was set,\n"
+"  and that it knows nothing about. The reverse applies as well: the\n"
+"  original hook, that we reinstate after the call to |fun| completes,\n"
+"  might be asked for the source code of compilations that |fun|\n"
+"  performed, and which, presumably, only |hook| knows how to find.\n"),
+
     JS_FS_HELP_END
 };
 
 #ifdef MOZ_PROFILING
 # define PROFILING_FUNCTION_COUNT 5
 # ifdef MOZ_CALLGRIND
 #  define CALLGRIND_FUNCTION_COUNT 3
 # else