• 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('--retry-limit', type=int, default=0,
151                              help='Retries each failure up to N times.')
152            self.add_argument('--terminal-width', type=int,
153                              default=self._host.terminal_width(),
154                              help=argparse.SUPPRESS)
155            self.add_argument('--overwrite', action='store_true',
156                              default=None,
157                              help=argparse.SUPPRESS)
158            self.add_argument('--no-overwrite', action='store_false',
159                              dest='overwrite', default=None,
160                              help=argparse.SUPPRESS)
161
162        if discovery or running:
163            self.add_argument('-P', '--path', action='append', default=[],
164                              help=('Adds dir to sys.path (can specify '
165                                    'multiple times).'))
166            self.add_argument('--top-level-dir', default=None,
167                              help=('Sets the top directory of project '
168                                    '(used when running subdirs).'))
169
170    def parse_args(self, args=None, namespace=None):
171        try:
172            rargs = super(ArgumentParser, self).parse_args(args=args,
173                                                           namespace=namespace)
174        except _Bailout:
175            return None
176
177        for val in rargs.metadata:
178            if '=' not in val:
179                self._print_message('Error: malformed --metadata "%s"' % val)
180                self.exit_status = 2
181
182        if rargs.test_results_server:
183            if not rargs.builder_name:
184                self._print_message('Error: --builder-name must be specified '
185                                    'along with --test-result-server')
186                self.exit_status = 2
187            if not rargs.master_name:
188                self._print_message('Error: --master-name must be specified '
189                                    'along with --test-result-server')
190                self.exit_status = 2
191            if not rargs.test_type:
192                self._print_message('Error: --test-type must be specified '
193                                    'along with --test-result-server')
194                self.exit_status = 2
195
196        if not rargs.suffixes:
197            rargs.suffixes = DEFAULT_SUFFIXES
198
199        if not rargs.coverage_omit:
200            rargs.coverage_omit = DEFAULT_COVERAGE_OMIT
201
202        if rargs.debugger:  # pragma: no cover
203            rargs.jobs = 1
204            rargs.passthrough = True
205
206        if rargs.overwrite is None:
207            rargs.overwrite = self._host.stdout.isatty() and not rargs.verbose
208
209        return rargs
210
211    # Redefining built-in 'file' pylint: disable=W0622
212
213    def _print_message(self, msg, file=None):
214        self._host.print_(msg=msg, stream=file, end='\n')
215
216    def print_help(self, file=None):
217        self._print_message(msg=self.format_help(), file=file)
218
219    def error(self, message, bailout=True):  # pylint: disable=W0221
220        self.exit(2, '%s: error: %s\n' % (self.prog, message), bailout=bailout)
221
222    def exit(self, status=0, message=None,  # pylint: disable=W0221
223             bailout=True):
224        self.exit_status = status
225        if message:
226            self._print_message(message, file=self._host.stderr)
227        if bailout:
228            raise _Bailout()
229
230    def optparse_options(self, skip=None):
231        skip = skip or []
232        options = []
233        for action in self._actions:
234            args = [flag for flag in action.option_strings if flag not in skip]
235            if not args or action.help == '==SUPPRESS==':
236                # must either be a positional argument like 'tests'
237                # or an option we want to skip altogether.
238                continue
239
240            kwargs = {
241                'default': action.default,
242                'dest': action.dest,
243                'help': action.help,
244                'metavar': action.metavar,
245                'type': action.type,
246                'action': _action_str(action)
247            }
248            options.append(optparse.make_option(*args, **kwargs))
249        return options
250
251    def argv_from_args(self, args):
252        default_parser = ArgumentParser(host=self._host)
253        default_args = default_parser.parse_args([])
254        argv = []
255        tests = []
256        d = vars(args)
257        for k in sorted(d.keys()):
258            v = d[k]
259            argname = _argname_from_key(k)
260            action = self._action_for_key(k)
261            action_str = _action_str(action)
262            if k == 'tests':
263                tests = v
264                continue
265            if getattr(default_args, k) == v:
266                # this arg has the default value, so skip it.
267                continue
268
269            assert action_str in ['append', 'count', 'store', 'store_true']
270            if action_str == 'append':
271                for el in v:
272                    argv.append(argname)
273                    argv.append(el)
274            elif action_str == 'count':
275                for _ in range(v):
276                    argv.append(argname)
277            elif action_str == 'store':
278                argv.append(argname)
279                argv.append(str(v))
280            else:
281                # action_str == 'store_true'
282                argv.append(argname)
283
284        return argv + tests
285
286    def _action_for_key(self, key):
287        for action in self._actions:
288            if action.dest == key:
289                return action
290
291        assert False, ('Could not find an action for %s'  # pragma: no cover
292                       % key)
293
294
295def _action_str(action):
296    # Access to a protected member pylint: disable=W0212
297    assert action.__class__ in (
298        argparse._AppendAction,
299        argparse._CountAction,
300        argparse._StoreAction,
301        argparse._StoreTrueAction
302    )
303
304    if isinstance(action, argparse._AppendAction):
305        return 'append'
306    if isinstance(action, argparse._CountAction):
307        return 'count'
308    if isinstance(action, argparse._StoreAction):
309        return 'store'
310    if isinstance(action, argparse._StoreTrueAction):
311        return 'store_true'
312
313
314def _argname_from_key(key):
315    return '--' + key.replace('_', '-')
316