blob: 8fda3dfb42a1eb5e6862703f44cae6084e0a5448 [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
epoger@google.comeb832592013-10-23 15:07:26 +0000183 def _read_dicts_from_root(root, pattern='*.json'):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000184 """Read all JSON dictionaries within a directory tree.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000185
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000186 Args:
epoger@google.comf9d134d2013-09-27 15:02:44 +0000187 root: path to root of directory tree
188 pattern: which files to read within root (fnmatch-style pattern)
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000189
190 Returns:
191 A meta-dictionary containing all the JSON dictionaries found within
192 the directory tree, keyed by the builder name of each dictionary.
epoger@google.com542b65f2013-10-15 20:10:33 +0000193
194 Raises:
195 IOError if root does not refer to an existing directory
epoger@google.comf9d134d2013-09-27 15:02:44 +0000196 """
epoger@google.com542b65f2013-10-15 20:10:33 +0000197 if not os.path.isdir(root):
198 raise IOError('no directory found at path %s' % root)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000199 meta_dict = {}
200 for dirpath, dirnames, filenames in os.walk(root):
201 for matching_filename in fnmatch.filter(filenames, pattern):
202 builder = os.path.basename(dirpath)
epoger@google.comeb832592013-10-23 15:07:26 +0000203 # If we are reading from the collection of actual results, skip over
204 # the Trybot results (we don't maintain baselines for them).
epoger@google.comf9d134d2013-09-27 15:02:44 +0000205 if builder.endswith('-Trybot'):
206 continue
207 fullpath = os.path.join(dirpath, matching_filename)
208 meta_dict[builder] = gm_json.LoadFromFile(fullpath)
209 return meta_dict
210
epoger@google.comeb832592013-10-23 15:07:26 +0000211 @staticmethod
212 def _write_dicts_to_root(meta_dict, root, pattern='*.json'):
213 """Write all per-builder dictionaries within meta_dict to files under
214 the root path.
215
216 Security note: this will only write to files that already exist within
217 the root path (as found by os.walk() within root), so we don't need to
218 worry about malformed content writing to disk outside of root.
219 However, the data written to those files is not double-checked, so it
220 could contain poisonous data.
221
222 Args:
223 meta_dict: a builder-keyed meta-dictionary containing all the JSON
224 dictionaries we want to write out
225 root: path to root of directory tree within which to write files
226 pattern: which files to write within root (fnmatch-style pattern)
227
228 Raises:
229 IOError if root does not refer to an existing directory
230 KeyError if the set of per-builder dictionaries written out was
231 different than expected
232 """
233 if not os.path.isdir(root):
234 raise IOError('no directory found at path %s' % root)
235 actual_builders_written = []
236 for dirpath, dirnames, filenames in os.walk(root):
237 for matching_filename in fnmatch.filter(filenames, pattern):
238 builder = os.path.basename(dirpath)
239 # We should never encounter Trybot *expectations*, but if we are
240 # writing into the actual-results dir, skip the Trybot actuals.
241 # (I don't know why we would ever write into the actual-results dir,
242 # though.)
243 if builder.endswith('-Trybot'):
244 continue
245 per_builder_dict = meta_dict.get(builder)
commit-bot@chromium.org7dd5d6e2013-12-11 20:19:42 +0000246 if per_builder_dict is not None:
epoger@google.comeb832592013-10-23 15:07:26 +0000247 fullpath = os.path.join(dirpath, matching_filename)
248 gm_json.WriteToFile(per_builder_dict, fullpath)
249 actual_builders_written.append(builder)
250
251 # Check: did we write out the set of per-builder dictionaries we
252 # expected to?
253 expected_builders_written = sorted(meta_dict.keys())
254 actual_builders_written.sort()
255 if expected_builders_written != actual_builders_written:
256 raise KeyError(
257 'expected to write dicts for builders %s, but actually wrote them '
258 'for builders %s' % (
259 expected_builders_written, actual_builders_written))
260
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000261 def _generate_pixel_diffs_if_needed(self, test, expected_image, actual_image):
262 """If expected_image and actual_image both exist but are different,
263 add the image pair to self._image_diff_db and generate pixel diffs.
264
265 Args:
266 test: string; name of test
267 expected_image: (hashType, hashDigest) tuple describing the expected image
268 actual_image: (hashType, hashDigest) tuple describing the actual image
269 """
270 if expected_image == actual_image:
271 return
272
273 (expected_hashtype, expected_hashdigest) = expected_image
274 (actual_hashtype, actual_hashdigest) = actual_image
275 if None in [expected_hashtype, expected_hashdigest,
276 actual_hashtype, actual_hashdigest]:
277 return
278
279 expected_url = gm_json.CreateGmActualUrl(
280 test_name=test, hash_type=expected_hashtype,
281 hash_digest=expected_hashdigest)
282 actual_url = gm_json.CreateGmActualUrl(
283 test_name=test, hash_type=actual_hashtype,
284 hash_digest=actual_hashdigest)
285 self._image_diff_db.add_image_pair(
286 expected_image_locator=expected_hashdigest,
287 expected_image_url=expected_url,
288 actual_image_locator=actual_hashdigest,
289 actual_image_url=actual_url)
290
epoger@google.comeb832592013-10-23 15:07:26 +0000291 def _load_actual_and_expected(self):
292 """Loads the results of all tests, across all builders (based on the
293 files within self._actuals_root and self._expected_root),
epoger@google.comdcb4e652013-10-11 18:45:33 +0000294 and stores them in self._results.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000295 """
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000296 logging.info('Reading actual-results JSON files from %s...' %
297 self._actuals_root)
epoger@google.comeb832592013-10-23 15:07:26 +0000298 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root)
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000299 logging.info('Reading expected-results JSON files from %s...' %
300 self._expected_root)
epoger@google.comeb832592013-10-23 15:07:26 +0000301 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
302
epoger@google.comdcb4e652013-10-11 18:45:33 +0000303 categories_all = {}
304 categories_failures = {}
epoger@google.com055e3b52013-10-26 14:31:11 +0000305
epoger@google.comdcb4e652013-10-11 18:45:33 +0000306 Results._ensure_included_in_category_dict(categories_all,
307 'resultType', [
epoger@google.com5f2bb002013-10-02 18:57:48 +0000308 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
309 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
310 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
311 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED,
312 ])
epoger@google.comdcb4e652013-10-11 18:45:33 +0000313 Results._ensure_included_in_category_dict(categories_failures,
314 'resultType', [
315 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
316 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
317 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
318 ])
epoger@google.com5f2bb002013-10-02 18:57:48 +0000319
epoger@google.comdcb4e652013-10-11 18:45:33 +0000320 data_all = []
321 data_failures = []
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000322 builders = sorted(actual_builder_dicts.keys())
323 num_builders = len(builders)
324 builder_num = 0
325 for builder in builders:
326 builder_num += 1
327 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' %
328 (builder_num, num_builders, builder))
epoger@google.comf9d134d2013-09-27 15:02:44 +0000329 actual_results_for_this_builder = (
epoger@google.comeb832592013-10-23 15:07:26 +0000330 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
epoger@google.comf9d134d2013-09-27 15:02:44 +0000331 for result_type in sorted(actual_results_for_this_builder.keys()):
332 results_of_this_type = actual_results_for_this_builder[result_type]
333 if not results_of_this_type:
334 continue
335 for image_name in sorted(results_of_this_type.keys()):
336 actual_image = results_of_this_type[image_name]
epoger@google.com055e3b52013-10-26 14:31:11 +0000337
338 # Default empty expectations; overwrite these if we find any real ones
339 expectations_per_test = None
340 expected_image = [None, None]
epoger@google.comf9d134d2013-09-27 15:02:44 +0000341 try:
epoger@google.com055e3b52013-10-26 14:31:11 +0000342 expectations_per_test = (
343 expected_builder_dicts
344 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name])
epoger@google.comf9d134d2013-09-27 15:02:44 +0000345 # TODO(epoger): assumes a single allowed digest per test
346 expected_image = (
epoger@google.com055e3b52013-10-26 14:31:11 +0000347 expectations_per_test
348 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0])
epoger@google.comf9d134d2013-09-27 15:02:44 +0000349 except (KeyError, TypeError):
350 # There are several cases in which we would expect to find
351 # no expectations for a given test:
352 #
353 # 1. result_type == NOCOMPARISON
354 # There are no expectations for this test yet!
355 #
epoger@google.com055e3b52013-10-26 14:31:11 +0000356 # 2. alternate rendering mode failures (e.g. serialized)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000357 # In cases like
358 # https://code.google.com/p/skia/issues/detail?id=1684
359 # ('tileimagefilter GM test failing in serialized render mode'),
360 # the gm-actuals will list a failure for the alternate
361 # rendering mode even though we don't have explicit expectations
362 # for the test (the implicit expectation is that it must
363 # render the same in all rendering modes).
364 #
epoger@google.com055e3b52013-10-26 14:31:11 +0000365 # Don't log type 1, because it is common.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000366 # Log other types, because they are rare and we should know about
367 # them, but don't throw an exception, because we need to keep our
368 # tools working in the meanwhile!
epoger@google.com055e3b52013-10-26 14:31:11 +0000369 if result_type != gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON:
epoger@google.comdcb4e652013-10-11 18:45:33 +0000370 logging.warning('No expectations found for test: %s' % {
epoger@google.comf9d134d2013-09-27 15:02:44 +0000371 'builder': builder,
372 'image_name': image_name,
373 'result_type': result_type,
epoger@google.comdcb4e652013-10-11 18:45:33 +0000374 })
epoger@google.comf9d134d2013-09-27 15:02:44 +0000375
376 # If this test was recently rebaselined, it will remain in
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000377 # the 'failed' set of actuals until all the bots have
epoger@google.comf9d134d2013-09-27 15:02:44 +0000378 # cycled (although the expectations have indeed been set
379 # from the most recent actuals). Treat these as successes
380 # instead of failures.
381 #
382 # TODO(epoger): Do we need to do something similar in
383 # other cases, such as when we have recently marked a test
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000384 # as ignoreFailure but it still shows up in the 'failed'
epoger@google.comf9d134d2013-09-27 15:02:44 +0000385 # category? Maybe we should not rely on the result_type
386 # categories recorded within the gm_actuals AT ALL, and
387 # instead evaluate the result_type ourselves based on what
388 # we see in expectations vs actual checksum?
389 if expected_image == actual_image:
390 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED
391 else:
392 updated_result_type = result_type
393
epoger@google.comf9d134d2013-09-27 15:02:44 +0000394 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups()
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000395 self._generate_pixel_diffs_if_needed(
396 test=test, expected_image=expected_image,
397 actual_image=actual_image)
epoger@google.comafaad3d2013-09-30 15:06:25 +0000398 results_for_this_test = {
epoger@google.com055e3b52013-10-26 14:31:11 +0000399 'resultType': updated_result_type,
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000400 'builder': builder,
401 'test': test,
402 'config': config,
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000403 'actualHashType': actual_image[0],
404 'actualHashDigest': str(actual_image[1]),
405 'expectedHashType': expected_image[0],
406 'expectedHashDigest': str(expected_image[1]),
epoger@google.com055e3b52013-10-26 14:31:11 +0000407
408 # FIELDS_PASSED_THRU_VERBATIM that may be overwritten below...
409 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False,
epoger@google.comafaad3d2013-09-30 15:06:25 +0000410 }
epoger@google.com055e3b52013-10-26 14:31:11 +0000411 if expectations_per_test:
412 for field in FIELDS_PASSED_THRU_VERBATIM:
413 results_for_this_test[field] = expectations_per_test.get(field)
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000414
415 if updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON:
416 pass # no diff record to calculate at all
417 elif updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED:
epoger@google.com214a0242013-11-22 19:26:18 +0000418 results_for_this_test['numDifferingPixels'] = 0
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000419 results_for_this_test['percentDifferingPixels'] = 0
420 results_for_this_test['weightedDiffMeasure'] = 0
epoger@google.com214a0242013-11-22 19:26:18 +0000421 results_for_this_test['maxDiffPerChannel'] = 0
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000422 else:
423 try:
424 diff_record = self._image_diff_db.get_diff_record(
425 expected_image_locator=expected_image[1],
426 actual_image_locator=actual_image[1])
epoger@google.com214a0242013-11-22 19:26:18 +0000427 results_for_this_test['numDifferingPixels'] = (
428 diff_record.get_num_pixels_differing())
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000429 results_for_this_test['percentDifferingPixels'] = (
430 diff_record.get_percent_pixels_differing())
431 results_for_this_test['weightedDiffMeasure'] = (
432 diff_record.get_weighted_diff_measure())
epoger@google.com214a0242013-11-22 19:26:18 +0000433 results_for_this_test['maxDiffPerChannel'] = (
434 diff_record.get_max_diff_per_channel())
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000435 except KeyError:
436 logging.warning('unable to find diff_record for ("%s", "%s")' %
437 (expected_image[1], actual_image[1]))
438 pass
439
epoger@google.comdcb4e652013-10-11 18:45:33 +0000440 Results._add_to_category_dict(categories_all, results_for_this_test)
441 data_all.append(results_for_this_test)
epoger@google.com055e3b52013-10-26 14:31:11 +0000442
443 # TODO(epoger): In effect, we have a list of resultTypes that we
444 # include in the different result lists (data_all and data_failures).
445 # This same list should be used by the calls to
446 # Results._ensure_included_in_category_dict() earlier on.
epoger@google.comdcb4e652013-10-11 18:45:33 +0000447 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED:
448 Results._add_to_category_dict(categories_failures,
epoger@google.com055e3b52013-10-26 14:31:11 +0000449 results_for_this_test)
epoger@google.comdcb4e652013-10-11 18:45:33 +0000450 data_failures.append(results_for_this_test)
451
452 self._results = {
453 RESULTS_ALL:
454 {'categories': categories_all, 'testData': data_all},
455 RESULTS_FAILURES:
456 {'categories': categories_failures, 'testData': data_failures},
457 }
epoger@google.comafaad3d2013-09-30 15:06:25 +0000458
459 @staticmethod
epoger@google.comdcb4e652013-10-11 18:45:33 +0000460 def _add_to_category_dict(category_dict, test_results):
epoger@google.com5f2bb002013-10-02 18:57:48 +0000461 """Add test_results to the category dictionary we are building.
epoger@google.comdcb4e652013-10-11 18:45:33 +0000462 (See documentation of self.get_results_of_type() for the format of this
463 dictionary.)
epoger@google.comafaad3d2013-09-30 15:06:25 +0000464
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000465 Args:
epoger@google.comafaad3d2013-09-30 15:06:25 +0000466 category_dict: category dict-of-dicts to add to; modify this in-place
467 test_results: test data with which to update category_list, in a dict:
468 {
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000469 'category_name': 'category_value',
470 'category_name': 'category_value',
epoger@google.comafaad3d2013-09-30 15:06:25 +0000471 ...
472 }
473 """
474 for category in CATEGORIES_TO_SUMMARIZE:
475 category_value = test_results.get(category)
epoger@google.comafaad3d2013-09-30 15:06:25 +0000476 if not category_dict.get(category):
477 category_dict[category] = {}
478 if not category_dict[category].get(category_value):
479 category_dict[category][category_value] = 0
480 category_dict[category][category_value] += 1
epoger@google.com5f2bb002013-10-02 18:57:48 +0000481
482 @staticmethod
epoger@google.comdcb4e652013-10-11 18:45:33 +0000483 def _ensure_included_in_category_dict(category_dict,
484 category_name, category_values):
epoger@google.com5f2bb002013-10-02 18:57:48 +0000485 """Ensure that the category name/value pairs are included in category_dict,
486 even if there aren't any results with that name/value pair.
epoger@google.comdcb4e652013-10-11 18:45:33 +0000487 (See documentation of self.get_results_of_type() for the format of this
488 dictionary.)
epoger@google.com5f2bb002013-10-02 18:57:48 +0000489
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000490 Args:
epoger@google.com5f2bb002013-10-02 18:57:48 +0000491 category_dict: category dict-of-dicts to modify
492 category_name: category name, as a string
493 category_values: list of values we want to make sure are represented
494 for this category
495 """
496 if not category_dict.get(category_name):
497 category_dict[category_name] = {}
498 for category_value in category_values:
499 if not category_dict[category_name].get(category_value):
500 category_dict[category_name][category_value] = 0
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000501
502
503def main():
504 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
505 datefmt='%m/%d/%Y %H:%M:%S',
506 level=logging.INFO)
507 parser = argparse.ArgumentParser()
508 parser.add_argument(
509 '--actuals', required=True,
510 help='Directory containing all actual-result JSON files')
511 parser.add_argument(
512 '--expectations', required=True,
513 help='Directory containing all expected-result JSON files')
514 parser.add_argument(
515 '--outfile', required=True,
516 help='File to write result summary into, in JSON format')
517 parser.add_argument(
518 '--workdir', default='.workdir',
519 help='Directory within which to download images and generate diffs')
520 args = parser.parse_args()
521 results = Results(actuals_root=args.actuals,
522 expected_root=args.expectations,
523 generated_images_root=args.workdir)
524 gm_json.WriteToFile(results.get_results_of_type(RESULTS_ALL), args.outfile)
525
526
527if __name__ == '__main__':
528 main()