/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; var Cr = Components.results; var CC = Components.Constructor; // Populate AppInfo before anything (like the shared loader) accesses // System.appinfo, which is a lazy getter. const _appInfo = {}; Cu.import("resource://testing-common/AppInfo.jsm", _appInfo); _appInfo.updateAppInfo({ ID: "devtools@tests.mozilla.org", name: "devtools-tests", version: "1", platformVersion: "42", crashReporter: true, }); const { require, loader } = Cu.import("resource://devtools/shared/Loader.jsm", {}); const { worker } = Cu.import("resource://devtools/shared/worker/loader.js", {}); const promise = require("promise"); const { Task } = require("devtools/shared/task"); const { console } = require("resource://gre/modules/Console.jsm"); const { NetUtil } = require("resource://gre/modules/NetUtil.jsm"); const Services = require("Services"); // 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", true); // Enable remote debugging for the relevant tests. Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { DebuggerServer } = require("devtools/server/main"); const { DebuggerServer: WorkerDebuggerServer } = worker.require("devtools/server/main"); const { DebuggerClient, ObjectClient } = require("devtools/shared/client/main"); const { MemoryFront } = require("devtools/shared/fronts/memory"); const { addDebuggerToGlobal } = Cu.import("resource://gre/modules/jsdebugger.jsm", {}); const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); var loadSubScript = Cc[ "@mozilla.org/moz/jssubscript-loader;1" ].getService(Ci.mozIJSSubScriptLoader).loadSubScript; /** * Initializes any test that needs to work with add-ons. */ function startupAddonsManager() { // Create a directory for extensions. const profileDir = do_get_profile().clone(); profileDir.append("extensions"); const internalManager = Cc["@mozilla.org/addons/integration;1"] .getService(Ci.nsIObserver) .QueryInterface(Ci.nsITimerCallback); internalManager.observe(null, "addons-startup", null); } /** * Create a `run_test` function that runs the given generator in a task after * having attached to a memory actor. When done, the memory actor is detached * from, the client is finished, and the test is finished. * * @param {GeneratorFunction} testGeneratorFunction * The generator function is passed (DebuggerClient, MemoryFront) * arguments. * * @returns `run_test` function */ function makeMemoryActorTest(testGeneratorFunction) { const TEST_GLOBAL_NAME = "test_MemoryActor"; return function run_test() { do_test_pending(); startTestDebuggerServer(TEST_GLOBAL_NAME).then(client => { DebuggerServer.registerModule("devtools/server/actors/heap-snapshot-file", { prefix: "heapSnapshotFile", constructor: "HeapSnapshotFileActor", type: { global: true } }); getTestTab(client, TEST_GLOBAL_NAME, function (tabForm, rootForm) { if (!tabForm || !rootForm) { ok(false, "Could not attach to test tab: " + TEST_GLOBAL_NAME); return; } Task.spawn(function* () { try { const memoryFront = new MemoryFront(client, tabForm, rootForm); yield memoryFront.attach(); yield* testGeneratorFunction(client, memoryFront); yield memoryFront.detach(); } catch (err) { DevToolsUtils.reportException("makeMemoryActorTest", err); ok(false, "Got an error: " + err); } finishClient(client); }); }); }); }; } /** * Save as makeMemoryActorTest but attaches the MemoryFront to the MemoryActor * scoped to the full runtime rather than to a tab. */ function makeFullRuntimeMemoryActorTest(testGeneratorFunction) { return function run_test() { do_test_pending(); startTestDebuggerServer("test_MemoryActor").then(client => { DebuggerServer.registerModule("devtools/server/actors/heap-snapshot-file", { prefix: "heapSnapshotFile", constructor: "HeapSnapshotFileActor", type: { global: true } }); getChromeActors(client).then(function (form) { if (!form) { ok(false, "Could not attach to chrome actors"); return; } Task.spawn(function* () { try { const rootForm = yield listTabs(client); const memoryFront = new MemoryFront(client, form, rootForm); yield memoryFront.attach(); yield* testGeneratorFunction(client, memoryFront); yield memoryFront.detach(); } catch (err) { DevToolsUtils.reportException("makeMemoryActorTest", err); ok(false, "Got an error: " + err); } finishClient(client); }); }); }); }; } function createTestGlobal(name) { let sandbox = Cu.Sandbox( Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) ); sandbox.__name = name; 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.listTabs(); } function findTab(tabs, title) { dump("Finding tab with title '" + title + "'.\n"); for (let tab of tabs) { if (tab.title === title) { return tab; } } return null; } function attachTab(client, tab) { dump("Attaching to tab with title '" + tab.title + "'.\n"); return client.attachTab(tab.actor); } function waitForNewSource(threadClient, url) { dump("Waiting for new source with url '" + url + "'.\n"); return waitForEvent(threadClient, "newSource", function (packet) { return packet.source.url === url; }); } function attachThread(tabClient, options = {}) { dump("Attaching to thread.\n"); return tabClient.attachThread(options); } function resume(threadClient) { dump("Resuming thread.\n"); return threadClient.resume(); } function getSources(threadClient) { dump("Getting sources.\n"); return threadClient.getSources(); } function findSource(sources, url) { dump("Finding source with url '" + url + "'.\n"); for (let source of sources) { if (source.url === url) { return source; } } return null; } function waitForPause(threadClient) { dump("Waiting for pause.\n"); return waitForEvent(threadClient, "paused"); } function setBreakpoint(sourceClient, location) { dump("Setting breakpoint.\n"); return sourceClient.setBreakpoint(location); } function dumpn(msg) { dump("DBG-TEST: " + msg + "\n"); } function testExceptionHook(ex) { try { do_report_unexpected_exception(ex); } catch (ex) { return {throw: ex}; } return undefined; } // Convert an nsIScriptError 'aFlags' value into an appropriate string. function scriptErrorFlagsToKind(aFlags) { var kind; if (aFlags & Ci.nsIScriptError.warningFlag) kind = "warning"; if (aFlags & Ci.nsIScriptError.exceptionFlag) kind = "exception"; else kind = "error"; if (aFlags & Ci.nsIScriptError.strictFlag) kind = "strict " + kind; return kind; } // Register a console listener, so console messages don't just disappear // into the ether. var errorCount = 0; var listener = { observe: function (aMessage) { try { errorCount++; try { // If we've been given an nsIScriptError, then we can print out // something nicely formatted, for tools like Emacs to pick up. var scriptError = aMessage.QueryInterface(Ci.nsIScriptError); dumpn(aMessage.sourceName + ":" + aMessage.lineNumber + ": " + scriptErrorFlagsToKind(aMessage.flags) + ": " + aMessage.errorMessage); var string = aMessage.errorMessage; } catch (x) { // Be a little paranoid with message, as the whole goal here is to lose // no information. try { var string = "" + aMessage.message; } catch (x) { var string = ""; } } // Make sure we exit all nested event loops so that the test can finish. while (DebuggerServer && DebuggerServer.xpcInspector && DebuggerServer.xpcInspector.eventLoopNestLevel > 0) { DebuggerServer.xpcInspector.exitNestedEventLoop(); } // In the world before bug 997440, exceptions were getting lost because of // the arbitrary JSContext being used in nsXPCWrappedJSClass::CallMethod. // In the new world, the wanderers have returned. However, because of the, // currently very-broken, exception reporting machinery in // XPCWrappedJSClass 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. } } }; var consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService); consoleService.registerListener(listener); function check_except(func) { try { func(); } catch (e) { do_check_true(true); return; } dumpn("Should have thrown an exception: " + func.toString()); do_check_true(false); } function testGlobal(aName) { let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] .createInstance(Ci.nsIPrincipal); let sandbox = Cu.Sandbox(systemPrincipal); sandbox.__name = aName; return sandbox; } function addTestGlobal(aName, aServer = DebuggerServer) { let global = testGlobal(aName); aServer.addTestGlobal(global); return global; } // List the DebuggerClient |aClient|'s tabs, look for one whose title is // |aTitle|, and apply |aCallback| to the packet's entry for that tab. function getTestTab(aClient, aTitle, aCallback) { aClient.listTabs(function (aResponse) { for (let tab of aResponse.tabs) { if (tab.title === aTitle) { aCallback(tab, aResponse); return; } } aCallback(null); }); } // Attach to |aClient|'s tab whose title is |aTitle|; pass |aCallback| the // response packet and a TabClient instance referring to that tab. function attachTestTab(aClient, aTitle, aCallback) { getTestTab(aClient, aTitle, function (aTab) { aClient.attachTab(aTab.actor, aCallback); }); } // Attach to |aClient|'s tab whose title is |aTitle|, and then attach to // that tab's thread. Pass |aCallback| the thread attach response packet, a // TabClient referring to the tab, and a ThreadClient referring to the // thread. function attachTestThread(aClient, aTitle, aCallback) { attachTestTab(aClient, aTitle, function (aTabResponse, aTabClient) { function onAttach(aResponse, aThreadClient) { aCallback(aResponse, aTabClient, aThreadClient, aTabResponse); } aTabClient.attachThread({ useSourceMaps: true, autoBlackBox: true }, onAttach); }); } // Attach to |aClient|'s tab whose title is |aTitle|, attach to the tab's // thread, and then resume it. Pass |aCallback| the thread's response to // the 'resume' packet, a TabClient for the tab, and a ThreadClient for the // thread. function attachTestTabAndResume(aClient, aTitle, aCallback = () => {}) { return new Promise((resolve, reject) => { attachTestThread(aClient, aTitle, function (aResponse, aTabClient, aThreadClient) { aThreadClient.resume(function (aResponse) { aCallback(aResponse, aTabClient, aThreadClient); resolve([aResponse, aTabClient, aThreadClient]); }); }); }); } /** * Initialize the testing debugger server. */ function initTestDebuggerServer(aServer = DebuggerServer) { aServer.registerModule("xpcshell-test/testactors"); // Allow incoming connections. aServer.init(function () { return true; }); } /** * Initialize the testing debugger server with a tab whose title is |title|. */ function startTestDebuggerServer(title, server = DebuggerServer) { initTestDebuggerServer(server); addTestGlobal(title); DebuggerServer.addTabActors(); let transport = DebuggerServer.connectPipe(); let client = new DebuggerClient(transport); return connect(client).then(() => client); } function finishClient(aClient) { aClient.close(function () { DebuggerServer.destroy(); do_test_finished(); }); } // Create a server, connect to it and fetch tab actors for the parent process; // pass |aCallback| the debugger client and tab actor form with all actor IDs. function get_chrome_actors(callback) { if (!DebuggerServer.initialized) { DebuggerServer.init(); DebuggerServer.addBrowserActors(); } DebuggerServer.allowChromeProcess = true; let client = new DebuggerClient(DebuggerServer.connectPipe()); client.connect() .then(() => client.getProcess()) .then(response => { callback(client, response.form); }); } function getChromeActors(client, server = DebuggerServer) { server.allowChromeProcess = true; return client.getProcess().then(response => response.form); } /** * Takes a relative file path and returns the absolute file url for it. */ function getFileUrl(aName, aAllowMissing = false) { let file = do_get_file(aName, aAllowMissing); 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(aName, aAllowMissing = false, aUsePlatformPathSeparator = false) { let file = do_get_file(aName, aAllowMissing); 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 (aUsePlatformPathSeparator && path.match(/^\w:/)) { path = path.replace(/\//g, "\\"); } return path; } /** * Returns the full text contents of the given file. */ function readFile(aFileName) { let f = do_get_file(aFileName); let 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(aFileName, aContent) { let file = do_get_file(aFileName, true); let stream = Cc["@mozilla.org/network/file-output-stream;1"] .createInstance(Ci.nsIFileOutputStream); stream.init(file, -1, -1, 0); try { do { let numWritten = stream.write(aContent, aContent.length); aContent = aContent.slice(numWritten); } while (aContent.length > 0); } finally { stream.close(); } } function connectPipeTracing() { return new TracingTransport(DebuggerServer.connectPipe()); } function TracingTransport(childTransport) { this.hooks = null; this.child = childTransport; this.child.hooks = this; this.expectations = []; this.packets = []; this.checkIndex = 0; } TracingTransport.prototype = { // Remove actor names normalize: function (packet) { return JSON.parse(JSON.stringify(packet, (key, value) => { if (key === "to" || key === "from" || key === "actor") { return ""; } return value; })); }, send: function (packet) { this.packets.push({ type: "sent", packet: this.normalize(packet) }); return this.child.send(packet); }, close: function () { return this.child.close(); }, ready: function () { return this.child.ready(); }, onPacket: function (packet) { this.packets.push({ type: "received", packet: this.normalize(packet) }); this.hooks.onPacket(packet); }, onClosed: function () { this.hooks.onClosed(); }, expectSend: function (expected) { let packet = this.packets[this.checkIndex++]; do_check_eq(packet.type, "sent"); deepEqual(packet.packet, this.normalize(expected)); }, expectReceive: function (expected) { let packet = this.packets[this.checkIndex++]; do_check_eq(packet.type, "received"); deepEqual(packet.packet, this.normalize(expected)); }, // Write your tests, call dumpLog at the end, inspect the output, // then sprinkle the calls through the right places in your test. dumpLog: function () { for (let entry of this.packets) { if (entry.type === "sent") { dumpn("trace.expectSend(" + entry.packet + ");"); } else { dumpn("trace.expectReceive(" + entry.packet + ");"); } } } }; function StubTransport() { } StubTransport.prototype.ready = function () {}; StubTransport.prototype.send = function () {}; StubTransport.prototype.close = function () {}; function executeSoon(aFunc) { Services.tm.mainThread.dispatch({ run: DevToolsUtils.makeInfallible(aFunc) }, Ci.nsIThread.DISPATCH_NORMAL); } // The do_check_* family of functions expect their last argument to be an // optional stack object. Unfortunately, most tests actually pass a in a string // containing an error message instead, which causes error reporting to break if // strict warnings as errors is turned on. To avoid this, we wrap these // functions here below to ensure the correct number of arguments is passed. // // TODO: Remove this once bug 906232 is resolved // var do_check_true_old = do_check_true; var do_check_true = function (condition) { do_check_true_old(condition); }; var do_check_false_old = do_check_false; var do_check_false = function (condition) { do_check_false_old(condition); }; var do_check_eq_old = do_check_eq; var do_check_eq = function (left, right) { do_check_eq_old(left, right); }; var do_check_neq_old = do_check_neq; var do_check_neq = function (left, right) { do_check_neq_old(left, right); }; var do_check_matches_old = do_check_matches; var do_check_matches = function (pattern, value) { do_check_matches_old(pattern, value); }; // 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 = do_check_true; /** * Create a promise that is resolved on the next occurence of the given event. * * @param DebuggerClient client * @param String event * @param Function predicate * @returns Promise */ function waitForEvent(client, type, predicate) { return new Promise(function (resolve) { function listener(type, packet) { if (!predicate(packet)) { return; } client.removeListener(listener); resolve(packet); } if (predicate) { client.addListener(type, listener); } else { client.addOneTimeListener(type, function (type, packet) { resolve(packet); }); } }); } /** * 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 DebuggerClient client * @returns Promise */ function executeOnNextTickAndWaitForPause(action, client) { const paused = waitForPause(client); executeSoon(action); return paused; } /** * Interrupt JS execution for the specified thread. * * @param ThreadClient threadClient * @returns Promise */ function interrupt(threadClient) { dumpn("Interrupting."); return threadClient.interrupt(); } /** * Resume JS execution for the specified thread and then wait for the next pause * event. * * @param DebuggerClient client * @param ThreadClient threadClient * @returns Promise */ function resumeAndWaitForPause(client, threadClient) { const paused = waitForPause(client); return resume(threadClient).then(() => paused); } /** * Resume JS execution for a single step and wait for the pause after the step * has been taken. * * @param DebuggerClient client * @param ThreadClient threadClient * @returns Promise */ function stepIn(client, threadClient) { dumpn("Stepping in."); const paused = waitForPause(client); return threadClient.stepIn() .then(() => paused); } /** * Resume JS execution for a step over and wait for the pause after the step * has been taken. * * @param DebuggerClient client * @param ThreadClient threadClient * @returns Promise */ function stepOver(client, threadClient) { dumpn("Stepping over."); return threadClient.stepOver() .then(() => waitForPause(client)); } /** * Get the list of `count` frames currently on stack, starting at the index * `first` for the specified thread. * * @param ThreadClient threadClient * @param Number first * @param Number count * @returns Promise */ function getFrames(threadClient, first, count) { dumpn("Getting frames."); return threadClient.getFrames(first, count); } /** * Black box the specified source. * * @param SourceClient sourceClient * @returns Promise */ function blackBox(sourceClient) { dumpn("Black boxing source: " + sourceClient.actor); return sourceClient.blackBox(); } /** * Stop black boxing the specified source. * * @param SourceClient sourceClient * @returns Promise */ function unBlackBox(sourceClient) { dumpn("Un-black boxing source: " + sourceClient.actor); return sourceClient.unblackBox(); } /** * Perform a "source" RDP request with the given SourceClient to get the source * content and content type. * * @param SourceClient sourceClient * @returns Promise */ function getSourceContent(sourceClient) { dumpn("Getting source content for " + sourceClient.actor); return sourceClient.source(); } /** * Get a source at the specified url. * * @param ThreadClient threadClient * @param string url * @returns Promise */ function getSource(threadClient, url) { let deferred = promise.defer(); threadClient.getSources((res) => { let source = res.sources.filter(function (s) { return s.url === url; }); if (source.length) { deferred.resolve(threadClient.source(source[0])); } else { deferred.reject(new Error("source not found")); } }); return deferred.promise; } /** * Do a fake reload which clears the thread debugger * * @param TabClient tabClient * @returns Promise */ function reload(tabClient) { let deferred = promise.defer(); tabClient._reload({}, deferred.resolve); return deferred.promise; } /** * 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) { let stackTable = thread.stackTable; let frameTable = thread.frameTable; let stringTable = thread.stringTable; let SAMPLE_STACK_SLOT = thread.samples.schema.stack; let STACK_PREFIX_SLOT = stackTable.schema.prefix; let STACK_FRAME_SLOT = stackTable.schema.frame; let 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]; let locations = []; while (stackIndex !== null) { let stackEntry = stackTable.data[stackIndex]; let 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(); }