Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(365)

Side by Side Diff: build/android/pylib/gtest/dispatch.py

Issue 18770008: [Android] Redesigns the sharder to allow replicated vs distributed tests (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Re-adds -f short form to gtest_filter switch Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « build/android/pylib/browsertests/setup.py ('k') | build/android/pylib/gtest/setup.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 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 """Dispatches GTests."""
6
7 import copy
8 import fnmatch
9 import glob
10 import logging
11 import os
12 import shutil
13 import sys
14
15 from pylib import android_commands
16 from pylib import cmd_helper
17 from pylib import constants
18 from pylib import ports
19 from pylib.base import base_test_result
20 from pylib.base import shard
21 from pylib.utils import emulator
22 from pylib.utils import report_results
23 from pylib.utils import xvfb
24
25 import gtest_config
26 import test_runner
27
28 sys.path.insert(0,
29 os.path.join(constants.DIR_SOURCE_ROOT, 'build', 'util', 'lib'))
30 from common import unittest_util
31
32
33 _ISOLATE_FILE_PATHS = {
34 'base_unittests': 'base/base_unittests.isolate',
35 'breakpad_unittests': 'breakpad/breakpad_unittests.isolate',
36 'cc_perftests': 'cc/cc_perftests.isolate',
37 'components_unittests': 'components/components_unittests.isolate',
38 'content_browsertests': 'content/content_browsertests.isolate',
39 'content_unittests': 'content/content_unittests.isolate',
40 'media_unittests': 'media/media_unittests.isolate',
41 'modules_unittests': 'third_party/webrtc/modules/modules_unittests.isolate',
42 'net_unittests': 'net/net_unittests.isolate',
43 'ui_unittests': 'ui/ui_unittests.isolate',
44 'unit_tests': 'chrome/unit_tests.isolate',
45 }
46
47 # Used for filtering large data deps at a finer grain than what's allowed in
48 # isolate files since pushing deps to devices is expensive.
49 # Wildcards are allowed.
50 _DEPS_EXCLUSION_LIST = [
51 'chrome/test/data/extensions/api_test',
52 'chrome/test/data/extensions/secure_shell',
53 'chrome/test/data/firefox*',
54 'chrome/test/data/gpu',
55 'chrome/test/data/image_decoding',
56 'chrome/test/data/import',
57 'chrome/test/data/page_cycler',
58 'chrome/test/data/perf',
59 'chrome/test/data/pyauto_private',
60 'chrome/test/data/safari_import',
61 'chrome/test/data/scroll',
62 'chrome/test/data/third_party',
63 'third_party/hunspell_dictionaries/*.dic',
64 # crbug.com/258690
65 'webkit/data/bmp_decoder',
66 'webkit/data/ico_decoder',
67 ]
68
69 _ISOLATE_SCRIPT = os.path.join(
70 constants.DIR_SOURCE_ROOT, 'tools', 'swarm_client', 'isolate.py')
71
72
73 def _GenerateDepsDirUsingIsolate(test_suite, build_type):
74 """Generate the dependency dir for the test suite using isolate.
75
76 Args:
77 test_suite: Name of the test suite (e.g. base_unittests).
78 build_type: Release/Debug
79 """
80 product_dir = os.path.join(cmd_helper.OutDirectory.get(), build_type)
81 assert os.path.isabs(product_dir)
82
83 if os.path.isdir(constants.ISOLATE_DEPS_DIR):
84 shutil.rmtree(constants.ISOLATE_DEPS_DIR)
85
86 isolate_rel_path = _ISOLATE_FILE_PATHS.get(test_suite)
87 if not isolate_rel_path:
88 logging.info('Did not find an isolate file for the test suite.')
89 return
90
91 isolate_abs_path = os.path.join(constants.DIR_SOURCE_ROOT, isolate_rel_path)
92 isolated_abs_path = os.path.join(
93 product_dir, '%s.isolated' % test_suite)
94 assert os.path.exists(isolate_abs_path)
95 isolate_cmd = [
96 'python', _ISOLATE_SCRIPT,
97 'remap',
98 '--isolate', isolate_abs_path,
99 '--isolated', isolated_abs_path,
100 '-V', 'PRODUCT_DIR=%s' % product_dir,
101 '-V', 'OS=android',
102 '--outdir', constants.ISOLATE_DEPS_DIR,
103 ]
104 assert not cmd_helper.RunCmd(isolate_cmd)
105
106 # We're relying on the fact that timestamps are preserved
107 # by the remap command (hardlinked). Otherwise, all the data
108 # will be pushed to the device once we move to using time diff
109 # instead of md5sum. Perform a sanity check here.
110 for root, _, filenames in os.walk(constants.ISOLATE_DEPS_DIR):
111 if filenames:
112 linked_file = os.path.join(root, filenames[0])
113 orig_file = os.path.join(
114 constants.DIR_SOURCE_ROOT,
115 os.path.relpath(linked_file, constants.ISOLATE_DEPS_DIR))
116 if os.stat(linked_file).st_ino == os.stat(orig_file).st_ino:
117 break
118 else:
119 raise Exception('isolate remap command did not use hardlinks.')
120
121 # Delete excluded files as defined by _DEPS_EXCLUSION_LIST.
122 old_cwd = os.getcwd()
123 try:
124 os.chdir(constants.ISOLATE_DEPS_DIR)
125 excluded_paths = [x for y in _DEPS_EXCLUSION_LIST for x in glob.glob(y)]
126 if excluded_paths:
127 logging.info('Excluding the following from dependency list: %s',
128 excluded_paths)
129 for p in excluded_paths:
130 if os.path.isdir(p):
131 shutil.rmtree(p)
132 else:
133 os.remove(p)
134 finally:
135 os.chdir(old_cwd)
136
137 # On Android, all pak files need to be in the top-level 'paks' directory.
138 paks_dir = os.path.join(constants.ISOLATE_DEPS_DIR, 'paks')
139 os.mkdir(paks_dir)
140 for root, _, filenames in os.walk(os.path.join(constants.ISOLATE_DEPS_DIR,
141 'out')):
142 for filename in fnmatch.filter(filenames, '*.pak'):
143 shutil.move(os.path.join(root, filename), paks_dir)
144
145 # Move everything in PRODUCT_DIR to top level.
146 deps_product_dir = os.path.join(constants.ISOLATE_DEPS_DIR, 'out', build_type)
147 if os.path.isdir(deps_product_dir):
148 for p in os.listdir(deps_product_dir):
149 shutil.move(os.path.join(deps_product_dir, p), constants.ISOLATE_DEPS_DIR)
150 os.rmdir(deps_product_dir)
151 os.rmdir(os.path.join(constants.ISOLATE_DEPS_DIR, 'out'))
152
153
154 def _FullyQualifiedTestSuites(exe, option_test_suite, build_type):
155 """Get a list of absolute paths to test suite targets.
156
157 Args:
158 exe: if True, use the executable-based test runner.
159 option_test_suite: the test_suite specified as an option.
160 build_type: 'Release' or 'Debug'.
161
162 Returns:
163 A list of tuples containing the suite and absolute path.
164 Ex. ('content_unittests',
165 '/tmp/chrome/src/out/Debug/content_unittests_apk/'
166 'content_unittests-debug.apk')
167
168 Raises:
169 Exception: If test suite not found.
170 """
171 def GetQualifiedSuite(suite):
172 if suite.is_suite_exe:
173 relpath = suite.name
174 else:
175 # out/(Debug|Release)/$SUITE_apk/$SUITE-debug.apk
176 relpath = os.path.join(suite.name + '_apk', suite.name + '-debug.apk')
177 return suite.name, os.path.join(test_suite_dir, relpath)
178
179 test_suite_dir = os.path.join(cmd_helper.OutDirectory.get(), build_type)
180 if option_test_suite:
181 all_test_suites = [gtest_config.Suite(exe, option_test_suite)]
182 else:
183 all_test_suites = gtest_config.STABLE_TEST_SUITES
184
185 # List of tuples (suite_name, suite_path)
186 qualified_test_suites = map(GetQualifiedSuite, all_test_suites)
187
188 for t, q in qualified_test_suites:
189 if not os.path.exists(q):
190 raise Exception('Test suite %s not found in %s.\n'
191 'Supported test suites:\n %s\n'
192 'Ensure it has been built.\n' %
193 (t, q, [s.name for s in gtest_config.STABLE_TEST_SUITES]))
194 return qualified_test_suites
195
196
197 def _GetDisabledTestsFilterFromFile(test_suite):
198 """Returns a gtest filter based on the *_disabled file.
199
200 Args:
201 test_suite: Name of the test suite (e.g. base_unittests).
202
203 Returns:
204 A gtest filter which excludes disabled tests.
205 Example: '*-StackTrace.*:StringPrintfTest.StringPrintfMisc'
206 """
207 filter_file_path = os.path.join(
208 os.path.abspath(os.path.dirname(__file__)),
209 'filter', '%s_disabled' % test_suite)
210
211 if not filter_file_path or not os.path.exists(filter_file_path):
212 logging.info('No filter file found at %s', filter_file_path)
213 return '*'
214
215 filters = [x for x in [x.strip() for x in file(filter_file_path).readlines()]
216 if x and x[0] != '#']
217 disabled_filter = '*-%s' % ':'.join(filters)
218 logging.info('Applying filter "%s" obtained from %s',
219 disabled_filter, filter_file_path)
220 return disabled_filter
221
222
223 def _GetTestsFromDevice(runner_factory, devices):
224 """Get a list of tests from a device.
225
226 Args:
227 runner_factory: callable that takes a device and index and returns a
228 TestRunner object.
229 devices: List of devices.
230
231 Returns:
232 All the tests in the test suite.
233 """
234 for device in devices:
235 try:
236 logging.info('Obtaining tests from %s', device)
237 runner = runner_factory(device, 0)
238 runner.test_package.Install()
239 return runner.test_package.GetAllTests()
240 except (android_commands.errors.WaitForResponseTimedOutError,
241 android_commands.errors.DeviceUnresponsiveError), e:
242 logging.warning('Failed obtaining tests from %s with exception: %s',
243 device, e)
244 raise Exception('No device available to get the list of tests.')
245
246
247 def _FilterTestsUsingPrefixes(all_tests, pre=False, manual=False):
248 """Removes tests with disabled prefixes.
249
250 Args:
251 all_tests: List of tests to filter.
252 pre: If True, include tests with _PRE prefix.
253 manual: If True, include tests with _MANUAL prefix.
254
255 Returns:
256 List of tests remaining.
257 """
258 filtered_tests = []
259 filter_prefixes = ['DISABLED_', 'FLAKY_', 'FAILS_']
260
261 if not pre:
262 filter_prefixes.append('PRE_')
263
264 if not manual:
265 filter_prefixes.append('MANUAL_')
266
267 for t in all_tests:
268 test_case, test = t.split('.', 1)
269 if not any([test_case.startswith(prefix) or test.startswith(prefix) for
270 prefix in filter_prefixes]):
271 filtered_tests.append(t)
272 return filtered_tests
273
274
275 def GetTestsFiltered(test_suite, gtest_filter, runner_factory, devices):
276 """Get all tests in the suite and filter them.
277
278 Obtains a list of tests from the test package on the device, and
279 applies the following filters in order:
280 1. Remove tests with disabled prefixes.
281 2. Remove tests specified in the *_disabled files in the 'filter' dir
282 3. Applies |gtest_filter|.
283
284 Args:
285 test_suite: Name of the test suite (e.g. base_unittests).
286 gtest_filter: A filter including negative and/or positive patterns.
287 runner_factory: callable that takes a device and index and returns a
288 TestRunner object.
289 devices: List of devices.
290
291 Returns:
292 List of tests remaining.
293 """
294 tests = _GetTestsFromDevice(runner_factory, devices)
295 tests = _FilterTestsUsingPrefixes(
296 tests, bool(gtest_filter), bool(gtest_filter))
297 tests = unittest_util.FilterTestNames(
298 tests, _GetDisabledTestsFilterFromFile(test_suite))
299
300 if gtest_filter:
301 tests = unittest_util.FilterTestNames(tests, gtest_filter)
302
303 return tests
304
305
306 def _RunATestSuite(options, suite_name):
307 """Run a single test suite.
308
309 Helper for Dispatch() to allow stop/restart of the emulator across
310 test bundles. If using the emulator, we start it on entry and stop
311 it on exit.
312
313 Args:
314 options: options for running the tests.
315 suite_name: name of the test suite being run.
316
317 Returns:
318 A tuple of (base_test_result.TestRunResult object, exit code).
319
320 Raises:
321 Exception: For various reasons including device failure or failing to reset
322 the test server port.
323 """
324 attached_devices = []
325 buildbot_emulators = []
326
327 if options.use_emulator:
328 buildbot_emulators = emulator.LaunchEmulators(options.emulator_count,
329 options.abi,
330 wait_for_boot=True)
331 attached_devices = [e.device for e in buildbot_emulators]
332 elif options.test_device:
333 attached_devices = [options.test_device]
334 else:
335 attached_devices = android_commands.GetAttachedDevices()
336
337 if not attached_devices:
338 raise Exception('A device must be attached and online.')
339
340 # Reset the test port allocation. It's important to do it before starting
341 # to dispatch any tests.
342 if not ports.ResetTestServerPortAllocation():
343 raise Exception('Failed to reset test server port.')
344
345 _GenerateDepsDirUsingIsolate(suite_name, options.build_type)
346
347 # Constructs a new TestRunner with the current options.
348 def RunnerFactory(device, shard_index):
349 return test_runner.TestRunner(
350 device,
351 options.test_suite,
352 options.test_arguments,
353 options.timeout,
354 options.cleanup_test_files,
355 options.tool,
356 options.build_type,
357 options.webkit,
358 options.push_deps,
359 constants.GTEST_TEST_PACKAGE_NAME,
360 constants.GTEST_TEST_ACTIVITY_NAME,
361 constants.GTEST_COMMAND_LINE_FILE)
362
363 # Get tests and split them up based on the number of devices.
364 tests = GetTestsFiltered(suite_name, options.test_filter,
365 RunnerFactory, attached_devices)
366 num_devices = len(attached_devices)
367 tests = [':'.join(tests[i::num_devices]) for i in xrange(num_devices)]
368 tests = [t for t in tests if t]
369
370 # Run tests.
371 test_results, exit_code = shard.ShardAndRunTests(
372 RunnerFactory, attached_devices, tests, options.build_type,
373 test_timeout=None, num_retries=options.num_retries)
374
375 report_results.LogFull(
376 results=test_results,
377 test_type='Unit test',
378 test_package=suite_name,
379 build_type=options.build_type,
380 flakiness_server=options.flakiness_dashboard_server)
381
382 if os.path.isdir(constants.ISOLATE_DEPS_DIR):
383 shutil.rmtree(constants.ISOLATE_DEPS_DIR)
384
385 for buildbot_emulator in buildbot_emulators:
386 buildbot_emulator.Shutdown()
387
388 return (test_results, exit_code)
389
390
391 def _ListTestSuites():
392 """Display a list of available test suites."""
393 print 'Available test suites are:'
394 for test_suite in gtest_config.STABLE_TEST_SUITES:
395 print test_suite
396
397
398 def Dispatch(options):
399 """Dispatches the tests, sharding if possible.
400
401 If options.use_emulator is True, all tests will be run in new emulator
402 instance.
403
404 Args:
405 options: options for running the tests.
406
407 Returns:
408 base_test_result.TestRunResults object with the results of running the tests
409 """
410 results = base_test_result.TestRunResults()
411
412 if options.test_suite == 'help':
413 _ListTestSuites()
414 return (results, 0)
415
416 if options.use_xvfb:
417 framebuffer = xvfb.Xvfb()
418 framebuffer.Start()
419
420 all_test_suites = _FullyQualifiedTestSuites(options.exe, options.test_suite,
421 options.build_type)
422 exit_code = 0
423 for suite_name, suite_path in all_test_suites:
424 # Give each test suite its own copy of options.
425 test_options = copy.deepcopy(options)
426 test_options.test_suite = suite_path
427 test_results, test_exit_code = _RunATestSuite(test_options, suite_name)
428 results.AddTestRunResults(test_results)
429 if test_exit_code and exit_code != constants.ERROR_EXIT_CODE:
430 exit_code = test_exit_code
431
432 if options.use_xvfb:
433 framebuffer.Stop()
434
435 return (results, exit_code)
OLDNEW
« no previous file with comments | « build/android/pylib/browsertests/setup.py ('k') | build/android/pylib/gtest/setup.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698