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

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

Issue 26891003: rebaseline_server: allow client to pull all results, or just failures (Closed) Base URL: http://skia.googlecode.com/svn/trunk/
Patch Set: todo_about_global Created 7 years, 2 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 | Annotate | Revision Log
« no previous file with comments | « no previous file | gm/rebaseline_server/server.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 """
11 11
12 # System-level imports 12 # System-level imports
13 import fnmatch 13 import fnmatch
14 import json 14 import json
15 import logging
15 import os 16 import os
16 import re 17 import re
17 import sys 18 import sys
18 19
19 # Imports from within Skia 20 # Imports from within Skia
20 # 21 #
21 # We need to add the 'gm' directory, so that we can import gm_json.py within 22 # We need to add the 'gm' directory, so that we can import gm_json.py within
22 # that directory. That script allows us to parse the actual-results.json file 23 # that directory. That script allows us to parse the actual-results.json file
23 # written out by the GM tool. 24 # written out by the GM tool.
24 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* 25 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end*
25 # so any dirs that are already in the PYTHONPATH will be preferred. 26 # so any dirs that are already in the PYTHONPATH will be preferred.
26 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 27 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
27 if GM_DIRECTORY not in sys.path: 28 if GM_DIRECTORY not in sys.path:
28 sys.path.append(GM_DIRECTORY) 29 sys.path.append(GM_DIRECTORY)
29 import gm_json 30 import gm_json
30 31
31 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) 32 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
32 CATEGORIES_TO_SUMMARIZE = [ 33 CATEGORIES_TO_SUMMARIZE = [
33 'builder', 'test', 'config', 'resultType', 34 'builder', 'test', 'config', 'resultType',
34 ] 35 ]
36 RESULTS_ALL = 'all'
37 RESULTS_FAILURES = 'failures'
35 38
36 class Results(object): 39 class Results(object):
37 """ Loads actual and expected results from all builders, supplying combined 40 """ Loads actual and expected results from all builders, supplying combined
38 reports as requested. """ 41 reports as requested.
42
43 Once this object has been constructed, the results are immutable. If you
44 want to update the results based on updated JSON file contents, you will
45 need to create a new Results object."""
39 46
40 def __init__(self, actuals_root, expected_root): 47 def __init__(self, actuals_root, expected_root):
41 """ 48 """
42 Args: 49 Args:
43 actuals_root: root directory containing all actual-results.json files 50 actuals_root: root directory containing all actual-results.json files
44 expected_root: root directory containing all expected-results.json files 51 expected_root: root directory containing all expected-results.json files
45 """ 52 """
46 self._actual_builder_dicts = Results._GetDictsFromRoot(actuals_root) 53 self._actual_builder_dicts = Results._get_dicts_from_root(actuals_root)
47 self._expected_builder_dicts = Results._GetDictsFromRoot(expected_root) 54 self._expected_builder_dicts = Results._get_dicts_from_root(expected_root)
48 self._all_results = Results._Combine( 55 self._combine_actual_and_expected()
49 actual_builder_dicts=self._actual_builder_dicts,
50 expected_builder_dicts=self._expected_builder_dicts)
51 56
52 def GetAll(self): 57 def get_results_of_type(self, type):
53 """Return results of all tests, as a dictionary in this form: 58 """Return results of some/all tests (depending on 'type' parameter).
59
60 Args:
61 type: string describing which types of results to include; must be one
62 of the RESULTS_* constants
63
64 Results are returned as a dictionary in this form:
54 65
55 { 66 {
56 'categories': # dictionary of categories listed in 67 'categories': # dictionary of categories listed in
57 # CATEGORIES_TO_SUMMARIZE, with the number of times 68 # CATEGORIES_TO_SUMMARIZE, with the number of times
58 # each value appears within its category 69 # each value appears within its category
59 { 70 {
60 'resultType': # category name 71 'resultType': # category name
61 { 72 {
62 'failed': 29, # category value and total number found of that value 73 'failed': 29, # category value and total number found of that value
63 'failure-ignored': 948, 74 'failure-ignored': 948,
64 'no-comparison': 4502, 75 'no-comparison': 4502,
65 'succeeded': 38609, 76 'succeeded': 38609,
66 }, 77 },
67 'builder': 78 'builder':
68 { 79 {
69 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286, 80 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug': 1286,
70 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134, 81 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release': 1134,
71 ... 82 ...
72 }, 83 },
73 ... # other categories from CATEGORIES_TO_SUMMARIZE 84 ... # other categories from CATEGORIES_TO_SUMMARIZE
74 }, # end of 'categories' dictionary 85 }, # end of 'categories' dictionary
75 86
76 'testData': # list of test results, with a dictionary for each 87 'testData': # list of test results, with a dictionary for each
77 [ 88 [
78 { 89 {
79 'index': 0, # index of this result within testData list
80 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug', 90 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug',
81 'test': 'bigmatrix', 91 'test': 'bigmatrix',
82 'config': '8888', 92 'config': '8888',
83 'resultType': 'failed', 93 'resultType': 'failed',
84 'expectedHashType': 'bitmap-64bitMD5', 94 'expectedHashType': 'bitmap-64bitMD5',
85 'expectedHashDigest': '10894408024079689926', 95 'expectedHashDigest': '10894408024079689926',
86 'actualHashType': 'bitmap-64bitMD5', 96 'actualHashType': 'bitmap-64bitMD5',
87 'actualHashDigest': '2409857384569', 97 'actualHashDigest': '2409857384569',
88 }, 98 },
89 ... 99 ...
90 ], # end of 'testData' list 100 ], # end of 'testData' list
91 } 101 }
92 """ 102 """
93 return self._all_results 103 return self._results[type]
94 104
95 @staticmethod 105 @staticmethod
96 def _GetDictsFromRoot(root, pattern='*.json'): 106 def _get_dicts_from_root(root, pattern='*.json'):
97 """Read all JSON dictionaries within a directory tree. 107 """Read all JSON dictionaries within a directory tree.
98 108
99 Args: 109 Args:
100 root: path to root of directory tree 110 root: path to root of directory tree
101 pattern: which files to read within root (fnmatch-style pattern) 111 pattern: which files to read within root (fnmatch-style pattern)
102 112
103 Returns: 113 Returns:
104 A meta-dictionary containing all the JSON dictionaries found within 114 A meta-dictionary containing all the JSON dictionaries found within
105 the directory tree, keyed by the builder name of each dictionary. 115 the directory tree, keyed by the builder name of each dictionary.
106 """ 116 """
107 meta_dict = {} 117 meta_dict = {}
108 for dirpath, dirnames, filenames in os.walk(root): 118 for dirpath, dirnames, filenames in os.walk(root):
109 for matching_filename in fnmatch.filter(filenames, pattern): 119 for matching_filename in fnmatch.filter(filenames, pattern):
110 builder = os.path.basename(dirpath) 120 builder = os.path.basename(dirpath)
111 if builder.endswith('-Trybot'): 121 if builder.endswith('-Trybot'):
112 continue 122 continue
113 fullpath = os.path.join(dirpath, matching_filename) 123 fullpath = os.path.join(dirpath, matching_filename)
114 meta_dict[builder] = gm_json.LoadFromFile(fullpath) 124 meta_dict[builder] = gm_json.LoadFromFile(fullpath)
115 return meta_dict 125 return meta_dict
116 126
117 @staticmethod 127 def _combine_actual_and_expected(self):
118 def _Combine(actual_builder_dicts, expected_builder_dicts):
119 """Gathers the results of all tests, across all builders (based on the 128 """Gathers the results of all tests, across all builders (based on the
120 contents of actual_builder_dicts and expected_builder_dicts). 129 contents of self._actual_builder_dicts and self._expected_builder_dicts),
121 130 and stores them in self._results.
122 This is a static method, because once we start refreshing results
123 asynchronously, we need to make sure we are not corrupting the object's
124 member variables.
125
126 Args:
127 actual_builder_dicts: a meta-dictionary of all actual JSON results,
128 as returned by _GetDictsFromRoot().
129 actual_builder_dicts: a meta-dictionary of all expected JSON results,
130 as returned by _GetDictsFromRoot().
131
132 Returns:
133 A list of all the results of all tests, in the same form returned by
134 self.GetAll().
135 """ 131 """
136 test_data = [] 132 categories_all = {}
137 category_dict = {} 133 categories_failures = {}
138 Results._EnsureIncludedInCategoryDict(category_dict, 'resultType', [ 134 Results._ensure_included_in_category_dict(categories_all,
135 'resultType', [
139 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 136 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
140 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 137 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
141 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 138 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
142 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, 139 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED,
143 ]) 140 ])
141 Results._ensure_included_in_category_dict(categories_failures,
142 'resultType', [
143 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
144 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
145 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
146 ])
144 147
145 for builder in sorted(actual_builder_dicts.keys()): 148 data_all = []
149 data_failures = []
150 for builder in sorted(self._actual_builder_dicts.keys()):
146 actual_results_for_this_builder = ( 151 actual_results_for_this_builder = (
147 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) 152 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
148 for result_type in sorted(actual_results_for_this_builder.keys()): 153 for result_type in sorted(actual_results_for_this_builder.keys()):
149 results_of_this_type = actual_results_for_this_builder[result_type] 154 results_of_this_type = actual_results_for_this_builder[result_type]
150 if not results_of_this_type: 155 if not results_of_this_type:
151 continue 156 continue
152 for image_name in sorted(results_of_this_type.keys()): 157 for image_name in sorted(results_of_this_type.keys()):
153 actual_image = results_of_this_type[image_name] 158 actual_image = results_of_this_type[image_name]
154 try: 159 try:
155 # TODO(epoger): assumes a single allowed digest per test 160 # TODO(epoger): assumes a single allowed digest per test
156 expected_image = ( 161 expected_image = (
157 expected_builder_dicts 162 self._expected_builder_dicts
158 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] 163 [builder][gm_json.JSONKEY_EXPECTEDRESULTS]
159 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] 164 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS]
160 [0]) 165 [0])
161 except (KeyError, TypeError): 166 except (KeyError, TypeError):
162 # There are several cases in which we would expect to find 167 # There are several cases in which we would expect to find
163 # no expectations for a given test: 168 # no expectations for a given test:
164 # 169 #
165 # 1. result_type == NOCOMPARISON 170 # 1. result_type == NOCOMPARISON
166 # There are no expectations for this test yet! 171 # There are no expectations for this test yet!
167 # 172 #
(...skipping 11 matching lines...) Expand all
179 # for the test (the implicit expectation is that it must 184 # for the test (the implicit expectation is that it must
180 # render the same in all rendering modes). 185 # render the same in all rendering modes).
181 # 186 #
182 # Don't log types 1 or 2, because they are common. 187 # Don't log types 1 or 2, because they are common.
183 # Log other types, because they are rare and we should know about 188 # Log other types, because they are rare and we should know about
184 # them, but don't throw an exception, because we need to keep our 189 # them, but don't throw an exception, because we need to keep our
185 # tools working in the meanwhile! 190 # tools working in the meanwhile!
186 if result_type not in [ 191 if result_type not in [
187 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 192 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
188 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED] : 193 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED] :
189 print 'WARNING: No expectations found for test: %s' % { 194 logging.warning('No expectations found for test: %s' % {
190 'builder': builder, 195 'builder': builder,
191 'image_name': image_name, 196 'image_name': image_name,
192 'result_type': result_type, 197 'result_type': result_type,
193 } 198 })
194 expected_image = [None, None] 199 expected_image = [None, None]
195 200
196 # If this test was recently rebaselined, it will remain in 201 # If this test was recently rebaselined, it will remain in
197 # the 'failed' set of actuals until all the bots have 202 # the 'failed' set of actuals until all the bots have
198 # cycled (although the expectations have indeed been set 203 # cycled (although the expectations have indeed been set
199 # from the most recent actuals). Treat these as successes 204 # from the most recent actuals). Treat these as successes
200 # instead of failures. 205 # instead of failures.
201 # 206 #
202 # TODO(epoger): Do we need to do something similar in 207 # TODO(epoger): Do we need to do something similar in
203 # other cases, such as when we have recently marked a test 208 # other cases, such as when we have recently marked a test
204 # as ignoreFailure but it still shows up in the 'failed' 209 # as ignoreFailure but it still shows up in the 'failed'
205 # category? Maybe we should not rely on the result_type 210 # category? Maybe we should not rely on the result_type
206 # categories recorded within the gm_actuals AT ALL, and 211 # categories recorded within the gm_actuals AT ALL, and
207 # instead evaluate the result_type ourselves based on what 212 # instead evaluate the result_type ourselves based on what
208 # we see in expectations vs actual checksum? 213 # we see in expectations vs actual checksum?
209 if expected_image == actual_image: 214 if expected_image == actual_image:
210 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED 215 updated_result_type = gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED
211 else: 216 else:
212 updated_result_type = result_type 217 updated_result_type = result_type
213 218
214 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups() 219 (test, config) = IMAGE_FILENAME_RE.match(image_name).groups()
215 results_for_this_test = { 220 results_for_this_test = {
216 'index': len(test_data),
217 'builder': builder, 221 'builder': builder,
218 'test': test, 222 'test': test,
219 'config': config, 223 'config': config,
220 'resultType': updated_result_type, 224 'resultType': updated_result_type,
221 'actualHashType': actual_image[0], 225 'actualHashType': actual_image[0],
222 'actualHashDigest': str(actual_image[1]), 226 'actualHashDigest': str(actual_image[1]),
223 'expectedHashType': expected_image[0], 227 'expectedHashType': expected_image[0],
224 'expectedHashDigest': str(expected_image[1]), 228 'expectedHashDigest': str(expected_image[1]),
225 } 229 }
226 Results._AddToCategoryDict(category_dict, results_for_this_test) 230 Results._add_to_category_dict(categories_all, results_for_this_test)
227 test_data.append(results_for_this_test) 231 data_all.append(results_for_this_test)
228 return {'categories': category_dict, 'testData': test_data} 232 if updated_result_type != gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED:
233 Results._add_to_category_dict(categories_failures,
234 results_for_this_test)
235 data_failures.append(results_for_this_test)
236
237 self._results = {
238 RESULTS_ALL:
239 {'categories': categories_all, 'testData': data_all},
240 RESULTS_FAILURES:
241 {'categories': categories_failures, 'testData': data_failures},
242 }
229 243
230 @staticmethod 244 @staticmethod
231 def _AddToCategoryDict(category_dict, test_results): 245 def _add_to_category_dict(category_dict, test_results):
232 """Add test_results to the category dictionary we are building. 246 """Add test_results to the category dictionary we are building.
233 (See documentation of self.GetAll() for the format of this dictionary.) 247 (See documentation of self.get_results_of_type() for the format of this
248 dictionary.)
234 249
235 Args: 250 Args:
236 category_dict: category dict-of-dicts to add to; modify this in-place 251 category_dict: category dict-of-dicts to add to; modify this in-place
237 test_results: test data with which to update category_list, in a dict: 252 test_results: test data with which to update category_list, in a dict:
238 { 253 {
239 'category_name': 'category_value', 254 'category_name': 'category_value',
240 'category_name': 'category_value', 255 'category_name': 'category_value',
241 ... 256 ...
242 } 257 }
243 """ 258 """
244 for category in CATEGORIES_TO_SUMMARIZE: 259 for category in CATEGORIES_TO_SUMMARIZE:
245 category_value = test_results.get(category) 260 category_value = test_results.get(category)
246 if not category_value: 261 if not category_value:
247 continue # test_results did not include this category, keep going 262 continue # test_results did not include this category, keep going
248 if not category_dict.get(category): 263 if not category_dict.get(category):
249 category_dict[category] = {} 264 category_dict[category] = {}
250 if not category_dict[category].get(category_value): 265 if not category_dict[category].get(category_value):
251 category_dict[category][category_value] = 0 266 category_dict[category][category_value] = 0
252 category_dict[category][category_value] += 1 267 category_dict[category][category_value] += 1
253 268
254 @staticmethod 269 @staticmethod
255 def _EnsureIncludedInCategoryDict(category_dict, 270 def _ensure_included_in_category_dict(category_dict,
256 category_name, category_values): 271 category_name, category_values):
257 """Ensure that the category name/value pairs are included in category_dict, 272 """Ensure that the category name/value pairs are included in category_dict,
258 even if there aren't any results with that name/value pair. 273 even if there aren't any results with that name/value pair.
259 (See documentation of self.GetAll() for the format of this dictionary.) 274 (See documentation of self.get_results_of_type() for the format of this
275 dictionary.)
260 276
261 Args: 277 Args:
262 category_dict: category dict-of-dicts to modify 278 category_dict: category dict-of-dicts to modify
263 category_name: category name, as a string 279 category_name: category name, as a string
264 category_values: list of values we want to make sure are represented 280 category_values: list of values we want to make sure are represented
265 for this category 281 for this category
266 """ 282 """
267 if not category_dict.get(category_name): 283 if not category_dict.get(category_name):
268 category_dict[category_name] = {} 284 category_dict[category_name] = {}
269 for category_value in category_values: 285 for category_value in category_values:
270 if not category_dict[category_name].get(category_value): 286 if not category_dict[category_name].get(category_value):
271 category_dict[category_name][category_value] = 0 287 category_dict[category_name][category_value] = 0
OLDNEW
« no previous file with comments | « no previous file | gm/rebaseline_server/server.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698