blob: 626a1b9d71e35cd3fd7539778163ad39a66e34f8 [file] [log] [blame]
epoger@google.com6e1e7852013-07-10 18:09:55 +00001#!/usr/bin/python
epoger@google.com2e0a0612012-05-25 19:48:05 +00002'''
epoger@google.com2e0a0612012-05-25 19:48:05 +00003Copyright 2012 Google Inc.
4
5Use of this source code is governed by a BSD-style license that can be
6found in the LICENSE file.
7'''
8
epoger@google.com61822a22013-07-16 18:56:32 +00009'''
10Generates a visual diff of all pending changes in the local SVN checkout.
11
12Launch with --help to see more information.
13
14TODO(epoger): Fix indentation in this file (2-space indents, not 4-space).
15'''
16
epoger@google.com2e0a0612012-05-25 19:48:05 +000017# common Python modules
18import optparse
19import os
20import re
21import shutil
epoger@google.com61822a22013-07-16 18:56:32 +000022import sys
epoger@google.com2e0a0612012-05-25 19:48:05 +000023import tempfile
epoger@google.com61822a22013-07-16 18:56:32 +000024import urllib2
epoger@google.com2e0a0612012-05-25 19:48:05 +000025
epoger@google.com61822a22013-07-16 18:56:32 +000026# Imports from within Skia
27#
28# 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.
37GM_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
42import jsondiff
epoger@google.com2e0a0612012-05-25 19:48:05 +000043import svn
44
45USAGE_STRING = 'Usage: %s [options]'
46HELP_STRING = '''
47
epoger@google.com6dbf6cd2012-05-29 21:28:12 +000048Generates a visual diff of all pending changes in the local SVN checkout.
49
50This includes a list of all files that have been added, deleted, or modified
51(as far as SVN knows about). For any image modifications, pixel diffs will
52be generated.
epoger@google.com2e0a0612012-05-25 19:48:05 +000053
54'''
55
epoger@google.com61822a22013-07-16 18:56:32 +000056IMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
57
epoger@google.com2e0a0612012-05-25 19:48:05 +000058TRUNK_PATH = os.path.join(os.path.dirname(__file__), os.pardir)
59
60OPTION_DEST_DIR = '--dest-dir'
epoger@google.com2e0a0612012-05-25 19:48:05 +000061OPTION_PATH_TO_SKDIFF = '--path-to-skdiff'
epoger@google.com61822a22013-07-16 18:56:32 +000062OPTION_SOURCE_DIR = '--source-dir'
epoger@google.com2e0a0612012-05-25 19:48:05 +000063
64def RunCommand(command):
65 """Run a command, raising an exception if it fails.
66
67 @param command the command as a single string
68 """
69 print 'running command [%s]...' % command
70 retval = os.system(command)
71 if retval is not 0:
72 raise Exception('command [%s] failed' % command)
73
74def FindPathToSkDiff(user_set_path=None):
75 """Return path to an existing skdiff binary, or raise an exception if we
76 cannot find one.
77
78 @param user_set_path if None, the user did not specify a path, so look in
79 some likely places; otherwise, only check at this path
80 """
81 if user_set_path is not None:
82 if os.path.isfile(user_set_path):
83 return user_set_path
84 raise Exception('unable to find skdiff at user-set path %s' %
85 user_set_path)
86 trunk_path = os.path.join(os.path.dirname(__file__), os.pardir)
87 possible_paths = [os.path.join(trunk_path, 'out', 'Release', 'skdiff'),
88 os.path.join(trunk_path, 'out', 'Debug', 'skdiff')]
89 for try_path in possible_paths:
90 if os.path.isfile(try_path):
91 return try_path
92 raise Exception('cannot find skdiff in paths %s; maybe you need to '
93 'specify the %s option or build skdiff?' % (
94 possible_paths, OPTION_PATH_TO_SKDIFF))
95
epoger@google.com61822a22013-07-16 18:56:32 +000096def _DownloadUrlToFile(source_url, dest_path):
97 """Download source_url, and save its contents to dest_path.
98 Raises an exception if there were any problems."""
99 reader = urllib2.urlopen(source_url)
100 writer = open(dest_path, 'wb')
101 writer.write(reader.read())
102 writer.close()
103
104def _CreateGSUrl(imagename, hash_type, hash_digest):
105 """Return the HTTP URL we can use to download this particular version of
106 the actually-generated GM image with this imagename.
107
108 imagename: name of the test image, e.g. 'perlinnoise_msaa4.png'
109 hash_type: string indicating the hash type used to generate hash_digest,
110 e.g. gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5
111 hash_digest: the hash digest of the image to retrieve
112 """
113 return gm_json.CreateGmActualUrl(
114 test_name=IMAGE_FILENAME_RE.match(imagename).group(1),
115 hash_type=hash_type,
116 hash_digest=hash_digest)
117
118def _CallJsonDiff(old_json_path, new_json_path,
119 old_flattened_dir, new_flattened_dir,
120 filename_prefix):
121 """Using jsondiff.py, write the images that differ between two GM
122 expectations summary files (old and new) into old_flattened_dir and
123 new_flattened_dir.
124
125 filename_prefix: prefix to prepend to filenames of all images we write
126 into the flattened directories
127 """
128 json_differ = jsondiff.GMDiffer()
129 diff_dict = json_differ.GenerateDiffDict(oldfile=old_json_path,
130 newfile=new_json_path)
131 for (imagename, results) in diff_dict.iteritems():
132 old_checksum = results['old']
133 new_checksum = results['new']
134 # TODO(epoger): Currently, this assumes that all images have been
135 # checksummed using gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5
136 old_image_url = _CreateGSUrl(
137 imagename=imagename,
138 hash_type=gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5,
139 hash_digest=old_checksum)
140 new_image_url = _CreateGSUrl(
141 imagename=imagename,
142 hash_type=gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5,
143 hash_digest=new_checksum)
144 _DownloadUrlToFile(
145 source_url=old_image_url,
146 dest_path=os.path.join(old_flattened_dir,
147 filename_prefix + imagename))
148 _DownloadUrlToFile(
149 source_url=new_image_url,
150 dest_path=os.path.join(new_flattened_dir,
151 filename_prefix + imagename))
152
153def SvnDiff(path_to_skdiff, dest_dir, source_dir):
154 """Generates a visual diff of all pending changes in source_dir.
epoger@google.com2e0a0612012-05-25 19:48:05 +0000155
156 @param path_to_skdiff
157 @param dest_dir existing directory within which to write results
epoger@google.com61822a22013-07-16 18:56:32 +0000158 @param source_dir
epoger@google.com2e0a0612012-05-25 19:48:05 +0000159 """
160 # Validate parameters, filling in default values if necessary and possible.
epoger@google.com61822a22013-07-16 18:56:32 +0000161 path_to_skdiff = os.path.abspath(FindPathToSkDiff(path_to_skdiff))
epoger@google.com2e0a0612012-05-25 19:48:05 +0000162 if not dest_dir:
163 dest_dir = tempfile.mkdtemp()
epoger@google.com61822a22013-07-16 18:56:32 +0000164 dest_dir = os.path.abspath(dest_dir)
165
166 os.chdir(source_dir)
epoger@google.com2e0a0612012-05-25 19:48:05 +0000167
168 # Prepare temporary directories.
169 modified_flattened_dir = os.path.join(dest_dir, 'modified_flattened')
170 original_flattened_dir = os.path.join(dest_dir, 'original_flattened')
171 diff_dir = os.path.join(dest_dir, 'diffs')
172 for dir in [modified_flattened_dir, original_flattened_dir, diff_dir] :
173 shutil.rmtree(dir, ignore_errors=True)
174 os.mkdir(dir)
175
epoger@google.com6dbf6cd2012-05-29 21:28:12 +0000176 # Get a list of all locally modified (including added/deleted) files,
177 # descending subdirectories.
epoger@google.com2e0a0612012-05-25 19:48:05 +0000178 svn_repo = svn.Svn('.')
epoger@google.com6dbf6cd2012-05-29 21:28:12 +0000179 modified_file_paths = svn_repo.GetFilesWithStatus(
180 svn.STATUS_ADDED | svn.STATUS_DELETED | svn.STATUS_MODIFIED)
epoger@google.com2e0a0612012-05-25 19:48:05 +0000181
182 # For each modified file:
183 # 1. copy its current contents into modified_flattened_dir
184 # 2. copy its original contents into original_flattened_dir
185 for modified_file_path in modified_file_paths:
epoger@google.com61822a22013-07-16 18:56:32 +0000186 if modified_file_path.endswith('.json'):
187 # Special handling for JSON files, in the hopes that they
188 # contain GM result summaries.
189 (_unused, original_file_path) = tempfile.mkstemp()
190 svn_repo.ExportBaseVersionOfFile(modified_file_path,
191 original_file_path)
192 platform_prefix = re.sub(os.sep, '__',
193 os.path.dirname(modified_file_path)) + '__'
194 _CallJsonDiff(old_json_path=original_file_path,
195 new_json_path=modified_file_path,
196 old_flattened_dir=original_flattened_dir,
197 new_flattened_dir=modified_flattened_dir,
198 filename_prefix=platform_prefix)
199 os.remove(original_file_path)
200 else:
201 dest_filename = re.sub(os.sep, '__', modified_file_path)
202 # If the file had STATUS_DELETED, it won't exist anymore...
203 if os.path.isfile(modified_file_path):
204 shutil.copyfile(modified_file_path,
205 os.path.join(modified_flattened_dir, dest_filename))
206 svn_repo.ExportBaseVersionOfFile(
207 modified_file_path,
208 os.path.join(original_flattened_dir, dest_filename))
epoger@google.com2e0a0612012-05-25 19:48:05 +0000209
210 # Run skdiff: compare original_flattened_dir against modified_flattened_dir
211 RunCommand('%s %s %s %s' % (path_to_skdiff, original_flattened_dir,
212 modified_flattened_dir, diff_dir))
213 print '\nskdiff results are ready in file://%s/index.html' % diff_dir
214
215def RaiseUsageException():
216 raise Exception('%s\nRun with --help for more detail.' % (
217 USAGE_STRING % __file__))
218
219def Main(options, args):
220 """Allow other scripts to call this script with fake command-line args.
221 """
222 num_args = len(args)
223 if num_args != 0:
224 RaiseUsageException()
epoger@google.com61822a22013-07-16 18:56:32 +0000225 SvnDiff(path_to_skdiff=options.path_to_skdiff, dest_dir=options.dest_dir,
226 source_dir=options.source_dir)
epoger@google.com2e0a0612012-05-25 19:48:05 +0000227
228if __name__ == '__main__':
229 parser = optparse.OptionParser(USAGE_STRING % '%prog' + HELP_STRING)
230 parser.add_option(OPTION_DEST_DIR,
231 action='store', type='string', default=None,
232 help='existing directory within which to write results; '
233 'if not set, will create a temporary directory which '
234 'will remain in place after this script completes')
235 parser.add_option(OPTION_PATH_TO_SKDIFF,
236 action='store', type='string', default=None,
237 help='path to already-built skdiff tool; if not set, '
238 'will search for it in typical directories near this '
239 'script')
epoger@google.com61822a22013-07-16 18:56:32 +0000240 parser.add_option(OPTION_SOURCE_DIR,
241 action='store', type='string', default='.',
242 help='root directory within which to compare all ' +
243 'files; defaults to "%default"')
epoger@google.com2e0a0612012-05-25 19:48:05 +0000244 (options, args) = parser.parse_args()
245 Main(options, args)