• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2
3"""
4Copyright 2014 Google Inc.
5
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
8
9Compare results of two render_pictures runs.
10"""
11
12# System-level imports
13import logging
14import os
15import re
16import time
17
18# Imports from within Skia
19import fix_pythonpath  # must do this first
20from pyutils import url_utils
21import gm_json
22import imagediffdb
23import imagepair
24import imagepairset
25import results
26
27# URL under which all render_pictures images can be found in Google Storage.
28# TODO(epoger): Move this default value into
29# https://skia.googlesource.com/buildbot/+/master/site_config/global_variables.json
30DEFAULT_IMAGE_BASE_URL = 'http://chromium-skia-gm.commondatastorage.googleapis.com/render_pictures/images'
31
32
33class RenderedPicturesComparisons(results.BaseComparisons):
34  """Loads results from two different render_pictures runs into an ImagePairSet.
35  """
36
37  def __init__(self, subdirs, actuals_root,
38               generated_images_root=results.DEFAULT_GENERATED_IMAGES_ROOT,
39               image_base_url=DEFAULT_IMAGE_BASE_URL,
40               diff_base_url=None):
41    """
42    Args:
43      actuals_root: root directory containing all render_pictures-generated
44          JSON files
45      subdirs: (string, string) tuple; pair of subdirectories within
46          actuals_root to compare
47      generated_images_root: directory within which to create all pixel diffs;
48          if this directory does not yet exist, it will be created
49      image_base_url: URL under which all render_pictures result images can
50          be found; this will be used to read images for comparison within
51          this code, and included in the ImagePairSet so its consumers know
52          where to download the images from
53      diff_base_url: base URL within which the client should look for diff
54          images; if not specified, defaults to a "file:///" URL representation
55          of generated_images_root
56    """
57    time_start = int(time.time())
58    self._image_diff_db = imagediffdb.ImageDiffDB(generated_images_root)
59    self._image_base_url = image_base_url
60    self._diff_base_url = (
61        diff_base_url or
62        url_utils.create_filepath_url(generated_images_root))
63    self._load_result_pairs(actuals_root, subdirs)
64    self._timestamp = int(time.time())
65    logging.info('Results complete; took %d seconds.' %
66                 (self._timestamp - time_start))
67
68  def _load_result_pairs(self, actuals_root, subdirs):
69    """Loads all JSON files found within two subdirs in actuals_root,
70    compares across those two subdirs, and stores the summary in self._results.
71
72    Args:
73      actuals_root: root directory containing all render_pictures-generated
74          JSON files
75      subdirs: (string, string) tuple; pair of subdirectories within
76          actuals_root to compare
77    """
78    logging.info(
79        'Reading actual-results JSON files from %s subdirs within %s...' % (
80            subdirs, actuals_root))
81    subdirA, subdirB = subdirs
82    subdirA_dicts = self._read_dicts_from_root(
83        os.path.join(actuals_root, subdirA))
84    subdirB_dicts = self._read_dicts_from_root(
85        os.path.join(actuals_root, subdirB))
86    logging.info('Comparing subdirs %s and %s...' % (subdirA, subdirB))
87
88    all_image_pairs = imagepairset.ImagePairSet(
89        descriptions=subdirs,
90        diff_base_url=self._diff_base_url)
91    failing_image_pairs = imagepairset.ImagePairSet(
92        descriptions=subdirs,
93        diff_base_url=self._diff_base_url)
94
95    all_image_pairs.ensure_extra_column_values_in_summary(
96        column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[
97            results.KEY__RESULT_TYPE__FAILED,
98            results.KEY__RESULT_TYPE__NOCOMPARISON,
99            results.KEY__RESULT_TYPE__SUCCEEDED,
100        ])
101    failing_image_pairs.ensure_extra_column_values_in_summary(
102        column_id=results.KEY__EXTRACOLUMNS__RESULT_TYPE, values=[
103            results.KEY__RESULT_TYPE__FAILED,
104            results.KEY__RESULT_TYPE__NOCOMPARISON,
105        ])
106
107    common_dict_paths = sorted(set(subdirA_dicts.keys() + subdirB_dicts.keys()))
108    num_common_dict_paths = len(common_dict_paths)
109    dict_num = 0
110    for dict_path in common_dict_paths:
111      dict_num += 1
112      logging.info('Generating pixel diffs for dict #%d of %d, "%s"...' %
113                   (dict_num, num_common_dict_paths, dict_path))
114      dictA = subdirA_dicts[dict_path]
115      dictB = subdirB_dicts[dict_path]
116      self._validate_dict_version(dictA)
117      self._validate_dict_version(dictB)
118      dictA_results = dictA[gm_json.JSONKEY_ACTUALRESULTS]
119      dictB_results = dictB[gm_json.JSONKEY_ACTUALRESULTS]
120      skp_names = sorted(set(dictA_results.keys() + dictB_results.keys()))
121      for skp_name in skp_names:
122        imagepairs_for_this_skp = []
123
124        whole_image_A = RenderedPicturesComparisons.get_multilevel(
125            dictA_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE)
126        whole_image_B = RenderedPicturesComparisons.get_multilevel(
127            dictB_results, skp_name, gm_json.JSONKEY_SOURCE_WHOLEIMAGE)
128        imagepairs_for_this_skp.append(self._create_image_pair(
129            test=skp_name, config=gm_json.JSONKEY_SOURCE_WHOLEIMAGE,
130            image_dict_A=whole_image_A, image_dict_B=whole_image_B))
131
132        tiled_images_A = RenderedPicturesComparisons.get_multilevel(
133            dictA_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES)
134        tiled_images_B = RenderedPicturesComparisons.get_multilevel(
135            dictB_results, skp_name, gm_json.JSONKEY_SOURCE_TILEDIMAGES)
136        # TODO(epoger): Report an error if we find tiles for A but not B?
137        if tiled_images_A and tiled_images_B:
138          # TODO(epoger): Report an error if we find a different number of tiles
139          # for A and B?
140          num_tiles = len(tiled_images_A)
141          for tile_num in range(num_tiles):
142            imagepairs_for_this_skp.append(self._create_image_pair(
143                test=skp_name,
144                config='%s-%d' % (gm_json.JSONKEY_SOURCE_TILEDIMAGES, tile_num),
145                image_dict_A=tiled_images_A[tile_num],
146                image_dict_B=tiled_images_B[tile_num]))
147
148        for imagepair in imagepairs_for_this_skp:
149          if imagepair:
150            all_image_pairs.add_image_pair(imagepair)
151            result_type = imagepair.extra_columns_dict\
152                [results.KEY__EXTRACOLUMNS__RESULT_TYPE]
153            if result_type != results.KEY__RESULT_TYPE__SUCCEEDED:
154              failing_image_pairs.add_image_pair(imagepair)
155
156    self._results = {
157      results.KEY__HEADER__RESULTS_ALL: all_image_pairs.as_dict(),
158      results.KEY__HEADER__RESULTS_FAILURES: failing_image_pairs.as_dict(),
159    }
160
161  def _validate_dict_version(self, result_dict):
162    """Raises Exception if the dict is not the type/version we know how to read.
163
164    Args:
165      result_dict: dictionary holding output of render_pictures
166    """
167    expected_header_type = 'ChecksummedImages'
168    expected_header_revision = 1
169
170    header = result_dict[gm_json.JSONKEY_HEADER]
171    header_type = header[gm_json.JSONKEY_HEADER_TYPE]
172    if header_type != expected_header_type:
173      raise Exception('expected header_type "%s", but got "%s"' % (
174          expected_header_type, header_type))
175    header_revision = header[gm_json.JSONKEY_HEADER_REVISION]
176    if header_revision != expected_header_revision:
177      raise Exception('expected header_revision %d, but got %d' % (
178          expected_header_revision, header_revision))
179
180  def _create_image_pair(self, test, config, image_dict_A, image_dict_B):
181    """Creates an ImagePair object for this pair of images.
182
183    Args:
184      test: string; name of the test
185      config: string; name of the config
186      image_dict_A: dict with JSONKEY_IMAGE_* keys, or None if no image
187      image_dict_B: dict with JSONKEY_IMAGE_* keys, or None if no image
188
189    Returns:
190      An ImagePair object, or None if both image_dict_A and image_dict_B are
191      None.
192    """
193    if (not image_dict_A) and (not image_dict_B):
194      return None
195
196    def _checksum_and_relative_url(dic):
197      if dic:
198        return ((dic[gm_json.JSONKEY_IMAGE_CHECKSUMALGORITHM],
199                 dic[gm_json.JSONKEY_IMAGE_CHECKSUMVALUE]),
200                dic[gm_json.JSONKEY_IMAGE_FILEPATH])
201      else:
202        return None, None
203
204    imageA_checksum, imageA_relative_url = _checksum_and_relative_url(
205        image_dict_A)
206    imageB_checksum, imageB_relative_url = _checksum_and_relative_url(
207        image_dict_B)
208
209    if not imageA_checksum:
210      result_type = results.KEY__RESULT_TYPE__NOCOMPARISON
211    elif not imageB_checksum:
212      result_type = results.KEY__RESULT_TYPE__NOCOMPARISON
213    elif imageA_checksum == imageB_checksum:
214      result_type = results.KEY__RESULT_TYPE__SUCCEEDED
215    else:
216      result_type = results.KEY__RESULT_TYPE__FAILED
217
218    extra_columns_dict = {
219        results.KEY__EXTRACOLUMNS__CONFIG: config,
220        results.KEY__EXTRACOLUMNS__RESULT_TYPE: result_type,
221        results.KEY__EXTRACOLUMNS__TEST: test,
222        # TODO(epoger): Right now, the client UI crashes if it receives
223        # results that do not include this column.
224        # Until we fix that, keep the client happy.
225        results.KEY__EXTRACOLUMNS__BUILDER: 'TODO',
226    }
227
228    try:
229      return imagepair.ImagePair(
230          image_diff_db=self._image_diff_db,
231          base_url=self._image_base_url,
232          imageA_relative_url=imageA_relative_url,
233          imageB_relative_url=imageB_relative_url,
234          extra_columns=extra_columns_dict)
235    except (KeyError, TypeError):
236      logging.exception(
237          'got exception while creating ImagePair for'
238          ' test="%s", config="%s", urlPair=("%s","%s")' % (
239              test, config, imageA_relative_url, imageB_relative_url))
240      return None
241
242
243# TODO(epoger): Add main() so this can be called by vm_run_skia_try.sh
244