diff options
Diffstat (limited to 'toolkit/components/workerloader')
18 files changed, 679 insertions, 0 deletions
diff --git a/toolkit/components/workerloader/moz.build b/toolkit/components/workerloader/moz.build new file mode 100644 index 0000000000..cb8dcdaab5 --- /dev/null +++ b/toolkit/components/workerloader/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.ini"] + +EXTRA_JS_MODULES.workers += ["require.js"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Async Tooling") diff --git a/toolkit/components/workerloader/require.js b/toolkit/components/workerloader/require.js new file mode 100644 index 0000000000..246c4a1884 --- /dev/null +++ b/toolkit/components/workerloader/require.js @@ -0,0 +1,182 @@ +/* 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/. */ + +/** + * Implementation of a CommonJS module loader for workers. + * + * Use: + * // in the .js file loaded by the constructor of the worker + * importScripts("resource://gre/modules/workers/require.js"); + * let module = require("resource://gre/modules/worker/myModule.js"); + * + * // in myModule.js + * // Load dependencies + * let SimpleTest = require("resource://gre/modules/workers/SimpleTest.js"); + * let Logger = require("resource://gre/modules/workers/Logger.js"); + * + * // Define things that will not be exported + * let someValue = // ... + * + * // Export symbols + * exports.foo = // ... + * exports.bar = // ... + * + * + * Note #1: + * Properties |fileName| and |stack| of errors triggered from a module + * contain file names that do not correspond to human-readable module paths. + * Human readers should rather use properties |moduleName| and |moduleStack|. + * + * Note #2: + * By opposition to some other module loader implementations, this module + * loader does not enforce separation of global objects. Consequently, if + * a module modifies a global object (e.g. |String.prototype|), all other + * modules in the same worker may be affected. + */ + +/* global require */ +/* exported require */ + +(function (exports) { + "use strict"; + + if (exports.require) { + // Avoid double-imports + return; + } + + // Simple implementation of |require| + let require = (function () { + /** + * Mapping from module URI to module exports. + * + * @keys {string} The absolute URI to a module. + * @values {object} The |exports| objects for that module. + */ + let modules = new Map(); + + /** + * A human-readable version of |stack|. + * + * @type {string} + */ + Object.defineProperty(Error.prototype, "moduleStack", { + get() { + return this.stack; + }, + }); + /** + * A human-readable version of |fileName|. + * + * @type {string} + */ + Object.defineProperty(Error.prototype, "moduleName", { + get() { + let match = this.stack.match(/\@(.*):.*:/); + if (match) { + return match[1]; + } + return "(unknown module)"; + }, + }); + + /** + * Import a module + * + * @param {string} baseURL The URL of the modules from which we load a new module. + * This will be null for the first loaded module and so expect an absolute URI in path. + * Note that this first parameter is bound before `require` method is passed outside + * of this module. So that typical callsites will only path the second `path` parameter. + * @param {string} path The path to the module. + * @return {*} An object containing the properties exported by the module. + */ + return function require(baseURL, path) { + let startTime = performance.now(); + if (typeof path != "string") { + throw new TypeError( + "The argument to require() must be a string got " + path + ); + } + + // Resolve relative paths + if ((path.startsWith("./") || path.startsWith("../")) && baseURL) { + path = new URL(path, baseURL).href; + } + + if (!path.includes("://")) { + throw new TypeError( + "The argument to require() must be a string uri, got " + path + ); + } + // Automatically add ".js" if there is no extension + let uri; + if (path.lastIndexOf(".") <= path.lastIndexOf("/")) { + uri = path + ".js"; + } else { + uri = path; + } + + // Exports provided by the module + let exports = Object.create(null); + + // Identification of the module + let module = { + id: path, + uri, + exports, + }; + + // Make module available immediately + // (necessary in case of circular dependencies) + if (modules.has(uri)) { + return modules.get(uri).exports; + } + modules.set(uri, module); + + try { + // Load source of module, synchronously + let xhr = new XMLHttpRequest(); + xhr.open("GET", uri, false); + xhr.responseType = "text"; + xhr.send(); + + let source = xhr.responseText; + if (source == "") { + // There doesn't seem to be a better way to detect that the file couldn't be found + throw new Error("Could not find module " + path); + } + // Use `Function` to leave this scope, use `eval` to start the line + // number from 1 that is observed by `source` and the error message + // thrown from the module, and also use `arguments` for accessing + // `source` and `uri` to avoid polluting the module's environment. + let code = new Function( + "exports", + "require", + "module", + `eval(arguments[3] + "\\n//# sourceURL=" + arguments[4] + "\\n")` + ); + code(exports, require.bind(null, path), module, source, uri); + } catch (ex) { + // Module loading has failed, exports should not be made available + // after all. + modules.delete(uri); + throw ex; + } finally { + ChromeUtils.addProfilerMarker("require", startTime, path); + } + + Object.freeze(module.exports); + Object.freeze(module); + return module.exports; + }; + })(); + + Object.freeze(require); + + Object.defineProperty(exports, "require", { + value: require.bind(null, null), + enumerable: true, + configurable: false, + }); +})(this); diff --git a/toolkit/components/workerloader/tests/chrome.ini b/toolkit/components/workerloader/tests/chrome.ini new file mode 100644 index 0000000000..c4aa9ec169 --- /dev/null +++ b/toolkit/components/workerloader/tests/chrome.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + moduleA-depends.js + moduleB-dependency.js + moduleC-circular.js + moduleD-circular.js + moduleE-throws-during-require.js + moduleF-syntax-error.js + moduleG-throws-later.js + moduleH-module-dot-exports.js + moduleI-depends.js + moduleJ-dependency.js + utils_mainthread.js + utils_worker.js + worker_test_loading.js + +[test_loading.xhtml] diff --git a/toolkit/components/workerloader/tests/moduleA-depends.js b/toolkit/components/workerloader/tests/moduleA-depends.js new file mode 100644 index 0000000000..0b531b399c --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleA-depends.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +// A trivial module that depends on an equally trivial module +var B = require("chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/moduleB-dependency.js"); + +// Ensure that the initial set of exports is empty +if (Object.keys(exports).length) { + throw new Error("exports should be empty, initially"); +} + +// Export some values +exports.A = true; +exports.importedFoo = B.foo; diff --git a/toolkit/components/workerloader/tests/moduleB-dependency.js b/toolkit/components/workerloader/tests/moduleB-dependency.js new file mode 100644 index 0000000000..e8c8549bd5 --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleB-dependency.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +exports.B = true; +exports.foo = "foo"; + +// Side-effect to detect if we attempt to re-execute this module. +if ("loadedB" in self) { + throw new Error("B has been evaluted twice"); +} +self.loadedB = true; diff --git a/toolkit/components/workerloader/tests/moduleC-circular.js b/toolkit/components/workerloader/tests/moduleC-circular.js new file mode 100644 index 0000000000..103c20b05c --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleC-circular.js @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +// Module C and module D have circular dependencies. +// This should not prevent from loading them. + +// This value is set before any circular dependency, it should be visible +// in D. +exports.enteredC = true; + +var D = require("chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/moduleD-circular.js"); + +// The following values are set after importing D. +// copiedFromD.copiedFromC should have only one field |enteredC| +exports.copiedFromD = JSON.parse(JSON.stringify(D)); +// exportedFromD.copiedFromC should have all the fields defined in |exports| +exports.exportedFromD = D; +exports.finishedC = true; diff --git a/toolkit/components/workerloader/tests/moduleD-circular.js b/toolkit/components/workerloader/tests/moduleD-circular.js new file mode 100644 index 0000000000..a84b99ac37 --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleD-circular.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +// Module C and module D have circular dependencies. +// This should not prevent from loading them. + +exports.enteredD = true; +var C = require("chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/moduleC-circular.js"); +exports.copiedFromC = JSON.parse(JSON.stringify(C)); +exports.exportedFromC = C; +exports.finishedD = true; diff --git a/toolkit/components/workerloader/tests/moduleE-throws-during-require.js b/toolkit/components/workerloader/tests/moduleE-throws-during-require.js new file mode 100644 index 0000000000..deafc3faa2 --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleE-throws-during-require.js @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +// Skip a few lines +// 7 +// 8 +// 9 +// 10 +// 11 +throw new Error("Let's see if this error is obtained with the right origin"); diff --git a/toolkit/components/workerloader/tests/moduleF-syntax-error.js b/toolkit/components/workerloader/tests/moduleF-syntax-error.js new file mode 100644 index 0000000000..c03fa32f8a --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleF-syntax-error.js @@ -0,0 +1,6 @@ +<!-- +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +--> +<?xml version="1.0" encoding="UTF-8" ?> +<foo>Anything that doesn't parse as JavaScript</foo> diff --git a/toolkit/components/workerloader/tests/moduleG-throws-later.js b/toolkit/components/workerloader/tests/moduleG-throws-later.js new file mode 100644 index 0000000000..21abe36c95 --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleG-throws-later.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +// Skip a few lines +// 7 +// 8 +// 9 +// 10 +// 11 +exports.doThrow = function doThrow() { + Array.prototype.sort.apply("foo"); // This will raise a native TypeError +}; diff --git a/toolkit/components/workerloader/tests/moduleH-module-dot-exports.js b/toolkit/components/workerloader/tests/moduleH-module-dot-exports.js new file mode 100644 index 0000000000..953b229d47 --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleH-module-dot-exports.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +// This should be overwritten by module.exports +exports.key = "wrong value"; + +module.exports = { + key: "value", +}; + +// This should also be overwritten by module.exports +exports.key = "another wrong value"; diff --git a/toolkit/components/workerloader/tests/moduleI-depends.js b/toolkit/components/workerloader/tests/moduleI-depends.js new file mode 100644 index 0000000000..7d251305e7 --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleI-depends.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +// I trivial module that depends on an equally trivial module +var J = require("./moduleJ-dependency.js"); + +// Ensure that the initial set of exports is empty +if (Object.keys(exports).length) { + throw new Error("exports should be empty, initially"); +} + +// Export some values +exports.I = true; +exports.importedFoo = J.foo; diff --git a/toolkit/components/workerloader/tests/moduleJ-dependency.js b/toolkit/components/workerloader/tests/moduleJ-dependency.js new file mode 100644 index 0000000000..53114f4fd6 --- /dev/null +++ b/toolkit/components/workerloader/tests/moduleJ-dependency.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env commonjs */ + +exports.J = true; +exports.foo = "foo"; + +// Side-effect to detect if we attempt to re-execute this module. +if ("loadedJ" in self) { + throw new Error("J has been evaluted twice"); +} +self.loadedJ = true; diff --git a/toolkit/components/workerloader/tests/test_loading.xhtml b/toolkit/components/workerloader/tests/test_loading.xhtml new file mode 100644 index 0000000000..0301eeb294 --- /dev/null +++ b/toolkit/components/workerloader/tests/test_loading.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Testing the worker loader" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="utils_mainthread.js"/> + <script type="application/javascript"> + <![CDATA[ + +let worker; +let main = this; + +function test() { + info("Starting test " + document.uri); + + worker = new ChromeWorker("worker_test_loading.js"); + SimpleTest.waitForExplicitFinish(); + info("Chrome worker created"); + worker_handler(worker); + worker.postMessage(document.uri); + ok(true, "Test in progress"); +}; +]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/toolkit/components/workerloader/tests/utils_mainthread.js b/toolkit/components/workerloader/tests/utils_mainthread.js new file mode 100644 index 0000000000..6a3891b6fc --- /dev/null +++ b/toolkit/components/workerloader/tests/utils_mainthread.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function worker_handler(worker) { + worker.onerror = function (error) { + error.preventDefault(); + ok(false, "error " + error.message); + }; + worker.onmessage = function (msg) { + // ok(true, "MAIN: onmessage " + JSON.stringify(msg.data)); + switch (msg.data.kind) { + case "is": + SimpleTest.ok( + msg.data.outcome, + msg.data.description + "( " + msg.data.a + " ==? " + msg.data.b + ")" + ); + return; + case "isnot": + SimpleTest.ok( + msg.data.outcome, + msg.data.description + "( " + msg.data.a + " !=? " + msg.data.b + ")" + ); + return; + case "ok": + SimpleTest.ok(msg.data.condition, msg.data.description); + return; + case "info": + SimpleTest.info(msg.data.description); + return; + case "finish": + SimpleTest.finish(); + return; + default: + SimpleTest.ok( + false, + "test_osfile.xul: wrong message " + JSON.stringify(msg.data) + ); + } + }; +} diff --git a/toolkit/components/workerloader/tests/utils_worker.js b/toolkit/components/workerloader/tests/utils_worker.js new file mode 100644 index 0000000000..ce4848d7af --- /dev/null +++ b/toolkit/components/workerloader/tests/utils_worker.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function log(text) { + dump("WORKER " + text + "\n"); +} + +function send(message) { + self.postMessage(message); +} + +function finish() { + send({ kind: "finish" }); +} + +function ok(condition, description) { + send({ kind: "ok", condition: !!condition, description: "" + description }); +} + +function is(a, b, description) { + let outcome = a == b; // Need to decide outcome here, as not everything can be serialized + send({ + kind: "is", + outcome, + description: "" + description, + a: "" + a, + b: "" + b, + }); +} + +function isnot(a, b, description) { + let outcome = a != b; // Need to decide outcome here, as not everything can be serialized + send({ + kind: "isnot", + outcome, + description: "" + description, + a: "" + a, + b: "" + b, + }); +} + +function info(description) { + send({ kind: "info", description: "" + description }); +} diff --git a/toolkit/components/workerloader/tests/worker_handler.js b/toolkit/components/workerloader/tests/worker_handler.js new file mode 100644 index 0000000000..f8154f421e --- /dev/null +++ b/toolkit/components/workerloader/tests/worker_handler.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function worker_handler(worker) { + worker.onerror = function (error) { + error.preventDefault(); + ok(false, "error " + error); + }; + worker.onmessage = function (msg) { + ok(true, "MAIN: onmessage " + JSON.stringify(msg.data)); + switch (msg.data.kind) { + case "is": + SimpleTest.ok( + msg.data.outcome, + msg.data.description + "( " + msg.data.a + " ==? " + msg.data.b + ")" + ); + return; + case "isnot": + SimpleTest.ok( + msg.data.outcome, + msg.data.description + "( " + msg.data.a + " !=? " + msg.data.b + ")" + ); + return; + case "ok": + SimpleTest.ok(msg.data.condition, msg.data.description); + return; + case "info": + SimpleTest.info(msg.data.description); + return; + case "finish": + SimpleTest.finish(); + return; + default: + SimpleTest.ok( + false, + "test_osfile.xul: wrong message " + JSON.stringify(msg.data) + ); + } + }; +} diff --git a/toolkit/components/workerloader/tests/worker_test_loading.js b/toolkit/components/workerloader/tests/worker_test_loading.js new file mode 100644 index 0000000000..330dd7692f --- /dev/null +++ b/toolkit/components/workerloader/tests/worker_test_loading.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-env mozilla/chrome-worker */ + +"use strict"; + +importScripts("utils_worker.js"); // Test suite code +info("Test suite configured"); + +/* import-globals-from /toolkit/components/workerloader/require.js */ +importScripts("resource://gre/modules/workers/require.js"); +info("Loader imported"); + +var PATH = + "chrome://mochitests/content/chrome/toolkit/components/workerloader/tests/"; +var tests = []; +var add_test = function (test) { + tests.push(test); +}; + +add_test(function test_setup() { + ok(typeof require != "undefined", "Function |require| is defined"); +}); + +// Test simple loading (moduleA-depends.js requires moduleB-dependency.js) +add_test(function test_load() { + let A = require(PATH + "moduleA-depends.js"); + ok(true, "Opened module A"); + + is(A.A, true, "Module A exported value A"); + ok(!("B" in A), "Module A did not export value B"); + is(A.importedFoo, "foo", "Module A re-exported B.foo"); + + // re-evaluating moduleB-dependency.js would cause an error, but re-requiring it shouldn't + let B = require(PATH + "moduleB-dependency.js"); + ok(true, "Managed to re-require module B"); + is(B.B, true, "Module B exported value B"); + is(B.foo, "foo", "Module B exported value foo"); +}); + +// Test simple circular loading (moduleC-circular.js and moduleD-circular.js require each other) +add_test(function test_circular() { + let C = require(PATH + "moduleC-circular.js"); + ok(true, "Loaded circular modules C and D"); + is( + C.copiedFromD.copiedFromC.enteredC, + true, + "Properties exported by C before requiring D can be seen by D immediately" + ); + + let D = require(PATH + "moduleD-circular.js"); + is( + D.exportedFromC.finishedC, + true, + "Properties exported by C after requiring D can be seen by D eventually" + ); +}); + +// Testing error cases +add_test(function test_exceptions() { + let should_throw = function (f) { + try { + f(); + return null; + } catch (ex) { + return ex; + } + }; + + let exn = should_throw(() => require(PATH + "this module doesn't exist")); + ok(!!exn, "Attempting to load a module that doesn't exist raises an error"); + + exn = should_throw(() => require(PATH + "moduleE-throws-during-require.js")); + ok( + !!exn, + "Attempting to load a module that throws at toplevel raises an error" + ); + is( + exn.moduleName, + PATH + "moduleE-throws-during-require.js", + "moduleName is correct" + ); + isnot( + exn.moduleStack.indexOf("moduleE-throws-during-require.js"), + -1, + "moduleStack contains the name of the module" + ); + is(exn.lineNumber, 12, "The error comes with the right line number"); + + exn = should_throw(() => require(PATH + "moduleF-syntaxerror.xml")); + ok(!!exn, "Attempting to load a non-well formatted module raises an error"); + + exn = should_throw(() => require(PATH + "moduleG-throws-later.js").doThrow()); + ok(!!exn, "G.doThrow() has raised an error"); + info(exn); + ok(exn.toString().startsWith("TypeError"), "The exception is a TypeError."); + is( + exn.moduleName, + PATH + "moduleG-throws-later.js", + "The name of the module is correct" + ); + isnot( + exn.moduleStack.indexOf("moduleG-throws-later.js"), + -1, + "The name of the right file appears somewhere in the stack" + ); + is(exn.lineNumber, 13, "The error comes with the right line number"); +}); + +function get_exn(f) { + try { + f(); + return undefined; + } catch (ex) { + return ex; + } +} + +// Test module.exports +add_test(function test_module_dot_exports() { + let H = require(PATH + "moduleH-module-dot-exports.js"); + is(H.key, "value", "module.exports worked"); + let H2 = require(PATH + "moduleH-module-dot-exports.js"); + is(H2.key, "value", "module.exports returned the same key"); + ok(H2 === H, "module.exports returned the same module the second time"); + let exn = get_exn(() => (H.key = "this should not be accepted")); + ok( + exn instanceof TypeError, + "Cannot alter value in module.exports after export" + ); + exn = get_exn(() => (H.key2 = "this should not be accepted, either")); + ok( + exn instanceof TypeError, + "Cannot add value to module.exports after export" + ); +}); + +// Test relative imports (moduleI-depends.js requires moduleJ-dependency.js) +add_test(function test_load() { + let I = require(PATH + "moduleI-depends.js"); + ok(true, "Opened module I"); + + is(I.I, true, "Module I exported value I"); + ok(!("J" in I), "Module I did not export value J"); + is(I.importedFoo, "foo", "Module I re-exported J.foo"); + + // re-evaluating moduleJ-dependency.js would cause an error, but re-requiring it shouldn't + let J = require(PATH + "moduleJ-dependency.js"); + ok(true, "Managed to re-require module J"); + is(J.J, true, "Module J exported value J"); + is(J.foo, "foo", "Module J exported value foo"); +}); + +self.onmessage = function (message) { + for (let test of tests) { + info("Entering " + test.name); + try { + test(); + } catch (ex) { + ok(false, "Test " + test.name + " failed"); + info(ex); + info(ex.stack); + } + info("Leaving " + test.name); + } + finish(); +}; |