• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 Google Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import argparse
16import optparse
17
18from typ.host import Host
19
20
21class _Bailout(Exception):
22    pass
23
24
25DEFAULT_COVERAGE_OMIT = ['*/typ/*', '*/site-packages/*']
26DEFAULT_STATUS_FORMAT = '[%f/%t] '
27DEFAULT_SUFFIXES = ['*_test.py', '*_unittest.py']
28
29
30class ArgumentParser(argparse.ArgumentParser):
31
32    @staticmethod
33    def add_option_group(parser, title, discovery=False,
34                         running=False, reporting=False, skip=None):
35        # TODO: Get rid of this when telemetry upgrades to argparse.
36        ap = ArgumentParser(add_help=False, version=False, discovery=discovery,
37                            running=running, reporting=reporting)
38        optlist = ap.optparse_options(skip=skip)
39        group = optparse.OptionGroup(parser, title)
40        group.add_options(optlist)
41        parser.add_option_group(group)
42
43    def __init__(self, host=None, add_help=True, version=True, discovery=True,
44                 reporting=True, running=True):
45        super(ArgumentParser, self).__init__(prog='typ', add_help=add_help)
46
47        self._host = host or Host()
48        self.exit_status = None
49
50        self.usage = '%(prog)s [options] [tests...]'
51
52        if version:
53            self.add_argument('-V', '--version', action='store_true',
54                              help='Print the typ version and exit.')
55
56        if discovery:
57            self.add_argument('-f', '--file-list', metavar='FILENAME',
58                              action='store',
59                              help=('Takes the list of tests from the file '
60                                    '(use "-" for stdin).'))
61            self.add_argument('--all', action='store_true',
62                              help=('Run all the tests, including the ones '
63                                    'normally skipped.'))
64            self.add_argument('--isolate', metavar='glob', default=[],
65                              action='append',
66                              help=('Globs of tests to run in isolation '
67                                    '(serially).'))
68            self.add_argument('--skip', metavar='glob', default=[],
69                              action='append',
70                              help=('Globs of test names to skip ('
71                                    'defaults to %(default)s).'))
72            self.add_argument('--suffixes', metavar='glob', default=[],
73                              action='append',
74                              help=('Globs of test filenames to look for ('
75                                    'can specify multiple times; defaults '
76                                    'to %s).' % DEFAULT_SUFFIXES))
77
78        if reporting:
79            self.add_argument('--builder-name',
80                              help=('Builder name to include in the '
81                                    'uploaded data.'))
82            self.add_argument('-c', '--coverage', action='store_true',
83                              help='Reports coverage information.')
84            self.add_argument('--coverage-source', action='append',
85                              default=[],
86                              help=('Directories to include when running and '
87                                    'reporting coverage (defaults to '
88                                    '--top-level-dir plus --path)'))
89            self.add_argument('--coverage-omit', action='append',
90                              default=[],
91                              help=('Globs to omit when reporting coverage '
92                                    '(defaults to %s).' %
93                                    DEFAULT_COVERAGE_OMIT))
94            self.add_argument('--coverage-annotate', action='store_true',
95                              help=('Produce an annotate source report.'))
96            self.add_argument('--coverage-show-missing', action='store_true',
97                              help=('Show missing line ranges in coverage '
98                                    'report.'))
99            self.add_argument('--master-name',
100                              help=('Buildbot master name to include in the '
101                                    'uploaded data.'))
102            self.add_argument('--metadata', action='append', default=[],
103                              help=('Optional key=value metadata that will '
104                                    'be included in the results.'))
105            self.add_argument('--test-results-server',
106                              help=('If specified, uploads the full results '
107                                    'to this server.'))
108            self.add_argument('--test-type',
109                              help=('Name of test type to include in the '
110                                    'uploaded data (e.g., '
111                                    '"telemetry_unittests").'))
112            self.add_argument('--write-full-results-to', metavar='FILENAME',
113                              action='store',
114                              help=('If specified, writes the full results to '
115                                    'that path.'))
116            self.add_argument('--write-trace-to', metavar='FILENAME',
117                              action='store',
118                              help=('If specified, writes the trace to '
119                                    'that path.'))
120            self.add_argument('tests', nargs='*', default=[],
121                              help=argparse.SUPPRESS)
122
123        if running:
124            self.add_argument('-d', '--debugger', action='store_true',
125                              help='Runs the tests under the debugger.')
126            self.add_argument('-j', '--jobs', metavar='N', type=int,
127                              default=self._host.cpu_count(),
128                              help=('Runs N jobs in parallel '
129                                    '(defaults to %(default)s).'))
130            self.add_argument('-l', '--list-only', action='store_true',
131                              help='Lists all the test names found and exits.')
132            self.add_argument('-n', '--dry-run', action='store_true',
133                              help=argparse.SUPPRESS)
134            self.add_argument('-q', '--quiet', action='store_true',
135                              default=False,
136                              help=('Runs as quietly as possible '
137                                    '(only prints errors).'))
138            self.add_argument('-s', '--status-format',
139                              default=self._host.getenv('NINJA_STATUS',
140                                                        DEFAULT_STATUS_FORMAT),
141                              help=argparse.SUPPRESS)
142            self.add_argument('-t', '--timing', action='store_true',
143                              help='Prints timing info.')
144            self.add_argument('-v', '--verbose', action='count', default=0,
145                              help=('Prints more stuff (can specify multiple '
146                                    'times for more output).'))
147            self.add_argument('--passthrough', action='store_true',
148                              default=False,
149                              help='Prints all output while running.')
150            self.add_argument('--total-shards', default=1, type=int,
151                              help=('Total number of shards being used for '
152                                    'this test run. (The user of '
153                                    'this script is responsible for spawning '
154                                    'all of the shards.)'))
155            self.add_argument('--shard-index', default=0, type=int,
156                              help=('Shard index (0..total_shards-1) of this '
157                                    'test run.'))
158            self.add_argument('--retry-limit', type=int, default=0,
159                              help='Retries each failure up to N times.')
160            self.add_argument('--terminal-width', type=int,
161                              default=self._host.terminal_width(),
162                              help=argparse.SUPPRESS)
163            self.add_argument('--overwrite', action='store_true',
164                              default=None,
165                              help=argparse.SUPPRESS)
166            self.add_argument('--no-overwrite', action='store_false',
167                              dest='overwrite', default=None,
168                              help=argparse.SUPPRESS)
169
170        if discovery or running:
171            self.add_argument('-P', '--path', action='append', default=[],
172                              help=('Adds dir to sys.path (can specify '
173                                    'multiple times).'))
174            self.add_argument('--top-level-dir', default=None,
175                              help=('Sets the top directory of project '
176                                    '(used when running subdirs).'))
177
178    def parse_args(self, args=None, namespace=None):
179        try:
180            rargs = super(ArgumentParser, self).parse_args(args=args,
181                                                           namespace=namespace)
182        except _Bailout:
183            return None
184
185        for val in rargs.metadata:
186            if '=' not in val:
187                self._print_message('Error: malformed --metadata "%s"' % val)
188                self.exit_status = 2
189
190        if rargs.test_results_server:
191            if not rargs.builder_name:
192                self._print_message('Error: --builder-name must be specified '
193                                    'along with --test-result-server')
194                self.exit_status = 2
195            if not rargs.master_name:
196                self._print_message('Error: --master-name must be specified '
197                                    'along with --test-result-server')
198                self.exit_status = 2
199            if not rargs.test_type:
200                self._print_message('Error: --test-type must be specified '
201                                    'along with --test-result-server')
202                self.exit_status = 2
203
204        if rargs.total_shards < 1:
205            self._print_message('Error: --total-shards must be at least 1')
206            self.exit_status = 2
207
208        if rargs.shard_index < 0:
209            self._print_message('Error: --shard-index must be at least 0')
210            self.exit_status = 2
211
212        if rargs.shard_index >= rargs.total_shards:
213            self._print_message('Error: --shard-index must be no more than '
214                                'the number of shards (%i) minus 1' %
215                                rargs.total_shards)
216            self.exit_status = 2
217
218        if not rargs.suffixes:
219            rargs.suffixes = DEFAULT_SUFFIXES
220
221        if not rargs.coverage_omit:
222            rargs.coverage_omit = DEFAULT_COVERAGE_OMIT
223
224        if rargs.debugger:  # pragma: no cover
225            rargs.jobs = 1
226            rargs.passthrough = True
227
228        if rargs.overwrite is None:
229            rargs.overwrite = self._host.stdout.isatty() and not rargs.verbose
230
231        return rargs
232
233    # Redefining built-in 'file' pylint: disable=W0622
234
235    def _print_message(self, msg, file=None):
236        self._host.print_(msg=msg, stream=file, end='\n')
237
238    def print_help(self, file=None):
239        self._print_message(msg=self.format_help(), file=file)
240
241    def error(self, message, bailout=True):  # pylint: disable=W0221
242        self.exit(2, '%s: error: %s\n' % (self.prog, message), bailout=bailout)
243
244    def exit(self, status=0, message=None,  # pylint: disable=W0221
245             bailout=True):
246        self.exit_status = status
247        if message:
248            self._print_message(message, file=self._host.stderr)
249        if bailout:
250            raise _Bailout()
251
252    def optparse_options(self, skip=None):
253        skip = skip or []
254        options = []
255        for action in self._actions:
256            args = [flag for flag in action.option_strings if flag not in skip]
257            if not args or action.help == '==SUPPRESS==':
258                # must either be a positional argument like 'tests'
259                # or an option we want to skip altogether.
260                continue
261
262            kwargs = {
263                'default': action.default,
264                'dest': action.dest,
265                'help': action.help,
266                'metavar': action.metavar,
267                'type': action.type,
268                'action': _action_str(action)
269            }
270            options.append(optparse.make_option(*args, **kwargs))
271        return options
272
273    def argv_from_args(self, args):
274        default_parser = ArgumentParser(host=self._host)
275        default_args = default_parser.parse_args([])
276        argv = []
277        tests = []
278        d = vars(args)
279        for k in sorted(d.keys()):
280            v = d[k]
281            argname = _argname_from_key(k)
282            action = self._action_for_key(k)
283            action_str = _action_str(action)
284            if k == 'tests':
285                tests = v
286                continue
287            if getattr(default_args, k) == v:
288                # this arg has the default value, so skip it.
289                continue
290
291            assert action_str in ['append', 'count', 'store', 'store_true']
292            if action_str == 'append':
293                for el in v:
294                    argv.append(argname)
295                    argv.append(el)
296            elif action_str == 'count':
297                for _ in range(v):
298                    argv.append(argname)
299            elif action_str == 'store':
300                argv.append(argname)
301                argv.append(str(v))
302            else:
303                # action_str == 'store_true'
304                argv.append(argname)
305
306        return argv + tests
307
308    def _action_for_key(self, key):
309        for action in self._actions:
310            if action.dest == key:
311                return action
312
313        assert False, ('Could not find an action for %s'  # pragma: no cover
314                       % key)
315
316
317def _action_str(action):
318    # Access to a protected member pylint: disable=W0212
319    assert action.__class__ in (
320        argparse._AppendAction,
321        argparse._CountAction,
322        argparse._StoreAction,
323        argparse._StoreTrueAction
324    )
325
326    if isinstance(action, argparse._AppendAction):
327        return 'append'
328    if isinstance(action, argparse._CountAction):
329        return 'count'
330    if isinstance(action, argparse._StoreAction):
331        return 'store'
332    if isinstance(action, argparse._StoreTrueAction):
333        return 'store_true'
334
335
336def _argname_from_key(key):
337    return '--' + key.replace('_', '-')
338