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

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: misc 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') | gm/rebaseline_server/server.py » ('J')
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
epoger 2013/10/22 21:17:45 I think your fear is well-placed. I think we are
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)
134 if builder.endswith('-Trybot'): 181 if builder.endswith('-Trybot'):
135 continue 182 continue
136 fullpath = os.path.join(dirpath, matching_filename) 183 fullpath = os.path.join(dirpath, matching_filename)
137 meta_dict[builder] = gm_json.LoadFromFile(fullpath) 184 meta_dict[builder] = gm_json.LoadFromFile(fullpath)
138 return meta_dict 185 return meta_dict
139 186
140 def _combine_actual_and_expected(self): 187 @staticmethod
141 """Gathers the results of all tests, across all builders (based on the 188 def _write_dicts_to_root(meta_dict, root, pattern='*.json'):
142 contents of self._actual_builder_dicts and self._expected_builder_dicts), 189 """Write all per-builder dictionaries within meta_dict to files under
190 the root path.
191
192 Security note: this will only write to files that already exist within
193 the root path (as found by os.walk() within root), so we don't need to
194 worry about malformed content writing to disk outside of root.
195 However, the data written to those files is not double-checked, so it
196 could contain poisonous data.
borenet 2013/10/23 14:22:03 Thanks! We can feel somewhat secure since we're us
197
198 Args:
199 meta_dict: a builder-keyed meta-dictionary containing all the JSON
200 dictionaries we want to write out
201 root: path to root of directory tree within which to write files
202 pattern: which files to write within root (fnmatch-style pattern)
203
204 Raises:
205 IOError if root does not refer to an existing directory
206 KeyError if the set of per-builder dictionaries written out was
207 different than expected
208 """
209 if not os.path.isdir(root):
210 raise IOError('no directory found at path %s' % root)
211 actual_builders_written = []
212 for dirpath, dirnames, filenames in os.walk(root):
213 for matching_filename in fnmatch.filter(filenames, pattern):
214 builder = os.path.basename(dirpath)
215 if builder.endswith('-Trybot'):
216 continue
borenet 2013/10/23 14:22:03 We don't have any expectations with "-Trybot". Do
epoger 2013/10/23 14:42:14 Added some comments here and in _read_dicts_from_r
borenet 2013/10/23 14:47:34 Thanks!
217 per_builder_dict = meta_dict.get(builder)
218 if per_builder_dict:
219 fullpath = os.path.join(dirpath, matching_filename)
220 gm_json.WriteToFile(per_builder_dict, fullpath)
221 actual_builders_written.append(builder)
222
223 # Check: did we write out the set of per-builder dictionaries we
224 # expected to?
225 expected_builders_written = sorted(meta_dict.keys())
226 actual_builders_written.sort()
227 if expected_builders_written != actual_builders_written:
228 raise KeyError(
229 'expected to write dicts for builders %s, but actually wrote them '
230 'for builders %s' % (
231 expected_builders_written, actual_builders_written))
232
233 def _load_actual_and_expected(self):
234 """Loads the results of all tests, across all builders (based on the
235 files within self._actuals_root and self._expected_root),
143 and stores them in self._results. 236 and stores them in self._results.
144 """ 237 """
238 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root)
239 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
240
145 categories_all = {} 241 categories_all = {}
146 categories_failures = {} 242 categories_failures = {}
147 Results._ensure_included_in_category_dict(categories_all, 243 Results._ensure_included_in_category_dict(categories_all,
148 'resultType', [ 244 'resultType', [
149 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 245 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
150 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 246 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
151 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 247 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
152 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, 248 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED,
153 ]) 249 ])
154 Results._ensure_included_in_category_dict(categories_failures, 250 Results._ensure_included_in_category_dict(categories_failures,
155 'resultType', [ 251 'resultType', [
156 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 252 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
157 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 253 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
158 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 254 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
159 ]) 255 ])
160 256
161 data_all = [] 257 data_all = []
162 data_failures = [] 258 data_failures = []
163 for builder in sorted(self._actual_builder_dicts.keys()): 259 for builder in sorted(actual_builder_dicts.keys()):
164 actual_results_for_this_builder = ( 260 actual_results_for_this_builder = (
165 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) 261 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
166 for result_type in sorted(actual_results_for_this_builder.keys()): 262 for result_type in sorted(actual_results_for_this_builder.keys()):
167 results_of_this_type = actual_results_for_this_builder[result_type] 263 results_of_this_type = actual_results_for_this_builder[result_type]
168 if not results_of_this_type: 264 if not results_of_this_type:
169 continue 265 continue
170 for image_name in sorted(results_of_this_type.keys()): 266 for image_name in sorted(results_of_this_type.keys()):
171 actual_image = results_of_this_type[image_name] 267 actual_image = results_of_this_type[image_name]
172 try: 268 try:
173 # TODO(epoger): assumes a single allowed digest per test 269 # TODO(epoger): assumes a single allowed digest per test
174 expected_image = ( 270 expected_image = (
175 self._expected_builder_dicts 271 expected_builder_dicts
176 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] 272 [builder][gm_json.JSONKEY_EXPECTEDRESULTS]
177 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] 273 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS]
178 [0]) 274 [0])
179 except (KeyError, TypeError): 275 except (KeyError, TypeError):
180 # There are several cases in which we would expect to find 276 # There are several cases in which we would expect to find
181 # no expectations for a given test: 277 # no expectations for a given test:
182 # 278 #
183 # 1. result_type == NOCOMPARISON 279 # 1. result_type == NOCOMPARISON
184 # There are no expectations for this test yet! 280 # There are no expectations for this test yet!
185 # 281 #
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after
291 category_dict: category dict-of-dicts to modify 387 category_dict: category dict-of-dicts to modify
292 category_name: category name, as a string 388 category_name: category name, as a string
293 category_values: list of values we want to make sure are represented 389 category_values: list of values we want to make sure are represented
294 for this category 390 for this category
295 """ 391 """
296 if not category_dict.get(category_name): 392 if not category_dict.get(category_name):
297 category_dict[category_name] = {} 393 category_dict[category_name] = {}
298 for category_value in category_values: 394 for category_value in category_values:
299 if not category_dict[category_name].get(category_value): 395 if not category_dict[category_name].get(category_value):
300 category_dict[category_name][category_value] = 0 396 category_dict[category_name][category_value] = 0
OLDNEW
« no previous file with comments | « no previous file | gm/rebaseline_server/server.py » ('j') | gm/rebaseline_server/server.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698