summaryrefslogtreecommitdiffstats
path: root/build/android/run_tests.py
blob: c4e183179e90ad19fb6f99ce828f8c2b34c14fb5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/env python
#
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Runs all the native unit tests.

1. Copy over test binary to /data/local on device.
2. Resources: chrome/unit_tests requires resources (chrome.pak and en-US.pak)
   to be deployed to the device. We use the device's $EXTERNAL_STORAGE as the
   base dir (which maps to Context.getExternalFilesDir()).
3. Environment:
3.1. chrome/unit_tests requires (via chrome_paths.cc) a directory named:
     $EXTERNAL_STORAGE + /chrome/test/data
4. Run the binary in the device and stream the log to the host.
4.1. Optionally, filter specific tests.
4.2. If we're running a single test suite and we have multiple devices
     connected, we'll shard the tests.
5. Clean up the device.

Suppressions:

Individual tests in a test binary can be suppressed by listing it in
the gtest_filter directory in a file of the same name as the test binary,
one test per line. Here is an example:

  $ cat gtest_filter/base_unittests_disabled
  DataPackTest.Load
  ReadOnlyFileUtilTest.ContentsEqual

This file is generated by the tests running on devices. If running on emulator,
additonal filter file which lists the tests only failed in emulator will be
loaded. We don't care about the rare testcases which succeeded on emuatlor, but
failed on device.
"""

import copy
import fnmatch
import logging
import optparse
import os
import signal
import subprocess
import sys
import time

from pylib import android_commands
from pylib import buildbot_report
from pylib import cmd_helper
from pylib import ports
from pylib.base_test_sharder import BaseTestSharder
from pylib.gtest import debug_info
from pylib.gtest.single_test_runner import SingleTestRunner
from pylib.utils import emulator
from pylib.utils import run_tests_helper
from pylib.utils import test_options_parser
from pylib.utils import time_profile
from pylib.utils import xvfb


_TEST_SUITES = ['base_unittests',
                'cc_unittests',
                'content_unittests',
                'gpu_unittests',
                'ipc_tests',
                'media_unittests',
                'net_unittests',
                'sql_unittests',
                'sync_unit_tests',
                'ui_unittests',
                'unit_tests',
                'webkit_compositor_bindings_unittests',
                'android_webview_unittests',
               ]


def FullyQualifiedTestSuites(exe, option_test_suite, build_type):
  """Get a list of absolute paths to test suite targets.

  Args:
    exe: if True, use the executable-based test runner.
    option_test_suite: the test_suite specified as an option.
    build_type: 'Release' or 'Debug'.
  """
  test_suite_dir = os.path.join(cmd_helper.OutDirectory.get(), build_type)
  if option_test_suite:
    all_test_suites = [option_test_suite]
  else:
    all_test_suites = _TEST_SUITES

  if exe:
    qualified_test_suites = [os.path.join(test_suite_dir, t)
                             for t in all_test_suites]
  else:
    # out/(Debug|Release)/$SUITE_apk/$SUITE-debug.apk
    qualified_test_suites = [os.path.join(test_suite_dir,
                                          t + '_apk',
                                          t + '-debug.apk')
                             for t in all_test_suites]
  for t, q in zip(all_test_suites, qualified_test_suites):
    if not os.path.exists(q):
      raise Exception('Test suite %s not found in %s.\n'
                      'Supported test suites:\n %s\n'
                      'Ensure it has been built.\n' %
                      (t, q, _TEST_SUITES))
  return qualified_test_suites


class TestSharder(BaseTestSharder):
  """Responsible for sharding the tests on the connected devices."""

  def __init__(self, attached_devices, test_suite, gtest_filter,
               test_arguments, timeout, cleanup_test_files, tool,
               log_dump_name, build_type, in_webkit_checkout,
               flakiness_server=None):
    BaseTestSharder.__init__(self, attached_devices, build_type)
    self.test_suite = test_suite
    self.gtest_filter = gtest_filter or ''
    self.test_arguments = test_arguments
    self.timeout = timeout
    self.cleanup_test_files = cleanup_test_files
    self.tool = tool
    self.log_dump_name = log_dump_name
    self.in_webkit_checkout = in_webkit_checkout
    self.flakiness_server = flakiness_server
    self.all_tests = []
    if not self.gtest_filter:
      # No filter has been specified, let's add all tests then.
      self.all_tests, self.attached_devices = self._GetAllEnabledTests()
    self.tests = self.all_tests

  def _GetAllEnabledTests(self):
    """Get all enabled tests and available devices.

    Obtains a list of enabled tests from the test package on the device,
    then filters it again using the diabled list on the host.

    Returns:
      Tuple of (all enabled tests, available devices).

    Raises Exception if all devices failed.
    """
    # TODO(frankf): This method is doing too much in a non-systematic way.
    # If the intention is to drop flaky devices, why not go through all devices
    # instead of breaking on the first succesfull run?
    available_devices = list(self.attached_devices)
    while available_devices:
      try:
        return (self._GetTestsFromDevice(available_devices[-1]),
                available_devices)
      except Exception as e:
        logging.warning('Failed obtaining tests from %s %s',
                        available_devices[-1], e)
        available_devices.pop()

    raise Exception('No device available to get the list of tests.')

  def _GetTestsFromDevice(self, device):
    logging.info('Obtaining tests from %s', device)
    test_runner = SingleTestRunner(
        device,
        self.test_suite,
        self.gtest_filter,
        self.test_arguments,
        self.timeout,
        self.cleanup_test_files,
        self.tool,
        0,
        not not self.log_dump_name,
        self.build_type,
        self.in_webkit_checkout)
    # The executable/apk needs to be copied before we can call GetAllTests.
    test_runner.test_package.StripAndCopyExecutable()
    all_tests = test_runner.test_package.GetAllTests()
    disabled_list = test_runner.GetDisabledTests()
    # Only includes tests that do not have any match in the disabled list.
    all_tests = filter(lambda t:
                       not any([fnmatch.fnmatch(t, disabled_pattern)
                                for disabled_pattern in disabled_list]),
                       all_tests)
    return all_tests

  def CreateShardedTestRunner(self, device, index):
    """Creates a suite-specific test runner.

    Args:
      device: Device serial where this shard will run.
      index: Index of this device in the pool.

    Returns:
      A SingleTestRunner object.
    """
    device_num = len(self.attached_devices)
    shard_size = (len(self.tests) + device_num - 1) / device_num
    shard_test_list = self.tests[index * shard_size : (index + 1) * shard_size]
    test_filter = ':'.join(shard_test_list) + self.gtest_filter
    return SingleTestRunner(
        device,
        self.test_suite,
        test_filter,
        self.test_arguments,
        self.timeout,
        self.cleanup_test_files, self.tool, index,
        not not self.log_dump_name,
        self.build_type,
        self.in_webkit_checkout)

  def OnTestsCompleted(self, test_runners, test_results):
    """Notifies that we completed the tests."""
    test_results.LogFull(
        test_type='Unit test',
        test_package=test_runners[0].test_package.test_suite_basename,
        build_type=self.build_type,
        all_tests=self.all_tests,
        flakiness_server=self.flakiness_server)
    test_results.PrintAnnotation()

    if self.log_dump_name:
      # Zip all debug info outputs into a file named by log_dump_name.
      debug_info.GTestDebugInfo.ZipAndCleanResults(
          os.path.join(
              cmd_helper.OutDirectory.get(), self.build_type,
              'debug_info_dumps'),
          self.log_dump_name)


def _RunATestSuite(options):
  """Run a single test suite.

  Helper for Dispatch() to allow stop/restart of the emulator across
  test bundles.  If using the emulator, we start it on entry and stop
  it on exit.

  Args:
    options: options for running the tests.

  Returns:
    0 if successful, number of failing tests otherwise.
  """
  step_name = os.path.basename(options.test_suite).replace('-debug.apk', '')
  buildbot_report.PrintNamedStep(step_name)
  attached_devices = []
  buildbot_emulators = []

  if options.use_emulator:
    buildbot_emulators = emulator.LaunchEmulators(options.emulator_count,
                                                  wait_for_boot=True)
    attached_devices = [e.device for e in buildbot_emulators]
  elif options.test_device:
    attached_devices = [options.test_device]
  else:
    attached_devices = android_commands.GetAttachedDevices()

  if not attached_devices:
    logging.critical('A device must be attached and online.')
    buildbot_report.PrintError()
    return 1

  # Reset the test port allocation. It's important to do it before starting
  # to dispatch any tests.
  if not ports.ResetTestServerPortAllocation():
    raise Exception('Failed to reset test server port.')

  if options.gtest_filter:
    logging.warning('Sharding is not possible with these configurations.')
    attached_devices = [attached_devices[0]]

  sharder = TestSharder(
      attached_devices,
      options.test_suite,
      options.gtest_filter,
      options.test_arguments,
      options.timeout,
      options.cleanup_test_files,
      options.tool,
      options.log_dump,
      options.build_type,
      options.webkit,
      options.flakiness_dashboard_server)
  test_results = sharder.RunShardedTests()

  for buildbot_emulator in buildbot_emulators:
    buildbot_emulator.Shutdown()

  return len(test_results.failed)


def Dispatch(options):
  """Dispatches the tests, sharding if possible.

  If options.use_emulator is True, all tests will be run in new emulator
  instance.

  Args:
    options: options for running the tests.

  Returns:
    0 if successful, number of failing tests otherwise.
  """
  if options.test_suite == 'help':
    ListTestSuites()
    return 0

  if options.use_xvfb:
    framebuffer = xvfb.Xvfb()
    framebuffer.Start()

  all_test_suites = FullyQualifiedTestSuites(options.exe, options.test_suite,
                                             options.build_type)
  failures = 0
  for suite in all_test_suites:
    # Give each test suite its own copy of options.
    test_options = copy.deepcopy(options)
    test_options.test_suite = suite
    failures += _RunATestSuite(test_options)

  if options.use_xvfb:
    framebuffer.Stop()
  return failures


def ListTestSuites():
  """Display a list of available test suites."""
  print 'Available test suites are:'
  for test_suite in _TEST_SUITES:
    print test_suite


def main(argv):
  option_parser = optparse.OptionParser()
  test_options_parser.AddGTestOptions(option_parser)
  options, args = option_parser.parse_args(argv)

  if len(args) > 1:
    option_parser.error('Unknown argument: %s' % args[1:])

  run_tests_helper.SetLogLevel(options.verbose_count)

  if options.out_directory:
    cmd_helper.OutDirectory.set(options.out_directory)

  if options.use_emulator:
    emulator.DeleteAllTempAVDs()

  failed_tests_count = Dispatch(options)

  # Failures of individual test suites are communicated by printing a
  # STEP_FAILURE message.
  # Returning a success exit status also prevents the buildbot from incorrectly
  # marking the last suite as failed if there were failures in other suites in
  # the batch (this happens because the exit status is a sum of all failures
  # from all suites, but the buildbot associates the exit status only with the
  # most recent step).
  if options.exit_code:
    return failed_tests_count
  return 0


if __name__ == '__main__':
  sys.exit(main(sys.argv))