aboutsummaryrefslogtreecommitdiffhomepage
path: root/gm/rebaseline_server
diff options
context:
space:
mode:
authorGravatar commit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>2014-02-13 17:17:05 +0000
committerGravatar commit-bot@chromium.org <commit-bot@chromium.org@2bbb7eff-a529-9590-31e7-b0007b416f81>2014-02-13 17:17:05 +0000
commit536b15ffb05a20f3681ed72749a3bc09c5386c10 (patch)
treef51bb863e0764cd5088652f43fde63ac61fbbb81 /gm/rebaseline_server
parented3bb1a3a5deab5b6d465466b85d5c45db561ca5 (diff)
rebaseline_server: create ImagePairSet-- holds a number of ImagePairs to examine
See https://goto.google.com/ChangingRbsJson and bug 1919 for additional context BUG=skia:1919 NOTRY=True R=rmistry@google.com Author: epoger@google.com Review URL: https://codereview.chromium.org/139343018 git-svn-id: http://skia.googlecode.com/svn/trunk@13434 2bbb7eff-a529-9590-31e7-b0007b416f81
Diffstat (limited to 'gm/rebaseline_server')
-rw-r--r--gm/rebaseline_server/column.py64
-rw-r--r--gm/rebaseline_server/imagepair.py36
-rwxr-xr-xgm/rebaseline_server/imagepair_test.py10
-rw-r--r--gm/rebaseline_server/imagepairset.py122
-rwxr-xr-xgm/rebaseline_server/imagepairset_test.py173
5 files changed, 381 insertions, 24 deletions
diff --git a/gm/rebaseline_server/column.py b/gm/rebaseline_server/column.py
new file mode 100644
index 0000000000..7bce15a250
--- /dev/null
+++ b/gm/rebaseline_server/column.py
@@ -0,0 +1,64 @@
+#!/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.
+
+ColumnHeaderFactory class (see class docstring for details)
+"""
+
+# Keys used within dictionary representation of each column header.
+KEY__HEADER_TEXT = 'headerText'
+KEY__HEADER_URL = 'headerUrl'
+KEY__IS_FILTERABLE = 'isFilterable'
+KEY__IS_SORTABLE = 'isSortable'
+KEY__VALUES_AND_COUNTS = 'valuesAndCounts'
+
+
+class ColumnHeaderFactory(object):
+ """Factory which assembles the header for a single column of data."""
+
+ def __init__(self, header_text, header_url=None,
+ is_filterable=True, is_sortable=True,
+ include_values_and_counts=True):
+ """
+ Args:
+ header_text: string; text the client should display within column header.
+ header_url: string; target URL if user clicks on column header.
+ If None, nothing to click on.
+ is_filterable: boolean; whether client should allow filtering on this
+ column.
+ is_sortable: boolean; whether client should allow sorting on this column.
+ include_values_and_counts: boolean; whether the set of values found
+ within this column, and their counts, should be available for the
+ client to display.
+ """
+ self._header_text = header_text
+ self._header_url = header_url
+ self._is_filterable = is_filterable
+ self._is_sortable = is_sortable
+ self._include_values_and_counts = include_values_and_counts
+
+ def create_as_dict(self, values_and_counts_dict=None):
+ """Creates the header for this column, in dictionary form.
+
+ Creates the header for this column in dictionary form, as needed when
+ constructing the JSON representation. Uses the KEY__* constants as keys.
+
+ Args:
+ values_and_counts_dict: dictionary mapping each possible column value
+ to its count (how many entries in the column have this value), or
+ None if this information is not available.
+ """
+ asdict = {
+ KEY__HEADER_TEXT: self._header_text,
+ KEY__IS_FILTERABLE: self._is_filterable,
+ KEY__IS_SORTABLE: self._is_sortable,
+ }
+ if self._header_url:
+ asdict[KEY__HEADER_URL] = self._header_url
+ if self._include_values_and_counts and values_and_counts_dict:
+ asdict[KEY__VALUES_AND_COUNTS] = values_and_counts_dict
+ return asdict
diff --git a/gm/rebaseline_server/imagepair.py b/gm/rebaseline_server/imagepair.py
index 1c71bd989c..bba36fa8c4 100644
--- a/gm/rebaseline_server/imagepair.py
+++ b/gm/rebaseline_server/imagepair.py
@@ -12,18 +12,16 @@ ImagePair class (see class docstring for details)
import posixpath
# Keys used within ImagePair dictionary representations.
-KEY_DIFFERENCE_DATA = 'differenceData'
-KEY_EXPECTATIONS_DATA = 'expectationsData'
-KEY_EXTRA_COLUMN_VALUES = 'extraColumnValues'
-KEY_IMAGE_A_URL = 'imageAUrl'
-KEY_IMAGE_B_URL = 'imageBUrl'
-KEY_IS_DIFFERENT = 'isDifferent'
+KEY__DIFFERENCE_DATA = 'differenceData'
+KEY__EXPECTATIONS_DATA = 'expectations'
+KEY__EXTRA_COLUMN_VALUES = 'extraColumns'
+KEY__IMAGE_A_URL = 'imageAUrl'
+KEY__IMAGE_B_URL = 'imageBUrl'
+KEY__IS_DIFFERENT = 'isDifferent'
class ImagePair(object):
- """
- Describes a pair of images, along with optional metadata (pixel difference
- metrics, whether to ignore mismatches, etc.)
+ """Describes a pair of images, pixel difference info, and optional metadata.
"""
def __init__(self, image_diff_db,
@@ -63,21 +61,21 @@ class ImagePair(object):
actual_image_locator=imageB_relative_url)
def as_dict(self):
- """
- Return a dictionary describing this ImagePair, as needed when constructing
- the JSON representation. Uses the KEY_* constants as keys.
+ """Returns a dictionary describing this ImagePair.
+
+ Uses the KEY__* constants as keys.
"""
asdict = {
- KEY_IMAGE_A_URL: self.imageA_relative_url,
- KEY_IMAGE_B_URL: self.imageB_relative_url,
+ KEY__IMAGE_A_URL: self.imageA_relative_url,
+ KEY__IMAGE_B_URL: self.imageB_relative_url,
}
if self.expectations_dict:
- asdict[KEY_EXPECTATIONS_DATA] = self.expectations_dict
+ asdict[KEY__EXPECTATIONS_DATA] = self.expectations_dict
if self.extra_columns_dict:
- asdict[KEY_EXTRA_COLUMN_VALUES] = self.extra_columns_dict
+ asdict[KEY__EXTRA_COLUMN_VALUES] = self.extra_columns_dict
if self.diff_record and (self.diff_record.get_num_pixels_differing() > 0):
- asdict[KEY_IS_DIFFERENT] = True
- asdict[KEY_DIFFERENCE_DATA] = self.diff_record.as_dict()
+ asdict[KEY__IS_DIFFERENT] = True
+ asdict[KEY__DIFFERENCE_DATA] = self.diff_record.as_dict()
else:
- asdict[KEY_IS_DIFFERENT] = False
+ asdict[KEY__IS_DIFFERENT] = False
return asdict
diff --git a/gm/rebaseline_server/imagepair_test.py b/gm/rebaseline_server/imagepair_test.py
index fc1f2759e7..d29438e530 100755
--- a/gm/rebaseline_server/imagepair_test.py
+++ b/gm/rebaseline_server/imagepair_test.py
@@ -32,11 +32,11 @@ class ImagePairTest(unittest.TestCase):
shutil.rmtree(self._temp_dir)
def shortDescription(self):
- """Tell unittest framework to not print docstrings for test cases."""
+ """Tells unittest framework to not print docstrings for test cases."""
return None
def test_endToEnd(self):
- """Test ImagePair, using a real ImageDiffDB to download real images.
+ """Tests ImagePair, using a real ImageDiffDB to download real images.
TODO(epoger): Either in addition to or instead of this end-to-end test,
we should perform some tests using either:
@@ -65,7 +65,7 @@ class ImagePairTest(unittest.TestCase):
},
# expected output:
{
- 'extraColumnValues': {
+ 'extraColumns': {
'builder': 'MyBuilder',
'test': 'MyTest',
},
@@ -115,11 +115,11 @@ class ImagePairTest(unittest.TestCase):
'percentDifferingPixels': 100.00,
'weightedDiffMeasure': 66.66666666666667,
},
- 'expectationsData': {
+ 'expectations': {
'bugs': [1001, 1002],
'ignoreFailure': True,
},
- 'extraColumnValues': {
+ 'extraColumns': {
'builder': 'MyBuilder',
'test': 'MyTest',
},
diff --git a/gm/rebaseline_server/imagepairset.py b/gm/rebaseline_server/imagepairset.py
new file mode 100644
index 0000000000..2e173f537f
--- /dev/null
+++ b/gm/rebaseline_server/imagepairset.py
@@ -0,0 +1,122 @@
+#!/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.
+"""
+
+import column
+
+# Keys used within dictionary representation of ImagePairSet.
+KEY__COLUMNHEADERS = 'columnHeaders'
+KEY__IMAGEPAIRS = 'imagePairs'
+KEY__IMAGESETS = 'imageSets'
+KEY__IMAGESETS__BASE_URL = 'baseUrl'
+KEY__IMAGESETS__DESCRIPTION = 'description'
+
+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.
+ """
+
+ def __init__(self, descriptions=None):
+ """
+ Args:
+ 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
+ self._image_pair_dicts = []
+
+ 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...
+ if not self._image_pair_dicts:
+ self._base_url = image_pair.base_url
+
+ if image_pair.base_url != self._base_url:
+ raise Exception('added ImagePair with base_url "%s" instead of "%s"' % (
+ image_pair.base_url, self._base_url))
+ self._image_pair_dicts.append(image_pair.as_dict())
+ extra_columns_dict = image_pair.extra_columns_dict
+ if extra_columns_dict:
+ for column_id, value in extra_columns_dict.iteritems():
+ self._add_extra_column_entry(column_id, value)
+
+ 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
+
+ def _add_extra_column_entry(self, column_id, value):
+ """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).
+ """
+ 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)
+ instances_of_this_value += 1
+ 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
+
+ def as_dict(self):
+ """Returns a dictionary describing this package of ImagePairs.
+
+ Uses the KEY__* constants as keys.
+ """
+ return {
+ KEY__COLUMNHEADERS: self._column_headers_as_dict(),
+ KEY__IMAGEPAIRS: self._image_pair_dicts,
+ KEY__IMAGESETS: [{
+ KEY__IMAGESETS__BASE_URL: self._base_url,
+ KEY__IMAGESETS__DESCRIPTION: self._descriptions[0],
+ }, {
+ KEY__IMAGESETS__BASE_URL: self._base_url,
+ KEY__IMAGESETS__DESCRIPTION: self._descriptions[1],
+ }],
+ }
diff --git a/gm/rebaseline_server/imagepairset_test.py b/gm/rebaseline_server/imagepairset_test.py
new file mode 100755
index 0000000000..8f1edfce40
--- /dev/null
+++ b/gm/rebaseline_server/imagepairset_test.py
@@ -0,0 +1,173 @@
+#!/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.
+
+Test imagepairset.py
+"""
+
+# System-level imports
+import unittest
+
+# Local imports
+import column
+import imagepair
+import imagepairset
+
+
+BASE_URL_1 = 'http://base/url/1'
+BASE_URL_2 = 'http://base/url/2'
+IMAGEPAIR_1_AS_DICT = {
+ imagepair.KEY__EXTRA_COLUMN_VALUES: {
+ 'builder': 'MyBuilder',
+ 'test': 'test1',
+ },
+ imagepair.KEY__IMAGE_A_URL: 'test1/1111.png',
+ imagepair.KEY__IMAGE_B_URL: 'test1/1111.png',
+ imagepair.KEY__IS_DIFFERENT: False,
+}
+IMAGEPAIR_2_AS_DICT = {
+ imagepair.KEY__DIFFERENCE_DATA: {
+ 'maxDiffPerChannel': [1, 2, 3],
+ 'numDifferingPixels': 111,
+ 'percentDifferingPixels': 22.222,
+ 'weightedDiffMeasure': 33.333,
+ },
+ imagepair.KEY__EXTRA_COLUMN_VALUES: {
+ 'builder': 'MyBuilder',
+ 'test': 'test2',
+ },
+ imagepair.KEY__IMAGE_A_URL: 'test2/2222.png',
+ imagepair.KEY__IMAGE_B_URL: 'test2/22223.png',
+ imagepair.KEY__IS_DIFFERENT: True,
+}
+IMAGEPAIR_3_AS_DICT = {
+ imagepair.KEY__DIFFERENCE_DATA: {
+ 'maxDiffPerChannel': [4, 5, 6],
+ 'numDifferingPixels': 111,
+ 'percentDifferingPixels': 44.444,
+ 'weightedDiffMeasure': 33.333,
+ },
+ imagepair.KEY__EXPECTATIONS_DATA: {
+ 'bugs': [1001, 1002],
+ 'ignoreFailure': True,
+ },
+ imagepair.KEY__EXTRA_COLUMN_VALUES: {
+ 'builder': 'MyBuilder',
+ 'test': 'test3',
+ },
+ imagepair.KEY__IMAGE_A_URL: 'test3/3333.png',
+ imagepair.KEY__IMAGE_B_URL: 'test3/33334.png',
+ imagepair.KEY__IS_DIFFERENT: True,
+}
+SET_A_DESCRIPTION = 'expectations'
+SET_B_DESCRIPTION = 'actuals'
+
+
+class ImagePairSetTest(unittest.TestCase):
+
+ def setUp(self):
+ self.maxDiff = None # do not truncate diffs when tests fail
+
+ def shortDescription(self):
+ """Tells unittest framework to not print docstrings for test cases."""
+ return None
+
+ def test_success(self):
+ """Assembles some ImagePairs into an ImagePairSet, and validates results.
+ """
+ image_pairs = [
+ MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_1_AS_DICT),
+ MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_2_AS_DICT),
+ MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_3_AS_DICT),
+ ]
+ expected_imageset_dict = {
+ 'columnHeaders': {
+ 'builder': {
+ 'headerText': 'builder',
+ 'isFilterable': True,
+ 'isSortable': True,
+ 'valuesAndCounts': {
+ 'MyBuilder': 3
+ },
+ },
+ 'test': {
+ 'headerText': 'which GM test',
+ 'headerUrl': 'http://learn/about/gm/tests',
+ 'isFilterable': True,
+ 'isSortable': False,
+ },
+ },
+ 'imagePairs': [
+ IMAGEPAIR_1_AS_DICT,
+ IMAGEPAIR_2_AS_DICT,
+ IMAGEPAIR_3_AS_DICT,
+ ],
+ 'imageSets': [
+ {
+ 'baseUrl': BASE_URL_1,
+ 'description': SET_A_DESCRIPTION,
+ },
+ {
+ 'baseUrl': BASE_URL_1,
+ 'description': SET_B_DESCRIPTION,
+ },
+ ],
+ }
+
+ image_pair_set = imagepairset.ImagePairSet(
+ descriptions=(SET_A_DESCRIPTION, SET_B_DESCRIPTION))
+ for image_pair in image_pairs:
+ image_pair_set.add_image_pair(image_pair)
+ # The 'builder' column header uses the default settings,
+ # but the 'test' column header has manual adjustments.
+ image_pair_set.set_column_header_factory(
+ 'test',
+ column.ColumnHeaderFactory(
+ header_text='which GM test',
+ header_url='http://learn/about/gm/tests',
+ is_filterable=True,
+ is_sortable=False,
+ include_values_and_counts=False))
+ self.assertEqual(image_pair_set.as_dict(), expected_imageset_dict)
+
+ def test_mismatched_base_url(self):
+ """Confirms that mismatched base_urls will cause an exception."""
+ image_pair_set = imagepairset.ImagePairSet()
+ image_pair_set.add_image_pair(
+ MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_1_AS_DICT))
+ image_pair_set.add_image_pair(
+ MockImagePair(base_url=BASE_URL_1, dict_to_return=IMAGEPAIR_2_AS_DICT))
+ with self.assertRaises(Exception):
+ image_pair_set.add_image_pair(
+ MockImagePair(base_url=BASE_URL_2,
+ dict_to_return=IMAGEPAIR_3_AS_DICT))
+
+
+class MockImagePair(object):
+ """Mock ImagePair object, which will return canned results."""
+ def __init__(self, base_url, dict_to_return):
+ """
+ Args:
+ base_url: base_url attribute for this object
+ dict_to_return: dictionary to return from as_dict()
+ """
+ self.base_url = base_url
+ self.extra_columns_dict = dict_to_return.get(
+ imagepair.KEY__EXTRA_COLUMN_VALUES, None)
+ self._dict_to_return = dict_to_return
+
+ def as_dict(self):
+ return self._dict_to_return
+
+
+def main():
+ suite = unittest.TestLoader().loadTestsFromTestCase(ImagePairSetTest)
+ unittest.TextTestRunner(verbosity=2).run(suite)
+
+
+if __name__ == '__main__':
+ main()