diff options
Diffstat (limited to 'ipc/glue/test/browser')
33 files changed, 2148 insertions, 0 deletions
diff --git a/ipc/glue/test/browser/browser.toml b/ipc/glue/test/browser/browser.toml new file mode 100644 index 0000000000..8f94bc130d --- /dev/null +++ b/ipc/glue/test/browser/browser.toml @@ -0,0 +1,94 @@ +[DEFAULT] +support-files = ["head.js"] +# Set this since we want to continue monitoring the disabling of pref since we +# still allow it a little bit. +environment = "MOZ_DONT_LOCK_UTILITY_PLZ_FILE_A_BUG=1" + +["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_audio_telemetry_utility_EME.js"] +support-files = [ + "head-telemetry.js", + "../../../../dom/media/test/eme_standalone.js", + "../../../../dom/media/test/short-aac-encrypted-audio.mp4" +] + +["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_filepicker_crashed.js"] +run-if = ["os == 'win'"] +skip-if = [ + "!crashreporter", + "ccov", +] + +["browser_utility_geolocation_crashed.js"] +run-if = ["os == 'win'"] +skip-if = [ + "!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", + "head-multiple.js" +] + +["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_fallback.toml b/ipc/glue/test/browser/browser_audio_fallback.toml new file mode 100644 index 0000000000..0de2a1c9e7 --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_fallback.toml @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = [ + "head.js", + "head-multiple.js", +] +prefs = ["media.allow-audio-non-utility=true"] +# Set this since we want to continue monitoring the disabling of pref since we +# still allow it a little bit. +environment = "MOZ_DONT_LOCK_UTILITY_PLZ_FILE_A_BUG=1" + +["browser_utility_multipleAudio_fallback.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", +] diff --git a/ipc/glue/test/browser/browser_audio_fallback_content.toml b/ipc/glue/test/browser/browser_audio_fallback_content.toml new file mode 100644 index 0000000000..3efc6409ac --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_fallback_content.toml @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = [ + "head.js", + "head-multiple.js", +] +prefs = [ + "media.allow-audio-non-utility=true", + "media.rdd-process.enabled=false", +] + +["browser_utility_multipleAudio_fallback_content.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", +] diff --git a/ipc/glue/test/browser/browser_audio_locked.toml b/ipc/glue/test/browser/browser_audio_locked.toml new file mode 100644 index 0000000000..9f0607bf5f --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_locked.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["browser_utility_audio_locked.js"] diff --git a/ipc/glue/test/browser/browser_audio_shutdown.toml b/ipc/glue/test/browser/browser_audio_shutdown.toml new file mode 100644 index 0000000000..f99fff7830 --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_shutdown.toml @@ -0,0 +1,5 @@ +[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..89f5126794 --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_telemetry_content.js @@ -0,0 +1,39 @@ +/* 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(); + await SpecialPowers.pushPrefEnv({ + set: [["media.allow-audio-non-utility", true]], + }); +}); + +add_task(async function testAudioDecodingInContent() { + await runTest({ expectUtility: false, expectRDD: false }); +}); + +add_task(async function testContentTelemetry() { + const codecs = ["vorbis", "mp3", "aac", "flac"]; + const extraKey = getExtraKey({ + rddPref: false, + utilityPref: false, + allowNonUtility: true, + }); + 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..ec0944303b --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_telemetry_rdd.js @@ -0,0 +1,36 @@ +/* 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(); + await SpecialPowers.pushPrefEnv({ + set: [["media.allow-audio-non-utility", true]], + }); +}); + +add_task(async function testAudioDecodingInRDD() { + await runTest({ expectUtility: false, expectRDD: true }); +}); + +add_task(async function testRDDTelemetry() { + const extraKey = getExtraKey({ + rddPref: true, + utilityPref: false, + allowNonUtility: true, + }); + 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..e121c89049 --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_telemetry_utility.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 testAudioDecodingInUtility() { + await runTest({ expectUtility: true, expectRDD: true }); +}); + +add_task(async function testUtilityTelemetry() { + const platform = Services.appinfo.OS; + const extraKey = getExtraKey({ rddPref: true, utilityPref: true }); + for (let exp of utilityPerCodecs[platform]) { + await verifyTelemetryForProcess(exp.process, exp.codecs, extraKey); + } + await verifyNoTelemetryForProcess( + "rdd", + ["vorbis", "mp3", "aac", "flac"], + extraKey + ); +}); diff --git a/ipc/glue/test/browser/browser_audio_telemetry_utility_EME.js b/ipc/glue/test/browser/browser_audio_telemetry_utility_EME.js new file mode 100644 index 0000000000..7d2e9a4e78 --- /dev/null +++ b/ipc/glue/test/browser/browser_audio_telemetry_utility_EME.js @@ -0,0 +1,31 @@ +/* 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 +); + +SimpleTest.requestCompleteLog(); + +add_setup(async function testNoTelemetry() { + await Telemetry.clearScalars(); +}); + +add_task(async function testAudioDecodingInUtility() { + await runTestWithEME(); +}); + +add_task(async function testUtilityTelemetry() { + const platform = Services.appinfo.OS; + const extraKey = getExtraKey({ rddPref: true, utilityPref: true }); + for (let exp of utilityPerCodecs[platform]) { + if (exp.codecs.includes("aac")) { + await verifyTelemetryForProcess(exp.process, ["aac"], extraKey); + } + } +}); 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_child_hang.toml b/ipc/glue/test/browser/browser_child_hang.toml new file mode 100644 index 0000000000..ddc8b95670 --- /dev/null +++ b/ipc/glue/test/browser/browser_child_hang.toml @@ -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_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_locked.js b/ipc/glue/test/browser/browser_utility_audio_locked.js new file mode 100644 index 0000000000..4be22de425 --- /dev/null +++ b/ipc/glue/test/browser/browser_utility_audio_locked.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head-multiple.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/ipc/glue/test/browser/head-multiple.js", + this +); + +add_setup(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["media.utility-process.enabled", false]], + }); +}); + +add_task(async function testAudioDecodingInUtility() { + // TODO: When getting rid of audio decoding on non utility at all, this + // should be removed + // We only lock the preference in Nightly builds so far, but on beta we expect + // audio decoding error + await runTest({ + expectUtility: isNightlyOnly(), + expectError: !isNightlyOnly(), + }); +}); 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..a0a4be63f6 --- /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", "ffvpx 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", "ffvpx 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_filepicker_crashed.js b/ipc/glue/test/browser/browser_utility_filepicker_crashed.js new file mode 100644 index 0000000000..e8eb83cf30 --- /dev/null +++ b/ipc/glue/test/browser/browser_utility_filepicker_crashed.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +SimpleTest.requestCompleteLog(); + +// Wait until the child process with the given PID has indeed been terminated. +// +// Note that `checkUtilityExists`, and other functions deriving from the output +// of `ChromeUtils.requestProcInfo()`, do not suffice for this purpose! It is an +// attested failure mode that the file-dialog utility process has been removed +// from the proc-info list, but is still live with the file-picker dialog still +// displayed. +function untilChildProcessDead(pid) { + return utilityProcessTest().untilChildProcessDead(pid); +} + +async function fileDialogProcessExists() { + return !!(await tryGetUtilityPid("windowsFileDialog")); +} + +// Poll for the creation of a file dialog process. +function untilFileDialogProcessExists(options = { maxTime: 2000 }) { + // milliseconds + const maxTime = options.maxTime ?? 2000, + pollTime = options.pollTime ?? 100; + const count = maxTime / pollTime; + + return TestUtils.waitForCondition( + () => tryGetUtilityPid("windowsFileDialog", { quiet: true }), + "waiting for file dialog process", + pollTime, // interval + count // maxTries + ); +} + +function openFileDialog() { + const process = (async () => { + await untilFileDialogProcessExists(); + let pid = await tryGetUtilityPid("windowsFileDialog"); + ok(pid, `pid should be acquired in openFileDialog::process (got ${pid})`); + // HACK: Wait briefly for the file dialog to open. + // + // If this is not done, we may attempt to crash the process while it's in + // the middle of creating and showing the file dialog window. There _should_ + // be no problem with this, but `::MiniDumpWriteDump()` occasionally fails + // with mysterious errors (`ERROR_BAD_LENGTH`) if we crashed the process + // while that was happening, yielding no minidump and therefore a failing + // test. + // + // Use of an arbitrary timeout could presumably be avoided by setting a + // window hook for the file dialog being shown and `await`ing on that. + // + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(res => setTimeout(res, 1000)); + return pid; + })(); + + const file = new Promise((resolve, reject) => { + info("Opening Windows file dialog"); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, "Test: browser_utility_filepicker_crashed.js", fp.modeOpen); + fp.open(result => { + ok( + result == fp.returnCancel, + "filepicker should resolve to cancellation" + ); + resolve(); + }); + }); + + return { process, file }; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // remote, no fallback + ["widget.windows.utility_process_file_picker", 2], + ], + }); +}); + +function makeTask(description, Describe, action) { + let task = async function () { + if (await fileDialogProcessExists()) { + // If this test proceeds, it will probably cause whatever other test has a + // file dialog open to fail. + // + // (We shouldn't be running two such tests in parallel on the same Fx + // instance, but that's not obvious at this level.) + ok(false, "another test has a file dialog open; aborting"); + return; + } + + const { process, file } = openFileDialog(); + const pid = await process; + const untilDead = untilChildProcessDead(pid); + + info(Describe + " the file-dialog utility process"); + await action(); + + // the file-picker's callback should have been promptly cancelled + const _before = Date.now(); + await file; + const _after = Date.now(); + const delta = _after - _before; + info(`file callback resolved after ${description} after ${delta}ms`); + + // depending on the test configuration, this may take some time while + // cleanup occurs + await untilDead; + }; + + // give this task a legible name + Object.defineProperty(task, "name", { + value: "testFileDialogProcess-" + Describe.replace(" ", ""), + }); + + return task; +} + +for (let [description, Describe, action] of [ + ["crash", "Crash", () => crashSomeUtilityActor("windowsFileDialog")], + [ + "being killed", + "Kill", + () => cleanUtilityProcessShutdown("windowsFileDialog", true), + ], + // Unfortunately, a controlled shutdown doesn't actually terminate the utility + // process; the file dialog remains open. (This is expected to be resolved with + // bug 1837008.) + /* [ + "shutdown", + "Shut down", + () => cleanUtilityProcessShutdown("windowsFileDialog"), + ] */ +]) { + add_task(makeTask(description, Describe, action)); + add_task(testCleanup); +} + +async function testCleanup() { + const killFileDialogProcess = async () => { + if (await tryGetUtilityPid("windowsFileDialog", { quiet: true })) { + await cleanUtilityProcessShutdown("windowsFileDialog", true); + return true; + } + return false; + }; + + // If a test failure occurred, the file dialog process may or may not already + // exist... + if (await killFileDialogProcess()) { + console.warn("File dialog process found and killed"); + return; + } + + // ... and if not, may or may not be pending creation. + info("Active file dialog process not found; waiting..."); + try { + await untilFileDialogProcessExists({ maxTime: 1000 }); + } catch (e) { + info("File dialog process not found during cleanup (as expected)"); + return; + } + await killFileDialogProcess(); + console.warn("Delayed file dialog process found and killed"); +} 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..107cd2e234 --- /dev/null +++ b/ipc/glue/test/browser/browser_utility_multipleAudio.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head-multiple.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/ipc/glue/test/browser/head-multiple.js", + this +); + +add_setup(async function checkAudioDecodingNonUtility() { + const isAudioDecodingNonUtilityAllowed = await SpecialPowers.getBoolPref( + "media.allow-audio-non-utility" + ); + ok( + !isAudioDecodingNonUtilityAllowed, + "Audio decoding should not be allowed on non utility processes by default" + ); +}); + +add_task(async function testAudioDecodingInUtility() { + await runTest({ expectUtility: true }); +}); + +add_task(async function testFailureAudioDecodingInRDD() { + await runTest({ expectUtility: false, expectError: true }); +}); + +add_task(async function testFailureAudioDecodingInContent() { + const platform = Services.appinfo.OS; + if (platform === "WINNT") { + ok( + true, + "Manually skipping on Windows because of gfx killing us, cf browser.ini" + ); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [["media.rdd-process.enabled", false]], + }); + await runTest({ expectUtility: false, expectRDD: false, expectError: true }); +}); diff --git a/ipc/glue/test/browser/browser_utility_multipleAudio_fallback.js b/ipc/glue/test/browser/browser_utility_multipleAudio_fallback.js new file mode 100644 index 0000000000..cbebd08287 --- /dev/null +++ b/ipc/glue/test/browser/browser_utility_multipleAudio_fallback.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head-multiple.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/ipc/glue/test/browser/head-multiple.js", + this +); + +add_setup(async function checkAudioDecodingNonUtility() { + const isAudioDecodingNonUtilityAllowed = await SpecialPowers.getBoolPref( + "media.allow-audio-non-utility" + ); + ok(isAudioDecodingNonUtilityAllowed, "Audio decoding has been allowed"); +}); + +add_task(async function testFallbackAudioDecodingInRDD() { + await runTest({ expectUtility: false, expectError: false }); +}); diff --git a/ipc/glue/test/browser/browser_utility_multipleAudio_fallback_content.js b/ipc/glue/test/browser/browser_utility_multipleAudio_fallback_content.js new file mode 100644 index 0000000000..f07a29985f --- /dev/null +++ b/ipc/glue/test/browser/browser_utility_multipleAudio_fallback_content.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head-multiple.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/ipc/glue/test/browser/head-multiple.js", + this +); + +add_setup(async function checkAudioDecodingNonUtility() { + const isAudioDecodingNonUtilityAllowed = await SpecialPowers.getBoolPref( + "media.allow-audio-non-utility" + ); + ok(isAudioDecodingNonUtilityAllowed, "Audio decoding has been allowed"); +}); + +add_task(async function testFallbackAudioDecodingInContent() { + await runTest({ expectContent: true }); +}); 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-multiple.js b/ipc/glue/test/browser/head-multiple.js new file mode 100644 index 0000000000..0ab098448e --- /dev/null +++ b/ipc/glue/test/browser/head-multiple.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from head.js */ + +async function runTest({ + expectUtility = false, + expectRDD = false, + expectContent = false, + expectError = false, +}) { + info(`Running tests with decoding from somewhere`); + info(` expectUtility: ${expectUtility}`); + info(` expectRDD: ${expectRDD}`); + info(` expectContent: ${expectContent}`); + + // 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 && !expectContent && !expectError + ? expectation.process + : "RDD", + expectation.decoder, + expectContent, + false, // expectJava + expectError + ) + ); + } + + 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); + } +} diff --git a/ipc/glue/test/browser/head-telemetry.js b/ipc/glue/test/browser/head-telemetry.js new file mode 100644 index 0000000000..46841347fa --- /dev/null +++ b/ipc/glue/test/browser/head-telemetry.js @@ -0,0 +1,269 @@ +/* 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} [${process}, ${codecNames}, ${extra}]`, + kInterval, + kRetries + ); +} + +async function runTest({ + expectUtility = false, + expectRDD = false, + expectError = false, +}) { + info( + `Running tests with decoding from Utility or RDD: expectUtility=${expectUtility} expectRDD=${expectRDD} expectError=${expectError}` + ); + + 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, + false, + expectError + ); + + info("Stop tab"); + await stop(tab); + + info("Remove tab"); + await BrowserTestUtils.removeTab(tab); + } +} + +async function runTestWithEME() { + info(`Running tests with decoding from Utility for EME`); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["media.utility-process.enabled", true], + ["toolkit.telemetry.ipcBatchTimeout", 0], + ], + }); + + const platform = Services.appinfo.OS; + + for (let { src, expectations } of audioTestDataEME()) { + if (!(platform in expectations)) { + info(`Skipping ${src} for ${platform}`); + continue; + } + + const expectation = expectations[platform]; + + info(`Add EME media tab`); + let tab = await addMediaTabWithEME(src.sourceBuffer, src.audioFile); + + info("Play tab"); + await play( + tab, + expectation.process, + expectation.decoder, + false, // expectContent + false, // expectJava + false, // expectError + true // withEME + ); + + 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}]` + ); + } + }); +} + +function getExtraKey({ utilityPref, rddPref, allowNonUtility }) { + let extraKey = ""; + if (!rddPref) { + extraKey += ",rdd-disabled"; + } + if (!utilityPref) { + extraKey += ",utility-disabled"; + } + // TODO: This needs to be removed when getting rid of ability to decode on + // non utility at all + if (allowNonUtility) { + extraKey += ",allow-non-utility"; + } + return extraKey; +} diff --git a/ipc/glue/test/browser/head.js b/ipc/glue/test/browser/head.js new file mode 100644 index 0000000000..8acff88273 --- /dev/null +++ b/ipc/glue/test/browser/head.js @@ -0,0 +1,562 @@ +/* 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, options = {}) { + let procInfos = (await ChromeUtils.requestProcInfo()).children.filter(p => { + return ( + p.type === "utility" && + (actor == undefined || + p.utilityActors.find(a => a.actorName.startsWith(actor))) + ); + }); + + if (!options?.quiet) { + info(`Utility process infos = ${JSON.stringify(procInfos)}`); + } + return procInfos; +} + +async function tryGetUtilityPid(actor, options = {}) { + let process = await getUtilityProcesses(actor, options); + if (!options?.quiet) { + ok(process.length <= 1, `at most one ${actor} process exists`); + } + return process[0]?.pid; +} + +async function checkUtilityExists(actor) { + info(`Looking for a running ${actor} utility process`); + const utilityPid = await tryGetUtilityPid(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 tryGetUtilityPid(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: "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", + }, + }, + }, + { + src: "small-shot.mp3", + 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", + }, + }, + }, + { + 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", + }, + }, + }, + ]; +} + +function audioTestDataEME() { + return [ + { + src: { + audioFile: + "https://example.com/browser/ipc/glue/test/browser/short-aac-encrypted-audio.mp4", + sourceBuffer: "audio/mp4", + }, + expectations: { + Linux: { + process: "Utility Generic", + decoder: "ffmpeg audio decoder", + }, + WINNT: { + process: "Utility WMF", + decoder: "wmf audio decoder", + }, + Darwin: { + process: "Utility AppleMedia", + decoder: "apple coremedia 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 addMediaTabWithEME(sourceBuffer, audioFile) { + const tab = BrowserTestUtils.addTab( + gBrowser, + "https://example.com/browser/", + { + forceNewProcess: true, + } + ); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn( + browser, + [sourceBuffer, audioFile], + createAudioElementEME + ); + return tab; +} + +async function play( + tab, + expectUtility, + expectDecoder, + expectContent = false, + expectJava = false, + expectError = false, + withEME = false +) { + let browser = tab.linkedBrowser; + return SpecialPowers.spawn( + browser, + [ + expectUtility, + expectDecoder, + expectContent, + expectJava, + expectError, + withEME, + ], + 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 createAudioElementEME(sourceBuffer, audioFile) { + // Helper to clone data into content so the EME helper can use the data. + function cloneIntoContent(data) { + return Cu.cloneInto(data, content.wrappedJSObject); + } + + // Load the EME helper into content. + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/ipc/glue/test/browser/eme_standalone.js", + content + ); + + let audio = content.document.createElement("audio"); + audio.setAttribute("controls", "true"); + audio.setAttribute("loop", true); + audio.setAttribute("_sourceBufferType", sourceBuffer); + audio.setAttribute("_audioUrl", audioFile); + content.document.body.appendChild(audio); + + let emeHelper = new content.wrappedJSObject.EmeHelper(); + emeHelper.SetKeySystem( + content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString() + ); + emeHelper.SetInitDataTypes(cloneIntoContent(["keyids", "cenc"])); + emeHelper.SetAudioCapabilities( + cloneIntoContent([{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }]) + ); + emeHelper.AddKeyIdAndKey( + "2cdb0ed6119853e7850671c3e9906c3c", + "808B9ADAC384DE1E4F56140F4AD76194" + ); + emeHelper.onerror = error => { + is(false, `Got unexpected error from EME helper: ${error}`); + }; + await emeHelper.ConfigureEme(audio); + // Done setting up EME. +} + +async function checkAudioDecoder( + expectedProcess, + expectedDecoder, + expectContent = false, + expectJava = false, + expectError = false, + withEME = 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 + (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("error", async err => { + info( + `Received HTML media error: ${audio.error.code}: ${audio.error.message}` + ); + if (expectError) { + const w = typeof content !== "undefined" ? content.window : window; + ok( + audio.error.code === w.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED || + w.MediaError.MEDIA_ERR_DECODE, + "Media supported but decoding failed" + ); + resolve(); + } else { + info(`Unexpected error`); + reject(); + } + }); + + audio.addEventListener("canplaythrough", startPlaybackHandler, { + once: true, + }); + }); + + if (!withEME) { + // We need to make sure the decoder is ready before play()ing otherwise we + // could get into bad situations + audio.load(); + } else { + // For EME we need to create and load content ourselves. We do this here + // because if we do it in createAudioElementEME() above then we end up + // with events fired before we get a chance to listen to them here + async function once(target, name) { + return new Promise(r => target.addEventListener(name, r, { once: true })); + } + + // Setup MSE. + const ms = new content.wrappedJSObject.MediaSource(); + audio.src = content.wrappedJSObject.URL.createObjectURL(ms); + await once(ms, "sourceopen"); + const sb = ms.addSourceBuffer(audio.getAttribute("_sourceBufferType")); + let fetchResponse = await content.fetch(audio.getAttribute("_audioUrl")); + let dataBuffer = await fetchResponse.arrayBuffer(); + sb.appendBuffer(dataBuffer); + await once(sb, "updateend"); + ms.endOfStream(); + await once(ms, "sourceended"); + } + + return checkPromise; +} + +async function runMochitestUtilityAudio( + src, + { + expectUtility, + expectDecoder, + expectContent = false, + expectJava = false, + expectError = 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, + expectError + ); + + 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); +} + +function isNightlyOnly() { + const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + return AppConstants.NIGHTLY_BUILD; +} diff --git a/ipc/glue/test/browser/mochitest_audio_off.toml b/ipc/glue/test/browser/mochitest_audio_off.toml new file mode 100644 index 0000000000..d174ea3939 --- /dev/null +++ b/ipc/glue/test/browser/mochitest_audio_off.toml @@ -0,0 +1,12 @@ +[DEFAULT] +run-if = ["os == '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.toml b/ipc/glue/test/browser/mochitest_audio_on.toml new file mode 100644 index 0000000000..908f4005f1 --- /dev/null +++ b/ipc/glue/test/browser/mochitest_audio_on.toml @@ -0,0 +1,12 @@ +[DEFAULT] +run-if = ["os == '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..671bdea5de --- /dev/null +++ b/ipc/glue/test/browser/moz.build @@ -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/. + +BROWSER_CHROME_MANIFESTS += [ + "browser.toml", + "browser_audio_fallback.toml", + "browser_audio_fallback_content.toml", + "browser_audio_locked.toml", + "browser_audio_shutdown.toml", + "browser_child_hang.toml", +] +MOCHITEST_MANIFESTS += ["mochitest_audio_off.toml", "mochitest_audio_on.toml"] 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..619cfaf11d --- /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, expectError: true }); + } catch (ex) { + ok(false, "Failure"); + } + } + + for (let src of [ + "small-shot.m4a", + ]) { + try { + await runMochitestUtilityAudio(src, { expectUtility: "", expectDecoder: "android decoder", expectContent: false, expectJava: true, expectError: false }); + } 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> |