OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # | |
3 # Copyright 2016 The Chromium Authors. All rights reserved. | |
4 # Use of this source code is governed by a BSD-style license that can be | |
5 # found in the LICENSE file. | |
6 | |
7 import argparse | |
8 import collections | |
9 import logging | |
10 import os | |
11 import posixpath | |
12 import re | |
13 import shutil | |
14 import sys | |
15 import tempfile | |
16 import zipfile | |
17 | |
18 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) | |
19 import devil_chromium | |
20 from devil.android import device_utils | |
21 from devil.utils import cmd_helper | |
22 from pylib.constants import host_paths | |
23 | |
24 sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'build')) | |
25 import find_depot_tools # pylint: disable=import-error | |
26 | |
27 sys.path.append(os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party')) | |
28 import jinja2 # pylint: disable=import-error | |
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 logging.exception('Error importing PIL library. Image diffs will not be ' | |
37 'displayed properly unless PIL module is installed.') | |
38 | |
39 _RE_IMAGE_NAME = re.compile( | |
40 r'(?P<test_class>\w+)\.' | |
41 r'(?P<description>\w+)\.' | |
42 r'(?P<device_model>\w+)\.' | |
43 r'(?P<orientation>port|land)\.png') | |
44 | |
45 _RENDER_TEST_BASE_URL = 'https://storage.googleapis.com/chromium-render-tests/' | |
46 _RENDER_TEST_BUCKET = 'gs://chromium-render-tests/' | |
47 | |
48 _JINJA_TEMPLATE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
49 _JINJA_TEMPLATE_FILENAME = 'render_webpage.html.jinja2' | |
50 | |
51 | |
52 def _UploadFiles(upload_dir, files): | |
53 """Upload files to the render tests GS bucket.""" | |
54 if files: | |
55 google_storage_upload_dir = os.path.join(_RENDER_TEST_BUCKET, upload_dir) | |
56 cmd = [os.path.join(find_depot_tools.DEPOT_TOOLS_PATH, 'gsutil.py'), | |
57 '-m', 'cp'] | |
58 cmd.extend(files) | |
59 cmd.append(google_storage_upload_dir) | |
60 cmd_helper.RunCmd(cmd) | |
61 | |
62 | |
63 def _GoogleStorageUrl(upload_dir, filename): | |
64 return os.path.join( | |
65 _RENDER_TEST_BASE_URL, upload_dir, os.path.basename(filename)) | |
66 | |
67 | |
68 def _ComputeImageDiff(failure_image, golden_image): | |
69 """Compute mask showing which pixels are different between two images.""" | |
70 return (ImageChops.difference(failure_image, golden_image) | |
71 .convert('L') | |
72 .point(lambda i: 255 if i else 0)) | |
73 | |
74 | |
75 def ProcessRenderTestResults(devices, render_results_dir, | |
76 upload_dir, html_file): | |
77 """Grabs render results from device and generates webpage displaying results. | |
78 | |
79 Args: | |
80 devices: List of DeviceUtils objects to grab results from. | |
81 render_results_path: Path where render test results are storage. | |
82 Will look for failures render test results on the device in | |
83 /sdcard/chromium_tests_root/<render_results_path>/failures/ | |
84 and will look for golden images at Chromium src/<render_results_path>/. | |
85 upload_dir: Directory to upload the render test results to. | |
86 html_file: File to write the test results to. | |
87 """ | |
88 results_dict = collections.defaultdict(lambda: collections.defaultdict(list)) | |
89 | |
90 diff_upload_dir = os.path.join(upload_dir, 'diffs') | |
91 failure_upload_dir = os.path.join(upload_dir, 'failures') | |
92 golden_upload_dir = os.path.join(upload_dir, 'goldens') | |
93 | |
94 diff_images = [] | |
95 failure_images = [] | |
96 golden_images = [] | |
97 | |
98 temp_dir = None | |
99 try: | |
100 temp_dir = tempfile.mkdtemp() | |
101 | |
102 for device in devices: | |
103 failures_device_dir = posixpath.join( | |
104 device.GetExternalStoragePath(), | |
105 'chromium_tests_root', render_results_dir, 'failures') | |
106 device.PullFile(failures_device_dir, temp_dir) | |
107 | |
108 for failure_filename in os.listdir(os.path.join(temp_dir, 'failures')): | |
109 m = _RE_IMAGE_NAME.match(failure_filename) | |
110 if not m: | |
111 logging.warning( | |
112 'Unexpected file in render test failures, %s', failure_filename) | |
113 continue | |
114 failure_file = os.path.join(temp_dir, 'failures', failure_filename) | |
115 | |
116 # Check to make sure we have golden image for this failure. | |
117 golden_file = os.path.join( | |
118 host_paths.DIR_SOURCE_ROOT, render_results_dir, failure_filename) | |
119 if not os.path.exists(golden_file): | |
120 logging.error('Cannot find golden image for %s', failure_filename) | |
121 continue | |
122 | |
123 # Compute image diff between failure and golden. | |
124 if can_compute_diffs: | |
125 diff_image = _ComputeImageDiff( | |
126 Image.open(failure_file), Image.open(golden_file)) | |
127 diff_filename = '_diff'.join( | |
128 os.path.splitext(os.path.basename(failure_file))) | |
129 diff_file = os.path.join(temp_dir, diff_filename) | |
130 diff_image.save(diff_file) | |
131 diff_images.append(diff_file) | |
132 | |
133 failure_images.append(failure_file) | |
134 golden_images.append(golden_file) | |
135 | |
136 test_class = m.group('test_class') | |
137 device_model = m.group('device_model') | |
138 | |
139 results_entry = { | |
140 'description': m.group('description'), | |
141 'orientation': m.group('orientation'), | |
142 'failure_image': _GoogleStorageUrl(failure_upload_dir, failure_file), | |
143 'golden_image': _GoogleStorageUrl(golden_upload_dir, golden_file), | |
144 } | |
145 if can_compute_diffs: | |
146 results_entry.update( | |
147 {'diff_image': _GoogleStorageUrl(diff_upload_dir, diff_file)}) | |
148 results_dict[test_class][device_model].append(results_entry) | |
149 | |
150 if can_compute_diffs: | |
151 _UploadFiles(diff_upload_dir, diff_images) | |
152 _UploadFiles(failure_upload_dir, failure_images) | |
153 _UploadFiles(golden_upload_dir, golden_images) | |
154 | |
155 if failure_images: | |
156 failures_zipfile = os.path.join(temp_dir, 'failures.zip') | |
157 with zipfile.ZipFile(failures_zipfile, mode='w') as zf: | |
158 for failure_file in failure_images: | |
159 zf.write(failure_file, os.path.join( | |
160 render_results_dir, os.path.basename(failure_file))) | |
161 failure_zip_url = _GoogleStorageUrl(upload_dir, failures_zipfile) | |
162 _UploadFiles(upload_dir, [failures_zipfile]) | |
163 else: | |
164 failure_zip_url = None | |
165 | |
166 jinja2_env = jinja2.Environment( | |
167 loader=jinja2.FileSystemLoader(_JINJA_TEMPLATE_DIR), | |
168 trim_blocks=True) | |
169 template = jinja2_env.get_template(_JINJA_TEMPLATE_FILENAME) | |
170 # pylint: disable=no-member | |
171 processed_template_output = template.render( | |
172 full_results=dict(results_dict), | |
173 failure_zip_url=failure_zip_url, show_diffs=can_compute_diffs) | |
174 # pylint: enable=no-member | |
175 with open(html_file, 'wb') as f: | |
176 f.write(processed_template_output) | |
177 finally: | |
178 if temp_dir: | |
179 shutil.rmtree(temp_dir) | |
180 | |
181 | |
182 def main(): | |
183 parser = argparse.ArgumentParser() | |
184 | |
185 parser.add_argument('--render-results-dir', | |
186 required=True, | |
187 help='Path on device to look for render test images') | |
188 parser.add_argument('--output-html-file', | |
189 required=True, | |
190 help='File to output the results webpage.') | |
191 parser.add_argument('-d', '--device', dest='devices', action='append', | |
192 default=[], | |
193 help='Device to look for render test results on. ' | |
194 'Default is to look on all connected devices.') | |
195 parser.add_argument('--adb-path', type=os.path.abspath, | |
196 help='Absolute path to the adb binary to use.') | |
197 parser.add_argument('--buildername', type=str, required=True, | |
198 help='Bot buildername. Used to generate path to upload ' | |
199 'render test results') | |
200 parser.add_argument('--build-number', type=str, required=True, | |
201 help='Bot build number. Used to generate path to upload ' | |
202 'render test results') | |
203 | |
204 args = parser.parse_args() | |
205 devil_chromium.Initialize(adb_path=args.adb_path) | |
206 devices = device_utils.DeviceUtils.HealthyDevices(device_arg=args.devices) | |
207 | |
208 upload_dir = os.path.join(args.buildername, args.build_number) | |
209 ProcessRenderTestResults( | |
210 devices, args.render_results_dir, upload_dir, args.output_html_file) | |
211 | |
212 | |
213 if __name__ == '__main__': | |
214 sys.exit(main()) | |
OLD | NEW |