Bug 1839315: part 4) Link from `SheetLoadData::mWasAlternate` to spec. r=emilio DONTBUILD
[gecko.git] / tools / tryselect / mach_commands.py
blob3643f3aa286df98948b349e3fe03a4b092373afc
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 import argparse
7 import importlib
8 import os
9 import sys
11 from mach.decorators import Command, SettingsProvider, SubCommand
12 from mach.util import get_state_dir
13 from mozbuild.base import BuildEnvironmentNotFoundException
14 from mozbuild.util import memoize
16 CONFIG_ENVIRONMENT_NOT_FOUND = """
17 No config environment detected. This means we are unable to properly
18 detect test files in the specified paths or tags. Please run:
20 $ mach configure
22 and try again.
23 """.lstrip()
26 class get_parser:
27 def __init__(self, selector):
28 self.selector = selector
30 def __call__(self):
31 mod = importlib.import_module("tryselect.selectors.{}".format(self.selector))
32 return getattr(mod, "{}Parser".format(self.selector.capitalize()))()
35 def generic_parser():
36 from tryselect.cli import BaseTryParser
38 parser = BaseTryParser()
39 parser.add_argument("argv", nargs=argparse.REMAINDER)
40 return parser
43 @SettingsProvider
44 class TryConfig:
45 @classmethod
46 def config_settings(cls):
47 from mach.registrar import Registrar
49 desc = (
50 "The default selector to use when running `mach try` without a subcommand."
52 choices = Registrar.command_handlers["try"].subcommand_handlers.keys()
54 return [
55 ("try.default", "string", desc, "auto", {"choices": choices}),
57 "try.maxhistory",
58 "int",
59 "Maximum number of pushes to save in history.",
60 10,
65 def init(command_context):
66 from tryselect import push
68 push.MAX_HISTORY = command_context._mach_context.settings["try"]["maxhistory"]
71 @memoize
72 def presets(command_context):
73 from tryselect.preset import MergedHandler
75 # Create our handler using both local and in-tree presets. The first
76 # path in this list will be treated as the 'user' file for the purposes
77 # of saving and editing. All subsequent paths are 'read-only'. We check
78 # an environment variable first for testing purposes.
79 if os.environ.get("MACH_TRY_PRESET_PATHS"):
80 preset_paths = os.environ["MACH_TRY_PRESET_PATHS"].split(os.pathsep)
81 else:
82 preset_paths = [
83 os.path.join(get_state_dir(), "try_presets.yml"),
84 os.path.join(
85 command_context.topsrcdir, "tools", "tryselect", "try_presets.yml"
89 return MergedHandler(*preset_paths)
92 def handle_presets(
93 command_context, preset_action=None, save=None, preset=None, **kwargs
95 """Handle preset related arguments.
97 This logic lives here so that the underlying selectors don't need
98 special preset handling. They can all save and load presets the same
99 way.
101 from tryselect.util.dicttools import merge
103 user_presets = presets(command_context).handlers[0]
104 if preset_action == "list":
105 presets(command_context).list()
106 sys.exit()
108 if preset_action == "edit":
109 user_presets.edit()
110 sys.exit()
112 parser = command_context._mach_context.handler.parser
113 subcommand = command_context._mach_context.handler.subcommand
114 if "preset" not in parser.common_groups:
115 return kwargs
117 default = parser.get_default
118 if save:
119 selector = (
120 subcommand or command_context._mach_context.settings["try"]["default"]
123 # Only save non-default values for simplicity.
124 kwargs = {k: v for k, v in kwargs.items() if v != default(k)}
125 user_presets.save(save, selector=selector, **kwargs)
126 print("preset saved, run with: --preset={}".format(save))
127 sys.exit()
129 if preset:
130 if preset not in presets(command_context):
131 command_context._mach_context.parser.error(
132 "preset '{}' does not exist".format(preset)
135 name = preset
136 preset = presets(command_context)[name]
137 selector = preset.pop("selector")
138 preset.pop("description", None) # description isn't used by any selectors
140 if not subcommand:
141 subcommand = selector
142 elif subcommand != selector:
143 print(
144 "error: preset '{}' exists for a different selector "
145 "(did you mean to run 'mach try {}' instead?)".format(name, selector)
147 sys.exit(1)
149 # Order of precedence is defaults -> presets -> cli. Configuration
150 # from the right overwrites configuration from the left.
151 defaults = {}
152 nondefaults = {}
153 for k, v in kwargs.items():
154 if v == default(k):
155 defaults[k] = v
156 else:
157 nondefaults[k] = v
159 kwargs = merge(defaults, preset, nondefaults)
161 return kwargs
164 def handle_try_config(command_context, **kwargs):
165 from tryselect.util.dicttools import merge
167 to_validate = []
168 kwargs.setdefault("try_config", {})
169 for cls in command_context._mach_context.handler.parser.task_configs.values():
170 try_config = cls.try_config(**kwargs)
171 if try_config is not None:
172 to_validate.append(cls)
173 kwargs["try_config"] = merge(kwargs["try_config"], try_config)
175 for name in cls.dests:
176 del kwargs[name]
178 # Validate task_configs after they have all been parsed to avoid
179 # depending on the order they were processed.
180 for cls in to_validate:
181 cls.validate(**kwargs)
182 return kwargs
185 def run(command_context, **kwargs):
186 kwargs = handle_presets(command_context, **kwargs)
188 if command_context._mach_context.handler.parser.task_configs:
189 kwargs = handle_try_config(command_context, **kwargs)
191 mod = importlib.import_module(
192 "tryselect.selectors.{}".format(
193 command_context._mach_context.handler.subcommand
196 return mod.run(**kwargs)
199 @Command(
200 "try",
201 category="ci",
202 description="Push selected tasks to the try server",
203 parser=generic_parser,
204 virtualenv_name="try",
206 def try_default(command_context, argv=None, **kwargs):
207 """Push selected tests to the try server.
209 The |mach try| command is a frontend for scheduling tasks to
210 run on try server using selectors. A selector is a subcommand
211 that provides its own set of command line arguments and are
212 listed below.
214 If no subcommand is specified, the `auto` selector is run by
215 default. Run |mach try auto --help| for more information on
216 scheduling with the `auto` selector.
218 init(command_context)
219 subcommand = command_context._mach_context.handler.subcommand
220 # We do special handling of presets here so that `./mach try --preset foo`
221 # works no matter what subcommand 'foo' was saved with.
222 preset = kwargs["preset"]
223 if preset:
224 if preset not in presets(command_context):
225 command_context._mach_context.handler.parser.error(
226 "preset '{}' does not exist".format(preset)
229 subcommand = presets(command_context)[preset]["selector"]
231 sub = subcommand or command_context._mach_context.settings["try"]["default"]
232 return command_context._mach_context.commands.dispatch(
233 "try", command_context._mach_context, subcommand=sub, argv=argv, **kwargs
237 @SubCommand(
238 "try",
239 "fuzzy",
240 description="Select tasks on try using a fuzzy finder",
241 parser=get_parser("fuzzy"),
242 virtualenv_name="try",
244 def try_fuzzy(command_context, **kwargs):
245 """Select which tasks to run with a fuzzy finding interface (fzf).
247 When entering the fzf interface you'll be confronted by two panes. The
248 one on the left contains every possible task you can schedule, the one
249 on the right contains the list of selected tasks. In other words, the
250 tasks that will be scheduled once you you press <enter>.
252 At first fzf will automatically select whichever task is under your
253 cursor, which simplifies the case when you are looking for a single
254 task. But normally you'll want to select many tasks. To accomplish
255 you'll generally start by typing a query in the search bar to filter
256 down the list of tasks (see Extended Search below). Then you'll either:
258 A) Move the cursor to each task you want and press <tab> to select it.
259 Notice it now shows up in the pane to the right.
263 B) Press <ctrl-a> to select every task that matches your filter.
265 You can delete your query, type a new one and select further tasks as
266 many times as you like. Once you are happy with your selection, press
267 <enter> to push the selected tasks to try.
269 All selected task labels and their dependencies will be scheduled. This
270 means you can select a test task and its build will automatically be
271 filled in.
274 Keyboard Shortcuts
275 ------------------
277 When in the fuzzy finder interface, start typing to filter down the
278 task list. Then use the following keyboard shortcuts to select tasks:
280 Ctrl-K / Up => Move cursor up
281 Ctrl-J / Down => Move cursor down
282 Tab => Select task + move cursor down
283 Shift-Tab => Select task + move cursor up
284 Ctrl-A => Select all currently filtered tasks
285 Ctrl-D => De-select all currently filtered tasks
286 Ctrl-T => Toggle select all currently filtered tasks
287 Alt-Bspace => Clear query from input bar
288 Enter => Accept selection and exit
289 Ctrl-C / Esc => Cancel selection and exit
290 ? => Toggle preview pane
292 There are many more shortcuts enabled by default, you can also define
293 your own shortcuts by setting `--bind` in the $FZF_DEFAULT_OPTS
294 environment variable. See `man fzf` for more info.
297 Extended Search
298 ---------------
300 When typing in search terms, the following modifiers can be applied:
302 'word: exact match (line must contain the literal string "word")
303 ^word: exact prefix match (line must start with literal "word")
304 word$: exact suffix match (line must end with literal "word")
305 !word: exact negation match (line must not contain literal "word")
306 'a | 'b: OR operator (joins two exact match operators together)
308 For example:
310 ^start 'exact | !ignore fuzzy end$
313 Documentation
314 -------------
316 For more detailed documentation, please see:
317 https://firefox-source-docs.mozilla.org/tools/try/selectors/fuzzy.html
319 init(command_context)
320 if kwargs.pop("interactive"):
321 kwargs["query"].append("INTERACTIVE")
323 if kwargs.pop("intersection"):
324 kwargs["intersect_query"] = kwargs["query"]
325 del kwargs["query"]
327 if kwargs.get("save") and not kwargs.get("query"):
328 # If saving preset without -q/--query, allow user to use the
329 # interface to build the query.
330 kwargs_copy = kwargs.copy()
331 kwargs_copy["dry_run"] = True
332 kwargs_copy["save"] = None
333 kwargs["query"] = run(command_context, save_query=True, **kwargs_copy)
334 if not kwargs["query"]:
335 return
337 if kwargs.get("paths"):
338 kwargs["test_paths"] = kwargs["paths"]
340 return run(command_context, **kwargs)
343 @SubCommand(
344 "try",
345 "chooser",
346 description="Schedule tasks by selecting them from a web interface.",
347 parser=get_parser("chooser"),
348 virtualenv_name="try",
350 def try_chooser(command_context, **kwargs):
351 """Push tasks selected from a web interface to try.
353 This selector will build the taskgraph and spin up a dynamically
354 created 'trychooser-like' web-page on the localhost. After a selection
355 has been made, pressing the 'Push' button will automatically push the
356 selection to try.
358 init(command_context)
359 command_context.activate_virtualenv()
361 return run(command_context, **kwargs)
364 @SubCommand(
365 "try",
366 "auto",
367 description="Automatically determine which tasks to run. This runs the same "
368 "set of tasks that would be run on autoland. This "
369 "selector is EXPERIMENTAL.",
370 parser=get_parser("auto"),
371 virtualenv_name="try",
373 def try_auto(command_context, **kwargs):
374 init(command_context)
375 return run(command_context, **kwargs)
378 @SubCommand(
379 "try",
380 "again",
381 description="Schedule a previously generated (non try syntax) push again.",
382 parser=get_parser("again"),
383 virtualenv_name="try",
385 def try_again(command_context, **kwargs):
386 init(command_context)
387 return run(command_context, **kwargs)
390 @SubCommand(
391 "try",
392 "empty",
393 description="Push to try without scheduling any tasks.",
394 parser=get_parser("empty"),
395 virtualenv_name="try",
397 def try_empty(command_context, **kwargs):
398 """Push to try, running no builds or tests
400 This selector does not prompt you to run anything, it just pushes
401 your patches to try, running no builds or tests by default. After
402 the push finishes, you can manually add desired jobs to your push
403 via Treeherder's Add New Jobs feature, located in the per-push
404 menu.
406 init(command_context)
407 return run(command_context, **kwargs)
410 @SubCommand(
411 "try",
412 "syntax",
413 description="Select tasks on try using try syntax",
414 parser=get_parser("syntax"),
415 virtualenv_name="try",
417 def try_syntax(command_context, **kwargs):
418 """Push the current tree to try, with the specified syntax.
420 Build options, platforms and regression tests may be selected
421 using the usual try options (-b, -p and -u respectively). In
422 addition, tests in a given directory may be automatically
423 selected by passing that directory as a positional argument to the
424 command. For example:
426 mach try -b d -p linux64 dom testing/web-platform/tests/dom
428 would schedule a try run for linux64 debug consisting of all
429 tests under dom/ and testing/web-platform/tests/dom.
431 Test selection using positional arguments is available for
432 mochitests, reftests, xpcshell tests and web-platform-tests.
434 Tests may be also filtered by passing --tag to the command,
435 which will run only tests marked as having the specified
436 tags e.g.
438 mach try -b d -p win64 --tag media
440 would run all tests tagged 'media' on Windows 64.
442 If both positional arguments or tags and -u are supplied, the
443 suites in -u will be run in full. Where tests are selected by
444 positional argument they will be run in a single chunk.
446 If no build option is selected, both debug and opt will be
447 scheduled. If no platform is selected a default is taken from
448 the AUTOTRY_PLATFORM_HINT environment variable, if set.
450 The command requires either its own mercurial extension ("push-to-try",
451 installable from mach vcs-setup) or a git repo using git-cinnabar
452 (installable from mach vcs-setup).
455 init(command_context)
456 try:
457 if command_context.substs.get("MOZ_ARTIFACT_BUILDS"):
458 kwargs["local_artifact_build"] = True
459 except BuildEnvironmentNotFoundException:
460 # If we don't have a build locally, we can't tell whether
461 # an artifact build is desired, but we still want the
462 # command to succeed, if possible.
463 pass
465 config_status = os.path.join(command_context.topobjdir, "config.status")
466 if (kwargs["paths"] or kwargs["tags"]) and not config_status:
467 print(CONFIG_ENVIRONMENT_NOT_FOUND)
468 sys.exit(1)
470 return run(command_context, **kwargs)
473 @SubCommand(
474 "try",
475 "coverage",
476 description="Select tasks on try using coverage data",
477 parser=get_parser("coverage"),
478 virtualenv_name="try",
480 def try_coverage(command_context, **kwargs):
481 """Select which tasks to use using coverage data."""
482 init(command_context)
483 return run(command_context, **kwargs)
486 @SubCommand(
487 "try",
488 "release",
489 description="Push the current tree to try, configured for a staging release.",
490 parser=get_parser("release"),
491 virtualenv_name="try",
493 def try_release(command_context, **kwargs):
494 """Push the current tree to try, configured for a staging release."""
495 init(command_context)
496 return run(command_context, **kwargs)
499 @SubCommand(
500 "try",
501 "scriptworker",
502 description="Run scriptworker tasks against a recent release.",
503 parser=get_parser("scriptworker"),
504 virtualenv_name="try",
506 def try_scriptworker(command_context, **kwargs):
507 """Run scriptworker tasks against a recent release.
509 Requires VPN and shipit access.
511 init(command_context)
512 return run(command_context, **kwargs)
515 @SubCommand(
516 "try",
517 "compare",
518 description="Push two try jobs, one on your current commit and another on the one you specify",
519 parser=get_parser("compare"),
520 virtualenv_name="try",
522 def try_compare(command_context, **kwargs):
523 init(command_context)
524 return run(command_context, **kwargs)
527 @SubCommand(
528 "try",
529 "perf",
530 description="Try selector for running performance tests.",
531 parser=get_parser("perf"),
532 virtualenv_name="try",
534 def try_perf(command_context, **kwargs):
535 init(command_context)
536 return run(command_context, **kwargs)