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

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
jbudorick 2017/04/11 00:36:44 I've usually seen (and written) this kind of thing
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'
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 270 matching lines...) Expand 10 before | Expand all | Expand 10 after
320 # TODO(jbudorick): Make instrumentation tests output a JSON so this 341 # TODO(jbudorick): Make instrumentation tests output a JSON so this
321 # doesn't have to parse the output. 342 # doesn't have to parse the output.
322 result_code, result_bundle, statuses = ( 343 result_code, result_bundle, statuses = (
323 self._test_instance.ParseAmInstrumentRawOutput(output)) 344 self._test_instance.ParseAmInstrumentRawOutput(output))
324 results = self._test_instance.GenerateTestResults( 345 results = self._test_instance.GenerateTestResults(
325 result_code, result_bundle, statuses, start_ms, duration_ms) 346 result_code, result_bundle, statuses, start_ms, duration_ms)
326 for result in results: 347 for result in results:
327 if logcat_url: 348 if logcat_url:
328 result.SetLink('logcat', logcat_url) 349 result.SetLink('logcat', logcat_url)
329 350
351 self._ProcessRenderTestResults(device, results)
352
330 # Update the result name if the test used flags. 353 # Update the result name if the test used flags.
331 if flags: 354 if flags:
332 for r in results: 355 for r in results:
333 if r.GetName() == test_name: 356 if r.GetName() == test_name:
334 r.SetName(test_display_name) 357 r.SetName(test_display_name)
335 358
336 # Add UNKNOWN results for any missing tests. 359 # Add UNKNOWN results for any missing tests.
337 iterable_test = test if isinstance(test, list) else [test] 360 iterable_test = test if isinstance(test, list) else [test]
338 test_names = set(self._GetUniqueTestName(t) for t in iterable_test) 361 test_names = set(self._GetUniqueTestName(t) for t in iterable_test)
339 results_names = set(r.GetName() for r in results) 362 results_names = set(r.GetName() for r in results)
(...skipping 11 matching lines...) Expand all
351 # - optionally taking a screenshot 374 # - optionally taking a screenshot
352 # - logging the raw output at INFO level 375 # - logging the raw output at INFO level
353 # - clearing the application state while persisting permissions 376 # - clearing the application state while persisting permissions
354 if any(r.GetType() not in (base_test_result.ResultType.PASS, 377 if any(r.GetType() not in (base_test_result.ResultType.PASS,
355 base_test_result.ResultType.SKIP) 378 base_test_result.ResultType.SKIP)
356 for r in results): 379 for r in results):
357 if self._test_instance.screenshot_dir: 380 if self._test_instance.screenshot_dir:
358 file_name = '%s-%s.png' % ( 381 file_name = '%s-%s.png' % (
359 test_display_name, 382 test_display_name,
360 time.strftime('%Y%m%dT%H%M%S', time.localtime())) 383 time.strftime('%Y%m%dT%H%M%S', time.localtime()))
361 saved_dir = device.TakeScreenshot( 384 screenshot_file = device.TakeScreenshot(
362 os.path.join(self._test_instance.screenshot_dir, file_name)) 385 os.path.join(self._test_instance.screenshot_dir, file_name))
363 logging.info( 386 logging.info(
364 'Saved screenshot for %s to %s.', 387 'Saved screenshot for %s to %s.',
365 test_display_name, saved_dir) 388 test_display_name, screenshot_file)
389 if self._test_instance.should_save_images:
390 link = google_storage_helper.upload(
391 google_storage_helper.unique_name('screenshot', device=device),
392 screenshot_file,
393 bucket='chromium-result-details/images')
jbudorick 2017/04/11 00:36:43 We shouldn't be hardcoding the bucket. If we use s
mikecase (-- gone --) 2017/04/26 18:01:51 Agreed. Added test runner arg --gs-results-bucket.
394 for result in results:
395 result.SetLink('failure_screenshot', link)
396
366 logging.info('detected failure in %s. raw output:', test_display_name) 397 logging.info('detected failure in %s. raw output:', test_display_name)
367 for l in output: 398 for l in output:
368 logging.info(' %s', l) 399 logging.info(' %s', l)
369 if (not self._env.skip_clear_data 400 if (not self._env.skip_clear_data
370 and self._test_instance.package_info): 401 and self._test_instance.package_info):
371 permissions = ( 402 permissions = (
372 self._test_instance.apk_under_test.GetPermissions() 403 self._test_instance.apk_under_test.GetPermissions()
373 if self._test_instance.apk_under_test 404 if self._test_instance.apk_under_test
374 else None) 405 else None)
375 device.ClearApplicationState(self._test_instance.package_info.package, 406 device.ClearApplicationState(self._test_instance.package_info.package,
376 permissions=permissions) 407 permissions=permissions)
377
378 else: 408 else:
379 logging.debug('raw output from %s:', test_display_name) 409 logging.debug('raw output from %s:', test_display_name)
380 for l in output: 410 for l in output:
381 logging.debug(' %s', l) 411 logging.debug(' %s', l)
382 if self._test_instance.coverage_directory: 412 if self._test_instance.coverage_directory:
383 device.PullFile(coverage_directory, 413 device.PullFile(coverage_directory,
384 self._test_instance.coverage_directory) 414 self._test_instance.coverage_directory)
385 device.RunShellCommand( 415 device.RunShellCommand(
386 'rm -f %s' % posixpath.join(coverage_directory, '*'), 416 'rm -f %s' % posixpath.join(coverage_directory, '*'),
387 check_return=True, shell=True) 417 check_return=True, shell=True)
388 if self._test_instance.store_tombstones: 418 if self._test_instance.store_tombstones:
389 tombstones_url = None 419 tombstones_url = None
390 for result in results: 420 for result in results:
391 if result.GetType() == base_test_result.ResultType.CRASH: 421 if result.GetType() == base_test_result.ResultType.CRASH:
392 if not tombstones_url: 422 if not tombstones_url:
393 resolved_tombstones = tombstones.ResolveTombstones( 423 resolved_tombstones = tombstones.ResolveTombstones(
394 device, 424 device,
395 resolve_all_tombstones=True, 425 resolve_all_tombstones=True,
396 include_stack_symbols=False, 426 include_stack_symbols=False,
397 wipe_tombstones=True) 427 wipe_tombstones=True)
398 stream_name = 'tombstones_%s_%s' % ( 428 stream_name = 'tombstones_%s_%s' % (
399 time.strftime('%Y%m%dT%H%M%S', time.localtime()), 429 time.strftime('%Y%m%dT%H%M%S', time.localtime()),
400 device.serial) 430 device.serial)
401 tombstones_url = logdog_helper.text( 431 tombstones_url = logdog_helper.text(
402 stream_name, resolved_tombstones) 432 stream_name, resolved_tombstones)
403 result.SetLink('tombstones', tombstones_url) 433 result.SetLink('tombstones', tombstones_url)
404 return results, None 434 return results, None
405 435
436 def _ProcessRenderTestResults(self, device, results):
437 if not self._test_instance.should_save_images:
438 return
439
440 render_results_dir = RENDER_TESTS_RESULTS_DIR.get(self._test_instance.suite)
441 if not render_results_dir:
442 return
443
444 failure_images_device_dir = posixpath.join(
445 device.GetExternalStoragePath(),
446 'chromium_tests_root', render_results_dir, 'failures')
447 if not device.FileExists(failure_images_device_dir):
448 return
449
450 with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
451 device.PullFile(failure_images_device_dir, temp_dir)
452 device.RemovePath(failure_images_device_dir, recursive=True)
453
454 for failure_filename in os.listdir(os.path.join(temp_dir, 'failures')):
455
456 m = RE_RENDER_IMAGE_NAME.match(failure_filename)
457 if not m:
458 logging.warning('Unexpected file in render test failures: %s',
459 failure_filename)
460 continue
461
462 failure_filepath = os.path.join(temp_dir, 'failures', failure_filename)
463 failure_link = google_storage_helper.upload(
464 google_storage_helper.unique_name(failure_filename, device=device),
465 failure_filepath,
466 bucket='chromium-render-tests')
jbudorick 2017/04/11 00:36:43 Again, I'm concerned about the hard-coded bucket n
mikecase (-- gone --) 2017/04/26 18:01:51 Fixed
467
468 golden_filepath = os.path.join(
469 host_paths.DIR_SOURCE_ROOT, render_results_dir, failure_filename)
470 if not os.path.exists(golden_filepath):
471 logging.error('Cannot find golden image for %s', failure_filename)
472 continue
473 golden_link = google_storage_helper.upload(
474 google_storage_helper.unique_name(failure_filename, device=device),
475 golden_filepath,
476 bucket='chromium-render-tests')
477
478 if can_compute_diffs:
479 diff_filename = '_diff'.join(os.path.splitext(failure_filename))
480 diff_filepath = os.path.join(temp_dir, diff_filename)
481 (ImageChops.difference(
482 Image.open(failure_filepath), Image.open(golden_filepath))
483 .convert('L')
484 .point(lambda i: 255 if i else 0)
485 .save(diff_filepath))
486 diff_link = google_storage_helper.upload(
487 google_storage_helper.unique_name(diff_filename, device=device),
488 diff_filepath,
489 bucket='chromium-render-tests')
490 else:
491 diff_link = ''
492 logging.error('Error importing PIL library. Image diffs for '
493 'render test results will not be computed.')
494
495 with tempfile.NamedTemporaryFile(suffix='.html') as temp_html:
496 temp_html.write('''
497 <html>
498 <table>
499 <tr>
500 <th>Failure</th>
501 <th>Golden</th>
502 <th>Diff</th>
503 </tr>
504 <tr>
505 <td><img src="%s"/></td>
506 <td><img src="%s"/></td>
507 <td><img src="%s"/></td>
508 </tr>
509 </table>
510 </html>
511 ''' % (failure_link, golden_link, diff_link))
512 temp_html.flush()
513 html_results_link = google_storage_helper.upload(
514 google_storage_helper.unique_name('render_html', device=device),
515 temp_html.name,
516 bucket='chromium-render-tests',
517 content_type='text/html')
518 for result in results:
519 result.SetLink(failure_filename, html_results_link)
520
406 #override 521 #override
407 def _ShouldRetry(self, test): 522 def _ShouldRetry(self, test):
408 if 'RetryOnFailure' in test.get('annotations', {}): 523 if 'RetryOnFailure' in test.get('annotations', {}):
409 return True 524 return True
410 525
411 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been 526 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been
412 # enabled for a while. See crbug.com/619055 for more details. 527 # enabled for a while. See crbug.com/619055 for more details.
413 logging.error('Default retries are being phased out. crbug.com/619055') 528 logging.error('Default retries are being phased out. crbug.com/619055')
414 return False 529 return False
415 530
(...skipping 15 matching lines...) Expand all
431 if k in annotations: 546 if k in annotations:
432 timeout = v 547 timeout = v
433 break 548 break
434 else: 549 else:
435 logging.warning('Using default 1 minute timeout for %s', test_name) 550 logging.warning('Using default 1 minute timeout for %s', test_name)
436 timeout = 60 551 timeout = 60
437 552
438 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) 553 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations)
439 554
440 return timeout 555 return timeout
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698