diff options
Diffstat (limited to 'taskcluster/gecko_taskgraph')
-rw-r--r-- | taskcluster/gecko_taskgraph/morph.py | 25 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/parameters.py | 4 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/target_tasks.py | 50 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/test/test_morph.py | 96 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/test/test_target_tasks.py | 15 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/test/test_util_partials.py | 32 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/artifact.py | 4 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/artifacts.yml | 4 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/bootstrap.py | 2 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/job/mozharness_test.py | 11 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/snap_test.py | 2 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/source_test.py | 29 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/test/other.py | 29 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/transforms/test/raptor.py | 23 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/util/partials.py | 10 | ||||
-rw-r--r-- | taskcluster/gecko_taskgraph/util/perftest.py | 18 |
16 files changed, 292 insertions, 62 deletions
diff --git a/taskcluster/gecko_taskgraph/morph.py b/taskcluster/gecko_taskgraph/morph.py index 1d03ddaab6..42fe4597fa 100644 --- a/taskcluster/gecko_taskgraph/morph.py +++ b/taskcluster/gecko_taskgraph/morph.py @@ -254,10 +254,31 @@ def add_eager_cache_index_tasks(taskgraph, label_to_taskid, parameters, graph_co @register_morph def add_try_task_duplicates(taskgraph, label_to_taskid, parameters, graph_config): - try_config = parameters["try_task_config"] + return _add_try_task_duplicates( + taskgraph, label_to_taskid, parameters, graph_config + ) + + +# this shim function exists so we can call it from the unittests. +# this works around an issue with +# third_party/python/taskcluster_taskgraph/taskgraph/morph.py#40 +def _add_try_task_duplicates(taskgraph, label_to_taskid, parameters, graph_config): + try_config = parameters.get("try_task_config", {}) + tasks = try_config.get("tasks", []) + glob_tasks = {x.strip("-*") for x in tasks if x.endswith("-*")} + tasks = set(tasks) - glob_tasks + rebuild = try_config.get("rebuild") if rebuild: for task in taskgraph.tasks.values(): - if task.label in try_config.get("tasks", []): + chunk_index = -1 + if task.label.endswith("-cf"): + chunk_index = -2 + label_parts = task.label.split("-") + label_no_chunk = "-".join(label_parts[:chunk_index]) + + if label_parts[chunk_index].isnumeric() and label_no_chunk in glob_tasks: + task.attributes["task_duplicates"] = rebuild + elif task.label in tasks: task.attributes["task_duplicates"] = rebuild return taskgraph, label_to_taskid diff --git a/taskcluster/gecko_taskgraph/parameters.py b/taskcluster/gecko_taskgraph/parameters.py index 2a61a71b96..7e3de1372f 100644 --- a/taskcluster/gecko_taskgraph/parameters.py +++ b/taskcluster/gecko_taskgraph/parameters.py @@ -74,6 +74,10 @@ gecko_parameters_schema = { "worker-overrides", description="Mapping of worker alias to worker pools to use for those aliases.", ): {str: str}, + Optional( + "worker-types", + description="List of worker types that we will use to run tasks on.", + ): [str], Optional("routes"): [str], }, Required("version"): str, diff --git a/taskcluster/gecko_taskgraph/target_tasks.py b/taskcluster/gecko_taskgraph/target_tasks.py index 2f445d3f95..fcbfab4e17 100644 --- a/taskcluster/gecko_taskgraph/target_tasks.py +++ b/taskcluster/gecko_taskgraph/target_tasks.py @@ -4,6 +4,7 @@ import itertools +import logging import os import re from datetime import datetime, timedelta @@ -21,6 +22,9 @@ from gecko_taskgraph.util.attributes import ( from gecko_taskgraph.util.hg import find_hg_revision_push_info, get_hg_commit_message from gecko_taskgraph.util.platforms import platform_family +logger = logging.getLogger(__name__) + + # Some tasks show up in the target task set, but are possibly special cases, # uncommon tasks, or tasks running against limited hardware set that they # should only be selectable with --full. @@ -247,9 +251,9 @@ def accept_raptor_android_build(platform): if "p5" in platform and "aarch64" in platform: return False if "p6" in platform and "aarch64" in platform: - return False + return True if "s21" in platform and "aarch64" in platform: - return False + return True if "a51" in platform: return True return False @@ -299,16 +303,30 @@ def _try_task_config(full_task_graph, parameters, graph_config): pattern_tasks = [x for x in requested_tasks if x.endswith("-*")] tasks = list(set(requested_tasks) - set(pattern_tasks)) matched_tasks = [] + missing = set() for pattern in pattern_tasks: - matched_tasks.extend( - [ - t - for t in full_task_graph.graph.nodes - if t.split(pattern.replace("*", ""))[-1].isnumeric() - ] - ) + found = [ + t + for t in full_task_graph.graph.nodes + if t.split(pattern.replace("*", ""))[-1].isnumeric() + ] + if found: + matched_tasks.extend(found) + else: + missing.add(pattern) + + if "MOZHARNESS_TEST_PATHS" in parameters["try_task_config"].get("env", {}): + matched_tasks = [x for x in matched_tasks if x.endswith("-1")] - return list(set(tasks) | set(matched_tasks)) + selected_tasks = set(tasks) | set(matched_tasks) + missing.update(selected_tasks - set(full_task_graph.tasks)) + + if missing: + missing_str = "\n ".join(sorted(missing)) + logger.warning( + f"The following tasks were requested but do not exist in the full task graph and will be skipped:\n {missing_str}" + ) + return list(selected_tasks - missing) def _try_option_syntax(full_task_graph, parameters, graph_config): @@ -731,6 +749,7 @@ def target_tasks_larch(full_task_graph, parameters, graph_config): "l10n" in task.kind or "msix" in task.kind or "android" in task.attributes.get("build_platform", "") + or (task.kind == "test" and "msix" in task.label) ): return False # otherwise reduce tests only @@ -800,6 +819,8 @@ def target_tasks_custom_car_perf_testing(full_task_graph, parameters, graph_conf if "browsertime" in try_name and ( "custom-car" in try_name or "cstm-car-m" in try_name ): + if "hw-s21" in platform and "speedometer3" not in try_name: + return False return True return False @@ -853,6 +874,8 @@ def target_tasks_general_perf_testing(full_task_graph, parameters, graph_config) return True # Android selection elif accept_raptor_android_build(platform): + if "hw-s21" in platform and "speedometer3" not in try_name: + return False if "chrome-m" in try_name and ( ("ebay" in try_name and "live" not in try_name) or ( @@ -935,6 +958,8 @@ def target_tasks_speedometer_tests(full_task_graph, parameters, graph_config): platform ): try_name = attributes.get("raptor_try_name") + if "hw-s21" in platform and "speedometer3" not in try_name: + return False if ( "browsertime" in try_name and "speedometer" in try_name @@ -950,7 +975,9 @@ def target_tasks_nightly_linux(full_task_graph, parameters, graph_config): """Select the set of tasks required for a nightly build of linux. The nightly build process involves a pipeline of builds, signing, and, eventually, uploading the tasks to balrog.""" - filter = make_desktop_nightly_filter({"linux64-shippable", "linux-shippable"}) + filter = make_desktop_nightly_filter( + {"linux64-shippable", "linux-shippable", "linux-aarch64-shippable"} + ) return [l for l, t in full_task_graph.tasks.items() if filter(t, parameters)] @@ -1060,6 +1087,7 @@ def target_tasks_searchfox(full_task_graph, parameters, graph_config): "searchfox-macosx64-searchfox/debug", "searchfox-win64-searchfox/debug", "searchfox-android-armv7-searchfox/debug", + "searchfox-ios-searchfox/debug", "source-test-file-metadata-bugzilla-components", "source-test-file-metadata-test-info-all", "source-test-wpt-metadata-summary", diff --git a/taskcluster/gecko_taskgraph/test/test_morph.py b/taskcluster/gecko_taskgraph/test/test_morph.py index c29fb58207..c3c499769c 100644 --- a/taskcluster/gecko_taskgraph/test/test_morph.py +++ b/taskcluster/gecko_taskgraph/test/test_morph.py @@ -26,6 +26,102 @@ def make_taskgraph(): return inner +@pytest.mark.parametrize( + "params,expected", + ( + pytest.param( + { + "try_mode": "try_task_config", + "try_task_config": { + "rebuild": 10, + "tasks": ["b"], + }, + "project": "try", + }, + {"b": 10}, + id="duplicates no chunks", + ), + pytest.param( + { + "try_mode": "try_task_config", + "try_task_config": { + "rebuild": 10, + "tasks": ["a-*"], + }, + "project": "try", + }, + {"a-1": 10, "a-2": 10}, + id="duplicates with chunks", + ), + pytest.param( + { + "try_mode": "try_task_config", + "try_task_config": { + "rebuild": 10, + "tasks": ["a-*", "b"], + }, + "project": "try", + }, + {"a-1": 10, "a-2": 10, "b": 10}, + id="duplicates with and without chunks", + ), + pytest.param( + { + "try_mode": "try_task_config", + "try_task_config": { + "tasks": ["a-*"], + }, + "project": "try", + }, + {"a-1": -1, "a-2": -1}, + id="no rebuild, no duplicates", + ), + pytest.param( + { + "try_mode": "try_task_config", + "try_task_config": { + "rebuild": 0, + "tasks": ["a-*"], + }, + "project": "try", + }, + {"a-1": -1, "a-2": -1}, + id="rebuild of zero", + ), + pytest.param( + { + "try_mode": "try_task_config", + "try_task_config": { + "rebuild": 100, + "tasks": ["a-*"], + }, + "project": "try", + }, + {"a-1": 100, "a-2": 100}, + id="rebuild 100", + ), + ), +) +def test_try_task_duplicates(make_taskgraph, graph_config, params, expected): + taskb = Task(kind="test", label="b", attributes={}, task={}) + task1 = Task(kind="test", label="a-1", attributes={}, task={}) + task2 = Task(kind="test", label="a-2", attributes={}, task={}) + taskgraph, label_to_taskid = make_taskgraph( + { + taskb.label: taskb, + task1.label: task1, + task2.label: task2, + } + ) + + taskgraph, label_to_taskid = morph._add_try_task_duplicates( + taskgraph, label_to_taskid, params, graph_config + ) + for label in expected: + task = taskgraph.tasks[label] + assert task.attributes.get("task_duplicates", -1) == expected[label] + + def test_make_index_tasks(make_taskgraph, graph_config): task_def = { "routes": [ diff --git a/taskcluster/gecko_taskgraph/test/test_target_tasks.py b/taskcluster/gecko_taskgraph/test/test_target_tasks.py index 2bbc57fcf3..22582f9040 100644 --- a/taskcluster/gecko_taskgraph/test/test_target_tasks.py +++ b/taskcluster/gecko_taskgraph/test/test_target_tasks.py @@ -210,6 +210,21 @@ class TestTargetTasks(unittest.TestCase): } self.assertEqual(sorted(method(tg, params, {})), ["ddd-1", "ddd-2"]) + def test_try_task_config_regex_with_paths(self): + "try_mode = try_task_config uses the try config with regex instead of chunk numbers" + tg = self.make_task_graph() + method = target_tasks.get_method("try_tasks") + params = { + "try_mode": "try_task_config", + "try_task_config": { + "new-test-config": True, + "tasks": ["ddd-*"], + "env": {"MOZHARNESS_TEST_PATHS": "foo/bar"}, + }, + "project": "try", + } + self.assertEqual(sorted(method(tg, params, {})), ["ddd-1"]) + def test_try_task_config_absolute(self): "try_mode = try_task_config uses the try config with full task labels" tg = self.make_task_graph() diff --git a/taskcluster/gecko_taskgraph/test/test_util_partials.py b/taskcluster/gecko_taskgraph/test/test_util_partials.py index 3630d7b0ec..4f8bea8662 100644 --- a/taskcluster/gecko_taskgraph/test/test_util_partials.py +++ b/taskcluster/gecko_taskgraph/test/test_util_partials.py @@ -30,18 +30,34 @@ release_blob = { def nightly_blob(release): - return { - "platforms": { - "WINNT_x86_64-msvc": { - "locales": { - "en-US": { - "buildID": release[-14:], - "completes": [{"fileUrl": release}], + # Added for bug 1883046, where we identified a case where a Balrog release + # that does not contain completes will throw an unnecessary exception. + if release == "Firefox-mozilla-central-nightly-20211001214601": + return { + "platforms": { + "WINNT_x86_64-msvc": { + "locales": { + "en-US": { + "buildID": release[-14:], + "partials": [{"fileUrl": release}], + } + } + } + } + } + else: + return { + "platforms": { + "WINNT_x86_64-msvc": { + "locales": { + "en-US": { + "buildID": release[-14:], + "completes": [{"fileUrl": release}], + } } } } } - } class TestReleaseHistory(unittest.TestCase): diff --git a/taskcluster/gecko_taskgraph/transforms/artifact.py b/taskcluster/gecko_taskgraph/transforms/artifact.py index 559148f7b4..b14f723d4a 100644 --- a/taskcluster/gecko_taskgraph/transforms/artifact.py +++ b/taskcluster/gecko_taskgraph/transforms/artifact.py @@ -85,9 +85,11 @@ def set_artifact_expiration(config, jobs): art_dict = manifest["macos"] elif plat.startswith("android"): art_dict = manifest["android"] + elif plat.startswith("ios"): + art_dict = manifest["ios"] else: print( - 'The platform name "{plat}" didn\'t start with', + f'The platform name "{plat}" didn\'t start with', '"win", "mac", "android", or "linux".', file=sys.stderr, ) diff --git a/taskcluster/gecko_taskgraph/transforms/artifacts.yml b/taskcluster/gecko_taskgraph/transforms/artifacts.yml index 26f06640ad..efcbc70f15 100644 --- a/taskcluster/gecko_taskgraph/transforms/artifacts.yml +++ b/taskcluster/gecko_taskgraph/transforms/artifacts.yml @@ -18,3 +18,7 @@ android: target.crashreporter-symbols-full.tar.zst: shortest sccache.log: shortest sccache-stats.json: shortest + +ios: + sccache.log: shortest + sccache-stats.json: shortest diff --git a/taskcluster/gecko_taskgraph/transforms/bootstrap.py b/taskcluster/gecko_taskgraph/transforms/bootstrap.py index e4537cab01..9c02fc6819 100644 --- a/taskcluster/gecko_taskgraph/transforms/bootstrap.py +++ b/taskcluster/gecko_taskgraph/transforms/bootstrap.py @@ -59,7 +59,7 @@ def bootstrap_tasks(config, tasks): f"python3 bootstrap.py --no-interactive --application-choice {app}", "cd mozilla-unified", # After bootstrap, configure should go through without its own auto-bootstrap. - "./mach configure --disable-bootstrap", + "./mach configure --enable-bootstrap=no-update", # Then a build should go through too. "./mach build", ] diff --git a/taskcluster/gecko_taskgraph/transforms/job/mozharness_test.py b/taskcluster/gecko_taskgraph/transforms/job/mozharness_test.py index eb4aea609f..060f37c9c2 100644 --- a/taskcluster/gecko_taskgraph/transforms/job/mozharness_test.py +++ b/taskcluster/gecko_taskgraph/transforms/job/mozharness_test.py @@ -15,6 +15,7 @@ from gecko_taskgraph.transforms.job import configure_taskdesc_for_run, run_job_u from gecko_taskgraph.transforms.job.common import get_expiration, support_vcs_checkout from gecko_taskgraph.transforms.test import normpath, test_description_schema from gecko_taskgraph.util.attributes import is_try +from gecko_taskgraph.util.perftest import is_external_browser VARIANTS = [ "shippable", @@ -63,9 +64,13 @@ def test_packages_url(taskdesc): ) # for android shippable we need to add 'en-US' to the artifact url test = taskdesc["run"]["test"] - if "android" in test["test-platform"] and ( - get_variant(test["test-platform"]) - in ("shippable", "shippable-qr", "shippable-lite", "shippable-lite-qr") + if ( + "android" in test["test-platform"] + and ( + get_variant(test["test-platform"]) + in ("shippable", "shippable-qr", "shippable-lite", "shippable-lite-qr") + ) + and not is_external_browser(test.get("try-name", "")) ): head, tail = os.path.split(artifact_url) artifact_url = os.path.join(head, "en-US", tail) diff --git a/taskcluster/gecko_taskgraph/transforms/snap_test.py b/taskcluster/gecko_taskgraph/transforms/snap_test.py index e6d879f225..d49276843d 100644 --- a/taskcluster/gecko_taskgraph/transforms/snap_test.py +++ b/taskcluster/gecko_taskgraph/transforms/snap_test.py @@ -43,6 +43,8 @@ def fill_template(config, tasks): timeout = 10 if collection != "opt": timeout = 60 + task["task"]["payload"]["env"]["BUILD_IS_DEBUG"] = "1" + task["task"]["payload"]["env"]["TEST_TIMEOUT"] = "{}".format(timeout) yield task diff --git a/taskcluster/gecko_taskgraph/transforms/source_test.py b/taskcluster/gecko_taskgraph/transforms/source_test.py index 5c561e8114..e3eeb7c819 100644 --- a/taskcluster/gecko_taskgraph/transforms/source_test.py +++ b/taskcluster/gecko_taskgraph/transforms/source_test.py @@ -10,7 +10,6 @@ treeherder configuration and attributes for that platform. import copy import os -import taskgraph from taskgraph.transforms.base import TransformSequence from taskgraph.util.attributes import keymatch from taskgraph.util.schema import Schema, optionally_keyed_by, resolve_keyed_by @@ -18,7 +17,6 @@ from taskgraph.util.treeherder import join_symbol, split_symbol from voluptuous import Any, Extra, Optional, Required from gecko_taskgraph.transforms.job import job_description_schema -from gecko_taskgraph.util.hg import get_json_automationrelevance source_test_description_schema = Schema( { @@ -239,33 +237,6 @@ def set_code_review_env(config, jobs): @transforms.add -def set_base_revision_in_tgdiff(config, jobs): - # Don't attempt to download 'json-automation' locally as the revision may - # not exist in the repository. - if not os.environ.get("MOZ_AUTOMATION") or taskgraph.fast: - yield from jobs - return - - data = get_json_automationrelevance( - config.params["head_repository"], config.params["head_rev"] - ) - for job in jobs: - if job["name"] != "taskgraph-diff": - yield job - continue - - job["task-context"] = { - "from-object": { - "base_rev": data["changesets"][0]["parents"][0], - }, - "substitution-fields": [ - "run.command", - ], - } - yield job - - -@transforms.add def set_worker_exit_code(config, jobs): for job in jobs: worker = job["worker"] diff --git a/taskcluster/gecko_taskgraph/transforms/test/other.py b/taskcluster/gecko_taskgraph/transforms/test/other.py index dc258ef97a..b8cb95cff7 100644 --- a/taskcluster/gecko_taskgraph/transforms/test/other.py +++ b/taskcluster/gecko_taskgraph/transforms/test/other.py @@ -16,6 +16,7 @@ from taskgraph.util.taskcluster import get_artifact_path, get_index_url from voluptuous import Any, Optional, Required from gecko_taskgraph.transforms.test.variant import TEST_VARIANTS +from gecko_taskgraph.util.perftest import is_external_browser from gecko_taskgraph.util.platforms import platform_family from gecko_taskgraph.util.templates import merge @@ -260,6 +261,20 @@ def handle_keyed_by(config, tasks): @transforms.add +def setup_raptor_external_browser_platforms(config, tasks): + for task in tasks: + if task["suite"] != "raptor": + yield task + continue + + if is_external_browser(task["try-name"]): + task["build-platform"] = "linux64/opt" + task["build-label"] = "build-linux64/opt" + + yield task + + +@transforms.add def set_target(config, tasks): for task in tasks: build_platform = task["build-platform"] @@ -308,25 +323,25 @@ def setup_browsertime(config, tasks): ts = { "by-test-platform": { - "android.*": ["browsertime", "linux64-geckodriver", "linux64-node-16"], - "linux.*": ["browsertime", "linux64-geckodriver", "linux64-node-16"], + "android.*": ["browsertime", "linux64-geckodriver", "linux64-node"], + "linux.*": ["browsertime", "linux64-geckodriver", "linux64-node"], "macosx1015.*": [ "browsertime", "macosx64-geckodriver", - "macosx64-node-16", + "macosx64-node", ], "macosx1400.*": [ "browsertime", "macosx64-aarch64-geckodriver", - "macosx64-aarch64-node-16", + "macosx64-aarch64-node", ], "windows.*aarch64.*": [ "browsertime", "win32-geckodriver", - "win32-node-16", + "win32-node", ], - "windows.*-32.*": ["browsertime", "win32-geckodriver", "win32-node-16"], - "windows.*-64.*": ["browsertime", "win64-geckodriver", "win64-node-16"], + "windows.*-32.*": ["browsertime", "win32-geckodriver", "win32-node"], + "windows.*-64.*": ["browsertime", "win64-geckodriver", "win64-node"], }, } diff --git a/taskcluster/gecko_taskgraph/transforms/test/raptor.py b/taskcluster/gecko_taskgraph/transforms/test/raptor.py index 0667d22bb2..18e21e6a1e 100644 --- a/taskcluster/gecko_taskgraph/transforms/test/raptor.py +++ b/taskcluster/gecko_taskgraph/transforms/test/raptor.py @@ -10,6 +10,7 @@ from voluptuous import Extra, Optional, Required from gecko_taskgraph.transforms.test import test_description_schema from gecko_taskgraph.util.copy_task import copy_task +from gecko_taskgraph.util.perftest import is_external_browser transforms = TransformSequence() task_transforms = TransformSequence() @@ -316,6 +317,28 @@ def add_extra_options(config, tests): yield test +@transforms.add +def modify_mozharness_configs(config, tests): + for test in tests: + if not is_external_browser(test["app"]): + yield test + continue + + test_platform = test["test-platform"] + mozharness = test.setdefault("mozharness", {}) + if "mac" in test_platform: + mozharness["config"] = ["raptor/mac_external_browser_config.py"] + elif "windows" in test_platform: + mozharness["config"] = ["raptor/windows_external_browser_config.py"] + elif "linux" in test_platform: + mozharness["config"] = ["raptor/linux_external_browser_config.py"] + elif "android" in test_platform: + test["target"] = "target.tar.bz2" + mozharness["config"] = ["raptor/android_hw_external_browser_config.py"] + + yield test + + @task_transforms.add def add_scopes_and_proxy(config, tasks): for task in tasks: diff --git a/taskcluster/gecko_taskgraph/util/partials.py b/taskcluster/gecko_taskgraph/util/partials.py index 1a3affcc42..b04fc64e17 100644 --- a/taskcluster/gecko_taskgraph/util/partials.py +++ b/taskcluster/gecko_taskgraph/util/partials.py @@ -251,7 +251,17 @@ def _populate_nightly_history(product, branch, maxbuilds=4, maxsearch=10): builds[platform][locale] = dict() if len(builds[platform][locale]) >= maxbuilds: continue + if "buildID" not in history["platforms"][platform]["locales"][locale]: + continue buildid = history["platforms"][platform]["locales"][locale]["buildID"] + if ( + "completes" not in history["platforms"][platform]["locales"][locale] + or len( + history["platforms"][platform]["locales"][locale]["completes"] + ) + == 0 + ): + continue url = history["platforms"][platform]["locales"][locale]["completes"][0][ "fileUrl" ] diff --git a/taskcluster/gecko_taskgraph/util/perftest.py b/taskcluster/gecko_taskgraph/util/perftest.py new file mode 100644 index 0000000000..01b153be37 --- /dev/null +++ b/taskcluster/gecko_taskgraph/util/perftest.py @@ -0,0 +1,18 @@ +# 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/. + + +def is_external_browser(label): + if any( + external_browser in label + for external_browser in ( + "safari", + "chrome", + "custom-car", + "chrome-m", + "cstm-car-m", + ) + ): + return True + return False |