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

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

Issue 2786773002: (Reland) Add failure screenshots and images to results detail. (Closed)
Patch Set: (Reland) Add failure screenshots and images to results detail. Created 3 years, 8 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 logging 5 import logging
6 import os 6 import os
7 import posixpath 7 import posixpath
8 import re 8 import re
9 import tempfile
9 import time 10 import time
10 11
11 from devil.android import device_errors 12 from devil.android import device_errors
12 from devil.android import flag_changer 13 from devil.android import flag_changer
13 from devil.android.sdk import shared_prefs 14 from devil.android.sdk import shared_prefs
14 from devil.utils import reraiser_thread 15 from devil.utils import reraiser_thread
15 from pylib import valgrind_tools 16 from pylib import valgrind_tools
16 from pylib.android import logdog_logcat_monitor 17 from pylib.android import logdog_logcat_monitor
18 from pylib.constants import host_paths
17 from pylib.base import base_test_result 19 from pylib.base import base_test_result
18 from pylib.instrumentation import instrumentation_test_instance 20 from pylib.instrumentation import instrumentation_test_instance
19 from pylib.local.device import local_device_environment 21 from pylib.local.device import local_device_environment
20 from pylib.local.device import local_device_test_run 22 from pylib.local.device import local_device_test_run
23 from pylib.utils import google_storage_helper
21 from pylib.utils import logdog_helper 24 from pylib.utils import logdog_helper
22 from py_trace_event import trace_event 25 from py_trace_event import trace_event
23 from py_utils import contextlib_ext 26 from py_utils import contextlib_ext
27 from py_utils import tempfile_ext
24 import tombstones 28 import tombstones
25 29
30 try:
31 from PIL import Image # pylint: disable=import-error
32 from PIL import ImageChops # pylint: disable=import-error
33 can_compute_diffs = True
34 except ImportError:
35 can_compute_diffs = False
36
26 _TAG = 'test_runner_py' 37 _TAG = 'test_runner_py'
27 38
28 TIMEOUT_ANNOTATIONS = [ 39 TIMEOUT_ANNOTATIONS = [
29 ('Manual', 10 * 60 * 60), 40 ('Manual', 10 * 60 * 60),
30 ('IntegrationTest', 30 * 60), 41 ('IntegrationTest', 30 * 60),
31 ('External', 10 * 60), 42 ('External', 10 * 60),
32 ('EnormousTest', 10 * 60), 43 ('EnormousTest', 10 * 60),
33 ('LargeTest', 5 * 60), 44 ('LargeTest', 5 * 60),
34 ('MediumTest', 3 * 60), 45 ('MediumTest', 3 * 60),
35 ('SmallTest', 1 * 60), 46 ('SmallTest', 1 * 60),
36 ] 47 ]
37 48
38 LOGCAT_FILTERS = ['*:e', 'chromium:v', 'cr_*:v'] 49 LOGCAT_FILTERS = ['*:e', 'chromium:v', 'cr_*:v']
39 50
51 RE_RENDER_IMAGE_NAME = re.compile(
52 r'(?P<test_class>\w+)\.'
53 r'(?P<description>\w+)\.'
54 r'(?P<device_model>\w+)\.'
55 r'(?P<orientation>port|land)\.png')
56
57 RENDER_TESTS_RESULTS_DIR = {
58 'ChromePublicTest': 'chrome/test/data/android/render_tests'
jbudorick 2017/04/29 01:12:26 Can we have the APK store this information in the
mikecase (-- gone --) 2017/05/03 21:25:14 I think this is a great idea. Unfortunately, I can
59 }
60
40 # TODO(jbudorick): Make this private once the instrumentation test_runner is 61 # TODO(jbudorick): Make this private once the instrumentation test_runner is
41 # deprecated. 62 # deprecated.
42 def DidPackageCrashOnDevice(package_name, device): 63 def DidPackageCrashOnDevice(package_name, device):
43 # Dismiss any error dialogs. Limit the number in case we have an error 64 # Dismiss any error dialogs. Limit the number in case we have an error
44 # loop or we are failing to dismiss. 65 # loop or we are failing to dismiss.
45 try: 66 try:
46 for _ in xrange(10): 67 for _ in xrange(10):
47 package = device.DismissCrashDialogIfNeeded() 68 package = device.DismissCrashDialogIfNeeded()
48 if not package: 69 if not package:
49 return False 70 return False
(...skipping 293 matching lines...) Expand 10 before | Expand all | Expand 10 after
343 # TODO(jbudorick): Make instrumentation tests output a JSON so this 364 # TODO(jbudorick): Make instrumentation tests output a JSON so this
344 # doesn't have to parse the output. 365 # doesn't have to parse the output.
345 result_code, result_bundle, statuses = ( 366 result_code, result_bundle, statuses = (
346 self._test_instance.ParseAmInstrumentRawOutput(output)) 367 self._test_instance.ParseAmInstrumentRawOutput(output))
347 results = self._test_instance.GenerateTestResults( 368 results = self._test_instance.GenerateTestResults(
348 result_code, result_bundle, statuses, start_ms, duration_ms) 369 result_code, result_bundle, statuses, start_ms, duration_ms)
349 for result in results: 370 for result in results:
350 if logcat_url: 371 if logcat_url:
351 result.SetLink('logcat', logcat_url) 372 result.SetLink('logcat', logcat_url)
352 373
374 self._ProcessRenderTestResults(device, results)
375
353 # Update the result name if the test used flags. 376 # Update the result name if the test used flags.
354 if flags: 377 if flags:
355 for r in results: 378 for r in results:
356 if r.GetName() == test_name: 379 if r.GetName() == test_name:
357 r.SetName(test_display_name) 380 r.SetName(test_display_name)
358 381
359 # Add UNKNOWN results for any missing tests. 382 # Add UNKNOWN results for any missing tests.
360 iterable_test = test if isinstance(test, list) else [test] 383 iterable_test = test if isinstance(test, list) else [test]
361 test_names = set(self._GetUniqueTestName(t) for t in iterable_test) 384 test_names = set(self._GetUniqueTestName(t) for t in iterable_test)
362 results_names = set(r.GetName() for r in results) 385 results_names = set(r.GetName() for r in results)
(...skipping 11 matching lines...) Expand all
374 # - optionally taking a screenshot 397 # - optionally taking a screenshot
375 # - logging the raw output at INFO level 398 # - logging the raw output at INFO level
376 # - clearing the application state while persisting permissions 399 # - clearing the application state while persisting permissions
377 if any(r.GetType() not in (base_test_result.ResultType.PASS, 400 if any(r.GetType() not in (base_test_result.ResultType.PASS,
378 base_test_result.ResultType.SKIP) 401 base_test_result.ResultType.SKIP)
379 for r in results): 402 for r in results):
380 if self._test_instance.screenshot_dir: 403 if self._test_instance.screenshot_dir:
381 file_name = '%s-%s.png' % ( 404 file_name = '%s-%s.png' % (
382 test_display_name, 405 test_display_name,
383 time.strftime('%Y%m%dT%H%M%S', time.localtime())) 406 time.strftime('%Y%m%dT%H%M%S', time.localtime()))
384 saved_dir = device.TakeScreenshot( 407 screenshot_file = device.TakeScreenshot(
385 os.path.join(self._test_instance.screenshot_dir, file_name)) 408 os.path.join(self._test_instance.screenshot_dir, file_name))
386 logging.info( 409 logging.info(
387 'Saved screenshot for %s to %s.', 410 'Saved screenshot for %s to %s.',
388 test_display_name, saved_dir) 411 test_display_name, screenshot_file)
412 if self._test_instance.should_save_images:
jbudorick 2017/04/29 01:12:26 We only get here if screenshot_dir is also set. Do
mikecase (-- gone --) 2017/05/03 21:25:14 I had a CL to do that I uploaded after the first t
413 link = google_storage_helper.upload(
414 google_storage_helper.unique_name('screenshot', device=device),
415 screenshot_file,
416 bucket=self._test_instance.gs_results_bucket + '/screenshots')
417 for result in results:
418 result.SetLink('failure_screenshot', link)
419
389 logging.info('detected failure in %s. raw output:', test_display_name) 420 logging.info('detected failure in %s. raw output:', test_display_name)
390 for l in output: 421 for l in output:
391 logging.info(' %s', l) 422 logging.info(' %s', l)
392 if (not self._env.skip_clear_data 423 if (not self._env.skip_clear_data
393 and self._test_instance.package_info): 424 and self._test_instance.package_info):
394 permissions = ( 425 permissions = (
395 self._test_instance.apk_under_test.GetPermissions() 426 self._test_instance.apk_under_test.GetPermissions()
396 if self._test_instance.apk_under_test 427 if self._test_instance.apk_under_test
397 else None) 428 else None)
398 device.ClearApplicationState(self._test_instance.package_info.package, 429 device.ClearApplicationState(self._test_instance.package_info.package,
399 permissions=permissions) 430 permissions=permissions)
400
401 else: 431 else:
402 logging.debug('raw output from %s:', test_display_name) 432 logging.debug('raw output from %s:', test_display_name)
403 for l in output: 433 for l in output:
404 logging.debug(' %s', l) 434 logging.debug(' %s', l)
405 if self._test_instance.coverage_directory: 435 if self._test_instance.coverage_directory:
406 device.PullFile(coverage_directory, 436 device.PullFile(coverage_directory,
407 self._test_instance.coverage_directory) 437 self._test_instance.coverage_directory)
408 device.RunShellCommand( 438 device.RunShellCommand(
409 'rm -f %s' % posixpath.join(coverage_directory, '*'), 439 'rm -f %s' % posixpath.join(coverage_directory, '*'),
410 check_return=True, shell=True) 440 check_return=True, shell=True)
411 if self._test_instance.store_tombstones: 441 if self._test_instance.store_tombstones:
412 tombstones_url = None 442 tombstones_url = None
413 for result in results: 443 for result in results:
414 if result.GetType() == base_test_result.ResultType.CRASH: 444 if result.GetType() == base_test_result.ResultType.CRASH:
415 if not tombstones_url: 445 if not tombstones_url:
416 resolved_tombstones = tombstones.ResolveTombstones( 446 resolved_tombstones = tombstones.ResolveTombstones(
417 device, 447 device,
418 resolve_all_tombstones=True, 448 resolve_all_tombstones=True,
419 include_stack_symbols=False, 449 include_stack_symbols=False,
420 wipe_tombstones=True) 450 wipe_tombstones=True)
421 stream_name = 'tombstones_%s_%s' % ( 451 stream_name = 'tombstones_%s_%s' % (
422 time.strftime('%Y%m%dT%H%M%S', time.localtime()), 452 time.strftime('%Y%m%dT%H%M%S', time.localtime()),
423 device.serial) 453 device.serial)
424 tombstones_url = logdog_helper.text( 454 tombstones_url = logdog_helper.text(
425 stream_name, '\n'.join(resolved_tombstones)) 455 stream_name, '\n'.join(resolved_tombstones))
426 result.SetLink('tombstones', tombstones_url) 456 result.SetLink('tombstones', tombstones_url)
427 return results, None 457 return results, None
428 458
459 def _ProcessRenderTestResults(self, device, results):
460 if not self._test_instance.should_save_images:
461 return
462
463 render_results_dir = RENDER_TESTS_RESULTS_DIR.get(self._test_instance.suite)
464 if not render_results_dir:
465 return
466
467 failure_images_device_dir = posixpath.join(
468 device.GetExternalStoragePath(),
469 'chromium_tests_root', render_results_dir, 'failures')
470 if not device.FileExists(failure_images_device_dir):
471 return
472
473 with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
474 device.PullFile(failure_images_device_dir, temp_dir)
475 device.RemovePath(failure_images_device_dir, recursive=True)
476
477 for failure_filename in os.listdir(os.path.join(temp_dir, 'failures')):
478
479 m = RE_RENDER_IMAGE_NAME.match(failure_filename)
480 if not m:
481 logging.warning('Unexpected file in render test failures: %s',
482 failure_filename)
483 continue
484
485 failure_filepath = os.path.join(temp_dir, 'failures', failure_filename)
486 failure_link = google_storage_helper.upload(
487 google_storage_helper.unique_name(failure_filename, device=device),
488 failure_filepath,
489 bucket=self._test_instance.gs_results_bucket + '/render_tests')
jbudorick 2017/04/29 01:12:26 You've got this in here four times. Should probabl
mikecase (-- gone --) 2017/05/03 21:25:14 Fixed. But going to move render stuff stuff to new
490
491 golden_filepath = os.path.join(
492 host_paths.DIR_SOURCE_ROOT, render_results_dir, failure_filename)
493 if not os.path.exists(golden_filepath):
494 logging.error('Cannot find golden image for %s', failure_filename)
495 continue
496 golden_link = google_storage_helper.upload(
497 google_storage_helper.unique_name(failure_filename, device=device),
498 golden_filepath,
499 bucket=self._test_instance.gs_results_bucket + '/render_tests')
500
501 if can_compute_diffs:
502 diff_filename = '_diff'.join(os.path.splitext(failure_filename))
503 diff_filepath = os.path.join(temp_dir, diff_filename)
504 (ImageChops.difference(
jbudorick 2017/04/29 01:12:26 We're already comparing the images in the APK. Sho
mikecase (-- gone --) 2017/05/03 21:25:14 Changing to Java side diffing. Will make CL a bit
505 Image.open(failure_filepath), Image.open(golden_filepath))
506 .convert('L')
507 .point(lambda i: 255 if i else 0)
508 .save(diff_filepath))
509 diff_link = google_storage_helper.upload(
510 google_storage_helper.unique_name(diff_filename, device=device),
511 diff_filepath,
512 bucket=self._test_instance.gs_results_bucket + '/render_tests')
513 else:
514 diff_link = ''
515 logging.error('Error importing PIL library. Image diffs for '
516 'render test results will not be computed.')
517
518 with tempfile.NamedTemporaryFile(suffix='.html') as temp_html:
519 temp_html.write('''
jbudorick 2017/04/29 01:12:26 nit: pull this into a constant at module scope.
mikecase (-- gone --) 2017/05/03 21:25:14 Done.
520 <html>
521 <table>
522 <tr>
523 <th>Failure</th>
524 <th>Golden</th>
525 <th>Diff</th>
526 </tr>
527 <tr>
528 <td><img src="%s"/></td>
529 <td><img src="%s"/></td>
530 <td><img src="%s"/></td>
531 </tr>
532 </table>
533 </html>
534 ''' % (failure_link, golden_link, diff_link))
535 temp_html.flush()
536 html_results_link = google_storage_helper.upload(
537 google_storage_helper.unique_name('render_html', device=device),
538 temp_html.name,
539 bucket=self._test_instance.gs_results_bucket + '/render_tests',
540 content_type='text/html')
541 for result in results:
542 result.SetLink(failure_filename, html_results_link)
543
429 #override 544 #override
430 def _ShouldRetry(self, test): 545 def _ShouldRetry(self, test):
431 if 'RetryOnFailure' in test.get('annotations', {}): 546 if 'RetryOnFailure' in test.get('annotations', {}):
432 return True 547 return True
433 548
434 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been 549 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been
435 # enabled for a while. See crbug.com/619055 for more details. 550 # enabled for a while. See crbug.com/619055 for more details.
436 logging.error('Default retries are being phased out. crbug.com/619055') 551 logging.error('Default retries are being phased out. crbug.com/619055')
437 return False 552 return False
438 553
(...skipping 15 matching lines...) Expand all
454 if k in annotations: 569 if k in annotations:
455 timeout = v 570 timeout = v
456 break 571 break
457 else: 572 else:
458 logging.warning('Using default 1 minute timeout for %s', test_name) 573 logging.warning('Using default 1 minute timeout for %s', test_name)
459 timeout = 60 574 timeout = 60
460 575
461 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) 576 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations)
462 577
463 return timeout 578 return timeout
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698