aboutsummaryrefslogtreecommitdiffhomepage
path: root/gm/rebaseline_server/writable_expectations.py
blob: 09b9cf76896906786b2e86861201778c437f26ca (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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/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.

Expectations on local disk that we can modify.
"""

# System-level imports
import logging
import os
import re

# Must fix up PYTHONPATH before importing from within Skia
import rs_fixpypath  # pylint: disable=W0611

# Imports from within Skia
from py.utils import git_utils
import compare_rendered_pictures
import gm_json
import imagepair
import results

FILEPATH_RE = re.compile('.+/' + gm_json.IMAGE_FILENAME_PATTERN)

SKIA_REPO = os.path.abspath(os.path.join(
    os.path.dirname(__file__), os.pardir, os.pardir, '.git'))


class WritableExpectations(git_utils.NewGitCheckout):
  """Expectations on local disk that we can modify."""

  def __init__(self, set_descriptions):
    """Creates a sandbox on local disk containing writable expectations.

    You must use the 'with' statement to create this object in such a way that
    it cleans up after itself:

    with WritableExpectations(*args) as writable_expectations:
      # make modifications
      # use the modified results
    # the sandbox on local disk is automatically cleaned up here

    Args:
      set_descriptions: SET_DESCRIPTIONS dict describing the set we want to
          update expectations within; this tells us the subdirectory within the
          Skia repo where we keep these expectations, and the commithash at
          which the user evaluated new baselines.
    """
    file_section = set_descriptions[results.KEY__SET_DESCRIPTIONS__SECTION]
    assert file_section == gm_json.JSONKEY_EXPECTEDRESULTS

    source_dir = _unicode_to_ascii(
        set_descriptions[results.KEY__SET_DESCRIPTIONS__DIR])
    assert source_dir.startswith(compare_rendered_pictures.REPO_URL_PREFIX)
    repo_subdir = source_dir[len(compare_rendered_pictures.REPO_URL_PREFIX):]
    repo_revision = _unicode_to_ascii(
        set_descriptions[results.KEY__SET_DESCRIPTIONS__REPO_REVISION])

    logging.info('Creating a writable Skia checkout at revision "%s"...' %
                 repo_revision)
    super(WritableExpectations, self).__init__(
        repository=SKIA_REPO, commit=repo_revision, subdir=repo_subdir)

  def modify(self, modifications):
    """Modify the contents of the checkout, using modifications from the UI.

    Args:
      modifications: data[KEY__LIVE_EDITS__MODIFICATIONS] coming back from the
          rebaseline_server UI frontend
    """
    logging.info('Reading in dicts from writable Skia checkout in %s ...' %
                 self.root)
    dicts = results.BaseComparisons.read_dicts_from_root(self.root)

    # Make sure we have expected-results sections in all our output dicts.
    for pathname, adict in dicts.iteritems():
      if not adict:
        adict = {
          # TODO(stephana): These values should be defined as constants
          # somewhere, to be kept in sync between this file and
          # compare_rendered_pictures.py.
          gm_json.JSONKEY_HEADER: {
            gm_json.JSONKEY_HEADER_TYPE: 'ChecksummedImages',
            gm_json.JSONKEY_HEADER_REVISION: 1,
          }
        }
      if not adict.get(gm_json.JSONKEY_EXPECTEDRESULTS, None):
        adict[gm_json.JSONKEY_EXPECTEDRESULTS] = {}
      dicts[pathname] = adict

    for modification in modifications:
      expectations = modification[imagepair.KEY__IMAGEPAIRS__EXPECTATIONS]
      _add_image_info_to_expectations(
          expectations=expectations,
          filepath=modification[imagepair.KEY__IMAGEPAIRS__IMAGE_B_URL])
      extra_columns = modification[imagepair.KEY__IMAGEPAIRS__EXTRACOLUMNS]
      dictname = modification[imagepair.KEY__IMAGEPAIRS__SOURCE_JSON_FILE]
      dict_to_modify = dicts[dictname][gm_json.JSONKEY_EXPECTEDRESULTS]
      test_name = extra_columns[compare_rendered_pictures.COLUMN__SOURCE_SKP]
      test_record = dict_to_modify.get(test_name, {})
      if (extra_columns[compare_rendered_pictures.COLUMN__TILED_OR_WHOLE] ==
          compare_rendered_pictures.COLUMN__TILED_OR_WHOLE__TILED):
        test_tiles_list = test_record.get(
            gm_json.JSONKEY_SOURCE_TILEDIMAGES, [])
        tilenum = int(extra_columns[compare_rendered_pictures.COLUMN__TILENUM])
        _replace_list_item(test_tiles_list, tilenum, expectations)
        test_record[gm_json.JSONKEY_SOURCE_TILEDIMAGES] = test_tiles_list
      else:
        test_record[gm_json.JSONKEY_SOURCE_WHOLEIMAGE] = expectations
      dict_to_modify[test_name] = test_record

    # Write the modified files back to disk.
    self._write_dicts_to_root(meta_dict=dicts, root=self.root)

  def get_diffs(self):
    """Return patchfile describing any modifications to this checkout."""
    return self._run_in_git_root(args=[git_utils.GIT, 'diff'])

  @staticmethod
  def _write_dicts_to_root(meta_dict, root):
    """Write out multiple dictionaries in JSON format.

    Args:
      meta_dict: a builder-keyed meta-dictionary containing all the JSON
                 dictionaries we want to write out
      root: path to root of directory tree within which to write files
    """
    if not os.path.isdir(root):
      raise IOError('no directory found at path %s' % root)

    for rel_path in meta_dict.keys():
      full_path = os.path.join(root, rel_path)
      gm_json.WriteToFile(meta_dict[rel_path], full_path)


def _unicode_to_ascii(unicode_string):
  """Returns the plain ASCII form of a unicode string.

  TODO(stephana): We created this because we get unicode strings out of the
  JSON file, while the git filenames and revision tags are plain ASCII.
  There may be a better way to handle this... maybe set the JSON util to just
  return ASCII strings?
  """
  return unicode_string.encode('ascii', 'ignore')


def _replace_list_item(a_list, index, value):
  """Replaces value at index "index" within a_list.

  Args:
    a_list: a list
    index: index indicating which item in a_list to replace
    value: value to set a_list[index] to

  If a_list does not contain this index, it will be extended with None entries
  to that length.
  """
  length = len(a_list)
  while index >= length:
    a_list.append(None)
    length += 1
  a_list[index] = value


def _add_image_info_to_expectations(expectations, filepath):
  """Add JSONKEY_IMAGE_* info to an existing expectations dictionary.

  TODO(stephana): This assumes that the checksumAlgorithm and checksumValue
  can be derived from the filepath, which is currently true but may not always
  be true.

  Args:
    expectations: the expectations dict to augment
    filepath: relative path to the image file
  """
  (checksum_algorithm, checksum_value) = FILEPATH_RE.match(filepath).groups()
  expectations[gm_json.JSONKEY_IMAGE_CHECKSUMALGORITHM] = checksum_algorithm
  expectations[gm_json.JSONKEY_IMAGE_CHECKSUMVALUE] = checksum_value
  expectations[gm_json.JSONKEY_IMAGE_FILEPATH] = filepath