epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | """ |
| 4 | Copyright 2013 Google Inc. |
| 5 | |
| 6 | Use of this source code is governed by a BSD-style license that can be |
| 7 | found in the LICENSE file. |
| 8 | |
| 9 | Calulate differences between image pairs, and store them in a database. |
| 10 | """ |
| 11 | |
| 12 | import contextlib |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 13 | import json |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 14 | import logging |
| 15 | import os |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 16 | import re |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 17 | import shutil |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 18 | import sys |
| 19 | import tempfile |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 20 | import urllib |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 21 | |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 22 | # Set the PYTHONPATH to include the tools directory. |
| 23 | sys.path.append( |
| 24 | os.path.join( |
| 25 | os.path.dirname(os.path.realpath(__file__)), os.pardir, os.pardir, |
| 26 | 'tools')) |
| 27 | import find_run_binary |
| 28 | |
commit-bot@chromium.org | 4d0f008 | 2014-02-18 14:38:22 +0000 | [diff] [blame] | 29 | SKPDIFF_BINARY = find_run_binary.find_path_to_program('skpdiff') |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 30 | |
rmistry@google.com | 5861e52 | 2013-12-21 19:07:40 +0000 | [diff] [blame] | 31 | DEFAULT_IMAGE_SUFFIX = '.png' |
| 32 | DEFAULT_IMAGES_SUBDIR = 'images' |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 33 | |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 34 | DISALLOWED_FILEPATH_CHAR_REGEX = re.compile('[^\w\-]') |
| 35 | |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 36 | RGBDIFFS_SUBDIR = 'diffs' |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 37 | WHITEDIFFS_SUBDIR = 'whitediffs' |
| 38 | |
commit-bot@chromium.org | 16f4180 | 2014-02-26 19:05:20 +0000 | [diff] [blame] | 39 | # Keys used within DiffRecord dictionary representations. |
| 40 | # NOTE: Keep these in sync with static/constants.js |
commit-bot@chromium.org | 68a3815 | 2014-05-12 20:40:29 +0000 | [diff] [blame] | 41 | KEY__DIFFERENCES__MAX_DIFF_PER_CHANNEL = 'maxDiffPerChannel' |
| 42 | KEY__DIFFERENCES__NUM_DIFF_PIXELS = 'numDifferingPixels' |
| 43 | KEY__DIFFERENCES__PERCENT_DIFF_PIXELS = 'percentDifferingPixels' |
| 44 | KEY__DIFFERENCES__PERCEPTUAL_DIFF = 'perceptualDifference' |
commit-bot@chromium.org | 16f4180 | 2014-02-26 19:05:20 +0000 | [diff] [blame] | 45 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 46 | |
| 47 | class DiffRecord(object): |
| 48 | """ Record of differences between two images. """ |
| 49 | |
| 50 | def __init__(self, storage_root, |
| 51 | expected_image_url, expected_image_locator, |
rmistry@google.com | 5861e52 | 2013-12-21 19:07:40 +0000 | [diff] [blame] | 52 | actual_image_url, actual_image_locator, |
| 53 | expected_images_subdir=DEFAULT_IMAGES_SUBDIR, |
| 54 | actual_images_subdir=DEFAULT_IMAGES_SUBDIR, |
| 55 | image_suffix=DEFAULT_IMAGE_SUFFIX): |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 56 | """Download this pair of images (unless we already have them on local disk), |
| 57 | and prepare a DiffRecord for them. |
| 58 | |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 59 | TODO(epoger): Make this asynchronously download images, rather than blocking |
| 60 | until the images have been downloaded and processed. |
| 61 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 62 | Args: |
| 63 | storage_root: root directory on local disk within which we store all |
| 64 | images |
| 65 | expected_image_url: file or HTTP url from which we will download the |
| 66 | expected image |
| 67 | expected_image_locator: a unique ID string under which we will store the |
| 68 | expected image within storage_root (probably including a checksum to |
| 69 | guarantee uniqueness) |
| 70 | actual_image_url: file or HTTP url from which we will download the |
| 71 | actual image |
| 72 | actual_image_locator: a unique ID string under which we will store the |
| 73 | actual image within storage_root (probably including a checksum to |
| 74 | guarantee uniqueness) |
rmistry@google.com | 5861e52 | 2013-12-21 19:07:40 +0000 | [diff] [blame] | 75 | expected_images_subdir: the subdirectory expected images are stored in. |
| 76 | actual_images_subdir: the subdirectory actual images are stored in. |
| 77 | image_suffix: the suffix of images. |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 78 | """ |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 79 | expected_image_locator = _sanitize_locator(expected_image_locator) |
| 80 | actual_image_locator = _sanitize_locator(actual_image_locator) |
| 81 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 82 | # Download the expected/actual images, if we don't have them already. |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 83 | # TODO(rmistry): Add a parameter that just tries to use already-present |
| 84 | # image files rather than downloading them. |
commit-bot@chromium.org | 8cc39a6 | 2014-03-04 16:46:22 +0000 | [diff] [blame] | 85 | expected_image_file = os.path.join( |
| 86 | storage_root, expected_images_subdir, |
| 87 | str(expected_image_locator) + image_suffix) |
| 88 | actual_image_file = os.path.join( |
| 89 | storage_root, actual_images_subdir, |
| 90 | str(actual_image_locator) + image_suffix) |
| 91 | try: |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 92 | _download_file(expected_image_file, expected_image_url) |
commit-bot@chromium.org | 8cc39a6 | 2014-03-04 16:46:22 +0000 | [diff] [blame] | 93 | except Exception: |
| 94 | logging.exception('unable to download expected_image_url %s to file %s' % |
| 95 | (expected_image_url, expected_image_file)) |
| 96 | raise |
| 97 | try: |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 98 | _download_file(actual_image_file, actual_image_url) |
commit-bot@chromium.org | 8cc39a6 | 2014-03-04 16:46:22 +0000 | [diff] [blame] | 99 | except Exception: |
| 100 | logging.exception('unable to download actual_image_url %s to file %s' % |
| 101 | (actual_image_url, actual_image_file)) |
| 102 | raise |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 103 | |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 104 | # Get all diff images and values from skpdiff binary. |
| 105 | skpdiff_output_dir = tempfile.mkdtemp() |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 106 | try: |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 107 | skpdiff_summary_file = os.path.join(skpdiff_output_dir, |
| 108 | 'skpdiff-output.json') |
| 109 | skpdiff_rgbdiff_dir = os.path.join(skpdiff_output_dir, 'rgbDiff') |
| 110 | skpdiff_whitediff_dir = os.path.join(skpdiff_output_dir, 'whiteDiff') |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 111 | expected_img = os.path.join(storage_root, expected_images_subdir, |
| 112 | str(expected_image_locator) + image_suffix) |
| 113 | actual_img = os.path.join(storage_root, actual_images_subdir, |
| 114 | str(actual_image_locator) + image_suffix) |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 115 | |
| 116 | # TODO: Call skpdiff ONCE for all image pairs, instead of calling it |
| 117 | # repeatedly. This will allow us to parallelize a lot more work. |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 118 | find_run_binary.run_command( |
commit-bot@chromium.org | 4d0f008 | 2014-02-18 14:38:22 +0000 | [diff] [blame] | 119 | [SKPDIFF_BINARY, '-p', expected_img, actual_img, |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 120 | '--jsonp', 'false', |
| 121 | '--output', skpdiff_summary_file, |
| 122 | '--differs', 'perceptual', 'different_pixels', |
| 123 | '--rgbDiffDir', skpdiff_rgbdiff_dir, |
| 124 | '--whiteDiffDir', skpdiff_whitediff_dir, |
| 125 | ]) |
| 126 | |
| 127 | # Get information out of the skpdiff_summary_file. |
| 128 | with contextlib.closing(open(skpdiff_summary_file)) as fp: |
| 129 | data = json.load(fp) |
| 130 | |
| 131 | # For now, we can assume there is only one record in the output summary, |
| 132 | # since we passed skpdiff only one pair of images. |
| 133 | record = data['records'][0] |
| 134 | self._width = record['width'] |
| 135 | self._height = record['height'] |
| 136 | # TODO: make max_diff_per_channel a tuple instead of a list, because the |
| 137 | # structure is meaningful (first element is red, second is green, etc.) |
| 138 | # See http://stackoverflow.com/a/626871 |
| 139 | self._max_diff_per_channel = [ |
| 140 | record['maxRedDiff'], record['maxGreenDiff'], record['maxBlueDiff']] |
| 141 | rgb_diff_path = record['rgbDiffPath'] |
| 142 | white_diff_path = record['whiteDiffPath'] |
| 143 | per_differ_stats = record['diffs'] |
| 144 | for stats in per_differ_stats: |
| 145 | differ_name = stats['differName'] |
| 146 | if differ_name == 'different_pixels': |
| 147 | self._num_pixels_differing = stats['pointsOfInterest'] |
| 148 | elif differ_name == 'perceptual': |
| 149 | perceptual_similarity = stats['result'] |
| 150 | |
| 151 | # skpdiff returns the perceptual similarity; convert it to get the |
| 152 | # perceptual difference percentage. |
| 153 | # skpdiff outputs -1 if the images are different sizes. Treat any |
| 154 | # output that does not lie in [0, 1] as having 0% perceptual |
| 155 | # similarity. |
| 156 | if not 0 <= perceptual_similarity <= 1: |
| 157 | perceptual_similarity = 0 |
| 158 | self._perceptual_difference = 100 - (perceptual_similarity * 100) |
| 159 | |
| 160 | # Store the rgbdiff and whitediff images generated above. |
| 161 | diff_image_locator = _get_difference_locator( |
| 162 | expected_image_locator=expected_image_locator, |
| 163 | actual_image_locator=actual_image_locator) |
| 164 | basename = str(diff_image_locator) + image_suffix |
| 165 | _mkdir_unless_exists(os.path.join(storage_root, RGBDIFFS_SUBDIR)) |
| 166 | _mkdir_unless_exists(os.path.join(storage_root, WHITEDIFFS_SUBDIR)) |
| 167 | # TODO: Modify skpdiff's behavior so we can tell it exactly where to |
| 168 | # write the image files into, rather than having to move them around |
| 169 | # after skpdiff writes them out. |
| 170 | shutil.copyfile(rgb_diff_path, |
| 171 | os.path.join(storage_root, RGBDIFFS_SUBDIR, basename)) |
| 172 | shutil.copyfile(white_diff_path, |
| 173 | os.path.join(storage_root, WHITEDIFFS_SUBDIR, basename)) |
| 174 | |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 175 | finally: |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 176 | shutil.rmtree(skpdiff_output_dir) |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 177 | |
epoger | 6132b43 | 2014-07-09 07:59:06 -0700 | [diff] [blame^] | 178 | # TODO(epoger): Use properties instead of getters throughout. |
| 179 | # See http://stackoverflow.com/a/6618176 |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 180 | def get_num_pixels_differing(self): |
| 181 | """Returns the absolute number of pixels that differ.""" |
| 182 | return self._num_pixels_differing |
| 183 | |
| 184 | def get_percent_pixels_differing(self): |
| 185 | """Returns the percentage of pixels that differ, as a float between |
| 186 | 0 and 100 (inclusive).""" |
| 187 | return ((float(self._num_pixels_differing) * 100) / |
| 188 | (self._width * self._height)) |
| 189 | |
commit-bot@chromium.org | 44546f8 | 2014-02-11 18:21:26 +0000 | [diff] [blame] | 190 | def get_perceptual_difference(self): |
| 191 | """Returns the perceptual difference percentage.""" |
| 192 | return self._perceptual_difference |
| 193 | |
epoger@google.com | 214a024 | 2013-11-22 19:26:18 +0000 | [diff] [blame] | 194 | def get_max_diff_per_channel(self): |
| 195 | """Returns the maximum difference between the expected and actual images |
| 196 | for each R/G/B channel, as a list.""" |
| 197 | return self._max_diff_per_channel |
| 198 | |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 199 | def as_dict(self): |
| 200 | """Returns a dictionary representation of this DiffRecord, as needed when |
| 201 | constructing the JSON representation.""" |
| 202 | return { |
commit-bot@chromium.org | 68a3815 | 2014-05-12 20:40:29 +0000 | [diff] [blame] | 203 | KEY__DIFFERENCES__NUM_DIFF_PIXELS: self._num_pixels_differing, |
| 204 | KEY__DIFFERENCES__PERCENT_DIFF_PIXELS: |
commit-bot@chromium.org | 16f4180 | 2014-02-26 19:05:20 +0000 | [diff] [blame] | 205 | self.get_percent_pixels_differing(), |
commit-bot@chromium.org | 68a3815 | 2014-05-12 20:40:29 +0000 | [diff] [blame] | 206 | KEY__DIFFERENCES__MAX_DIFF_PER_CHANNEL: self._max_diff_per_channel, |
| 207 | KEY__DIFFERENCES__PERCEPTUAL_DIFF: self._perceptual_difference, |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 208 | } |
| 209 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 210 | |
| 211 | class ImageDiffDB(object): |
| 212 | """ Calculates differences between image pairs, maintaining a database of |
| 213 | them for download.""" |
| 214 | |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 215 | def __init__(self, storage_root): |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 216 | """ |
| 217 | Args: |
| 218 | storage_root: string; root path within the DB will store all of its stuff |
| 219 | """ |
| 220 | self._storage_root = storage_root |
| 221 | |
| 222 | # Dictionary of DiffRecords, keyed by (expected_image_locator, |
| 223 | # actual_image_locator) tuples. |
| 224 | self._diff_dict = {} |
| 225 | |
epoger | 6132b43 | 2014-07-09 07:59:06 -0700 | [diff] [blame^] | 226 | @property |
| 227 | def storage_root(self): |
| 228 | return self._storage_root |
| 229 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 230 | def add_image_pair(self, |
| 231 | expected_image_url, expected_image_locator, |
| 232 | actual_image_url, actual_image_locator): |
| 233 | """Download this pair of images (unless we already have them on local disk), |
| 234 | and prepare a DiffRecord for them. |
| 235 | |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 236 | TODO(epoger): Make this asynchronously download images, rather than blocking |
| 237 | until the images have been downloaded and processed. |
| 238 | When we do that, we should probably add a new method that will block |
| 239 | until all of the images have been downloaded and processed. Otherwise, |
| 240 | we won't know when it's safe to start calling get_diff_record(). |
| 241 | jcgregorio notes: maybe just make ImageDiffDB thread-safe and create a |
| 242 | thread-pool/worker queue at a higher level that just uses ImageDiffDB? |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 243 | |
| 244 | Args: |
| 245 | expected_image_url: file or HTTP url from which we will download the |
| 246 | expected image |
| 247 | expected_image_locator: a unique ID string under which we will store the |
| 248 | expected image within storage_root (probably including a checksum to |
| 249 | guarantee uniqueness) |
| 250 | actual_image_url: file or HTTP url from which we will download the |
| 251 | actual image |
| 252 | actual_image_locator: a unique ID string under which we will store the |
| 253 | actual image within storage_root (probably including a checksum to |
| 254 | guarantee uniqueness) |
| 255 | """ |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 256 | expected_image_locator = _sanitize_locator(expected_image_locator) |
| 257 | actual_image_locator = _sanitize_locator(actual_image_locator) |
| 258 | key = (expected_image_locator, actual_image_locator) |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 259 | if not key in self._diff_dict: |
| 260 | try: |
| 261 | new_diff_record = DiffRecord( |
| 262 | self._storage_root, |
| 263 | expected_image_url=expected_image_url, |
| 264 | expected_image_locator=expected_image_locator, |
| 265 | actual_image_url=actual_image_url, |
| 266 | actual_image_locator=actual_image_locator) |
commit-bot@chromium.org | a47e7ac | 2013-12-19 20:01:34 +0000 | [diff] [blame] | 267 | except Exception: |
commit-bot@chromium.org | 6844958 | 2014-04-01 22:16:33 +0000 | [diff] [blame] | 268 | # If we can't create a real DiffRecord for this (expected, actual) pair, |
| 269 | # store None and the UI will show whatever information we DO have. |
| 270 | # Fixes http://skbug.com/2368 . |
| 271 | logging.exception( |
| 272 | 'got exception while creating a DiffRecord for ' |
| 273 | 'expected_image_url=%s , actual_image_url=%s; returning None' % ( |
| 274 | expected_image_url, actual_image_url)) |
| 275 | new_diff_record = None |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 276 | self._diff_dict[key] = new_diff_record |
| 277 | |
| 278 | def get_diff_record(self, expected_image_locator, actual_image_locator): |
| 279 | """Returns the DiffRecord for this image pair. |
| 280 | |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 281 | Raises a KeyError if we don't have a DiffRecord for this image pair. |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 282 | """ |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 283 | key = (_sanitize_locator(expected_image_locator), |
| 284 | _sanitize_locator(actual_image_locator)) |
| 285 | return self._diff_dict[key] |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 286 | |
| 287 | |
| 288 | # Utility functions |
| 289 | |
epoger | 54f1ad8 | 2014-07-02 07:43:04 -0700 | [diff] [blame] | 290 | def _download_file(local_filepath, url): |
| 291 | """Download a file from url to local_filepath, unless it is already there. |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 292 | |
| 293 | Args: |
| 294 | local_filepath: path on local disk where the image should be stored |
| 295 | url: URL from which we can download the image if we don't have it yet |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 296 | """ |
| 297 | if not os.path.exists(local_filepath): |
| 298 | _mkdir_unless_exists(os.path.dirname(local_filepath)) |
| 299 | with contextlib.closing(urllib.urlopen(url)) as url_handle: |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 300 | with open(local_filepath, 'wb') as file_handle: |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 301 | shutil.copyfileobj(fsrc=url_handle, fdst=file_handle) |
epoger@google.com | 214a024 | 2013-11-22 19:26:18 +0000 | [diff] [blame] | 302 | |
commit-bot@chromium.org | 16f4180 | 2014-02-26 19:05:20 +0000 | [diff] [blame] | 303 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 304 | def _mkdir_unless_exists(path): |
| 305 | """Unless path refers to an already-existing directory, create it. |
| 306 | |
| 307 | Args: |
| 308 | path: path on local disk |
| 309 | """ |
commit-bot@chromium.org | c9b511f | 2014-04-15 18:50:12 +0000 | [diff] [blame] | 310 | if not os.path.isdir(path): |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 311 | os.makedirs(path) |
| 312 | |
commit-bot@chromium.org | 16f4180 | 2014-02-26 19:05:20 +0000 | [diff] [blame] | 313 | |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 314 | def _sanitize_locator(locator): |
| 315 | """Returns a sanitized version of a locator (one in which we know none of the |
| 316 | characters will have special meaning in filenames). |
| 317 | |
| 318 | Args: |
| 319 | locator: string, or something that can be represented as a string |
| 320 | """ |
| 321 | return DISALLOWED_FILEPATH_CHAR_REGEX.sub('_', str(locator)) |
| 322 | |
commit-bot@chromium.org | 16f4180 | 2014-02-26 19:05:20 +0000 | [diff] [blame] | 323 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 324 | def _get_difference_locator(expected_image_locator, actual_image_locator): |
| 325 | """Returns the locator string used to look up the diffs between expected_image |
| 326 | and actual_image. |
| 327 | |
commit-bot@chromium.org | 16f4180 | 2014-02-26 19:05:20 +0000 | [diff] [blame] | 328 | We must keep this function in sync with getImageDiffRelativeUrl() in |
| 329 | static/loader.js |
| 330 | |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 331 | Args: |
| 332 | expected_image_locator: locator string pointing at expected image |
| 333 | actual_image_locator: locator string pointing at actual image |
| 334 | |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 335 | Returns: already-sanitized locator where the diffs between expected and |
| 336 | actual images can be found |
epoger@google.com | 9dddf6f | 2013-11-08 16:25:25 +0000 | [diff] [blame] | 337 | """ |
commit-bot@chromium.org | 9985ef5 | 2014-02-10 18:19:30 +0000 | [diff] [blame] | 338 | return "%s-vs-%s" % (_sanitize_locator(expected_image_locator), |
| 339 | _sanitize_locator(actual_image_locator)) |