OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 ''' | 3 ''' |
4 Copyright 2012 Google Inc. | 4 Copyright 2012 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 | 9 |
10 ''' | 10 ''' |
11 Rebaselines the given GM tests, on all bots and all configurations. | 11 Rebaselines the given GM tests, on all bots and all configurations. |
12 Must be run from the gm-expected directory. If run from a git or SVN | 12 Must be run from the gm-expected directory. If run from a git or SVN |
13 checkout, the files will be added to the staging area for commit. | 13 checkout, the files will be added to the staging area for commit. |
14 ''' | 14 ''' |
15 | 15 |
16 # System-level imports | 16 # System-level imports |
17 import argparse | 17 import argparse |
18 import os | 18 import os |
| 19 import re |
19 import subprocess | 20 import subprocess |
20 import sys | 21 import sys |
21 import urllib2 | 22 import urllib2 |
22 | 23 |
23 # Imports from within Skia | 24 # Imports from within Skia |
24 # | 25 # |
25 # We need to add the 'gm' directory, so that we can import gm_json.py within | 26 # We need to add the 'gm' directory, so that we can import gm_json.py within |
26 # that directory. That script allows us to parse the actual-results.json file | 27 # that directory. That script allows us to parse the actual-results.json file |
27 # written out by the GM tool. | 28 # written out by the GM tool. |
28 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | 29 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
98 if not subdirs: | 99 if not subdirs: |
99 self._subdirs = sorted(SUBDIR_MAPPING.keys()) | 100 self._subdirs = sorted(SUBDIR_MAPPING.keys()) |
100 self._missing_json_is_fatal = False | 101 self._missing_json_is_fatal = False |
101 else: | 102 else: |
102 self._subdirs = subdirs | 103 self._subdirs = subdirs |
103 self._missing_json_is_fatal = True | 104 self._missing_json_is_fatal = True |
104 self._json_base_url = json_base_url | 105 self._json_base_url = json_base_url |
105 self._json_filename = json_filename | 106 self._json_filename = json_filename |
106 self._dry_run = dry_run | 107 self._dry_run = dry_run |
107 self._add_new = add_new | 108 self._add_new = add_new |
| 109 self._googlestorage_gm_actuals_root = ( |
| 110 'http://chromium-skia-gm.commondatastorage.googleapis.com/gm') |
| 111 self._testname_pattern = re.compile('(\S+)_(\S+).png') |
108 self._is_svn_checkout = ( | 112 self._is_svn_checkout = ( |
109 os.path.exists('.svn') or | 113 os.path.exists('.svn') or |
110 os.path.exists(os.path.join(os.pardir, '.svn'))) | 114 os.path.exists(os.path.join(os.pardir, '.svn'))) |
111 self._is_git_checkout = ( | 115 self._is_git_checkout = ( |
112 os.path.exists('.git') or | 116 os.path.exists('.git') or |
113 os.path.exists(os.path.join(os.pardir, '.git'))) | 117 os.path.exists(os.path.join(os.pardir, '.git'))) |
114 | 118 |
115 # If dry_run is False, execute subprocess.call(cmd). | 119 # If dry_run is False, execute subprocess.call(cmd). |
116 # If dry_run is True, print the command we would have otherwise run. | 120 # If dry_run is True, print the command we would have otherwise run. |
117 # Raises a CommandFailedException if the command fails. | 121 # Raises a CommandFailedException if the command fails. |
118 def _Call(self, cmd): | 122 def _Call(self, cmd): |
119 if self._dry_run: | 123 if self._dry_run: |
120 print '%s' % ' '.join(cmd) | 124 print '%s' % ' '.join(cmd) |
121 return | 125 return |
122 if subprocess.call(cmd) != 0: | 126 if subprocess.call(cmd) != 0: |
123 raise CommandFailedException('error running command: ' + | 127 raise CommandFailedException('error running command: ' + |
124 ' '.join(cmd)) | 128 ' '.join(cmd)) |
125 | 129 |
| 130 # Download a single actual result from GoogleStorage, returning True if it |
| 131 # succeeded. |
| 132 def _DownloadFromGoogleStorage(self, infilename, outfilename, all_results): |
| 133 test_name = self._testname_pattern.match(infilename).group(1) |
| 134 if not test_name: |
| 135 print '# unable to find test_name for infilename %s' % infilename |
| 136 return False |
| 137 try: |
| 138 hash_type, hash_value = all_results[infilename] |
| 139 except KeyError: |
| 140 print ('# unable to find filename %s in all_results dict' % |
| 141 infilename) |
| 142 return False |
| 143 url = '%s/%s/%s/%s.png' % (self._googlestorage_gm_actuals_root, |
| 144 hash_type, test_name, hash_value) |
| 145 try: |
| 146 self._DownloadFile(source_url=url, dest_filename=outfilename) |
| 147 return True |
| 148 except CommandFailedException: |
| 149 print '# Couldn\'t fetch gs_url %s' % url |
| 150 return False |
| 151 |
| 152 # Download a single actual result from skia-autogen, returning True if it |
| 153 # succeeded. |
| 154 def _DownloadFromAutogen(self, infilename, outfilename, |
| 155 expectations_subdir, builder_name): |
| 156 url = ('http://skia-autogen.googlecode.com/svn/gm-actual/' + |
| 157 expectations_subdir + '/' + builder_name + '/' + |
| 158 expectations_subdir + '/' + infilename) |
| 159 try: |
| 160 self._DownloadFile(source_url=url, dest_filename=outfilename) |
| 161 return True |
| 162 except CommandFailedException: |
| 163 print '# Couldn\'t fetch autogen_url %s' % url |
| 164 return False |
| 165 |
126 # Download a single file, raising a CommandFailedException if it fails. | 166 # Download a single file, raising a CommandFailedException if it fails. |
127 def _DownloadFile(self, source_url, dest_filename): | 167 def _DownloadFile(self, source_url, dest_filename): |
128 # Download into a temporary file and then rename it afterwards, | 168 # Download into a temporary file and then rename it afterwards, |
129 # so that we don't corrupt the existing file if it fails midway thru. | 169 # so that we don't corrupt the existing file if it fails midway thru. |
130 temp_filename = os.path.join(os.path.dirname(dest_filename), | 170 temp_filename = os.path.join(os.path.dirname(dest_filename), |
131 '.temp-' + os.path.basename(dest_filename)) | 171 '.temp-' + os.path.basename(dest_filename)) |
132 | 172 |
133 # TODO(epoger): Replace calls to "curl"/"mv" (which will only work on | 173 # TODO(epoger): Replace calls to "curl"/"mv" (which will only work on |
134 # Unix) with a Python HTTP library (which should work cross-platform) | 174 # Unix) with a Python HTTP library (which should work cross-platform) |
135 self._Call([ 'curl', '--fail', '--silent', source_url, | 175 self._Call([ 'curl', '--fail', '--silent', source_url, |
136 '--output', temp_filename ]) | 176 '--output', temp_filename ]) |
137 self._Call([ 'mv', temp_filename, dest_filename ]) | 177 self._Call([ 'mv', temp_filename, dest_filename ]) |
138 | 178 |
139 # Returns the full contents of a URL, as a single string. | 179 # Returns the full contents of a URL, as a single string. |
140 # | 180 # |
141 # Unlike standard URL handling, we allow relative "file:" URLs; | 181 # Unlike standard URL handling, we allow relative "file:" URLs; |
142 # for example, "file:one/two" resolves to the file ./one/two | 182 # for example, "file:one/two" resolves to the file ./one/two |
143 # (relative to current working dir) | 183 # (relative to current working dir) |
144 def _GetContentsOfUrl(self, url): | 184 def _GetContentsOfUrl(self, url): |
145 file_prefix = 'file:' | 185 file_prefix = 'file:' |
146 if url.startswith(file_prefix): | 186 if url.startswith(file_prefix): |
147 filename = url[len(file_prefix):] | 187 filename = url[len(file_prefix):] |
148 return open(filename, 'r').read() | 188 return open(filename, 'r').read() |
149 else: | 189 else: |
150 return urllib2.urlopen(url).read() | 190 return urllib2.urlopen(url).read() |
151 | 191 |
| 192 # Returns a dictionary of actual results from actual-results.json file. |
| 193 # |
| 194 # The dictionary returned has this format: |
| 195 # { |
| 196 # u'imageblur_565.png': [u'bitmap-64bitMD5', 3359963596899141322], |
| 197 # u'imageblur_8888.png': [u'bitmap-64bitMD5', 4217923806027861152], |
| 198 # u'shadertext3_8888.png': [u'bitmap-64bitMD5', 3713708307125704716] |
| 199 # } |
| 200 # |
| 201 # If the JSON actual result summary file cannot be loaded, the behavior |
| 202 # depends on self._missing_json_is_fatal: |
| 203 # - if true: execution will halt with an exception |
| 204 # - if false: we will log an error message but return an empty dictionary |
| 205 # |
| 206 # params: |
| 207 # json_url: URL pointing to a JSON actual result summary file |
| 208 # sections: a list of section names to include in the results, e.g. |
| 209 # [gm_json.JSONKEY_ACTUALRESULTS_FAILED, |
| 210 # gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON] ; |
| 211 # if None, then include ALL sections. |
| 212 def _GetActualResults(self, json_url, sections=None): |
| 213 try: |
| 214 json_contents = self._GetContentsOfUrl(json_url) |
| 215 except (urllib2.HTTPError, IOError): |
| 216 message = 'unable to load JSON summary URL %s' % json_url |
| 217 if self._missing_json_is_fatal: |
| 218 raise ValueError(message) |
| 219 else: |
| 220 print '# %s' % message |
| 221 return {} |
| 222 |
| 223 json_dict = gm_json.LoadFromString(json_contents) |
| 224 results_to_return = {} |
| 225 actual_results = json_dict[gm_json.JSONKEY_ACTUALRESULTS] |
| 226 if not sections: |
| 227 sections = actual_results.keys() |
| 228 for section in sections: |
| 229 section_results = actual_results[section] |
| 230 if section_results: |
| 231 results_to_return.update(section_results) |
| 232 return results_to_return |
| 233 |
152 # Returns a list of files that require rebaselining. | 234 # Returns a list of files that require rebaselining. |
153 # | 235 # |
154 # Note that this returns a list of FILES, like this: | 236 # Note that this returns a list of FILES, like this: |
155 # ['imageblur_565.png', 'xfermodes_pdf.png'] | 237 # ['imageblur_565.png', 'xfermodes_pdf.png'] |
156 # rather than a list of TESTS, like this: | 238 # rather than a list of TESTS, like this: |
157 # ['imageblur', 'xfermodes'] | 239 # ['imageblur', 'xfermodes'] |
158 # | 240 # |
159 # If the JSON actual result summary file cannot be loaded, the behavior | |
160 # depends on self._missing_json_is_fatal: | |
161 # - if true: execution will halt with an exception | |
162 # - if false: we will log an error message but return an empty list so we | |
163 # go on to the next platform | |
164 # | |
165 # params: | 241 # params: |
166 # json_url: URL pointing to a JSON actual result summary file | 242 # json_url: URL pointing to a JSON actual result summary file |
167 # add_new: if True, then return files listed in any of these sections: | 243 # add_new: if True, then return files listed in any of these sections: |
168 # - JSONKEY_ACTUALRESULTS_FAILED | 244 # - JSONKEY_ACTUALRESULTS_FAILED |
169 # - JSONKEY_ACTUALRESULTS_NOCOMPARISON | 245 # - JSONKEY_ACTUALRESULTS_NOCOMPARISON |
170 # if False, then return files listed in these sections: | 246 # if False, then return files listed in these sections: |
171 # - JSONKEY_ACTUALRESULTS_FAILED | 247 # - JSONKEY_ACTUALRESULTS_FAILED |
172 # | 248 # |
173 def _GetFilesToRebaseline(self, json_url, add_new): | 249 def _GetFilesToRebaseline(self, json_url, add_new): |
174 if self._dry_run: | 250 if self._dry_run: |
175 print '' | 251 print '' |
176 print '#' | 252 print '#' |
177 print ('# Getting files to rebaseline from JSON summary URL %s ...' | 253 print ('# Getting files to rebaseline from JSON summary URL %s ...' |
178 % json_url) | 254 % json_url) |
179 try: | |
180 json_contents = self._GetContentsOfUrl(json_url) | |
181 except urllib2.HTTPError: | |
182 message = 'unable to load JSON summary URL %s' % json_url | |
183 if self._missing_json_is_fatal: | |
184 raise ValueError(message) | |
185 else: | |
186 print '# %s' % message | |
187 return [] | |
188 | |
189 json_dict = gm_json.LoadFromString(json_contents) | |
190 actual_results = json_dict[gm_json.JSONKEY_ACTUALRESULTS] | |
191 sections = [gm_json.JSONKEY_ACTUALRESULTS_FAILED] | 255 sections = [gm_json.JSONKEY_ACTUALRESULTS_FAILED] |
192 if add_new: | 256 if add_new: |
193 sections.append(gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON) | 257 sections.append(gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON) |
194 | 258 results_to_rebaseline = self._GetActualResults(json_url=json_url, |
195 files_to_rebaseline = [] | 259 sections=sections) |
196 for section in sections: | 260 files_to_rebaseline = results_to_rebaseline.keys() |
197 section_results = actual_results[section] | 261 files_to_rebaseline.sort() |
198 if section_results: | |
199 files_to_rebaseline.extend(section_results.keys()) | |
200 | |
201 print '# ... found files_to_rebaseline %s' % files_to_rebaseline | 262 print '# ... found files_to_rebaseline %s' % files_to_rebaseline |
202 if self._dry_run: | 263 if self._dry_run: |
203 print '#' | 264 print '#' |
204 return files_to_rebaseline | 265 return files_to_rebaseline |
205 | 266 |
206 # Rebaseline a single file. | 267 # Rebaseline a single file. |
207 def _RebaselineOneFile(self, expectations_subdir, builder_name, | 268 def _RebaselineOneFile(self, expectations_subdir, builder_name, |
208 infilename, outfilename): | 269 infilename, outfilename, all_results): |
209 if self._dry_run: | 270 if self._dry_run: |
210 print '' | 271 print '' |
211 print '# ' + infilename | 272 print '# ' + infilename |
212 url = ('http://skia-autogen.googlecode.com/svn/gm-actual/' + | |
213 expectations_subdir + '/' + builder_name + '/' + | |
214 expectations_subdir + '/' + infilename) | |
215 | 273 |
216 # Try to download this file, but if that fails, keep going... | 274 # First try to download this result image from Google Storage. |
| 275 # If that fails, try skia-autogen. |
| 276 # If that fails too, just go on to the next file. |
217 # | 277 # |
218 # This not treated as a fatal failure because not all | 278 # This not treated as a fatal failure because not all |
219 # platforms generate all configs (e.g., Android does not | 279 # platforms generate all configs (e.g., Android does not |
220 # generate PDF). | 280 # generate PDF). |
221 # | 281 # |
222 # We could tweak the list of configs within this tool to | 282 # TODO(epoger): Once we are downloading only files that the |
223 # reflect which combinations the bots actually generate, and | 283 # actual-results.json file told us to, this should become a |
224 # then fail if any of those expected combinations are | 284 # fatal error. (If the actual-results.json file told us that |
225 # missing... but then this tool would become useless every | 285 # the test failed with XXX results, we should be able to download |
226 # time someone tweaked the configs on the bots without | 286 # those results every time.) |
227 # updating this script. | 287 if not self._DownloadFromGoogleStorage(infilename=infilename, |
228 try: | 288 outfilename=outfilename, |
229 self._DownloadFile(source_url=url, dest_filename=outfilename) | 289 all_results=all_results): |
230 except CommandFailedException: | 290 if not self._DownloadFromAutogen(infilename=infilename, |
231 print '# Couldn\'t fetch ' + url | 291 outfilename=outfilename, |
232 return | 292 expectations_subdir=expectations_su
bdir, |
| 293 builder_name=builder_name): |
| 294 print '# Couldn\'t fetch infilename ' + infilename |
| 295 return |
233 | 296 |
234 # Add this file to version control (if appropriate). | 297 # Add this file to version control (if appropriate). |
235 if self._add_new: | 298 if self._add_new: |
236 if self._is_svn_checkout: | 299 if self._is_svn_checkout: |
237 cmd = [ 'svn', 'add', '--quiet', outfilename ] | 300 cmd = [ 'svn', 'add', '--quiet', outfilename ] |
238 self._Call(cmd) | 301 self._Call(cmd) |
239 cmd = [ 'svn', 'propset', '--quiet', 'svn:mime-type', | 302 cmd = [ 'svn', 'propset', '--quiet', 'svn:mime-type', |
240 'image/png', outfilename ]; | 303 'image/png', outfilename ]; |
241 self._Call(cmd) | 304 self._Call(cmd) |
242 elif self._is_git_checkout: | 305 elif self._is_git_checkout: |
243 cmd = [ 'git', 'add', outfilename ] | 306 cmd = [ 'git', 'add', outfilename ] |
244 self._Call(cmd) | 307 self._Call(cmd) |
245 | 308 |
246 # Rebaseline the given configs for a single test. | 309 # Rebaseline the given configs for a single test. |
247 # | 310 # |
248 # params: | 311 # params: |
249 # expectations_subdir | 312 # expectations_subdir |
250 # builder_name | 313 # builder_name |
251 # test: a single test to rebaseline | 314 # test: a single test to rebaseline |
252 def _RebaselineOneTest(self, expectations_subdir, builder_name, test): | 315 # all_results: a dictionary of all actual results |
| 316 def _RebaselineOneTest(self, expectations_subdir, builder_name, test, |
| 317 all_results): |
253 if self._configs: | 318 if self._configs: |
254 configs = self._configs | 319 configs = self._configs |
255 else: | 320 else: |
256 if (expectations_subdir == 'base-shuttle-win7-intel-angle'): | 321 if (expectations_subdir == 'base-shuttle-win7-intel-angle'): |
257 configs = [ 'angle', 'anglemsaa16' ] | 322 configs = [ 'angle', 'anglemsaa16' ] |
258 else: | 323 else: |
259 configs = [ '565', '8888', 'gpu', 'pdf', 'mesa', 'msaa16', | 324 configs = [ '565', '8888', 'gpu', 'pdf', 'mesa', 'msaa16', |
260 'msaa4' ] | 325 'msaa4' ] |
261 if self._dry_run: | 326 if self._dry_run: |
262 print '' | 327 print '' |
263 print '# ' + expectations_subdir + ':' | 328 print '# ' + expectations_subdir + ':' |
264 for config in configs: | 329 for config in configs: |
265 infilename = test + '_' + config + '.png' | 330 infilename = test + '_' + config + '.png' |
266 outfilename = os.path.join(expectations_subdir, infilename); | 331 outfilename = os.path.join(expectations_subdir, infilename); |
267 self._RebaselineOneFile(expectations_subdir=expectations_subdir, | 332 self._RebaselineOneFile(expectations_subdir=expectations_subdir, |
268 builder_name=builder_name, | 333 builder_name=builder_name, |
269 infilename=infilename, | 334 infilename=infilename, |
270 outfilename=outfilename) | 335 outfilename=outfilename, |
| 336 all_results=all_results) |
271 | 337 |
272 # Rebaseline all platforms/tests/types we specified in the constructor. | 338 # Rebaseline all platforms/tests/types we specified in the constructor. |
273 def RebaselineAll(self): | 339 def RebaselineAll(self): |
274 for subdir in self._subdirs: | 340 for subdir in self._subdirs: |
275 if not subdir in SUBDIR_MAPPING.keys(): | 341 if not subdir in SUBDIR_MAPPING.keys(): |
276 raise Exception(('unrecognized platform subdir "%s"; ' + | 342 raise Exception(('unrecognized platform subdir "%s"; ' + |
277 'should be one of %s') % ( | 343 'should be one of %s') % ( |
278 subdir, SUBDIR_MAPPING.keys())) | 344 subdir, SUBDIR_MAPPING.keys())) |
279 builder_name = SUBDIR_MAPPING[subdir] | 345 builder_name = SUBDIR_MAPPING[subdir] |
| 346 json_url = '/'.join([self._json_base_url, |
| 347 subdir, builder_name, subdir, |
| 348 self._json_filename]) |
| 349 all_results = self._GetActualResults(json_url=json_url) |
| 350 |
280 if self._tests: | 351 if self._tests: |
281 for test in self._tests: | 352 for test in self._tests: |
282 self._RebaselineOneTest(expectations_subdir=subdir, | 353 self._RebaselineOneTest(expectations_subdir=subdir, |
283 builder_name=builder_name, | 354 builder_name=builder_name, |
284 test=test) | 355 test=test, all_results=all_results) |
285 else: # get the raw list of files that need rebaselining from JSON | 356 else: # get the raw list of files that need rebaselining from JSON |
286 json_url = '/'.join([self._json_base_url, | |
287 subdir, builder_name, subdir, | |
288 self._json_filename]) | |
289 filenames = self._GetFilesToRebaseline(json_url=json_url, | 357 filenames = self._GetFilesToRebaseline(json_url=json_url, |
290 add_new=self._add_new) | 358 add_new=self._add_new) |
291 for filename in filenames: | 359 for filename in filenames: |
292 outfilename = os.path.join(subdir, filename); | 360 outfilename = os.path.join(subdir, filename); |
293 self._RebaselineOneFile(expectations_subdir=subdir, | 361 self._RebaselineOneFile(expectations_subdir=subdir, |
294 builder_name=builder_name, | 362 builder_name=builder_name, |
295 infilename=filename, | 363 infilename=filename, |
296 outfilename=outfilename) | 364 outfilename=outfilename, |
| 365 all_results=all_results) |
297 | 366 |
298 # main... | 367 # main... |
299 | 368 |
300 parser = argparse.ArgumentParser() | 369 parser = argparse.ArgumentParser() |
301 parser.add_argument('--add-new', action='store_true', | 370 parser.add_argument('--add-new', action='store_true', |
302 help='in addition to the standard behavior of ' + | 371 help='in addition to the standard behavior of ' + |
303 'updating expectations for failing tests, add ' + | 372 'updating expectations for failing tests, add ' + |
304 'expectations for tests which don\'t have expectations ' + | 373 'expectations for tests which don\'t have expectations ' + |
305 'yet.') | 374 'yet.') |
306 parser.add_argument('--configs', metavar='CONFIG', nargs='+', | 375 parser.add_argument('--configs', metavar='CONFIG', nargs='+', |
(...skipping 22 matching lines...) Expand all Loading... |
329 '"--tests aaclip bigmatrix"; if unspecified, then all ' + | 398 '"--tests aaclip bigmatrix"; if unspecified, then all ' + |
330 'failing tests (according to the actual-results.json ' + | 399 'failing tests (according to the actual-results.json ' + |
331 'file) will be rebaselined.') | 400 'file) will be rebaselined.') |
332 args = parser.parse_args() | 401 args = parser.parse_args() |
333 rebaseliner = Rebaseliner(tests=args.tests, configs=args.configs, | 402 rebaseliner = Rebaseliner(tests=args.tests, configs=args.configs, |
334 subdirs=args.subdirs, dry_run=args.dry_run, | 403 subdirs=args.subdirs, dry_run=args.dry_run, |
335 json_base_url=args.json_base_url, | 404 json_base_url=args.json_base_url, |
336 json_filename=args.json_filename, | 405 json_filename=args.json_filename, |
337 add_new=args.add_new) | 406 add_new=args.add_new) |
338 rebaseliner.RebaselineAll() | 407 rebaseliner.RebaselineAll() |
OLD | NEW |