1# Copyright 2012 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.
4import logging
5import sys
6
7from telemetry.core import util
8from telemetry.core import platform as platform_module
9from telemetry import decorators
10from telemetry.internal.browser import browser_finder
11from telemetry.internal.browser import browser_finder_exceptions
12from telemetry.internal.browser import browser_options
13from telemetry.internal.platform import android_device
14from telemetry.internal.util import binary_manager
15from telemetry.internal.util import command_line
16from telemetry.internal.util import ps_util
17from telemetry.testing import browser_test_case
18from telemetry.testing import options_for_unittests
19
20from catapult_base import xvfb
21
22import typ
23
24
25class RunTestsCommand(command_line.OptparseCommand):
26  """Run unit tests"""
27
28  usage = '[test_name ...] [<options>]'
29  xvfb_process = None
30
31  def __init__(self):
32    super(RunTestsCommand, self).__init__()
33    self.stream = sys.stdout
34
35  @classmethod
36  def CreateParser(cls):
37    options = browser_options.BrowserFinderOptions()
38    options.browser_type = 'any'
39    parser = options.CreateParser('%%prog %s' % cls.usage)
40    return parser
41
42  @classmethod
43  def AddCommandLineArgs(cls, parser, _):
44    parser.add_option('--start-xvfb', action='store_true',
45                      default=False, help='Start Xvfb display if needed.')
46    parser.add_option('--repeat-count', type='int', default=1,
47                      help='Repeats each a provided number of times.')
48    parser.add_option('--no-browser', action='store_true', default=False,
49                      help='Don\'t require an actual browser to run the tests.')
50    parser.add_option('-d', '--also-run-disabled-tests',
51                      dest='run_disabled_tests',
52                      action='store_true', default=False,
53                      help='Ignore @Disabled and @Enabled restrictions.')
54    parser.add_option('--exact-test-filter', action='store_true', default=False,
55                      help='Treat test filter as exact matches (default is '
56                           'substring matches).')
57    parser.add_option('--client-config', dest='client_config', default=None)
58    parser.add_option('--disable-logging-config', action='store_true',
59                      default=False, help='Configure logging (default on)')
60
61    typ.ArgumentParser.add_option_group(parser,
62                                        "Options for running the tests",
63                                        running=True,
64                                        skip=['-d', '-v', '--verbose'])
65    typ.ArgumentParser.add_option_group(parser,
66                                        "Options for reporting the results",
67                                        reporting=True)
68
69  @classmethod
70  def ProcessCommandLineArgs(cls, parser, args, _):
71    # We retry failures by default unless we're running a list of tests
72    # explicitly.
73    if not args.retry_limit and not args.positional_args:
74      args.retry_limit = 3
75
76    if args.no_browser:
77      return
78
79    if args.start_xvfb and xvfb.ShouldStartXvfb():
80      cls.xvfb_process = xvfb.StartXvfb()
81    try:
82      possible_browser = browser_finder.FindBrowser(args)
83    except browser_finder_exceptions.BrowserFinderException, ex:
84      parser.error(ex)
85
86    if not possible_browser:
87      parser.error('No browser found of type %s. Cannot run tests.\n'
88                   'Re-run with --browser=list to see '
89                   'available browser types.' % args.browser_type)
90
91  @classmethod
92  def main(cls, args=None, stream=None):  # pylint: disable=arguments-differ
93    # We override the superclass so that we can hook in the 'stream' arg.
94    parser = cls.CreateParser()
95    cls.AddCommandLineArgs(parser, None)
96    options, positional_args = parser.parse_args(args)
97    options.positional_args = positional_args
98
99    try:
100      # Must initialize the DependencyManager before calling
101      # browser_finder.FindBrowser(args)
102      binary_manager.InitDependencyManager(options.client_config)
103      cls.ProcessCommandLineArgs(parser, options, None)
104
105      obj = cls()
106      if stream is not None:
107        obj.stream = stream
108      return obj.Run(options)
109    finally:
110      if cls.xvfb_process:
111        cls.xvfb_process.kill()
112
113  def Run(self, args):
114    runner = typ.Runner()
115    if self.stream:
116      runner.host.stdout = self.stream
117
118    if args.no_browser:
119      possible_browser = None
120      platform = platform_module.GetHostPlatform()
121    else:
122      possible_browser = browser_finder.FindBrowser(args)
123      platform = possible_browser.platform
124
125    # Telemetry seems to overload the system if we run one test per core,
126    # so we scale things back a fair amount. Many of the telemetry tests
127    # are long-running, so there's a limit to how much parallelism we
128    # can effectively use for now anyway.
129    #
130    # It should be possible to handle multiple devices if we adjust the
131    # browser_finder code properly, but for now we only handle one on ChromeOS.
132    if platform.GetOSName() == 'chromeos':
133      runner.args.jobs = 1
134    elif platform.GetOSName() == 'android':
135      android_devs = android_device.FindAllAvailableDevices(args)
136      runner.args.jobs = len(android_devs)
137      if runner.args.jobs == 0:
138        raise RuntimeError("No Android device found")
139      print 'Running tests with %d Android device(s).' % runner.args.jobs
140    elif platform.GetOSVersionName() == 'xp':
141      # For an undiagnosed reason, XP falls over with more parallelism.
142      # See crbug.com/388256
143      runner.args.jobs = max(int(args.jobs) // 4, 1)
144    else:
145      runner.args.jobs = max(int(args.jobs) // 2, 1)
146
147    runner.args.metadata = args.metadata
148    runner.args.passthrough = args.passthrough
149    runner.args.path = args.path
150    runner.args.retry_limit = args.retry_limit
151    runner.args.test_results_server = args.test_results_server
152    runner.args.test_type = args.test_type
153    runner.args.top_level_dir = args.top_level_dir
154    runner.args.write_full_results_to = args.write_full_results_to
155    runner.args.write_trace_to = args.write_trace_to
156    runner.args.list_only = args.list_only
157
158    runner.args.path.append(util.GetUnittestDataDir())
159
160    # Always print out these info for the ease of debugging.
161    runner.args.timing = True
162    runner.args.verbose = 3
163
164    runner.classifier = GetClassifier(args, possible_browser)
165    runner.context = args
166    runner.setup_fn = _SetUpProcess
167    runner.teardown_fn = _TearDownProcess
168    runner.win_multiprocessing = typ.WinMultiprocessing.importable
169    try:
170      ret, _, _ = runner.run()
171    except KeyboardInterrupt:
172      print >> sys.stderr, "interrupted, exiting"
173      ret = 130
174    return ret
175
176
177def GetClassifier(args, possible_browser):
178
179  def ClassifyTestWithoutBrowser(test_set, test):
180    name = test.id()
181    if (not args.positional_args
182        or _MatchesSelectedTest(name, args.positional_args,
183                                  args.exact_test_filter)):
184      # TODO(telemetry-team): Make sure that all telemetry unittest that invokes
185      # actual browser are subclasses of browser_test_case.BrowserTestCase
186      # (crbug.com/537428)
187      if issubclass(test.__class__, browser_test_case.BrowserTestCase):
188        test_set.tests_to_skip.append(typ.TestInput(
189            name, msg='Skip the test because it requires a browser.'))
190      else:
191        test_set.parallel_tests.append(typ.TestInput(name))
192
193  def ClassifyTestWithBrowser(test_set, test):
194    name = test.id()
195    if (not args.positional_args
196        or _MatchesSelectedTest(name, args.positional_args,
197                                args.exact_test_filter)):
198      assert hasattr(test, '_testMethodName')
199      method = getattr(
200          test, test._testMethodName)  # pylint: disable=protected-access
201      should_skip, reason = decorators.ShouldSkip(method, possible_browser)
202      if should_skip and not args.run_disabled_tests:
203        test_set.tests_to_skip.append(typ.TestInput(name, msg=reason))
204      elif decorators.ShouldBeIsolated(method, possible_browser):
205        test_set.isolated_tests.append(typ.TestInput(name))
206      else:
207        test_set.parallel_tests.append(typ.TestInput(name))
208
209  if possible_browser:
210    return ClassifyTestWithBrowser
211  else:
212    return ClassifyTestWithoutBrowser
213
214
215def _MatchesSelectedTest(name, selected_tests, selected_tests_are_exact):
216  if not selected_tests:
217    return False
218  if selected_tests_are_exact:
219    return any(name in selected_tests)
220  else:
221    return any(test in name for test in selected_tests)
222
223
224def _SetUpProcess(child, context): # pylint: disable=unused-argument
225  ps_util.EnableListingStrayProcessesUponExitHook()
226  if binary_manager.NeedsInit():
227    # Typ doesn't keep the DependencyManager initialization in the child
228    # processes.
229    binary_manager.InitDependencyManager(context.client_config)
230  args = context
231  # We need to reset the handlers in case some other parts of telemetry already
232  # set it to make this work.
233  if not args.disable_logging_config:
234    logging.getLogger().handlers = []
235    logging.basicConfig(
236        level=logging.INFO,
237        format='(%(levelname)s) %(asctime)s %(module)s.%(funcName)s:%(lineno)d'
238              '  %(message)s')
239  if args.device and args.device == 'android':
240    android_devices = android_device.FindAllAvailableDevices(args)
241    if not android_devices:
242      raise RuntimeError("No Android device found")
243    android_devices.sort(key=lambda device: device.name)
244    args.device = android_devices[child.worker_num-1].guid
245  options_for_unittests.Push(args)
246
247
248def _TearDownProcess(child, context): # pylint: disable=unused-argument
249  # It's safe to call teardown_browser even if we did not start any browser
250  # in any of the tests.
251  browser_test_case.teardown_browser()
252  options_for_unittests.Pop()
253
254
255if __name__ == '__main__':
256  ret_code = RunTestsCommand.main()
257  sys.exit(ret_code)
258