OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 """ | 3 """ |
4 Copyright 2013 Google Inc. | 4 Copyright 2013 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 Calulate differences between image pairs, and store them in a database. | 9 Calulate differences between image pairs, and store them in a database. |
10 """ | 10 """ |
(...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
94 expected_image_locator = _sanitize_locator(expected_image_locator) | 94 expected_image_locator = _sanitize_locator(expected_image_locator) |
95 actual_image_locator = _sanitize_locator(actual_image_locator) | 95 actual_image_locator = _sanitize_locator(actual_image_locator) |
96 | 96 |
97 # Download the expected/actual images, if we don't have them already. | 97 # Download the expected/actual images, if we don't have them already. |
98 expected_image_file = os.path.join( | 98 expected_image_file = os.path.join( |
99 storage_root, expected_images_subdir, | 99 storage_root, expected_images_subdir, |
100 str(expected_image_locator) + image_suffix) | 100 str(expected_image_locator) + image_suffix) |
101 actual_image_file = os.path.join( | 101 actual_image_file = os.path.join( |
102 storage_root, actual_images_subdir, | 102 storage_root, actual_images_subdir, |
103 str(actual_image_locator) + image_suffix) | 103 str(actual_image_locator) + image_suffix) |
104 try: | 104 for image_file, image_url in [ |
105 _download_file(gs, expected_image_file, expected_image_url) | 105 (expected_image_file, expected_image_url), |
106 except Exception: | 106 (actual_image_file, actual_image_url)]: |
107 logging.exception('unable to download expected_image_url %s to file %s' % | 107 if image_file and image_url: |
108 (expected_image_url, expected_image_file)) | 108 try: |
109 raise | 109 _download_file(gs, image_file, image_url) |
110 try: | 110 except Exception: |
111 _download_file(gs, actual_image_file, actual_image_url) | 111 logging.exception('unable to download image_url %s to file %s' % |
112 except Exception: | 112 (image_url, image_file)) |
113 logging.exception('unable to download actual_image_url %s to file %s' % | 113 raise |
114 (actual_image_url, actual_image_file)) | |
115 raise | |
116 | 114 |
117 # Get all diff images and values from skpdiff binary. | 115 # Return early if we do not need to generate diffs. |
116 if (expected_image_url == actual_image_url or | |
117 not expected_image_url or not actual_image_url): | |
118 return | |
119 | |
120 # Get all diff images and values using the skpdiff binary. | |
118 skpdiff_output_dir = tempfile.mkdtemp() | 121 skpdiff_output_dir = tempfile.mkdtemp() |
119 try: | 122 try: |
120 skpdiff_summary_file = os.path.join(skpdiff_output_dir, | 123 skpdiff_summary_file = os.path.join(skpdiff_output_dir, |
121 'skpdiff-output.json') | 124 'skpdiff-output.json') |
122 skpdiff_rgbdiff_dir = os.path.join(skpdiff_output_dir, 'rgbDiff') | 125 skpdiff_rgbdiff_dir = os.path.join(skpdiff_output_dir, 'rgbDiff') |
123 skpdiff_whitediff_dir = os.path.join(skpdiff_output_dir, 'whiteDiff') | 126 skpdiff_whitediff_dir = os.path.join(skpdiff_output_dir, 'whiteDiff') |
124 expected_img = os.path.join(storage_root, expected_images_subdir, | |
125 str(expected_image_locator) + image_suffix) | |
126 actual_img = os.path.join(storage_root, actual_images_subdir, | |
127 str(actual_image_locator) + image_suffix) | |
128 | 127 |
129 # TODO(epoger): Consider calling skpdiff ONCE for all image pairs, | 128 # TODO(epoger): Consider calling skpdiff ONCE for all image pairs, |
130 # instead of calling it separately for each image pair. | 129 # instead of calling it separately for each image pair. |
131 # Pro: we'll incur less overhead from making repeated system calls, | 130 # Pro: we'll incur less overhead from making repeated system calls, |
132 # spinning up the skpdiff binary, etc. | 131 # spinning up the skpdiff binary, etc. |
133 # Con: we would have to wait until all image pairs were loaded before | 132 # Con: we would have to wait until all image pairs were loaded before |
134 # generating any of the diffs? | 133 # generating any of the diffs? |
135 find_run_binary.run_command( | 134 find_run_binary.run_command( |
136 [SKPDIFF_BINARY, '-p', expected_img, actual_img, | 135 [SKPDIFF_BINARY, '-p', expected_image_file, actual_image_file, |
137 '--jsonp', 'false', | 136 '--jsonp', 'false', |
138 '--output', skpdiff_summary_file, | 137 '--output', skpdiff_summary_file, |
139 '--differs', 'perceptual', 'different_pixels', | 138 '--differs', 'perceptual', 'different_pixels', |
140 '--rgbDiffDir', skpdiff_rgbdiff_dir, | 139 '--rgbDiffDir', skpdiff_rgbdiff_dir, |
141 '--whiteDiffDir', skpdiff_whitediff_dir, | 140 '--whiteDiffDir', skpdiff_whitediff_dir, |
142 ]) | 141 ]) |
143 | 142 |
144 # Get information out of the skpdiff_summary_file. | 143 # Get information out of the skpdiff_summary_file. |
145 with contextlib.closing(open(skpdiff_summary_file)) as fp: | 144 with contextlib.closing(open(skpdiff_summary_file)) as fp: |
146 data = json.load(fp) | 145 data = json.load(fp) |
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
254 # previously downloaded to local disk. | 253 # previously downloaded to local disk. |
255 # I guess we should figure out how expensive it is to download vs diff the | 254 # I guess we should figure out how expensive it is to download vs diff the |
256 # image pairs... if diffing them is expensive too, we can write these | 255 # image pairs... if diffing them is expensive too, we can write these |
257 # _diff_dict objects out to disk if there's too many to hold in RAM. | 256 # _diff_dict objects out to disk if there's too many to hold in RAM. |
258 # Or we could use virtual memory to handle that automatically. | 257 # Or we could use virtual memory to handle that automatically. |
259 self._diff_dict = {} | 258 self._diff_dict = {} |
260 self._diff_dict_writelock = threading.RLock() | 259 self._diff_dict_writelock = threading.RLock() |
261 | 260 |
262 # Set up the queue for asynchronously loading DiffRecords, and start the | 261 # Set up the queue for asynchronously loading DiffRecords, and start the |
263 # worker threads reading from it. | 262 # worker threads reading from it. |
264 self._tasks_queue = Queue.Queue(maxsize=2*num_worker_threads) | 263 # The queue maxsize must be 0 (infinite size queue), so that asynchronous |
264 # calls can return as soon as possible. | |
265 self._tasks_queue = Queue.Queue(maxsize=0) | |
265 self._workers = [] | 266 self._workers = [] |
266 for i in range(num_worker_threads): | 267 for i in range(num_worker_threads): |
267 worker = threading.Thread(target=self.worker, args=(i,)) | 268 worker = threading.Thread(target=self.worker, args=(i,)) |
268 worker.daemon = True | 269 worker.daemon = True |
269 worker.start() | 270 worker.start() |
270 self._workers.append(worker) | 271 self._workers.append(worker) |
271 | 272 |
272 def worker(self, worker_num): | 273 def worker(self, worker_num): |
273 """Launch a worker thread that pulls tasks off self._tasks_queue. | 274 """Launch a worker thread that pulls tasks off self._tasks_queue. |
274 | 275 |
(...skipping 28 matching lines...) Expand all Loading... | |
303 expected_image_url, expected_image_locator, | 304 expected_image_url, expected_image_locator, |
304 actual_image_url, actual_image_locator): | 305 actual_image_url, actual_image_locator): |
305 """Asynchronously prepare a DiffRecord for a pair of images. | 306 """Asynchronously prepare a DiffRecord for a pair of images. |
306 | 307 |
307 This method will return quickly; calls to get_diff_record() will block | 308 This method will return quickly; calls to get_diff_record() will block |
308 until the DiffRecord is available (or we have given up on creating it). | 309 until the DiffRecord is available (or we have given up on creating it). |
309 | 310 |
310 If we already have a DiffRecord for this particular image pair, no work | 311 If we already have a DiffRecord for this particular image pair, no work |
311 will be done. | 312 will be done. |
312 | 313 |
314 If expected_image_url (or its locator) is None, just download actual_image. | |
315 If actual_image_url (or its locator) is None, just download expected_image. | |
316 | |
313 Args: | 317 Args: |
314 expected_image_url: file, GS, or HTTP url from which we will download the | 318 expected_image_url: file, GS, or HTTP url from which we will download the |
315 expected image | 319 expected image |
316 expected_image_locator: a unique ID string under which we will store the | 320 expected_image_locator: a unique ID string under which we will store the |
317 expected image within storage_root (probably including a checksum to | 321 expected image within storage_root (probably including a checksum to |
318 guarantee uniqueness) | 322 guarantee uniqueness) |
319 actual_image_url: file, GS, or HTTP url from which we will download the | 323 actual_image_url: file, GS, or HTTP url from which we will download the |
320 actual image | 324 actual image |
321 actual_image_locator: a unique ID string under which we will store the | 325 actual_image_locator: a unique ID string under which we will store the |
322 actual image within storage_root (probably including a checksum to | 326 actual image within storage_root (probably including a checksum to |
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
430 pass | 434 pass |
431 | 435 |
432 | 436 |
433 def _sanitize_locator(locator): | 437 def _sanitize_locator(locator): |
434 """Returns a sanitized version of a locator (one in which we know none of the | 438 """Returns a sanitized version of a locator (one in which we know none of the |
435 characters will have special meaning in filenames). | 439 characters will have special meaning in filenames). |
436 | 440 |
437 Args: | 441 Args: |
438 locator: string, or something that can be represented as a string | 442 locator: string, or something that can be represented as a string |
439 """ | 443 """ |
440 return DISALLOWED_FILEPATH_CHAR_REGEX.sub('_', str(locator)) | 444 if locator: |
441 | 445 return DISALLOWED_FILEPATH_CHAR_REGEX.sub('_', str(locator)) |
446 else: | |
447 return locator | |
rmistry
2014/08/06 17:49:01
Is it ok for this to be an empty string or None? s
epoger
2014/08/06 17:53:34
We actually need to allow for None. Added a comme
| |
442 | 448 |
443 def _get_difference_locator(expected_image_locator, actual_image_locator): | 449 def _get_difference_locator(expected_image_locator, actual_image_locator): |
444 """Returns the locator string used to look up the diffs between expected_image | 450 """Returns the locator string used to look up the diffs between expected_image |
445 and actual_image. | 451 and actual_image. |
446 | 452 |
447 We must keep this function in sync with getImageDiffRelativeUrl() in | 453 We must keep this function in sync with getImageDiffRelativeUrl() in |
448 static/loader.js | 454 static/loader.js |
449 | 455 |
450 Args: | 456 Args: |
451 expected_image_locator: locator string pointing at expected image | 457 expected_image_locator: locator string pointing at expected image |
452 actual_image_locator: locator string pointing at actual image | 458 actual_image_locator: locator string pointing at actual image |
453 | 459 |
454 Returns: already-sanitized locator where the diffs between expected and | 460 Returns: already-sanitized locator where the diffs between expected and |
455 actual images can be found | 461 actual images can be found |
456 """ | 462 """ |
457 return "%s-vs-%s" % (_sanitize_locator(expected_image_locator), | 463 return "%s-vs-%s" % (_sanitize_locator(expected_image_locator), |
458 _sanitize_locator(actual_image_locator)) | 464 _sanitize_locator(actual_image_locator)) |
OLD | NEW |