diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/components/backgroundtasks/tests | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/backgroundtasks/tests')
47 files changed, 2926 insertions, 0 deletions
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..ce04bd1bdf --- /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.import( + "resource://testing-common/backgroundtasks/BackgroundTask_unique_profile.jsm" + ); + 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..e8a6d882af --- /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.import( + "resource://testing-common/backgroundtasks/BackgroundTask_backgroundtask_specific_pref.jsm" + ); + 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..a879265b44 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_targeting.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/. */ + +import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs"; + +const { ASRouterTargeting } = ChromeUtils.import( + "resource://activity-stream/lib/ASRouterTargeting.jsm" +); + +// 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.ini b/toolkit/components/backgroundtasks/tests/browser/browser.ini new file mode 100644 index 0000000000..1524d431b4 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/browser/browser.ini @@ -0,0 +1,11 @@ +# 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/. + +[DEFAULT] +skip-if = toolkit == '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..c5ec373e08 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/browser/browser_xpcom_graph_wait.js @@ -0,0 +1,407 @@ +/* -*- 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/PromiseUtils.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_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..087c557acf --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_experiments.js @@ -0,0 +1,416 @@ +/* -*- 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.import( + "resource://activity-stream/lib/ASRouterTargeting.jsm" +); + +// 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; + +// Arrange a dummy Remote Settings server so that no non-local network +// connections are opened. +// And arrange dummy task profile. +add_setup(() => { + 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); + }); +}); + +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], +]; + +// 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(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(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..06f28616b4 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_profile_is_slim.js @@ -0,0 +1,136 @@ +/* -*- 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.import("resource://testing-common/httpd.js"); +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..6fb1ee6f27 --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtask_removeDirectory.js @@ -0,0 +1,245 @@ +/* -*- 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`); + } +}); 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..f16766c23c --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/test_backgroundtasksutils.js @@ -0,0 +1,198 @@ +/* -*- 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; +XPCOMUtils.defineLazyModuleGetters(lazy, { + ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm", +}); + +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(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.ini b/toolkit/components/backgroundtasks/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..25878ce0fd --- /dev/null +++ b/toolkit/components/backgroundtasks/tests/xpcshell/xpcshell.ini @@ -0,0 +1,43 @@ +# 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/. + +[DEFAULT] +firefox-appdir = browser +skip-if = toolkit == 'android' +head = head.js +support-files = + CatBackgroundTaskRegistrationComponents.manifest + experiment.json + +[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] |