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

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: make_server_handle_edits Created 7 years, 1 month 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
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'])
borenet 2013/10/22 19:33:16 Should we verify that there are no funny character
epoger 2013/10/22 21:17:45 Added a "Security note" to _write_dicts_to_root().
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)
borenet 2013/10/22 19:33:16 I'm scared of what happens when multiple users try
epoger 2013/10/22 21:17:45 If you mean multiple users committing edits to the
borenet 2013/10/23 14:22:03 Gotcha. I was envisioning this as a single server
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 Args:
193 meta_dict: a builder-keyed meta-dictionary containing all the JSON
194 dictionaries we want to write out
195 root: path to root of directory tree within which to write files
196 pattern: which files to write within root (fnmatch-style pattern)
197
198 Raises:
199 IOError if root does not refer to an existing directory
200 KeyError if the set of per-builder dictionaries written out was
201 different than expected
202 """
203 if not os.path.isdir(root):
204 raise IOError('no directory found at path %s' % root)
205 actual_builders_written = []
206 for dirpath, dirnames, filenames in os.walk(root):
207 for matching_filename in fnmatch.filter(filenames, pattern):
208 builder = os.path.basename(dirpath)
209 if builder.endswith('-Trybot'):
210 continue
211 per_builder_dict = meta_dict.get(builder)
212 if per_builder_dict:
213 fullpath = os.path.join(dirpath, matching_filename)
214 gm_json.WriteToFile(per_builder_dict, fullpath)
215 actual_builders_written.append(builder)
216
217 # Check: did we write out the set of per-builder dictionaries we
218 # expected to?
219 expected_builders_written = sorted(meta_dict.keys())
220 actual_builders_written.sort()
221 if expected_builders_written != actual_builders_written:
222 raise KeyError(
223 'expected to write dicts for builders %s, but actually wrote them '
224 'for builders %s' % (
225 expected_builders_written, actual_builders_written))
226
227 def _load_actual_and_expected(self):
228 """Loads the results of all tests, across all builders (based on the
229 files within self._actuals_root and self._expected_root),
143 and stores them in self._results. 230 and stores them in self._results.
144 """ 231 """
232 actual_builder_dicts = Results._read_dicts_from_root(self._actuals_root)
233 expected_builder_dicts = Results._read_dicts_from_root(self._expected_root)
234
145 categories_all = {} 235 categories_all = {}
146 categories_failures = {} 236 categories_failures = {}
147 Results._ensure_included_in_category_dict(categories_all, 237 Results._ensure_included_in_category_dict(categories_all,
148 'resultType', [ 238 'resultType', [
149 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 239 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
150 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 240 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
151 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 241 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
152 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED, 242 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED,
153 ]) 243 ])
154 Results._ensure_included_in_category_dict(categories_failures, 244 Results._ensure_included_in_category_dict(categories_failures,
155 'resultType', [ 245 'resultType', [
156 gm_json.JSONKEY_ACTUALRESULTS_FAILED, 246 gm_json.JSONKEY_ACTUALRESULTS_FAILED,
157 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED, 247 gm_json.JSONKEY_ACTUALRESULTS_FAILUREIGNORED,
158 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON, 248 gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON,
159 ]) 249 ])
160 250
161 data_all = [] 251 data_all = []
162 data_failures = [] 252 data_failures = []
163 for builder in sorted(self._actual_builder_dicts.keys()): 253 for builder in sorted(actual_builder_dicts.keys()):
164 actual_results_for_this_builder = ( 254 actual_results_for_this_builder = (
165 self._actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS]) 255 actual_builder_dicts[builder][gm_json.JSONKEY_ACTUALRESULTS])
166 for result_type in sorted(actual_results_for_this_builder.keys()): 256 for result_type in sorted(actual_results_for_this_builder.keys()):
167 results_of_this_type = actual_results_for_this_builder[result_type] 257 results_of_this_type = actual_results_for_this_builder[result_type]
168 if not results_of_this_type: 258 if not results_of_this_type:
169 continue 259 continue
170 for image_name in sorted(results_of_this_type.keys()): 260 for image_name in sorted(results_of_this_type.keys()):
171 actual_image = results_of_this_type[image_name] 261 actual_image = results_of_this_type[image_name]
172 try: 262 try:
173 # TODO(epoger): assumes a single allowed digest per test 263 # TODO(epoger): assumes a single allowed digest per test
174 expected_image = ( 264 expected_image = (
175 self._expected_builder_dicts 265 expected_builder_dicts
176 [builder][gm_json.JSONKEY_EXPECTEDRESULTS] 266 [builder][gm_json.JSONKEY_EXPECTEDRESULTS]
177 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] 267 [image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS]
178 [0]) 268 [0])
179 except (KeyError, TypeError): 269 except (KeyError, TypeError):
180 # There are several cases in which we would expect to find 270 # There are several cases in which we would expect to find
181 # no expectations for a given test: 271 # no expectations for a given test:
182 # 272 #
183 # 1. result_type == NOCOMPARISON 273 # 1. result_type == NOCOMPARISON
184 # There are no expectations for this test yet! 274 # There are no expectations for this test yet!
185 # 275 #
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after
291 category_dict: category dict-of-dicts to modify 381 category_dict: category dict-of-dicts to modify
292 category_name: category name, as a string 382 category_name: category name, as a string
293 category_values: list of values we want to make sure are represented 383 category_values: list of values we want to make sure are represented
294 for this category 384 for this category
295 """ 385 """
296 if not category_dict.get(category_name): 386 if not category_dict.get(category_name):
297 category_dict[category_name] = {} 387 category_dict[category_name] = {}
298 for category_value in category_values: 388 for category_value in category_values:
299 if not category_dict[category_name].get(category_value): 389 if not category_dict[category_name].get(category_value):
300 category_dict[category_name][category_value] = 0 390 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