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

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

Powered by Google App Engine
This is Rietveld 408576698