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