blob: 692cb1abc181648105ada29d1f817f278890e4b9 [file] [log] [blame]
senorblanco@chromium.org782f3b42012-10-29 18:06:26 +00001#!/usr/bin/python
2
3'''
4Copyright 2012 Google Inc.
5
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
8'''
9
10'''
senorblanco@chromium.org123a0b52012-11-29 21:50:34 +000011Rebaselines the given GM tests, on all bots and all configurations.
12Must be run from the gm-expected directory. If run from a git or SVN
13checkout, the files will be added to the staging area for commit.
epoger@google.com61822a22013-07-16 18:56:32 +000014
15TODO(epoger): Fix indentation in this file (2-space indents, not 4-space).
senorblanco@chromium.org782f3b42012-10-29 18:06:26 +000016'''
17
epoger@google.com99ba65a2013-06-05 15:43:37 +000018# System-level imports
epoger@google.com9166bf52013-05-30 15:46:19 +000019import argparse
epoger@google.comec3397b2013-05-29 17:09:43 +000020import os
epoger@google.come78d2072013-06-12 17:44:14 +000021import re
epoger@google.comec3397b2013-05-29 17:09:43 +000022import sys
epoger@google.com99ba65a2013-06-05 15:43:37 +000023import urllib2
24
epoger@google.com99a8ec92013-06-19 18:56:59 +000025# Imports from local directory
26import rebaseline_imagefiles
27
epoger@google.com99ba65a2013-06-05 15:43:37 +000028# Imports from within Skia
29#
epoger@google.comdad53102013-06-12 14:25:30 +000030# We need to add the 'gm' directory, so that we can import gm_json.py within
31# that directory. That script allows us to parse the actual-results.json file
32# written out by the GM tool.
33# Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end*
34# so any dirs that are already in the PYTHONPATH will be preferred.
35#
36# This assumes that the 'gm' directory has been checked out as a sibling of
37# the 'tools' directory containing this script, which will be the case if
38# 'trunk' was checked out as a single unit.
epoger@google.com99ba65a2013-06-05 15:43:37 +000039GM_DIRECTORY = os.path.realpath(
40 os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm'))
41if GM_DIRECTORY not in sys.path:
42 sys.path.append(GM_DIRECTORY)
43import gm_json
44
epoger@google.comec3397b2013-05-29 17:09:43 +000045# Mapping of gm-expectations subdir (under
46# https://skia.googlecode.com/svn/gm-expected/ )
47# to builder name (see list at http://108.170.217.252:10117/builders )
epoger@google.com9166bf52013-05-30 15:46:19 +000048SUBDIR_MAPPING = {
epoger@google.comec3397b2013-05-29 17:09:43 +000049 'base-shuttle-win7-intel-float':
50 'Test-Win7-ShuttleA-HD2000-x86-Release',
51 'base-shuttle-win7-intel-angle':
52 'Test-Win7-ShuttleA-HD2000-x86-Release-ANGLE',
53 'base-shuttle-win7-intel-directwrite':
54 'Test-Win7-ShuttleA-HD2000-x86-Release-DirectWrite',
55 'base-shuttle_ubuntu12_ati5770':
56 'Test-Ubuntu12-ShuttleA-ATI5770-x86_64-Release',
57 'base-macmini':
58 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release',
59 'base-macmini-lion-float':
60 'Test-Mac10.7-MacMini4.1-GeForce320M-x86-Release',
61 'base-android-galaxy-nexus':
62 'Test-Android-GalaxyNexus-SGX540-Arm7-Debug',
63 'base-android-nexus-7':
64 'Test-Android-Nexus7-Tegra3-Arm7-Release',
65 'base-android-nexus-s':
66 'Test-Android-NexusS-SGX540-Arm7-Release',
67 'base-android-xoom':
68 'Test-Android-Xoom-Tegra2-Arm7-Release',
69 'base-android-nexus-10':
70 'Test-Android-Nexus10-MaliT604-Arm7-Release',
robertphillips@google.com63e96272013-07-02 12:54:37 +000071 'base-android-nexus-4':
72 'Test-Android-Nexus4-Adreno320-Arm7-Release',
epoger@google.comec3397b2013-05-29 17:09:43 +000073}
74
epoger@google.com9166bf52013-05-30 15:46:19 +000075
epoger@google.com66ba9f92013-07-11 19:20:30 +000076class _InternalException(Exception):
epoger@google.comdb29a312013-06-04 14:58:47 +000077 pass
78
epoger@google.comffcbdbf2013-07-16 17:35:39 +000079# Object that handles exceptions, either raising them immediately or collecting
80# them to display later on.
81class ExceptionHandler(object):
82
83 # params:
84 # keep_going_on_failure: if False, report failures and quit right away;
85 # if True, collect failures until
86 # ReportAllFailures() is called
87 def __init__(self, keep_going_on_failure=False):
88 self._keep_going_on_failure = keep_going_on_failure
89 self._failures_encountered = []
90 self._exiting = False
91
92 # Exit the program with the given status value.
93 def _Exit(self, status=1):
94 self._exiting = True
95 sys.exit(status)
96
97 # We have encountered an exception; either collect the info and keep going,
98 # or exit the program right away.
99 def RaiseExceptionOrContinue(self, e):
100 # If we are already quitting the program, propagate any exceptions
101 # so that the proper exit status will be communicated to the shell.
102 if self._exiting:
103 raise e
104
105 if self._keep_going_on_failure:
106 print >> sys.stderr, 'WARNING: swallowing exception %s' % e
107 self._failures_encountered.append(e)
108 else:
109 print >> sys.stderr, e
110 print >> sys.stderr, (
111 'Halting at first exception; to keep going, re-run ' +
112 'with the --keep-going-on-failure option set.')
113 self._Exit()
114
115 def ReportAllFailures(self):
116 if self._failures_encountered:
117 print >> sys.stderr, ('Encountered %d failures (see above).' %
118 len(self._failures_encountered))
119 self._Exit()
120
121
epoger@google.com99a8ec92013-06-19 18:56:59 +0000122# Object that rebaselines a JSON expectations file (not individual image files).
epoger@google.com99a8ec92013-06-19 18:56:59 +0000123class JsonRebaseliner(object):
epoger@google.com9166bf52013-05-30 15:46:19 +0000124
125 # params:
epoger@google.coma783f2b2013-07-08 17:51:58 +0000126 # expectations_root: root directory of all expectations JSON files
127 # expectations_filename: filename (under expectations_root) of JSON
128 # expectations file; typically
129 # "expected-results.json"
130 # actuals_base_url: base URL from which to read actual-result JSON files
131 # actuals_filename: filename (under actuals_base_url) from which to read a
132 # summary of results; typically "actual-results.json"
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000133 # exception_handler: reference to rebaseline.ExceptionHandler object
epoger@google.com99ba65a2013-06-05 15:43:37 +0000134 # tests: list of tests to rebaseline, or None if we should rebaseline
135 # whatever files the JSON results summary file tells us to
epoger@google.com9de25e32013-07-10 15:27:18 +0000136 # configs: which configs to run for each test, or None if we should
137 # rebaseline whatever configs the JSON results summary file tells
138 # us to
epoger@google.comdad53102013-06-12 14:25:30 +0000139 # add_new: if True, add expectations for tests which don't have any yet
epoger@google.coma783f2b2013-07-08 17:51:58 +0000140 def __init__(self, expectations_root, expectations_filename,
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000141 actuals_base_url, actuals_filename, exception_handler,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000142 tests=None, configs=None, add_new=False):
epoger@google.com99a8ec92013-06-19 18:56:59 +0000143 self._expectations_root = expectations_root
epoger@google.coma783f2b2013-07-08 17:51:58 +0000144 self._expectations_filename = expectations_filename
epoger@google.com9166bf52013-05-30 15:46:19 +0000145 self._tests = tests
146 self._configs = configs
epoger@google.coma783f2b2013-07-08 17:51:58 +0000147 self._actuals_base_url = actuals_base_url
148 self._actuals_filename = actuals_filename
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000149 self._exception_handler = exception_handler
epoger@google.comdad53102013-06-12 14:25:30 +0000150 self._add_new = add_new
epoger@google.com61822a22013-07-16 18:56:32 +0000151 self._image_filename_re = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
epoger@google.com9166bf52013-05-30 15:46:19 +0000152
epoger@google.coma783f2b2013-07-08 17:51:58 +0000153 # Returns the full contents of filepath, as a single string.
154 # If filepath looks like a URL, try to read it that way instead of as
155 # a path on local storage.
epoger@google.com66ba9f92013-07-11 19:20:30 +0000156 #
157 # Raises _InternalException if there is a problem.
epoger@google.coma783f2b2013-07-08 17:51:58 +0000158 def _GetFileContents(self, filepath):
159 if filepath.startswith('http:') or filepath.startswith('https:'):
epoger@google.com66ba9f92013-07-11 19:20:30 +0000160 try:
161 return urllib2.urlopen(filepath).read()
162 except urllib2.HTTPError as e:
163 raise _InternalException('unable to read URL %s: %s' % (
164 filepath, e))
epoger@google.com99ba65a2013-06-05 15:43:37 +0000165 else:
epoger@google.coma783f2b2013-07-08 17:51:58 +0000166 return open(filepath, 'r').read()
epoger@google.com99ba65a2013-06-05 15:43:37 +0000167
epoger@google.come78d2072013-06-12 17:44:14 +0000168 # Returns a dictionary of actual results from actual-results.json file.
169 #
170 # The dictionary returned has this format:
171 # {
172 # u'imageblur_565.png': [u'bitmap-64bitMD5', 3359963596899141322],
173 # u'imageblur_8888.png': [u'bitmap-64bitMD5', 4217923806027861152],
174 # u'shadertext3_8888.png': [u'bitmap-64bitMD5', 3713708307125704716]
175 # }
176 #
epoger@google.com66ba9f92013-07-11 19:20:30 +0000177 # If the JSON actual result summary file cannot be loaded, logs a warning
178 # message and returns None.
179 # If the JSON actual result summary file can be loaded, but we have
180 # trouble parsing it, raises an Exception.
epoger@google.come78d2072013-06-12 17:44:14 +0000181 #
182 # params:
183 # json_url: URL pointing to a JSON actual result summary file
184 # sections: a list of section names to include in the results, e.g.
185 # [gm_json.JSONKEY_ACTUALRESULTS_FAILED,
186 # gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON] ;
187 # if None, then include ALL sections.
188 def _GetActualResults(self, json_url, sections=None):
epoger@google.com66ba9f92013-07-11 19:20:30 +0000189 try:
190 json_contents = self._GetFileContents(json_url)
191 except _InternalException:
192 print >> sys.stderr, (
193 'could not read json_url %s ; skipping this platform.' %
194 json_url)
195 return None
epoger@google.come78d2072013-06-12 17:44:14 +0000196 json_dict = gm_json.LoadFromString(json_contents)
197 results_to_return = {}
198 actual_results = json_dict[gm_json.JSONKEY_ACTUALRESULTS]
199 if not sections:
200 sections = actual_results.keys()
201 for section in sections:
202 section_results = actual_results[section]
203 if section_results:
204 results_to_return.update(section_results)
205 return results_to_return
206
epoger@google.com99a8ec92013-06-19 18:56:59 +0000207 # Rebaseline all tests/types we specified in the constructor,
208 # within this gm-expectations subdir.
209 #
210 # params:
211 # subdir : e.g. 'base-shuttle-win7-intel-float'
212 # builder : e.g. 'Test-Win7-ShuttleA-HD2000-x86-Release'
213 def RebaselineSubdir(self, subdir, builder):
epoger@google.coma783f2b2013-07-08 17:51:58 +0000214 # Read in the actual result summary, and extract all the tests whose
215 # results we need to update.
216 actuals_url = '/'.join([self._actuals_base_url,
217 subdir, builder, subdir,
218 self._actuals_filename])
epoger@google.comb248dd52013-07-17 00:09:10 +0000219 # In most cases, we won't need to re-record results that are already
220 # succeeding, but including the SUCCEEDED results will allow us to
221 # re-record expectations if they somehow get out of sync.
222 sections = [gm_json.JSONKEY_ACTUALRESULTS_FAILED,
223 gm_json.JSONKEY_ACTUALRESULTS_SUCCEEDED]
epoger@google.coma783f2b2013-07-08 17:51:58 +0000224 if self._add_new:
225 sections.append(gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON)
226 results_to_update = self._GetActualResults(json_url=actuals_url,
227 sections=sections)
epoger@google.come78d2072013-06-12 17:44:14 +0000228
epoger@google.coma783f2b2013-07-08 17:51:58 +0000229 # Read in current expectations.
230 expectations_json_filepath = os.path.join(
231 self._expectations_root, subdir, self._expectations_filename)
232 expectations_dict = gm_json.LoadFromFile(expectations_json_filepath)
epoger@google.com4b383012013-07-16 21:10:54 +0000233 expected_results = expectations_dict[gm_json.JSONKEY_EXPECTEDRESULTS]
epoger@google.coma783f2b2013-07-08 17:51:58 +0000234
235 # Update the expectations in memory, skipping any tests/configs that
236 # the caller asked to exclude.
237 skipped_images = []
238 if results_to_update:
239 for (image_name, image_results) in results_to_update.iteritems():
epoger@google.com61822a22013-07-16 18:56:32 +0000240 (test, config) = \
241 self._image_filename_re.match(image_name).groups()
epoger@google.coma783f2b2013-07-08 17:51:58 +0000242 if self._tests:
243 if test not in self._tests:
244 skipped_images.append(image_name)
245 continue
246 if self._configs:
247 if config not in self._configs:
248 skipped_images.append(image_name)
249 continue
epoger@google.com4b383012013-07-16 21:10:54 +0000250 if not expected_results.get(image_name):
251 expected_results[image_name] = {}
252 expected_results[image_name] \
epoger@google.com61822a22013-07-16 18:56:32 +0000253 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] = \
254 [image_results]
epoger@google.coma783f2b2013-07-08 17:51:58 +0000255
256 # Write out updated expectations.
257 gm_json.WriteToFile(expectations_dict, expectations_json_filepath)
258
epoger@google.comec3397b2013-05-29 17:09:43 +0000259
epoger@google.com9166bf52013-05-30 15:46:19 +0000260# main...
epoger@google.comec3397b2013-05-29 17:09:43 +0000261
epoger@google.com9166bf52013-05-30 15:46:19 +0000262parser = argparse.ArgumentParser()
epoger@google.coma783f2b2013-07-08 17:51:58 +0000263parser.add_argument('--actuals-base-url',
264 help='base URL from which to read files containing JSON ' +
265 'summaries of actual GM results; defaults to %(default)s',
266 default='http://skia-autogen.googlecode.com/svn/gm-actual')
267parser.add_argument('--actuals-filename',
268 help='filename (within platform-specific subdirectories ' +
269 'of ACTUALS_BASE_URL) to read a summary of results from; ' +
270 'defaults to %(default)s',
271 default='actual-results.json')
272# TODO(epoger): Add test that exercises --add-new argument.
epoger@google.comdad53102013-06-12 14:25:30 +0000273parser.add_argument('--add-new', action='store_true',
274 help='in addition to the standard behavior of ' +
275 'updating expectations for failing tests, add ' +
276 'expectations for tests which don\'t have expectations ' +
277 'yet.')
epoger@google.coma783f2b2013-07-08 17:51:58 +0000278# TODO(epoger): Add test that exercises --configs argument.
epoger@google.com9166bf52013-05-30 15:46:19 +0000279parser.add_argument('--configs', metavar='CONFIG', nargs='+',
280 help='which configurations to rebaseline, e.g. ' +
epoger@google.com9de25e32013-07-10 15:27:18 +0000281 '"--configs 565 8888", as a filter over the full set of ' +
282 'results in ACTUALS_FILENAME; if unspecified, rebaseline ' +
283 '*all* configs that are available.')
epoger@google.coma783f2b2013-07-08 17:51:58 +0000284# TODO(epoger): The --dry-run argument will no longer be needed once we
285# are only rebaselining JSON files.
epoger@google.com82f31782013-06-11 15:45:46 +0000286parser.add_argument('--dry-run', action='store_true',
epoger@google.com9166bf52013-05-30 15:46:19 +0000287 help='instead of actually downloading files or adding ' +
288 'files to checkout, display a list of operations that ' +
289 'we would normally perform')
epoger@google.coma783f2b2013-07-08 17:51:58 +0000290parser.add_argument('--expectations-filename',
291 help='filename (under EXPECTATIONS_ROOT) to read ' +
292 'current expectations from, and to write new ' +
293 'expectations into; defaults to %(default)s',
294 default='expected-results.json')
epoger@google.com99a8ec92013-06-19 18:56:59 +0000295parser.add_argument('--expectations-root',
296 help='root of expectations directory to update-- should ' +
297 'contain one or more base-* subdirectories. Defaults to ' +
298 '%(default)s',
299 default='.')
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000300parser.add_argument('--keep-going-on-failure', action='store_true',
301 help='instead of halting at the first error encountered, ' +
302 'keep going and rebaseline as many tests as possible, ' +
303 'and then report the full set of errors at the end')
epoger@google.com9166bf52013-05-30 15:46:19 +0000304parser.add_argument('--subdirs', metavar='SUBDIR', nargs='+',
305 help='which platform subdirectories to rebaseline; ' +
306 'if unspecified, rebaseline all subdirs, same as ' +
307 '"--subdirs %s"' % ' '.join(sorted(SUBDIR_MAPPING.keys())))
epoger@google.coma783f2b2013-07-08 17:51:58 +0000308# TODO(epoger): Add test that exercises --tests argument.
epoger@google.com99ba65a2013-06-05 15:43:37 +0000309parser.add_argument('--tests', metavar='TEST', nargs='+',
epoger@google.com9166bf52013-05-30 15:46:19 +0000310 help='which tests to rebaseline, e.g. ' +
epoger@google.com9de25e32013-07-10 15:27:18 +0000311 '"--tests aaclip bigmatrix", as a filter over the full ' +
312 'set of results in ACTUALS_FILENAME; if unspecified, ' +
313 'rebaseline *all* tests that are available.')
epoger@google.com9166bf52013-05-30 15:46:19 +0000314args = parser.parse_args()
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000315exception_handler = ExceptionHandler(
316 keep_going_on_failure=args.keep_going_on_failure)
epoger@google.com99a8ec92013-06-19 18:56:59 +0000317if args.subdirs:
318 subdirs = args.subdirs
319 missing_json_is_fatal = True
320else:
321 subdirs = sorted(SUBDIR_MAPPING.keys())
322 missing_json_is_fatal = False
323for subdir in subdirs:
324 if not subdir in SUBDIR_MAPPING.keys():
325 raise Exception(('unrecognized platform subdir "%s"; ' +
326 'should be one of %s') % (
327 subdir, SUBDIR_MAPPING.keys()))
328 builder = SUBDIR_MAPPING[subdir]
329
330 # We instantiate different Rebaseliner objects depending
331 # on whether we are rebaselining an expected-results.json file, or
332 # individual image files. Different gm-expected subdirectories may move
333 # from individual image files to JSON-format expectations at different
334 # times, so we need to make this determination per subdirectory.
335 #
336 # See https://goto.google.com/ChecksumTransitionDetail
337 expectations_json_file = os.path.join(args.expectations_root, subdir,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000338 args.expectations_filename)
epoger@google.com99a8ec92013-06-19 18:56:59 +0000339 if os.path.isfile(expectations_json_file):
epoger@google.com99a8ec92013-06-19 18:56:59 +0000340 rebaseliner = JsonRebaseliner(
341 expectations_root=args.expectations_root,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000342 expectations_filename=args.expectations_filename,
epoger@google.com99a8ec92013-06-19 18:56:59 +0000343 tests=args.tests, configs=args.configs,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000344 actuals_base_url=args.actuals_base_url,
345 actuals_filename=args.actuals_filename,
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000346 exception_handler=exception_handler,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000347 add_new=args.add_new)
epoger@google.com99a8ec92013-06-19 18:56:59 +0000348 else:
epoger@google.com89fa4b92013-07-10 16:54:10 +0000349 # TODO(epoger): When we get rid of the ImageRebaseliner implementation,
350 # we should raise an Exception in this case (no JSON expectations file
351 # found to update), to prevent a recurrence of
352 # https://code.google.com/p/skia/issues/detail?id=1403 ('rebaseline.py
353 # script fails with misleading output when run outside of gm-expected
354 # dir')
epoger@google.com99a8ec92013-06-19 18:56:59 +0000355 rebaseliner = rebaseline_imagefiles.ImageRebaseliner(
356 expectations_root=args.expectations_root,
357 tests=args.tests, configs=args.configs,
358 dry_run=args.dry_run,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000359 json_base_url=args.actuals_base_url,
360 json_filename=args.actuals_filename,
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000361 exception_handler=exception_handler,
epoger@google.com99a8ec92013-06-19 18:56:59 +0000362 add_new=args.add_new,
363 missing_json_is_fatal=missing_json_is_fatal)
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000364
epoger@google.com3e7399f2013-07-10 17:23:47 +0000365 try:
366 rebaseliner.RebaselineSubdir(subdir=subdir, builder=builder)
367 except BaseException as e:
epoger@google.comffcbdbf2013-07-16 17:35:39 +0000368 exception_handler.RaiseExceptionOrContinue(e)
369
370exception_handler.ReportAllFailures()