diff options
Diffstat (limited to '')
222 files changed, 18060 insertions, 0 deletions
diff --git a/devtools/server/tests/xpcshell/.eslintrc.js b/devtools/server/tests/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..b3d0382a56 --- /dev/null +++ b/devtools/server/tests/xpcshell/.eslintrc.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + extends: "../../../.eslintrc.xpcshell.js", + rules: { + "no-debugger": 0, + }, +}; diff --git a/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json new file mode 100644 index 0000000000..cad9442b80 --- /dev/null +++ b/devtools/server/tests/xpcshell/addons/web-extension-upgrade/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor Upgrade", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test-addons-actor@mozilla.org" + } + } +} diff --git a/devtools/server/tests/xpcshell/addons/web-extension/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension/manifest.json new file mode 100644 index 0000000000..47f07671e5 --- /dev/null +++ b/devtools/server/tests/xpcshell/addons/web-extension/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test-addons-actor@mozilla.org" + } + } +} diff --git a/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json b/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json new file mode 100644 index 0000000000..e1ba91f4fb --- /dev/null +++ b/devtools/server/tests/xpcshell/addons/web-extension2/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 2, + "name": "Test Addons Actor 2", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "test-addons-actor2@mozilla.org" + } + } +} diff --git a/devtools/server/tests/xpcshell/completions.js b/devtools/server/tests/xpcshell/completions.js new file mode 100644 index 0000000000..5e77e4e886 --- /dev/null +++ b/devtools/server/tests/xpcshell/completions.js @@ -0,0 +1,23 @@ +"use strict"; +/* exported global doRet doThrow */ + +function ret() { + return 2; +} + +function throws() { + throw new Error("yo"); +} + +function doRet() { + debugger; + const r = ret(); + return r; +} + +function doThrow() { + debugger; + try { + throws(); + } catch (e) {} +} diff --git a/devtools/server/tests/xpcshell/head_dbg.js b/devtools/server/tests/xpcshell/head_dbg.js new file mode 100644 index 0000000000..807fe5965e --- /dev/null +++ b/devtools/server/tests/xpcshell/head_dbg.js @@ -0,0 +1,982 @@ +/* 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 = "<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; diff --git a/devtools/server/tests/xpcshell/hello-actor.js b/devtools/server/tests/xpcshell/hello-actor.js new file mode 100644 index 0000000000..f4fc63cb86 --- /dev/null +++ b/devtools/server/tests/xpcshell/hello-actor.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint no-unused-vars: ["error", {"vars": "local"}] */ + +"use strict"; + +const protocol = require("resource://devtools/shared/protocol.js"); + +const helloSpec = protocol.generateActorSpec({ + typeName: "helloActor", + + methods: { + hello: {}, + }, +}); + +class HelloActor extends protocol.Actor { + constructor(conn) { + super(conn, helloSpec); + } + + hello() {} +} diff --git a/devtools/server/tests/xpcshell/post_init_global_actors.js b/devtools/server/tests/xpcshell/post_init_global_actors.js new file mode 100644 index 0000000000..4ec5fb8078 --- /dev/null +++ b/devtools/server/tests/xpcshell/post_init_global_actors.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); + +class PostInitGlobalActor extends Actor { + constructor(conn) { + super(conn, { typeName: "postInitGlobal", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PostInitGlobalActor = PostInitGlobalActor; diff --git a/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js b/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js new file mode 100644 index 0000000000..9b0b4c053e --- /dev/null +++ b/devtools/server/tests/xpcshell/post_init_target_scoped_actors.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); + +class PostInitTargetScopedActor extends Actor { + constructor(conn) { + super(conn, { typeName: "postInitTargetScoped", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PostInitTargetScopedActor = PostInitTargetScopedActor; diff --git a/devtools/server/tests/xpcshell/pre_init_global_actors.js b/devtools/server/tests/xpcshell/pre_init_global_actors.js new file mode 100644 index 0000000000..f5e14aaaa9 --- /dev/null +++ b/devtools/server/tests/xpcshell/pre_init_global_actors.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); + +class PreInitGlobalActor extends Actor { + constructor(conn) { + super(conn, { typeName: "preInitGlobal", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PreInitGlobalActor = PreInitGlobalActor; diff --git a/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js b/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js new file mode 100644 index 0000000000..360d4b52a0 --- /dev/null +++ b/devtools/server/tests/xpcshell/pre_init_target_scoped_actors.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); + +class PreInitTargetScopedActor extends Actor { + constructor(conn) { + super(conn, { typeName: "preInitTargetScoped", methods: [] }); + + this.requestTypes = { + ping: this.onPing, + }; + } + + onPing() { + return { message: "pong" }; + } +} + +exports.PreInitTargetScopedActor = PreInitTargetScopedActor; diff --git a/devtools/server/tests/xpcshell/registertestactors-lazy.js b/devtools/server/tests/xpcshell/registertestactors-lazy.js new file mode 100644 index 0000000000..ef04e7a8d2 --- /dev/null +++ b/devtools/server/tests/xpcshell/registertestactors-lazy.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var { + RetVal, + Actor, + FrontClassWithSpec, + generateActorSpec, +} = require("resource://devtools/shared/protocol.js"); + +const lazySpec = generateActorSpec({ + typeName: "lazy", + + methods: { + hello: { + response: { str: RetVal("string") }, + }, + }, +}); + +class LazyActor extends Actor { + constructor(conn, id) { + super(conn, lazySpec); + + Services.obs.notifyObservers(null, "actor", "instantiated"); + } + + hello(str) { + return "world"; + } +} +exports.LazyActor = LazyActor; + +Services.obs.notifyObservers(null, "actor", "loaded"); + +class LazyFront extends FrontClassWithSpec(lazySpec) { + constructor(client) { + super(client); + } +} +exports.LazyFront = LazyFront; diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js new file mode 100644 index 0000000000..575915c4fd --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-in-gcd-script.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; var b = 2; var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js new file mode 100644 index 0000000000..1fbf8ef16e --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-minified.js @@ -0,0 +1,8 @@ +"use strict"; + +function other(){ var a = 1; } function test(){ var a = 1; var b = 2; var c = 3; } + +function f() { + other(); + test(); +}
\ No newline at end of file diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js new file mode 100644 index 0000000000..adce39193d --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets-in-gcd-script.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js new file mode 100644 index 0000000000..5faefc3c88 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column-with-no-offsets.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-column.js b/devtools/server/tests/xpcshell/setBreakpoint-on-column.js new file mode 100644 index 0000000000..d92231e651 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-column.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var b = 2; var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js new file mode 100644 index 0000000000..fb96be8aba --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-in-gcd-script.js @@ -0,0 +1,9 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; + var b = 2; + var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js new file mode 100644 index 0000000000..b30ebb5049 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-offsets.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + for (var i = 0; i < 1; ++i) { + ; + } +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js new file mode 100644 index 0000000000..d92231e651 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-multiple-statements.js @@ -0,0 +1,5 @@ +"use strict"; + +function f() { + var a = 1; var b = 2; var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js new file mode 100644 index 0000000000..b03d400794 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets-in-gcd-script.js @@ -0,0 +1,9 @@ +"use strict"; + +function f() {} + +(function () { + var a = 1; + + var c = 3; +})(); diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js new file mode 100644 index 0000000000..1268cf8db0 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line-with-no-offsets.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + var a = 1; + + var c = 3; +} diff --git a/devtools/server/tests/xpcshell/setBreakpoint-on-line.js b/devtools/server/tests/xpcshell/setBreakpoint-on-line.js new file mode 100644 index 0000000000..1b15e2a5e7 --- /dev/null +++ b/devtools/server/tests/xpcshell/setBreakpoint-on-line.js @@ -0,0 +1,7 @@ +"use strict"; + +function f() { + var a = 1; + var b = 2; + var c = 3; +} diff --git a/devtools/server/tests/xpcshell/source-03.js b/devtools/server/tests/xpcshell/source-03.js new file mode 100644 index 0000000000..af623a2eb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/source-03.js @@ -0,0 +1,7 @@ +/* eslint-disable */ + +function init() { + var a = foo(); +} + +function foo() {} diff --git a/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee new file mode 100644 index 0000000000..73a400a219 --- /dev/null +++ b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.coffee @@ -0,0 +1,6 @@ +foo = (n) -> + return "foo" + i for i in [0...n] + +[first, second, third] = foo(3) + +debugger
\ No newline at end of file diff --git a/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map new file mode 100644 index 0000000000..dcee3c33c3 --- /dev/null +++ b/devtools/server/tests/xpcshell/source-map-data/sourcemapped.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "file": "sourcemapped.js", + "sourceRoot": "", + "sources": [ + "sourcemapped.coffee" + ], + "names": [], + "mappings": ";AAAA;CAAA,KAAA,yBAAA;CAAA;CAAA,CAAA,CAAA,MAAO;CACL,IAAA,GAAA;AAAA,CAAA,EAAA,MAA0B,qDAA1B;CAAA,EAAe,EAAR,QAAA;CAAP,IADI;CAAN,EAAM;;CAAN,CAGA,CAAyB,IAAA;;CAEzB,UALA;CAAA" +}
\ No newline at end of file diff --git a/devtools/server/tests/xpcshell/sourcemapped.js b/devtools/server/tests/xpcshell/sourcemapped.js new file mode 100644 index 0000000000..94d130903b --- /dev/null +++ b/devtools/server/tests/xpcshell/sourcemapped.js @@ -0,0 +1,16 @@ +// Generated by CoffeeScript 1.6.1 +(function () { + var first, foo, second, third, _ref; + + foo = function (n) { + var i, _i; + for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) { + return "foo" + i; + } + }; + + _ref = foo(3), first = _ref[0], second = _ref[1], third = _ref[2]; + + debugger; + +}).call(this); diff --git a/devtools/server/tests/xpcshell/stepping-async.js b/devtools/server/tests/xpcshell/stepping-async.js new file mode 100644 index 0000000000..0ee37883bc --- /dev/null +++ b/devtools/server/tests/xpcshell/stepping-async.js @@ -0,0 +1,31 @@ +"use strict"; +/* exported stuff */ + +async function timer() { + return Promise.resolve(); +} + +function oops() { + return `oops`; +} + +async function inner() { + oops(); + await timer(); + Promise.resolve().then(async () => { + Promise.resolve().then(() => { + oops(); + }); + oops(); + }); + oops(); +} + +async function stuff() { + debugger; + const task = inner(); + oops(); + await task; + oops(); + debugger; +} diff --git a/devtools/server/tests/xpcshell/stepping.js b/devtools/server/tests/xpcshell/stepping.js new file mode 100644 index 0000000000..2134bea38d --- /dev/null +++ b/devtools/server/tests/xpcshell/stepping.js @@ -0,0 +1,36 @@ +"use strict"; +/* exported global arithmetic composition chaining nested */ + +const obj = { b }; + +function a() { + return obj; +} + +function b() { + return 2; +} + +function arithmetic() { + debugger; + a() + b(); +} + +function composition() { + debugger; + b(a()); +} + +function chaining() { + debugger; + a().b(); +} + +function c() { + return b(); +} + +function nested() { + debugger; + c(); +} diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js new file mode 100644 index 0000000000..7df3cbd2ba --- /dev/null +++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_01.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can tell the memory actor to take a heap snapshot over the RDP +// and then create a HeapSnapshot instance from the resulting file. + +add_task(async () => { + const { memoryFront } = await createTabMemoryFront(); + + const snapshotFilePath = await memoryFront.saveHeapSnapshot(); + ok( + !!(await IOUtils.stat(snapshotFilePath)), + "Should have the heap snapshot file" + ); + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok( + HeapSnapshot.isInstance(snapshot), + "And we should be able to read a HeapSnapshot instance from the file" + ); +}); diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js new file mode 100644 index 0000000000..91593d845f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_02.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can properly stream heap snapshot files over the RDP as bulk +// data. + +add_task(async () => { + const { memoryFront } = await createTabMemoryFront(); + + const snapshotFilePath = await memoryFront.saveHeapSnapshot({ + forceCopy: true, + }); + ok( + !!(await IOUtils.stat(snapshotFilePath)), + "Should have the heap snapshot file" + ); + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok( + HeapSnapshot.isInstance(snapshot), + "And we should be able to read a HeapSnapshot instance from the file" + ); +}); diff --git a/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js new file mode 100644 index 0000000000..b212abbced --- /dev/null +++ b/devtools/server/tests/xpcshell/test_MemoryActor_saveHeapSnapshot_03.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can save full runtime heap snapshots when attached to the +// ParentProcessTargetActor or a ContentProcessTargetActor. + +add_task(async () => { + const { memoryFront } = await createMainProcessMemoryFront(); + + const snapshotFilePath = await memoryFront.saveHeapSnapshot(); + ok( + !!(await IOUtils.stat(snapshotFilePath)), + "Should have the heap snapshot file" + ); + const snapshot = ChromeUtils.readHeapSnapshot(snapshotFilePath); + ok( + HeapSnapshot.isInstance(snapshot), + "And we should be able to read a HeapSnapshot instance from the file" + ); +}); diff --git a/devtools/server/tests/xpcshell/test_add_actors.js b/devtools/server/tests/xpcshell/test_add_actors.js new file mode 100644 index 0000000000..8077109d71 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_add_actors.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Get the object, from the server side, for a given actor ID +function getActorInstance(connID, actorID) { + return DevToolsServer._connections[connID].getActor(actorID); +} + +/** + * The purpose of these tests is to verify that it's possible to add actors + * both before and after the DevToolsServer has been initialized, so addons + * that add actors don't have to poll the object for its initialization state + * in order to add actors after initialization but rather can add actors anytime + * regardless of the object's state. + */ +add_task(async function () { + ActorRegistry.registerModule("resource://test/pre_init_global_actors.js", { + prefix: "preInitGlobal", + constructor: "PreInitGlobalActor", + type: { global: true }, + }); + ActorRegistry.registerModule( + "resource://test/pre_init_target_scoped_actors.js", + { + prefix: "preInitTargetScoped", + constructor: "PreInitTargetScopedActor", + type: { target: true }, + } + ); + + const client = await startTestDevToolsServer("example tab"); + + ActorRegistry.registerModule("resource://test/post_init_global_actors.js", { + prefix: "postInitGlobal", + constructor: "PostInitGlobalActor", + type: { global: true }, + }); + ActorRegistry.registerModule( + "resource://test/post_init_target_scoped_actors.js", + { + prefix: "postInitTargetScoped", + constructor: "PostInitTargetScopedActor", + type: { target: true }, + } + ); + + let actors = await client.mainRoot.rootForm; + const tabs = await client.mainRoot.listTabs(); + const tabDescriptor = tabs[0]; + + // 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(); + + const tabTarget = await tabDescriptor.getTarget(); + + Assert.equal(tabs.length, 1); + + let reply = await client.request({ + to: actors.preInitGlobalActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + reply = await client.request({ + to: tabTarget.targetForm.preInitTargetScopedActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + reply = await client.request({ + to: actors.postInitGlobalActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + reply = await client.request({ + to: tabTarget.targetForm.postInitTargetScopedActor, + type: "ping", + }); + Assert.equal(reply.message, "pong"); + + // Consider that there is only one connection, and the first one is ours + const connID = Object.keys(DevToolsServer._connections)[0]; + const postInitGlobalActor = getActorInstance( + connID, + actors.postInitGlobalActor + ); + const preInitGlobalActor = getActorInstance( + connID, + actors.preInitGlobalActor + ); + actors = await client.mainRoot.getRoot(); + Assert.equal( + postInitGlobalActor, + getActorInstance(connID, actors.postInitGlobalActor) + ); + Assert.equal( + preInitGlobalActor, + getActorInstance(connID, actors.preInitGlobalActor) + ); + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_addon_debugging_connect.js b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js new file mode 100644 index 0000000000..221e73d256 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addon_debugging_connect.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", +}); + +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +AddonTestUtils.init(this); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +ExtensionTestUtils.init(this); + +function watchFrameUpdates(front) { + const collected = []; + + const listener = data => { + collected.push(data); + }; + + front.on("frameUpdate", listener); + let unsubscribe = () => { + unsubscribe = null; + front.off("frameUpdate", listener); + return collected; + }; + + return unsubscribe; +} + +function promiseFrameUpdate(front, matcher = () => true) { + return new Promise(resolve => { + const listener = data => { + if (matcher(data)) { + resolve(); + front.off("frameUpdate", listener); + } + }; + + front.on("frameUpdate", listener); + }); +} + +// Bug 1302702 - Test connect to a webextension addon +add_task( + { + // This test needs to run only when the extension are running in a separate + // child process, otherwise attachThread would pause the main process and this + // test would get stuck. + skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions, + }, + async function test_webextension_addon_debugging_connect() { + await promiseStartupManager(); + + // Install and start a test webextension. + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + background() { + const { browser } = this; + browser.test.log("background script executed"); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("background page ready", window.location.href); + }, + }); + await extension.startup(); + const bgPageURL = await extension.awaitMessage("background page ready"); + + const commands = await CommandsFactory.forAddon(extension.id); + + // Connect to the target addon actor and wait for the updated list of frames. + const addonTarget = await commands.descriptorFront.getTarget(); + ok(addonTarget, "Got an RDP target"); + + const { frames } = await addonTarget.listFrames(); + const backgroundPageFrame = frames + .filter(frame => { + return ( + frame.url && frame.url.endsWith("/_generated_background_page.html") + ); + }) + .pop(); + ok(backgroundPageFrame, "Found the frame for the background page"); + + const threadFront = await addonTarget.attachThread(); + + ok(threadFront, "Got a threadFront for the target addon"); + equal(threadFront.paused, false, "The addon threadActor isn't paused"); + + equal( + lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size, + 1, + "The expected number of debug browser has been created by the addon actor" + ); + + const unwatchFrameUpdates = watchFrameUpdates(addonTarget); + + const promiseBgPageFrameUpdate = promiseFrameUpdate(addonTarget, data => { + return data.frames?.some(frame => frame.url === bgPageURL); + }); + + // Reload the addon through the RDP protocol. + await addonTarget.reload(); + info("Wait background page to be fully reloaded"); + await extension.awaitMessage("background page ready"); + info("Wait background page frameUpdate event"); + await promiseBgPageFrameUpdate; + + equal( + lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size, + 1, + "The number of debug browser has not been changed after an addon reload" + ); + + const frameUpdates = unwatchFrameUpdates(); + const [frameUpdate] = frameUpdates; + + equal( + frameUpdates.length, + 1, + "Expect 1 frameUpdate events to have been received" + ); + equal( + frameUpdate.frames?.length, + 1, + "Expect 1 frame in the frameUpdate event " + ); + Assert.deepEqual( + { + url: frameUpdate.frames[0].url, + }, + { + url: bgPageURL, + }, + "Got the expected frame update when the addon background page was loaded back" + ); + + await commands.destroy(); + + // Check that if we close the debugging client without uninstalling the addon, + // the webextension debugging actor should release the debug browser. + equal( + lazy.ExtensionParent.DebugUtils.debugBrowserPromises.size, + 0, + "The debug browser has been released when the RDP connection has been closed" + ); + + await extension.unload(); + } +); diff --git a/devtools/server/tests/xpcshell/test_addon_events.js b/devtools/server/tests/xpcshell/test_addon_events.js new file mode 100644 index 0000000000..262a604953 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addon_events.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +add_task(async function testReloadExitedAddon() { + await startupAddonsManager(); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + // Retrieve the current list of addons to be notified of the next list update. + // We will also call listAddons every time we receive the event "addonListChanged" for + // the same reason. + await client.mainRoot.listAddons(); + + info("Install the addon"); + const addonFile = do_get_file("addons/web-extension", false); + + let installedAddon; + await expectAddonListChanged(client, async () => { + installedAddon = await AddonManager.installTemporaryAddon(addonFile); + }); + ok(true, "Received onAddonListChanged when installing addon"); + + info("Disable the addon"); + await expectAddonListChanged(client, () => installedAddon.disable()); + ok(true, "Received onAddonListChanged when disabling addon"); + + info("Enable the addon"); + await expectAddonListChanged(client, () => installedAddon.enable()); + ok(true, "Received onAddonListChanged when enabling addon"); + + info("Put the addon in pending uninstall mode"); + await expectAddonListChanged(client, () => installedAddon.uninstall(true)); + ok(true, "Received onAddonListChanged when addon moves to pending uninstall"); + + info("Cancel uninstall for addon"); + await expectAddonListChanged(client, () => installedAddon.cancelUninstall()); + ok(true, "Received onAddonListChanged when addon uninstall is canceled"); + + info("Completely uninstall the addon"); + await expectAddonListChanged(client, () => installedAddon.uninstall()); + ok(true, "Received onAddonListChanged when addon is uninstalled"); + + await close(client); +}); + +async function expectAddonListChanged(client, predicate) { + const onAddonListChanged = client.mainRoot.once("addonListChanged"); + await predicate(); + await onAddonListChanged; + await client.mainRoot.listAddons(); +} diff --git a/devtools/server/tests/xpcshell/test_addon_reload.js b/devtools/server/tests/xpcshell/test_addon_reload.js new file mode 100644 index 0000000000..e0054f03cc --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addon_reload.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +function promiseAddonEvent(event) { + return new Promise(resolve => { + const listener = { + [event](...args) { + AddonManager.removeAddonListener(listener); + resolve(args); + }, + }; + + AddonManager.addAddonListener(listener); + }); +} + +function promiseWebExtensionStartup() { + const { Management } = ChromeUtils.importESModule( + "resource://gre/modules/Extension.sys.mjs" + ); + + return new Promise(resolve => { + const listener = (evt, extension) => { + Management.off("ready", listener); + resolve(extension); + }; + + Management.on("ready", listener); + }); +} + +async function reloadAddon(addonFront) { + // The add-on will be re-installed after a successful reload. + const onInstalled = promiseAddonEvent("onInstalled"); + await addonFront.reload(); + await onInstalled; +} + +function getSupportFile(path) { + const allowMissing = false; + return do_get_file(path, allowMissing); +} + +add_task(async function testReloadExitedAddon() { + await startupAddonsManager(); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + // Install our main add-on to trigger reloads on. + const addonFile = getSupportFile("addons/web-extension"); + const [installedAddon] = await Promise.all([ + AddonManager.installTemporaryAddon(addonFile), + promiseWebExtensionStartup(), + ]); + + // Install a decoy add-on. + const addonFile2 = getSupportFile("addons/web-extension2"); + const [installedAddon2] = await Promise.all([ + AddonManager.installTemporaryAddon(addonFile2), + promiseWebExtensionStartup(), + ]); + + const addonFront = await client.mainRoot.getAddon({ id: installedAddon.id }); + + await Promise.all([reloadAddon(addonFront), promiseWebExtensionStartup()]); + + // Uninstall the decoy add-on, which should cause its actor to exit. + const onUninstalled = promiseAddonEvent("onUninstalled"); + installedAddon2.uninstall(); + await onUninstalled; + + // Try to re-list all add-ons after a reload. + // This was throwing an exception because of the exited actor. + const newAddonFront = await client.mainRoot.getAddon({ + id: installedAddon.id, + }); + equal(newAddonFront.id, addonFront.id); + + // The fronts should be the same after the reload + equal(newAddonFront, addonFront); + + const onAddonListChanged = client.mainRoot.once("addonListChanged"); + + // Install an upgrade version of the first add-on. + const addonUpgradeFile = getSupportFile("addons/web-extension-upgrade"); + const [upgradedAddon] = await Promise.all([ + AddonManager.installTemporaryAddon(addonUpgradeFile), + promiseWebExtensionStartup(), + ]); + + // Waiting for addonListChanged unsolicited event + await onAddonListChanged; + + // re-list all add-ons after an upgrade. + const upgradedAddonFront = await client.mainRoot.getAddon({ + id: upgradedAddon.id, + }); + equal(upgradedAddonFront.id, addonFront.id); + // The fronts should be the same after the upgrade. + equal(upgradedAddonFront, addonFront); + + // The addon metadata has been updated. + equal(upgradedAddonFront.name, "Test Addons Actor Upgrade"); + + await close(client); +}); diff --git a/devtools/server/tests/xpcshell/test_addons_actor.js b/devtools/server/tests/xpcshell/test_addons_actor.js new file mode 100644 index 0000000000..ba9fda6c3d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_addons_actor.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function connect() { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + const addons = await client.mainRoot.getFront("addons"); + return [client, addons]; +} + +// The AddonsManager test helper can only be called once per test script. +// This `setup` task will run first. +add_task(async function setup() { + await startupAddonsManager(); +}); + +add_task(async function testSuccessfulInstall() { + const [client, addons] = await connect(); + + const allowMissing = false; + const usePlatformSeparator = true; + const addonPath = getFilePath( + "addons/web-extension", + allowMissing, + usePlatformSeparator + ); + const installedAddon = await addons.installTemporaryAddon(addonPath, false); + equal(installedAddon.id, "test-addons-actor@mozilla.org"); + // The returned object is currently not a proper actor. + equal(installedAddon.actor, false); + + const addonList = await client.mainRoot.listAddons(); + ok(addonList && addonList.map(a => a.name), "Received list of add-ons"); + const addon = addonList.find(a => a.id === installedAddon.id); + ok(addon, "Test add-on appeared in root install list"); + + await close(client); +}); + +add_task(async function testNonExistantPath() { + const [client, addons] = await connect(); + + await Assert.rejects( + addons.installTemporaryAddon("some-non-existant-path", false), + /Could not install add-on.*Component returned failure/ + ); + + await close(client); +}); diff --git a/devtools/server/tests/xpcshell/test_animation_name.js b/devtools/server/tests/xpcshell/test_animation_name.js new file mode 100644 index 0000000000..e88911334c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_animation_name.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that AnimationPlayerActor.getName returns the right name depending on +// the type of an animation and the various properties available on it. + +const { + AnimationPlayerActor, +} = require("resource://devtools/server/actors/animation.js"); + +function run_test() { + // Mock a window with just the properties the AnimationPlayerActor uses. + const window = {}; + window.MutationObserver = class { + constructor() { + this.observe = () => {}; + } + }; + window.Animation = class { + constructor() { + this.effect = { target: getMockNode() }; + } + }; + + window.CSSAnimation = class extends window.Animation {}; + window.CSSTransition = class extends window.Animation {}; + + // Helper to get a mock DOM node. + function getMockNode() { + return { + ownerDocument: { + defaultView: window, + }, + }; + } + + // Objects in this array should contain the following properties: + // - desc {String} For logging + // - animation {Object} An animation object instantiated from one of the mock + // window animation constructors. + // - props {Objet} Properties of this object will be added to the animation + // object. + // - expectedName {String} The expected name returned by + // AnimationPlayerActor.getName. + const TEST_DATA = [ + { + desc: "Animation with an id", + animation: new window.Animation(), + props: { id: "animation-id" }, + expectedName: "animation-id", + }, + { + desc: "Animation without an id", + animation: new window.Animation(), + props: {}, + expectedName: "", + }, + { + desc: "CSSTransition with an id", + animation: new window.CSSTransition(), + props: { id: "transition-with-id", transitionProperty: "width" }, + expectedName: "transition-with-id", + }, + { + desc: "CSSAnimation with an id", + animation: new window.CSSAnimation(), + props: { id: "animation-with-id", animationName: "move" }, + expectedName: "animation-with-id", + }, + { + desc: "CSSTransition without an id", + animation: new window.CSSTransition(), + props: { transitionProperty: "width" }, + expectedName: "width", + }, + { + desc: "CSSAnimation without an id", + animation: new window.CSSAnimation(), + props: { animationName: "move" }, + expectedName: "move", + }, + ]; + + for (const { desc, animation, props, expectedName } of TEST_DATA) { + info(desc); + for (const key in props) { + animation[key] = props[key]; + } + const actor = new AnimationPlayerActor({}, animation); + Assert.equal(actor.getName(), expectedName); + } +} diff --git a/devtools/server/tests/xpcshell/test_animation_type.js b/devtools/server/tests/xpcshell/test_animation_type.js new file mode 100644 index 0000000000..261b5ef2ac --- /dev/null +++ b/devtools/server/tests/xpcshell/test_animation_type.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test the output of AnimationPlayerActor.getType(). + +const { + ANIMATION_TYPES, + AnimationPlayerActor, +} = require("resource://devtools/server/actors/animation.js"); + +function run_test() { + // Mock a window with just the properties the AnimationPlayerActor uses. + const window = {}; + window.MutationObserver = class { + constructor() { + this.observe = () => {}; + } + }; + window.Animation = class { + constructor() { + this.effect = { target: getMockNode() }; + } + }; + + window.CSSAnimation = class extends window.Animation {}; + window.CSSTransition = class extends window.Animation {}; + + // Helper to get a mock DOM node. + function getMockNode() { + return { + ownerDocument: { + defaultView: window, + }, + }; + } + + // Objects in this array should contain the following properties: + // - desc {String} For logging + // - animation {Object} An animation object instantiated from one of the mock + // window animation constructors. + // - expectedType {String} The expected type returned by + // AnimationPlayerActor.getType. + const TEST_DATA = [ + { + desc: "Test CSSAnimation type", + animation: new window.CSSAnimation(), + expectedType: ANIMATION_TYPES.CSS_ANIMATION, + }, + { + desc: "Test CSSTransition type", + animation: new window.CSSTransition(), + expectedType: ANIMATION_TYPES.CSS_TRANSITION, + }, + { + desc: "Test ScriptAnimation type", + animation: new window.Animation(), + expectedType: ANIMATION_TYPES.SCRIPT_ANIMATION, + }, + { + desc: "Test unknown type", + animation: { effect: { target: getMockNode() } }, + expectedType: ANIMATION_TYPES.UNKNOWN, + }, + ]; + + for (const { desc, animation, expectedType } of TEST_DATA) { + info(desc); + const actor = new AnimationPlayerActor({}, animation); + Assert.equal(actor.getType(), expectedType); + } +} diff --git a/devtools/server/tests/xpcshell/test_attach.js b/devtools/server/tests/xpcshell/test_attach.js new file mode 100644 index 0000000000..fb7d232e76 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_attach.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ThreadFront } = require("resource://devtools/client/fronts/thread.js"); +const { + WindowGlobalTargetFront, +} = require("resource://devtools/client/fronts/targets/window-global.js"); + +/** + * Very naive test that checks threadClearTest helper. + * It ensures that the thread front is correctly attached. + */ +add_task( + threadFrontTest(({ threadFront, debuggee, client, targetFront }) => { + ok(true, "Thread actor was able to attach"); + ok(threadFront instanceof ThreadFront, "Thread Front is valid"); + Assert.equal(threadFront.state, "attached", "Thread Front is resumed"); + Assert.equal( + Cu.getSandboxMetadata(debuggee), + undefined, + "Debuggee client is valid (getSandboxMetadata did not fail)" + ); + ok(client instanceof DevToolsClient, "Client is valid"); + ok(targetFront instanceof WindowGlobalTargetFront, "TargetFront is valid"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_blackboxing-01.js b/devtools/server/tests/xpcshell/test_blackboxing-01.js new file mode 100644 index 0000000000..6c549b908e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-01.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test basic black boxing. + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + await testBlackBox(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +const testBlackBox = async function () { + const packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront); + + const bpSource = await getSourceById(gThreadFront, packet.frame.where.actor); + + await setBreakpoint(gThreadFront, { sourceUrl: bpSource.url, line: 2 }); + await resume(gThreadFront); + + let sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL); + + Assert.ok( + !sourceForm.isBlackBoxed, + "By default the source is not black boxed." + ); + + // Test that we can step into `doStuff` when we are not black boxed. + await runTest( + async function onSteppedLocation(location) { + const source = await getSourceFormById(gThreadFront, location.actor); + Assert.equal(source.url, BLACK_BOXED_URL); + Assert.equal(location.line, 2); + }, + async function onDebuggerStatementFrames(frames) { + for (const frame of frames) { + const source = await getSourceFormById(gThreadFront, frame.where.actor); + Assert.ok(!source.isBlackBoxed); + } + } + ); + + const blackboxedSource = await getSource(gThreadFront, BLACK_BOXED_URL); + await blackBox(blackboxedSource); + sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL); + Assert.ok(sourceForm.isBlackBoxed); + + // Test that we step through `doStuff` when we are black boxed and its frame + // doesn't show up. + await runTest( + async function onSteppedLocation(location) { + const source = await getSourceFormById(gThreadFront, location.actor); + Assert.equal(source.url, SOURCE_URL); + Assert.equal(location.line, 4); + }, + async function onDebuggerStatementFrames(frames) { + for (const frame of frames) { + const source = await getSourceFormById(gThreadFront, frame.where.actor); + if (source.url == BLACK_BOXED_URL) { + Assert.ok(source.isBlackBoxed); + } else { + Assert.ok(!source.isBlackBoxed); + } + } + } + ); + + await unBlackBox(blackboxedSource); + sourceForm = await getSourceForm(gThreadFront, BLACK_BOXED_URL); + Assert.ok(!sourceForm.isBlackBoxed); + + // Test that we can step into `doStuff` again. + await runTest( + async function onSteppedLocation(location) { + const source = await getSourceFormById(gThreadFront, location.actor); + Assert.equal(source.url, BLACK_BOXED_URL); + Assert.equal(location.line, 2); + }, + async function onDebuggerStatementFrames(frames) { + for (const frame of frames) { + const source = await getSourceFormById(gThreadFront, frame.where.actor); + Assert.ok(!source.isBlackBoxed); + } + } + ); +}; + +function evalCode() { + /* eslint-disable mozilla/var-only-at-top-level, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + function doStuff(k) { // line 1 + var arg = 15; // line 2 - Step in here + k(arg); // line 3 + }, // line 4 + gDebuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + + // prettier-ignore + Cu.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 - Break here + function (n) { // line 3 - Step through `doStuff` to here + (() => {})(); // line 4 + debugger; // line 5 + } // line 6 + ); // line 7 + } + "\n" // line 8 + + "debugger;", // line 9 + gDebuggee, + "1.8", + SOURCE_URL, + 1 + ); +} + +const runTest = async function (onSteppedLocation, onDebuggerStatementFrames) { + let packet = await executeOnNextTickAndWaitForPause( + gDebuggee.runTest, + gThreadFront + ); + Assert.equal(packet.why.type, "breakpoint"); + + await stepIn(gThreadFront); + + const location = await getCurrentLocation(); + await onSteppedLocation(location); + + packet = await resumeAndWaitForPause(gThreadFront); + Assert.equal(packet.why.type, "debuggerStatement"); + + const { frames } = await getFrames(gThreadFront, 0, 100); + await onDebuggerStatementFrames(frames); + + return resume(gThreadFront); +}; + +const getCurrentLocation = async function () { + const response = await getFrames(gThreadFront, 0, 1); + return response.frames[0].where; +}; diff --git a/devtools/server/tests/xpcshell/test_blackboxing-02.js b/devtools/server/tests/xpcshell/test_blackboxing-02.js new file mode 100644 index 0000000000..66efaee6c8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-02.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't hit breakpoints in black boxed sources, and that when we + * unblack box the source again, the breakpoint hasn't disappeared and we will + * hit it again. + */ + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Set up + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + threadFront.setBreakpoint({ sourceUrl: BLACK_BOXED_URL, line: 2 }, {}); + await threadFront.resume(); + + // Test the breakpoint in the black boxed source + const { error, sources } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + const sourceFront = threadFront.source( + sources.filter(s => s.url == BLACK_BOXED_URL)[0] + ); + + await blackBox(sourceFront); + + const packet1 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet1.why.type, + "debuggerStatement", + "We should pass over the breakpoint since the source is black boxed." + ); + + await threadFront.resume(); + + // Test the breakpoint in the unblack boxed source + await unBlackBox(sourceFront); + + const packet2 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet2.why.type, + "breakpoint", + "We should hit the breakpoint again" + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + function doStuff(k) { // line 1 + const arg = 15; // line 2 - Break here + k(arg); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + debugger; // line 5 + } // line 6 + ); // line 7 + } // line 8 + + "\n debugger;", // line 9 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-03.js b/devtools/server/tests/xpcshell/test_blackboxing-03.js new file mode 100644 index 0000000000..f97c8e70f4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-03.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't stop at debugger statements inside black boxed sources. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Set up + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + threadFront.setBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + await threadFront.resume(); + + // Test the debugger statement in the black boxed source + await threadFront.getSources(); + const sourceFront = await getSource(threadFront, BLACK_BOXED_URL); + + await blackBox(sourceFront); + + const packet2 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet2.why.type, + "breakpoint", + "We should pass over the debugger statement." + ); + + threadFront.removeBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + + await threadFront.resume(); + + // Test the debugger statement in the unblack boxed source + await unBlackBox(sourceFront); + + const packet3 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet3.why.type, + "debuggerStatement", + "We should stop at the debugger statement again" + ); + await threadFront.resume(); + + // Test the debugger statement in the black boxed range + threadFront.setBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + + await blackBox(sourceFront, { + start: { line: 1, column: 0 }, + end: { line: 9, column: 0 }, + }); + + const packet4 = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet4.why.type, + "breakpoint", + "We should pass over the debugger statement." + ); + + threadFront.removeBreakpoint({ sourceUrl: source.url, line: 4 }, {}); + await unBlackBox(sourceFront); + await threadFront.resume(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function evalCode(debuggee) { + /* eslint-disable no-multi-spaces, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + function doStuff(k) { // line 1 + debugger; // line 2 - Break here + k(100); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + Math.abs(n); // line 4 - Break here + } // line 5 + ); // line 6 + } // line 7 + + "\n debugger;", // line 8 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-multi-spaces, no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-04.js b/devtools/server/tests/xpcshell/test_blackboxing-04.js new file mode 100644 index 0000000000..13345c40e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-04.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test behavior of blackboxing sources we are currently paused in. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Set up + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + threadFront.setBreakpoint({ sourceUrl: BLACK_BOXED_URL, line: 2 }, {}); + + // Test black boxing a source while pausing in the source + const { error, sources } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + const sourceFront = threadFront.source( + sources.filter(s => s.url == BLACK_BOXED_URL)[0] + ); + + const pausedInSource = await blackBox(sourceFront); + Assert.ok( + pausedInSource, + "We should be notified that we are currently paused in this source" + ); + await threadFront.resume(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function evalCode(debuggee) { + /* eslint-disable no-multi-spaces, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + + function doStuff(k) { // line 1 + debugger; // line 2 + k(100); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + return n; // line 4 + } // line 5 + ); // line 6 + } + // line 7 + "\n runTest();", // line 8 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-multi-spaces, no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-05.js b/devtools/server/tests/xpcshell/test_blackboxing-05.js new file mode 100644 index 0000000000..388c87da88 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-05.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test exceptions inside black boxed sources. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const { error } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + + const sourceFront = await getSource(threadFront, BLACK_BOXED_URL); + await blackBox(sourceFront); + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + + const packet = await resumeAndWaitForPause(threadFront); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + Assert.equal( + source.url, + SOURCE_URL, + "We shouldn't pause while in the black boxed source." + ); + + await unBlackBox(sourceFront); + await blackBox(sourceFront, { + start: { line: 1, column: 0 }, + end: { line: 4, column: 0 }, + }); + + await threadFront.resume(); + + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const packet2 = await resumeAndWaitForPause(threadFront); + const source2 = await getSourceById(threadFront, packet2.frame.where.actor); + + Assert.equal( + source2.url, + SOURCE_URL, + "We shouldn't pause while in the black boxed source." + ); + + await threadFront.resume(); + }) +); + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +function evalCode(debuggee) { + /* eslint-disable no-multi-spaces, no-unreachable, no-undef */ + // prettier-ignore + Cu.evalInSandbox( + "" + + function doStuff(k) { // line 1 + throw new Error("error msg"); // line 2 + k(100); // line 3 + }, // line 4 + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + // prettier-ignore + Cu.evalInSandbox( + "" + + function runTest() { // line 1 + doStuff( // line 2 + function(n) { // line 3 + debugger; // line 4 + } // line 5 + ); // line 6 + } + // line 7 + "\ndebugger;\n" + // line 8 + "try { runTest() } catch (ex) { }", // line 9 + debuggee, + "1.8", + SOURCE_URL, + 1 + ); + /* eslint-enable no-multi-spaces, no-unreachable, no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_blackboxing-08.js b/devtools/server/tests/xpcshell/test_blackboxing-08.js new file mode 100644 index 0000000000..d20d8b3966 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_blackboxing-08.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test blackbox ranges + */ + +async function testFinish({ threadFront, devToolsClient }) { + await threadFront.resume(); + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + const { threadFront } = dbg; + + await invokeAndPause(dbg, `chaining()`); + + const { sources } = await getSources(threadFront); + const sourceFront = threadFront.source(sources[0]); + + await setBreakpoint(threadFront, { sourceUrl: sourceFront.url, line: 7 }); + await setBreakpoint(threadFront, { sourceUrl: sourceFront.url, line: 11 }); + + // 1. lets blackbox function a, and assert that we pause in b + const range = { start: { line: 6, column: 0 }, end: { line: 8, colum: 1 } }; + blackBox(sourceFront, range); + const paused = await resumeAndWaitForPause(threadFront); + equal(paused.frame.where.line, 11, "paused inside of b"); + await threadFront.resume(); + + // 2. lets unblackbox function a, and assert that we pause in a + unBlackBox(sourceFront, range); + await invokeAndPause(dbg, `chaining()`); + const paused2 = await resumeAndWaitForPause(threadFront); + equal(paused2.frame.where.line, 7, "paused inside of a"); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-01.js b/devtools/server/tests/xpcshell/test_breakpoint-01.js new file mode 100644 index 0000000000..be46d97cfb --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-01.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic breakpoint functionality. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + info("Wait for the debugger statement to be hit"); + const packet1 = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet1.frame.where.actor); + const location = { sourceUrl: source.url, line: debuggee.line0 + 3 }; + + threadFront.setBreakpoint(location, {}); + + const packet2 = await resumeAndWaitForPause(threadFront); + + info("Paused at the breakpoint"); + Assert.equal(packet2.frame.where.actor, source.actor); + Assert.equal(packet2.frame.where.line, location.line); + Assert.equal(packet2.why.type, "breakpoint"); + + info("Check that the breakpoint worked."); + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + /* + * Be sure to run debuggee code in its own HTML 'task', so that when we call + * the onDebuggerStatement hook, the test's own microtasks don't get suspended + * along with the debuggee's. + */ + do_timeout(0, () => { + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n", // line0 + 3 + debuggee + ); + }); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-03.js b/devtools/server/tests/xpcshell/test_breakpoint-03.js new file mode 100644 index 0000000000..f598660a98 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-03.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint on a line without code will skip + * forward when we know the script isn't GCed (the debugger is connected, + * so it's kept alive). + */ + +var test_no_skip_breakpoint = async function (source, location, debuggee) { + const [response, bpClient] = await source.setBreakpoint( + Object.assign({}, location, { noSliding: true }) + ); + + Assert.ok(!response.actualLocation); + Assert.equal(bpClient.location.line, debuggee.line0 + 3); + await bpClient.remove(); +}; + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const location = { line: debuggee.line0 + 3 }; + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + // First, make sure that we can disable sliding with the + // `noSliding` option. + await test_no_skip_breakpoint(source, location, debuggee); + + // Now make sure that the breakpoint properly slides forward one line. + const [response, bpClient] = await source.setBreakpoint(location); + Assert.ok(!!response.actualLocation); + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + threadFront.resume(); + }); + + // Use `evalInSandbox` to make the debugger treat it as normal + // globally-scoped code, where breakpoint sliding rules apply. + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "// A comment.\n" + // line0 + 3 + "var b = 2;", // line0 + 4 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-04.js b/devtools/server/tests/xpcshell/test_breakpoint-04.js new file mode 100644 index 0000000000..8b7137f85d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-04.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line in a child script works. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { sourceUrl: source.url, line: debuggee.line0 + 3 }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 5); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + // Check the return value. + Assert.equal(packet2.frame.where.actor, source.actor); + Assert.equal(packet2.frame.where.line, location.line); + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " this.b = 2;\n" + // line0 + 3 + "}\n" + // line0 + 4 + "debugger;\n" + // line0 + 5 + "foo();\n", // line0 + 6 + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-05.js b/devtools/server/tests/xpcshell/test_breakpoint-05.js new file mode 100644 index 0000000000..f678b285b1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-05.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in a child script + * will skip forward. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 3 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + }); + + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "foo();\n", // line0 + 7 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-06.js b/devtools/server/tests/xpcshell/test_breakpoint-06.js new file mode 100644 index 0000000000..79ddcdc3d4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-06.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in a deeply-nested + * child script will skip forward. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 5 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + }); + + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " function bar() {\n" + // line0 + 2 + " function baz() {\n" + // line0 + 3 + " this.a = 1;\n" + // line0 + 4 + " // A comment.\n" + // line0 + 5 + " this.b = 2;\n" + // line0 + 6 + " }\n" + // line0 + 7 + " baz();\n" + // line0 + 8 + " }\n" + // line0 + 9 + " bar();\n" + // line0 + 10 + "}\n" + // line0 + 11 + "debugger;\n" + // line0 + 12 + "foo();\n", // line0 + 13 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-07.js b/devtools/server/tests/xpcshell/test_breakpoint-07.js new file mode 100644 index 0000000000..e6391747bb --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-07.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in the second child + * script will skip forward. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 6 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + }); + + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " bar();\n" + // line0 + 2 + "}\n" + // line0 + 3 + "function bar() {\n" + // line0 + 4 + " this.a = 1;\n" + // line0 + 5 + " // A comment.\n" + // line0 + 6 + " this.b = 2;\n" + // line0 + 7 + "}\n" + // line0 + 8 + "debugger;\n" + // line0 + 9 + "foo();\n", // line0 + 10 + debuggee + ); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-08.js b/devtools/server/tests/xpcshell/test_breakpoint-08.js new file mode 100644 index 0000000000..bff0cc3b52 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-08.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line without code in a child script + * will skip forward, in a file with two scripts. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const line = debuggee.line0 + 3; + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + + // this test has been disabled for a long time so the functionality doesn't work + const response = await threadFront.setBreakpoint( + { sourceUrl: source.url, line }, + {} + ); + // check that the breakpoint has properly skipped forward one line. + assert.equal(response.actuallocation.source.actor, source.actor); + // This is wrong - location is not defined, but the test has been disabled + // for a long time and currently doesn't work. + // eslint-disable-next-line no-undef + Assert.equal(response.actualLocation.line, location.line + 1); + + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + // eslint-disable-next-line no-undef + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], response.bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + // Remove the breakpoint. + response.bpClient.remove(function (response) { + threadFront.resume().then(resolve); + }); + }); + + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n", // line0 + 5 + debuggee, + "1.7", + "script1.js"); + + // prettier-ignore + Cu.evalInSandbox("var line1 = Error().lineNumber;\n" + + "debugger;\n" + // line1 + 1 + "foo();\n", // line1 + 2 + debuggee, + "1.7", + "script2.js"); + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-09.js b/devtools/server/tests/xpcshell/test_breakpoint-09.js new file mode 100644 index 0000000000..90b334102d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-09.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that removing a breakpoint works. + */ + +let done = false; + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { sourceUrl: source.url, line: debuggee.line0 + 2 }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 7); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.frame.where.actor, source.actorID); + Assert.equal(packet2.frame.where.line, location.line); + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, undefined); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + await client.waitForRequestsToSettle(); + + done = true; + threadFront.once("paused", function (packet) { + // The breakpoint should not be hit again. + threadFront.resume().then(function () { + Assert.ok(false); + }); + }); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo(stop) {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " if (stop) return;\n" + // line0 + 3 + " delete this.a;\n" + // line0 + 4 + " foo(true);\n" + // line0 + 5 + "}\n" + // line0 + 6 + "debugger;\n" + // line0 + 7 + "foo();\n", // line0 + 8 + debuggee); + if (!done) { + Assert.ok(false); + } +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-10.js b/devtools/server/tests/xpcshell/test_breakpoint-10.js new file mode 100644 index 0000000000..fd114f173d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-10.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that setting a breakpoint in a line with multiple entry points + * triggers no matter which entry point we reach. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 3, + column: 5, + }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 1); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.i, 0); + // Check pause location + Assert.equal(packet2.frame.where.line, debuggee.line0 + 3); + Assert.equal(packet2.frame.where.column, 5); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + await client.waitForRequestsToSettle(); + + const location2 = { + sourceUrl: source.url, + line: debuggee.line0 + 3, + column: 12, + }; + threadFront.setBreakpoint(location2, {}); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + const packet3 = await waitForPause(threadFront); + // Check the return value. + Assert.equal(packet3.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.i, 1); + // Check execution location + Assert.equal(packet3.frame.where.line, debuggee.line0 + 3); + Assert.equal(packet3.frame.where.column, 12); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location2); + await client.waitForRequestsToSettle(); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a, i = 0;\n" + // line0 + 2 + "for (i = 1; i <= 2; i++) {\n" + // line0 + 3 + " a = i;\n" + // line0 + 4 + "}\n", // line0 + 5 + debuggee); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-11.js b/devtools/server/tests/xpcshell/test_breakpoint-11.js new file mode 100644 index 0000000000..a29cd2f768 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-11.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Make sure that setting a breakpoint in a line with bytecodes in multiple + * scripts, sets the breakpoint in all of them (bug 793214). + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 2, + column: 8, + }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 1); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + await resume(threadFront); + + const packet2 = await waitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, undefined); + // Check execution location + Assert.equal(packet2.frame.where.line, debuggee.line0 + 2); + Assert.equal(packet2.frame.where.column, 8); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location); + + const location2 = { + sourceUrl: source.url, + line: debuggee.line0 + 2, + column: 32, + }; + threadFront.setBreakpoint(location2, {}); + + await resume(threadFront); + const packet3 = await waitForPause(threadFront); + + // Check the return value. + Assert.equal(packet3.why.type, "breakpoint"); + // Check that the breakpoint worked. + Assert.equal(debuggee.a.b, 1); + Assert.equal(debuggee.res, undefined); + // Check execution location + Assert.equal(packet3.frame.where.line, debuggee.line0 + 2); + Assert.equal(packet3.frame.where.column, 32); + + // Remove the breakpoint. + threadFront.removeBreakpoint(location2); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = { b: 1, f: function() { return 2; } };\n" + // line0+2 + "var res = a.f();\n", // line0 + 3 + debuggee); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-12.js b/devtools/server/tests/xpcshell/test_breakpoint-12.js new file mode 100644 index 0000000000..44b524f1cf --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-12.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Make sure that setting a breakpoint twice in a line without bytecodes works + * as expected. + */ + +const NUM_BREAKPOINTS = 10; +var gBpActor; +var gCount; + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.once("paused", async function (packet) { + const source = await getSourceById( + threadFront, + packet.frame.where.actor + ); + const location = { line: debuggee.line0 + 3 }; + + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + gBpActor = response.actor; + + // Set more breakpoints at the same location. + set_breakpoints(source, location); + }); + }); + + /* eslint-disable no-multi-spaces */ + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 + " // A comment.\n" + // line0 + 3 + " this.b = 2;\n" + // line0 + 4 + "}\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "foo();\n", // line0 + 7 + debuggee + ); + /* eslint-enable no-multi-spaces */ + + // Set many breakpoints at the same location. + function set_breakpoints(source, location) { + Assert.notEqual(gCount, NUM_BREAKPOINTS); + source.setBreakpoint(location).then(function ([response, bpClient]) { + // Check that the breakpoint has properly skipped forward one line. + Assert.equal(response.actualLocation.source.actor, source.actor); + Assert.equal(response.actualLocation.line, location.line + 1); + // Check that the same breakpoint actor was returned. + Assert.equal(response.actor, gBpActor); + + if (++gCount < NUM_BREAKPOINTS) { + set_breakpoints(source, location); + return; + } + + // After setting all the breakpoints, check that only one has effectively + // remained. + threadFront.once("paused", function (packet) { + // Check the return value. + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line + 1); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.why.actors[0], bpClient.actor); + // Check that the breakpoint worked. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + + threadFront.once("paused", function (packet) { + // We don't expect any more pauses after the breakpoint was hit once. + Assert.ok(false); + }); + threadFront.resume().then(function () { + // Give any remaining breakpoints a chance to trigger. + do_timeout(1000, resolve); + }); + }); + // Continue until the breakpoint is hit. + threadFront.resume(); + }); + } + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-13.js b/devtools/server/tests/xpcshell/test_breakpoint-13.js new file mode 100644 index 0000000000..2265f3449a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-13.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that execution doesn't pause twice while stepping, when encountering + * either a breakpoint or a debugger statement. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + await threadFront.setBreakpoint( + { sourceUrl: source.url, line: 3, column: 6 }, + {} + ); + + info("Check that the stepping worked."); + const packet1 = await stepIn(threadFront); + Assert.equal(packet1.frame.where.line, 6); + Assert.equal(packet1.why.type, "resumeLimit"); + + info("Entered the foo function call frame."); + const packet2 = await stepIn(threadFront); + Assert.equal(packet2.frame.where.line, 3); + Assert.equal(packet2.why.type, "resumeLimit"); + + info("Check that the breakpoint wasn't the reason for this pause"); + const packet3 = await stepIn(threadFront); + Assert.equal(packet3.frame.where.line, 4); + Assert.equal(packet3.why.type, "resumeLimit"); + Assert.equal(packet3.why.frameFinished.return.type, "undefined"); + + info("Check that the debugger statement wasn't the reason for this pause."); + const packet4 = await stepIn(threadFront); + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + Assert.equal(packet4.frame.where.line, 7); + Assert.equal(packet4.why.type, "resumeLimit"); + + info("Check that the debugger statement wasn't the reason for this pause."); + const packet5 = await stepIn(threadFront); + Assert.equal(packet5.frame.where.line, 8); + Assert.equal(packet5.why.type, "resumeLimit"); + + info("Remove the breakpoint and finish."); + await stepIn(threadFront); + threadFront.removeBreakpoint({ sourceUrl: source.url, line: 3 }); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` + function foo() { + this.a = 1; // <-- breakpoint set here + } + debugger; + foo(); + debugger; + var b = 2; + `, + debuggee, + "1.8", + "test_breakpoint-13.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-14.js b/devtools/server/tests/xpcshell/test_breakpoint-14.js new file mode 100644 index 0000000000..835edb1385 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-14.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/** + * Check that a breakpoint or a debugger statement cause execution to pause even + * in a stepped-over function. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 2, + }; + + //Pause at debugger statement. + Assert.equal(packet.frame.where.line, debuggee.line0 + 4); + Assert.equal(packet.why.type, "debuggerStatement"); + + threadFront.setBreakpoint(location, {}); + + const testCallbacks = [ + function (packet) { + // Check that the stepping worked. + Assert.equal(packet.frame.where.line, debuggee.line0 + 5); + Assert.equal(packet.why.type, "resumeLimit"); + }, + function (packet) { + // Reached the breakpoint. + Assert.equal(packet.frame.where.line, location.line); + Assert.equal(packet.why.type, "breakpoint"); + Assert.notEqual(packet.why.type, "resumeLimit"); + }, + function (packet) { + // The frame is about to be popped while stepping. + Assert.equal(packet.frame.where.line, debuggee.line0 + 3); + Assert.notEqual(packet.why.type, "breakpoint"); + Assert.equal(packet.why.type, "resumeLimit"); + Assert.equal(packet.why.frameFinished.return.type, "undefined"); + }, + function (packet) { + // Check that the debugger statement wasn't the reason for this pause. + Assert.equal(debuggee.a, 1); + Assert.equal(debuggee.b, undefined); + Assert.equal(packet.frame.where.line, debuggee.line0 + 6); + Assert.notEqual(packet.why.type, "debuggerStatement"); + Assert.equal(packet.why.type, "resumeLimit"); + }, + function (packet) { + // Check that the debugger statement wasn't the reason for this pause. + Assert.equal(packet.frame.where.line, debuggee.line0 + 7); + Assert.notEqual(packet.why.type, "debuggerStatement"); + Assert.equal(packet.why.type, "resumeLimit"); + }, + ]; + + for (const callback of testCallbacks) { + const waiter = waitForPause(threadFront); + threadFront.stepOver(); + const packet = await waiter; + callback(packet); + } + + // Remove the breakpoint and finish. + threadFront.removeBreakpoint(location); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox("var line0 = Error().lineNumber;\n" + + "function foo() {\n" + // line0 + 1 + " this.a = 1;\n" + // line0 + 2 <-- Breakpoint is set here. + "}\n" + // line0 + 3 + "debugger;\n" + // line0 + 4 + "foo();\n" + // line0 + 5 + "debugger;\n" + // line0 + 6 + "var b = 2;\n", // line0 + 7 + debuggee); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-16.js b/devtools/server/tests/xpcshell/test_breakpoint-16.js new file mode 100644 index 0000000000..a42306eee1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-16.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that we can set breakpoints in columns, not just lines. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 1, + column: 55, + }; + + let timesBreakpointHit = 0; + threadFront.setBreakpoint(location, {}); + + while (timesBreakpointHit < 3) { + await resume(threadFront); + const packet = await waitForPause(threadFront); + await testAssertions( + packet, + debuggee, + source, + location, + timesBreakpointHit + ); + + timesBreakpointHit++; + } + + threadFront.removeBreakpoint(location); + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "(function () { debugger; this.acc = 0; for (var i = 0; i < 3; i++) this.acc++; }());", + debuggee + ); +} + +async function testAssertions( + packet, + debuggee, + source, + location, + timesBreakpointHit +) { + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line); + Assert.equal(packet.frame.where.column, location.column); + + Assert.equal(debuggee.acc, timesBreakpointHit); + const environment = await packet.frame.getEnvironment(); + Assert.equal(environment.bindings.variables.i.value, timesBreakpointHit); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-17.js b/devtools/server/tests/xpcshell/test_breakpoint-17.js new file mode 100644 index 0000000000..c52e6547ef --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-17.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/** + * Test that when we add 2 breakpoints to the same line at different columns and + * then remove one of them, we don't remove them both. + */ + +const code = + "(" + + function (global) { + global.foo = function () { + Math.abs(-1); + Math.log(0.5); + debugger; + }; + debugger; + } + + "(this))"; + +const firstLocation = { + line: 3, + column: 4, +}; + +const secondLocation = { + line: 3, + column: 18, +}; + +add_task( + threadFrontTest(({ threadFront, debuggee }) => { + return new Promise(resolve => { + threadFront.on("paused", async packet => { + const [first, second] = await set_breakpoints(packet, threadFront); + test_different_actors(first, second); + await test_remove_one(first, second, threadFront, debuggee); + resolve(); + }); + + Cu.evalInSandbox(code, debuggee, "1.8", "http://example.com/", 1); + }); + }) +); + +async function set_breakpoints(packet, threadFront) { + const source = await getSourceById(threadFront, packet.frame.where.actor); + return new Promise(resolve => { + let first, second; + + source + .setBreakpoint(firstLocation) + .then(function ([{ actualLocation }, breakpointClient]) { + Assert.ok(!actualLocation, "Should not get an actualLocation"); + first = breakpointClient; + + source + .setBreakpoint(secondLocation) + .then(function ([{ actualLocation }, breakpointClient]) { + Assert.ok(!actualLocation, "Should not get an actualLocation"); + second = breakpointClient; + + resolve([first, second]); + }); + }); + }); +} + +function test_different_actors(first, second) { + Assert.notEqual( + first.actor, + second.actor, + "Each breakpoint should have a different actor" + ); +} + +function test_remove_one(first, second, threadFront, debuggee) { + return new Promise(resolve => { + first.remove(function ({ error }) { + Assert.ok(!error, "Should not get an error removing a breakpoint"); + + let hitSecond; + threadFront.on("paused", function _onPaused({ why, frame }) { + if (why.type == "breakpoint") { + hitSecond = true; + Assert.equal( + why.actors.length, + 1, + "Should only be paused because of one breakpoint actor" + ); + Assert.equal( + why.actors[0], + second.actor, + "Should be paused because of the correct breakpoint actor" + ); + Assert.equal( + frame.where.line, + secondLocation.line, + "Should be at the right line" + ); + Assert.equal( + frame.where.column, + secondLocation.column, + "Should be at the right column" + ); + threadFront.resume(); + return; + } + + if (why.type == "debuggerStatement") { + threadFront.off("paused", _onPaused); + Assert.ok( + hitSecond, + "We should still hit `second`, but not `first`." + ); + + resolve(); + return; + } + + Assert.ok(false, "Should never get here"); + }); + + threadFront.resume().then(() => debuggee.foo()); + }); + }); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-18.js b/devtools/server/tests/xpcshell/test_breakpoint-18.js new file mode 100644 index 0000000000..b2c86458d0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-18.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that we only break on offsets that are entry points for the line we are + * breaking on. Bug 907278. + */ + +add_task( + threadFrontTest(async ({ threadFront, client, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location, {}); + await client.waitForRequestsToSettle(); + + debuggee.console = { log: x => void x }; + + await resume(threadFront); + + const packet2 = await executeOnNextTickAndWaitForPause( + debuggee.test, + threadFront + ); + Assert.equal(packet2.why.type, "breakpoint"); + + const packet3 = await resumeAndWaitForPause(threadFront); + testDbgStatement(packet3); + + await resume(threadFront); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + "debugger;\n" + + function test() { + console.log("foo bar"); + debugger; + }, + debuggee, + "1.8", + "http://example.com/", + 1 + ); +} + +function testDbgStatement({ why }) { + // Should continue to the debugger statement. + Assert.equal(why.type, "debuggerStatement"); + // Not break on another offset from the same line (that isn't an entry point + // to the line) + Assert.notEqual(why.type, "breakpoint"); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-19.js b/devtools/server/tests/xpcshell/test_breakpoint-19.js new file mode 100644 index 0000000000..013acdfaf1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-19.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Make sure that setting a breakpoint in a not-yet-existing script doesn't throw + * an error (see bug 897567). Also make sure that this breakpoint works. + */ + +const URL = "test.js"; + +function setUpCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "" + function test() { // 1 + var a = 1; // 2 + debugger; // 3 + } + // 4 + "\ndebugger;", // 5 + debuggee, + "1.8", + URL + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + setBreakpoint(threadFront, { sourceUrl: URL, line: 2 }); + + await executeOnNextTickAndWaitForPause( + () => setUpCode(debuggee), + threadFront + ); + await resume(threadFront); + + const packet = await executeOnNextTickAndWaitForPause( + debuggee.test, + threadFront + ); + equal(packet.why.type, "breakpoint"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-20.js b/devtools/server/tests/xpcshell/test_breakpoint-20.js new file mode 100644 index 0000000000..886d44164d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-20.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that when two of the "same" source are loaded concurrently (like e10s + * frame scripts), breakpoints get hit in scripts defined by all sources. + */ + +var gDebuggee; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gDebuggee = debuggee; + await testBreakpoint(threadFront); + }) +); + +const testBreakpoint = async function (threadFront) { + evalSetupCode(); + + // Load the test source once. + + evalTestCode(); + equal( + gDebuggee.functions.length, + 1, + "The test code should have added a function." + ); + + // Set a breakpoint in the test source. + + const source = await getSource(threadFront, "test.js"); + setBreakpoint(threadFront, { sourceUrl: source.url, line: 3 }); + + // Load the test source again. + + evalTestCode(); + equal( + gDebuggee.functions.length, + 2, + "The test code should have added another function." + ); + + // Should hit our breakpoint in a script defined by the first instance of the + // test source. + + const bpPause1 = await executeOnNextTickAndWaitForPause( + gDebuggee.functions[0], + threadFront + ); + equal( + bpPause1.why.type, + "breakpoint", + "Should pause because of hitting our breakpoint (not debugger statement)." + ); + const dbgStmtPause1 = await executeOnNextTickAndWaitForPause( + () => resume(threadFront), + threadFront + ); + equal( + dbgStmtPause1.why.type, + "debuggerStatement", + "And we should hit the debugger statement after the pause." + ); + await resume(threadFront); + + // Should also hit our breakpoint in a script defined by the second instance + // of the test source. + + const bpPause2 = await executeOnNextTickAndWaitForPause( + gDebuggee.functions[1], + threadFront + ); + equal( + bpPause2.why.type, + "breakpoint", + "Should pause because of hitting our breakpoint (not debugger statement)." + ); + const dbgStmtPause2 = await executeOnNextTickAndWaitForPause( + () => resume(threadFront), + threadFront + ); + equal( + dbgStmtPause2.why.type, + "debuggerStatement", + "And we should hit the debugger statement after the pause." + ); +}; + +function evalSetupCode() { + Cu.evalInSandbox("this.functions = [];", gDebuggee, "1.8", "setup.js", 1); +} + +function evalTestCode() { + Cu.evalInSandbox( + ` // 1 + this.functions.push(function () { // 2 + var setBreakpointHere = 1; // 3 + debugger; // 4 + }); // 5 + `, + gDebuggee, + "1.8", + "test.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-21.js b/devtools/server/tests/xpcshell/test_breakpoint-21.js new file mode 100644 index 0000000000..da7a87f91c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-21.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1122064 - make sure that scripts introduced via onNewScripts + * properly populate the `ScriptStore` with all there nested child + * scripts, so you can set breakpoints on deeply nested scripts + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Populate the `ScriptStore` so that we only test that the script + // is added through `onNewScript` + await getSources(threadFront); + + let packet = await executeOnNextTickAndWaitForPause(() => { + evalCode(debuggee); + }, threadFront); + const source = await getSourceById(threadFront, packet.frame.where.actor); + const location = { + sourceUrl: source.url, + line: debuggee.line0 + 8, + }; + + setBreakpoint(threadFront, location); + + await resume(threadFront); + packet = await waitForPause(threadFront); + Assert.equal(packet.why.type, "breakpoint"); + Assert.equal(packet.frame.where.actor, source.actor); + Assert.equal(packet.frame.where.line, location.line); + + await resume(threadFront); + }) +); + +function evalCode(debuggee) { + // Start a new script + /* eslint-disable mozilla/var-only-at-top-level, max-nested-callbacks, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n(" + function () { + debugger; + var a = (function () { + return (function () { + return (function () { + return (function () { + return (function () { + var x = 10; // This line gets a breakpoint + return 1; + })(); + })(); + })(); + })(); + })(); + } + ")()", + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, max-nested-callbacks, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-22.js b/devtools/server/tests/xpcshell/test_breakpoint-22.js new file mode 100644 index 0000000000..067dfa3fa2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-22.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1333219 - make that setBreakpoint fails when script is not found + * at the specified line. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Populate the `ScriptStore` so that we only test that the script + // is added through `onNewScript` + await getSources(threadFront); + + const packet = await executeOnNextTickAndWaitForPause(() => { + evalCode(debuggee); + }, threadFront); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + const location = { + line: debuggee.line0 + 2, + }; + + const [res] = await setBreakpoint(source, location); + ok(!res.error); + + const location2 = { + line: debuggee.line0 + 7, + }; + + await source.setBreakpoint(location2).then( + () => { + do_throw("no code shall not be found the specified line or below it"); + }, + reason => { + Assert.equal(reason.error, "noCodeAtLineColumn"); + ok(reason.message); + } + ); + + await resume(threadFront); + }) +); + +function evalCode(debuggee) { + // Start a new script + Cu.evalInSandbox( + ` +var line0 = Error().lineNumber; +function some_function() { + // breakpoint is valid here -- it slides one line below (line0 + 2) +} +debugger; +// no breakpoint is allowed after the EOF (line0 + 6) +`, + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-23.js b/devtools/server/tests/xpcshell/test_breakpoint-23.js new file mode 100644 index 0000000000..8f07190ea9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-23.js @@ -0,0 +1,35 @@ +/* eslint-disable max-nested-callbacks */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1552453 - Verify that breakpoints are hit for evaluated + * scripts that contain a source url pragma. + */ +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + await threadFront.setBreakpoint( + { sourceUrl: "http://example.com/code.js", line: 2, column: 1 }, + {} + ); + + info("Create a new script with the displayUrl code.js"); + const onNewSource = waitForEvent(threadFront, "newSource"); + await commands.scriptCommand.execute( + "function f() {\n return 5; \n}\n//# sourceURL=http://example.com/code.js" + ); + const sourcePacket = await onNewSource; + + equal(sourcePacket.source.url, "http://example.com/code.js"); + + info("Evaluate f() and pause at line 2"); + const onExecutionDone = commands.scriptCommand.execute("f()"); + const pausedPacket = await waitForPause(threadFront); + equal(pausedPacket.why.type, "breakpoint"); + equal(pausedPacket.frame.where.line, 2); + resume(threadFront); + await onExecutionDone; + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-24.js b/devtools/server/tests/xpcshell/test_breakpoint-24.js new file mode 100644 index 0000000000..a240a237f0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-24.js @@ -0,0 +1,239 @@ +/* eslint-disable max-nested-callbacks */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 1441183 - Verify that the debugger advances to a new location + * when encountering debugger statements and brakpoints + * + * Bug 1613165 - Verify that debugger statement is not disabled by + * adding/removing a breakpoint + */ +add_task( + threadFrontTest(async props => { + await testDebuggerStatements(props); + await testBreakpoints(props); + await testBreakpointsAndDebuggerStatements(props); + await testLoops(props); + await testRemovingBreakpoint(props); + await testAddingBreakpoint(props); + }) +); + +// Ensure that we advance to the next line when we +// step to a debugger statement and resume. +async function testDebuggerStatements({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + debugger; + debugger; + } + foo(); + //# sourceURL=http://example.com/code.js`); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "stepOver", + ], + [ + "paused at the second debugger statement", + { line: 3, type: "resumeLimit" }, + "resume", + ], + [ + "paused at the third debugger statement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Ensure that we advance to the next line when we hit a breakpoint +// on a line with a debugger statement and resume. +async function testBreakpointsAndDebuggerStatements({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + debugger; + debugger; + } + foo(); + //# sourceURL=http://example.com/testBreakpointsAndDebuggerStatements.js`); + + threadFront.setBreakpoint( + { + sourceUrl: "http://example.com/testBreakpointsAndDebuggerStatements.js", + line: 3, + }, + {} + ); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "resume", + ], + [ + "paused at the breakpoint at the second debugger statement", + { line: 3, type: "breakpoint" }, + "resume", + ], + [ + "pause at the third debugger statement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Ensure that we advance to the next line when we step to +// a line with a breakpoint and resume. +async function testBreakpoints({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + a(); + debugger; + } + function a() {} + foo(); + //# sourceURL=http://example.com/testBreakpoints.js`); + + threadFront.setBreakpoint( + { sourceUrl: "http://example.com/testBreakpoints.js", line: 3, column: 6 }, + {} + ); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "stepOver", + ], + ["paused at a()", { line: 3, type: "resumeLimit" }, "resume"], + [ + "pause at the second debugger satement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Ensure that we advance to the next line when we step to +// a line with a breakpoint and resume. +async function testLoops({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + let i = 0; + debugger; + while (i++ < 2) { + debugger; + } + debugger; + } + foo(); + //# sourceURL=http://example.com/testLoops.js`); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 3, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the second debugger satement", + { line: 5, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the second debugger satement (2nd time)", + { line: 5, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the third debugger satement", + { line: 7, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +// Bug 1613165 - ensure that if you pause on a breakpoint on a line with +// debugger statement, remove the breakpoint, and try to pause on the +// debugger statement before pausing anywhere else, debugger pauses instead of +// skipping debugger statement. +async function testRemovingBreakpoint({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + } + foo(); + foo(); + //# sourceURL=http://example.com/testRemovingBreakpoint.js`); + + const location = { + sourceUrl: "http://example.com/testRemovingBreakpoint.js", + line: 2, + column: 6, + }; + + threadFront.setBreakpoint(location, {}); + + info("paused at the breakpoint at the first debugger statement"); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, 2); + Assert.equal(packet.why.type, "breakpoint"); + threadFront.removeBreakpoint(location); + + info("paused at the first debugger statement"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 2); + Assert.equal(packet2.why.type, "debuggerStatement"); + await threadFront.resume(); +} + +// Bug 1613165 - ensure if you pause on a debugger statement, add a +// breakpoint on the same line, and try to pause on the breakpoint +// before pausing anywhere else, debugger pauses on that line instead of +// skipping breakpoint. +async function testAddingBreakpoint({ commands, threadFront }) { + commands.scriptCommand.execute(`function foo(stop) { + debugger; + } + foo(); + foo(); + //# sourceURL=http://example.com/testAddingBreakpoint.js`); + + const location = { + sourceUrl: "http://example.com/testAddingBreakpoint.js", + line: 2, + column: 6, + }; + + info("paused at the first debugger statement"); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, 2); + Assert.equal(packet.why.type, "debuggerStatement"); + threadFront.setBreakpoint(location, {}); + + info("paused at the breakpoint at the first debugger statement"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 2); + Assert.equal(packet2.why.type, "breakpoint"); + await threadFront.resume(); +} + +async function performActions(threadFront, actions) { + for (const action of actions) { + await performAction(threadFront, action); + } +} + +async function performAction(threadFront, [description, result, action]) { + info(description); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, result.line); + Assert.equal(packet.why.type, result.type); + await threadFront[action](); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-25.js b/devtools/server/tests/xpcshell/test_breakpoint-25.js new file mode 100644 index 0000000000..f155234c96 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-25.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Ensure that the debugger resume page execution when the connection drops + * and when the target is detached. + */ + +add_task( + threadFrontTest(({ threadFront, debuggee, targetFront }) => { + return new Promise(resolve => { + (async () => { + await executeOnNextTickAndWaitForPause(evalCode, threadFront); + + ok(true, "The page is paused"); + ok(!debuggee.foo, "foo is still false after we hit the breakpoint"); + + await targetFront.detach(); + + // Closing the connection will force the thread actor to resume page + // execution + ok(debuggee.foo, "foo is true after target's detach request"); + + resolve(); + })(); + + function evalCode() { + /* eslint-disable */ + Cu.evalInSandbox("var foo = false;\n", debuggee); + /* eslint-enable */ + ok(!debuggee.foo, "foo is false at startup"); + + /* eslint-disable */ + Cu.evalInSandbox("debugger;\n" + "foo = true;\n", debuggee); + /* eslint-enable */ + } + }); + }) +); + +add_task( + threadFrontTest(({ threadFront, client, debuggee }) => { + return new Promise(resolve => { + (async () => { + await executeOnNextTickAndWaitForPause(evalCode, threadFront); + + ok(true, "The page is paused"); + ok(!debuggee.foo, "foo is still false after we hit the breakpoint"); + + await client.close(); + + // `close` will force the destruction of the thread actor, which, + // will resume the page execution. But all of that seems to be + // synchronous and we have to spin the event loop in order to ensure + // having the content javascript to execute the resumed code. + await new Promise(executeSoon); + + // Closing the connection will force the thread actor to resume page + // execution + ok(debuggee.foo, "foo is true after client close"); + executeSoon(resolve); + dump("resolved\n"); + })(); + + function evalCode() { + /* eslint-disable */ + Cu.evalInSandbox("var foo = false;\n", debuggee); + /* eslint-enable */ + ok(!debuggee.foo, "foo is false at startup"); + + /* eslint-disable */ + Cu.evalInSandbox("debugger;\n" + "foo = true;\n", debuggee); + /* eslint-enable */ + } + }); + }) +); diff --git a/devtools/server/tests/xpcshell/test_breakpoint-26.js b/devtools/server/tests/xpcshell/test_breakpoint-26.js new file mode 100644 index 0000000000..8624171252 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-26.js @@ -0,0 +1,63 @@ +/* eslint-disable max-nested-callbacks */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Bug 925269 - Verify that debugger statements are skipped + * if there is a falsey conditional breakpoint at the same location. + */ +add_task( + threadFrontTest(async props => { + await testBreakpointsAndDebuggerStatements(props); + }) +); + +async function testBreakpointsAndDebuggerStatements({ commands, threadFront }) { + commands.scriptCommand.execute( + `function foo(stop) { + debugger; + debugger; + debugger; + } + foo(); + //# sourceURL=http://example.com/testBreakpointsAndDebuggerStatements.js` + ); + + threadFront.setBreakpoint( + { + sourceUrl: "http://example.com/testBreakpointsAndDebuggerStatements.js", + line: 3, + column: 6, + }, + { condition: "false" } + ); + + await performActions(threadFront, [ + [ + "paused at first debugger statement", + { line: 2, type: "debuggerStatement" }, + "resume", + ], + [ + "pause at the third debugger statement", + { line: 4, type: "debuggerStatement" }, + "resume", + ], + ]); +} + +async function performActions(threadFront, actions) { + for (const action of actions) { + await performAction(threadFront, action); + } +} + +async function performAction(threadFront, [description, result, action]) { + info(description); + const packet = await waitForEvent(threadFront, "paused"); + Assert.equal(packet.frame.where.line, result.line); + Assert.equal(packet.why.type, result.type); + await threadFront[action](); +} diff --git a/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js b/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js new file mode 100644 index 0000000000..e45096095e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_breakpoint-actor-map.js @@ -0,0 +1,257 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the functionality of the BreakpointActorMap object. + +const { + BreakpointActorMap, +} = require("resource://devtools/server/actors/utils/breakpoint-actor-map.js"); + +function run_test() { + test_get_actor(); + test_set_actor(); + test_delete_actor(); + test_find_actors(); + test_duplicate_actors(); +} + +function test_get_actor() { + const bpStore = new BreakpointActorMap(); + const location = { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 3, + }; + const columnLocation = { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 5, + generatedColumn: 15, + }; + + // Shouldn't have breakpoint + Assert.equal( + null, + bpStore.getActor(location), + "Breakpoint not added and shouldn't exist." + ); + + bpStore.setActor(location, {}); + Assert.ok( + !!bpStore.getActor(location), + "Breakpoint added but not found in Breakpoint Store." + ); + + bpStore.deleteActor(location); + Assert.equal( + null, + bpStore.getActor(location), + "Breakpoint removed but still exists." + ); + + // Same checks for breakpoint with a column + Assert.equal( + null, + bpStore.getActor(columnLocation), + "Breakpoint with column not added and shouldn't exist." + ); + + bpStore.setActor(columnLocation, {}); + Assert.ok( + !!bpStore.getActor(columnLocation), + "Breakpoint with column added but not found in Breakpoint Store." + ); + + bpStore.deleteActor(columnLocation); + Assert.equal( + null, + bpStore.getActor(columnLocation), + "Breakpoint with column removed but still exists in Breakpoint Store." + ); +} + +function test_set_actor() { + // Breakpoint with column + const bpStore = new BreakpointActorMap(); + let location = { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 9, + }; + bpStore.setActor(location, {}); + Assert.ok( + !!bpStore.getActor(location), + "We should have the column breakpoint we just added" + ); + + // Breakpoint without column (whole line breakpoint) + location = { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 103, + }; + bpStore.setActor(location, {}); + Assert.ok( + !!bpStore.getActor(location), + "We should have the whole line breakpoint we just added" + ); +} + +function test_delete_actor() { + // Breakpoint with column + const bpStore = new BreakpointActorMap(); + let location = { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 9, + }; + bpStore.setActor(location, {}); + bpStore.deleteActor(location); + Assert.equal( + bpStore.getActor(location), + null, + "We should not have the column breakpoint anymore" + ); + + // Breakpoint without column (whole line breakpoint) + location = { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 103, + }; + bpStore.setActor(location, {}); + bpStore.deleteActor(location); + Assert.equal( + bpStore.getActor(location), + null, + "We should not have the whole line breakpoint anymore" + ); +} + +function test_find_actors() { + const bps = [ + { generatedSourceActor: { actor: "actor1" }, generatedLine: 10 }, + { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 3, + }, + { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 10, + generatedColumn: 10, + }, + { + generatedSourceActor: { actor: "actor1" }, + generatedLine: 23, + generatedColumn: 89, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 10, + generatedColumn: 1, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 20, + generatedColumn: 5, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 30, + generatedColumn: 34, + }, + { + generatedSourceActor: { actor: "actor2" }, + generatedLine: 40, + generatedColumn: 56, + }, + ]; + + const bpStore = new BreakpointActorMap(); + + for (const bp of bps) { + bpStore.setActor(bp, bp); + } + + // All breakpoints + + let bpSet = new Set(bps); + for (const bp of bpStore.findActors()) { + bpSet.delete(bp); + } + Assert.equal(bpSet.size, 0, "Should be able to iterate over all breakpoints"); + + // Breakpoints by URL + + bpSet = new Set( + bps.filter(bp => { + return bp.generatedSourceActor.actorID === "actor1"; + }) + ); + for (const bp of bpStore.findActors({ + generatedSourceActor: { actorID: "actor1" }, + })) { + bpSet.delete(bp); + } + Assert.equal(bpSet.size, 0, "Should be able to filter the iteration by url"); + + // Breakpoints by URL and line + + bpSet = new Set( + bps.filter(bp => { + return ( + bp.generatedSourceActor.actorID === "actor1" && bp.generatedLine === 10 + ); + }) + ); + let first = true; + for (const bp of bpStore.findActors({ + generatedSourceActor: { actorID: "actor1" }, + generatedLine: 10, + })) { + if (first) { + Assert.equal( + bp.generatedColumn, + undefined, + "Should always get the whole line breakpoint first" + ); + first = false; + } else { + Assert.notEqual( + bp.generatedColumn, + undefined, + "Should not get the whole line breakpoint any time other than first." + ); + } + bpSet.delete(bp); + } + Assert.equal( + bpSet.size, + 0, + "Should be able to filter the iteration by url and line" + ); +} + +function test_duplicate_actors() { + const bpStore = new BreakpointActorMap(); + + // Breakpoint with column + let location = { + generatedSourceActor: { actorID: "foo-actor" }, + generatedLine: 10, + generatedColumn: 9, + }; + bpStore.setActor(location, {}); + bpStore.setActor(location, {}); + Assert.equal(bpStore.size, 1, "We should have only 1 column breakpoint"); + bpStore.deleteActor(location); + + // Breakpoint without column (whole line breakpoint) + location = { + generatedSourceActor: { actorID: "foo-actor" }, + generatedLine: 15, + }; + bpStore.setActor(location, {}); + bpStore.setActor(location, {}); + Assert.equal(bpStore.size, 1, "We should have only 1 whole line breakpoint"); + bpStore.deleteActor(location); +} diff --git a/devtools/server/tests/xpcshell/test_client_request.js b/devtools/server/tests/xpcshell/test_client_request.js new file mode 100644 index 0000000000..3fc897281c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_client_request.js @@ -0,0 +1,261 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the DevToolsClient.request API. + +var gClient, gActorId; + +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); + +class TestActor extends Actor { + constructor(conn) { + super(conn, { typeName: "test", methods: [] }); + + this.requestTypes = { + hello: this.hello, + error: this.error, + }; + } + + hello() { + return { hello: "world" }; + } + + error() { + return { error: "code", message: "human message" }; + } +} + +function run_test() { + ActorRegistry.addGlobalActor( + { + constructorName: "TestActor", + constructorFun: TestActor, + }, + "test" + ); + + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + add_test(init); + add_test(test_client_request_callback); + add_test(test_client_request_promise); + add_test(test_client_request_promise_error); + add_test(test_client_request_event_emitter); + add_test(test_close_client_while_sending_requests); + add_test(test_client_request_after_close); + add_test(test_client_request_after_close_callback); + run_next_test(); +} + +function init() { + gClient = new DevToolsClient(DevToolsServer.connectPipe()); + gClient + .connect() + .then(() => gClient.mainRoot.rootForm) + .then(response => { + gActorId = response.test; + run_next_test(); + }); +} + +function checkStack(expectedName) { + let stack = Components.stack; + while (stack) { + info(stack.name); + if (stack.name == expectedName) { + // Reached back to outer function before request + ok(true, "Complete stack"); + return; + } + stack = stack.asyncCaller || stack.caller; + } + ok(false, "Incomplete stack"); +} + +function test_client_request_callback() { + // Test that DevToolsClient.request accepts a `onResponse` callback as 2nd argument + gClient.request( + { + to: gActorId, + type: "hello", + }, + response => { + Assert.equal(response.from, gActorId); + Assert.equal(response.hello, "world"); + checkStack("test_client_request_callback"); + run_next_test(); + } + ); +} + +function test_client_request_promise() { + // Test that DevToolsClient.request returns a promise that resolves on response + const request = gClient.request({ + to: gActorId, + type: "hello", + }); + + request.then(response => { + Assert.equal(response.from, gActorId); + Assert.equal(response.hello, "world"); + checkStack("test_client_request_promise/<"); + run_next_test(); + }); +} + +function test_client_request_promise_error() { + // Test that DevToolsClient.request returns a promise that reject when server + // returns an explicit error message + const request = gClient.request({ + to: gActorId, + type: "error", + }); + + request.then( + () => { + do_throw("Promise shouldn't be resolved on error"); + }, + response => { + Assert.equal(response.from, gActorId); + Assert.equal(response.error, "code"); + Assert.equal(response.message, "human message"); + checkStack("test_client_request_promise_error/<"); + run_next_test(); + } + ); +} + +function test_client_request_event_emitter() { + // Test that DevToolsClient.request returns also an EventEmitter object + const request = gClient.request({ + to: gActorId, + type: "hello", + }); + request.on("json-reply", reply => { + Assert.equal(reply.from, gActorId); + Assert.equal(reply.hello, "world"); + checkStack("test_client_request_event_emitter"); + run_next_test(); + }); +} + +function test_close_client_while_sending_requests() { + // First send a first request that will be "active" + // while the connection is closed. + // i.e. will be sent but no response received yet. + const activeRequest = gClient.request({ + to: gActorId, + type: "hello", + }); + + // Pile up a second one that will be "pending". + // i.e. won't event be sent. + const pendingRequest = gClient.request({ + to: gActorId, + type: "hello", + }); + + const expectReply = new Promise(resolve => { + gClient.expectReply("root", function (response) { + Assert.equal(response.error, "connectionClosed"); + Assert.equal( + response.message, + "server side packet can't be received as the connection just closed." + ); + resolve(); + }); + }); + + gClient.close().then(() => { + activeRequest + .then( + () => { + ok( + false, + "First request unexpectedly succeed while closing the connection" + ); + }, + response => { + Assert.equal(response.error, "connectionClosed"); + Assert.equal( + response.message, + "'hello' active request packet to '" + + gActorId + + "' can't be sent as the connection just closed." + ); + } + ) + .then(() => pendingRequest) + .then( + () => { + ok( + false, + "Second request unexpectedly succeed while closing the connection" + ); + }, + response => { + Assert.equal(response.error, "connectionClosed"); + Assert.equal( + response.message, + "'hello' pending request packet to '" + + gActorId + + "' can't be sent as the connection just closed." + ); + } + ) + .then(() => expectReply) + .then(run_next_test); + }); +} + +function test_client_request_after_close() { + // Test that DevToolsClient.request fails after we called client.close() + // (with promise API) + const request = gClient.request({ + to: gActorId, + type: "hello", + }); + + request.then( + response => { + ok(false, "Request succeed even after client.close"); + }, + response => { + ok(true, "Request failed after client.close"); + Assert.equal(response.error, "connectionClosed"); + ok( + response.message.match( + /'hello' request packet to '.*' can't be sent as the connection is closed./ + ) + ); + run_next_test(); + } + ); +} + +function test_client_request_after_close_callback() { + // Test that DevToolsClient.request fails after we called client.close() + // (with callback API) + gClient + .request( + { + to: gActorId, + type: "hello", + }, + response => { + ok(true, "Request failed after client.close"); + Assert.equal(response.error, "connectionClosed"); + ok( + response.message.match( + /'hello' request packet to '.*' can't be sent as the connection is closed./ + ) + ); + run_next_test(); + } + ) + .catch(() => info("Caught rejected promise as expected")); +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js new file mode 100644 index 0000000000..8f2e58f651 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-01.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check conditional breakpoint when condition evaluates to true. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + let hitBreakpoint = false; + + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet1.frame.where.actor); + const location = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location, { condition: "a === 1" }); + + // Continue until the breakpoint is hit. + const packet2 = await resumeAndWaitForPause(threadFront); + + Assert.equal(hitBreakpoint, false); + hitBreakpoint = true; + + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + Assert.equal(packet2.frame.where.line, 3); + + // Remove the breakpoint. + await threadFront.removeBreakpoint(location); + + await threadFront.resume(); + + Assert.equal(hitBreakpoint, true); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // line 1 + "var a = 1;\n" + // line 2 + "var b = 2;\n", // line 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js new file mode 100644 index 0000000000..18742c4048 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-02.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check conditional breakpoint when condition evaluates to false. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet1.frame.where.actor); + const location1 = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location1, { condition: "a === 2" }); + + const location2 = { sourceUrl: source.url, line: 4 }; + threadFront.setBreakpoint(location2, { condition: "a === 1" }); + + // Continue until the breakpoint is hit. + const packet2 = await resumeAndWaitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.why.type, "breakpoint"); + Assert.equal(packet2.frame.where.line, 4); + + // Remove the breakpoint. + await threadFront.removeBreakpoint(location2); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // line 1 + "var a = 1;\n" + // line 2 + "var b = 2;\n" + // line 3 + "b++;" + // line 4 + "debugger;", // line 5 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js new file mode 100644 index 0000000000..7b26614a2c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-03.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * If pauseOnExceptions is checked, when condition throws, + * make sure conditional breakpoint pauses. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet1.frame.where.actor); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + const location = { sourceUrl: source.url, line: 3 }; + threadFront.setBreakpoint(location, { condition: "throw new Error()" }); + + // Continue until the breakpoint is hit. + const packet2 = await resumeAndWaitForPause(threadFront); + + // Check the return value. + Assert.equal(packet2.why.type, "exception"); + Assert.equal(packet2.frame.where.line, 1); + + // Step over twice. + await stepOver(threadFront); + const packet3 = await stepOver(threadFront); + + // Check the return value. + Assert.equal(packet3.why.type, "breakpointConditionThrown"); + Assert.equal(packet3.frame.where.line, 3); + + // Remove the breakpoint. + await threadFront.removeBreakpoint(location); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // line 1 + "var a = 1;\n" + // line 2 + "var b = 2;\n", // line 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js b/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js new file mode 100644 index 0000000000..508f67fef8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_conditional_breakpoint-04.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Confirm that we ignore breakpoint condition exceptions + * unless pause-on-exceptions is set to true. + * + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await threadFront.setBreakpoint( + { sourceUrl: "conditional_breakpoint-04.js", line: 3 }, + { condition: "throw new Error()" } + ); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(packet.frame.where.line, 1); + Assert.equal(packet.why.type, "debuggerStatement"); + + const pausedPacket = await resumeAndWaitForPause(threadFront); + Assert.equal(pausedPacket.frame.where.line, 4); + Assert.equal(pausedPacket.why.type, "debuggerStatement"); + + // Remove the breakpoint. + await threadFront.removeBreakpoint({ + sourceUrl: "conditional_breakpoint-04.js", + line: 3, + }); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + `debugger; + var a = 1; + var b = 2; + debugger;`, + debuggee, + "1.8", + "conditional_breakpoint-04.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js b/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js new file mode 100644 index 0000000000..d69291485d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_connection_closes_all_pools.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Pool } = require("resource://devtools/shared/protocol/Pool.js"); +const { + DevToolsServerConnection, +} = require("resource://devtools/server/devtools-server-connection.js"); +const { + LocalDebuggerTransport, +} = require("resource://devtools/shared/transport/local-transport.js"); + +// Helper class to assert how many times a Pool was destroyed +class FakeActor extends Pool { + constructor(...args) { + super(...args); + this.destroyedCount = 0; + } + + destroy() { + this.destroyedCount++; + super.destroy(); + } +} + +add_task(async function () { + const transport = new LocalDebuggerTransport(); + const conn = new DevToolsServerConnection("prefix", transport); + + // Setup a flat pool hierarchy with multiple pools: + // + // - pool1 + // | + // \- actor1 + // + // - pool2 + // | + // |- actor2a + // | + // \- actor2b + // + // From the point of view of the DevToolsServerConnection, the only pools + // registered in _extraPools should be pool1 and pool2. Even though actor1, + // actor2a and actor2b extend Pool, they don't manage other pools. + const actor1 = new FakeActor(conn); + const pool1 = new Pool(conn, "pool-1"); + pool1.manage(actor1); + + const actor2a = new FakeActor(conn); + const actor2b = new FakeActor(conn); + const pool2 = new Pool(conn, "pool-2"); + pool2.manage(actor2a); + pool2.manage(actor2b); + + ok(!!actor1.actorID, "actor1 has a valid actorID"); + ok(!!actor2a.actorID, "actor2a has a valid actorID"); + ok(!!actor2b.actorID, "actor2b has a valid actorID"); + + conn.close(); + + equal(actor1.destroyedCount, 1, "actor1 was successfully destroyed"); + equal(actor2a.destroyedCount, 1, "actor2 was successfully destroyed"); + equal(actor2b.destroyedCount, 1, "actor2 was successfully destroyed"); +}); + +add_task(async function () { + const transport = new LocalDebuggerTransport(); + const conn = new DevToolsServerConnection("prefix", transport); + + // Setup a nested pool hierarchy: + // + // - pool + // | + // \- parentActor + // | + // \- childActor + // + // Since parentActor is also a Pool from the point of view of the + // DevToolsServerConnection, it will attempt to destroy it when looping on + // this._extraPools. But since `parentActor` is also a direct child of `pool`, + // it has already been destroyed by the Pool destroy() mechanism. + // + // Here we check that we don't call destroy() too many times on a single Pool. + // Even though Pool::destroy() is stable when called multiple times, we can't + // guarantee the same for classes inheriting Pool. + const childActor = new FakeActor(conn); + const parentActor = new FakeActor(conn); + const pool = new Pool(conn, "pool"); + pool.manage(parentActor); + parentActor.manage(childActor); + + ok(!!parentActor.actorID, "customActor has a valid actorID"); + ok(!!childActor.actorID, "childActor has a valid actorID"); + + conn.close(); + + equal(parentActor.destroyedCount, 1, "parentActor was destroyed once"); + equal(parentActor.destroyedCount, 1, "customActor was destroyed once"); +}); diff --git a/devtools/server/tests/xpcshell/test_console_eval-01.js b/devtools/server/tests/xpcshell/test_console_eval-01.js new file mode 100644 index 0000000000..abb6ddc605 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_console_eval-01.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that is possible to evaluate JS with evaluation timeouts in place. + */ + +add_task( + threadFrontTest(async ({ commands }) => { + await commands.scriptCommand.execute(` + function fib(n) { + if (n == 1 || n == 0) { + return 1; + } + + return fib(n-1) + fib(n-2) + } + `); + + const normalResult = await commands.scriptCommand.execute("fib(1)", { + eager: true, + }); + Assert.equal(normalResult.result, 1, "normal eval"); + + const timeoutResult = await commands.scriptCommand.execute("fib(100)", { + eager: true, + }); + Assert.equal(typeof timeoutResult.result, "object", "timeout eval"); + Assert.equal(timeoutResult.result.type, "undefined", "timeout eval type"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_console_eval-02.js b/devtools/server/tests/xpcshell/test_console_eval-02.js new file mode 100644 index 0000000000..11b3d130b4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_console_eval-02.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that bound functions can be eagerly evaluated. + */ + +add_task( + threadFrontTest(async ({ commands }) => { + await commands.scriptCommand.execute(` + var obj = [1, 2, 3]; + var fn = obj.includes.bind(obj, 2); + `); + + const normalResult = await commands.scriptCommand.execute("fn()", { + eager: true, + }); + Assert.equal(normalResult.result, true, "normal eval"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_dbgactor.js b/devtools/server/tests/xpcshell/test_dbgactor.js new file mode 100644 index 0000000000..cb0cf8f7d7 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_dbgactor.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService( + Ci.nsIJSInspector +); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + Assert.equal(xpcInspector.eventLoopNestLevel, 0); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(false, "error" in packet); + Assert.ok("actor" in packet); + Assert.ok("why" in packet); + Assert.equal(packet.why.type, "debuggerStatement"); + + // Reach around the protocol to check that the debuggee is in the state + // we expect. + Assert.ok(debuggee.a); + Assert.ok(!debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 1); + + // Let the debuggee continue execution. + await threadFront.resume(); + + // Now make sure that we've run the code after the debugger statement... + Assert.ok(debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 0); + }) +); + +function evalCode(debuggee) { + Cu.evalInSandbox( + "var a = true; var b = false; debugger; var b = true;", + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js b/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js new file mode 100644 index 0000000000..254f582460 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_dbgclient_debuggerstatement.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService( + Ci.nsIJSInspector +); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(threadFront.state, "paused"); + // Reach around the protocol to check that the debuggee is in the state + // we expect. + Assert.ok(debuggee.a); + Assert.ok(!debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 1); + + await threadFront.resume(); + + // Now make sure that we've run the code after the debugger statement... + Assert.ok(debuggee.b); + + Assert.equal(xpcInspector.eventLoopNestLevel, 0); + }) +); + +function evalCode(debuggee) { + Cu.evalInSandbox( + "var a = true; var b = false; debugger; var b = true;", + debuggee + ); +} diff --git a/devtools/server/tests/xpcshell/test_dbgglobal.js b/devtools/server/tests/xpcshell/test_dbgglobal.js new file mode 100644 index 0000000000..407e270da4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_dbgglobal.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + SocketListener, +} = require("resource://devtools/shared/security/socket.js"); + +function run_test() { + // Should get an exception if we try to interact with DevToolsServer + // before we initialize it... + const socketListener = new SocketListener(DevToolsServer, {}); + Assert.throws( + () => DevToolsServer.addSocketListener(socketListener), + /DevToolsServer has not been initialized/, + "addSocketListener should throw before it has been initialized" + ); + Assert.throws( + DevToolsServer.closeAllSocketListeners, + /this is undefined/, + "closeAllSocketListeners should throw before it has been initialized" + ); + Assert.throws( + DevToolsServer.connectPipe, + /this is undefined/, + "connectPipe should throw before it has been initialized" + ); + + // Allow incoming connections. + DevToolsServer.init(); + + // These should still fail because we haven't added a createRootActor + // implementation yet. + Assert.throws( + DevToolsServer.closeAllSocketListeners, + /this is undefined/, + "closeAllSocketListeners should throw if createRootActor hasn't been added" + ); + Assert.throws( + DevToolsServer.connectPipe, + /this is undefined/, + "closeAllSocketListeners should throw if createRootActor hasn't been added" + ); + + const { createRootActor } = require("xpcshell-test/testactors"); + DevToolsServer.setRootActor(createRootActor); + + // Now they should work. + DevToolsServer.addSocketListener(socketListener); + DevToolsServer.closeAllSocketListeners(); + + // Make sure we got the test's root actor all set up. + const client1 = DevToolsServer.connectPipe(); + client1.hooks = { + onPacket(packet1) { + Assert.equal(packet1.from, "root"); + Assert.equal(packet1.applicationType, "xpcshell-tests"); + + // Spin up a second connection, make sure it has its own root + // actor. + const client2 = DevToolsServer.connectPipe(); + client2.hooks = { + onPacket(packet2) { + Assert.equal(packet2.from, "root"); + Assert.notEqual( + packet1.testConnectionPrefix, + packet2.testConnectionPrefix + ); + client2.close(); + }, + onTransportClosed(result) { + client1.close(); + }, + }; + client2.ready(); + }, + + onTransportClosed(result) { + do_test_finished(); + }, + }; + + client1.ready(); + do_test_pending(); +} diff --git a/devtools/server/tests/xpcshell/test_extension_storage_actor.js b/devtools/server/tests/xpcshell/test_extension_storage_actor.js new file mode 100644 index 0000000000..9816854cf8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_extension_storage_actor.js @@ -0,0 +1,1155 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* globals browser */ + +"use strict"; + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { + createMissingIndexedDBDirs, + extensionScriptWithMessageListener, + ext_no_bg, + getExtensionConfig, + openAddonStoragePanel, + shutdown, + startupExtension, +} = require("resource://test/webextension-helpers.js"); + +const l10n = new Localization(["devtools/client/storage.ftl"], true); +const sessionString = l10n.formatValueSync("storage-expires-session"); + +// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /sendRemoveListener on closed conduit/ +); + +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; +const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; + +AddonTestUtils.init(this); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +ExtensionTestUtils.init(this); + +add_setup(async function setup() { + await promiseStartupManager(); + const dir = createMissingIndexedDBDirs(); + + Assert.ok( + dir.exists(), + "Should have a 'storage/permanent' dir in the profile dir" + ); +}); + +add_task(async function test_extension_store_exists() { + const extension = await startupExtension(getExtensionConfig()); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + ok(extensionStorage, "Should have an extensionStorage store"); + + await shutdown(extension, commands); +}); + +add_task( + { + // This test currently fails if the extension runs in the main process + // like in Thunderbird (see bug 1575183 comment #15 for details). + skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions, + }, + async function test_extension_origin_matches_debugger_target() { + async function background() { + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + + const extension = await startupExtension( + getExtensionConfig({ background }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { hosts } = extensionStorage; + const expectedHost = await extension.awaitMessage("extension-origin"); + ok( + expectedHost in hosts, + "Should have the expected extension host in the extensionStorage store" + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Background page modifies items while storage panel is open. + * - Load extension with background page. + * - Open the add-on debugger storage panel. + * - With the panel still open, from the extension background page: + * - Bulk add storage items + * - Edit the values of some of the storage items + * - Remove some storage items + * - Clear all storage items + * - For each modification, the storage data in the panel should match the + * changes made by the extension. + */ +add_task(async function test_panel_live_updates() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const host = await extension.awaitMessage("extension-origin"); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); + + info("Waiting for extension to bulk add 50 items to storage local"); + const bulkStorageItems = {}; + // limited by MAX_STORE_OBJECT_COUNT in devtools/server/actors/resources/storage/index.js + const numItems = 2; + for (let i = 1; i <= numItems; i++) { + bulkStorageItems[i] = i; + } + + // fireOnChanged avoids the race condition where the extension + // modifies storage then immediately tries to access storage before + // the storage actor has finished updating. + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { + ...bulkStorageItems, + a: 123, + b: [4, 5], + c: { d: 678 }, + d: true, + e: "hi", + f: null, + }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items added by extension match items in extensionStorage store" + ); + const bulkStorageObjects = []; + for (const [name, value] of Object.entries(bulkStorageItems)) { + bulkStorageObjects.push({ + area: "local", + name, + value: { str: String(value) }, + isValueEditable: true, + }); + } + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "[4,5]" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: '{"d":678}' }, + isValueEditable: true, + }, + { + area: "local", + name: "d", + value: { str: "true" }, + isValueEditable: true, + }, + { + area: "local", + name: "e", + value: { str: "hi" }, + isValueEditable: true, + }, + { + area: "local", + name: "f", + value: { str: "null" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to edit a few storage item values"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { + a: ["c", "d"], + b: 456, + c: false, + }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items edited by extension match items in extensionStorage store" + ); + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: '["c","d"]' }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: "false" }, + isValueEditable: true, + }, + { + area: "local", + name: "d", + value: { str: "true" }, + isValueEditable: true, + }, + { + area: "local", + name: "e", + value: { str: "hi" }, + isValueEditable: true, + }, + { + area: "local", + name: "f", + value: { str: "null" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to remove a few storage item values"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-remove", ["d", "e", "f"]); + await extension.awaitMessage("storage-local-remove:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Confirming items removed by extension were removed in extensionStorage store" + ); + data = (await extensionStorage.getStoreObjects(host, null, { sessionString })) + .data; + Assert.deepEqual( + data, + [ + ...bulkStorageObjects, + { + area: "local", + name: "a", + value: { str: '["c","d"]' }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: "false" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + info("Waiting for extension to remove all remaining storage items"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-clear"); + await extension.awaitMessage("storage-local-clear:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info("Confirming extensionStorage store was cleared"); + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: No bg page. Transient page adds item before storage panel opened. + * - Load extension with no background page. + * - Open an extension page in a tab that adds a local storage item. + * - With the extension page still open, open the add-on storage panel. + * - The data in the storage panel should match the items added by the extension. + */ +add_task( + async function test_panel_data_matches_extension_with_transient_page_open() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const url = extension.extension.baseURI.resolve( + "extension_page_in_tab.html" + ); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await contentPage.close(); + await shutdown(extension, commands); + } +); + +/** + * Test case: No bg page. Transient page adds item then closes before storage panel opened. + * - Load extension with no background page. + * - Open an extension page in a tab that adds a local storage item. + * - Close all extension pages. + * - Open the add-on storage panel. + * - The data in the storage panel should match the item added by the extension. + */ +add_task(async function test_panel_data_matches_extension_with_no_pages_open() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + await contentPage.close(); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: No bg page. Storage panel live updates when a transient page adds an item. + * - Load extension with no background page. + * - Open the add-on storage panel. + * - With the storage panel still open, open an extension page in a new tab that adds an + * item. + * - The data in the storage panel should live update to match the item added by the + * extension. + * - If an extension page adds the same data again, the data in the storage panel should + * not change. + */ +add_task( + async function test_panel_data_live_updates_for_extension_without_bg_page() { + const extension = await startupExtension( + getExtensionConfig({ files: ext_no_bg.files }) + ); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const url = extension.extension.baseURI.resolve( + "extension_page_in_tab.html" + ); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [], + "Got the expected results on empty storage.local" + ); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "The results are unchanged when an extension page adds duplicate items" + ); + + await contentPage.close(); + await shutdown(extension, commands); + } +); + +/** + * Test case: Bg page adds item while storage panel is open. Panel edits item's value. + * - Load extension with background page. + * - Open the add-on storage panel. + * - With the storage panel still open, add item from the background page. + * - Edit the value of the item in the storage panel + * - The data in the storage panel should match the item added by the extension. + * - The storage actor is correctly parsing and setting the string representation of + * the value in the storage local database when the item's value is edited in the + * storage panel + */ +add_task( + async function test_editing_items_in_panel_parses_supported_values_correctly() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const oldItem = { a: 123 }; + const key = Object.keys(oldItem)[0]; + const oldValue = oldItem[key]; + // A tuple representing information for a new value entered into the panel for oldItem: + // [ + // value, + // editItem string representation of value, + // toStoreObject string representation of value, + // ] + const valueInfo = [ + [true, "true", "true"], + ["hi", "hi", "hi"], + [456, "456", "456"], + [{ b: 789 }, "{b: 789}", '{"b":789}'], + [[1, 2, 3], "[1, 2, 3]", "[1,2,3]"], + [null, "null", "null"], + ]; + for (const [value, editItemValueStr, toStoreObjectValueStr] of valueInfo) { + info("Setting a storage item through the extension"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", oldItem); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + info( + "Editing the storage item in the panel with a new value of a different type" + ); + // When the user edits an item in the panel, they are entering a string into a + // textbox. This string is parsed by the storage actor's editItem method. + await extensionStorage.editItem({ + host, + field: "value", + items: { name: key, value: editItemValueStr }, + oldValue, + }); + + info( + "Verifying item in the storage actor matches the item edited in the panel" + ); + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: key, + value: { str: toStoreObjectValueStr }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + // The view layer is separate from the database layer; therefore while values are + // stringified (via toStoreObject) for display in the client, the value (and its type) + // in the database is unchanged. + info( + "Verifying the expected new value matches the value fetched in the extension" + ); + extension.sendMessage("storage-local-get", key); + const extItem = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + value, + extItem[key], + `The string value ${editItemValueStr} was correctly parsed to ${value}` + ); + } + + await shutdown(extension, commands); + } +); + +/** + * Test case: Modifying storage items from the panel update extension storage local data. + * - Load extension with background page. + * - Open the add-on storage panel. From the panel: + * - Edit the value of a storage item, + * - Remove a storage item, + * - Remove all of the storage items, + * - For each modification, the storage data retrieved by the extension should match the + * data in the panel. + */ +add_task( + async function test_modifying_items_in_panel_updates_extension_storage_data() { + const extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const DEFAULT_VALUE = "value"; // global in devtools/server/actors/resources/storage/index.js + let items = { + guid_1: DEFAULT_VALUE, + guid_2: DEFAULT_VALUE, + guid_3: DEFAULT_VALUE, + }; + + info("Adding storage items from the extension"); + let storesUpdate = extensionStorage.once("single-store-update"); + extension.sendMessage("storage-local-set", items); + await extension.awaitMessage("storage-local-set:done"); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + let data = await storesUpdate; + Assert.deepEqual( + { + added: { + extensionStorage: { + [host]: ["guid_1", "guid_2", "guid_3"], + }, + }, + changed: undefined, + deleted: undefined, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + info("Waiting for panel to edit some items"); + storesUpdate = extensionStorage.once("single-store-update"); + await extensionStorage.editItem({ + host, + field: "value", + items: { name: "guid_1", value: "anotherValue" }, + DEFAULT_VALUE, + }); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + data = await storesUpdate; + Assert.deepEqual( + { + added: undefined, + changed: { + extensionStorage: { + [host]: ["guid_1"], + }, + }, + deleted: undefined, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + items = { + guid_1: "anotherValue", + guid_2: DEFAULT_VALUE, + guid_3: DEFAULT_VALUE, + }; + extension.sendMessage("storage-local-get", Object.keys(items)); + let extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + info("Waiting for panel to remove an item"); + storesUpdate = extensionStorage.once("single-store-update"); + await extensionStorage.removeItem(host, "guid_3"); + + info("Waiting for the storage actor to emit a 'stores-update' event"); + data = await storesUpdate; + Assert.deepEqual( + { + added: undefined, + changed: undefined, + deleted: { + extensionStorage: { + [host]: ["guid_3"], + }, + }, + }, + data, + "The change data from the storage actor's 'stores-update' event matches the changes made in the client." + ); + + items = { + guid_1: "anotherValue", + guid_2: DEFAULT_VALUE, + }; + extension.sendMessage("storage-local-get", Object.keys(items)); + extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + info("Waiting for panel to remove all items"); + const storesCleared = extensionStorage.once("single-store-cleared"); + await extensionStorage.removeAll(host); + + info("Waiting for the storage actor to emit a 'stores-cleared' event"); + data = await storesCleared; + Assert.deepEqual( + { + clearedHostsOrPaths: { + [host]: [], + }, + }, + data, + "The change data from the storage actor's 'stores-cleared' event matches the changes made in the client." + ); + + items = {}; + extension.sendMessage("storage-local-get", Object.keys(items)); + extItems = await extension.awaitMessage("storage-local-get:done"); + Assert.deepEqual( + items, + extItems, + `The storage items in the extension match the items in the panel` + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Storage panel shows extension storage data added prior to extension startup + * - Load extension that adds a storage item + * - Uninstall the extension + * - Reinstall the extension + * - Open the add-on storage panel. + * - The data in the storage panel should match the data added the first time the extension + * was installed + * Related test case: Storage panel shows extension storage data when an extension that has + * already migrated to the IndexedDB storage backend prior to extension startup adds + * another storage item. + * - (Building from previous steps) + * - The reinstalled extension adds a storage item + * - The data in the storage panel should live update with both items: the item added from + * the first and the item added from the reinstall. + */ +add_task( + async function test_panel_data_matches_data_added_prior_to_ext_startup() { + // The pref to leave the addonid->uuid mapping around after uninstall so that we can + // re-attach to the same storage + Services.prefs.setBoolPref(LEAVE_UUID_PREF, true); + + // The pref to prevent cleaning up storage on uninstall + Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, true); + + let extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + const host = await extension.awaitMessage("extension-origin"); + + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + await shutdown(extension); + + // Reinstall the same extension + extension = await startupExtension( + getExtensionConfig({ background: extensionScriptWithMessageListener }) + ); + + await extension.awaitMessage("extension-origin"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + // Related test case + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { b: 456 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + + data = ( + await extensionStorage.getStoreObjects(host, null, { sessionString }) + ).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + { + area: "local", + name: "b", + value: { str: "456" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false); + Services.prefs.setBoolPref(LEAVE_UUID_PREF, false); + + await shutdown(extension, commands); + } +); + +add_task( + function cleanup_for_test_panel_data_matches_data_added_prior_to_ext_startup() { + Services.prefs.clearUserPref(LEAVE_UUID_PREF); + Services.prefs.clearUserPref(LEAVE_STORAGE_PREF); + } +); + +/** + * Test case: Transient page adds an item to storage. With storage panel open, + * reload extension. + * - Load extension with no background page. + * - Open transient page that adds a storage item on message. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item added prior to reloading. + */ +add_task(async function test_panel_live_reload_for_extension_without_bg_page() { + const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ + manifest, + files: ext_no_bg.files, + }) + ); + + info("Opening extension page in a tab"); + const url = extension.extension.baseURI.resolve("extension_page_in_tab.html"); + const contentPage = await ExtensionTestUtils.loadContentPage(url, { + extension, + }); + + const host = await extension.awaitMessage("extension-origin"); + + info("Waiting for extension page in a tab to add storage item"); + extension.sendMessage("storage-local-fireOnChanged"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + await extension.awaitMessage("storage-local-onChanged"); + await contentPage.close(); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Updating extension to version 2.0"); + await extension.upgrade( + getExtensionConfig({ + manifest, + files: ext_no_bg.files, + }) + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); + +/** + * Test case: Bg page auto adds item(s). With storage panel open, reload extension. + * - Load extension with background page that automatically adds a storage item on startup. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item(s) added by the reloaded + * extension. + */ +add_task( + async function test_panel_live_reload_when_extension_auto_adds_items() { + async function background() { + await browser.storage.local.set({ a: { b: 123 }, c: { d: 456 } }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + await extension.upgrade( + getExtensionConfig({ + manifest, + background, + }) + ); + + await extension.awaitMessage("extension-origin"); + + const { data } = await extensionStorage.getStoreObjects(host, null, { + sessionString, + }); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: '{"b":123}' }, + isValueEditable: true, + }, + { + area: "local", + name: "c", + value: { str: '{"d":456}' }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); + } +); + +/** + * Test case: Bg page adds one storage.local item and one storage.sync item. + * - Load extension with background page that automatically adds two storage items on startup. + * - Open the add-on storage panel. + * - Assert that only the storage.local item is shown in the panel. + */ +add_task( + async function test_panel_data_only_updates_for_storage_local_changes() { + async function background() { + await browser.storage.local.set({ a: { b: 123 } }); + await browser.storage.sync.set({ c: { d: 456 } }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); + } + + // Using the storage.sync API requires a non-temporary extension ID, see Bug 1323228. + const EXTENSION_ID = + "test_panel_data_only_updates_for_storage_local_changes@xpcshell.mozilla.org"; + const manifest = { + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading and starting extension"); + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Opening storage panel"); + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: '{"b":123}' }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); + } +); + +// This test verifies that Bug 1802929 fix doesn't regress. +add_task(async function test_live_update_with_no_extension_listener() { + const EXTENSION_ID = "test_with_no_listeners@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + function background() { + browser.test.onMessage.addListener(async (msg, ...args) => { + if (msg !== "storage-local-api-call") { + browser.test.fail(`Got unexpected test message: ${msg}`); + return; + } + + const [{ method, methodArgs }] = args; + const res = await browser.storage.local[method](...methodArgs); + browser.test.sendMessage(`${msg}:done`, res); + }); + } + + const extension = await startupExtension( + getExtensionConfig({ manifest, background }) + ); + + const { target, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + const { baseURI } = extension.extension; + const host = `${baseURI.scheme}://${baseURI.host}`; + + let { data } = await extensionStorage.getStoreObjects(host); + Assert.deepEqual(data, [], "Got the expected results on empty storage.local"); + + async function testStorageLocalUpdate(storageValue) { + info("Store extension data"); + await extension.sendMessage("storage-local-api-call", { + method: "set", + methodArgs: [{ storageKeyName: storageValue }], + }); + await extension.awaitMessage("storage-local-api-call:done"); + + info("Verify stored extension data"); + await extension.sendMessage("storage-local-api-call", { + method: "get", + methodArgs: [], + }); + + Assert.deepEqual( + await extension.awaitMessage("storage-local-api-call:done"), + { storageKeyName: storageValue }, + "Got the expected value from browser.storage.local.get" + ); + + await TestUtils.waitForCondition(async () => { + const res = await extensionStorage.getStoreObjects(host); + return res.data?.length > 0; + }, "Wait for the extension storage panel updates"); + + data = (await extensionStorage.getStoreObjects(host)).data; + Assert.deepEqual( + data, + [ + { + area: "local", + name: "storageKeyName", + value: { str: `${storageValue}` }, + isValueEditable: true, + }, + ], + "Expected DevTools Storage panel data to have been updated" + ); + } + + await testStorageLocalUpdate("aStorageValue 01"); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + await extension.upgrade(getExtensionConfig({ manifest, background })); + + await testStorageLocalUpdate("aStorageValue 02"); + + await shutdown(extension, target); +}); diff --git a/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js b/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js new file mode 100644 index 0000000000..5d2285b9e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_extension_storage_actor_upgrade.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Note: this test used to be in test_extension_storage_actor.js, but seems to + * fail frequently as soon as we start auto-attaching targets. + * See Bug 1618059. + */ + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { + createMissingIndexedDBDirs, + extensionScriptWithMessageListener, + getExtensionConfig, + openAddonStoragePanel, + shutdown, + startupExtension, +} = require("resource://test/webextension-helpers.js"); + +const l10n = new Localization(["devtools/client/storage.ftl"], true); +const sessionString = l10n.formatValueSync("storage-expires-session"); + +// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed. +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); +PromiseTestUtils.allowMatchingRejectionsGlobally( + /Message manager disconnected/ +); + +const { createAppInfo, promiseStartupManager } = AddonTestUtils; + +AddonTestUtils.init(this); +createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42"); + +ExtensionTestUtils.init(this); + +add_task(async function setup() { + await promiseStartupManager(); + const dir = createMissingIndexedDBDirs(); + + Assert.ok( + dir.exists(), + "Should have a 'storage/permanent' dir in the profile dir" + ); +}); + +/** + * Test case: Bg page adds an item to storage. With storage panel open, reload extension. + * - Load extension with background page that adds a storage item on message. + * - Open the add-on storage panel. + * - With the storage panel still open, reload the extension. + * - The data in the storage panel should match the item added prior to reloading. + */ +add_task(async function test_panel_live_reload() { + const EXTENSION_ID = "test_panel_live_reload@xpcshell.mozilla.org"; + let manifest = { + version: "1.0", + browser_specific_settings: { + gecko: { + id: EXTENSION_ID, + }, + }, + }; + + info("Loading extension version 1.0"); + const extension = await startupExtension( + getExtensionConfig({ + manifest, + background: extensionScriptWithMessageListener, + }) + ); + + info("Waiting for message from test extension"); + const host = await extension.awaitMessage("extension-origin"); + + info("Adding storage item"); + extension.sendMessage("storage-local-set", { a: 123 }); + await extension.awaitMessage("storage-local-set:done"); + + const { commands, extensionStorage } = await openAddonStoragePanel( + extension.id + ); + + manifest = { + ...manifest, + version: "2.0", + }; + // "Reload" is most similar to an upgrade, as e.g. storage data is preserved + info("Update to version 2.0"); + + // Wait for the storage front to receive an event for the storage panel refresh + // when the extension has been reloaded. + const promiseStoragePanelUpdated = new Promise(resolve => { + extensionStorage.on( + "single-store-update", + function updateListener(updates) { + info(`Got stores-update event: ${JSON.stringify(updates)}`); + const extStorageAdded = updates.added?.extensionStorage; + if (host in extStorageAdded && extStorageAdded[host].length) { + extensionStorage.off("single-store-update", updateListener); + resolve(); + } + } + ); + }); + + await extension.upgrade( + getExtensionConfig({ + manifest, + background: extensionScriptWithMessageListener, + }) + ); + + await Promise.all([ + extension.awaitMessage("extension-origin"), + promiseStoragePanelUpdated, + ]); + + const { data } = await extensionStorage.getStoreObjects(host, null, { + sessionString, + }); + Assert.deepEqual( + data, + [ + { + area: "local", + name: "a", + value: { str: "123" }, + isValueEditable: true, + }, + ], + "Got the expected results on populated storage.local" + ); + + await shutdown(extension, commands); +}); diff --git a/devtools/server/tests/xpcshell/test_format_command.js b/devtools/server/tests/xpcshell/test_format_command.js new file mode 100644 index 0000000000..529e560a0f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_format_command.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + formatCommand, +} = require("resource://devtools/server/actors/webconsole/commands/parser.js"); + +const testcases = [ + { input: ":help", expectedOutput: "help()" }, + { + input: ":screenshot --fullscreen", + expectedOutput: 'screenshot({"fullscreen":true})', + }, + { + input: ":screenshot --fullscreen true", + expectedOutput: 'screenshot({"fullscreen":true})', + }, + { input: ":screenshot ", expectedOutput: "screenshot()" }, + { + input: ":screenshot --dpr 0.5 --fullpage --chrome", + expectedOutput: 'screenshot({"dpr":0.5,"fullpage":true,"chrome":true})', + }, + { + input: ":screenshot 'filename'", + expectedOutput: 'screenshot({"filename":"filename"})', + }, + { + input: ":screenshot filename", + expectedOutput: 'screenshot({"filename":"filename"})', + }, + { + input: + ":screenshot --name 'filename' --name `filename` --name \"filename\"", + expectedOutput: 'screenshot({"name":["filename","filename","filename"]})', + }, + { + input: ":screenshot 'filename1' 'filename2' 'filename3'", + expectedOutput: 'screenshot({"filename":"filename1"})', + }, + { + input: ":screenshot --chrome --chrome", + expectedOutput: 'screenshot({"chrome":true})', + }, + { + input: ':screenshot "file name with spaces"', + expectedOutput: 'screenshot({"filename":"file name with spaces"})', + }, + { + input: ":screenshot 'filename1' --name 'filename2'", + expectedOutput: 'screenshot({"filename":"filename1","name":"filename2"})', + }, + { + input: ":screenshot --name 'filename1' 'filename2'", + expectedOutput: 'screenshot({"name":"filename1","filename":"filename2"})', + }, + { + input: ':screenshot "fo\\"o bar"', + expectedOutput: 'screenshot({"filename":"fo\\\\\\"o bar"})', + }, + { + input: ':screenshot "foo b\\"ar"', + expectedOutput: 'screenshot({"filename":"foo b\\\\\\"ar"})', + }, +]; + +const edgecases = [ + { input: ":", expectedError: /'' is not a valid command/ }, + { input: ":invalid", expectedError: /'invalid' is not a valid command/ }, + { input: ":screenshot :help", expectedError: /Invalid command/ }, + { input: ":screenshot --", expectedError: /invalid flag/ }, + { + input: ':screenshot "fo"o bar', + expectedError: + /String has unescaped `"` in \["fo"o\.\.\.\], may miss a space between arguments/, + }, + { + input: ':screenshot "foo b"ar', + expectedError: + // eslint-disable-next-line max-len + /String has unescaped `"` in \["foo b"ar\.\.\.\], may miss a space between arguments/, + }, + { input: ": screenshot", expectedError: /'' is not a valid command/ }, + { + input: ':screenshot "file name', + expectedError: /String does not terminate/, + }, + { + input: ':screenshot "file name --clipboard', + expectedError: /String does not terminate before flag "clipboard"/, + }, + { + input: "::screenshot", + expectedError: /':screenshot' is not a valid command/, + }, +]; + +function run_test() { + testcases.forEach(testcase => { + Assert.equal(formatCommand(testcase.input), testcase.expectedOutput); + }); + + edgecases.forEach(testcase => { + Assert.throws( + () => formatCommand(testcase.input), + testcase.expectedError, + `"${testcase.input}" should throw expected error` + ); + }); +} diff --git a/devtools/server/tests/xpcshell/test_forwardingprefix.js b/devtools/server/tests/xpcshell/test_forwardingprefix.js new file mode 100644 index 0000000000..e917350da5 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_forwardingprefix.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* Exercise prefix-based forwarding of packets to other transports. */ + +const { RootActor } = require("resource://devtools/server/actors/root.js"); + +var gMainConnection, gMainTransport; +var gSubconnection1, gSubconnection2; +var gClient; + +function run_test() { + DevToolsServer.init(); + + add_test(createMainConnection); + add_test(TestNoForwardingYet); + add_test(createSubconnection1); + add_test(TestForwardPrefix1OnlyRoot); + add_test(createSubconnection2); + add_test(TestForwardPrefix12OnlyRoot); + add_test(TestForwardPrefix12WithActor1); + add_test(TestForwardPrefix12WithActor12); + run_next_test(); +} + +/* + * Create a pipe connection, and return an object |{ conn, transport }|, + * where |conn| is the new DevToolsServerConnection instance, and + * |transport| is the client side of the transport on which it communicates + * (that is, packets sent on |transport| go to the new connection, and + * |transport|'s hooks receive replies). + * + * |prefix| is optional; if present, it's the prefix (minus the '/') for + * actors in the new connection. + */ +function newConnection(prefix) { + let conn; + DevToolsServer.createRootActor = function (connection) { + conn = connection; + return new RootActor(connection, {}); + }; + + const transport = DevToolsServer.connectPipe(prefix); + + return { conn, transport }; +} + +/* Create the main connection for these tests. */ +function createMainConnection() { + ({ conn: gMainConnection, transport: gMainTransport } = newConnection()); + gClient = new DevToolsClient(gMainTransport); + gClient.connect().then(([type, traits]) => run_next_test()); +} + +/* + * Exchange 'echo' messages with five actors: + * - root + * - prefix1/root + * - prefix1/actor + * - prefix2/root + * - prefix2/actor + * + * Expect proper echos from those named in |reachables|, and 'noSuchActor' + * errors from the others. When we've gotten all our replies (errors or + * otherwise), call |completed|. + * + * To avoid deep stacks, we call completed from the next tick. + */ +async function tryActors(reachables, completed) { + for (const actor of [ + "root", + "prefix1/root", + "prefix1/actor", + "prefix2/root", + "prefix2/actor", + ]) { + let response; + try { + if (actor.endsWith("root")) { + // Root actor doesn't expose any echo method, + // so fallback on getRoot which returns `{ from: "root" }`. + // For the top level root actor, we have to use its front. + if (actor == "root") { + response = await gClient.mainRoot.getRoot(); + } else { + response = await gClient.request({ to: actor, type: "getRoot" }); + } + } else { + response = await gClient.request({ + to: actor, + type: "echo", + value: "tango", + }); + } + } catch (e) { + response = e; + } + if (reachables.has(actor)) { + if (actor.endsWith("root")) { + // RootActor's getRoot response is almost empty on xpcshell + Assert.deepEqual({ from: actor }, response); + } else { + Assert.deepEqual( + { from: actor, to: actor, type: "echo", value: "tango" }, + response + ); + } + } else { + Assert.deepEqual( + { + from: actor, + error: "noSuchActor", + message: "No such actor for ID: " + actor, + }, + response + ); + } + } + executeSoon(completed, "tryActors callback " + completed.name); +} + +/* + * With no forwarding established, sending messages to root should work, + * but sending messages to prefixed actor names, or anyone else, should get + * an error. + */ +function TestNoForwardingYet() { + tryActors(new Set(["root"]), run_next_test); +} + +/* + * Create a new pipe connection which forwards its reply packets to + * gMainConnection's client, and to which gMainConnection forwards packets + * directed to actors whose names begin with |prefix + '/'|, and. + * + * Return an object { conn, transport }, as for newConnection. + */ +function newSubconnection(prefix) { + const { conn, transport } = newConnection(prefix); + transport.hooks = { + onPacket: packet => gMainConnection.send(packet), + }; + gMainConnection.setForwarding(prefix, transport); + + return { conn, transport }; +} + +/* Create a second root actor, to which we can forward things. */ +function createSubconnection1() { + const { conn, transport } = newSubconnection("prefix1"); + gSubconnection1 = conn; + transport.ready(); + gClient.expectReply("prefix1/root", reply => run_next_test()); +} + +// Establish forwarding, but don't put any actors in that server. +function TestForwardPrefix1OnlyRoot() { + tryActors(new Set(["root", "prefix1/root"]), run_next_test); +} + +/* Create a third root actor, to which we can forward things. */ +function createSubconnection2() { + const { conn, transport } = newSubconnection("prefix2"); + gSubconnection2 = conn; + transport.ready(); + gClient.expectReply("prefix2/root", reply => run_next_test()); +} + +function TestForwardPrefix12OnlyRoot() { + tryActors(new Set(["root", "prefix1/root", "prefix2/root"]), run_next_test); +} + +// A dumb actor that implements 'echo'. +// +// It's okay that both subconnections' actors behave identically, because +// the reply-sending code attaches the replying actor's name to the packet, +// so simply matching the 'from' field in the reply ensures that we heard +// from the right actor. +const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); +class EchoActor extends Actor { + constructor(conn) { + super(conn, { typeName: "EchoActor", methods: [] }); + + this.requestTypes = { + echo: EchoActor.prototype.onEcho, + }; + } + + onEcho(request) { + /* + * Request packets are frozen. Copy request, so that + * DevToolsServerConnection.onPacket can attach a 'from' property. + */ + return JSON.parse(JSON.stringify(request)); + } +} + +function TestForwardPrefix12WithActor1() { + const actor = new EchoActor(gSubconnection1); + actor.actorID = "prefix1/actor"; + gSubconnection1.addActor(actor); + + tryActors( + new Set(["root", "prefix1/root", "prefix1/actor", "prefix2/root"]), + run_next_test + ); +} + +function TestForwardPrefix12WithActor12() { + const actor = new EchoActor(gSubconnection2); + actor.actorID = "prefix2/actor"; + gSubconnection2.addActor(actor); + + tryActors( + new Set([ + "root", + "prefix1/root", + "prefix1/actor", + "prefix2/root", + "prefix2/actor", + ]), + run_next_test + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-01.js b/devtools/server/tests/xpcshell/test_frameactor-01.js new file mode 100644 index 0000000000..18c75d0abe --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-01.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that we get a frame actor along with a debugger statement. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.ok(!!packet.frame); + Assert.ok(!!packet.frame.getActorByID); + Assert.equal(packet.frame.displayName, "stopMe"); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + } + stopMe(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-02.js b/devtools/server/tests/xpcshell/test_frameactor-02.js new file mode 100644 index 0000000000..9529d2f324 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-02.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that two pauses in a row will keep the same frame actor. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const packet2 = await resumeAndWaitForPause(threadFront); + + Assert.equal(packet1.frame.actor, packet2.frame.actor); + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + debugger; + } + stopMe(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-03.js b/devtools/server/tests/xpcshell/test_frameactor-03.js new file mode 100644 index 0000000000..7feecd14e0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-03.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that a frame actor is properly expired when the frame goes away. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + const frameActorID = packet.frame.actorID; + { + const { frames } = await threadFront.getFrames(0, null); + ok( + frames.some(f => f.actorID === frameActorID), + "The paused frame is returned by getFrames" + ); + + Assert.equal(frames.length, 3, "Thread front has 3 frames"); + } + + await resumeAndWaitForPause(threadFront); + await checkFramesLength(threadFront, 2); + { + const { frames } = await threadFront.getFrames(0, null); + ok( + !frames.some(f => f.actorID === frameActorID), + "The paused frame is no longer returned by getFrames" + ); + + Assert.equal(frames.length, 2, "Thread front has 2 frames"); + } + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + } + stopMe(); + debugger; + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-04.js b/devtools/server/tests/xpcshell/test_frameactor-04.js new file mode 100644 index 0000000000..200ee9968d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-04.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify the "frames" request on the thread. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const response = await threadFront.getFrames(0, 1000); + for (let i = 0; i < response.frames.length; i++) { + const expected = frameFixtures[i]; + const actual = response.frames[i]; + + Assert.equal( + expected.displayname, + actual.displayname, + "Frame displayname" + ); + Assert.equal(expected.type, actual.type, "Frame displayname"); + } + + await threadFront.resume(); + }) +); + +var frameFixtures = [ + // Function calls... + { type: "call", displayName: "depth3" }, + { type: "call", displayName: "depth2" }, + { type: "call", displayName: "depth1" }, + + // Anonymous function call in our eval... + { type: "call", displayName: undefined }, + + // The eval itself. + { type: "eval", displayName: "(eval)" }, +]; + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function depth3() { + debugger; + } + function depth2() { + depth3(); + } + function depth1() { + depth2(); + } + depth1(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor-05.js b/devtools/server/tests/xpcshell/test_frameactor-05.js new file mode 100644 index 0000000000..90456191e7 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor-05.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + await checkFramesLength(threadFront, 5); + + await resumeAndWaitForPause(threadFront); + await checkFramesLength(threadFront, 2); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function depth3() { + debugger; + } + function depth2() { + depth3(); + } + function depth1() { + depth2(); + } + depth1(); + debugger; + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js b/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js new file mode 100644 index 0000000000..5967e8a086 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_frameactor_wasm-01.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that wasm frame(s) can be requested from the client. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await threadFront.reconfigure({ + observeAsmJS: true, + observeWasm: true, + }); + + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const frameResponse = await threadFront.getFrames(0, null); + + Assert.equal(frameResponse.frames.length, 4); + + const wasmFrame = frameResponse.frames[1]; + Assert.equal(wasmFrame.type, "wasmcall"); + Assert.equal(wasmFrame.this, undefined); + + const location = wasmFrame.where; + const source = await getSourceById(threadFront, location.actor); + Assert.equal(location.line > 0, true); + Assert.equal(location.column > 0, true); + Assert.equal(/^wasm:(?:[^:]*:)*?[0-9a-f]{16}$/.test(source.url), true); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable comma-spacing, max-len */ + debuggee.eval( + "(" + + function () { + // WebAssembly bytecode was generated by running: + // js -e 'print(wasmTextToBinary("(module(import \"a\" \"b\")(func(export \"c\")call 0))"))' + const m = new WebAssembly.Module( + new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 132, 128, 128, 128, 0, 1, 96, 0, 0, + 2, 135, 128, 128, 128, 0, 1, 1, 97, 1, 98, 0, 0, 3, 130, 128, 128, + 128, 0, 1, 0, 6, 129, 128, 128, 128, 0, 0, 7, 133, 128, 128, 128, 0, + 1, 1, 99, 0, 1, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, + 0, 16, 0, 11, + ]) + ); + const i = new WebAssembly.Instance(m, { + a: { + b: () => { + debugger; + }, + }, + }); + i.exports.c(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framearguments-01.js b/devtools/server/tests/xpcshell/test_framearguments-01.js new file mode 100644 index 0000000000..524d43f58c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framearguments-01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check a frame actor's arguments property. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + Assert.equal(args.length, 6); + Assert.equal(args[0], 42); + Assert.equal(args[1], true); + Assert.equal(args[2], "nasu"); + Assert.equal(args[3].type, "null"); + Assert.equal(args[4].type, "undefined"); + Assert.equal(args[5].type, "object"); + Assert.equal(args[5].class, "Object"); + Assert.ok(!!args[5].actor); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(number, bool, string, null_, undef, object) { + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-01.js b/devtools/server/tests/xpcshell/test_framebindings-01.js new file mode 100644 index 0000000000..ecf6f02e97 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-01.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check a frame actor's bindings property. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const bindings = environment.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + + Assert.equal(args.length, 6); + Assert.equal(args[0].number.value, 42); + Assert.equal(args[1].bool.value, true); + Assert.equal(args[2].string.value, "nasu"); + Assert.equal(args[3].null_.value.type, "null"); + Assert.equal(args[4].undef.value.type, "undefined"); + Assert.equal(args[5].object.value.type, "object"); + Assert.equal(args[5].object.value.class, "Object"); + Assert.ok(!!args[5].object.value.actor); + + Assert.equal(vars.a.value, 1); + Assert.equal(vars.b.value, true); + Assert.equal(vars.c.value.type, "object"); + Assert.equal(vars.c.value.class, "Object"); + Assert.ok(!!vars.c.value.actor); + + const objClient = threadFront.pauseGrip(vars.c.value); + const response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.a.configurable, true); + Assert.equal(response.ownProperties.a.enumerable, true); + Assert.equal(response.ownProperties.a.writable, true); + Assert.equal(response.ownProperties.a.value, "a"); + + Assert.equal(response.ownProperties.b.configurable, true); + Assert.equal(response.ownProperties.b.enumerable, true); + Assert.equal(response.ownProperties.b.writable, true); + Assert.equal(response.ownProperties.b.value.type, "undefined"); + Assert.equal(false, "class" in response.ownProperties.b.value); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number, bool, string, null_, undef, object) { + var a = 1; + var b = true; + var c = { a: "a", b: undefined }; + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-02.js b/devtools/server/tests/xpcshell/test_framebindings-02.js new file mode 100644 index 0000000000..48c243193b --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-02.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check a frame actor's parent bindings. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + let parentEnv = environment.parent; + const bindings = parentEnv.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + Assert.notEqual(parentEnv, undefined); + Assert.equal(args.length, 0); + Assert.equal(vars.stopMe.value.type, "object"); + Assert.equal(vars.stopMe.value.class, "Function"); + Assert.ok(!!vars.stopMe.value.actor); + + // Skip the global lexical scope. + parentEnv = parentEnv.parent.parent; + Assert.notEqual(parentEnv, undefined); + const objClient = threadFront.pauseGrip(parentEnv.object); + const response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.Object.value.getGrip().type, "object"); + Assert.equal( + response.ownProperties.Object.value.getGrip().class, + "Function" + ); + Assert.ok(!!response.ownProperties.Object.value.actorID); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number, bool, string, null_, undef, object) { + var a = 1; + var b = true; + var c = { a: "a" }; + eval(""); + debugger; + } + stopMe(42, true, "nasu", null, undefined, { foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-03.js b/devtools/server/tests/xpcshell/test_framebindings-03.js new file mode 100644 index 0000000000..46dc777ef1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-03.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* strict mode code may not contain 'with' statements */ +/* eslint-disable strict */ + +/** + * Check a |with| frame actor's bindings. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + Assert.notEqual(env, undefined); + + const parentEnv = env.parent; + Assert.notEqual(parentEnv, undefined); + + const bindings = parentEnv.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + Assert.equal(args.length, 1); + Assert.equal(args[0].number.value, 10); + Assert.equal(vars.r.value, 10); + Assert.equal(vars.a.value, Math.PI * 100); + Assert.equal(vars.arguments.value.class, "Arguments"); + Assert.ok(!!vars.arguments.value.actor); + + const objClient = threadFront.pauseGrip(env.object); + const response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.PI.value, Math.PI); + Assert.equal(response.ownProperties.cos.value.getGrip().type, "object"); + Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function"); + Assert.ok(!!response.ownProperties.cos.value.actorID); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number) { + var a; + var r = number; + with (Math) { + a = PI * r * r; + debugger; + } + } + stopMe(10); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-04.js b/devtools/server/tests/xpcshell/test_framebindings-04.js new file mode 100644 index 0000000000..1e3cc1485c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-04.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* strict mode code may not contain 'with' statements */ +/* eslint-disable strict */ + +/** + * Check the environment bindings of a |with| within a |with|. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + Assert.notEqual(env, undefined); + + const objClient = threadFront.pauseGrip(env.object); + let response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.one.value, 1); + Assert.equal(response.ownProperties.two.value, 2); + Assert.equal(response.ownProperties.foo, undefined); + + let parentEnv = env.parent; + Assert.notEqual(parentEnv, undefined); + + const parentClient = threadFront.pauseGrip(parentEnv.object); + response = await parentClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.PI.value, Math.PI); + Assert.equal(response.ownProperties.cos.value.getGrip().type, "object"); + Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function"); + Assert.ok(!!response.ownProperties.cos.value.actorID); + + parentEnv = parentEnv.parent; + Assert.notEqual(parentEnv, undefined); + + const bindings = parentEnv.bindings; + const args = bindings.arguments; + const vars = bindings.variables; + Assert.equal(args.length, 1); + Assert.equal(args[0].number.value, 10); + Assert.equal(vars.r.value, 10); + Assert.equal(vars.a.value, Math.PI * 100); + Assert.equal(vars.arguments.value.class, "Arguments"); + Assert.ok(!!vars.arguments.value.actor); + Assert.equal(vars.foo.value, 2 * Math.PI); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(number) { + var a, + obj = { one: 1, two: 2 }; + var r = number; + with (Math) { + a = PI * r * r; + with (obj) { + var foo = two * PI; + debugger; + } + } + } + stopMe(10); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-05.js b/devtools/server/tests/xpcshell/test_framebindings-05.js new file mode 100644 index 0000000000..6206fe8668 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-05.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check the environment bindings of a |with| in global scope. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + Assert.notEqual(env, undefined); + + const objClient = threadFront.pauseGrip(env.object); + let response = await objClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.PI.value, Math.PI); + Assert.equal(response.ownProperties.cos.value.getGrip().type, "object"); + Assert.equal(response.ownProperties.cos.value.getGrip().class, "Function"); + Assert.ok(!!response.ownProperties.cos.value.actorID); + + // Skip the global lexical scope. + const parentEnv = env.parent.parent; + Assert.notEqual(parentEnv, undefined); + + const parentClient = threadFront.pauseGrip(parentEnv.object); + response = await parentClient.getPrototypeAndProperties(); + Assert.equal(response.ownProperties.a.value, Math.PI * 100); + Assert.equal(response.ownProperties.r.value, 10); + Assert.equal(response.ownProperties.Object.value.getGrip().type, "object"); + Assert.equal( + response.ownProperties.Object.value.getGrip().class, + "Function" + ); + Assert.ok(!!response.ownProperties.Object.value.actorID); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "var a, r = 10;\n" + + "with (Math) {\n" + + " a = PI * r * r;\n" + + " debugger;\n" + + "}" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-06.js b/devtools/server/tests/xpcshell/test_framebindings-06.js new file mode 100644 index 0000000000..52ab0cfe7c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-06.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const env = await packet.frame.getEnvironment(); + equal(env.type, "function"); + equal(env.function.displayName, "banana3"); + let parent = env.parent; + equal(parent.type, "block"); + ok("banana3" in parent.bindings.variables); + parent = parent.parent; + equal(parent.type, "function"); + equal(parent.function.displayName, "banana2"); + parent = parent.parent; + equal(parent.type, "block"); + ok("banana2" in parent.bindings.variables); + parent = parent.parent; + equal(parent.type, "function"); + equal(parent.function.displayName, "banana"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "function banana(x) {\n" + + " return function banana2(y) {\n" + + " return function banana3(z) {\n" + + ' eval("");\n' + + " debugger;\n" + + " };\n" + + " };\n" + + "}\n" + + "banana('x')('y')('z');\n" + ); +} diff --git a/devtools/server/tests/xpcshell/test_framebindings-07.js b/devtools/server/tests/xpcshell/test_framebindings-07.js new file mode 100644 index 0000000000..77d43dfba8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_framebindings-07.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + Assert.equal(environment.type, "function"); + Assert.equal(environment.bindings.arguments[0].z.value, "z"); + + const parent = environment.parent; + Assert.equal(parent.type, "block"); + Assert.equal(parent.bindings.variables.banana3.value.class, "Function"); + + const grandpa = parent.parent; + Assert.equal(grandpa.type, "function"); + Assert.equal(grandpa.bindings.arguments[0].y.value, "y"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + "function banana(x) {\n" + + " return function banana2(y) {\n" + + " return function banana3(z) {\n" + + ' eval("");\n' + + " debugger;\n" + + " };\n" + + " };\n" + + "}\n" + + "banana('x')('y')('z');\n" + ); +} diff --git a/devtools/server/tests/xpcshell/test_front_destroy.js b/devtools/server/tests/xpcshell/test_front_destroy.js new file mode 100644 index 0000000000..33e2ac827a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_front_destroy.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that fronts throw errors if they are called after being destroyed. + */ + +"use strict"; + +// HACK: ServiceWorkerManager requires the "profile-change-teardown" to cleanly +// shutdown, and setting _profileInitialized to `true` will trigger those +// notifications (see /testing/xpcshell/head.js). +// eslint-disable-next-line no-undef +_profileInitialized = true; + +add_task(async function test() { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + info("Create and connect the DevToolsClient"); + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + + info("Get the device front and check calling getDescription() on it"); + const front = await client.mainRoot.getFront("device"); + const description = await front.getDescription(); + ok( + !!description, + "Check that the getDescription() method returns a valid response." + ); + + info("Destroy the device front and try calling getDescription again"); + front.destroy(); + Assert.throws( + () => front.getDescription(), + /Can not send request 'getDescription' because front 'device' is already destroyed\./, + "Check device front throws when getDescription() is called after destroy()" + ); + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_functiongrips-01.js b/devtools/server/tests/xpcshell/test_functiongrips-01.js new file mode 100644 index 0000000000..5abce26875 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_functiongrips-01.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + // Test named function + function evalCode() { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval("stopMe(stopMe)"); + } + + const packet1 = await executeOnNextTickAndWaitForPause( + () => evalCode(), + threadFront + ); + + const args1 = packet1.frame.arguments; + + Assert.equal(args1[0].class, "Function"); + Assert.equal(args1[0].name, "stopMe"); + Assert.equal(args1[0].displayName, "stopMe"); + + await threadFront.resume(); + + // Test inferred name function + const packet2 = await executeOnNextTickAndWaitForPause( + () => + debuggee.eval( + "var o = { m: function(foo, bar, baz) { } }; stopMe(o.m)" + ), + threadFront + ); + + const args2 = packet2.frame.arguments; + + Assert.equal(args2[0].class, "Function"); + // No name for an anonymous function, but it should have an inferred name. + Assert.equal(args2[0].name, undefined); + Assert.equal(args2[0].displayName, "m"); + + await threadFront.resume(); + + // Test anonymous function + const packet3 = await executeOnNextTickAndWaitForPause( + () => debuggee.eval("stopMe(function(foo, bar, baz) { })"), + threadFront + ); + + const args3 = packet3.frame.arguments; + + Assert.equal(args3[0].class, "Function"); + // No name for an anonymous function, and no inferred name, either. + Assert.equal(args3[0].name, undefined); + Assert.equal(args3[0].displayName, undefined); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_getRuleText.js b/devtools/server/tests/xpcshell/test_getRuleText.js new file mode 100644 index 0000000000..fe53dca158 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_getRuleText.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getRuleText, +} = require("resource://devtools/server/actors/utils/style-utils.js"); + +const TEST_DATA = [ + { + desc: "Empty input", + input: "", + line: 1, + column: 1, + throws: true, + }, + { + desc: "Simplest test case", + input: "#id{color:red;background:yellow;}", + line: 1, + column: 1, + expected: { offset: 4, text: "color:red;background:yellow;" }, + }, + { + desc: "Multiple rules test case", + input: + "#id{color:red;background:yellow;}.class-one .class-two " + + "{ position:absolute; line-height: 45px}", + line: 1, + column: 34, + expected: { offset: 56, text: " position:absolute; line-height: 45px" }, + }, + { + desc: "Unclosed rule", + input: "#id{color:red;background:yellow;", + line: 1, + column: 1, + expected: { offset: 4, text: "color:red;background:yellow;" }, + }, + { + desc: "Null input", + input: null, + line: 1, + column: 1, + throws: true, + }, + { + desc: "Missing loc", + input: "#id{color:red;background:yellow;}", + throws: true, + }, + { + desc: "Multi-lines CSS", + input: [ + "/* this is a multi line css */", + "body {", + " color: green;", + " background-repeat: no-repeat", + "}", + " /*something else here */", + "* {", + " color: purple;", + "}", + ].join("\n"), + line: 7, + column: 1, + expected: { offset: 116, text: "\n color: purple;\n" }, + }, + { + desc: "Multi-lines CSS and multi-line rule", + input: [ + "/* ", + "* some comments", + "*/", + "", + "body {", + " margin: 0;", + " padding: 15px 15px 2px 15px;", + " color: red;", + "}", + "", + "#header .btn, #header .txt {", + " font-size: 100%;", + "}", + "", + "#header #information {", + " color: #dddddd;", + " font-size: small;", + "}", + ].join("\n"), + line: 5, + column: 1, + expected: { + offset: 30, + text: "\n margin: 0;\n padding: 15px 15px 2px 15px;\n color: red;\n", + }, + }, + { + desc: "Content string containing a } character", + input: " #id{border:1px solid red;content: '}';color:red;}", + line: 1, + column: 4, + expected: { + offset: 7, + text: "border:1px solid red;content: '}';color:red;", + }, + }, + { + desc: "Rule contains no tokens", + input: "div{}", + line: 1, + column: 1, + expected: { offset: 4, text: "" }, + }, +]; + +function run_test() { + for (const test of TEST_DATA) { + info("Starting test: " + test.desc); + info("Input string " + test.input); + let output; + try { + output = getRuleText(test.input, test.line, test.column); + if (test.throws) { + info("Test should have thrown"); + Assert.ok(false); + } + } catch (e) { + info("getRuleText threw an exception with the given input string"); + if (test.throws) { + info("Exception expected"); + Assert.ok(true); + } else { + info("Exception unexpected\n" + e); + Assert.ok(false); + } + } + if (output) { + deepEqual(output, test.expected); + } + } +} diff --git a/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js b/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js new file mode 100644 index 0000000000..3aa9915192 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_getTextAtLineColumn.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + getTextAtLineColumn, +} = require("resource://devtools/server/actors/utils/style-utils.js"); + +const TEST_DATA = [ + { + desc: "simplest", + input: "#id{color:red;background:yellow;}", + line: 1, + column: 5, + expected: { offset: 4, text: "color:red;background:yellow;}" }, + }, + { + desc: "multiple lines", + input: "one\n two\n three", + line: 3, + column: 3, + expected: { offset: 11, text: "three" }, + }, +]; + +function run_test() { + for (const test of TEST_DATA) { + info("Starting test: " + test.desc); + info("Input string " + test.input); + + const output = getTextAtLineColumn(test.input, test.line, test.column); + deepEqual(output, test.expected); + } +} diff --git a/devtools/server/tests/xpcshell/test_getyoungestframe.js b/devtools/server/tests/xpcshell/test_getyoungestframe.js new file mode 100644 index 0000000000..f08628b7ed --- /dev/null +++ b/devtools/server/tests/xpcshell/test_getyoungestframe.js @@ -0,0 +1,38 @@ +/* eslint-disable strict */ +function run_test() { + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); + const xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService( + Ci.nsIJSInspector + ); + const g = createTestGlobal("test1"); + + const dbg = makeDebugger(); + dbg.uncaughtExceptionHook = testExceptionHook; + + dbg.addDebuggee(g); + dbg.onDebuggerStatement = function (frame) { + Assert.ok(frame === dbg.getNewestFrame()); + // Execute from the nested event loop, dbg.getNewestFrame() won't + // be working anymore. + + executeSoon(function () { + try { + Assert.ok(frame === dbg.getNewestFrame()); + } finally { + xpcInspector.exitNestedEventLoop("test"); + } + }); + xpcInspector.enterNestedEventLoop("test"); + }; + + g.eval("function debuggerStatement() { debugger; }; debuggerStatement();"); + + dbg.disable(); +} diff --git a/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js b/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js new file mode 100644 index 0000000000..fe04161aab --- /dev/null +++ b/devtools/server/tests/xpcshell/test_ignore_caught_exceptions.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting ignoreCaughtExceptions will cause the debugger to ignore + * caught exceptions, but not uncaught ones. + */ + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: true, + }); + await resume(threadFront); + const paused = await waitForPause(threadFront); + Assert.equal(paused.why.type, "exception"); + equal(paused.frame.where.line, 6, "paused at throw"); + + await resume(threadFront); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + try { + Cu.evalInSandbox(` // 1 + debugger; // 2 + try { // 3 + throw "foo"; // 4 + } catch (e) {} // 5 + throw "bar"; // 6 + `, // 7 + debuggee, + "1.8", + "test_pause_exceptions-03.js", + 1 + ); + } catch (e) {} +} diff --git a/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js b/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js new file mode 100644 index 0000000000..50d28ffdc0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_ignore_no_interface_exceptions.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that the debugger automatically ignores NS_ERROR_NO_INTERFACE + * exceptions, but not normal ones. + */ + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + await threadFront.pauseOnExceptions(true, false); + const paused = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal(paused.frame.where.line, 6, "paused at throw"); + + await resume(threadFront); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox(` // 1 + function QueryInterface() { // 2 + throw Cr.NS_ERROR_NO_INTERFACE; // 3 + } // 4 + function stopMe() { // 5 + throw 42; // 6 + } // 7 + try { // 8 + QueryInterface(); // 9 + } catch (e) {} // 10 + try { // 11 + stopMe(); // 12 + } catch (e) {}`, // 13 + debuggee, + "1.8", + "test_ignore_no_interface_exceptions.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_interrupt.js b/devtools/server/tests/xpcshell/test_interrupt.js new file mode 100644 index 0000000000..07593a7360 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_interrupt.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client, targetFront }) => { + const onPaused = waitForEvent(threadFront, "paused"); + await threadFront.interrupt(); + await onPaused; + Assert.equal(threadFront.paused, true); + await threadFront.resume(); + Assert.equal(threadFront.paused, false); + }) +); diff --git a/devtools/server/tests/xpcshell/test_layout-reflows-observer.js b/devtools/server/tests/xpcshell/test_layout-reflows-observer.js new file mode 100644 index 0000000000..74f31b97fe --- /dev/null +++ b/devtools/server/tests/xpcshell/test_layout-reflows-observer.js @@ -0,0 +1,311 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the LayoutChangesObserver + +/* eslint-disable mozilla/use-chromeutils-generateqi */ + +var { + getLayoutChangesObserver, + releaseLayoutChangesObserver, + LayoutChangesObserver, +} = require("resource://devtools/server/actors/reflow.js"); +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); + +// Override set/clearTimeout on LayoutChangesObserver to avoid depending on +// time in this unit test. This means that LayoutChangesObserver.eventLoopTimer +// will be the timeout callback instead of the timeout itself, so test cases +// will need to execute it to fake a timeout +LayoutChangesObserver.prototype._setTimeout = cb => cb; +LayoutChangesObserver.prototype._clearTimeout = function () {}; + +// Mock the targetActor since we only really want to test the LayoutChangesObserver +// and don't want to depend on a window object, nor want to test protocol.js +class MockTargetActor extends EventEmitter { + constructor() { + super(); + this.docShell = new MockDocShell(); + this.window = new MockWindow(this.docShell); + this.windows = [this.window]; + this.attached = true; + } + + get chromeEventHandler() { + return this.docShell.chromeEventHandler; + } + + isDestroyed() { + return false; + } +} + +function MockWindow(docShell) { + this.docShell = docShell; +} +MockWindow.prototype = { + QueryInterface() { + const self = this; + return { + getInterface() { + return { + QueryInterface() { + return self.docShell; + }, + }; + }, + }; + }, + setTimeout(cb) { + // Simply return the cb itself so that we can execute it in the test instead + // of depending on a real timeout + return cb; + }, + clearTimeout() {}, +}; + +function MockDocShell() { + this.observer = null; +} +MockDocShell.prototype = { + addWeakReflowObserver(observer) { + this.observer = observer; + }, + removeWeakReflowObserver() {}, + get chromeEventHandler() { + return { + addEventListener: (type, cb) => { + if (type === "resize") { + this.resizeCb = cb; + } + }, + removeEventListener: (type, cb) => { + if (type === "resize" && cb === this.resizeCb) { + this.resizeCb = null; + } + }, + }; + }, + mockResize() { + if (this.resizeCb) { + this.resizeCb(); + } + }, +}; + +function run_test() { + instancesOfObserversAreSharedBetweenWindows(); + eventsAreBatched(); + noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts(); + observerIsAlreadyStarted(); + destroyStopsObserving(); + stoppingAndStartingSeveralTimesWorksCorrectly(); + reflowsArentStackedWhenStopped(); + stackedReflowsAreResetOnStop(); +} + +function instancesOfObserversAreSharedBetweenWindows() { + info( + "Checking that when requesting twice an instances of the observer " + + "for the same WindowGlobalTargetActor, the instance is shared" + ); + + info("Checking 2 instances of the observer for the targetActor 1"); + const targetActor1 = new MockTargetActor(); + const obs11 = getLayoutChangesObserver(targetActor1); + const obs12 = getLayoutChangesObserver(targetActor1); + Assert.equal(obs11, obs12); + + info("Checking 2 instances of the observer for the targetActor 2"); + const targetActor2 = new MockTargetActor(); + const obs21 = getLayoutChangesObserver(targetActor2); + const obs22 = getLayoutChangesObserver(targetActor2); + Assert.equal(obs21, obs22); + + info( + "Checking that observers instances for 2 different targetActors are " + + "different" + ); + Assert.notEqual(obs11, obs21); + + releaseLayoutChangesObserver(targetActor1); + releaseLayoutChangesObserver(targetActor1); + releaseLayoutChangesObserver(targetActor2); + releaseLayoutChangesObserver(targetActor2); +} + +function eventsAreBatched() { + info( + "Checking that reflow events are batched and only sent when the " + + "timeout expires" + ); + + // Note that in this test, we mock the target actor and its window property, so we also + // mock the setTimeout/clearTimeout mechanism and just call the callback manually + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + const reflowsEvents = []; + const onReflows = reflows => reflowsEvents.push(reflows); + observer.on("reflows", onReflows); + + const resizeEvents = []; + const onResize = () => resizeEvents.push("resize"); + observer.on("resize", onResize); + + info("Fake one reflow event"); + targetActor.window.docShell.observer.reflow(); + info("Checking that no batched reflow event has been emitted"); + Assert.equal(reflowsEvents.length, 0); + + info("Fake another reflow event"); + targetActor.window.docShell.observer.reflow(); + info("Checking that still no batched reflow event has been emitted"); + Assert.equal(reflowsEvents.length, 0); + + info("Fake a few of resize events too"); + targetActor.window.docShell.mockResize(); + targetActor.window.docShell.mockResize(); + targetActor.window.docShell.mockResize(); + info("Checking that still no batched resize event has been emitted"); + Assert.equal(resizeEvents.length, 0); + + info("Faking timeout expiration and checking that events are sent"); + observer.eventLoopTimer(); + Assert.equal(reflowsEvents.length, 1); + Assert.equal(reflowsEvents[0].length, 2); + Assert.equal(resizeEvents.length, 1); + + observer.off("reflows", onReflows); + observer.off("resize", onResize); + releaseLayoutChangesObserver(targetActor); +} + +function noEventsAreSentWhenThereAreNoReflowsAndLoopTimeouts() { + info( + "Checking that if no reflows were detected and the event batching " + + "loop expires, then no reflows event is sent" + ); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + const reflowsEvents = []; + const onReflows = reflows => reflowsEvents.push(reflows); + observer.on("reflows", onReflows); + + info("Faking timeout expiration and checking for reflows"); + observer.eventLoopTimer(); + Assert.equal(reflowsEvents.length, 0); + + observer.off("reflows", onReflows); + releaseLayoutChangesObserver(targetActor); +} + +function observerIsAlreadyStarted() { + info("Checking that the observer is already started when getting it"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + Assert.ok(observer.isObserving); + + observer.stop(); + Assert.ok(!observer.isObserving); + + observer.start(); + Assert.ok(observer.isObserving); + + releaseLayoutChangesObserver(targetActor); +} + +function destroyStopsObserving() { + info("Checking that the destroying the observer stops it"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + Assert.ok(observer.isObserving); + + observer.destroy(); + Assert.ok(!observer.isObserving); + + releaseLayoutChangesObserver(targetActor); +} + +function stoppingAndStartingSeveralTimesWorksCorrectly() { + info( + "Checking that the stopping and starting several times the observer" + + " works correctly" + ); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + Assert.ok(observer.isObserving); + observer.start(); + observer.start(); + observer.start(); + Assert.ok(observer.isObserving); + + observer.stop(); + Assert.ok(!observer.isObserving); + + observer.stop(); + observer.stop(); + Assert.ok(!observer.isObserving); + + releaseLayoutChangesObserver(targetActor); +} + +function reflowsArentStackedWhenStopped() { + info("Checking that when stopped, reflows aren't stacked in the observer"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + info("Stoping the observer"); + observer.stop(); + + info("Faking reflows"); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + + info("Checking that reflows aren't recorded"); + Assert.equal(observer.reflows.length, 0); + + info("Starting the observer and faking more reflows"); + observer.start(); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + targetActor.window.docShell.observer.reflow(); + + info("Checking that reflows are recorded"); + Assert.equal(observer.reflows.length, 3); + + releaseLayoutChangesObserver(targetActor); +} + +function stackedReflowsAreResetOnStop() { + info("Checking that stacked reflows are reset on stop"); + + const targetActor = new MockTargetActor(); + const observer = getLayoutChangesObserver(targetActor); + + targetActor.window.docShell.observer.reflow(); + Assert.equal(observer.reflows.length, 1); + + observer.stop(); + Assert.equal(observer.reflows.length, 0); + + targetActor.window.docShell.observer.reflow(); + Assert.equal(observer.reflows.length, 0); + + observer.start(); + Assert.equal(observer.reflows.length, 0); + + targetActor.window.docShell.observer.reflow(); + Assert.equal(observer.reflows.length, 1); + + releaseLayoutChangesObserver(targetActor); +} diff --git a/devtools/server/tests/xpcshell/test_listsources-01.js b/devtools/server/tests/xpcshell/test_listsources-01.js new file mode 100644 index 0000000000..306825278c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_listsources-01.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic getSources functionality. + */ + +var gNumTimesSourcesSent = 0; + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + client.request = (function (origRequest) { + return function (request, onResponse) { + if (request.type === "sources") { + ++gNumTimesSourcesSent; + } + return origRequest.call(this, request, onResponse); + }; + })(client.request); + + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const response = await threadFront.getSources(); + + Assert.ok( + response.sources.some(function (s) { + return s.url && s.url.match(/test_listsources-01.js/); + }) + ); + + Assert.ok( + gNumTimesSourcesSent <= 1, + "Should only send one sources request at most, even though we" + + " might have had to send one to determine feature support." + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "var line0 = Error().lineNumber;\n" + + "debugger;\n" + // line0 + 1 + "var a = 1;\n" + // line0 + 2 + "var b = 2;\n", // line0 + 3 + debuggee + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_listsources-02.js b/devtools/server/tests/xpcshell/test_listsources-02.js new file mode 100644 index 0000000000..a2f9cc3bda --- /dev/null +++ b/devtools/server/tests/xpcshell/test_listsources-02.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check getting sources before there are any. + */ + +var gNumTimesSourcesSent = 0; + +add_task( + threadFrontTest(async ({ threadFront, client }) => { + client.request = (function (origRequest) { + return function (request, onResponse) { + if (request.type === "sources") { + ++gNumTimesSourcesSent; + } + return origRequest.call(this, request, onResponse); + }; + })(client.request); + + // Test listing zero sources + const packet = await threadFront.getSources(); + + Assert.ok(!packet.error); + Assert.ok(!!packet.sources); + Assert.equal(packet.sources.length, 0); + + Assert.ok( + gNumTimesSourcesSent <= 1, + "Should only send one sources request at most, even though we" + + " might have had to send one to determine feature support." + ); + }) +); diff --git a/devtools/server/tests/xpcshell/test_listsources-03.js b/devtools/server/tests/xpcshell/test_listsources-03.js new file mode 100644 index 0000000000..f8af5aca6e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_listsources-03.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check getSources functionality when there are lots of sources. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const response = await threadFront.getSources(); + + Assert.ok( + !response.error, + "There shouldn't be an error fetching large amounts of sources." + ); + + Assert.ok( + response.sources.some(function (s) { + return s.url.match(/foo-999.js$/); + }) + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + for (let i = 0; i < 1000; i++) { + Cu.evalInSandbox( + "function foo###() {return ###;}".replace(/###/g, i), + debuggee, + "1.8", + "http://example.com/foo-" + i + ".js", + 1 + ); + } + debuggee.eval("debugger;"); +} diff --git a/devtools/server/tests/xpcshell/test_logpoint-01.js b/devtools/server/tests/xpcshell/test_logpoint-01.js new file mode 100644 index 0000000000..a5cb4f2197 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_logpoint-01.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that logpoints generate console messages. + */ + +const Resources = require("resource://devtools/server/actors/resources/index.js"); + +add_task( + threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => { + let lastMessage, lastExpression; + const targetActor = threadActor._parent; + // Only Workers are evaluating through the WebConsoleActor. + // Tabs will be evaluating directly via the frame object. + targetActor._consoleActor = { + evaluateJS(expression) { + lastExpression = expression; + }, + }; + + // And then listen for resource RDP event. + // Bug 1646677: But we should probably migrate this test to ResourceCommand so that + // we don't have to hack the server side via Resource.watchResources call. + targetActor.on("resource-available-form", resources => { + if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) { + lastMessage = resources[0].message; + } + }); + + // But both tabs and processes will be going through the ConsoleMessages module + // We force watching for console message first, + await Resources.watchResources(targetActor, [ + Resources.TYPES.CONSOLE_MESSAGE, + ]); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + + // Set a logpoint which should invoke console.log. + threadFront.setBreakpoint( + { + sourceUrl: source.url, + line: 3, + }, + { logValue: "a" } + ); + await client.waitForRequestsToSettle(); + + // Execute the rest of the code. + await threadFront.resume(); + + // NOTE: logpoints evaluated in a worker have a lastExpression + if (lastMessage) { + Assert.equal(lastMessage.level, "logPoint"); + Assert.equal(lastMessage.arguments[0], "three"); + Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp)); + } else { + Assert.equal(lastExpression.text, "console.log(...[a])"); + Assert.equal(lastExpression.lineNumber, 3); + } + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // 1 + "var a = 'three';\n" + // 2 + "var b = 2;\n", // 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_logpoint-02.js b/devtools/server/tests/xpcshell/test_logpoint-02.js new file mode 100644 index 0000000000..d84d3fc324 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_logpoint-02.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that conditions are respected when specified in a logpoint. + */ + +const Resources = require("resource://devtools/server/actors/resources/index.js"); + +add_task( + threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => { + let lastMessage, lastExpression; + const targetActor = threadActor._parent; + // Only Workers are evaluating through the WebConsoleActor. + // Tabs will be evaluating directly via the frame object. + targetActor._consoleActor = { + evaluateJS(expression) { + lastExpression = expression; + }, + }; + + // And then listen for resource RDP event. + // Bug 1646677: But we should probably migrate this test to ResourceCommand so that + // we don't have to hack the server side via Resource.watchResources call. + targetActor.on("resource-available-form", resources => { + if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) { + lastMessage = resources[0].message; + } + }); + + // But both tabs and processes will be going through the ConsoleMessages module + // We force watching for console message first, + await Resources.watchResources(targetActor, [ + Resources.TYPES.CONSOLE_MESSAGE, + ]); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + + // Set a logpoint which should invoke console.log. + threadFront.setBreakpoint( + { + sourceUrl: source.url, + line: 4, + }, + { logValue: "a", condition: "a === 5" } + ); + await client.waitForRequestsToSettle(); + + // Execute the rest of the code. + await threadFront.resume(); + + // NOTE: logpoints evaluated in a worker have a lastExpression + if (lastMessage) { + Assert.equal(lastMessage.level, "logPoint"); + Assert.equal(lastMessage.arguments[0], 5); + Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp)); + } else { + Assert.equal(lastExpression.text, "console.log(...[a])"); + Assert.equal(lastExpression.lineNumber, 4); + } + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // 1 + "var a = 1;\n" + // 2 + "while (a < 10) {\n" + // 3 + " a++;\n" + // 4 + "}", + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_logpoint-03.js b/devtools/server/tests/xpcshell/test_logpoint-03.js new file mode 100644 index 0000000000..b5d4440889 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_logpoint-03.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that logpoints generate console errors if the logpoint statement is invalid. + */ + +const Resources = require("resource://devtools/server/actors/resources/index.js"); + +add_task( + threadFrontTest(async ({ threadActor, threadFront, debuggee, client }) => { + let lastMessage, lastExpression; + const targetActor = threadActor._parent; + // Only Workers are evaluating through the WebConsoleActor. + // Tabs will be evaluating directly via the frame object. + targetActor._consoleActor = { + evaluateJS(expression) { + lastExpression = expression; + }, + }; + + // And then listen for resource RDP event. + // Bug 1646677: But we should probably migrate this test to ResourceCommand so that + // we don't have to hack the server side via Resource.watchResources call. + targetActor.on("resource-available-form", resources => { + if (resources[0].resourceType == Resources.TYPES.CONSOLE_MESSAGE) { + lastMessage = resources[0].message; + } + }); + + // But both tabs and processes will be going through the ConsoleMessages module + // We force watching for console message first, + await Resources.watchResources(targetActor, [ + Resources.TYPES.CONSOLE_MESSAGE, + ]); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const source = await getSourceById(threadFront, packet.frame.where.actor); + + // Set a logpoint which should throw an error message. + await threadFront.setBreakpoint( + { + sourceUrl: source.url, + line: 3, + }, + { logValue: "c" } + ); + + // Execute the rest of the code. + await threadFront.resume(); + + // NOTE: logpoints evaluated in a worker have a lastExpression + if (lastMessage) { + Assert.equal(lastMessage.level, "logPointError"); + Assert.equal(lastMessage.arguments[0], "c is not defined"); + Assert.ok(/\d+\.\d+/.test(lastMessage.timeStamp)); + } else { + Assert.equal(lastExpression.text, "console.log(...[c])"); + Assert.equal(lastExpression.lineNumber, 3); + } + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + "debugger;\n" + // 1 + "var a = 'three';\n" + // 2 + "var b = 2;\n", // 3 + debuggee, + "1.8", + "test.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_longstringgrips-01.js b/devtools/server/tests/xpcshell/test_longstringgrips-01.js new file mode 100644 index 0000000000..ac0b228c17 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_longstringgrips-01.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var gDebuggee; +var gClient; +var gThreadFront; + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, client }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + gClient = client; + test_longstring_grip(); + }, + { waitForFinish: true } + ) +); + +function test_longstring_grip() { + const longString = + "All I want is to be a monkey of moderate intelligence who" + + " wears a suit... that's why I'm transferring to business school! Maybe I" + + " love you so much, I love you no matter who you are pretending to be." + + " Enough about your promiscuous mother, Hermes! We have bigger problems." + + " For example, if you killed your grandfather, you'd cease to exist! What" + + " kind of a father would I be if I said no? Yep, I remember. They came in" + + " last at the Olympics, then retired to promote alcoholic beverages! And" + + " remember, don't do anything that affects anything, unless it turns out" + + " you were supposed to, in which case, for the love of God, don't not do" + + " it!"; + + DevToolsServer.LONG_STRING_LENGTH = 200; + + gThreadFront.once("paused", function (packet) { + const args = packet.frame.arguments; + Assert.equal(args.length, 1); + const grip = args[0]; + + try { + Assert.equal(grip.type, "longString"); + Assert.equal(grip.length, longString.length); + Assert.equal( + grip.initial, + longString.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH) + ); + + const longStringFront = createLongStringFront(gClient, grip); + longStringFront.substring(22, 28).then(function (response) { + try { + Assert.equal(response, "monkey"); + } finally { + gThreadFront.resume().then(function () { + finishClient(gClient); + }); + } + }); + } catch (error) { + gThreadFront.resume().then(function () { + finishClient(gClient); + do_throw(error); + }); + } + }); + + gDebuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + gDebuggee.eval('stopMe("' + longString + '")'); +} diff --git a/devtools/server/tests/xpcshell/test_nativewrappers.js b/devtools/server/tests/xpcshell/test_nativewrappers.js new file mode 100644 index 0000000000..170a2a1e6e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nativewrappers.js @@ -0,0 +1,39 @@ +/* eslint-disable strict */ +function run_test() { + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); + const g = createTestGlobal("test1"); + + const dbg = makeDebugger(); + dbg.addDebuggee(g); + dbg.onDebuggerStatement = function (frame) { + const args = frame.arguments; + try { + args[0]; + Assert.ok(true); + } catch (ex) { + Assert.ok(false); + } + }; + + g.eval("function stopMe(arg) {debugger;}"); + + const g2 = createTestGlobal("test2"); + g2.g = g; + // Not using the "stringify a function" trick because that runs afoul of the + // Cu.importGlobalProperties lint and we don't need it here anyway. + g2.eval(`(function createBadEvent() { + Cu.importGlobalProperties(["DOMParser"]); + let parser = new DOMParser(); + let doc = parser.parseFromString("<foo></foo>", "text/xml"); + g.stopMe(doc.createEvent("MouseEvent")); + } )()`); + + dbg.disable(); +} diff --git a/devtools/server/tests/xpcshell/test_nesting-03.js b/devtools/server/tests/xpcshell/test_nesting-03.js new file mode 100644 index 0000000000..0a64e751cd --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nesting-03.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that we can detect nested event loops in tabs with the same URL. + +add_task(async function () { + const GLOBAL_NAME = "test-nesting1"; + + initTestDevToolsServer(); + addTestGlobal(GLOBAL_NAME); + addTestGlobal(GLOBAL_NAME); + + // Connect two thread actors, debugging the same debuggee, and both being paused. + const firstClient = new DevToolsClient(DevToolsServer.connectPipe()); + await firstClient.connect(); + const { threadFront: firstThreadFront } = await attachTestThread( + firstClient, + GLOBAL_NAME + ); + await firstThreadFront.interrupt(); + + const secondClient = new DevToolsClient(DevToolsServer.connectPipe()); + await secondClient.connect(); + const { threadFront: secondThreadFront } = await attachTestThread( + secondClient, + GLOBAL_NAME + ); + await secondThreadFront.interrupt(); + + // Then check how concurrent resume work + let result; + try { + result = await firstThreadFront.resume(); + } catch (e) { + Assert.ok(e.includes("wrongOrder"), "rejects with the wrong order"); + } + Assert.ok(!result, "no response"); + + result = await secondThreadFront.resume(); + Assert.ok(true, "resumed as expected"); + + await firstThreadFront.resume(); + + Assert.ok(true, "resumed as expected"); + await firstClient.close(); + + await finishClient(secondClient); +}); diff --git a/devtools/server/tests/xpcshell/test_nesting-04.js b/devtools/server/tests/xpcshell/test_nesting-04.js new file mode 100644 index 0000000000..dcee257c40 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nesting-04.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that we never pause while being already paused. + * i.e. we don't support more than one nested event loops. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + await threadFront.setBreakpoint({ sourceUrl: "nesting-04.js", line: 2 }); + + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + Assert.equal(packet.frame.where.line, 5); + Assert.equal(packet.why.type, "debuggerStatement"); + + info("Test calling interrupt"); + const onPaused = waitForPause(threadFront); + await threadFront.interrupt(); + // interrupt() doesn't return anything, but bailout while emitting a paused packet + // But we don't pause again, the reason prove it so + const paused = await onPaused; + equal(paused.why.type, "alreadyPaused"); + + info("Test by evaluating code via the console"); + const { result } = await commands.scriptCommand.execute( + "debugger; functionWithDebuggerStatement()", + { + frameActor: packet.frame.actorID, + } + ); + // The fact that it returned immediately means that we did not pause + equal(result, 42); + + info("Test by calling code from chrome context"); + // This should be equivalent to any actor somehow triggering some page's JS + const rv = debuggee.functionWithDebuggerStatement(); + // The fact that it returned immediately means that we did not pause + equal(rv, 42); + + info("Test by stepping over a function that breaks"); + // This will only step over the debugger; statement we just break on + const step1 = await stepOver(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 6); + + // stepOver will actually resume and re-pause on the breakpoint + const step2 = await stepOver(threadFront); + equal(step2.why.type, "breakpoint"); + equal(step2.frame.where.line, 2); + + // Sanity check to ensure that the functionWithDebuggerStatement really pauses + info("Resume and pause on the breakpoint"); + const pausedPacket = await resumeAndWaitForPause(threadFront); + Assert.equal(pausedPacket.frame.where.line, 2); + // The breakpoint takes over the debugger statement + Assert.equal(pausedPacket.why.type, "breakpoint"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + `function functionWithDebuggerStatement() { + debugger; + return 42; + } + debugger; + functionWithDebuggerStatement(); + var a = 1; + functionWithDebuggerStatement();`, + debuggee, + "1.8", + "nesting-04.js", + 1 + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_new_source-01.js b/devtools/server/tests/xpcshell/test_new_source-01.js new file mode 100644 index 0000000000..929865baa8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_new_source-01.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic newSource packet sent from server. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + Cu.evalInSandbox( + function inc(n) { + return n + 1; + }.toString(), + debuggee + ); + + const sourcePacket = await waitForEvent(threadFront, "newSource"); + + Assert.ok(!!sourcePacket.source); + Assert.ok(!!sourcePacket.source.url.match(/test_new_source-01.js$/)); + }) +); diff --git a/devtools/server/tests/xpcshell/test_new_source-02.js b/devtools/server/tests/xpcshell/test_new_source-02.js new file mode 100644 index 0000000000..15259b884a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_new_source-02.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that sourceURL has the correct effect when using threadFront.eval. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const packet1 = await waitForEvent(threadFront, "newSource"); + + Assert.ok(!!packet1.source); + Assert.ok(packet1.source.introductionType, "eval"); + + commands.scriptCommand.execute( + "function f() { }\n//# sourceURL=http://example.com/code.js" + ); + + const packet2 = await waitForEvent(threadFront, "newSource"); + dump(JSON.stringify(packet2, null, 2)); + Assert.ok(!!packet2.source); + Assert.ok(!!packet2.source.url.match(/example\.com/)); + }) +); + +function evalCode(debuggee) { + /* eslint-disable */ + debuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); + /* eslint-enable */ +} diff --git a/devtools/server/tests/xpcshell/test_nodelistactor.js b/devtools/server/tests/xpcshell/test_nodelistactor.js new file mode 100644 index 0000000000..eab6bb07e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_nodelistactor.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that a NodeListActor initialized with null nodelist doesn't cause +// exceptions when calling NodeListActor.form. + +const { + NodeListActor, +} = require("resource://devtools/server/actors/inspector/node.js"); + +function run_test() { + check_actor_for_list(null); + check_actor_for_list([]); + check_actor_for_list(["fakenode"]); +} + +function check_actor_for_list(nodelist) { + info("Checking NodeListActor with nodelist '" + nodelist + "' works."); + const actor = new NodeListActor({}, nodelist); + const form = actor.form(); + + // No exception occured as a exceptions abort the test. + ok(true, "No exceptions occured."); + equal( + form.length, + nodelist ? nodelist.length : 0, + "NodeListActor reported correct length." + ); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-02.js b/devtools/server/tests/xpcshell/test_objectgrips-02.js new file mode 100644 index 0000000000..810a5009c0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-02.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objectFront = threadFront.pauseGrip(args[0]); + const response = await objectFront.getPrototype(); + Assert.ok(response.prototype != undefined); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval( + function Constr() { + this.a = 1; + }.toString() + ); + debuggee.eval( + "Constr.prototype = { b: true, c: 'foo' }; var o = new Constr(); stopMe(o)" + ); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-03.js b/devtools/server/tests/xpcshell/test_objectgrips-03.js new file mode 100644 index 0000000000..c8a51d41d3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-03.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objClient = threadFront.pauseGrip(args[0]); + let response = await objClient.getProperty("x"); + Assert.equal(response.descriptor.configurable, true); + Assert.equal(response.descriptor.enumerable, true); + Assert.equal(response.descriptor.writable, true); + Assert.equal(response.descriptor.value, 10); + + response = await objClient.getProperty("y"); + Assert.equal(response.descriptor.configurable, true); + Assert.equal(response.descriptor.enumerable, true); + Assert.equal(response.descriptor.writable, true); + Assert.equal(response.descriptor.value, "kaiju"); + + response = await objClient.getProperty("a"); + Assert.equal(response.descriptor.configurable, true); + Assert.equal(response.descriptor.enumerable, true); + Assert.equal(response.descriptor.get.type, "object"); + Assert.equal(response.descriptor.get.class, "Function"); + Assert.equal(response.descriptor.set.type, "undefined"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })"); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-04.js b/devtools/server/tests/xpcshell/test_objectgrips-04.js new file mode 100644 index 0000000000..d08705db3c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-04.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objectFront = threadFront.pauseGrip(args[0]); + const { ownProperties, prototype } = + await objectFront.getPrototypeAndProperties(); + Assert.equal(ownProperties.x.configurable, true); + Assert.equal(ownProperties.x.enumerable, true); + Assert.equal(ownProperties.x.writable, true); + Assert.equal(ownProperties.x.value, 10); + + Assert.equal(ownProperties.y.configurable, true); + Assert.equal(ownProperties.y.enumerable, true); + Assert.equal(ownProperties.y.writable, true); + Assert.equal(ownProperties.y.value, "kaiju"); + + Assert.equal(ownProperties.a.configurable, true); + Assert.equal(ownProperties.a.enumerable, true); + Assert.equal(ownProperties.a.get.getGrip().type, "object"); + Assert.equal(ownProperties.a.get.getGrip().class, "Function"); + Assert.equal(ownProperties.a.set.type, "undefined"); + + Assert.ok(prototype != undefined); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })"); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-05.js b/devtools/server/tests/xpcshell/test_objectgrips-05.js new file mode 100644 index 0000000000..4c6f0f107a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-05.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that frozen objects report themselves as frozen in their + * grip. + */ + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const obj1 = packet.frame.arguments[0]; + Assert.ok(obj1.frozen); + + const obj1Client = threadFront.pauseGrip(obj1); + Assert.ok(obj1Client.isFrozen); + + const obj2 = packet.frame.arguments[1]; + Assert.ok(!obj2.frozen); + + const obj2Client = threadFront.pauseGrip(obj2); + Assert.ok(!obj2Client.isFrozen); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + /* eslint-disable no-undef */ + debuggee.eval( + "(" + + function () { + const obj1 = {}; + Object.freeze(obj1); + stopMe(obj1, {}); + } + + "())" + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-06.js b/devtools/server/tests/xpcshell/test_objectgrips-06.js new file mode 100644 index 0000000000..ef3d2b5b66 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-06.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that sealed objects report themselves as sealed in their + * grip. + */ + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const obj1 = packet.frame.arguments[0]; + Assert.ok(obj1.sealed); + + const obj1Client = threadFront.pauseGrip(obj1); + Assert.ok(obj1Client.isSealed); + + const obj2 = packet.frame.arguments[1]; + Assert.ok(!obj2.sealed); + + const obj2Client = threadFront.pauseGrip(obj2); + Assert.ok(!obj2Client.isSealed); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + /* eslint-disable no-undef */ + debuggee.eval( + "(" + + function () { + const obj1 = {}; + Object.seal(obj1); + stopMe(obj1, {}); + } + + "())" + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-07.js b/devtools/server/tests/xpcshell/test_objectgrips-07.js new file mode 100644 index 0000000000..2a3a0bf00e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-07.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test checks that objects which are not extensible report themselves as + * such. + */ + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [f, s, ne, e] = packet.frame.arguments; + const [fClient, sClient, neClient, eClient] = packet.frame.arguments.map( + a => threadFront.pauseGrip(a) + ); + + Assert.ok(!f.extensible); + Assert.ok(!fClient.isExtensible); + + Assert.ok(!s.extensible); + Assert.ok(!sClient.isExtensible); + + Assert.ok(!ne.extensible); + Assert.ok(!neClient.isExtensible); + + Assert.ok(e.extensible); + Assert.ok(eClient.isExtensible); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + /* eslint-disable no-undef */ + debuggee.eval( + "(" + + function () { + const f = {}; + Object.freeze(f); + const s = {}; + Object.seal(s); + const ne = {}; + Object.preventExtensions(ne); + stopMe(f, s, ne, {}); + } + + "())" + ); + /* eslint-enable no-undef */ +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-08.js b/devtools/server/tests/xpcshell/test_objectgrips-08.js new file mode 100644 index 0000000000..1a37f19fb8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-08.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + + Assert.equal(args[0].class, "Object"); + + const objClient = threadFront.pauseGrip(args[0]); + const response = await objClient.getPrototypeAndProperties(); + const { a, b, c, d, e, f, g } = response.ownProperties; + testPropertyType(a, "Infinity"); + testPropertyType(b, "-Infinity"); + testPropertyType(c, "NaN"); + testPropertyType(d, "-0"); + testPropertyType(e, "BigInt"); + testPropertyType(f, "BigInt"); + testPropertyType(g, "BigInt"); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval( + `stopMe({ + a: Infinity, + b: -Infinity, + c: NaN, + d: -0, + e: 1n, + f: -2n, + g: 0n, + })` + ); +} + +function testPropertyType(prop, expectedType) { + Assert.equal(prop.configurable, true); + Assert.equal(prop.enumerable, true); + Assert.equal(prop.writable, true); + Assert.equal(prop.value.type, expectedType); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-14.js b/devtools/server/tests/xpcshell/test_objectgrips-14.js new file mode 100644 index 0000000000..cff8611e7d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-14.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test out of scope objects with synchronous functions. + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + await testObjectGroup(); + }) +); + +function evalCode() { + evalCallback(gDebuggee, function runTest() { + const ugh = []; + let i = 0; + + (function () { + (function () { + ugh.push(i++); + debugger; + })(); + })(); + + debugger; + }); +} + +const testObjectGroup = async function () { + let packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront); + + const environment = await packet.frame.getEnvironment(); + const ugh = environment.parent.parent.bindings.variables.ugh; + const ughClient = await gThreadFront.pauseGrip(ugh.value); + + packet = await getPrototypeAndProperties(ughClient); + packet = await resumeAndWaitForPause(gThreadFront); + + const environment2 = await packet.frame.getEnvironment(); + const ugh2 = environment2.bindings.variables.ugh; + const ugh2Client = gThreadFront.pauseGrip(ugh2.value); + + packet = await getPrototypeAndProperties(ugh2Client); + Assert.equal(packet.ownProperties.length.value, 1); + + await resume(gThreadFront); +}; diff --git a/devtools/server/tests/xpcshell/test_objectgrips-15.js b/devtools/server/tests/xpcshell/test_objectgrips-15.js new file mode 100644 index 0000000000..3a7aba89c8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-15.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test out of scope objects with async functions. + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + await testObjectGroup(); + }) +); + +function evalCode() { + evalCallback(gDebuggee, function runTest() { + const ugh = []; + let i = 0; + + function foo() { + ugh.push(i++); + debugger; + } + + Promise.resolve().then(foo).then(foo); + }); +} + +const testObjectGroup = async function () { + let packet = await executeOnNextTickAndWaitForPause(evalCode, gThreadFront); + + const environment = await packet.frame.getEnvironment(); + const ugh = environment.parent.bindings.variables.ugh; + const ughClient = await gThreadFront.pauseGrip(ugh.value); + + packet = await getPrototypeAndProperties(ughClient); + + packet = await resumeAndWaitForPause(gThreadFront); + const environment2 = await packet.frame.getEnvironment(); + const ugh2 = environment2.parent.bindings.variables.ugh; + const ugh2Client = gThreadFront.pauseGrip(ugh2.value); + + packet = await getPrototypeAndProperties(ugh2Client); + Assert.equal(packet.ownProperties.length.value, 2); + + await resume(gThreadFront); +}; diff --git a/devtools/server/tests/xpcshell/test_objectgrips-16.js b/devtools/server/tests/xpcshell/test_objectgrips-16.js new file mode 100644 index 0000000000..785c3bc36d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-16.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + eval_code, + threadFront + ); + const [grip] = packet.frame.arguments; + + // Checks grip.preview properties. + check_preview(grip); + + const objClient = threadFront.pauseGrip(grip); + const response = await objClient.getPrototypeAndProperties(); + // Checks the result of getPrototypeAndProperties. + check_prototype_and_properties(response); + + await threadFront.resume(); + + function eval_code() { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval(` + stopMe({ + [Symbol()]: "first unnamed symbol", + [Symbol()]: "second unnamed symbol", + [Symbol("named")] : "named symbol", + [Symbol.iterator] : function* () { + yield 1; + yield 2; + }, + x: 10, + }); + `); + } + + function check_preview(grip) { + Assert.equal(grip.class, "Object"); + + const { preview } = grip; + Assert.equal(preview.ownProperties.x.configurable, true); + Assert.equal(preview.ownProperties.x.enumerable, true); + Assert.equal(preview.ownProperties.x.writable, true); + Assert.equal(preview.ownProperties.x.value, 10); + + const [ + firstUnnamedSymbol, + secondUnnamedSymbol, + namedSymbol, + iteratorSymbol, + ] = preview.ownSymbols; + + Assert.equal(firstUnnamedSymbol.name, undefined); + Assert.equal(firstUnnamedSymbol.type, "symbol"); + Assert.equal(firstUnnamedSymbol.descriptor.configurable, true); + Assert.equal(firstUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(firstUnnamedSymbol.descriptor.writable, true); + Assert.equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol"); + + Assert.equal(secondUnnamedSymbol.name, undefined); + Assert.equal(secondUnnamedSymbol.type, "symbol"); + Assert.equal(secondUnnamedSymbol.descriptor.configurable, true); + Assert.equal(secondUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(secondUnnamedSymbol.descriptor.writable, true); + Assert.equal( + secondUnnamedSymbol.descriptor.value, + "second unnamed symbol" + ); + + Assert.equal(namedSymbol.name, "named"); + Assert.equal(namedSymbol.type, "symbol"); + Assert.equal(namedSymbol.descriptor.configurable, true); + Assert.equal(namedSymbol.descriptor.enumerable, true); + Assert.equal(namedSymbol.descriptor.writable, true); + Assert.equal(namedSymbol.descriptor.value, "named symbol"); + + Assert.equal(iteratorSymbol.name, "Symbol.iterator"); + Assert.equal(iteratorSymbol.type, "symbol"); + Assert.equal(iteratorSymbol.descriptor.configurable, true); + Assert.equal(iteratorSymbol.descriptor.enumerable, true); + Assert.equal(iteratorSymbol.descriptor.writable, true); + Assert.equal(iteratorSymbol.descriptor.value.class, "Function"); + } + + function check_prototype_and_properties(response) { + Assert.equal(response.ownProperties.x.configurable, true); + Assert.equal(response.ownProperties.x.enumerable, true); + Assert.equal(response.ownProperties.x.writable, true); + Assert.equal(response.ownProperties.x.value, 10); + + const [ + firstUnnamedSymbol, + secondUnnamedSymbol, + namedSymbol, + iteratorSymbol, + ] = response.ownSymbols; + + Assert.equal(firstUnnamedSymbol.name, "Symbol()"); + Assert.equal(firstUnnamedSymbol.descriptor.configurable, true); + Assert.equal(firstUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(firstUnnamedSymbol.descriptor.writable, true); + Assert.equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol"); + + Assert.equal(secondUnnamedSymbol.name, "Symbol()"); + Assert.equal(secondUnnamedSymbol.descriptor.configurable, true); + Assert.equal(secondUnnamedSymbol.descriptor.enumerable, true); + Assert.equal(secondUnnamedSymbol.descriptor.writable, true); + Assert.equal( + secondUnnamedSymbol.descriptor.value, + "second unnamed symbol" + ); + + Assert.equal(namedSymbol.name, "Symbol(named)"); + Assert.equal(namedSymbol.descriptor.configurable, true); + Assert.equal(namedSymbol.descriptor.enumerable, true); + Assert.equal(namedSymbol.descriptor.writable, true); + Assert.equal(namedSymbol.descriptor.value, "named symbol"); + + Assert.equal(iteratorSymbol.name, "Symbol(Symbol.iterator)"); + Assert.equal(iteratorSymbol.descriptor.configurable, true); + Assert.equal(iteratorSymbol.descriptor.enumerable, true); + Assert.equal(iteratorSymbol.descriptor.writable, true); + Assert.equal(iteratorSymbol.descriptor.value.class, "Function"); + } + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-17.js b/devtools/server/tests/xpcshell/test_objectgrips-17.js new file mode 100644 index 0000000000..edaea88eaa --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-17.js @@ -0,0 +1,320 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +async function testPrincipal(options, globalPrincipal, debuggeeHasXrays) { + const { debuggee } = options; + // Create a global object with the specified security principal. + // If none is specified, use the debuggee. + if (globalPrincipal === undefined) { + await test(options, { + global: debuggee, + subsumes: true, + isOpaque: false, + globalIsInvisible: false, + }); + return; + } + + const debuggeePrincipal = Cu.getObjectPrincipal(debuggee); + const sameOrigin = debuggeePrincipal.origin === globalPrincipal.origin; + const subsumes = debuggeePrincipal.subsumes(globalPrincipal); + for (const globalHasXrays of [true, false]) { + const isOpaque = + subsumes && + globalPrincipal !== systemPrincipal && + ((sameOrigin && debuggeeHasXrays) || globalHasXrays); + for (const globalIsInvisible of [true, false]) { + let global = Cu.Sandbox(globalPrincipal, { + wantXrays: globalHasXrays, + invisibleToDebugger: globalIsInvisible, + }); + // Previously, the Sandbox constructor would (bizarrely) waive xrays on + // the return Sandbox if wantXrays was false. This has now been fixed, + // but we need to mimic that behavior here to make the test continue + // to pass. + if (!globalHasXrays) { + global = Cu.waiveXrays(global); + } + await test(options, { global, subsumes, isOpaque, globalIsInvisible }); + } + } +} + +async function test({ threadFront, debuggee }, testOptions) { + const { global } = testOptions; + const packet = await executeOnNextTickAndWaitForPause(eval_code, threadFront); + // Get the grips. + const [proxyGrip, inheritsProxyGrip, inheritsProxy2Grip] = + packet.frame.arguments; + + // Check the grip of the proxy object. + check_proxy_grip(debuggee, testOptions, proxyGrip); + + // Check the target and handler slots of the proxy object. + const proxyClient = threadFront.pauseGrip(proxyGrip); + const proxySlots = await proxyClient.getProxySlots(); + check_proxy_slots(debuggee, testOptions, proxyGrip, proxySlots); + + // Check the prototype and properties of the proxy object. + const proxyResponse = await proxyClient.getPrototypeAndProperties(); + check_properties(testOptions, proxyResponse.ownProperties, true, false); + check_prototype(debuggee, testOptions, proxyResponse.prototype, true, false); + + // Check the prototype and properties of the object which inherits from the proxy. + const inheritsProxyClient = threadFront.pauseGrip(inheritsProxyGrip); + const inheritsProxyResponse = + await inheritsProxyClient.getPrototypeAndProperties(); + check_properties( + testOptions, + inheritsProxyResponse.ownProperties, + false, + false + ); + check_prototype( + debuggee, + testOptions, + inheritsProxyResponse.prototype, + false, + false + ); + + // The prototype chain was not iterated if the object was inaccessible, so now check + // another object which inherits from the proxy, but was created in the debuggee. + const inheritsProxy2Client = threadFront.pauseGrip(inheritsProxy2Grip); + const inheritsProxy2Response = + await inheritsProxy2Client.getPrototypeAndProperties(); + check_properties( + testOptions, + inheritsProxy2Response.ownProperties, + false, + true + ); + check_prototype( + debuggee, + testOptions, + inheritsProxy2Response.prototype, + false, + true + ); + + // Check that none of the above ran proxy traps. + strictEqual(global.trapDidRun, false, "No proxy trap did run."); + + // Resume the debugger and finish the current test. + await threadFront.resume(); + + function eval_code() { + // Create objects in `global`, and debug them in `debuggee`. They may get various + // kinds of security wrappers, or no wrapper at all. + // To detect that no proxy trap runs, the proxy handler should define all possible + // traps, but the list is long and may change. Therefore a second proxy is used as + // the handler, so that a single `get` trap suffices. + global.eval(` + var trapDidRun = false; + var proxy = new Proxy({}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + var inheritsProxy = Object.create(proxy, {x:{value:1}}); + `); + const data = Cu.createObjectIn(debuggee, { defineAs: "data" }); + data.proxy = global.proxy; + data.inheritsProxy = global.inheritsProxy; + debuggee.eval(` + var inheritsProxy2 = Object.create(data.proxy, {x:{value:1}}); + stopMe(data.proxy, data.inheritsProxy, inheritsProxy2); + `); + } +} + +function check_proxy_grip(debuggee, testOptions, grip) { + const { global, isOpaque, subsumes, globalIsInvisible } = testOptions; + const { preview } = grip; + + if (global === debuggee) { + // The proxy has no security wrappers. + strictEqual(grip.class, "Proxy", "The grip has a Proxy class."); + strictEqual( + preview.ownPropertiesLength, + 2, + "The preview has 2 properties." + ); + const props = preview.ownProperties; + ok(props["<target>"].value, "<target> contains the [[ProxyTarget]]."); + ok(props["<handler>"].value, "<handler> contains the [[ProxyHandler]]."); + } else if (isOpaque) { + // The proxy has opaque security wrappers. + strictEqual(grip.class, "Opaque", "The grip has an Opaque class."); + strictEqual(grip.ownPropertyLength, 0, "The grip has no properties."); + } else if (!subsumes) { + // The proxy belongs to compartment not subsumed by the debuggee. + strictEqual(grip.class, "Restricted", "The grip has a Restricted class."); + strictEqual( + grip.ownPropertyLength, + undefined, + "The grip doesn't know the number of properties." + ); + } else if (globalIsInvisible) { + // The proxy belongs to an invisible-to-debugger compartment. + strictEqual( + grip.class, + "InvisibleToDebugger: Object", + "The grip has an InvisibleToDebugger class." + ); + ok( + !("ownPropertyLength" in grip), + "The grip doesn't know the number of properties." + ); + } else { + // The proxy has non-opaque security wrappers. + strictEqual(grip.class, "Proxy", "The grip has a Proxy class."); + strictEqual( + preview.ownPropertiesLength, + 0, + "The preview has no properties." + ); + ok(!("<target>" in preview), "The preview has no <target> property."); + ok(!("<handler>" in preview), "The preview has no <handler> property."); + } +} + +function check_proxy_slots(debuggee, testOptions, grip, proxySlots) { + const { global } = testOptions; + + if (grip.class !== "Proxy") { + strictEqual( + proxySlots, + null, + "Slots can only be retrived for Proxy grips." + ); + } else if (global === debuggee) { + const { proxyTarget, proxyHandler } = proxySlots; + strictEqual( + proxyTarget.getGrip().type, + "object", + "There is a [[ProxyTarget]] grip." + ); + strictEqual( + proxyHandler.getGrip().type, + "object", + "There is a [[ProxyHandler]] grip." + ); + } else { + const { proxyTarget, proxyHandler } = proxySlots; + strictEqual( + proxyTarget.type, + "undefined", + "There is no [[ProxyTarget]] grip." + ); + strictEqual( + proxyHandler.type, + "undefined", + "There is no [[ProxyHandler]] grip." + ); + } +} + +function check_properties(testOptions, props, isProxy, createdInDebuggee) { + const { subsumes, globalIsInvisible } = testOptions; + const ownPropertiesLength = Reflect.ownKeys(props).length; + + if (createdInDebuggee || (!isProxy && subsumes && !globalIsInvisible)) { + // The debuggee can access the properties. + strictEqual(ownPropertiesLength, 1, "1 own property was retrieved."); + strictEqual(props.x.value, 1, "The property has the right value."); + } else { + // The debuggee is not allowed to access the object. + strictEqual(ownPropertiesLength, 0, "No own property could be retrieved."); + } +} + +function check_prototype( + debuggee, + testOptions, + proto, + isProxy, + createdInDebuggee +) { + const { global, isOpaque, subsumes, globalIsInvisible } = testOptions; + if (isOpaque && !globalIsInvisible && !createdInDebuggee) { + // The object is or inherits from a proxy with opaque security wrappers. + // The debuggee sees `Object.prototype` when retrieving the prototype. + strictEqual( + proto.getGrip().class, + "Object", + "The prototype has a Object class." + ); + } else if (isProxy && isOpaque && globalIsInvisible) { + // The object is a proxy with opaque security wrappers in an invisible global. + // The debuggee sees an inaccessible `Object.prototype` when retrieving the prototype. + strictEqual( + proto.getGrip().class, + "InvisibleToDebugger: Object", + "The prototype has an InvisibleToDebugger class." + ); + } else if ( + createdInDebuggee || + (!isProxy && subsumes && !globalIsInvisible) + ) { + // The object inherits from a proxy and has no security wrappers or non-opaque ones. + // The debuggee sees the proxy when retrieving the prototype. + check_proxy_grip( + debuggee, + { global, isOpaque, subsumes, globalIsInvisible }, + proto.getGrip() + ); + } else { + // The debuggee is not allowed to access the object. It sees a null prototype. + strictEqual(proto.type, "null", "The prototype is null."); + } +} + +function createNullPrincipal() { + return Services.scriptSecurityManager.createNullPrincipal({}); +} + +async function run_tests_in_principal( + options, + debuggeePrincipal, + debuggeeHasXrays +) { + const { debuggee } = options; + debuggee.eval( + function stopMe(arg1, arg2) { + debugger; + }.toString() + ); + + // Test objects created in the debuggee. + await testPrincipal(options, undefined, debuggeeHasXrays); + + // Test objects created in a system principal new global. + await testPrincipal(options, systemPrincipal, debuggeeHasXrays); + + // Test objects created in a cross-origin null principal new global. + await testPrincipal(options, createNullPrincipal(), debuggeeHasXrays); + + if (debuggeePrincipal != systemPrincipal) { + // Test objects created in a same-origin principal new global. + await testPrincipal(options, debuggeePrincipal, debuggeeHasXrays); + } +} + +for (const principal of [systemPrincipal, createNullPrincipal()]) { + for (const wantXrays of [true, false]) { + add_task( + threadFrontTest( + options => run_tests_in_principal(options, principal, wantXrays), + { principal, wantXrays } + ) + ); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-18.js b/devtools/server/tests/xpcshell/test_objectgrips-18.js new file mode 100644 index 0000000000..90c38d99a9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-18.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + eval_code, + threadFront + ); + + const [grip] = packet.frame.arguments; + + const objectFront = threadFront.pauseGrip(grip); + + // Checks the result of enumProperties. + let response = await objectFront.enumProperties({}); + await check_enum_properties(response); + + // Checks the result of enumSymbols. + response = await objectFront.enumSymbols(); + await check_enum_symbols(response); + + await threadFront.resume(); + + function eval_code() { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + var obj = Array.from({length: 10}) + .reduce((res, _, i) => { + res["property_" + i + "_key"] = "property_" + i + "_value"; + res[Symbol("symbol_" + i)] = "symbol_" + i + "_value"; + return res; + }, {}); + + obj[Symbol()] = "first unnamed symbol"; + obj[Symbol()] = "second unnamed symbol"; + obj[Symbol.iterator] = function* () { + yield 1; + yield 2; + }; + + stopMe(obj); + `); + } + + async function check_enum_properties(iterator) { + equal(iterator.count, 10, "iterator.count has the expected value"); + + info("Check iterator.slice response for all properties"); + let sliceResponse = await iterator.slice(0, iterator.count); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"), + "The response object has an ownProperties property" + ); + + let { ownProperties } = sliceResponse; + let names = Object.keys(ownProperties); + equal( + names.length, + iterator.count, + "The response has the expected number of properties" + ); + for (let i = 0; i < names.length; i++) { + const name = names[i]; + equal(name, `property_${i}_key`); + equal(ownProperties[name].value, `property_${i}_value`); + } + + info("Check iterator.all response"); + const allResponse = await iterator.all(); + deepEqual( + allResponse, + sliceResponse, + "iterator.all response has the expected data" + ); + + info("Check iterator response for 2 properties only"); + sliceResponse = await iterator.slice(2, 2); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"), + "The response object has an ownProperties property" + ); + + ownProperties = sliceResponse.ownProperties; + names = Object.keys(ownProperties); + equal( + names.length, + 2, + "The response has the expected number of properties" + ); + equal(names[0], `property_2_key`); + equal(names[1], `property_3_key`); + equal(ownProperties[names[0]].value, `property_2_value`); + equal(ownProperties[names[1]].value, `property_3_value`); + } + + async function check_enum_symbols(iterator) { + equal(iterator.count, 13, "iterator.count has the expected value"); + + info("Check iterator.slice response for all symbols"); + let sliceResponse = await iterator.slice(0, iterator.count); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"), + "The response object has an ownSymbols property" + ); + + let { ownSymbols } = sliceResponse; + equal( + ownSymbols.length, + iterator.count, + "The response has the expected number of symbols" + ); + for (let i = 0; i < 10; i++) { + const symbol = ownSymbols[i]; + equal(symbol.name, `Symbol(symbol_${i})`); + equal(symbol.descriptor.value, `symbol_${i}_value`); + } + const firstUnnamedSymbol = ownSymbols[10]; + equal(firstUnnamedSymbol.name, "Symbol()"); + equal(firstUnnamedSymbol.descriptor.value, "first unnamed symbol"); + + const secondUnnamedSymbol = ownSymbols[11]; + equal(secondUnnamedSymbol.name, "Symbol()"); + equal(secondUnnamedSymbol.descriptor.value, "second unnamed symbol"); + + const iteratorSymbol = ownSymbols[12]; + equal(iteratorSymbol.name, "Symbol(Symbol.iterator)"); + equal(iteratorSymbol.descriptor.value.getGrip().class, "Function"); + + info("Check iterator.all response"); + const allResponse = await iterator.all(); + deepEqual( + allResponse, + sliceResponse, + "iterator.all response has the expected data" + ); + + info("Check iterator response for 2 symbols only"); + sliceResponse = await iterator.slice(9, 2); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownSymbols"), + "The response object has an ownSymbols property" + ); + + ownSymbols = sliceResponse.ownSymbols; + equal( + ownSymbols.length, + 2, + "The response has the expected number of symbols" + ); + equal(ownSymbols[0].name, "Symbol(symbol_9)"); + equal(ownSymbols[0].descriptor.value, "symbol_9_value"); + equal(ownSymbols[1].name, "Symbol()"); + equal(ownSymbols[1].descriptor.value, "first unnamed symbol"); + } + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-19.js b/devtools/server/tests/xpcshell/test_objectgrips-19.js new file mode 100644 index 0000000000..655c7d0f43 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-19.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + const tests = [ + { + value: true, + class: "Boolean", + }, + { + value: 123, + class: "Number", + }, + { + value: "foo", + class: "String", + }, + { + value: Symbol("bar"), + class: "Symbol", + name: "bar", + }, + ]; + for (const data of tests) { + debuggee.primitive = data.value; + const packet = await executeOnNextTickAndWaitForPause(() => { + debuggee.eval("stopMe(Object(primitive));"); + }, threadFront); + + const [grip] = packet.frame.arguments; + check_wrapped_primitive_grip(grip, data); + + await threadFront.resume(); + } + }) +); + +function check_wrapped_primitive_grip(grip, data) { + strictEqual(grip.class, data.class, "The grip has the proper class."); + + if (!grip.preview) { + // In a worker thread Cu does not exist, the objects are considered unsafe and + // can't be unwrapped, so there is no preview. + return; + } + + const value = grip.preview.wrappedValue; + if (data.class === "Symbol") { + strictEqual( + value.type, + "symbol", + "The wrapped value grip has symbol type." + ); + strictEqual( + value.name, + data.name, + "The wrapped value grip has the proper name." + ); + } else { + strictEqual(value, data.value, "The wrapped value is the primitive one."); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-20.js b/devtools/server/tests/xpcshell/test_objectgrips-20.js new file mode 100644 index 0000000000..5027ca31a7 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-20.js @@ -0,0 +1,387 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that onEnumProperties returns the expected data +// when passing `ignoreNonIndexedProperties` and `ignoreIndexedProperties` options +// with various objects. (See Bug 1403065) + +const DO_NOT_CHECK_VALUE = Symbol(); + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + const testCases = [ + { + evaledObject: { a: 10 }, + expectedIndexedProperties: [], + expectedNonIndexedProperties: [["a", 10]], + }, + { + evaledObject: { length: 10 }, + expectedIndexedProperties: [], + expectedNonIndexedProperties: [["length", 10]], + }, + { + evaledObject: { a: 10, 0: "indexed" }, + expectedIndexedProperties: [["0", "indexed"]], + expectedNonIndexedProperties: [["a", 10]], + }, + { + evaledObject: { 1: 1, length: 42, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", 42], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: 2.34, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", 2.34], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: -0, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", -0], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: -10, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", -10], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: true, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", true], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: null, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", DO_NOT_CHECK_VALUE], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: Math.pow(2, 53), a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", 9007199254740992], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: "fake", a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", "fake"], + ["a", 10], + ], + }, + { + evaledObject: { 1: 1, length: Infinity, a: 10 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [ + ["length", DO_NOT_CHECK_VALUE], + ["a", 10], + ], + }, + { + evaledObject: { 0: 0, length: 0 }, + expectedIndexedProperties: [["0", 0]], + expectedNonIndexedProperties: [["length", 0]], + }, + { + evaledObject: { 0: 0, 1: 1, length: 1 }, + expectedIndexedProperties: [ + ["0", 0], + ["1", 1], + ], + expectedNonIndexedProperties: [["length", 1]], + }, + { + evaledObject: { length: 0 }, + expectedIndexedProperties: [], + expectedNonIndexedProperties: [["length", 0]], + }, + { + evaledObject: { 1: 1 }, + expectedIndexedProperties: [["1", 1]], + expectedNonIndexedProperties: [], + }, + { + evaledObject: { a: 1, [2 ** 32 - 2]: 2, [2 ** 32 - 1]: 3 }, + expectedIndexedProperties: [["4294967294", 2]], + expectedNonIndexedProperties: [ + ["a", 1], + ["4294967295", 3], + ], + }, + { + evaledObject: `(() => { + x = [12, 42]; + x.foo = 90; + return x; + })()`, + expectedIndexedProperties: [ + ["0", 12], + ["1", 42], + ], + expectedNonIndexedProperties: [ + ["length", 2], + ["foo", 90], + ], + }, + { + evaledObject: `(() => { + x = [12, 42]; + x.length = 3; + return x; + })()`, + expectedIndexedProperties: [ + ["0", 12], + ["1", 42], + ["2", undefined], + ], + expectedNonIndexedProperties: [["length", 3]], + }, + { + evaledObject: `(() => { + x = [12, 42]; + x.length = 1; + return x; + })()`, + expectedIndexedProperties: [["0", 12]], + expectedNonIndexedProperties: [["length", 1]], + }, + { + evaledObject: `(() => { + x = [, 42,,]; + x.foo = 90; + return x; + })()`, + expectedIndexedProperties: [ + ["0", undefined], + ["1", 42], + ["2", undefined], + ], + expectedNonIndexedProperties: [ + ["length", 3], + ["foo", 90], + ], + }, + { + evaledObject: `(() => { + x = Array(2); + x.foo = "bar"; + x.bar = "foo"; + return x; + })()`, + expectedIndexedProperties: [ + ["0", undefined], + ["1", undefined], + ], + expectedNonIndexedProperties: [ + ["length", 2], + ["foo", "bar"], + ["bar", "foo"], + ], + }, + { + evaledObject: `(() => { + x = new Int8Array(new ArrayBuffer(2)); + x.foo = "bar"; + x.bar = "foo"; + return x; + })()`, + expectedIndexedProperties: [ + ["0", 0], + ["1", 0], + ], + expectedNonIndexedProperties: [ + ["foo", "bar"], + ["bar", "foo"], + ["length", 2], + ["buffer", DO_NOT_CHECK_VALUE], + ["byteLength", 2], + ["byteOffset", 0], + ], + }, + { + evaledObject: `(() => { + x = new Int8Array([1, 2]); + Object.defineProperty(x, 'length', {value: 0}); + return x; + })()`, + expectedIndexedProperties: [ + ["0", 1], + ["1", 2], + ], + expectedNonIndexedProperties: [ + ["length", 0], + ["buffer", DO_NOT_CHECK_VALUE], + ["byteLength", 2], + ["byteOffset", 0], + ], + }, + { + evaledObject: `(() => { + x = new Int32Array([1, 2]); + Object.setPrototypeOf(x, null); + return x; + })()`, + expectedIndexedProperties: [ + ["0", 1], + ["1", 2], + ], + expectedNonIndexedProperties: [], + }, + { + evaledObject: `(() => { + return new (class extends Int8Array {})([1, 2]); + })()`, + expectedIndexedProperties: [ + ["0", 1], + ["1", 2], + ], + expectedNonIndexedProperties: [ + ["length", 2], + ["buffer", DO_NOT_CHECK_VALUE], + ["byteLength", 2], + ["byteOffset", 0], + ], + }, + ]; + + for (const test of testCases) { + await test_object_grip(debuggee, client, threadFront, test); + } + }) +); + +async function test_object_grip( + debuggee, + dbgClient, + threadFront, + testData = {} +) { + const { + evaledObject, + expectedIndexedProperties, + expectedNonIndexedProperties, + } = testData; + + const packet = await executeOnNextTickAndWaitForPause(eval_code, threadFront); + + const [grip] = packet.frame.arguments; + + const objClient = threadFront.pauseGrip(grip); + + info(` + Check enumProperties response for + ${ + typeof evaledObject === "string" + ? evaledObject + : JSON.stringify(evaledObject) + } + `); + + // Checks the result of enumProperties. + let response = await objClient.enumProperties({ + ignoreNonIndexedProperties: true, + }); + await check_enum_properties(response, expectedIndexedProperties); + + response = await objClient.enumProperties({ + ignoreIndexedProperties: true, + }); + await check_enum_properties(response, expectedNonIndexedProperties); + + await threadFront.resume(); + + function eval_code() { + // Be sure to run debuggee code in its own HTML 'task', so that when we call + // the onDebuggerStatement hook, the test's own microtasks don't get suspended + // along with the debuggee's. + do_timeout(0, () => { + debuggee.eval(` + stopMe(${ + typeof evaledObject === "string" + ? evaledObject + : JSON.stringify(evaledObject) + }); + `); + }); + } +} + +async function check_enum_properties(iterator, expected = []) { + equal( + iterator.count, + expected.length, + "iterator.count has the expected value" + ); + + info("Check iterator.slice response for all properties"); + const sliceResponse = await iterator.slice(0, iterator.count); + ok( + sliceResponse && + Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"), + "The response object has an ownProperties property" + ); + + const { ownProperties } = sliceResponse; + const names = Object.getOwnPropertyNames(ownProperties); + equal( + names.length, + expected.length, + "The response has the expected number of properties" + ); + for (let i = 0; i < names.length; i++) { + const name = names[i]; + const [key, value] = expected[i]; + equal(name, key, "Property has the expected name"); + const property = ownProperties[name]; + + if (value === DO_NOT_CHECK_VALUE) { + return; + } + + if (value === undefined) { + equal( + property, + undefined, + `Response has no value for the "${key}" property` + ); + } else { + const propValue = property.hasOwnProperty("value") + ? property.value + : property.getterValue; + equal(propValue, value, `Property "${key}" has the expected value`); + } + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-21.js b/devtools/server/tests/xpcshell/test_objectgrips-21.js new file mode 100644 index 0000000000..cfb4f7486f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-21.js @@ -0,0 +1,398 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +// Run test_unsafe_grips twice, one against a system principal debuggee +// and another time with a null principal debuggee + +// The following tests work like this: +// - The specified code is evaluated in a system principal. +// `Cu`, `systemPrincipal` and `Services` are provided as global variables. +// - The resulting object is debugged in a system or null principal debuggee, +// depending on in which list the test is placed. +// It is tested according to the specified test parameters. +// - An ordinary object that inherits from the resulting one is also debugged. +// This is just to check that it can be normally debugged even with an unsafe +// object in the prototype. The specified test parameters do not apply. + +// The following tests are defined via properties with the following defaults. +const defaults = { + // The class of the grip. + class: "Restricted", + + // The stringification of the object + string: "", + + // Whether the object (not its grip) has class "Function". + isFunction: false, + + // Whether the grip has a preview property. + hasPreview: true, + + // Code that assigns the object to be tested into the obj variable. + code: "var obj = {}", + + // The type of the grip of the prototype. + protoType: "null", + + // Whether the object has some own string properties. + hasOwnPropertyNames: false, + + // Whether the object has some own symbol properties. + hasOwnPropertySymbols: false, + + // The descriptor obtained when retrieving property "x" or Symbol("x"). + property: undefined, + + // Code evaluated after the test, whose result is expected to be true. + afterTest: "true == true", +}; + +// The following tests use a system principal debuggee. +const systemPrincipalTests = [ + { + // Dead objects throw a TypeError when accessing properties. + class: "DeadObject", + string: "<dead object>", + code: ` + var obj = Cu.Sandbox(null); + Cu.nukeSandbox(obj); + `, + property: descriptor({ value: "TypeError" }), + }, + { + // This proxy checks that no trap runs (using a second proxy as the handler + // there is no need to maintain a list of all possible traps). + class: "Proxy", + string: "<proxy>", + code: ` + var trapDidRun = false; + var obj = new Proxy({}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + `, + afterTest: "trapDidRun === false", + }, + { + // Like the previous test, but now the proxy has a Function class. + class: "Proxy", + string: "<proxy>", + isFunction: true, + code: ` + var trapDidRun = false; + var obj = new Proxy(function(){}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called.(function)"); + }})); + `, + afterTest: "trapDidRun === false", + }, + { + // Invisisible-to-debugger objects can't be unwrapped, so we don't know if + // they are proxies. Thus they shouldn't be accessed. + class: "InvisibleToDebugger: Array", + string: "<invisibleToDebugger>", + hasPreview: false, + code: ` + var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger: true}); + var obj = s.eval("[1, 2, 3]"); + `, + }, + { + // Like the previous test, but now the object has a Function class. + class: "InvisibleToDebugger: Function", + string: "<invisibleToDebugger>", + isFunction: true, + hasPreview: false, + code: ` + var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger: true}); + var obj = s.eval("(function func(arg){})"); + `, + }, + { + // Cu.Sandbox is a WrappedNative that throws when accessing properties. + class: "nsXPCComponents_utils_Sandbox", + string: "[object nsXPCComponents_utils_Sandbox]", + code: `var obj = Cu.Sandbox;`, + protoType: "object", + }, +]; + +// The following tests run code in a system principal, but the resulting object +// is debugged in a null principal. +const nullPrincipalTests = [ + { + // The null principal gets undefined when attempting to access properties. + string: "[object Object]", + code: `var obj = {x: -1};`, + }, + { + // For arrays it's an error instead of undefined. + string: "[object Object]", + code: `var obj = [1, 2, 3];`, + property: descriptor({ value: "Error" }), + }, + { + // For functions it's also an error. + string: "function func(arg){}", + isFunction: true, + hasPreview: false, + code: `var obj = function func(arg){};`, + property: descriptor({ value: "Error" }), + }, + { + // Check that no proxy trap runs. + string: "[object Object]", + code: ` + var trapDidRun = false; + var obj = new Proxy([], new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + `, + property: descriptor({ value: "Error" }), + afterTest: `trapDidRun === false`, + }, + { + // Like the previous test, but now the object is a callable Proxy. + string: "function () {\n [native code]\n}", + isFunction: true, + hasPreview: false, + code: ` + var trapDidRun = false; + var obj = new Proxy(function(){}, new Proxy({}, {get: (_, trap) => { + trapDidRun = true; + throw new Error("proxy trap '" + trap + "' was called."); + }})); + `, + property: descriptor({ value: "Error" }), + afterTest: `trapDidRun === false`, + }, + { + // Cross-origin Window objects do expose some properties and have a preview. + string: "[object Object]", + code: `var obj = Services.appShell.createWindowlessBrowser().document.defaultView;`, + hasOwnPropertyNames: true, + hasOwnPropertySymbols: true, + property: descriptor({ value: "SecurityError" }), + previewUrl: "about:blank", + }, + { + // Cross-origin Location objects do expose some properties and have a preview. + string: "[object Object]", + code: `var obj = Services.appShell.createWindowlessBrowser().document.defaultView + .location;`, + hasOwnPropertyNames: true, + hasOwnPropertySymbols: true, + property: descriptor({ value: "SecurityError" }), + }, +]; + +function descriptor(descr) { + return Object.assign( + { + configurable: false, + writable: false, + enumerable: false, + value: undefined, + }, + descr + ); +} + +async function test_unsafe_grips( + { threadFront, debuggee, isWorkerServer }, + tests +) { + debuggee.eval( + function stopMe(arg1, arg2) { + debugger; + }.toString() + ); + + for (let data of tests) { + data = { ...defaults, ...data }; + + // Run the code and test the results. + const sandbox = Cu.Sandbox(systemPrincipal); + Object.assign(sandbox, { Services, systemPrincipal, Cu }); + sandbox.eval(data.code); + debuggee.obj = sandbox.obj; + const inherits = `Object.create(obj, { + x: {value: 1}, + [Symbol.for("x")]: {value: 2} + })`; + + const packet = await executeOnNextTickAndWaitForPause( + () => debuggee.eval(`stopMe(obj, ${inherits});`), + threadFront + ); + + const [objGrip, inheritsGrip] = packet.frame.arguments; + for (const grip of [objGrip, inheritsGrip]) { + const isUnsafe = grip === objGrip; + // If `isUnsafe` is true, the parameters in `data` will be used to assert + // against `objGrip`, the grip of the object `obj` created by the test. + // Otherwise, the grip will refer to `inherits`, an ordinary object which + // inherits from `obj`. Then all checks are hardcoded because in every test + // all methods are expected to work the same on `inheritsGrip`. + check_grip(grip, data, isUnsafe, isWorkerServer); + + const objClient = threadFront.pauseGrip(grip); + let response, slice; + + response = await objClient.getPrototypeAndProperties(); + check_properties(response.ownProperties, data, isUnsafe); + check_symbols(response.ownSymbols, data, isUnsafe); + check_prototype(response.prototype, data, isUnsafe, isWorkerServer); + + response = await objClient.enumProperties({ + ignoreIndexedProperties: true, + }); + slice = await response.slice(0, response.count); + check_properties(slice.ownProperties, data, isUnsafe); + + response = await objClient.enumProperties({}); + slice = await response.slice(0, response.count); + check_properties(slice.ownProperties, data, isUnsafe); + + response = await objClient.getProperty("x"); + check_property(response.descriptor, data, isUnsafe); + + response = await objClient.enumSymbols(); + slice = await response.slice(0, response.count); + check_symbol_names(slice.ownSymbols, data, isUnsafe); + + response = await objClient.getProperty(Symbol.for("x")); + check_symbol(response.descriptor, data, isUnsafe); + + response = await objClient.getPrototype(); + check_prototype(response.prototype, data, isUnsafe, isWorkerServer); + + await objClient.release(); + } + + await threadFront.resume(); + + ok(sandbox.eval(data.afterTest), "Check after test passes"); + } +} + +function check_grip(grip, data, isUnsafe, isWorkerServer) { + if (isUnsafe) { + strictEqual(grip.class, data.class, "The grip has the proper class."); + strictEqual("preview" in grip, data.hasPreview, "Check preview presence."); + // preview.url isn't populated on worker server. + if (data.previewUrl && !isWorkerServer) { + console.trace(); + strictEqual( + grip.preview.url, + data.previewUrl, + `Check preview.url for "${data.code}".` + ); + } + } else { + strictEqual(grip.class, "Object", "The grip has 'Object' class."); + ok("preview" in grip, "The grip has a preview."); + } +} + +function check_properties(props, data, isUnsafe) { + const propNames = Reflect.ownKeys(props); + check_property_names(propNames, data, isUnsafe); + if (isUnsafe) { + deepEqual(props.x, undefined, "The property does not exist."); + } else { + strictEqual(props.x.value, 1, "The property has the right value."); + } +} + +function check_property_names(props, data, isUnsafe) { + if (isUnsafe) { + strictEqual( + !!props.length, + data.hasOwnPropertyNames, + "Check presence of own string properties." + ); + } else { + strictEqual(props.length, 1, "1 own property was retrieved."); + strictEqual(props[0], "x", "The property has the right name."); + } +} + +function check_property(descr, data, isUnsafe) { + if (isUnsafe) { + deepEqual(descr, data.property, "Got the right property descriptor."); + } else { + strictEqual(descr.value, 1, "The property has the right value."); + } +} + +function check_symbols(symbols, data, isUnsafe) { + check_symbol_names(symbols, data, isUnsafe); + if (!isUnsafe) { + check_symbol(symbols[0].descriptor, data, isUnsafe); + } +} + +function check_symbol_names(props, data, isUnsafe) { + if (isUnsafe) { + strictEqual( + !!props.length, + data.hasOwnPropertySymbols, + "Check presence of own symbol properties." + ); + } else { + strictEqual(props.length, 1, "1 own symbol property was retrieved."); + strictEqual(props[0].name, "Symbol(x)", "The symbol has the right name."); + } +} + +function check_symbol(descr, data, isUnsafe) { + if (isUnsafe) { + deepEqual( + descr, + data.property, + "Got the right symbol property descriptor." + ); + } else { + strictEqual(descr.value, 2, "The symbol property has the right value."); + } +} + +function check_prototype(proto, data, isUnsafe, isWorkerServer) { + const protoGrip = proto && proto.getGrip ? proto.getGrip() : proto; + if (isUnsafe) { + deepEqual(protoGrip.type, data.protoType, "Got the right prototype type."); + } else { + check_grip(protoGrip, data, true, isWorkerServer); + } +} + +// threadFrontTest uses systemPrincipal by default, but let's be explicit here. +add_task( + threadFrontTest( + options => { + return test_unsafe_grips(options, systemPrincipalTests, "system"); + }, + { principal: systemPrincipal } + ) +); + +const nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); +add_task( + threadFrontTest( + options => { + return test_unsafe_grips(options, nullPrincipalTests, "null"); + }, + { principal: nullPrincipal } + ) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-22.js b/devtools/server/tests/xpcshell/test_objectgrips-22.js new file mode 100644 index 0000000000..34264f5534 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-22.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + const objClient = threadFront.pauseGrip(grip); + const iterator = await objClient.enumSymbols(); + const { ownSymbols } = await iterator.slice(0, iterator.count); + + strictEqual(ownSymbols.length, 1, "There is 1 symbol property."); + const { name, descriptor } = ownSymbols[0]; + strictEqual(name, "Symbol(sym)", "Got right symbol name."); + deepEqual( + descriptor, + { + configurable: false, + enumerable: false, + writable: false, + value: 1, + }, + "Got right property descriptor." + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + debuggee.eval( + `stopMe(Object.defineProperty({}, Symbol("sym"), {value: 1}));` + ); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-23.js b/devtools/server/tests/xpcshell/test_objectgrips-23.js new file mode 100644 index 0000000000..b44beb2c2d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-23.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that ES6 classes grip have the expected properties. + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + strictEqual( + grip.class, + "Function", + `Grip has expected value for "class" property` + ); + strictEqual( + grip.isClassConstructor, + true, + `Grip has expected value for "isClassConstructor" property` + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval(` + class MyClass {}; + stopMe(MyClass); + + function stopMe(arg1) { + debugger; + } + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-24.js b/devtools/server/tests/xpcshell/test_objectgrips-24.js new file mode 100644 index 0000000000..9d541c108d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-24.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that ES6 classes grip have the expected properties. + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + debuggee.eval( + function stopMe() { + debugger; + }.toString() + ); + + const tests = [ + { + fn: `function(){}`, + isAsync: false, + isGenerator: false, + }, + { + fn: `async function(){}`, + isAsync: true, + isGenerator: false, + }, + { + fn: `function *(){}`, + isAsync: false, + isGenerator: true, + }, + { + fn: `async function *(){}`, + isAsync: true, + isGenerator: true, + }, + ]; + + for (const { fn, isAsync, isGenerator } of tests) { + const packet = await executeOnNextTickAndWaitForPause( + () => debuggee.eval(`stopMe(${fn})`), + threadFront + ); + const [grip] = packet.frame.arguments; + strictEqual(grip.class, "Function"); + strictEqual(grip.isAsync, isAsync); + strictEqual(grip.isGenerator, isGenerator); + + await threadFront.resume(); + } + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-25.js b/devtools/server/tests/xpcshell/test_objectgrips-25.js new file mode 100644 index 0000000000..f80572bb19 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-25.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test object with private properties (preview + enumPrivateProperties) + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(obj) { + debugger; + }.toString() + ); + debuggee.eval(` + class MyClass { + constructor(name, password) { + this.name = name; + this.#password = password; + } + + #password; + #salt = "sEcr3t"; + #getSaltedPassword() { + return this.#password + this.#salt; + } + } + + stopMe(new MyClass("Susie", "p4$$w0rD")); + `); +} + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + + let { privateProperties } = grip.preview; + strictEqual( + privateProperties.length, + 2, + "There is 2 private properties in the grip preview" + ); + let [password, salt] = privateProperties; + + strictEqual( + password.name, + "#password", + "Got expected name for #password private property in preview" + ); + deepEqual( + password.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "p4$$w0rD", + }, + "Got expected property descriptor for #password in preview" + ); + + strictEqual( + salt.name, + "#salt", + "Got expected name for #salt private property in preview" + ); + deepEqual( + salt.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "sEcr3t", + }, + "Got expected property descriptor for #salt in preview" + ); + + const objClient = threadFront.pauseGrip(grip); + const iterator = await objClient.enumPrivateProperties(); + ({ privateProperties } = await iterator.slice(0, iterator.count)); + + strictEqual( + privateProperties.length, + 2, + "enumPrivateProperties returned 2 private properties." + ); + [password, salt] = privateProperties; + + strictEqual( + password.name, + "#password", + "Got expected name for #password private property via enumPrivateProperties" + ); + deepEqual( + password.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "p4$$w0rD", + }, + "Got expected property descriptor for #password via enumPrivateProperties" + ); + + strictEqual( + salt.name, + "#salt", + "Got expected name for #salt private property via enumPrivateProperties" + ); + deepEqual( + salt.descriptor, + { + configurable: true, + enumerable: false, + writable: true, + value: "sEcr3t", + }, + "Got expected property descriptor for #salt via enumPrivateProperties" + ); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js new file mode 100644 index 0000000000..f576f16a5e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-01.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + const objectFront = threadFront.pauseGrip(arg1); + + const obj1 = ( + await objectFront.getPropertyValue("obj1", null) + ).value.return.getGrip(); + const obj2 = ( + await objectFront.getPropertyValue("obj2", null) + ).value.return.getGrip(); + + info(`Retrieve "context" function reference`); + const context = (await objectFront.getPropertyValue("context", null)).value + .return; + info(`Retrieve "sum" function reference`); + const sum = (await objectFront.getPropertyValue("sum", null)).value.return; + info(`Retrieve "error" function reference`); + const error = (await objectFront.getPropertyValue("error", null)).value + .return; + + assert_response(await context.apply(obj1, [obj1]), { + return: "correct context", + }); + assert_response(await context.apply(obj2, [obj2]), { + return: "correct context", + }); + assert_response(await context.apply(obj1, [obj2]), { + return: "wrong context", + }); + assert_response(await context.apply(obj2, [obj1]), { + return: "wrong context", + }); + // eslint-disable-next-line no-useless-call + assert_response(await sum.apply(null, [1, 2, 3, 4, 5, 6, 7]), { + return: 1 + 2 + 3 + 4 + 5 + 6 + 7, + }); + // eslint-disable-next-line no-useless-call + assert_response(await error.apply(null, []), { + throw: "an error", + }); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + obj1: {}, + obj2: {}, + context(arg) { + return this === arg ? "correct context" : "wrong context"; + }, + sum(...parts) { + return parts.reduce((acc, v) => acc + v, 0); + }, + error() { + throw "an error"; + }, + }); + `); +} + +function assert_response({ value }, expected) { + assert_completion(value, expected); +} + +function assert_completion(value, expected) { + if (expected && "return" in expected) { + assert_value(value.return, expected.return); + } + if (expected && "throw" in expected) { + assert_value(value.throw, expected.throw); + } + if (!expected) { + assert_value(value, expected); + } +} + +function assert_value(actual, expected) { + Assert.equal(typeof actual, typeof expected); + + if (typeof expected === "object") { + // Note: We aren't using deepEqual here because we're only doing a cursory + // check of a few properties, not a full comparison of the result, since + // the full outputs includes stuff like preview info that we don't need. + for (const key of Object.keys(expected)) { + assert_value(actual[key], expected[key]); + } + } else { + Assert.equal(actual, expected); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js new file mode 100644 index 0000000000..743286281c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-02.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + await threadFront.pauseGrip(arg1).threadGrip(); + const obj = arg1; + await threadFront.resume(); + + const objectFront = threadFront.pauseGrip(obj); + + const method = (await objectFront.getPropertyValue("method", null)).value + .return; + + const methodCalled = method.apply(obj, []); + + // Ensure that we actually paused at the `debugger;` line. + const packet2 = await waitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.frame.where.column, 8); + + await threadFront.resume(); + await methodCalled; + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + method(){ + debugger; + }, + }); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js new file mode 100644 index 0000000000..6a3e919661 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-fn-apply-03.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + await threadFront.pauseGrip(arg1).threadGrip(); + const obj = arg1; + + const objectFront = threadFront.pauseGrip(obj); + + const method = (await objectFront.getPropertyValue("method", null)).value + .return; + + try { + await method.apply(obj, []); + Assert.ok(false, "expected exception"); + } catch (err) { + Assert.ok(!!err.message.match(/debugee object is not callable/)); + } + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + method: {}, + }); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js b/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js new file mode 100644 index 0000000000..b60b7328c2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-nested-promise.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip1, grip2] = packet.frame.arguments; + strictEqual(grip1.class, "Promise", "promise1 has a promise grip."); + strictEqual(grip2.class, "Promise", "promise2 has a promise grip."); + + const objClient1 = threadFront.pauseGrip(grip1); + const objClient2 = threadFront.pauseGrip(grip2); + const { promiseState: state1 } = await objClient1.getPromiseState(); + const { promiseState: state2 } = await objClient2.getPromiseState(); + + strictEqual(state1.state, "fulfilled", "promise1 was fulfilled."); + strictEqual(state1.value, objClient2, "promise1 fulfilled with promise2."); + ok(!state1.hasOwnProperty("reason"), "promise1 has no rejection reason."); + + strictEqual(state2.state, "rejected", "promise2 was rejected."); + strictEqual(state2.reason, objClient1, "promise2 rejected with promise1."); + ok(!state2.hasOwnProperty("value"), "promise2 has no resolution value."); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg) { + debugger; + }.toString() + ); + + debuggee.eval(` + var resolve; + var promise1 = new Promise(r => {resolve = r}); + Object.setPrototypeOf(promise1, null); + var promise2 = Promise.reject(promise1); + promise2.catch(() => {}); + Object.setPrototypeOf(promise2, null); + resolve(promise2); + stopMe(promise1, promise2); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js b/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js new file mode 100644 index 0000000000..5b0667c055 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-nested-proxy.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + const objClient = threadFront.pauseGrip(grip); + const { proxyTarget, proxyHandler } = await objClient.getProxySlots(); + + strictEqual(grip.class, "Proxy", "Its a proxy grip."); + strictEqual( + proxyTarget.getGrip().class, + "Proxy", + "The target is also a proxy." + ); + strictEqual( + proxyHandler.getGrip().class, + "Proxy", + "The handler is also a proxy." + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg) { + debugger; + }.toString() + ); + + debuggee.eval(` + var proxy = new Proxy({}, {}); + for (let i = 0; i < 1e5; ++i) + proxy = new Proxy(proxy, proxy); + stopMe(proxy); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js new file mode 100644 index 0000000000..69da96a741 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-01.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + const objFront = threadFront.pauseGrip(arg1); + + const expectedValues = { + stringProp: { + return: "a value", + }, + stringNormal: { + return: "a value", + }, + stringAbrupt: { + throw: "a value", + }, + objectNormal: { + return: { + _grip: { + type: "object", + class: "Object", + ownPropertyLength: 1, + preview: { + kind: "Object", + ownProperties: { + prop: { + value: 4, + }, + }, + }, + }, + }, + }, + objectAbrupt: { + throw: { + _grip: { + type: "object", + class: "Object", + ownPropertyLength: 1, + preview: { + kind: "Object", + ownProperties: { + prop: { + value: 4, + }, + }, + }, + }, + }, + }, + context: { + return: "correct context", + }, + method: { + return: { + _grip: { + type: "object", + class: "Function", + name: "method", + }, + }, + }, + }; + + for (const [key, expected] of Object.entries(expectedValues)) { + const { value } = await objFront.getPropertyValue(key, null); + assert_completion(value, expected); + } + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + var obj = { + stringProp: "a value", + get stringNormal(){ + return "a value"; + }, + get stringAbrupt() { + throw "a value"; + }, + get objectNormal() { + return { prop: 4 }; + }, + get objectAbrupt() { + throw { prop: 4 }; + }, + get context(){ + return this === obj ? "correct context" : "wrong context"; + }, + method() { + return "a value"; + }, + }; + stopMe(obj); + `); +} + +function assert_completion(value, expected) { + if (expected && "return" in expected) { + assert_value(value.return, expected.return); + } + if (expected && "throw" in expected) { + assert_value(value.throw, expected.throw); + } + if (!expected) { + assert_value(value, expected); + } +} + +function assert_value(actual, expected) { + Assert.equal(typeof actual, typeof expected); + + if (typeof expected === "object") { + // Note: We aren't using deepEqual here because we're only doing a cursory + // check of a few properties, not a full comparison of the result, since + // the full outputs includes stuff like preview info that we don't need. + for (const key of Object.keys(expected)) { + assert_value(actual[key], expected[key]); + } + } else { + Assert.equal(actual, expected); + } +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js new file mode 100644 index 0000000000..bc7337128c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-02.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const arg1 = packet.frame.arguments[0]; + Assert.equal(arg1.class, "Object"); + + const obj = threadFront.pauseGrip(arg1); + await obj.threadGrip(); + + const objClient = obj; + await threadFront.resume(); + + const objClientCalled = objClient.getPropertyValue("prop", null); + + // Ensure that we actually paused at the `debugger;` line. + const packet2 = await waitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.frame.where.column, 8); + + await threadFront.resume(); + await objClientCalled; + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arg1) { + debugger; + }.toString() + ); + + debuggee.eval(` + stopMe({ + get prop(){ + debugger; + }, + }); + `); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js b/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js new file mode 100644 index 0000000000..e9b130db79 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-property-value-03.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const { frame } = packet; + try { + const grips = frame.arguments; + const objClient = threadFront.pauseGrip(grips[0]); + const classes = [ + "Object", + "Object", + "Array", + "Boolean", + "Number", + "String", + ]; + for (const [i, grip] of grips.entries()) { + Assert.equal(grip.class, classes[i]); + await check_getter(objClient, grip.actor, i); + } + await check_getter(objClient, null, 0); + await check_getter(objClient, "invalid receiver actorId", 0); + } finally { + await threadFront.resume(); + } + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe() { + debugger; + }.toString() + ); + + debuggee.eval(` + var obj = { + get getter() { + return objects.indexOf(this); + }, + }; + var objects = [obj, {}, [], new Boolean(), new Number(), new String()]; + stopMe(...objects); + `); +} + +async function check_getter(objClient, receiverId, expected) { + const { value } = await objClient.getPropertyValue("getter", receiverId); + Assert.equal(value.return, expected); +} diff --git a/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js b/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js new file mode 100644 index 0000000000..76a6b32f4b --- /dev/null +++ b/devtools/server/tests/xpcshell/test_objectgrips-sparse-array.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const [grip] = packet.frame.arguments; + await threadFront.resume(); + + strictEqual(grip.class, "Array", "The grip has an Array class"); + + const { items } = grip.preview; + strictEqual(items[0], null, "The empty slot has null as grip preview"); + deepEqual( + items[1], + { type: "undefined" }, + "The undefined value has grip value of type undefined" + ); + }) +); + +function evalCode(debuggee) { + debuggee.eval( + function stopMe(arr) { + debugger; + }.toString() + ); + debuggee.eval("stopMe([, undefined])"); +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-01.js b/devtools/server/tests/xpcshell/test_pause_exceptions-01.js new file mode 100644 index 0000000000..74bbae55c3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-01.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting pauseOnExceptions to true will cause the debuggee to pause + * when an exception is thrown. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + const packet = await resumeAndWaitForPause(threadFront); + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, 42); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + /* eslint-disable no-throw-literal */ + // prettier-ignore + debuggee.eval("(" + function () { + function stopMe() { + debugger; + throw 42; + } + try { + stopMe(); + } catch (e) {} + } + ")()"); + /* eslint-enable no-throw-literal */ +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-02.js b/devtools/server/tests/xpcshell/test_pause_exceptions-02.js new file mode 100644 index 0000000000..00631b071f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-02.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting pauseOnExceptions to true when the debugger isn't in a + * paused state will not cause the debuggee to pause when an exception is thrown. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, commands }) => { + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, 42); + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + /* eslint-disable no-throw-literal */ + // prettier-ignore + debuggee.eval("(" + function () { // 1 + function stopMe() { // 2 + throw 42; // 3 + } // 4 + try { // 5 + stopMe(); // 6 + } catch (e) {} // 7 + } + ")()"); +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-03.js b/devtools/server/tests/xpcshell/test_pause_exceptions-03.js new file mode 100644 index 0000000000..4fb13f4cf9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-03.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that setting pauseOnExceptions to true will cause the debuggee to pause + * when an exception is thrown. + */ + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, commands }) => { + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: false, + }); + await resume(threadFront); + const paused = await waitForPause(threadFront); + Assert.equal(paused.why.type, "exception"); + equal(paused.frame.where.line, 4, "paused at throw"); + + await resume(threadFront); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function stopMe() { // 2 + debugger; // 3 + throw 42; // 4 + } // 5 + try { // 6 + stopMe(); // 7 + } catch (e) {}`, // 8 + debuggee, + "1.8", + "test_pause_exceptions-03.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_pause_exceptions-04.js b/devtools/server/tests/xpcshell/test_pause_exceptions-04.js new file mode 100644 index 0000000000..6246b112e0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pause_exceptions-04.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { waitForTick } = require("resource://devtools/shared/DevToolsUtils.js"); + +/** + * Test that setting pauseOnExceptions to true and then to false will not cause + * the debuggee to pause when an exception is thrown. + */ + +add_task( + threadFrontTest( + async ({ threadFront, client, debuggee, commands }) => { + let onResume = null; + let packet = null; + + threadFront.once("paused", function (pkt) { + packet = pkt; + onResume = threadFront.resume(); + }); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: true, + }); + + await evaluateTestCode(debuggee, "42"); + + await onResume; + + Assert.equal(!!packet, true); + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, "42"); + packet = null; + + threadFront.once("paused", function (pkt) { + packet = pkt; + onResume = threadFront.resume(); + }); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: false, + ignoreCaughtExceptions: true, + }); + + await evaluateTestCode(debuggee, "43"); + + // Test that the paused listener callback hasn't been called + // on the thrown error from dontStopMe() + Assert.equal(!!packet, false); + + await commands.threadConfigurationCommand.updateConfiguration({ + pauseOnExceptions: true, + ignoreCaughtExceptions: true, + }); + + await evaluateTestCode(debuggee, "44"); + + await onResume; + + // Test that the paused listener callback has been called + // on the thrown error from stopMeAgain() + Assert.equal(!!packet, true); + Assert.equal(packet.why.type, "exception"); + Assert.equal(packet.why.exception, "44"); + }, + { + // Bug 1508289, exception tests fails in worker scope + doNotRunWorker: true, + } + ) +); + +async function evaluateTestCode(debuggee, throwValue) { + await waitForTick(); + try { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function stopMeAgain() { // 2 + throw ${throwValue}; // 3 + } // 4 + stopMeAgain(); // 5 + `, // 6 + debuggee, + "1.8", + "test_pause_exceptions-04.js", + 1 + ); + } catch (e) {} +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-01.js b/devtools/server/tests/xpcshell/test_pauselifetime-01.js new file mode 100644 index 0000000000..db20a02521 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-01.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that pause-lifetime grips go away correctly after a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const pauseActor = packet.actor; + + // Make a bogus request to the pause-lifetime actor. Should get + // unrecognized-packet-type (and not no-such-actor). + try { + await client.request({ to: pauseActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "unrecognizedPacketType"); + } + + await threadFront.resume(); + + // Now that we've resumed, should get no-such-actor for the + // same request. + try { + await client.request({ to: pauseActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "noSuchActor"); + } + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe() { + debugger; + } + stopMe(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-02.js b/devtools/server/tests/xpcshell/test_pauselifetime-02.js new file mode 100644 index 0000000000..e936df6177 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-02.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that pause-lifetime grips go away correctly after a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + const objActor = args[0].actor; + Assert.equal(args[0].class, "Object"); + Assert.ok(!!objActor); + + // Make a bogus request to the grip actor. Should get + // unrecognized-packet-type (and not no-such-actor). + try { + await client.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "unrecognizedPacketType"); + } + + await threadFront.resume(); + + // Now that we've resumed, should get no-such-actor for the + // same request. + try { + await client.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.equal(e.error, "noSuchActor"); + } + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(obj) { + debugger; + } + stopMe({ foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-03.js b/devtools/server/tests/xpcshell/test_pauselifetime-03.js new file mode 100644 index 0000000000..558ac8b910 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-03.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that pause-lifetime grip clients are marked invalid after a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + const objActor = args[0].actor; + Assert.equal(args[0].class, "Object"); + Assert.ok(!!objActor); + + const objectFront = threadFront.pauseGrip(args[0]); + Assert.ok(objectFront.valid); + + // Make a bogus request to the grip actor. Should get + // unrecognized-packet-type (and not no-such-actor). + try { + const objFront = client.getFrontByID(objActor); + await objFront.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.ok(!!e.message.match(/unrecognizedPacketType/)); + } + Assert.ok(objectFront.valid); + + await threadFront.resume(); + + // Now that we've resumed, should get no-such-actor for the + // same request. + try { + const objFront = client.getFrontByID(objActor); + await objFront.request({ to: objActor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + Assert.ok(!!e.message.match(/noSuchActor/)); + } + Assert.ok(!objectFront.valid); + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(obj) { + debugger; + } + stopMe({ foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_pauselifetime-04.js b/devtools/server/tests/xpcshell/test_pauselifetime-04.js new file mode 100644 index 0000000000..7d226260f0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_pauselifetime-04.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that requesting a pause actor for the same value multiple + * times returns the same actor. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const args = packet.frame.arguments; + const objActor1 = args[0].actor; + + const response = await threadFront.getFrames(0, 1); + const frame = response.frames[0]; + Assert.equal(objActor1, frame.arguments[0].actor); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(obj) { + debugger; + } + stopMe({ foo: "bar" }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_promise_state-01.js b/devtools/server/tests/xpcshell/test_promise_state-01.js new file mode 100644 index 0000000000..d02b64a67e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promise_state-01.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable max-nested-callbacks */ + +"use strict"; + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * pending. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const grip = environment.bindings.variables.p.value; + + ok(grip.preview); + equal(grip.class, "Promise"); + equal(grip.preview.ownProperties["<state>"].value, "pending"); + + const objClient = threadFront.pauseGrip(grip); + const { promiseState } = await objClient.getPromiseState(); + equal(promiseState.state, "pending"); + }) +); + +function evalCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "doTest();\n" + + function doTest() { + var p = new Promise(function () {}); + debugger; + }, + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_promise_state-02.js b/devtools/server/tests/xpcshell/test_promise_state-02.js new file mode 100644 index 0000000000..e1219f545c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promise_state-02.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable max-nested-callbacks */ + +"use strict"; + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * fulfilled. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const grip = environment.bindings.variables.p.value; + + ok(grip.preview); + equal(grip.class, "Promise"); + equal(grip.preview.ownProperties["<state>"].value, "fulfilled"); + equal( + grip.preview.ownProperties["<value>"].value.actorID, + packet.frame.arguments[0].actorID, + "The promise's fulfilled state value in the preview should be the same " + + "value passed to the then function" + ); + + const objClient = threadFront.pauseGrip(grip); + const { promiseState } = await objClient.getPromiseState(); + equal(promiseState.state, "fulfilled"); + equal( + promiseState.value.getGrip().actorID, + packet.frame.arguments[0].actorID, + "The promise's fulfilled state value in getPromiseState() should be " + + "the same value passed to the then function" + ); + }) +); + +function evalCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "doTest();\n" + + function doTest() { + var resolved = Promise.resolve({}); + resolved.then(() => { + var p = resolved; + debugger; + }); + }, + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_promise_state-03.js b/devtools/server/tests/xpcshell/test_promise_state-03.js new file mode 100644 index 0000000000..8ec1fa3717 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promise_state-03.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable max-nested-callbacks */ + +"use strict"; + +/** + * Test that the preview in a Promise's grip is correct when the Promise is + * rejected. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + const environment = await packet.frame.getEnvironment(); + const grip = environment.bindings.variables.p.value; + ok(grip.preview); + equal(grip.class, "Promise"); + equal(grip.preview.ownProperties["<state>"].value, "rejected"); + equal( + grip.preview.ownProperties["<reason>"].value.actorID, + packet.frame.arguments[0].actorID, + "The promise's rejected state reason in the preview should be the same " + + "value passed to the then function" + ); + + const objClient = threadFront.pauseGrip(grip); + const { promiseState } = await objClient.getPromiseState(); + equal(promiseState.state, "rejected"); + equal( + promiseState.reason.getGrip().actorID, + packet.frame.arguments[0].actorID, + "The promise's rejected state value in getPromiseState() should be " + + "the same value passed to the then function" + ); + }) +); + +function evalCode(debuggee) { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "doTest();\n" + + function doTest() { + var resolved = Promise.reject(new Error("uh oh")); + resolved.catch(() => { + var p = resolved; + debugger; + }); + }, + debuggee + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ +} diff --git a/devtools/server/tests/xpcshell/test_promises_run_to_completion.js b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js new file mode 100644 index 0000000000..4d1e8745fe --- /dev/null +++ b/devtools/server/tests/xpcshell/test_promises_run_to_completion.js @@ -0,0 +1,132 @@ +// Bug 1145201: Promise then-handlers can still be executed while the debugger is paused. +// +// When a promise is resolved, for each of its callbacks, a microtask is queued +// to run the callback. At various points, the HTML spec says the browser must +// "perform a microtask checkpoint", which means to draw microtasks from the +// queue and run them, until the queue is empty. +// +// The HTML spec is careful to perform a microtask checkpoint directly after +// each invocation of an event handler or DOM callback, so that code using +// promises can trust that its promise callbacks run promptly, in a +// deterministic order, without DOM events or other outside influences +// intervening. +// +// When the JavaScript debugger interrupts the execution of debuggee content +// code, it naturally must process events for its own user interface and promise +// callbacks. However, it must not run any debuggee microtasks. The debuggee has +// been interrupted in the midst of executing some other code, and the +// JavaScript spec promises developers: "Once execution of a Job is initiated, +// the Job always executes to completion. No other Job may be initiated until +// the currently running Job completes." [1] This promise would be broken if the +// debugger's own event processing ran debuggee microtasks during the +// interruption. +// +// Looking at things from the other side, a microtask checkpoint must be +// performed before returning from a debugger callback, rather than being put +// off until the debuggee performs its next microtask checkpoint, so that +// debugger microtasks are not interleaved with debuggee microtasks. A debuggee +// microtask could hit a breakpoint or otherwise re-enter the debugger, which +// might be quite surprised to see a new debugger callback begin before its +// previous promise callbacks could finish. +// +// [1]: https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues + +"use strict"; + +const Debugger = require("Debugger"); + +function test_promises_run_to_completion() { + const g = createTestGlobal( + "test global for test_promises_run_to_completion.js" + ); + const dbg = new Debugger(g); + g.Assert = Assert; + const log = [""]; + g.log = log; + + dbg.onDebuggerStatement = function handleDebuggerStatement(frame) { + dbg.onDebuggerStatement = undefined; + + // Exercise the promise machinery: resolve a promise and perform a microtask + // queue. When called from a debugger hook, the debuggee's microtasks should not + // run. + log[0] += "debug-handler("; + Promise.resolve(42).then(v => { + Assert.equal( + v, + 42, + "debugger callback promise handler got the right value" + ); + log[0] += "debug-react"; + }); + log[0] += "("; + force_microtask_checkpoint(); + log[0] += ")"; + + Promise.resolve(42).then(v => { + // The microtask running this callback should be handled as we leave the + // onDebuggerStatement Debugger callback, and should not be interleaved + // with debuggee microtasks. + log[0] += "(trailing)"; + }); + + log[0] += ")"; + }; + + // Evaluate some debuggee code that resolves a promise, and then enters the debugger. + Cu.evalInSandbox( + ` + log[0] += "eval("; + Promise.resolve(42).then(function debuggeePromiseCallback(v) { + Assert.equal(v, 42, "debuggee promise handler got the right value"); + // Debugger microtask checkpoints must not run debuggee microtasks, so + // this callback should run at the next microtask checkpoint *not* + // performed by the debugger. + log[0] += "eval-react"; + }); + + log[0] += "debugger("; + debugger; + log[0] += "))"; + `, + g + ); + + // Let other microtasks run. This should run the debuggee's promise callback. + log[0] += "final("; + force_microtask_checkpoint(); + log[0] += ")"; + + Assert.equal( + log[0], + `\ +eval(\ +debugger(\ +debug-handler(\ +(debug-react)\ +)\ +(trailing)\ +))\ +final(\ +eval-react\ +)`, + "microtasks ran as expected" + ); + + run_next_test(); +} + +function force_microtask_checkpoint() { + // Services.tm.spinEventLoopUntilEmpty only performs a microtask checkpoint if + // there is actually an event to run. So make one up. + let ran = false; + Services.tm.dispatchToMainThread(() => { + ran = true; + }); + Services.tm.spinEventLoopUntil( + "Test(test_promises_run_to_completion.js:force_microtask_checkpoint)", + () => ran + ); +} + +add_test(test_promises_run_to_completion); diff --git a/devtools/server/tests/xpcshell/test_register_actor.js b/devtools/server/tests/xpcshell/test_register_actor.js new file mode 100644 index 0000000000..f38ab73572 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_register_actor.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function run_test() { + // Allow incoming connections. + DevToolsServer.keepAlive = true; + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + add_test(test_lazy_api); + add_test(manual_remove); + add_test(cleanup); + run_next_test(); +} + +// Bug 988237: Test the new lazy actor actor-register +function test_lazy_api() { + let isActorLoaded = false; + let isActorInstantiated = false; + function onActorEvent(subject, topic, data) { + if (data == "loaded") { + isActorLoaded = true; + } else if (data == "instantiated") { + isActorInstantiated = true; + } + } + Services.obs.addObserver(onActorEvent, "actor"); + ActorRegistry.registerModule("xpcshell-test/registertestactors-lazy", { + prefix: "lazy", + constructor: "LazyActor", + type: { global: true, target: true }, + }); + // The actor is immediatly registered, but not loaded + Assert.ok( + ActorRegistry.targetScopedActorFactories.hasOwnProperty("lazyActor") + ); + Assert.ok(ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + Assert.ok(!isActorLoaded); + Assert.ok(!isActorInstantiated); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + client.connect().then(function onConnect() { + client.mainRoot.rootForm.then(onRootForm); + }); + function onRootForm(response) { + // On rootForm, the actor is still not loaded, + // but we can see its name in the list of available actors + Assert.ok(!isActorLoaded); + Assert.ok(!isActorInstantiated); + Assert.ok("lazyActor" in response); + + const { LazyFront } = require("xpcshell-test/registertestactors-lazy"); + const front = new LazyFront(client); + // As this Front isn't instantiated by protocol.js, we have to manually + // set its actor ID and manage it: + front.actorID = response.lazyActor; + client.addActorPool(front); + front.manage(front); + + front.hello().then(onRequest); + } + function onRequest(response) { + Assert.equal(response, "world"); + + // Finally, the actor is loaded on the first request being made to it + Assert.ok(isActorLoaded); + Assert.ok(isActorInstantiated); + + Services.obs.removeObserver(onActorEvent, "actor"); + client.close().then(() => run_next_test()); + } +} + +function manual_remove() { + Assert.ok(ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + ActorRegistry.removeGlobalActor("lazyActor"); + Assert.ok(!ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + + run_next_test(); +} + +function cleanup() { + DevToolsServer.destroy(); + + // Check that all actors are unregistered on server destruction + Assert.ok( + !ActorRegistry.targetScopedActorFactories.hasOwnProperty("lazyActor") + ); + Assert.ok(!ActorRegistry.globalActorFactories.hasOwnProperty("lazyActor")); + + run_next_test(); +} diff --git a/devtools/server/tests/xpcshell/test_requestTypes.js b/devtools/server/tests/xpcshell/test_requestTypes.js new file mode 100644 index 0000000000..8787ae5f85 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_requestTypes.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { rootSpec } = require("resource://devtools/shared/specs/root.js"); +const { + generateRequestTypes, +} = require("resource://devtools/shared/protocol/Actor.js"); + +add_task(async function () { + DevToolsServer.init(); + DevToolsServer.registerAllActors(); + + const client = new DevToolsClient(DevToolsServer.connectPipe()); + await client.connect(); + + const response = await client.mainRoot.requestTypes(); + const expectedRequestTypes = Object.keys(generateRequestTypes(rootSpec)); + + Assert.ok(Array.isArray(response.requestTypes)); + Assert.equal( + JSON.stringify(response.requestTypes), + JSON.stringify(expectedRequestTypes) + ); + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_restartFrame-01.js b/devtools/server/tests/xpcshell/test_restartFrame-01.js new file mode 100644 index 0000000000..cb13ae2d7e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_restartFrame-01.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check restarting a frame and stepping out of the + * restarted frame. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +async function restartFrame0(dbg, func, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into a()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn]); + + info("restart the youngest frame a()"); + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[0].actorID; + const packet = await restartFrame(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + "pause location in the restarted frame a()" + ); +} + +async function restartFrame1(dbg, func, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into b()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn, stepIn]); + + info("restart the frame with index 1"); + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[1].actorID; + const packet = await restartFrame(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + "pause location in the restarted frame c()" + ); +} + +async function stepOutRestartedFrame( + dbg, + restartedFrameName, + expectedLocation, + expectedCallstackLength +) { + const { threadFront } = dbg; + const { frames } = await threadFront.frames(0, 5); + + Assert.equal( + frames.length, + expectedCallstackLength, + `the callstack length after restarting frame ${restartedFrameName}()` + ); + + info(`step out of the restarted frame ${restartedFrameName}()`); + const frameActorID = frames[0].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual(getPauseLocation(packet), expectedLocation, `step out location`); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + + info(`Test restarting the youngest frame`); + await restartFrame0(dbg, "arithmetic", { line: 7, column: 2 }); + await stepOutRestartedFrame(dbg, "a", { line: 16, column: 8 }, 3); + await dbg.threadFront.resume(); + + info(`Test restarting the frame with the index 1`); + await restartFrame1(dbg, "nested", { line: 30, column: 2 }); + await stepOutRestartedFrame(dbg, "c", { line: 36, column: 0 }, 3); + await dbg.threadFront.resume(); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_safe-getter.js b/devtools/server/tests/xpcshell/test_safe-getter.js new file mode 100644 index 0000000000..65bf3414ea --- /dev/null +++ b/devtools/server/tests/xpcshell/test_safe-getter.js @@ -0,0 +1,54 @@ +/* eslint-disable strict */ +function run_test() { + Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); + }); + const { addDebuggerToGlobal } = ChromeUtils.importESModule( + "resource://gre/modules/jsdebugger.sys.mjs" + ); + addDebuggerToGlobal(globalThis); + const g = createTestGlobal("test", { + wantGlobalProperties: ["ChromeUtils"], + }); + const dbg = new Debugger(); + const gw = dbg.addDebuggee(g); + + g.eval(` + // This is not a CCW. + Object.defineProperty(this, "bar", { + get: function() { return "bar"; }, + configurable: true, + enumerable: true + }); + + const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" + ); + + // This is a CCW. + XPCOMUtils.defineLazyScriptGetter( + this, "foo", "chrome://global/content/viewZoomOverlay.js"); + `); + + // Neither scripted getter should be considered safe. + assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("bar"))); + assert(!DevToolsUtils.hasSafeGetter(gw.getOwnPropertyDescriptor("foo"))); + + // Create an object in a less privileged sandbox. + const obj = gw.makeDebuggeeValue( + Cu.waiveXrays( + Cu.Sandbox(null).eval(` + Object.defineProperty({}, "bar", { + get: function() { return "bar"; }, + configurable: true, + enumerable: true + }); + `) + ) + ); + + // After waiving Xrays, the object has 2 wrappers. Both must be removed + // in order to detect that the getter is not safe. + assert(!DevToolsUtils.hasSafeGetter(obj.getOwnPropertyDescriptor("bar"))); +} diff --git a/devtools/server/tests/xpcshell/test_sessionDataHelpers.js b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js new file mode 100644 index 0000000000..847311811f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_sessionDataHelpers.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test SessionDataHelpers. + */ + +"use strict"; + +const { SessionDataHelpers } = ChromeUtils.import( + "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm" +); +const { SUPPORTED_DATA } = SessionDataHelpers; +const { TARGETS } = SUPPORTED_DATA; + +function run_test() { + const sessionData = { + [TARGETS]: [], + }; + + SessionDataHelpers.addSessionDataEntry(sessionData, TARGETS, [ + "frame", + "worker", + ]); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "the two elements were added" + ); + + SessionDataHelpers.addSessionDataEntry(sessionData, TARGETS, ["frame"]); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "addSessionDataEntry ignore duplicates" + ); + + SessionDataHelpers.addSessionDataEntry(sessionData, TARGETS, ["process"]); + deepEqual( + sessionData[TARGETS], + ["frame", "worker", "process"], + "the third element is added" + ); + + let removed = SessionDataHelpers.removeSessionDataEntry( + sessionData, + TARGETS, + ["process"] + ); + ok(removed, "removedSessionDataEntry returned true as it removed an element"); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "the element has been remove" + ); + + removed = SessionDataHelpers.removeSessionDataEntry(sessionData, TARGETS, [ + "not-existing", + ]); + ok( + !removed, + "removedSessionDataEntry returned false as no element has been removed" + ); + deepEqual( + sessionData[TARGETS], + ["frame", "worker"], + "no change made to the array" + ); + + removed = SessionDataHelpers.removeSessionDataEntry(sessionData, TARGETS, [ + "frame", + "worker", + ]); + ok( + removed, + "removedSessionDataEntry returned true as elements have been removed" + ); + deepEqual(sessionData[TARGETS], [], "all elements were removed"); +} diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js new file mode 100644 index 0000000000..9140e92d7c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-beginning-of-a-minified-fn.js @@ -0,0 +1,41 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column-minified.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + + // Pause inside of the nested function so we can make sure that we don't + // add any other breakpoints at other places on this line. + const location = { sourceUrl: source.url, line: 3, column: 56 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + Assert.equal(where.column, 56); + + const environment = await packet.frame.getEnvironment(); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value.type, "undefined"); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js new file mode 100644 index 0000000000..f9df5adad4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-at-the-end-of-a-minified-fn.js @@ -0,0 +1,41 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column-minified.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + + // Pause inside of the nested function so we can make sure that we don't + // add any other breakpoints at other places on this line. + const location = { sourceUrl: source.url, line: 3, column: 81 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + Assert.equal(where.column, 81); + + const environment = await packet.frame.getEnvironment(); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value, 2); + Assert.equal(variables.c.value, 3); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js new file mode 100644 index 0000000000..797cb6cd65 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column-in-gcd-script.js @@ -0,0 +1,46 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column-in-gcd-script.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, targetFront }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + Cu.forceGC(); + Cu.forceGC(); + Cu.forceGC(); + + const { source } = await promise; + + const location = { sourceUrl: source.url, line: 6, column: 21 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + reload(targetFront).then(function () { + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + }); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.line, location.line); + Assert.equal(where.column, location.column); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js new file mode 100644 index 0000000000..200d8b44e6 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-column.js @@ -0,0 +1,36 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-column.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + + const location = { sourceUrl: source.url, line: 4, column: 21 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + Assert.equal(where.column, location.column); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js new file mode 100644 index 0000000000..565402551e --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-in-gcd-script.js @@ -0,0 +1,45 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line-in-gcd-script.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, targetFront }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + Cu.forceGC(); + Cu.forceGC(); + Cu.forceGC(); + + const { source } = await promise; + + const location = { sourceUrl: source.url, line: 7 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + reload(targetFront).then(function () { + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + }); + }, threadFront); + const why = packet.why; + const environment = await packet.frame.getEnvironment(); + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.line, location.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js new file mode 100644 index 0000000000..2debc26b93 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-offsets.js @@ -0,0 +1,52 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-multiple-offsets.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { sourceUrl: sourceFront.url, line: 4 }; + setBreakpoint(threadFront, location); + + let packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + let why = packet.why; + let environment = await packet.frame.getEnvironment(); + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + let frame = packet.frame; + let where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + let variables = environment.bindings.variables; + Assert.equal(variables.i.value.type, "undefined"); + + const location2 = { sourceUrl: sourceFront.url, line: 7 }; + setBreakpoint(threadFront, location2); + + packet = await executeOnNextTickAndWaitForPause( + () => resume(threadFront), + threadFront + ); + environment = await packet.frame.getEnvironment(); + why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + frame = packet.frame; + where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location2.line); + variables = environment.bindings.variables; + Assert.equal(variables.i.value, 1); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js new file mode 100644 index 0000000000..f5ec75a353 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-multiple-statements.js @@ -0,0 +1,38 @@ +"use strict"; + +const SOURCE_URL = getFileUrl( + "setBreakpoint-on-line-with-multiple-statements.js" +); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { sourceUrl: sourceFront.url, line: 4 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const why = packet.why; + const environment = await packet.frame.getEnvironment(); + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value.type, "undefined"); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js new file mode 100644 index 0000000000..1bcdadbe4a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js @@ -0,0 +1,56 @@ +"use strict"; + +const SOURCE_URL = getFileUrl( + "setBreakpoint-on-line-with-no-offsets-in-gcd-script.js" +); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, targetFront }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + Cu.forceGC(); + Cu.forceGC(); + Cu.forceGC(); + + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { line: 7 }; + let [packet, breakpointClient] = await setBreakpoint( + sourceFront, + location + ); + Assert.ok(packet.isPending); + Assert.equal(false, "actualLocation" in packet); + + packet = await executeOnNextTickAndWaitForPause(function () { + reload(targetFront).then(function () { + loadSubScriptWithOptions(SOURCE_URL, { + target: debuggee, + ignoreCache: true, + }); + }); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + Assert.equal(packet.type, "paused"); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + Assert.equal(why.actors[0], breakpointClient.actor); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, 8); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js new file mode 100644 index 0000000000..5700097ea6 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line-with-no-offsets.js @@ -0,0 +1,44 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line-with-no-offsets.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { line: 5 }; + let [packet, breakpointClient] = await setBreakpoint( + sourceFront, + location + ); + Assert.ok(!packet.isPending); + Assert.ok("actualLocation" in packet); + const actualLocation = packet.actualLocation; + Assert.equal(actualLocation.line, 6); + + packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + Assert.equal(packet.type, "paused"); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + Assert.equal(why.actors[0], breakpointClient.actor); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, actualLocation.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js new file mode 100644 index 0000000000..93e01b757c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_setBreakpoint-on-line.js @@ -0,0 +1,36 @@ +"use strict"; + +const SOURCE_URL = getFileUrl("setBreakpoint-on-line.js"); + +add_task( + threadFrontTest( + async ({ threadFront, debuggee }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + loadSubScript(SOURCE_URL, debuggee); + const { source } = await promise; + const sourceFront = threadFront.source(source); + + const location = { sourceUrl: sourceFront.url, line: 5 }; + setBreakpoint(threadFront, location); + + const packet = await executeOnNextTickAndWaitForPause(function () { + Cu.evalInSandbox("f()", debuggee); + }, threadFront); + const environment = await packet.frame.getEnvironment(); + const why = packet.why; + Assert.equal(why.type, "breakpoint"); + Assert.equal(why.actors.length, 1); + const frame = packet.frame; + const where = frame.where; + Assert.equal(where.actor, source.actor); + Assert.equal(where.line, location.line); + const variables = environment.bindings.variables; + Assert.equal(variables.a.value, 1); + Assert.equal(variables.b.value.type, "undefined"); + Assert.equal(variables.c.value.type, "undefined"); + + await resume(threadFront); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js b/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js new file mode 100644 index 0000000000..6876f0a532 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_shapes_highlighter_helpers.js @@ -0,0 +1,274 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test the helper functions of the shapes highlighter. + */ + +"use strict"; + +const { + splitCoords, + coordToPercent, + evalCalcExpression, + shapeModeToCssPropertyName, + getCirclePath, + getDecimalPrecision, + getUnit, +} = require("resource://devtools/server/actors/highlighters/shapes.js"); + +function run_test() { + test_split_coords(); + test_coord_to_percent(); + test_eval_calc_expression(); + test_shape_mode_to_css_property_name(); + test_get_circle_path(); + test_get_decimal_precision(); + test_get_unit(); + run_next_test(); +} + +function test_split_coords() { + const tests = [ + { + desc: "splitCoords for basic coordinate pair", + expr: "30% 20%", + expected: ["30%", "20%"], + }, + { + desc: "splitCoords for coord pair with calc()", + expr: "calc(50px + 20%) 30%", + expected: ["calc(50px\u00a0+\u00a020%)", "30%"], + }, + ]; + + for (const { desc, expr, expected } of tests) { + deepEqual(splitCoords(expr), expected, desc); + } +} + +function test_coord_to_percent() { + const size = 1000; + const tests = [ + { + desc: "coordToPercent for percent value", + expr: "50%", + expected: 50, + }, + { + desc: "coordToPercent for px value", + expr: "500px", + expected: 50, + }, + { + desc: "coordToPercent for zero value", + expr: "0", + expected: 0, + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(coordToPercent(expr, size), expected, desc); + } +} + +function test_eval_calc_expression() { + const size = 1000; + const tests = [ + { + desc: "evalCalcExpression with one value", + expr: "50%", + expected: 50, + }, + { + desc: "evalCalcExpression with percent and px values", + expr: "50% + 100px", + expected: 60, + }, + { + desc: "evalCalcExpression with a zero value", + expr: "0 + 100px", + expected: 10, + }, + { + desc: "evalCalcExpression with a negative value", + expr: "-200px+50%", + expected: 30, + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(evalCalcExpression(expr, size), expected, desc); + } +} + +function test_shape_mode_to_css_property_name() { + const tests = [ + { + desc: "shapeModeToCssPropertyName for clip-path", + expr: "cssClipPath", + expected: "clipPath", + }, + { + desc: "shapeModeToCssPropertyName for shape-outside", + expr: "cssShapeOutside", + expected: "shapeOutside", + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(shapeModeToCssPropertyName(expr), expected, desc); + } +} + +function test_get_circle_path() { + const tests = [ + { + desc: "getCirclePath with size 5, no resizing, no zoom, 1:1 ratio", + size: 5, + cx: 0, + cy: 0, + width: 100, + height: 100, + zoom: 1, + expected: "M-5,0a5,5 0 1,0 10,0a5,5 0 1,0 -10,0", + }, + { + desc: "getCirclePath with size 7, resizing, no zoom, 1:1 ratio", + size: 7, + cx: 0, + cy: 0, + width: 200, + height: 200, + zoom: 1, + expected: "M-3.5,0a3.5,3.5 0 1,0 7,0a3.5,3.5 0 1,0 -7,0", + }, + { + desc: "getCirclePath with size 5, resizing, zoom, 1:1 ratio", + size: 5, + cx: 0, + cy: 0, + width: 200, + height: 200, + zoom: 2, + expected: "M-1.25,0a1.25,1.25 0 1,0 2.5,0a1.25,1.25 0 1,0 -2.5,0", + }, + { + desc: "getCirclePath with size 5, resizing, zoom, non-square ratio", + size: 5, + cx: 0, + cy: 0, + width: 100, + height: 200, + zoom: 2, + expected: "M-2.5,0a2.5,1.25 0 1,0 5,0a2.5,1.25 0 1,0 -5,0", + }, + ]; + + for (const { desc, size, cx, cy, width, height, zoom, expected } of tests) { + equal(getCirclePath(size, cx, cy, width, height, zoom), expected, desc); + } +} + +function test_get_decimal_precision() { + const tests = [ + { + desc: "getDecimalPrecision with px", + expr: "px", + expected: 0, + }, + { + desc: "getDecimalPrecision with %", + expr: "%", + expected: 2, + }, + { + desc: "getDecimalPrecision with em", + expr: "em", + expected: 2, + }, + { + desc: "getDecimalPrecision with undefined", + expr: undefined, + expected: 0, + }, + { + desc: "getDecimalPrecision with empty string", + expr: "", + expected: 0, + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(getDecimalPrecision(expr), expected, desc); + } +} + +function test_get_unit() { + const tests = [ + { + desc: "getUnit with %", + expr: "30%", + expected: "%", + }, + { + desc: "getUnit with px", + expr: "400px", + expected: "px", + }, + { + desc: "getUnit with em", + expr: "4em", + expected: "em", + }, + { + desc: "getUnit with 0", + expr: "0", + expected: "px", + }, + { + desc: "getUnit with 0%", + expr: "0%", + expected: "%", + }, + { + desc: "getUnit with 0.00%", + expr: "0.00%", + expected: "%", + }, + { + desc: "getUnit with 0px", + expr: "0px", + expected: "px", + }, + { + desc: "getUnit with 0em", + expr: "0em", + expected: "em", + }, + { + desc: "getUnit with calc", + expr: "calc(30px + 5%)", + expected: "px", + }, + { + desc: "getUnit with var", + expr: "var(--variable)", + expected: "px", + }, + { + desc: "getUnit with closest-side", + expr: "closest-side", + expected: "px", + }, + { + desc: "getUnit with farthest-side", + expr: "farthest-side", + expected: "px", + }, + ]; + + for (const { desc, expr, expected } of tests) { + equal(getUnit(expr), expected, desc); + } +} diff --git a/devtools/server/tests/xpcshell/test_source-01.js b/devtools/server/tests/xpcshell/test_source-01.js new file mode 100644 index 0000000000..5cb7a6da52 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-01.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test ensures that we can create SourceActors and SourceFronts properly, +// and that they can communicate over the protocol to fetch the source text for +// a given script. + +const SOURCE_URL = "http://example.com/foobar.js"; +const SOURCE_CONTENT = "stopMe()"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + DevToolsServer.LONG_STRING_LENGTH = 200; + + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + const response = await threadFront.getSources(); + + Assert.ok(!!response); + Assert.ok(!!response.sources); + + const source = response.sources.filter(function (s) { + return s.url === SOURCE_URL; + })[0]; + + Assert.ok(!!source); + + const sourceFront = threadFront.source(source); + const response2 = await sourceFront.source(); + + Assert.ok(!!response2); + Assert.ok(!!response2.contentType); + Assert.ok(response2.contentType.includes("javascript")); + + Assert.ok(!!response2.source); + Assert.equal(SOURCE_CONTENT, response2.source); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + "" + + function stopMe(arg1) { + debugger; + }, + debuggee, + "1.8", + getFileUrl("test_source-01.js") + ); + + Cu.evalInSandbox(SOURCE_CONTENT, debuggee, "1.8", SOURCE_URL); +} diff --git a/devtools/server/tests/xpcshell/test_source-02.js b/devtools/server/tests/xpcshell/test_source-02.js new file mode 100644 index 0000000000..9cb88cb0e4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-02.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test ensures that we can create SourceActors and SourceFronts properly, +// and that they can communicate over the protocol to fetch the source text for +// a given script. + +const SOURCE_URL = "http://example.com/foobar.js"; +const SOURCE_CONTENT = ` + stopMe(); + for(var i = 0; i < 2; i++) { + debugger; + } +`; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + DevToolsServer.LONG_STRING_LENGTH = 200; + + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + let response = await threadFront.getSources(); + Assert.ok(!!response); + Assert.ok(!!response.sources); + + const source = response.sources.filter(function (s) { + return s.url === SOURCE_URL; + })[0]; + + Assert.ok(!!source); + + const sourceFront = threadFront.source(source); + response = await sourceFront.getBreakpointPositionsCompressed(); + Assert.ok(!!response); + + Assert.deepEqual(response, { + 2: [2], + 3: [14, 17, 24], + 4: [4], + 6: [0], + }); + + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + "" + + function stopMe(arg1) { + debugger; + }, + debuggee, + "1.8", + getFileUrl("test_source-02.js") + ); + + Cu.evalInSandbox(SOURCE_CONTENT, debuggee, "1.8", SOURCE_URL); +} diff --git a/devtools/server/tests/xpcshell/test_source-03.js b/devtools/server/tests/xpcshell/test_source-03.js new file mode 100644 index 0000000000..d0cd4839a0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-03.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SOURCE_URL = getFileUrl("source-03.js"); + +add_task( + threadFrontTest( + async ({ threadFront, server }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + + // Create a two globals in the default junk sandbox compartment so that + // both globals are part of the same compartment. + server.allowNewThreadGlobals(); + const debuggee1 = Cu.Sandbox(systemPrincipal); + debuggee1.__name = "debuggee2.js"; + const debuggee2 = Cu.Sandbox(systemPrincipal); + debuggee2.__name = "debuggee2.js"; + server.disallowNewThreadGlobals(); + + // Load two copies of the source file. The first call to "loadSubScript" will + // create a ScriptSourceObject and a JSScript which references it. + // The second call will attempt to re-use JSScript objects because that is + // what loadSubScript does for instances of the same file that are loaded + // in the system principal in the same compartment. + // + // We explicitly want this because it is an edge case of the server. Most + // of the time a Debugger.Source will only have a single Debugger.Script + // associated with a given function, but in the context of explicitly + // cloned JSScripts, this is not the case, and we need to handle that. + loadSubScript(SOURCE_URL, debuggee1); + loadSubScript(SOURCE_URL, debuggee2); + + await promise; + + // We want to set a breakpoint and make sure that the breakpoint is properly + // set on _both_ files backed + await setBreakpoint(threadFront, { + sourceUrl: SOURCE_URL, + line: 4, + }); + + const { sources } = await getSources(threadFront); + + // Note: Since we load the file twice, we end up with two copies of the + // source object, and so two sources here. + Assert.equal(sources.length, 2); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the first global. + let pausedOne = false; + let onResumed = null; + threadFront.once("paused", function (packet) { + pausedOne = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee1, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedOne, true); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the second global. + let pausedTwo = false; + threadFront.once("paused", function (packet) { + pausedTwo = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee2, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedTwo, true); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_source-04.js b/devtools/server/tests/xpcshell/test_source-04.js new file mode 100644 index 0000000000..a3e3bef25f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_source-04.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SOURCE_URL = getFileUrl("source-03.js"); + +add_task( + threadFrontTest( + async ({ threadFront, server }) => { + const promise = waitForNewSource(threadFront, SOURCE_URL); + + // Create two globals in the default junk sandbox compartment so that + // both globals are part of the same compartment. + server.allowNewThreadGlobals(); + const debuggee1 = Cu.Sandbox(systemPrincipal); + debuggee1.__name = "debuggee2.js"; + const debuggee2 = Cu.Sandbox(systemPrincipal); + debuggee2.__name = "debuggee2.js"; + server.disallowNewThreadGlobals(); + + // Load first copy of the source file. The first call to "loadSubScript" will + // create a ScriptSourceObject and a JSScript which references it. + loadSubScript(SOURCE_URL, debuggee1); + + await promise; + + // We want to set a breakpoint and make sure that the breakpoint is properly + // set on _both_ files backed + await setBreakpoint(threadFront, { + sourceUrl: SOURCE_URL, + line: 4, + }); + + const { sources } = await getSources(threadFront); + Assert.equal(sources.length, 1); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the first global. + let pausedOne = false; + let onResumed = null; + threadFront.once("paused", function (packet) { + pausedOne = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee1, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedOne, true); + + // Load second copy of the source file. The second call will attempt to + // re-use JSScript objects because that is what loadSubScript does for + // instances of the same file that are loaded in the system principal in + // the same compartment. + // + // We explicitly want this because it is an edge case of the server. Most + // of the time a Debugger.Source will only have a single Debugger.Script + // associated with a given function, but in the context of explicitly + // cloned JSScripts, this is not the case, and we need to handle that. + loadSubScript(SOURCE_URL, debuggee2); + + // Ensure that the breakpoint was properly applied to the JSScipt loaded + // in the second global. + let pausedTwo = false; + threadFront.once("paused", function (packet) { + pausedTwo = true; + onResumed = resume(threadFront); + }); + Cu.evalInSandbox("init()", debuggee2, "1.8", "test.js", 1); + await onResumed; + Assert.equal(pausedTwo, true); + }, + { doNotRunWorker: true } + ) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-01.js b/devtools/server/tests/xpcshell/test_stepping-01.js new file mode 100644 index 0000000000..0c66404510 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-01.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check scenarios where we're leaving function a and + * going to the function b's call-site. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +function getPauseReturn(packet) { + return packet.why.frameFinished.return; +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function stepOutOfA(dbg, func, expectedLocation) { + await invokeAndPause(dbg, `${func}()`); + const { threadFront } = dbg; + await steps(threadFront, [stepOver, stepIn]); + + const packet = await stepOut(threadFront); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + + await threadFront.resume(); +} + +async function stepOverInA(dbg, func, expectedLocation) { + await invokeAndPause(dbg, `${func}()`); + const { threadFront } = dbg; + await steps(threadFront, [stepOver, stepIn]); + + let packet = await stepOver(threadFront); + equal(getPauseReturn(packet).ownPropertyLength, 1, "a() is returning obj"); + + packet = await stepOver(threadFront); + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + await dbg.threadFront.resume(); +} + +async function testStep(dbg, func, expectedValue) { + await stepOverInA(dbg, func, expectedValue); + await stepOutOfA(dbg, func, expectedValue); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + + await testStep(dbg, "arithmetic", { line: 16, column: 8 }); + await testStep(dbg, "composition", { line: 21, column: 3 }); + await testStep(dbg, "chaining", { line: 26, column: 6 }); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-02.js b/devtools/server/tests/xpcshell/test_stepping-02.js new file mode 100644 index 0000000000..c9df671839 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-02.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic step-in functionality. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 2, + "Should be at debugger statement on line 2" + ); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + const step1 = await stepIn(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 3); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + const step3 = await stepIn(threadFront); + equal(step3.why.type, "resumeLimit"); + equal(step3.frame.where.line, 4); + equal(debuggee.a, 1); + equal(debuggee.b, undefined); + + const step4 = await stepIn(threadFront); + equal(step4.why.type, "resumeLimit"); + equal(step4.frame.where.line, 4); + equal(debuggee.a, 1); + equal(debuggee.b, 2); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + debugger; // 2 + var a = 1; // 3 + var b = 2;`, // 4 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-03.js b/devtools/server/tests/xpcshell/test_stepping-03.js new file mode 100644 index 0000000000..88422ac0cc --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-03.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic step-out functionality. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const step1 = await stepOut(threadFront); + equal(step1.frame.where.line, 8); + equal(step1.why.type, "resumeLimit"); + + equal(debuggee.a, 1); + equal(debuggee.b, 2); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function f() { // 2 + debugger; // 3 + this.a = 1; // 4 + this.b = 2; // 5 + } // 6 + f(); // 7 + `, // 8 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-04.js b/devtools/server/tests/xpcshell/test_stepping-04.js new file mode 100644 index 0000000000..37a9f843d0 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-04.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that stepping over a function call does not pause inside the function. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + dumpn("Step Over to f()"); + const step1 = await stepOver(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 6); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + dumpn("Step Over f()"); + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 7); + equal(step2.why.type, "resumeLimit"); + equal(debuggee.a, 1); + equal(debuggee.b, undefined); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + function f() { // 2 + this.a = 1; // 3 + } // 4 + debugger; // 5 + f(); // 6 + let b = 2; // 7 + `, // 8 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-05.js b/devtools/server/tests/xpcshell/test_stepping-05.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-05.js diff --git a/devtools/server/tests/xpcshell/test_stepping-06.js b/devtools/server/tests/xpcshell/test_stepping-06.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-06.js diff --git a/devtools/server/tests/xpcshell/test_stepping-07.js b/devtools/server/tests/xpcshell/test_stepping-07.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-07.js diff --git a/devtools/server/tests/xpcshell/test_stepping-08.js b/devtools/server/tests/xpcshell/test_stepping-08.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-08.js diff --git a/devtools/server/tests/xpcshell/test_stepping-09.js b/devtools/server/tests/xpcshell/test_stepping-09.js new file mode 100644 index 0000000000..da59ed963c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-09.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the end of the parent if it fails to stop + * anywhere else. Bug 1504358. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 2, + "Should be at debugger statement on line 2" + ); + + dumpn("Step out of inner and into outer"); + const step2 = await stepOut(threadFront); + // The bug was that we'd step right past the end of the function and never pause. + equal(step2.frame.where.line, 2); + equal(step2.frame.where.column, 31); + deepEqual(step2.why.frameFinished.return, { type: "undefined" }); + }) +); + +function evaluateTestCode(debuggee) { + // By placing the inner and outer on the same line, this triggers the server's + // logic to skip steps for these functions, meaning that onPop is the only + // thing that will cause it to pop. + Cu.evalInSandbox( + ` + function outer(){ inner(); return 42; } function inner(){ debugger; } + outer(); + `, + debuggee, + "1.8", + "test_stepping-09-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-10.js b/devtools/server/tests/xpcshell/test_stepping-10.js new file mode 100644 index 0000000000..6ea95c3fd3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-10.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the parent and the parent's parent. + * This checks for the failure found in bug 1530549. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 3, + "Should be at debugger statement on line 3" + ); + + dumpn("Step out of inner and into var statement IIFE"); + const step2 = await stepOut(threadFront); + equal(step2.frame.where.line, 4); + deepEqual(step2.why.frameFinished.return, { type: "undefined" }); + + dumpn("Step out of vars and into script body"); + const step3 = await stepOut(threadFront); + equal(step3.frame.where.line, 9); + deepEqual(step3.why.frameFinished.return, { type: "undefined" }); + }) +); + +function evaluateTestCode(debuggee) { + Cu.evalInSandbox( + ` + (function() { + (function(){debugger;})(); + var a = 1; + a = 2; + a = 3; + a = 4; + })(); + `, + debuggee, + "1.8", + "test_stepping-10-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-11.js b/devtools/server/tests/xpcshell/test_stepping-11.js new file mode 100644 index 0000000000..8cbd285d89 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-11.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic stepping for console evaluations. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + commands.scriptCommand.execute(`(function(){ + debugger; + var a = 1; + var b = 2; + })();`); + + await waitForEvent(threadFront, "paused"); + const packet = await stepOver(threadFront); + Assert.equal(packet.frame.where.line, 3, "step to line 3"); + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-12.js b/devtools/server/tests/xpcshell/test_stepping-12.js new file mode 100644 index 0000000000..de96faf59f --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-12.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the parent and the parent's parent. + * This checks for the failure found in bug 1530549. + */ + +const sourceUrl = "test_stepping-10-test-code.js"; + +add_task( + threadFrontTest(async args => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + await testGenerator(args); + await testAwait(args); + await testInterleaving(args); + await testMultipleSteps(args); + }) +); + +async function testAwait({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function() { + debugger; + r = await Promise.resolve('yay'); + a = 4; + })(); + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + dumpn("Step Over and land on line 5"); + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 4); + equal(step1.frame.where.column, 10); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 5); + equal(step2.frame.where.column, 10); + equal(debuggee.r, "yay"); + await resume(threadFront); +} + +async function testInterleaving({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function simpleRace() { + debugger; + this.result = await new Promise((r) => { + Promise.resolve().then(() => { debugger }); + Promise.resolve().then(r('yay')) + }) + var a = 2; + debugger; + })() + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + dumpn("Step Over and land on line 5"); + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 4); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 5); + equal(step2.frame.where.column, 43); + + const step3 = await resumeAndWaitForPause(threadFront); + equal(step3.frame.where.line, 9); + equal(debuggee.result, "yay"); + + await resume(threadFront); +} + +async function testMultipleSteps({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function simpleRace() { + debugger; + await Promise.resolve(); + var a = 2; + await Promise.resolve(); + var b = 2; + await Promise.resolve(); + debugger; + })() + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 4); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 5); + + const step3 = await stepOver(threadFront); + equal(step3.frame.where.line, 6); + resume(threadFront); +} + +async function testGenerator({ threadFront, debuggee }) { + function evaluateTestCode() { + Cu.evalInSandbox( + ` + (async function() { + function* makeSteps() { + debugger; + yield 1; + yield 2; + return 3; + } + const s = makeSteps(); + s.next(); + s.next(); + s.next(); + })() + `, + debuggee, + "1.8", + sourceUrl, + 1 + ); + } + + await executeOnNextTickAndWaitForPause(evaluateTestCode, threadFront); + + const step1 = await stepOver(threadFront); + equal(step1.frame.where.line, 5); + + const step2 = await stepOver(threadFront); + equal(step2.frame.where.line, 6); + + const step3 = await stepOver(threadFront); + equal(step3.frame.where.line, 7); + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-13.js b/devtools/server/tests/xpcshell/test_stepping-13.js new file mode 100644 index 0000000000..cbdb78ce2d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-13.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that is possible to step into both the inner and outer function + * calls. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + commands.scriptCommand.execute( + `(function () { + const a = () => { return 2 }; + debugger; + a(a()) + })()` + ); + + await waitForEvent(threadFront, "paused"); + const step1 = await stepOver(threadFront); + Assert.equal(step1.frame.where.line, 4, "step to line 4"); + + const step2 = await stepIn(threadFront); + Assert.equal(step2.frame.where.line, 2, "step in to line 2"); + + const step3 = await stepOut(threadFront); + Assert.equal(step3.frame.where.line, 4, "step back to line 4"); + Assert.equal(step3.frame.where.column, 9, "step out to column 9"); + + const step4 = await stepIn(threadFront); + Assert.equal(step4.frame.where.line, 2, "step in to line 2"); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-14.js b/devtools/server/tests/xpcshell/test_stepping-14.js new file mode 100644 index 0000000000..6d64a53a66 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-14.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that is possible to step into both the inner and outer function + * calls. + */ + +add_task( + threadFrontTest(async ({ commands, threadFront }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + + commands.scriptCommand.execute(`(function () { + async function f() { + const p = Promise.resolve(43); + await p; + return p; + } + + function call_f() { + Promise.resolve(42).then(forty_two => { + return forty_two; + }); + + f().then(v => { + return v; + }); + } + debugger; + call_f(); + })()`); + + const packet = await waitForEvent(threadFront, "paused"); + const location = { + sourceId: packet.frame.where.actor, + line: 4, + column: 10, + }; + + await threadFront.setBreakpoint(location, {}); + + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4, "landed at await"); + + const packet3 = await stepIn(threadFront); + Assert.equal(packet3.frame.where.line, 5, "step to the next line"); + + await threadFront.resume(); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-15.js b/devtools/server/tests/xpcshell/test_stepping-15.js new file mode 100644 index 0000000000..9e79b93687 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-15.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test stepping from inside a blackboxed function + * test-page: https://dbg-blackbox-stepping.glitch.me/ + */ + +async function invokeAndPause({ global, threadFront }, expression, url) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global, "1.8", url, 1), + threadFront + ); +} +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + const dbg = { global: debuggee, threadFront }; + + // Test stepping from a blackboxed location + async function testStepping(action, expectedLine) { + commands.scriptCommand.execute(`outermost()`); + await waitForPause(threadFront); + await blackBox(blackboxedSourceFront); + const packet = await action(threadFront); + const { line, actor } = packet.frame.where; + equal(actor, unblackboxedActor, "paused in unblackboxed source"); + equal(line, expectedLine, "paused at correct line"); + await threadFront.resume(); + await unBlackBox(blackboxedSourceFront); + } + + invokeAndPause( + dbg, + `function outermost() { + const value = blackboxed1(); + return value + 1; + } + function innermost() { + return 1; + }`, + "http://example.com/unblackboxed.js" + ); + invokeAndPause( + dbg, + `function blackboxed1() { + return blackboxed2(); + } + function blackboxed2() { + return innermost(); + }`, + "http://example.com/blackboxed.js" + ); + + const { sources } = await getSources(threadFront); + const blackboxedSourceFront = threadFront.source( + sources.find(source => source.url == "http://example.com/blackboxed.js") + ); + const unblackboxedActor = sources.find( + source => source.url == "http://example.com/unblackboxed.js" + ).actor; + + await setBreakpoint(threadFront, { + sourceUrl: blackboxedSourceFront.url, + line: 5, + }); + + info("Step Out to outermost"); + await testStepping(stepOut, 3); + + info("Step Over to outermost"); + await testStepping(stepOver, 3); + + info("Step In to innermost"); + await testStepping(stepIn, 6); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-16.js b/devtools/server/tests/xpcshell/test_stepping-16.js new file mode 100644 index 0000000000..e3bd94b747 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-16.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test stepping from inside a blackboxed function + * test-page: https://dbg-blackbox-stepping2.glitch.me/ + */ + +async function invokeAndPause({ global, threadFront }, expression, url) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global, "1.8", url, 1), + threadFront + ); +} + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + const dbg = { global: debuggee, threadFront }; + invokeAndPause( + dbg, + `function outermost() { + blackboxed( + function inner1() { + return 1; + }, + function inner2() { + return 2; + } + ); + }`, + "http://example.com/unblackboxed.js" + ); + invokeAndPause( + dbg, + `function blackboxed(...args) { + for (const arg of args) { + arg(); + } + }`, + "http://example.com/blackboxed.js" + ); + + const { sources } = await getSources(threadFront); + const blackboxedSourceFront = threadFront.source( + sources.find(source => source.url == "http://example.com/blackboxed.js") + ); + const unblackboxedSource = sources.find( + source => source.url == "http://example.com/unblackboxed.js" + ); + const unblackboxedActor = unblackboxedSource.actor; + const unblackboxedSourceFront = threadFront.source(unblackboxedSource); + + await setBreakpoint(threadFront, { + sourceUrl: unblackboxedSourceFront.url, + line: 4, + }); + blackBox(blackboxedSourceFront); + + async function testStepping(action, expectedLine) { + commands.scriptCommand.execute("outermost()"); + await waitForPause(threadFront); + await stepOver(threadFront); + const packet = await action(threadFront); + const { actor, line } = packet.frame.where; + equal(actor, unblackboxedActor, "Paused in unblackboxed source"); + equal(line, expectedLine, "Paused at correct line"); + await threadFront.resume(); + } + + info("Step Out to outermost"); + await testStepping(stepOut, 10); + + info("Step Over to outermost"); + await testStepping(stepOver, 10); + + info("Step In to inner2"); + await testStepping(stepIn, 7); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-17.js b/devtools/server/tests/xpcshell/test_stepping-17.js new file mode 100644 index 0000000000..816946fa4c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-17.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Check that you can step from one script or event to another + */ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + Cu.evalInSandbox( + `function blackboxed(callback) { return () => callback(); }`, + debuggee, + "1.8", + "http://example.com/blackboxed.js", + 1 + ); + + const { sources } = await getSources(threadFront); + const blackboxedSourceFront = threadFront.source( + sources.find(source => source.url == "http://example.com/blackboxed.js") + ); + blackBox(blackboxedSourceFront); + + const testStepping = async function (wrapperName, stepHandler, message) { + commands.scriptCommand.execute(`(function () { + const p = Promise.resolve(); + p.then(${wrapperName}(() => { debugger; })) + .then(${wrapperName}(() => { })); + })();`); + + await waitForEvent(threadFront, "paused"); + const step = await stepHandler(threadFront); + Assert.equal(step.frame.where.line, 4, message); + await resume(threadFront); + }; + + const stepTwice = async function () { + await stepOver(threadFront); + return stepOver(threadFront); + }; + + await testStepping("", stepTwice, "Step over on the outermost frame"); + await testStepping("blackboxed", stepTwice, "Step over with blackboxing"); + await testStepping("", stepOut, "Step out on the outermost frame"); + await testStepping("blackboxed", stepOut, "Step out with blackboxing"); + + commands.scriptCommand.execute(`(async function () { + const p = Promise.resolve(); + const p2 = p.then(() => { + debugger; + return "async stepping!"; + }); + debugger; + await p; + const result = await p2; + return result; + })(); + `); + + await waitForEvent(threadFront, "paused"); + await stepOver(threadFront); + await stepOver(threadFront); + const step = await stepOut(threadFront); + await resume(threadFront); + Assert.equal(step.frame.where.line, 9, "Step out of promise into async fn"); + }) +); diff --git a/devtools/server/tests/xpcshell/test_stepping-18.js b/devtools/server/tests/xpcshell/test_stepping-18.js new file mode 100644 index 0000000000..e8581835d3 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-18.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check scenarios where we're leaving function a and + * going to the function b's call-site. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +async function stepOutOfA(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into a()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn, stepIn]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step over location in ${func}` + ); + + await dbg.threadFront.resume(); +} + +async function stepOverInA(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + info("pause and step into a()"); + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOver(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step over location in ${func}` + ); + + await dbg.threadFront.resume(); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping.js"); + + info(`Test step over with the 1st frame`); + await stepOverInA(dbg, "arithmetic", 0, { line: 8, column: 0 }); + + info(`Test step over with the 2nd frame`); + await stepOverInA(dbg, "arithmetic", 1, { line: 17, column: 0 }); + + info(`Test step out with the 1st frame`); + await stepOutOfA(dbg, "nested", 0, { line: 31, column: 0 }); + + info(`Test step out with the 2nd frame`); + await stepOutOfA(dbg, "nested", 1, { line: 36, column: 0 }); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-19.js b/devtools/server/tests/xpcshell/test_stepping-19.js new file mode 100644 index 0000000000..7ab21c7b66 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-19.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that step out stops at the async parent's frame. + */ + +async function testFinish({ threadFront, devToolsClient }) { + await close(devToolsClient); + + do_test_finished(); +} + +async function invokeAndPause({ global, threadFront }, expression) { + return executeOnNextTickAndWaitForPause( + () => Cu.evalInSandbox(expression, global), + threadFront + ); +} + +async function steps(threadFront, sequence) { + const locations = []; + for (const cmd of sequence) { + const packet = await step(threadFront, cmd); + locations.push(getPauseLocation(packet)); + } + return locations; +} + +async function step(threadFront, cmd) { + return cmd(threadFront); +} + +function getPauseLocation(packet) { + const { line, column } = packet.frame.where; + return { line, column }; +} + +async function stepOutBeforeTimer(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + + await resumeAndWaitForPause(threadFront); + await resume(threadFront); +} + +async function stepOutAfterTimer(dbg, func, frameIndex, expectedLocation) { + const { threadFront } = dbg; + + await invokeAndPause(dbg, `${func}()`); + await steps(threadFront, [stepOver, stepIn, stepOver, stepOver]); + + const { frames } = await threadFront.frames(0, 5); + const frameActorID = frames[frameIndex].actorID; + const packet = await stepOut(threadFront, frameActorID); + + deepEqual( + getPauseLocation(packet), + expectedLocation, + `step out location in ${func}` + ); + + await resumeAndWaitForPause(threadFront); + await dbg.threadFront.resume(); +} + +function run_test() { + return (async function () { + const dbg = await setupTestFromUrl("stepping-async.js"); + + info(`Test stepping out before timer;`); + await stepOutBeforeTimer(dbg, "stuff", 0, { line: 27, column: 2 }); + + info(`Test stepping out after timer;`); + await stepOutAfterTimer(dbg, "stuff", 0, { line: 29, column: 2 }); + + await testFinish(dbg); + })(); +} diff --git a/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js b/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js new file mode 100644 index 0000000000..3ec4fd994d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_stepping-with-skip-breakpoints.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check basic step-over functionality with pause points + * for the first statement and end of the last statement. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + dumpn("Evaluating test code and waiting for first debugger statement"); + const dbgStmt = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + equal( + dbgStmt.frame.where.line, + 2, + "Should be at debugger statement on line 2" + ); + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + const source = await getSource( + threadFront, + "test_stepping-01-test-code.js" + ); + + // Add pause points for the first and end of the last statement. + // Note: we intentionally ignore the second statement. + source.setPausePoints([ + { + location: { line: 3, column: 8 }, + types: { breakpoint: true, stepOver: true }, + }, + { + location: { line: 4, column: 14 }, + types: { breakpoint: true, stepOver: true }, + }, + ]); + + dumpn("Step Over to line 3"); + const step1 = await stepOver(threadFront); + equal(step1.why.type, "resumeLimit"); + equal(step1.frame.where.line, 3); + equal(step1.frame.where.column, 12); + + equal(debuggee.a, undefined); + equal(debuggee.b, undefined); + + dumpn("Step Over to line 4"); + const step2 = await stepOver(threadFront); + equal(step2.why.type, "resumeLimit"); + equal(step2.frame.where.line, 4); + equal(step2.frame.where.column, 12); + + equal(debuggee.a, 1); + equal(debuggee.b, undefined); + + dumpn("Step Over to the end of line 4"); + const step3 = await stepOver(threadFront); + equal(step3.why.type, "resumeLimit"); + equal(step3.frame.where.line, 4); + equal(step3.frame.where.column, 14); + equal(debuggee.a, 1); + equal(debuggee.b, 2); + }) +); + +function evaluateTestCode(debuggee) { + // prettier-ignore + Cu.evalInSandbox( + ` // 1 + debugger; // 2 + var a = 1; // 3 + var b = 2;`, // 4 + debuggee, + "1.8", + "test_stepping-01-test-code.js", + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_symbolactor.js b/devtools/server/tests/xpcshell/test_symbolactor.js new file mode 100644 index 0000000000..0d04a2bd1d --- /dev/null +++ b/devtools/server/tests/xpcshell/test_symbolactor.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + SymbolActor, +} = require("resource://devtools/server/actors/object/symbol.js"); + +function run_test() { + test_SA_destroy(); + test_SA_form(); + test_SA_raw(); +} + +const SYMBOL_NAME = "abc"; +const TEST_SYMBOL = Symbol(SYMBOL_NAME); + +function makeMockSymbolActor() { + const symbol = TEST_SYMBOL; + const mockConn = null; + const actor = new SymbolActor(mockConn, symbol); + actor.actorID = "symbol1"; + const parentPool = { + symbolActors: { + [symbol]: actor, + }, + unmanage: () => {}, + }; + actor.getParent = () => parentPool; + return actor; +} + +function test_SA_destroy() { + const actor = makeMockSymbolActor(); + strictEqual(actor.getParent().symbolActors[TEST_SYMBOL], actor); + + actor.destroy(); + strictEqual(TEST_SYMBOL in actor.getParent().symbolActors, false); +} + +function test_SA_form() { + const actor = makeMockSymbolActor(); + const form = actor.form(); + strictEqual(form.type, "symbol"); + strictEqual(form.actor, actor.actorID); + strictEqual(form.name, SYMBOL_NAME); +} + +function test_SA_raw() { + const actor = makeMockSymbolActor(); + strictEqual(actor.rawValue(), TEST_SYMBOL); +} diff --git a/devtools/server/tests/xpcshell/test_symbols-01.js b/devtools/server/tests/xpcshell/test_symbols-01.js new file mode 100644 index 0000000000..5352542e83 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_symbols-01.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we can represent ES6 Symbols over the RDP. + */ + +const URL = "foo.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await testSymbols(threadFront, debuggee); + }) +); + +async function testSymbols(threadFront, debuggee) { + const evalCode = () => { + /* eslint-disable mozilla/var-only-at-top-level, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "(" + function () { + var symbolWithName = Symbol("Chris"); + var symbolWithoutName = Symbol(); + var iteratorSymbol = Symbol.iterator; + debugger; + } + "())", + debuggee, + "1.8", + URL, + 1 + ); + /* eslint-enable mozilla/var-only-at-top-level, no-unused-vars */ + }; + + const packet = await executeOnNextTickAndWaitForPause(evalCode, threadFront); + const environment = await packet.frame.getEnvironment(); + const { symbolWithName, symbolWithoutName, iteratorSymbol } = + environment.bindings.variables; + + equal(symbolWithName.value.type, "symbol"); + equal(symbolWithName.value.name, "Chris"); + + equal(symbolWithoutName.value.type, "symbol"); + ok(!("name" in symbolWithoutName.value)); + + equal(iteratorSymbol.value.type, "symbol"); + equal(iteratorSymbol.value.name, "Symbol.iterator"); +} diff --git a/devtools/server/tests/xpcshell/test_symbols-02.js b/devtools/server/tests/xpcshell/test_symbols-02.js new file mode 100644 index 0000000000..12d4ef80c8 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_symbols-02.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that we don't run debuggee code when getting symbol names. + */ + +const URL = "foo.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await testSymbols(threadFront, debuggee); + }) +); + +async function testSymbols(threadFront, debuggee) { + const evalCode = () => { + /* eslint-disable mozilla/var-only-at-top-level, no-extend-native, no-unused-vars */ + // prettier-ignore + Cu.evalInSandbox( + "(" + function () { + Symbol.prototype.toString = () => { + throw new Error("lololol"); + }; + var sym = Symbol("le troll"); + debugger; + } + "())", + debuggee, + "1.8", + URL, + 1 + ); + /* eslint-enable mozilla/var-only-at-top-level, no-extend-native, no-unused-vars */ + }; + + const packet = await executeOnNextTickAndWaitForPause(evalCode, threadFront); + const environment = await packet.frame.getEnvironment(); + const { sym } = environment.bindings.variables; + + equal(sym.value.type, "symbol"); + equal(sym.value.name, "le troll"); +} diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-01.js b/devtools/server/tests/xpcshell/test_threadlifetime-01.js new file mode 100644 index 0000000000..d2e8234fb9 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_threadlifetime-01.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that thread-lifetime grips last past a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const pauseGrip = packet.frame.arguments[0]; + + // Create a thread-lifetime actor for this object. + const response = await client.request({ + to: pauseGrip.actor, + type: "threadGrip", + }); + // Successful promotion won't return an error. + Assert.equal(response.error, undefined); + + const packet2 = await resumeAndWaitForPause(threadFront); + + // Verify that the promoted actor is returned again. + Assert.equal(pauseGrip.actor, packet2.frame.arguments[0].actor); + // Now that we've resumed, should get unrecognizePacketType for the + // promoted grip. + try { + await client.request({ to: pauseGrip.actor, type: "bogusRequest" }); + ok(false, "bogusRequest should throw"); + } catch (e) { + Assert.equal(e.error, "unrecognizedPacketType"); + ok(true, "bogusRequest thrown"); + } + await threadFront.resume(); + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-02.js b/devtools/server/tests/xpcshell/test_threadlifetime-02.js new file mode 100644 index 0000000000..c35350a48c --- /dev/null +++ b/devtools/server/tests/xpcshell/test_threadlifetime-02.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that thread-lifetime grips last past a resume. + */ + +add_task( + threadFrontTest(async ({ threadFront, debuggee, client }) => { + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + const pauseGrip = packet.frame.arguments[0]; + + // Create a thread-lifetime actor for this object. + const response = await client.request({ + to: pauseGrip.actor, + type: "threadGrip", + }); + // Successful promotion won't return an error. + Assert.equal(response.error, undefined); + + const packet2 = await resumeAndWaitForPause(threadFront); + + // Verify that the promoted actor is returned again. + Assert.equal(pauseGrip.actor, packet2.frame.arguments[0].actor); + // Now that we've resumed, release the thread-lifetime grip. + const objFront = new ObjectFront( + threadFront.conn, + threadFront.targetFront, + threadFront, + pauseGrip + ); + await objFront.release(); + const objFront2 = new ObjectFront( + threadFront.conn, + threadFront.targetFront, + threadFront, + pauseGrip + ); + + try { + await objFront2 + .request({ to: pauseGrip.actor, type: "bogusRequest" }) + .catch(function (error) { + Assert.ok(!!error.message.match(/noSuchActor/)); + threadFront.resume(); + throw new Error(); + }); + ok(false, "bogusRequest should throw"); + } catch (e) { + ok(true, "bogusRequest thrown"); + } + }) +); + +function evaluateTestCode(debuggee) { + debuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_threadlifetime-04.js b/devtools/server/tests/xpcshell/test_threadlifetime-04.js new file mode 100644 index 0000000000..302985b4a1 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_threadlifetime-04.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Check that requesting a thread-lifetime actor twice for the same + * value returns the same actor. + */ + +var gDebuggee; +var gClient; +var gThreadFront; + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, client }) => { + gThreadFront = threadFront; + gClient = client; + gDebuggee = debuggee; + test_thread_lifetime(); + }, + { waitForFinish: true } + ) +); + +function test_thread_lifetime() { + gThreadFront.once("paused", function (packet) { + const pauseGrip = packet.frame.arguments[0]; + + gClient.request( + { to: pauseGrip.actor, type: "threadGrip" }, + function (response) { + // Successful promotion won't return an error. + Assert.equal(response.error, undefined); + + const threadGrip1 = response.from; + + gClient.request( + { to: pauseGrip.actor, type: "threadGrip" }, + function (response) { + Assert.equal(threadGrip1, response.from); + gThreadFront.resume().then(function () { + threadFrontTestFinished(); + }); + } + ); + } + ); + }); + + gDebuggee.eval( + "(" + + function () { + function stopMe(arg1) { + debugger; + } + stopMe({ obj: true }); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_unsafeDereference.js b/devtools/server/tests/xpcshell/test_unsafeDereference.js new file mode 100644 index 0000000000..53b70420c6 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_unsafeDereference.js @@ -0,0 +1,130 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +/* eslint-disable strict */ + +// Test Debugger.Object.prototype.unsafeDereference in the presence of +// interesting cross-compartment wrappers. +// +// This is not really a devtools server test; it's more of a Debugger test. +// But we need xpcshell and Components.utils.Sandbox to get +// cross-compartment wrappers with interesting properties, and this is the +// xpcshell test directory most closely related to the JS Debugger API. + +addDebuggerToGlobal(globalThis); + +// Add a method to Debugger.Object for fetching value properties +// conveniently. +Debugger.Object.prototype.getProperty = function (name) { + const desc = this.getOwnPropertyDescriptor(name); + if (!desc) { + return undefined; + } + if (!desc.value) { + throw Error( + "Debugger.Object.prototype.getProperty: " + + "not a value property: " + + name + ); + } + return desc.value; +}; + +function run_test() { + // Create a low-privilege sandbox, and a chrome-privilege sandbox. + const contentBox = Cu.Sandbox("http://www.example.com"); + const chromeBox = Cu.Sandbox(this); + + // Create an objects in this compartment, and one in each sandbox. We'll + // refer to the objects as "mainObj", "contentObj", and "chromeObj", in + // variable and property names. + const mainObj = { name: "mainObj" }; + Cu.evalInSandbox('var contentObj = { name: "contentObj" };', contentBox); + Cu.evalInSandbox('var chromeObj = { name: "chromeObj" };', chromeBox); + + // Give each global a pointer to all the other globals' objects. + contentBox.mainObj = chromeBox.mainObj = mainObj; + const contentObj = (chromeBox.contentObj = contentBox.contentObj); + const chromeObj = (contentBox.chromeObj = chromeBox.chromeObj); + + // First, a whole bunch of basic sanity checks, to ensure that JavaScript + // evaluated in various scopes really does see the world the way this + // test expects it to. + + // The objects appear as global variables in the sandbox, and as + // the sandbox object's properties in chrome. + Assert.ok(Cu.evalInSandbox("mainObj", contentBox) === contentBox.mainObj); + Assert.ok( + Cu.evalInSandbox("contentObj", contentBox) === contentBox.contentObj + ); + Assert.ok(Cu.evalInSandbox("chromeObj", contentBox) === contentBox.chromeObj); + Assert.ok(Cu.evalInSandbox("mainObj", chromeBox) === chromeBox.mainObj); + Assert.ok(Cu.evalInSandbox("contentObj", chromeBox) === chromeBox.contentObj); + Assert.ok(Cu.evalInSandbox("chromeObj", chromeBox) === chromeBox.chromeObj); + + // We (the main global) can see properties of all objects in all globals. + Assert.ok(contentBox.mainObj.name === "mainObj"); + Assert.ok(contentBox.contentObj.name === "contentObj"); + Assert.ok(contentBox.chromeObj.name === "chromeObj"); + + // chromeBox can see properties of all objects in all globals. + Assert.equal(Cu.evalInSandbox("mainObj.name", chromeBox), "mainObj"); + Assert.equal(Cu.evalInSandbox("contentObj.name", chromeBox), "contentObj"); + Assert.equal(Cu.evalInSandbox("chromeObj.name", chromeBox), "chromeObj"); + + // contentBox can see properties of the content object, but not of either + // chrome object, because by default, content -> chrome wrappers hide all + // object properties. + Assert.equal(Cu.evalInSandbox("mainObj.name", contentBox), undefined); + Assert.equal(Cu.evalInSandbox("contentObj.name", contentBox), "contentObj"); + Assert.equal(Cu.evalInSandbox("chromeObj.name", contentBox), undefined); + + // When viewing an object in compartment A from the vantage point of + // compartment B, Debugger should give the same results as debuggee code + // would. + + // Create a debugger, debugging our two sandboxes. + const dbg = new Debugger(); + + // Create Debugger.Object instances referring to the two sandboxes, as + // seen from their own compartments. + const contentBoxDO = dbg.addDebuggee(contentBox); + const chromeBoxDO = dbg.addDebuggee(chromeBox); + + // Use Debugger to view the objects from contentBox. We should get the + // same D.O instance from both getProperty and makeDebuggeeValue, and the + // same property visibility we checked for above. + const mainFromContentDO = contentBoxDO.getProperty("mainObj"); + Assert.equal(mainFromContentDO, contentBoxDO.makeDebuggeeValue(mainObj)); + Assert.equal(mainFromContentDO.getProperty("name"), undefined); + Assert.equal(mainFromContentDO.unsafeDereference(), mainObj); + + const contentFromContentDO = contentBoxDO.getProperty("contentObj"); + Assert.equal( + contentFromContentDO, + contentBoxDO.makeDebuggeeValue(contentObj) + ); + Assert.equal(contentFromContentDO.getProperty("name"), "contentObj"); + Assert.equal(contentFromContentDO.unsafeDereference(), contentObj); + + const chromeFromContentDO = contentBoxDO.getProperty("chromeObj"); + Assert.equal(chromeFromContentDO, contentBoxDO.makeDebuggeeValue(chromeObj)); + Assert.equal(chromeFromContentDO.getProperty("name"), undefined); + Assert.equal(chromeFromContentDO.unsafeDereference(), chromeObj); + + // Similarly, viewing from chromeBox. + const mainFromChromeDO = chromeBoxDO.getProperty("mainObj"); + Assert.equal(mainFromChromeDO, chromeBoxDO.makeDebuggeeValue(mainObj)); + Assert.equal(mainFromChromeDO.getProperty("name"), "mainObj"); + Assert.equal(mainFromChromeDO.unsafeDereference(), mainObj); + + const contentFromChromeDO = chromeBoxDO.getProperty("contentObj"); + Assert.equal(contentFromChromeDO, chromeBoxDO.makeDebuggeeValue(contentObj)); + Assert.equal(contentFromChromeDO.getProperty("name"), "contentObj"); + Assert.equal(contentFromChromeDO.unsafeDereference(), contentObj); + + const chromeFromChromeDO = chromeBoxDO.getProperty("chromeObj"); + Assert.equal(chromeFromChromeDO, chromeBoxDO.makeDebuggeeValue(chromeObj)); + Assert.equal(chromeFromChromeDO.getProperty("name"), "chromeObj"); + Assert.equal(chromeFromChromeDO.unsafeDereference(), chromeObj); +} diff --git a/devtools/server/tests/xpcshell/test_wasm_source-01.js b/devtools/server/tests/xpcshell/test_wasm_source-01.js new file mode 100644 index 0000000000..fe8e43e236 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_wasm_source-01.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow, max-nested-callbacks */ + +"use strict"; + +/** + * Verify if client can receive binary wasm + */ + +var gDebuggee; +var gThreadFront; + +add_task( + threadFrontTest( + async ({ threadFront, debuggee, client }) => { + gThreadFront = threadFront; + gDebuggee = debuggee; + + await gThreadFront.reconfigure({ + observeAsmJS: true, + observeWasm: true, + }); + + test_source(); + }, + { waitForFinish: true, doNotRunWorker: true } + ) +); + +const EXPECTED_CONTENT = String.fromCharCode( + 0, + 97, + 115, + 109, + 1, + 0, + 0, + 0, + 1, + 132, + 128, + 128, + 128, + 0, + 1, + 96, + 0, + 0, + 3, + 130, + 128, + 128, + 128, + 0, + 1, + 0, + 6, + 129, + 128, + 128, + 128, + 0, + 0, + 7, + 133, + 128, + 128, + 128, + 0, + 1, + 1, + 102, + 0, + 0, + 10, + 136, + 128, + 128, + 128, + 0, + 1, + 130, + 128, + 128, + 128, + 0, + 0, + 11 +); + +function test_source() { + gThreadFront.once("paused", function (packet) { + gThreadFront.getSources().then(function (response) { + Assert.ok(!!response); + Assert.ok(!!response.sources); + + const source = response.sources.filter(function (s) { + return s.introductionType === "wasm"; + })[0]; + + Assert.ok(!!source); + + const sourceFront = gThreadFront.source(source); + sourceFront.source().then(function (response) { + Assert.ok(!!response); + Assert.ok(!!response.contentType); + Assert.ok(response.contentType.includes("wasm")); + + const sourceContent = response.source; + Assert.ok(!!sourceContent); + Assert.equal(typeof sourceContent, "object"); + Assert.ok("binary" in sourceContent); + Assert.equal(EXPECTED_CONTENT, sourceContent.binary); + + gThreadFront.resume().then(function () { + threadFrontTestFinished(); + }); + }); + }); + }); + + /* eslint-disable comma-spacing, max-len */ + gDebuggee.eval( + "(" + + function () { + // WebAssembly bytecode was generated by running: + // js -e 'print(wasmTextToBinary("(module(func(export \"f\")))"))' + const m = new WebAssembly.Module( + new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 132, 128, 128, 128, 0, 1, 96, 0, 0, + 3, 130, 128, 128, 128, 0, 1, 0, 6, 129, 128, 128, 128, 0, 0, 7, 133, + 128, 128, 128, 0, 1, 1, 102, 0, 0, 10, 136, 128, 128, 128, 0, 1, + 130, 128, 128, 128, 0, 0, 11, + ]) + ); + const i = new WebAssembly.Instance(m); + debugger; + i.exports.f(); + } + + ")()" + ); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-01.js b/devtools/server/tests/xpcshell/test_watchpoint-01.js new file mode 100644 index 0000000000..2d1d0e78f4 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-01.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/* +- Tests adding set and get watchpoints. +- Tests removing a watchpoint. +- Tests removing all watchpoints. +*/ + +add_task( + threadFrontTest(async args => { + await testSetWatchpoint(args); + await testGetWatchpoint(args); + await testRemoveWatchpoint(args); + await testRemoveWatchpoints(args); + }) +); + +async function testSetWatchpoint({ commands, threadFront, debuggee }) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + thread: threadFront.actor, + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + } // + stopMe({a: { b: 1 }})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add set watchpoint"); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + let result = await evaluateJS("obj.a"); + Assert.equal(result.getGrip().preview.ownProperties.b.value, 1); + + result = await evaluateJS("obj.a.b"); + Assert.equal(result, 1); + + info("Test that watchpoint triggers pause on set"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "setWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value.ownPropertyLength, 1); + + await resume(threadFront); +} + +async function testGetWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a + 4; // 4 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add get watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "get"); + + info("Test that watchpoint triggers pause on get."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "getWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value, 1); + + await resume(threadFront); +} + +async function testRemoveWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info(`Test that we paused on the debugger statement`); + Assert.equal(packet.frame.where.line, 3); + + info(`Add set watchpoint`); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info(`Remove set watchpoint`); + await objClient.removeWatchpoint("a"); + + info(`Test that we do not pause on set`); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 5); + + await resume(threadFront); +} + +async function testRemoveWatchpoints({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-01.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add and then remove set watchpoint"); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + await objClient.removeWatchpoints(); + + info("Test that we do not pause on set"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 5); + + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-02.js b/devtools/server/tests/xpcshell/test_watchpoint-02.js new file mode 100644 index 0000000000..d0739c8a00 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-02.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/* +Test that debugger advances instead of pausing twice on the +same line when encountering both a watchpoint and a breakpoint. +*/ + +add_task( + threadFrontTest(async args => { + await testBreakpointAndSetWatchpoint(args); + await testBreakpointAndGetWatchpoint(args); + await testLoops(args); + }) +); + +// Test that we advance to the next line when a location +// has both a breakpoint and set watchpoint. +async function testBreakpointAndSetWatchpoint({ + commands, + threadFront, + debuggee, +}) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-02.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we pause on the debugger statement."); + Assert.equal(packet.frame.where.line, 3); + Assert.equal(packet.why.type, "debuggerStatement"); + + info("Add set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info("Add breakpoint."); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + const location = { + sourceUrl: source.url, + line: 4, + }; + + threadFront.setBreakpoint(location, {}); + + info("Test that pause occurs on breakpoint."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "breakpoint"); + + const packet3 = await resumeAndWaitForPause(threadFront); + + info("Test that we pause on the second debugger statement."); + Assert.equal(packet3.frame.where.line, 5); + Assert.equal(packet3.why.type, "debuggerStatement"); + + info("Test that the value has updated."); + const result = await evaluateJS("obj.a"); + Assert.equal(result, 2); + + info("Remove breakpoint and finish."); + threadFront.removeBreakpoint(location, {}); + + await resume(threadFront); +} + +// Test that we advance to the next line when a location +// has both a breakpoint and get watchpoint. +async function testBreakpointAndGetWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a + 4; // 4 + debugger; // 5 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-02.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we pause on the debugger statement."); + Assert.equal(packet.frame.where.line, 3); + + info("Add get watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "get"); + + info("Add breakpoint."); + const source = await getSourceById(threadFront, packet.frame.where.actor); + + const location = { + sourceUrl: source.url, + line: 4, + }; + + threadFront.setBreakpoint(location, {}); + + info("Test that pause occurs on breakpoint."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "breakpoint"); + + const packet3 = await resumeAndWaitForPause(threadFront); + + info("Test that we pause on the second debugger statement."); + Assert.equal(packet3.frame.where.line, 5); + Assert.equal(packet3.why.type, "debuggerStatement"); + + info("Remove breakpoint and finish."); + threadFront.removeBreakpoint(location, {}); + + await resume(threadFront); +} + +// Test that we can pause multiple times +// on the same line for a watchpoint. +async function testLoops({ commands, threadFront, debuggee }) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + let i = 0; // 3 + debugger; // 4 + while (i++ < 2) { // 5 + obj.a = 2; // 6 + } // 7 + debugger; // 8 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-02.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we pause on the debugger statement."); + Assert.equal(packet.frame.where.line, 4); + Assert.equal(packet.why.type, "debuggerStatement"); + + info("Add set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info("Test that watchpoint triggers pause on set."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 6); + Assert.equal(packet2.why.type, "setWatchpoint"); + let result = await evaluateJS("obj.a"); + Assert.equal(result, 1); + + info("Test that watchpoint triggers pause on set (2nd time)."); + const packet3 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet3.frame.where.line, 6); + Assert.equal(packet3.why.type, "setWatchpoint"); + let result2 = await evaluateJS("obj.a"); + Assert.equal(result2, 2); + + info("Test that we pause on second debugger statement."); + const packet4 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet4.frame.where.line, 8); + Assert.equal(packet4.why.type, "debuggerStatement"); + + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-03.js b/devtools/server/tests/xpcshell/test_watchpoint-03.js new file mode 100644 index 0000000000..33f4fbd2a2 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-03.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; +/* +See Bug 1601311. +Tests that removing a watchpoint does not change the value of the property that had the watchpoint. +*/ + +add_task( + threadFrontTest(async ({ commands, threadFront, debuggee }) => { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + debugger; // 5 + } // + + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-03.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement."); + Assert.equal(packet.frame.where.line, 3); + + info("Add set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "set"); + + info("Test that we pause on set."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + + const packet3 = await resumeAndWaitForPause(threadFront); + + info("Test that we pause on the second debugger statement."); + Assert.equal(packet3.frame.where.line, 5); + Assert.equal(packet3.why.type, "debuggerStatement"); + + info("Remove watchpoint."); + await objClient.removeWatchpoint("a"); + + info("Test that the value has updated."); + const result = await evaluateJS("obj.a"); + Assert.equal(result, 2); + + info("Finish test."); + await resume(threadFront); + }) +); diff --git a/devtools/server/tests/xpcshell/test_watchpoint-04.js b/devtools/server/tests/xpcshell/test_watchpoint-04.js new file mode 100644 index 0000000000..4ee6eadd5a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-04.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that watchpoints ignore blackboxed sources + */ + +const BLACK_BOXED_URL = "http://example.com/blackboxme.js"; +const SOURCE_URL = "http://example.com/source.js"; + +add_task( + threadFrontTest(async ({ threadFront, debuggee }) => { + await executeOnNextTickAndWaitForPause( + () => evalCode(debuggee), + threadFront + ); + + info(`blackbox the source`); + const { error, sources } = await threadFront.getSources(); + Assert.ok(!error, "Should not get an error: " + error); + const sourceFront = threadFront.source( + sources.filter(s => s.url == BLACK_BOXED_URL)[0] + ); + + await blackBox(sourceFront); + + await threadFront.resume(); + const packet = await executeOnNextTickAndWaitForPause( + debuggee.runTest, + threadFront + ); + + Assert.equal( + packet.frame.where.line, + 3, + "Paused at first debugger statement" + ); + + await addWatchpoint(threadFront, packet.frame, "obj", "a", "set"); + + info(`Resume and skip the watchpoint`); + const pausePacket = await resumeAndWaitForPause(threadFront); + + Assert.equal( + pausePacket.frame.where.line, + 5, + "Paused at second debugger statement" + ); + + await threadFront.resume(); + }) +); + +function evalCode(debuggee) { + Cu.evalInSandbox( + `function doStuff(obj) { + obj.a = 2; + }`, + debuggee, + "1.8", + BLACK_BOXED_URL, + 1 + ); + Cu.evalInSandbox( + `function runTest() { + const obj = {a: 1} + debugger + doStuff(obj); + debugger + }; debugger;`, + debuggee, + "1.8", + SOURCE_URL, + 1 + ); +} diff --git a/devtools/server/tests/xpcshell/test_watchpoint-05.js b/devtools/server/tests/xpcshell/test_watchpoint-05.js new file mode 100644 index 0000000000..4d25a59399 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_watchpoint-05.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +/* eslint-disable no-shadow */ + +"use strict"; + +/* +- Adds a 'get or set' watchpoint. Tests that the debugger will pause on both get and set. +*/ + +add_task( + threadFrontTest(async args => { + await testGetPauseWithGetOrSetWatchpoint(args); + await testSetPauseWithGetOrSetWatchpoint(args); + }) +); + +async function testGetPauseWithGetOrSetWatchpoint({ threadFront, debuggee }) { + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a + 4; // 4 + } // + stopMe({a: 1})`, + debuggee, + "1.8", + "test_watchpoint-05.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add get or set watchpoint."); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "getorset"); + + info("Test that watchpoint triggers pause on get."); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "getWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value, 1); + + await resume(threadFront); +} + +async function testSetPauseWithGetOrSetWatchpoint({ + commands, + threadFront, + debuggee, +}) { + async function evaluateJS(input) { + const { result } = await commands.scriptCommand.execute(input, { + frameActor: packet.frame.actorID, + }); + return result; + } + + function evaluateTestCode(debuggee) { + /* eslint-disable */ + Cu.evalInSandbox( + ` // 1 + function stopMe(obj) { // 2 + debugger; // 3 + obj.a = 2; // 4 + } // + stopMe({a: { b: 1 }})`, + debuggee, + "1.8", + "test_watchpoint-05.js" + ); + /* eslint-disable */ + } + + const packet = await executeOnNextTickAndWaitForPause( + () => evaluateTestCode(debuggee), + threadFront + ); + + info("Test that we paused on the debugger statement"); + Assert.equal(packet.frame.where.line, 3); + + info("Add get or set watchpoint"); + const args = packet.frame.arguments; + const obj = args[0]; + const objClient = threadFront.pauseGrip(obj); + await objClient.addWatchpoint("a", "obj.a", "getorset"); + + let result = await evaluateJS("obj.a"); + Assert.equal(result.getGrip().preview.ownProperties.b.value, 1); + + result = await evaluateJS("obj.a.b"); + Assert.equal(result, 1); + + info("Test that watchpoint triggers pause on set"); + const packet2 = await resumeAndWaitForPause(threadFront); + Assert.equal(packet2.frame.where.line, 4); + Assert.equal(packet2.why.type, "setWatchpoint"); + Assert.equal(obj.preview.ownProperties.a.value.ownPropertyLength, 1); + + await resume(threadFront); +} diff --git a/devtools/server/tests/xpcshell/test_webext_apis.js b/devtools/server/tests/xpcshell/test_webext_apis.js new file mode 100644 index 0000000000..5a2f2b990a --- /dev/null +++ b/devtools/server/tests/xpcshell/test_webext_apis.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const DistinctDevToolsServer = getDistinctDevToolsServer(); +ExtensionTestUtils.init(this); + +add_setup(async () => { + Services.prefs.setBoolPref("extensions.blocklist.enabled", false); + await startupAddonsManager(); +}); + +// Basic request wrapper that sends a request and resolves on the next packet. +// Will only work for very basic scenarios, without events emitted on the server +// etc... +async function sendRequest(transport, request) { + return new Promise(resolve => { + transport.hooks = { + onPacket: packet => { + dump(`received packet: ${JSON.stringify(packet)}\n`); + // Let's resolve only when we get a packet that is related to our + // request. It is needed because some methods do not return the correct + // response right away. This is the case of the `reload` method, which + // receives a `addonListChanged` message first and then a `reload` + // message. + if (packet.from === request.to) { + resolve(packet); + } + }, + }; + transport.send(request); + }); +} + +// If this test case fails, please reach out to webext peers because +// https://github.com/mozilla/web-ext relies on the APIs tested here. +add_task(async function test_webext_run_apis() { + DistinctDevToolsServer.init(); + DistinctDevToolsServer.registerAllActors(); + + const transport = DistinctDevToolsServer.connectPipe(); + + // After calling connectPipe, the root actor will be created on the server + // and a packet will be emitted after a tick. Wait for the initial packet. + await new Promise(resolve => { + transport.hooks = { onPacket: resolve }; + }); + + const getRootResponse = await sendRequest(transport, { + to: "root", + type: "getRoot", + }); + + ok(getRootResponse, "received a response after calling RootActor::getRoot"); + ok(getRootResponse.addonsActor, "getRoot returned an addonsActor id"); + + // installTemporaryAddon + const addonId = "test-addons-actor@mozilla.org"; + const addonPath = getFilePath("addons/web-extension", false, true); + const promiseStarted = AddonTestUtils.promiseWebExtensionStartup(addonId); + const { addon } = await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "installTemporaryAddon", + addonPath, + // The openDevTools parameter is not always passed by web-ext. This test + // omits it, to make sure that the request without the flag is accepted. + // openDevTools: false, + }); + await promiseStarted; + + ok(addon, "addonsActor allows to install a temporary add-on"); + equal(addon.id, addonId, "temporary add-on is the expected one"); + equal(addon.actor, false, "temporary add-on does not have an actor"); + + // listAddons + let { addons } = await sendRequest(transport, { + to: "root", + type: "listAddons", + }); + ok(Array.isArray(addons), "listAddons() returns a list of add-ons"); + equal(addons.length, 1, "expected an add-on installed"); + + const installedAddon = addons[0]; + equal(installedAddon.id, addonId, "installed add-on is the expected one"); + ok(installedAddon.actor, "returned add-on has an actor"); + + // reload + const promiseReloaded = AddonTestUtils.promiseAddonEvent("onInstalled"); + const promiseRestarted = AddonTestUtils.promiseWebExtensionStartup(addonId); + await sendRequest(transport, { + to: installedAddon.actor, + type: "reload", + }); + await Promise.all([promiseReloaded, promiseRestarted]); + + // uninstallAddon + const promiseUninstalled = new Promise(resolve => { + const listener = {}; + listener.onUninstalled = uninstalledAddon => { + if (uninstalledAddon.id == addonId) { + AddonManager.removeAddonListener(listener); + resolve(); + } + }; + AddonManager.addAddonListener(listener); + }); + await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "uninstallAddon", + addonId, + }); + await promiseUninstalled; + + ({ addons } = await sendRequest(transport, { + to: "root", + type: "listAddons", + })); + equal(addons.length, 0, "expected no add-on installed"); + + // Attempt to uninstall an add-on that is (no longer) installed. + let error = await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "uninstallAddon", + addonId, + }); + equal( + error?.message, + `Could not uninstall add-on "${addonId}"`, + "expected error" + ); + + // Attempt to uninstall a non-temporarily loaded extension, which we do not + // allow at the moment. We start by loading an extension, then we call the + // `uninstallAddon`. + const id = "not-a-temporary@extension"; + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id } }, + }, + useAddonManager: "permanent", + }); + await extension.startup(); + + error = await sendRequest(transport, { + to: getRootResponse.addonsActor, + type: "uninstallAddon", + addonId: id, + }); + equal(error?.message, `Could not uninstall add-on "${id}"`, "expected error"); + + await extension.unload(); + + transport.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_webextension_descriptor.js b/devtools/server/tests/xpcshell/test_webextension_descriptor.js new file mode 100644 index 0000000000..00cdeea605 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_webextension_descriptor.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const DistinctDevToolsServer = getDistinctDevToolsServer(); +ExtensionTestUtils.init(this); + +add_setup(async () => { + Services.prefs.setBoolPref("extensions.blocklist.enabled", false); + await startupAddonsManager(); + + // We intentionally generate install-time manifest warnings, so don't trigger + // the special test-only mode of converting them to errors. + Services.prefs.setBoolPref( + "extensions.webextensions.warnings-as-errors", + false + ); + + DistinctDevToolsServer.init(); + DistinctDevToolsServer.registerAllActors(); +}); + +// Verifies: +// - listAddons +// - WebExtensionDescriptorActor output +// Also a regression test for bug 1837185, that AddonManager.sys.mjs and +// ExtensionParent.sys.mjs are imported from the correct loader. +add_task(async function test_listAddons_and_WebExtensionDescriptor() { + const transport = DistinctDevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + + const getRootResponse = await client.mainRoot.getRoot(); + + ok(getRootResponse, "received a response after calling RootActor::getRoot"); + ok(getRootResponse.addonsActor, "getRoot returned an addonsActor id"); + + const ADDON_ID = "with@warning"; + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + name: "DummyExtensionWithUnknownManifestKey", + unknown_manifest_key: "this is an unknown manifest key", + browser_specific_settings: { gecko: { id: ADDON_ID } }, + }, + background: `browser.test.sendMessage("background_started");`, + }); + await extension.startup(); + await extension.awaitMessage("background_started"); + + // listAddons: addon after new install. + { + const listAddonsResponse = await client.mainRoot.listAddons(); + const addon = listAddonsResponse.find(a => a.id === ADDON_ID); + ok(addon, "listAddons() returns a list of add-ons including with@warning"); + + // Inspect all raw properties of the message, to make sure that we always + // have full coverage for all current and future properties. + const { actor, url, warnings, ...addonMinusSomeKeys } = addon._form; + const actorPattern = /^server\d+\.conn\d+\.webExtensionDescriptor\d+$/; + ok(actorPattern.test(actor), `actor is webExtensionDescriptor: ${actor}`); + // We don't care about the exact path, just a dummy check: + ok(url.endsWith(".xpi"), `url is path to the xpi file`); + + deepEqual( + warnings, + [ + "Reading manifest: Warning processing unknown_manifest_key: An unexpected property was found in the WebExtension manifest.", + ], + "Can retrieve warnings." + ); + + // Verify that the other remaining keys have a meaningful value. + // This is mainly to have some form of verification on the value of the + // properties. If this check ever fails, double-check whether the proposed + // change makes sense and if it does just update the test expectation here. + deepEqual( + addonMinusSomeKeys, + { + backgroundScriptStatus: undefined, + debuggable: true, + hidden: false, + iconDataURL: undefined, + iconURL: null, + id: ADDON_ID, + isSystem: false, + isWebExtension: true, + manifestURL: `moz-extension://${extension.uuid}/manifest.json`, + name: "DummyExtensionWithUnknownManifestKey", + persistentBackgroundScript: true, + temporarilyInstalled: false, + traits: { + supportsReloadDescriptor: true, + watcher: true, + }, + }, + "WebExtensionDescriptorActor content matches the add-on" + ); + } + + await extension.upgrade({ + manifest: { + name: "Updated_extension", + new_unknown_manifest_key: "different warning than before", + browser_specific_settings: { gecko: { id: ADDON_ID } }, + }, + background: `browser.test.sendMessage("updated_done");`, + }); + await extension.awaitMessage("updated_done"); + + // listAddons: addon after update. + { + const listAddonsResponse = await client.mainRoot.listAddons(); + const addon = listAddonsResponse.find(a => a.id === ADDON_ID); + ok(addon, "listAddons() should still list the add-on after update"); + equal(addon.name, "Updated_extension", "Got updated name"); + deepEqual( + addon.warnings, + [ + "Reading manifest: Warning processing new_unknown_manifest_key: An unexpected property was found in the WebExtension manifest.", + ], + "Can retrieve new warnings for updated add-on." + ); + } + + await extension.unload(); + + // listAddons: addon after removal - gone. + { + const listAddonsResponse = await client.mainRoot.listAddons(); + const addon = listAddonsResponse.find(a => a.id === ADDON_ID); + deepEqual(addon, null, "Add-on should be gone after removal"); + } + + await client.close(); +}); diff --git a/devtools/server/tests/xpcshell/test_xpcshell_debugging.js b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js new file mode 100644 index 0000000000..2f7cf70e42 --- /dev/null +++ b/devtools/server/tests/xpcshell/test_xpcshell_debugging.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the xpcshell-test debug support. Ideally we should have this test +// next to the xpcshell support code, but that's tricky... + +// HACK: ServiceWorkerManager requires the "profile-change-teardown" to cleanly +// shutdown, and setting _profileInitialized to `true` will trigger those +// notifications (see /testing/xpcshell/head.js). +// eslint-disable-next-line no-undef +_profileInitialized = true; + +add_task(async function () { + const testFile = do_get_file("xpcshell_debugging_script.js"); + + // _setupDevToolsServer is from xpcshell-test's head.js + /* global _setupDevToolsServer */ + let testInitialized = false; + const { DevToolsServer } = _setupDevToolsServer([testFile.path], () => { + testInitialized = true; + }); + const transport = DevToolsServer.connectPipe(); + const client = new DevToolsClient(transport); + await client.connect(); + + // Ensure that global actors are available. Just test the device actor. + const deviceFront = await client.mainRoot.getFront("device"); + const desc = await deviceFront.getDescription(); + equal( + desc.geckobuildid, + Services.appinfo.platformBuildID, + "device actor works" + ); + + // Even though we have no tabs, getMainProcess gives us the chrome debugger. + const targetDescriptor = await client.mainRoot.getMainProcess(); + const front = await targetDescriptor.getTarget(); + const watcher = await targetDescriptor.getWatcher(); + + const threadFront = await front.attachThread(); + + // Checks that the thread actor initializes immediately and that _setupDevToolsServer + // callback gets called. + ok(testInitialized); + + const onPause = waitForPause(threadFront); + + // Now load our test script, + // in another event loop so that the test can keep running! + Services.tm.dispatchToMainThread(() => { + load(testFile.path); + }); + + // and our "paused" listener should get hit. + info("Wait for first paused event"); + const packet1 = await onPause; + equal( + packet1.why.type, + "breakpoint", + "yay - hit the breakpoint at the first line in our script" + ); + + // Resume again - next stop should be our "debugger" statement. + info("Wait for second pause event"); + const packet2 = await resumeAndWaitForPause(threadFront); + equal( + packet2.why.type, + "debuggerStatement", + "yay - hit the 'debugger' statement in our script" + ); + + info("Dynamically add a breakpoint after the debugger statement"); + const breakpointsFront = await watcher.getBreakpointListActor(); + await breakpointsFront.setBreakpoint( + { sourceUrl: testFile.path, line: 11 }, + {} + ); + + // Resume again - next stop should be the new breakpoint. + info("Wait for third pause event"); + const packet3 = await resumeAndWaitForPause(threadFront); + equal( + packet3.why.type, + "breakpoint", + "yay - hit the breakpoint added after starting the test" + ); + finishClient(client); +}); diff --git a/devtools/server/tests/xpcshell/testactors.js b/devtools/server/tests/xpcshell/testactors.js new file mode 100644 index 0000000000..af208fe93e --- /dev/null +++ b/devtools/server/tests/xpcshell/testactors.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + LazyPool, + createExtraActors, +} = require("resource://devtools/shared/protocol/lazy-pool.js"); +const { RootActor } = require("resource://devtools/server/actors/root.js"); +const { ThreadActor } = require("resource://devtools/server/actors/thread.js"); +const { + DevToolsServer, +} = require("resource://devtools/server/devtools-server.js"); +const { + ActorRegistry, +} = require("resource://devtools/server/actors/utils/actor-registry.js"); +const { + SourcesManager, +} = require("resource://devtools/server/actors/utils/sources-manager.js"); +const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js"); +const protocol = require("resource://devtools/shared/protocol.js"); +const { + windowGlobalTargetSpec, +} = require("resource://devtools/shared/specs/targets/window-global.js"); +const { + tabDescriptorSpec, +} = require("resource://devtools/shared/specs/descriptors/tab.js"); +const Targets = require("resource://devtools/server/actors/targets/index.js"); +const { + createContentProcessSessionContext, +} = require("resource://devtools/server/actors/watcher/session-context.js"); + +var gTestGlobals = new Set(); +DevToolsServer.addTestGlobal = function (global) { + gTestGlobals.add(global); +}; +DevToolsServer.removeTestGlobal = function (global) { + gTestGlobals.delete(global); +}; + +DevToolsServer.getTestGlobal = function (name) { + for (const g of gTestGlobals) { + if (g.__name == name) { + return g; + } + } + + return null; +}; + +var gAllowNewThreadGlobals = false; +DevToolsServer.allowNewThreadGlobals = function () { + gAllowNewThreadGlobals = true; +}; +DevToolsServer.disallowNewThreadGlobals = function () { + gAllowNewThreadGlobals = false; +}; + +// A mock tab list, for use by tests. This simply presents each global in +// gTestGlobals as a tab, and the list is fixed: it never calls its +// onListChanged handler. +// +// As implemented now, we consult gTestGlobals when we're constructed, not +// when we're iterated over, so tests have to add their globals before the +// root actor is created. +function TestTabList(connection) { + this.conn = connection; + + // An array of actors for each global added with + // DevToolsServer.addTestGlobal. + this._descriptorActors = []; + + // A pool mapping those actors' names to the actors. + this._descriptorActorPool = new LazyPool(connection); + + for (const global of gTestGlobals) { + const actor = new TestTargetActor(connection, global); + this._descriptorActorPool.manage(actor); + + const descriptorActor = new TestDescriptorActor(connection, actor); + this._descriptorActorPool.manage(descriptorActor); + + this._descriptorActors.push(descriptorActor); + } +} + +TestTabList.prototype = { + constructor: TestTabList, + destroy() {}, + getList() { + return Promise.resolve([...this._descriptorActors]); + }, + // Helper method only available for the xpcshell implementation of tablist. + getTargetActorForTab(title) { + const descriptorActor = this._descriptorActors.find(d => d.title === title); + if (!descriptorActor) { + return null; + } + return descriptorActor._targetActor; + }, +}; + +exports.createRootActor = function createRootActor(connection) { + ActorRegistry.registerModule("devtools/server/actors/webconsole", { + prefix: "console", + constructor: "WebConsoleActor", + type: { target: true }, + }); + const root = new RootActor(connection, { + tabList: new TestTabList(connection), + globalActorFactories: ActorRegistry.globalActorFactories, + }); + + root.applicationType = "xpcshell-tests"; + return root; +}; + +class TestDescriptorActor extends protocol.Actor { + constructor(conn, targetActor) { + super(conn, tabDescriptorSpec); + this._targetActor = targetActor; + } + + // We don't exercise the selected tab in xpcshell tests. + get selected() { + return false; + } + + get title() { + return this._targetActor.title; + } + + form() { + const form = { + actor: this.actorID, + traits: {}, + selected: this.selected, + title: this._targetActor.title, + url: this._targetActor.url, + }; + + return form; + } + + getFavicon() { + return ""; + } + + getTarget() { + return this._targetActor.form(); + } +} + +class TestTargetActor extends protocol.Actor { + constructor(conn, global) { + super(conn, windowGlobalTargetSpec); + + this.sessionContext = createContentProcessSessionContext(); + this._global = global; + this._global.wrappedJSObject = global; + this.threadActor = new ThreadActor(this, this._global); + this.conn.addActor(this.threadActor); + this._extraActors = {}; + // This is a hack in order to enable threadActor to be accessed from getFront + this._extraActors.threadActor = this.threadActor; + this.makeDebugger = makeDebugger.bind(null, { + findDebuggees: () => [this._global], + shouldAddNewGlobalAsDebuggee: g => gAllowNewThreadGlobals, + }); + this.dbg = this.makeDebugger(); + this.notifyResources = this.notifyResources.bind(this); + } + + targetType = Targets.TYPES.FRAME; + + get window() { + return this._global; + } + + // Both title and url point to this._global.__name + get title() { + return this._global.__name; + } + + get url() { + return this._global.__name; + } + + get sourcesManager() { + if (!this._sourcesManager) { + this._sourcesManager = new SourcesManager(this.threadActor); + } + return this._sourcesManager; + } + + form() { + const response = { + actor: this.actorID, + title: this.title, + threadActor: this.threadActor.actorID, + }; + + // Walk over target-scoped actors and add them to a new LazyPool. + const actorPool = new LazyPool(this.conn); + const actors = createExtraActors( + ActorRegistry.targetScopedActorFactories, + actorPool, + this + ); + if (actorPool?._poolMap.size > 0) { + this._descriptorActorPool = actorPool; + this.conn.addActorPool(this._descriptorActorPool); + } + + return { ...response, ...actors }; + } + + detach(request) { + this.threadActor.destroy(); + return { type: "detached" }; + } + + reload(request) { + this.sourcesManager.reset(); + this.threadActor.clearDebuggees(); + this.threadActor.dbg.addDebuggees(); + return {}; + } + + removeActorByName(name) { + const actor = this._extraActors[name]; + if (this._descriptorActorPool) { + this._descriptorActorPool.removeActor(actor); + } + delete this._extraActors[name]; + } + + notifyResources(updateType, resources) { + this.emit(`resource-${updateType}-form`, resources); + } +} diff --git a/devtools/server/tests/xpcshell/webextension-helpers.js b/devtools/server/tests/xpcshell/webextension-helpers.js new file mode 100644 index 0000000000..46968f09e7 --- /dev/null +++ b/devtools/server/tests/xpcshell/webextension-helpers.js @@ -0,0 +1,197 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* globals browser */ + +"use strict"; + +/** + * Test helpers shared by the devtools server xpcshell tests related to webextensions. + */ + +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +/** + * Loads and starts up a test extension given the provided extension configuration. + * + * @param {Object} extConfig - The extension configuration object + * @return {ExtensionWrapper} extension - Resolves with an extension object once the + * extension has started up. + */ +async function startupExtension(extConfig) { + const extension = ExtensionTestUtils.loadExtension(extConfig); + + await extension.startup(); + + return extension; +} +exports.startupExtension = startupExtension; + +/** + * Initializes the extensionStorage actor for a given extension. This is effectively + * what happens when the addon storage panel is opened in the browser. + * + * @param {String} - id, The addon id + * @return {Object} - Resolves with the DevTools "commands" objact and the extensionStorage + * resource/front. + */ +async function openAddonStoragePanel(id) { + const commands = await CommandsFactory.forAddon(id); + await commands.targetCommand.startListening(); + + // Fetch the EXTENSION_STORAGE resource. + // Unfortunately, we can't use resourceCommand.waitForNextResource as it would destroy + // the actor by immediately unwatching for the resource type. + const extensionStorage = await new Promise(resolve => { + commands.resourceCommand.watchResources( + [commands.resourceCommand.TYPES.EXTENSION_STORAGE], + { + onAvailable(resources) { + resolve(resources[0]); + }, + } + ); + }); + + return { commands, extensionStorage }; +} +exports.openAddonStoragePanel = openAddonStoragePanel; + +/** + * Builds the extension configuration object passed into ExtensionTestUtils.loadExtension + * + * @param {Object} options - Options, if any, to add to the configuration + * @param {Function} options.background - A function comprising the test extension's + * background script if provided + * @param {Object} options.files - An object whose keys correspond to file names and + * values map to the file contents + * @param {Object} options.manifest - An object representing the extension's manifest + * @return {Object} - The extension configuration object + */ +function getExtensionConfig(options = {}) { + const { manifest, ...otherOptions } = options; + const baseConfig = { + manifest: { + ...manifest, + permissions: ["storage"], + }, + useAddonManager: "temporary", + }; + return { + ...baseConfig, + ...otherOptions, + }; +} +exports.getExtensionConfig = getExtensionConfig; + +/** + * Shared files for a test extension that has no background page but adds storage + * items via a transient extension page in a tab + */ +const ext_no_bg = { + files: { + "extension_page_in_tab.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>Extension Page in a Tab</h1> + <script src="extension_page_in_tab.js"></script> + </body> + </html>`, + "extension_page_in_tab.js": extensionScriptWithMessageListener, + }, +}; +exports.ext_no_bg = ext_no_bg; + +/** + * An extension script that can be used in any extension context (e.g. as a background + * script or as an extension page script loaded in a tab). + */ +async function extensionScriptWithMessageListener() { + let fireOnChanged = false; + browser.storage.onChanged.addListener(() => { + if (fireOnChanged) { + // Do not fire it again until explicitly requested again using the "storage-local-fireOnChanged" test message. + fireOnChanged = false; + browser.test.sendMessage("storage-local-onChanged"); + } + }); + + browser.test.onMessage.addListener(async (msg, ...args) => { + let item = null; + switch (msg) { + case "storage-local-set": + await browser.storage.local.set(args[0]); + break; + case "storage-local-get": + item = await browser.storage.local.get(args[0]); + break; + case "storage-local-remove": + await browser.storage.local.remove(args[0]); + break; + case "storage-local-clear": + await browser.storage.local.clear(); + break; + case "storage-local-fireOnChanged": { + // Allow the storage.onChanged listener to send a test event + // message when onChanged is being fired. + fireOnChanged = true; + // Do not fire fireOnChanged:done. + return; + } + default: + browser.test.fail(`Unexpected test message: ${msg}`); + } + + browser.test.sendMessage(`${msg}:done`, item); + }); + // window is available in background scripts + // eslint-disable-next-line no-undef + browser.test.sendMessage("extension-origin", window.location.origin); +} +exports.extensionScriptWithMessageListener = extensionScriptWithMessageListener; + +/** + * Shutdown procedure common to all tasks. + * + * @param {Object} extension - The test extension + * @param {Object} commands - The web extension commands used by the DevTools to interact with the backend + */ +async function shutdown(extension, commands) { + if (commands) { + await commands.destroy(); + } + await extension.unload(); +} +exports.shutdown = shutdown; + +/** + * Mocks the missing 'storage/permanent' directory needed by the "indexedDB" + * storage actor's 'populateStoresForHosts' method. This + * directory exists in a full browser i.e. mochitest. + */ +function createMissingIndexedDBDirs() { + const dir = Services.dirsvc.get("ProfD", Ci.nsIFile).clone(); + dir.append("storage"); + if (!dir.exists()) { + dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } + dir.append("permanent"); + if (!dir.exists()) { + dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } + + return dir; +} +exports.createMissingIndexedDBDirs = createMissingIndexedDBDirs; diff --git a/devtools/server/tests/xpcshell/xpcshell.ini b/devtools/server/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..9bfe8f5e47 --- /dev/null +++ b/devtools/server/tests/xpcshell/xpcshell.ini @@ -0,0 +1,249 @@ +[DEFAULT] +tags = devtools +head = head_dbg.js +firefox-appdir = browser +skip-if = toolkit == 'android' +# While not every devtools test uses evalInSandbox over 80 do, so it's easier to +# set allow_parent_unrestricted_js_loads for all the tests here. +# Similar story for the eval restrictions +prefs = + security.allow_parent_unrestricted_js_loads=true + security.allow_eval_with_system_principal=true + security.allow_eval_in_parent_process=true + +support-files = + completions.js + webextension-helpers.js + source-map-data/sourcemapped.coffee + source-map-data/sourcemapped.map + post_init_global_actors.js + post_init_target_scoped_actors.js + pre_init_global_actors.js + pre_init_target_scoped_actors.js + registertestactors-lazy.js + sourcemapped.js + testactors.js + hello-actor.js + stepping.js + stepping-async.js + source-03.js + setBreakpoint-on-column.js + setBreakpoint-on-column-minified.js + setBreakpoint-on-column-in-gcd-script.js + setBreakpoint-on-column-with-no-offsets.js + setBreakpoint-on-column-with-no-offsets-in-gcd-script.js + setBreakpoint-on-line.js + setBreakpoint-on-line-in-gcd-script.js + setBreakpoint-on-line-with-multiple-offsets.js + setBreakpoint-on-line-with-multiple-statements.js + setBreakpoint-on-line-with-no-offsets.js + setBreakpoint-on-line-with-no-offsets-in-gcd-script.js + addons/web-extension/manifest.json + addons/web-extension-upgrade/manifest.json + addons/web-extension2/manifest.json + +[test_addon_debugging_connect.js] +[test_addon_events.js] +[test_addon_reload.js] +[test_addons_actor.js] +[test_animation_name.js] +[test_animation_type.js] +[test_nesting-03.js] +[test_nesting-04.js] +[test_forwardingprefix.js] +[test_getyoungestframe.js] +[test_dbgactor.js] +[test_dbgglobal.js] +[test_dbgclient_debuggerstatement.js] +[test_attach.js] +[test_MemoryActor_saveHeapSnapshot_01.js] +[test_MemoryActor_saveHeapSnapshot_02.js] +[test_MemoryActor_saveHeapSnapshot_03.js] +[test_blackboxing-01.js] +[test_blackboxing-02.js] +[test_blackboxing-03.js] +[test_blackboxing-04.js] +[test_blackboxing-05.js] +[test_blackboxing-08.js] +[test_extension_storage_actor.js] +skip-if = tsan # Unreasonably slow, bug 1612707 +[test_extension_storage_actor_upgrade.js] +[test_frameactor-01.js] +[test_frameactor-02.js] +[test_frameactor-03.js] +[test_frameactor-04.js] +[test_frameactor-05.js] +[test_frameactor_wasm-01.js] +[test_framearguments-01.js] +[test_getRuleText.js] +[test_getTextAtLineColumn.js] +[test_pauselifetime-01.js] +[test_pauselifetime-02.js] +[test_pauselifetime-03.js] +[test_pauselifetime-04.js] +[test_threadlifetime-01.js] +[test_threadlifetime-02.js] +[test_threadlifetime-04.js] +[test_functiongrips-01.js] +[test_front_destroy.js] +[test_nativewrappers.js] +[test_nodelistactor.js] +[test_format_command.js] +[test_register_actor.js] +[test_breakpoint-01.js] +[test_breakpoint-03.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_breakpoint-04.js] +[test_breakpoint-05.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_breakpoint-06.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_breakpoint-07.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_breakpoint-08.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_breakpoint-09.js] +[test_breakpoint-10.js] +[test_breakpoint-11.js] +[test_breakpoint-12.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_breakpoint-13.js] +[test_breakpoint-14.js] +[test_breakpoint-16.js] +[test_breakpoint-17.js] +skip-if = true # tests for breakpoint actors are obsolete bug 1524374 +[test_breakpoint-18.js] +[test_breakpoint-19.js] +skip-if = true +reason = bug 1104838 +[test_breakpoint-20.js] +[test_breakpoint-21.js] +[test_breakpoint-22.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_breakpoint-23.js] +[test_breakpoint-24.js] +[test_breakpoint-25.js] +[test_breakpoint-26.js] +[test_conditional_breakpoint-01.js] +[test_conditional_breakpoint-02.js] +[test_conditional_breakpoint-03.js] +[test_conditional_breakpoint-04.js] +[test_console_eval-01.js] +[test_console_eval-02.js] +[test_logpoint-01.js] +[test_logpoint-02.js] +[test_logpoint-03.js] +[test_listsources-01.js] +[test_listsources-02.js] +[test_listsources-03.js] +[test_new_source-01.js] +[test_new_source-02.js] +[test_objectgrips-02.js] +[test_objectgrips-03.js] +[test_objectgrips-04.js] +[test_objectgrips-05.js] +[test_objectgrips-06.js] +[test_objectgrips-07.js] +[test_objectgrips-08.js] +[test_objectgrips-14.js] +[test_objectgrips-15.js] +[test_objectgrips-16.js] +[test_objectgrips-17.js] +[test_objectgrips-18.js] +[test_objectgrips-19.js] +[test_objectgrips-20.js] +[test_objectgrips-21.js] +[test_objectgrips-22.js] +[test_objectgrips-23.js] +[test_objectgrips-24.js] +[test_objectgrips-25.js] +[test_objectgrips-property-value-01.js] +[test_objectgrips-property-value-02.js] +[test_objectgrips-property-value-03.js] +[test_objectgrips-sparse-array.js] +[test_objectgrips-fn-apply-01.js] +[test_objectgrips-fn-apply-02.js] +[test_objectgrips-fn-apply-03.js] +[test_objectgrips-nested-promise.js] +[test_objectgrips-nested-proxy.js] +[test_promise_state-01.js] +[test_promise_state-02.js] +[test_promise_state-03.js] +[test_interrupt.js] +[test_stepping-01.js] +[test_stepping-02.js] +[test_stepping-03.js] +[test_stepping-04.js] +[test_stepping-05.js] +[test_stepping-06.js] +[test_stepping-07.js] +[test_stepping-08.js] +[test_stepping-09.js] +[test_stepping-10.js] +[test_stepping-11.js] +[test_stepping-12.js] +[test_stepping-13.js] +[test_stepping-14.js] +[test_stepping-15.js] +[test_stepping-16.js] +[test_stepping-17.js] +[test_stepping-18.js] +[test_stepping-19.js] +[test_stepping-with-skip-breakpoints.js] +[test_framebindings-01.js] +[test_framebindings-02.js] +[test_framebindings-03.js] +[test_framebindings-04.js] +[test_framebindings-05.js] +[test_framebindings-06.js] +[test_framebindings-07.js] +[test_pause_exceptions-01.js] +[test_pause_exceptions-02.js] +[test_pause_exceptions-03.js] +[test_pause_exceptions-04.js] +[test_longstringgrips-01.js] +[test_source-01.js] +[test_source-02.js] +[test_source-03.js] +[test_source-04.js] +[test_wasm_source-01.js] +[test_watchpoint-01.js] +[test_watchpoint-02.js] +[test_watchpoint-03.js] +[test_watchpoint-04.js] +skip-if = apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs +[test_watchpoint-05.js] +[test_breakpoint-actor-map.js] +skip-if = true # tests for breakpoint actors are obsolete bug 1524374 +[test_unsafeDereference.js] +[test_add_actors.js] +[test_ignore_caught_exceptions.js] +[test_ignore_no_interface_exceptions.js] +[test_requestTypes.js] +reason = bug 937197 +[test_layout-reflows-observer.js] +[test_client_request.js] +[test_symbols-01.js] +[test_symbols-02.js] +[test_xpcshell_debugging.js] +support-files = xpcshell_debugging_script.js +[test_setBreakpoint-at-the-beginning-of-a-minified-fn.js] +[test_setBreakpoint-at-the-end-of-a-minified-fn.js] +[test_setBreakpoint-on-column.js] +[test_setBreakpoint-on-column-in-gcd-script.js] +[test_setBreakpoint-on-line.js] +[test_setBreakpoint-on-line-in-gcd-script.js] +[test_setBreakpoint-on-line-with-multiple-offsets.js] +[test_setBreakpoint-on-line-with-multiple-statements.js] +[test_setBreakpoint-on-line-with-no-offsets.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_setBreakpoint-on-line-with-no-offsets-in-gcd-script.js] +skip-if = true # breakpoint sliding is not supported bug 1525685 +[test_safe-getter.js] +[test_shapes_highlighter_helpers.js] +[test_symbolactor.js] +[test_webext_apis.js] +[test_webextension_descriptor.js] +[test_restartFrame-01.js] +[test_connection_closes_all_pools.js] +[test_sessionDataHelpers.js] diff --git a/devtools/server/tests/xpcshell/xpcshell_debugging_script.js b/devtools/server/tests/xpcshell/xpcshell_debugging_script.js new file mode 100644 index 0000000000..f762b1c3e8 --- /dev/null +++ b/devtools/server/tests/xpcshell/xpcshell_debugging_script.js @@ -0,0 +1,11 @@ +dump("hello from the debugee!\n"); +// We should hit the above dump as we set a breakpoint on the first line + +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This is a file that test_xpcshell_debugging.js debugs. + +debugger; // and why not check we hit this!? + +dump("try to set a breakpoint here"); |