Bug 1518572 - [tryselect] Store all arguments when saving a preset r=gbrown
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 25 Feb 2019 19:47:29 +0000
changeset 460964 1b5a4da48de351e8f1145e3c3e49c4cd86cd9ad0
parent 460963 d7a45b84e0636abebded47eef06460871aa50ff9
child 460965 24105ffcf6f5e8a45ba79809d8158fc266b195d6
push id35613
push usernerli@mozilla.com
push dateTue, 26 Feb 2019 03:52:35 +0000
treeherdermozilla-central@faec87a80ed1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgbrown
bugs1518572
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1518572 - [tryselect] Store all arguments when saving a preset r=gbrown Differential Revision: https://phabricator.services.mozilla.com/D20523
tools/tryselect/cli.py
tools/tryselect/mach_commands.py
tools/tryselect/preset.py
tools/tryselect/selectors/fuzzy.py
tools/tryselect/selectors/syntax.py
tools/tryselect/templates.py
tools/tryselect/test/test_preset.t
tools/tryselect/util/__init__.py
tools/tryselect/util/dicttools.py
--- a/tools/tryselect/cli.py
+++ b/tools/tryselect/cli.py
@@ -1,19 +1,21 @@
 # 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/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import os
 import subprocess
+import sys
 import tempfile
 from argparse import ArgumentParser
 
+from .preset import presets
 from .templates import all_templates
 
 
 COMMON_ARGUMENT_GROUPS = {
     'push': [
         [['-m', '--message'],
          {'const': 'editor',
           'default': '{msg}',
@@ -39,27 +41,25 @@ COMMON_ARGUMENT_GROUPS = {
          {'default': None,
           'help': 'Save selection for future use with --preset.',
           }],
         [['--preset'],
          {'default': None,
           'help': 'Load a saved selection.',
           }],
         [['--list-presets'],
-         {'action': 'store_const',
-          'const': 'list_presets',
-          'dest': 'mod_presets',
-          'default': None,
+         {'action': 'store_true',
+          'dest': 'list_presets',
+          'default': False,
           'help': 'List available preset selections.',
           }],
         [['--edit-presets'],
-         {'action': 'store_const',
-          'const': 'edit_presets',
-          'dest': 'mod_presets',
-          'default': None,
+         {'action': 'store_true',
+          'dest': 'edit_presets',
+          'default': False,
           'help': 'Edit the preset file.',
           }],
     ],
     'task': [
         [['--full'],
          {'action': 'store_true',
           'default': False,
           'help': "Use the full set of tasks as input to fzf (instead of "
@@ -85,20 +85,26 @@ class BaseTryParser(ArgumentParser):
 
         group = self.add_argument_group("{} arguments".format(self.name))
         for cli, kwargs in self.arguments:
             group.add_argument(*cli, **kwargs)
 
         for name in self.common_groups:
             group = self.add_argument_group("{} arguments".format(name))
             arguments = COMMON_ARGUMENT_GROUPS[name]
+
+            # Preset arguments are all mutually exclusive.
+            if name == 'preset':
+                group = group.add_mutually_exclusive_group()
+
             for cli, kwargs in arguments:
                 group.add_argument(*cli, **kwargs)
 
         group = self.add_argument_group("template arguments")
+        self.set_defaults(templates={})
         self.templates = {t: all_templates[t]() for t in self.templates}
         for template in self.templates.values():
             template.add_arguments(group)
 
     def validate(self, args):
         if hasattr(args, 'message'):
             if args.message == 'editor':
                 if 'EDITOR' not in os.environ:
@@ -106,20 +112,31 @@ class BaseTryParser(ArgumentParser):
 
                 with tempfile.NamedTemporaryFile(mode='r') as fh:
                     subprocess.call([os.environ['EDITOR'], fh.name])
                     args.message = fh.read().strip()
 
             if '{msg}' not in args.message:
                 args.message = '{}\n\n{}'.format(args.message, '{msg}')
 
+        if 'preset' in self.common_groups:
+            if args.list_presets:
+                presets.list()
+                sys.exit()
+
+            if args.edit_presets:
+                presets.edit()
+                sys.exit()
+
+            if args.preset and args.preset not in presets:
+                self.error("preset '{}' does not exist".format(args.preset))
+
     def parse_known_args(self, *args, **kwargs):
         args, remainder = ArgumentParser.parse_known_args(self, *args, **kwargs)
         self.validate(args)
 
         if self.templates:
-            args.templates = {}
             for cls in self.templates.itervalues():
                 context = cls.context(**vars(args))
                 if context is not None:
                     args.templates.update(context)
 
         return args, remainder
--- a/tools/tryselect/mach_commands.py
+++ b/tools/tryselect/mach_commands.py
@@ -62,16 +62,64 @@ class TryConfig(object):
 
 @CommandProvider
 class TrySelect(MachCommandBase):
 
     def __init__(self, context):
         super(TrySelect, self).__init__(context)
         from tryselect import push
         push.MAX_HISTORY = self._mach_context.settings['try']['maxhistory']
+        self.subcommand = self._mach_context.handler.subcommand
+
+    def handle_presets(self, save, preset, **kwargs):
+        """Handle preset related arguments.
+
+        This logic lives here so that the underlying selectors don't need
+        special preset handling. They can all save and load presets the same
+        way.
+        """
+        from tryselect.preset import presets
+        from tryselect.util.dicttools import merge
+
+        default = self._mach_context.handler.parser.get_default
+        if save:
+            selector = self.subcommand or self._mach_context.settings['try']['default']
+
+            # Only save non-default values for simplicity.
+            kwargs = {k: v for k, v in kwargs.items() if v != default(k)}
+            presets.save(save, selector=selector, **kwargs)
+            print('preset saved, run with: --preset={}'.format(save))
+            sys.exit()
+
+        if preset:
+            name = preset
+            preset = presets[name]
+            selector = preset['selector']
+
+            if not self.subcommand:
+                self.subcommand = selector
+            elif self.subcommand != selector:
+                print("error: preset '{}' exists for a different selector "
+                      "(did you mean to run 'mach try {}' instead?)".format(
+                        name, selector))
+                sys.exit(1)
+
+            # Order of precedence is defaults -> presets -> cli. Configuration
+            # from the right overwrites configuration from the left.
+            defaults = {}
+            nondefaults = {}
+            for k, v in kwargs.items():
+                if v == default(k):
+                    defaults[k] = v
+                else:
+                    nondefaults[k] = v
+
+            kwargs = merge(defaults, preset, nondefaults)
+
+        return kwargs
 
     @Command('try',
              category='ci',
              description='Push selected tasks to the try server',
              parser=generic_parser)
     def try_default(self, argv, **kwargs):
         """Push selected tests to the try server.
 
@@ -79,28 +127,22 @@ class TrySelect(MachCommandBase):
         run on try server using selectors. A selector is a subcommand
         that provides its own set of command line arguments and are
         listed below.
 
         If no subcommand is specified, the `syntax` selector is run by
         default. Run |mach try syntax --help| for more information on
         scheduling with the `syntax` selector.
         """
-        from tryselect import preset
-        if kwargs['mod_presets']:
-            getattr(preset, kwargs['mod_presets'])()
-            return
-
         # We do special handling of presets here so that `./mach try --preset foo`
         # works no matter what subcommand 'foo' was saved with.
-        sub = self._mach_context.settings['try']['default']
         if kwargs['preset']:
-            _, section = preset.load(kwargs['preset'])
-            sub = 'syntax' if section == 'try' else section
+            kwargs = self.handle_presets(**kwargs)
 
+        sub = self.subcommand or self._mach_context.settings['try']['default']
         return self._mach_context.commands.dispatch(
             'try', subcommand=sub, context=self._mach_context, argv=argv, **kwargs)
 
     @SubCommand('try',
                 'fuzzy',
                 description='Select tasks on try using a fuzzy finder',
                 parser=get_parser('fuzzy'))
     def try_fuzzy(self, **kwargs):
@@ -142,17 +184,24 @@ class TrySelect(MachCommandBase):
           !word: exact negation match (line must not contain literal "word")
           'a | 'b: OR operator (joins two exact match operators together)
 
         For example:
 
           ^start 'exact | !ignore fuzzy end$
         """
         from tryselect.selectors.fuzzy import run_fuzzy_try
-        return run_fuzzy_try(**kwargs)
+        if kwargs.get('save') and not kwargs.get('query'):
+            # If saving preset without -q/--query, allow user to use the
+            # interface to build the query.
+            kwargs_copy = kwargs.copy()
+            kwargs_copy['push'] = False
+            kwargs['query'] = run_fuzzy_try(**kwargs_copy)
+
+        return run_fuzzy_try(**self.handle_presets(**kwargs))
 
     @SubCommand('try',
                 'chooser',
                 description='Schedule tasks by selecting them from a web '
                             'interface.',
                 parser=get_parser('chooser'))
     def try_chooser(self, **kwargs):
         """Push tasks selected from a web interface to try.
@@ -232,16 +281,17 @@ class TrySelect(MachCommandBase):
 
         The command requires either its own mercurial extension ("push-to-try",
         installable from mach vcs-setup) or a git repo using git-cinnabar
         (installable from mach vcs-setup --git).
 
         """
         from tryselect.selectors.syntax import AutoTry
 
+        kwargs = self.handle_presets(**kwargs)
         try:
             if self.substs.get("MOZ_ARTIFACT_BUILDS"):
                 kwargs['local_artifact_build'] = True
         except BuildEnvironmentNotFoundException:
             # If we don't have a build locally, we can't tell whether
             # an artifact build is desired, but we still want the
             # command to succeed, if possible.
             pass
--- a/tools/tryselect/preset.py
+++ b/tools/tryselect/preset.py
@@ -1,69 +1,57 @@
 # 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/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
-import ConfigParser
 import os
 import subprocess
 
+import yaml
 from mozboot.util import get_state_dir
 
 
-CONFIG_PATH = os.path.join(get_state_dir(), "autotry.ini")
+class PresetHandler(object):
+    config_path = os.path.join(get_state_dir(), "try_presets.yml")
 
+    def __init__(self):
+        self._presets = {}
 
-def list_presets(section=None):
-    config = ConfigParser.RawConfigParser()
+    @property
+    def presets(self):
+        if not self._presets and os.path.isfile(self.config_path):
+            with open(self.config_path, 'r') as fh:
+                self._presets = yaml.safe_load(fh) or {}
+
+        return self._presets
+
+    def __contains__(self, name):
+        return name in self.presets
+
+    def __getitem__(self, name):
+        return self.presets[name]
 
-    data = []
-    if config.read([CONFIG_PATH]):
-        sections = [section] if section else config.sections()
-        for s in sections:
-            try:
-                data.extend(config.items(s))
-            except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
-                pass
+    def __str__(self):
+        return yaml.safe_dump(self.presets, default_flow_style=False)
+
+    def list(self):
+        if not self.presets:
+            print("no presets found")
+        else:
+            print(self)
 
-    if not data:
-        print("No presets found")
+    def edit(self):
+        if 'EDITOR' not in os.environ:
+            print("error: must set the $EDITOR environment variable to use --edit-presets")
+            return
 
-    for name, value in data:
-        print("%s: %s" % (name, value))
+        subprocess.call([os.environ['EDITOR'], self.config_path])
+
+    def save(self, name, **data):
+        self.presets[name] = data
+
+        with open(self.config_path, "w") as fh:
+            fh.write(str(self))
 
 
-def edit_presets(section=None):
-    if 'EDITOR' not in os.environ:
-        print("error: must set the $EDITOR environment variable to use --edit-presets")
-        return
-    subprocess.call([os.environ['EDITOR'], CONFIG_PATH])
-
-
-def load(name, section=None):
-    config = ConfigParser.RawConfigParser()
-    if not config.read([CONFIG_PATH]):
-        return
-
-    sections = [section] if section else config.sections()
-    for s in sections:
-        try:
-            return config.get(s, name), s
-        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
-            pass
-    return None, None
-
-
-def save(section, name, data):
-    config = ConfigParser.RawConfigParser()
-    config.read([CONFIG_PATH])
-
-    if not config.has_section(section):
-        config.add_section(section)
-
-    config.set(section, name, data)
-
-    with open(CONFIG_PATH, "w") as f:
-        config.write(f)
-
-    print('preset saved, run with: --preset={}'.format(name))
+presets = PresetHandler()
--- a/tools/tryselect/selectors/fuzzy.py
+++ b/tools/tryselect/selectors/fuzzy.py
@@ -10,17 +10,16 @@ import re
 import subprocess
 import sys
 from distutils.spawn import find_executable
 
 from mozboot.util import get_state_dir
 from mozterm import Terminal
 from six import string_types
 
-from .. import preset as pset
 from ..cli import BaseTryParser
 from ..tasks import generate_tasks, filter_tasks_by_paths
 from ..push import check_working_directory, push_to_try, vcs
 
 terminal = Terminal()
 
 here = os.path.abspath(os.path.dirname(__file__))
 
@@ -200,21 +199,17 @@ def run_fzf(cmd, tasks):
     return query, selected
 
 
 def filter_target_task(task):
     return not any(re.search(pattern, task) for pattern in TARGET_TASK_FILTERS)
 
 
 def run_fuzzy_try(update=False, query=None, templates=None, full=False, parameters=None,
-                  save=False, preset=None, mod_presets=False, push=True, message='{msg}',
-                  paths=None, **kwargs):
-    if mod_presets:
-        return getattr(pset, mod_presets)(section='fuzzy')
-
+                  save=False, push=True, message='{msg}', paths=None, **kwargs):
     fzf = fzf_bootstrap(update)
 
     if not fzf:
         print(FZF_NOT_FOUND)
         return 1
 
     check_working_directory(push)
     tg = generate_tasks(parameters, full, root=vcs.path)
@@ -242,19 +237,16 @@ def run_fuzzy_try(update=False, query=No
 
     if kwargs['exact']:
         base_cmd.append('--exact')
 
     query = query or []
     if isinstance(query, string_types):
         query = [query]
 
-    if preset:
-        query.append(pset.load(preset, section='fuzzy')[0])
-
     commands = []
     if query:
         for q in query:
             commands.append(base_cmd + ['-f', q])
     else:
         commands.append(base_cmd)
 
     queries = []
@@ -265,17 +257,17 @@ def run_fuzzy_try(update=False, query=No
             queries.append(query)
             selected.update(tasks)
 
     if not selected:
         print("no tasks selected")
         return
 
     if save:
-        pset.save('fuzzy', save, queries[0])
+        return queries
 
     # build commit message
     msg = "Fuzzy"
     args = []
     if paths:
         args.append("paths={}".format(':'.join(paths)))
     if query:
         args.extend(["query={}".format(q) for q in queries])
--- a/tools/tryselect/selectors/syntax.py
+++ b/tools/tryselect/selectors/syntax.py
@@ -7,28 +7,28 @@ from __future__ import absolute_import, 
 import os
 import re
 import sys
 from collections import defaultdict
 
 import mozpack.path as mozpath
 from moztest.resolve import TestResolver
 
-from .. import preset
 from ..cli import BaseTryParser
 from ..push import push_to_try
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
 class SyntaxParser(BaseTryParser):
     name = 'syntax'
     arguments = [
         [['paths'],
          {'nargs': '*',
+          'default': [],
           'help': 'Paths to search for tests to run on try.',
           }],
         [['-b', '--build'],
          {'dest': 'builds',
           'default': 'do',
           'help': 'Build types to run (d for debug, o for optimized).',
           }],
         [['-p', '--platform'],
@@ -316,17 +316,18 @@ class AutoTry(object):
         self.mach_context = mach_context
 
     @property
     def resolver(self):
         if self._resolver is None:
             self._resolver = TestResolver.from_environment(cwd=here)
         return self._resolver
 
-    def split_try_string(self, data):
+    @classmethod
+    def split_try_string(cls, data):
         return re.findall(r'(?:\[.*?\]|\S)+', data)
 
     def paths_by_flavor(self, paths=None, tags=None):
         paths_by_flavor = defaultdict(set)
 
         if not (paths or tags):
             return dict(paths_by_flavor)
 
@@ -539,32 +540,16 @@ class AutoTry(object):
 
         extra_values = {k['dest'] for k in SyntaxParser.pass_through_arguments.values()}
         extra_args = {k: v for k, v in kwargs.items()
                       if k in extra_values and v}
 
         return kwargs["builds"], platforms, tests, talos, jobs, paths, tags, extra_args
 
     def run(self, **kwargs):
-        if kwargs["mod_presets"]:
-            getattr(preset, kwargs["mod_presets"])(section='try')
-            sys.exit()
-
-        if kwargs["preset"]:
-            value = preset.load(kwargs["preset"], section='try')[0]
-            defaults = vars(SyntaxParser().parse_args(self.split_try_string(value)))
-
-            if defaults is None:
-                print("No saved configuration called %s found in autotry.ini" % kwargs["preset"],
-                      file=sys.stderr)
-
-            for key, value in kwargs.iteritems():
-                if value in (None, []) and key in defaults:
-                    kwargs[key] = defaults[key]
-
         if not any(kwargs[item] for item in ("paths", "tests", "tags")):
             if kwargs['detect_paths']:
                 res = self.resolver.get_outgoing_metadata()
                 kwargs['paths'] = res['paths']
                 kwargs['tags'] = res['tags']
             else:
                 kwargs['paths'] = set()
                 kwargs['tags'] = set()
@@ -616,13 +601,8 @@ class AutoTry(object):
             for flavor, paths in paths_by_flavor.iteritems():
                 print("%s: %s" % (flavor, ",".join(paths)))
 
         if kwargs["verbose"]:
             print('The following try syntax was calculated:\n%s' % msg)
 
         push_to_try('syntax', kwargs["message"].format(msg=msg), push=kwargs['push'],
                     closed_tree=kwargs["closed_tree"])
-
-        if kwargs["save"]:
-            assert msg.startswith("try: ")
-            msg = msg[len("try: "):]
-            preset.save('try', kwargs["save"], msg)
--- a/tools/tryselect/templates.py
+++ b/tools/tryselect/templates.py
@@ -61,17 +61,17 @@ class Artifact(Template):
                 }
         except BuildEnvironmentNotFoundException:
             pass
 
 
 class Path(Template):
 
     def add_arguments(self, parser):
-        parser.add_argument('paths', nargs='*',
+        parser.add_argument('paths', nargs='*', default=[],
                             help='Run tasks containing tests under the specified path(s).')
 
     def context(self, paths, **kwargs):
         if not paths:
             return
 
         for p in paths:
             if not os.path.exists(p):
--- a/tools/tryselect/test/test_preset.t
+++ b/tools/tryselect/test/test_preset.t
@@ -1,94 +1,129 @@
   $ . $TESTDIR/setup.sh
   $ cd $topsrcdir
 
 Test preset with no subcommand
 
   $ ./mach try $testargs --save foo -b do -p linux -u mochitests -t none --tag foo
-  Commit message:
-  try: -b do -p linux -u mochitests -t none --tag foo
-  
-  Pushed via `mach try syntax`
   preset saved, run with: --preset=foo
 
   $ ./mach try $testargs --preset foo
   Commit message:
   try: -b do -p linux -u mochitests -t none --tag foo
   
   Pushed via `mach try syntax`
 
   $ ./mach try syntax $testargs --preset foo
   Commit message:
   try: -b do -p linux -u mochitests -t none --tag foo
   
   Pushed via `mach try syntax`
 
   $ ./mach try $testargs --list-presets
-  foo: -b do -p linux -u mochitests -t none --tag foo
+  foo:
+    no_artifact: true
+    platforms:
+    - linux
+    selector: syntax
+    tags:
+    - foo
+    talos:
+    - none
+    tests:
+    - mochitests
+  
   $ unset EDITOR
   $ ./mach try $testargs --edit-presets
   error: must set the $EDITOR environment variable to use --edit-presets
   $ export EDITOR=cat
   $ ./mach try $testargs --edit-presets
-  [try]
-  foo = -b do -p linux -u mochitests -t none --tag foo
-  
+  foo:
+    no_artifact: true
+    platforms:
+    - linux
+    selector: syntax
+    tags:
+    - foo
+    talos:
+    - none
+    tests:
+    - mochitests
 
 Test preset with syntax subcommand
 
   $ ./mach try syntax $testargs --save bar -b do -p win32 -u none -t all --tag bar
-  Commit message:
-  try: -b do -p win32 -u none -t all --tag bar
-  
-  Pushed via `mach try syntax`
   preset saved, run with: --preset=bar
 
   $ ./mach try syntax $testargs --preset bar
   Commit message:
   try: -b do -p win32 -u none -t all --tag bar
   
   Pushed via `mach try syntax`
 
   $ ./mach try $testargs --preset bar
   Commit message:
   try: -b do -p win32 -u none -t all --tag bar
   
   Pushed via `mach try syntax`
 
   $ ./mach try syntax $testargs --list-presets
-  foo: -b do -p linux -u mochitests -t none --tag foo
-  bar: -b do -p win32 -u none -t all --tag bar
+  bar:
+    no_artifact: true
+    platforms:
+    - win32
+    push: false
+    selector: syntax
+    tags:
+    - bar
+    talos:
+    - all
+    tests:
+    - none
+  foo:
+    no_artifact: true
+    platforms:
+    - linux
+    selector: syntax
+    tags:
+    - foo
+    talos:
+    - none
+    tests:
+    - mochitests
+  
   $ ./mach try syntax $testargs --edit-presets
-  [try]
-  foo = -b do -p linux -u mochitests -t none --tag foo
-  bar = -b do -p win32 -u none -t all --tag bar
-  
+  bar:
+    no_artifact: true
+    platforms:
+    - win32
+    push: false
+    selector: syntax
+    tags:
+    - bar
+    talos:
+    - all
+    tests:
+    - none
+  foo:
+    no_artifact: true
+    platforms:
+    - linux
+    selector: syntax
+    tags:
+    - foo
+    talos:
+    - none
+    tests:
+    - mochitests
 
 Test preset with fuzzy subcommand
 
   $ ./mach try fuzzy $testargs --save baz -q "'baz"
   preset saved, run with: --preset=baz
-  Commit message:
-  Fuzzy query='baz
-  
-  Pushed via `mach try fuzzy`
-  Calculated try_task_config.json:
-  {
-      "tasks": [
-          "build-baz"
-      ],
-      "templates": {
-          "env": {
-              "TRY_SELECTOR": "fuzzy"
-          }
-      },
-      "version": 1
-  }
-  
   $ ./mach try fuzzy $testargs --preset baz
   Commit message:
   Fuzzy query='baz
   
   Pushed via `mach try fuzzy`
   Calculated try_task_config.json:
   {
       "tasks": [
@@ -100,17 +135,17 @@ Test preset with fuzzy subcommand
           }
       },
       "version": 1
   }
   
 
   $ ./mach try fuzzy $testargs --preset baz -q "'foo"
   Commit message:
-  Fuzzy query='foo&query='baz
+  Fuzzy query='baz&query='foo
   
   Pushed via `mach try fuzzy`
   Calculated try_task_config.json:
   {
       "tasks": [
           "build-baz",
           "test/foo-debug",
           "test/foo-opt"
@@ -137,17 +172,68 @@ Test preset with fuzzy subcommand
           "env": {
               "TRY_SELECTOR": "fuzzy"
           }
       },
       "version": 1
   }
   
   $ ./mach try fuzzy $testargs --list-presets
-  baz: 'baz
+  bar:
+    no_artifact: true
+    platforms:
+    - win32
+    push: false
+    selector: syntax
+    tags:
+    - bar
+    talos:
+    - all
+    tests:
+    - none
+  baz:
+    no_artifact: true
+    push: false
+    query:
+    - '''baz'
+    selector: fuzzy
+  foo:
+    no_artifact: true
+    platforms:
+    - linux
+    selector: syntax
+    tags:
+    - foo
+    talos:
+    - none
+    tests:
+    - mochitests
+  
   $ ./mach try fuzzy $testargs --edit-presets
-  [try]
-  foo = -b do -p linux -u mochitests -t none --tag foo
-  bar = -b do -p win32 -u none -t all --tag bar
-  
-  [fuzzy]
-  baz = 'baz
-  
+  bar:
+    no_artifact: true
+    platforms:
+    - win32
+    push: false
+    selector: syntax
+    tags:
+    - bar
+    talos:
+    - all
+    tests:
+    - none
+  baz:
+    no_artifact: true
+    push: false
+    query:
+    - '''baz'
+    selector: fuzzy
+  foo:
+    no_artifact: true
+    platforms:
+    - linux
+    selector: syntax
+    tags:
+    - foo
+    talos:
+    - none
+    tests:
+    - mochitests
new file mode 100644
copy from taskcluster/taskgraph/util/templates.py
copy to tools/tryselect/util/dicttools.py