158190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger#!/usr/bin/python
258190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger# -*- coding: utf-8 -*-
358190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
458190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenbergerfrom __future__ import print_function
5e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport argparse
658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenbergerimport BaseHTTPServer
7e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport json
858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenbergerimport os
958190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenbergerimport os.path
10e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport re
11e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport subprocess
12e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport sys
13e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport tempfile
14e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport urllib2
15e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
16e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger# Grab the script path because that is where all the static assets are
17e27eefc4844477cee5d32f51ab45ff62020cdb36Derek SollenbergerSCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
18e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
19e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger# Find the tools directory for python imports
20e27eefc4844477cee5d32f51ab45ff62020cdb36Derek SollenbergerTOOLS_DIR = os.path.dirname(SCRIPT_DIR)
21e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
22e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger# Find the root of the skia trunk for finding skpdiff binary
23e27eefc4844477cee5d32f51ab45ff62020cdb36Derek SollenbergerSKIA_ROOT_DIR = os.path.dirname(TOOLS_DIR)
24e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
25e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger# Find the default location of gm expectations
26e27eefc4844477cee5d32f51ab45ff62020cdb36Derek SollenbergerDEFAULT_GM_EXPECTATIONS_DIR = os.path.join(SKIA_ROOT_DIR, 'expectations', 'gm')
27e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
28e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger# Imports from within Skia
29e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerif TOOLS_DIR not in sys.path:
30e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    sys.path.append(TOOLS_DIR)
31e27eefc4844477cee5d32f51ab45ff62020cdb36Derek SollenbergerGM_DIR = os.path.join(SKIA_ROOT_DIR, 'gm')
32e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerif GM_DIR not in sys.path:
33e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    sys.path.append(GM_DIR)
34e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport gm_json
35e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerimport jsondiff
3658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
3758190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger# A simple dictionary of file name extensions to MIME types. The empty string
3858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger# entry is used as the default when no extension was given or if the extension
3958190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger# has no entry in this dictionary.
4058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek SollenbergerMIME_TYPE_MAP = {'': 'application/octet-stream',
4158190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger                 'html': 'text/html',
4258190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger                 'css': 'text/css',
4358190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger                 'png': 'image/png',
44e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                 'js': 'application/javascript',
45e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                 'json': 'application/json'
4658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger                 }
4758190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
4858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
49e27eefc4844477cee5d32f51ab45ff62020cdb36Derek SollenbergerIMAGE_FILENAME_RE = re.compile(gm_json.IMAGE_FILENAME_PATTERN)
50e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
51e27eefc4844477cee5d32f51ab45ff62020cdb36Derek SollenbergerSKPDIFF_INVOKE_FORMAT = '{} --jsonp=false -o {} -f {} {}'
52e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
53e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
54e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef get_skpdiff_path(user_path=None):
55e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """Find the skpdiff binary.
56e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
57e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param user_path If none, searches in Release and Debug out directories of
58e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger           the skia root. If set, checks that the path is a real file and
59e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger           returns it.
60e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """
61e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    skpdiff_path = None
62e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    possible_paths = []
63e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
64e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # Use the user given path, or try out some good default paths.
65e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    if user_path:
66e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        possible_paths.append(user_path)
67e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    else:
68e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        possible_paths.append(os.path.join(SKIA_ROOT_DIR, 'out',
69e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                           'Release', 'skpdiff'))
70e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        possible_paths.append(os.path.join(SKIA_ROOT_DIR, 'out',
710a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger                                           'Release', 'skpdiff.exe'))
720a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger        possible_paths.append(os.path.join(SKIA_ROOT_DIR, 'out',
73e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                           'Debug', 'skpdiff'))
740a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger        possible_paths.append(os.path.join(SKIA_ROOT_DIR, 'out',
750a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger                                           'Debug', 'skpdiff.exe'))
76e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # Use the first path that actually points to the binary
77e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    for possible_path in possible_paths:
78e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        if os.path.isfile(possible_path):
79e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            skpdiff_path = possible_path
80e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            break
81e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
82e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # If skpdiff was not found, print out diagnostic info for the user.
83e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    if skpdiff_path is None:
84e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        print('Could not find skpdiff binary. Either build it into the ' +
85e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger              'default directory, or specify the path on the command line.')
86e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        print('skpdiff paths tried:')
87e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        for possible_path in possible_paths:
88e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            print('   ', possible_path)
89e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    return skpdiff_path
90e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
91e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
92e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef download_file(url, output_path):
93e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """Download the file at url and place it in output_path"""
94e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    reader = urllib2.urlopen(url)
95e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    with open(output_path, 'wb') as writer:
96e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        writer.write(reader.read())
97e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
98e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
99e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef download_gm_image(image_name, image_path, hash_val):
100e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """Download the gm result into the given path.
101e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
102e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param image_name The GM file name, for example imageblur_gpu.png.
103e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param image_path Path to place the image.
104e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param hash_val   The hash value of the image.
105e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """
106e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    if hash_val is None:
107e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        return
108e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
109e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # Separate the test name from a image name
110e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    image_match = IMAGE_FILENAME_RE.match(image_name)
111e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    test_name = image_match.group(1)
112e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
113e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # Calculate the URL of the requested image
114e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    image_url = gm_json.CreateGmActualUrl(
115e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        test_name, gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5, hash_val)
116e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
117e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # Download the image as requested
118e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    download_file(image_url, image_path)
119e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
120e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
121e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef get_image_set_from_skpdiff(skpdiff_records):
122e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """Get the set of all images references in the given records.
123e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
124e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param skpdiff_records An array of records, which are dictionary objects.
125e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """
126e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    expected_set = frozenset([r['baselinePath'] for r in skpdiff_records])
127e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    actual_set = frozenset([r['testPath'] for r in skpdiff_records])
128e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    return expected_set | actual_set
129e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
130e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
131e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef set_expected_hash_in_json(expected_results_json, image_name, hash_value):
132e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """Set the expected hash for the object extracted from
133e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    expected-results.json. Note that this only work with bitmap-64bitMD5 hash
134e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    types.
135e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
136e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param expected_results_json The Python dictionary with the results to
137e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    modify.
138e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param image_name            The name of the image to set the hash of.
139e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param hash_value            The hash to set for the image.
140e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """
141e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    expected_results = expected_results_json[gm_json.JSONKEY_EXPECTEDRESULTS]
142e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
143e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    if image_name in expected_results:
144e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        expected_results[image_name][gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS][0][1] = hash_value
145e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    else:
146e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        expected_results[image_name] = {
147e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            gm_json.JSONKEY_EXPECTEDRESULTS_ALLOWEDDIGESTS:
148e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            [
149e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                [
150e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    gm_json.JSONKEY_HASHTYPE_BITMAP_64BITMD5,
151e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    hash_value
152e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                ]
153e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            ]
154e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        }
155e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
156e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
157e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef get_head_version(path):
158e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """Get the version of the file at the given path stored inside the HEAD of
159e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    the git repository. It is returned as a string.
160e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
161e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    @param path The path of the file whose HEAD is returned. It is assumed the
162e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    path is inside a git repo rooted at SKIA_ROOT_DIR.
163e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """
164e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
165e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # git-show will not work with absolute paths. This ensures we give it a path
1660a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger    # relative to the skia root. This path also has to use forward slashes, even
1670a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger    # on windows.
1680a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger    git_path = os.path.relpath(path, SKIA_ROOT_DIR).replace('\\', '/')
169e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    git_show_proc = subprocess.Popen(['git', 'show', 'HEAD:' + git_path],
170e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                     stdout=subprocess.PIPE)
171e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
172e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # When invoked outside a shell, git will output the last committed version
173e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # of the file directly to stdout.
174e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    git_version_content, _ = git_show_proc.communicate()
175e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    return git_version_content
176e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
177e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
178e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerclass GMInstance:
179e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """Information about a GM test result on a specific device:
180e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger     - device_name = the name of the device that rendered it
181e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger     - image_name = the GM test name and config
182e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger     - expected_hash = the current expected hash value
183e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger     - actual_hash = the actual hash value
184e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger     - is_rebaselined = True if actual_hash is what is currently in the expected
185e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        results file, False otherwise.
186e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    """
187e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def __init__(self,
188e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                 device_name, image_name,
189e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                 expected_hash, actual_hash,
190e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                 is_rebaselined):
191e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.device_name = device_name
192e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.image_name = image_name
193e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.expected_hash = expected_hash
194e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.actual_hash = actual_hash
195e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.is_rebaselined = is_rebaselined
196e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
197e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
198e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerclass ExpectationsManager:
199e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def __init__(self, expectations_dir, expected_name, updated_name,
200e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                 skpdiff_path):
201e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """
202e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param expectations_dir   The directory to traverse for results files.
203e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger               This should resemble expectations/gm in the Skia trunk.
204e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param expected_name      The name of the expected result files. These
205e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger               are in the format of expected-results.json.
206e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param updated_name       The name of the updated expected result files.
207e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger               Normally this matches --expectations-filename-output for the
208e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger               rebaseline.py tool.
209e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param skpdiff_path       The path used to execute the skpdiff command.
210e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """
211e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._expectations_dir = expectations_dir
212e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._expected_name = expected_name
213e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._updated_name = updated_name
214e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._skpdiff_path = skpdiff_path
215e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._generate_gm_comparison()
216e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
217e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def _generate_gm_comparison(self):
218e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """Generate all the data needed to compare GMs:
219e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger           - determine which GMs changed
220e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger           - download the changed images
221e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger           - compare them with skpdiff
222e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """
223e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
224e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Get the expectations and compare them with actual hashes
225e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._get_expectations()
226e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
227e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
228e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Create a temporary file tree that makes sense for skpdiff to operate
2290a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger        # on. We take the realpath of the new temp directory because some OSs
2300a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger        # (*cough* osx) put the temp directory behind a symlink that gets
2310a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger        # resolved later down the pipeline and breaks the image map.
2320a657bbc2c6fc9daf699942e023050536d5ec95fDerek Sollenberger        image_output_dir = os.path.realpath(tempfile.mkdtemp('skpdiff'))
233e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        expected_image_dir = os.path.join(image_output_dir, 'expected')
234e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        actual_image_dir = os.path.join(image_output_dir, 'actual')
235e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        os.mkdir(expected_image_dir)
236e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        os.mkdir(actual_image_dir)
237e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
238e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Download expected and actual images that differed into the temporary
239e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # file tree.
240e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._download_expectation_images(expected_image_dir, actual_image_dir)
241e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
242e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Invoke skpdiff with our downloaded images and place its results in the
243e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # temporary directory.
244e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._skpdiff_output_path = os.path.join(image_output_dir,
245e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                                'skpdiff_output.json')
246e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        skpdiff_cmd = SKPDIFF_INVOKE_FORMAT.format(self._skpdiff_path,
247e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                                   self._skpdiff_output_path,
248e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                                   expected_image_dir,
249e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                                   actual_image_dir)
250e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        os.system(skpdiff_cmd)
251e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._load_skpdiff_output()
252e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
253e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
254e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def _get_expectations(self):
255e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """Fills self._expectations with GMInstance objects for each test whose
256e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        expectation is different between the following two files:
257e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger         - the local filesystem's updated results file
258e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger         - git's head version of the expected results file
259e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """
260e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        differ = jsondiff.GMDiffer()
261e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._expectations = []
262e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        for root, dirs, files in os.walk(self._expectations_dir):
263e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            for expectation_file in files:
264e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # There are many files in the expectations directory. We only
265e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # care about expected results.
266e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                if expectation_file != self._expected_name:
267e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    continue
268e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
269e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # Get the name of the results file, and be sure there is an
270e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # updated result to compare against. If there is not, there is
271e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # no point in diffing this device.
272e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                expected_file_path = os.path.join(root, self._expected_name)
273e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                updated_file_path = os.path.join(root, self._updated_name)
274e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                if not os.path.isfile(updated_file_path):
275e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    continue
276e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
277e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # Always get the expected results from git because we may have
278e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # changed them in a previous instance of the server.
279e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                expected_contents = get_head_version(expected_file_path)
280e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                updated_contents = None
281e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                with open(updated_file_path, 'rb') as updated_file:
282e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    updated_contents = updated_file.read()
283e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
284e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # Read the expected results on disk to determine what we've
285e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # already rebaselined.
286e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                commited_contents = None
287e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                with open(expected_file_path, 'rb') as expected_file:
288e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    commited_contents = expected_file.read()
289e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
290e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # Find all expectations that did not match.
291e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                expected_diff = differ.GenerateDiffDictFromStrings(
292e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    expected_contents,
293e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    updated_contents)
294e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
295e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # Generate a set of images that have already been rebaselined
296e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # onto disk.
297e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                rebaselined_diff = differ.GenerateDiffDictFromStrings(
298e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    expected_contents,
299e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    commited_contents)
300e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
301e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                rebaselined_set = set(rebaselined_diff.keys())
302e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
303e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # The name of the device corresponds to the name of the folder
304e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # we are in.
305e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                device_name = os.path.basename(root)
306e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
307e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                # Store old and new versions of the expectation for each GM
308e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                for image_name, hashes in expected_diff.iteritems():
309e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                    self._expectations.append(
310e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        GMInstance(device_name, image_name,
311e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                   hashes['old'], hashes['new'],
312e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                   image_name in rebaselined_set))
313e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
314e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def _load_skpdiff_output(self):
315e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """Loads the results of skpdiff and annotates them with whether they
316e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        have already been rebaselined or not. The resulting data is store in
317e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.skpdiff_records."""
318e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.skpdiff_records = None
319e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        with open(self._skpdiff_output_path, 'rb') as skpdiff_output_file:
320e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.skpdiff_records = json.load(skpdiff_output_file)['records']
321e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            for record in self.skpdiff_records:
322e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                record['isRebaselined'] = self.image_map[record['baselinePath']][1].is_rebaselined
323e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
324e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
325e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def _download_expectation_images(self, expected_image_dir, actual_image_dir):
326e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """Download the expected and actual images for the _expectations array.
327e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
328e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param expected_image_dir The directory to download expected images
329e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger               into.
330e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param actual_image_dir   The directory to download actual images into.
331e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """
332e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        image_map = {}
333e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
334e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Look through expectations and download their images.
335e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        for expectation in self._expectations:
336e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # Build appropriate paths to download the images into.
337e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            expected_image_path = os.path.join(expected_image_dir,
338e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                               expectation.device_name + '-' +
339e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                               expectation.image_name)
340e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
341e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            actual_image_path = os.path.join(actual_image_dir,
342e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                             expectation.device_name + '-' +
343e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                             expectation.image_name)
344e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
345e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            print('Downloading %s for device %s' % (
346e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                expectation.image_name, expectation.device_name))
347e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
348e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # Download images
349e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            download_gm_image(expectation.image_name,
350e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                              expected_image_path,
351e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                              expectation.expected_hash)
352e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
353e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            download_gm_image(expectation.image_name,
354e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                              actual_image_path,
355e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                              expectation.actual_hash)
356e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
357e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # Annotate the expectations with where the images were downloaded
358e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # to.
359e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            expectation.expected_image_path = expected_image_path
360e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            expectation.actual_image_path = actual_image_path
361e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
362e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # Map the image paths back to the expectations.
363e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            image_map[expected_image_path] = (False, expectation)
364e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            image_map[actual_image_path] = (True, expectation)
365e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
366e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.image_map = image_map
367e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
368e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def _set_expected_hash(self, device_name, image_name, hash_value):
369e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """Set the expected hash for the image of the given device. This always
370e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        writes directly to the expected results file of the given device
371e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
372e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param device_name The name of the device to write the hash to.
373e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param image_name  The name of the image whose hash to set.
374e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param hash_value  The value of the hash to set.
375e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """
376e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
377e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Retrieve the expected results file as it is in the working tree
378e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        json_path = os.path.join(self._expectations_dir, device_name,
379e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                 self._expected_name)
380e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        expectations = gm_json.LoadFromFile(json_path)
381e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
382e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Set the specified hash.
383e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        set_expected_hash_in_json(expectations, image_name, hash_value)
384e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
385e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Write it out to disk using gm_json to keep the formatting consistent.
386e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        gm_json.WriteToFile(expectations, json_path)
387e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
388e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def commit_rebaselines(self, rebaselines):
389e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """Sets the expected results file to use the hashes of the images in
390e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        the rebaselines list. If a expected result image is not in rebaselines
391e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        at all, the old hash will be used.
392e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
393e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        @param rebaselines A list of image paths to use the hash of.
394e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        """
395e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Reset all expectations to their old hashes because some of them may
396e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # have been set to the new hash by a previous call to this function.
397e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        for expectation in self._expectations:
398e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            expectation.is_rebaselined = False
399e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self._set_expected_hash(expectation.device_name,
400e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                    expectation.image_name,
401e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                    expectation.expected_hash)
402e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
403e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Take all the images to rebaseline
404e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        for image_path in rebaselines:
405e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # Get the metadata about the image at the path.
406e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            is_actual, expectation = self.image_map[image_path]
407e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
408e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            expectation.is_rebaselined = is_actual
409e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            expectation_hash = expectation.actual_hash if is_actual else\
410e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                               expectation.expected_hash
411e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
412e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # Write out that image's hash directly to the expected results file.
413e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self._set_expected_hash(expectation.device_name,
414e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                    expectation.image_name,
415e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                    expectation_hash)
416e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
417e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self._load_skpdiff_output()
418e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
419e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
42058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenbergerclass SkPDiffHandler(BaseHTTPServer.BaseHTTPRequestHandler):
42158190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    def send_file(self, file_path):
42258190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # Grab the extension if there is one
42358190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        extension = os.path.splitext(file_path)[1]
42458190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        if len(extension) >= 1:
42558190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger            extension = extension[1:]
42658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
42758190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # Determine the MIME type of the file from its extension
42858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP[''])
42958190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
43058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # Open the file and send it over HTTP
431e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        if os.path.isfile(file_path):
432e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            with open(file_path, 'rb') as sending_file:
433e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                self.send_response(200)
434e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                self.send_header('Content-type', mime_type)
435e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                self.end_headers()
436e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                self.wfile.write(sending_file.read())
437e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        else:
438e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.send_error(404)
43958190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
44058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    def serve_if_in_dir(self, dir_path, file_path):
44158190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # Determine if the file exists relative to the given dir_path AND exists
44258190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # under the dir_path. This is to prevent accidentally serving files
44358190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # outside the directory intended using symlinks, or '../'.
44458190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        real_path = os.path.normpath(os.path.join(dir_path, file_path))
44558190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        if os.path.commonprefix([real_path, dir_path]) == dir_path:
44658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger            if os.path.isfile(real_path):
44758190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger                self.send_file(real_path)
44858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger                return True
44958190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        return False
45058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
45158190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    def do_GET(self):
45258190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # Simple rewrite rule of the root path to 'viewer.html'
45358190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        if self.path == '' or self.path == '/':
45458190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger            self.path = '/viewer.html'
45558190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
45658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # The [1:] chops off the leading '/'
45758190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        file_path = self.path[1:]
45858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
459e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Handle skpdiff_output.json manually because it is was processed by the
460e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # server when it was started and does not exist as a file.
461e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        if file_path == 'skpdiff_output.json':
462e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.send_response(200)
463e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.send_header('Content-type', MIME_TYPE_MAP['json'])
464e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.end_headers()
465e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
466e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # Add JSONP padding to the JSON because the web page expects it. It
467e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # expects it because it was designed to run with or without a web
468e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # server. Without a web server, the only way to load JSON is with
469e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            # JSONP.
470e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            skpdiff_records = self.server.expectations_manager.skpdiff_records
471e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.wfile.write('var SkPDiffRecords = ')
472e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            json.dump({'records': skpdiff_records}, self.wfile)
473e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.wfile.write(';')
474e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            return
475e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
476e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # Attempt to send static asset files first.
477e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        if self.serve_if_in_dir(SCRIPT_DIR, file_path):
47858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger            return
47958190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
480e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # WARNING: Serving any file the user wants is incredibly insecure. Its
481e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # redeeming quality is that we only serve gm files on a white list.
482e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        if self.path in self.server.image_set:
483e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.send_file(self.path)
48458190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger            return
48558190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
48658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        # If no file to send was found, just give the standard 404
48758190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger        self.send_error(404)
48858190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
489e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    def do_POST(self):
490e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        if self.path == '/commit_rebaselines':
491e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            content_length = int(self.headers['Content-length'])
492e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            request_data = json.loads(self.rfile.read(content_length))
493e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            rebaselines = request_data['rebaselines']
494e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.server.expectations_manager.commit_rebaselines(rebaselines)
495e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.send_response(200)
496e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.send_header('Content-type', 'application/json')
497e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.end_headers()
498e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            self.wfile.write('{"success":true}')
499e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger            return
500e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
501e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        # If the we have no handler for this path, give em' the 404
502e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        self.send_error(404)
503e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
504e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
505e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef run_server(expectations_manager, port=8080):
506e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # It's important to parse the results file so that we can make a set of
507e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # images that the web page might request.
508e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    skpdiff_records = expectations_manager.skpdiff_records
509e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    image_set = get_image_set_from_skpdiff(skpdiff_records)
51058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
51158190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    # Do not bind to interfaces other than localhost because the server will
51258190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    # attempt to serve files relative to the root directory as a last resort
51358190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    # before 404ing. This means all of your files can be accessed from this
51458190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    # server, so DO NOT let this server listen to anything but localhost.
515e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    server_address = ('127.0.0.1', port)
51658190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    http_server = BaseHTTPServer.HTTPServer(server_address, SkPDiffHandler)
517e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    http_server.image_set = image_set
518e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    http_server.expectations_manager = expectations_manager
519e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    print('Navigate thine browser to: http://{}:{}/'.format(*server_address))
52058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    http_server.serve_forever()
52158190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger
522e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
523e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenbergerdef main():
524e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    parser = argparse.ArgumentParser()
525e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    parser.add_argument('--port', '-p', metavar='PORT',
526e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        type=int,
527e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        default=8080,
528e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        help='port to bind the server to; ' +
529e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        'defaults to %(default)s',
530e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        )
531e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
532e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    parser.add_argument('--expectations-dir', metavar='EXPECTATIONS_DIR',
533e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        default=DEFAULT_GM_EXPECTATIONS_DIR,
534e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        help='path to the gm expectations; ' +
535e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        'defaults to %(default)s'
536e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        )
537e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
538e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    parser.add_argument('--expected',
539e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        metavar='EXPECTATIONS_FILE_NAME',
540e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        default='expected-results.json',
541e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        help='the file name of the expectations JSON; ' +
542e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        'defaults to %(default)s'
543e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        )
544e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
545e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    parser.add_argument('--updated',
546e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        metavar='UPDATED_FILE_NAME',
547e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        default='updated-results.json',
548e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        help='the file name of the updated expectations JSON;' +
549e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        ' defaults to %(default)s'
550e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        )
551e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
552e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    parser.add_argument('--skpdiff-path', metavar='SKPDIFF_PATH',
553e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        default=None,
554e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        help='the path to the skpdiff binary to use; ' +
555e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        'defaults to out/Release/skpdiff or out/Default/skpdiff'
556e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                        )
557e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
558e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    args = vars(parser.parse_args())  # Convert args into a python dict
559e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
560e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # Make sure we have access to an skpdiff binary
561e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    skpdiff_path = get_skpdiff_path(args['skpdiff_path'])
562e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    if skpdiff_path is None:
563e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger        sys.exit(1)
564e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
565e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    # Print out the paths of things for easier debugging
566e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    print('script dir         :', SCRIPT_DIR)
567e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    print('tools dir          :', TOOLS_DIR)
568e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    print('root dir           :', SKIA_ROOT_DIR)
569e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    print('expectations dir   :', args['expectations_dir'])
570e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    print('skpdiff path       :', skpdiff_path)
571e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
572e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    expectations_manager = ExpectationsManager(args['expectations_dir'],
573e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                               args['expected'],
574e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                               args['updated'],
575e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger                                               skpdiff_path)
576e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
577e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger    run_server(expectations_manager, port=args['port'])
578e27eefc4844477cee5d32f51ab45ff62020cdb36Derek Sollenberger
57958190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenbergerif __name__ == '__main__':
58058190644c30e1c4aa8e527f3503c58f841e0fcf3Derek Sollenberger    main()
581