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

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: Adds tagging of tests (for replication) 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
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
14 from pylib import android_commands
15 from pylib import cmd_helper
16 from pylib import constants
17 from pylib import ports
18 from pylib.base import base_test_result
19 from pylib.base import shard
20 from pylib.utils import emulator
21 from pylib.utils import report_results
22 from pylib.utils import xvfb
23
24 import gtest_config
25 import test_runner
26
27
28 # TODO(frankf): Add more test targets here after making sure we don't
29 # blow up the dependency size (and the world).
30 _ISOLATE_FILE_PATHS = {
31 'base_unittests': 'base/base_unittests.isolate',
32 'unit_tests': 'chrome/unit_tests.isolate',
33 }
34
35 # Used for filtering large data deps at a finer grain than what's allowed in
36 # isolate files since pushing deps to devices is expensive.
37 # Wildcards are allowed.
38 _DEPS_EXCLUSION_LIST = [
39 'chrome/test/data/extensions/api_test',
40 'chrome/test/data/extensions/secure_shell',
41 'chrome/test/data/firefox*',
42 'chrome/test/data/gpu',
43 'chrome/test/data/image_decoding',
44 'chrome/test/data/import',
45 'chrome/test/data/page_cycler',
46 'chrome/test/data/perf',
47 'chrome/test/data/pyauto_private',
48 'chrome/test/data/safari_import',
49 'chrome/test/data/scroll',
50 'chrome/test/data/third_party',
51 'third_party/hunspell_dictionaries/*.dic',
52 ]
53
54 _ISOLATE_SCRIPT = os.path.join(
55 constants.DIR_SOURCE_ROOT, 'tools', 'swarm_client', 'isolate.py')
56
57
58 def _GenerateDepsDirUsingIsolate(test_suite, build_type):
59 """Generate the dependency dir for the test suite using isolate.
60
61 Args:
62 test_suite: The test suite basename (e.g. base_unittests).
63 build_type: Release/Debug
64
65 Returns:
66 If an isolate file exists, returns path to dependency dir on the host.
67 Otherwise, returns False.
68 """
69 product_dir = os.path.join(cmd_helper.OutDirectory.get(), build_type)
70 assert os.path.isabs(product_dir)
71 isolate_rel_path = _ISOLATE_FILE_PATHS.get(test_suite)
72 if not isolate_rel_path:
73 return False
74
75 isolate_abs_path = os.path.join(constants.DIR_SOURCE_ROOT, isolate_rel_path)
76 isolated_abs_path = os.path.join(
77 product_dir, '%s.isolated' % test_suite)
78 assert os.path.exists(isolate_abs_path)
79 deps_dir = os.path.join(product_dir, 'isolate_deps_dir')
80 if os.path.isdir(deps_dir):
81 shutil.rmtree(deps_dir)
82 isolate_cmd = [
83 'python', _ISOLATE_SCRIPT,
84 'remap',
85 '--isolate', isolate_abs_path,
86 '--isolated', isolated_abs_path,
87 '-V', 'PRODUCT_DIR=%s' % product_dir,
88 '-V', 'OS=android',
89 '--outdir', deps_dir,
90 ]
91 assert not cmd_helper.RunCmd(isolate_cmd)
92
93 # We're relying on the fact that timestamps are preserved
94 # by the remap command (hardlinked). Otherwise, all the data
95 # will be pushed to the device once we move to using time diff
96 # instead of md5sum. Perform a sanity check here.
97 for root, _, filenames in os.walk(deps_dir):
98 if filenames:
99 linked_file = os.path.join(root, filenames[0])
100 orig_file = os.path.join(
101 constants.DIR_SOURCE_ROOT,
102 os.path.relpath(linked_file, deps_dir))
103 if os.stat(linked_file).st_ino == os.stat(orig_file).st_ino:
104 break
105 else:
106 raise Exception('isolate remap command did not use hardlinks.')
107
108 # Delete excluded files as defined by _DEPS_EXCLUSION_LIST.
109 old_cwd = os.getcwd()
110 try:
111 os.chdir(deps_dir)
112 excluded_paths = [x for y in _DEPS_EXCLUSION_LIST for x in glob.glob(y)]
113 if excluded_paths:
114 logging.info('Excluding the following from dependency list: %s',
115 excluded_paths)
116 for p in excluded_paths:
117 if os.path.isdir(p):
118 shutil.rmtree(p)
119 else:
120 os.remove(p)
121 finally:
122 os.chdir(old_cwd)
123
124 # On Android, all pak files need to be in the top-level 'paks' directory.
125 paks_dir = os.path.join(deps_dir, 'paks')
126 os.mkdir(paks_dir)
127 for root, _, filenames in os.walk(os.path.join(deps_dir, 'out')):
128 for filename in fnmatch.filter(filenames, '*.pak'):
129 shutil.move(os.path.join(root, filename), paks_dir)
130
131 # Move everything in PRODUCT_DIR to top level.
132 deps_product_dir = os.path.join(deps_dir, 'out', build_type)
133 if os.path.isdir(deps_product_dir):
134 for p in os.listdir(deps_product_dir):
135 shutil.move(os.path.join(deps_product_dir, p), deps_dir)
136 os.rmdir(deps_product_dir)
137 os.rmdir(os.path.join(deps_dir, 'out'))
138
139 return deps_dir
140
141
142 def _FullyQualifiedTestSuites(exe, option_test_suite, build_type):
143 """Get a list of absolute paths to test suite targets.
144
145 Args:
146 exe: if True, use the executable-based test runner.
147 option_test_suite: the test_suite specified as an option.
148 build_type: 'Release' or 'Debug'.
149
150 Returns:
151 A list of tuples containing the suite and absolute path.
152 Ex. ('content_unittests',
153 '/tmp/chrome/src/out/Debug/content_unittests_apk/'
154 'content_unittests-debug.apk')
155
156 Raises:
157 Exception: If test suite not found.
158 """
159 def GetQualifiedSuite(suite):
160 if suite.is_suite_exe:
161 relpath = suite.name
162 else:
163 # out/(Debug|Release)/$SUITE_apk/$SUITE-debug.apk
164 relpath = os.path.join(suite.name + '_apk', suite.name + '-debug.apk')
165 return suite.name, os.path.join(test_suite_dir, relpath)
166
167 test_suite_dir = os.path.join(cmd_helper.OutDirectory.get(), build_type)
168 if option_test_suite:
169 all_test_suites = [gtest_config.Suite(exe, option_test_suite)]
170 else:
171 all_test_suites = gtest_config.STABLE_TEST_SUITES
172
173 # List of tuples (suite_name, suite_path)
174 qualified_test_suites = map(GetQualifiedSuite, all_test_suites)
175
176 for t, q in qualified_test_suites:
177 if not os.path.exists(q):
178 raise Exception('Test suite %s not found in %s.\n'
179 'Supported test suites:\n %s\n'
180 'Ensure it has been built.\n' %
181 (t, q, [s.name for s in gtest_config.STABLE_TEST_SUITES]))
182 return qualified_test_suites
183
184
185 def GetTestsFromDevice(runner):
186 """Get a list of tests from a device, excluding disabled tests.
187
188 Args:
189 runner: a TestRunner.
190 Returns:
191 All non-disabled tests on the device.
192 """
193 # The executable/apk needs to be copied before we can call GetAllTests.
194 runner.test_package.StripAndCopyExecutable()
195 all_tests = runner.test_package.GetAllTests()
196 # Only includes tests that do not have any match in the disabled list.
197 disabled_list = runner.GetDisabledTests()
198 return filter(lambda t: not any([fnmatch.fnmatch(t, disabled_pattern)
199 for disabled_pattern in disabled_list]),
200 all_tests)
201
202
203 def GetAllEnabledTests(runner_factory, devices):
204 """Get all enabled tests.
205
206 Obtains a list of enabled tests from the test package on the device,
207 then filters it again using the disabled list on the host.
208
209 Args:
210 runner_factory: callable that takes a devices and returns a TestRunner.
211 devices: list of devices.
212
213 Returns:
214 List of all enabled tests.
215
216 Raises:
217 Exception: If no devices available.
218 """
219 for device in devices:
220 try:
221 logging.info('Obtaining tests from %s', device)
222 runner = runner_factory(device, 0)
223 return GetTestsFromDevice(runner)
224 except Exception as e:
225 logging.warning('Failed obtaining tests from %s with exception: %s',
226 device, e)
227 raise Exception('No device available to get the list of tests.')
228
229
230 def _RunATestSuite(options, suite_name):
231 """Run a single test suite.
232
233 Helper for Dispatch() to allow stop/restart of the emulator across
234 test bundles. If using the emulator, we start it on entry and stop
235 it on exit.
236
237 Args:
238 options: options for running the tests.
239 suite_name: name of the test suite being run.
240
241 Returns:
242 A tuple of (base_test_result.TestRunResult object, exit code).
243
244 Raises:
245 Exception: For various reasons including device failure or failing to reset
246 the test server port.
247 """
248 attached_devices = []
249 buildbot_emulators = []
250
251 if options.use_emulator:
252 buildbot_emulators = emulator.LaunchEmulators(options.emulator_count,
253 options.abi,
254 wait_for_boot=True)
255 attached_devices = [e.device for e in buildbot_emulators]
256 elif options.test_device:
257 attached_devices = [options.test_device]
258 else:
259 attached_devices = android_commands.GetAttachedDevices()
260
261 if not attached_devices:
262 raise Exception('A device must be attached and online.')
263
264 # Reset the test port allocation. It's important to do it before starting
265 # to dispatch any tests.
266 if not ports.ResetTestServerPortAllocation():
267 raise Exception('Failed to reset test server port.')
268
269 deps_dir = _GenerateDepsDirUsingIsolate(suite_name, options.build_type)
270
271 # Constructs a new TestRunner with the current options.
272 def RunnerFactory(device, shard_index):
273 return test_runner.TestRunner(
274 device,
275 options.test_suite,
276 options.test_arguments,
277 options.timeout,
278 options.cleanup_test_files,
279 options.tool,
280 options.build_type,
281 options.webkit,
282 options.push_deps,
283 constants.GTEST_TEST_PACKAGE_NAME,
284 constants.GTEST_TEST_ACTIVITY_NAME,
285 constants.GTEST_COMMAND_LINE_FILE,
286 deps_dir=deps_dir)
287
288 # Get tests and split them up based on the number of devices.
289 if options.test_filter:
290 all_tests = [t for t in options.test_filter.split(':') if t]
291 else:
292 all_tests = GetAllEnabledTests(RunnerFactory, attached_devices)
293 num_devices = len(attached_devices)
294 tests = [':'.join(all_tests[i::num_devices]) for i in xrange(num_devices)]
295 tests = [t for t in tests if t]
296
297 # Run tests.
298 test_results, exit_code = shard.ShardAndRunTests(
299 RunnerFactory, attached_devices, tests, options.build_type,
300 test_timeout=None, num_retries=options.num_retries)
301
302 report_results.LogFull(
303 results=test_results,
304 test_type='Unit test',
305 test_package=suite_name,
306 build_type=options.build_type,
307 flakiness_server=options.flakiness_dashboard_server)
308
309 for buildbot_emulator in buildbot_emulators:
310 buildbot_emulator.Shutdown()
311
312 return (test_results, exit_code)
313
314
315 def _ListTestSuites():
316 """Display a list of available test suites."""
317 print 'Available test suites are:'
318 for test_suite in gtest_config.STABLE_TEST_SUITES:
319 print test_suite
320
321
322 def Dispatch(options):
323 """Dispatches the tests, sharding if possible.
324
325 If options.use_emulator is True, all tests will be run in new emulator
326 instance.
327
328 Args:
329 options: options for running the tests.
330
331 Returns:
332 base_test_result.TestRunResults object with the results of running the tests
333 """
334 results = base_test_result.TestRunResults()
335
336 if options.test_suite == 'help':
337 _ListTestSuites()
338 return (results, 0)
339
340 if options.use_xvfb:
341 framebuffer = xvfb.Xvfb()
342 framebuffer.Start()
343
344 all_test_suites = _FullyQualifiedTestSuites(options.exe, options.test_suite,
345 options.build_type)
346 exit_code = 0
347 for suite_name, suite_path in all_test_suites:
348 # Give each test suite its own copy of options.
349 test_options = copy.deepcopy(options)
350 test_options.test_suite = suite_path
351 test_results, test_exit_code = _RunATestSuite(test_options, suite_name)
352 results.AddTestRunResults(test_results)
353 if test_exit_code and exit_code != constants.ERROR_EXIT_CODE:
354 exit_code = test_exit_code
355
356 if options.use_xvfb:
357 framebuffer.Stop()
358
359 return (results, exit_code)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698