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

Side by Side Diff: build/android/pylib/local/device/local_device_instrumentation_test_run.py

Issue 2933993002: Add local results details pages.
Patch Set: Add --local-output arg which enables local results detail pages. Created 3 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
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 contextlib 5 import contextlib
6 import logging 6 import logging
7 import os 7 import os
8 import posixpath 8 import posixpath
9 import re 9 import re
10 import sys 10 import sys
11 import tempfile 11 import tempfile
12 import time 12 import time
13 13
14 from devil.android import crash_handler 14 from devil.android import crash_handler
15 from devil.android import device_errors 15 from devil.android import device_errors
16 from devil.android import device_temp_file 16 from devil.android import device_temp_file
17 from devil.android import flag_changer 17 from devil.android import flag_changer
18 from devil.android import logcat_monitor
18 from devil.android.tools import system_app 19 from devil.android.tools import system_app
19 from devil.utils import reraiser_thread 20 from devil.utils import reraiser_thread
20 from pylib import valgrind_tools 21 from pylib import valgrind_tools
21 from pylib.android import logdog_logcat_monitor
22 from pylib.base import base_test_result 22 from pylib.base import base_test_result
23 from pylib.base import output_manager
23 from pylib.constants import host_paths 24 from pylib.constants import host_paths
24 from pylib.instrumentation import instrumentation_test_instance 25 from pylib.instrumentation import instrumentation_test_instance
25 from pylib.local.device import local_device_environment 26 from pylib.local.device import local_device_environment
26 from pylib.local.device import local_device_test_run 27 from pylib.local.device import local_device_test_run
27 from pylib.utils import google_storage_helper 28
28 from pylib.utils import logdog_helper
29 from pylib.utils import shared_preference_utils 29 from pylib.utils import shared_preference_utils
30
30 from py_trace_event import trace_event 31 from py_trace_event import trace_event
31 from py_utils import contextlib_ext 32 from py_utils import contextlib_ext
32 from py_utils import tempfile_ext
33 import tombstones 33 import tombstones
34 34
35 with host_paths.SysPath( 35 with host_paths.SysPath(
36 os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party'), 0): 36 os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party'), 0):
37 import jinja2 # pylint: disable=import-error 37 import jinja2 # pylint: disable=import-error
38 import markupsafe # pylint: disable=import-error,unused-import 38 import markupsafe # pylint: disable=import-error,unused-import
39 39
40 40
41 _JINJA_TEMPLATE_DIR = os.path.join( 41 _JINJA_TEMPLATE_DIR = os.path.join(
42 host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'pylib', 'instrumentation') 42 host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'pylib', 'instrumentation')
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after
105 return False 105 return False
106 106
107 107
108 _CURRENT_FOCUS_CRASH_RE = re.compile( 108 _CURRENT_FOCUS_CRASH_RE = re.compile(
109 r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}') 109 r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}')
110 110
111 111
112 class LocalDeviceInstrumentationTestRun( 112 class LocalDeviceInstrumentationTestRun(
113 local_device_test_run.LocalDeviceTestRun): 113 local_device_test_run.LocalDeviceTestRun):
114 def __init__(self, env, test_instance): 114 def __init__(self, env, test_instance):
115 super(LocalDeviceInstrumentationTestRun, self).__init__(env, test_instance) 115 super(LocalDeviceInstrumentationTestRun, self).__init__(
116 env, test_instance)
116 self._flag_changers = {} 117 self._flag_changers = {}
117 self._ui_capture_dir = dict() 118 self._ui_capture_dir = dict()
118 self._replace_package_contextmanager = None 119 self._replace_package_contextmanager = None
119 120
120 #override 121 #override
121 def TestPackage(self): 122 def TestPackage(self):
122 return self._test_instance.suite 123 return self._test_instance.suite
123 124
124 #override 125 #override
125 def SetUp(self): 126 def SetUp(self):
(...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after
325 coverage_basename = '%s.ec' % ('%s_group' % test[0]['method'] 326 coverage_basename = '%s.ec' % ('%s_group' % test[0]['method']
326 if isinstance(test, list) else test['method']) 327 if isinstance(test, list) else test['method'])
327 extras['coverage'] = 'true' 328 extras['coverage'] = 'true'
328 coverage_directory = os.path.join( 329 coverage_directory = os.path.join(
329 device.GetExternalStoragePath(), 'chrome', 'test', 'coverage') 330 device.GetExternalStoragePath(), 'chrome', 'test', 'coverage')
330 coverage_device_file = os.path.join( 331 coverage_device_file = os.path.join(
331 coverage_directory, coverage_basename) 332 coverage_directory, coverage_basename)
332 extras['coverageFile'] = coverage_device_file 333 extras['coverageFile'] = coverage_device_file
333 # Save screenshot if screenshot dir is specified (save locally) or if 334 # Save screenshot if screenshot dir is specified (save locally) or if
334 # a GS bucket is passed (save in cloud). 335 # a GS bucket is passed (save in cloud).
335 screenshot_device_file = None 336 screenshot_device_file = device_temp_file.DeviceTempFile(
336 if (self._test_instance.screenshot_dir or 337 device.adb, suffix='.png', dir=device.GetExternalStoragePath())
337 self._test_instance.gs_results_bucket): 338 extras[EXTRA_SCREENSHOT_FILE] = screenshot_device_file.name
338 screenshot_device_file = device_temp_file.DeviceTempFile(
339 device.adb, suffix='.png', dir=device.GetExternalStoragePath())
340 extras[EXTRA_SCREENSHOT_FILE] = screenshot_device_file.name
341 339
342 extras[EXTRA_UI_CAPTURE_DIR] = self._ui_capture_dir[device] 340 extras[EXTRA_UI_CAPTURE_DIR] = self._ui_capture_dir[device]
343 341
344 if isinstance(test, list): 342 if isinstance(test, list):
345 if not self._test_instance.driver_apk: 343 if not self._test_instance.driver_apk:
346 raise Exception('driver_apk does not exist. ' 344 raise Exception('driver_apk does not exist. '
347 'Please build it and try again.') 345 'Please build it and try again.')
348 if any(t.get('is_junit4') for t in test): 346 if any(t.get('is_junit4') for t in test):
349 raise Exception('driver apk does not support JUnit4 tests') 347 raise Exception('driver apk does not support JUnit4 tests')
350 348
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
403 self._flag_changers[str(device)].PushFlags( 401 self._flag_changers[str(device)].PushFlags(
404 add=flags_to_add, remove=flags_to_remove) 402 add=flags_to_add, remove=flags_to_remove)
405 403
406 time_ms = lambda: int(time.time() * 1e3) 404 time_ms = lambda: int(time.time() * 1e3)
407 start_ms = time_ms() 405 start_ms = time_ms()
408 406
409 stream_name = 'logcat_%s_%s_%s' % ( 407 stream_name = 'logcat_%s_%s_%s' % (
410 test_name.replace('#', '.'), 408 test_name.replace('#', '.'),
411 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), 409 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()),
412 device.serial) 410 device.serial)
413 logmon = logdog_logcat_monitor.LogdogLogcatMonitor(
414 device.adb, stream_name, filter_specs=LOGCAT_FILTERS)
415 411
416 with contextlib_ext.Optional( 412 try:
jbudorick 2017/07/19 22:45:09 It would be nice if we could make ArchiveAndDelete
mikecase (-- gone --) 2017/07/26 21:21:37 This is what I wanted, unfortunatly it didnt work
417 logmon, self._test_instance.should_save_logcat): 413 logcat_file = tempfile.NamedTemporaryFile(delete=False)
418 with _LogTestEndpoints(device, test_name): 414 with logcat_monitor.LogcatMonitor(
419 with contextlib_ext.Optional( 415 device.adb, filter_specs=LOGCAT_FILTERS,
420 trace_event.trace(test_name), 416 output_file=logcat_file.name) as logmon:
421 self._env.trace_output): 417 with _LogTestEndpoints(device, test_name):
422 output = device.StartInstrumentation( 418 with contextlib_ext.Optional(
423 target, raw=True, extras=extras, timeout=timeout, retries=0) 419 trace_event.trace(test_name),
420 self._env.trace_output):
421 output = device.StartInstrumentation(
422 target, raw=True, extras=extras, timeout=timeout, retries=0)
423 logmon.Close()
424 finally:
425 logcat_url = self._env.output_manager.ArchiveAndDeleteFile(
426 logcat_file.name, stream_name, 'logcat')
424 427
425 logcat_url = logmon.GetLogcatURL()
426 duration_ms = time_ms() - start_ms 428 duration_ms = time_ms() - start_ms
427 if flags_to_add or flags_to_remove: 429 if flags_to_add or flags_to_remove:
428 self._flag_changers[str(device)].Restore() 430 self._flag_changers[str(device)].Restore()
429 if test_timeout_scale: 431 if test_timeout_scale:
430 valgrind_tools.SetChromeTimeoutScale( 432 valgrind_tools.SetChromeTimeoutScale(
431 device, self._test_instance.timeout_scale) 433 device, self._test_instance.timeout_scale)
432 434
433 # TODO(jbudorick): Make instrumentation tests output a JSON so this 435 # TODO(jbudorick): Make instrumentation tests output a JSON so this
434 # doesn't have to parse the output. 436 # doesn't have to parse the output.
435 result_code, result_bundle, statuses = ( 437 result_code, result_bundle, statuses = (
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
471 if r.GetType() == base_test_result.ResultType.UNKNOWN: 473 if r.GetType() == base_test_result.ResultType.UNKNOWN:
472 r.SetType(base_test_result.ResultType.CRASH) 474 r.SetType(base_test_result.ResultType.CRASH)
473 475
474 # Handle failures by: 476 # Handle failures by:
475 # - optionally taking a screenshot 477 # - optionally taking a screenshot
476 # - logging the raw output at INFO level 478 # - logging the raw output at INFO level
477 # - clearing the application state while persisting permissions 479 # - clearing the application state while persisting permissions
478 if any(r.GetType() not in (base_test_result.ResultType.PASS, 480 if any(r.GetType() not in (base_test_result.ResultType.PASS,
479 base_test_result.ResultType.SKIP) 481 base_test_result.ResultType.SKIP)
480 for r in results): 482 for r in results):
481 with contextlib_ext.Optional( 483 self._SaveScreenshot(device, screenshot_device_file, test_display_name,
482 tempfile_ext.NamedTemporaryDirectory(), 484 results)
483 self._test_instance.screenshot_dir is None and
484 self._test_instance.gs_results_bucket) as screenshot_host_dir:
485 screenshot_host_dir = (
486 self._test_instance.screenshot_dir or screenshot_host_dir)
487 self._SaveScreenshot(device, screenshot_host_dir,
488 screenshot_device_file, test_display_name,
489 results)
490 485
491 logging.info('detected failure in %s. raw output:', test_display_name) 486 logging.info('detected failure in %s. raw output:', test_display_name)
492 for l in output: 487 for l in output:
493 logging.info(' %s', l) 488 logging.info(' %s', l)
494 if (not self._env.skip_clear_data 489 if (not self._env.skip_clear_data
495 and self._test_instance.package_info): 490 and self._test_instance.package_info):
496 permissions = ( 491 permissions = (
497 self._test_instance.apk_under_test.GetPermissions() 492 self._test_instance.apk_under_test.GetPermissions()
498 if self._test_instance.apk_under_test 493 if self._test_instance.apk_under_test
499 else None) 494 else None)
(...skipping 12 matching lines...) Expand all
512 if self._test_instance.store_tombstones: 507 if self._test_instance.store_tombstones:
513 tombstones_url = None 508 tombstones_url = None
514 for result in results: 509 for result in results:
515 if result.GetType() == base_test_result.ResultType.CRASH: 510 if result.GetType() == base_test_result.ResultType.CRASH:
516 if not tombstones_url: 511 if not tombstones_url:
517 resolved_tombstones = tombstones.ResolveTombstones( 512 resolved_tombstones = tombstones.ResolveTombstones(
518 device, 513 device,
519 resolve_all_tombstones=True, 514 resolve_all_tombstones=True,
520 include_stack_symbols=False, 515 include_stack_symbols=False,
521 wipe_tombstones=True) 516 wipe_tombstones=True)
522 stream_name = 'tombstones_%s_%s' % ( 517 try:
523 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), 518 tombstone_file = tempfile.NamedTemporaryFile(delete=False)
524 device.serial) 519 tombstone_filename = 'tombstones_%s_%s' % (
525 tombstones_url = logdog_helper.text( 520 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()),
526 stream_name, '\n'.join(resolved_tombstones)) 521 device.serial)
527 result.SetLink('tombstones', tombstones_url) 522 tombstone_file.write('\n'.join(resolved_tombstones))
523 tombstone_file.flush()
524 finally:
525 tombstones_url = self._env.output_manager.ArchiveAndDeleteFile(
526 tombstone_file.name, tombstone_filename, 'tombstones')
527 result.SetLink('tombstones', tombstones_url)
528 return results, None 528 return results, None
529 529
530 def _SaveScreenshot(self, device, screenshot_host_dir, screenshot_device_file, 530 def _SaveScreenshot(self, device, screenshot_device_file, test_name, results):
531 test_name, results): 531 screenshot_filename = '%s-%s.png' % (
532 if screenshot_host_dir: 532 test_name, time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()))
533 screenshot_host_file = os.path.join(
534 screenshot_host_dir,
535 '%s-%s.png' % (
536 test_name,
537 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime())))
538 if device.FileExists(screenshot_device_file.name): 533 if device.FileExists(screenshot_device_file.name):
539 try: 534 try:
540 device.PullFile(screenshot_device_file.name, screenshot_host_file) 535 screenshot_host_file = tempfile.NamedTemporaryFile(delete=False)
536 try:
537 device.PullFile(screenshot_device_file.name,
538 screenshot_host_file.name)
539 finally:
540 screenshot_device_file.close()
541 screenshot_host_file.flush()
541 finally: 542 finally:
542 screenshot_device_file.close() 543 screenshot_url = self._env.output_manager.ArchiveAndDeleteFile(
543 544 screenshot_host_file.name, screenshot_filename,
544 logging.info( 545 'screenshot', output_manager.Datatype.IMAGE)
545 'Saved screenshot for %s to %s.',
546 test_name, screenshot_host_file)
547 if self._test_instance.gs_results_bucket:
548 link = google_storage_helper.upload(
549 google_storage_helper.unique_name(
550 'screenshot', device=device),
551 screenshot_host_file,
552 bucket=('%s/screenshots' %
553 self._test_instance.gs_results_bucket))
554 for result in results: 546 for result in results:
555 result.SetLink('post_test_screenshot', link) 547 result.SetLink('post_test_screenshot', screenshot_url)
556 548
557 def _ProcessRenderTestResults( 549 def _ProcessRenderTestResults(
558 self, device, render_tests_device_output_dir, results): 550 self, device, render_tests_device_output_dir, results):
559 # If GS results bucket is specified, will archive render result images.
560 # If render image dir is specified, will pull the render result image from
561 # the device and leave in the directory.
562 if not (bool(self._test_instance.gs_results_bucket) or
563 bool(self._test_instance.render_results_dir)):
564 return
565 551
566 failure_images_device_dir = posixpath.join( 552 failure_images_device_dir = posixpath.join(
567 render_tests_device_output_dir, 'failures') 553 render_tests_device_output_dir, 'failures')
568 if not device.FileExists(failure_images_device_dir): 554 if not device.FileExists(failure_images_device_dir):
569 return 555 return
570 556
571 diff_images_device_dir = posixpath.join( 557 diff_images_device_dir = posixpath.join(
572 render_tests_device_output_dir, 'diffs') 558 render_tests_device_output_dir, 'diffs')
573 559
574 golden_images_device_dir = posixpath.join( 560 golden_images_device_dir = posixpath.join(
575 render_tests_device_output_dir, 'goldens') 561 render_tests_device_output_dir, 'goldens')
576 562
577 with contextlib_ext.Optional( 563 for failure_filename in device.ListDirectory(failure_images_device_dir):
578 tempfile_ext.NamedTemporaryDirectory(),
579 not bool(self._test_instance.render_results_dir)) as render_temp_dir:
580 render_host_dir = (
581 self._test_instance.render_results_dir or render_temp_dir)
582 564
583 if not os.path.exists(render_host_dir): 565 try:
584 os.makedirs(render_host_dir) 566 failure_image_host_file = tempfile.NamedTemporaryFile(delete=False)
567 device.PullFile(
568 posixpath.join(failure_images_device_dir, failure_filename),
569 failure_image_host_file)
570 failure_image_host_file.flush()
571 finally:
572 failure_link = self._env.output_manager.ArchiveAndDeleteFile(
573 failure_image_host_file.name, 'fail_%s' % failure_filename,
574 'render_tests', output_manager.Datatype.IMAGE)
585 575
586 # Pull all render test results from device. 576 if device.PathExists(
587 device.PullFile(failure_images_device_dir, render_host_dir) 577 posixpath.join(golden_images_device_dir, failure_filename)):
588 578 try:
589 if device.FileExists(diff_images_device_dir): 579 golden_image_host_file = tempfile.NamedTemporaryFile(delete=False)
590 device.PullFile(diff_images_device_dir, render_host_dir) 580 device.PullFile(
591 else: 581 posixpath.join(golden_images_device_dir, failure_filename),
592 logging.error('Diff images not found on device.') 582 golden_image_host_file)
593 583 golden_image_host_file.flush()
594 if device.FileExists(golden_images_device_dir): 584 finally:
595 device.PullFile(golden_images_device_dir, render_host_dir) 585 golden_link = self._env.output_manager.ArchiveAndDeleteFile(
596 else: 586 golden_image_host_file.name, 'golden_%s' % failure_filename,
597 logging.error('Golden images not found on device.') 587 'render_tests', output_manager.Datatype.IMAGE)
598
599 # Upload results to Google Storage.
600 if self._test_instance.gs_results_bucket:
601 self._UploadRenderTestResults(render_host_dir, results)
602
603 def _UploadRenderTestResults(self, render_host_dir, results):
604 render_tests_bucket = (
605 self._test_instance.gs_results_bucket + '/render_tests')
606
607 for failure_filename in os.listdir(
608 os.path.join(render_host_dir, 'failures')):
609 m = RE_RENDER_IMAGE_NAME.match(failure_filename)
610 if not m:
611 logging.warning('Unexpected file in render test failures: %s',
612 failure_filename)
613 continue
614
615 failure_filepath = os.path.join(
616 render_host_dir, 'failures', failure_filename)
617 failure_link = google_storage_helper.upload_content_addressed(
618 failure_filepath, bucket=render_tests_bucket)
619
620 golden_filepath = os.path.join(
621 render_host_dir, 'goldens', failure_filename)
622 if os.path.exists(golden_filepath):
623 golden_link = google_storage_helper.upload_content_addressed(
624 golden_filepath, bucket=render_tests_bucket)
625 else: 588 else:
626 golden_link = '' 589 golden_link = ''
627 590
628 diff_filepath = os.path.join( 591 if device.PathExists(
629 render_host_dir, 'diffs', failure_filename) 592 posixpath.join(diff_images_device_dir, failure_filename)):
630 if os.path.exists(diff_filepath): 593 try:
631 diff_link = google_storage_helper.upload_content_addressed( 594 diff_image_host_file = tempfile.NamedTemporaryFile(delete=False)
632 diff_filepath, bucket=render_tests_bucket) 595 device.PullFile(
596 posixpath.join(diff_images_device_dir, failure_filename),
597 diff_image_host_file)
598 diff_image_host_file.flush()
599 finally:
600 diff_link = self._env.output_manager.ArchiveAndDeleteFile(
601 diff_image_host_file.name, 'diff_%s' % failure_filename,
602 'render_tests', output_manager.Datatype.IMAGE)
633 else: 603 else:
634 diff_link = '' 604 diff_link = ''
635 605
636 with tempfile.NamedTemporaryFile(suffix='.html') as temp_html: 606 jinja2_env = jinja2.Environment(
637 jinja2_env = jinja2.Environment( 607 loader=jinja2.FileSystemLoader(_JINJA_TEMPLATE_DIR),
638 loader=jinja2.FileSystemLoader(_JINJA_TEMPLATE_DIR), 608 trim_blocks=True)
639 trim_blocks=True) 609 template = jinja2_env.get_template(_JINJA_TEMPLATE_FILENAME)
640 template = jinja2_env.get_template(_JINJA_TEMPLATE_FILENAME) 610 # pylint: disable=no-member
641 # pylint: disable=no-member 611 processed_template_output = template.render(
642 processed_template_output = template.render( 612 test_name=failure_filename,
643 test_name=failure_filename, 613 failure_link=failure_link,
644 failure_link=failure_link, 614 golden_link=golden_link,
645 golden_link=golden_link, 615 diff_link=diff_link)
646 diff_link=diff_link)
647 616
648 temp_html.write(processed_template_output) 617 try:
649 temp_html.flush() 618 html_results = tempfile.NamedTemporaryFile(delete=False)
650 html_results_link = google_storage_helper.upload_content_addressed( 619 html_results.write(processed_template_output)
651 temp_html.name, 620 html_results.flush()
652 bucket=render_tests_bucket, 621 finally:
653 content_type='text/html') 622 html_results_link = self._env.output_manager.ArchiveAndDeleteFile(
623 html_results.name,
624 '%s.html' % failure_filename, 'render_tests',
625 output_manager.Datatype.HTML)
654 for result in results: 626 for result in results:
655 result.SetLink(failure_filename, html_results_link) 627 result.SetLink(failure_filename, html_results_link)
656 628
657 #override 629 #override
658 def _ShouldRetry(self, test): 630 def _ShouldRetry(self, test):
659 if 'RetryOnFailure' in test.get('annotations', {}): 631 if 'RetryOnFailure' in test.get('annotations', {}):
660 return True 632 return True
661 633
662 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been 634 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been
663 # enabled for a while. See crbug.com/619055 for more details. 635 # enabled for a while. See crbug.com/619055 for more details.
(...skipping 25 matching lines...) Expand all
689 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) 661 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations)
690 662
691 return timeout 663 return timeout
692 664
693 def _IsRenderTest(test): 665 def _IsRenderTest(test):
694 """Determines if a test or list of tests has a RenderTest amongst them.""" 666 """Determines if a test or list of tests has a RenderTest amongst them."""
695 if not isinstance(test, list): 667 if not isinstance(test, list):
696 test = [test] 668 test = [test]
697 return any([RENDER_TEST_FEATURE_ANNOTATION in t['annotations'].get( 669 return any([RENDER_TEST_FEATURE_ANNOTATION in t['annotations'].get(
698 FEATURE_ANNOTATION, {}).get('value', ()) for t in test]) 670 FEATURE_ANNOTATION, {}).get('value', ()) for t in test])
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698