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