Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(175)

Side by Side Diff: gm/rebaseline_server/compare_rendered_pictures.py

Issue 424263005: teach rebaseline_server to generate diffs of rendered SKPs (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Ravi comments Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 2
3 """ 3 """
4 Copyright 2014 Google Inc. 4 Copyright 2014 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 Compare results of two render_pictures runs. 9 Compare results of two render_pictures runs.
10
11 TODO(epoger): Start using this module to compare ALL images (whether they
12 were generated from GMs or SKPs), and rename it accordingly.
10 """ 13 """
11 14
12 # System-level imports 15 # System-level imports
13 import logging 16 import logging
14 import os 17 import os
18 import shutil
19 import tempfile
15 import time 20 import time
16 21
17 # Must fix up PYTHONPATH before importing from within Skia 22 # Must fix up PYTHONPATH before importing from within Skia
18 import fix_pythonpath # pylint: disable=W0611 23 import fix_pythonpath # pylint: disable=W0611
19 24
20 # Imports from within Skia 25 # Imports from within Skia
26 from py.utils import gs_utils
21 from py.utils import url_utils 27 from py.utils import url_utils
28 import buildbot_globals
29 import column
22 import gm_json 30 import gm_json
23 import imagediffdb 31 import imagediffdb
24 import imagepair 32 import imagepair
25 import imagepairset 33 import imagepairset
26 import results 34 import results
27 35
28 # URL under which all render_pictures images can be found in Google Storage. 36 # URL under which all render_pictures images can be found in Google Storage.
29 # 37 #
30 # pylint: disable=C0301 38 # TODO(epoger): In order to allow live-view of GMs and other images, read this
31 # TODO(epoger): Move this default value into 39 # from the input summary files, or allow the caller to set it within the
32 # https://skia.googlesource.com/buildbot/+/master/site_config/global_variables.j son 40 # GET_live_results call.
33 # pylint: enable=C0301 41 DEFAULT_IMAGE_BASE_GS_URL = 'gs://' + buildbot_globals.Get('skp_images_bucket')
34 DEFAULT_IMAGE_BASE_URL = ( 42
35 'http://chromium-skia-gm.commondatastorage.googleapis.com/' 43 # Column descriptors, and display preferences for them.
36 'render_pictures/images') 44 COLUMN__RESULT_TYPE = results.KEY__EXTRACOLUMNS__RESULT_TYPE
45 COLUMN__SOURCE_SKP = 'sourceSkpFile'
46 COLUMN__TILED_OR_WHOLE = 'tiledOrWhole'
47 COLUMN__TILENUM = 'tilenum'
48 FREEFORM_COLUMN_IDS = [
49 COLUMN__TILENUM,
50 ]
51 ORDERED_COLUMN_IDS = [
52 COLUMN__RESULT_TYPE,
53 COLUMN__SOURCE_SKP,
54 COLUMN__TILED_OR_WHOLE,
55 COLUMN__TILENUM,
56 ]
37 57
38 58
39 class RenderedPicturesComparisons(results.BaseComparisons): 59 class RenderedPicturesComparisons(results.BaseComparisons):
40 """Loads results from two different render_pictures runs into an ImagePairSet. 60 """Loads results from multiple render_pictures runs into an ImagePairSet.
41 """ 61 """
42 62
43 def __init__(self, subdirs, actuals_root, 63 def __init__(self, setA_dirs, setB_dirs, image_diff_db,
44 generated_images_root=results.DEFAULT_GENERATED_IMAGES_ROOT, 64 image_base_gs_url=DEFAULT_IMAGE_BASE_GS_URL,
45 image_base_url=DEFAULT_IMAGE_BASE_URL, 65 diff_base_url=None, setA_label='setA',
46 diff_base_url=None): 66 setB_label='setB', gs=None,
67 truncate_results=False):
47 """ 68 """
48 Args: 69 Args:
49 actuals_root: root directory containing all render_pictures-generated 70 setA_dirs: list of root directories to copy all JSON summaries from,
50 JSON files 71 and to use as setA within the comparisons
51 subdirs: (string, string) tuple; pair of subdirectories within 72 setB_dirs: list of root directories to copy all JSON summaries from,
52 actuals_root to compare 73 and to use as setB within the comparisons
53 generated_images_root: directory within which to create all pixel diffs; 74 image_diff_db: ImageDiffDB instance
54 if this directory does not yet exist, it will be created 75 image_base_gs_url: "gs://" URL pointing at the Google Storage bucket/dir
55 image_base_url: URL under which all render_pictures result images can 76 under which all render_pictures result images can
56 be found; this will be used to read images for comparison within 77 be found; this will be used to read images for comparison within
57 this code, and included in the ImagePairSet so its consumers know 78 this code, and included in the ImagePairSet (as an HTTP URL) so its
58 where to download the images from 79 consumers know where to download the images from
59 diff_base_url: base URL within which the client should look for diff 80 diff_base_url: base URL within which the client should look for diff
60 images; if not specified, defaults to a "file:///" URL representation 81 images; if not specified, defaults to a "file:///" URL representation
61 of generated_images_root 82 of image_diff_db's storage_root
83 setA_label: description to use for results in setA
84 setB_label: description to use for results in setB
85 gs: instance of GSUtils object we can use to download summary files
86 truncate_results: FOR MANUAL TESTING: if True, truncate the set of images
87 we process, to speed up testing.
62 """ 88 """
63 time_start = int(time.time()) 89 super(RenderedPicturesComparisons, self).__init__()
64 self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root) 90 self._image_diff_db = image_diff_db
65 self._image_base_url = image_base_url 91 self._image_base_gs_url = image_base_gs_url
66 self._diff_base_url = ( 92 self._diff_base_url = (
67 diff_base_url or 93 diff_base_url or
68 url_utils.create_filepath_url(generated_images_root)) 94 url_utils.create_filepath_url(image_diff_db.storage_root))
69 self._load_result_pairs(actuals_root, subdirs) 95 self._setA_label = setA_label
70 self._timestamp = int(time.time()) 96 self._setB_label = setB_label
71 logging.info('Results complete; took %d seconds.' % 97 self._gs = gs
72 (self._timestamp - time_start)) 98 self.truncate_results = truncate_results
73 99
74 def _load_result_pairs(self, actuals_root, subdirs): 100 tempdir = tempfile.mkdtemp()
75 """Loads all JSON files found within two subdirs in actuals_root, 101 try:
76 compares across those two subdirs, and stores the summary in self._results. 102 setA_root = os.path.join(tempdir, 'setA')
103 setB_root = os.path.join(tempdir, 'setB')
104 for source_dir in setA_dirs:
105 self._copy_dir_contents(source_dir=source_dir, dest_dir=setA_root)
106 for source_dir in setB_dirs:
107 self._copy_dir_contents(source_dir=source_dir, dest_dir=setB_root)
108
109 time_start = int(time.time())
110 # TODO(epoger): For now, this assumes that we are always comparing two
111 # sets of actual results, not actuals vs expectations. Allow the user
112 # to control this.
113 self._results = self._load_result_pairs(
114 setA_root=setA_root, setA_section=gm_json.JSONKEY_ACTUALRESULTS,
115 setB_root=setB_root, setB_section=gm_json.JSONKEY_ACTUALRESULTS)
116 self._timestamp = int(time.time())
117 logging.info('Number of download file collisions: %s' %
118 imagediffdb.global_file_collisions)
119 logging.info('Results complete; took %d seconds.' %
120 (self._timestamp - time_start))
121 finally:
122 shutil.rmtree(tempdir)
123
124 def _load_result_pairs(self, setA_root, setA_section, setB_root,
125 setB_section):
126 """Loads all JSON image summaries from 2 directory trees and compares them.
77 127
78 Args: 128 Args:
79 actuals_root: root directory containing all render_pictures-generated 129 setA_root: root directory containing JSON summaries of rendering results
80 JSON files 130 setA_section: which section (gm_json.JSONKEY_ACTUALRESULTS or
81 subdirs: (string, string) tuple; pair of subdirectories within 131 gm_json.JSONKEY_EXPECTEDRESULTS) to load from the summaries in setA
82 actuals_root to compare 132 setB_root: root directory containing JSON summaries of rendering results
133 setB_section: which section (gm_json.JSONKEY_ACTUALRESULTS or
134 gm_json.JSONKEY_EXPECTEDRESULTS) to load from the summaries in setB
135
136 Returns the summary of all image diff results.
83 """ 137 """
84 logging.info( 138 logging.info('Reading JSON image summaries from dirs %s and %s...' % (
85 'Reading actual-results JSON files from %s subdirs within %s...' % ( 139 setA_root, setB_root))
86 subdirs, actuals_root)) 140 setA_dicts = self._read_dicts_from_root(setA_root)
87 subdirA, subdirB = subdirs 141 setB_dicts = self._read_dicts_from_root(setB_root)
88 subdirA_dicts = self._read_dicts_from_root( 142 logging.info('Comparing summary dicts...')
89 os.path.join(actuals_root, subdirA))
90 subdirB_dicts = self._read_dicts_from_root(
91 os.path.join(actuals_root, subdirB))
92 logging.info('Comparing subdirs %s and %s...' % (subdirA, subdirB))
93 143
94 all_image_pairs = imagepairset.ImagePairSet( 144 all_image_pairs = imagepairset.ImagePairSet(
95 descriptions=subdirs, 145 descriptions=(self._setA_label, self._setB_label),
96 diff_base_url=self._diff_base_url) 146 diff_base_url=self._diff_base_url)
97 failing_image_pairs = imagepairset.ImagePairSet( 147 failing_image_pairs = imagepairset.ImagePairSet(
98 descriptions=subdirs, 148 descriptions=(self._setA_label, self._setB_label),
99 diff_base_url=self._diff_base_url) 149 diff_base_url=self._diff_base_url)
100 150
151 # Override settings for columns that should be filtered using freeform text.
152 for column_id in FREEFORM_COLUMN_IDS:
153 factory = column.ColumnHeaderFactory(
154 header_text=column_id, use_freeform_filter=True)
155 all_image_pairs.set_column_header_factory(
156 column_id=column_id, column_header_factory=factory)
157 failing_image_pairs.set_column_header_factory(
158 column_id=column_id, column_header_factory=factory)
159
101 all_image_pairs.ensure_extra_column_values_in_summary( 160 all_image_pairs.ensure_extra_column_values_in_summary(
102 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ 161 column_id=COLUMN__RESULT_TYPE, values=[
103 results.KEY__RESULT_TYPE__FAILED, 162 results.KEY__RESULT_TYPE__FAILED,
104 results.KEY__RESULT_TYPE__NOCOMPARISON, 163 results.KEY__RESULT_TYPE__NOCOMPARISON,
105 results.KEY__RESULT_TYPE__SUCCEEDED, 164 results.KEY__RESULT_TYPE__SUCCEEDED,
106 ]) 165 ])
107 failing_image_pairs.ensure_extra_column_values_in_summary( 166 failing_image_pairs.ensure_extra_column_values_in_summary(
108 column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[ 167 column_id=COLUMN__RESULT_TYPE, values=[
109 results.KEY__RESULT_TYPE__FAILED, 168 results.KEY__RESULT_TYPE__FAILED,
110 results.KEY__RESULT_TYPE__NOCOMPARISON, 169 results.KEY__RESULT_TYPE__NOCOMPARISON,
111 ]) 170 ])
112 171
113 common_dict_paths = sorted(set(subdirA_dicts.keys() + subdirB_dicts.keys())) 172 union_dict_paths = sorted(set(setA_dicts.keys() + setB_dicts.keys()))
114 num_common_dict_paths = len(common_dict_paths) 173 num_union_dict_paths = len(union_dict_paths)
115 dict_num = 0 174 dict_num = 0
116 for dict_path in common_dict_paths: 175 for dict_path in union_dict_paths:
117 dict_num += 1 176 dict_num += 1
118 logging.info('Generating pixel diffs for dict #%d of %d, "%s"...' % 177 logging.info('Generating pixel diffs for dict #%d of %d, "%s"...' %
119 (dict_num, num_common_dict_paths, dict_path)) 178 (dict_num, num_union_dict_paths, dict_path))
120 dictA = subdirA_dicts[dict_path] 179
121 dictB = subdirB_dicts[dict_path] 180 dictA = self.get_default(setA_dicts, None, dict_path)
122 self._validate_dict_version(dictA) 181 self._validate_dict_version(dictA)
182 dictA_results = self.get_default(dictA, {}, setA_section)
183
184 dictB = self.get_default(setB_dicts, None, dict_path)
123 self._validate_dict_version(dictB) 185 self._validate_dict_version(dictB)
124 dictA_results = dictA[gm_json.JSONKEY_ACTUALRESULTS] 186 dictB_results = self.get_default(dictB, {}, setB_section)
125 dictB_results = dictB[gm_json.JSONKEY_ACTUALRESULTS] 187
126 skp_names = sorted(set(dictA_results.keys() + dictB_results.keys())) 188 skp_names = sorted(set(dictA_results.keys() + dictB_results.keys()))
189 # Just for manual testing... truncate to an arbitrary subset.
190 if self.truncate_results:
191 skp_names = skp_names[1:3]
127 for skp_name in skp_names: 192 for skp_name in skp_names:
128 imagepairs_for_this_skp = [] 193 imagepairs_for_this_skp = []
129 194
130 whole_image_A = RenderedPicturesComparisons.get_multilevel( 195 whole_image_A = self.get_default(
131 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) 196 dictA_results, None,
132 whole_image_B = RenderedPicturesComparisons.get_multilevel( 197 skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE)
133 dictB_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE) 198 whole_image_B = self.get_default(
199 dictB_results, None,
200 skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE)
134 imagepairs_for_this_skp.append(self._create_image_pair( 201 imagepairs_for_this_skp.append(self._create_image_pair(
135 test=skp_name, config=gm_json.JSONKEY_SOURCE_WHOLEIMAGE, 202 image_dict_A=whole_image_A, image_dict_B=whole_image_B,
136 image_dict_A=whole_image_A, image_dict_B=whole_image_B)) 203 source_skp_name=skp_name, tilenum=None))
137 204
138 tiled_images_A = RenderedPicturesComparisons.get_multilevel( 205 tiled_images_A = self.get_default(
139 dictA_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES) 206 dictA_results, None,
140 tiled_images_B = RenderedPicturesComparisons.get_multilevel( 207 skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES)
141 dictB_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES) 208 tiled_images_B = self.get_default(
209 dictB_results, None,
210 skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES)
142 # TODO(epoger): Report an error if we find tiles for A but not B? 211 # TODO(epoger): Report an error if we find tiles for A but not B?
143 if tiled_images_A and tiled_images_B: 212 if tiled_images_A and tiled_images_B:
144 # TODO(epoger): Report an error if we find a different number of tiles 213 # TODO(epoger): Report an error if we find a different number of tiles
145 # for A and B? 214 # for A and B?
146 num_tiles = len(tiled_images_A) 215 num_tiles = len(tiled_images_A)
147 for tile_num in range(num_tiles): 216 for tile_num in range(num_tiles):
148 imagepairs_for_this_skp.append(self._create_image_pair( 217 imagepairs_for_this_skp.append(self._create_image_pair(
149 test=skp_name,
150 config='%s-%d' % (gm_json.JSONKEY_SOURCE_TILEDIMAGES, tile_num),
151 image_dict_A=tiled_images_A[tile_num], 218 image_dict_A=tiled_images_A[tile_num],
152 image_dict_B=tiled_images_B[tile_num])) 219 image_dict_B=tiled_images_B[tile_num],
220 source_skp_name=skp_name, tilenum=tile_num))
153 221
154 for one_imagepair in imagepairs_for_this_skp: 222 for one_imagepair in imagepairs_for_this_skp:
155 if one_imagepair: 223 if one_imagepair:
156 all_image_pairs.add_image_pair(one_imagepair) 224 all_image_pairs.add_image_pair(one_imagepair)
157 result_type = one_imagepair.extra_columns_dict\ 225 result_type = one_imagepair.extra_columns_dict\
158 [results.KEY__EXTRACOLUMNS__RESULT_TYPE] 226 [COLUMN__RESULT_TYPE]
159 if result_type != results.KEY__RESULT_TYPE__SUCCEEDED: 227 if result_type != results.KEY__RESULT_TYPE__SUCCEEDED:
160 failing_image_pairs.add_image_pair(one_imagepair) 228 failing_image_pairs.add_image_pair(one_imagepair)
161 229
162 # pylint: disable=W0201 230 return {
163 self._results = { 231 results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(
164 results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(), 232 column_ids_in_order=ORDERED_COLUMN_IDS),
165 results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(), 233 results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(
234 column_ids_in_order=ORDERED_COLUMN_IDS),
166 } 235 }
167 236
168 def _validate_dict_version(self, result_dict): 237 def _validate_dict_version(self, result_dict):
169 """Raises Exception if the dict is not the type/version we know how to read. 238 """Raises Exception if the dict is not the type/version we know how to read.
170 239
171 Args: 240 Args:
172 result_dict: dictionary holding output of render_pictures 241 result_dict: dictionary holding output of render_pictures; if None,
242 this method will return without raising an Exception
173 """ 243 """
174 expected_header_type = 'ChecksummedImages' 244 expected_header_type = 'ChecksummedImages'
175 expected_header_revision = 1 245 expected_header_revision = 1
176 246
247 if result_dict == None:
248 return
177 header = result_dict[gm_json.JSONKEY_HEADER] 249 header = result_dict[gm_json.JSONKEY_HEADER]
178 header_type = header[gm_json.JSONKEY_HEADER_TYPE] 250 header_type = header[gm_json.JSONKEY_HEADER_TYPE]
179 if header_type != expected_header_type: 251 if header_type != expected_header_type:
180 raise Exception('expected header_type "%s", but got "%s"' % ( 252 raise Exception('expected header_type "%s", but got "%s"' % (
181 expected_header_type, header_type)) 253 expected_header_type, header_type))
182 header_revision = header[gm_json.JSONKEY_HEADER_REVISION] 254 header_revision = header[gm_json.JSONKEY_HEADER_REVISION]
183 if header_revision != expected_header_revision: 255 if header_revision != expected_header_revision:
184 raise Exception('expected header_revision %d, but got %d' % ( 256 raise Exception('expected header_revision %d, but got %d' % (
185 expected_header_revision, header_revision)) 257 expected_header_revision, header_revision))
186 258
187 def _create_image_pair(self, test, config, image_dict_A, image_dict_B): 259 def _create_image_pair(self, image_dict_A, image_dict_B, source_skp_name,
260 tilenum):
188 """Creates an ImagePair object for this pair of images. 261 """Creates an ImagePair object for this pair of images.
189 262
190 Args: 263 Args:
191 test: string; name of the test
192 config: string; name of the config
193 image_dict_A: dict with JSONKEY_IMAGE_* keys, or None if no image 264 image_dict_A: dict with JSONKEY_IMAGE_* keys, or None if no image
194 image_dict_B: dict with JSONKEY_IMAGE_* keys, or None if no image 265 image_dict_B: dict with JSONKEY_IMAGE_* keys, or None if no image
266 source_skp_name: string; name of the source SKP file
267 tilenum: which tile, or None if a wholeimage
195 268
196 Returns: 269 Returns:
197 An ImagePair object, or None if both image_dict_A and image_dict_B are 270 An ImagePair object, or None if both image_dict_A and image_dict_B are
198 None. 271 None.
199 """ 272 """
200 if (not image_dict_A) and (not image_dict_B): 273 if (not image_dict_A) and (not image_dict_B):
201 return None 274 return None
202 275
203 def _checksum_and_relative_url(dic): 276 def _checksum_and_relative_url(dic):
204 if dic: 277 if dic:
(...skipping 11 matching lines...) Expand all
216 if not imageA_checksum: 289 if not imageA_checksum:
217 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON 290 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON
218 elif not imageB_checksum: 291 elif not imageB_checksum:
219 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON 292 result_type = results.KEY__RESULT_TYPE__NOCOMPARISON
220 elif imageA_checksum == imageB_checksum: 293 elif imageA_checksum == imageB_checksum:
221 result_type = results.KEY__RESULT_TYPE__SUCCEEDED 294 result_type = results.KEY__RESULT_TYPE__SUCCEEDED
222 else: 295 else:
223 result_type = results.KEY__RESULT_TYPE__FAILED 296 result_type = results.KEY__RESULT_TYPE__FAILED
224 297
225 extra_columns_dict = { 298 extra_columns_dict = {
226 results.KEY__EXTRACOLUMNS__CONFIG: config, 299 COLUMN__RESULT_TYPE: result_type,
227 results.KEY__EXTRACOLUMNS__RESULT_TYPE: result_type, 300 COLUMN__SOURCE_SKP: source_skp_name,
228 results.KEY__EXTRACOLUMNS__TEST: test,
229 # TODO(epoger): Right now, the client UI crashes if it receives
230 # results that do not include this column.
231 # Until we fix that, keep the client happy.
232 results.KEY__EXTRACOLUMNS__BUILDER: 'TODO',
233 } 301 }
302 if tilenum == None:
303 extra_columns_dict[COLUMN__TILED_OR_WHOLE] = 'whole'
304 extra_columns_dict[COLUMN__TILENUM] = 'N/A'
305 else:
306 extra_columns_dict[COLUMN__TILED_OR_WHOLE] = 'tiled'
307 extra_columns_dict[COLUMN__TILENUM] = str(tilenum)
234 308
235 try: 309 try:
236 return imagepair.ImagePair( 310 return imagepair.ImagePair(
237 image_diff_db=self._image_diff_db, 311 image_diff_db=self._image_diff_db,
238 base_url=self._image_base_url, 312 base_url=self._image_base_gs_url,
239 imageA_relative_url=imageA_relative_url, 313 imageA_relative_url=imageA_relative_url,
240 imageB_relative_url=imageB_relative_url, 314 imageB_relative_url=imageB_relative_url,
241 extra_columns=extra_columns_dict) 315 extra_columns=extra_columns_dict)
242 except (KeyError, TypeError): 316 except (KeyError, TypeError):
243 logging.exception( 317 logging.exception(
244 'got exception while creating ImagePair for' 318 'got exception while creating ImagePair for'
245 ' test="%s", config="%s", urlPair=("%s","%s")' % ( 319 ' urlPair=("%s","%s"), source_skp_name="%s", tilenum="%s"' % (
246 test, config, imageA_relative_url, imageB_relative_url)) 320 imageA_relative_url, imageB_relative_url, source_skp_name,
321 tilenum))
247 return None 322 return None
248 323
324 def _copy_dir_contents(self, source_dir, dest_dir):
325 """Copy all contents of source_dir into dest_dir, recursing into subdirs.
249 326
250 # TODO(epoger): Add main() so this can be called by vm_run_skia_try.sh 327 Args:
328 source_dir: path to source dir (GS URL or local filepath)
329 dest_dir: path to destination dir (local filepath)
330
331 The copy operates as a "merge with overwrite": any files in source_dir will
332 be "overlaid" on top of the existing content in dest_dir. Existing files
333 with the same names will be overwritten.
334 """
335 if gs_utils.GSUtils.is_gs_url(source_dir):
336 (bucket, path) = gs_utils.GSUtils.split_gs_url(source_dir)
337 self._gs.download_dir_contents(source_bucket=bucket, source_dir=path,
338 dest_dir=dest_dir)
339 else:
340 shutil.copytree(source_dir, dest_dir)
OLDNEW
« no previous file with comments | « gm/rebaseline_server/base_unittest.py ('k') | gm/rebaseline_server/compare_rendered_pictures_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698