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

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

Issue 2701473003: Add failure screenshots and render test images to results detail. (Closed)
Patch Set: Add failure screenshots and render test images to results detail. Created 3 years, 9 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
49 _RE_RENDER_IMAGE_NAME = re.compile(
50 r'(?P<test_class>\w+)\.'
51 r'(?P<description>\w+)\.'
52 r'(?P<device_model>\w+)\.'
53 r'(?P<orientation>port|land)\.png')
54
55 RENDER_TESTS_RESULTS_DIR = {
56 'ChromePublicTest': 'chrome/test/data/android/render_tests'
57 }
38 58
39 # TODO(jbudorick): Make this private once the instrumentation test_runner is 59 # TODO(jbudorick): Make this private once the instrumentation test_runner is
40 # deprecated. 60 # deprecated.
41 def DidPackageCrashOnDevice(package_name, device): 61 def DidPackageCrashOnDevice(package_name, device):
42 # Dismiss any error dialogs. Limit the number in case we have an error 62 # Dismiss any error dialogs. Limit the number in case we have an error
43 # loop or we are failing to dismiss. 63 # loop or we are failing to dismiss.
44 try: 64 try:
45 for _ in xrange(10): 65 for _ in xrange(10):
46 package = device.DismissCrashDialogIfNeeded() 66 package = device.DismissCrashDialogIfNeeded()
47 if not package: 67 if not package:
(...skipping 266 matching lines...) Expand 10 before | Expand all | Expand 10 after
314 # TODO(jbudorick): Make instrumentation tests output a JSON so this 334 # TODO(jbudorick): Make instrumentation tests output a JSON so this
315 # doesn't have to parse the output. 335 # doesn't have to parse the output.
316 result_code, result_bundle, statuses = ( 336 result_code, result_bundle, statuses = (
317 self._test_instance.ParseAmInstrumentRawOutput(output)) 337 self._test_instance.ParseAmInstrumentRawOutput(output))
318 results = self._test_instance.GenerateTestResults( 338 results = self._test_instance.GenerateTestResults(
319 result_code, result_bundle, statuses, start_ms, duration_ms) 339 result_code, result_bundle, statuses, start_ms, duration_ms)
320 for result in results: 340 for result in results:
321 if logcat_url: 341 if logcat_url:
322 result.SetLink('logcat', logcat_url) 342 result.SetLink('logcat', logcat_url)
323 343
344 self._ProcessRenderTestResults(device, results)
345
324 # Update the result name if the test used flags. 346 # Update the result name if the test used flags.
325 if flags: 347 if flags:
326 for r in results: 348 for r in results:
327 if r.GetName() == test_name: 349 if r.GetName() == test_name:
328 r.SetName(test_display_name) 350 r.SetName(test_display_name)
329 351
330 # Add UNKNOWN results for any missing tests. 352 # Add UNKNOWN results for any missing tests.
331 iterable_test = test if isinstance(test, list) else [test] 353 iterable_test = test if isinstance(test, list) else [test]
332 test_names = set(self._GetUniqueTestName(t) for t in iterable_test) 354 test_names = set(self._GetUniqueTestName(t) for t in iterable_test)
333 results_names = set(r.GetName() for r in results) 355 results_names = set(r.GetName() for r in results)
(...skipping 11 matching lines...) Expand all
345 # - optionally taking a screenshot 367 # - optionally taking a screenshot
346 # - logging the raw output at INFO level 368 # - logging the raw output at INFO level
347 # - clearing the application state while persisting permissions 369 # - clearing the application state while persisting permissions
348 if any(r.GetType() not in (base_test_result.ResultType.PASS, 370 if any(r.GetType() not in (base_test_result.ResultType.PASS,
349 base_test_result.ResultType.SKIP) 371 base_test_result.ResultType.SKIP)
350 for r in results): 372 for r in results):
351 if self._test_instance.screenshot_dir: 373 if self._test_instance.screenshot_dir:
352 file_name = '%s-%s.png' % ( 374 file_name = '%s-%s.png' % (
353 test_display_name, 375 test_display_name,
354 time.strftime('%Y%m%dT%H%M%S', time.localtime())) 376 time.strftime('%Y%m%dT%H%M%S', time.localtime()))
355 saved_dir = device.TakeScreenshot( 377 screenshot_file = device.TakeScreenshot(
356 os.path.join(self._test_instance.screenshot_dir, file_name)) 378 os.path.join(self._test_instance.screenshot_dir, file_name))
357 logging.info( 379 logging.info(
358 'Saved screenshot for %s to %s.', 380 'Saved screenshot for %s to %s.',
359 test_display_name, saved_dir) 381 test_display_name, screenshot_file)
382 if self._test_instance.should_save_images:
383 link = google_storage_helper.upload(
384 google_storage_helper.unique_name('screenshot', device=device),
385 screenshot_file,
386 bucket='chromium-render-tests')
387 for result in results:
388 result.SetLink('failure_screenshot', link)
389
360 logging.info('detected failure in %s. raw output:', test_display_name) 390 logging.info('detected failure in %s. raw output:', test_display_name)
361 for l in output: 391 for l in output:
362 logging.info(' %s', l) 392 logging.info(' %s', l)
363 if (not self._env.skip_clear_data 393 if (not self._env.skip_clear_data
364 and self._test_instance.package_info): 394 and self._test_instance.package_info):
365 permissions = ( 395 permissions = (
366 self._test_instance.apk_under_test.GetPermissions() 396 self._test_instance.apk_under_test.GetPermissions()
367 if self._test_instance.apk_under_test 397 if self._test_instance.apk_under_test
368 else None) 398 else None)
369 device.ClearApplicationState(self._test_instance.package_info.package, 399 device.ClearApplicationState(self._test_instance.package_info.package,
370 permissions=permissions) 400 permissions=permissions)
371
372 else: 401 else:
373 logging.debug('raw output from %s:', test_display_name) 402 logging.debug('raw output from %s:', test_display_name)
374 for l in output: 403 for l in output:
375 logging.debug(' %s', l) 404 logging.debug(' %s', l)
376 if self._test_instance.coverage_directory: 405 if self._test_instance.coverage_directory:
377 device.PullFile(coverage_directory, 406 device.PullFile(coverage_directory,
378 self._test_instance.coverage_directory) 407 self._test_instance.coverage_directory)
379 device.RunShellCommand('rm -f %s' % os.path.join(coverage_directory, 408 device.RunShellCommand('rm -f %s' % os.path.join(coverage_directory,
380 '*')) 409 '*'))
381 if self._test_instance.store_tombstones: 410 if self._test_instance.store_tombstones:
382 tombstones_url = None 411 tombstones_url = None
383 for result in results: 412 for result in results:
384 if result.GetType() == base_test_result.ResultType.CRASH: 413 if result.GetType() == base_test_result.ResultType.CRASH:
385 if not tombstones_url: 414 if not tombstones_url:
386 resolved_tombstones = tombstones.ResolveTombstones( 415 resolved_tombstones = tombstones.ResolveTombstones(
387 device, 416 device,
388 resolve_all_tombstones=True, 417 resolve_all_tombstones=True,
389 include_stack_symbols=False, 418 include_stack_symbols=False,
390 wipe_tombstones=True) 419 wipe_tombstones=True)
391 stream_name = 'tombstones_%s_%s' % ( 420 stream_name = 'tombstones_%s_%s' % (
392 time.strftime('%Y%m%dT%H%M%S', time.localtime()), 421 time.strftime('%Y%m%dT%H%M%S', time.localtime()),
393 device.serial) 422 device.serial)
394 tombstones_url = logdog_helper.text( 423 tombstones_url = logdog_helper.text(
395 stream_name, resolved_tombstones) 424 stream_name, resolved_tombstones)
396 result.SetLink('tombstones', tombstones_url) 425 result.SetLink('tombstones', tombstones_url)
397 return results, None 426 return results, None
398 427
428 def _ProcessRenderTestResults(self, device, results):
429 render_results_dir = RENDER_TESTS_RESULTS_DIR.get(self._test_instance.suite)
430 if not render_results_dir:
431 return
432
433 failure_images_device_dir = posixpath.join(
434 device.GetExternalStoragePath(),
435 'chromium_tests_root', render_results_dir, 'failures')
436 if not device.FileExists(failure_images_device_dir):
437 return
438
439 if self._test_instance.should_save_images:
440 with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
441 device.PullFile(failure_images_device_dir, temp_dir)
442 device.RemovePath(failure_images_device_dir, recursive=True)
443
444 for failure_filename in os.listdir(
445 os.path.join(temp_dir, 'failures')):
446
447 m = _RE_RENDER_IMAGE_NAME.match(failure_filename)
448 if not m:
449 logging.warning('Unexpected file in render test failures: %s',
450 failure_filename)
451 continue
452
453 failure_filepath = os.path.join(
454 temp_dir, 'failures', failure_filename)
455 failure_link = google_storage_helper.upload(
456 google_storage_helper.unique_name(
457 failure_filename, device=device),
458 failure_filepath,
459 bucket='chromium-render-tests')
460
461 golden_filepath = os.path.join(
462 host_paths.DIR_SOURCE_ROOT, render_results_dir,
463 failure_filename)
464 if not os.path.exists(golden_filepath):
465 logging.error('Cannot find golden image for %s', failure_filename)
466 continue
467 golden_link = google_storage_helper.upload(
468 google_storage_helper.unique_name(
469 failure_filename, device=device),
470 golden_filepath,
471 bucket='chromium-render-tests')
472
473 if can_compute_diffs:
474 diff_filename = '_diff'.join(
475 os.path.splitext(failure_filename))
476 diff_filepath = os.path.join(temp_dir, diff_filename)
477 (ImageChops.difference(
478 Image.open(failure_filepath), Image.open(golden_filepath))
479 .convert('L')
480 .point(lambda i: 255 if i else 0)
481 .save(diff_filepath))
482 diff_link = google_storage_helper.upload(
483 google_storage_helper.unique_name(
484 diff_filename, device=device),
485 diff_filepath,
486 bucket='chromium-render-tests')
487 else:
488 diff_link = ''
489 logging.error('Error importing PIL library. Image diffs for '
490 'render test results will not be computed.')
491
492 with tempfile.NamedTemporaryFile(suffix='.html') as temp_html:
493 temp_html.write('''
494 <html>
495 <table>
496 <tr>
497 <th>Failure</th>
498 <th>Golden</th>
499 <th>Diff</th>
500 </tr>
501 <tr>
502 <td><img src="%s"/></td>
503 <td><img src="%s"/></td>
504 <td><img src="%s"/></td>
505 </tr>
506 </table>
507 </html>
508 ''' % (failure_link, golden_link, diff_link))
509 html_results_link = google_storage_helper.upload(
510 google_storage_helper.unique_name(
511 'render_html', device=device),
512 temp_html.name,
513 bucket='chromium-render-tests')
514 for result in results:
515 result.SetLink(failure_filename, html_results_link)
516
399 #override 517 #override
400 def _ShouldRetry(self, test): 518 def _ShouldRetry(self, test):
401 if 'RetryOnFailure' in test.get('annotations', {}): 519 if 'RetryOnFailure' in test.get('annotations', {}):
402 return True 520 return True
403 521
404 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been 522 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been
405 # enabled for a while. See crbug.com/619055 for more details. 523 # enabled for a while. See crbug.com/619055 for more details.
406 logging.error('Default retries are being phased out. crbug.com/619055') 524 logging.error('Default retries are being phased out. crbug.com/619055')
407 return False 525 return False
408 526
(...skipping 16 matching lines...) Expand all
425 timeout = v 543 timeout = v
426 break 544 break
427 else: 545 else:
428 logging.warning('Using default 1 minute timeout for %s', test_name) 546 logging.warning('Using default 1 minute timeout for %s', test_name)
429 timeout = 60 547 timeout = 60
430 548
431 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) 549 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations)
432 550
433 return timeout 551 return timeout
434 552
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698