OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
epoger
2013/07/12 18:55:00
Tested as follows:
EXPECTATIONS_DIR=experimental/
| |
2 ''' | 2 ''' |
3 Generates a visual diff of all pending changes in the local SVN checkout. | 3 Generates a visual diff of all pending changes in the local SVN checkout. |
4 | 4 |
5 Launch with --help to see more information. | 5 Launch with --help to see more information. |
6 | 6 |
7 | 7 |
8 Copyright 2012 Google Inc. | 8 Copyright 2012 Google Inc. |
9 | 9 |
10 Use of this source code is governed by a BSD-style license that can be | 10 Use of this source code is governed by a BSD-style license that can be |
11 found in the LICENSE file. | 11 found in the LICENSE file. |
12 ''' | 12 ''' |
13 | 13 |
14 # common Python modules | 14 # common Python modules |
15 import optparse | 15 import optparse |
16 import os | 16 import os |
17 import re | 17 import re |
18 import shutil | 18 import shutil |
19 import sys | |
19 import tempfile | 20 import tempfile |
21 import urllib2 | |
20 | 22 |
21 # modules declared within this same directory | 23 # Imports from within Skia |
24 # | |
25 # 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 # written out by the GM tool. | |
28 # Make sure that the 'gm' dir is in the PYTHONPATH, but add it at the *end* | |
29 # so any dirs that are already in the PYTHONPATH will be preferred. | |
30 # | |
31 # This assumes that the 'gm' directory has been checked out as a sibling of | |
32 # the 'tools' directory containing this script, which will be the case if | |
33 # 'trunk' was checked out as a single unit. | |
34 GM_DIRECTORY = os.path.realpath( | |
35 os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gm')) | |
36 if GM_DIRECTORY not in sys.path: | |
37 sys.path.append(GM_DIRECTORY) | |
38 import gm_json | |
39 import jsondiff | |
22 import svn | 40 import svn |
23 | 41 |
24 USAGE_STRING = 'Usage: %s [options]' | 42 USAGE_STRING = 'Usage: %s [options]' |
25 HELP_STRING = ''' | 43 HELP_STRING = ''' |
26 | 44 |
27 Generates a visual diff of all pending changes in the local SVN checkout. | 45 Generates a visual diff of all pending changes in the local SVN checkout. |
28 | 46 |
29 This includes a list of all files that have been added, deleted, or modified | 47 This includes a list of all files that have been added, deleted, or modified |
30 (as far as SVN knows about). For any image modifications, pixel diffs will | 48 (as far as SVN knows about). For any image modifications, pixel diffs will |
31 be generated. | 49 be generated. |
32 | 50 |
33 ''' | 51 ''' |
34 | 52 |
53 GOOGLESTORAGE_GM_ACTUALS_ROOT = ( | |
54 'http://chromium-skia-gm.commondatastorage.googleapis.com/gm') | |
55 TESTNAME_PATTERN = re.compile('(\S+)_(\S+).png') | |
borenet
2013/07/12 19:48:04
This is now listed in three places. Not sure how
epoger
2013/07/16 17:29:55
Not so hot, thanks for calling it to my attention.
| |
56 | |
35 TRUNK_PATH = os.path.join(os.path.dirname(__file__), os.pardir) | 57 TRUNK_PATH = os.path.join(os.path.dirname(__file__), os.pardir) |
36 | 58 |
37 OPTION_DEST_DIR = '--dest-dir' | 59 OPTION_DEST_DIR = '--dest-dir' |
38 # default DEST_DIR is determined at runtime | |
39 OPTION_PATH_TO_SKDIFF = '--path-to-skdiff' | 60 OPTION_PATH_TO_SKDIFF = '--path-to-skdiff' |
40 # default PATH_TO_SKDIFF is determined at runtime | 61 OPTION_SOURCE_DIR = '--source-dir' |
41 | 62 |
42 def RunCommand(command): | 63 def RunCommand(command): |
43 """Run a command, raising an exception if it fails. | 64 """Run a command, raising an exception if it fails. |
44 | 65 |
45 @param command the command as a single string | 66 @param command the command as a single string |
46 """ | 67 """ |
47 print 'running command [%s]...' % command | 68 print 'running command [%s]...' % command |
48 retval = os.system(command) | 69 retval = os.system(command) |
49 if retval is not 0: | 70 if retval is not 0: |
50 raise Exception('command [%s] failed' % command) | 71 raise Exception('command [%s] failed' % command) |
(...skipping 13 matching lines...) Expand all Loading... | |
64 trunk_path = os.path.join(os.path.dirname(__file__), os.pardir) | 85 trunk_path = os.path.join(os.path.dirname(__file__), os.pardir) |
65 possible_paths = [os.path.join(trunk_path, 'out', 'Release', 'skdiff'), | 86 possible_paths = [os.path.join(trunk_path, 'out', 'Release', 'skdiff'), |
66 os.path.join(trunk_path, 'out', 'Debug', 'skdiff')] | 87 os.path.join(trunk_path, 'out', 'Debug', 'skdiff')] |
67 for try_path in possible_paths: | 88 for try_path in possible_paths: |
68 if os.path.isfile(try_path): | 89 if os.path.isfile(try_path): |
69 return try_path | 90 return try_path |
70 raise Exception('cannot find skdiff in paths %s; maybe you need to ' | 91 raise Exception('cannot find skdiff in paths %s; maybe you need to ' |
71 'specify the %s option or build skdiff?' % ( | 92 'specify the %s option or build skdiff?' % ( |
72 possible_paths, OPTION_PATH_TO_SKDIFF)) | 93 possible_paths, OPTION_PATH_TO_SKDIFF)) |
73 | 94 |
74 def SvnDiff(path_to_skdiff, dest_dir): | 95 def _DownloadUrlToFile(source_url, dest_path): |
75 """Generates a visual diff of all pending changes in the local SVN checkout. | 96 """Download source_url, and save its contents to dest_path. |
97 Raises an exception if there were any problems.""" | |
98 reader = urllib2.urlopen(source_url) | |
99 writer = open(dest_path, 'w') | |
borenet
2013/07/12 19:48:04
You didn't need to open in binary mode for this to
epoger
2013/07/16 17:29:55
Good catch! I was testing it on Linux, where it d
| |
100 writer.write(reader.read()) | |
101 writer.close() | |
102 | |
103 def _CreateGSUrl(imagename, hash_type, hash_digest): | |
104 """Return the HTTP URL we can use to download this particular version of | |
105 the actually-generated GM image with this imagename. | |
106 | |
107 imagename: name of the test image, e.g. 'perlinnoise_msaa4.png' | |
108 hash_type: string indicating the hash type used to generate hash_digest, | |
109 e.g. gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5 | |
110 hash_digest: the hash digest of the image to retrieve | |
111 """ | |
borenet
2013/07/12 19:48:04
Can you add a TODO to consolidate these things?
epoger
2013/07/16 17:29:55
Consolidate which things? Do you mean TESTNAME_PA
borenet
2013/07/16 17:42:13
I was thinking maybe a BuildGMActualGSURLFromImage
epoger
2013/07/16 18:22:48
Sounds good... I created a new gm_json.CreateGmAct
| |
112 test_name = TESTNAME_PATTERN.match(imagename).group(1) | |
113 return '%s/%s/%s/%s.png' % (GOOGLESTORAGE_GM_ACTUALS_ROOT, | |
114 hash_type, test_name, hash_digest) | |
115 | |
116 def _CallJsonDiff(old_json_path, new_json_path, | |
117 old_flattened_dir, new_flattened_dir, | |
118 filename_prefix): | |
119 """Using jsondiff.py, write the images that differ between two GM | |
120 expectations summary files (old and new) into old_flattened_dir and | |
121 new_flattened_dir. | |
122 | |
123 filename_prefix: prefix to prepend to filenames of all images we write | |
124 into the flattened directories | |
125 """ | |
126 json_differ = jsondiff.GMDiffer() | |
127 diff_dict = json_differ.GenerateDiffDict(oldfile=old_json_path, | |
128 newfile=new_json_path) | |
129 for (imagename, results) in diff_dict.iteritems(): | |
130 old_checksum = results['old'] | |
131 new_checksum = results['new'] | |
132 # TODO(epoger): Currently, this assumes that all images have been | |
133 # checksummed using gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5 | |
134 old_image_url = _CreateGSUrl( | |
135 imagename=imagename, | |
136 hash_type=gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, | |
137 hash_digest=old_checksum) | |
138 new_image_url = _CreateGSUrl( | |
139 imagename=imagename, | |
140 hash_type=gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, | |
141 hash_digest=new_checksum) | |
142 _DownloadUrlToFile( | |
143 source_url=old_image_url, | |
144 dest_path=os.path.join(old_flattened_dir, | |
145 filename_prefix + imagename)) | |
146 _DownloadUrlToFile( | |
147 source_url=new_image_url, | |
148 dest_path=os.path.join(new_flattened_dir, | |
149 filename_prefix + imagename)) | |
150 | |
151 def SvnDiff(path_to_skdiff, dest_dir, source_dir): | |
152 """Generates a visual diff of all pending changes in source_dir. | |
76 | 153 |
77 @param path_to_skdiff | 154 @param path_to_skdiff |
78 @param dest_dir existing directory within which to write results | 155 @param dest_dir existing directory within which to write results |
156 @param source_dir | |
79 """ | 157 """ |
80 # Validate parameters, filling in default values if necessary and possible. | 158 # Validate parameters, filling in default values if necessary and possible. |
81 path_to_skdiff = FindPathToSkDiff(path_to_skdiff) | 159 path_to_skdiff = os.path.abspath(FindPathToSkDiff(path_to_skdiff)) |
82 if not dest_dir: | 160 if not dest_dir: |
83 dest_dir = tempfile.mkdtemp() | 161 dest_dir = tempfile.mkdtemp() |
162 dest_dir = os.path.abspath(dest_dir) | |
163 | |
164 os.chdir(source_dir) | |
84 | 165 |
85 # Prepare temporary directories. | 166 # Prepare temporary directories. |
86 modified_flattened_dir = os.path.join(dest_dir, 'modified_flattened') | 167 modified_flattened_dir = os.path.join(dest_dir, 'modified_flattened') |
87 original_flattened_dir = os.path.join(dest_dir, 'original_flattened') | 168 original_flattened_dir = os.path.join(dest_dir, 'original_flattened') |
88 diff_dir = os.path.join(dest_dir, 'diffs') | 169 diff_dir = os.path.join(dest_dir, 'diffs') |
89 for dir in [modified_flattened_dir, original_flattened_dir, diff_dir] : | 170 for dir in [modified_flattened_dir, original_flattened_dir, diff_dir] : |
90 shutil.rmtree(dir, ignore_errors=True) | 171 shutil.rmtree(dir, ignore_errors=True) |
91 os.mkdir(dir) | 172 os.mkdir(dir) |
92 | 173 |
93 # Get a list of all locally modified (including added/deleted) files, | 174 # Get a list of all locally modified (including added/deleted) files, |
94 # descending subdirectories. | 175 # descending subdirectories. |
95 svn_repo = svn.Svn('.') | 176 svn_repo = svn.Svn('.') |
96 modified_file_paths = svn_repo.GetFilesWithStatus( | 177 modified_file_paths = svn_repo.GetFilesWithStatus( |
97 svn.STATUS_ADDED | svn.STATUS_DELETED | svn.STATUS_MODIFIED) | 178 svn.STATUS_ADDED | svn.STATUS_DELETED | svn.STATUS_MODIFIED) |
98 | 179 |
99 # For each modified file: | 180 # For each modified file: |
100 # 1. copy its current contents into modified_flattened_dir | 181 # 1. copy its current contents into modified_flattened_dir |
101 # 2. copy its original contents into original_flattened_dir | 182 # 2. copy its original contents into original_flattened_dir |
102 for modified_file_path in modified_file_paths: | 183 for modified_file_path in modified_file_paths: |
103 dest_filename = re.sub(os.sep, '__', modified_file_path) | 184 if modified_file_path.endswith('.json'): |
104 # If the file had STATUS_DELETED, it won't exist anymore... | 185 # Special handling for JSON files, in the hopes that they |
105 if os.path.isfile(modified_file_path): | 186 # contain GM result summaries. |
106 shutil.copyfile(modified_file_path, | 187 (_unused, original_file_path) = tempfile.mkstemp() |
107 os.path.join(modified_flattened_dir, dest_filename)) | 188 svn_repo.ExportBaseVersionOfFile(modified_file_path, |
108 svn_repo.ExportBaseVersionOfFile( | 189 original_file_path) |
109 modified_file_path, | 190 platform_prefix = re.sub(os.sep, '__', |
110 os.path.join(original_flattened_dir, dest_filename)) | 191 os.path.dirname(modified_file_path)) + '__' |
192 _CallJsonDiff(old_json_path=original_file_path, | |
193 new_json_path=modified_file_path, | |
194 old_flattened_dir=original_flattened_dir, | |
195 new_flattened_dir=modified_flattened_dir, | |
196 filename_prefix=platform_prefix) | |
197 os.remove(original_file_path) | |
198 else: | |
199 dest_filename = re.sub(os.sep, '__', modified_file_path) | |
200 # If the file had STATUS_DELETED, it won't exist anymore... | |
201 if os.path.isfile(modified_file_path): | |
202 shutil.copyfile(modified_file_path, | |
203 os.path.join(modified_flattened_dir, dest_filena me)) | |
204 svn_repo.ExportBaseVersionOfFile( | |
205 modified_file_path, | |
206 os.path.join(original_flattened_dir, dest_filename)) | |
111 | 207 |
112 # Run skdiff: compare original_flattened_dir against modified_flattened_dir | 208 # Run skdiff: compare original_flattened_dir against modified_flattened_dir |
113 RunCommand('%s %s %s %s' % (path_to_skdiff, original_flattened_dir, | 209 RunCommand('%s %s %s %s' % (path_to_skdiff, original_flattened_dir, |
114 modified_flattened_dir, diff_dir)) | 210 modified_flattened_dir, diff_dir)) |
115 print '\nskdiff results are ready in file://%s/index.html' % diff_dir | 211 print '\nskdiff results are ready in file://%s/index.html' % diff_dir |
116 | 212 |
117 def RaiseUsageException(): | 213 def RaiseUsageException(): |
118 raise Exception('%s\nRun with --help for more detail.' % ( | 214 raise Exception('%s\nRun with --help for more detail.' % ( |
119 USAGE_STRING % __file__)) | 215 USAGE_STRING % __file__)) |
120 | 216 |
121 def Main(options, args): | 217 def Main(options, args): |
122 """Allow other scripts to call this script with fake command-line args. | 218 """Allow other scripts to call this script with fake command-line args. |
123 """ | 219 """ |
124 num_args = len(args) | 220 num_args = len(args) |
125 if num_args != 0: | 221 if num_args != 0: |
126 RaiseUsageException() | 222 RaiseUsageException() |
127 SvnDiff(path_to_skdiff=options.path_to_skdiff, dest_dir=options.dest_dir) | 223 SvnDiff(path_to_skdiff=options.path_to_skdiff, dest_dir=options.dest_dir, |
224 source_dir=options.source_dir) | |
128 | 225 |
129 if __name__ == '__main__': | 226 if __name__ == '__main__': |
130 parser = optparse.OptionParser(USAGE_STRING % '%prog' + HELP_STRING) | 227 parser = optparse.OptionParser(USAGE_STRING % '%prog' + HELP_STRING) |
131 parser.add_option(OPTION_DEST_DIR, | 228 parser.add_option(OPTION_DEST_DIR, |
132 action='store', type='string', default=None, | 229 action='store', type='string', default=None, |
133 help='existing directory within which to write results; ' | 230 help='existing directory within which to write results; ' |
134 'if not set, will create a temporary directory which ' | 231 'if not set, will create a temporary directory which ' |
135 'will remain in place after this script completes') | 232 'will remain in place after this script completes') |
136 parser.add_option(OPTION_PATH_TO_SKDIFF, | 233 parser.add_option(OPTION_PATH_TO_SKDIFF, |
137 action='store', type='string', default=None, | 234 action='store', type='string', default=None, |
138 help='path to already-built skdiff tool; if not set, ' | 235 help='path to already-built skdiff tool; if not set, ' |
139 'will search for it in typical directories near this ' | 236 'will search for it in typical directories near this ' |
140 'script') | 237 'script') |
238 parser.add_option(OPTION_SOURCE_DIR, | |
239 action='store', type='string', default='.', | |
240 help='root directory within which to compare all ' + | |
241 'files; defaults to "%default"') | |
141 (options, args) = parser.parse_args() | 242 (options, args) = parser.parse_args() |
142 Main(options, args) | 243 Main(options, args) |
OLD | NEW |