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.
5 """Parses the command line, discovers the appropriate tests, and runs them.
7 Handles test configuration, but all the logic for
8 actually running the test is in Test and PageRunner."""
16 from telemetry import benchmark
17 from telemetry import decorators
18 from telemetry.core import browser_finder
19 from telemetry.core import browser_options
20 from telemetry.core import command_line
21 from telemetry.core import discover
22 from telemetry.core import environment
23 from telemetry.core import util
24 from telemetry.page import page_set
25 from telemetry.page import page_test
26 from telemetry.page import profile_creator
27 from telemetry.util import find_dependencies
30 class Deps(find_dependencies.FindDependenciesCommand):
31 """Prints all dependencies"""
34 main_module = sys.modules['__main__']
35 args.positional_args.append(os.path.realpath(main_module.__file__))
36 return super(Deps, self).Run(args)
39 class Help(command_line.OptparseCommand):
40 """Display help information about a command"""
45 if len(args.positional_args) == 1:
46 commands = _MatchingCommands(args.positional_args[0])
47 if len(commands) == 1:
49 parser = command.CreateParser()
50 command.AddCommandLineArgs(parser)
54 print >> sys.stderr, ('usage: %s [command] [<options>]' % _ScriptName())
55 print >> sys.stderr, 'Available commands are:'
56 for command in _Commands():
57 print >> sys.stderr, ' %-10s %s' % (
58 command.Name(), command.Description())
59 print >> sys.stderr, ('"%s help <command>" to see usage information '
60 'for a specific command.' % _ScriptName())
64 class List(command_line.OptparseCommand):
65 """Lists the available tests"""
67 usage = '[test_name] [<options>]'
70 def CreateParser(cls):
71 options = browser_options.BrowserFinderOptions()
72 parser = options.CreateParser('%%prog %s %s' % (cls.Name(), cls.usage))
76 def AddCommandLineArgs(cls, parser):
77 parser.add_option('-j', '--json-output-file', type='string')
78 parser.add_option('-n', '--num-shards', type='int', default=1)
81 def ProcessCommandLineArgs(cls, parser, args):
82 if not args.positional_args:
84 elif len(args.positional_args) == 1:
85 args.tests = _MatchTestName(args.positional_args[0], exact_matches=False)
87 parser.error('Must provide at most one test name.')
90 if args.json_output_file:
91 possible_browser = browser_finder.FindBrowser(args)
92 with open(args.json_output_file, 'w') as f:
93 f.write(_GetJsonTestList(possible_browser, args.tests, args.num_shards))
95 _PrintTestList(args.tests)
99 class Run(command_line.OptparseCommand):
100 """Run one or more tests (default)"""
102 usage = 'test_name [page_set] [<options>]'
105 def CreateParser(cls):
106 options = browser_options.BrowserFinderOptions()
107 parser = options.CreateParser('%%prog %s %s' % (cls.Name(), cls.usage))
111 def AddCommandLineArgs(cls, parser):
112 benchmark.AddCommandLineArgs(parser)
114 # Allow tests to add their own command line options.
116 for arg in sys.argv[1:]:
117 matching_tests += _MatchTestName(arg)
120 # TODO(dtu): After move to argparse, add command-line args for all tests
121 # to subparser. Using subparsers will avoid duplicate arguments.
122 matching_test = matching_tests.pop()
123 matching_test.AddCommandLineArgs(parser)
124 # The test's options override the defaults!
125 matching_test.SetArgumentDefaults(parser)
128 def ProcessCommandLineArgs(cls, parser, args):
129 if not args.positional_args:
130 _PrintTestList(_Tests())
133 input_test_name = args.positional_args[0]
134 matching_tests = _MatchTestName(input_test_name)
135 if not matching_tests:
136 print >> sys.stderr, 'No test named "%s".' % input_test_name
138 _PrintTestList(_Tests())
141 if len(matching_tests) > 1:
142 print >> sys.stderr, 'Multiple tests named "%s".' % input_test_name
143 print >> sys.stderr, 'Did you mean one of these?'
145 _PrintTestList(matching_tests)
148 test_class = matching_tests.pop()
149 if issubclass(test_class, page_test.PageTest):
150 if len(args.positional_args) < 2:
151 parser.error('Need to specify a page set for "%s".' % test_class.Name())
152 if len(args.positional_args) > 2:
153 parser.error('Too many arguments.')
154 page_set_path = args.positional_args[1]
155 if not os.path.exists(page_set_path):
156 parser.error('Page set not found.')
157 if not (os.path.isfile(page_set_path) and
158 discover.IsPageSetFile(page_set_path)):
159 parser.error('Unsupported page set file format.')
161 class TestWrapper(benchmark.Benchmark):
165 def CreatePageSet(cls, options):
166 return page_set.PageSet.FromFile(page_set_path)
168 test_class = TestWrapper
170 if len(args.positional_args) > 1:
171 parser.error('Too many arguments.')
173 assert issubclass(test_class, benchmark.Benchmark), (
174 'Trying to run a non-Benchmark?!')
176 benchmark.ProcessCommandLineArgs(parser, args)
177 test_class.ProcessCommandLineArgs(parser, args)
179 cls._test = test_class
182 return min(255, self._test().Run(args))
186 return os.path.basename(sys.argv[0])
190 """Generates a list of all classes in this file that subclass Command."""
191 for _, cls in inspect.getmembers(sys.modules[__name__]):
192 if not inspect.isclass(cls):
194 if not issubclass(cls, command_line.Command):
198 def _MatchingCommands(string):
199 return [command for command in _Commands()
200 if command.Name().startswith(string)]
205 for base_dir in config.base_paths:
206 tests += discover.DiscoverClasses(base_dir, base_dir, benchmark.Benchmark,
207 index_by_class_name=True).values()
208 page_tests = discover.DiscoverClasses(base_dir, base_dir,
210 index_by_class_name=True).values()
211 tests += [test_class for test_class in page_tests
212 if not issubclass(test_class, profile_creator.ProfileCreator)]
216 def _MatchTestName(input_test_name, exact_matches=True):
217 def _Matches(input_string, search_string):
218 if search_string.startswith(input_string):
220 for part in search_string.split('.'):
221 if part.startswith(input_string):
227 # Don't add aliases to search dict, only allow exact matching for them.
228 if input_test_name in config.test_aliases:
229 exact_match = config.test_aliases[input_test_name]
231 exact_match = input_test_name
233 for test_class in _Tests():
234 if exact_match == test_class.Name():
238 return [test_class for test_class in _Tests()
239 if _Matches(input_test_name, test_class.Name())]
242 def _GetJsonTestList(possible_browser, test_classes, num_shards):
243 """Returns a list of all enabled tests in a JSON format expected by buildbots.
245 JSON format (see build/android/pylib/perf/test_runner.py):
249 "device_affinity": int,
250 "cmd": "script_to_execute foo"
253 "device_affinity": int,
254 "cmd": "script_to_execute bar"
264 for test_class in test_classes:
265 if not issubclass(test_class, benchmark.Benchmark):
267 if not decorators.IsEnabled(test_class, possible_browser):
269 name = test_class.Name()
270 output['steps'][name] = {
271 'cmd': ' '.join([sys.executable, os.path.realpath(sys.argv[0]),
272 '--browser=%s' % possible_browser.browser_type,
273 '-v', '--output-format=buildbot', name]),
274 # TODO(tonyg): Currently we set the device affinity to a stable hash of
275 # the test name. This somewhat evenly distributes benchmarks among the
276 # requested number of shards. However, it is far from optimal in terms of
277 # cycle time. We should add a test size decorator (e.g. small, medium,
278 # large) and let that inform sharding.
279 'device_affinity': int(hashlib.sha1(name).hexdigest(), 16) % num_shards
281 return json.dumps(output, indent=2, sort_keys=True)
284 def _PrintTestList(tests):
286 print >> sys.stderr, 'No tests found!'
289 # Align the test names to the longest one.
290 format_string = ' %%-%ds %%s' % max(len(t.Name()) for t in tests)
292 filtered_tests = [test_class for test_class in tests
293 if issubclass(test_class, benchmark.Benchmark)]
295 print >> sys.stderr, 'Available tests are:'
296 for test_class in sorted(filtered_tests, key=lambda t: t.Name()):
297 print >> sys.stderr, format_string % (
298 test_class.Name(), test_class.Description())
301 filtered_tests = [test_class for test_class in tests
302 if issubclass(test_class, page_test.PageTest)]
304 print >> sys.stderr, 'Available page tests are:'
305 for test_class in sorted(filtered_tests, key=lambda t: t.Name()):
306 print >> sys.stderr, format_string % (
307 test_class.Name(), test_class.Description())
311 config = environment.Environment([util.GetBaseDir()])
315 # Get the command name from the command line.
316 if len(sys.argv) > 1 and sys.argv[1] == '--help':
320 for arg in sys.argv[1:]:
321 if not arg.startswith('-'):
325 # Validate and interpret the command name.
326 commands = _MatchingCommands(command_name)
327 if len(commands) > 1:
328 print >> sys.stderr, ('"%s" is not a %s command. Did you mean one of these?'
329 % (command_name, _ScriptName()))
330 for command in commands:
331 print >> sys.stderr, ' %-10s %s' % (
332 command.Name(), command.Description())
335 command = commands[0]
339 # Parse and run the command.
340 parser = command.CreateParser()
341 command.AddCommandLineArgs(parser)
342 options, args = parser.parse_args()
345 options.positional_args = args
346 command.ProcessCommandLineArgs(parser, options)
347 return command().Run(options)