зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1662925 - Test enhancements. r=nika
Differential Revision: https://phabricator.services.mozilla.com/D92020
This commit is contained in:
Родитель
52eb306b64
Коммит
f2243a9fe7
|
@ -25,3 +25,4 @@ support-files =
|
|||
[browser_serviceworker_fetch_new_process.js]
|
||||
support-files =
|
||||
file_service_worker_fetch_synthetic.js
|
||||
server_fetch_synthetic.sjs
|
||||
|
|
|
@ -3,17 +3,29 @@ const DIRPATH = getRootDirectory(gTestPath).replace(
|
|||
""
|
||||
);
|
||||
|
||||
/**
|
||||
* We choose blob contents that will roundtrip cleanly through the `textContent`
|
||||
* of our returned HTML page.
|
||||
*/
|
||||
const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`;
|
||||
|
||||
add_task(async function setup() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
// Set preferences so that opening a page with the origin "example.org"
|
||||
// will result in a remoteType of "privilegedmozilla" for both the
|
||||
// page and the ServiceWorker.
|
||||
["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
|
||||
["browser.tabs.remote.separatedMozillaDomains", "example.org"],
|
||||
["dom.ipc.processCount.privilegedmozilla", 1],
|
||||
["dom.ipc.processCount.webIsolated", 1],
|
||||
["dom.ipc.processPrelaunch.enabled", false],
|
||||
["dom.serviceWorkers.enabled", true],
|
||||
["dom.serviceWorkers.testing.enabled", true],
|
||||
["dom.serviceworkers.parent_intercept", true],
|
||||
// ServiceWorker worker instances should stay alive until explicitly
|
||||
// caused to terminate by dropping these timeouts to 0 in
|
||||
// `waitForWorkerAndProcessShutdown`.
|
||||
["dom.serviceWorkers.idle_timeout", 299999],
|
||||
["dom.serviceWorkers.idle_extended_timeout", 299999],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -24,13 +36,52 @@ function countRemoteType(remoteType) {
|
|||
).length;
|
||||
}
|
||||
|
||||
async function waitForWorkerIdleShutdown(swRegInfo, remoteType) {
|
||||
info(`waiting for ${remoteType} procs to shut down`);
|
||||
ok(swRegInfo.activeWorker, "worker isn't currently active?");
|
||||
/**
|
||||
* Helper function to get a list of all current processes and their remote
|
||||
* types. Note that when in used in a templated literal that it is
|
||||
* synchronously invoked when the string is evaluated and captures system state
|
||||
* at that instant.
|
||||
*/
|
||||
function debugRemotes() {
|
||||
return ChromeUtils.getAllDOMProcesses()
|
||||
.map(p => p.remoteType || "parent")
|
||||
.join(",");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for there to be zero processes of the given remoteType. This check is
|
||||
* considered successful if there are already no processes of the given type
|
||||
* at this very moment.
|
||||
*/
|
||||
async function waitForNoProcessesOfType(remoteType) {
|
||||
info(`waiting for there to be no ${remoteType} procs`);
|
||||
await TestUtils.waitForCondition(
|
||||
() => countRemoteType(remoteType) == 0,
|
||||
"wait for the worker's process to shutdown"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a ServiceWorkerRegistrationInfo with an active ServiceWorker that
|
||||
* has no active ExtendableEvents but would otherwise continue running thanks
|
||||
* to the idle keepalive:
|
||||
* - Assert that there is a ServiceWorker instance in the given registration's
|
||||
* active slot. (General invariant check.)
|
||||
* - Assert that a single process with the given remoteType currently exists.
|
||||
* (This doesn't mean the SW is alive in that process, though this test
|
||||
* verifies that via other checks when appropriate.)
|
||||
* - Induce the worker to shutdown by temporarily dropping the idle timeout to 0
|
||||
* and causing the idle timer to be reset due to rapid debugger attach/detach.
|
||||
* - Wait for the the single process with the given remoteType to go away.
|
||||
* - Reset the idle timeouts back to their previous high values.
|
||||
*/
|
||||
async function waitForWorkerAndProcessShutdown(swRegInfo, remoteType) {
|
||||
info(`terminating worker and waiting for ${remoteType} procs to shut down`);
|
||||
ok(swRegInfo.activeWorker, "worker should be in the active slot");
|
||||
is(
|
||||
countRemoteType(remoteType),
|
||||
1,
|
||||
`should have a single ${remoteType} process`
|
||||
`should have a single ${remoteType} process but have: ${debugRemotes()}`
|
||||
);
|
||||
|
||||
// Let's not wait too long for the process to shutdown.
|
||||
|
@ -49,35 +100,29 @@ async function waitForWorkerIdleShutdown(swRegInfo, remoteType) {
|
|||
swRegInfo.activeWorker.detachDebugger();
|
||||
|
||||
// Eventually the length will reach 0, meaning we're done!
|
||||
await TestUtils.waitForCondition(
|
||||
() => countRemoteType(remoteType) == 0,
|
||||
"wait for the worker's process to shutdown"
|
||||
);
|
||||
await waitForNoProcessesOfType(remoteType);
|
||||
|
||||
is(
|
||||
countRemoteType(remoteType),
|
||||
0,
|
||||
"processes with `remoteType` type should have shut down"
|
||||
`processes with remoteType=${remoteType} type should have shut down`
|
||||
);
|
||||
|
||||
// Make sure we never kill workers on idle except when this is called.
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["dom.serviceWorkers.idle_timeout", 299999],
|
||||
["dom.serviceWorkers.idle_extended_timeout", 299999],
|
||||
],
|
||||
});
|
||||
await SpecialPowers.popPrefEnv();
|
||||
}
|
||||
|
||||
async function do_test_sw(host, remoteType) {
|
||||
info(`entering test: host=${host}, remoteType=${remoteType}`);
|
||||
async function do_test_sw(host, remoteType, swMode, fileBlob) {
|
||||
info(
|
||||
`### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}`
|
||||
);
|
||||
|
||||
const prin = Services.scriptSecurityManager.createContentPrincipal(
|
||||
Services.io.newURI(`https://${host}`),
|
||||
{}
|
||||
);
|
||||
const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`;
|
||||
const scope = `https://${host}/${DIRPATH}scope`;
|
||||
const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`;
|
||||
|
||||
const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
|
||||
Ci.nsIServiceWorkerManager
|
||||
|
@ -97,55 +142,182 @@ async function do_test_sw(host, remoteType) {
|
|||
() => swRegInfo.activeWorker,
|
||||
"wait for the worker to become active"
|
||||
);
|
||||
await waitForWorkerIdleShutdown(swRegInfo, remoteType);
|
||||
await waitForWorkerAndProcessShutdown(swRegInfo, remoteType);
|
||||
|
||||
await BrowserTestUtils.withNewTab("about:blank", async browser => {
|
||||
// NOTE: We intentionally trigger the navigation from content in order to
|
||||
// make sure frontend doesn't eagerly process-switch for us.
|
||||
SpecialPowers.spawn(browser, [scope], async scope => {
|
||||
content.location.href = `${scope}/intercepted.html`;
|
||||
});
|
||||
await BrowserTestUtils.browserLoaded(browser);
|
||||
info(
|
||||
`test navigation interception with mode=${swMode} starting from about:blank`
|
||||
);
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: "about:blank",
|
||||
},
|
||||
async browser => {
|
||||
// NOTE: We intentionally trigger the navigation from content in order to
|
||||
// make sure frontend doesn't eagerly process-switch for us.
|
||||
SpecialPowers.spawn(
|
||||
browser,
|
||||
[scope, swMode, fileBlob],
|
||||
// eslint-disable-next-line no-shadow
|
||||
async (scope, swMode, fileBlob) => {
|
||||
const pageUrl = `${scope}?mode=${swMode}`;
|
||||
if (!fileBlob) {
|
||||
content.location.href = pageUrl;
|
||||
} else {
|
||||
const doc = content.document;
|
||||
const formElem = doc.createElement("form");
|
||||
doc.body.appendChild(formElem);
|
||||
|
||||
is(
|
||||
countRemoteType(remoteType),
|
||||
1,
|
||||
"should have spawned a content process with the correct remoteType"
|
||||
);
|
||||
formElem.action = pageUrl;
|
||||
formElem.method = "POST";
|
||||
formElem.enctype = "multipart/form-data";
|
||||
|
||||
// Ensure the worker was loaded in this process.
|
||||
const workerDebuggerURLs = await SpecialPowers.spawn(
|
||||
browser,
|
||||
[sw],
|
||||
async url => {
|
||||
await content.navigator.serviceWorker.ready;
|
||||
const wdm = Cc[
|
||||
"@mozilla.org/dom/workers/workerdebuggermanager;1"
|
||||
].getService(Ci.nsIWorkerDebuggerManager);
|
||||
const fileElem = doc.createElement("input");
|
||||
formElem.appendChild(fileElem);
|
||||
|
||||
return Array.from(wdm.getWorkerDebuggerEnumerator())
|
||||
.map(wd => {
|
||||
return wd.url;
|
||||
})
|
||||
.filter(swURL => swURL == url);
|
||||
}
|
||||
);
|
||||
Assert.deepEqual(
|
||||
workerDebuggerURLs,
|
||||
[sw],
|
||||
"The worker should be running in the correct child process"
|
||||
);
|
||||
fileElem.type = "file";
|
||||
fileElem.name = "foo";
|
||||
|
||||
// Unregister the worker.
|
||||
await SpecialPowers.spawn(browser, [], async () => {
|
||||
let registration = await content.navigator.serviceWorker.ready;
|
||||
await registration.unregister();
|
||||
});
|
||||
});
|
||||
fileElem.mozSetFileArray([fileBlob]);
|
||||
|
||||
await waitForWorkerIdleShutdown(swRegInfo, remoteType);
|
||||
formElem.submit();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await BrowserTestUtils.browserLoaded(browser);
|
||||
|
||||
is(
|
||||
countRemoteType(remoteType),
|
||||
1,
|
||||
`should have spawned a content process with remoteType=${remoteType}`
|
||||
);
|
||||
|
||||
const { source, blobContents } = await SpecialPowers.spawn(
|
||||
browser,
|
||||
[],
|
||||
() => {
|
||||
return {
|
||||
source: content.document.getElementById("source").textContent,
|
||||
blobContents: content.document.getElementById("blob").textContent,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
is(
|
||||
source,
|
||||
swMode === "synthetic" ? "ServiceWorker" : "ServerJS",
|
||||
"The page contents should come from the right place."
|
||||
);
|
||||
|
||||
is(
|
||||
blobContents,
|
||||
fileBlob ? TEST_BLOB_CONTENTS : "",
|
||||
"The request blob contents should be the blob/empty as appropriate."
|
||||
);
|
||||
|
||||
// Ensure the worker was loaded in this process.
|
||||
const workerDebuggerURLs = await SpecialPowers.spawn(
|
||||
browser,
|
||||
[sw],
|
||||
async url => {
|
||||
if (!content.navigator.serviceWorker.controller) {
|
||||
throw new Error("document not controlled!");
|
||||
}
|
||||
const wdm = Cc[
|
||||
"@mozilla.org/dom/workers/workerdebuggermanager;1"
|
||||
].getService(Ci.nsIWorkerDebuggerManager);
|
||||
|
||||
return Array.from(wdm.getWorkerDebuggerEnumerator())
|
||||
.map(wd => {
|
||||
return wd.url;
|
||||
})
|
||||
.filter(swURL => swURL == url);
|
||||
}
|
||||
);
|
||||
Assert.deepEqual(
|
||||
workerDebuggerURLs,
|
||||
[sw],
|
||||
"The worker should be running in the correct child process"
|
||||
);
|
||||
|
||||
// Unregister the ServiceWorker. The registration will continue to control
|
||||
// `browser` and therefore continue to exist and its worker to continue
|
||||
// running until the tab is closed.
|
||||
await SpecialPowers.spawn(browser, [], async () => {
|
||||
let registration = await content.navigator.serviceWorker.ready;
|
||||
await registration.unregister();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Now that the controlled tab is closed and the registration has been
|
||||
// removed, the ServiceWorker will be made redundant which will forcibly
|
||||
// terminate it, which will result in the shutdown of the given content
|
||||
// process. Wait for that to happen both as a verification and so the next
|
||||
// test has a sufficiently clean slate.
|
||||
await waitForNoProcessesOfType(remoteType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a File-backed blob. This will happen synchronously from the main
|
||||
* thread, which isn't optimal, but the test blocks on this progress anyways.
|
||||
* Bug 1669578 has been filed on improving this idiom and avoiding the sync
|
||||
* writes.
|
||||
*/
|
||||
async function makeFileBlob(blobContents) {
|
||||
const tmpFile = Cc["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Ci.nsIDirectoryService)
|
||||
.QueryInterface(Ci.nsIProperties)
|
||||
.get("TmpD", Ci.nsIFile);
|
||||
tmpFile.append("test-file-backed-blob.txt");
|
||||
tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
|
||||
|
||||
var outStream = Cc[
|
||||
"@mozilla.org/network/file-output-stream;1"
|
||||
].createInstance(Ci.nsIFileOutputStream);
|
||||
outStream.init(
|
||||
tmpFile,
|
||||
0x02 | 0x08 | 0x20, // write, create, truncate
|
||||
0o666,
|
||||
0
|
||||
);
|
||||
outStream.write(blobContents, blobContents.length);
|
||||
outStream.close();
|
||||
|
||||
const fileBlob = await File.createFromNsIFile(tmpFile);
|
||||
return fileBlob;
|
||||
}
|
||||
|
||||
add_task(async function test() {
|
||||
await do_test_sw("example.org", "privilegedmozilla");
|
||||
// ## Isolated Privileged Process
|
||||
// Trigger a straightforward intercepted navigation with no request body that
|
||||
// returns a synthetic response.
|
||||
await do_test_sw("example.org", "privilegedmozilla", "synthetic", null);
|
||||
|
||||
// Trigger an intercepted navigation with FormData containing an
|
||||
// <input type="file"> which will result in the request body containing a
|
||||
// RemoteLazyInputStream which will be consumed in the content process by the
|
||||
// ServiceWorker while generating the synthetic response.
|
||||
const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS);
|
||||
await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob);
|
||||
|
||||
// Trigger an intercepted navigation with FormData containing an
|
||||
// <input type="file"> which will result in the request body containing a
|
||||
// RemoteLazyInputStream which will be relayed back to the parent process
|
||||
// via direct invocation of fetch() on the event.request but without any
|
||||
// cloning.
|
||||
await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob);
|
||||
|
||||
// Same as the above but cloning the request before fetching it.
|
||||
await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob);
|
||||
|
||||
// ## Fission Isolation
|
||||
if (Services.appinfo.fissionAutostart) {
|
||||
const fissionUrl = "example.com";
|
||||
const fissionRemoteType = `webIsolated=https://example.com`;
|
||||
|
||||
await do_test_sw(fissionUrl, fissionRemoteType, "synthetic", null);
|
||||
await do_test_sw(fissionUrl, fissionRemoteType, "synthetic", fileBlob);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,14 +2,59 @@ addEventListener("install", function(evt) {
|
|||
evt.waitUntil(self.skipWaiting());
|
||||
});
|
||||
|
||||
/**
|
||||
* Given a multipart/form-data encoded string that we know to have only a single
|
||||
* part, return the contents of the part. (MIME multipart encoding is too
|
||||
* exciting to delve into.)
|
||||
*/
|
||||
function extractBlobFromMultipartFormData(text) {
|
||||
const lines = text.split(/\r\n/g);
|
||||
const firstBlank = lines.indexOf("");
|
||||
const foo = lines.slice(firstBlank + 1, -2).join("\n");
|
||||
return foo;
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", event => {
|
||||
event.respondWith(
|
||||
new Response(
|
||||
`<!DOCTYPE HTML><body>
|
||||
<h1 id="url">${event.request.url}</h1>
|
||||
<h1 id="location">${self.location.href}</h1>
|
||||
</body>`,
|
||||
{ headers: { "Content-Type": "text/html" } }
|
||||
)
|
||||
);
|
||||
const url = new URL(event.request.url);
|
||||
const mode = url.searchParams.get("mode");
|
||||
|
||||
if (mode === "synthetic") {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
// This works even if there wasn't a body explicitly associated with the
|
||||
// request. We just get a zero-length string in that case.
|
||||
const requestBodyContents = await event.request.text();
|
||||
const blobContents = extractBlobFromMultipartFormData(
|
||||
requestBodyContents
|
||||
);
|
||||
|
||||
return new Response(
|
||||
`<!DOCTYPE HTML><head><meta charset="utf-8"/></head><body>
|
||||
<h1 id="url">${event.request.url}</h1>
|
||||
<div id="source">ServiceWorker</div>
|
||||
<div id="blob">${blobContents}</div>
|
||||
</body>`,
|
||||
{ headers: { "Content-Type": "text/html" } }
|
||||
);
|
||||
})()
|
||||
);
|
||||
} else if (mode === "fetch") {
|
||||
event.respondWith(fetch(event.request));
|
||||
} else if (mode === "clone") {
|
||||
// In order for the act of cloning to be interesting, we want the original
|
||||
// request to remain alive so that any pipes end up having to buffer.
|
||||
self.originalRequest = event.request;
|
||||
event.respondWith(fetch(event.request.clone()));
|
||||
} else {
|
||||
event.respondWith(
|
||||
new Response(
|
||||
`<!DOCTYPE HTML><head><meta charset="utf-8"/></head><body>
|
||||
<h1 id="error">Bad mode: ${mode}</h1>
|
||||
<div id="source">ServiceWorker::Error</div>
|
||||
<div id="blob">No, this is an error.</div>
|
||||
</body>`,
|
||||
{ headers: { "Content-Type": "text/html" }, status: 400 }
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
const CC = Components.Constructor;
|
||||
const BinaryInputStream = CC(
|
||||
"@mozilla.org/binaryinputstream;1",
|
||||
"nsIBinaryInputStream",
|
||||
"setInputStream"
|
||||
);
|
||||
|
||||
function log(str) {
|
||||
//dump(`SJS LOG: ${str}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a multipart/form-data encoded string that we know to have only a single
|
||||
* part, return the contents of the part. (MIME multipart encoding is too
|
||||
* exciting to delve into.)
|
||||
*/
|
||||
function extractBlobFromMultipartFormData(text) {
|
||||
const lines = text.split(/\r\n/g);
|
||||
const firstBlank = lines.indexOf("");
|
||||
const foo = lines.slice(firstBlank + 1, -2).join("\n");
|
||||
return foo;
|
||||
}
|
||||
|
||||
async function handleRequest(request, response) {
|
||||
let blobContents = "";
|
||||
if (request.method !== "POST") {
|
||||
} else {
|
||||
var body = new BinaryInputStream(request.bodyInputStream);
|
||||
|
||||
var avail;
|
||||
var bytes = [];
|
||||
|
||||
while ((avail = body.available()) > 0) {
|
||||
Array.prototype.push.apply(bytes, body.readByteArray(avail));
|
||||
}
|
||||
let requestBodyContents = String.fromCharCode.apply(null, bytes);
|
||||
log(requestBodyContents);
|
||||
blobContents = extractBlobFromMultipartFormData(requestBodyContents);
|
||||
}
|
||||
|
||||
log("Setting Headers")
|
||||
response.setHeader("Content-Type", "text/html", false);
|
||||
response.setStatusLine(request.httpVersion, "200", "OK");
|
||||
response.write(`<!DOCTYPE HTML><head><meta charset="utf-8"/></head><body>
|
||||
<h1 id="url">${request.scheme}${request.host}${request.port}${request.path}</h1>
|
||||
<div id="source">ServerJS</div>
|
||||
<div id="blob">${blobContents}</div>
|
||||
</body>`);
|
||||
log("Done");
|
||||
}
|
Загрузка…
Ссылка в новой задаче