Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(340)

Side by Side Diff: gm/rebaseline_server/results.py

Issue 178253010: rebaseline_server: use new intermediate JSON format (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: incorporate Ravi's suggestions Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « gm/rebaseline_server/imagepairset_test.py ('k') | gm/rebaseline_server/results_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 2
3 """ 3 """
4 Copyright 2013 Google Inc. 4 Copyright 2013 Google Inc.
5 5
6 Use of this source code is governed by a BSD-style license that can be 6 Use of this source code is governed by a BSD-style license that can be
7 found in the LICENSE file. 7 found in the LICENSE file.
8 8
9 Repackage expected/actual GM results as needed by our HTML rebaseline viewer. 9 Repackage expected/actual GM results as needed by our HTML rebaseline viewer.
10 """ 10 """
(...skipping 14 matching lines...) Expand all
25 # that directory. That script allows us to parse the actual-results.json file 25 # that directory. That script allows us to parse the actual-results.json file
26 # written out by the GM tool. 26 # written out by the GM tool.
27 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* 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. 28 # so any dirs that are already in the PYTHONPATH will be preferred.
29 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 29 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
30 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY) 30 GM_DIRECTORY = os.path.dirname(PARENT_DIRECTORY)
31 if GM_DIRECTORY not in sys.path: 31 if GM_DIRECTORY not in sys.path:
32 sys.path.append(GM_DIRECTORY) 32 sys.path.append(GM_DIRECTORY)
33 import gm_json 33 import gm_json
34 import imagediffdb 34 import imagediffdb
35 import imagepair
36 import imagepairset
37
38 # Keys used to link an image to a particular GM test.
39 # NOTE: Keep these in sync with static/constants.js
40 KEY__EXPECTATIONS__BUGS = gm_json.JSONKEY_EXPECTEDRESULTS_BUGS
41 KEY__EXPECTATIONS__IGNOREFAILURE = gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE
42 KEY__EXPECTATIONS__REVIEWED = gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED
43 KEY__EXTRACOLUMN__BUILDER = 'builder'
44 KEY__EXTRACOLUMN__CONFIG = 'config'
45 KEY__EXTRACOLUMN__RESULT_TYPE = 'resultType'
46 KEY__EXTRACOLUMN__TEST = 'test'
47 KEY__HEADER__RESULTS_ALL = 'all'
48 KEY__HEADER__RESULTS_FAILURES = 'failures'
49 KEY__NEW_IMAGE_URL = 'newImageUrl'
50 KEY__RESULT_TYPE__FAILED = gm_json.JSONKEY_ACTUALRESULTS_FAILED
51 KEY__RESULT_TYPE__FAILUREIGNORED = gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED
52 KEY__RESULT_TYPE__NOCOMPARISON = gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON
53 KEY__RESULT_TYPE__SUCCEEDED = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED
54
55 EXPECTATION_FIELDS_PASSED_THRU_VERBATIM = [
56 KEY__EXPECTATIONS__BUGS,
57 KEY__EXPECTATIONS__IGNOREFAILURE,
58 KEY__EXPECTATIONS__REVIEWED,
59 ]
35 60
36 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) 61 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
37 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config) 62 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config)
38 63
39 FIELDS_PASSED_THRU_VERBATIM = [ 64 IMAGEPAIR_SET_DESCRIPTIONS = ('expected image', 'actual image')
40 gm_json.JSONKEY_EXPECTEDRESULTS_BUGS,
41 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE,
42 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED,
43 ]
44 CATEGORIES_TO_SUMMARIZE = [
45 'builder', 'test', 'config', 'resultType',
46 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE,
47 gm_json.JSONKEY_EXPECTEDRESULTS_REVIEWED,
48 ]
49 65
50 RESULTS_ALL = 'all'
51 RESULTS_FAILURES = 'failures'
52 66
53 class Results(object): 67 class Results(object):
54 """ Loads actual and expected results from all builders, supplying combined 68 """ Loads actual and expected GM results into an ImagePairSet.
55 reports as requested. 69
70 Loads actual and expected results from all builders, except for those skipped
71 by _ignore_builder().
56 72
57 Once this object has been constructed, the results (in self._results[]) 73 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 74 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.""" 75 file contents, you will need to create a new Results object."""
60 76
61 def __init__(self, actuals_root, expected_root, generated_images_root): 77 def __init__(self, actuals_root, expected_root, generated_images_root):
62 """ 78 """
63 Args: 79 Args:
64 actuals_root: root directory containing all actual-results.json files 80 actuals_root: root directory containing all actual-results.json files
65 expected_root: root directory containing all expected-results.json files 81 expected_root: root directory containing all expected-results.json files
(...skipping 21 matching lines...) Expand all
87 103
88 Note that this will NOT update the results stored in self._results[] ; 104 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 105 in order to see those updates, you must instantiate a new Results object
90 based on the (now updated) files on disk. 106 based on the (now updated) files on disk.
91 107
92 Args: 108 Args:
93 modifications: a list of dictionaries, one for each expectation to update: 109 modifications: a list of dictionaries, one for each expectation to update:
94 110
95 [ 111 [
96 { 112 {
97 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', 113 imagepair.KEY__EXPECTATIONS_DATA: {
98 'test': 'bigmatrix', 114 KEY__EXPECTATIONS__BUGS: [123, 456],
99 'config': '8888', 115 KEY__EXPECTATIONS__IGNOREFAILURE: false,
100 'expectedHashType': 'bitmap-64bitMD5', 116 KEY__EXPECTATIONS__REVIEWED: true,
101 'expectedHashDigest': '10894408024079689926', 117 },
102 'bugs': [123, 456], 118 imagepair.KEY__EXTRA_COLUMN_VALUES: {
103 'ignore-failure': false, 119 KEY__EXTRACOLUMN__BUILDER: 'Test-Mac10.6-MacMini4.1-GeForce320M-x 86-Debug',
104 'reviewed-by-human': true, 120 KEY__EXTRACOLUMN__CONFIG: '8888',
121 KEY__EXTRACOLUMN__TEST: 'bigmatrix',
122 },
123 KEY__NEW_IMAGE_URL: 'bitmap-64bitMD5/bigmatrix/10894408024079689926 .png',
105 }, 124 },
106 ... 125 ...
107 ] 126 ]
108 127
109 """ 128 """
110 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) 129 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
111 for mod in modifications: 130 for mod in modifications:
112 image_name = IMAGE_FILENAME_FORMATTER % (mod['test'], mod['config']) 131 image_name = IMAGE_FILENAME_FORMATTER % (
113 # TODO(epoger): assumes a single allowed digest per test 132 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__TEST],
114 allowed_digests = [[mod['expectedHashType'], 133 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__CONFIG])
115 int(mod['expectedHashDigest'])]] 134 _, hash_type, hash_digest = gm_json.SplitGmRelativeUrl(
135 mod[KEY__NEW_IMAGE_URL])
136 allowed_digests = [[hash_type, int(hash_digest)]]
116 new_expectations = { 137 new_expectations = {
117 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests, 138 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests,
118 } 139 }
119 for field in FIELDS_PASSED_THRU_VERBATIM: 140 for field in EXPECTATION_FIELDS_PASSED_THRU_VERBATIM:
120 value = mod.get(field) 141 value = mod[imagepair.KEY__EXPECTATIONS_DATA].get(field)
121 if value is not None: 142 if value is not None:
122 new_expectations[field] = value 143 new_expectations[field] = value
123 builder_dict = expected_builder_dicts[mod['builder']] 144 builder_dict = expected_builder_dicts[
145 mod[imagepair.KEY__EXTRA_COLUMN_VALUES][KEY__EXTRACOLUMN__BUILDER]]
124 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS) 146 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS)
125 if not builder_expectations: 147 if not builder_expectations:
126 builder_expectations = {} 148 builder_expectations = {}
127 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations 149 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations
128 builder_expectations[image_name] = new_expectations 150 builder_expectations[image_name] = new_expectations
129 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root) 151 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root)
130 152
131 def get_results_of_type(self, type): 153 def get_results_of_type(self, type):
132 """Return results of some/all tests (depending on 'type' parameter). 154 """Return results of some/all tests (depending on 'type' parameter).
133 155
134 Args: 156 Args:
135 type: string describing which types of results to include; must be one 157 type: string describing which types of results to include; must be one
136 of the RESULTS_* constants 158 of the RESULTS_* constants
137 159
138 Results are returned as a dictionary in this form: 160 Results are returned in a dictionary as output by ImagePairSet.as_dict().
139
140 {
141 'categories': # dictionary of categories listed in
142 # CATEGORIES_TO_SUMMARIZE, with the number of times
143 # each value appears within its category
144 {
145 'resultType': # category name
146 {
147 'failed': 29, # category value and total number found of that value
148 'failure-ignored': 948,
149 'no-comparison': 4502,
150 'succeeded': 38609,
151 },
152 'builder':
153 {
154 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286,
155 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134,
156 ...
157 },
158 ... # other categories from CATEGORIES_TO_SUMMARIZE
159 }, # end of 'categories' dictionary
160
161 'testData': # list of test results, with a dictionary for each
162 [
163 {
164 'resultType': 'failed',
165 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug',
166 'test': 'bigmatrix',
167 'config': '8888',
168 'expectedHashType': 'bitmap-64bitMD5',
169 'expectedHashDigest': '10894408024079689926',
170 'actualHashType': 'bitmap-64bitMD5',
171 'actualHashDigest': '2409857384569',
172 'bugs': [123, 456],
173 'ignore-failure': false,
174 'reviewed-by-human': true,
175 },
176 ...
177 ], # end of 'testData' list
178 }
179 """ 161 """
180 return self._results[type] 162 return self._results[type]
181 163
182 @staticmethod 164 @staticmethod
183 def _ignore_builder(builder): 165 def _ignore_builder(builder):
184 """Returns True if we should ignore expectations and actuals for a builder. 166 """Returns True if we should ignore expectations and actuals for a builder.
185 167
186 This allows us to ignore builders for which we don't maintain expectations 168 This allows us to ignore builders for which we don't maintain expectations
187 (trybots, Valgrind, ASAN, TSAN), and avoid problems like 169 (trybots, Valgrind, ASAN, TSAN), and avoid problems like
188 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server 170 https://code.google.com/p/skia/issues/detail?id=2036 ('rebaseline_server
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
220 for dirpath, dirnames, filenames in os.walk(root): 202 for dirpath, dirnames, filenames in os.walk(root):
221 for matching_filename in fnmatch.filter(filenames, pattern): 203 for matching_filename in fnmatch.filter(filenames, pattern):
222 builder = os.path.basename(dirpath) 204 builder = os.path.basename(dirpath)
223 if Results._ignore_builder(builder): 205 if Results._ignore_builder(builder):
224 continue 206 continue
225 fullpath = os.path.join(dirpath, matching_filename) 207 fullpath = os.path.join(dirpath, matching_filename)
226 meta_dict[builder] = gm_json.LoadFromFile(fullpath) 208 meta_dict[builder] = gm_json.LoadFromFile(fullpath)
227 return meta_dict 209 return meta_dict
228 210
229 @staticmethod 211 @staticmethod
212 def _create_relative_url(hashtype_and_digest, test_name):
213 """Returns the URL for this image, relative to GM_ACTUALS_ROOT_HTTP_URL.
214
215 If we don't have a record of this image, returns None.
216
217 Args:
218 hashtype_and_digest: (hash_type, hash_digest) tuple, or None if we
219 don't have a record of this image
220 test_name: string; name of the GM test that created this image
221 """
222 if not hashtype_and_digest:
223 return None
224 return gm_json.CreateGmRelativeUrl(
225 test_name=test_name,
226 hash_type=hashtype_and_digest[0],
227 hash_digest=hashtype_and_digest[1])
228
229 @staticmethod
230 def _write_dicts_to_root(meta_dict, root, pattern='*.json'): 230 def _write_dicts_to_root(meta_dict, root, pattern='*.json'):
231 """Write all per-builder dictionaries within meta_dict to files under 231 """Write all per-builder dictionaries within meta_dict to files under
232 the root path. 232 the root path.
233 233
234 Security note: this will only write to files that already exist within 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 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. 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 237 However, the data written to those files is not double-checked, so it
238 could contain poisonous data. 238 could contain poisonous data.
239 239
(...skipping 25 matching lines...) Expand all
265 # Check: did we write out the set of per-builder dictionaries we 265 # Check: did we write out the set of per-builder dictionaries we
266 # expected to? 266 # expected to?
267 expected_builders_written = sorted(meta_dict.keys()) 267 expected_builders_written = sorted(meta_dict.keys())
268 actual_builders_written.sort() 268 actual_builders_written.sort()
269 if expected_builders_written != actual_builders_written: 269 if expected_builders_written != actual_builders_written:
270 raise KeyError( 270 raise KeyError(
271 'expected to write dicts for builders %s, but actually wrote them ' 271 'expected to write dicts for builders %s, but actually wrote them '
272 'for builders %s' % ( 272 'for builders %s' % (
273 expected_builders_written, actual_builders_written)) 273 expected_builders_written, actual_builders_written))
274 274
275 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
305 def _load_actual_and_expected(self): 275 def _load_actual_and_expected(self):
306 """Loads the results of all tests, across all builders (based on the 276 """Loads the results of all tests, across all builders (based on the
307 files within self._actuals_root and self._expected_root), 277 files within self._actuals_root and self._expected_root),
308 and stores them in self._results. 278 and stores them in self._results.
309 """ 279 """
310 logging.info('Reading actual-results JSON files from %s...' % 280 logging.info('Reading actual-results JSON files from %s...' %
311 self._actuals_root) 281 self._actuals_root)
312 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root) 282 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root)
313 logging.info('Reading expected-results JSON files from %s...' % 283 logging.info('Reading expected-results JSON files from %s...' %
314 self._expected_root) 284 self._expected_root)
315 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root) 285 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
316 286
317 categories_all = {} 287 all_image_pairs = imagepairset.ImagePairSet(IMAGEPAIR_SET_DESCRIPTIONS)
318 categories_failures = {} 288 failing_image_pairs = imagepairset.ImagePairSet(IMAGEPAIR_SET_DESCRIPTIONS)
319 289
320 Results._ensure_included_in_category_dict(categories_all, 290 all_image_pairs.ensure_extra_column_values_in_summary(
321 'resultType', [ 291 column_id=KEY__EXTRACOLUMN__RESULT_TYPE, values=[
322 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 292 KEY__RESULT_TYPE__FAILED,
323 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 293 KEY__RESULT_TYPE__FAILUREIGNORED,
324 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 294 KEY__RESULT_TYPE__NOCOMPARISON,
325 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, 295 KEY__RESULT_TYPE__SUCCEEDED,
326 ]) 296 ])
327 Results._ensure_included_in_category_dict(categories_failures, 297 failing_image_pairs.ensure_extra_column_values_in_summary(
328 'resultType', [ 298 column_id=KEY__EXTRACOLUMN__RESULT_TYPE, values=[
329 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 299 KEY__RESULT_TYPE__FAILED,
330 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 300 KEY__RESULT_TYPE__FAILUREIGNORED,
331 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 301 KEY__RESULT_TYPE__NOCOMPARISON,
332 ]) 302 ])
333 303
334 data_all = []
335 data_failures = []
336 builders = sorted(actual_builder_dicts.keys()) 304 builders = sorted(actual_builder_dicts.keys())
337 num_builders = len(builders) 305 num_builders = len(builders)
338 builder_num = 0 306 builder_num = 0
339 for builder in builders: 307 for builder in builders:
340 builder_num += 1 308 builder_num += 1
341 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' % 309 logging.info('Generating pixel diffs for builder #%d of %d, "%s"...' %
342 (builder_num, num_builders, builder)) 310 (builder_num, num_builders, builder))
343 actual_results_for_this_builder = ( 311 actual_results_for_this_builder = (
344 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) 312 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
345 for result_type in sorted(actual_results_for_this_builder.keys()): 313 for result_type in sorted(actual_results_for_this_builder.keys()):
346 results_of_this_type = actual_results_for_this_builder[result_type] 314 results_of_this_type = actual_results_for_this_builder[result_type]
347 if not results_of_this_type: 315 if not results_of_this_type:
348 continue 316 continue
349 for image_name in sorted(results_of_this_type.keys()): 317 for image_name in sorted(results_of_this_type.keys()):
350 actual_image = results_of_this_type[image_name] 318 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups()
319 actual_image_relative_url = Results._create_relative_url(
320 hashtype_and_digest=results_of_this_type[image_name],
321 test_name=test)
351 322
352 # Default empty expectations; overwrite these if we find any real ones 323 # Default empty expectations; overwrite these if we find any real ones
353 expectations_per_test = None 324 expectations_per_test = None
354 expected_image = [None, None] 325 expected_image_relative_url = None
326 expectations_dict = None
355 try: 327 try:
356 expectations_per_test = ( 328 expectations_per_test = (
357 expected_builder_dicts 329 expected_builder_dicts
358 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name]) 330 [builder][gm_json.JSONKEY_EXPECTEDRESULTS][image_name])
359 # TODO(epoger): assumes a single allowed digest per test 331 # TODO(epoger): assumes a single allowed digest per test, which is
360 expected_image = ( 332 # fine; see https://code.google.com/p/skia/issues/detail?id=1787
333 expected_image_hashtype_and_digest = (
361 expectations_per_test 334 expectations_per_test
362 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0]) 335 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0])
336 expected_image_relative_url = Results._create_relative_url(
337 hashtype_and_digest=expected_image_hashtype_and_digest,
338 test_name=test)
339 expectations_dict = {}
340 for field in EXPECTATION_FIELDS_PASSED_THRU_VERBATIM:
341 expectations_dict[field] = expectations_per_test.get(field)
363 except (KeyError, TypeError): 342 except (KeyError, TypeError):
364 # There are several cases in which we would expect to find 343 # There are several cases in which we would expect to find
365 # no expectations for a given test: 344 # no expectations for a given test:
366 # 345 #
367 # 1. result_type == NOCOMPARISON 346 # 1. result_type == NOCOMPARISON
368 # There are no expectations for this test yet! 347 # There are no expectations for this test yet!
369 # 348 #
370 # 2. alternate rendering mode failures (e.g. serialized) 349 # 2. alternate rendering mode failures (e.g. serialized)
371 # In cases like 350 # In cases like
372 # https://code.google.com/p/skia/issues/detail?id=1684 351 # https://code.google.com/p/skia/issues/detail?id=1684
373 # ('tileimagefilter GM test failing in serialized render mode'), 352 # ('tileimagefilter GM test failing in serialized render mode'),
374 # the gm-actuals will list a failure for the alternate 353 # the gm-actuals will list a failure for the alternate
375 # rendering mode even though we don't have explicit expectations 354 # rendering mode even though we don't have explicit expectations
376 # for the test (the implicit expectation is that it must 355 # for the test (the implicit expectation is that it must
377 # render the same in all rendering modes). 356 # render the same in all rendering modes).
378 # 357 #
379 # Don't log type 1, because it is common. 358 # Don't log type 1, because it is common.
380 # Log other types, because they are rare and we should know about 359 # 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 360 # them, but don't throw an exception, because we need to keep our
382 # tools working in the meanwhile! 361 # tools working in the meanwhile!
383 if result_type != gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON: 362 if result_type != KEY__RESULT_TYPE__NOCOMPARISON:
384 logging.warning('No expectations found for test: %s' % { 363 logging.warning('No expectations found for test: %s' % {
385 'builder': builder, 364 KEY__EXTRACOLUMN__BUILDER: builder,
365 KEY__EXTRACOLUMN__RESULT_TYPE: result_type,
386 'image_name': image_name, 366 'image_name': image_name,
387 'result_type': result_type,
388 }) 367 })
389 368
390 # If this test was recently rebaselined, it will remain in 369 # If this test was recently rebaselined, it will remain in
391 # the 'failed' set of actuals until all the bots have 370 # the 'failed' set of actuals until all the bots have
392 # cycled (although the expectations have indeed been set 371 # cycled (although the expectations have indeed been set
393 # from the most recent actuals). Treat these as successes 372 # from the most recent actuals). Treat these as successes
394 # instead of failures. 373 # instead of failures.
395 # 374 #
396 # TODO(epoger): Do we need to do something similar in 375 # TODO(epoger): Do we need to do something similar in
397 # other cases, such as when we have recently marked a test 376 # other cases, such as when we have recently marked a test
398 # as ignoreFailure but it still shows up in the 'failed' 377 # as ignoreFailure but it still shows up in the 'failed'
399 # category? Maybe we should not rely on the result_type 378 # category? Maybe we should not rely on the result_type
400 # categories recorded within the gm_actuals AT ALL, and 379 # categories recorded within the gm_actuals AT ALL, and
401 # instead evaluate the result_type ourselves based on what 380 # instead evaluate the result_type ourselves based on what
402 # we see in expectations vs actual checksum? 381 # we see in expectations vs actual checksum?
403 if expected_image == actual_image: 382 if expected_image_relative_url == actual_image_relative_url:
404 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED 383 updated_result_type = KEY__RESULT_TYPE__SUCCEEDED
405 else: 384 else:
406 updated_result_type = result_type 385 updated_result_type = result_type
407 386 extra_columns_dict = {
408 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() 387 KEY__EXTRACOLUMN__RESULT_TYPE: updated_result_type,
409 self._generate_pixel_diffs_if_needed( 388 KEY__EXTRACOLUMN__BUILDER: builder,
410 test=test, expected_image=expected_image, 389 KEY__EXTRACOLUMN__TEST: test,
411 actual_image=actual_image) 390 KEY__EXTRACOLUMN__CONFIG: config,
412 results_for_this_test = {
413 'resultType': updated_result_type,
414 'builder': builder,
415 'test': test,
416 'config': config,
417 'actualHashType': actual_image[0],
418 'actualHashDigest': str(actual_image[1]),
419 'expectedHashType': expected_image[0],
420 'expectedHashDigest': str(expected_image[1]),
421
422 # FIELDS_PASSED_THRU_VERBATIM that may be overwritten below...
423 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False,
424 } 391 }
425 if expectations_per_test: 392 image_pair = imagepair.ImagePair(
426 for field in FIELDS_PASSED_THRU_VERBATIM: 393 image_diff_db=self._image_diff_db,
427 results_for_this_test[field] = expectations_per_test.get(field) 394 base_url=gm_json.GM_ACTUALS_ROOT_HTTP_URL,
428 395 imageA_relative_url=expected_image_relative_url,
429 if updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON: 396 imageB_relative_url=actual_image_relative_url,
430 pass # no diff record to calculate at all 397 expectations=expectations_dict,
431 elif updated_result_type == gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED: 398 extra_columns=extra_columns_dict)
432 results_for_this_test['numDifferingPixels'] = 0 399 all_image_pairs.add_image_pair(image_pair)
433 results_for_this_test['percentDifferingPixels'] = 0 400 if updated_result_type != KEY__RESULT_TYPE__SUCCEEDED:
434 results_for_this_test['weightedDiffMeasure'] = 0 401 failing_image_pairs.add_image_pair(image_pair)
435 results_for_this_test['perceptualDifference'] = 0
436 results_for_this_test['maxDiffPerChannel'] = 0
437 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])
442 results_for_this_test['numDifferingPixels'] = (
443 diff_record.get_num_pixels_differing())
444 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())
448 results_for_this_test['perceptualDifference'] = (
449 diff_record.get_perceptual_difference())
450 results_for_this_test['maxDiffPerChannel'] = (
451 diff_record.get_max_diff_per_channel())
452 except KeyError:
453 logging.warning('unable to find diff_record for ("%s", "%s")' %
454 (expected_image[1], actual_image[1]))
455 pass
456
457 Results._add_to_category_dict(categories_all, results_for_this_test)
458 data_all.append(results_for_this_test)
459
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.
464 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED:
465 Results._add_to_category_dict(categories_failures,
466 results_for_this_test)
467 data_failures.append(results_for_this_test)
468 402
469 self._results = { 403 self._results = {
470 RESULTS_ALL: 404 KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(),
471 {'categories': categories_all, 'testData': data_all}, 405 KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(),
472 RESULTS_FAILURES:
473 {'categories': categories_failures, 'testData': data_failures},
474 } 406 }
475 407
476 @staticmethod
477 def _add_to_category_dict(category_dict, test_results):
478 """Add test_results to the category dictionary we are building.
479 (See documentation of self.get_results_of_type() for the format of this
480 dictionary.)
481
482 Args:
483 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 {
486 'category_name': 'category_value',
487 'category_name': 'category_value',
488 ...
489 }
490 """
491 for category in CATEGORIES_TO_SUMMARIZE:
492 category_value = test_results.get(category)
493 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
498
499 @staticmethod
500 def _ensure_included_in_category_dict(category_dict,
501 category_name, category_values):
502 """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.
504 (See documentation of self.get_results_of_type() for the format of this
505 dictionary.)
506
507 Args:
508 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
518
519 408
520 def main(): 409 def main():
521 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', 410 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
522 datefmt='%m/%d/%Y %H:%M:%S', 411 datefmt='%m/%d/%Y %H:%M:%S',
523 level=logging.INFO) 412 level=logging.INFO)
524 parser = argparse.ArgumentParser() 413 parser = argparse.ArgumentParser()
525 parser.add_argument( 414 parser.add_argument(
526 '--actuals', required=True, 415 '--actuals', required=True,
527 help='Directory containing all actual-result JSON files') 416 help='Directory containing all actual-result JSON files')
528 parser.add_argument( 417 parser.add_argument(
529 '--expectations', required=True, 418 '--expectations', required=True,
530 help='Directory containing all expected-result JSON files') 419 help='Directory containing all expected-result JSON files')
531 parser.add_argument( 420 parser.add_argument(
532 '--outfile', required=True, 421 '--outfile', required=True,
533 help='File to write result summary into, in JSON format') 422 help='File to write result summary into, in JSON format')
534 parser.add_argument( 423 parser.add_argument(
535 '--workdir', default='.workdir', 424 '--workdir', default='.workdir',
536 help='Directory within which to download images and generate diffs') 425 help='Directory within which to download images and generate diffs')
537 args = parser.parse_args() 426 args = parser.parse_args()
538 results = Results(actuals_root=args.actuals, 427 results = Results(actuals_root=args.actuals,
539 expected_root=args.expectations, 428 expected_root=args.expectations,
540 generated_images_root=args.workdir) 429 generated_images_root=args.workdir)
541 gm_json.WriteToFile(results.get_results_of_type(RESULTS_ALL), args.outfile) 430 gm_json.WriteToFile(results.get_results_of_type(KEY__HEADER__RESULTS_ALL),
431 args.outfile)
542 432
543 433
544 if __name__ == '__main__': 434 if __name__ == '__main__':
545 main() 435 main()
OLDNEW
« no previous file with comments | « gm/rebaseline_server/imagepairset_test.py ('k') | gm/rebaseline_server/results_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698