fune/tools/tryselect/task_config.py
Andrew Halberstadt 95449daa6d Bug 1732723 - Rename "taskgraph" Python module to "gecko_taskgraph". r=jmaher
For a long time two copies of the 'taskgraph' module have existed in parallel.
We've attempted to keep them in sync, but over time they have diverged and the
maintenance burden has increased.

In order to reduce this burden, we'd like to re-join the two code bases. The
canonical repo will be the one that lives outside of mozilla-central, and this
module will depend on it. Since they both have the same module name (taskgraph)
we need to rename the version in mozilla-central to avoid collisions.

Other consumers of 'taskgraph' (like mobile repos) have standardized on
'<project>_taskgraph' as their module names. So replicating that here as well.

Differential Revision: https://phabricator.services.mozilla.com/D127118
2021-09-30 09:50:08 -04:00

533 lines
15 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 six
import subprocess
import sys
from abc import ABCMeta, abstractmethod, abstractproperty
from argparse import Action, SUPPRESS
from textwrap import dedent
import mozpack.path as mozpath
from mozbuild.base import BuildEnvironmentNotFoundException, MozbuildObject
from .tasks import resolve_tests_by_suite
here = os.path.abspath(os.path.dirname(__file__))
build = MozbuildObject.from_environment(cwd=here)
class TryConfig:
__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 try_config(self, **kwargs):
pass
def validate(self, **kwargs):
pass
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}
if no_artifact:
return
if self.is_artifact_build():
print("Artifact builds enabled, pass --no-artifact to disable")
return {"use-artifact-builds": 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 whitelist).",
},
],
]
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.
cmd = ["ssh", "-G", "hg.mozilla.org"]
output = subprocess.check_output(
cmd, universal_newlines=True
).splitlines()
address = [
l.rsplit(" ", 1)[-1] for l in output if l.startswith("user")
][0]
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):
if kwargs["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 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 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": {}}
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 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."
),
},
],
]
def try_config(self, worker_overrides, worker_suffixes, **kwargs):
from gecko_taskgraph.config import load_graph_config
from gecko_taskgraph.util.workertypes import get_worker_type
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,
alias,
level="1",
release_level="staging",
)
overrides[alias] = "{provisioner}/{worker_type}{suffix}".format(
provisioner=provisioner, worker_type=worker_type, suffix=suffix
)
if overrides:
return {"worker-overrides": overrides}
all_task_configs = {
"artifact": Artifact,
"browsertime": Browsertime,
"chemspill-prio": ChemspillPrio,
"disable-pgo": DisablePgo,
"env": Environment,
"gecko-profile": GeckoProfile,
"path": Path,
"pernosco": Pernosco,
"rebuild": Rebuild,
"routes": Routes,
"worker-overrides": WorkerOverrides,
}