Index: build/android/pylib/local/device/local_device_instrumentation_test_run.py |
diff --git a/build/android/pylib/local/device/local_device_instrumentation_test_run.py b/build/android/pylib/local/device/local_device_instrumentation_test_run.py |
index 9d6a65d549b14e5723d1a531c4c1fd583f58cd51..d25f242b385619e6c1bc63ec4d06649c4ab03e03 100644 |
--- a/build/android/pylib/local/device/local_device_instrumentation_test_run.py |
+++ b/build/android/pylib/local/device/local_device_instrumentation_test_run.py |
@@ -6,6 +6,8 @@ import logging |
import os |
import posixpath |
import re |
+import sys |
+import tempfile |
import time |
from devil.android import device_errors |
@@ -16,6 +18,7 @@ from devil.utils import reraiser_thread |
from pylib import valgrind_tools |
from pylib.android import logdog_logcat_monitor |
from pylib.base import base_test_result |
+from pylib.constants import host_paths |
from pylib.instrumentation import instrumentation_test_instance |
from pylib.local.device import local_device_environment |
from pylib.local.device import local_device_test_run |
@@ -26,6 +29,15 @@ from py_utils import contextlib_ext |
from py_utils import tempfile_ext |
import tombstones |
+sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party')) |
+import jinja2 # pylint: disable=import-error |
+import markupsafe # pylint: disable=import-error,unused-import |
+ |
+ |
+_JINJA_TEMPLATE_DIR = os.path.join( |
+ host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'pylib', 'instrumentation') |
+_JINJA_TEMPLATE_FILENAME = 'render_test.html.jinja' |
+ |
_TAG = 'test_runner_py' |
TIMEOUT_ANNOTATIONS = [ |
@@ -43,6 +55,16 @@ LOGCAT_FILTERS = ['*:e', 'chromium:v', 'cr_*:v'] |
EXTRA_SCREENSHOT_FILE = ( |
'org.chromium.base.test.ScreenshotOnFailureStatement.ScreenshotFile') |
+FEATURE_ANNOTATION = 'Feature' |
+RENDER_TEST_FEATURE_ANNOTATION = 'RenderTest' |
+ |
+# This needs to be kept in sync with formatting in |RenderUtils.imageName| |
+RE_RENDER_IMAGE_NAME = re.compile( |
+ r'(?P<test_class>\w+)\.' |
+ r'(?P<description>\w+)\.' |
+ r'(?P<device_model>\w+)\.' |
+ r'(?P<orientation>port|land)\.png') |
+ |
# TODO(jbudorick): Make this private once the instrumentation test_runner is |
# deprecated. |
def DidPackageCrashOnDevice(package_name, device): |
@@ -249,7 +271,8 @@ class LocalDeviceInstrumentationTestRun( |
def _RunTest(self, device, test): |
extras = {} |
- flags = None |
+ flags_to_add = [] |
+ flags_to_remove = [] |
test_timeout_scale = None |
if self._test_instance.coverage_directory: |
coverage_basename = '%s.ec' % ('%s_group' % test[0]['method'] |
@@ -304,7 +327,8 @@ class LocalDeviceInstrumentationTestRun( |
self._test_instance.test_package, self._test_instance.test_runner) |
extras['class'] = test_name |
if 'flags' in test: |
- flags = test['flags'] |
+ flags_to_add.extend(test['flags'].add) |
+ flags_to_remove.extend(test['flags'].remove) |
timeout = self._GetTimeoutFromAnnotations( |
test['annotations'], test_display_name) |
@@ -316,10 +340,19 @@ class LocalDeviceInstrumentationTestRun( |
logging.info('preparing to run %s: %s', test_display_name, test) |
- if flags: |
+ render_tests_device_output_dir = None |
+ if _IsRenderTest(test): |
+ # TODO(mikecase): Add DeviceTempDirectory class and use that instead. |
+ render_tests_device_output_dir = posixpath.join( |
+ device.GetExternalStoragePath(), |
+ 'render_test_output_dir') |
+ flags_to_add.append('--render-test-output-dir=%s' % |
+ render_tests_device_output_dir) |
+ |
+ if flags_to_add or flags_to_remove: |
self._CreateFlagChangerIfNeeded(device) |
self._flag_changers[str(device)].PushFlags( |
- add=flags.add, remove=flags.remove) |
+ add=flags_to_add, remove=flags_to_remove) |
try: |
device.RunShellCommand( |
@@ -348,7 +381,7 @@ class LocalDeviceInstrumentationTestRun( |
['log', '-p', 'i', '-t', _TAG, 'END %s' % test_name], |
check_return=True) |
duration_ms = time_ms() - start_ms |
- if flags: |
+ if flags_to_add or flags_to_remove: |
self._flag_changers[str(device)].Restore() |
if test_timeout_scale: |
valgrind_tools.SetChromeTimeoutScale( |
@@ -364,8 +397,19 @@ class LocalDeviceInstrumentationTestRun( |
if logcat_url: |
result.SetLink('logcat', logcat_url) |
+ if _IsRenderTest(test): |
+ # Render tests do not cause test failure by default. So we have to check |
+ # to see if any failure images were generated even if the test does not |
+ # fail. |
+ try: |
+ self._ProcessRenderTestResults( |
+ device, render_tests_device_output_dir, results) |
+ finally: |
+ device.RemovePath(render_tests_device_output_dir, |
+ recursive=True, force=True) |
+ |
# Update the result name if the test used flags. |
- if flags: |
+ if flags_to_add or flags_to_remove: |
for r in results: |
if r.GetName() == test_name: |
r.SetName(test_display_name) |
@@ -467,6 +511,97 @@ class LocalDeviceInstrumentationTestRun( |
for result in results: |
result.SetLink('post_test_screenshot', link) |
+ def _ProcessRenderTestResults( |
+ self, device, render_tests_device_output_dir, results): |
+ # Will archive test images if we are given a GS bucket to store the results |
+ # in and are given a results file to output the links to. |
+ if not bool(self._test_instance.gs_results_bucket): |
+ return |
+ |
+ failure_images_device_dir = posixpath.join( |
+ render_tests_device_output_dir, 'failures') |
+ |
+ if not device.FileExists(failure_images_device_dir): |
+ return |
+ |
+ render_tests_bucket = ( |
+ self._test_instance.gs_results_bucket + '/render_tests') |
+ |
+ diff_images_device_dir = posixpath.join( |
+ render_tests_device_output_dir, 'diffs') |
+ |
+ golden_images_device_dir = posixpath.join( |
+ render_tests_device_output_dir, 'goldens') |
+ |
+ with tempfile_ext.NamedTemporaryDirectory() as temp_dir: |
+ device.PullFile(failure_images_device_dir, temp_dir) |
+ |
+ if device.FileExists(diff_images_device_dir): |
+ device.PullFile(diff_images_device_dir, temp_dir) |
+ else: |
+ logging.error('Diff images not found on device.') |
+ |
+ if device.FileExists(golden_images_device_dir): |
+ device.PullFile(golden_images_device_dir, temp_dir) |
+ else: |
+ logging.error('Golden images not found on device.') |
+ |
+ for failure_filename in os.listdir(os.path.join(temp_dir, 'failures')): |
+ |
+ m = RE_RENDER_IMAGE_NAME.match(failure_filename) |
+ if not m: |
+ logging.warning('Unexpected file in render test failures: %s', |
+ failure_filename) |
+ continue |
+ |
+ failure_filepath = os.path.join(temp_dir, 'failures', failure_filename) |
+ failure_link = google_storage_helper.upload( |
+ google_storage_helper.unique_name( |
+ 'failure_%s' % failure_filename, device=device), |
+ failure_filepath, |
+ bucket=render_tests_bucket) |
+ |
+ golden_filepath = os.path.join(temp_dir, 'goldens', failure_filename) |
+ if os.path.exists(golden_filepath): |
+ golden_link = google_storage_helper.upload( |
+ google_storage_helper.unique_name( |
+ 'golden_%s' % failure_filename, device=device), |
+ golden_filepath, |
+ bucket=render_tests_bucket) |
+ else: |
+ golden_link = '' |
+ |
+ diff_filepath = os.path.join(temp_dir, 'diffs', failure_filename) |
+ if os.path.exists(diff_filepath): |
+ diff_link = google_storage_helper.upload( |
+ google_storage_helper.unique_name( |
+ 'diff_%s' % failure_filename, device=device), |
+ diff_filepath, |
+ bucket=render_tests_bucket) |
+ else: |
+ diff_link = '' |
+ |
+ with tempfile.NamedTemporaryFile(suffix='.html') as temp_html: |
+ jinja2_env = jinja2.Environment( |
+ loader=jinja2.FileSystemLoader(_JINJA_TEMPLATE_DIR), |
+ trim_blocks=True) |
+ template = jinja2_env.get_template(_JINJA_TEMPLATE_FILENAME) |
+ # pylint: disable=no-member |
+ processed_template_output = template.render( |
+ failure_link=failure_link, |
+ golden_link=golden_link, |
+ diff_link=diff_link) |
+ |
+ temp_html.write(processed_template_output) |
+ temp_html.flush() |
+ html_results_link = google_storage_helper.upload( |
+ google_storage_helper.unique_name('render_html', device=device), |
+ temp_html.name, |
+ bucket=render_tests_bucket, |
+ content_type='text/html') |
+ for result in results: |
+ result.SetLink(failure_filename, html_results_link) |
+ |
#override |
def _ShouldRetry(self, test): |
if 'RetryOnFailure' in test.get('annotations', {}): |
@@ -502,3 +637,10 @@ class LocalDeviceInstrumentationTestRun( |
timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) |
return timeout |
+ |
+def _IsRenderTest(test): |
+ """Determines if a test or list of tests has a RenderTest amongst them.""" |
+ if not isinstance(test, list): |
+ test = [test] |
+ return any([RENDER_TEST_FEATURE_ANNOTATION in t['annotations'].get( |
+ FEATURE_ANNOTATION, ()) for t in test]) |