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

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

Issue 28903008: rebaseline_server: add tabs, and ability to submit new baselines to the server (Closed) Base URL: http://skia.googlecode.com/svn/trunk/
Patch Set: more 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 """
(...skipping 13 matching lines...) Expand all
24 # that directory. That script allows us to parse the actual-results.json file 24 # that directory. That script allows us to parse the actual-results.json file
25 # written out by the GM tool. 25 # written out by the GM tool.
26 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* 26 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end*
27 # so any dirs that are already in the PYTHONPATH will be preferred. 27 # so any dirs that are already in the PYTHONPATH will be preferred.
28 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 28 GM_DIRECTORY = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
29 if GM_DIRECTORY not in sys.path: 29 if GM_DIRECTORY not in sys.path:
30 sys.path.append(GM_DIRECTORY) 30 sys.path.append(GM_DIRECTORY)
31 import gm_json 31 import gm_json
32 32
33 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN) 33 IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
34 IMAGE_FILENAME_FORMATTER = '%s_%s.png' # pass in (testname, config)
35
34 CATEGORIES_TO_SUMMARIZE = [ 36 CATEGORIES_TO_SUMMARIZE = [
35 'builder', 'test', 'config', 'resultType', 37 'builder', 'test', 'config', 'resultType',
36 ] 38 ]
37 RESULTS_ALL = 'all' 39 RESULTS_ALL = 'all'
38 RESULTS_FAILURES = 'failures' 40 RESULTS_FAILURES = 'failures'
39 41
40 class Results(object): 42 class Results(object):
41 """ Loads actual and expected results from all builders, supplying combined 43 """ Loads actual and expected results from all builders, supplying combined
42 reports as requested. 44 reports as requested.
43 45
44 Once this object has been constructed, the results are immutable. If you 46 Once this object has been constructed, the results (in self._results[])
45 want to update the results based on updated JSON file contents, you will 47 are immutable. If you want to update the results based on updated JSON
46 need to create a new Results object.""" 48 file contents, you will need to create a new Results object."""
47 49
48 def __init__(self, actuals_root, expected_root): 50 def __init__(self, actuals_root, expected_root):
49 """ 51 """
50 Args: 52 Args:
51 actuals_root: root directory containing all actual-results.json files 53 actuals_root: root directory containing all actual-results.json files
52 expected_root: root directory containing all expected-results.json files 54 expected_root: root directory containing all expected-results.json files
53 """ 55 """
54 self._actual_builder_dicts = Results._get_dicts_from_root(actuals_root) 56 self._actuals_root = actuals_root
55 self._expected_builder_dicts = Results._get_dicts_from_root(expected_root) 57 self._expected_root = expected_root
56 self._combine_actual_and_expected() 58 self._load_actual_and_expected()
57 self._timestamp = int(time.time()) 59 self._timestamp = int(time.time())
58 60
59 def get_timestamp(self): 61 def get_timestamp(self):
60 """Return the time at which this object was created, in seconds past epoch 62 """Return the time at which this object was created, in seconds past epoch
61 (UTC). 63 (UTC).
62 """ 64 """
63 return self._timestamp 65 return self._timestamp
64 66
67 def edit_expectations(self, modifications):
68 """Edit the expectations stored within this object and write them back
69 to disk.
70
71 Note that this will NOT update the results stored in self._results[] ;
72 in order to see those updates, you must instantiate a new Results object
73 based on the (now updated) files on disk.
74
75 Args:
76 modifications: a list of dictionaries, one for each expectation to update:
77
78 [
79 {
80 'builder': 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Debug',
81 'test': 'bigmatrix',
82 'config': '8888',
83 'expectedHashType': 'bitmap-64bitMD5',
84 'expectedHashDigest': '10894408024079689926',
85 },
86 ...
87 ]
88
89 TODO(epoger): For now, this does not allow the caller to set any fields
90 other than expectedHashType/expectedHashDigest, and assumes that
91 ignore-failure should be set to False. We need to add support
92 for other fields (notes, bugs, etc.) and ignore-failure=True.
93 """
94 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
95 for mod in modifications:
96 image_name = IMAGE_FILENAME_FORMATTER % (mod['test'], mod['config'])
97 # TODO(epoger): assumes a single allowed digest per test
98 allowed_digests = [[mod['expectedHashType'],
99 int(mod['expectedHashDigest'])]]
100 new_expectations = {
101 gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS: allowed_digests,
102 gm_json.JSONKEY_EXPECTEDRESULTS_IGNOREFAILURE: False,
103 }
104 builder_dict = expected_builder_dicts[mod['builder']]
105 builder_expectations = builder_dict.get(gm_json.JSONKEY_EXPECTEDRESULTS)
106 if not builder_expectations:
107 builder_expectations = {}
108 builder_dict[gm_json.JSONKEY_EXPECTEDRESULTS] = builder_expectations
109 builder_expectations[image_name] = new_expectations
110 Results._write_dicts_to_root(expected_builder_dicts, self._expected_root)
111
65 def get_results_of_type(self, type): 112 def get_results_of_type(self, type):
66 """Return results of some/all tests (depending on 'type' parameter). 113 """Return results of some/all tests (depending on 'type' parameter).
67 114
68 Args: 115 Args:
69 type: string describing which types of results to include; must be one 116 type: string describing which types of results to include; must be one
70 of the RESULTS_* constants 117 of the RESULTS_* constants
71 118
72 Results are returned as a dictionary in this form: 119 Results are returned as a dictionary in this form:
73 120
74 { 121 {
(...skipping 29 matching lines...) Expand all
104 'actualHashType': 'bitmap-64bitMD5', 151 'actualHashType': 'bitmap-64bitMD5',
105 'actualHashDigest': '2409857384569', 152 'actualHashDigest': '2409857384569',
106 }, 153 },
107 ... 154 ...
108 ], # end of 'testData' list 155 ], # end of 'testData' list
109 } 156 }
110 """ 157 """
111 return self._results[type] 158 return self._results[type]
112 159
113 @staticmethod 160 @staticmethod
114 def _get_dicts_from_root(root, pattern='*.json'): 161 def _read_dicts_from_root(root, pattern='*.json'):
115 """Read all JSON dictionaries within a directory tree. 162 """Read all JSON dictionaries within a directory tree.
116 163
117 Args: 164 Args:
118 root: path to root of directory tree 165 root: path to root of directory tree
119 pattern: which files to read within root (fnmatch-style pattern) 166 pattern: which files to read within root (fnmatch-style pattern)
120 167
121 Returns: 168 Returns:
122 A meta-dictionary containing all the JSON dictionaries found within 169 A meta-dictionary containing all the JSON dictionaries found within
123 the directory tree, keyed by the builder name of each dictionary. 170 the directory tree, keyed by the builder name of each dictionary.
124 171
125 Raises: 172 Raises:
126 IOError if root does not refer to an existing directory 173 IOError if root does not refer to an existing directory
127 """ 174 """
128 if not os.path.isdir(root): 175 if not os.path.isdir(root):
129 raise IOError('no directory found at path %s' % root) 176 raise IOError('no directory found at path %s' % root)
130 meta_dict = {} 177 meta_dict = {}
131 for dirpath, dirnames, filenames in os.walk(root): 178 for dirpath, dirnames, filenames in os.walk(root):
132 for matching_filename in fnmatch.filter(filenames, pattern): 179 for matching_filename in fnmatch.filter(filenames, pattern):
133 builder = os.path.basename(dirpath) 180 builder = os.path.basename(dirpath)
181 # If we are reading from the collection of actual results, skip over
182 # the Trybot results (we don't maintain baselines for them).
134 if builder.endswith('-Trybot'): 183 if builder.endswith('-Trybot'):
135 continue 184 continue
136 fullpath = os.path.join(dirpath, matching_filename) 185 fullpath = os.path.join(dirpath, matching_filename)
137 meta_dict[builder] = gm_json.LoadFromFile(fullpath) 186 meta_dict[builder] = gm_json.LoadFromFile(fullpath)
138 return meta_dict 187 return meta_dict
139 188
140 def _combine_actual_and_expected(self): 189 @staticmethod
141 """Gathers the results of all tests, across all builders (based on the 190 def _write_dicts_to_root(meta_dict, root, pattern='*.json'):
142 contents of self._actual_builder_dicts and self._expected_builder_dicts), 191 """Write all per-builder dictionaries within meta_dict to files under
192 the root path.
193
194 Security note: this will only write to files that already exist within
195 the root path (as found by os.walk() within root), so we don't need to
196 worry about malformed content writing to disk outside of root.
197 However, the data written to those files is not double-checked, so it
198 could contain poisonous data.
199
200 Args:
201 meta_dict: a builder-keyed meta-dictionary containing all the JSON
202 dictionaries we want to write out
203 root: path to root of directory tree within which to write files
204 pattern: which files to write within root (fnmatch-style pattern)
205
206 Raises:
207 IOError if root does not refer to an existing directory
208 KeyError if the set of per-builder dictionaries written out was
209 different than expected
210 """
211 if not os.path.isdir(root):
212 raise IOError('no directory found at path %s' % root)
213 actual_builders_written = []
214 for dirpath, dirnames, filenames in os.walk(root):
215 for matching_filename in fnmatch.filter(filenames, pattern):
216 builder = os.path.basename(dirpath)
217 # We should never encounter Trybot *expectations*, but if we are
218 # writing into the actual-results dir, skip the Trybot actuals.
219 # (I don't know why we would ever write into the actual-results dir,
220 # though.)
221 if builder.endswith('-Trybot'):
222 continue
223 per_builder_dict = meta_dict.get(builder)
224 if per_builder_dict:
225 fullpath = os.path.join(dirpath, matching_filename)
226 gm_json.WriteToFile(per_builder_dict, fullpath)
227 actual_builders_written.append(builder)
228
229 # Check: did we write out the set of per-builder dictionaries we
230 # expected to?
231 expected_builders_written = sorted(meta_dict.keys())
232 actual_builders_written.sort()
233 if expected_builders_written != actual_builders_written:
234 raise KeyError(
235 'expected to write dicts for builders %s, but actually wrote them '
236 'for builders %s' % (
237 expected_builders_written, actual_builders_written))
238
239 def _load_actual_and_expected(self):
240 """Loads the results of all tests, across all builders (based on the
241 files within self._actuals_root and self._expected_root),
143 and stores them in self._results. 242 and stores them in self._results.
144 """ 243 """
244 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root)
245 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
246
145 categories_all = {} 247 categories_all = {}
146 categories_failures = {} 248 categories_failures = {}
147 Results._ensure_included_in_category_dict(categories_all, 249 Results._ensure_included_in_category_dict(categories_all,
148 'resultType', [ 250 'resultType', [
149 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 251 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
150 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 252 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
151 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 253 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
152 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, 254 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED,
153 ]) 255 ])
154 Results._ensure_included_in_category_dict(categories_failures, 256 Results._ensure_included_in_category_dict(categories_failures,
155 'resultType', [ 257 'resultType', [
156 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 258 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
157 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 259 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
158 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 260 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
159 ]) 261 ])
160 262
161 data_all = [] 263 data_all = []
162 data_failures = [] 264 data_failures = []
163 for builder in sorted(self._actual_builder_dicts.keys()): 265 for builder in sorted(actual_builder_dicts.keys()):
164 actual_results_for_this_builder = ( 266 actual_results_for_this_builder = (
165 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) 267 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
166 for result_type in sorted(actual_results_for_this_builder.keys()): 268 for result_type in sorted(actual_results_for_this_builder.keys()):
167 results_of_this_type = actual_results_for_this_builder[result_type] 269 results_of_this_type = actual_results_for_this_builder[result_type]
168 if not results_of_this_type: 270 if not results_of_this_type:
169 continue 271 continue
170 for image_name in sorted(results_of_this_type.keys()): 272 for image_name in sorted(results_of_this_type.keys()):
171 actual_image = results_of_this_type[image_name] 273 actual_image = results_of_this_type[image_name]
172 try: 274 try:
173 # TODO(epoger): assumes a single allowed digest per test 275 # TODO(epoger): assumes a single allowed digest per test
174 expected_image = ( 276 expected_image = (
175 self._expected_builder_dicts 277 expected_builder_dicts
176 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] 278 [builder][gm_json.JSONKEY_EXPECTEDRESULTS]
177 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] 279 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS]
178 [0]) 280 [0])
179 except (KeyError, TypeError): 281 except (KeyError, TypeError):
180 # There are several cases in which we would expect to find 282 # There are several cases in which we would expect to find
181 # no expectations for a given test: 283 # no expectations for a given test:
182 # 284 #
183 # 1. result_type == NOCOMPARISON 285 # 1. result_type == NOCOMPARISON
184 # There are no expectations for this test yet! 286 # There are no expectations for this test yet!
185 # 287 #
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after
291 category_dict: category dict-of-dicts to modify 393 category_dict: category dict-of-dicts to modify
292 category_name: category name, as a string 394 category_name: category name, as a string
293 category_values: list of values we want to make sure are represented 395 category_values: list of values we want to make sure are represented
294 for this category 396 for this category
295 """ 397 """
296 if not category_dict.get(category_name): 398 if not category_dict.get(category_name):
297 category_dict[category_name] = {} 399 category_dict[category_name] = {}
298 for category_value in category_values: 400 for category_value in category_values:
299 if not category_dict[category_name].get(category_value): 401 if not category_dict[category_name].get(category_value):
300 category_dict[category_name][category_value] = 0 402 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