Bug 1653367 [wpt PR 24632] - Update interfaces/streams.idl, a=testonly
[gecko.git] / tools / tryselect / mach_commands.py
blobd692a6be1cd6ac1d84c7bcdcb6de0b2933909b4a
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/.
5 from __future__ import absolute_import, print_function, unicode_literals
7 import argparse
8 import importlib
9 import os
10 import sys
12 import six
13 from mach.decorators import (
14 CommandProvider,
15 Command,
16 SettingsProvider,
17 SubCommand,
19 from mozboot.util import get_state_dir
20 from mozbuild.base import BuildEnvironmentNotFoundException, MachCommandBase
23 CONFIG_ENVIRONMENT_NOT_FOUND = '''
24 No config environment detected. This means we are unable to properly
25 detect test files in the specified paths or tags. Please run:
27 $ mach configure
29 and try again.
30 '''.lstrip()
33 class get_parser(object):
34 def __init__(self, selector):
35 self.selector = selector
37 def __call__(self):
38 mod = importlib.import_module('tryselect.selectors.{}'.format(self.selector))
39 return getattr(mod, '{}Parser'.format(self.selector.capitalize()))()
42 def generic_parser():
43 from tryselect.cli import BaseTryParser
44 parser = BaseTryParser()
45 parser.add_argument('argv', nargs=argparse.REMAINDER)
46 return parser
49 @SettingsProvider
50 class TryConfig(object):
52 @classmethod
53 def config_settings(cls):
54 from mach.registrar import Registrar
56 desc = "The default selector to use when running `mach try` without a subcommand."
57 choices = Registrar.command_handlers['try'].subcommand_handlers.keys()
59 return [
60 ('try.default', 'string', desc, 'auto', {'choices': choices}),
61 ('try.maxhistory', 'int', "Maximum number of pushes to save in history.", 10),
65 @CommandProvider
66 class TrySelect(MachCommandBase):
68 def __init__(self, context):
69 super(TrySelect, self).__init__(context)
70 from tryselect import push
71 push.MAX_HISTORY = self._mach_context.settings['try']['maxhistory']
72 self.subcommand = self._mach_context.handler.subcommand
73 self.parser = self._mach_context.handler.parser
74 self._presets = None
76 @property
77 def presets(self):
78 if self._presets:
79 return self._presets
81 from tryselect.preset import MergedHandler
83 # Create our handler using both local and in-tree presets. The first
84 # path in this list will be treated as the 'user' file for the purposes
85 # of saving and editing. All subsequent paths are 'read-only'. We check
86 # an environment variable first for testing purposes.
87 if os.environ.get('MACH_TRY_PRESET_PATHS'):
88 preset_paths = os.environ['MACH_TRY_PRESET_PATHS'].split(os.pathsep)
89 else:
90 preset_paths = [
91 os.path.join(get_state_dir(), 'try_presets.yml'),
92 os.path.join(self.topsrcdir, 'tools', 'tryselect', 'try_presets.yml'),
95 self._presets = MergedHandler(*preset_paths)
96 return self._presets
98 def handle_presets(self, preset_action, save, preset, **kwargs):
99 """Handle preset related arguments.
101 This logic lives here so that the underlying selectors don't need
102 special preset handling. They can all save and load presets the same
103 way.
105 from tryselect.util.dicttools import merge
107 user_presets = self.presets.handlers[0]
108 if preset_action == 'list':
109 self.presets.list()
110 sys.exit()
112 if preset_action == 'edit':
113 user_presets.edit()
114 sys.exit()
116 default = self.parser.get_default
117 if save:
118 selector = self.subcommand or self._mach_context.settings['try']['default']
120 # Only save non-default values for simplicity.
121 kwargs = {k: v for k, v in kwargs.items() if v != default(k)}
122 user_presets.save(save, selector=selector, **kwargs)
123 print('preset saved, run with: --preset={}'.format(save))
124 sys.exit()
126 if preset:
127 if preset not in self.presets:
128 self.parser.error("preset '{}' does not exist".format(preset))
130 name = preset
131 preset = self.presets[name]
132 selector = preset.pop('selector')
133 preset.pop('description', None) # description isn't used by any selectors
135 if not self.subcommand:
136 self.subcommand = selector
137 elif self.subcommand != selector:
138 print("error: preset '{}' exists for a different selector "
139 "(did you mean to run 'mach try {}' instead?)".format(
140 name, selector))
141 sys.exit(1)
143 # Order of precedence is defaults -> presets -> cli. Configuration
144 # from the right overwrites configuration from the left.
145 defaults = {}
146 nondefaults = {}
147 for k, v in kwargs.items():
148 if v == default(k):
149 defaults[k] = v
150 else:
151 nondefaults[k] = v
153 kwargs = merge(defaults, preset, nondefaults)
155 return kwargs
157 def handle_try_config(self, **kwargs):
158 from tryselect.util.dicttools import merge
160 to_validate = []
161 kwargs.setdefault('try_config', {})
162 for cls in six.itervalues(self.parser.task_configs):
163 try_config = cls.try_config(**kwargs)
164 if try_config is not None:
165 to_validate.append(cls)
166 kwargs['try_config'] = merge(kwargs['try_config'], try_config)
168 for name in cls.dests:
169 del kwargs[name]
171 # Validate task_configs after they have all been parsed to avoid
172 # depending on the order they were processed.
173 for cls in to_validate:
174 cls.validate(**kwargs)
175 return kwargs
177 def run(self, **kwargs):
178 if 'preset' in self.parser.common_groups:
179 kwargs = self.handle_presets(**kwargs)
181 if self.parser.task_configs:
182 kwargs = self.handle_try_config(**kwargs)
184 mod = importlib.import_module('tryselect.selectors.{}'.format(self.subcommand))
185 return mod.run(**kwargs)
187 @Command('try',
188 category='ci',
189 description='Push selected tasks to the try server',
190 parser=generic_parser)
191 def try_default(self, argv=None, **kwargs):
192 """Push selected tests to the try server.
194 The |mach try| command is a frontend for scheduling tasks to
195 run on try server using selectors. A selector is a subcommand
196 that provides its own set of command line arguments and are
197 listed below.
199 If no subcommand is specified, the `auto` selector is run by
200 default. Run |mach try auto --help| for more information on
201 scheduling with the `auto` selector.
203 # We do special handling of presets here so that `./mach try --preset foo`
204 # works no matter what subcommand 'foo' was saved with.
205 preset = kwargs['preset']
206 if preset:
207 if preset not in self.presets:
208 self.parser.error("preset '{}' does not exist".format(preset))
210 self.subcommand = self.presets[preset]['selector']
212 sub = self.subcommand or self._mach_context.settings['try']['default']
213 return self._mach_context.commands.dispatch(
214 'try', subcommand=sub, context=self._mach_context, argv=argv, **kwargs)
216 @SubCommand('try',
217 'fuzzy',
218 description='Select tasks on try using a fuzzy finder',
219 parser=get_parser('fuzzy'))
220 def try_fuzzy(self, **kwargs):
221 """Select which tasks to run with a fuzzy finding interface (fzf).
223 When entering the fzf interface you'll be confronted by two panes. The
224 one on the left contains every possible task you can schedule, the one
225 on the right contains the list of selected tasks. In other words, the
226 tasks that will be scheduled once you you press <enter>.
228 At first fzf will automatically select whichever task is under your
229 cursor, which simplifies the case when you are looking for a single
230 task. But normally you'll want to select many tasks. To accomplish
231 you'll generally start by typing a query in the search bar to filter
232 down the list of tasks (see Extended Search below). Then you'll either:
234 A) Move the cursor to each task you want and press <tab> to select it.
235 Notice it now shows up in the pane to the right.
239 B) Press <ctrl-a> to select every task that matches your filter.
241 You can delete your query, type a new one and select further tasks as
242 many times as you like. Once you are happy with your selection, press
243 <enter> to push the selected tasks to try.
245 All selected task labels and their dependencies will be scheduled. This
246 means you can select a test task and its build will automatically be
247 filled in.
250 Keyboard Shortcuts
251 ------------------
253 When in the fuzzy finder interface, start typing to filter down the
254 task list. Then use the following keyboard shortcuts to select tasks:
256 Ctrl-K / Up => Move cursor up
257 Ctrl-J / Down => Move cursor down
258 Tab => Select task + move cursor down
259 Shift-Tab => Select task + move cursor up
260 Ctrl-A => Select all currently filtered tasks
261 Ctrl-D => De-select all currently filtered tasks
262 Ctrl-T => Toggle select all currently filtered tasks
263 Alt-Bspace => Clear query from input bar
264 Enter => Accept selection and exit
265 Ctrl-C / Esc => Cancel selection and exit
266 ? => Toggle preview pane
268 There are many more shortcuts enabled by default, you can also define
269 your own shortcuts by setting `--bind` in the $FZF_DEFAULT_OPTS
270 environment variable. See `man fzf` for more info.
273 Extended Search
274 ---------------
276 When typing in search terms, the following modifiers can be applied:
278 'word: exact match (line must contain the literal string "word")
279 ^word: exact prefix match (line must start with literal "word")
280 word$: exact suffix match (line must end with literal "word")
281 !word: exact negation match (line must not contain literal "word")
282 'a | 'b: OR operator (joins two exact match operators together)
284 For example:
286 ^start 'exact | !ignore fuzzy end$
289 Documentation
290 -------------
292 For more detailed documentation, please see:
293 https://firefox-source-docs.mozilla.org/tools/try/selectors/fuzzy.html
295 if kwargs.pop('interactive'):
296 kwargs['query'].append('INTERACTIVE')
298 if kwargs.pop('intersection'):
299 kwargs['intersect_query'] = kwargs['query']
300 del kwargs['query']
302 if kwargs.get('save') and not kwargs.get('query'):
303 # If saving preset without -q/--query, allow user to use the
304 # interface to build the query.
305 kwargs_copy = kwargs.copy()
306 kwargs_copy['push'] = False
307 kwargs_copy['save'] = None
308 kwargs['query'] = self.run(save_query=True, **kwargs_copy)
309 if not kwargs['query']:
310 return
312 if kwargs.get('paths'):
313 kwargs['test_paths'] = kwargs['paths']
315 return self.run(**kwargs)
317 @SubCommand('try',
318 'chooser',
319 description='Schedule tasks by selecting them from a web '
320 'interface.',
321 parser=get_parser('chooser'))
322 def try_chooser(self, **kwargs):
323 """Push tasks selected from a web interface to try.
325 This selector will build the taskgraph and spin up a dynamically
326 created 'trychooser-like' web-page on the localhost. After a selection
327 has been made, pressing the 'Push' button will automatically push the
328 selection to try.
330 self._activate_virtualenv()
331 path = os.path.join('tools', 'tryselect', 'selectors', 'chooser', 'requirements.txt')
332 self.virtualenv_manager.install_pip_requirements(path, quiet=True)
334 return self.run(**kwargs)
336 @SubCommand('try',
337 'auto',
338 description='Automatically determine which tasks to run. This runs the same '
339 'set of tasks that would be run on autoland. This '
340 'selector is EXPERIMENTAL.',
341 parser=get_parser('auto'))
342 def try_auto(self, **kwargs):
343 return self.run(**kwargs)
345 @SubCommand('try',
346 'again',
347 description='Schedule a previously generated (non try syntax) '
348 'push again.',
349 parser=get_parser('again'))
350 def try_again(self, **kwargs):
351 return self.run(**kwargs)
353 @SubCommand('try',
354 'empty',
355 description='Push to try without scheduling any tasks.',
356 parser=get_parser('empty'))
357 def try_empty(self, **kwargs):
358 """Push to try, running no builds or tests
360 This selector does not prompt you to run anything, it just pushes
361 your patches to try, running no builds or tests by default. After
362 the push finishes, you can manually add desired jobs to your push
363 via Treeherder's Add New Jobs feature, located in the per-push
364 menu.
366 return self.run(**kwargs)
368 @SubCommand('try',
369 'syntax',
370 description='Select tasks on try using try syntax',
371 parser=get_parser('syntax'))
372 def try_syntax(self, **kwargs):
373 """Push the current tree to try, with the specified syntax.
375 Build options, platforms and regression tests may be selected
376 using the usual try options (-b, -p and -u respectively). In
377 addition, tests in a given directory may be automatically
378 selected by passing that directory as a positional argument to the
379 command. For example:
381 mach try -b d -p linux64 dom testing/web-platform/tests/dom
383 would schedule a try run for linux64 debug consisting of all
384 tests under dom/ and testing/web-platform/tests/dom.
386 Test selection using positional arguments is available for
387 mochitests, reftests, xpcshell tests and web-platform-tests.
389 Tests may be also filtered by passing --tag to the command,
390 which will run only tests marked as having the specified
391 tags e.g.
393 mach try -b d -p win64 --tag media
395 would run all tests tagged 'media' on Windows 64.
397 If both positional arguments or tags and -u are supplied, the
398 suites in -u will be run in full. Where tests are selected by
399 positional argument they will be run in a single chunk.
401 If no build option is selected, both debug and opt will be
402 scheduled. If no platform is selected a default is taken from
403 the AUTOTRY_PLATFORM_HINT environment variable, if set.
405 The command requires either its own mercurial extension ("push-to-try",
406 installable from mach vcs-setup) or a git repo using git-cinnabar
407 (installable from mach vcs-setup).
410 try:
411 if self.substs.get("MOZ_ARTIFACT_BUILDS"):
412 kwargs['local_artifact_build'] = True
413 except BuildEnvironmentNotFoundException:
414 # If we don't have a build locally, we can't tell whether
415 # an artifact build is desired, but we still want the
416 # command to succeed, if possible.
417 pass
419 config_status = os.path.join(self.topobjdir, 'config.status')
420 if (kwargs['paths'] or kwargs['tags']) and not config_status:
421 print(CONFIG_ENVIRONMENT_NOT_FOUND)
422 sys.exit(1)
424 return self.run(**kwargs)
426 @SubCommand('try',
427 'coverage',
428 description='Select tasks on try using coverage data',
429 parser=get_parser('coverage'))
430 def try_coverage(self, **kwargs):
431 """Select which tasks to use using coverage data.
433 return self.run(**kwargs)
435 @SubCommand('try',
436 'release',
437 description='Push the current tree to try, configured for a staging release.',
438 parser=get_parser('release'))
439 def try_release(self, **kwargs):
440 """Push the current tree to try, configured for a staging release.
442 return self.run(**kwargs)
444 @SubCommand('try',
445 'scriptworker',
446 description='Run scriptworker tasks against a recent release.',
447 parser=get_parser('scriptworker'))
448 def try_scriptworker(self, **kwargs):
449 """Run scriptworker tasks against a recent release.
451 Requires VPN and shipit access.
453 return self.run(**kwargs)