219 lines
6.2 KiB
Python
219 lines
6.2 KiB
Python
# 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}
|