992 lines
29 KiB
JavaScript
992 lines
29 KiB
JavaScript
/* 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.importESModule(
|
|
"resource://devtools/shared/loader/worker-loader.sys.mjs"
|
|
);
|
|
|
|
const { NetUtil } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/NetUtil.sys.mjs"
|
|
);
|
|
|
|
// Always log packets when running tests. runxpcshelltests.py will throw
|
|
// the output away anyway, unless you give it the --verbose flag.
|
|
Services.prefs.setBoolPref("devtools.debugger.log", false);
|
|
// Enable remote debugging for the relevant tests.
|
|
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
|
|
|
|
const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
|
|
const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
|
|
const {
|
|
ActorRegistry,
|
|
} = require("resource://devtools/server/actors/utils/actor-registry.js");
|
|
const {
|
|
DevToolsServer,
|
|
} = require("resource://devtools/server/devtools-server.js");
|
|
const { DevToolsServer: WorkerDevToolsServer } = worker.require(
|
|
"resource://devtools/server/devtools-server.js"
|
|
);
|
|
const {
|
|
DevToolsClient,
|
|
} = require("resource://devtools/client/devtools-client.js");
|
|
const { ObjectFront } = require("resource://devtools/client/fronts/object.js");
|
|
const {
|
|
LongStringFront,
|
|
} = require("resource://devtools/client/fronts/string.js");
|
|
const {
|
|
createCommandsDictionary,
|
|
} = require("resource://devtools/shared/commands/index.js");
|
|
const {
|
|
CommandsFactory,
|
|
} = require("resource://devtools/shared/commands/commands-factory.js");
|
|
|
|
const { addDebuggerToGlobal } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/jsdebugger.sys.mjs"
|
|
);
|
|
|
|
const { AddonTestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/AddonTestUtils.sys.mjs"
|
|
);
|
|
const { getAppInfo } = ChromeUtils.importESModule(
|
|
"resource://testing-common/AppInfo.sys.mjs"
|
|
);
|
|
|
|
const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
|
|
Ci.nsIPrincipal
|
|
);
|
|
|
|
var { loadSubScript, loadSubScriptWithOptions } = Services.scriptloader;
|
|
|
|
/**
|
|
* The logic here must resemble the logic of --start-debugger-server as closely
|
|
* as possible. DevToolsStartup.sys.mjs uses a distinct loader that results in
|
|
* the existence of two isolated module namespaces. In practice, this can cause
|
|
* bugs such as bug 1837185.
|
|
*/
|
|
function getDistinctDevToolsServer() {
|
|
const {
|
|
useDistinctSystemPrincipalLoader,
|
|
releaseDistinctSystemPrincipalLoader,
|
|
} = ChromeUtils.importESModule(
|
|
"resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
|
|
{ global: "shared" }
|
|
);
|
|
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 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;
|
|
|
|
// Pass any configuration, in order to ensure starting all the thread actors
|
|
// and have them to handle debugger statements.
|
|
await commands.threadConfigurationCommand.updateConfiguration({
|
|
skipBreakpoints: false,
|
|
});
|
|
|
|
const threadFront = await targetFront.getFront("thread");
|
|
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 commands = await createCommandsDictionary(descriptorFront);
|
|
|
|
// Pass any configuration, in order to ensure starting all the thread actor
|
|
// and have it to notify about all sources
|
|
await commands.threadConfigurationCommand.updateConfiguration({
|
|
skipBreakpoints: false,
|
|
});
|
|
|
|
const threadFront = await targetFront.getFront("thread");
|
|
|
|
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;
|