summaryrefslogtreecommitdiffstats
path: root/toolkit/components/backgroundtasks/tests
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/backgroundtasks/tests')
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_automaticrestart.sys.mjs52
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_backgroundtask_specific_pref.sys.mjs31
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs51
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_file_exists.sys.mjs30
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs23
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_localization.sys.mjs29
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_minruntime.sys.mjs13
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_no_output.sys.mjs13
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_not_ephemeral_profile.sys.mjs14
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs26
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_profile_is_slim.sys.mjs25
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_shouldnotprocessupdates.sys.mjs13
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs27
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs49
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_timeout.sys.mjs19
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_unique_profile.sys.mjs65
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_update_sync_manager.sys.mjs24
-rw-r--r--toolkit/components/backgroundtasks/tests/BackgroundTask_wait.sys.mjs17
-rw-r--r--toolkit/components/backgroundtasks/tests/browser/browser.toml8
-rw-r--r--toolkit/components/backgroundtasks/tests/browser/browser_backgroundtask_specific_pref.js23
-rw-r--r--toolkit/components/backgroundtasks/tests/browser/browser_xpcom_graph_wait.js406
-rw-r--r--toolkit/components/backgroundtasks/tests/browser/head.js15
-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
49 files changed, 3128 insertions, 0 deletions
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_automaticrestart.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_automaticrestart.sys.mjs
new file mode 100644
index 0000000000..dce6af083f
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_automaticrestart.sys.mjs
@@ -0,0 +1,52 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+export async function runBackgroundTask(commandLine) {
+ let pid = Services.appinfo.processID;
+ let finalProcessDelaySec = 10;
+ let waitTimeoutSec = 30;
+ let s = "";
+ for (let i = 0; i < commandLine.length; i++) {
+ if (i > 0) {
+ s += " ";
+ }
+ s += "'" + commandLine.getArgument(i) + "'";
+ }
+ console.log(`runBackgroundTask: automaticrestart ${pid}: '${s}'`);
+ let updateProcessor = Cc[
+ "@mozilla.org/updates/update-processor;1"
+ ].createInstance(Ci.nsIUpdateProcessor);
+ // When this background task is first called by test_backgroundtask_automaticrestart.js,
+ // it will enter the else-if "no-wait" block and launch a new background task. That
+ // task then re-enters this code in the if "restart-pid" block, and writes to the file.
+ // This file is then read by the original test file to verify that the new background
+ // task was launched and completed successfully.
+ if (commandLine.findFlag("restart-pid", false) != -1) {
+ await IOUtils.writeUTF8(commandLine.getArgument(1), `written from ${pid}`, {
+ mode: "overwrite",
+ });
+ // This timeout is meant to activate the waitForProcessExit consistently.
+ // There shouldn't be any race conditions here as we always wait for
+ // this process to exit before reading the file, but this makes what's
+ // happening a bit more apparent and prevents cases where this process
+ // can exit before we hit the waitForProcessExit call. This isn't an
+ // error case but it defeats the purpose of the test.
+ return new Promise(resolve =>
+ setTimeout(resolve, finalProcessDelaySec * 1000)
+ );
+ } else if (commandLine.getArgument(0) == "-no-wait") {
+ let newPid =
+ updateProcessor.attemptAutomaticApplicationRestartWithLaunchArgs([
+ "-test-only-automatic-restart-no-wait",
+ ]);
+ console.log(
+ `runBackgroundTask: spawned automatic restart task from ${pid} with pid ${newPid}`
+ );
+ updateProcessor.waitForProcessExit(newPid, waitTimeoutSec * 1000);
+ }
+ return 0;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_backgroundtask_specific_pref.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_backgroundtask_specific_pref.sys.mjs
new file mode 100644
index 0000000000..64c347a37f
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_backgroundtask_specific_pref.sys.mjs
@@ -0,0 +1,31 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export async function runBackgroundTask(commandLine) {
+ let pref = commandLine.length
+ ? commandLine.getArgument(0)
+ : "test.backgroundtask_specific_pref.exitCode";
+
+ // 0, 1, 2, 3 are all meaningful exit codes already.
+ let exitCode = Services.prefs.getIntPref(pref, 4);
+
+ console.error(
+ `runBackgroundTask: backgroundtask_specific_pref read pref '${pref}' with value ${exitCode}`
+ );
+
+ if (commandLine.length > 1) {
+ let newValue = Number.parseInt(commandLine.getArgument(1), 10);
+ console.error(
+ `runBackgroundTask: backgroundtask_specific_pref wrote pref '${pref}' with value ${newValue}`
+ );
+ Services.prefs.setIntPref(pref, newValue);
+ }
+
+ console.error(
+ `runBackgroundTask: backgroundtask_specific_pref exiting with exitCode ${exitCode}`
+ );
+
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs
new file mode 100644
index 0000000000..10764bc1f7
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_crash.sys.mjs
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export async function runBackgroundTask(commandLine) {
+ // This task depends on `CrashTestUtils.jsm` and requires the
+ // sibling `testcrasher` library to be in the current working
+ // directory. Fail right away if we can't find the module or the
+ // native library.
+ let cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ let protocolHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ var curDirURI = Services.io.newFileURI(cwd);
+ protocolHandler.setSubstitution("test", curDirURI);
+
+ const { CrashTestUtils } = ChromeUtils.importESModule(
+ "resource://test/CrashTestUtils.sys.mjs"
+ );
+
+ // Get the temp dir.
+ var tmpd = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ tmpd.initWithPath(Services.env.get("XPCSHELL_TEST_TEMP_DIR"));
+
+ // We need to call this or crash events go in an undefined location.
+ Services.appinfo.UpdateCrashEventsDir();
+
+ // Setting the minidump path is not allowed in content processes,
+ // but background tasks run in the parent.
+ Services.appinfo.minidumpPath = tmpd;
+
+ // Arguments are [crashType, key1, value1, key2, value2, ...].
+ let i = 0;
+ let crashType = Number.parseInt(commandLine.getArgument(i));
+ i += 1;
+ while (i + 1 < commandLine.length) {
+ let key = commandLine.getArgument(i);
+ let value = commandLine.getArgument(i + 1);
+ i += 2;
+ Services.appinfo.annotateCrashReport(key, value);
+ }
+
+ console.log(`Crashing with crash type ${crashType}`);
+
+ // Now actually crash.
+ CrashTestUtils.crash(crashType);
+
+ // This is moot, since we crashed, but...
+ return 1;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_file_exists.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_file_exists.sys.mjs
new file mode 100644
index 0000000000..6cfcb75291
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_file_exists.sys.mjs
@@ -0,0 +1,30 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+
+/**
+ * Return 0 (success) if the given absolute file path exists, 11
+ * (failure) otherwise.
+ */
+export function runBackgroundTask(commandLine) {
+ let path = commandLine.getArgument(0);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(path);
+
+ let exitCode;
+ let exists = file.exists();
+ if (exists) {
+ exitCode = EXIT_CODE.SUCCESS;
+ } else {
+ exitCode = 11;
+ }
+
+ console.error(
+ `runBackgroundTask: '${path}' exists: ${exists}; ` +
+ `exiting with status ${exitCode}`
+ );
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs
new file mode 100644
index 0000000000..45cf00a449
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_jsdebugger.sys.mjs
@@ -0,0 +1,23 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This task is intended to be interrupted by the JS debugger in tests.
+ *
+ * This task exposes its `exitCode` so that in the future the JS
+ * debugger can change its exit code dynamically from a failing exit
+ * code to exit code 0.
+ */
+
+export function runBackgroundTask(commandLine) {
+ // In the future, will be modifed by the JS debugger (to 0, success).
+ var exposedExitCode = 0;
+
+ console.error(
+ `runBackgroundTask: will exit with exitCode: ${exposedExitCode}`
+ );
+
+ return exposedExitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_localization.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_localization.sys.mjs
new file mode 100644
index 0000000000..48a67cc4de
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_localization.sys.mjs
@@ -0,0 +1,29 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+
+/**
+ * Return 0 (success) if in the given resource file, the given string
+ * identifier has the given string value, 11 (failure) otherwise.
+ */
+export async function runBackgroundTask(commandLine) {
+ let resource = commandLine.getArgument(0);
+ let id = commandLine.getArgument(1);
+ let expected = commandLine.getArgument(2);
+
+ let l10n = new Localization([resource]);
+ let value = await l10n.formatValue(id);
+
+ let exitCode = value == expected ? EXIT_CODE.SUCCESS : 11;
+
+ console.error(
+ `runBackgroundTask: in resource '${resource}': for id '${id}', ` +
+ `expected is '${expected}' and value is '${value}'; ` +
+ `exiting with status ${exitCode}`
+ );
+
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_minruntime.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_minruntime.sys.mjs
new file mode 100644
index 0000000000..2b6a98cc8f
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_minruntime.sys.mjs
@@ -0,0 +1,13 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+
+// Increase the minimum runtime before shutdown
+export const backgroundTaskMinRuntimeMS = 2000;
+
+export async function runBackgroundTask() {
+ return EXIT_CODE.SUCCESS;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_no_output.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_no_output.sys.mjs
new file mode 100644
index 0000000000..ed165f4206
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_no_output.sys.mjs
@@ -0,0 +1,13 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export async function runBackgroundTask(commandLine) {
+ // Exact same behaviour as `unique_profile`, but with a task name
+ // that is recognized as a task that should produce no output.
+ const taskModule = ChromeUtils.importESModule(
+ "resource://testing-common/backgroundtasks/BackgroundTask_unique_profile.sys.mjs"
+ );
+ return taskModule.runBackgroundTask(commandLine);
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_not_ephemeral_profile.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_not_ephemeral_profile.sys.mjs
new file mode 100644
index 0000000000..173beb56b2
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_not_ephemeral_profile.sys.mjs
@@ -0,0 +1,14 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export async function runBackgroundTask(commandLine) {
+ // Exact same behaviour as `backgroundtask_specific_pref`, but with
+ // a task name that is recognized as a task that should not use an
+ // ephemeral profile.
+ const taskModule = ChromeUtils.importESModule(
+ "resource://testing-common/backgroundtasks/BackgroundTask_backgroundtask_specific_pref.sys.mjs"
+ );
+ return taskModule.runBackgroundTask(commandLine);
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
new file mode 100644
index 0000000000..90bb71db8e
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
@@ -0,0 +1,26 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EnterprisePolicyTesting } from "resource://testing-common/EnterprisePolicyTesting.sys.mjs";
+
+export async function runBackgroundTask(commandLine) {
+ let filePath = commandLine.getArgument(0);
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson(filePath);
+
+ let checker = Cc["@mozilla.org/updates/update-checker;1"].getService(
+ Ci.nsIUpdateChecker
+ );
+ let actual = await checker.getUpdateURL(checker.BACKGROUND_CHECK);
+ let expected = commandLine.getArgument(1);
+
+ // 0, 1, 2, 3 are all meaningful exit codes already.
+ let exitCode = expected == actual ? 0 : 4;
+ console.error(
+ `runBackgroundTask: policies read AppUpdateURL '${actual}',
+ expected '${expected}', exiting with exitCode ${exitCode}`
+ );
+
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_profile_is_slim.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_profile_is_slim.sys.mjs
new file mode 100644
index 0000000000..e95a6f07e2
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_profile_is_slim.sys.mjs
@@ -0,0 +1,25 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A task that exercises various functionality to witness contents of
+// the temporary profile created during background tasks. This will
+// be a dumping ground for functionality that writes to the profile.
+
+import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+
+export async function runBackgroundTask(commandLine) {
+ console.error("runBackgroundTask: is_profile_slim");
+
+ // For now, just verify contents of profile after a network request.
+ if (commandLine.length != 1) {
+ console.error("Single URL argument required");
+ return 1;
+ }
+
+ let response = await fetch(commandLine.getArgument(0));
+ console.error(`response status code: ${response.status}`);
+
+ return response.ok ? EXIT_CODE.SUCCESS : 11;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldnotprocessupdates.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldnotprocessupdates.sys.mjs
new file mode 100644
index 0000000000..e869cd0d9d
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldnotprocessupdates.sys.mjs
@@ -0,0 +1,13 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export async function runBackgroundTask(commandLine) {
+ // Exact same behaviour as `shouldprocessupdates`, but with a task name that
+ // is not recognized as a task that should process updates.
+ const taskModule = ChromeUtils.importESModule(
+ "resource://testing-common/backgroundtasks/BackgroundTask_shouldprocessupdates.sys.mjs"
+ );
+ return taskModule.runBackgroundTask(commandLine);
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs
new file mode 100644
index 0000000000..4030f42e5d
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_shouldprocessupdates.sys.mjs
@@ -0,0 +1,27 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export async function runBackgroundTask(commandLine) {
+ const get = Services.env.get("MOZ_TEST_PROCESS_UPDATES");
+ let exitCode = 81;
+ if (get == "ShouldNotProcessUpdates(): OtherInstanceRunning") {
+ exitCode = 80;
+ }
+ if (get == "ShouldNotProcessUpdates(): DevToolsLaunching") {
+ exitCode = 79;
+ }
+ if (get == "ShouldNotProcessUpdates(): NotAnUpdatingTask") {
+ exitCode = 78;
+ }
+ console.debug(`runBackgroundTask: shouldprocessupdates`, {
+ exists: Services.env.exists("MOZ_TEST_PROCESS_UPDATES"),
+ get,
+ });
+ console.error(
+ `runBackgroundTask: shouldprocessupdates exiting with exitCode ${exitCode}`
+ );
+
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs
new file mode 100644
index 0000000000..fd47d18470
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.sys.mjs
@@ -0,0 +1,49 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+
+import { ASRouterTargeting } from "resource:///modules/asrouter/ASRouterTargeting.sys.mjs";
+
+// Background tasks are "live" with a temporary profile that doesn't map common
+// network preferences to https://mochi.test in the way that regular testing
+// profiles do. Therefore, certain targeting getters will fail due to non-local
+// network connections. Exclude these.
+const EXCLUDED_NAMES = [
+ "region", // Queries Mozilla Location Services.
+ "needsUpdate", // Queries Balrog update server.
+];
+
+/**
+ * Return 0 (success) if all targeting getters succeed, 11 (failure)
+ * otherwise.
+ */
+export async function runBackgroundTask(commandLine) {
+ let exitCode = EXIT_CODE.SUCCESS;
+
+ // Can't use `ASRouterTargeting.getEnvironmentSnapshot`, since that
+ // ignores errors, and this is testing that every getter succeeds.
+ let target = ASRouterTargeting.Environment;
+ let environment = {};
+ for (let name of Object.keys(target)) {
+ if (EXCLUDED_NAMES.includes(name)) {
+ continue;
+ }
+
+ try {
+ console.debug(`Fetching property ${name}`);
+ environment[name] = await target[name];
+ } catch (e) {
+ exitCode = 11;
+ console.error(`Caught exception for property ${name}:`, e);
+ }
+ }
+
+ console.log(`ASRouterTargeting.Environment:`, environment);
+
+ console.error(`runBackgroundTask: exiting with status ${exitCode}`);
+
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_timeout.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_timeout.sys.mjs
new file mode 100644
index 0000000000..357019486b
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_timeout.sys.mjs
@@ -0,0 +1,19 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+// Time out in just a single second. The task is set up to run for 5 minutes,
+// so it should always time out.
+export const backgroundTaskTimeoutSec = 1;
+
+export async function runBackgroundTask() {
+ await new Promise(resolve => {
+ const fiveMinutesInMs = 5 * 60 * 1000;
+ setTimeout(resolve, fiveMinutesInMs);
+ });
+ return EXIT_CODE.SUCCESS;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_unique_profile.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_unique_profile.sys.mjs
new file mode 100644
index 0000000000..741046292f
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_unique_profile.sys.mjs
@@ -0,0 +1,65 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Subprocess } from "resource://gre/modules/Subprocess.sys.mjs";
+
+export async function runBackgroundTask(commandLine) {
+ let sentinel = commandLine.getArgument(0);
+ let count =
+ commandLine.length > 1
+ ? Number.parseInt(commandLine.getArgument(1), 10)
+ : 1;
+
+ let main = await ChromeUtils.requestProcInfo();
+ let info = [main.pid, Services.dirsvc.get("ProfD", Ci.nsIFile).path];
+
+ // `dump` prints to the console without formatting.
+ dump(`${count}: ${sentinel}${JSON.stringify(info)}${sentinel}\n`);
+
+ // Maybe launch a child.
+ if (count <= 1) {
+ return 0;
+ }
+
+ let command = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
+ let args = [
+ "--backgroundtask",
+ "unique_profile",
+ sentinel,
+ (count - 1).toString(),
+ ];
+
+ // We must assemble all of the string fragments from stdout.
+ let stdoutChunks = [];
+ let proc = await Subprocess.call({
+ command,
+ arguments: args,
+ stderr: "stdout",
+ // Don't inherit this task's profile path.
+ environmentAppend: true,
+ environment: { XRE_PROFILE_PATH: null },
+ }).then(p => {
+ p.stdin.close();
+ const dumpPipe = async pipe => {
+ let data = await pipe.readString();
+ while (data) {
+ data = await pipe.readString();
+ stdoutChunks.push(data);
+ }
+ };
+ dumpPipe(p.stdout);
+
+ return p;
+ });
+
+ let { exitCode } = await proc.wait();
+
+ let stdout = stdoutChunks.join("");
+ for (let line of stdout.split(/\r\n|\r|\n/).slice(0, -1)) {
+ dump(`${count}> ${line}\n`);
+ }
+
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_update_sync_manager.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_update_sync_manager.sys.mjs
new file mode 100644
index 0000000000..dfdbd817e9
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_update_sync_manager.sys.mjs
@@ -0,0 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export async function runBackgroundTask(commandLine) {
+ let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
+ Ci.nsIUpdateSyncManager
+ );
+
+ if (commandLine.length) {
+ let appPath = commandLine.getArgument(0);
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(appPath);
+ syncManager.resetLock(file);
+ }
+
+ let exitCode = syncManager.isOtherInstanceRunning() ? 80 : 81;
+ console.error(
+ `runBackgroundTask: update_sync_manager exiting with exitCode ${exitCode}`
+ );
+
+ return exitCode;
+}
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_wait.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_wait.sys.mjs
new file mode 100644
index 0000000000..ae940faaa2
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_wait.sys.mjs
@@ -0,0 +1,17 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+export function runBackgroundTask(commandLine) {
+ let delay = 10;
+ if (commandLine.length) {
+ delay = Number.parseInt(commandLine.getArgument(0));
+ }
+
+ console.error(`runBackgroundTask: wait ${delay} seconds`);
+
+ return new Promise(resolve => setTimeout(resolve, delay * 1000));
+}
diff --git a/toolkit/components/backgroundtasks/tests/browser/browser.toml b/toolkit/components/backgroundtasks/tests/browser/browser.toml
new file mode 100644
index 0000000000..80bc728294
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/browser/browser.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+head = "head.js"
+
+["browser_backgroundtask_specific_pref.js"]
+
+["browser_xpcom_graph_wait.js"]
+skip-if = ["tsan"] # TSan times out on pretty much all profiler-consuming tests.
diff --git a/toolkit/components/backgroundtasks/tests/browser/browser_backgroundtask_specific_pref.js b/toolkit/components/backgroundtasks/tests/browser/browser_backgroundtask_specific_pref.js
new file mode 100644
index 0000000000..b80ee2f593
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/browser/browser_backgroundtask_specific_pref.js
@@ -0,0 +1,23 @@
+/* -*- 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_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.
+ // mochitest-chrome tests locally test both unpackaged and packaged
+ // builds (with `--appname=dist`).
+ 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/browser/browser_xpcom_graph_wait.js b/toolkit/components/backgroundtasks/tests/browser/browser_xpcom_graph_wait.js
new file mode 100644
index 0000000000..501b50fa7a
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/browser/browser_xpcom_graph_wait.js
@@ -0,0 +1,406 @@
+/* -*- 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 test records code loaded during a dummy background task.
+ *
+ * To run this test similar to try server, you need to run:
+ * ./mach package
+ * ./mach test --appname=dist <path to test>
+ *
+ * If you made changes that cause this test to fail, it's likely
+ * because you are changing the application startup process. In
+ * general, you should prefer to defer loading code as long as you
+ * can, especially if it's not going to be used in background tasks.
+ */
+
+"use strict";
+
+const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+// Shortcuts for conditions.
+const LINUX = AppConstants.platform == "linux";
+const WIN = AppConstants.platform == "win";
+const MAC = AppConstants.platform == "macosx";
+
+const backgroundtaskPhases = {
+ AfterRunBackgroundTaskNamed: {
+ allowlist: {
+ modules: [
+ "resource://gre/modules/AppConstants.sys.mjs",
+ "resource://gre/modules/AsyncShutdown.sys.mjs",
+ "resource://gre/modules/BackgroundTasksManager.sys.mjs",
+ "resource://gre/modules/Console.sys.mjs",
+ "resource://gre/modules/EnterprisePolicies.sys.mjs",
+ "resource://gre/modules/EnterprisePoliciesParent.sys.mjs",
+ "resource://gre/modules/XPCOMUtils.sys.mjs",
+ "resource://gre/modules/nsAsyncShutdown.sys.mjs",
+ ],
+ // Human-readable contract IDs are many-to-one mapped to CIDs, so this
+ // list is a little misleading. For example, all of
+ // "@mozilla.org/xre/app-info;1", "@mozilla.org/xre/runtime;1", and
+ // "@mozilla.org/toolkit/crash-reporter;1", map to the CID
+ // {95d89e3e-a169-41a3-8e56-719978e15b12}, but only one is listed here.
+ // We could be more precise by listing CIDs, but that's a good deal harder
+ // to read and modify.
+ services: [
+ "@mozilla.org/async-shutdown-service;1",
+ "@mozilla.org/backgroundtasks;1",
+ "@mozilla.org/backgroundtasksmanager;1",
+ "@mozilla.org/base/telemetry;1",
+ "@mozilla.org/categorymanager;1",
+ "@mozilla.org/chrome/chrome-registry;1",
+ "@mozilla.org/cookieService;1",
+ "@mozilla.org/docloaderservice;1",
+ "@mozilla.org/embedcomp/window-watcher;1",
+ "@mozilla.org/enterprisepolicies;1",
+ "@mozilla.org/file/directory_service;1",
+ "@mozilla.org/intl/stringbundle;1",
+ "@mozilla.org/layout/content-policy;1",
+ "@mozilla.org/memory-reporter-manager;1",
+ "@mozilla.org/network/captive-portal-service;1",
+ "@mozilla.org/network/effective-tld-service;1",
+ "@mozilla.org/network/idn-service;1",
+ "@mozilla.org/network/io-service;1",
+ "@mozilla.org/network/network-link-service;1",
+ "@mozilla.org/network/protocol;1?name=file",
+ "@mozilla.org/network/protocol;1?name=jar",
+ "@mozilla.org/network/protocol;1?name=resource",
+ "@mozilla.org/network/socket-transport-service;1",
+ "@mozilla.org/network/stream-transport-service;1",
+ "@mozilla.org/network/url-parser;1?auth=maybe",
+ "@mozilla.org/network/url-parser;1?auth=no",
+ "@mozilla.org/network/url-parser;1?auth=yes",
+ "@mozilla.org/observer-service;1",
+ "@mozilla.org/power/powermanagerservice;1",
+ "@mozilla.org/preferences-service;1",
+ "@mozilla.org/process/environment;1",
+ "@mozilla.org/storage/service;1",
+ "@mozilla.org/thirdpartyutil;1",
+ "@mozilla.org/toolkit/app-startup;1",
+ {
+ name: "@mozilla.org/widget/appshell/mac;1",
+ condition: MAC,
+ },
+ {
+ name: "@mozilla.org/widget/appshell/gtk;1",
+ condition: LINUX,
+ },
+ {
+ name: "@mozilla.org/widget/appshell/win;1",
+ condition: WIN,
+ },
+ "@mozilla.org/xpcom/debug;1",
+ "@mozilla.org/xre/app-info;1",
+ "@mozilla.org/mime;1",
+ ],
+ },
+ },
+ AfterFindRunBackgroundTask: {
+ allowlist: {
+ modules: [
+ // We have a profile marker for this, even though it failed to load!
+ "resource:///modules/backgroundtasks/BackgroundTask_wait.sys.mjs",
+
+ "resource://gre/modules/ConsoleAPIStorage.sys.mjs",
+ "resource://gre/modules/Timer.sys.mjs",
+
+ // We have a profile marker for this, even though it failed to load!
+ "resource://gre/modules/backgroundtasks/BackgroundTask_wait.sys.mjs",
+
+ "resource://testing-common/backgroundtasks/BackgroundTask_wait.sys.mjs",
+ ],
+ services: ["@mozilla.org/consoleAPI-storage;1"],
+ },
+ },
+ AfterAwaitRunBackgroundTask: {
+ allowlist: {
+ modules: [],
+ services: [],
+ },
+ },
+};
+
+function getStackFromProfile(profile, stack, libs) {
+ const stackPrefixCol = profile.stackTable.schema.prefix;
+ const stackFrameCol = profile.stackTable.schema.frame;
+ const frameLocationCol = profile.frameTable.schema.location;
+
+ let index = 1;
+ let result = [];
+ while (stack) {
+ let sp = profile.stackTable.data[stack];
+ let frame = profile.frameTable.data[sp[stackFrameCol]];
+ stack = sp[stackPrefixCol];
+ frame = profile.stringTable[frame[frameLocationCol]];
+
+ if (frame.startsWith("0x")) {
+ try {
+ let addr = frame.slice("0x".length);
+ addr = Number.parseInt(addr, 16);
+ for (let lib of libs) {
+ if (lib.start <= addr && addr <= lib.end) {
+ // Only handle two digits for now.
+ let indexString = index.toString(10);
+ if (indexString.length == 1) {
+ indexString = "0" + indexString;
+ }
+ let offset = addr - lib.start;
+ frame = `#${indexString}: ???[${lib.debugPath} ${
+ "+0x" + offset.toString(16)
+ }]`;
+ break;
+ }
+ }
+ } catch (e) {
+ // Fall through.
+ }
+ }
+
+ if (frame != "js::RunScript" && !frame.startsWith("next (self-hosted:")) {
+ result.push(frame);
+ index += 1;
+ }
+ }
+ return result;
+}
+
+add_task(async function test_xpcom_graph_wait() {
+ TestUtils.assertPackagedBuild();
+
+ let profilePath = Services.env.get("MOZ_UPLOAD_DIR");
+ profilePath =
+ profilePath ||
+ (await IOUtils.createUniqueDirectory(
+ PathUtils.profileDir,
+ "testBackgroundTask",
+ 0o700
+ ));
+
+ profilePath = PathUtils.join(profilePath, "profile_backgroundtask_wait.json");
+ await IOUtils.remove(profilePath, { ignoreAbsent: true });
+
+ let extraEnv = {
+ MOZ_PROFILER_STARTUP: "1",
+ MOZ_PROFILER_SHUTDOWN: profilePath,
+ };
+
+ let exitCode = await do_backgroundtask("wait", { extraEnv });
+ Assert.equal(0, exitCode);
+
+ let rootProfile = await IOUtils.readJSON(profilePath);
+ let profile = rootProfile.threads[0];
+
+ const nameCol = profile.markers.schema.name;
+ const dataCol = profile.markers.schema.data;
+
+ function newMarkers() {
+ return {
+ // The equivalent of `Cu.loadedJSModules` + `Cu.loadedESModules`.
+ modules: [],
+ services: [],
+ };
+ }
+
+ let phases = {};
+ let markersForCurrentPhase = newMarkers();
+
+ // If a subsequent phase loads an already loaded resource, that's
+ // fine. Track all loaded resources to ignore such repeated loads.
+ let markersForAllPhases = newMarkers();
+
+ for (let m of profile.markers.data) {
+ let markerName = profile.stringTable[m[nameCol]];
+ if (markerName.startsWith("BackgroundTasksManager:")) {
+ phases[markerName.split("BackgroundTasksManager:")[1]] =
+ markersForCurrentPhase;
+ markersForCurrentPhase = newMarkers();
+ continue;
+ }
+
+ if (
+ ![
+ "ChromeUtils.import", // JSMs.
+ "ChromeUtils.importESModule", // System ESMs.
+ "ChromeUtils.importESModule static import",
+ "GetService", // XPCOM services.
+ ].includes(markerName)
+ ) {
+ continue;
+ }
+
+ let markerData = m[dataCol];
+ if (
+ markerName == "ChromeUtils.import" ||
+ markerName == "ChromeUtils.importESModule" ||
+ markerName == "ChromeUtils.importESModule static import"
+ ) {
+ let module = markerData.name;
+ if (!markersForAllPhases.modules.includes(module)) {
+ markersForAllPhases.modules.push(module);
+ markersForCurrentPhase.modules.push(module);
+ }
+ }
+
+ if (markerName == "GetService") {
+ // We get a CID from the marker itself, but not a human-readable contract
+ // ID. Now, most of the time, the stack will contain a label like
+ // `GetServiceByContractID @...;1`, and we could extract the contract ID
+ // from that. But there are multiple ways to instantiate services, and
+ // not all of them are annotated in this manner. Therefore we "go the
+ // other way" and use the component manager's mapping from contract IDs to
+ // CIDs. This opens up the possibility for that mapping to be different
+ // between `--backgroundtask` and `xpcshell`, but that's not an issue
+ // right at this moment. It's worth noting that one CID can (and
+ // sometimes does) correspond to more than one contract ID.
+ let cid = markerData.name;
+
+ if (!markersForAllPhases.services.includes(cid)) {
+ markersForAllPhases.services.push(cid);
+ markersForCurrentPhase.services.push(cid);
+ }
+ }
+ }
+
+ // Turn `["1", {name: "2", condition: false}, {name: "3", condition: true}]`
+ // into `new Set(["1", "3"])`.
+ function filterConditions(l) {
+ let set = new Set([]);
+ for (let entry of l) {
+ if (typeof entry == "object") {
+ if ("condition" in entry && !entry.condition) {
+ continue;
+ }
+ entry = entry.name;
+ }
+ set.add(entry);
+ }
+ return set;
+ }
+
+ for (let phaseName in backgroundtaskPhases) {
+ for (let listName in backgroundtaskPhases[phaseName]) {
+ for (let scriptType in backgroundtaskPhases[phaseName][listName]) {
+ backgroundtaskPhases[phaseName][listName][scriptType] =
+ filterConditions(
+ backgroundtaskPhases[phaseName][listName][scriptType]
+ );
+ }
+
+ // Turn human-readable contract IDs into CIDs. It's worth noting that one
+ // CID can (and sometimes does) correspond to more than one contract ID.
+ let services = Array.from(
+ backgroundtaskPhases[phaseName][listName].services || new Set([])
+ );
+ services = services
+ .map(contractID => {
+ try {
+ return Cm.contractIDToCID(contractID).toString();
+ } catch (e) {
+ return null;
+ }
+ })
+ .filter(cid => cid);
+ services.sort();
+ backgroundtaskPhases[phaseName][listName].services = new Set(services);
+ info(
+ `backgroundtaskPhases[${phaseName}][${listName}].services = ${JSON.stringify(
+ services.map(c => c.toString())
+ )}`
+ );
+ }
+ }
+
+ // Turn `{CID}` into `{CID} (@contractID)` or `{CID} (one of
+ // @contractID1, ..., @contractIDn)` as appropriate.
+ function renderResource(resource) {
+ const UUID_PATTERN =
+ /^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/i;
+ if (UUID_PATTERN.test(resource)) {
+ let foundContractIDs = [];
+ for (let contractID of Cm.getContractIDs()) {
+ try {
+ if (resource == Cm.contractIDToCID(contractID).toString()) {
+ foundContractIDs.push(contractID);
+ }
+ } catch (e) {
+ // This can throw for contract IDs that are filtered. The common
+ // reason is that they're limited to a particular process.
+ }
+ }
+ if (!foundContractIDs.length) {
+ return `${resource} (CID with no human-readable contract IDs)`;
+ } else if (foundContractIDs.length == 1) {
+ return `${resource} (CID with human-readable contract ID ${foundContractIDs[0]})`;
+ }
+ foundContractIDs.sort();
+ return `${resource} (CID with human-readable contract IDs ${JSON.stringify(
+ foundContractIDs
+ )})`;
+ }
+
+ return resource;
+ }
+
+ for (let phase in backgroundtaskPhases) {
+ let loadedList = phases[phase];
+ let allowlist = backgroundtaskPhases[phase].allowlist || null;
+ if (allowlist) {
+ for (let scriptType in allowlist) {
+ loadedList[scriptType] = loadedList[scriptType].filter(c => {
+ if (!allowlist[scriptType].has(c)) {
+ return true;
+ }
+ allowlist[scriptType].delete(c);
+ return false;
+ });
+ Assert.deepEqual(
+ loadedList[scriptType],
+ [],
+ `${phase}: should have no unexpected ${scriptType} loaded`
+ );
+
+ // Present errors in deterministic order.
+ let unexpected = Array.from(loadedList[scriptType]);
+ unexpected.sort();
+ for (let script of unexpected) {
+ // It would be nice to show stacks here, but that can be follow-up.
+ ok(
+ false,
+ `${phase}: unexpected ${scriptType}: ${renderResource(script)}`
+ );
+ }
+ Assert.deepEqual(
+ allowlist[scriptType].size,
+ 0,
+ `${phase}: all ${scriptType} allowlist entries should have been used`
+ );
+ let unused = Array.from(allowlist[scriptType]);
+ unused.sort();
+ for (let script of unused) {
+ ok(
+ false,
+ `${phase}: unused ${scriptType} allowlist entry: ${renderResource(
+ script
+ )}`
+ );
+ }
+ }
+ }
+ let denylist = backgroundtaskPhases[phase].denylist || null;
+ if (denylist) {
+ for (let scriptType in denylist) {
+ let resources = denylist[scriptType];
+ resources.sort();
+ for (let resource of resources) {
+ let loaded = loadedList[scriptType].includes(resource);
+ let message = `${phase}: ${renderResource(resource)} is not allowed`;
+ // It would be nice to show stacks here, but that can be follow-up.
+ ok(!loaded, message);
+ }
+ }
+ }
+ }
+});
diff --git a/toolkit/components/backgroundtasks/tests/browser/head.js b/toolkit/components/backgroundtasks/tests/browser/head.js
new file mode 100644
index 0000000000..703a3d64c9
--- /dev/null
+++ b/toolkit/components/backgroundtasks/tests/browser/head.js
@@ -0,0 +1,15 @@
+/* -*- 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 { BackgroundTasksTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BackgroundTasksTestUtils.sys.mjs"
+);
+BackgroundTasksTestUtils.init(this);
+const do_backgroundtask = BackgroundTasksTestUtils.do_backgroundtask.bind(
+ BackgroundTasksTestUtils
+);
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"]