OLD | NEW |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import logging | 5 import logging |
6 import os | 6 import os |
7 import posixpath | 7 import posixpath |
8 import re | 8 import re |
| 9 import sys |
| 10 import tempfile |
9 import time | 11 import time |
10 | 12 |
11 from devil.android import device_errors | 13 from devil.android import device_errors |
12 from devil.android import device_temp_file | 14 from devil.android import device_temp_file |
13 from devil.android import flag_changer | 15 from devil.android import flag_changer |
14 from devil.android.sdk import shared_prefs | 16 from devil.android.sdk import shared_prefs |
15 from devil.utils import reraiser_thread | 17 from devil.utils import reraiser_thread |
16 from pylib import valgrind_tools | 18 from pylib import valgrind_tools |
17 from pylib.android import logdog_logcat_monitor | 19 from pylib.android import logdog_logcat_monitor |
18 from pylib.base import base_test_result | 20 from pylib.base import base_test_result |
| 21 from pylib.constants import host_paths |
19 from pylib.instrumentation import instrumentation_test_instance | 22 from pylib.instrumentation import instrumentation_test_instance |
20 from pylib.local.device import local_device_environment | 23 from pylib.local.device import local_device_environment |
21 from pylib.local.device import local_device_test_run | 24 from pylib.local.device import local_device_test_run |
22 from pylib.utils import google_storage_helper | 25 from pylib.utils import google_storage_helper |
23 from pylib.utils import logdog_helper | 26 from pylib.utils import logdog_helper |
24 from py_trace_event import trace_event | 27 from py_trace_event import trace_event |
25 from py_utils import contextlib_ext | 28 from py_utils import contextlib_ext |
26 from py_utils import tempfile_ext | 29 from py_utils import tempfile_ext |
27 import tombstones | 30 import tombstones |
28 | 31 |
| 32 sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party')) |
| 33 import jinja2 # pylint: disable=import-error |
| 34 import markupsafe # pylint: disable=import-error,unused-import |
| 35 |
| 36 |
| 37 _JINJA_TEMPLATE_DIR = os.path.join( |
| 38 host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'pylib', 'instrumentation') |
| 39 _JINJA_TEMPLATE_FILENAME = 'render_test.html.jinja' |
| 40 |
29 _TAG = 'test_runner_py' | 41 _TAG = 'test_runner_py' |
30 | 42 |
31 TIMEOUT_ANNOTATIONS = [ | 43 TIMEOUT_ANNOTATIONS = [ |
32 ('Manual', 10 * 60 * 60), | 44 ('Manual', 10 * 60 * 60), |
33 ('IntegrationTest', 30 * 60), | 45 ('IntegrationTest', 30 * 60), |
34 ('External', 10 * 60), | 46 ('External', 10 * 60), |
35 ('EnormousTest', 10 * 60), | 47 ('EnormousTest', 10 * 60), |
36 ('LargeTest', 5 * 60), | 48 ('LargeTest', 5 * 60), |
37 ('MediumTest', 3 * 60), | 49 ('MediumTest', 3 * 60), |
38 ('SmallTest', 1 * 60), | 50 ('SmallTest', 1 * 60), |
39 ] | 51 ] |
40 | 52 |
41 LOGCAT_FILTERS = ['*:e', 'chromium:v', 'cr_*:v'] | 53 LOGCAT_FILTERS = ['*:e', 'chromium:v', 'cr_*:v'] |
42 | 54 |
43 EXTRA_SCREENSHOT_FILE = ( | 55 EXTRA_SCREENSHOT_FILE = ( |
44 'org.chromium.base.test.ScreenshotOnFailureStatement.ScreenshotFile') | 56 'org.chromium.base.test.ScreenshotOnFailureStatement.ScreenshotFile') |
45 | 57 |
| 58 FEATURE_ANNOTATION = 'Feature' |
| 59 RENDER_TEST_FEATURE_ANNOTATION = 'RenderTest' |
| 60 |
| 61 # This needs to be kept in sync with formatting in |RenderUtils.imageName| |
| 62 RE_RENDER_IMAGE_NAME = re.compile( |
| 63 r'(?P<test_class>\w+)\.' |
| 64 r'(?P<description>\w+)\.' |
| 65 r'(?P<device_model>\w+)\.' |
| 66 r'(?P<orientation>port|land)\.png') |
| 67 |
46 # TODO(jbudorick): Make this private once the instrumentation test_runner is | 68 # TODO(jbudorick): Make this private once the instrumentation test_runner is |
47 # deprecated. | 69 # deprecated. |
48 def DidPackageCrashOnDevice(package_name, device): | 70 def DidPackageCrashOnDevice(package_name, device): |
49 # Dismiss any error dialogs. Limit the number in case we have an error | 71 # Dismiss any error dialogs. Limit the number in case we have an error |
50 # loop or we are failing to dismiss. | 72 # loop or we are failing to dismiss. |
51 try: | 73 try: |
52 for _ in xrange(10): | 74 for _ in xrange(10): |
53 package = device.DismissCrashDialogIfNeeded() | 75 package = device.DismissCrashDialogIfNeeded() |
54 if not package: | 76 if not package: |
55 return False | 77 return False |
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
242 return tests | 264 return tests |
243 | 265 |
244 #override | 266 #override |
245 def _GetUniqueTestName(self, test): | 267 def _GetUniqueTestName(self, test): |
246 return instrumentation_test_instance.GetUniqueTestName(test) | 268 return instrumentation_test_instance.GetUniqueTestName(test) |
247 | 269 |
248 #override | 270 #override |
249 def _RunTest(self, device, test): | 271 def _RunTest(self, device, test): |
250 extras = {} | 272 extras = {} |
251 | 273 |
252 flags = None | 274 flags_to_add = [] |
| 275 flags_to_remove = [] |
253 test_timeout_scale = None | 276 test_timeout_scale = None |
254 if self._test_instance.coverage_directory: | 277 if self._test_instance.coverage_directory: |
255 coverage_basename = '%s.ec' % ('%s_group' % test[0]['method'] | 278 coverage_basename = '%s.ec' % ('%s_group' % test[0]['method'] |
256 if isinstance(test, list) else test['method']) | 279 if isinstance(test, list) else test['method']) |
257 extras['coverage'] = 'true' | 280 extras['coverage'] = 'true' |
258 coverage_directory = os.path.join( | 281 coverage_directory = os.path.join( |
259 device.GetExternalStoragePath(), 'chrome', 'test', 'coverage') | 282 device.GetExternalStoragePath(), 'chrome', 'test', 'coverage') |
260 coverage_device_file = os.path.join( | 283 coverage_device_file = os.path.join( |
261 coverage_directory, coverage_basename) | 284 coverage_directory, coverage_basename) |
262 extras['coverageFile'] = coverage_device_file | 285 extras['coverageFile'] = coverage_device_file |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
297 test_display_name = self._GetUniqueTestName(test) | 320 test_display_name = self._GetUniqueTestName(test) |
298 if test['is_junit4']: | 321 if test['is_junit4']: |
299 target = '%s/%s' % ( | 322 target = '%s/%s' % ( |
300 self._test_instance.test_package, | 323 self._test_instance.test_package, |
301 self._test_instance.test_runner_junit4) | 324 self._test_instance.test_runner_junit4) |
302 else: | 325 else: |
303 target = '%s/%s' % ( | 326 target = '%s/%s' % ( |
304 self._test_instance.test_package, self._test_instance.test_runner) | 327 self._test_instance.test_package, self._test_instance.test_runner) |
305 extras['class'] = test_name | 328 extras['class'] = test_name |
306 if 'flags' in test: | 329 if 'flags' in test: |
307 flags = test['flags'] | 330 flags_to_add.extend(test['flags'].add) |
| 331 flags_to_remove.extend(test['flags'].remove) |
308 timeout = self._GetTimeoutFromAnnotations( | 332 timeout = self._GetTimeoutFromAnnotations( |
309 test['annotations'], test_display_name) | 333 test['annotations'], test_display_name) |
310 | 334 |
311 test_timeout_scale = self._GetTimeoutScaleFromAnnotations( | 335 test_timeout_scale = self._GetTimeoutScaleFromAnnotations( |
312 test['annotations']) | 336 test['annotations']) |
313 if test_timeout_scale and test_timeout_scale != 1: | 337 if test_timeout_scale and test_timeout_scale != 1: |
314 valgrind_tools.SetChromeTimeoutScale( | 338 valgrind_tools.SetChromeTimeoutScale( |
315 device, test_timeout_scale * self._test_instance.timeout_scale) | 339 device, test_timeout_scale * self._test_instance.timeout_scale) |
316 | 340 |
317 logging.info('preparing to run %s: %s', test_display_name, test) | 341 logging.info('preparing to run %s: %s', test_display_name, test) |
318 | 342 |
319 if flags: | 343 render_tests_device_output_dir = None |
| 344 if _IsRenderTest(test): |
| 345 # TODO(mikecase): Add DeviceTempDirectory class and use that instead. |
| 346 render_tests_device_output_dir = posixpath.join( |
| 347 device.GetExternalStoragePath(), |
| 348 'render_test_output_dir') |
| 349 flags_to_add.append('--render-test-output-dir=%s' % |
| 350 render_tests_device_output_dir) |
| 351 |
| 352 if flags_to_add or flags_to_remove: |
320 self._CreateFlagChangerIfNeeded(device) | 353 self._CreateFlagChangerIfNeeded(device) |
321 self._flag_changers[str(device)].PushFlags( | 354 self._flag_changers[str(device)].PushFlags( |
322 add=flags.add, remove=flags.remove) | 355 add=flags_to_add, remove=flags_to_remove) |
323 | 356 |
324 try: | 357 try: |
325 device.RunShellCommand( | 358 device.RunShellCommand( |
326 ['log', '-p', 'i', '-t', _TAG, 'START %s' % test_name], | 359 ['log', '-p', 'i', '-t', _TAG, 'START %s' % test_name], |
327 check_return=True) | 360 check_return=True) |
328 time_ms = lambda: int(time.time() * 1e3) | 361 time_ms = lambda: int(time.time() * 1e3) |
329 start_ms = time_ms() | 362 start_ms = time_ms() |
330 | 363 |
331 stream_name = 'logcat_%s_%s_%s' % ( | 364 stream_name = 'logcat_%s_%s_%s' % ( |
332 test_name.replace('#', '.'), | 365 test_name.replace('#', '.'), |
333 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), | 366 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), |
334 device.serial) | 367 device.serial) |
335 logmon = logdog_logcat_monitor.LogdogLogcatMonitor( | 368 logmon = logdog_logcat_monitor.LogdogLogcatMonitor( |
336 device.adb, stream_name, filter_specs=LOGCAT_FILTERS) | 369 device.adb, stream_name, filter_specs=LOGCAT_FILTERS) |
337 | 370 |
338 with contextlib_ext.Optional( | 371 with contextlib_ext.Optional( |
339 logmon, self._test_instance.should_save_logcat): | 372 logmon, self._test_instance.should_save_logcat): |
340 with contextlib_ext.Optional( | 373 with contextlib_ext.Optional( |
341 trace_event.trace(test_name), | 374 trace_event.trace(test_name), |
342 self._env.trace_output): | 375 self._env.trace_output): |
343 output = device.StartInstrumentation( | 376 output = device.StartInstrumentation( |
344 target, raw=True, extras=extras, timeout=timeout, retries=0) | 377 target, raw=True, extras=extras, timeout=timeout, retries=0) |
345 logcat_url = logmon.GetLogcatURL() | 378 logcat_url = logmon.GetLogcatURL() |
346 finally: | 379 finally: |
347 device.RunShellCommand( | 380 device.RunShellCommand( |
348 ['log', '-p', 'i', '-t', _TAG, 'END %s' % test_name], | 381 ['log', '-p', 'i', '-t', _TAG, 'END %s' % test_name], |
349 check_return=True) | 382 check_return=True) |
350 duration_ms = time_ms() - start_ms | 383 duration_ms = time_ms() - start_ms |
351 if flags: | 384 if flags_to_add or flags_to_remove: |
352 self._flag_changers[str(device)].Restore() | 385 self._flag_changers[str(device)].Restore() |
353 if test_timeout_scale: | 386 if test_timeout_scale: |
354 valgrind_tools.SetChromeTimeoutScale( | 387 valgrind_tools.SetChromeTimeoutScale( |
355 device, self._test_instance.timeout_scale) | 388 device, self._test_instance.timeout_scale) |
356 | 389 |
357 # TODO(jbudorick): Make instrumentation tests output a JSON so this | 390 # TODO(jbudorick): Make instrumentation tests output a JSON so this |
358 # doesn't have to parse the output. | 391 # doesn't have to parse the output. |
359 result_code, result_bundle, statuses = ( | 392 result_code, result_bundle, statuses = ( |
360 self._test_instance.ParseAmInstrumentRawOutput(output)) | 393 self._test_instance.ParseAmInstrumentRawOutput(output)) |
361 results = self._test_instance.GenerateTestResults( | 394 results = self._test_instance.GenerateTestResults( |
362 result_code, result_bundle, statuses, start_ms, duration_ms) | 395 result_code, result_bundle, statuses, start_ms, duration_ms) |
363 for result in results: | 396 for result in results: |
364 if logcat_url: | 397 if logcat_url: |
365 result.SetLink('logcat', logcat_url) | 398 result.SetLink('logcat', logcat_url) |
366 | 399 |
| 400 if _IsRenderTest(test): |
| 401 # Render tests do not cause test failure by default. So we have to check |
| 402 # to see if any failure images were generated even if the test does not |
| 403 # fail. |
| 404 try: |
| 405 self._ProcessRenderTestResults( |
| 406 device, render_tests_device_output_dir, results) |
| 407 finally: |
| 408 device.RemovePath(render_tests_device_output_dir, |
| 409 recursive=True, force=True) |
| 410 |
367 # Update the result name if the test used flags. | 411 # Update the result name if the test used flags. |
368 if flags: | 412 if flags_to_add or flags_to_remove: |
369 for r in results: | 413 for r in results: |
370 if r.GetName() == test_name: | 414 if r.GetName() == test_name: |
371 r.SetName(test_display_name) | 415 r.SetName(test_display_name) |
372 | 416 |
373 # Add UNKNOWN results for any missing tests. | 417 # Add UNKNOWN results for any missing tests. |
374 iterable_test = test if isinstance(test, list) else [test] | 418 iterable_test = test if isinstance(test, list) else [test] |
375 test_names = set(self._GetUniqueTestName(t) for t in iterable_test) | 419 test_names = set(self._GetUniqueTestName(t) for t in iterable_test) |
376 results_names = set(r.GetName() for r in results) | 420 results_names = set(r.GetName() for r in results) |
377 results.extend( | 421 results.extend( |
378 base_test_result.BaseTestResult(u, base_test_result.ResultType.UNKNOWN) | 422 base_test_result.BaseTestResult(u, base_test_result.ResultType.UNKNOWN) |
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
460 if self._test_instance.gs_results_bucket: | 504 if self._test_instance.gs_results_bucket: |
461 link = google_storage_helper.upload( | 505 link = google_storage_helper.upload( |
462 google_storage_helper.unique_name( | 506 google_storage_helper.unique_name( |
463 'screenshot', device=device), | 507 'screenshot', device=device), |
464 screenshot_host_file, | 508 screenshot_host_file, |
465 bucket=('%s/screenshots' % | 509 bucket=('%s/screenshots' % |
466 self._test_instance.gs_results_bucket)) | 510 self._test_instance.gs_results_bucket)) |
467 for result in results: | 511 for result in results: |
468 result.SetLink('post_test_screenshot', link) | 512 result.SetLink('post_test_screenshot', link) |
469 | 513 |
| 514 def _ProcessRenderTestResults( |
| 515 self, device, render_tests_device_output_dir, results): |
| 516 # Will archive test images if we are given a GS bucket to store the results |
| 517 # in and are given a results file to output the links to. |
| 518 if not bool(self._test_instance.gs_results_bucket): |
| 519 return |
| 520 |
| 521 failure_images_device_dir = posixpath.join( |
| 522 render_tests_device_output_dir, 'failures') |
| 523 |
| 524 if not device.FileExists(failure_images_device_dir): |
| 525 return |
| 526 |
| 527 render_tests_bucket = ( |
| 528 self._test_instance.gs_results_bucket + '/render_tests') |
| 529 |
| 530 diff_images_device_dir = posixpath.join( |
| 531 render_tests_device_output_dir, 'diffs') |
| 532 |
| 533 golden_images_device_dir = posixpath.join( |
| 534 render_tests_device_output_dir, 'goldens') |
| 535 |
| 536 with tempfile_ext.NamedTemporaryDirectory() as temp_dir: |
| 537 device.PullFile(failure_images_device_dir, temp_dir) |
| 538 |
| 539 if device.FileExists(diff_images_device_dir): |
| 540 device.PullFile(diff_images_device_dir, temp_dir) |
| 541 else: |
| 542 logging.error('Diff images not found on device.') |
| 543 |
| 544 if device.FileExists(golden_images_device_dir): |
| 545 device.PullFile(golden_images_device_dir, temp_dir) |
| 546 else: |
| 547 logging.error('Golden images not found on device.') |
| 548 |
| 549 for failure_filename in os.listdir(os.path.join(temp_dir, 'failures')): |
| 550 |
| 551 m = RE_RENDER_IMAGE_NAME.match(failure_filename) |
| 552 if not m: |
| 553 logging.warning('Unexpected file in render test failures: %s', |
| 554 failure_filename) |
| 555 continue |
| 556 |
| 557 failure_filepath = os.path.join(temp_dir, 'failures', failure_filename) |
| 558 failure_link = google_storage_helper.upload( |
| 559 google_storage_helper.unique_name( |
| 560 'failure_%s' % failure_filename, device=device), |
| 561 failure_filepath, |
| 562 bucket=render_tests_bucket) |
| 563 |
| 564 golden_filepath = os.path.join(temp_dir, 'goldens', failure_filename) |
| 565 if os.path.exists(golden_filepath): |
| 566 golden_link = google_storage_helper.upload( |
| 567 google_storage_helper.unique_name( |
| 568 'golden_%s' % failure_filename, device=device), |
| 569 golden_filepath, |
| 570 bucket=render_tests_bucket) |
| 571 else: |
| 572 golden_link = '' |
| 573 |
| 574 diff_filepath = os.path.join(temp_dir, 'diffs', failure_filename) |
| 575 if os.path.exists(diff_filepath): |
| 576 diff_link = google_storage_helper.upload( |
| 577 google_storage_helper.unique_name( |
| 578 'diff_%s' % failure_filename, device=device), |
| 579 diff_filepath, |
| 580 bucket=render_tests_bucket) |
| 581 else: |
| 582 diff_link = '' |
| 583 |
| 584 with tempfile.NamedTemporaryFile(suffix='.html') as temp_html: |
| 585 jinja2_env = jinja2.Environment( |
| 586 loader=jinja2.FileSystemLoader(_JINJA_TEMPLATE_DIR), |
| 587 trim_blocks=True) |
| 588 template = jinja2_env.get_template(_JINJA_TEMPLATE_FILENAME) |
| 589 # pylint: disable=no-member |
| 590 processed_template_output = template.render( |
| 591 failure_link=failure_link, |
| 592 golden_link=golden_link, |
| 593 diff_link=diff_link) |
| 594 |
| 595 temp_html.write(processed_template_output) |
| 596 temp_html.flush() |
| 597 html_results_link = google_storage_helper.upload( |
| 598 google_storage_helper.unique_name('render_html', device=device), |
| 599 temp_html.name, |
| 600 bucket=render_tests_bucket, |
| 601 content_type='text/html') |
| 602 for result in results: |
| 603 result.SetLink(failure_filename, html_results_link) |
| 604 |
470 #override | 605 #override |
471 def _ShouldRetry(self, test): | 606 def _ShouldRetry(self, test): |
472 if 'RetryOnFailure' in test.get('annotations', {}): | 607 if 'RetryOnFailure' in test.get('annotations', {}): |
473 return True | 608 return True |
474 | 609 |
475 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been | 610 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been |
476 # enabled for a while. See crbug.com/619055 for more details. | 611 # enabled for a while. See crbug.com/619055 for more details. |
477 logging.error('Default retries are being phased out. crbug.com/619055') | 612 logging.error('Default retries are being phased out. crbug.com/619055') |
478 return False | 613 return False |
479 | 614 |
(...skipping 15 matching lines...) Expand all Loading... |
495 if k in annotations: | 630 if k in annotations: |
496 timeout = v | 631 timeout = v |
497 break | 632 break |
498 else: | 633 else: |
499 logging.warning('Using default 1 minute timeout for %s', test_name) | 634 logging.warning('Using default 1 minute timeout for %s', test_name) |
500 timeout = 60 | 635 timeout = 60 |
501 | 636 |
502 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) | 637 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) |
503 | 638 |
504 return timeout | 639 return timeout |
| 640 |
| 641 def _IsRenderTest(test): |
| 642 """Determines if a test or list of tests has a RenderTest amongst them.""" |
| 643 if not isinstance(test, list): |
| 644 test = [test] |
| 645 return any([RENDER_TEST_FEATURE_ANNOTATION in t['annotations'].get( |
| 646 FEATURE_ANNOTATION, ()) for t in test]) |
OLD | NEW |