summaryrefslogtreecommitdiffstats
path: root/toolkit/components/backgroundtasks/tests/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/backgroundtasks/tests/xpcshell')
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/CatBackgroundTaskRegistrationComponents.manifest4
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/experiment.json102
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/head.js22
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_automaticrestart.js45
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_deletes_profile.js128
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_exitcodes.js49
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js449
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_help.js20
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_localization.js40
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_locked_profile.js28
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_minruntime.js21
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_no_output.js57
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_not_ephemeral_profile.js24
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_policies.js45
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_is_slim.js138
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_service_configuration.js64
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_removeDirectory.js301
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_shouldprocessupdates.js45
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_simultaneous_instances.js100
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_specific_pref.js20
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_targeting.js17
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_unique_profile.js39
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_update_sync_manager.js48
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtasksutils.js197
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_with_backgroundtask.js50
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/test_manifest_without_backgroundtask.js38
-rw-r--r--toolkit/components/backgroundtasks/tests/xpcshell/xpcshell.toml64
27 files changed, 2155 insertions, 0 deletions
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"]