summaryrefslogtreecommitdiffstats
path: root/ipc/glue/test
diff options
context:
space:
mode:
Diffstat (limited to 'ipc/glue/test')
-rw-r--r--ipc/glue/test/browser/browser.ini59
-rw-r--r--ipc/glue/test/browser/browser_audio_shutdown.ini6
-rw-r--r--ipc/glue/test/browser/browser_audio_telemetry_content.js32
-rw-r--r--ipc/glue/test/browser/browser_audio_telemetry_rdd.js29
-rw-r--r--ipc/glue/test/browser/browser_audio_telemetry_utility.js27
-rw-r--r--ipc/glue/test/browser/browser_child_hang.ini7
-rw-r--r--ipc/glue/test/browser/browser_child_hang.js37
-rw-r--r--ipc/glue/test/browser/browser_utility_audioDecodeCrash.js95
-rw-r--r--ipc/glue/test/browser/browser_utility_audio_shutdown.js76
-rw-r--r--ipc/glue/test/browser/browser_utility_crashReporter.js34
-rw-r--r--ipc/glue/test/browser/browser_utility_geolocation_crashed.js70
-rw-r--r--ipc/glue/test/browser/browser_utility_hard_kill.js13
-rw-r--r--ipc/glue/test/browser/browser_utility_hard_kill_delayed.js57
-rw-r--r--ipc/glue/test/browser/browser_utility_memoryReport.js76
-rw-r--r--ipc/glue/test/browser/browser_utility_multipleAudio.js73
-rw-r--r--ipc/glue/test/browser/browser_utility_profiler.js76
-rw-r--r--ipc/glue/test/browser/browser_utility_start_clean_shutdown.js9
-rw-r--r--ipc/glue/test/browser/head-telemetry.js205
-rw-r--r--ipc/glue/test/browser/head.js424
-rw-r--r--ipc/glue/test/browser/mochitest_audio_off.ini12
-rw-r--r--ipc/glue/test/browser/mochitest_audio_on.ini12
-rw-r--r--ipc/glue/test/browser/moz.build12
-rw-r--r--ipc/glue/test/browser/test_utility_audio_off.html44
-rw-r--r--ipc/glue/test/browser/test_utility_audio_on.html45
-rw-r--r--ipc/glue/test/gtest/TestAsyncBlockers.cpp166
-rw-r--r--ipc/glue/test/gtest/TestUtilityProcess.cpp155
-rw-r--r--ipc/glue/test/gtest/moz.build21
-rw-r--r--ipc/glue/test/utility_process_xpcom/UtilityProcessTest.cpp164
-rw-r--r--ipc/glue/test/utility_process_xpcom/UtilityProcessTest.h29
-rw-r--r--ipc/glue/test/utility_process_xpcom/components.conf15
-rw-r--r--ipc/glue/test/utility_process_xpcom/moz.build21
-rw-r--r--ipc/glue/test/utility_process_xpcom/nsIUtilityProcessTest.idl44
32 files changed, 2145 insertions, 0 deletions
diff --git a/ipc/glue/test/browser/browser.ini b/ipc/glue/test/browser/browser.ini
new file mode 100644
index 0000000000..9dc84c3914
--- /dev/null
+++ b/ipc/glue/test/browser/browser.ini
@@ -0,0 +1,59 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_audio_telemetry_content.js]
+skip-if =
+ (os == 'win') # gfx blocks us because media.rdd-process.enabled=false disables PDMFactory::AllDecodersAreRemote()
+support-files =
+ head-telemetry.js
+ ../../../../dom/media/test/small-shot.ogg
+ ../../../../dom/media/test/small-shot.mp3
+ ../../../../dom/media/test/small-shot.m4a
+ ../../../../dom/media/test/small-shot.flac
+[browser_audio_telemetry_rdd.js]
+support-files =
+ head-telemetry.js
+ ../../../../dom/media/test/small-shot.ogg
+ ../../../../dom/media/test/small-shot.mp3
+ ../../../../dom/media/test/small-shot.m4a
+ ../../../../dom/media/test/small-shot.flac
+[browser_audio_telemetry_utility.js]
+support-files =
+ head-telemetry.js
+ ../../../../dom/media/test/small-shot.ogg
+ ../../../../dom/media/test/small-shot.mp3
+ ../../../../dom/media/test/small-shot.m4a
+ ../../../../dom/media/test/small-shot.flac
+[browser_utility_audioDecodeCrash.js]
+support-files =
+ ../../../../dom/media/test/small-shot.ogg
+ ../../../../dom/media/test/small-shot.mp3
+ ../../../../dom/media/test/small-shot.m4a
+ ../../../../dom/media/test/small-shot.flac
+skip-if =
+ !crashreporter
+ ccov
+[browser_utility_crashReporter.js]
+skip-if =
+ !crashreporter
+ ccov
+[browser_utility_geolocation_crashed.js]
+skip-if =
+ !(os == 'win' && os_version == '10.0') # Geolocation is remoted only on Windows 8+
+ !crashreporter
+ ccov
+[browser_utility_hard_kill.js]
+[browser_utility_hard_kill_delayed.js] # bug 1754572: we really want hard_kill to be rust before hard_kill_delayed
+[browser_utility_memoryReport.js]
+skip-if = tsan # bug 1754554
+[browser_utility_multipleAudio.js]
+support-files =
+ ../../../../dom/media/test/small-shot.ogg
+ ../../../../dom/media/test/small-shot.mp3
+ ../../../../dom/media/test/small-shot.m4a
+ ../../../../dom/media/test/small-shot.flac
+[browser_utility_profiler.js]
+support-files =
+ ../../../../tools/profiler/tests/shared-head.js
+skip-if = tsan # from tools/profiler/tests/browser/browser.ini, timing out on profiler tests?
+[browser_utility_start_clean_shutdown.js]
diff --git a/ipc/glue/test/browser/browser_audio_shutdown.ini b/ipc/glue/test/browser/browser_audio_shutdown.ini
new file mode 100644
index 0000000000..a0ce9796f0
--- /dev/null
+++ b/ipc/glue/test/browser/browser_audio_shutdown.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files = head.js
+
+[browser_utility_audio_shutdown.js]
+support-files =
+ ../../../../dom/media/test/small-shot.ogg
diff --git a/ipc/glue/test/browser/browser_audio_telemetry_content.js b/ipc/glue/test/browser/browser_audio_telemetry_content.js
new file mode 100644
index 0000000000..5d69175d5c
--- /dev/null
+++ b/ipc/glue/test/browser/browser_audio_telemetry_content.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head-telemetry.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/ipc/glue/test/browser/head-telemetry.js",
+ this
+);
+
+add_setup(async function testNoTelemetry() {
+ await Telemetry.clearScalars();
+});
+
+add_task(async function testAudioDecodingInContent() {
+ await runTest({ expectUtility: false, expectRDD: false });
+});
+
+add_task(async function testUtilityTelemetry() {
+ const codecs = ["vorbis", "mp3", "aac", "flac"];
+ const extraKey = ",rdd-disabled,utility-disabled";
+ await verifyTelemetryForProcess("tab", codecs, extraKey);
+
+ const platform = Services.appinfo.OS;
+ for (let exp of utilityPerCodecs[platform]) {
+ await verifyNoTelemetryForProcess(exp.process, exp.codecs, extraKey);
+ }
+
+ await verifyNoTelemetryForProcess("rdd", codecs, extraKey);
+});
diff --git a/ipc/glue/test/browser/browser_audio_telemetry_rdd.js b/ipc/glue/test/browser/browser_audio_telemetry_rdd.js
new file mode 100644
index 0000000000..b4e41b562e
--- /dev/null
+++ b/ipc/glue/test/browser/browser_audio_telemetry_rdd.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head-telemetry.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/ipc/glue/test/browser/head-telemetry.js",
+ this
+);
+
+add_setup(async function testNoTelemetry() {
+ await Telemetry.clearScalars();
+});
+
+add_task(async function testAudioDecodingInRDD() {
+ await runTest({ expectUtility: false, expectRDD: true });
+});
+
+add_task(async function testRDDTelemetry() {
+ const extraKey = ",utility-disabled";
+ const platform = Services.appinfo.OS;
+ for (let exp of utilityPerCodecs[platform]) {
+ await verifyNoTelemetryForProcess(exp.process, exp.codecs, extraKey);
+ }
+ const codecs = ["vorbis", "mp3", "aac", "flac"];
+ await verifyTelemetryForProcess("rdd", codecs, extraKey);
+});
diff --git a/ipc/glue/test/browser/browser_audio_telemetry_utility.js b/ipc/glue/test/browser/browser_audio_telemetry_utility.js
new file mode 100644
index 0000000000..07b492cc12
--- /dev/null
+++ b/ipc/glue/test/browser/browser_audio_telemetry_utility.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head-telemetry.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/ipc/glue/test/browser/head-telemetry.js",
+ this
+);
+
+add_setup(async function testNoTelemetry() {
+ await Telemetry.clearScalars();
+});
+
+add_task(async function testAudioDecodingInUtility() {
+ await runTest({ expectUtility: true, expectRDD: true });
+});
+
+add_task(async function testUtilityTelemetry() {
+ const platform = Services.appinfo.OS;
+ for (let exp of utilityPerCodecs[platform]) {
+ await verifyTelemetryForProcess(exp.process, exp.codecs);
+ }
+ await verifyNoTelemetryForProcess("rdd", ["vorbis", "mp3", "aac", "flac"]);
+});
diff --git a/ipc/glue/test/browser/browser_child_hang.ini b/ipc/glue/test/browser/browser_child_hang.ini
new file mode 100644
index 0000000000..94931561b0
--- /dev/null
+++ b/ipc/glue/test/browser/browser_child_hang.ini
@@ -0,0 +1,7 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+[DEFAULT]
+tags = ipc
+environment = MOZ_TEST_CHILD_EXIT_HANG=3
+
+[browser_child_hang.js]
diff --git a/ipc/glue/test/browser/browser_child_hang.js b/ipc/glue/test/browser/browser_child_hang.js
new file mode 100644
index 0000000000..cf890a6c61
--- /dev/null
+++ b/ipc/glue/test/browser/browser_child_hang.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+//
+// Try to open a tab. This provides code coverage for a few things,
+// although currently there's no automated functional test of correctness:
+//
+// * On opt builds, when the tab is closed and the process exits, it
+// will hang for 3s and the parent will kill it after 2s.
+//
+// * On debug[*] builds, the parent process will wait until the
+// process exits normally; but also, on browser shutdown, the
+// preallocated content processes will block parent shutdown in
+// WillDestroyCurrentMessageLoop.
+//
+// [*] Also sanitizer and code coverage builds.
+//
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "https://example.com/",
+ forceNewProcess: true,
+ },
+ async function (browser) {
+ // browser.frameLoader.remoteTab.osPid is the child pid; once we
+ // have a way to get notifications about child process termination
+ // events, that could be useful.
+ ok(true, "Browser isn't broken");
+ }
+ );
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 4000));
+ ok(true, "Still running after child process (hopefully) exited");
+});
diff --git a/ipc/glue/test/browser/browser_utility_audioDecodeCrash.js b/ipc/glue/test/browser/browser_utility_audioDecodeCrash.js
new file mode 100644
index 0000000000..1c7551c623
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_audioDecodeCrash.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function getAudioDecoderPid(expectation) {
+ info("Finding a running AudioDecoder");
+
+ const actor = expectation.replace("Utility ", "");
+
+ let audioDecoderProcess = (await ChromeUtils.requestProcInfo()).children.find(
+ p =>
+ p.type === "utility" &&
+ p.utilityActors.find(a => a.actorName === `audioDecoder_${actor}`)
+ );
+ ok(
+ audioDecoderProcess,
+ `Found the AudioDecoder ${actor} process at ${audioDecoderProcess.pid}`
+ );
+ return audioDecoderProcess.pid;
+}
+
+async function crashDecoder(expectation) {
+ const audioPid = await getAudioDecoderPid(expectation);
+ ok(audioPid > 0, `Found an audio decoder ${audioPid}`);
+ const actorIsAudioDecoder = actorNames => {
+ return actorNames
+ .split(",")
+ .some(actorName => actorName.trim().startsWith("audio-decoder-"));
+ };
+ info(`Crashing audio decoder ${audioPid}`);
+ await crashSomeUtility(audioPid, actorIsAudioDecoder);
+}
+
+async function runTest(src, withClose, expectation) {
+ info(`Add media tabs: ${src}`);
+ let tab = await addMediaTab(src);
+
+ info("Play tab");
+ await play(tab, expectation.process, expectation.decoder);
+
+ info("Crash decoder");
+ await crashDecoder(expectation.process);
+
+ if (withClose) {
+ info("Stop tab");
+ await stop(tab);
+
+ info("Remove tab");
+ await BrowserTestUtils.removeTab(tab);
+
+ info("Create tab again");
+ tab = await addMediaTab(src);
+ }
+
+ info("Play tab again");
+ await play(tab, expectation.process, expectation.decoder);
+
+ info("Stop tab");
+ await stop(tab);
+
+ info("Remove tab");
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.utility-process.enabled", true]],
+ });
+});
+
+async function testAudioCrash(withClose) {
+ info(`Running tests for audio decoder process crashing: ${withClose}`);
+
+ SimpleTest.expectChildProcessCrash();
+
+ const platform = Services.appinfo.OS;
+
+ for (let { src, expectations } of audioTestData()) {
+ if (!(platform in expectations)) {
+ info(`Skipping ${src} for ${platform}`);
+ continue;
+ }
+
+ await runTest(src, withClose, expectations[platform]);
+ }
+}
+
+add_task(async function testAudioCrashSimple() {
+ await testAudioCrash(false);
+});
+
+add_task(async function testAudioCrashClose() {
+ await testAudioCrash(true);
+});
diff --git a/ipc/glue/test/browser/browser_utility_audio_shutdown.js b/ipc/glue/test/browser/browser_utility_audio_shutdown.js
new file mode 100644
index 0000000000..31811cfd55
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_audio_shutdown.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// The purpose of that test is to reproduce edge case behaviors that one can
+// have while running whole ipc/glue/test/browser/ suite but that could this
+// way be intermittent and hard to diagnose. By having such a test we make sure
+// it is cleanly reproduced and wont regress somewhat silently.
+
+"use strict";
+
+async function runTest(src, process, decoder) {
+ info(`Add media tabs: ${src}`);
+ let tab = await addMediaTab(src);
+
+ info("Play tab");
+ await play(tab, process, decoder);
+
+ info("Stop tab");
+ await stop(tab);
+
+ info("Remove tab");
+ await BrowserTestUtils.removeTab(tab);
+}
+
+async function findGenericAudioDecoder() {
+ const audioDecoders = (await ChromeUtils.requestProcInfo()).children.filter(
+ p => {
+ return (
+ p.type === "utility" &&
+ p.utilityActors.find(a => a.actorName === "audioDecoder_Generic")
+ );
+ }
+ );
+ ok(audioDecoders.length === 1, "Only one audio decoder present");
+ return audioDecoders[0].pid;
+}
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.utility-process.enabled", true]],
+ });
+});
+
+add_task(async function testKill() {
+ await runTest("small-shot.ogg", "Utility Generic", "vorbis audio decoder");
+
+ await cleanUtilityProcessShutdown(
+ "audioDecoder_Generic",
+ true /* preferKill */
+ );
+
+ info("Waiting 15s to trigger mShutdownBlockers assertions");
+ await new Promise((resolve, reject) => {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ setTimeout(resolve, 15 * 1000);
+ });
+
+ ok(true, "Waited 15s to trigger mShutdownBlockers assertions: over");
+});
+
+add_task(async function testShutdown() {
+ await runTest("small-shot.ogg", "Utility Generic", "vorbis audio decoder");
+
+ const audioDecoderPid = await findGenericAudioDecoder();
+ ok(audioDecoderPid > 0, `Valid PID found: ${audioDecoderPid}`);
+
+ await cleanUtilityProcessShutdown("audioDecoder_Generic");
+
+ info("Waiting 15s to trigger mShutdownBlockers assertions");
+ await new Promise((resolve, reject) => {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ setTimeout(resolve, 15 * 1000);
+ });
+
+ ok(true, "Waited 15s to trigger mShutdownBlockers assertions: over");
+});
diff --git a/ipc/glue/test/browser/browser_utility_crashReporter.js b/ipc/glue/test/browser/browser_utility_crashReporter.js
new file mode 100644
index 0000000000..73e6c6355a
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_crashReporter.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function startAndCrashUtility(numUnknownActors, actorsCheck) {
+ const actors = Array(numUnknownActors).fill("unknown");
+ const utilityPid = await startUtilityProcess(actors);
+ await crashSomeUtility(utilityPid, actorsCheck);
+}
+
+// When running full suite, previous tests may have left some utility
+// processes running and this might interfere with our testing.
+add_setup(async function ensureNoExistingProcess() {
+ await killUtilityProcesses();
+});
+
+add_task(async function utilityNoActor() {
+ await startAndCrashUtility(0, actorNames => {
+ return actorNames === undefined;
+ });
+});
+
+add_task(async function utilityOneActor() {
+ await startAndCrashUtility(1, actorNames => {
+ return actorNames === kGenericUtilityActor;
+ });
+});
+
+add_task(async function utilityManyActors() {
+ await startAndCrashUtility(42, actorNames => {
+ return actorNames === Array(42).fill("unknown").join(", ");
+ });
+});
diff --git a/ipc/glue/test/browser/browser_utility_geolocation_crashed.js b/ipc/glue/test/browser/browser_utility_geolocation_crashed.js
new file mode 100644
index 0000000000..b0c341b69f
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_geolocation_crashed.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function getGeolocation() {
+ info("Requesting geolocation");
+
+ let resolve;
+ let promise = new Promise((_resolve, _reject) => {
+ resolve = _resolve;
+ });
+
+ navigator.geolocation.getCurrentPosition(
+ () => {
+ ok(true, "geolocation succeeded");
+ resolve(undefined);
+ },
+ () => {
+ ok(false, "geolocation failed");
+ resolve(undefined);
+ }
+ );
+
+ return promise;
+}
+
+add_setup(async function () {
+ // Avoid the permission doorhanger and cache that would trigger instead
+ // of re-requesting location. Setting geo.timeout to 0 causes it to
+ // retry the system geolocation (incl. recreating the utility process)
+ // instead of reusing the MLS geolocation fallback it found the first time.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["geo.prompt.testing", true],
+ ["geo.prompt.testing.allow", true],
+ ["geo.provider.network.debug.requestCache.enabled", false],
+ ["geo.provider.testing", false],
+ ["geo.timeout", 0],
+ ],
+ });
+});
+
+add_task(async function testGeolocationProcessCrash() {
+ info("Start the Windows utility process");
+ await getGeolocation();
+
+ info("Crash the utility process");
+ await crashSomeUtilityActor("windowsUtils");
+
+ info("Restart the Windows utility process");
+ await getGeolocation();
+
+ info("Confirm the restarted process");
+ await checkUtilityExists("windowsUtils");
+
+ info("Kill the utility process");
+ await cleanUtilityProcessShutdown("windowsUtils", true);
+
+ info("Restart the Windows utility process again");
+ await getGeolocation();
+
+ info("Confirm the restarted process");
+ await checkUtilityExists("windowsUtils");
+});
+
+add_task(async function testCleanup() {
+ info("Clean up to avoid confusing future tests");
+ await cleanUtilityProcessShutdown("windowsUtils", true);
+});
diff --git a/ipc/glue/test/browser/browser_utility_hard_kill.js b/ipc/glue/test/browser/browser_utility_hard_kill.js
new file mode 100644
index 0000000000..516ef64045
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_hard_kill.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ await startUtilityProcess(["unknown"]);
+
+ SimpleTest.expectChildProcessCrash();
+
+ info("Hard kill Utility Process");
+ await cleanUtilityProcessShutdown("unknown", true /* preferKill */);
+});
diff --git a/ipc/glue/test/browser/browser_utility_hard_kill_delayed.js b/ipc/glue/test/browser/browser_utility_hard_kill_delayed.js
new file mode 100644
index 0000000000..ffda4d3988
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_hard_kill_delayed.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ const utilityPid = await startUtilityProcess();
+
+ SimpleTest.expectChildProcessCrash();
+
+ const utilityProcessGone = TestUtils.topicObserved("ipc:utility-shutdown");
+
+ info("Hard kill Utility Process");
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+
+ // Here we really want to exercise the fact that kill() might not be done
+ // right now but a bit later, and we should wait for the process to be dead
+ // before considering the test is finished.
+ //
+ // Without this, we get into bug 1754572 (where there was no setTimeout nor
+ // the wait) where the kill() operation ends up really killing the child a
+ // bit after the current test has been finished ; unfortunately, this happened
+ // right after the next test, browser_utility_memoryReport.js did start and
+ // even worse, after it thought it had started a new utility process. We were
+ // in fact re-using the one we started here, and when we wanted to query its
+ // pid in the browser_utility_memoryReport.js then the kill() happened, so
+ // no more process and the test intermittently failed.
+ //
+ // The timeout value of 50ms should be long enough to allow the test to finish
+ // and the next one to start and get a reference on the process we launched,
+ // and yet allow us to kill the process in the middle of the next test. Higher
+ // values would allow browser_utility_memoryReport.js to complete without
+ // reproducing the issue (both locally and on try).
+ //
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => {
+ ProcessTools.kill(utilityPid);
+ }, 50);
+
+ info(`Waiting for utility process ${utilityPid} to go away.`);
+ let [subject, data] = await utilityProcessGone;
+ ok(
+ subject instanceof Ci.nsIPropertyBag2,
+ "Subject needs to be a nsIPropertyBag2 to clean up properly"
+ );
+ is(
+ parseInt(data, 10),
+ utilityPid,
+ `Should match the crashed PID ${utilityPid} with ${data}`
+ );
+
+ // Make sure the process is dead, otherwise there is a risk of race for
+ // writing leak logs
+ utilityProcessTest().noteIntentionalCrash(utilityPid);
+});
diff --git a/ipc/glue/test/browser/browser_utility_memoryReport.js b/ipc/glue/test/browser/browser_utility_memoryReport.js
new file mode 100644
index 0000000000..8cec61b8be
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_memoryReport.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// When running full suite, previous audio decoding tests might have left some
+// running and this might interfere with our testing
+add_setup(async function ensureNoExistingProcess() {
+ await killUtilityProcesses();
+});
+
+add_task(async () => {
+ const utilityPid = await startUtilityProcess();
+
+ const gMgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
+ Ci.nsIMemoryReporterManager
+ );
+ ok(utilityPid !== undefined, `Utility process is running as ${utilityPid}`);
+
+ var utilityReports = [];
+
+ const performCollection = new Promise((resolve, reject) => {
+ // Record the reports from the live memory reporters then process them.
+ let handleReport = function (
+ aProcess,
+ aUnsafePath,
+ aKind,
+ aUnits,
+ aAmount,
+ aDescription
+ ) {
+ const expectedProcess = `Utility (pid ${utilityPid}, sandboxingKind ${kGenericUtilitySandbox})`;
+ if (aProcess !== expectedProcess) {
+ return;
+ }
+
+ let report = {
+ process: aProcess,
+ path: aUnsafePath,
+ kind: aKind,
+ units: aUnits,
+ amount: aAmount,
+ description: aDescription,
+ };
+
+ utilityReports.push(report);
+ };
+
+ info("Memory report: Perform the call");
+ gMgr.getReports(handleReport, null, resolve, null, false);
+ });
+
+ await performCollection;
+
+ info(
+ `Collected ${utilityReports.length} reports from utility process ${utilityPid}`
+ );
+ ok(!!utilityReports.length, "Collected some reports");
+ ok(
+ utilityReports.filter(r => r.path === "vsize" && r.amount > 0).length === 1,
+ "Collected vsize report"
+ );
+ ok(
+ utilityReports.filter(r => r.path === "resident" && r.amount > 0).length ===
+ 1,
+ "Collected resident report"
+ );
+ ok(
+ !!utilityReports.filter(
+ r => r.path.search(/^explicit\/.*/) >= 0 && r.amount > 0
+ ).length,
+ "Collected some explicit/ report"
+ );
+
+ await cleanUtilityProcessShutdown();
+});
diff --git a/ipc/glue/test/browser/browser_utility_multipleAudio.js b/ipc/glue/test/browser/browser_utility_multipleAudio.js
new file mode 100644
index 0000000000..d88ee014e2
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_multipleAudio.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function runTest(expectUtility) {
+ info(
+ `Running tests with decoding from Utility or RDD: expectUtility=${expectUtility}`
+ );
+
+ // Utility should now be the default, so dont toggle the pref unless we test
+ // RDD
+ if (!expectUtility) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.utility-process.enabled", expectUtility]],
+ });
+ }
+
+ const platform = Services.appinfo.OS;
+
+ for (let { src, expectations } of audioTestData()) {
+ if (!(platform in expectations)) {
+ info(`Skipping ${src} for ${platform}`);
+ continue;
+ }
+
+ const expectation = expectations[platform];
+
+ info(`Add media tabs: ${src}`);
+ let tabs = [await addMediaTab(src), await addMediaTab(src)];
+ let playback = [];
+
+ info("Play tabs");
+ for (let tab of tabs) {
+ playback.push(
+ play(
+ tab,
+ expectUtility ? expectation.process : "RDD",
+ expectation.decoder
+ )
+ );
+ }
+
+ info("Wait all playback");
+ await Promise.all(playback);
+
+ let allstop = [];
+ info("Stop tabs");
+ for (let tab of tabs) {
+ allstop.push(stop(tab));
+ }
+
+ info("Wait all stop");
+ await Promise.all(allstop);
+
+ let remove = [];
+ info("Remove tabs");
+ for (let tab of tabs) {
+ remove.push(BrowserTestUtils.removeTab(tab));
+ }
+
+ info("Wait all tabs to be removed");
+ await Promise.all(remove);
+ }
+}
+
+add_task(async function testAudioDecodingInUtility() {
+ await runTest(true);
+});
+
+add_task(async function testAudioDecodingInRDD() {
+ await runTest(false);
+});
diff --git a/ipc/glue/test/browser/browser_utility_profiler.js b/ipc/glue/test/browser/browser_utility_profiler.js
new file mode 100644
index 0000000000..084cd67747
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_profiler.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from /tools/profiler/tests/shared-head.js */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/tools/profiler/tests/browser/shared-head.js",
+ this
+);
+
+// When running full suite, previous tests may have left some utility
+// processes running and this might interfere with our testing.
+add_setup(async function ensureNoExistingProcess() {
+ await killUtilityProcesses();
+});
+
+add_task(async () => {
+ const utilityPid = await startUtilityProcess();
+
+ info("Start the profiler");
+ await startProfiler();
+
+ let profile;
+ await TestUtils.waitForCondition(async () => {
+ profile = await Services.profiler.getProfileDataAsync();
+ return (
+ // Search for process name to not be disturbed by other types of utility
+ // e.g. Utility AudioDecoder
+ profile.processes.filter(
+ ps => ps.threads[0].processName === "Utility Process"
+ ).length === 1
+ );
+ }, "Give time for the profiler to start and collect some samples");
+
+ info(`Check that the utility process ${utilityPid} is present.`);
+ let utilityProcessIndex = profile.processes.findIndex(
+ p => p.threads[0].pid == utilityPid
+ );
+ Assert.notEqual(utilityProcessIndex, -1, "Could find index of utility");
+
+ Assert.equal(
+ profile.processes[utilityProcessIndex].threads[0].processType,
+ "utility",
+ "Profile has processType utility"
+ );
+
+ Assert.equal(
+ profile.processes[utilityProcessIndex].threads[0].name,
+ "GeckoMain",
+ "Profile has correct main thread name"
+ );
+
+ Assert.equal(
+ profile.processes[utilityProcessIndex].threads[0].processName,
+ "Utility Process",
+ "Profile has correct process name"
+ );
+
+ Assert.greater(
+ profile.processes[utilityProcessIndex].threads.length,
+ 0,
+ "The utility process should have threads"
+ );
+
+ Assert.equal(
+ profile.threads.length,
+ 1,
+ "The parent process should have only one thread"
+ );
+
+ Services.profiler.StopProfiler();
+
+ await cleanUtilityProcessShutdown();
+});
diff --git a/ipc/glue/test/browser/browser_utility_start_clean_shutdown.js b/ipc/glue/test/browser/browser_utility_start_clean_shutdown.js
new file mode 100644
index 0000000000..62a9e4065b
--- /dev/null
+++ b/ipc/glue/test/browser/browser_utility_start_clean_shutdown.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async () => {
+ await startUtilityProcess();
+ await cleanUtilityProcessShutdown();
+});
diff --git a/ipc/glue/test/browser/head-telemetry.js b/ipc/glue/test/browser/head-telemetry.js
new file mode 100644
index 0000000000..64a6aee1c4
--- /dev/null
+++ b/ipc/glue/test/browser/head-telemetry.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from head.js */
+
+const Telemetry = Services.telemetry;
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+
+/* eslint-disable mozilla/no-redeclare-with-import-autofix */
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MEDIA_AUDIO_PROCESS = "media.audio_process_per_codec_name";
+
+const utilityPerCodecs = {
+ Linux: [
+ {
+ process: "utility+audioDecoder_Generic",
+ codecs: ["vorbis", "mp3", "aac", "flac"],
+ },
+ ],
+ WINNT: [
+ {
+ process: "utility+audioDecoder_Generic",
+ codecs: ["vorbis", "mp3", "flac"],
+ },
+ {
+ process: "utility+audioDecoder_WMF",
+ codecs: ["aac"],
+ },
+ ],
+ Darwin: [
+ {
+ process: "utility+audioDecoder_Generic",
+ codecs: ["vorbis", "mp3", "flac"],
+ },
+ {
+ process: "utility+audioDecoder_AppleMedia",
+ codecs: ["aac"],
+ },
+ ],
+};
+
+const kInterval = 300; /* ms */
+const kRetries = 5;
+
+/**
+ * This function waits until utility scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForKeyedScalars(process) {
+ await ContentTaskUtils.waitForCondition(
+ () => {
+ const scalars = Telemetry.getSnapshotForKeyedScalars("main", false);
+ return Object.keys(scalars).includes("content");
+ },
+ `Waiting for ${process} scalars to have been set`,
+ kInterval,
+ kRetries
+ );
+}
+
+async function waitForValue(process, codecNames, extra = "") {
+ await ContentTaskUtils.waitForCondition(
+ () => {
+ const telemetry = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).content;
+ if (telemetry && MEDIA_AUDIO_PROCESS in telemetry) {
+ const keyProcMimeTypes = Object.keys(telemetry[MEDIA_AUDIO_PROCESS]);
+ const found = codecNames.every(item =>
+ keyProcMimeTypes.includes(`${process},${item}${extra}`)
+ );
+ return found;
+ }
+ return false;
+ },
+ `Waiting for ${MEDIA_AUDIO_PROCESS}`,
+ kInterval,
+ kRetries
+ );
+}
+
+async function runTest({ expectUtility = false, expectRDD = false }) {
+ info(
+ `Running tests with decoding from Utility or RDD: expectUtility=${expectUtility} expectRDD=${expectRDD}`
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.utility-process.enabled", expectUtility],
+ ["media.rdd-process.enabled", expectRDD],
+ ["toolkit.telemetry.ipcBatchTimeout", 0],
+ ],
+ });
+
+ const platform = Services.appinfo.OS;
+
+ for (let { src, expectations } of audioTestData()) {
+ if (!(platform in expectations)) {
+ info(`Skipping ${src} for ${platform}`);
+ continue;
+ }
+
+ const expectation = expectations[platform];
+
+ info(`Add media tab: ${src}`);
+ let tab = await addMediaTab(src);
+
+ info("Play tab");
+ await play(
+ tab,
+ expectUtility ? expectation.process : "RDD",
+ expectation.decoder,
+ !expectUtility && !expectRDD
+ );
+
+ info("Stop tab");
+ await stop(tab);
+
+ info("Remove tab");
+ await BrowserTestUtils.removeTab(tab);
+ }
+}
+
+function getTelemetry() {
+ const telemetry = Telemetry.getSnapshotForKeyedScalars("main", false).content;
+ return telemetry;
+}
+
+async function verifyTelemetryForProcess(process, codecNames, extraKey = "") {
+ // Once scalars are set by the utility process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForKeyedScalars(process);
+ await waitForValue(process, codecNames, extraKey);
+
+ const telemetry = getTelemetry();
+
+ // The amount here depends on how many times RemoteAudioDecoderParent::RemoteAudioDecoderParent
+ // gets called, which might be more than actual audio files being playback, e.g., we would get one for metadata loading, then one for playback etc.
+ // But we dont care really we just want to ensure 0 on RDD, Content and others
+ // in the wild.[${codecName}]
+ codecNames.forEach(codecName => {
+ Assert.equal(
+ telemetry[MEDIA_AUDIO_PROCESS][`${process},${codecName}${extraKey}`],
+ 1,
+ `${MEDIA_AUDIO_PROCESS} must have the correct value (${process}, ${codecName}).`
+ );
+ });
+}
+
+async function verifyNoTelemetryForProcess(process, codecNames, extraKey = "") {
+ try {
+ await waitForKeyedScalars(process);
+ await waitForValue(process, codecNames, extraKey);
+ } catch (ex) {
+ if (ex.indexOf("timed out after") > 0) {
+ Assert.ok(
+ true,
+ `Expected timeout ${process}[${MEDIA_AUDIO_PROCESS}] for ${codecNames}`
+ );
+ } else {
+ Assert.ok(
+ false,
+ `Unexpected exception on ${process}[${MEDIA_AUDIO_PROCESS}] for ${codecNames}: ${ex}`
+ );
+ }
+ }
+
+ const telemetry = getTelemetry();
+
+ // There could be races with telemetry for power usage coming up
+ codecNames.forEach(codecName => {
+ if (telemetry) {
+ if (telemetry && MEDIA_AUDIO_PROCESS in telemetry) {
+ Assert.ok(
+ !(
+ `${process},${codecName}${extraKey}` in
+ telemetry[MEDIA_AUDIO_PROCESS]
+ ),
+ `Some telemetry but no ${process}[${MEDIA_AUDIO_PROCESS}][${codecName}]`
+ );
+ } else {
+ Assert.ok(
+ !(MEDIA_AUDIO_PROCESS in telemetry),
+ `No telemetry for ${process}[${MEDIA_AUDIO_PROCESS}][${codecName}]`
+ );
+ }
+ } else {
+ Assert.equal(
+ undefined,
+ telemetry,
+ `No telemetry for ${process}[${MEDIA_AUDIO_PROCESS}][${codecName}]`
+ );
+ }
+ });
+}
diff --git a/ipc/glue/test/browser/head.js b/ipc/glue/test/browser/head.js
new file mode 100644
index 0000000000..14c0859eb3
--- /dev/null
+++ b/ipc/glue/test/browser/head.js
@@ -0,0 +1,424 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const utilityProcessTest = () => {
+ return Cc["@mozilla.org/utility-process-test;1"].createInstance(
+ Ci.nsIUtilityProcessTest
+ );
+};
+
+const kGenericUtilitySandbox = 0;
+const kGenericUtilityActor = "unknown";
+
+// Start a generic utility process with the given array of utility actor names
+// registered.
+async function startUtilityProcess(actors = []) {
+ info("Start a UtilityProcess");
+ return utilityProcessTest().startProcess(actors);
+}
+
+// Returns an array of process infos for utility processes of the given type
+// or all utility processes if actor is not defined.
+async function getUtilityProcesses(actor = undefined) {
+ let procInfos = (await ChromeUtils.requestProcInfo()).children.filter(p => {
+ return (
+ p.type === "utility" &&
+ (actor == undefined ||
+ p.utilityActors.find(a => a.actorName.startsWith(actor)))
+ );
+ });
+
+ info(`Utility process infos = ${JSON.stringify(procInfos)}`);
+ return procInfos;
+}
+
+async function getUtilityPid(actor) {
+ let process = await getUtilityProcesses(actor);
+ is(process.length, 1, `exactly one ${actor} process exists`);
+ return process[0].pid;
+}
+
+async function checkUtilityExists(actor) {
+ info(`Looking for a running ${actor} utility process`);
+ const utilityPid = await getUtilityPid(actor);
+ ok(utilityPid > 0, `Found ${actor} utility process ${utilityPid}`);
+ return utilityPid;
+}
+
+// "Cleanly stop" a utility process. This will never leave a crash dump file.
+// preferKill will "kill" the process (e.g. SIGABRT) instead of using the
+// UtilityProcessManager.
+// To "crash" -- i.e. shutdown and generate a crash dump -- use
+// crashSomeUtility().
+async function cleanUtilityProcessShutdown(actor, preferKill = false) {
+ info(`${preferKill ? "Kill" : "Clean shutdown"} Utility Process ${actor}`);
+
+ const utilityPid = await getUtilityPid(actor);
+ ok(utilityPid !== undefined, `Must have PID for ${actor} utility process`);
+
+ const utilityProcessGone = TestUtils.topicObserved(
+ "ipc:utility-shutdown",
+ (subject, data) => parseInt(data, 10) === utilityPid
+ );
+
+ if (preferKill) {
+ SimpleTest.expectChildProcessCrash();
+ info(`Kill Utility Process ${utilityPid}`);
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+ ProcessTools.kill(utilityPid);
+ } else {
+ info(`Stopping Utility Process ${utilityPid}`);
+ await utilityProcessTest().stopProcess(actor);
+ }
+
+ let [subject, data] = await utilityProcessGone;
+ ok(
+ subject instanceof Ci.nsIPropertyBag2,
+ "Subject needs to be a nsIPropertyBag2 to clean up properly"
+ );
+ is(
+ parseInt(data, 10),
+ utilityPid,
+ `Should match the crashed PID ${utilityPid} with ${data}`
+ );
+
+ // Make sure the process is dead, otherwise there is a risk of race for
+ // writing leak logs
+ utilityProcessTest().noteIntentionalCrash(utilityPid);
+
+ ok(!subject.hasKey("dumpID"), "There should be no dumpID");
+}
+
+async function killUtilityProcesses() {
+ let utilityProcesses = await getUtilityProcesses();
+ for (const utilityProcess of utilityProcesses) {
+ for (const actor of utilityProcess.utilityActors) {
+ info(`Stopping ${actor.actorName} utility process`);
+ await cleanUtilityProcessShutdown(actor.actorName, /* preferKill */ true);
+ }
+ }
+}
+
+function audioTestData() {
+ return [
+ {
+ src: "small-shot.ogg",
+ expectations: {
+ Android: {
+ process: "Utility Generic",
+ decoder: "vorbis audio decoder",
+ },
+ Linux: {
+ process: "Utility Generic",
+ decoder: "vorbis audio decoder",
+ },
+ WINNT: {
+ process: "Utility Generic",
+ decoder: "vorbis audio decoder",
+ },
+ Darwin: {
+ process: "Utility Generic",
+ decoder: "vorbis audio decoder",
+ },
+ },
+ },
+ {
+ src: "small-shot.mp3",
+ expectations: {
+ Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" },
+ Linux: {
+ process: "Utility Generic",
+ decoder: "ffvpx audio decoder",
+ },
+ WINNT: {
+ process: SpecialPowers.getBoolPref("media.ffvpx.mp3.enabled")
+ ? "Utility Generic"
+ : "Utility WMF",
+ decoder: SpecialPowers.getBoolPref("media.ffvpx.mp3.enabled")
+ ? "ffvpx audio decoder"
+ : "wmf audio decoder",
+ },
+ Darwin: {
+ process: SpecialPowers.getBoolPref("media.ffvpx.mp3.enabled")
+ ? "Utility Generic"
+ : "Utility AppleMedia",
+ decoder: SpecialPowers.getBoolPref("media.ffvpx.mp3.enabled")
+ ? "ffvpx audio decoder"
+ : "apple coremedia decoder",
+ },
+ },
+ },
+ {
+ src: "small-shot.m4a",
+ expectations: {
+ // Add Android after Bug 1771196
+ Linux: {
+ process: "Utility Generic",
+ decoder: "ffmpeg audio decoder",
+ },
+ WINNT: {
+ process: "Utility WMF",
+ decoder: "wmf audio decoder",
+ },
+ Darwin: {
+ process: "Utility AppleMedia",
+ decoder: "apple coremedia decoder",
+ },
+ },
+ },
+ {
+ src: "small-shot.flac",
+ expectations: {
+ Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" },
+ Linux: {
+ process: "Utility Generic",
+ decoder: "ffvpx audio decoder",
+ },
+ WINNT: {
+ process: "Utility Generic",
+ decoder: "ffvpx audio decoder",
+ },
+ Darwin: {
+ process: "Utility Generic",
+ decoder: "ffvpx audio decoder",
+ },
+ },
+ },
+ ];
+}
+
+async function addMediaTab(src) {
+ const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ forceNewProcess: true,
+ });
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ await SpecialPowers.spawn(browser, [src], createAudioElement);
+ return tab;
+}
+
+async function play(
+ tab,
+ expectUtility,
+ expectDecoder,
+ expectContent = false,
+ expectJava = false
+) {
+ let browser = tab.linkedBrowser;
+ return SpecialPowers.spawn(
+ browser,
+ [expectUtility, expectDecoder, expectContent, expectJava],
+ checkAudioDecoder
+ );
+}
+
+async function stop(tab) {
+ let browser = tab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [], async function () {
+ let audio = content.document.querySelector("audio");
+ audio.pause();
+ });
+}
+
+async function createAudioElement(src) {
+ const doc = typeof content !== "undefined" ? content.document : document;
+ const ROOT = "https://example.com/browser/ipc/glue/test/browser";
+ let audio = doc.createElement("audio");
+ audio.setAttribute("controls", "true");
+ audio.setAttribute("loop", true);
+ audio.src = `${ROOT}/${src}`;
+ doc.body.appendChild(audio);
+}
+
+async function checkAudioDecoder(
+ expectedProcess,
+ expectedDecoder,
+ expectContent = false,
+ expectJava = false
+) {
+ const doc = typeof content !== "undefined" ? content.document : document;
+ let audio = doc.querySelector("audio");
+ const checkPromise = new Promise((resolve, reject) => {
+ const timeUpdateHandler = async ev => {
+ const debugInfo = await SpecialPowers.wrap(audio).mozRequestDebugInfo();
+ const audioDecoderName = debugInfo.decoder.reader.audioDecoderName;
+
+ const isExpectedDecoder =
+ audioDecoderName.indexOf(`${expectedDecoder}`) == 0;
+ ok(
+ isExpectedDecoder,
+ `playback ${audio.src} was from decoder '${audioDecoderName}', expected '${expectedDecoder}'`
+ );
+
+ const isExpectedProcess =
+ audioDecoderName.indexOf(`(${expectedProcess} remote)`) > 0;
+ const isJavaRemote = audioDecoderName.indexOf("(remote)") > 0;
+ const isOk =
+ (isExpectedProcess && !isJavaRemote && !expectContent && !expectJava) || // Running in Utility/RDD
+ (expectJava && !isExpectedProcess && isJavaRemote) || // Running in Java remote
+ (expectContent && !isExpectedProcess && !isJavaRemote); // Running in Content
+
+ ok(
+ isOk,
+ `playback ${audio.src} was from process '${audioDecoderName}', expected '${expectedProcess}'`
+ );
+
+ if (isOk) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ const startPlaybackHandler = async ev => {
+ ok(
+ await audio.play().then(
+ _ => true,
+ _ => false
+ ),
+ "audio started playing"
+ );
+
+ audio.addEventListener("timeupdate", timeUpdateHandler, { once: true });
+ };
+
+ audio.addEventListener("canplaythrough", startPlaybackHandler, {
+ once: true,
+ });
+ });
+
+ // We need to make sure the decoder is ready before play()ing otherwise we
+ // could get into bad situations
+ audio.load();
+ return checkPromise;
+}
+
+async function runMochitestUtilityAudio(
+ src,
+ {
+ expectUtility,
+ expectDecoder,
+ expectContent = false,
+ expectJava = false,
+ } = {}
+) {
+ info(`Add media: ${src}`);
+ await createAudioElement(src);
+ let audio = document.querySelector("audio");
+ ok(audio, "Found an audio element created");
+
+ info(`Play media: ${src}`);
+ await checkAudioDecoder(
+ expectUtility,
+ expectDecoder,
+ expectContent,
+ expectJava
+ );
+
+ info(`Pause media: ${src}`);
+ await audio.pause();
+
+ info(`Remove media: ${src}`);
+ document.body.removeChild(audio);
+}
+
+async function crashSomeUtility(utilityPid, actorsCheck) {
+ SimpleTest.expectChildProcessCrash();
+
+ const crashMan = Services.crashmanager;
+ const utilityProcessGone = TestUtils.topicObserved(
+ "ipc:utility-shutdown",
+ (subject, data) => {
+ info(`ipc:utility-shutdown: data=${data} subject=${subject}`);
+ return parseInt(data, 10) === utilityPid;
+ }
+ );
+
+ info("prune any previous crashes");
+ const future = new Date(Date.now() + 1000 * 60 * 60 * 24);
+ await crashMan.pruneOldCrashes(future);
+
+ info("crash Utility Process");
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+
+ info(`Crash Utility Process ${utilityPid}`);
+ ProcessTools.crash(utilityPid);
+
+ info(`Waiting for utility process ${utilityPid} to go away.`);
+ let [subject, data] = await utilityProcessGone;
+ ok(
+ parseInt(data, 10) === utilityPid,
+ `Should match the crashed PID ${utilityPid} with ${data}`
+ );
+ ok(
+ subject instanceof Ci.nsIPropertyBag2,
+ "Subject needs to be a nsIPropertyBag2 to clean up properly"
+ );
+
+ // Make sure the process is dead, otherwise there is a risk of race for
+ // writing leak logs
+ utilityProcessTest().noteIntentionalCrash(utilityPid);
+
+ const dumpID = subject.getPropertyAsAString("dumpID");
+ ok(dumpID, "There should be a dumpID");
+
+ await crashMan.ensureCrashIsPresent(dumpID);
+ await crashMan.getCrashes().then(crashes => {
+ is(crashes.length, 1, "There should be only one record");
+ const crash = crashes[0];
+ ok(
+ crash.isOfType(
+ crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY],
+ crashMan.CRASH_TYPE_CRASH
+ ),
+ "Record should be a utility process crash"
+ );
+ ok(crash.id === dumpID, "Record should have an ID");
+ ok(
+ actorsCheck(crash.metadata.UtilityActorsName),
+ `Record should have the correct actors name for: ${crash.metadata.UtilityActorsName}`
+ );
+ });
+
+ let minidumpDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ minidumpDirectory.append("minidumps");
+
+ let dumpfile = minidumpDirectory.clone();
+ dumpfile.append(dumpID + ".dmp");
+ if (dumpfile.exists()) {
+ info(`Removal of ${dumpfile.path}`);
+ dumpfile.remove(false);
+ }
+
+ let extrafile = minidumpDirectory.clone();
+ extrafile.append(dumpID + ".extra");
+ info(`Removal of ${extrafile.path}`);
+ if (extrafile.exists()) {
+ extrafile.remove(false);
+ }
+}
+
+// Crash a utility process and generate a crash dump. To close a utility
+// process (forcefully or not) without a generating a crash, use
+// cleanUtilityProcessShutdown.
+async function crashSomeUtilityActor(
+ actor,
+ actorsCheck = () => {
+ return true;
+ }
+) {
+ // Get PID for utility type
+ const procInfos = await getUtilityProcesses(actor);
+ ok(
+ procInfos.length == 1,
+ `exactly one ${actor} utility process should be found`
+ );
+ const utilityPid = procInfos[0].pid;
+ return crashSomeUtility(utilityPid, actorsCheck);
+}
diff --git a/ipc/glue/test/browser/mochitest_audio_off.ini b/ipc/glue/test/browser/mochitest_audio_off.ini
new file mode 100644
index 0000000000..1e105c5f87
--- /dev/null
+++ b/ipc/glue/test/browser/mochitest_audio_off.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+run-if = toolkit == 'android' && !isolated_process # Bug 1771452
+support-files =
+ head.js
+ ../../../../dom/media/test/small-shot.ogg
+ ../../../../dom/media/test/small-shot.mp3
+ ../../../../dom/media/test/small-shot.m4a
+ ../../../../dom/media/test/small-shot.flac
+prefs =
+ media.utility-process.enabled=false
+
+[test_utility_audio_off.html]
diff --git a/ipc/glue/test/browser/mochitest_audio_on.ini b/ipc/glue/test/browser/mochitest_audio_on.ini
new file mode 100644
index 0000000000..8d74572341
--- /dev/null
+++ b/ipc/glue/test/browser/mochitest_audio_on.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+run-if = toolkit == 'android' && !isolated_process # Bug 1771452
+support-files =
+ head.js
+ ../../../../dom/media/test/small-shot.ogg
+ ../../../../dom/media/test/small-shot.mp3
+ ../../../../dom/media/test/small-shot.m4a
+ ../../../../dom/media/test/small-shot.flac
+prefs =
+ media.utility-process.enabled=true
+
+[test_utility_audio_on.html]
diff --git a/ipc/glue/test/browser/moz.build b/ipc/glue/test/browser/moz.build
new file mode 100644
index 0000000000..d0a85bebcd
--- /dev/null
+++ b/ipc/glue/test/browser/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser.ini",
+ "browser_audio_shutdown.ini",
+ "browser_child_hang.ini",
+]
+MOCHITEST_MANIFESTS += ["mochitest_audio_off.ini", "mochitest_audio_on.ini"]
diff --git a/ipc/glue/test/browser/test_utility_audio_off.html b/ipc/glue/test/browser/test_utility_audio_off.html
new file mode 100644
index 0000000000..7df18ed383
--- /dev/null
+++ b/ipc/glue/test/browser/test_utility_audio_off.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Audio decoder not in Utility process</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+(async function() {
+ const platform = SpecialPowers.Services.appinfo.OS;
+ for (let {src, expectations} of audioTestData()) {
+ if (!(platform in expectations)) {
+ info(`Skipping ${src} for ${platform}`);
+ continue;
+ }
+
+ try {
+ await runMochitestUtilityAudio(src, { expectUtility: "", expectDecoder: expectations[platform].decoder, expectContent: true, expectJava: false });
+ } catch (ex) {
+ ok(false, "Failure");
+ }
+ }
+
+ for (let src of [
+ "small-shot.m4a",
+ ]) {
+ try {
+ await runMochitestUtilityAudio(src, { expectUtility: "", expectDecoder: "android decoder", expectContent: false, expectJava: true });
+ } catch (ex) {
+ ok(false, `Failure ${ex}`);
+ }
+ }
+
+ SimpleTest.finish();
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/ipc/glue/test/browser/test_utility_audio_on.html b/ipc/glue/test/browser/test_utility_audio_on.html
new file mode 100644
index 0000000000..f473527520
--- /dev/null
+++ b/ipc/glue/test/browser/test_utility_audio_on.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Audio decoder in Utility process</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+(async function() {
+ const platform = SpecialPowers.Services.appinfo.OS;
+ for (let {src, expectations} of audioTestData()) {
+ if (!(platform in expectations)) {
+ info(`Skipping ${src} for ${platform}`);
+ continue;
+ }
+
+ try {
+ await runMochitestUtilityAudio(src, { expectUtility: expectations[platform].process, expectDecoder: expectations[platform].decoder, expectContent: false, expectJava: false });
+ } catch (ex) {
+ ok(false, "Failure");
+ }
+ }
+
+ // Remove all after Bug 1771196
+ for (let src of [
+ "small-shot.m4a",
+ ]) {
+ try {
+ await runMochitestUtilityAudio(src, { expectUtility: "", expectDecoder: "android decoder", expectContent: false, expectJava: true });
+ } catch (ex) {
+ ok(false, `Failure ${ex}`);
+ }
+ }
+
+ SimpleTest.finish();
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/ipc/glue/test/gtest/TestAsyncBlockers.cpp b/ipc/glue/test/gtest/TestAsyncBlockers.cpp
new file mode 100644
index 0000000000..6f8d298621
--- /dev/null
+++ b/ipc/glue/test/gtest/TestAsyncBlockers.cpp
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/ipc/AsyncBlockers.h"
+#include "mozilla/gtest/MozHelpers.h"
+
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsINamed.h"
+
+using namespace mozilla;
+using namespace mozilla::ipc;
+
+#define PROCESS_EVENTS_UNTIL(_done) \
+ SpinEventLoopUntil("TestAsyncBlockers"_ns, [&]() { return _done; });
+
+class TestAsyncBlockers : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ SAVE_GDB_SLEEP(mOldSleepDuration);
+ return;
+ }
+
+ void TearDown() final { RESTORE_GDB_SLEEP(mOldSleepDuration); }
+
+ private:
+#if defined(HAS_GDB_SLEEP_DURATION)
+ unsigned int mOldSleepDuration = 0;
+#endif // defined(HAS_GDB_SLEEP_DURATION)
+};
+
+class Blocker {};
+
+TEST_F(TestAsyncBlockers, Register) {
+ AsyncBlockers blockers;
+ Blocker* blocker = new Blocker();
+ blockers.Register(blocker);
+ EXPECT_TRUE(true);
+}
+
+TEST_F(TestAsyncBlockers, Register_Deregister) {
+ AsyncBlockers blockers;
+ Blocker* blocker = new Blocker();
+ blockers.Register(blocker);
+ blockers.Deregister(blocker);
+ EXPECT_TRUE(true);
+}
+
+TEST_F(TestAsyncBlockers, Register_WaitUntilClear) {
+ AsyncBlockers blockers;
+ bool done = false;
+
+ Blocker* blocker = new Blocker();
+ blockers.Register(blocker);
+
+ blockers.WaitUntilClear(5 * 1000)->Then(GetCurrentSerialEventTarget(),
+ __func__, [&]() {
+ EXPECT_TRUE(true);
+ done = true;
+ });
+
+ NS_ProcessPendingEvents(nullptr);
+
+ blockers.Deregister(blocker);
+
+ PROCESS_EVENTS_UNTIL(done);
+}
+
+class AsyncBlockerTimerCallback : public nsITimerCallback, public nsINamed {
+ protected:
+ virtual ~AsyncBlockerTimerCallback();
+
+ public:
+ explicit AsyncBlockerTimerCallback() {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+};
+
+NS_IMPL_ISUPPORTS(AsyncBlockerTimerCallback, nsITimerCallback, nsINamed)
+
+AsyncBlockerTimerCallback::~AsyncBlockerTimerCallback() = default;
+
+NS_IMETHODIMP
+AsyncBlockerTimerCallback::Notify(nsITimer* timer) {
+ // If we resolve through this, it means
+ // blockers.WaitUntilClear() started to wait for
+ // the completion of the timeout which is not
+ // good.
+ EXPECT_TRUE(false);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AsyncBlockerTimerCallback::GetName(nsACString& aName) {
+ aName.AssignLiteral("AsyncBlockerTimerCallback");
+ return NS_OK;
+}
+
+TEST_F(TestAsyncBlockers, NoRegister_WaitUntilClear) {
+ AsyncBlockers blockers;
+ bool done = false;
+
+ nsCOMPtr<nsITimer> timer = NS_NewTimer();
+ ASSERT_TRUE(timer);
+
+ RefPtr<AsyncBlockerTimerCallback> timerCb = new AsyncBlockerTimerCallback();
+ timer->InitWithCallback(timerCb, 1 * 1000, nsITimer::TYPE_ONE_SHOT);
+
+ blockers.WaitUntilClear(10 * 1000)->Then(GetCurrentSerialEventTarget(),
+ __func__, [&]() {
+ // If we resolve through this
+ // before the nsITimer it means we
+ // have been resolved before the 5s
+ // timeout
+ EXPECT_TRUE(true);
+ timer->Cancel();
+ done = true;
+ });
+
+ PROCESS_EVENTS_UNTIL(done);
+}
+
+TEST_F(TestAsyncBlockers, Register_WaitUntilClear_0s) {
+ AsyncBlockers blockers;
+ bool done = false;
+
+ Blocker* blocker = new Blocker();
+ blockers.Register(blocker);
+
+ blockers.WaitUntilClear(0)->Then(GetCurrentSerialEventTarget(), __func__,
+ [&]() {
+ EXPECT_TRUE(true);
+ done = true;
+ });
+
+ NS_ProcessPendingEvents(nullptr);
+
+ blockers.Deregister(blocker);
+
+ PROCESS_EVENTS_UNTIL(done);
+}
+
+#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && !defined(ANDROID) && \
+ !(defined(XP_DARWIN) && !defined(MOZ_DEBUG))
+static void DeregisterEmpty_Test() {
+ mozilla::gtest::DisableCrashReporter();
+
+ AsyncBlockers blockers;
+ Blocker* blocker = new Blocker();
+ blockers.Deregister(blocker);
+}
+
+TEST_F(TestAsyncBlockers, DeregisterEmpty) {
+ ASSERT_DEATH_IF_SUPPORTED(DeregisterEmpty_Test(), "");
+}
+#endif // defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && !defined(ANDROID) &&
+ // !(defined(XP_DARWIN) && !defined(MOZ_DEBUG))
+
+#undef PROCESS_EVENTS_UNTIL
diff --git a/ipc/glue/test/gtest/TestUtilityProcess.cpp b/ipc/glue/test/gtest/TestUtilityProcess.cpp
new file mode 100644
index 0000000000..c5d19c992f
--- /dev/null
+++ b/ipc/glue/test/gtest/TestUtilityProcess.cpp
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/SpinEventLoopUntil.h"
+
+#include "mozilla/ipc/UtilityProcessManager.h"
+
+#if defined(MOZ_WIDGET_ANDROID) || defined(XP_MACOSX)
+# include "nsIAppShellService.h"
+# include "nsServiceManagerUtils.h"
+#endif // defined(MOZ_WIDGET_ANDROID) || defined(XP_MACOSX)
+
+#if defined(XP_WIN)
+# include "mozilla/gtest/MozHelpers.h"
+# include "mozilla/ipc/UtilityProcessImpl.h"
+#endif // defined(XP_WIN)
+
+#ifdef MOZ_WIDGET_ANDROID
+# define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/widget/appshell/android;1"
+#endif // MOZ_WIDGET_ANDROID
+
+#ifdef XP_MACOSX
+# define NS_APPSHELLSERVICE_CONTRACTID "@mozilla.org/widget/appshell/mac;1"
+#endif // XP_MACOSX
+
+using namespace mozilla;
+using namespace mozilla::ipc;
+
+#define WAIT_FOR_EVENTS \
+ SpinEventLoopUntil("UtilityProcess::emptyUtil"_ns, [&]() { return done; });
+
+bool setupDone = false;
+
+class UtilityProcess : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ if (setupDone) {
+ return;
+ }
+
+#if defined(MOZ_WIDGET_ANDROID) || defined(XP_MACOSX)
+ appShell = do_GetService(NS_APPSHELLSERVICE_CONTRACTID);
+#endif // defined(MOZ_WIDGET_ANDROID) || defined(XP_MACOSX)
+
+#if defined(XP_WIN) && defined(MOZ_SANDBOX)
+ mozilla::SandboxBroker::GeckoDependentInitialize();
+#endif // defined(XP_WIN) && defined(MOZ_SANDBOX)
+
+ setupDone = true;
+ }
+
+#if defined(MOZ_WIDGET_ANDROID) || defined(XP_MACOSX)
+ nsCOMPtr<nsIAppShellService> appShell;
+#endif // defined(MOZ_WIDGET_ANDROID) || defined(XP_MACOSX)
+};
+
+TEST_F(UtilityProcess, ProcessManager) {
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+ ASSERT_NE(utilityProc, nullptr);
+}
+
+TEST_F(UtilityProcess, NoProcess) {
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+ EXPECT_NE(utilityProc, nullptr);
+
+ Maybe<int32_t> noPid =
+ utilityProc->ProcessPid(SandboxingKind::GENERIC_UTILITY);
+ ASSERT_TRUE(noPid.isNothing());
+}
+
+TEST_F(UtilityProcess, LaunchProcess) {
+ bool done = false;
+
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+ EXPECT_NE(utilityProc, nullptr);
+
+ int32_t thisPid = base::GetCurrentProcId();
+ EXPECT_GE(thisPid, 1);
+
+ utilityProc->LaunchProcess(SandboxingKind::GENERIC_UTILITY)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&]() mutable {
+ EXPECT_TRUE(true);
+
+ Maybe<int32_t> utilityPid =
+ utilityProc->ProcessPid(SandboxingKind::GENERIC_UTILITY);
+ EXPECT_TRUE(utilityPid.isSome());
+ EXPECT_GE(*utilityPid, 1);
+ EXPECT_NE(*utilityPid, thisPid);
+
+ printf_stderr("UtilityProcess running as %d\n", *utilityPid);
+
+ done = true;
+ },
+ [&](nsresult aError) mutable {
+ EXPECT_TRUE(false);
+ done = true;
+ });
+
+ WAIT_FOR_EVENTS;
+}
+
+TEST_F(UtilityProcess, DestroyProcess) {
+ bool done = false;
+
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+
+ utilityProc->LaunchProcess(SandboxingKind::GENERIC_UTILITY)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&]() {
+ Maybe<int32_t> utilityPid =
+ utilityProc->ProcessPid(SandboxingKind::GENERIC_UTILITY);
+ EXPECT_TRUE(utilityPid.isSome());
+ EXPECT_GE(*utilityPid, 1);
+
+ utilityProc->CleanShutdown(SandboxingKind::GENERIC_UTILITY);
+
+ utilityPid =
+ utilityProc->ProcessPid(SandboxingKind::GENERIC_UTILITY);
+ EXPECT_TRUE(utilityPid.isNothing());
+
+ EXPECT_TRUE(true);
+ done = true;
+ },
+ [&](nsresult aError) {
+ EXPECT_TRUE(false);
+ done = true;
+ });
+
+ WAIT_FOR_EVENTS;
+}
+
+#if defined(XP_WIN)
+static void LoadLibraryCrash_Test() {
+ mozilla::gtest::DisableCrashReporter();
+ // Just a uuidgen name to have something random
+ UtilityProcessImpl::LoadLibraryOrCrash(
+ L"2b49036e-6ba3-400c-a297-38fa1f6c5255.dll");
+}
+
+TEST_F(UtilityProcess, LoadLibraryCrash) {
+ ASSERT_DEATH_IF_SUPPORTED(LoadLibraryCrash_Test(), "");
+}
+#endif // defined(XP_WIN)
+
+#undef WAIT_FOR_EVENTS
diff --git a/ipc/glue/test/gtest/moz.build b/ipc/glue/test/gtest/moz.build
new file mode 100644
index 0000000000..7b72cc7fe1
--- /dev/null
+++ b/ipc/glue/test/gtest/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Library("ipcgluetest")
+
+UNIFIED_SOURCES = [
+ "TestAsyncBlockers.cpp",
+ "TestUtilityProcess.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/widget",
+ "/widget/android",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/ipc/glue/test/utility_process_xpcom/UtilityProcessTest.cpp b/ipc/glue/test/utility_process_xpcom/UtilityProcessTest.cpp
new file mode 100644
index 0000000000..3828c6971b
--- /dev/null
+++ b/ipc/glue/test/utility_process_xpcom/UtilityProcessTest.cpp
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#if defined(ENABLE_TESTS)
+# include "mozilla/ipc/UtilityProcessTest.h"
+# include "mozilla/ipc/UtilityProcessManager.h"
+# include "mozilla/dom/Promise.h"
+# include "mozilla/ProcInfo.h"
+# include "mozilla/IntentionalCrash.h"
+
+namespace mozilla::ipc {
+
+static UtilityActorName UtilityActorNameFromString(
+ const nsACString& aStringName) {
+ using namespace mozilla::dom;
+
+ // We use WebIDLUtilityActorNames because UtilityActorNames is not designed
+ // for iteration.
+ for (size_t i = 0; i < WebIDLUtilityActorNameValues::Count; ++i) {
+ auto idlName = static_cast<UtilityActorName>(i);
+ const nsDependentCSubstring idlNameString(
+ WebIDLUtilityActorNameValues::GetString(idlName));
+ if (idlNameString.Equals(aStringName)) {
+ return idlName;
+ }
+ }
+ MOZ_CRASH("Unknown utility actor name");
+}
+
+// Find the utility process with the given actor or any utility process if
+// the actor is UtilityActorName::EndGuard_.
+static SandboxingKind FindUtilityProcessWithActor(UtilityActorName aActorName) {
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+ MOZ_ASSERT(utilityProc, "No UtilityprocessManager?");
+
+ for (size_t i = 0; i < SandboxingKind::COUNT; ++i) {
+ auto sbKind = static_cast<SandboxingKind>(i);
+ if (!utilityProc->Process(sbKind)) {
+ continue;
+ }
+ if (aActorName == UtilityActorName::EndGuard_) {
+ return sbKind;
+ }
+ for (auto actor : utilityProc->GetActors(sbKind)) {
+ if (actor == aActorName) {
+ return sbKind;
+ }
+ }
+ }
+
+ return SandboxingKind::COUNT;
+}
+
+NS_IMETHODIMP
+UtilityProcessTest::StartProcess(const nsTArray<nsCString>& aActorsToRegister,
+ JSContext* aCx,
+ mozilla::dom::Promise** aOutPromise) {
+ NS_ENSURE_ARG(aOutPromise);
+ *aOutPromise = nullptr;
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<dom::Promise> promise = dom::Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+ MOZ_ASSERT(utilityProc, "No UtilityprocessManager?");
+
+ auto actors = aActorsToRegister.Clone();
+
+ utilityProc->LaunchProcess(SandboxingKind::GENERIC_UTILITY)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [promise, utilityProc, actors = std::move(actors)] {
+ RefPtr<UtilityProcessParent> utilityParent =
+ utilityProc->GetProcessParent(SandboxingKind::GENERIC_UTILITY);
+ Maybe<int32_t> utilityPid =
+ utilityProc->ProcessPid(SandboxingKind::GENERIC_UTILITY);
+ for (size_t i = 0; i < actors.Length(); ++i) {
+ auto uan = UtilityActorNameFromString(actors[i]);
+ utilityProc->RegisterActor(utilityParent, uan);
+ }
+ if (utilityPid.isSome()) {
+ promise->MaybeResolve(*utilityPid);
+ } else {
+ promise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
+ }
+ },
+ [promise](nsresult aError) {
+ MOZ_ASSERT_UNREACHABLE(
+ "UtilityProcessTest; failure to get Utility process");
+ promise->MaybeReject(aError);
+ });
+
+ promise.forget(aOutPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UtilityProcessTest::NoteIntentionalCrash(uint32_t aPid) {
+ mozilla::NoteIntentionalCrash("utility", aPid);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UtilityProcessTest::StopProcess(const char* aActorName) {
+ using namespace mozilla::dom;
+
+ SandboxingKind sbKind;
+ if (aActorName) {
+ const nsDependentCString actorStringName(aActorName);
+ UtilityActorName actorName = UtilityActorNameFromString(actorStringName);
+ sbKind = FindUtilityProcessWithActor(actorName);
+ } else {
+ sbKind = FindUtilityProcessWithActor(UtilityActorName::EndGuard_);
+ }
+
+ if (sbKind == SandboxingKind::COUNT) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Attempted to stop process for actor when no "
+ "such process exists");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+ MOZ_ASSERT(utilityProc, "No UtilityprocessManager?");
+
+ utilityProc->CleanShutdown(sbKind);
+ Maybe<int32_t> utilityPid = utilityProc->ProcessPid(sbKind);
+ MOZ_RELEASE_ASSERT(utilityPid.isNothing(),
+ "Should not have a utility process PID anymore");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UtilityProcessTest::TestTelemetryProbes() {
+ RefPtr<UtilityProcessManager> utilityProc =
+ UtilityProcessManager::GetSingleton();
+ MOZ_ASSERT(utilityProc, "No UtilityprocessManager?");
+
+ for (RefPtr<UtilityProcessParent>& parent :
+ utilityProc->GetAllProcessesProcessParent()) {
+ Unused << parent->SendTestTelemetryProbes();
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(UtilityProcessTest, nsIUtilityProcessTest)
+
+} // namespace mozilla::ipc
+#endif // defined(ENABLE_TESTS)
diff --git a/ipc/glue/test/utility_process_xpcom/UtilityProcessTest.h b/ipc/glue/test/utility_process_xpcom/UtilityProcessTest.h
new file mode 100644
index 0000000000..6c80fce71b
--- /dev/null
+++ b/ipc/glue/test/utility_process_xpcom/UtilityProcessTest.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+#ifndef _include_ipc_glue_UtilityProcessTest_h_
+#define _include_ipc_glue_UtilityProcessTest_h_
+
+#if defined(ENABLE_TESTS)
+# include "nsServiceManagerUtils.h"
+# include "nsIUtilityProcessTest.h"
+
+namespace mozilla::ipc {
+
+class UtilityProcessTest final : public nsIUtilityProcessTest {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIUTILITYPROCESSTEST
+
+ UtilityProcessTest() = default;
+
+ private:
+ ~UtilityProcessTest() = default;
+};
+
+} // namespace mozilla::ipc
+#endif // defined(ENABLE_TESTS)
+
+#endif // _include_ipc_glue_UtilityProcessTest_h_
diff --git a/ipc/glue/test/utility_process_xpcom/components.conf b/ipc/glue/test/utility_process_xpcom/components.conf
new file mode 100644
index 0000000000..25208ba7fc
--- /dev/null
+++ b/ipc/glue/test/utility_process_xpcom/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{0a4478f4-c5ae-4fb1-8686-d5b09fb99afb}',
+ 'contract_ids': ['@mozilla.org/utility-process-test;1'],
+ 'type': 'mozilla::ipc::UtilityProcessTest',
+ 'headers': ['mozilla/ipc/UtilityProcessTest.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
diff --git a/ipc/glue/test/utility_process_xpcom/moz.build b/ipc/glue/test/utility_process_xpcom/moz.build
new file mode 100644
index 0000000000..f04b436cbe
--- /dev/null
+++ b/ipc/glue/test/utility_process_xpcom/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS.mozilla.ipc += ["UtilityProcessTest.h"]
+
+UNIFIED_SOURCES += ["UtilityProcessTest.cpp"]
+
+XPCOM_MANIFESTS += ["components.conf"]
+
+XPIDL_MODULE = "utility_process_xpcom_test"
+
+XPIDL_SOURCES += [
+ "nsIUtilityProcessTest.idl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/ipc/glue/test/utility_process_xpcom/nsIUtilityProcessTest.idl b/ipc/glue/test/utility_process_xpcom/nsIUtilityProcessTest.idl
new file mode 100644
index 0000000000..839c4f3673
--- /dev/null
+++ b/ipc/glue/test/utility_process_xpcom/nsIUtilityProcessTest.idl
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(0a4478f4-c5ae-4fb1-8686-d5b09fb99afb)]
+interface nsIUtilityProcessTest : nsISupports
+{
+ /**
+ * ** Test-only Method **
+ *
+ * Allowing to start Utility Process from JS code.
+ *
+ * actorsToAdd: An array of actor names, taken from WebIDLUtilityActorName.
+ * Unlike normal utility processes, test processes launched this way do not
+ * have any associated actor names unless specified here. Empty by default.
+ */
+ [implicit_jscontext]
+ Promise startProcess([optional] in Array<ACString> actorsToAdd);
+
+ /**
+ * ** Test-only Method **
+ *
+ * Note that we are going to manually crash a process
+ */
+ void noteIntentionalCrash(in unsigned long pid);
+
+ /**
+ * ** Test-only Method **
+ *
+ * Allowing to stop Utility Process from JS code.
+ * Default behavior is to stop any utility process.
+ */
+ void stopProcess([optional] in string utilityActorName);
+
+ /**
+ * ** Test-only Method **
+ *
+ * Sending Telemetry probes
+ */
+ void testTelemetryProbes();
+};