tor-browser

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

other.py (43549B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import copy
      6 import hashlib
      7 import re
      8 
      9 from mozbuild.schedules import INCLUSIVE_COMPONENTS
     10 from taskgraph.transforms.base import TransformSequence
     11 from taskgraph.util import json
     12 from taskgraph.util.attributes import keymatch
     13 from taskgraph.util.keyed_by import evaluate_keyed_by
     14 from taskgraph.util.readonlydict import ReadOnlyDict
     15 from taskgraph.util.schema import Schema, resolve_keyed_by
     16 from taskgraph.util.taskcluster import get_artifact_path
     17 from taskgraph.util.templates import merge
     18 from voluptuous import Any, Optional, Required
     19 
     20 from gecko_taskgraph.transforms.test.variant import TEST_VARIANTS
     21 from gecko_taskgraph.util.perftest import is_external_browser
     22 from gecko_taskgraph.util.platforms import platform_family
     23 from gecko_taskgraph.util.taskcluster import get_index_url
     24 
     25 transforms = TransformSequence()
     26 
     27 
     28 @transforms.add
     29 def limit_platforms(config, tasks):
     30    for task in tasks:
     31        if not task["limit-platforms"]:
     32            yield task
     33            continue
     34 
     35        limited_platforms = {key: key for key in task["limit-platforms"]}
     36        if keymatch(limited_platforms, task["test-platform"]):
     37            yield task
     38 
     39 
     40 @transforms.add
     41 def handle_suite_category(config, tasks):
     42    for task in tasks:
     43        task.setdefault("suite", {})
     44 
     45        if isinstance(task["suite"], str):
     46            task["suite"] = {"name": task["suite"]}
     47 
     48        suite = task["suite"].setdefault("name", task["test-name"])
     49        category = task["suite"].setdefault("category", suite)
     50 
     51        task.setdefault("attributes", {})
     52        task["attributes"]["unittest_suite"] = suite
     53        task["attributes"]["unittest_category"] = category
     54 
     55        script = task["mozharness"]["script"]
     56        category_arg = None
     57        if suite.startswith("test-verify") or suite.startswith("test-coverage"):
     58            pass
     59        elif script in ("android_emulator_unittest.py", "android_hardware_unittest.py"):
     60            category_arg = "--test-suite"
     61        elif script == "desktop_unittest.py":
     62            category_arg = f"--{category}-suite"
     63 
     64        if category_arg:
     65            resolve_keyed_by(
     66                task,
     67                "mozharness.extra-options",
     68                item_name=task["test-name"],
     69                enforce_single_match=False,
     70                variant=task["attributes"].get("unittest_variant"),
     71            )
     72 
     73            task["mozharness"].setdefault("extra-options", [])
     74            extra = task["mozharness"]["extra-options"]
     75            if not any(arg.startswith(category_arg) for arg in extra):
     76                extra.append(f"{category_arg}={suite}")
     77 
     78        # From here on out we only use the suite name.
     79        task["suite"] = suite
     80 
     81        # in the future we might need to refactor new-test-config to be suite specific
     82        if "mochitest" in task["suite"] and config.params["try_task_config"].get(
     83            "new-test-config", False
     84        ):
     85            task = merge(
     86                task, {"mozharness": {"extra-options": ["--restartAfterFailure"]}}
     87            )
     88        yield task
     89 
     90 
     91 @transforms.add
     92 def setup_talos(config, tasks):
     93    """Add options that are specific to talos jobs (identified by suite=talos)"""
     94    for task in tasks:
     95        if task["suite"] != "talos":
     96            yield task
     97            continue
     98 
     99        resolve_keyed_by(
    100            task,
    101            "mozharness.extra-options",
    102            item_name=task["test-name"],
    103            enforce_single_match=False,
    104            variant=task["attributes"].get("unittest_variant"),
    105        )
    106 
    107        extra_options = task.setdefault("mozharness", {}).setdefault(
    108            "extra-options", []
    109        )
    110        extra_options.append("--use-talos-json")
    111 
    112        if config.params.get("project", None):
    113            extra_options.append("--project=%s" % config.params["project"])
    114 
    115        if "pdfpaint" in task["try-name"]:
    116            max_chunks = 10
    117            for chunk in range(1, max_chunks + 1):
    118                new_task = copy.deepcopy(task)
    119                new_task["mozharness"]["extra-options"].append(
    120                    f"--pdfPaintChunk={chunk}"
    121                )
    122                new_task["test-name"] = task["test-name"].replace(
    123                    "pdfpaint", f"pdfpaint-{chunk}"
    124                )
    125                new_task["try-name"] = task["try-name"].replace(
    126                    "pdfpaint", f"pdfpaint-{chunk}"
    127                )
    128                new_task["treeherder-symbol"] = task["treeherder-symbol"].replace(
    129                    "pdfpaint", f"pdfpaint-{chunk}"
    130                )
    131                yield new_task
    132            continue
    133 
    134        yield task
    135 
    136 
    137 @transforms.add
    138 def setup_browsertime_flag(config, tasks):
    139    """Optionally add `--browsertime` flag to Raptor pageload tests."""
    140 
    141    browsertime_flag = config.params["try_task_config"].get("browsertime", False)
    142 
    143    for task in tasks:
    144        if not browsertime_flag or task["suite"] != "raptor":
    145            yield task
    146            continue
    147 
    148        if task["treeherder-symbol"].startswith("Rap"):
    149            # The Rap group is subdivided as Rap{-fenix,-refbrow(...),
    150            # so `taskgraph.util.treeherder.replace_group` isn't appropriate.
    151            task["treeherder-symbol"] = task["treeherder-symbol"].replace(
    152                "Rap", "Btime", 1
    153            )
    154 
    155        extra_options = task.setdefault("mozharness", {}).setdefault(
    156            "extra-options", []
    157        )
    158        extra_options.append("--browsertime")
    159 
    160        yield task
    161 
    162 
    163 @transforms.add
    164 def handle_artifact_prefix(config, tasks):
    165    """Handle translating `artifact_prefix` appropriately"""
    166    for task in tasks:
    167        if task["build-attributes"].get("artifact_prefix"):
    168            task.setdefault("attributes", {}).setdefault(
    169                "artifact_prefix", task["build-attributes"]["artifact_prefix"]
    170            )
    171        yield task
    172 
    173 
    174 @transforms.add
    175 def set_treeherder_machine_platform(config, tasks):
    176    """Set the appropriate task.extra.treeherder.machine.platform"""
    177    translation = {
    178        # Linux64 build platform for asan is specified differently to
    179        # treeherder.
    180        "macosx1400-64/opt": "osx-1300/opt",
    181        "macosx1400-64-shippable/opt": "osx-1400-shippable/opt",
    182        "macosx1500-64/opt": "osx-1500/opt",
    183        "macosx1500-64-shippable/opt": "osx-1500-shippable/opt",
    184        "win64-asan/opt": "windows11-64-24h2/asan",
    185        "win64-aarch64/opt": "windows11-aarch64/opt",
    186    }
    187    for task in tasks:
    188        # For most desktop platforms, the above table is not used for "regular"
    189        # builds, so we'll always pick the test platform here.
    190        # On macOS though, the regular builds are in the table.  This causes a
    191        # conflict in `verify_task_graph_symbol` once you add a new test
    192        # platform based on regular macOS builds, such as for QR.
    193        # Since it's unclear if the regular macOS builds can be removed from
    194        # the table, workaround the issue for QR.
    195        if "android" in task["test-platform"] and "pgo/opt" in task["test-platform"]:
    196            platform_new = task["test-platform"].replace("-pgo/opt", "/pgo")
    197            task["treeherder-machine-platform"] = platform_new
    198        elif "android-em-" in task["test-platform"]:
    199            task["treeherder-machine-platform"] = task["test-platform"]
    200        elif "android-hw" in task["test-platform"]:
    201            task["treeherder-machine-platform"] = task["test-platform"]
    202 
    203        # Bug 1602863 - must separately define linux64/asan and linux1804-64/asan
    204        # otherwise causes an exception during taskgraph generation about
    205        # duplicate treeherder platform/symbol.
    206        elif "linux64-asan/opt" in task["test-platform"]:
    207            task["treeherder-machine-platform"] = "linux64/asan"
    208        elif "linux1804-asan/opt" in task["test-platform"]:
    209            task["treeherder-machine-platform"] = "linux1804-64/asan"
    210        elif "-qr" in task["test-platform"]:
    211            task["treeherder-machine-platform"] = task["test-platform"]
    212        else:
    213            task["treeherder-machine-platform"] = translation.get(
    214                task["build-platform"], task["test-platform"]
    215            )
    216        yield task
    217 
    218 
    219 @transforms.add
    220 def set_download_symbols(config, tasks):
    221    """In general, we download symbols immediately for debug builds, but only
    222    on demand for everything else. ASAN builds shouldn't download
    223    symbols since they don't product symbol zips see bug 1283879"""
    224    for task in tasks:
    225        if task["test-platform"].split("/")[-1] == "debug":
    226            task["mozharness"]["download-symbols"] = True
    227        elif "asan" in task["build-platform"] or "tsan" in task["build-platform"]:
    228            if "download-symbols" in task["mozharness"]:
    229                del task["mozharness"]["download-symbols"]
    230        else:
    231            task["mozharness"]["download-symbols"] = "ondemand"
    232        yield task
    233 
    234 
    235 @transforms.add
    236 def handle_keyed_by(config, tasks):
    237    """Resolve fields that can be keyed by platform, etc."""
    238    fields = [
    239        "instance-size",
    240        "docker-image",
    241        "max-run-time",
    242        "chunks",
    243        "suite",
    244        "run-on-projects",
    245        "os-groups",
    246        "run-as-administrator",
    247        "workdir",
    248        "worker-type",
    249        "virtualization",
    250        "fetches.fetch",
    251        "fetches.toolchain",
    252        "target",
    253        "webrender-run-on-projects",
    254        "mozharness.extra-options",
    255        "mozharness.requires-signed-builds",
    256        "build-signing-label",
    257        "dependencies",
    258    ]
    259    for task in tasks:
    260        for field in fields:
    261            resolve_keyed_by(
    262                task,
    263                field,
    264                item_name=task["test-name"],
    265                enforce_single_match=False,
    266                project=config.params["project"],
    267                variant=task["attributes"].get("unittest_variant"),
    268            )
    269        yield task
    270 
    271 
    272 @transforms.add
    273 def setup_raptor_external_browser_platforms(config, tasks):
    274    for task in tasks:
    275        if task["suite"] != "raptor":
    276            yield task
    277            continue
    278 
    279        if is_external_browser(task["try-name"]):
    280            if "win" in task["build-label"]:
    281                task["build-platform"] = "windows2012-64/opt"
    282                task["build-label"] = "build-win64/opt"
    283            else:
    284                task["build-platform"] = "linux64/opt"
    285                task["build-label"] = "build-linux64/opt"
    286 
    287        yield task
    288 
    289 
    290 @transforms.add
    291 def set_target(config, tasks):
    292    for task in tasks:
    293        build_platform = task["build-platform"]
    294        target = None
    295        if "target" in task:
    296            target = task["target"]
    297        if not target:
    298            if build_platform.startswith("macosx"):
    299                target = "target.dmg"
    300            elif build_platform.startswith("android"):
    301                target = "target.apk"
    302            elif build_platform.startswith("win"):
    303                target = "target.zip"
    304            else:
    305                target = "target.tar.xz"
    306 
    307        if isinstance(target, dict):
    308            if "index" in target:
    309                # TODO Remove hardcoded mobile artifact prefix
    310                index_url = get_index_url(target["index"])
    311                installer_url = "{}/artifacts/public/{}".format(
    312                    index_url, target["name"]
    313                )
    314                task["mozharness"]["installer-url"] = installer_url
    315            else:
    316                task["mozharness"]["installer-url"] = (
    317                    f"<{target['upstream-task']}/{target['name']}>"
    318                )
    319        else:
    320            task["mozharness"]["build-artifact-name"] = get_artifact_path(task, target)
    321 
    322        yield task
    323 
    324 
    325 @transforms.add
    326 def setup_browsertime(config, tasks):
    327    """Configure browsertime dependencies for Raptor pageload tests that have
    328    `--browsertime` extra option."""
    329 
    330    for task in tasks:
    331        # We need to make non-trivial changes to various fetches, and our
    332        # `by-test-platform` may not be "compatible" with existing
    333        # `by-test-platform` filters.  Therefore we do everything after
    334        # `handle_keyed_by` so that existing fields have been resolved down to
    335        # simple lists.  But we use the `by-test-platform` machinery to express
    336        # filters so that when the time comes to move browsertime into YAML
    337        # files, the transition is straight-forward.
    338        extra_options = task.get("mozharness", {}).get("extra-options", [])
    339 
    340        if task["suite"] != "raptor":
    341            yield task
    342            continue
    343 
    344        ts = {
    345            "by-test-platform": {
    346                "android.*": ["browsertime", "linux64-geckodriver", "linux64-node"],
    347                "linux.*": ["browsertime", "linux64-geckodriver", "linux64-node"],
    348                "macosx1470.*": [
    349                    "browsertime",
    350                    "macosx64-geckodriver",
    351                    "macosx64-node",
    352                ],
    353                "macosx1400.*": [
    354                    "browsertime",
    355                    "macosx64-aarch64-geckodriver",
    356                    "macosx64-aarch64-node",
    357                ],
    358                "macosx1500.*": [
    359                    "browsertime",
    360                    "macosx64-aarch64-geckodriver",
    361                    "macosx64-aarch64-node",
    362                ],
    363                "windows.*aarch64.*": [
    364                    "browsertime",
    365                    "win32-geckodriver",
    366                    "win32-node",
    367                ],
    368                "windows.*-64.*": ["browsertime", "win64-geckodriver", "win64-node"],
    369            },
    370        }
    371 
    372        task.setdefault("fetches", {}).setdefault("toolchain", []).extend(
    373            evaluate_keyed_by(ts, "fetches.toolchain", task)
    374        )
    375 
    376        fs = {
    377            "by-test-platform": {
    378                "android.*": ["linux64-ffmpeg-7.1"],
    379                "linux.*": ["linux64-ffmpeg-7.1"],
    380                "macosx1470.*": ["mac64-ffmpeg-7.1"],
    381                "macosx1400.*": ["mac64-ffmpeg-7.1"],
    382                "macosx1500.*": ["mac64-ffmpeg-7.1"],
    383                "windows.*aarch64.*": ["win64-ffmpeg-7.1"],
    384                "windows.*-64.*": ["win64-ffmpeg-7.1"],
    385            },
    386        }
    387 
    388        cd_fetches = {
    389            "android.*": [
    390                "linux64-cft-cd-backup",
    391                "linux64-cft-cd-stable",
    392                "linux64-cft-cd-beta",
    393            ],
    394            "linux.*": [
    395                "linux64-cft-cd-backup",
    396                "linux64-cft-cd-stable",
    397                "linux64-cft-cd-beta",
    398            ],
    399            "macosx1470.*": [
    400                "mac-cft-cd-backup",
    401                "mac-cft-cd-stable",
    402                "mac-cft-cd-beta",
    403            ],
    404            "macosx1400.*": [
    405                "mac-cft-cd-arm-backup",
    406                "mac-cft-cd-arm-stable",
    407                "mac-cft-cd-arm-beta",
    408            ],
    409            "macosx1500.*": [
    410                "mac-cft-cd-arm-backup",
    411                "mac-cft-cd-arm-stable",
    412                "mac-cft-cd-arm-beta",
    413            ],
    414            "windows.*-64.*": [
    415                "win64-cft-cd-backup",
    416                "win64-cft-cd-stable",
    417                "win64-cft-cd-beta",
    418            ],
    419        }
    420 
    421        chromium_fetches = {
    422            "linux.*": ["linux64-cft-cd-canary"],
    423            "macosx1400.*": ["mac-cft-cd-arm-canary"],
    424            "macosx1470.*": ["mac-cft-cd-canary"],
    425            "macosx1500.*": ["mac-cft-cd-arm-canary"],
    426            "windows.*-64.*": ["win64-cft-cd-canary"],
    427            "android.*": ["linux64-cft-cd-canary"],
    428        }
    429 
    430        cd_extracted_name = {
    431            "windows": "{}chromedriver.exe",
    432            "mac": "{}chromedriver",
    433            "default": "{}chromedriver",
    434        }
    435 
    436        if "--app=chrome" in extra_options or "--app=chrome-m" in extra_options:
    437            # Only add the chromedriver fetches when chrome is running
    438            for platform in cd_fetches:
    439                fs["by-test-platform"][platform].extend(cd_fetches[platform])
    440        if "--app=custom-car" in extra_options or "--app=cstm-car-m" in extra_options:
    441            for platform in chromium_fetches:
    442                fs["by-test-platform"][platform].extend(chromium_fetches[platform])
    443 
    444            # The Chrome-for-Testing chromedrivers are repackaged into the following
    445            # platform specific archives. The versions will always be compatible as
    446            # these are fetched from the `Canary` channel.
    447            cd_extracted_name = {
    448                "windows": "cft-chromedriver-win64/chromedriver.exe",
    449                "mac": "cft-chromedriver-mac/chromedriver",
    450                "default": "cft-chromedriver-linux/chromedriver",
    451            }
    452 
    453        # Disable the Raptor install step
    454        if "--app=chrome-m" in extra_options or "--app=cstm-car-m" in extra_options:
    455            extra_options.append("--no-install")
    456 
    457        task.setdefault("fetches", {}).setdefault("fetch", []).extend(
    458            evaluate_keyed_by(fs, "fetches.fetch", task)
    459        )
    460 
    461        extra_options.extend((
    462            "--browsertime-browsertimejs",
    463            "$MOZ_FETCHES_DIR/browsertime/node_modules/browsertime/bin/browsertime.js",
    464        ))  # noqa: E501
    465 
    466        eos = {
    467            "by-test-platform": {
    468                "windows.*": [
    469                    "--browsertime-node",
    470                    "$MOZ_FETCHES_DIR/node/node.exe",
    471                    "--browsertime-geckodriver",
    472                    "$MOZ_FETCHES_DIR/geckodriver.exe",
    473                    "--browsertime-chromedriver",
    474                    "$MOZ_FETCHES_DIR/" + cd_extracted_name["windows"],
    475                    "--browsertime-ffmpeg",
    476                    "$MOZ_FETCHES_DIR/ffmpeg-n7.1-latest-win64-gpl-shared-7.1/bin/ffmpeg.exe",
    477                ],
    478                "macosx.*": [
    479                    "--browsertime-node",
    480                    "$MOZ_FETCHES_DIR/node/bin/node",
    481                    "--browsertime-geckodriver",
    482                    "$MOZ_FETCHES_DIR/geckodriver",
    483                    "--browsertime-chromedriver",
    484                    "$MOZ_FETCHES_DIR/" + cd_extracted_name["mac"],
    485                    "--browsertime-ffmpeg",
    486                    "$MOZ_FETCHES_DIR/ffmpeg-7.1/bin/ffmpeg",
    487                ],
    488                "default": [
    489                    "--browsertime-node",
    490                    "$MOZ_FETCHES_DIR/node/bin/node",
    491                    "--browsertime-geckodriver",
    492                    "$MOZ_FETCHES_DIR/geckodriver",
    493                    "--browsertime-chromedriver",
    494                    "$MOZ_FETCHES_DIR/" + cd_extracted_name["default"],
    495                    "--browsertime-ffmpeg",
    496                    "$MOZ_FETCHES_DIR/ffmpeg-n7.1-linux64-gpl-7.1/bin/ffmpeg",
    497                ],
    498            }
    499        }
    500 
    501        extra_options.extend(evaluate_keyed_by(eos, "mozharness.extra-options", task))
    502 
    503        yield task
    504 
    505 
    506 def get_mobile_project(task):
    507    """Returns the mobile project of the specified task or None."""
    508 
    509    if not task["build-platform"].startswith("android"):
    510        return
    511 
    512    mobile_projects = ("fenix", "geckoview", "refbrow", "chrome-m", "cstm-car-m")
    513 
    514    for name in mobile_projects:
    515        if name in task["test-name"]:
    516            return name
    517 
    518    target = None
    519    if "target" in task:
    520        resolve_keyed_by(
    521            task, "target", item_name=task["test-name"], enforce_single_match=False
    522        )
    523        target = task["target"]
    524    if target:
    525        if isinstance(target, dict):
    526            target = target["name"]
    527 
    528        for name in mobile_projects:
    529            if name in target:
    530                return name
    531 
    532    return None
    533 
    534 
    535 @transforms.add
    536 def disable_wpt_timeouts_on_autoland(config, tasks):
    537    """do not run web-platform-tests that are expected TIMEOUT on autoland"""
    538    for task in tasks:
    539        if (
    540            "web-platform-tests" in task["test-name"]
    541            and config.params["project"] == "autoland"
    542        ):
    543            resolve_keyed_by(
    544                task,
    545                "mozharness.extra-options",
    546                item_name=task["test-name"],
    547                enforce_single_match=False,
    548                variant=task["attributes"].get("unittest_variant"),
    549            )
    550 
    551            task["mozharness"].setdefault("extra-options", []).append("--skip-timeout")
    552        yield task
    553 
    554 
    555 @transforms.add
    556 def enable_code_coverage(config, tasks):
    557    """Enable code coverage for the ccov build-platforms"""
    558    for task in tasks:
    559        if "ccov" in task["build-platform"]:
    560            # Do not run tests on fuzzing builds
    561            if "fuzzing" in task["build-platform"]:
    562                task["run-on-projects"] = []
    563                continue
    564 
    565            # Skip this transform for android code coverage builds.
    566            if "android" in task["build-platform"]:
    567                task.setdefault("fetches", {}).setdefault("toolchain", []).append(
    568                    "linux64-grcov"
    569                )
    570                task["mozharness"].setdefault("extra-options", []).append(
    571                    "--java-code-coverage"
    572                )
    573                yield task
    574                continue
    575            task["mozharness"].setdefault("extra-options", []).append("--code-coverage")
    576            task["instance-size"] = "xlarge-noscratch"
    577            if "jittest" in task["test-name"]:
    578                task["instance-size"] = "xlarge"
    579 
    580            # Temporarily disable Mac tests on mozilla-central
    581            if "mac" in task["build-platform"]:
    582                task["run-on-projects"] = []
    583 
    584            # Ensure we always run on the projects defined by the build, unless the test
    585            # is try only or shouldn't run at all.
    586            if task["run-on-projects"] not in [[]]:
    587                task["run-on-projects"] = "built-projects"
    588 
    589            # Ensure we don't optimize test suites out.
    590            # We always want to run all test suites for coverage purposes.
    591            task.pop("schedules-component", None)
    592            task.pop("when", None)
    593            task["optimization"] = None
    594 
    595            # Add a toolchain and a fetch task for the grcov binary.
    596            if any(p in task["build-platform"] for p in ("linux", "osx", "win")):
    597                task.setdefault("fetches", {})
    598                task["fetches"].setdefault("fetch", [])
    599                task["fetches"].setdefault("toolchain", [])
    600                task["fetches"].setdefault("build", [])
    601 
    602            if "linux" in task["build-platform"]:
    603                task["fetches"]["toolchain"].append("linux64-grcov")
    604            elif "osx" in task["build-platform"]:
    605                task["fetches"]["toolchain"].append("macosx64-grcov")
    606            elif "win" in task["build-platform"]:
    607                task["fetches"]["toolchain"].append("win64-grcov")
    608 
    609            task["fetches"]["build"].append({"artifact": "target.mozinfo.json"})
    610 
    611            if "talos" in task["test-name"]:
    612                task["max-run-time"] = 7200
    613                if "linux" in task["build-platform"]:
    614                    task["docker-image"] = {"in-tree": "ubuntu1804-test"}
    615                task["mozharness"]["extra-options"].append("--add-option")
    616                task["mozharness"]["extra-options"].append("--cycles,1")
    617                task["mozharness"]["extra-options"].append("--add-option")
    618                task["mozharness"]["extra-options"].append("--tppagecycles,1")
    619                task["mozharness"]["extra-options"].append("--add-option")
    620                task["mozharness"]["extra-options"].append("--no-upload-results")
    621                task["mozharness"]["extra-options"].append("--add-option")
    622                task["mozharness"]["extra-options"].append("--tptimeout,15000")
    623            if "raptor" in task["test-name"]:
    624                task["max-run-time"] = 1800
    625        yield task
    626 
    627 
    628 @transforms.add
    629 def handle_run_on_projects(config, tasks):
    630    """Handle translating `built-projects` appropriately"""
    631    for task in tasks:
    632        if task["run-on-projects"] == "built-projects":
    633            task["run-on-projects"] = task["build-attributes"].get(
    634                "run_on_projects", ["all"]
    635            )
    636 
    637        if task.pop("built-projects-only", False):
    638            built_projects = set(
    639                task["build-attributes"].get("run_on_projects", {"all"})
    640            )
    641            run_on_projects = set(task.get("run-on-projects", set()))
    642 
    643            # If 'all' exists in run-on-projects, then the intersection of both
    644            # is built-projects. Similarly if 'all' exists in built-projects,
    645            # the intersection is run-on-projects (so do nothing). When neither
    646            # contains 'all', take the actual set intersection.
    647            if "all" in run_on_projects:
    648                task["run-on-projects"] = sorted(built_projects)
    649            elif "all" not in built_projects:
    650                task["run-on-projects"] = sorted(run_on_projects & built_projects)
    651        yield task
    652 
    653 
    654 @transforms.add
    655 def handle_tier(config, tasks):
    656    """Set the tier based on policy for all test descriptions that do not
    657    specify a tier otherwise."""
    658    for task in tasks:
    659        if "tier" in task:
    660            resolve_keyed_by(
    661                task,
    662                "tier",
    663                item_name=task["test-name"],
    664                variant=task["attributes"].get("unittest_variant"),
    665                enforce_single_match=False,
    666            )
    667 
    668        # only override if not set for the test
    669        if "tier" not in task or task["tier"] == "default":
    670            if task["test-platform"] in [
    671                "linux64/opt",
    672                "linux64/debug",
    673                "linux64-shippable/opt",
    674                "linux64-devedition/opt",
    675                "linux64-asan/opt",
    676                "linux64-qr/opt",
    677                "linux64-qr/debug",
    678                "linux64-shippable-qr/opt",
    679                "linux1804-64/opt",
    680                "linux1804-64/debug",
    681                "linux1804-64-shippable/opt",
    682                "linux1804-64-devedition/opt",
    683                "linux1804-64-qr/opt",
    684                "linux1804-64-qr/debug",
    685                "linux1804-64-shippable-qr/opt",
    686                "linux1804-64-asan-qr/opt",
    687                "linux1804-64-tsan-qr/opt",
    688                "linux2204-64-wayland/debug",
    689                "linux2204-64-wayland/opt",
    690                "linux2204-64-wayland-shippable/opt",
    691                "linux2404-64/opt",
    692                "linux2404-64/debug",
    693                "linux2404-64-shippable/opt",
    694                "linux2404-64-devedition/opt",
    695                "linux2404-64-asan/opt",
    696                "linux2404-64-tsan/opt",
    697                "windows11-32-24h2/debug",
    698                "windows11-32-24h2/opt",
    699                "windows11-32-24h2-shippable/opt",
    700                "windows11-64-24h2-hw-ref/opt",
    701                "windows11-64-24h2-hw-ref-shippable/opt",
    702                "windows11-64-24h2/opt",
    703                "windows11-64-24h2/debug",
    704                "windows11-64-24h2-shippable/opt",
    705                "windows11-64-24h2-devedition/opt",
    706                "windows11-64-24h2-asan/opt",
    707                "macosx1015-64/opt",
    708                "macosx1015-64/debug",
    709                "macosx1015-64-shippable/opt",
    710                "macosx1015-64-devedition/opt",
    711                "macosx1015-64-devedition-qr/opt",
    712                "macosx1015-64-qr/opt",
    713                "macosx1015-64-shippable-qr/opt",
    714                "macosx1015-64-qr/debug",
    715                "macosx1470-64/opt",
    716                "macosx1470-64/debug",
    717                "macosx1470-64-shippable/opt",
    718                "macosx1470-64-devedition/opt",
    719                "macosx1400-64-shippable-qr/opt",
    720                "macosx1400-64-qr/debug",
    721                "macosx1500-64-shippable/opt",
    722                "macosx1500-64/debug",
    723                "android-em-14-x86_64-shippable/opt",
    724                "android-em-14-x86_64/opt",
    725                "android-em-14-x86_64-shippable-lite/opt",
    726                "android-em-14-x86_64-lite/opt",
    727                "android-em-14-x86_64/debug",
    728                "android-em-14-x86_64/debug-isolated-process",
    729                "android-em-14-x86-shippable/opt",
    730                "android-em-14-x86/opt",
    731            ]:
    732                task["tier"] = 1
    733            else:
    734                task["tier"] = 2
    735 
    736        yield task
    737 
    738 
    739 @transforms.add
    740 def apply_raptor_tier_optimization(config, tasks):
    741    for task in tasks:
    742        if task["suite"] != "raptor":
    743            yield task
    744            continue
    745 
    746        if "regression-tests" in task["test-name"]:
    747            # Don't optimize the regression tests
    748            yield task
    749            continue
    750 
    751        if not task["test-platform"].startswith("android-hw"):
    752            task["optimization"] = {"skip-unless-expanded": None}
    753            if task["tier"] > 1:
    754                task["optimization"] = {"skip-unless-backstop": None}
    755 
    756        if task["attributes"].get("unittest_variant"):
    757            task["tier"] = max(task["tier"], 2)
    758        yield task
    759 
    760 
    761 @transforms.add
    762 def disable_try_only_platforms(config, tasks):
    763    """Turns off platforms that should only run on try."""
    764    try_only_platforms = ()
    765    for task in tasks:
    766        if any(re.match(k + "$", task["test-platform"]) for k in try_only_platforms):
    767            task["run-on-projects"] = []
    768        yield task
    769 
    770 
    771 @transforms.add
    772 def ensure_spi_disabled_on_all_but_spi(config, tasks):
    773    for task in tasks:
    774        variant = task["attributes"].get("unittest_variant") or ""
    775        has_no_setpref = (
    776            "gtest",
    777            "cppunit",
    778            "jittest",
    779            "junit",
    780            "raptor",
    781            "reftest",
    782            "web-platform-tests",
    783        )
    784 
    785        if (
    786            all(s not in task["suite"] for s in has_no_setpref)
    787            and "socketprocess" not in variant
    788        ):
    789            task["mozharness"]["extra-options"].append(
    790                "--setpref=media.peerconnection.mtransport_process=false"
    791            )
    792            task["mozharness"]["extra-options"].append(
    793                "--setpref=network.process.enabled=false"
    794            )
    795 
    796        yield task
    797 
    798 
    799 test_setting_description_schema = Schema(
    800    {
    801        Required("_hash"): str,
    802        "platform": {
    803            Required("arch"): Any("32", "64", "aarch64", "arm7", "x86", "x86_64"),
    804            Required("os"): {
    805                Required("name"): Any("android", "linux", "macosx", "windows"),
    806                Required("version"): str,
    807                Optional("build"): str,
    808            },
    809            Optional("device"): str,
    810            Optional("display"): "wayland",
    811            Optional("machine"): "hw-ref",
    812        },
    813        "build": {
    814            Required("type"): Any("opt", "debug", "debug-isolated-process"),
    815            Any(
    816                "asan",
    817                "ccov",
    818                "clang-trunk",
    819                "devedition",
    820                "lite",
    821                "mingwclang",
    822                "nightlyasrelease",
    823                "shippable",
    824                "tsan",
    825            ): bool,
    826        },
    827        "runtime": {Any(*list(TEST_VARIANTS.keys()) + ["1proc"]): bool},
    828    },
    829    check=False,
    830 )
    831 """Schema test settings must conform to. Validated by
    832 :py:func:`~test.test_mozilla_central.test_test_setting`"""
    833 
    834 
    835 @transforms.add
    836 def set_test_setting(config, tasks):
    837    """A test ``setting`` is the set of configuration that uniquely
    838    distinguishes a test task from other tasks that run the same suite
    839    (ignoring chunks).
    840 
    841    There are three different types of information that make up a setting:
    842 
    843    1. Platform - Information describing the underlying platform tests run on,
    844    e.g, OS, CPU architecture, etc.
    845 
    846    2. Build - Information describing the build being tested, e.g build type,
    847    ccov, asan/tsan, etc.
    848 
    849    3. Runtime - Information describing which runtime parameters are enabled,
    850    e.g, prefs, environment variables, etc.
    851 
    852    This transform adds a ``test-setting`` object to the ``extra`` portion of
    853    all test tasks, of the form:
    854 
    855    .. code-block::
    856 
    857        {
    858            "platform": { ... },
    859            "build": { ... },
    860            "runtime": { ... }
    861        }
    862 
    863    This information could be derived from the label, but consuming this
    864    object is less brittle.
    865    """
    866    # Some attributes have a dash in them which complicates parsing. Ensure we
    867    # don't split them up.
    868    # TODO Rename these so they don't have a dash.
    869    dash_attrs = [
    870        "clang-trunk",
    871        "hw-ref",
    872    ]
    873    dash_token = "%D%"
    874    platform_re = re.compile(r"(\D+)(\d*)")
    875 
    876    for task in tasks:
    877        setting = {
    878            "platform": {
    879                "os": {},
    880            },
    881            "build": {},
    882            "runtime": {},
    883        }
    884 
    885        # parse platform and build information out of 'test-platform'
    886        platform, build_type = task["test-platform"].split("/", 1)
    887 
    888        # ensure dashed attributes don't get split up
    889        for attr in dash_attrs:
    890            if attr in platform:
    891                platform = platform.replace(attr, attr.replace("-", dash_token))
    892 
    893        parts = platform.split("-")
    894 
    895        # restore dashes now that split is finished
    896        for i, part in enumerate(parts):
    897            if dash_token in part:
    898                parts[i] = part.replace(dash_token, "-")
    899 
    900        match = platform_re.match(parts.pop(0))
    901        assert match
    902        os_name, os_version = match.groups()
    903 
    904        device = machine = os_build = display = None
    905        if os_name == "android":
    906            device = parts.pop(0)
    907            if device == "hw":
    908                device = parts.pop(0)
    909            else:
    910                device = "emulator"
    911 
    912            os_version = parts.pop(0)
    913            if parts[0].isdigit():
    914                os_version = f"{os_version}.{parts.pop(0)}"
    915 
    916            if parts[0] == "android":
    917                parts.pop(0)
    918 
    919            arch = parts.pop(0)
    920 
    921        else:
    922            arch = parts.pop(0)
    923            if parts and (parts[0].isdigit() or parts[0] in ["24h2"]):
    924                os_build = parts.pop(0)
    925 
    926            if parts and parts[0] == "hw-ref":
    927                machine = parts.pop(0)
    928 
    929            if parts and parts[0] == "wayland":
    930                display = parts.pop(0)
    931 
    932            if parts and parts[0] == "aarch64":
    933                arch = parts.pop(0)
    934 
    935        # It's not always possible to glean the exact architecture used from
    936        # the task, so sometimes this will just be set to "32" or "64".
    937        setting["platform"]["arch"] = arch
    938        setting["platform"]["os"] = {
    939            "name": os_name,
    940            "version": os_version,
    941        }
    942 
    943        if os_build:
    944            setting["platform"]["os"]["build"] = os_build
    945 
    946        if device:
    947            setting["platform"]["device"] = device
    948 
    949        if machine:
    950            setting["platform"]["machine"] = machine
    951 
    952        if display:
    953            setting["platform"]["display"] = display
    954 
    955        # parse remaining parts as build attributes
    956        setting["build"]["type"] = build_type
    957        while parts:
    958            attr = parts.pop(0)
    959            if attr == "qr":
    960                # all tasks are webrender now, no need to store it
    961                continue
    962 
    963            setting["build"][attr] = True
    964 
    965        unittest_variant = task["attributes"].get("unittest_variant")
    966        if unittest_variant:
    967            for variant in unittest_variant.split("+"):
    968                setting["runtime"][variant] = True
    969 
    970        # add a hash of the setting object for easy comparisons
    971        setting["_hash"] = hashlib.sha256(
    972            json.dumps(setting, sort_keys=True).encode("utf-8")
    973        ).hexdigest()[:12]
    974 
    975        task["test-setting"] = ReadOnlyDict(**setting)
    976        yield task
    977 
    978 
    979 @transforms.add
    980 def allow_software_gl_layers(config, tasks):
    981    """
    982    Handle the "allow-software-gl-layers" property for platforms where it
    983    applies.
    984    """
    985    for task in tasks:
    986        if task.get("allow-software-gl-layers"):
    987            # This should be set always once bug 1296086 is resolved.
    988            task["mozharness"].setdefault("extra-options", []).append(
    989                "--allow-software-gl-layers"
    990            )
    991 
    992        yield task
    993 
    994 
    995 @transforms.add
    996 def enable_webrender(config, tasks):
    997    """
    998    Handle the "webrender" property by passing a flag to mozharness if it is
    999    enabled.
   1000    """
   1001    for task in tasks:
   1002        # TODO: this was all conditionally in enable_webrender- do we still need this?
   1003        extra_options = task["mozharness"].setdefault("extra-options", [])
   1004        # We only want to 'setpref' on tests that have a profile
   1005        if not task["attributes"]["unittest_category"] in [
   1006            "cppunittest",
   1007            "geckoview-junit",
   1008            "gtest",
   1009            "jittest",
   1010            "raptor",
   1011        ]:
   1012            extra_options.append("--setpref=layers.d3d11.enable-blacklist=false")
   1013 
   1014        yield task
   1015 
   1016 
   1017 @transforms.add
   1018 def set_schedules_for_webrender_android(config, tasks):
   1019    """android-hw has limited resources, we need webrender on phones"""
   1020    for task in tasks:
   1021        if task["suite"] in ["crashtest", "reftest"] and task[
   1022            "test-platform"
   1023        ].startswith("android-hw"):
   1024            task["schedules-component"] = "android-hw-gfx"
   1025        yield task
   1026 
   1027 
   1028 @transforms.add
   1029 def set_retry_exit_status(config, tasks):
   1030    """Set the retry exit status to TBPL_RETRY, the value returned by mozharness
   1031    scripts to indicate a transient failure that should be retried."""
   1032    for task in tasks:
   1033        task["retry-exit-status"] = [4]
   1034        yield task
   1035 
   1036 
   1037 @transforms.add
   1038 def set_profile(config, tasks):
   1039    """Set profiling mode for tests."""
   1040    ttconfig = config.params["try_task_config"]
   1041    profile = ttconfig.get("gecko-profile", False)
   1042    settings = (
   1043        "gecko-profile-interval",
   1044        "gecko-profile-entries",
   1045        "gecko-profile-threads",
   1046        "gecko-profile-features",
   1047    )
   1048 
   1049    for task in tasks:
   1050        if profile and task["suite"] in ["talos", "raptor"]:
   1051            extras = task["mozharness"]["extra-options"]
   1052            extras.append("--gecko-profile")
   1053            for setting in settings:
   1054                value = ttconfig.get(setting)
   1055                if value is not None:
   1056                    # These values can contain spaces (eg the "DOM Worker"
   1057                    # thread) and the command is constructed in different,
   1058                    # incompatible ways on different platforms.
   1059 
   1060                    if task["test-platform"].startswith("win"):
   1061                        # Double quotes for Windows (single won't work).
   1062                        extras.append("--" + setting + '="' + str(value) + '"')
   1063                    else:
   1064                        # Other platforms keep things as separate values,
   1065                        # rather than joining with spaces.
   1066                        extras.append("--" + setting + "=" + str(value))
   1067 
   1068        yield task
   1069 
   1070 
   1071 @transforms.add
   1072 def add_gecko_profile_symbolication_deps(config, tasks):
   1073    """Add symbolication dependencies when profiling raptor, talos, or mochitest tests"""
   1074 
   1075    try_task_config = config.params.get("try_task_config", {})
   1076    gecko_profile = try_task_config.get("gecko-profile", False)
   1077    env = try_task_config.get("env", {})
   1078    startup_profile = env.get("MOZ_PROFILER_STARTUP") == "1"
   1079 
   1080    for task in tasks:
   1081        if (gecko_profile and task["suite"] in ["talos", "raptor"]) or (
   1082            startup_profile and "mochitest" in task["suite"]
   1083        ):
   1084            fetches = task.setdefault("fetches", {})
   1085            fetch_toolchains = fetches.setdefault("toolchain", [])
   1086            fetch_toolchains.append("symbolicator-cli")
   1087 
   1088            test_platform = task["test-platform"]
   1089 
   1090            if "macosx" in test_platform and "aarch64" in test_platform:
   1091                fetch_toolchains.append("macosx64-aarch64-samply")
   1092            elif "macosx" in test_platform:
   1093                fetch_toolchains.append("macosx64-samply")
   1094            elif "win" in test_platform:
   1095                fetch_toolchains.append("win64-samply")
   1096            else:
   1097                fetch_toolchains.append("linux64-samply")
   1098 
   1099            # Add node as a dependency for talos and mochitest tasks if needed.
   1100            # node is used to run symbolicator-cli, our profile symbolication tool
   1101            if task["suite"] == "talos" or "mochitest" in task["suite"]:
   1102                if "macosx" in test_platform and "aarch64" in test_platform:
   1103                    node_toolchain = "macosx64-aarch64-node"
   1104                elif "macosx" in test_platform:
   1105                    node_toolchain = "macosx64-node"
   1106                elif "win" in test_platform:
   1107                    node_toolchain = "win64-node"
   1108                else:
   1109                    node_toolchain = "linux64-node"
   1110 
   1111                if node_toolchain not in fetch_toolchains:
   1112                    fetch_toolchains.append(node_toolchain)
   1113 
   1114        yield task
   1115 
   1116 
   1117 @transforms.add
   1118 def set_tag(config, tasks):
   1119    """Set test for a specific tag."""
   1120    tag = None
   1121    for task in tasks:
   1122        if tag:
   1123            task["mozharness"]["extra-options"].extend(["--tag", tag])
   1124        yield task
   1125 
   1126 
   1127 @transforms.add
   1128 def set_test_type(config, tasks):
   1129    types = ["mochitest", "reftest", "talos", "raptor", "geckoview-junit", "gtest"]
   1130    for task in tasks:
   1131        for test_type in types:
   1132            if test_type in task["suite"] and "web-platform" not in task["suite"]:
   1133                task.setdefault("tags", {})["test-type"] = test_type
   1134        yield task
   1135 
   1136 
   1137 @transforms.add
   1138 def set_schedules_components(config, tasks):
   1139    for task in tasks:
   1140        if "optimization" in task or "when" in task:
   1141            yield task
   1142            continue
   1143 
   1144        category = task["attributes"]["unittest_category"]
   1145        schedules = task.get("schedules-component", category)
   1146        if isinstance(schedules, str):
   1147            schedules = [schedules]
   1148 
   1149        schedules = set(schedules)
   1150        if schedules & set(INCLUSIVE_COMPONENTS):
   1151            # if this is an "inclusive" test, then all files which might
   1152            # cause it to run are annotated with SCHEDULES in moz.build,
   1153            # so do not include the platform or any other components here
   1154            task["schedules-component"] = sorted(schedules)
   1155            yield task
   1156            continue
   1157 
   1158        schedules.add(category)
   1159        schedules.add(platform_family(task["build-platform"]))
   1160        schedules.add("firefox")
   1161 
   1162        task["schedules-component"] = sorted(schedules)
   1163        yield task
   1164 
   1165 
   1166 @transforms.add
   1167 def enable_parallel_marking_in_tsan_tests(config, tasks):
   1168    """Enable parallel marking in TSAN tests"""
   1169    skip_list = ["cppunittest", "gtest"]
   1170    for task in tasks:
   1171        if "-tsan-" in task["test-platform"]:
   1172            if task["suite"] not in skip_list:
   1173                extra_options = task["mozharness"].setdefault("extra-options", [])
   1174                extra_options.append(
   1175                    "--setpref=javascript.options.mem.gc_parallel_marking=true"
   1176                )
   1177 
   1178        yield task
   1179 
   1180 
   1181 @transforms.add
   1182 def set_webgpu_ignore_blocklist(config, tasks):
   1183    """
   1184    Ignore the WebGPU blocklist on Linux because CI's Mesa is old
   1185 
   1186    See <https://bugzilla.mozilla.org/show_bug.cgi?id=1985348>
   1187    """
   1188    for task in tasks:
   1189        if "web-platform-tests-webgpu" in task["test-name"] and task[
   1190            "test-platform"
   1191        ].startswith("linux"):
   1192            extra_options = task["mozharness"].setdefault("extra-options", [])
   1193            extra_options.append("--setpref=gfx.webgpu.ignore-blocklist=true")
   1194 
   1195        yield task