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