Home | History | Annotate | Download | only in rebaseline_server
      1 #!/usr/bin/python
      2 
      3 """
      4 Copyright 2014 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 ImagePairSet class; see its docstring below.
     10 """
     11 
     12 # System-level imports
     13 import posixpath
     14 
     15 # Local imports
     16 import column
     17 import imagepair
     18 
     19 # Keys used within dictionary representation of ImagePairSet.
     20 # NOTE: Keep these in sync with static/constants.js
     21 KEY__ROOT__EXTRACOLUMNHEADERS = 'extraColumnHeaders'
     22 KEY__ROOT__HEADER = 'header'
     23 KEY__ROOT__IMAGEPAIRS = 'imagePairs'
     24 KEY__ROOT__IMAGESETS = 'imageSets'
     25 KEY__IMAGESETS__FIELD__BASE_URL = 'baseUrl'
     26 KEY__IMAGESETS__FIELD__DESCRIPTION = 'description'
     27 KEY__IMAGESETS__SET__DIFFS = 'diffs'
     28 KEY__IMAGESETS__SET__IMAGE_A = 'imageA'
     29 KEY__IMAGESETS__SET__IMAGE_B = 'imageB'
     30 KEY__IMAGESETS__SET__WHITEDIFFS = 'whiteDiffs'
     31 
     32 DEFAULT_DESCRIPTIONS = ('setA', 'setB')
     33 
     34 
     35 class ImagePairSet(object):
     36   """A collection of ImagePairs, representing two arbitrary sets of images.
     37 
     38   These could be:
     39   - images generated before and after a code patch
     40   - expected and actual images for some tests
     41   - or any other pairwise set of images.
     42   """
     43 
     44   def __init__(self, diff_base_url, descriptions=None):
     45     """
     46     Args:
     47       diff_base_url: base URL indicating where diff images can be loaded from
     48       descriptions: a (string, string) tuple describing the two image sets.
     49           If not specified, DEFAULT_DESCRIPTIONS will be used.
     50     """
     51     self._column_header_factories = {}
     52     self._descriptions = descriptions or DEFAULT_DESCRIPTIONS
     53     self._extra_column_tallies = {}  # maps column_id -> values
     54                                      #                -> instances_per_value
     55     self._image_pair_dicts = []
     56     self._image_base_url = None
     57     self._diff_base_url = diff_base_url
     58 
     59   def add_image_pair(self, image_pair):
     60     """Adds an ImagePair; this may be repeated any number of times."""
     61     # Special handling when we add the first ImagePair...
     62     if not self._image_pair_dicts:
     63       self._image_base_url = image_pair.base_url
     64 
     65     if image_pair.base_url != self._image_base_url:
     66       raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
     67           image_pair.base_url, self._image_base_url))
     68     self._image_pair_dicts.append(image_pair.as_dict())
     69     extra_columns_dict = image_pair.extra_columns_dict
     70     if extra_columns_dict:
     71       for column_id, value in extra_columns_dict.iteritems():
     72         self._add_extra_column_value_to_summary(column_id, value)
     73 
     74   def set_column_header_factory(self, column_id, column_header_factory):
     75     """Overrides the default settings for one of the extraColumn headers.
     76 
     77     Args:
     78       column_id: string; unique ID of this column (must match a key within
     79           an ImagePair's extra_columns dictionary)
     80       column_header_factory: a ColumnHeaderFactory object
     81     """
     82     self._column_header_factories[column_id] = column_header_factory
     83 
     84   def get_column_header_factory(self, column_id):
     85     """Returns the ColumnHeaderFactory object for a particular extraColumn.
     86 
     87     Args:
     88       column_id: string; unique ID of this column (must match a key within
     89           an ImagePair's extra_columns dictionary)
     90     """
     91     column_header_factory = self._column_header_factories.get(column_id, None)
     92     if not column_header_factory:
     93       column_header_factory = column.ColumnHeaderFactory(header_text=column_id)
     94       self._column_header_factories[column_id] = column_header_factory
     95     return column_header_factory
     96 
     97   def ensure_extra_column_values_in_summary(self, column_id, values):
     98     """Ensure this column_id/value pair is part of the extraColumns summary.
     99 
    100     Args:
    101       column_id: string; unique ID of this column
    102       value: string; a possible value for this column
    103     """
    104     for value in values:
    105       self._add_extra_column_value_to_summary(
    106           column_id=column_id, value=value, addend=0)
    107 
    108   def _add_extra_column_value_to_summary(self, column_id, value, addend=1):
    109     """Records one column_id/value extraColumns pair found within an ImagePair.
    110 
    111     We use this information to generate tallies within the column header
    112     (how many instances we saw of a particular value, within a particular
    113     extraColumn).
    114 
    115     Args:
    116       column_id: string; unique ID of this column (must match a key within
    117           an ImagePair's extra_columns dictionary)
    118       value: string; a possible value for this column
    119       addend: integer; how many instances to add to the tally
    120     """
    121     known_values_for_column = self._extra_column_tallies.get(column_id, None)
    122     if not known_values_for_column:
    123       known_values_for_column = {}
    124       self._extra_column_tallies[column_id] = known_values_for_column
    125     instances_of_this_value = known_values_for_column.get(value, 0)
    126     instances_of_this_value += addend
    127     known_values_for_column[value] = instances_of_this_value
    128 
    129   def _column_headers_as_dict(self):
    130     """Returns all column headers as a dictionary."""
    131     asdict = {}
    132     for column_id, values_for_column in self._extra_column_tallies.iteritems():
    133       column_header_factory = self.get_column_header_factory(column_id)
    134       asdict[column_id] = column_header_factory.create_as_dict(
    135           values_for_column)
    136     return asdict
    137 
    138   def as_dict(self):
    139     """Returns a dictionary describing this package of ImagePairs.
    140 
    141     Uses the KEY__* constants as keys.
    142     """
    143     key_description = KEY__IMAGESETS__FIELD__DESCRIPTION
    144     key_base_url = KEY__IMAGESETS__FIELD__BASE_URL
    145     return {
    146         KEY__ROOT__EXTRACOLUMNHEADERS: self._column_headers_as_dict(),
    147         KEY__ROOT__IMAGEPAIRS: self._image_pair_dicts,
    148         KEY__ROOT__IMAGESETS: {
    149             KEY__IMAGESETS__SET__IMAGE_A: {
    150                 key_description: self._descriptions[0],
    151                 key_base_url: self._image_base_url,
    152             },
    153             KEY__IMAGESETS__SET__IMAGE_B: {
    154                 key_description: self._descriptions[1],
    155                 key_base_url: self._image_base_url,
    156             },
    157             KEY__IMAGESETS__SET__DIFFS: {
    158                 key_description: 'color difference per channel',
    159                 key_base_url: posixpath.join(
    160                     self._diff_base_url, 'diffs'),
    161             },
    162             KEY__IMAGESETS__SET__WHITEDIFFS: {
    163                 key_description: 'differing pixels in white',
    164                 key_base_url: posixpath.join(
    165                     self._diff_base_url, 'whitediffs'),
    166             },
    167         },
    168     }
    169