OLD | NEW |
| (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) | |
OLD | NEW |