/* 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.import("resource://gre/modules/NetUtil.jsm"); // 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 = ""; } } // 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 */ 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 */ 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;