tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 0a437e60ab98a0ce22ace3522192cf31288818cf
parent 0fb9775c851781926ccb039a06ec513d5fe152bd
Author: Ben Chatterton <bchatterton@mozilla.com>
Date:   Thu, 20 Nov 2025 21:07:45 +0000

Bug 1994228: Run limited update test set on push_firefox, r=taskgraph-reviewers,bhearsum

Differential Revision: https://phabricator.services.mozilla.com/D272050

Diffstat:
Mtaskcluster/config.yml | 2+-
Mtaskcluster/gecko_taskgraph/transforms/update_test.py | 49++++++++++++++++++++++++++++++++++---------------
Mtaskcluster/kinds/update-test/kind.yml | 34++++++++++++++++++++++++++++++++--
Mtesting/update/mach_commands.py | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mtesting/update/test_apply_update.py | 20++++++++++++++++----
Mtesting/update/test_background_update.py | 33+++++++++++++++++++++++++--------
6 files changed, 167 insertions(+), 42 deletions(-)

diff --git a/taskcluster/config.yml b/taskcluster/config.yml @@ -194,7 +194,7 @@ treeherder: 'Boot': 'Bootstrap' 'Attr-L10n': 'Build Attribution' 'android-l10n': 'Import strings from android-l10n repo' - 'update-test': 'Test updates to release from older firefox versions' + 'updt-tst': 'Test updates to release from older firefox versions' # The below symbols are from firefox-android (fenix/focus/android-components) 'buildconfig': 'Make sure the content of .buildconfig.yml matches what gradle knows about its projects' diff --git a/taskcluster/gecko_taskgraph/transforms/update_test.py b/taskcluster/gecko_taskgraph/transforms/update_test.py @@ -12,7 +12,7 @@ from taskgraph.util.copy import deepcopy from taskgraph.util.schema import resolve_keyed_by from typing_extensions import final -from gecko_taskgraph.util.attributes import task_name +from gecko_taskgraph.util.attributes import is_try, task_name @final @@ -65,15 +65,15 @@ def infix_treeherder_symbol(symbol, infix): @transforms.add def set_task_configuration(config, tasks): release_type = ReleaseType.release - config_tasks = {} if config.params["release_type"] == "beta": release_type = ReleaseType.beta elif config.params["release_type"].startswith("esr"): esr_version = int(config.params["release_type"].split("esr")[1]) release_type = ReleaseType.esr if esr_version < ESR_SUPPORT_CUTOFF: - return None + yield None + config_tasks = {} for dep in config.kind_dependencies_tasks.values(): if "update-verify-config" in dep.kind: config_tasks[task_name(dep)] = dep @@ -107,17 +107,36 @@ def set_task_configuration(config, tasks): del this_task["test-platforms"] if this_task["shipping-product"] == "firefox": - if release_type == ReleaseType.beta: - this_task["name"] = this_task["name"] + "-beta" - this_task["run"]["command"] = ( - this_task["run"]["command"] + " --channel beta-localtest" + product_channel = release_type.name.lower() + if this_task["shipping-phase"] == "promote": + update_channel = "-localtest" + elif this_task["shipping-phase"] == "push": + update_channel = "-cdntest" + else: + raise OSError("Expected promote or push shipping-phase.") + + if this_task["shipping-phase"] != "promote": + this_task["treeherder"]["symbol"] = infix_treeherder_symbol( + this_task["treeherder"]["symbol"], + this_task["shipping-phase"][:6], ) - elif release_type == ReleaseType.esr: - this_task["name"] = this_task["name"] + "-esr" + if release_type != ReleaseType.release: + this_task["name"] = this_task["name"] + f"-{product_channel}" + this_task["run"]["command"] = ( + this_task["run"]["command"] + + f" --channel {product_channel}{update_channel}" + ) + + if release_type == ReleaseType.esr: this_task["run"]["command"] = ( - this_task["run"]["command"] - + f" --channel esr-localtest --esr-version {esr_version}" + this_task["run"]["command"] + f" --esr-version {esr_version}" ) + + if is_try(config.params): + this_task["run"]["command"] = ( + this_task["run"]["command"] + " --use-balrog-staging" + ) + this_task["worker"]["env"]["BALROG_STAGING"] = "1" this_task["name"] = this_task["name"].replace("linux-docker-", "") this_task["index"]["job-name"] = "update-test-" + this_task["name"] @@ -152,14 +171,14 @@ def parametrize_by_locale(config, tasks): this_task["run"]["command"] + f" --source-locale {locale}" ) this_task["description"] = ( - f'{this_task["description"]}, locale coverage: {locale}' + f"{this_task['description']}, locale coverage: {locale}" ) this_task["name"] = this_task["name"].replace("locale", locale) this_task["index"][ "job-name" - ] = f'{this_task["index"]["job-name"]}-{locale}' + ] = f"{this_task['index']['job-name']}-{locale}" this_task["treeherder"]["symbol"] = infix_treeherder_symbol( - this_task["treeherder"]["symbol"], locale + this_task["treeherder"]["symbol"], locale.replace("-", "") ) yield this_task @@ -190,7 +209,7 @@ def parametrize_by_source_version(config, tasks): this_task["name"] = this_task["name"].replace("-source-version", ago_tag) this_task["index"]["job-name"] = this_task["index"]["job-name"] + ago_tag this_task["treeherder"]["symbol"] = infix_treeherder_symbol( - this_task["treeherder"]["symbol"], ago_tag.split("-", 2)[-1] + this_task["treeherder"]["symbol"], "oldst" if v == 0 else f"bk{v}" ) yield this_task diff --git a/taskcluster/kinds/update-test/kind.yml b/taskcluster/kinds/update-test/kind.yml @@ -20,13 +20,12 @@ transforms: task-defaults: run-on-repo-type: [hg] - shipping-phase: promote shipping-product: firefox run-on-releases: [release, release-rc, beta, esr140] treeherder: kind: other tier: 2 - symbol: update-test(firefox) + symbol: updt-tst(fx) worker: max-run-time: 10800 artifacts: @@ -54,6 +53,7 @@ task-defaults: tasks: update-test-locale-fx: + shipping-phase: promote test-platforms: - windows11-64-24h2-shippable - windows10-64-2009-shippable-qr @@ -62,6 +62,7 @@ tasks: - macosx1015-64-shippable-qr update-test-linux-docker-locale-fx: + shipping-phase: promote worker: artifacts: - type: directory @@ -77,6 +78,7 @@ tasks: - linux2404-64-shippable update-test-source-version-fx: + shipping-phase: promote test-platforms: - windows11-64-24h2-shippable - windows10-64-2009-shippable-qr @@ -85,6 +87,7 @@ tasks: - macosx1015-64-shippable-qr update-test-linux-docker-source-version-fx: + shipping-phase: promote worker: artifacts: - type: directory @@ -100,6 +103,7 @@ tasks: - linux2404-64-shippable update-test-fx-bkg: + shipping-phase: promote run: command: >- ./mach update-test --test-type Background @@ -111,6 +115,7 @@ tasks: - macosx1015-64-shippable-qr update-test-linux-docker-fx-bkg: + shipping-phase: promote worker: artifacts: - type: directory @@ -124,3 +129,28 @@ tasks: . $HOME/scripts/xvfb.sh && start_xvfb '1600x1200x24' 0 && ./mach update-test --test-type Background test-platforms: - linux2404-64-shippable + + update-test-push-locale-fx: + run-on-releases: [release, esr140] + shipping-phase: push + test-platforms: + - windows11-64-24h2-shippable + - macosx1500-aarch64-shippable + + update-test-linux-docker-push-locale-fx: + run-on-releases: [release, esr140] + shipping-phase: push + worker-type: t-linux-docker-amd + worker: + artifacts: + - type: directory + path: /builds/worker/artifacts/update-test + name: public/update-test + env: + UPLOAD_DIR: /builds/worker/artifacts + VERSION_LOG_FILENAME: update-test-version-info.log + run: + command: >- + . $HOME/scripts/xvfb.sh && start_xvfb '1600x1200x24' 0 && ./mach update-test + test-platforms: + - linux2404-64-shippable diff --git a/testing/update/mach_commands.py b/testing/update/mach_commands.py @@ -2,6 +2,7 @@ # 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 json import logging import re import subprocess @@ -21,6 +22,12 @@ from mozbuild.base import BinaryNotFoundException from mozlog.structured import commandline from mozrelease.update_verify import UpdateVerifyConfig +STAGING_POLICY_PAYLOAD = { + "policies": { + "AppUpdateURL": "https://stage.balrog.nonprod.cloudops.mozgcp.net/update/6/Firefox/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml" + } +} + @dataclass class UpdateTestConfig: @@ -40,6 +47,7 @@ class UpdateTestConfig: config_source = None release_type: ReleaseType = ReleaseType.release esr_version = None + staging_update = False def __post_init__(self): if environ.get("UPLOAD_DIR"): @@ -165,8 +173,16 @@ def get_valid_source_versions(config): """ ftp_content = requests.get(config.ftp_server).content.decode() # All versions start with e.g. 140.0, so beta and release can be int'ed - latest_version = int(config.target_version.split(".")[0]) - major_minor = ".".join(config.target_version.split(".")[:1]) + ver_head, ver_tail = config.target_version.split(".", 1) + latest_version = int(ver_head) + latest_minor_str = "" + # Versions like 130.10.1 and 130.0 are possible, capture the minor number + for c in ver_tail: + try: + int(c) + latest_minor_str = latest_minor_str + c + except ValueError: + break valid_versions: list[str] = [] for major in range(latest_version - config.major_version_range, latest_version + 1): @@ -174,17 +190,20 @@ def get_valid_source_versions(config): if config.release_type == ReleaseType.esr and major != latest_version: continue for minor in range(0, 11): - if f"{major}.{minor}" == major_minor: - break - elif f"/{major}.{minor}/" in ftp_content: - minor_versions.append(minor) - valid_versions.append(f"{major}.{minor}") - elif ( - config.release_type == ReleaseType.esr - and f"/{major}.{minor}esr/" in ftp_content + if ( + config.release_type == ReleaseType.release + and f"/{major}.{minor}/" in ftp_content ): + if f"{major}.{minor}" == config.target_version: + break minor_versions.append(minor) valid_versions.append(f"{major}.{minor}") + elif config.release_type == ReleaseType.esr and re.compile( + rf"/{major}\.{minor}.*/" + ).search(ftp_content): + minor_versions.append(minor) + if f"/{major}.{minor}esr" in ftp_content: + valid_versions.append(f"{major}.{minor}") elif config.release_type == ReleaseType.beta and minor == 0: # Release 1xx.0 is not available, but 1xx.0b1 is: minor_versions.append(minor) @@ -192,7 +211,7 @@ def get_valid_source_versions(config): sep = "b" if config.release_type == ReleaseType.beta else "." for minor in minor_versions: - for dot in range(1, 15): + for dot in range(0, 15): if f"{major}.{minor}{sep}{dot}" == config.target_version: break if config.release_type == ReleaseType.esr: @@ -275,6 +294,25 @@ def get_binary_path(config: UpdateTestConfig, **kwargs) -> str: fh.write(response.content) fx_location = mozinstall.install(installer_filename, installed_app_dir) print(f"Firefox installed to {fx_location}") + + if config.staging_update: + print("Writing enterprise policy for update server") + fx_path = Path(fx_location) + policy_path = None + if mozinfo.os in ["linux", "win"]: + policy_path = fx_path / "distribution" + elif mozinfo.os == "mac": + policy_path = fx_path / "Contents" / "Resources" / "distribution" + else: + raise ValueError("Invalid OS.") + makedirs(policy_path) + policy_loc = policy_path / "policies.json" + print(f"Creating {policy_loc}...") + with policy_loc.open("w") as fh: + json.dump(STAGING_POLICY_PAYLOAD, fh, indent=2) + with policy_loc.open() as fh: + print(fh.read()) + return fx_location @@ -299,6 +337,9 @@ def get_binary_path(config: UpdateTestConfig, **kwargs) -> str: help="ESR version, if set with --channel=esr, will only update within ESR major version", ) @CommandArgument("--uv-config-file", help="Update Verify config file") +@CommandArgument( + "--use-balrog-staging", action="store_true", help="Update from staging, not prod" +) def build(command_context, binary_path, **kwargs): config = UpdateTestConfig() @@ -337,6 +378,9 @@ def build(command_context, binary_path, **kwargs): config.tempdir = tempdir_name test_type = kwargs.get("test_type") + if kwargs.get("use_balrog_staging"): + config.staging_update = True + # Select update channel if kwargs.get("channel"): config.set_channel(kwargs["channel"], kwargs.get("esr_version")) @@ -491,6 +535,9 @@ def bits_pretest(): def bits_posttest(config): + if config.staging_update: + # If we are in try, we didn't run the full test and BITS will fail. + return None config.log_file_path.close() sys.stdout = sys.__stdout__ @@ -516,7 +563,7 @@ def bits_posttest(config): ) except (UnicodeDecodeError, AssertionError) as e: failed = 1 - print(e) + logging.error(e.__traceback__) finally: Path(config.log_file_path.name).unlink() diff --git a/testing/update/test_apply_update.py b/testing/update/test_apply_update.py @@ -25,10 +25,8 @@ class TestApplyUpdate(MarionetteTestCase): update_url = self.marionette.execute_async_script( """ (async function() { - let { UpdateUtils } = ChromeUtils.importESModule( - "resource://gre/modules/UpdateUtils.sys.mjs" - ); - let url = await UpdateUtils.formatUpdateURL(Services.appinfo.updateURL); + const checker = Cc["@mozilla.org/updates/update-checker;1"].getService(Ci.nsIUpdateChecker); + let url = await checker.wrappedJSObject.getUpdateURL(checker.BACKGROUND_CHECK); return url; })().then(arguments[0]); """ @@ -52,11 +50,24 @@ class TestApplyUpdate(MarionetteTestCase): self.marionette.set_context(self.marionette.CONTEXT_CONTENT) initial_ver = self.marionette.find_element(By.ID, "version").text + # Try runs build unsigned updates, releases can't update on unsigned MARs + # ...so we're just going to check that balrog gives a reasonably-named file that exists + if environ.get("BALROG_STAGING"): + print("staging") + patch_url = root[0][0].get("URL") + assert ( + f"{target_ver}" in patch_url + ), f"{target_ver} not in patch url: {patch_url}" + patch_response = requests.get(patch_url) + patch_response.raise_for_status() + return True + Wait(self.marionette, timeout=10).until( expected.element_displayed(By.ID, "downloadAndInstallButton") ) self.marionette.find_element(By.ID, "downloadAndInstallButton").click() + # Long timeouts are a known issue - Bug 2000040 Wait(self.marionette, timeout=240).until( expected.element_displayed(By.ID, "updateButton") ) @@ -70,6 +81,7 @@ class TestApplyUpdate(MarionetteTestCase): self.marionette.set_pref("remote.log.level", "Trace") self.marionette.set_pref("remote.system-access-check.enabled", False) self.marionette.navigate(self.about_fx_url) + Wait(self.marionette, timeout=240).until( expected.element_displayed(By.ID, "noUpdatesFound") ) diff --git a/testing/update/test_background_update.py b/testing/update/test_background_update.py @@ -9,18 +9,22 @@ from marionette_driver.wait import Wait from marionette_harness import MarionetteTestCase +def get_update_server_response(update_url, force: int): + response = requests.get(f"{update_url}?force={force}") + if response.status_code != 200: + raise Exception( + f"Tried to fetch update.xml but got response code {response.status_code}" + ) + + return ET.fromstring(response.text) + + def get_possible_target_versions(update_url): """If throttled to a lower target version, return both possible versions""" versions = [] for n in range(2): - response = requests.get(f"{update_url}?force={n}") - if response.status_code != 200: - raise Exception( - f"Tried to fetch update.xml but got response code {response.status_code}" - ) - # Get the target version - root = ET.fromstring(response.text) + root = get_update_server_response(update_url, n) versions.append(root[0].get("appVersion")) return list(set(versions)) @@ -63,10 +67,24 @@ class TestBackgroundUpdate(MarionetteTestCase): fh.write(f"Target version options: {', '.join(target_vers)}\n") # Wait for the background update to be ready by checking for popup + # Long timeouts are a known issue - Bug 2000040 Wait(self.marionette, timeout=100).until( lambda _: self.marionette.find_elements(By.ID, "appMenu-notification-popup") ) + # Try runs build unsigned updates, releases can't update on unsigned MARs + # ...so we're just going to check that balrog gives a reasonably-named file that exists + # We run the code here to maximize what gets run in try. + if environ.get("BALROG_STAGING"): + root = get_update_server_response(update_url, 1) + patch_url = root[0][0].get("URL") + assert ( + f"/{target_vers[-1]}-candidates" in patch_url + ), f'"/{target_vers[-1]}-candidates not in patch url: {patch_url}' + patch_response = requests.get(patch_url) + patch_response.raise_for_status() + return True + # Dismiss the popup self.marionette.find_element(By.ID, "urlbar-input").click() self.marionette.find_element(By.ID, "PanelUI-menu-button").click() @@ -126,5 +144,4 @@ class TestBackgroundUpdate(MarionetteTestCase): ) def tearDown(self): - MarionetteTestCase.tearDown(self)