blob: 3b57bc1e206381cd0066eecbae5983521e5035d1 [file] [log] [blame]
epoger@google.comf9d134d2013-09-27 15:02:44 +00001#!/usr/bin/python
2
epoger@google.com9fb6c8a2013-10-09 18:05:58 +00003"""
epoger@google.comf9d134d2013-09-27 15:02:44 +00004Copyright 2013 Google Inc.
5
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
epoger@google.comf9d134d2013-09-27 15:02:44 +00008
epoger@google.comf9d134d2013-09-27 15:02:44 +00009Repackage expected/actual GM results as needed by our HTML rebaseline viewer.
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000010"""
epoger@google.comf9d134d2013-09-27 15:02:44 +000011
12# System-level imports
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +000013import argparse
epoger@google.comf9d134d2013-09-27 15:02:44 +000014import fnmatch
15import json
epoger@google.comdcb4e652013-10-11 18:45:33 +000016import logging
epoger@google.comf9d134d2013-09-27 15:02:44 +000017import os
18import re
19import sys
epoger@google.com542b65f2013-10-15 20:10:33 +000020import time
epoger@google.comf9d134d2013-09-27 15:02:44 +000021
22# Imports from within Skia
23#
24# We need to add the 'gm' directory, so that we can import gm_json.py within
25# that directory. That script allows us to parse the actual-results.json file
26# written out by the GM tool.
27# Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end*
28# so any dirs that are already in the PYTHONPATH will be preferred.
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +000029PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
30GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY)
epoger@google.comf9d134d2013-09-27 15:02:44 +000031if GM_DIRECTORY not in sys.path:
32 sys.path.append(GM_DIRECTORY)
33import gm_json
epoger@google.com9dddf6f2013-11-08 16:25:25 +000034import imagediffdb
epoger@google.comf9d134d2013-09-27 15:02:44 +000035
36IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
epoger@google.comeb832592013-10-23 15:07:26 +000037IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config)
38
epoger@google.com055e3b52013-10-26 14:31:11 +000039FIELDS_PASSED_THRU_VERBATIM = [
40 gm_json.JSONKEY_EXPECTEDRESULTS_BUGS,
41 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE,
42 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED,
43]
epoger@google.comafaad3d2013-09-30 15:06:25 +000044CATEGORIES_TO_SUMMARIZE = [
45 'builder', 'test', 'config', 'resultType',
epoger@google.com055e3b52013-10-26 14:31:11 +000046 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE,
47 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED,
epoger@google.comafaad3d2013-09-30 15:06:25 +000048]
epoger@google.com055e3b52013-10-26 14:31:11 +000049
epoger@google.comdcb4e652013-10-11 18:45:33 +000050RESULTS_ALL = 'all'
51RESULTS_FAILURES = 'failures'
epoger@google.comf9d134d2013-09-27 15:02:44 +000052
53class Results(object):
54 """ Loads actual and expected results from all builders, supplying combined
epoger@google.comdcb4e652013-10-11 18:45:33 +000055 reports as requested.
56
epoger@google.comeb832592013-10-23 15:07:26 +000057 Once this object has been constructed, the results (in self._results[])
58 are immutable. If you want to update the results based on updated JSON
59 file contents, you will need to create a new Results object."""
epoger@google.comf9d134d2013-09-27 15:02:44 +000060
epoger@google.com9dddf6f2013-11-08 16:25:25 +000061 def __init__(self, actuals_root, expected_root, generated_images_root):
epoger@google.comf9d134d2013-09-27 15:02:44 +000062 """
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000063 Args:
epoger@google.comf9d134d2013-09-27 15:02:44 +000064 actuals_root: root directory containing all actual-results.json files
65 expected_root: root directory containing all expected-results.json files
epoger@google.com214a0242013-11-22 19:26:18 +000066 generated_images_root: directory within which to create all pixel diffs;
epoger@google.com9dddf6f2013-11-08 16:25:25 +000067 if this directory does not yet exist, it will be created
epoger@google.comf9d134d2013-09-27 15:02:44 +000068 """
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +000069 time_start = int(time.time())
epoger@google.com9dddf6f2013-11-08 16:25:25 +000070 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root)
epoger@google.comeb832592013-10-23 15:07:26 +000071 self._actuals_root = actuals_root
72 self._expected_root = expected_root
73 self._load_actual_and_expected()
epoger@google.com542b65f2013-10-15 20:10:33 +000074 self._timestamp = int(time.time())
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +000075 logging.info('Results complete; took %d seconds.' %
76 (self._timestamp - time_start))
epoger@google.com542b65f2013-10-15 20:10:33 +000077
78 def get_timestamp(self):
79 """Return the time at which this object was created, in seconds past epoch
80 (UTC).
81 """
82 return self._timestamp
epoger@google.comf9d134d2013-09-27 15:02:44 +000083
epoger@google.comeb832592013-10-23 15:07:26 +000084 def edit_expectations(self, modifications):
85 """Edit the expectations stored within this object and write them back
86 to disk.
87
88 Note that this will NOT update the results stored in self._results[] ;
89 in order to see those updates, you must instantiate a new Results object
90 based on the (now updated) files on disk.
91
92 Args:
93 modifications: a list of dictionaries, one for each expectation to update:
94
95 [
96 {
97 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug',
98 'test': 'bigmatrix',
99 'config': '8888',
100 'expectedHashType': 'bitmap-64bitMD5',
101 'expectedHashDigest': '10894408024079689926',
epoger@google.com055e3b52013-10-26 14:31:11 +0000102 'bugs': [123, 456],
103 'ignore-failure': false,
104 'reviewed-by-human': true,
epoger@google.comeb832592013-10-23 15:07:26 +0000105 },
106 ...
107 ]
108
epoger@google.comeb832592013-10-23 15:07:26 +0000109 """
110 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
111 for mod in modifications:
112 image_name = IMAGE_FILENAME_FORMATTER % (mod['test'], mod['config'])
113 # TODO(epoger): assumes a single allowed digest per test
114 allowed_digests = [[mod['expectedHashType'],
115 int(mod['expectedHashDigest'])]]
116 new_expectations = {
117 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests,
epoger@google.comeb832592013-10-23 15:07:26 +0000118 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000119 for field in FIELDS_PASSED_THRU_VERBATIM:
120 value = mod.get(field)
121 if value is not None:
122 new_expectations[field] = value
epoger@google.comeb832592013-10-23 15:07:26 +0000123 builder_dict = expected_builder_dicts[mod['builder']]
124 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS)
125 if not builder_expectations:
126 builder_expectations = {}
127 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations
128 builder_expectations[image_name] = new_expectations
129 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root)
130
epoger@google.comdcb4e652013-10-11 18:45:33 +0000131 def get_results_of_type(self, type):
132 """Return results of some/all tests (depending on 'type' parameter).
133
134 Args:
135 type: string describing which types of results to include; must be one
136 of the RESULTS_* constants
137
138 Results are returned as a dictionary in this form:
epoger@google.comf9d134d2013-09-27 15:02:44 +0000139
epoger@google.comafaad3d2013-09-30 15:06:25 +0000140 {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000141 'categories': # dictionary of categories listed in
epoger@google.comafaad3d2013-09-30 15:06:25 +0000142 # CATEGORIES_TO_SUMMARIZE, with the number of times
143 # each value appears within its category
epoger@google.comf9d134d2013-09-27 15:02:44 +0000144 {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000145 'resultType': # category name
epoger@google.comafaad3d2013-09-30 15:06:25 +0000146 {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000147 'failed': 29, # category value and total number found of that value
148 'failure-ignored': 948,
149 'no-comparison': 4502,
150 'succeeded': 38609,
epoger@google.comafaad3d2013-09-30 15:06:25 +0000151 },
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000152 'builder':
epoger@google.comafaad3d2013-09-30 15:06:25 +0000153 {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000154 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286,
155 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134,
epoger@google.comafaad3d2013-09-30 15:06:25 +0000156 ...
157 },
158 ... # other categories from CATEGORIES_TO_SUMMARIZE
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000159 }, # end of 'categories' dictionary
epoger@google.comafaad3d2013-09-30 15:06:25 +0000160
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000161 'testData': # list of test results, with a dictionary for each
epoger@google.comafaad3d2013-09-30 15:06:25 +0000162 [
163 {
epoger@google.com055e3b52013-10-26 14:31:11 +0000164 'resultType': 'failed',
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000165 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug',
166 'test': 'bigmatrix',
167 'config': '8888',
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000168 'expectedHashType': 'bitmap-64bitMD5',
169 'expectedHashDigest': '10894408024079689926',
170 'actualHashType': 'bitmap-64bitMD5',
171 'actualHashDigest': '2409857384569',
epoger@google.com055e3b52013-10-26 14:31:11 +0000172 'bugs': [123, 456],
173 'ignore-failure': false,
174 'reviewed-by-human': true,
epoger@google.comafaad3d2013-09-30 15:06:25 +0000175 },
176 ...
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000177 ], # end of 'testData' list
epoger@google.comafaad3d2013-09-30 15:06:25 +0000178 }
epoger@google.comf9d134d2013-09-27 15:02:44 +0000179 """
epoger@google.comdcb4e652013-10-11 18:45:33 +0000180 return self._results[type]
epoger@google.comf9d134d2013-09-27 15:02:44 +0000181
182 @staticmethod
commit-bot@chromium.org04747a52014-01-15 19:16:09 +0000183 def _ignore_builder(builder):
184 """Returns True if we should ignore expectations and actuals for a builder.
185
186 This allows us to ignore builders for which we don't maintain expectations
187 (trybots, Valgrind, ASAN, TSAN), and avoid problems like
188 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server
189 produces error when trying to add baselines for ASAN/TSAN builders')
190
191 Args:
192 builder: name of this builder, as a string
193
194 Returns:
195 True if we should ignore expectations and actuals for this builder.
196 """
197 return (builder.endswith('-Trybot') or
198 ('Valgrind' in builder) or
199 ('TSAN' in builder) or
200 ('ASAN' in builder))
201
202 @staticmethod
epoger@google.comeb832592013-10-23 15:07:26 +0000203 def _read_dicts_from_root(root, pattern='*.json'):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000204 """Read all JSON dictionaries within a directory tree.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000205
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000206 Args:
epoger@google.comf9d134d2013-09-27 15:02:44 +0000207 root: path to root of directory tree
208 pattern: which files to read within root (fnmatch-style pattern)
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000209
210 Returns:
211 A meta-dictionary containing all the JSON dictionaries found within
212 the directory tree, keyed by the builder name of each dictionary.
epoger@google.com542b65f2013-10-15 20:10:33 +0000213
214 Raises:
215 IOError if root does not refer to an existing directory
epoger@google.comf9d134d2013-09-27 15:02:44 +0000216 """
epoger@google.com542b65f2013-10-15 20:10:33 +0000217 if not os.path.isdir(root):
218 raise IOError('no directory found at path %s' % root)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000219 meta_dict = {}
220 for dirpath, dirnames, filenames in os.walk(root):
221 for matching_filename in fnmatch.filter(filenames, pattern):
222 builder = os.path.basename(dirpath)
commit-bot@chromium.org04747a52014-01-15 19:16:09 +0000223 if Results._ignore_builder(builder):
epoger@google.comf9d134d2013-09-27 15:02:44 +0000224 continue
225 fullpath = os.path.join(dirpath, matching_filename)
226 meta_dict[builder] = gm_json.LoadFromFile(fullpath)
227 return meta_dict
228
epoger@google.comeb832592013-10-23 15:07:26 +0000229 @staticmethod
230 def _write_dicts_to_root(meta_dict, root, pattern='*.json'):
231 """Write all per-builder dictionaries within meta_dict to files under
232 the root path.
233
234 Security note: this will only write to files that already exist within
235 the root path (as found by os.walk() within root), so we don't need to
236 worry about malformed content writing to disk outside of root.
237 However, the data written to those files is not double-checked, so it
238 could contain poisonous data.
239
240 Args:
241 meta_dict: a builder-keyed meta-dictionary containing all the JSON
242 dictionaries we want to write out
243 root: path to root of directory tree within which to write files
244 pattern: which files to write within root (fnmatch-style pattern)
245
246 Raises:
247 IOError if root does not refer to an existing directory
248 KeyError if the set of per-builder dictionaries written out was
249 different than expected
250 """
251 if not os.path.isdir(root):
252 raise IOError('no directory found at path %s' % root)
253 actual_builders_written = []
254 for dirpath, dirnames, filenames in os.walk(root):
255 for matching_filename in fnmatch.filter(filenames, pattern):
256 builder = os.path.basename(dirpath)
commit-bot@chromium.org04747a52014-01-15 19:16:09 +0000257 if Results._ignore_builder(builder):
epoger@google.comeb832592013-10-23 15:07:26 +0000258 continue
259 per_builder_dict = meta_dict.get(builder)
commit-bot@chromium.org7dd5d6e2013-12-11 20:19:42 +0000260 if per_builder_dict is not None:
epoger@google.comeb832592013-10-23 15:07:26 +0000261 fullpath = os.path.join(dirpath, matching_filename)
262 gm_json.WriteToFile(per_builder_dict, fullpath)
263 actual_builders_written.append(builder)
264
265 # Check: did we write out the set of per-builder dictionaries we
266 # expected to?
267 expected_builders_written = sorted(meta_dict.keys())
268 actual_builders_written.sort()
269 if expected_builders_written != actual_builders_written:
270 raise KeyError(
271 'expected to write dicts for builders %s, but actually wrote them '
272 'for builders %s' % (
273 expected_builders_written, actual_builders_written))
274
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000275 def _generate_pixel_diffs_if_needed(self, test, expected_image, actual_image):
276 """If expected_image and actual_image both exist but are different,
277 add the image pair to self._image_diff_db and generate pixel diffs.
278
279 Args:
280 test: string; name of test
281 expected_image: (hashType, hashDigest) tuple describing the expected image
282 actual_image: (hashType, hashDigest) tuple describing the actual image
283 """
284 if expected_image == actual_image:
285 return
286
287 (expected_hashtype, expected_hashdigest) = expected_image
288 (actual_hashtype, actual_hashdigest) = actual_image
289 if None in [expected_hashtype, expected_hashdigest,
290 actual_hashtype, actual_hashdigest]:
291 return
292
293 expected_url = gm_json.CreateGmActualUrl(
294 test_name=test, hash_type=expected_hashtype,
295 hash_digest=expected_hashdigest)
296 actual_url = gm_json.CreateGmActualUrl(
297 test_name=test, hash_type=actual_hashtype,
298 hash_digest=actual_hashdigest)
299 self._image_diff_db.add_image_pair(
300 expected_image_locator=expected_hashdigest,
301 expected_image_url=expected_url,
302 actual_image_locator=actual_hashdigest,
303 actual_image_url=actual_url)
304
epoger@google.comeb832592013-10-23 15:07:26 +0000305 def _load_actual_and_expected(self):
306 """Loads the results of all tests, across all builders (based on the
307 files within self._actuals_root and self._expected_root),
epoger@google.comdcb4e652013-10-11 18:45:33 +0000308 and stores them in self._results.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000309 """
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000310 logging.info('Reading actual-results JSON files from %s...' %
311 self._actuals_root)
epoger@google.comeb832592013-10-23 15:07:26 +0000312 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root)
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000313 logging.info('Reading expected-results JSON files from %s...' %
314 self._expected_root)
epoger@google.comeb832592013-10-23 15:07:26 +0000315 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
316
epoger@google.comdcb4e652013-10-11 18:45:33 +0000317 categories_all = {}
318 categories_failures = {}
epoger@google.com055e3b52013-10-26 14:31:11 +0000319
epoger@google.comdcb4e652013-10-11 18:45:33 +0000320 Results._ensure_included_in_category_dict(categories_all,
321 'resultType', [
epoger@google.com5f2bb002013-10-02 18:57:48 +0000322 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
323 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
324 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
325 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED,
326 ])
epoger@google.comdcb4e652013-10-11 18:45:33 +0000327 Results._ensure_included_in_category_dict(categories_failures,
328 'resultType', [
329 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
330 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
331 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
332 ])
epoger@google.com5f2bb002013-10-02 18:57:48 +0000333
epoger@google.comdcb4e652013-10-11 18:45:33 +0000334 data_all = []
335 data_failures = []
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000336 builders = sorted(actual_builder_dicts.keys())
337 num_builders = len(builders)
338 builder_num = 0
339 for builder in builders:
340 builder_num += 1
341 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' %
342 (builder_num, num_builders, builder))
epoger@google.comf9d134d2013-09-27 15:02:44 +0000343 actual_results_for_this_builder = (
epoger@google.comeb832592013-10-23 15:07:26 +0000344 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
epoger@google.comf9d134d2013-09-27 15:02:44 +0000345 for result_type in sorted(actual_results_for_this_builder.keys()):
346 results_of_this_type = actual_results_for_this_builder[result_type]
347 if not results_of_this_type:
348 continue
349 for image_name in sorted(results_of_this_type.keys()):
350 actual_image = results_of_this_type[image_name]
epoger@google.com055e3b52013-10-26 14:31:11 +0000351
352 # Default empty expectations; overwrite these if we find any real ones
353 expectations_per_test = None
354 expected_image = [None, None]
epoger@google.comf9d134d2013-09-27 15:02:44 +0000355 try:
epoger@google.com055e3b52013-10-26 14:31:11 +0000356 expectations_per_test = (
357 expected_builder_dicts
358 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name])
epoger@google.comf9d134d2013-09-27 15:02:44 +0000359 # TODO(epoger): assumes a single allowed digest per test
360 expected_image = (
epoger@google.com055e3b52013-10-26 14:31:11 +0000361 expectations_per_test
362 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0])
epoger@google.comf9d134d2013-09-27 15:02:44 +0000363 except (KeyError, TypeError):
364 # There are several cases in which we would expect to find
365 # no expectations for a given test:
366 #
367 # 1. result_type == NOCOMPARISON
368 # There are no expectations for this test yet!
369 #
epoger@google.com055e3b52013-10-26 14:31:11 +0000370 # 2. alternate rendering mode failures (e.g. serialized)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000371 # In cases like
372 # https://code.google.com/p/skia/issues/detail?id=1684
373 # ('tileimagefilter GM test failing in serialized render mode'),
374 # the gm-actuals will list a failure for the alternate
375 # rendering mode even though we don't have explicit expectations
376 # for the test (the implicit expectation is that it must
377 # render the same in all rendering modes).
378 #
epoger@google.com055e3b52013-10-26 14:31:11 +0000379 # Don't log type 1, because it is common.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000380 # Log other types, because they are rare and we should know about
381 # them, but don't throw an exception, because we need to keep our
382 # tools working in the meanwhile!
epoger@google.com055e3b52013-10-26 14:31:11 +0000383 if result_type != gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON:
epoger@google.comdcb4e652013-10-11 18:45:33 +0000384 logging.warning('No expectations found for test: %s' % {
epoger@google.comf9d134d2013-09-27 15:02:44 +0000385 'builder': builder,
386 'image_name': image_name,
387 'result_type': result_type,
epoger@google.comdcb4e652013-10-11 18:45:33 +0000388 })
epoger@google.comf9d134d2013-09-27 15:02:44 +0000389
390 # If this test was recently rebaselined, it will remain in
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000391 # the 'failed' set of actuals until all the bots have
epoger@google.comf9d134d2013-09-27 15:02:44 +0000392 # cycled (although the expectations have indeed been set
393 # from the most recent actuals). Treat these as successes
394 # instead of failures.
395 #
396 # TODO(epoger): Do we need to do something similar in
397 # other cases, such as when we have recently marked a test
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000398 # as ignoreFailure but it still shows up in the 'failed'
epoger@google.comf9d134d2013-09-27 15:02:44 +0000399 # category? Maybe we should not rely on the result_type
400 # categories recorded within the gm_actuals AT ALL, and
401 # instead evaluate the result_type ourselves based on what
402 # we see in expectations vs actual checksum?
403 if expected_image == actual_image:
404 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED
405 else:
406 updated_result_type = result_type
407
epoger@google.comf9d134d2013-09-27 15:02:44 +0000408 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups()
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000409 self._generate_pixel_diffs_if_needed(
410 test=test, expected_image=expected_image,
411 actual_image=actual_image)
epoger@google.comafaad3d2013-09-30 15:06:25 +0000412 results_for_this_test = {
epoger@google.com055e3b52013-10-26 14:31:11 +0000413 'resultType': updated_result_type,
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000414 'builder': builder,
415 'test': test,
416 'config': config,
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000417 'actualHashType': actual_image[0],
418 'actualHashDigest': str(actual_image[1]),
419 'expectedHashType': expected_image[0],
420 'expectedHashDigest': str(expected_image[1]),
epoger@google.com055e3b52013-10-26 14:31:11 +0000421
422 # FIELDS_PASSED_THRU_VERBATIM that may be overwritten below...
423 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False,
epoger@google.comafaad3d2013-09-30 15:06:25 +0000424 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000425 if expectations_per_test:
426 for field in FIELDS_PASSED_THRU_VERBATIM:
427 results_for_this_test[field] = expectations_per_test.get(field)
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000428
429 if updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON:
430 pass # no diff record to calculate at all
431 elif updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED:
epoger@google.com214a0242013-11-22 19:26:18 +0000432 results_for_this_test['numDifferingPixels'] = 0
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000433 results_for_this_test['percentDifferingPixels'] = 0
434 results_for_this_test['weightedDiffMeasure'] = 0
commit-bot@chromium.org44546f82014-02-11 18:21:26 +0000435 results_for_this_test['perceptualDifference'] = 0
epoger@google.com214a0242013-11-22 19:26:18 +0000436 results_for_this_test['maxDiffPerChannel'] = 0
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000437 else:
438 try:
439 diff_record = self._image_diff_db.get_diff_record(
440 expected_image_locator=expected_image[1],
441 actual_image_locator=actual_image[1])
epoger@google.com214a0242013-11-22 19:26:18 +0000442 results_for_this_test['numDifferingPixels'] = (
443 diff_record.get_num_pixels_differing())
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000444 results_for_this_test['percentDifferingPixels'] = (
445 diff_record.get_percent_pixels_differing())
446 results_for_this_test['weightedDiffMeasure'] = (
447 diff_record.get_weighted_diff_measure())
commit-bot@chromium.org44546f82014-02-11 18:21:26 +0000448 results_for_this_test['perceptualDifference'] = (
449 diff_record.get_perceptual_difference())
epoger@google.com214a0242013-11-22 19:26:18 +0000450 results_for_this_test['maxDiffPerChannel'] = (
451 diff_record.get_max_diff_per_channel())
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000452 except KeyError:
453 logging.warning('unable to find diff_record for ("%s", "%s")' %
454 (expected_image[1], actual_image[1]))
455 pass
456
epoger@google.comdcb4e652013-10-11 18:45:33 +0000457 Results._add_to_category_dict(categories_all, results_for_this_test)
458 data_all.append(results_for_this_test)
epoger@google.com055e3b52013-10-26 14:31:11 +0000459
460 # TODO(epoger): In effect, we have a list of resultTypes that we
461 # include in the different result lists (data_all and data_failures).
462 # This same list should be used by the calls to
463 # Results._ensure_included_in_category_dict() earlier on.
epoger@google.comdcb4e652013-10-11 18:45:33 +0000464 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED:
465 Results._add_to_category_dict(categories_failures,
epoger@google.com055e3b52013-10-26 14:31:11 +0000466 results_for_this_test)
epoger@google.comdcb4e652013-10-11 18:45:33 +0000467 data_failures.append(results_for_this_test)
468
469 self._results = {
470 RESULTS_ALL:
471 {'categories': categories_all, 'testData': data_all},
472 RESULTS_FAILURES:
473 {'categories': categories_failures, 'testData': data_failures},
474 }
epoger@google.comafaad3d2013-09-30 15:06:25 +0000475
476 @staticmethod
epoger@google.comdcb4e652013-10-11 18:45:33 +0000477 def _add_to_category_dict(category_dict, test_results):
epoger@google.com5f2bb002013-10-02 18:57:48 +0000478 """Add test_results to the category dictionary we are building.
epoger@google.comdcb4e652013-10-11 18:45:33 +0000479 (See documentation of self.get_results_of_type() for the format of this
480 dictionary.)
epoger@google.comafaad3d2013-09-30 15:06:25 +0000481
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000482 Args:
epoger@google.comafaad3d2013-09-30 15:06:25 +0000483 category_dict: category dict-of-dicts to add to; modify this in-place
484 test_results: test data with which to update category_list, in a dict:
485 {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000486 'category_name': 'category_value',
487 'category_name': 'category_value',
epoger@google.comafaad3d2013-09-30 15:06:25 +0000488 ...
489 }
490 """
491 for category in CATEGORIES_TO_SUMMARIZE:
492 category_value = test_results.get(category)
epoger@google.comafaad3d2013-09-30 15:06:25 +0000493 if not category_dict.get(category):
494 category_dict[category] = {}
495 if not category_dict[category].get(category_value):
496 category_dict[category][category_value] = 0
497 category_dict[category][category_value] += 1
epoger@google.com5f2bb002013-10-02 18:57:48 +0000498
499 @staticmethod
epoger@google.comdcb4e652013-10-11 18:45:33 +0000500 def _ensure_included_in_category_dict(category_dict,
501 category_name, category_values):
epoger@google.com5f2bb002013-10-02 18:57:48 +0000502 """Ensure that the category name/value pairs are included in category_dict,
503 even if there aren't any results with that name/value pair.
epoger@google.comdcb4e652013-10-11 18:45:33 +0000504 (See documentation of self.get_results_of_type() for the format of this
505 dictionary.)
epoger@google.com5f2bb002013-10-02 18:57:48 +0000506
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000507 Args:
epoger@google.com5f2bb002013-10-02 18:57:48 +0000508 category_dict: category dict-of-dicts to modify
509 category_name: category name, as a string
510 category_values: list of values we want to make sure are represented
511 for this category
512 """
513 if not category_dict.get(category_name):
514 category_dict[category_name] = {}
515 for category_value in category_values:
516 if not category_dict[category_name].get(category_value):
517 category_dict[category_name][category_value] = 0
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000518
519
520def main():
521 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
522 datefmt='%m/%d/%Y %H:%M:%S',
523 level=logging.INFO)
524 parser = argparse.ArgumentParser()
525 parser.add_argument(
526 '--actuals', required=True,
527 help='Directory containing all actual-result JSON files')
528 parser.add_argument(
529 '--expectations', required=True,
530 help='Directory containing all expected-result JSON files')
531 parser.add_argument(
532 '--outfile', required=True,
533 help='File to write result summary into, in JSON format')
534 parser.add_argument(
535 '--workdir', default='.workdir',
536 help='Directory within which to download images and generate diffs')
537 args = parser.parse_args()
538 results = Results(actuals_root=args.actuals,
539 expected_root=args.expectations,
540 generated_images_root=args.workdir)
541 gm_json.WriteToFile(results.get_results_of_type(RESULTS_ALL), args.outfile)
542
543
544if __name__ == '__main__':
545 main()