forked from mirrors/gecko-dev
656 lines
20 KiB
Python
656 lines
20 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/.
|
|
|
|
"""
|
|
Templates provide a way of modifying the task definition of selected tasks.
|
|
They are added to 'try_task_config.json' and processed by the transforms.
|
|
"""
|
|
|
|
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
from abc import ABCMeta, abstractmethod, abstractproperty
|
|
from argparse import SUPPRESS, Action
|
|
from contextlib import contextmanager
|
|
from textwrap import dedent
|
|
|
|
import mozpack.path as mozpath
|
|
import requests
|
|
import six
|
|
from mozbuild.base import BuildEnvironmentNotFoundException, MozbuildObject
|
|
from mozversioncontrol import Repository
|
|
from taskgraph.util import taskcluster
|
|
|
|
from .tasks import resolve_tests_by_suite
|
|
from .util.ssh import get_ssh_user
|
|
|
|
here = pathlib.Path(__file__).parent
|
|
build = MozbuildObject.from_environment(cwd=str(here))
|
|
|
|
|
|
@contextmanager
|
|
def try_config_commit(vcs: Repository, commit_message: str):
|
|
"""Context manager that creates and removes a try config commit."""
|
|
# Add the `try_task_config.json` file if it exists.
|
|
try_task_config_path = pathlib.Path(build.topsrcdir) / "try_task_config.json"
|
|
if try_task_config_path.exists():
|
|
vcs.add_remove_files("try_task_config.json")
|
|
|
|
try:
|
|
# Create a try config commit.
|
|
vcs.create_try_commit(commit_message)
|
|
|
|
yield
|
|
finally:
|
|
# Revert the try config commit.
|
|
vcs.remove_current_commit()
|
|
|
|
|
|
class ParameterConfig:
|
|
__metaclass__ = ABCMeta
|
|
|
|
def __init__(self):
|
|
self.dests = set()
|
|
|
|
def add_arguments(self, parser):
|
|
for cli, kwargs in self.arguments:
|
|
action = parser.add_argument(*cli, **kwargs)
|
|
self.dests.add(action.dest)
|
|
|
|
@abstractproperty
|
|
def arguments(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_parameters(self, **kwargs) -> dict:
|
|
pass
|
|
|
|
def validate(self, **kwargs):
|
|
pass
|
|
|
|
|
|
class TryConfig(ParameterConfig):
|
|
@abstractmethod
|
|
def try_config(self, **kwargs) -> dict:
|
|
pass
|
|
|
|
def get_parameters(self, **kwargs):
|
|
result = self.try_config(**kwargs)
|
|
if not result:
|
|
return None
|
|
return {"try_task_config": result}
|
|
|
|
|
|
class Artifact(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--artifact"],
|
|
{"action": "store_true", "help": "Force artifact builds where possible."},
|
|
],
|
|
[
|
|
["--no-artifact"],
|
|
{
|
|
"action": "store_true",
|
|
"help": "Disable artifact builds even if being used locally.",
|
|
},
|
|
],
|
|
]
|
|
|
|
def add_arguments(self, parser):
|
|
group = parser.add_mutually_exclusive_group()
|
|
return super().add_arguments(group)
|
|
|
|
@classmethod
|
|
def is_artifact_build(cls):
|
|
try:
|
|
return build.substs.get("MOZ_ARTIFACT_BUILDS", False)
|
|
except BuildEnvironmentNotFoundException:
|
|
return False
|
|
|
|
def try_config(self, artifact, no_artifact, **kwargs):
|
|
if artifact:
|
|
return {"use-artifact-builds": True, "disable-pgo": True}
|
|
|
|
if no_artifact:
|
|
return
|
|
|
|
if self.is_artifact_build():
|
|
print("Artifact builds enabled, pass --no-artifact to disable")
|
|
return {"use-artifact-builds": True, "disable-pgo": True}
|
|
|
|
|
|
class Pernosco(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--pernosco"],
|
|
{
|
|
"action": "store_true",
|
|
"default": None,
|
|
"help": "Opt-in to analysis by the Pernosco debugging service.",
|
|
},
|
|
],
|
|
[
|
|
["--no-pernosco"],
|
|
{
|
|
"dest": "pernosco",
|
|
"action": "store_false",
|
|
"default": None,
|
|
"help": "Opt-out of the Pernosco debugging service (if you are on the include list).",
|
|
},
|
|
],
|
|
]
|
|
|
|
def add_arguments(self, parser):
|
|
group = parser.add_mutually_exclusive_group()
|
|
return super().add_arguments(group)
|
|
|
|
def try_config(self, pernosco, **kwargs):
|
|
if pernosco is None:
|
|
return
|
|
|
|
if pernosco:
|
|
try:
|
|
# The Pernosco service currently requires a Mozilla e-mail address to
|
|
# log in. Prevent people with non-Mozilla addresses from using this
|
|
# flag so they don't end up consuming time and resources only to
|
|
# realize they can't actually log in and see the reports.
|
|
address = get_ssh_user()
|
|
if not address.endswith("@mozilla.com"):
|
|
print(
|
|
dedent(
|
|
"""\
|
|
Pernosco requires a Mozilla e-mail address to view its reports. Please
|
|
push to try with an @mozilla.com address to use --pernosco.
|
|
|
|
Current user: {}
|
|
""".format(
|
|
address
|
|
)
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
|
|
except (subprocess.CalledProcessError, IndexError):
|
|
print("warning: failed to detect current user for 'hg.mozilla.org'")
|
|
print("Pernosco requires a Mozilla e-mail address to view its reports.")
|
|
while True:
|
|
answer = input(
|
|
"Do you have an @mozilla.com address? [Y/n]: "
|
|
).lower()
|
|
if answer == "n":
|
|
sys.exit(1)
|
|
elif answer == "y":
|
|
break
|
|
|
|
return {
|
|
"env": {
|
|
"PERNOSCO": str(int(pernosco)),
|
|
}
|
|
}
|
|
|
|
def validate(self, **kwargs):
|
|
try_config = kwargs["try_config_params"].get("try_task_config") or {}
|
|
if try_config.get("use-artifact-builds"):
|
|
print(
|
|
"Pernosco does not support artifact builds at this time. "
|
|
"Please try again with '--no-artifact'."
|
|
)
|
|
sys.exit(1)
|
|
|
|
|
|
class Path(TryConfig):
|
|
arguments = [
|
|
[
|
|
["paths"],
|
|
{
|
|
"nargs": "*",
|
|
"default": [],
|
|
"help": "Run tasks containing tests under the specified path(s).",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, paths, **kwargs):
|
|
if not paths:
|
|
return
|
|
|
|
for p in paths:
|
|
if not os.path.exists(p):
|
|
print("error: '{}' is not a valid path.".format(p), file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
paths = [
|
|
mozpath.relpath(mozpath.join(os.getcwd(), p), build.topsrcdir)
|
|
for p in paths
|
|
]
|
|
return {
|
|
"env": {
|
|
"MOZHARNESS_TEST_PATHS": six.ensure_text(
|
|
json.dumps(resolve_tests_by_suite(paths))
|
|
),
|
|
}
|
|
}
|
|
|
|
|
|
class Environment(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--env"],
|
|
{
|
|
"action": "append",
|
|
"default": None,
|
|
"help": "Set an environment variable, of the form FOO=BAR. "
|
|
"Can be passed in multiple times.",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, env, **kwargs):
|
|
if not env:
|
|
return
|
|
return {
|
|
"env": dict(e.split("=", 1) for e in env),
|
|
}
|
|
|
|
|
|
class ExistingTasks(ParameterConfig):
|
|
TREEHERDER_PUSH_ENDPOINT = (
|
|
"https://treeherder.mozilla.org/api/project/try/push/?count=1&author={user}"
|
|
)
|
|
TREEHERDER_PUSH_URL = (
|
|
"https://treeherder.mozilla.org/jobs?repo={branch}&revision={revision}"
|
|
)
|
|
|
|
arguments = [
|
|
[
|
|
["-E", "--use-existing-tasks"],
|
|
{
|
|
"const": "last_try_push",
|
|
"default": None,
|
|
"nargs": "?",
|
|
"help": """
|
|
Use existing tasks from a previous push. Without args this
|
|
uses your most recent try push. You may also specify
|
|
`rev=<revision>` where <revision> is the head revision of the
|
|
try push or `task-id=<task id>` where <task id> is the Decision
|
|
task id of the push. This last method even works for non-try
|
|
branches.
|
|
""",
|
|
},
|
|
]
|
|
]
|
|
|
|
def find_decision_task(self, use_existing_tasks):
|
|
branch = "try"
|
|
if use_existing_tasks == "last_try_push":
|
|
# Use existing tasks from user's previous try push.
|
|
user = get_ssh_user()
|
|
url = self.TREEHERDER_PUSH_ENDPOINT.format(user=user)
|
|
res = requests.get(url, headers={"User-Agent": "gecko-mach-try/1.0"})
|
|
res.raise_for_status()
|
|
data = res.json()
|
|
if data["meta"]["count"] == 0:
|
|
raise Exception(f"Could not find a try push for '{user}'!")
|
|
revision = data["results"][0]["revision"]
|
|
|
|
elif use_existing_tasks.startswith("rev="):
|
|
revision = use_existing_tasks[len("rev=") :]
|
|
|
|
else:
|
|
raise Exception("Unable to parse '{use_existing_tasks}'!")
|
|
|
|
url = self.TREEHERDER_PUSH_URL.format(branch=branch, revision=revision)
|
|
print(f"Using existing tasks from: {url}")
|
|
index_path = f"gecko.v2.{branch}.revision.{revision}.taskgraph.decision"
|
|
return taskcluster.find_task_id(index_path)
|
|
|
|
def get_parameters(self, use_existing_tasks, **kwargs):
|
|
if not use_existing_tasks:
|
|
return
|
|
|
|
if use_existing_tasks.startswith("task-id="):
|
|
tid = use_existing_tasks[len("task-id=") :]
|
|
else:
|
|
tid = self.find_decision_task(use_existing_tasks)
|
|
|
|
label_to_task_id = taskcluster.get_artifact(tid, "public/label-to-taskid.json")
|
|
return {"existing_tasks": label_to_task_id}
|
|
|
|
|
|
class RangeAction(Action):
|
|
def __init__(self, min, max, *args, **kwargs):
|
|
self.min = min
|
|
self.max = max
|
|
kwargs["metavar"] = "[{}-{}]".format(self.min, self.max)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
name = option_string or self.dest
|
|
if values < self.min:
|
|
parser.error("{} can not be less than {}".format(name, self.min))
|
|
if values > self.max:
|
|
parser.error("{} can not be more than {}".format(name, self.max))
|
|
setattr(namespace, self.dest, values)
|
|
|
|
|
|
class Rebuild(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--rebuild"],
|
|
{
|
|
"action": RangeAction,
|
|
"min": 2,
|
|
"max": 20,
|
|
"default": None,
|
|
"type": int,
|
|
"help": "Rebuild all selected tasks the specified number of times.",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, rebuild, **kwargs):
|
|
if not rebuild:
|
|
return
|
|
|
|
if (
|
|
not kwargs.get("new_test_config", False)
|
|
and kwargs.get("full")
|
|
and rebuild > 3
|
|
):
|
|
print(
|
|
"warning: limiting --rebuild to 3 when using --full. "
|
|
"Use custom push actions to add more."
|
|
)
|
|
rebuild = 3
|
|
|
|
return {
|
|
"rebuild": rebuild,
|
|
}
|
|
|
|
|
|
class Routes(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--route"],
|
|
{
|
|
"action": "append",
|
|
"dest": "routes",
|
|
"help": (
|
|
"Additional route to add to the tasks "
|
|
"(note: these will not be added to the decision task)"
|
|
),
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, routes, **kwargs):
|
|
if routes:
|
|
return {
|
|
"routes": routes,
|
|
}
|
|
|
|
|
|
class ChemspillPrio(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--chemspill-prio"],
|
|
{
|
|
"action": "store_true",
|
|
"help": "Run at a higher priority than most try jobs (chemspills only).",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, chemspill_prio, **kwargs):
|
|
if chemspill_prio:
|
|
return {"chemspill-prio": True}
|
|
|
|
|
|
class GeckoProfile(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--gecko-profile"],
|
|
{
|
|
"dest": "profile",
|
|
"action": "store_true",
|
|
"default": False,
|
|
"help": "Create and upload a gecko profile during talos/raptor tasks.",
|
|
},
|
|
],
|
|
[
|
|
["--gecko-profile-interval"],
|
|
{
|
|
"dest": "gecko_profile_interval",
|
|
"type": float,
|
|
"help": "How frequently to take samples (ms)",
|
|
},
|
|
],
|
|
[
|
|
["--gecko-profile-entries"],
|
|
{
|
|
"dest": "gecko_profile_entries",
|
|
"type": int,
|
|
"help": "How many samples to take with the profiler",
|
|
},
|
|
],
|
|
[
|
|
["--gecko-profile-features"],
|
|
{
|
|
"dest": "gecko_profile_features",
|
|
"type": str,
|
|
"default": None,
|
|
"help": "Set the features enabled for the profiler.",
|
|
},
|
|
],
|
|
[
|
|
["--gecko-profile-threads"],
|
|
{
|
|
"dest": "gecko_profile_threads",
|
|
"type": str,
|
|
"help": "Comma-separated list of threads to sample.",
|
|
},
|
|
],
|
|
# For backwards compatibility
|
|
[
|
|
["--talos-profile"],
|
|
{
|
|
"dest": "profile",
|
|
"action": "store_true",
|
|
"default": False,
|
|
"help": SUPPRESS,
|
|
},
|
|
],
|
|
# This is added for consistency with the 'syntax' selector
|
|
[
|
|
["--geckoProfile"],
|
|
{
|
|
"dest": "profile",
|
|
"action": "store_true",
|
|
"default": False,
|
|
"help": SUPPRESS,
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(
|
|
self,
|
|
profile,
|
|
gecko_profile_interval,
|
|
gecko_profile_entries,
|
|
gecko_profile_features,
|
|
gecko_profile_threads,
|
|
**kwargs,
|
|
):
|
|
if profile or not all(
|
|
s is None for s in (gecko_profile_features, gecko_profile_threads)
|
|
):
|
|
cfg = {
|
|
"gecko-profile": True,
|
|
"gecko-profile-interval": gecko_profile_interval,
|
|
"gecko-profile-entries": gecko_profile_entries,
|
|
"gecko-profile-features": gecko_profile_features,
|
|
"gecko-profile-threads": gecko_profile_threads,
|
|
}
|
|
return {key: value for key, value in cfg.items() if value is not None}
|
|
|
|
|
|
class Browsertime(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--browsertime"],
|
|
{
|
|
"action": "store_true",
|
|
"help": "Use browsertime during Raptor tasks.",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, browsertime, **kwargs):
|
|
if browsertime:
|
|
return {
|
|
"browsertime": True,
|
|
}
|
|
|
|
|
|
class DisablePgo(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--disable-pgo"],
|
|
{
|
|
"action": "store_true",
|
|
"help": "Don't run PGO builds",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, disable_pgo, **kwargs):
|
|
if disable_pgo:
|
|
return {
|
|
"disable-pgo": True,
|
|
}
|
|
|
|
|
|
class NewConfig(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--new-test-config"],
|
|
{
|
|
"action": "store_true",
|
|
"help": "When a test fails (mochitest only) restart the browser and start from the next test",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, new_test_config, **kwargs):
|
|
if new_test_config:
|
|
return {
|
|
"new-test-config": True,
|
|
}
|
|
|
|
|
|
class WorkerOverrides(TryConfig):
|
|
arguments = [
|
|
[
|
|
["--worker-override"],
|
|
{
|
|
"action": "append",
|
|
"dest": "worker_overrides",
|
|
"help": (
|
|
"Override the worker pool used for a given taskgraph worker alias. "
|
|
"The argument should be `<alias>=<worker-pool>`. "
|
|
"Can be specified multiple times."
|
|
),
|
|
},
|
|
],
|
|
[
|
|
["--worker-suffix"],
|
|
{
|
|
"action": "append",
|
|
"dest": "worker_suffixes",
|
|
"help": (
|
|
"Override the worker pool used for a given taskgraph worker alias, "
|
|
"by appending a suffix to the work-pool. "
|
|
"The argument should be `<alias>=<suffix>`. "
|
|
"Can be specified multiple times."
|
|
),
|
|
},
|
|
],
|
|
[
|
|
["--worker-type"],
|
|
{
|
|
"action": "append",
|
|
"dest": "worker_types",
|
|
"default": [],
|
|
"help": "Select tasks that only run on the specified worker.",
|
|
},
|
|
],
|
|
]
|
|
|
|
def try_config(self, worker_overrides, worker_suffixes, worker_types, **kwargs):
|
|
from gecko_taskgraph.util.workertypes import get_worker_type
|
|
from taskgraph.config import load_graph_config
|
|
|
|
overrides = {}
|
|
if worker_overrides:
|
|
for override in worker_overrides:
|
|
alias, worker_pool = override.split("=", 1)
|
|
if alias in overrides:
|
|
print(
|
|
"Can't override worker alias {alias} more than once. "
|
|
"Already set to use {previous}, but also asked to use {new}.".format(
|
|
alias=alias, previous=overrides[alias], new=worker_pool
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
overrides[alias] = worker_pool
|
|
|
|
if worker_suffixes:
|
|
root = build.topsrcdir
|
|
root = os.path.join(root, "taskcluster", "ci")
|
|
graph_config = load_graph_config(root)
|
|
for worker_suffix in worker_suffixes:
|
|
alias, suffix = worker_suffix.split("=", 1)
|
|
if alias in overrides:
|
|
print(
|
|
"Can't override worker alias {alias} more than once. "
|
|
"Already set to use {previous}, but also asked "
|
|
"to add suffix {suffix}.".format(
|
|
alias=alias, previous=overrides[alias], suffix=suffix
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
provisioner, worker_type = get_worker_type(
|
|
graph_config, worker_type=alias, parameters={"level": "1"}
|
|
)
|
|
overrides[alias] = "{provisioner}/{worker_type}{suffix}".format(
|
|
provisioner=provisioner, worker_type=worker_type, suffix=suffix
|
|
)
|
|
|
|
retVal = {}
|
|
if worker_types:
|
|
retVal["worker-types"] = list(overrides.keys()) + worker_types
|
|
|
|
if overrides:
|
|
retVal["worker-overrides"] = overrides
|
|
return retVal
|
|
|
|
|
|
all_task_configs = {
|
|
"artifact": Artifact,
|
|
"browsertime": Browsertime,
|
|
"chemspill-prio": ChemspillPrio,
|
|
"disable-pgo": DisablePgo,
|
|
"env": Environment,
|
|
"existing-tasks": ExistingTasks,
|
|
"gecko-profile": GeckoProfile,
|
|
"new-test-config": NewConfig,
|
|
"path": Path,
|
|
"pernosco": Pernosco,
|
|
"rebuild": Rebuild,
|
|
"routes": Routes,
|
|
"worker-overrides": WorkerOverrides,
|
|
}
|