aboutsummaryrefslogtreecommitdiffhomepage
path: root/gm/rebaseline_server/imagepair.py
blob: 0ac0c4213883568a01fbbea8cbdae69fd1416f09 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/usr/bin/python

"""
Copyright 2014 Google Inc.

Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.

ImagePair class (see class docstring for details)
"""

import posixpath


# Keys used within ImagePair dictionary representations.
# NOTE: Keep these in sync with static/constants.js
KEY__IMAGEPAIRS__DIFFERENCES = 'differenceData'
KEY__IMAGEPAIRS__EXPECTATIONS = 'expectations'
KEY__IMAGEPAIRS__EXTRACOLUMNS = 'extraColumns'
KEY__IMAGEPAIRS__IMAGE_A_URL = 'imageAUrl'
KEY__IMAGEPAIRS__IMAGE_B_URL = 'imageBUrl'
KEY__IMAGEPAIRS__IS_DIFFERENT = 'isDifferent'
KEY__IMAGEPAIRS__SOURCE_JSON_FILE = 'sourceJsonFile'

# If self._diff_record is set to this, we haven't asked ImageDiffDB for the
# image diff details yet.
_DIFF_RECORD_STILL_LOADING = 'still_loading'


class ImagePair(object):
  """Describes a pair of images, pixel difference info, and optional metadata.
  """

  def __init__(self, image_diff_db,
               base_url, imageA_relative_url, imageB_relative_url,
               expectations=None, extra_columns=None, source_json_file=None,
               download_all_images=False):
    """
    Args:
      image_diff_db: ImageDiffDB instance we use to generate/store image diffs
      base_url: base of all image URLs
      imageA_relative_url: string; URL pointing at an image, relative to
          base_url; or None, if this image is missing
      imageB_relative_url: string; URL pointing at an image, relative to
          base_url; or None, if this image is missing
      expectations: optional dictionary containing expectations-specific
          metadata (ignore-failure, bug numbers, etc.)
      extra_columns: optional dictionary containing more metadata (test name,
          builder name, etc.)
      source_json_file: relative path of the JSON file where each image came
          from; this will be the same for both imageA and imageB, within their
          respective directories
      download_all_images: if True, download any images associated with this
          image pair, even if we don't need them to generate diffs
          (imageA == imageB, or one of them is missing)
    """
    self._image_diff_db = image_diff_db
    self.base_url = base_url
    self.imageA_relative_url = imageA_relative_url
    self.imageB_relative_url = imageB_relative_url
    self.expectations_dict = expectations
    self.extra_columns_dict = extra_columns
    self.source_json_file = source_json_file
    if not imageA_relative_url or not imageB_relative_url:
      self._is_different = True
      self._diff_record = None
    elif imageA_relative_url == imageB_relative_url:
      self._is_different = False
      self._diff_record = None
    else:
      # Tell image_diff_db to add an entry for this diff asynchronously.
      # Later on, we will call image_diff_db.get_diff_record() to find it.
      self._is_different = True
      self._diff_record = _DIFF_RECORD_STILL_LOADING

    if self._diff_record != None or download_all_images:
      image_diff_db.add_image_pair(
          expected_image_locator=imageA_relative_url,
          expected_image_url=self.posixpath_join(base_url, imageA_relative_url),
          actual_image_locator=imageB_relative_url,
          actual_image_url=self.posixpath_join(base_url, imageB_relative_url))

  def as_dict(self):
    """Returns a dictionary describing this ImagePair.

    Uses the KEY__IMAGEPAIRS__* constants as keys.
    """
    asdict = {
        KEY__IMAGEPAIRS__IMAGE_A_URL: self.imageA_relative_url,
        KEY__IMAGEPAIRS__IMAGE_B_URL: self.imageB_relative_url,
    }
    asdict[KEY__IMAGEPAIRS__IS_DIFFERENT] = self._is_different
    if self.expectations_dict:
      asdict[KEY__IMAGEPAIRS__EXPECTATIONS] = self.expectations_dict
    if self.extra_columns_dict:
      asdict[KEY__IMAGEPAIRS__EXTRACOLUMNS] = self.extra_columns_dict
    if self.source_json_file:
      asdict[KEY__IMAGEPAIRS__SOURCE_JSON_FILE] = self.source_json_file
    if self._diff_record is _DIFF_RECORD_STILL_LOADING:
      # We have waited as long as we can to ask ImageDiffDB for details of
      # this image diff.  Now we must block until ImageDiffDB can provide
      # those details.
      #
      # TODO(epoger): Is it wasteful for every imagepair to have its own
      # reference to image_diff_db?  If so, we could pass an image_diff_db
      # reference into this method call instead...
      self._diff_record = self._image_diff_db.get_diff_record(
          expected_image_locator=self.imageA_relative_url,
          actual_image_locator=self.imageB_relative_url)
    if self._diff_record != None:
      asdict[KEY__IMAGEPAIRS__DIFFERENCES] = self._diff_record.as_dict()
    return asdict

  @staticmethod
  def posixpath_join(*args):
    """Wrapper around posixpath.join().

    Returns posixpath.join(*args), or None if any arg is None.
    """
    for arg in args:
      if arg == None:
        return None
    return posixpath.join(*args)