2014-02-13 17:17:05 +00:00
|
|
|
#!/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.
|
|
|
|
|
|
|
|
ImagePairSet class; see its docstring below.
|
|
|
|
"""
|
|
|
|
|
2014-03-17 14:22:02 +00:00
|
|
|
# System-level imports
|
|
|
|
import posixpath
|
|
|
|
|
2014-08-05 17:07:22 +00:00
|
|
|
# Must fix up PYTHONPATH before importing from within Skia
|
2014-08-18 20:37:59 +00:00
|
|
|
import rs_fixpypath # pylint: disable=W0611
|
2014-08-05 17:07:22 +00:00
|
|
|
|
|
|
|
# Imports from within Skia
|
2014-02-13 17:17:05 +00:00
|
|
|
import column
|
2014-07-02 14:43:04 +00:00
|
|
|
import imagediffdb
|
2014-08-05 17:07:22 +00:00
|
|
|
from py.utils import gs_utils
|
2014-02-13 17:17:05 +00:00
|
|
|
|
|
|
|
# Keys used within dictionary representation of ImagePairSet.
|
2014-02-26 19:05:20 +00:00
|
|
|
# NOTE: Keep these in sync with static/constants.js
|
2014-05-12 20:40:29 +00:00
|
|
|
KEY__ROOT__EXTRACOLUMNHEADERS = 'extraColumnHeaders'
|
2014-07-09 13:19:20 +00:00
|
|
|
KEY__ROOT__EXTRACOLUMNORDER = 'extraColumnOrder'
|
2014-05-12 20:40:29 +00:00
|
|
|
KEY__ROOT__HEADER = 'header'
|
|
|
|
KEY__ROOT__IMAGEPAIRS = 'imagePairs'
|
|
|
|
KEY__ROOT__IMAGESETS = 'imageSets'
|
2014-03-17 14:22:02 +00:00
|
|
|
KEY__IMAGESETS__FIELD__BASE_URL = 'baseUrl'
|
|
|
|
KEY__IMAGESETS__FIELD__DESCRIPTION = 'description'
|
|
|
|
KEY__IMAGESETS__SET__DIFFS = 'diffs'
|
|
|
|
KEY__IMAGESETS__SET__IMAGE_A = 'imageA'
|
|
|
|
KEY__IMAGESETS__SET__IMAGE_B = 'imageB'
|
|
|
|
KEY__IMAGESETS__SET__WHITEDIFFS = 'whiteDiffs'
|
2014-02-13 17:17:05 +00:00
|
|
|
|
|
|
|
DEFAULT_DESCRIPTIONS = ('setA', 'setB')
|
|
|
|
|
|
|
|
|
|
|
|
class ImagePairSet(object):
|
|
|
|
"""A collection of ImagePairs, representing two arbitrary sets of images.
|
|
|
|
|
|
|
|
These could be:
|
|
|
|
- images generated before and after a code patch
|
|
|
|
- expected and actual images for some tests
|
|
|
|
- or any other pairwise set of images.
|
|
|
|
"""
|
|
|
|
|
2014-03-20 17:27:46 +00:00
|
|
|
def __init__(self, diff_base_url, descriptions=None):
|
2014-02-13 17:17:05 +00:00
|
|
|
"""
|
|
|
|
Args:
|
2014-03-20 17:27:46 +00:00
|
|
|
diff_base_url: base URL indicating where diff images can be loaded from
|
2014-02-13 17:17:05 +00:00
|
|
|
descriptions: a (string, string) tuple describing the two image sets.
|
|
|
|
If not specified, DEFAULT_DESCRIPTIONS will be used.
|
|
|
|
"""
|
|
|
|
self._column_header_factories = {}
|
|
|
|
self._descriptions = descriptions or DEFAULT_DESCRIPTIONS
|
|
|
|
self._extra_column_tallies = {} # maps column_id -> values
|
|
|
|
# -> instances_per_value
|
2014-08-22 11:46:30 +00:00
|
|
|
self._imageA_base_url = None
|
|
|
|
self._imageB_base_url = None
|
2014-03-20 17:27:46 +00:00
|
|
|
self._diff_base_url = diff_base_url
|
2014-02-13 17:17:05 +00:00
|
|
|
|
2014-08-05 17:07:22 +00:00
|
|
|
# We build self._image_pair_objects incrementally as calls come into
|
|
|
|
# add_image_pair(); self._image_pair_dicts is filled in lazily (so that
|
|
|
|
# we put off asking ImageDiffDB for results as long as possible).
|
|
|
|
self._image_pair_objects = []
|
|
|
|
self._image_pair_dicts = None
|
|
|
|
|
2014-02-13 17:17:05 +00:00
|
|
|
def add_image_pair(self, image_pair):
|
|
|
|
"""Adds an ImagePair; this may be repeated any number of times."""
|
|
|
|
# Special handling when we add the first ImagePair...
|
2014-08-05 17:07:22 +00:00
|
|
|
if not self._image_pair_objects:
|
2014-08-22 11:46:30 +00:00
|
|
|
self._imageA_base_url = image_pair.imageA_base_url
|
|
|
|
self._imageB_base_url = image_pair.imageB_base_url
|
2014-02-13 17:17:05 +00:00
|
|
|
|
2014-08-22 11:46:30 +00:00
|
|
|
if(image_pair.imageA_base_url != self._imageA_base_url):
|
2014-02-13 17:17:05 +00:00
|
|
|
raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
|
2014-08-22 11:46:30 +00:00
|
|
|
image_pair.imageA_base_url, self._imageA_base_url))
|
|
|
|
if(image_pair.imageB_base_url != self._imageB_base_url):
|
|
|
|
raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
|
|
|
|
image_pair.imageB_base_url, self._imageB_base_url))
|
2014-08-05 17:07:22 +00:00
|
|
|
self._image_pair_objects.append(image_pair)
|
2014-02-13 17:17:05 +00:00
|
|
|
extra_columns_dict = image_pair.extra_columns_dict
|
|
|
|
if extra_columns_dict:
|
|
|
|
for column_id, value in extra_columns_dict.iteritems():
|
2014-02-26 19:05:20 +00:00
|
|
|
self._add_extra_column_value_to_summary(column_id, value)
|
2014-02-13 17:17:05 +00:00
|
|
|
|
|
|
|
def set_column_header_factory(self, column_id, column_header_factory):
|
|
|
|
"""Overrides the default settings for one of the extraColumn headers.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
column_id: string; unique ID of this column (must match a key within
|
|
|
|
an ImagePair's extra_columns dictionary)
|
|
|
|
column_header_factory: a ColumnHeaderFactory object
|
|
|
|
"""
|
|
|
|
self._column_header_factories[column_id] = column_header_factory
|
|
|
|
|
|
|
|
def get_column_header_factory(self, column_id):
|
|
|
|
"""Returns the ColumnHeaderFactory object for a particular extraColumn.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
column_id: string; unique ID of this column (must match a key within
|
|
|
|
an ImagePair's extra_columns dictionary)
|
|
|
|
"""
|
|
|
|
column_header_factory = self._column_header_factories.get(column_id, None)
|
|
|
|
if not column_header_factory:
|
|
|
|
column_header_factory = column.ColumnHeaderFactory(header_text=column_id)
|
|
|
|
self._column_header_factories[column_id] = column_header_factory
|
|
|
|
return column_header_factory
|
|
|
|
|
2014-02-26 19:05:20 +00:00
|
|
|
def ensure_extra_column_values_in_summary(self, column_id, values):
|
|
|
|
"""Ensure this column_id/value pair is part of the extraColumns summary.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
column_id: string; unique ID of this column
|
|
|
|
value: string; a possible value for this column
|
|
|
|
"""
|
|
|
|
for value in values:
|
|
|
|
self._add_extra_column_value_to_summary(
|
|
|
|
column_id=column_id, value=value, addend=0)
|
|
|
|
|
|
|
|
def _add_extra_column_value_to_summary(self, column_id, value, addend=1):
|
2014-02-13 17:17:05 +00:00
|
|
|
"""Records one column_id/value extraColumns pair found within an ImagePair.
|
|
|
|
|
|
|
|
We use this information to generate tallies within the column header
|
|
|
|
(how many instances we saw of a particular value, within a particular
|
|
|
|
extraColumn).
|
2014-02-26 19:05:20 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
column_id: string; unique ID of this column (must match a key within
|
|
|
|
an ImagePair's extra_columns dictionary)
|
|
|
|
value: string; a possible value for this column
|
|
|
|
addend: integer; how many instances to add to the tally
|
2014-02-13 17:17:05 +00:00
|
|
|
"""
|
|
|
|
known_values_for_column = self._extra_column_tallies.get(column_id, None)
|
|
|
|
if not known_values_for_column:
|
|
|
|
known_values_for_column = {}
|
|
|
|
self._extra_column_tallies[column_id] = known_values_for_column
|
|
|
|
instances_of_this_value = known_values_for_column.get(value, 0)
|
2014-02-26 19:05:20 +00:00
|
|
|
instances_of_this_value += addend
|
2014-02-13 17:17:05 +00:00
|
|
|
known_values_for_column[value] = instances_of_this_value
|
|
|
|
|
|
|
|
def _column_headers_as_dict(self):
|
|
|
|
"""Returns all column headers as a dictionary."""
|
|
|
|
asdict = {}
|
|
|
|
for column_id, values_for_column in self._extra_column_tallies.iteritems():
|
|
|
|
column_header_factory = self.get_column_header_factory(column_id)
|
|
|
|
asdict[column_id] = column_header_factory.create_as_dict(
|
|
|
|
values_for_column)
|
|
|
|
return asdict
|
|
|
|
|
2014-07-09 13:19:20 +00:00
|
|
|
def as_dict(self, column_ids_in_order=None):
|
2014-02-13 17:17:05 +00:00
|
|
|
"""Returns a dictionary describing this package of ImagePairs.
|
|
|
|
|
|
|
|
Uses the KEY__* constants as keys.
|
2014-07-09 13:19:20 +00:00
|
|
|
|
2014-08-05 17:07:22 +00:00
|
|
|
Args:
|
2014-07-09 13:19:20 +00:00
|
|
|
column_ids_in_order: A list of all extracolumn IDs in the desired display
|
|
|
|
order. If unspecified, they will be displayed in alphabetical order.
|
|
|
|
If specified, this list must contain all the extracolumn IDs!
|
|
|
|
(It may contain extra column IDs; they will be ignored.)
|
2014-02-13 17:17:05 +00:00
|
|
|
"""
|
2014-07-09 13:19:20 +00:00
|
|
|
all_column_ids = set(self._extra_column_tallies.keys())
|
|
|
|
if column_ids_in_order == None:
|
|
|
|
column_ids_in_order = sorted(all_column_ids)
|
|
|
|
else:
|
|
|
|
# Make sure the caller listed all column IDs, and throw away any extras.
|
|
|
|
specified_column_ids = set(column_ids_in_order)
|
|
|
|
forgotten_column_ids = all_column_ids - specified_column_ids
|
|
|
|
assert not forgotten_column_ids, (
|
|
|
|
'column_ids_in_order %s missing these column_ids: %s' % (
|
|
|
|
column_ids_in_order, forgotten_column_ids))
|
|
|
|
column_ids_in_order = [c for c in column_ids_in_order
|
|
|
|
if c in all_column_ids]
|
|
|
|
|
2014-03-17 14:22:02 +00:00
|
|
|
key_description = KEY__IMAGESETS__FIELD__DESCRIPTION
|
|
|
|
key_base_url = KEY__IMAGESETS__FIELD__BASE_URL
|
2014-08-22 11:46:30 +00:00
|
|
|
if gs_utils.GSUtils.is_gs_url(self._imageA_base_url):
|
|
|
|
valueA_base_url = self._convert_gs_url_to_http_url(self._imageA_base_url)
|
|
|
|
else:
|
|
|
|
valueA_base_url = self._imageA_base_url
|
|
|
|
if gs_utils.GSUtils.is_gs_url(self._imageB_base_url):
|
|
|
|
valueB_base_url = self._convert_gs_url_to_http_url(self._imageB_base_url)
|
2014-08-05 17:07:22 +00:00
|
|
|
else:
|
2014-08-22 11:46:30 +00:00
|
|
|
valueB_base_url = self._imageB_base_url
|
2014-08-05 17:07:22 +00:00
|
|
|
|
|
|
|
# We've waited as long as we can to ask ImageDiffDB for details of the
|
|
|
|
# image diffs, so that it has time to compute them.
|
|
|
|
if self._image_pair_dicts == None:
|
|
|
|
self._image_pair_dicts = [ip.as_dict() for ip in self._image_pair_objects]
|
|
|
|
|
2014-02-13 17:17:05 +00:00
|
|
|
return {
|
2014-05-12 20:40:29 +00:00
|
|
|
KEY__ROOT__EXTRACOLUMNHEADERS: self._column_headers_as_dict(),
|
2014-07-09 13:19:20 +00:00
|
|
|
KEY__ROOT__EXTRACOLUMNORDER: column_ids_in_order,
|
2014-05-12 20:40:29 +00:00
|
|
|
KEY__ROOT__IMAGEPAIRS: self._image_pair_dicts,
|
|
|
|
KEY__ROOT__IMAGESETS: {
|
2014-03-17 14:22:02 +00:00
|
|
|
KEY__IMAGESETS__SET__IMAGE_A: {
|
|
|
|
key_description: self._descriptions[0],
|
2014-08-22 11:46:30 +00:00
|
|
|
key_base_url: valueA_base_url,
|
2014-03-17 14:22:02 +00:00
|
|
|
},
|
|
|
|
KEY__IMAGESETS__SET__IMAGE_B: {
|
|
|
|
key_description: self._descriptions[1],
|
2014-08-22 11:46:30 +00:00
|
|
|
key_base_url: valueB_base_url,
|
2014-03-17 14:22:02 +00:00
|
|
|
},
|
|
|
|
KEY__IMAGESETS__SET__DIFFS: {
|
|
|
|
key_description: 'color difference per channel',
|
|
|
|
key_base_url: posixpath.join(
|
2014-07-02 14:43:04 +00:00
|
|
|
self._diff_base_url, imagediffdb.RGBDIFFS_SUBDIR),
|
2014-03-17 14:22:02 +00:00
|
|
|
},
|
|
|
|
KEY__IMAGESETS__SET__WHITEDIFFS: {
|
|
|
|
key_description: 'differing pixels in white',
|
|
|
|
key_base_url: posixpath.join(
|
2014-07-02 14:43:04 +00:00
|
|
|
self._diff_base_url, imagediffdb.WHITEDIFFS_SUBDIR),
|
2014-03-17 14:22:02 +00:00
|
|
|
},
|
|
|
|
},
|
2014-02-13 17:17:05 +00:00
|
|
|
}
|
2014-08-05 17:07:22 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _convert_gs_url_to_http_url(gs_url):
|
|
|
|
"""Returns HTTP URL that can be used to download this Google Storage file.
|
|
|
|
|
|
|
|
TODO(epoger): Create functionality like this within gs_utils.py instead of
|
|
|
|
here? See https://codereview.chromium.org/428493005/ ('create
|
|
|
|
anyfile_utils.py for copying files between HTTP/GS/local filesystem')
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gs_url: "gs://bucket/path" format URL
|
|
|
|
"""
|
|
|
|
bucket, path = gs_utils.GSUtils.split_gs_url(gs_url)
|
|
|
|
http_url = 'http://storage.cloud.google.com/' + bucket
|
|
|
|
if path:
|
|
|
|
http_url += '/' + path
|
|
|
|
return http_url
|