blob: 5c33e309d17a3b126a35306e606485c9249040c8 [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)
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000031TRUNK_DIRECTORY = os.path.dirname(GM_DIRECTORY)
epoger@google.comf9d134d2013-09-27 15:02:44 +000032if GM_DIRECTORY not in sys.path:
33 sys.path.append(GM_DIRECTORY)
34import gm_json
epoger@google.com9dddf6f2013-11-08 16:25:25 +000035import imagediffdb
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000036import imagepair
37import imagepairset
38
39# Keys used to link an image to a particular GM test.
40# NOTE: Keep these in sync with static/constants.js
41KEY__EXPECTATIONS__BUGS = gm_json.JSONKEY_EXPECTEDRESULTS_BUGS
42KEY__EXPECTATIONS__IGNOREFAILURE = gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE
43KEY__EXPECTATIONS__REVIEWED = gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED
44KEY__EXTRACOLUMN__BUILDER = 'builder'
45KEY__EXTRACOLUMN__CONFIG = 'config'
46KEY__EXTRACOLUMN__RESULT_TYPE = 'resultType'
47KEY__EXTRACOLUMN__TEST = 'test'
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000048KEY__HEADER = 'header'
49KEY__HEADER__DATAHASH = 'dataHash'
50KEY__HEADER__IS_EDITABLE = 'isEditable'
51KEY__HEADER__IS_EXPORTED = 'isExported'
52KEY__HEADER__IS_STILL_LOADING = 'resultsStillLoading'
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000053KEY__HEADER__RESULTS_ALL = 'all'
54KEY__HEADER__RESULTS_FAILURES = 'failures'
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000055KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE = 'timeNextUpdateAvailable'
56KEY__HEADER__TIME_UPDATED = 'timeUpdated'
57KEY__HEADER__TYPE = 'type'
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000058KEY__NEW_IMAGE_URL = 'newImageUrl'
59KEY__RESULT_TYPE__FAILED = gm_json.JSONKEY_ACTUALRESULTS_FAILED
60KEY__RESULT_TYPE__FAILUREIGNORED = gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED
61KEY__RESULT_TYPE__NOCOMPARISON = gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON
62KEY__RESULT_TYPE__SUCCEEDED = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED
63
64EXPECTATION_FIELDS_PASSED_THRU_VERBATIM = [
65 KEY__EXPECTATIONS__BUGS,
66 KEY__EXPECTATIONS__IGNOREFAILURE,
67 KEY__EXPECTATIONS__REVIEWED,
68]
epoger@google.comf9d134d2013-09-27 15:02:44 +000069
70IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
epoger@google.comeb832592013-10-23 15:07:26 +000071IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config)
72
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000073IMAGEPAIR_SET_DESCRIPTIONS = ('expected image', 'actual image')
epoger@google.com055e3b52013-10-26 14:31:11 +000074
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000075DEFAULT_ACTUALS_DIR = '.gm-actuals'
76DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
77DEFAULT_GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static',
78 'generated-images')
79
epoger@google.comf9d134d2013-09-27 15:02:44 +000080
81class Results(object):
commit-bot@chromium.org16f41802014-02-26 19:05:20 +000082 """ Loads actual and expected GM results into an ImagePairSet.
83
84 Loads actual and expected results from all builders, except for those skipped
85 by _ignore_builder().
epoger@google.comdcb4e652013-10-11 18:45:33 +000086
epoger@google.comeb832592013-10-23 15:07:26 +000087 Once this object has been constructed, the results (in self._results[])
88 are immutable. If you want to update the results based on updated JSON
89 file contents, you will need to create a new Results object."""
epoger@google.comf9d134d2013-09-27 15:02:44 +000090
commit-bot@chromium.org7498d952014-03-13 14:56:29 +000091 def __init__(self, actuals_root=DEFAULT_ACTUALS_DIR,
92 expected_root=DEFAULT_EXPECTATIONS_DIR,
93 generated_images_root=DEFAULT_GENERATED_IMAGES_ROOT):
epoger@google.comf9d134d2013-09-27 15:02:44 +000094 """
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000095 Args:
epoger@google.comf9d134d2013-09-27 15:02:44 +000096 actuals_root: root directory containing all actual-results.json files
97 expected_root: root directory containing all expected-results.json files
epoger@google.com214a0242013-11-22 19:26:18 +000098 generated_images_root: directory within which to create all pixel diffs;
epoger@google.com9dddf6f2013-11-08 16:25:25 +000099 if this directory does not yet exist, it will be created
epoger@google.comf9d134d2013-09-27 15:02:44 +0000100 """
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000101 time_start = int(time.time())
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000102 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root)
epoger@google.comeb832592013-10-23 15:07:26 +0000103 self._actuals_root = actuals_root
104 self._expected_root = expected_root
105 self._load_actual_and_expected()
epoger@google.com542b65f2013-10-15 20:10:33 +0000106 self._timestamp = int(time.time())
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000107 logging.info('Results complete; took %d seconds.' %
108 (self._timestamp - time_start))
epoger@google.com542b65f2013-10-15 20:10:33 +0000109
110 def get_timestamp(self):
111 """Return the time at which this object was created, in seconds past epoch
112 (UTC).
113 """
114 return self._timestamp
epoger@google.comf9d134d2013-09-27 15:02:44 +0000115
epoger@google.comeb832592013-10-23 15:07:26 +0000116 def edit_expectations(self, modifications):
117 """Edit the expectations stored within this object and write them back
118 to disk.
119
120 Note that this will NOT update the results stored in self._results[] ;
121 in order to see those updates, you must instantiate a new Results object
122 based on the (now updated) files on disk.
123
124 Args:
125 modifications: a list of dictionaries, one for each expectation to update:
126
127 [
128 {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000129 imagepair.KEY__EXPECTATIONS_DATA: {
130 KEY__EXPECTATIONS__BUGS: [123, 456],
131 KEY__EXPECTATIONS__IGNOREFAILURE: false,
132 KEY__EXPECTATIONS__REVIEWED: true,
133 },
134 imagepair.KEY__EXTRA_COLUMN_VALUES: {
135 KEY__EXTRACOLUMN__BUILDER: 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug',
136 KEY__EXTRACOLUMN__CONFIG: '8888',
137 KEY__EXTRACOLUMN__TEST: 'bigmatrix',
138 },
139 KEY__NEW_IMAGE_URL: 'bitmap-64bitMD5/bigmatrix/10894408024079689926.png',
epoger@google.comeb832592013-10-23 15:07:26 +0000140 },
141 ...
142 ]
143
epoger@google.comeb832592013-10-23 15:07:26 +0000144 """
145 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
146 for mod in modifications:
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000147 image_name = IMAGE_FILENAME_FORMATTER % (
148 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__TEST],
149 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__CONFIG])
150 _, hash_type, hash_digest = gm_json.SplitGmRelativeUrl(
151 mod[KEY__NEW_IMAGE_URL])
152 allowed_digests = [[hash_type, int(hash_digest)]]
epoger@google.comeb832592013-10-23 15:07:26 +0000153 new_expectations = {
154 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests,
epoger@google.comeb832592013-10-23 15:07:26 +0000155 }
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000156 for field in EXPECTATION_FIELDS_PASSED_THRU_VERBATIM:
157 value = mod[imagepair.KEY__EXPECTATIONS_DATA].get(field)
epoger@google.com055e3b52013-10-26 14:31:11 +0000158 if value is not None:
159 new_expectations[field] = value
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000160 builder_dict = expected_builder_dicts[
161 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__BUILDER]]
epoger@google.comeb832592013-10-23 15:07:26 +0000162 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS)
163 if not builder_expectations:
164 builder_expectations = {}
165 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations
166 builder_expectations[image_name] = new_expectations
167 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root)
168
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000169 def get_results_of_type(self, results_type):
170 """Return results of some/all tests (depending on 'results_type' parameter).
epoger@google.comdcb4e652013-10-11 18:45:33 +0000171
172 Args:
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000173 results_type: string describing which types of results to include; must
174 be one of the RESULTS_* constants
epoger@google.comdcb4e652013-10-11 18:45:33 +0000175
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000176 Results are returned in a dictionary as output by ImagePairSet.as_dict().
epoger@google.comf9d134d2013-09-27 15:02:44 +0000177 """
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000178 return self._results[results_type]
179
180 def get_packaged_results_of_type(self, results_type, reload_seconds=None,
181 is_editable=False, is_exported=True):
182 """ Package the results of some/all tests as a complete response_dict.
183
184 Args:
185 results_type: string indicating which set of results to return;
186 must be one of the RESULTS_* constants
187 reload_seconds: if specified, note that new results may be available once
188 these results are reload_seconds old
189 is_editable: whether clients are allowed to submit new baselines
190 is_exported: whether these results are being made available to other
191 network hosts
192 """
193 response_dict = self._results[results_type]
194 time_updated = self.get_timestamp()
195 response_dict[KEY__HEADER] = {
196 # Timestamps:
197 # 1. when this data was last updated
198 # 2. when the caller should check back for new data (if ever)
199 KEY__HEADER__TIME_UPDATED: time_updated,
200 KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
201 (time_updated+reload_seconds) if reload_seconds else None),
202
203 # The type we passed to get_results_of_type()
204 KEY__HEADER__TYPE: results_type,
205
206 # Hash of dataset, which the client must return with any edits--
207 # this ensures that the edits were made to a particular dataset.
208 KEY__HEADER__DATAHASH: str(hash(repr(
209 response_dict[imagepairset.KEY__IMAGEPAIRS]))),
210
211 # Whether the server will accept edits back.
212 KEY__HEADER__IS_EDITABLE: is_editable,
213
214 # Whether the service is accessible from other hosts.
215 KEY__HEADER__IS_EXPORTED: is_exported,
216 }
217 return response_dict
epoger@google.comf9d134d2013-09-27 15:02:44 +0000218
219 @staticmethod
commit-bot@chromium.org04747a52014-01-15 19:16:09 +0000220 def _ignore_builder(builder):
221 """Returns True if we should ignore expectations and actuals for a builder.
222
223 This allows us to ignore builders for which we don't maintain expectations
224 (trybots, Valgrind, ASAN, TSAN), and avoid problems like
225 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server
226 produces error when trying to add baselines for ASAN/TSAN builders')
227
228 Args:
229 builder: name of this builder, as a string
230
231 Returns:
232 True if we should ignore expectations and actuals for this builder.
233 """
234 return (builder.endswith('-Trybot') or
235 ('Valgrind' in builder) or
236 ('TSAN' in builder) or
237 ('ASAN' in builder))
238
239 @staticmethod
epoger@google.comeb832592013-10-23 15:07:26 +0000240 def _read_dicts_from_root(root, pattern='*.json'):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000241 """Read all JSON dictionaries within a directory tree.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000242
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000243 Args:
epoger@google.comf9d134d2013-09-27 15:02:44 +0000244 root: path to root of directory tree
245 pattern: which files to read within root (fnmatch-style pattern)
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000246
247 Returns:
248 A meta-dictionary containing all the JSON dictionaries found within
249 the directory tree, keyed by the builder name of each dictionary.
epoger@google.com542b65f2013-10-15 20:10:33 +0000250
251 Raises:
252 IOError if root does not refer to an existing directory
epoger@google.comf9d134d2013-09-27 15:02:44 +0000253 """
epoger@google.com542b65f2013-10-15 20:10:33 +0000254 if not os.path.isdir(root):
255 raise IOError('no directory found at path %s' % root)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000256 meta_dict = {}
257 for dirpath, dirnames, filenames in os.walk(root):
258 for matching_filename in fnmatch.filter(filenames, pattern):
259 builder = os.path.basename(dirpath)
commit-bot@chromium.org04747a52014-01-15 19:16:09 +0000260 if Results._ignore_builder(builder):
epoger@google.comf9d134d2013-09-27 15:02:44 +0000261 continue
262 fullpath = os.path.join(dirpath, matching_filename)
263 meta_dict[builder] = gm_json.LoadFromFile(fullpath)
264 return meta_dict
265
epoger@google.comeb832592013-10-23 15:07:26 +0000266 @staticmethod
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000267 def _create_relative_url(hashtype_and_digest, test_name):
268 """Returns the URL for this image, relative to GM_ACTUALS_ROOT_HTTP_URL.
269
270 If we don't have a record of this image, returns None.
271
272 Args:
273 hashtype_and_digest: (hash_type, hash_digest) tuple, or None if we
274 don't have a record of this image
275 test_name: string; name of the GM test that created this image
276 """
277 if not hashtype_and_digest:
278 return None
279 return gm_json.CreateGmRelativeUrl(
280 test_name=test_name,
281 hash_type=hashtype_and_digest[0],
282 hash_digest=hashtype_and_digest[1])
283
284 @staticmethod
epoger@google.comeb832592013-10-23 15:07:26 +0000285 def _write_dicts_to_root(meta_dict, root, pattern='*.json'):
286 """Write all per-builder dictionaries within meta_dict to files under
287 the root path.
288
289 Security note: this will only write to files that already exist within
290 the root path (as found by os.walk() within root), so we don't need to
291 worry about malformed content writing to disk outside of root.
292 However, the data written to those files is not double-checked, so it
293 could contain poisonous data.
294
295 Args:
296 meta_dict: a builder-keyed meta-dictionary containing all the JSON
297 dictionaries we want to write out
298 root: path to root of directory tree within which to write files
299 pattern: which files to write within root (fnmatch-style pattern)
300
301 Raises:
302 IOError if root does not refer to an existing directory
303 KeyError if the set of per-builder dictionaries written out was
304 different than expected
305 """
306 if not os.path.isdir(root):
307 raise IOError('no directory found at path %s' % root)
308 actual_builders_written = []
309 for dirpath, dirnames, filenames in os.walk(root):
310 for matching_filename in fnmatch.filter(filenames, pattern):
311 builder = os.path.basename(dirpath)
commit-bot@chromium.org04747a52014-01-15 19:16:09 +0000312 if Results._ignore_builder(builder):
epoger@google.comeb832592013-10-23 15:07:26 +0000313 continue
314 per_builder_dict = meta_dict.get(builder)
commit-bot@chromium.org7dd5d6e2013-12-11 20:19:42 +0000315 if per_builder_dict is not None:
epoger@google.comeb832592013-10-23 15:07:26 +0000316 fullpath = os.path.join(dirpath, matching_filename)
317 gm_json.WriteToFile(per_builder_dict, fullpath)
318 actual_builders_written.append(builder)
319
320 # Check: did we write out the set of per-builder dictionaries we
321 # expected to?
322 expected_builders_written = sorted(meta_dict.keys())
323 actual_builders_written.sort()
324 if expected_builders_written != actual_builders_written:
325 raise KeyError(
326 'expected to write dicts for builders %s, but actually wrote them '
327 'for builders %s' % (
328 expected_builders_written, actual_builders_written))
329
330 def _load_actual_and_expected(self):
331 """Loads the results of all tests, across all builders (based on the
332 files within self._actuals_root and self._expected_root),
epoger@google.comdcb4e652013-10-11 18:45:33 +0000333 and stores them in self._results.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000334 """
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000335 logging.info('Reading actual-results JSON files from %s...' %
336 self._actuals_root)
epoger@google.comeb832592013-10-23 15:07:26 +0000337 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root)
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000338 logging.info('Reading expected-results JSON files from %s...' %
339 self._expected_root)
epoger@google.comeb832592013-10-23 15:07:26 +0000340 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
341
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000342 all_image_pairs = imagepairset.ImagePairSet(IMAGEPAIR_SET_DESCRIPTIONS)
343 failing_image_pairs = imagepairset.ImagePairSet(IMAGEPAIR_SET_DESCRIPTIONS)
epoger@google.com055e3b52013-10-26 14:31:11 +0000344
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000345 all_image_pairs.ensure_extra_column_values_in_summary(
346 column_id=KEY__EXTRACOLUMN__RESULT_TYPE, values=[
347 KEY__RESULT_TYPE__FAILED,
348 KEY__RESULT_TYPE__FAILUREIGNORED,
349 KEY__RESULT_TYPE__NOCOMPARISON,
350 KEY__RESULT_TYPE__SUCCEEDED,
epoger@google.com5f2bb002013-10-02 18:57:48 +0000351 ])
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000352 failing_image_pairs.ensure_extra_column_values_in_summary(
353 column_id=KEY__EXTRACOLUMN__RESULT_TYPE, values=[
354 KEY__RESULT_TYPE__FAILED,
355 KEY__RESULT_TYPE__FAILUREIGNORED,
356 KEY__RESULT_TYPE__NOCOMPARISON,
epoger@google.comdcb4e652013-10-11 18:45:33 +0000357 ])
epoger@google.com5f2bb002013-10-02 18:57:48 +0000358
commit-bot@chromium.orga6ecbb82013-12-19 19:08:31 +0000359 builders = sorted(actual_builder_dicts.keys())
360 num_builders = len(builders)
361 builder_num = 0
362 for builder in builders:
363 builder_num += 1
364 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' %
365 (builder_num, num_builders, builder))
epoger@google.comf9d134d2013-09-27 15:02:44 +0000366 actual_results_for_this_builder = (
epoger@google.comeb832592013-10-23 15:07:26 +0000367 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
epoger@google.comf9d134d2013-09-27 15:02:44 +0000368 for result_type in sorted(actual_results_for_this_builder.keys()):
369 results_of_this_type = actual_results_for_this_builder[result_type]
370 if not results_of_this_type:
371 continue
372 for image_name in sorted(results_of_this_type.keys()):
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000373 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups()
374 actual_image_relative_url = Results._create_relative_url(
375 hashtype_and_digest=results_of_this_type[image_name],
376 test_name=test)
epoger@google.com055e3b52013-10-26 14:31:11 +0000377
378 # Default empty expectations; overwrite these if we find any real ones
379 expectations_per_test = None
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000380 expected_image_relative_url = None
381 expectations_dict = None
epoger@google.comf9d134d2013-09-27 15:02:44 +0000382 try:
epoger@google.com055e3b52013-10-26 14:31:11 +0000383 expectations_per_test = (
384 expected_builder_dicts
385 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name])
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000386 # TODO(epoger): assumes a single allowed digest per test, which is
387 # fine; see https://code.google.com/p/skia/issues/detail?id=1787
388 expected_image_hashtype_and_digest = (
epoger@google.com055e3b52013-10-26 14:31:11 +0000389 expectations_per_test
390 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0])
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000391 expected_image_relative_url = Results._create_relative_url(
392 hashtype_and_digest=expected_image_hashtype_and_digest,
393 test_name=test)
394 expectations_dict = {}
395 for field in EXPECTATION_FIELDS_PASSED_THRU_VERBATIM:
396 expectations_dict[field] = expectations_per_test.get(field)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000397 except (KeyError, TypeError):
398 # There are several cases in which we would expect to find
399 # no expectations for a given test:
400 #
401 # 1. result_type == NOCOMPARISON
402 # There are no expectations for this test yet!
403 #
epoger@google.com055e3b52013-10-26 14:31:11 +0000404 # 2. alternate rendering mode failures (e.g. serialized)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000405 # In cases like
406 # https://code.google.com/p/skia/issues/detail?id=1684
407 # ('tileimagefilter GM test failing in serialized render mode'),
408 # the gm-actuals will list a failure for the alternate
409 # rendering mode even though we don't have explicit expectations
410 # for the test (the implicit expectation is that it must
411 # render the same in all rendering modes).
412 #
epoger@google.com055e3b52013-10-26 14:31:11 +0000413 # Don't log type 1, because it is common.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000414 # Log other types, because they are rare and we should know about
415 # them, but don't throw an exception, because we need to keep our
416 # tools working in the meanwhile!
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000417 if result_type != KEY__RESULT_TYPE__NOCOMPARISON:
epoger@google.comdcb4e652013-10-11 18:45:33 +0000418 logging.warning('No expectations found for test: %s' % {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000419 KEY__EXTRACOLUMN__BUILDER: builder,
420 KEY__EXTRACOLUMN__RESULT_TYPE: result_type,
epoger@google.comf9d134d2013-09-27 15:02:44 +0000421 'image_name': image_name,
epoger@google.comdcb4e652013-10-11 18:45:33 +0000422 })
epoger@google.comf9d134d2013-09-27 15:02:44 +0000423
424 # If this test was recently rebaselined, it will remain in
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000425 # the 'failed' set of actuals until all the bots have
epoger@google.comf9d134d2013-09-27 15:02:44 +0000426 # cycled (although the expectations have indeed been set
427 # from the most recent actuals). Treat these as successes
428 # instead of failures.
429 #
430 # TODO(epoger): Do we need to do something similar in
431 # other cases, such as when we have recently marked a test
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000432 # as ignoreFailure but it still shows up in the 'failed'
epoger@google.comf9d134d2013-09-27 15:02:44 +0000433 # category? Maybe we should not rely on the result_type
434 # categories recorded within the gm_actuals AT ALL, and
435 # instead evaluate the result_type ourselves based on what
436 # we see in expectations vs actual checksum?
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000437 if expected_image_relative_url == actual_image_relative_url:
438 updated_result_type = KEY__RESULT_TYPE__SUCCEEDED
epoger@google.comf9d134d2013-09-27 15:02:44 +0000439 else:
440 updated_result_type = result_type
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000441 extra_columns_dict = {
442 KEY__EXTRACOLUMN__RESULT_TYPE: updated_result_type,
443 KEY__EXTRACOLUMN__BUILDER: builder,
444 KEY__EXTRACOLUMN__TEST: test,
445 KEY__EXTRACOLUMN__CONFIG: config,
epoger@google.comafaad3d2013-09-30 15:06:25 +0000446 }
commit-bot@chromium.org3b987592014-03-04 00:58:21 +0000447 try:
448 image_pair = imagepair.ImagePair(
449 image_diff_db=self._image_diff_db,
450 base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
451 imageA_relative_url=expected_image_relative_url,
452 imageB_relative_url=actual_image_relative_url,
453 expectations=expectations_dict,
454 extra_columns=extra_columns_dict)
455 all_image_pairs.add_image_pair(image_pair)
456 if updated_result_type != KEY__RESULT_TYPE__SUCCEEDED:
457 failing_image_pairs.add_image_pair(image_pair)
458 except Exception:
459 logging.exception('got exception while creating new ImagePair')
epoger@google.comdcb4e652013-10-11 18:45:33 +0000460
461 self._results = {
commit-bot@chromium.org16f41802014-02-26 19:05:20 +0000462 KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(),
463 KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(),
epoger@google.comdcb4e652013-10-11 18:45:33 +0000464 }
epoger@google.comafaad3d2013-09-30 15:06:25 +0000465
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000466
467def main():
468 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
469 datefmt='%m/%d/%Y %H:%M:%S',
470 level=logging.INFO)
471 parser = argparse.ArgumentParser()
472 parser.add_argument(
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000473 '--actuals', default=DEFAULT_ACTUALS_DIR,
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000474 help='Directory containing all actual-result JSON files')
475 parser.add_argument(
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000476 '--expectations', default=DEFAULT_EXPECTATIONS_DIR,
477 help='Directory containing all expected-result JSON files; defaults to '
478 '\'%(default)s\' .')
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000479 parser.add_argument(
480 '--outfile', required=True,
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000481 help='File to write result summary into, in JSON format.')
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000482 parser.add_argument(
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000483 '--results', default=KEY__HEADER__RESULTS_FAILURES,
484 help='Which result types to include. Defaults to \'%(default)s\'; '
485 'must be one of ' +
486 str([KEY__HEADER__RESULTS_FAILURES, KEY__HEADER__RESULTS_ALL]))
487 parser.add_argument(
488 '--workdir', default=DEFAULT_GENERATED_IMAGES_ROOT,
489 help='Directory within which to download images and generate diffs; '
490 'defaults to \'%(default)s\' .')
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000491 args = parser.parse_args()
492 results = Results(actuals_root=args.actuals,
493 expected_root=args.expectations,
494 generated_images_root=args.workdir)
commit-bot@chromium.org7498d952014-03-13 14:56:29 +0000495 gm_json.WriteToFile(
496 results.get_packaged_results_of_type(results_type=args.results),
497 args.outfile)
commit-bot@chromium.org7b06c8e2013-12-23 22:47:15 +0000498
499
500if __name__ == '__main__':
501 main()