From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- ...atBackgroundTaskRegistrationComponents.manifest | 4 + .../backgroundtasks/tests/xpcshell/experiment.json | 102 +++++ .../backgroundtasks/tests/xpcshell/head.js | 22 + .../test_backgroundtask_automaticrestart.js | 45 +++ .../test_backgroundtask_deletes_profile.js | 128 ++++++ .../xpcshell/test_backgroundtask_exitcodes.js | 49 +++ .../xpcshell/test_backgroundtask_experiments.js | 449 +++++++++++++++++++++ .../tests/xpcshell/test_backgroundtask_help.js | 20 + .../xpcshell/test_backgroundtask_localization.js | 40 ++ .../xpcshell/test_backgroundtask_locked_profile.js | 28 ++ .../xpcshell/test_backgroundtask_minruntime.js | 21 + .../xpcshell/test_backgroundtask_no_output.js | 57 +++ .../test_backgroundtask_not_ephemeral_profile.js | 24 ++ .../tests/xpcshell/test_backgroundtask_policies.js | 45 +++ .../test_backgroundtask_profile_is_slim.js | 138 +++++++ ...backgroundtask_profile_service_configuration.js | 64 +++ .../test_backgroundtask_removeDirectory.js | 301 ++++++++++++++ .../test_backgroundtask_shouldprocessupdates.js | 45 +++ .../test_backgroundtask_simultaneous_instances.js | 100 +++++ .../xpcshell/test_backgroundtask_specific_pref.js | 20 + .../xpcshell/test_backgroundtask_targeting.js | 17 + .../xpcshell/test_backgroundtask_unique_profile.js | 39 ++ .../test_backgroundtask_update_sync_manager.js | 48 +++ .../tests/xpcshell/test_backgroundtasksutils.js | 197 +++++++++ .../xpcshell/test_manifest_with_backgroundtask.js | 50 +++ .../test_manifest_without_backgroundtask.js | 38 ++ .../backgroundtasks/tests/xpcshell/xpcshell.toml | 64 +++ 27 files changed, 2155 insertions(+) create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/CatBackgroundTaskRegistrationComponents.manifest create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/experiment.json create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/head.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_automaticrestart.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_deletes_profile.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_exitcodes.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_help.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_localization.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_locked_profile.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_minruntime.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_no_output.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_not_ephemeral_profile.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_policies.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_is_slim.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_service_configuration.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_removeDirectory.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_shouldprocessupdates.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_simultaneous_instances.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_specific_pref.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_targeting.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_unique_profile.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_update_sync_manager.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtasksutils.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_with_backgroundtask.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_without_backgroundtask.js create mode 100644 toolkit/components/backgroundtasks/tests/xpcshell/xpcshell.toml (limited to 'toolkit/components/backgroundtasks/tests/xpcshell') diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/CatBackgroundTaskRegistrationComponents.manifest b/toolkit/components/backgroundtasks/tests/xpcshell/CatBackgroundTaskRegistrationComponents.manifest new file mode 100644 index 0000000000..6a675fc234 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/CatBackgroundTaskRegistrationComponents.manifest @@ -0,0 +1,4 @@ +category test-cat CatRegisteredComponent @unit.test.com/cat-registered-component;1 +category test-cat CatBackgroundTaskRegisteredComponent @unit.test.com/cat-backgroundtask-registered-component;1 backgroundtask +category test-cat CatBackgroundTaskAlwaysRegisteredComponent @unit.test.com/cat-backgroundtask-alwaysregistered-component;1 backgroundtask=1 +category test-cat CatBackgroundTaskNotRegisteredComponent @unit.test.com/cat-backgroundtask-notregistered-component;1 backgroundtask=0 diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/experiment.json b/toolkit/components/backgroundtasks/tests/xpcshell/experiment.json new file mode 100644 index 0000000000..606cff3de9 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/experiment.json @@ -0,0 +1,102 @@ +{ + "permissions": {}, + "data": { + "slug": "test-experiment", + "appId": "firefox-desktop", + "appName": "firefox_desktop", + "channel": "", + "endDate": null, + "branches": [ + { + "slug": "treatment-a", + "ratio": 1, + "feature": { + "value": {}, + "enabled": false, + "featureId": "this-is-included-for-desktop-pre-95-support" + }, + "features": [ + { + "value": { + "id": "test-experiment:treatment-a", + "groups": ["backgroundTaskMessage"], + "content": { + "body": "Body A", + "title": "Treatment A", + "tag": "should_be_overridden_a" + }, + "trigger": { + "id": "backgroundTask" + }, + "priority": 1, + "template": "toast_notification", + "frequency": { + "lifetime": 2 + }, + "targeting": "true" + }, + "enabled": true, + "featureId": "backgroundTaskMessage" + } + ] + }, + { + "slug": "treatment-b", + "ratio": 1, + "feature": { + "value": {}, + "enabled": false, + "featureId": "this-is-included-for-desktop-pre-95-support" + }, + "features": [ + { + "value": { + "id": "test-experiment:treatment-b", + "groups": ["backgroundTaskMessage"], + "content": { + "body": "Body B", + "title": "Treatment B" + }, + "trigger": { + "id": "backgroundTask" + }, + "priority": 1, + "template": "toast_notification", + "frequency": { + "lifetime": 2 + }, + "targeting": "true" + }, + "enabled": true, + "featureId": "backgroundTaskMessage" + } + ] + } + ], + "outcomes": [], + "arguments": {}, + "isRollout": false, + "probeSets": [], + "startDate": null, + "targeting": "('app.shield.optoutstudies.enabled'|preferenceValue) && (version|versionCompare('102.!') >= 0)", + "featureIds": ["backgroundTaskMessage"], + "application": "firefox-desktop", + "bucketConfig": { + "count": 10000, + "start": 0, + "total": 10000, + "namespace": "firefox-desktop-backgroundTaskMessage-1", + "randomizationUnit": "normandy_id" + }, + "schemaVersion": "1.8.0", + "userFacingName": "test-experiment", + "referenceBranch": "treatment-a", + "proposedDuration": 28, + "enrollmentEndDate": null, + "isEnrollmentPaused": false, + "proposedEnrollment": 7, + "userFacingDescription": "Test experiment to test supporting the Messaging System in Firefox background tasks.", + "id": "test-experiment", + "last_modified": 1657578927064 + } +} diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/head.js b/toolkit/components/backgroundtasks/tests/xpcshell/head.js new file mode 100644 index 0000000000..929d53d208 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/head.js @@ -0,0 +1,22 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const { BackgroundTasksTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BackgroundTasksTestUtils.sys.mjs" +); +BackgroundTasksTestUtils.init(this); +const do_backgroundtask = BackgroundTasksTestUtils.do_backgroundtask.bind( + BackgroundTasksTestUtils +); +const setupProfileService = BackgroundTasksTestUtils.setupProfileService.bind( + BackgroundTasksTestUtils +); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_automaticrestart.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_automaticrestart.js new file mode 100644 index 0000000000..ee1be8a3a9 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_automaticrestart.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// Run a background task which itself waits for a launched background task, which itself waits for a +// launched background task, etc. This is an easy way to ensure that tasks are running concurrently +// without requiring concurrency primitives. +add_task(async function test_backgroundtask_automatic_restart() { + let restartTimeoutSec = 30; + const path = await IOUtils.createUniqueFile( + PathUtils.tempDir, + "automatic_restart.txt" + ); + let fileExists = await IOUtils.exists(path); + ok(fileExists, `File at ${path} was created`); + let stdoutLines = []; + + // Test restart functionality. + let exitCode = await do_backgroundtask("automaticrestart", { + extraArgs: [`-no-wait`, path, `-attach-console`, `-no-remote`], + onStdoutLine: line => stdoutLines.push(line), + }); + Assert.equal(0, exitCode); + + let pid = -1; + for (let line of stdoutLines) { + if (line.includes("*** ApplyUpdate: launched")) { + let lineArr = line.split(" "); + pid = Number.parseInt(lineArr[lineArr.length - 2]); + } + } + console.log(`found launched pid ${pid}`); + + let updateProcessor = Cc[ + "@mozilla.org/updates/update-processor;1" + ].createInstance(Ci.nsIUpdateProcessor); + updateProcessor.waitForProcessExit(pid, restartTimeoutSec * 1000); + let finalMessage = await IOUtils.readUTF8(path); + ok(finalMessage.includes(`${pid}`), `New process message: ${finalMessage}`); + await IOUtils.remove(path, { ignoreAbsent: true }); + fileExists = await IOUtils.exists(path); + ok(!fileExists, `File at ${path} was removed`); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_deletes_profile.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_deletes_profile.js new file mode 100644 index 0000000000..7c9be2e780 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_deletes_profile.js @@ -0,0 +1,128 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +add_task(async function test_backgroundtask_deletes_profile() { + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let stdoutLines = []; + let exitCode = await do_backgroundtask("unique_profile", { + extraArgs: [sentinel], + onStdoutLine: line => stdoutLines.push(line), + }); + Assert.equal(0, exitCode); + + let profile; + for (let line of stdoutLines) { + if (line.includes(sentinel)) { + let info = JSON.parse(line.split(sentinel)[1]); + profile = info[1]; + } + } + Assert.ok(!!profile, `Found profile: ${profile}`); + + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(profile); + Assert.ok( + !dir.exists(), + `Temporary profile directory does not exist: ${profile}` + ); +}); + +let c = { + // See note about macOS and temporary directories below. + skip_if: () => AppConstants.platform == "macosx", +}; + +add_task(c, async function test_backgroundtask_cleans_up_stale_profiles() { + // Background tasks shutdown and removal raced with creating files in the + // temporary profile directory, leaving stale profile remnants on disk. We + // try to incrementally clean up such remnants. This test verifies that clean + // up succeeds as expected. In the future we might test that locked profiles + // are ignored and that a failure during a removal does not stop other + // removals from being processed, etc. + + // Background task temporary profiles are created in the OS temporary + // directory. On Windows, we can put the temporary directory in a + // testing-specific location by setting TMP, per + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw. + // On Linux, this works as well: see + // [GetSpecialSystemDirectory](https://searchfox.org/mozilla-central/source/xpcom/io/SpecialSystemDirectory.cpp). + // On macOS, we can't set the temporary directory in this manner so we skip + // this test. + let tmp = do_get_profile(); + tmp.append("Temp"); + tmp.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + + // Get the "profile prefix" that the clean up process generates by fishing the + // profile directory from the testing task that provides the profile path. + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let stdoutLines = []; + let exitCode = await do_backgroundtask("unique_profile", { + extraArgs: [sentinel], + extraEnv: { TMP: tmp.path }, + onStdoutLine: line => stdoutLines.push(line), + }); + Assert.equal(0, exitCode); + + let profile; + for (let line of stdoutLines) { + if (line.includes(sentinel)) { + let info = JSON.parse(line.split(sentinel)[1]); + profile = info[1]; + } + } + Assert.ok(!!profile, `Found profile: ${profile}`); + + Assert.ok( + profile.startsWith(tmp.path), + "Profile was created in test-specific temporary path" + ); + + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(profile); + + // Create a few "stale" profile directories. + let ps = []; + for (let i = 0; i < 3; i++) { + let p = dir.parent.clone(); + p.append(`${dir.leafName}_${i}`); + p.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + ps.push(p); + + let f = p.clone(); + f.append(`file_${i}`); + await IOUtils.writeJSON(f.path, {}); + } + + // Display logging for ease of debugging. + let moz_log = "BackgroundTasks:5"; + if (Services.env.exists("MOZ_LOG")) { + moz_log += `,${Services.env.get("MOZ_LOG")}`; + } + + // Invoke the task. + exitCode = await do_backgroundtask("unique_profile", { + extraArgs: [sentinel], + extraEnv: { + TMP: tmp.path, + MOZ_LOG: moz_log, + MOZ_BACKGROUNDTASKS_PURGE_STALE_PROFILES: "always", + }, + }); + Assert.equal(0, exitCode); + + // Verify none of the stale profile directories persist. + let es = []; // Expecteds. + let as = []; // Actuals. + for (let p of ps) { + as.push(!p.exists()); + es.push(true); + } + Assert.deepEqual(es, as, "All stale profile directories were cleaned up."); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_exitcodes.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_exitcodes.js new file mode 100644 index 0000000000..f2202ff60f --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_exitcodes.js @@ -0,0 +1,49 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This test exercises functionality and also ensures the exit codes, +// which are a public API, do not change over time. +const { EXIT_CODE } = ChromeUtils.importESModule( + "resource://gre/modules/BackgroundTasksManager.sys.mjs" +); + +add_task(async function test_success() { + let exitCode = await do_backgroundtask("success"); + Assert.equal(0, exitCode); + Assert.equal(EXIT_CODE.SUCCESS, exitCode); +}); + +add_task(async function test_failure() { + let exitCode = await do_backgroundtask("failure"); + Assert.equal(1, exitCode); + // There's no single exit code for failure. + Assert.notEqual(EXIT_CODE.SUCCESS, exitCode); +}); + +add_task(async function test_exception() { + let exitCode = await do_backgroundtask("exception"); + Assert.equal(3, exitCode); + Assert.equal(EXIT_CODE.EXCEPTION, exitCode); +}); + +add_task(async function test_not_found() { + let exitCode = await do_backgroundtask("not_found"); + Assert.equal(2, exitCode); + Assert.equal(EXIT_CODE.NOT_FOUND, exitCode); +}); + +add_task(async function test_timeout() { + const startTime = new Date(); + let exitCode = await do_backgroundtask("timeout"); + const endTime = new Date(); + const elapsedMs = endTime - startTime; + const fiveMinutesInMs = 5 * 60 * 1000; + Assert.less(elapsedMs, fiveMinutesInMs); + Assert.equal(4, exitCode); + Assert.equal(EXIT_CODE.TIMEOUT, exitCode); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js new file mode 100644 index 0000000000..9d834bfd5b --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js @@ -0,0 +1,449 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// This file tests several things. +// +// 1. We verify that we can forcefully opt-in to (particular branches of) +// experiments, that the resulting Firefox Messaging Experiment applies, and +// that the Firefox Messaging System respects lifetime frequency caps. +// 2. We verify that Nimbus randomization works with specific Normandy +// randomization IDs. +// 3. We verify that relevant opt-out prefs disable the Nimbus and Firefox +// Messaging System experience. + +const { ASRouterTargeting } = ChromeUtils.importESModule( + "resource:///modules/asrouter/ASRouterTargeting.sys.mjs" +); +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +// These randomization IDs were extracted by hand from Firefox instances. +// Randomization is sufficiently stable to hard-code these IDs rather than +// generating new ones at test time. +const BRANCH_MAP = { + "treatment-a": { + randomizationId: "d0e95fc3-fb15-4bc4-8151-a89582a56e29", + title: "Treatment A", + text: "Body A", + }, + "treatment-b": { + randomizationId: "90a60347-66cc-4716-9fef-cf49dd992d51", + title: "Treatment B", + text: "Body B", + }, +}; + +setupProfileService(); + +let taskProfile; +let manager; + +// Arrange a dummy Remote Settings server so that no non-local network +// connections are opened. +// And arrange dummy task profile. +add_setup(async () => { + info("Setting up profile service"); + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let taskProfD = do_get_profile(); + taskProfD.append("test_backgroundtask_experiments_task"); + taskProfile = profileService.createUniqueProfile( + taskProfD, + "test_backgroundtask_experiments_task" + ); + + registerCleanupFunction(() => { + taskProfile.remove(true); + }); + + // Arrange fake experiment enrollment details. + manager = ExperimentFakes.manager(); + + await manager.onStartup(); + await manager.store.addEnrollment(ExperimentFakes.experiment("foo")); + manager.unenroll("foo", "some-reason"); + await manager.store.addEnrollment( + ExperimentFakes.experiment("bar", { active: false }) + ); + await manager.store.addEnrollment( + ExperimentFakes.experiment("baz", { active: true }) + ); + + manager.store.addEnrollment(ExperimentFakes.rollout("rol1")); + manager.unenroll("rol1", "some-reason"); + manager.store.addEnrollment(ExperimentFakes.rollout("rol2")); +}); + +function resetProfile(profile) { + profile.rootDir.remove(true); + profile.rootDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + info(`Reset profile '${profile.rootDir.path}'`); +} + +// Run the "message" background task with some default configuration. Return +// "info" items output from the task as an array and as a map. +async function doMessage({ extraArgs = [], extraEnv = {} } = {}) { + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let infoArray = []; + let exitCode = await do_backgroundtask("message", { + extraArgs: [ + "--sentinel", + sentinel, + // Use a test-specific non-ephemeral profile, not the system-wide shared + // task-specific profile. + "--profile", + taskProfile.rootDir.path, + // Don't actually show a toast notification. + "--disable-alerts-service", + // Don't contact Remote Settings server. Can be overridden by subsequent + // `--experiments ...`. + "--no-experiments", + ...extraArgs, + ], + extraEnv: { + MOZ_LOG: "Dump:5,BackgroundTasks:5", + ...extraEnv, + }, + onStdoutLine: line => { + if (line.includes(sentinel)) { + let info = JSON.parse(line.split(sentinel)[1]); + infoArray.push(info); + } + }, + }); + + Assert.equal( + 0, + exitCode, + "The message background task exited with exit code 0" + ); + + // Turn [{x:...}, {y:...}] into {x:..., y:...}. + let infoMap = Object.assign({}, ...infoArray); + + return { infoArray, infoMap }; +} + +// Opt-in to an experiment. Verify that the experiment state is non-ephemeral, +// i.e., persisted. Verify that messages are shown until we hit the lifetime +// frequency caps. +// +// It's awkward to inspect the `ASRouter.jsm` internal state directly in this +// manner, but this is the pattern for testing such things at the time of +// writing. +add_task(async function test_backgroundtask_caps() { + let experimentFile = do_get_file("experiment.json"); + let experimentFileURI = Services.io.newFileURI(experimentFile); + + let { infoMap } = await doMessage({ + extraArgs: [ + // Opt-in to an experiment from a file. + "--url", + `${experimentFileURI.spec}?optin_branch=treatment-a`, + ], + }); + + // Verify that the correct experiment and branch generated an impression. + let impressions = infoMap.ASRouterState.messageImpressions; + Assert.deepEqual(Object.keys(impressions), ["test-experiment:treatment-a"]); + Assert.equal(impressions["test-experiment:treatment-a"].length, 1); + + // Verify that the correct toast notification was shown. + let alert = infoMap.showAlert.args[0]; + Assert.equal(alert.title, "Treatment A"); + Assert.equal(alert.text, "Body A"); + Assert.equal(alert.name, "optin-test-experiment:treatment-a"); + + // Now, do it again. No need to opt-in to the experiment this time. + ({ infoMap } = await doMessage({})); + + // Verify that only the correct experiment and branch generated an impression. + impressions = infoMap.ASRouterState.messageImpressions; + Assert.deepEqual(Object.keys(impressions), ["test-experiment:treatment-a"]); + Assert.equal(impressions["test-experiment:treatment-a"].length, 2); + + // Verify that the correct toast notification was shown. + alert = infoMap.showAlert.args[0]; + Assert.equal(alert.title, "Treatment A"); + Assert.equal(alert.text, "Body A"); + Assert.equal(alert.name, "optin-test-experiment:treatment-a"); + + // A third time. We'll hit the lifetime frequency cap (which is 2). + ({ infoMap } = await doMessage({})); + + // Verify that the correct experiment and branch impressions are untouched. + impressions = infoMap.ASRouterState.messageImpressions; + Assert.deepEqual(Object.keys(impressions), ["test-experiment:treatment-a"]); + Assert.equal(impressions["test-experiment:treatment-a"].length, 2); + + // Verify that no toast notication was shown. + Assert.ok(!("showAlert" in infoMap), "No alert shown"); +}); + +// Test that background tasks are enrolled into branches based on the Normandy +// randomization ID as expected. Run the message task with a hard-coded list of +// a single experiment and known randomization IDs, and verify that the enrolled +// branches are as expected. +add_task(async function test_backgroundtask_randomization() { + let experimentFile = do_get_file("experiment.json"); + + for (let [branchSlug, branchDetails] of Object.entries(BRANCH_MAP)) { + // Start fresh each time. + resetProfile(taskProfile); + + // Invoke twice; verify the branch is consistent each time. + for (let count = 1; count <= 2; count++) { + let { infoMap } = await doMessage({ + extraArgs: [ + // Read experiments from a file. + "--experiments", + experimentFile.path, + // Fixed randomization ID yields a deterministic enrollment branch assignment. + "--randomizationId", + branchDetails.randomizationId, + ], + }); + + // Verify that only the correct experiment and branch generated an impression. + let impressions = infoMap.ASRouterState.messageImpressions; + Assert.deepEqual(Object.keys(impressions), [ + `test-experiment:${branchSlug}`, + ]); + Assert.equal(impressions[`test-experiment:${branchSlug}`].length, count); + + // Verify that the correct toast notification was shown. + let alert = infoMap.showAlert.args[0]; + Assert.equal(alert.title, branchDetails.title, "Title is correct"); + Assert.equal(alert.text, branchDetails.text, "Text is correct"); + Assert.equal( + alert.name, + `test-experiment:${branchSlug}`, + "Name (tag) is correct" + ); + } + } +}); + +// Test that background tasks respect the datareporting and studies opt-out +// preferences. +add_task(async function test_backgroundtask_optout_preferences() { + let experimentFile = do_get_file("experiment.json"); + + let OPTION_MAP = { + "--no-datareporting": { + "datareporting.healthreport.uploadEnabled": false, + "app.shield.optoutstudies.enabled": true, + }, + "--no-optoutstudies": { + "datareporting.healthreport.uploadEnabled": true, + "app.shield.optoutstudies.enabled": false, + }, + }; + + for (let [option, expectedPrefs] of Object.entries(OPTION_MAP)) { + // Start fresh each time. + resetProfile(taskProfile); + + let { infoMap } = await doMessage({ + extraArgs: [ + option, + // Read experiments from a file. Opting in to an experiment with + // `--url` does not consult relevant preferences. + "--experiments", + experimentFile.path, + ], + }); + + Assert.deepEqual(infoMap.taskProfilePrefs, expectedPrefs); + + // Verify that no experiment generated an impression. + let impressions = infoMap.ASRouterState.messageImpressions; + Assert.deepEqual( + impressions, + [], + `No impressions generated with ${option}` + ); + + // Verify that no toast notication was shown. + Assert.ok(!("showAlert" in infoMap), `No alert shown with ${option}`); + } +}); + +const TARGETING_LIST = [ + // Target based on background task details. + ["isBackgroundTaskMode", 1], + ["backgroundTaskName == 'message'", 1], + ["backgroundTaskName == 'unrecognized'", 0], + // Filter based on `defaultProfile` targeting snapshot. + ["(currentDate|date - defaultProfile.currentDate|date) > 0", 1], + ["(currentDate|date - defaultProfile.currentDate|date) > 999999", 0], + // Filter based on `defaultProfile` experiment enrollment details. + ["'baz' in defaultProfile.activeExperiments", 1], + ["'bar' in defaultProfile.previousExperiments", 1], + ["'rol2' in defaultProfile.activeRollouts", 1], + ["'rol1' in defaultProfile.previousRollouts", 1], + ["defaultProfile.enrollmentsMap['baz'] == 'treatment'", 1], + ["defaultProfile.enrollmentsMap['bar'] == 'treatment'", 1], + ["'unknown' in defaultProfile.enrollmentsMap", 0], +]; + +// Test that background tasks targeting works for Nimbus experiments. +add_task(async function test_backgroundtask_Nimbus_targeting() { + let experimentFile = do_get_file("experiment.json"); + let experimentData = await IOUtils.readJSON(experimentFile.path); + + // We can't take a full environment snapshot under `xpcshell`. Select a few + // items that do work. + let target = { + currentDate: ASRouterTargeting.Environment.currentDate, + firefoxVersion: ASRouterTargeting.Environment.firefoxVersion, + }; + let targetSnapshot = await ASRouterTargeting.getEnvironmentSnapshot({ + targets: [manager.createTargetingContext(), target], + }); + + for (let [targeting, expectedLength] of TARGETING_LIST) { + // Start fresh each time. + resetProfile(taskProfile); + + let snapshotFile = taskProfile.rootDir.clone(); + snapshotFile.append("targeting.snapshot.json"); + await IOUtils.writeJSON(snapshotFile.path, targetSnapshot); + + // Write updated experiment data. + experimentData.data.targeting = targeting; + let targetingExperimentFile = taskProfile.rootDir.clone(); + targetingExperimentFile.append("targeting.experiment.json"); + await IOUtils.writeJSON(targetingExperimentFile.path, experimentData); + + let { infoMap } = await doMessage({ + extraArgs: [ + "--experiments", + targetingExperimentFile.path, + "--targeting-snapshot", + snapshotFile.path, + ], + }); + + // Verify that the given targeting generated the expected number of impressions. + let impressions = infoMap.ASRouterState.messageImpressions; + Assert.equal( + Object.keys(impressions).length, + expectedLength, + `${expectedLength} impressions generated with targeting '${targeting}'` + ); + } +}); + +// Test that background tasks targeting works for Firefox Messaging System branches. +add_task(async function test_backgroundtask_Messaging_targeting() { + // Don't target the Nimbus experiment at all. Use a consistent + // randomization ID to always enroll in the first branch. Target + // the first branch of the Firefox Messaging Experiment to the given + // targeting. Therefore, we either get the first branch if the + // targeting matches, or nothing at all. + + let treatmentARandomizationId = BRANCH_MAP["treatment-a"].randomizationId; + + let experimentFile = do_get_file("experiment.json"); + let experimentData = await IOUtils.readJSON(experimentFile.path); + + // We can't take a full environment snapshot under `xpcshell`. Select a few + // items that do work. + let target = { + currentDate: ASRouterTargeting.Environment.currentDate, + firefoxVersion: ASRouterTargeting.Environment.firefoxVersion, + }; + let targetSnapshot = await ASRouterTargeting.getEnvironmentSnapshot({ + targets: [manager.createTargetingContext(), target], + }); + + for (let [targeting, expectedLength] of TARGETING_LIST) { + // Start fresh each time. + resetProfile(taskProfile); + + let snapshotFile = taskProfile.rootDir.clone(); + snapshotFile.append("targeting.snapshot.json"); + await IOUtils.writeJSON(snapshotFile.path, targetSnapshot); + + // Write updated experiment data. + experimentData.data.targeting = "true"; + experimentData.data.branches[0].features[0].value.targeting = targeting; + + let targetingExperimentFile = taskProfile.rootDir.clone(); + targetingExperimentFile.append("targeting.experiment.json"); + await IOUtils.writeJSON(targetingExperimentFile.path, experimentData); + + let { infoMap } = await doMessage({ + extraArgs: [ + "--experiments", + targetingExperimentFile.path, + "--targeting-snapshot", + snapshotFile.path, + "--randomizationId", + treatmentARandomizationId, + ], + }); + + // Verify that the given targeting generated the expected number of impressions. + let impressions = infoMap.ASRouterState.messageImpressions; + Assert.equal( + Object.keys(impressions).length, + expectedLength, + `${expectedLength} impressions generated with targeting '${targeting}'` + ); + + if (expectedLength > 0) { + // Verify that the correct toast notification was shown. + let alert = infoMap.showAlert.args[0]; + Assert.equal( + alert.title, + BRANCH_MAP["treatment-a"].title, + "Title is correct" + ); + Assert.equal( + alert.text, + BRANCH_MAP["treatment-a"].text, + "Text is correct" + ); + Assert.equal( + alert.name, + `test-experiment:treatment-a`, + "Name (tag) is correct" + ); + } + } +}); + +// Verify that `RemoteSettingsClient.sync` is invoked before any +// `RemoteSettingsClient.get` invocations. This ensures the Remote Settings +// recipe collection is not allowed to go stale. +add_task( + async function test_backgroundtask_RemoteSettingsClient_invokes_sync() { + let { infoArray, infoMap } = await doMessage({}); + + Assert.ok( + "RemoteSettingsClient.get" in infoMap, + "RemoteSettingsClient.get was invoked" + ); + + for (let info of infoArray) { + if ("RemoteSettingsClient.get" in info) { + const { options: calledOptions } = info["RemoteSettingsClient.get"]; + Assert.ok( + calledOptions.forceSync, + "RemoteSettingsClient.get was first called with `forceSync`" + ); + return; + } + } + } +); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_help.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_help.js new file mode 100644 index 0000000000..3dea69cb6a --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_help.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function test_backgroundtask_help_includes_jsdebugger_options() { + let lines = []; + let exitCode = await do_backgroundtask("success", { + extraArgs: ["--help"], + onStdoutLine: line => lines.push(line), + }); + Assert.equal(0, exitCode); + + Assert.ok(lines.some(line => line.includes("--jsdebugger"))); + Assert.ok(lines.some(line => line.includes("--wait-for-jsdebugger"))); + Assert.ok(lines.some(line => line.includes("--start-debugger-server"))); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_localization.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_localization.js new file mode 100644 index 0000000000..e61ea50f09 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_localization.js @@ -0,0 +1,40 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// Disable `xpc::IsInAutomation()` so that we don't generate fatal +// Localization errors. +Services.prefs.setBoolPref( + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", + false +); + +async function doOne(resource, id) { + let l10n = new Localization([resource], true); + let value = await l10n.formatValue(id); + Assert.ok(value, `${id} from ${resource} is not null: ${value}`); + + let exitCode = await do_backgroundtask("localization", { + extraArgs: [resource, id, value], + }); + Assert.equal(0, exitCode); +} + +add_task(async function test_localization() { + // Verify that the `l10n-registry` category is processed and that localization + // works as expected in background tasks. We can use any FTL resource and + // string identifier here as long as the value is short and can be passed as a + // command line argument safely (i.e., is ASCII). + + // One from toolkit/. + await doOne("toolkit/global/commonDialog.ftl", "common-dialog-title-system"); + if (AppConstants.MOZ_APP_NAME == "thunderbird") { + // And one from messenger/. + await doOne("messenger/messenger.ftl", "no-reply-title"); + } else { + // And one from browser/. + await doOne("browser/pageInfo.ftl", "not-set-date"); + } +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_locked_profile.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_locked_profile.js new file mode 100644 index 0000000000..ef70bcf51e --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_locked_profile.js @@ -0,0 +1,28 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +setupProfileService(); + +add_task(async function test_backgroundtask_locked_profile() { + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profile = profileService.createUniqueProfile( + do_get_profile(), + "test_locked_profile" + ); + let lock = profile.lock({}); + + try { + let exitCode = await do_backgroundtask("success", { + extraEnv: { XRE_PROFILE_PATH: lock.directory.path }, + }); + Assert.equal(1, exitCode); + } finally { + lock.unlock(); + } +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_minruntime.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_minruntime.js new file mode 100644 index 0000000000..faacf1b0e4 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_minruntime.js @@ -0,0 +1,21 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function test_backgroundtask_minruntime() { + let startTime = new Date().getTime(); + let exitCode = await do_backgroundtask("minruntime"); + Assert.equal(0, exitCode); + let finishTime = new Date().getTime(); + + // minruntime sets backgroundTaskMinRuntimeMS = 2000; + // Have some tolerance for flaky timers. + Assert.ok( + finishTime - startTime > 1800, + "Runtime was at least 2 seconds (approximately)." + ); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_no_output.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_no_output.js new file mode 100644 index 0000000000..e4553c6a7b --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_no_output.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// Run a background task which itself waits for a launched background task, +// which itself waits for a launched background task, etc. Verify no output is +// produced. +add_task(async function test_backgroundtask_no_output() { + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let count = 2; + let outputLines = []; + let exitCode = await do_backgroundtask("no_output", { + extraArgs: [sentinel, count.toString()], + // This is a misnomer: stdout is redirected to stderr, so this is _any_ + // output line. + onStdoutLine: line => outputLines.push(line), + }); + Assert.equal(0, exitCode); + + if (AppConstants.platform !== "win") { + // Check specific logs because there can still be some logs in certain conditions, + // e.g. in code coverage (see bug 1831778 and bug 1804833) + ok( + outputLines.every(l => !l.includes("*** You are running in")), + "Should not have logs by default" + ); + } +}); + +// Run a background task which itself waits for a launched background task, +// which itself waits for a launched background task, etc. Since we ignore the +// no output restriction, verify that output is produced. +add_task(async function test_backgroundtask_ignore_no_output() { + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let count = 2; + let outputLines = []; + let exitCode = await do_backgroundtask("no_output", { + extraArgs: [sentinel, count.toString()], + extraEnv: { MOZ_BACKGROUNDTASKS_IGNORE_NO_OUTPUT: "1" }, + // This is a misnomer: stdout is redirected to stderr, so this is _any_ + // output line. + onStdoutLine: line => { + if (line.includes(sentinel)) { + outputLines.push(line); + } + }, + }); + Assert.equal(0, exitCode); + + Assert.equal(count, outputLines.length); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_not_ephemeral_profile.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_not_ephemeral_profile.js new file mode 100644 index 0000000000..29ef0d542d --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_not_ephemeral_profile.js @@ -0,0 +1,24 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +add_task(async function test_not_ephemeral_profile() { + // Get the pref, and then update its value. We ignore the initial return, + // since the persistent profile may already exist. + let exitCode = await do_backgroundtask("not_ephemeral_profile", { + extraArgs: ["test.backgroundtask_specific_pref.exitCode", "80"], + }); + + // Do it again, confirming that the profile is persistent. + exitCode = await do_backgroundtask("not_ephemeral_profile", { + extraArgs: ["test.backgroundtask_specific_pref.exitCode", "81"], + }); + Assert.equal(80, exitCode); + + exitCode = await do_backgroundtask("not_ephemeral_profile", { + extraArgs: ["test.backgroundtask_specific_pref.exitCode", "82"], + }); + Assert.equal(81, exitCode); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_policies.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_policies.js new file mode 100644 index 0000000000..45f64e5f01 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_policies.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// In order to use the policy engine inside the xpcshell harness, we need to set +// up a dummy app info. In the backgroundtask itself, the application under +// test will configure real app info. This opens a possibility for some +// incompatibility, but there doesn't appear to be such an issue at this time. +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +updateAppInfo({ + name: "XPCShell", + ID: "xpcshell@tests.mozilla.org", + version: "48", + platformVersion: "48", +}); + +const { EnterprisePolicyTesting } = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +// This initializes the policy engine for xpcshell tests +let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService( + Ci.nsIObserver +); +policies.observe(null, "policies-startup", null); + +add_task(async function test_backgroundtask_policies() { + let url = "https://www.example.com/"; + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + AppUpdateURL: url, + }, + }); + + let filePath = Services.prefs.getStringPref("browser.policies.alternatePath"); + + let exitCode = await do_backgroundtask("policies", { + extraArgs: [filePath, url], + }); + Assert.equal(0, exitCode); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_is_slim.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_is_slim.js new file mode 100644 index 0000000000..a6d37c9f1c --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_is_slim.js @@ -0,0 +1,138 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// Invoke `profile_is_slim` background task, have it *not* remove its +// temporary profile, and verify that the temporary profile doesn't +// contain unexpected files and/or directories. +// +// N.b.: the background task invocation is a production Firefox +// running in automation; that means it can only connect to localhost +// (and it is not configured to connect to mochi.test, etc). +// Therefore we run our own server for it to connect to. + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); +let server; + +setupProfileService(); + +let successCount = 0; +function success(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let str = "success"; + response.write(str, str.length); + successCount += 1; +} + +add_setup(() => { + server = new HttpServer(); + server.registerPathHandler("/success", success); + server.start(-1); + + registerCleanupFunction(async () => { + await server.stop(); + }); +}); + +add_task(async function test_backgroundtask_profile_is_slim() { + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profD = do_get_profile(); + profD.append("test_profile_is_slim"); + let profile = profileService.createUniqueProfile( + profD, + "test_profile_is_slim" + ); + + registerCleanupFunction(() => { + profile.remove(true); + }); + + let exitCode = await do_backgroundtask("profile_is_slim", { + extraArgs: [`http://localhost:${server.identity.primaryPort}/success`], + extraEnv: { + XRE_PROFILE_PATH: profile.rootDir.path, + MOZ_BACKGROUNDTASKS_NO_REMOVE_PROFILE: "1", + }, + }); + + Assert.equal( + 0, + exitCode, + "The fetch background task exited with exit code 0" + ); + Assert.equal( + 1, + successCount, + "The fetch background task reached the test server 1 time" + ); + + assertProfileIsSlim(profile.rootDir); +}); + +const expected = [ + { relPath: "lock", condition: AppConstants.platform == "linux" }, + { + relPath: ".parentlock", + condition: + AppConstants.platform == "linux" || AppConstants.platform == "macosx", + }, + { + relPath: "parent.lock", + condition: + AppConstants.platform == "win" || AppConstants.platform == "macosx", + }, + // TODO: verify that directory is empty. + { relPath: "cache2", isDirectory: true }, + { relPath: "compatibility.ini" }, + { relPath: "crashes", isDirectory: true }, + { relPath: "data", isDirectory: true }, + { relPath: "local", isDirectory: true }, + { relPath: "minidumps", isDirectory: true }, + // `mozinfo.json` is an artifact of the testing infrastructure. + { relPath: "mozinfo.json" }, + { relPath: "pkcs11.txt" }, + { relPath: "prefs.js" }, + { relPath: "security_state", isDirectory: true }, + // TODO: verify that directory is empty. + { relPath: "startupCache", isDirectory: true }, + { relPath: "times.json" }, +]; + +async function assertProfileIsSlim(profile) { + Assert.ok(profile.exists(), `Profile directory does exist: ${profile.path}`); + + // We collect unexpected results, log them all, and then assert + // there are none to provide the most feedback per iteration. + let unexpected = []; + + let typeLabel = { true: "directory", false: "file" }; + + for (let entry of profile.directoryEntries) { + // `getRelativePath` always uses '/' as path separator. + let relPath = entry.getRelativePath(profile); + + info(`Witnessed directory entry '${relPath}'.`); + + let expectation = expected.find( + it => it.relPath == relPath && (!("condition" in it) || it.condition) + ); + if (!expectation) { + info(`UNEXPECTED: Directory entry '${relPath}' is NOT expected!`); + unexpected.push(relPath); + } else if ( + typeLabel[!!expectation.isDirectory] != typeLabel[entry.isDirectory()] + ) { + info(`UNEXPECTED: Directory entry '${relPath}' is NOT expected type!`); + unexpected.push(relPath); + } + } + + Assert.deepEqual([], unexpected, "Expected nothing to report"); +} diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_service_configuration.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_service_configuration.js new file mode 100644 index 0000000000..7d8e161046 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_service_configuration.js @@ -0,0 +1,64 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +setupProfileService(); + +async function doOne(extraArgs = [], extraEnv = {}) { + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let stdoutLines = []; + let exitCode = await do_backgroundtask("unique_profile", { + extraArgs: [sentinel, ...extraArgs], + extraEnv, + onStdoutLine: line => stdoutLines.push(line), + }); + Assert.equal(0, exitCode); + + let infos = []; + let profiles = []; + for (let line of stdoutLines) { + if (line.includes(sentinel)) { + let info = JSON.parse(line.split(sentinel)[1]); + infos.push(info); + profiles.push(info[1]); + } + } + + Assert.equal( + 1, + profiles.length, + `Found 1 profile: ${JSON.stringify(profiles)}` + ); + + return profiles[0]; +} + +// Run a background task which displays its profile directory, and verify that +// the "normal profile configuration" mechanisms override the background +// task-specific default. +add_task(async function test_backgroundtask_profile_service_configuration() { + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profile = profileService.createUniqueProfile( + do_get_profile(), + "test_locked_profile" + ); + let path = profile.rootDir.path; + + Assert.ok( + profile.rootDir.exists(), + `Temporary profile directory does exists: ${profile.rootDir}` + ); + + let profileDir = await doOne(["--profile", path]); + Assert.equal(profileDir, path); + + profileDir = await doOne([], { XRE_PROFILE_PATH: path }); + Assert.equal(profileDir, path); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_removeDirectory.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_removeDirectory.js new file mode 100644 index 0000000000..57da9ac175 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_removeDirectory.js @@ -0,0 +1,301 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// This test exercises functionality and also ensures the exit codes, +// which are a public API, do not change over time. +const { EXIT_CODE } = ChromeUtils.importESModule( + "resource://gre/modules/BackgroundTasksManager.sys.mjs" +); + +const LEAF_NAME = "newCacheFolder"; + +add_task(async function test_simple() { + // This test creates a simple folder in the profile and passes it to + // the background task to delete + + let dir = do_get_profile(); + dir.append(LEAF_NAME); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(dir.exists(), true); + + let extraDir = do_get_profile(); + extraDir.append("test.abc"); + extraDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(extraDir.exists(), true); + + let outputLines = []; + let exitCode = await do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "10", ".abc"], + onStdoutLine: line => outputLines.push(line), + }); + equal(exitCode, EXIT_CODE.SUCCESS); + equal(dir.exists(), false); + equal(extraDir.exists(), false); + + if (AppConstants.platform !== "win") { + // Check specific logs because there can still be some logs in certain conditions, + // e.g. in code coverage (see bug 1831778 and bug 1804833) + ok( + outputLines.every(l => !l.includes("*** You are running in")), + "Should not have logs by default" + ); + } +}); + +add_task(async function test_no_extension() { + // Test that not passing the extension is fine + + let dir = do_get_profile(); + dir.append(LEAF_NAME); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(dir.exists(), true); + + let exitCode = await do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "10"], + }); + equal(exitCode, EXIT_CODE.SUCCESS); + equal(dir.exists(), false); +}); + +add_task(async function test_createAfter() { + // First we create the task then we create the directory. + // The task will only wait for 10 seconds. + let dir = do_get_profile(); + dir.append(LEAF_NAME); + + let task = do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "10", ""], + }); + + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + let exitCode = await task; + equal(exitCode, EXIT_CODE.SUCCESS); + equal(dir.exists(), false); +}); + +add_task(async function test_folderNameWithSpaces() { + // We pass in a leaf name with spaces just to make sure the command line + // argument parsing works properly. + let dir = do_get_profile(); + let leafNameWithSpaces = `${LEAF_NAME} space`; + dir.append(leafNameWithSpaces); + + let task = do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, leafNameWithSpaces, "10", ""], + }); + + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + let exitCode = await task; + equal(exitCode, EXIT_CODE.SUCCESS); + equal(dir.exists(), false); +}); + +add_task(async function test_missing_folder() { + // We never create the directory. + // We only wait for 1 second + + let dir = do_get_profile(); + dir.append(LEAF_NAME); + let exitCode = await do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "1", ""], + }); + + equal(exitCode, EXIT_CODE.SUCCESS); + equal(dir.exists(), false); +}); + +add_task( + { skip_if: () => mozinfo.os != "win" }, + async function test_ro_file_in_folder() { + let dir = do_get_profile(); + dir.append(LEAF_NAME); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(dir.exists(), true); + + let file = dir.clone(); + file.append("ro_file"); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + file.QueryInterface(Ci.nsILocalFileWin).readOnly = true; + + // Make sure that we can move with a readonly file in the directory + dir.moveTo(null, "newName"); + + // This will fail because of the readonly file. + let exitCode = await do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, "newName", "1", ""], + }); + + equal(exitCode, EXIT_CODE.EXCEPTION); + + dir = do_get_profile(); + dir.append("newName"); + file = dir.clone(); + file.append("ro_file"); + equal(file.exists(), true); + + // Remove readonly attribute and try again. + // As a followup we might want to force an old cache dir to be deleted, even if it has readonly files in it. + file.QueryInterface(Ci.nsILocalFileWin).readOnly = false; + dir.remove(true); + equal(file.exists(), false); + equal(dir.exists(), false); + } +); + +add_task(async function test_purgeFile() { + // Test that only directories are purged by the background task + let file = do_get_profile(); + file.append(LEAF_NAME); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + equal(file.exists(), true); + + let exitCode = await do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "2", ""], + }); + equal(exitCode, EXIT_CODE.EXCEPTION); + equal(file.exists(), true); + + file.remove(true); + let dir = file.clone(); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(dir.exists(), true); + file = do_get_profile(); + file.append("test.abc"); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + equal(file.exists(), true); + let dir2 = do_get_profile(); + dir2.append("dir.abc"); + dir2.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(dir2.exists(), true); + + exitCode = await do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "2", ".abc"], + }); + + equal(exitCode, EXIT_CODE.SUCCESS); + equal(dir.exists(), false); + equal(dir2.exists(), false); + equal(file.exists(), true); + file.remove(true); +}); + +add_task(async function test_two_tasks() { + let dir1 = do_get_profile(); + dir1.append("leaf1.abc"); + + let dir2 = do_get_profile(); + dir2.append("leaf2.abc"); + + let tasks = []; + tasks.push( + do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, dir1.leafName, "5", ".abc"], + }) + ); + dir1.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + tasks.push( + do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, dir2.leafName, "5", ".abc"], + }) + ); + dir2.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + let [r1, r2] = await Promise.all(tasks); + + equal(r1, EXIT_CODE.SUCCESS); + equal(r2, EXIT_CODE.SUCCESS); + equal(dir1.exists(), false); + equal(dir2.exists(), false); +}); + +// This test creates a large number of tasks and directories. Spawning a huge +// number of tasks concurrently (say, 50 or 100) appears to cause problems; +// since doing so is rather artificial, we haven't tracked this down. +const TASK_COUNT = 20; +add_task(async function test_aLotOfTasks() { + let dirs = []; + let tasks = []; + + for (let i = 0; i < TASK_COUNT; i++) { + let dir = do_get_profile(); + dir.append(`leaf${i}.abc`); + + tasks.push( + do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, dir.leafName, "5", ".abc"], + extraEnv: { MOZ_LOG: "BackgroundTasks:5" }, + }) + ); + + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + dirs.push(dir); + } + + let results = await Promise.all(tasks); + for (let i in results) { + equal(results[i], EXIT_CODE.SUCCESS, `Task ${i} should succeed`); + equal(dirs[i].exists(), false, `leaf${i}.abc should not exist`); + } +}); + +add_task(async function test_suffix() { + // Make sure only the directories with the specified suffix will be removed + + let leaf = do_get_profile(); + leaf.append(LEAF_NAME); + leaf.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + let abc = do_get_profile(); + abc.append("foo.abc"); + abc.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(abc.exists(), true); + + let bcd = do_get_profile(); + bcd.append("foo.bcd"); + bcd.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + equal(bcd.exists(), true); + + // XXX: This should be able to skip passing LEAF_NAME (passing "" instead), + // but bug 1853920 and bug 1853921 causes incorrect arguments in that case. + let task = do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "10", ".abc"], + }); + + let exitCode = await task; + equal(exitCode, EXIT_CODE.SUCCESS); + equal(abc.exists(), false); + equal(bcd.exists(), true); +}); + +add_task(async function test_suffix_wildcard() { + // wildcard as a suffix should remove every subdirectories + + let leaf = do_get_profile(); + leaf.append(LEAF_NAME); + leaf.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + let abc = do_get_profile(); + abc.append("foo.abc"); + abc.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + let cde = do_get_profile(); + cde.append("foo.cde"); + cde.create(Ci.nsIFile.DIRECTORY_TYPE, 0o744); + + // XXX: This should be able to skip passing LEAF_NAME (passing "" instead), + // but bug 1853920 and bug 1853921 causes incorrect arguments in that case. + let task = do_backgroundtask("removeDirectory", { + extraArgs: [do_get_profile().path, LEAF_NAME, "10", "*"], + }); + + let exitCode = await task; + equal(exitCode, EXIT_CODE.SUCCESS); + equal(cde.exists(), false); + equal(cde.exists(), false); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_shouldprocessupdates.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_shouldprocessupdates.js new file mode 100644 index 0000000000..27dce8b99f --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_shouldprocessupdates.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +add_task(async function test_backgroundtask_shouldprocessupdates() { + // The task returns 81 if we should process updates, i.e., + // !ShouldNotProcessUpdates(), <= 80 otherwise. xpcshell itself counts as an + // instance, so the background task will see it and think another instance is + // running. N.b.: this isn't as robust as it could be: running Firefox + // instances and parallel tests interact here (mostly harmlessly). + // + // Since the behaviour under test (ShouldNotProcessUpdates()) happens at startup, + // we can't easily change the lock location of the background task. + + // `shouldprocessupdates` is an updating task, but there is another instance + // running, so we should not process updates. + let exitCode = await do_backgroundtask("shouldprocessupdates", { + extraArgs: ["--test-process-updates"], + }); + Assert.equal(80, exitCode); + + // If we change our lock location, the background task won't see our instance + // running. + let file = do_get_profile(); + file.append("customExePath"); + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + syncManager.resetLock(file); + + // Since we've changed the xpcshell executable name, the background task won't + // see us and think another instance is running. This time, there is no + // reason to not process updates. + exitCode = await do_backgroundtask("shouldprocessupdates"); + Assert.equal(81, exitCode); + + // `shouldnotprocessupdates` is not a recognized updating task, so we should + // not process updates. + exitCode = await do_backgroundtask("shouldnotprocessupdates", { + extraArgs: ["--test-process-updates"], + }); + Assert.equal(78, exitCode); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_simultaneous_instances.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_simultaneous_instances.js new file mode 100644 index 0000000000..acae920849 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_simultaneous_instances.js @@ -0,0 +1,100 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// Display logging for ease of debugging. +let moz_log = "BackgroundTasks:5"; + +// Background task temporary profiles are created in the OS temporary directory. +// On Windows, we can put the temporary directory in a testing-specific location +// by setting TMP, per +// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw. +// On Linux, this works as well: see +// [GetSpecialSystemDirectory](https://searchfox.org/mozilla-central/source/xpcom/io/SpecialSystemDirectory.cpp). +// On macOS, we can't set the temporary directory in this manner, so we will +// leak some temporary directories. It's important to test this functionality +// on macOS so we accept this. +let tmp = do_get_profile(); +tmp.append("Temp"); + +add_setup(() => { + if (Services.env.exists("MOZ_LOG")) { + moz_log += `,${Services.env.get("MOZ_LOG")}`; + } + + tmp.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700); +}); + +async function launch_one(index) { + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let stdoutLines = []; + let exitCode = await do_backgroundtask("unique_profile", { + extraArgs: [sentinel], + extraEnv: { + MOZ_BACKGROUNDTASKS_NO_REMOVE_PROFILE: "1", + MOZ_LOG: moz_log, + TMP: tmp.path, // Ignored on macOS. + }, + onStdoutLine: line => stdoutLines.push(line), + }); + + let profile; + for (let line of stdoutLines) { + if (line.includes(sentinel)) { + let info = JSON.parse(line.split(sentinel)[1]); + profile = info[1]; + } + } + + return { index, sentinel, exitCode, profile }; +} + +add_task(async function test_backgroundtask_simultaneous_instances() { + let expectedCount = 10; + let promises = []; + + for (let i = 0; i < expectedCount; i++) { + promises.push(launch_one(i)); + } + + let results = await Promise.all(promises); + + registerCleanupFunction(() => { + for (let result of results) { + if (!result.profile) { + return; + } + + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(result.profile); + try { + dir.remove(); + } catch (e) { + console.warn("Could not clean up profile directory", e); + } + } + }); + + for (let result of results) { + Assert.equal( + 0, + result.exitCode, + `Invocation ${result.index} with sentinel ${result.sentinel} exited with code 0` + ); + Assert.ok( + result.profile, + `Invocation ${result.index} with sentinel ${result.sentinel} yielded a temporary profile directory` + ); + } + + let profiles = new Set(results.map(it => it.profile)); + Assert.equal( + expectedCount, + profiles.size, + `Invocations used ${expectedCount} different temporary profile directories` + ); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_specific_pref.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_specific_pref.js new file mode 100644 index 0000000000..50f85b9bec --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_specific_pref.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +add_task(async function test_backgroundtask_specific_pref() { + // First, verify this pref isn't set in Gecko itself. + Assert.equal( + -1, + Services.prefs.getIntPref("test.backgroundtask_specific_pref.exitCode", -1) + ); + + // Second, verify that this pref is set in background tasks. + // xpcshell tests locally test unpackaged builds. + let exitCode = await do_backgroundtask("backgroundtask_specific_pref", { + extraArgs: ["test.backgroundtask_specific_pref.exitCode"], + }); + Assert.equal(79, exitCode); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_targeting.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_targeting.js new file mode 100644 index 0000000000..6b848ff3a7 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_targeting.js @@ -0,0 +1,17 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { EXIT_CODE } = ChromeUtils.importESModule( + "resource://gre/modules/BackgroundTasksManager.sys.mjs" +); + +add_task(async function test_targeting() { + // The task itself fails if any targeting getter fails. + let exitCode = await do_backgroundtask("targeting"); + Assert.equal(EXIT_CODE.SUCCESS, exitCode); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_unique_profile.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_unique_profile.js new file mode 100644 index 0000000000..6c17241932 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_unique_profile.js @@ -0,0 +1,39 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +// Run a background task which itself waits for a launched background task, which itself waits for a +// launched background task, etc. This is an easy way to ensure that tasks are running concurrently +// without requiring concurrency primitives. +add_task(async function test_backgroundtask_unique_profile() { + let sentinel = Services.uuid.generateUUID().toString(); + sentinel = sentinel.substring(1, sentinel.length - 1); + + let count = 3; + let stdoutLines = []; + let exitCode = await do_backgroundtask("unique_profile", { + extraArgs: [sentinel, count.toString()], + onStdoutLine: line => stdoutLines.push(line), + }); + Assert.equal(0, exitCode); + + let infos = []; + let profiles = new Set(); + for (let line of stdoutLines) { + if (line.includes(sentinel)) { + let info = JSON.parse(line.split(sentinel)[1]); + infos.push(info); + profiles.add(info[1]); + } + } + + Assert.equal( + count, + profiles.size, + `Found ${count} distinct profiles: ${JSON.stringify( + Array.from(profiles) + )} in: ${JSON.stringify(infos)}` + ); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_update_sync_manager.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_update_sync_manager.js new file mode 100644 index 0000000000..30009613d7 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_update_sync_manager.js @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function test_backgroundtask_update_sync_manager() { + // The task returns 80 if another instance is running, 81 otherwise. xpcshell + // itself counts as an instance, so the background task will see it and think + // another instance is running. + // + // This can also be achieved by overriding directory providers, but + // that's not particularly robust in the face of parallel testing. + // Doing it this way exercises `resetLock` with a path. + let exitCode = await do_backgroundtask("update_sync_manager", { + extraArgs: [Services.dirsvc.get("XREExeF", Ci.nsIFile).path], + }); + Assert.equal(80, exitCode, "Another instance is running"); + + // If we tell the backgroundtask to use a unique appPath, the + // background task won't see any other instances running. + let file = do_get_profile(); + file.append("customExePath"); + file.append("customExe"); + + exitCode = await do_backgroundtask("update_sync_manager", { + extraArgs: [file.path], + }); + Assert.equal(81, exitCode, "No other instance is running"); + + let upperCaseFile = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + upperCaseFile.initWithPath( + Services.dirsvc.get("XREExeF", Ci.nsIFile).path.toUpperCase() + ); + if (upperCaseFile.exists()) { + // The uppercased path can still be used to access the exe, indicating a + // case-insensitive filesystem (as is usual on Windows and macOS), so path + // normalization can be tested. + exitCode = await do_backgroundtask("update_sync_manager", { + extraArgs: [upperCaseFile.path], + }); + Assert.equal(80, exitCode, "Another instance is running"); + } +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtasksutils.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtasksutils.js new file mode 100644 index 0000000000..93ff0466b6 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtasksutils.js @@ -0,0 +1,197 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { BackgroundTasksUtils } = ChromeUtils.importESModule( + "resource://gre/modules/BackgroundTasksUtils.sys.mjs" +); + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs", +}); + +setupProfileService(); + +add_task(async function test_withProfileLock() { + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profilePath = do_get_profile(); + profilePath.append(`test_withProfileLock`); + let profile = profileService.createUniqueProfile( + profilePath, + "test_withProfileLock" + ); + + await BackgroundTasksUtils.withProfileLock(async lock => { + BackgroundTasksUtils._throwIfNotLocked(lock); + }, profile); + + // In debug builds, an unlocked lock crashes via `NS_ERROR` when + // `.directory` is invoked. There's no way to check that the lock + // is unlocked, so we can't realistically test this scenario in + // debug builds. + if (!AppConstants.DEBUG) { + await Assert.rejects( + BackgroundTasksUtils.withProfileLock(async lock => { + lock.unlock(); + BackgroundTasksUtils._throwIfNotLocked(lock); + }, profile), + /Profile is not locked/ + ); + } +}); + +add_task(async function test_readPreferences() { + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profilePath = do_get_profile(); + profilePath.append(`test_readPreferences`); + let profile = profileService.createUniqueProfile( + profilePath, + "test_readPreferences" + ); + + // Before we write any preferences, we fail to read. + await Assert.rejects( + BackgroundTasksUtils.withProfileLock( + lock => BackgroundTasksUtils.readPreferences(null, lock), + profile + ), + /NotFoundError/ + ); + + let s = `user_pref("testPref.bool1", true); + user_pref("testPref.bool2", false); + user_pref("testPref.int1", 23); + user_pref("testPref.int2", -1236); + `; + + let prefsFile = profile.rootDir.clone(); + prefsFile.append("prefs.js"); + await IOUtils.writeUTF8(prefsFile.path, s); + + // Now we can read all the prefs. + let prefs = await BackgroundTasksUtils.withProfileLock( + lock => BackgroundTasksUtils.readPreferences(null, lock), + profile + ); + let expected = { + "testPref.bool1": true, + "testPref.bool2": false, + "testPref.int1": 23, + "testPref.int2": -1236, + }; + Assert.deepEqual(prefs, expected, "Prefs read are correct"); + + // And we can filter the read as well. + prefs = await BackgroundTasksUtils.withProfileLock( + lock => + BackgroundTasksUtils.readPreferences(name => name.endsWith("1"), lock), + profile + ); + expected = { + "testPref.bool1": true, + "testPref.int1": 23, + }; + Assert.deepEqual(prefs, expected, "Filtered prefs read are correct"); +}); + +add_task(async function test_readTelemetryClientID() { + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profilePath = do_get_profile(); + profilePath.append(`test_readTelemetryClientID`); + let profile = profileService.createUniqueProfile( + profilePath, + "test_readTelemetryClientID" + ); + + // Before we write any state, we fail to read. + await Assert.rejects( + BackgroundTasksUtils.withProfileLock( + lock => BackgroundTasksUtils.readTelemetryClientID(lock), + profile + ), + /NotFoundError/ + ); + + let expected = { + clientID: "9cc75672-6830-4cb6-a7fd-089d6c7ce34c", + ecosystemClientID: "752f9d53-73fc-4006-a93c-d3e2e427f238", + }; + let prefsFile = profile.rootDir.clone(); + prefsFile.append("datareporting"); + prefsFile.append("state.json"); + await IOUtils.writeUTF8(prefsFile.path, JSON.stringify(expected)); + + // Now we can read the state. + let state = await BackgroundTasksUtils.withProfileLock( + lock => BackgroundTasksUtils.readTelemetryClientID(lock), + profile + ); + Assert.deepEqual(state, expected.clientID, "State read is correct"); +}); + +add_task( + { + skip_if: () => AppConstants.MOZ_BUILD_APP !== "browser", // ASRouter is Firefox-only. + }, + async function test_readFirefoxMessagingSystemTargetingSnapshot() { + let profileService = Cc[ + "@mozilla.org/toolkit/profile-service;1" + ].getService(Ci.nsIToolkitProfileService); + + let profilePath = do_get_profile(); + profilePath.append(`test_readFirefoxMessagingSystemTargetingSnapshot`); + let profile = profileService.createUniqueProfile( + profilePath, + "test_readFirefoxMessagingSystemTargetingSnapshot" + ); + + // Before we write any state, we fail to read. + await Assert.rejects( + BackgroundTasksUtils.withProfileLock( + lock => + BackgroundTasksUtils.readFirefoxMessagingSystemTargetingSnapshot( + lock + ), + profile + ), + /NotFoundError/ + ); + + // We can't take a full environment snapshot under `xpcshell`. Select a few + // items that do work. + let target = { + // `Date` instances and strings do not `deepEqual` each other. + currentDate: JSON.stringify( + await lazy.ASRouterTargeting.Environment.currentDate + ), + firefoxVersion: lazy.ASRouterTargeting.Environment.firefoxVersion, + }; + let expected = await lazy.ASRouterTargeting.getEnvironmentSnapshot({ + targets: [target], + }); + + let snapshotFile = profile.rootDir.clone(); + snapshotFile.append("targeting.snapshot.json"); + await IOUtils.writeUTF8(snapshotFile.path, JSON.stringify(expected)); + + // Now we can read the state. + let state = await BackgroundTasksUtils.withProfileLock( + lock => + BackgroundTasksUtils.readFirefoxMessagingSystemTargetingSnapshot(lock), + profile + ); + Assert.deepEqual(state, expected, "State read is correct"); + } +); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_with_backgroundtask.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_with_backgroundtask.js new file mode 100644 index 0000000000..f713fe5e78 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_with_backgroundtask.js @@ -0,0 +1,50 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +add_task(async function test_manifest_with_backgroundtask() { + let bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + + Assert.equal(false, bts.isBackgroundTaskMode); + Assert.equal(null, bts.backgroundTaskName()); + + bts.overrideBackgroundTaskNameForTesting("test-task"); + + Assert.equal(true, bts.isBackgroundTaskMode); + Assert.equal("test-task", bts.backgroundTaskName()); + + // Load test components. + do_load_manifest("CatBackgroundTaskRegistrationComponents.manifest"); + + const expectedEntries = new Map([ + [ + "CatBackgroundTaskRegisteredComponent", + "@unit.test.com/cat-backgroundtask-registered-component;1", + ], + [ + "CatBackgroundTaskAlwaysRegisteredComponent", + "@unit.test.com/cat-backgroundtask-alwaysregistered-component;1", + ], + ]); + + // Verify the correct entries are registered in the "test-cat" category. + for (let { entry, value } of Services.catMan.enumerateCategory("test-cat")) { + ok(expectedEntries.has(entry), `${entry} is expected`); + Assert.equal( + value, + expectedEntries.get(entry), + "${entry} has correct value." + ); + expectedEntries.delete(entry); + } + print("Check that all of the expected entries have been deleted."); + Assert.deepEqual( + Array.from(expectedEntries.keys()), + [], + "All expected entries have been deleted." + ); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_without_backgroundtask.js b/toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_without_backgroundtask.js new file mode 100644 index 0000000000..422d9a49ff --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_without_backgroundtask.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=4 ts=4 sts=4 et + * 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/. */ + +add_task(async function test_without_backgroundtask() { + let bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + bts.overrideBackgroundTaskNameForTesting(null); + + Assert.equal(false, bts.isBackgroundTaskMode); + Assert.equal(null, bts.backgroundTaskName()); + + // Load test components. + do_load_manifest("CatBackgroundTaskRegistrationComponents.manifest"); + + const EXPECTED_ENTRIES = new Map([ + ["CatRegisteredComponent", "@unit.test.com/cat-registered-component;1"], + [ + "CatBackgroundTaskNotRegisteredComponent", + "@unit.test.com/cat-backgroundtask-notregistered-component;1", + ], + ]); + + // Verify the correct entries are registered in the "test-cat" category. + for (let { entry, value } of Services.catMan.enumerateCategory("test-cat")) { + ok(EXPECTED_ENTRIES.has(entry), `${entry} is expected`); + Assert.equal(EXPECTED_ENTRIES.get(entry), value); + EXPECTED_ENTRIES.delete(entry); + } + Assert.deepEqual( + Array.from(EXPECTED_ENTRIES.keys()), + [], + "All expected entries have been deleted." + ); +}); diff --git a/toolkit/components/backgroundtasks/tests/xpcshell/xpcshell.toml b/toolkit/components/backgroundtasks/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..229f929c6b --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/xpcshell.toml @@ -0,0 +1,64 @@ +[DEFAULT] +firefox-appdir = "browser" +skip-if = ["os == 'android'"] +head = "head.js" +support-files = [ + "CatBackgroundTaskRegistrationComponents.manifest", + "experiment.json", +] + +["test_backgroundtask_automaticrestart.js"] +run-if = ["os == 'win'"] + +["test_backgroundtask_deletes_profile.js"] + +["test_backgroundtask_exitcodes.js"] + +["test_backgroundtask_experiments.js"] +tags = "remote-settings" +run-if = ["buildapp == 'browser'"] +reason = "ASRouter is Firefox-only." + +["test_backgroundtask_help.js"] + +["test_backgroundtask_localization.js"] + +["test_backgroundtask_locked_profile.js"] + +["test_backgroundtask_minruntime.js"] + +["test_backgroundtask_no_output.js"] +skip-if = ["ccov"] +reason = "Bug 1804825: code coverage harness prints [CodeCoverage] output in early startup." + +["test_backgroundtask_not_ephemeral_profile.js"] +run-sequentially = "Uses global profile directory `DefProfRt`" + +["test_backgroundtask_policies.js"] + +["test_backgroundtask_profile_is_slim.js"] + +["test_backgroundtask_profile_service_configuration.js"] + +["test_backgroundtask_removeDirectory.js"] +skip-if = ["os == 'android'"] # MultiInstanceLock doesn't work on Android + +["test_backgroundtask_shouldprocessupdates.js"] + +["test_backgroundtask_simultaneous_instances.js"] + +["test_backgroundtask_specific_pref.js"] + +["test_backgroundtask_targeting.js"] +run-if = ["buildapp == 'browser'"] +reason = "ASRouter is Firefox-only." + +["test_backgroundtask_unique_profile.js"] + +["test_backgroundtask_update_sync_manager.js"] + +["test_backgroundtasksutils.js"] + +["test_manifest_with_backgroundtask.js"] + +["test_manifest_without_backgroundtask.js"] -- cgit v1.2.3