Bug 1952682 - Introduce support for SpecialPowers.spawn in a content process r=nika
This patch introduces SpecialPowersForProcess + spawn() to enable callers to spawn a task in the specified content process, and adds a helper to ContentPage that is used in tests in bug 1952681. Differential Revision: https://phabricator.services.mozilla.com/D240779
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Page that embeds two cross-origin frames</title>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
async function addFrame(id, filename) {
|
||||||
|
let url = new URL(filename, document.URL);
|
||||||
|
url.hostname = url.hostname === "example.com" ? "example.org" : "example.com";
|
||||||
|
|
||||||
|
let f = document.createElement("iframe");
|
||||||
|
f.id = id;
|
||||||
|
f.src = url.href;
|
||||||
|
await new Promise(resolve => {
|
||||||
|
f.onload = resolve;
|
||||||
|
document.body.append(f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window === top) {
|
||||||
|
window.loadedPromise = Promise.all([
|
||||||
|
addFrame("frame1", "?this_is_child_frame_1"),
|
||||||
|
addFrame("frame2", "?this_is_child_frame_2"),
|
||||||
|
]).then(() => "frames_all_loaded");
|
||||||
|
} else {
|
||||||
|
document.body.append("This is child frame: " + location);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Tests SpecialPowersForProcess spawn() in xpcshell tests.
|
||||||
|
// A browser chrome mochitest version of this test exists at
|
||||||
|
// testing/mochitest/tests/browser/browser_SpecialPowersForProcessSpawn.js
|
||||||
|
|
||||||
|
const { XPCShellContentUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/XPCShellContentUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { SpecialPowersForProcess } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/SpecialPowersProcessActor.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
XPCShellContentUtils.init(this);
|
||||||
|
|
||||||
|
const server = XPCShellContentUtils.createHttpServer({
|
||||||
|
hosts: ["example.com", "example.org"],
|
||||||
|
});
|
||||||
|
server.registerFile(
|
||||||
|
"/file_xorigin_frames.html",
|
||||||
|
do_get_file("file_xorigin_frames.html")
|
||||||
|
);
|
||||||
|
|
||||||
|
const scope = this;
|
||||||
|
const interceptedMessages = [];
|
||||||
|
add_setup(() => {
|
||||||
|
const orig_do_report_result = scope.do_report_result;
|
||||||
|
scope.do_report_result = (passed, msg, stack) => {
|
||||||
|
if (msg?.startsWith?.("CHECK_THIS:")) {
|
||||||
|
interceptedMessages.push(msg);
|
||||||
|
}
|
||||||
|
return orig_do_report_result(passed, msg, stack);
|
||||||
|
};
|
||||||
|
const orig_info = scope.info;
|
||||||
|
scope.info = msg => {
|
||||||
|
if (msg?.startsWith?.("CHECK_THIS:")) {
|
||||||
|
interceptedMessages.push(msg);
|
||||||
|
}
|
||||||
|
return orig_info(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
scope.do_report_result = orig_do_report_result;
|
||||||
|
scope.info = orig_info;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests that SpecialPowersForProcess can spawn() in processes that the test
|
||||||
|
// grabbed off a contentPage, even after the original page navigated/closed.
|
||||||
|
add_task(async function test_SpecialPowersForProcess_spawn() {
|
||||||
|
interceptedMessages.length = 0;
|
||||||
|
|
||||||
|
const page = await XPCShellContentUtils.loadContentPage(
|
||||||
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
|
||||||
|
"http://example.com/file_xorigin_frames.html",
|
||||||
|
{ remote: true, remoteSubframes: true }
|
||||||
|
);
|
||||||
|
await page.spawn([], async () => {
|
||||||
|
Assert.equal(
|
||||||
|
await this.content.wrappedJSObject.loadedPromise,
|
||||||
|
"frames_all_loaded",
|
||||||
|
"All (cross-origin) frames have finished loading"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const proc1 = page.browsingContext.children[0].currentWindowGlobal.domProcess;
|
||||||
|
const proc2 = page.browsingContext.children[1].currentWindowGlobal.domProcess;
|
||||||
|
Assert.equal(proc1, proc2, "The two child frames share the same process");
|
||||||
|
|
||||||
|
const processBoundSpecialPowers = new SpecialPowersForProcess(scope, proc1);
|
||||||
|
|
||||||
|
await page.spawn([], async () => {
|
||||||
|
info("ContentPage.spawn: Change frame1 process");
|
||||||
|
const frame1 = this.content.document.getElementById("frame1");
|
||||||
|
Assert.throws(
|
||||||
|
() => frame1.contentDocument.location.search,
|
||||||
|
/TypeError: can't access property "location", frame1.contentDocument is null/,
|
||||||
|
"ContentPage.spawn: Assert, cannot read cross-origin content"
|
||||||
|
);
|
||||||
|
await new Promise(resolve => {
|
||||||
|
frame1.onload = resolve;
|
||||||
|
frame1.src = "/dummy?3";
|
||||||
|
});
|
||||||
|
// Verify that it is same-origin now.
|
||||||
|
Assert.equal(
|
||||||
|
frame1.contentDocument.location.search,
|
||||||
|
"?3",
|
||||||
|
"CHECK_THIS: ContentPage.spawn: Assert, frame1 is now same-origin"
|
||||||
|
);
|
||||||
|
info("CHECK_THIS: ContentPage.spawn: remove frame1");
|
||||||
|
frame1.remove();
|
||||||
|
|
||||||
|
// spawn() implementation has special logic to route Assert messages;
|
||||||
|
// Prepare to check that Assert can be called after spawn() returns.
|
||||||
|
this.content.assertAfterSpawnReturns = () => {
|
||||||
|
Assert.ok(true, "CHECK_THIS: ContentPage.spawn: asssert after return");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.spawn([], () => {
|
||||||
|
this.content.assertAfterSpawnReturns();
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.equal(page.browsingContext.children.length, 1, "frame1 was removed");
|
||||||
|
|
||||||
|
// Now frame1 has navigated (and switched processes) and removed, so if the
|
||||||
|
// SpecialPowers implementation were to rely on JSWindowActor, then that
|
||||||
|
// would break if we try to interact with it at this point. Check that we
|
||||||
|
// can connect just fine (because JSProcessActor should be used instead).
|
||||||
|
await processBoundSpecialPowers.spawn([], () => {
|
||||||
|
info("CHECK_THIS: process-bound spawn: still works");
|
||||||
|
Assert.equal(
|
||||||
|
typeof content,
|
||||||
|
"undefined",
|
||||||
|
"CHECK_THIS: process-bound spawn: no content global"
|
||||||
|
);
|
||||||
|
// Need a shared object that outlives this SpecialPowersSandbox instance:
|
||||||
|
const sharedGlobalObj = Cu.getGlobalForObject(Services);
|
||||||
|
// spawn() implementation has special logic to route Assert messages;
|
||||||
|
// Prepare to check that Assert can be called after spawn() returns.
|
||||||
|
sharedGlobalObj.assertAfterProcessBoundSpawnReturns = () => {
|
||||||
|
Assert.ok(true, "CHECK_THIS: process-bound spawn: asssert after return");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await processBoundSpecialPowers.spawn([], () => {
|
||||||
|
// Shared object that outlived the previous SpecialPowersSandbox instance:
|
||||||
|
const sharedGlobalObj = Cu.getGlobalForObject(Services);
|
||||||
|
sharedGlobalObj.assertAfterProcessBoundSpawnReturns();
|
||||||
|
delete sharedGlobalObj.assertAfterProcessBoundSpawnReturns;
|
||||||
|
});
|
||||||
|
await page.close();
|
||||||
|
await processBoundSpecialPowers.destroy();
|
||||||
|
|
||||||
|
Assert.throws(
|
||||||
|
() => processBoundSpecialPowers.spawn([], () => {}),
|
||||||
|
/this.actor is null/,
|
||||||
|
"Cannot spawn after destroy()"
|
||||||
|
);
|
||||||
|
|
||||||
|
const observedMessages = interceptedMessages.splice(0);
|
||||||
|
Assert.deepEqual(
|
||||||
|
observedMessages,
|
||||||
|
[
|
||||||
|
`CHECK_THIS: ContentPage.spawn: Assert, frame1 is now same-origin - "?3" == "?3"`,
|
||||||
|
"CHECK_THIS: ContentPage.spawn: remove frame1",
|
||||||
|
"CHECK_THIS: ContentPage.spawn: asssert after return - true == true",
|
||||||
|
"CHECK_THIS: process-bound spawn: still works",
|
||||||
|
`CHECK_THIS: process-bound spawn: no content global - "undefined" == "undefined"`,
|
||||||
|
"CHECK_THIS: process-bound spawn: asssert after return - true == true",
|
||||||
|
],
|
||||||
|
"Observed calls through spawn"
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
|
||||||
|
["test_SpecialPowersForProcessSpawn.js"]
|
||||||
|
prefs = ["dom.security.https_first=false"] #Disable https-first because createHttpServer does not support https
|
||||||
|
support-files = ["file_xorigin_frames.html"]
|
||||||
|
|
||||||
["test_SpecialPowersSandbox.js"]
|
["test_SpecialPowersSandbox.js"]
|
||||||
prefs = ["dom.security.https_first=false"] #Disable https-first because createHttpServer does not support https
|
prefs = ["dom.security.https_first=false"] #Disable https-first because createHttpServer does not support https
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ skip-if = [
|
|||||||
"verify",
|
"verify",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
["browser_SpecialPowersForProcessSpawn.js"]
|
||||||
|
support-files = ["../Harness_sanity/file_xorigin_frames.html"]
|
||||||
|
|
||||||
["browser_add_task.js"]
|
["browser_add_task.js"]
|
||||||
|
|
||||||
["browser_async.js"]
|
["browser_async.js"]
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Tests SpecialPowersForProcess spawn() in browser chrome mochitests.
|
||||||
|
// An xpcshell version of this test exists at
|
||||||
|
// testing/mochitest/tests/Harness_sanity/test_SpecialPowersForProcessSpawn.js
|
||||||
|
|
||||||
|
const { SpecialPowersForProcess } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/SpecialPowersProcessActor.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const scope = this;
|
||||||
|
const interceptedMessages = [];
|
||||||
|
add_setup(() => {
|
||||||
|
const orig_record = SimpleTest.record;
|
||||||
|
SimpleTest.record = (condition, name, diag, stack, expected) => {
|
||||||
|
if (name?.startsWith?.("CHECK_THIS:")) {
|
||||||
|
interceptedMessages.push(name);
|
||||||
|
}
|
||||||
|
return orig_record(condition, name, diag, stack, expected);
|
||||||
|
};
|
||||||
|
const orig_info = SimpleTest.info;
|
||||||
|
SimpleTest.info = msg => {
|
||||||
|
if (msg?.startsWith?.("CHECK_THIS:")) {
|
||||||
|
interceptedMessages.push(msg);
|
||||||
|
}
|
||||||
|
return orig_info(msg);
|
||||||
|
};
|
||||||
|
registerCleanupFunction(() => {
|
||||||
|
SimpleTest.record = orig_record;
|
||||||
|
SimpleTest.info = orig_info;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests that SpecialPowersForProcess can spawn() in processes that the test
|
||||||
|
// grabbed off a contentPage, even after the original page navigated/closed.
|
||||||
|
add_task(async function test_SpecialPowersForProcess_spawn() {
|
||||||
|
interceptedMessages.length = 0;
|
||||||
|
|
||||||
|
const tab = await BrowserTestUtils.openNewForegroundTab(
|
||||||
|
gBrowser,
|
||||||
|
"https://example.com/browser/testing/mochitest/tests/browser/file_xorigin_frames.html"
|
||||||
|
);
|
||||||
|
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
|
||||||
|
Assert.equal(
|
||||||
|
await this.content.wrappedJSObject.loadedPromise,
|
||||||
|
"frames_all_loaded",
|
||||||
|
"All (cross-origin) frames have finished loading"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const browsingContext = tab.linkedBrowser.browsingContext;
|
||||||
|
const proc1 = browsingContext.children[0].currentWindowGlobal.domProcess;
|
||||||
|
const proc2 = browsingContext.children[1].currentWindowGlobal.domProcess;
|
||||||
|
Assert.equal(proc1, proc2, "The two child frames share the same process");
|
||||||
|
|
||||||
|
const processBoundSpecialPowers = new SpecialPowersForProcess(scope, proc1);
|
||||||
|
|
||||||
|
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
|
||||||
|
info("ContentPage.spawn: Change frame1 process");
|
||||||
|
const frame1 = this.content.document.getElementById("frame1");
|
||||||
|
Assert.throws(
|
||||||
|
() => frame1.contentDocument.location.search,
|
||||||
|
/TypeError: can't access property "location", frame1.contentDocument is null/,
|
||||||
|
"ContentPage.spawn: Assert, cannot read cross-origin content"
|
||||||
|
);
|
||||||
|
await new Promise(resolve => {
|
||||||
|
frame1.onload = resolve;
|
||||||
|
frame1.src = "/dummy?3";
|
||||||
|
});
|
||||||
|
// Verify that it is same-origin now.
|
||||||
|
Assert.equal(
|
||||||
|
frame1.contentDocument.location.search,
|
||||||
|
"?3",
|
||||||
|
"CHECK_THIS: ContentPage.spawn: Assert, frame1 is now same-origin"
|
||||||
|
);
|
||||||
|
info("CHECK_THIS: ContentPage.spawn: remove frame1");
|
||||||
|
frame1.remove();
|
||||||
|
|
||||||
|
// spawn() implementation has special logic to route Assert messages;
|
||||||
|
// Prepare to check that Assert can be called after spawn() returns.
|
||||||
|
this.content.assertAfterSpawnReturns = () => {
|
||||||
|
Assert.ok(true, "CHECK_THIS: ContentPage.spawn: asssert after return");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
|
||||||
|
this.content.assertAfterSpawnReturns();
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.equal(browsingContext.children.length, 1, "frame1 was removed");
|
||||||
|
|
||||||
|
// Now frame1 has navigated (and switched processes) and removed, so if the
|
||||||
|
// SpecialPowers implementation were to rely on JSWindowActor, then that
|
||||||
|
// would break if we try to interact with it at this point. Check that we
|
||||||
|
// can connect just fine (because JSProcessActor should be used instead).
|
||||||
|
await processBoundSpecialPowers.spawn([], () => {
|
||||||
|
info("CHECK_THIS: process-bound spawn: still works");
|
||||||
|
Assert.equal(
|
||||||
|
typeof content,
|
||||||
|
"undefined",
|
||||||
|
"CHECK_THIS: process-bound spawn: no content global"
|
||||||
|
);
|
||||||
|
// Need a shared object that outlives this SpecialPowersSandbox instance:
|
||||||
|
const sharedGlobalObj = Cu.getGlobalForObject(Services);
|
||||||
|
// spawn() implementation has special logic to route Assert messages;
|
||||||
|
// Prepare to check that Assert can be called after spawn() returns.
|
||||||
|
sharedGlobalObj.assertAfterProcessBoundSpawnReturns = () => {
|
||||||
|
Assert.ok(true, "CHECK_THIS: process-bound spawn: asssert after return");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await processBoundSpecialPowers.spawn([], () => {
|
||||||
|
// Shared object that outlived the previous SpecialPowersSandbox instance:
|
||||||
|
const sharedGlobalObj = Cu.getGlobalForObject(Services);
|
||||||
|
sharedGlobalObj.assertAfterProcessBoundSpawnReturns();
|
||||||
|
delete sharedGlobalObj.assertAfterProcessBoundSpawnReturns;
|
||||||
|
});
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await processBoundSpecialPowers.destroy();
|
||||||
|
|
||||||
|
Assert.throws(
|
||||||
|
() => processBoundSpecialPowers.spawn([], () => {}),
|
||||||
|
/this.actor is null/,
|
||||||
|
"Cannot spawn after destroy()"
|
||||||
|
);
|
||||||
|
|
||||||
|
const observedMessages = interceptedMessages.splice(0);
|
||||||
|
Assert.deepEqual(
|
||||||
|
observedMessages,
|
||||||
|
[
|
||||||
|
`CHECK_THIS: ContentPage.spawn: Assert, frame1 is now same-origin - "?3" == "?3"`,
|
||||||
|
"CHECK_THIS: ContentPage.spawn: remove frame1",
|
||||||
|
"CHECK_THIS: ContentPage.spawn: asssert after return - true == true",
|
||||||
|
"CHECK_THIS: process-bound spawn: still works",
|
||||||
|
`CHECK_THIS: process-bound spawn: no content global - "undefined" == "undefined"`,
|
||||||
|
"CHECK_THIS: process-bound spawn: asssert after return - true == true",
|
||||||
|
],
|
||||||
|
"Observed calls through spawn"
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -23,6 +23,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||||||
ContentTask: "resource://testing-common/ContentTask.sys.mjs",
|
ContentTask: "resource://testing-common/ContentTask.sys.mjs",
|
||||||
HttpServer: "resource://testing-common/httpd.sys.mjs",
|
HttpServer: "resource://testing-common/httpd.sys.mjs",
|
||||||
SpecialPowersParent: "resource://testing-common/SpecialPowersParent.sys.mjs",
|
SpecialPowersParent: "resource://testing-common/SpecialPowersParent.sys.mjs",
|
||||||
|
SpecialPowersForProcess:
|
||||||
|
"resource://testing-common/SpecialPowersProcessActor.sys.mjs",
|
||||||
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,6 +261,15 @@ export class ContentPage {
|
|||||||
return this.SpecialPowers.spawn(this.browser, params, task);
|
return this.SpecialPowers.spawn(this.browser, params, task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a SpecialPowersForProcess instance associated with the content process
|
||||||
|
// of the currently loaded page. This allows callers to spawn() tasks that
|
||||||
|
// outlive the page (for as long as the page's process is around).
|
||||||
|
getCurrentContentProcessSpecialPowers() {
|
||||||
|
const testScope = XPCShellContentUtils.currentScope;
|
||||||
|
const domProcess = this.browsingContext.currentWindowGlobal.domProcess;
|
||||||
|
return new lazy.SpecialPowersForProcess(testScope, domProcess);
|
||||||
|
}
|
||||||
|
|
||||||
// Like spawn(), but uses the legacy ContentTask infrastructure rather than
|
// Like spawn(), but uses the legacy ContentTask infrastructure rather than
|
||||||
// SpecialPowers. Exists only because the author of the SpecialPowers
|
// SpecialPowers. Exists only because the author of the SpecialPowers
|
||||||
// migration did not have the time to fix all of the legacy users who relied
|
// migration did not have the time to fix all of the legacy users who relied
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ export class SpecialPowersChild extends JSWindowActorChild {
|
|||||||
|
|
||||||
case "Assert":
|
case "Assert":
|
||||||
{
|
{
|
||||||
|
// Handles info & Assert reports from SpecialPowersSandbox.sys.mjs.
|
||||||
if ("info" in message.data) {
|
if ("info" in message.data) {
|
||||||
(this.xpcshellScope || this.SimpleTest).info(message.data.info);
|
(this.xpcshellScope || this.SimpleTest).info(message.data.info);
|
||||||
break;
|
break;
|
||||||
@@ -311,12 +312,11 @@ export class SpecialPowersChild extends JSWindowActorChild {
|
|||||||
// An assertion has been done in a mochitest chrome script
|
// An assertion has been done in a mochitest chrome script
|
||||||
let { name, passed, stack, diag, expectFail } = message.data;
|
let { name, passed, stack, diag, expectFail } = message.data;
|
||||||
|
|
||||||
let { SimpleTest } = this;
|
if (this.xpcshellScope) {
|
||||||
if (SimpleTest) {
|
|
||||||
let expected = expectFail ? "fail" : "pass";
|
|
||||||
SimpleTest.record(passed, name, diag, stack, expected);
|
|
||||||
} else if (this.xpcshellScope) {
|
|
||||||
this.xpcshellScope.do_report_result(passed, name, stack);
|
this.xpcshellScope.do_report_result(passed, name, stack);
|
||||||
|
} else if (this.SimpleTest) {
|
||||||
|
let expected = expectFail ? "fail" : "pass";
|
||||||
|
this.SimpleTest.record(passed, name, diag, stack, expected);
|
||||||
} else {
|
} else {
|
||||||
// Well, this is unexpected.
|
// Well, this is unexpected.
|
||||||
dump(name + "\n");
|
dump(name + "\n");
|
||||||
@@ -1506,6 +1506,13 @@ export class SpecialPowersChild extends JSWindowActorChild {
|
|||||||
* The sandbox also has access to an Assert object, as provided by
|
* The sandbox also has access to an Assert object, as provided by
|
||||||
* Assert.sys.mjs. Any assertion methods called before the task resolves
|
* Assert.sys.mjs. Any assertion methods called before the task resolves
|
||||||
* will be relayed back to the test environment of the caller.
|
* will be relayed back to the test environment of the caller.
|
||||||
|
* Assertions triggered after a task returns may be relayed back if
|
||||||
|
* setAsDefaultAssertHandler() has been called, until this SpecialPowers
|
||||||
|
* instance is destroyed.
|
||||||
|
*
|
||||||
|
* If your assertions need to outlive this SpecialPowers instance,
|
||||||
|
* use SpecialPowersForProcess from SpecialPowersProcessActor.sys.mjs,
|
||||||
|
* which lives until the specified child process terminates.
|
||||||
*
|
*
|
||||||
* @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target
|
* @param {BrowsingContext or FrameLoaderOwner or WindowProxy} target
|
||||||
* The target in which to run the task. This may be any element
|
* The target in which to run the task. This may be any element
|
||||||
|
|||||||
@@ -194,10 +194,21 @@ export class SpecialPowersParent extends JSWindowActorParent {
|
|||||||
esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs",
|
esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
ChromeUtils.registerProcessActor("SpecialPowersProcessActor", {
|
||||||
|
child: {
|
||||||
|
esModuleURI:
|
||||||
|
"resource://testing-common/SpecialPowersProcessActor.sys.mjs",
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
esModuleURI:
|
||||||
|
"resource://testing-common/SpecialPowersProcessActor.sys.mjs",
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static unregisterActor() {
|
static unregisterActor() {
|
||||||
ChromeUtils.unregisterWindowActor("SpecialPowers");
|
ChromeUtils.unregisterWindowActor("SpecialPowers");
|
||||||
|
ChromeUtils.unregisterProcessActor("SpecialPowersProcessActor");
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
170
testing/specialpowers/content/SpecialPowersProcessActor.sys.mjs
Normal file
170
testing/specialpowers/content/SpecialPowersProcessActor.sys.mjs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
SpecialPowersSandbox:
|
||||||
|
"resource://testing-common/SpecialPowersSandbox.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextSpfpId = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpecialPowersForProcess wraps a content process, and allows the caller to
|
||||||
|
* spawn() tasks like SpecialPowers.spawn() and contentPage.spawn(), including
|
||||||
|
* Assert functionality. Assertion messages are passed back to the test scope,
|
||||||
|
* which must be passed along with the process to the constructor.
|
||||||
|
*/
|
||||||
|
export class SpecialPowersForProcess {
|
||||||
|
static instances = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SpecialPowersForProcess that enables callers to spawn tasks
|
||||||
|
* in the given content process.
|
||||||
|
*
|
||||||
|
* @param {any} scope
|
||||||
|
* The test scope to receive assertion messages.
|
||||||
|
* In test files this is often equivalent to globalThis.
|
||||||
|
* @param {nsIDOMProcessParent} domProcess
|
||||||
|
* The content process where the spawned code should run.
|
||||||
|
*/
|
||||||
|
constructor(testScope, domProcess) {
|
||||||
|
this.isXpcshellScope = !!testScope.do_get_profile;
|
||||||
|
this.isSimpleTestScope = !this.isXpcshellScope && !!testScope.SimpleTest;
|
||||||
|
if (!this.isXpcshellScope && !this.isSimpleTestScope) {
|
||||||
|
// Must be global of xpcshell test, or global of browser chrome mochitest.
|
||||||
|
throw new Error("testScope cannot receive assertion messages!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(domProcess instanceof Ci.nsIDOMProcessParent)) {
|
||||||
|
throw new Error("domProcess is not a nsIDOMProcessParent!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This actor is registered via SpecialPowersParent.registerActor().
|
||||||
|
// In mochitests that is part of the SpecialPowers add-on initialization.
|
||||||
|
// Xpcshell tests can initialize it with XPCShellContentUtils.init(), via
|
||||||
|
// XPCShellContentUtils.ensureInitialized().
|
||||||
|
this.actor = domProcess.getActor("SpecialPowersProcessActor");
|
||||||
|
|
||||||
|
this.testScope = testScope;
|
||||||
|
|
||||||
|
this.spfpId = nextSpfpId++;
|
||||||
|
SpecialPowersForProcess.instances.set(this.spfpId, this);
|
||||||
|
|
||||||
|
testScope.registerCleanupFunction(() => this.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (!this.testScope) {
|
||||||
|
// Already destroyed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SpecialPowersForProcess.instances.delete(this.spfpId);
|
||||||
|
this.actor = null;
|
||||||
|
this.testScope = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `SpecialPowers.spawn`, but spawns a task in a child process instead.
|
||||||
|
* The task has access to Assert.
|
||||||
|
*
|
||||||
|
* @param {Array<any>} args
|
||||||
|
* An array of arguments to pass to the task. All arguments
|
||||||
|
* must be structured clone compatible, and will be cloned
|
||||||
|
* before being passed to the task.
|
||||||
|
* @param {function} task
|
||||||
|
* The function to run in the context of the target process. The
|
||||||
|
* function will be stringified and re-evaluated in the context
|
||||||
|
* of the target's content process. It may return any structured
|
||||||
|
* clone compatible value, or a Promise which resolves to the
|
||||||
|
* same, which will be returned to the caller.
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
* A promise which resolves to the return value of the task, or
|
||||||
|
* which rejects if the task raises an exception. As this is
|
||||||
|
* being written, the rejection value will always be undefined
|
||||||
|
* in the cases where the task throws an error, though that may
|
||||||
|
* change in the future.
|
||||||
|
*/
|
||||||
|
spawn(args, task) {
|
||||||
|
return this.actor.sendQuery("Spawn", {
|
||||||
|
args,
|
||||||
|
task: String(task),
|
||||||
|
caller: Cu.getFunctionSourceLocation(task),
|
||||||
|
spfpId: this.spfpId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when ProxiedAssert is received; this data is sent from
|
||||||
|
// SpecialPowersSandbox's reportCallback in response to assertion messages.
|
||||||
|
reportCallback(data) {
|
||||||
|
if ("info" in data) {
|
||||||
|
if (this.isXpcshellScope) {
|
||||||
|
this.testScope.info(data.info);
|
||||||
|
} else if (this.isSimpleTestScope) {
|
||||||
|
this.testScope.SimpleTest.info(data.info);
|
||||||
|
} else {
|
||||||
|
// We checked this in the constructor, so this is unexpected.
|
||||||
|
throw new Error(`testScope cannot receive assertion messages!?!`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, diag, passed, stack, expectFail } = data;
|
||||||
|
if (this.isXpcshellScope) {
|
||||||
|
this.testScope.do_report_result(passed, name, stack);
|
||||||
|
} else if (this.isSimpleTestScope) {
|
||||||
|
// browser chrome mochitest
|
||||||
|
let expected = expectFail ? "fail" : "pass";
|
||||||
|
this.testScope.SimpleTest.record(passed, name, diag, stack, expected);
|
||||||
|
} else {
|
||||||
|
// We checked this in the constructor, so this is unexpected.
|
||||||
|
throw new Error(`testScope cannot receive assertion messages!?!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A minimal process actor that allows spawn() to run in the given process.
|
||||||
|
export class SpecialPowersProcessActorParent extends JSProcessActorParent {
|
||||||
|
receiveMessage(aMessage) {
|
||||||
|
switch (aMessage.name) {
|
||||||
|
case "ProxiedAssert": {
|
||||||
|
const { spfpId, data } = aMessage.data;
|
||||||
|
const spfp = SpecialPowersForProcess.instances.get(spfpId);
|
||||||
|
if (!spfp) {
|
||||||
|
dump(`Unexpected ProxiedAssert: ${uneval(data)}\n`);
|
||||||
|
throw new Error(`Unexpected message for ${spfpId} `);
|
||||||
|
}
|
||||||
|
spfp.reportCallback(data);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unknown SpecialPowersProcessActorParent action: ${aMessage.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpecialPowersProcessActorChild extends JSProcessActorChild {
|
||||||
|
receiveMessage(aMessage) {
|
||||||
|
switch (aMessage.name) {
|
||||||
|
case "Spawn": {
|
||||||
|
return this._spawnTaskInChild(aMessage.data);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unknown SpecialPowersProcessActorChild action: ${aMessage.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_spawnTaskInChild({ task, args, caller, spfpId }) {
|
||||||
|
let sb = new lazy.SpecialPowersSandbox(null, data => {
|
||||||
|
this.sendAsyncMessage("ProxiedAssert", { spfpId, data });
|
||||||
|
});
|
||||||
|
|
||||||
|
return sb.execute(task, args, caller);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ modules = [
|
|||||||
"content/SpecialPowersChild.sys.mjs",
|
"content/SpecialPowersChild.sys.mjs",
|
||||||
"content/SpecialPowersEventUtils.sys.mjs",
|
"content/SpecialPowersEventUtils.sys.mjs",
|
||||||
"content/SpecialPowersParent.sys.mjs",
|
"content/SpecialPowersParent.sys.mjs",
|
||||||
|
"content/SpecialPowersProcessActor.sys.mjs",
|
||||||
"content/SpecialPowersSandbox.sys.mjs",
|
"content/SpecialPowersSandbox.sys.mjs",
|
||||||
"content/WrapPrivileged.sys.mjs",
|
"content/WrapPrivileged.sys.mjs",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user