1# Copyright 2013 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Parses the command line, discovers the appropriate tests, and runs them. 6 7Handles test configuration, but all the logic for 8actually running the test is in Test and PageRunner.""" 9 10import inspect 11import json 12import os 13import sys 14 15from telemetry import decorators 16from telemetry import test 17from telemetry.core import browser_options 18from telemetry.core import command_line 19from telemetry.core import discover 20from telemetry.core import environment 21from telemetry.core import util 22from telemetry.page import page_set 23from telemetry.page import page_test 24from telemetry.page import profile_creator 25from telemetry.util import find_dependencies 26 27 28class Deps(find_dependencies.FindDependenciesCommand): 29 """Prints all dependencies""" 30 31 def Run(self, args): 32 main_module = sys.modules['__main__'] 33 args.positional_args.append(os.path.realpath(main_module.__file__)) 34 return super(Deps, self).Run(args) 35 36 37class Help(command_line.OptparseCommand): 38 """Display help information about a command""" 39 40 usage = '[command]' 41 42 def Run(self, args): 43 if len(args.positional_args) == 1: 44 commands = _MatchingCommands(args.positional_args[0]) 45 if len(commands) == 1: 46 command = commands[0] 47 parser = command.CreateParser() 48 command.AddCommandLineArgs(parser) 49 parser.print_help() 50 return 0 51 52 print >> sys.stderr, ('usage: %s <command> [<options>]' % _ScriptName()) 53 print >> sys.stderr, 'Available commands are:' 54 for command in _Commands(): 55 print >> sys.stderr, ' %-10s %s' % ( 56 command.Name(), command.Description()) 57 print >> sys.stderr, ('"%s help <command>" to see usage information ' 58 'for a specific command.' % _ScriptName()) 59 return 0 60 61 62class List(command_line.OptparseCommand): 63 """Lists the available tests""" 64 65 usage = '[test_name] [<options>]' 66 67 @classmethod 68 def AddCommandLineArgs(cls, parser): 69 parser.add_option('-j', '--json', action='store_true') 70 71 @classmethod 72 def ProcessCommandLineArgs(cls, parser, args): 73 if not args.positional_args: 74 args.tests = _Tests() 75 elif len(args.positional_args) == 1: 76 args.tests = _MatchTestName(args.positional_args[0], exact_matches=False) 77 else: 78 parser.error('Must provide at most one test name.') 79 80 def Run(self, args): 81 if args.json: 82 test_list = [] 83 for test_class in sorted(args.tests, key=lambda t: t.Name()): 84 test_list.append({ 85 'name': test_class.Name(), 86 'description': test_class.Description(), 87 'options': test_class.options, 88 }) 89 print json.dumps(test_list) 90 else: 91 _PrintTestList(args.tests) 92 return 0 93 94 95class Run(command_line.OptparseCommand): 96 """Run one or more tests""" 97 98 usage = 'test_name [page_set] [<options>]' 99 100 @classmethod 101 def CreateParser(cls): 102 options = browser_options.BrowserFinderOptions() 103 parser = options.CreateParser('%%prog %s %s' % (cls.Name(), cls.usage)) 104 return parser 105 106 @classmethod 107 def AddCommandLineArgs(cls, parser): 108 test.AddCommandLineArgs(parser) 109 110 # Allow tests to add their own command line options. 111 matching_tests = [] 112 for arg in sys.argv[1:]: 113 matching_tests += _MatchTestName(arg) 114 115 if matching_tests: 116 # TODO(dtu): After move to argparse, add command-line args for all tests 117 # to subparser. Using subparsers will avoid duplicate arguments. 118 matching_test = matching_tests.pop() 119 matching_test.AddCommandLineArgs(parser) 120 # The test's options override the defaults! 121 matching_test.SetArgumentDefaults(parser) 122 123 @classmethod 124 def ProcessCommandLineArgs(cls, parser, args): 125 if not args.positional_args: 126 _PrintTestList(_Tests()) 127 sys.exit(-1) 128 129 input_test_name = args.positional_args[0] 130 matching_tests = _MatchTestName(input_test_name) 131 if not matching_tests: 132 print >> sys.stderr, 'No test named "%s".' % input_test_name 133 print >> sys.stderr 134 _PrintTestList(_Tests()) 135 sys.exit(-1) 136 137 if len(matching_tests) > 1: 138 print >> sys.stderr, 'Multiple tests named "%s".' % input_test_name 139 print >> sys.stderr, 'Did you mean one of these?' 140 print >> sys.stderr 141 _PrintTestList(matching_tests) 142 sys.exit(-1) 143 144 test_class = matching_tests.pop() 145 if issubclass(test_class, page_test.PageTest): 146 if len(args.positional_args) < 2: 147 parser.error('Need to specify a page set for "%s".' % test_class.Name()) 148 if len(args.positional_args) > 2: 149 parser.error('Too many arguments.') 150 page_set_path = args.positional_args[1] 151 if not os.path.exists(page_set_path): 152 parser.error('Page set not found.') 153 if not (os.path.isfile(page_set_path) and 154 discover.IsPageSetFile(page_set_path)): 155 parser.error('Unsupported page set file format.') 156 157 class TestWrapper(test.Test): 158 test = test_class 159 160 @classmethod 161 def CreatePageSet(cls, options): 162 return page_set.PageSet.FromFile(page_set_path) 163 164 test_class = TestWrapper 165 else: 166 if len(args.positional_args) > 1: 167 parser.error('Too many arguments.') 168 169 assert issubclass(test_class, test.Test), 'Trying to run a non-Test?!' 170 171 test.ProcessCommandLineArgs(parser, args) 172 test_class.ProcessCommandLineArgs(parser, args) 173 174 cls._test = test_class 175 176 def Run(self, args): 177 return min(255, self._test().Run(args)) 178 179 180def _ScriptName(): 181 return os.path.basename(sys.argv[0]) 182 183 184def _Commands(): 185 """Generates a list of all classes in this file that subclass Command.""" 186 for _, cls in inspect.getmembers(sys.modules[__name__]): 187 if not inspect.isclass(cls): 188 continue 189 if not issubclass(cls, command_line.Command): 190 continue 191 yield cls 192 193def _MatchingCommands(string): 194 return [command for command in _Commands() 195 if command.Name().startswith(string)] 196 197@decorators.Cache 198def _Tests(): 199 tests = [] 200 for base_dir in config.base_paths: 201 tests += discover.DiscoverClasses(base_dir, base_dir, test.Test, 202 index_by_class_name=True).values() 203 page_tests = discover.DiscoverClasses(base_dir, base_dir, 204 page_test.PageTest, 205 index_by_class_name=True).values() 206 tests += [test_class for test_class in page_tests 207 if not issubclass(test_class, profile_creator.ProfileCreator)] 208 return tests 209 210 211def _MatchTestName(input_test_name, exact_matches=True): 212 def _Matches(input_string, search_string): 213 if search_string.startswith(input_string): 214 return True 215 for part in search_string.split('.'): 216 if part.startswith(input_string): 217 return True 218 return False 219 220 # Exact matching. 221 if exact_matches: 222 # Don't add aliases to search dict, only allow exact matching for them. 223 if input_test_name in config.test_aliases: 224 exact_match = config.test_aliases[input_test_name] 225 else: 226 exact_match = input_test_name 227 228 for test_class in _Tests(): 229 if exact_match == test_class.Name(): 230 return [test_class] 231 232 # Fuzzy matching. 233 return [test_class for test_class in _Tests() 234 if _Matches(input_test_name, test_class.Name())] 235 236 237def _PrintTestList(tests): 238 if not tests: 239 print >> sys.stderr, 'No tests found!' 240 return 241 242 # Align the test names to the longest one. 243 format_string = ' %%-%ds %%s' % max(len(t.Name()) for t in tests) 244 245 filtered_tests = [test_class for test_class in tests 246 if issubclass(test_class, test.Test)] 247 if filtered_tests: 248 print >> sys.stderr, 'Available tests are:' 249 for test_class in sorted(filtered_tests, key=lambda t: t.Name()): 250 print >> sys.stderr, format_string % ( 251 test_class.Name(), test_class.Description()) 252 print >> sys.stderr 253 254 filtered_tests = [test_class for test_class in tests 255 if issubclass(test_class, page_test.PageTest)] 256 if filtered_tests: 257 print >> sys.stderr, 'Available page tests are:' 258 for test_class in sorted(filtered_tests, key=lambda t: t.Name()): 259 print >> sys.stderr, format_string % ( 260 test_class.Name(), test_class.Description()) 261 print >> sys.stderr 262 263 264config = environment.Environment([util.GetBaseDir()]) 265 266 267def main(): 268 # Get the command name from the command line. 269 if len(sys.argv) > 1 and sys.argv[1] == '--help': 270 sys.argv[1] = 'help' 271 272 command_name = 'run' 273 for arg in sys.argv[1:]: 274 if not arg.startswith('-'): 275 command_name = arg 276 break 277 278 # Validate and interpret the command name. 279 commands = _MatchingCommands(command_name) 280 if len(commands) > 1: 281 print >> sys.stderr, ('"%s" is not a %s command. Did you mean one of these?' 282 % (command_name, _ScriptName())) 283 for command in commands: 284 print >> sys.stderr, ' %-10s %s' % ( 285 command.Name(), command.Description()) 286 return 1 287 if commands: 288 command = commands[0] 289 else: 290 command = Run 291 292 # Parse and run the command. 293 parser = command.CreateParser() 294 command.AddCommandLineArgs(parser) 295 options, args = parser.parse_args() 296 if commands: 297 args = args[1:] 298 options.positional_args = args 299 command.ProcessCommandLineArgs(parser, options) 300 return command().Run(options) 301