diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/tests/xpcshell/head_dbg.js | 984 |
1 files changed, 984 insertions, 0 deletions
diff --git a/devtools/server/tests/xpcshell/head_dbg.js b/devtools/server/tests/xpcshell/head_dbg.js new file mode 100644 index 0000000000..7161d5eaea --- /dev/null +++ b/devtools/server/tests/xpcshell/head_dbg.js @@ -0,0 +1,984 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: ["error", {"vars": "local"}] */ +/* eslint-disable no-shadow */ + +"use strict"; +var CC = Components.Constructor; + +// Populate AppInfo before anything (like the shared loader) accesses +// System.appinfo, which is a lazy getter. +const appInfo = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +appInfo.updateAppInfo({ + ID: "devtools@tests.mozilla.org", + name: "devtools-tests", + version: "1", + platformVersion: "42", + crashReporter: true, +}); + +const { require, loader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { worker } = ChromeUtils.import( + "resource://devtools/shared/loader/worker-loader.js" +); + +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +// Always log packets when running tests. runxpcshelltests.py will throw +// the output away anyway, unless you give it the --verbose flag. +Services.prefs.setBoolPref("devtools.debugger.log", false); +// Enable remote debugging for the relevant tests. +Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); + +const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js"); +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +const { + ActorRegistry, +} = require("resource://devtools/server/actors/utils/actor-registry.js"); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { DevToolsServer: WorkerDevToolsServer } = worker.require( + "resource://devtools/server/devtools-server.js" +); +const { + DevToolsClient, +} = require("resource://devtools/client/devtools-client.js"); +const { ObjectFront } = require("resource://devtools/client/fronts/object.js"); +const { + LongStringFront, +} = require("resource://devtools/client/fronts/string.js"); +const { + createCommandsDictionary, +} = require("resource://devtools/shared/commands/index.js"); +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { getAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); + +const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal +); + +var { loadSubScript, loadSubScriptWithOptions } = Services.scriptloader; + +/** + * The logic here must resemble the logic of --start-debugger-server as closely + * as possible. DevToolsStartup.sys.mjs uses a distinct loader that results in + * the existence of two isolated module namespaces. In practice, this can cause + * bugs such as bug 1837185. + */ +function getDistinctDevToolsServer() { + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + const requester = {}; + const distinctLoader = useDistinctSystemPrincipalLoader(requester); + registerCleanupFunction(() => { + releaseDistinctSystemPrincipalLoader(requester); + }); + + const { DevToolsServer: DistinctDevToolsServer } = distinctLoader.require( + "resource://devtools/server/devtools-server.js" + ); + return DistinctDevToolsServer; +} + +/** + * Initializes any test that needs to work with add-ons. + * + * Should be called once per test script that needs to use AddonTestUtils (and + * not once per test task!). + */ +async function startupAddonsManager() { + // Create a directory for extensions. + const profileDir = do_get_profile().clone(); + profileDir.append("extensions"); + + AddonTestUtils.init(globalThis); + AddonTestUtils.overrideCertDB(); + AddonTestUtils.appInfo = getAppInfo(); + + await AddonTestUtils.promiseStartupManager(); +} + +async function createTargetForFakeTab(title) { + const client = await startTestDevToolsServer(title); + + const tabs = await listTabs(client); + const tabDescriptor = findTab(tabs, title); + + // These xpcshell tests use mocked actors (xpcshell-test/testactors) + // which still don't support watcher actor. + // Because of that we still can't enable server side targets and target swiching. + tabDescriptor.disableTargetSwitching(); + + return tabDescriptor.getTarget(); +} + +async function createTargetForMainProcess() { + const commands = await CommandsFactory.forMainProcess(); + return commands.descriptorFront.getTarget(); +} + +/** + * Create a MemoryFront for a fake test tab. + */ +async function createTabMemoryFront() { + const target = await createTargetForFakeTab("test_memory"); + + // MemoryFront requires the HeadSnapshotActor actor to be available + // as a global actor. This isn't registered by startTestDevToolsServer which + // only register the target actors and not the browser ones. + DevToolsServer.registerActors({ browser: true }); + + const memoryFront = await target.getFront("memory"); + await memoryFront.attach(); + + registerCleanupFunction(async () => { + await memoryFront.detach(); + + // On XPCShell, the target isn't for a local tab and so target.destroy + // won't close the client. So do it so here. It will automatically destroy the target. + await target.client.close(); + }); + + return { target, memoryFront }; +} + +/** + * Same as createTabMemoryFront but attaches the MemoryFront to the MemoryActor + * scoped to the full runtime rather than to a tab. + */ +async function createMainProcessMemoryFront() { + const target = await createTargetForMainProcess(); + + const memoryFront = await target.getFront("memory"); + await memoryFront.attach(); + + registerCleanupFunction(async () => { + await memoryFront.detach(); + // For XPCShell, the main process target actor is ContentProcessTargetActor + // which doesn't expose any `detach` method. So that the target actor isn't + // destroyed when calling target.destroy. + // Close the client to cleanup everything. + await target.client.close(); + }); + + return { client: target.client, memoryFront }; +} + +function createLongStringFront(conn, form) { + // CAUTION -- do not replicate in the codebase. Instead, use marshalling + // This code is simulating how the LongStringFront would be created by protocol.js + // We should not use it like this in the codebase, this is done only for testing + // purposes until we can return a proper LongStringFront from the server. + const front = new LongStringFront(conn, form); + front.actorID = form.actor; + front.manage(front); + return front; +} + +function createTestGlobal(name, options) { + const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal + ); + // NOTE: The Sandbox constructor behaves differently based on the argument + // length. + const sandbox = options + ? Cu.Sandbox(principal, options) + : Cu.Sandbox(principal); + sandbox.__name = name; + // Expose a few mocks to better represent a Window object. + // These attributes will be used by DOCUMENT_EVENT resource listener. + sandbox.performance = { timing: {} }; + sandbox.document = { + readyState: "complete", + defaultView: sandbox, + }; + return sandbox; +} + +function connect(client) { + dump("Connecting client.\n"); + return client.connect(); +} + +function close(client) { + dump("Closing client.\n"); + return client.close(); +} + +function listTabs(client) { + dump("Listing tabs.\n"); + return client.mainRoot.listTabs(); +} + +function findTab(tabs, title) { + dump("Finding tab with title '" + title + "'.\n"); + for (const tab of tabs) { + if (tab.title === title) { + return tab; + } + } + return null; +} + +function waitForNewSource(threadFront, url) { + dump("Waiting for new source with url '" + url + "'.\n"); + return waitForEvent(threadFront, "newSource", function (packet) { + return packet.source.url === url; + }); +} + +function attachThread(targetFront, options = {}) { + dump("Attaching to thread.\n"); + return targetFront.attachThread(options); +} + +function resume(threadFront) { + dump("Resuming thread.\n"); + return threadFront.resume(); +} + +async function addWatchpoint(threadFront, frame, variable, property, type) { + const path = `${variable}.${property}`; + info(`Add an ${path} ${type} watchpoint`); + const environment = await frame.getEnvironment(); + const obj = environment.bindings.variables[variable]; + const objFront = threadFront.pauseGrip(obj.value); + return objFront.addWatchpoint(property, path, type); +} + +function getSources(threadFront) { + dump("Getting sources.\n"); + return threadFront.getSources(); +} + +function findSource(sources, url) { + dump("Finding source with url '" + url + "'.\n"); + for (const source of sources) { + if (source.url === url) { + return source; + } + } + return null; +} + +function waitForPause(threadFront) { + dump("Waiting for pause.\n"); + return waitForEvent(threadFront, "paused"); +} + +function waitForProperty(dbg, property) { + return new Promise(resolve => { + Object.defineProperty(dbg, property, { + set(newValue) { + resolve(newValue); + }, + }); + }); +} + +function setBreakpoint(threadFront, location) { + dump("Setting breakpoint.\n"); + return threadFront.setBreakpoint(location, {}); +} + +function getPrototypeAndProperties(objClient) { + dump("getting prototype and properties.\n"); + + return objClient.getPrototypeAndProperties(); +} + +function dumpn(msg) { + dump("DBG-TEST: " + msg + "\n"); +} + +function testExceptionHook(ex) { + try { + do_report_unexpected_exception(ex); + } catch (e) { + return { throw: e }; + } + return undefined; +} + +// Convert an nsIScriptError 'logLevel' value into an appropriate string. +function scriptErrorLogLevel(message) { + switch (message.logLevel) { + case Ci.nsIConsoleMessage.info: + return "info"; + case Ci.nsIConsoleMessage.warn: + return "warning"; + default: + Assert.equal(message.logLevel, Ci.nsIConsoleMessage.error); + return "error"; + } +} + +// Register a console listener, so console messages don't just disappear +// into the ether. +var errorCount = 0; +var listener = { + observe(message) { + try { + let string; + errorCount++; + try { + // If we've been given an nsIScriptError, then we can print out + // something nicely formatted, for tools like Emacs to pick up. + message.QueryInterface(Ci.nsIScriptError); + dumpn( + message.sourceName + + ":" + + message.lineNumber + + ": " + + scriptErrorLogLevel(message) + + ": " + + message.errorMessage + ); + string = message.errorMessage; + } catch (e1) { + // Be a little paranoid with message, as the whole goal here is to lose + // no information. + try { + string = "" + message.message; + } catch (e2) { + string = "<error converting error message to string>"; + } + } + + // Make sure we exit all nested event loops so that the test can finish. + while ( + DevToolsServer && + DevToolsServer.xpcInspector && + DevToolsServer.xpcInspector.eventLoopNestLevel > 0 + ) { + DevToolsServer.xpcInspector.exitNestedEventLoop(); + } + + // In the world before bug 997440, exceptions were getting lost because of + // the arbitrary JSContext being used in nsXPCWrappedJS::CallMethod. + // In the new world, the wanderers have returned. However, because of the, + // currently very-broken, exception reporting machinery in + // nsXPCWrappedJS these get reported as errors to the console, even if + // there's actually JS on the stack above that will catch them. If we + // throw an error here because of them our tests start failing. So, we'll + // just dump the message to the logs instead, to make sure the information + // isn't lost. + dumpn("head_dbg.js observed a console message: " + string); + } catch (_) { + // Swallow everything to avoid console reentrancy errors. We did our best + // to log above, but apparently that didn't cut it. + } + }, +}; + +Services.console.registerListener(listener); + +function addTestGlobal(name, server = DevToolsServer) { + const global = createTestGlobal(name); + server.addTestGlobal(global); + return global; +} + +// List the DevToolsClient |client|'s tabs, look for one whose title is +// |title|. +async function getTestTab(client, title) { + const tabs = await client.mainRoot.listTabs(); + for (const tab of tabs) { + if (tab.title === title) { + return tab; + } + } + return null; +} +/** + * Attach to the client's tab whose title is specified + * @param {Object} client + * @param {Object} title + * @returns commands + */ +async function attachTestTab(client, title) { + const descriptorFront = await getTestTab(client, title); + + // These xpcshell tests use mocked actors (xpcshell-test/testactors) + // which still don't support watcher actor. + // Because of that we still can't enable server side targets and target swiching. + descriptorFront.disableTargetSwitching(); + + const commands = await createCommandsDictionary(descriptorFront); + await commands.targetCommand.startListening(); + return commands; +} + +/** + * Attach to the client's tab whose title is specified, and then attach to + * that tab's thread. + * @param {Object} client + * @param {Object} title + * @returns {Object} + * targetFront + * threadFront + * commands + */ +async function attachTestThread(client, title) { + const commands = await attachTestTab(client, title); + const targetFront = commands.targetCommand.targetFront; + const threadFront = await targetFront.getFront("thread"); + await targetFront.attachThread({ + autoBlackBox: true, + }); + Assert.equal(threadFront.state, "attached", "Thread front is attached"); + return { targetFront, threadFront, commands }; +} + +/** + * Initialize the testing devtools server. + */ +function initTestDevToolsServer(server = DevToolsServer) { + if (server === WorkerDevToolsServer) { + const { createRootActor } = worker.require("xpcshell-test/testactors"); + server.setRootActor(createRootActor); + } else { + const { createRootActor } = require("xpcshell-test/testactors"); + server.setRootActor(createRootActor); + } + + // Allow incoming connections. + server.init(function () { + return true; + }); +} + +/** + * Initialize the testing devtools server with a tab whose title is |title|. + */ +async function startTestDevToolsServer(title, server = DevToolsServer) { + initTestDevToolsServer(server); + addTestGlobal(title); + DevToolsServer.registerActors({ target: true }); + + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + + await connect(client); + return client; +} + +async function finishClient(client) { + await client.close(); + DevToolsServer.destroy(); + do_test_finished(); +} + +/** + * Takes a relative file path and returns the absolute file url for it. + */ +function getFileUrl(name, allowMissing = false) { + const file = do_get_file(name, allowMissing); + return Services.io.newFileURI(file).spec; +} + +/** + * Returns the full path of the file with the specified name in a + * platform-independent and URL-like form. + */ +function getFilePath( + name, + allowMissing = false, + usePlatformPathSeparator = false +) { + const file = do_get_file(name, allowMissing); + let path = Services.io.newFileURI(file).spec; + let filePrePath = "file://"; + if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) { + filePrePath += "/"; + } + + path = path.slice(filePrePath.length); + + if (usePlatformPathSeparator && path.match(/^\w:/)) { + path = path.replace(/\//g, "\\"); + } + + return path; +} + +/** + * Returns the full text contents of the given file. + */ +function readFile(fileName) { + const f = do_get_file(fileName); + const s = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + s.init(f, -1, -1, false); + try { + return NetUtil.readInputStreamToString(s, s.available()); + } finally { + s.close(); + } +} + +function writeFile(fileName, content) { + const file = do_get_file(fileName, true); + const stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + stream.init(file, -1, -1, 0); + try { + do { + const numWritten = stream.write(content, content.length); + content = content.slice(numWritten); + } while (content.length); + } finally { + stream.close(); + } +} + +function StubTransport() {} +StubTransport.prototype.ready = function () {}; +StubTransport.prototype.send = function () {}; +StubTransport.prototype.close = function () {}; + +// Create async version of the object where calling each method +// is equivalent of calling it with asyncall. Mainly useful for +// destructuring objects with methods that take callbacks. +const Async = target => new Proxy(target, Async); +Async.get = (target, name) => + typeof target[name] === "function" + ? asyncall.bind(null, target[name], target) + : target[name]; + +// Calls async function that takes callback and errorback and returns +// returns promise representing result. +const asyncall = (fn, self, ...args) => + new Promise((...etc) => fn.call(self, ...args, ...etc)); + +const Test = task => () => { + add_task(task); + run_next_test(); +}; + +const assert = Assert.ok.bind(Assert); + +/** + * Create a promise that is resolved on the next occurence of the given event. + * + * @param ThreadFront threadFront + * @param String event + * @param Function predicate + * @returns Promise + */ +function waitForEvent(front, type, predicate) { + if (!predicate) { + return front.once(type); + } + + return new Promise(function (resolve) { + function listener(packet) { + if (!predicate(packet)) { + return; + } + front.off(type, listener); + resolve(packet); + } + front.on(type, listener); + }); +} + +/** + * Execute the action on the next tick and return a promise that is resolved on + * the next pause. + * + * When using promises and Task.jsm, we often want to do an action that causes a + * pause and continue the task once the pause has ocurred. Unfortunately, if we + * do the action that causes the pause within the task's current tick we will + * pause before we have a chance to yield the promise that waits for the pause + * and we enter a dead lock. The solution is to create the promise that waits + * for the pause, schedule the action to run on the next tick of the event loop, + * and finally yield the promise. + * + * @param Function action + * @param ThreadFront threadFront + * @returns Promise + */ +function executeOnNextTickAndWaitForPause(action, threadFront) { + const paused = waitForPause(threadFront); + executeSoon(action); + return paused; +} + +function evalCallback(debuggeeGlobal, func) { + Cu.evalInSandbox("(" + func + ")()", debuggeeGlobal, "1.8", "test.js", 1); +} + +/** + * Interrupt JS execution for the specified thread. + * + * @param ThreadFront threadFront + * @returns Promise + */ +function interrupt(threadFront) { + dumpn("Interrupting."); + return threadFront.interrupt(); +} + +/** + * Resume JS execution for the specified thread and then wait for the next pause + * event. + * + * @param DevToolsClient client + * @param ThreadFront threadFront + * @returns Promise + */ +async function resumeAndWaitForPause(threadFront) { + const paused = waitForPause(threadFront); + await resume(threadFront); + return paused; +} + +/** + * Resume JS execution for a single step and wait for the pause after the step + * has been taken. + * + * @param ThreadFront threadFront + * @returns Promise + */ +function stepIn(threadFront) { + dumpn("Stepping in."); + const paused = waitForPause(threadFront); + return threadFront.stepIn().then(() => paused); +} + +/** + * Resume JS execution for a step over and wait for the pause after the step + * has been taken. + * + * @param ThreadFront threadFront + * @returns Promise + */ +async function stepOver(threadFront, frameActor) { + dumpn("Stepping over."); + await threadFront.stepOver(frameActor); + return waitForPause(threadFront); +} + +/** + * Resume JS execution for a step out and wait for the pause after the step + * has been taken. + * + * @param DevToolsClient client + * @param ThreadFront threadFront + * @returns Promise + */ +async function stepOut(threadFront, frameActor) { + dumpn("Stepping out."); + await threadFront.stepOut(frameActor); + return waitForPause(threadFront); +} + +/** + * Restart specific frame and wait for the pause after the restart + * has been taken. + * + * @param DevToolsClient client + * @param ThreadFront threadFront + * @returns Promise + */ +async function restartFrame(threadFront, frameActor) { + dumpn("Restarting frame."); + await threadFront.restart(frameActor); + return waitForPause(threadFront); +} + +/** + * Get the list of `count` frames currently on stack, starting at the index + * `first` for the specified thread. + * + * @param ThreadFront threadFront + * @param Number first + * @param Number count + * @returns Promise + */ +function getFrames(threadFront, first, count) { + dumpn("Getting frames."); + return threadFront.getFrames(first, count); +} + +/** + * Black box the specified source. + * + * @param SourceFront sourceFront + * @returns Promise + */ +async function blackBox(sourceFront, range = null) { + dumpn("Black boxing source: " + sourceFront.actor); + const pausedInSource = await sourceFront.blackBox(range); + ok(true, "blackBox didn't throw"); + return pausedInSource; +} + +/** + * Stop black boxing the specified source. + * + * @param SourceFront sourceFront + * @returns Promise + */ +async function unBlackBox(sourceFront, range = null) { + dumpn("Un-black boxing source: " + sourceFront.actor); + await sourceFront.unblackBox(range); + ok(true, "unblackBox didn't throw"); +} + +/** + * Get a source at the specified url. + * + * @param ThreadFront threadFront + * @param string url + * @returns Promise<SourceFront> + */ +async function getSource(threadFront, url) { + const source = await getSourceForm(threadFront, url); + if (source) { + return threadFront.source(source); + } + + throw new Error("source not found"); +} + +async function getSourceById(threadFront, id) { + const form = await getSourceFormById(threadFront, id); + return threadFront.source(form); +} + +async function getSourceForm(threadFront, url) { + const { sources } = await threadFront.getSources(); + return sources.find(s => s.url === url); +} + +async function getSourceFormById(threadFront, id) { + const { sources } = await threadFront.getSources(); + return sources.find(source => source.actor == id); +} + +async function checkFramesLength(threadFront, expectedFrames) { + const frameResponse = await threadFront.getFrames(0, null); + Assert.equal( + frameResponse.frames.length, + expectedFrames, + "Thread front has the expected number of frames" + ); +} + +/** + * Do a reload which clears the thread debugger + * + * @param TabFront tabFront + * @returns Promise<response> + */ +function reload(tabFront) { + return tabFront.reload({}); +} + +/** + * Returns an array of stack location strings given a thread and a sample. + * + * @param object thread + * @param object sample + * @returns object + */ +function getInflatedStackLocations(thread, sample) { + const stackTable = thread.stackTable; + const frameTable = thread.frameTable; + const stringTable = thread.stringTable; + const SAMPLE_STACK_SLOT = thread.samples.schema.stack; + const STACK_PREFIX_SLOT = stackTable.schema.prefix; + const STACK_FRAME_SLOT = stackTable.schema.frame; + const FRAME_LOCATION_SLOT = frameTable.schema.location; + + // Build the stack from the raw data and accumulate the locations in + // an array. + let stackIndex = sample[SAMPLE_STACK_SLOT]; + const locations = []; + while (stackIndex !== null) { + const stackEntry = stackTable.data[stackIndex]; + const frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]]; + locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]); + stackIndex = stackEntry[STACK_PREFIX_SLOT]; + } + + // The profiler tree is inverted, so reverse the array. + return locations.reverse(); +} + +async function setupTestFromUrl(url) { + do_test_pending(); + + const { createRootActor } = require("xpcshell-test/testactors"); + DevToolsServer.setRootActor(createRootActor); + DevToolsServer.init(() => true); + + const global = createTestGlobal("test"); + DevToolsServer.addTestGlobal(global); + + const devToolsClient = new DevToolsClient(DevToolsServer.connectPipe()); + await connect(devToolsClient); + + const tabs = await listTabs(devToolsClient); + const descriptorFront = findTab(tabs, "test"); + + // These xpcshell tests use mocked actors (xpcshell-test/testactors) + // which still don't support watcher actor. + // Because of that we still can't enable server side targets and target swiching. + descriptorFront.disableTargetSwitching(); + + const targetFront = await descriptorFront.getTarget(); + + const threadFront = await attachThread(targetFront); + + const sourceUrl = getFileUrl(url); + const promise = waitForNewSource(threadFront, sourceUrl); + loadSubScript(sourceUrl, global); + const { source } = await promise; + + const sourceFront = threadFront.source(source); + return { global, devToolsClient, threadFront, sourceFront }; +} + +/** + * Run the given test function twice, one with a regular DevToolsServer, + * testing against a fake tab. And another one against a WorkerDevToolsServer, + * testing the worker codepath. + * + * @param Function test + * Test function to run twice. + * This test function is called with a dictionary: + * - Sandbox debuggee + * The custom JS debuggee created for this test. This is a Sandbox using system + * principals by default. + * - ThreadFront threadFront + * A reference to a ThreadFront instance that is attached to the debuggee. + * - DevToolsClient client + * A reference to the DevToolsClient used to communicated with the RDP server. + * @param Object options + * Optional arguments to tweak test environment + * - JSPrincipal principal + * Principal to use for the debuggee. Defaults to systemPrincipal. + * - boolean doNotRunWorker + * If true, do not run this tests in worker debugger context. Defaults to false. + * - bool wantXrays + * Whether the debuggee wants Xray vision with respect to same-origin objects + * outside the sandbox. Defaults to true. + * - bool waitForFinish + * Whether to wait for a call to threadFrontTestFinished after the test + * function finishes. + */ +function threadFrontTest(test, options = {}) { + const { + principal = systemPrincipal, + doNotRunWorker = false, + wantXrays = true, + waitForFinish = false, + } = options; + + async function runThreadFrontTestWithServer(server, test) { + // Setup a server and connect a client to it. + initTestDevToolsServer(server); + + // Create a custom debuggee and register it to the server. + // We are using a custom Sandbox as debuggee. Create a new zone because + // debugger and debuggee must be in different compartments. + const debuggee = Cu.Sandbox(principal, { freshZone: true, wantXrays }); + const scriptName = "debuggee.js"; + debuggee.__name = scriptName; + server.addTestGlobal(debuggee); + + const client = new DevToolsClient(server.connectPipe()); + await client.connect(); + + // Attach to the fake tab target and retrieve the ThreadFront instance. + // Automatically resume as the thread is paused by default after attach. + const { targetFront, threadFront, commands } = await attachTestThread( + client, + scriptName + ); + + // Cross the client/server boundary to retrieve the target actor & thread + // actor instances, used by some tests. + const rootActor = client.transport._serverConnection.rootActor; + const targetActor = + rootActor._parameters.tabList.getTargetActorForTab("debuggee.js"); + const { threadActor } = targetActor; + + // Run the test function + const args = { + threadActor, + threadFront, + debuggee, + client, + server, + targetFront, + commands, + isWorkerServer: server === WorkerDevToolsServer, + }; + if (waitForFinish) { + // Use dispatchToMainThread so that the test function does not have to + // finish executing before the test itself finishes. + const promise = new Promise( + resolve => (threadFrontTestFinished = resolve) + ); + Services.tm.dispatchToMainThread(() => test(args)); + await promise; + } else { + await test(args); + } + + // Cleanup the client after the test ran + await client.close(); + + server.removeTestGlobal(debuggee); + + // Also cleanup the created server + server.destroy(); + } + + return async () => { + dump(">>> Run thread front test against a regular DevToolsServer\n"); + await runThreadFrontTestWithServer(DevToolsServer, test); + + // Skip tests that fail in the worker context + if (!doNotRunWorker) { + dump(">>> Run thread front test against a worker DevToolsServer\n"); + await runThreadFrontTestWithServer(WorkerDevToolsServer, test); + } + }; +} + +// This callback is used in tandem with the waitForFinish option of +// threadFrontTest to support thread front tests that use promises to +// asynchronously finish the tests, instead of using async/await. +// Newly written tests should avoid using this. See bug 1596114 for migrating +// existing tests to async/await and removing this functionality. +let threadFrontTestFinished; |