Files
Alexandre Poirot 26d9f9bb91 Bug 1713093 - [devtools] Remove TargetMixin.attachThread. r=devtools-reviewers,nchevobbe
This code was only used for workers targets, which weren't passing breakpoints
from the server side via "sessionData" concept.
Instead, we were still relying on the frontend to pass them and only after that
manually requesting the thread to resume its execution.

Now, there is still a notion of "attach" around the thread actor.
While all the targets are immediately instantiating their related thread actor,
the thread actor isn't active by default.
It becomes active as soon as:
* SOURCE or THREAD_STATE resource is watched
* breakpoints are set via session data (breakpoint list actor)
* thread configuration are set via session data (thread configuration actor)
I had to do one of these things in order to keep test from working.
In the production code, we do most of these things by default on the toolbox startup.
We could revise this behavior by:
* always attaching the thread actor, but it may start pausing unexpectedly in some tests.
* decouple sources from breakpoints within the thread actor, so that watching for sources doesn't necessarily "attach" the thread actor and activate breakpoints/debugger statements.
* have a new thread configuration to explicitly enable the thread actor pausing features.

I'm often using thread configuration to enable pausing in tests.
This required to improve xpcshell test helpers to be able to use Watcher/Commands.

About browser_target_command_bfcache.js, the DevTools code is now so quick that the
back/forward requests at done too quickly one after the other and fail actually moving to
the other document.

Differential Revision: https://phabricator.services.mozilla.com/D166321
2024-03-20 12:30:17 +00:00

272 lines
8.1 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* exported attachConsole, attachConsoleToTab, attachConsoleToWorker,
closeDebugger, checkConsoleAPICalls, checkRawHeaders, runTests, nextTest, Ci, Cc,
withActiveServiceWorker, Services, consoleAPICall, createCommandsForTab, FRACTIONAL_NUMBER_REGEX, DevToolsServer */
const { require } = ChromeUtils.importESModule(
"resource://devtools/shared/loader/Loader.sys.mjs"
);
const {
DevToolsServer,
} = require("resource://devtools/server/devtools-server.js");
const {
CommandsFactory,
} = require("resource://devtools/shared/commands/commands-factory.js");
// timeStamp are the result of a number in microsecond divided by 1000.
// so we can't expect a precise number of decimals, or even if there would
// be decimals at all.
const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;
function attachConsole(listeners) {
return _attachConsole(listeners);
}
function attachConsoleToTab(listeners) {
return _attachConsole(listeners, true);
}
function attachConsoleToWorker(listeners) {
return _attachConsole(listeners, true, true);
}
var _attachConsole = async function (listeners, attachToTab, attachToWorker) {
try {
function waitForMessage(target) {
return new Promise(resolve => {
target.addEventListener("message", resolve, { once: true });
});
}
// Fetch the console actor out of the expected target
// ParentProcessTarget / WorkerTarget / FrameTarget
let commands, target, worker;
if (!attachToTab) {
commands = await CommandsFactory.forMainProcess();
target = await commands.descriptorFront.getTarget();
} else {
commands = await CommandsFactory.forCurrentTabInChromeMochitest();
// Descriptor's getTarget will only work if the TargetCommand watches for the first top target
await commands.targetCommand.startListening();
target = await commands.descriptorFront.getTarget();
if (attachToWorker) {
const workerName = "console-test-worker.js#" + new Date().getTime();
worker = new Worker(workerName);
await waitForMessage(worker);
const { workers } = await target.listWorkers();
target = workers.filter(w => w.url == workerName)[0];
if (!target) {
console.error(
"listWorkers failed. Unable to find the worker actor\n"
);
return null;
}
// This is still important to attach workers as target is still a descriptor front
// which "becomes" a target when calling this method:
await target.morphWorkerDescriptorIntoWorkerTarget();
}
}
const webConsoleFront = await target.getFront("console");
// By default the console isn't listening for anything,
// request listeners from here
const response = await webConsoleFront.startListeners(listeners);
return {
state: {
dbgClient: commands.client,
webConsoleFront,
actor: webConsoleFront.actor,
// Keep a strong reference to the Worker to avoid it being
// GCd during the test (bug 1237492).
// eslint-disable-next-line camelcase
_worker_ref: worker,
},
response,
};
} catch (error) {
console.error(
`attachConsole failed: ${error.error} ${error.message} - ` + error.stack
);
}
return null;
};
async function createCommandsForTab() {
const commands = await CommandsFactory.forMainProcess();
await commands.targetCommand.startListening();
return commands;
}
function closeDebugger(state, callback) {
const onClose = state.dbgClient.close();
state.dbgClient = null;
state.client = null;
if (typeof callback === "function") {
onClose.then(callback);
}
return onClose;
}
function checkConsoleAPICalls(consoleCalls, expectedConsoleCalls) {
is(
consoleCalls.length,
expectedConsoleCalls.length,
"received correct number of console calls"
);
expectedConsoleCalls.forEach(function (message, index) {
info("checking received console call #" + index);
checkConsoleAPICall(consoleCalls[index], expectedConsoleCalls[index]);
});
}
function checkConsoleAPICall(call, expected) {
is(
call.arguments?.length || 0,
expected.arguments?.length || 0,
"number of arguments"
);
checkObject(call, expected);
}
function checkObject(object, expected) {
if (object && object.getGrip) {
object = object.getGrip();
}
for (const name of Object.keys(expected)) {
const expectedValue = expected[name];
const value = object[name];
checkValue(name, value, expectedValue);
}
}
function checkValue(name, value, expected) {
if (expected === null) {
ok(!value, "'" + name + "' is null");
} else if (value === undefined) {
ok(false, "'" + name + "' is undefined");
} else if (value === null) {
ok(false, "'" + name + "' is null");
} else if (
typeof expected == "string" ||
typeof expected == "number" ||
typeof expected == "boolean"
) {
is(value, expected, "property '" + name + "'");
} else if (expected instanceof RegExp) {
ok(expected.test(value), name + ": " + expected + " matched " + value);
} else if (Array.isArray(expected)) {
info("checking array for property '" + name + "'");
checkObject(value, expected);
} else if (typeof expected == "object") {
info("checking object for property '" + name + "'");
checkObject(value, expected);
}
}
function checkHeadersOrCookies(array, expected) {
const foundHeaders = {};
for (const elem of array) {
if (!(elem.name in expected)) {
continue;
}
foundHeaders[elem.name] = true;
info("checking value of header " + elem.name);
checkValue(elem.name, elem.value, expected[elem.name]);
}
for (const header in expected) {
if (!(header in foundHeaders)) {
ok(false, header + " was not found");
}
}
}
function checkRawHeaders(text, expected) {
const headers = text.split(/\r\n|\n|\r/);
const arr = [];
for (const header of headers) {
const index = header.indexOf(": ");
if (index < 0) {
continue;
}
arr.push({
name: header.substr(0, index),
value: header.substr(index + 2),
});
}
checkHeadersOrCookies(arr, expected);
}
var gTestState = {};
function runTests(tests, endCallback) {
function* driver() {
let lastResult, sendToNext;
for (let i = 0; i < tests.length; i++) {
gTestState.index = i;
const fn = tests[i];
info("will run test #" + i + ": " + fn.name);
lastResult = fn(sendToNext, lastResult);
sendToNext = yield lastResult;
}
yield endCallback(sendToNext, lastResult);
}
gTestState.driver = driver();
return gTestState.driver.next();
}
function nextTest(message) {
return gTestState.driver.next(message);
}
function withActiveServiceWorker(win, url, scope) {
const opts = {};
if (scope) {
opts.scope = scope;
}
return win.navigator.serviceWorker.register(url, opts).then(swr => {
if (swr.active) {
return swr;
}
// Unfortunately we can't just use navigator.serviceWorker.ready promise
// here. If the service worker is for a scope that does not cover the window
// then the ready promise will never resolve. Instead monitor the service
// workers state change events to determine when its activated.
return new Promise(resolve => {
const sw = swr.waiting || swr.installing;
sw.addEventListener("statechange", function stateHandler() {
if (sw.state === "activated") {
sw.removeEventListener("statechange", stateHandler);
resolve(swr);
}
});
});
});
}
/**
*
* @param {Front} consoleFront
* @param {Function} consoleCall: A function which calls the consoleAPI, e.g. :
* `() => top.console.log("test")`.
* @returns {Promise} A promise that will be resolved with the packet sent by the server
* in response to the consoleAPI call.
*/
function consoleAPICall(consoleFront, consoleCall) {
const onConsoleAPICall = consoleFront.once("consoleAPICall");
consoleCall();
return onConsoleAPICall;
}