blob: 40d6c61444e8a7e4df0f00388def26ebe39842e7 [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.
senorblanco@chromium.org782f3b42012-10-29 18:06:26 +000014'''
15
epoger@google.com99ba65a2013-06-05 15:43:37 +000016# System-level imports
epoger@google.com9166bf52013-05-30 15:46:19 +000017import argparse
epoger@google.comec3397b2013-05-29 17:09:43 +000018import os
epoger@google.come78d2072013-06-12 17:44:14 +000019import re
epoger@google.comec3397b2013-05-29 17:09:43 +000020import sys
epoger@google.com99ba65a2013-06-05 15:43:37 +000021import urllib2
22
epoger@google.com99a8ec92013-06-19 18:56:59 +000023# Imports from local directory
24import rebaseline_imagefiles
25
epoger@google.com99ba65a2013-06-05 15:43:37 +000026# Imports from within Skia
27#
epoger@google.comdad53102013-06-12 14:25:30 +000028# We need to add the 'gm' directory, so that we can import gm_json.py within
29# that directory. That script allows us to parse the actual-results.json file
30# written out by the GM tool.
31# Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end*
32# so any dirs that are already in the PYTHONPATH will be preferred.
33#
34# This assumes that the 'gm' directory has been checked out as a sibling of
35# the 'tools' directory containing this script, which will be the case if
36# 'trunk' was checked out as a single unit.
epoger@google.com99ba65a2013-06-05 15:43:37 +000037GM_DIRECTORY = os.path.realpath(
38 os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm'))
39if GM_DIRECTORY not in sys.path:
40 sys.path.append(GM_DIRECTORY)
41import gm_json
42
epoger@google.comec3397b2013-05-29 17:09:43 +000043# Mapping of gm-expectations subdir (under
44# https://skia.googlecode.com/svn/gm-expected/ )
45# to builder name (see list at http://108.170.217.252:10117/builders )
epoger@google.com9166bf52013-05-30 15:46:19 +000046SUBDIR_MAPPING = {
epoger@google.comec3397b2013-05-29 17:09:43 +000047 'base-shuttle-win7-intel-float':
48 'Test-Win7-ShuttleA-HD2000-x86-Release',
49 'base-shuttle-win7-intel-angle':
50 'Test-Win7-ShuttleA-HD2000-x86-Release-ANGLE',
51 'base-shuttle-win7-intel-directwrite':
52 'Test-Win7-ShuttleA-HD2000-x86-Release-DirectWrite',
53 'base-shuttle_ubuntu12_ati5770':
54 'Test-Ubuntu12-ShuttleA-ATI5770-x86_64-Release',
55 'base-macmini':
56 'Test-Mac10.6-MacMini4.1-GeForce320M-x86-Release',
57 'base-macmini-lion-float':
58 'Test-Mac10.7-MacMini4.1-GeForce320M-x86-Release',
59 'base-android-galaxy-nexus':
60 'Test-Android-GalaxyNexus-SGX540-Arm7-Debug',
61 'base-android-nexus-7':
62 'Test-Android-Nexus7-Tegra3-Arm7-Release',
63 'base-android-nexus-s':
64 'Test-Android-NexusS-SGX540-Arm7-Release',
65 'base-android-xoom':
66 'Test-Android-Xoom-Tegra2-Arm7-Release',
67 'base-android-nexus-10':
68 'Test-Android-Nexus10-MaliT604-Arm7-Release',
robertphillips@google.com63e96272013-07-02 12:54:37 +000069 'base-android-nexus-4':
70 'Test-Android-Nexus4-Adreno320-Arm7-Release',
epoger@google.comec3397b2013-05-29 17:09:43 +000071}
72
epoger@google.com9166bf52013-05-30 15:46:19 +000073
epoger@google.comdb29a312013-06-04 14:58:47 +000074class CommandFailedException(Exception):
75 pass
76
epoger@google.com99a8ec92013-06-19 18:56:59 +000077# Object that rebaselines a JSON expectations file (not individual image files).
epoger@google.com99a8ec92013-06-19 18:56:59 +000078class JsonRebaseliner(object):
epoger@google.com9166bf52013-05-30 15:46:19 +000079
80 # params:
epoger@google.coma783f2b2013-07-08 17:51:58 +000081 # expectations_root: root directory of all expectations JSON files
82 # expectations_filename: filename (under expectations_root) of JSON
83 # expectations file; typically
84 # "expected-results.json"
85 # actuals_base_url: base URL from which to read actual-result JSON files
86 # actuals_filename: filename (under actuals_base_url) from which to read a
87 # summary of results; typically "actual-results.json"
epoger@google.com99ba65a2013-06-05 15:43:37 +000088 # tests: list of tests to rebaseline, or None if we should rebaseline
89 # whatever files the JSON results summary file tells us to
90 # configs: which configs to run for each test; this should only be
91 # specified if the list of tests was also specified (otherwise,
92 # the JSON file will give us test names and configs)
epoger@google.comdad53102013-06-12 14:25:30 +000093 # add_new: if True, add expectations for tests which don't have any yet
epoger@google.coma783f2b2013-07-08 17:51:58 +000094 def __init__(self, expectations_root, expectations_filename,
95 actuals_base_url, actuals_filename,
96 tests=None, configs=None, add_new=False):
epoger@google.com99ba65a2013-06-05 15:43:37 +000097 if configs and not tests:
98 raise ValueError('configs should only be specified if tests ' +
99 'were specified also')
epoger@google.com99a8ec92013-06-19 18:56:59 +0000100 self._expectations_root = expectations_root
epoger@google.coma783f2b2013-07-08 17:51:58 +0000101 self._expectations_filename = expectations_filename
epoger@google.com9166bf52013-05-30 15:46:19 +0000102 self._tests = tests
103 self._configs = configs
epoger@google.coma783f2b2013-07-08 17:51:58 +0000104 self._actuals_base_url = actuals_base_url
105 self._actuals_filename = actuals_filename
epoger@google.comdad53102013-06-12 14:25:30 +0000106 self._add_new = add_new
epoger@google.come78d2072013-06-12 17:44:14 +0000107 self._testname_pattern = re.compile('(\S+)_(\S+).png')
epoger@google.com9166bf52013-05-30 15:46:19 +0000108
epoger@google.coma783f2b2013-07-08 17:51:58 +0000109 # Returns the full contents of filepath, as a single string.
110 # If filepath looks like a URL, try to read it that way instead of as
111 # a path on local storage.
112 def _GetFileContents(self, filepath):
113 if filepath.startswith('http:') or filepath.startswith('https:'):
114 return urllib2.urlopen(filepath).read()
epoger@google.com99ba65a2013-06-05 15:43:37 +0000115 else:
epoger@google.coma783f2b2013-07-08 17:51:58 +0000116 return open(filepath, 'r').read()
epoger@google.com99ba65a2013-06-05 15:43:37 +0000117
epoger@google.come78d2072013-06-12 17:44:14 +0000118 # Returns a dictionary of actual results from actual-results.json file.
119 #
120 # The dictionary returned has this format:
121 # {
122 # u'imageblur_565.png': [u'bitmap-64bitMD5', 3359963596899141322],
123 # u'imageblur_8888.png': [u'bitmap-64bitMD5', 4217923806027861152],
124 # u'shadertext3_8888.png': [u'bitmap-64bitMD5', 3713708307125704716]
125 # }
126 #
epoger@google.coma783f2b2013-07-08 17:51:58 +0000127 # If the JSON actual result summary file cannot be loaded, raise an
128 # exception.
epoger@google.come78d2072013-06-12 17:44:14 +0000129 #
130 # params:
131 # json_url: URL pointing to a JSON actual result summary file
132 # sections: a list of section names to include in the results, e.g.
133 # [gm_json.JSONKEY_ACTUALRESULTS_FAILED,
134 # gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON] ;
135 # if None, then include ALL sections.
136 def _GetActualResults(self, json_url, sections=None):
epoger@google.coma783f2b2013-07-08 17:51:58 +0000137 json_contents = self._GetFileContents(json_url)
epoger@google.come78d2072013-06-12 17:44:14 +0000138 json_dict = gm_json.LoadFromString(json_contents)
139 results_to_return = {}
140 actual_results = json_dict[gm_json.JSONKEY_ACTUALRESULTS]
141 if not sections:
142 sections = actual_results.keys()
143 for section in sections:
144 section_results = actual_results[section]
145 if section_results:
146 results_to_return.update(section_results)
147 return results_to_return
148
epoger@google.com99a8ec92013-06-19 18:56:59 +0000149 # Rebaseline all tests/types we specified in the constructor,
150 # within this gm-expectations subdir.
151 #
152 # params:
153 # subdir : e.g. 'base-shuttle-win7-intel-float'
154 # builder : e.g. 'Test-Win7-ShuttleA-HD2000-x86-Release'
155 def RebaselineSubdir(self, subdir, builder):
epoger@google.coma783f2b2013-07-08 17:51:58 +0000156 # Read in the actual result summary, and extract all the tests whose
157 # results we need to update.
158 actuals_url = '/'.join([self._actuals_base_url,
159 subdir, builder, subdir,
160 self._actuals_filename])
161 sections = [gm_json.JSONKEY_ACTUALRESULTS_FAILED]
162 if self._add_new:
163 sections.append(gm_json.JSONKEY_ACTUALRESULTS_NOCOMPARISON)
164 results_to_update = self._GetActualResults(json_url=actuals_url,
165 sections=sections)
epoger@google.come78d2072013-06-12 17:44:14 +0000166
epoger@google.coma783f2b2013-07-08 17:51:58 +0000167 # Read in current expectations.
168 expectations_json_filepath = os.path.join(
169 self._expectations_root, subdir, self._expectations_filename)
170 expectations_dict = gm_json.LoadFromFile(expectations_json_filepath)
171
172 # Update the expectations in memory, skipping any tests/configs that
173 # the caller asked to exclude.
174 skipped_images = []
175 if results_to_update:
176 for (image_name, image_results) in results_to_update.iteritems():
177 (test, config) = self._testname_pattern.match(image_name).groups()
178 if self._tests:
179 if test not in self._tests:
180 skipped_images.append(image_name)
181 continue
182 if self._configs:
183 if config not in self._configs:
184 skipped_images.append(image_name)
185 continue
186 expectations_dict[gm_json.JSONKEY_EXPECTEDRESULTS] \
187 [image_name] \
188 [gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS] = \
189 [image_results]
190
191 # Write out updated expectations.
192 gm_json.WriteToFile(expectations_dict, expectations_json_filepath)
193
194 if skipped_images:
195 print ('Skipped these tests due to test/config filters: %s' %
196 skipped_images)
197
epoger@google.comec3397b2013-05-29 17:09:43 +0000198
epoger@google.com9166bf52013-05-30 15:46:19 +0000199# main...
epoger@google.comec3397b2013-05-29 17:09:43 +0000200
epoger@google.com9166bf52013-05-30 15:46:19 +0000201parser = argparse.ArgumentParser()
epoger@google.coma783f2b2013-07-08 17:51:58 +0000202parser.add_argument('--actuals-base-url',
203 help='base URL from which to read files containing JSON ' +
204 'summaries of actual GM results; defaults to %(default)s',
205 default='http://skia-autogen.googlecode.com/svn/gm-actual')
206parser.add_argument('--actuals-filename',
207 help='filename (within platform-specific subdirectories ' +
208 'of ACTUALS_BASE_URL) to read a summary of results from; ' +
209 'defaults to %(default)s',
210 default='actual-results.json')
211# TODO(epoger): Add test that exercises --add-new argument.
epoger@google.comdad53102013-06-12 14:25:30 +0000212parser.add_argument('--add-new', action='store_true',
213 help='in addition to the standard behavior of ' +
214 'updating expectations for failing tests, add ' +
215 'expectations for tests which don\'t have expectations ' +
216 'yet.')
epoger@google.coma783f2b2013-07-08 17:51:58 +0000217# TODO(epoger): Add test that exercises --configs argument.
218# TODO(epoger): Once we are only rebaselining JSON files, update the helpstring
219# to indicate that this is a *filter* over the config names that
220# actual-results.json tells us need to be rebaselined.
221# You don't need to specify tests also, etc.
epoger@google.com9166bf52013-05-30 15:46:19 +0000222parser.add_argument('--configs', metavar='CONFIG', nargs='+',
223 help='which configurations to rebaseline, e.g. ' +
224 '"--configs 565 8888"; if unspecified, run a default ' +
epoger@google.com99ba65a2013-06-05 15:43:37 +0000225 'set of configs. This should ONLY be specified if ' +
226 '--tests has also been specified.')
epoger@google.coma783f2b2013-07-08 17:51:58 +0000227# TODO(epoger): The --dry-run argument will no longer be needed once we
228# are only rebaselining JSON files.
epoger@google.com82f31782013-06-11 15:45:46 +0000229parser.add_argument('--dry-run', action='store_true',
epoger@google.com9166bf52013-05-30 15:46:19 +0000230 help='instead of actually downloading files or adding ' +
231 'files to checkout, display a list of operations that ' +
232 'we would normally perform')
epoger@google.coma783f2b2013-07-08 17:51:58 +0000233parser.add_argument('--expectations-filename',
234 help='filename (under EXPECTATIONS_ROOT) to read ' +
235 'current expectations from, and to write new ' +
236 'expectations into; defaults to %(default)s',
237 default='expected-results.json')
epoger@google.com99a8ec92013-06-19 18:56:59 +0000238parser.add_argument('--expectations-root',
239 help='root of expectations directory to update-- should ' +
240 'contain one or more base-* subdirectories. Defaults to ' +
241 '%(default)s',
242 default='.')
epoger@google.com9166bf52013-05-30 15:46:19 +0000243parser.add_argument('--subdirs', metavar='SUBDIR', nargs='+',
244 help='which platform subdirectories to rebaseline; ' +
245 'if unspecified, rebaseline all subdirs, same as ' +
246 '"--subdirs %s"' % ' '.join(sorted(SUBDIR_MAPPING.keys())))
epoger@google.coma783f2b2013-07-08 17:51:58 +0000247# TODO(epoger): Add test that exercises --tests argument.
248# TODO(epoger): Once we are only rebaselining JSON files, update the helpstring
249# to indicate that this is a *filter* over the test names that
250# actual-results.json tells us need to be rebaselined.
epoger@google.com99ba65a2013-06-05 15:43:37 +0000251parser.add_argument('--tests', metavar='TEST', nargs='+',
epoger@google.com9166bf52013-05-30 15:46:19 +0000252 help='which tests to rebaseline, e.g. ' +
epoger@google.com99ba65a2013-06-05 15:43:37 +0000253 '"--tests aaclip bigmatrix"; if unspecified, then all ' +
254 'failing tests (according to the actual-results.json ' +
255 'file) will be rebaselined.')
epoger@google.com9166bf52013-05-30 15:46:19 +0000256args = parser.parse_args()
epoger@google.com99a8ec92013-06-19 18:56:59 +0000257if args.subdirs:
258 subdirs = args.subdirs
259 missing_json_is_fatal = True
260else:
261 subdirs = sorted(SUBDIR_MAPPING.keys())
262 missing_json_is_fatal = False
263for subdir in subdirs:
264 if not subdir in SUBDIR_MAPPING.keys():
265 raise Exception(('unrecognized platform subdir "%s"; ' +
266 'should be one of %s') % (
267 subdir, SUBDIR_MAPPING.keys()))
268 builder = SUBDIR_MAPPING[subdir]
269
270 # We instantiate different Rebaseliner objects depending
271 # on whether we are rebaselining an expected-results.json file, or
272 # individual image files. Different gm-expected subdirectories may move
273 # from individual image files to JSON-format expectations at different
274 # times, so we need to make this determination per subdirectory.
275 #
276 # See https://goto.google.com/ChecksumTransitionDetail
277 expectations_json_file = os.path.join(args.expectations_root, subdir,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000278 args.expectations_filename)
epoger@google.com99a8ec92013-06-19 18:56:59 +0000279 if os.path.isfile(expectations_json_file):
epoger@google.com99a8ec92013-06-19 18:56:59 +0000280 rebaseliner = JsonRebaseliner(
281 expectations_root=args.expectations_root,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000282 expectations_filename=args.expectations_filename,
epoger@google.com99a8ec92013-06-19 18:56:59 +0000283 tests=args.tests, configs=args.configs,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000284 actuals_base_url=args.actuals_base_url,
285 actuals_filename=args.actuals_filename,
286 add_new=args.add_new)
epoger@google.com99a8ec92013-06-19 18:56:59 +0000287 else:
288 rebaseliner = rebaseline_imagefiles.ImageRebaseliner(
289 expectations_root=args.expectations_root,
290 tests=args.tests, configs=args.configs,
291 dry_run=args.dry_run,
epoger@google.coma783f2b2013-07-08 17:51:58 +0000292 json_base_url=args.actuals_base_url,
293 json_filename=args.actuals_filename,
epoger@google.com99a8ec92013-06-19 18:56:59 +0000294 add_new=args.add_new,
295 missing_json_is_fatal=missing_json_is_fatal)
296 rebaseliner.RebaselineSubdir(subdir=subdir, builder=builder)