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
13 from mach
.decorators
import (
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:
33 class get_parser(object):
34 def __init__(self
, selector
):
35 self
.selector
= selector
38 mod
= importlib
.import_module('tryselect.selectors.{}'.format(self
.selector
))
39 return getattr(mod
, '{}Parser'.format(self
.selector
.capitalize()))()
43 from tryselect
.cli
import BaseTryParser
44 parser
= BaseTryParser()
45 parser
.add_argument('argv', nargs
=argparse
.REMAINDER
)
50 class TryConfig(object):
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()
60 ('try.default', 'string', desc
, 'auto', {'choices': choices
}),
61 ('try.maxhistory', 'int', "Maximum number of pushes to save in history.", 10),
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
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
)
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
)
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
105 from tryselect
.util
.dicttools
import merge
107 user_presets
= self
.presets
.handlers
[0]
108 if preset_action
== 'list':
112 if preset_action
== 'edit':
116 default
= self
.parser
.get_default
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
))
127 if preset
not in self
.presets
:
128 self
.parser
.error("preset '{}' does not exist".format(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(
143 # Order of precedence is defaults -> presets -> cli. Configuration
144 # from the right overwrites configuration from the left.
147 for k
, v
in kwargs
.items():
153 kwargs
= merge(defaults
, preset
, nondefaults
)
157 def handle_try_config(self
, **kwargs
):
158 from tryselect
.util
.dicttools
import merge
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
:
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
)
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
)
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
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']
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
)
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
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.
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)
286 ^start 'exact | !ignore fuzzy end$
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']
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']:
312 if kwargs
.get('paths'):
313 kwargs
['test_paths'] = kwargs
['paths']
315 return self
.run(**kwargs
)
319 description
='Schedule tasks by selecting them from a web '
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
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
)
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
)
347 description
='Schedule a previously generated (non try syntax) '
349 parser
=get_parser('again'))
350 def try_again(self
, **kwargs
):
351 return self
.run(**kwargs
)
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
366 return self
.run(**kwargs
)
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
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).
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.
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
)
424 return self
.run(**kwargs
)
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
)
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
)
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
)