# 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 functools import hashlib import os import pathlib import sys import time import requests from taskgraph import create from taskgraph.util import json from taskgraph.util.taskcluster import requests_retry_session try: # TODO(py3): use time.monotonic() from time import monotonic except ImportError: from time import time as monotonic BUGBUG_BASE_URL = "https://bugbug.moz.tools" RETRY_TIMEOUT = 9 * 60 # seconds RETRY_INTERVAL = 10 # seconds # Preset confidence thresholds. CT_LOW = 0.7 CT_MEDIUM = 0.8 CT_HIGH = 0.9 GROUP_TRANSLATIONS = { "testing/web-platform/tests": "", "testing/web-platform/mozilla/tests": "/_mozilla", } def translate_group(group): for prefix, value in GROUP_TRANSLATIONS.items(): if group.startswith(prefix): return group.replace(prefix, value) return group class BugbugTimeoutException(Exception): pass @functools.cache def get_session(): s = requests.Session() s.headers.update({"X-API-KEY": "gecko-taskgraph"}) return requests_retry_session(retries=5, session=s) def _perfherder_artifact_path(base_path, perfherder_data): base_dir = base_path.parent stem = base_path.stem sequence = int(time.monotonic() * 1000) payload = json.dumps(perfherder_data, sort_keys=True).encode("utf-8") digest = hashlib.sha1(payload).hexdigest()[:8] return base_dir / f"{stem}-{sequence}-{digest}.json" def _write_perfherder_data(lower_is_better): if os.environ.get("MOZ_AUTOMATION", "0") == "1": perfherder_data = { "framework": {"name": "build_metrics"}, "suites": [ { "name": suite, "value": value, "lowerIsBetter": True, "shouldAlert": False, "subtests": [], } for suite, value in lower_is_better.items() ], } print(f"PERFHERDER_DATA: {json.dumps(perfherder_data)}", file=sys.stderr) perfherder_path = os.environ.get("MOZ_PERFHERDER_UPLOAD") decision_upload_dir = os.environ.get("MOZ_UPLOAD_DIR") if perfherder_path: upload_path = pathlib.Path(perfherder_path) elif decision_upload_dir: upload_path = ( pathlib.Path(decision_upload_dir) / "perfherder-data-bugbug.json" ) else: return upload_path.parent.mkdir(parents=True, exist_ok=True) target = _perfherder_artifact_path(upload_path, perfherder_data) with target.open("w", encoding="utf-8") as f: json.dump(perfherder_data, f) @functools.cache def push_schedules(branch, rev): # Noop if we're in test-action-callback if create.testing: return url = BUGBUG_BASE_URL + f"/push/{branch}/{rev}/schedules" start = monotonic() session = get_session() # On try there is no fallback and pulling is slower, so we allow bugbug more # time to compute the results. # See https://github.com/mozilla/bugbug/issues/1673. timeout = RETRY_TIMEOUT if branch == "try": timeout += int(timeout / 3) attempts = timeout / RETRY_INTERVAL i = 0 while i < attempts: r = session.get(url) r.raise_for_status() if r.status_code != 202: break time.sleep(RETRY_INTERVAL) i += 1 end = monotonic() _write_perfherder_data( lower_is_better={ "bugbug_push_schedules_time": end - start, "bugbug_push_schedules_retries": i, } ) data = r.json() if r.status_code == 202: raise BugbugTimeoutException(f"Timed out waiting for result from '{url}'") if "groups" in data: data["groups"] = {translate_group(k): v for k, v in data["groups"].items()} if "config_groups" in data: data["config_groups"] = { translate_group(k): v for k, v in data["config_groups"].items() } return data @functools.cache def patch_schedules(base_rev, patch_content, mode="quick"): """Query BugBug API with a patch to get test recommendations. This is used by `./mach test --auto` to get test recommendations for local changes. Args: base_rev (str): The base revision hash. patch_content (str): The patch content with commit metadata. mode (str): The mode of test selection, which determines the confidence threshold. One of 'extensive', 'moderate', or 'quick'. Returns: dict: A dictionary with containing test recommendations filtered by confidence threshold. Raises: BugbugTimeoutException: If the API times out. """ import hashlib import re # This ensures consistent hashing across multiple runs with identical # changes by stripping the date before hashing. filtered_content = re.sub(r"^Date: .*$", "", patch_content, flags=re.MULTILINE) patch_hash = hashlib.md5(filtered_content.encode("utf-8")).hexdigest() url = BUGBUG_BASE_URL + f"/patch/{base_rev}/{patch_hash}/schedules" session = get_session() r = session.post( url, data=patch_content.encode("utf-8"), headers={"Content-Type": "text/plain"}, ) r.raise_for_status() timeout = RETRY_TIMEOUT attempts = timeout / RETRY_INTERVAL i = 0 while i < attempts: if r.status_code != 202: break time.sleep(RETRY_INTERVAL) r = session.get(url) r.raise_for_status() i += 1 data = r.json() if r.status_code == 202: raise BugbugTimeoutException(f"Timed out waiting for result from '{url}'") if mode == "extensive": confidence_threshold = CT_LOW elif mode == "moderate": confidence_threshold = CT_MEDIUM elif mode == "quick": confidence_threshold = CT_HIGH else: raise ValueError( f"Invalid mode: '{mode}'; expected one of 'extensive', 'moderate', 'quick'" ) return {k: v for k, v in data["groups"].items() if v >= confidence_threshold}