1f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com#!/usr/bin/python
2f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
39fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com"""
4f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comCopyright 2013 Google Inc.
5f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
6f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comUse of this source code is governed by a BSD-style license that can be
7f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comfound in the LICENSE file.
8f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
9f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comHTTP server for our HTML rebaseline viewer.
109fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com"""
11f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
12f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# System-level imports
13f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport argparse
14f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport BaseHTTPServer
15f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport json
16dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.comimport logging
17f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport os
18f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport posixpath
19f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport re
20f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comimport shutil
21b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.comimport socket
22d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.comimport subprocess
23542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.comimport thread
24d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.comimport threading
25542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.comimport time
26fb261109ec2c33e9236667faf09c13574146e627epogerimport urllib
27dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.comimport urlparse
28f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
29e5481eca7e66fd194c3b897fa3b8911cfe420958epoger# Must fix up PYTHONPATH before importing from within Skia
303b5c86c7a2db25f82a8415b6c79d1ac580fc64aestephanaimport rs_fixpypath  # pylint: disable=W0611
31e5481eca7e66fd194c3b897fa3b8911cfe420958epoger
32f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# Imports from within Skia
33133931f4abdafa5bb0bdea3a02af1e5a70d5ac98epogerfrom py.utils import gs_utils
34fb261109ec2c33e9236667faf09c13574146e627epogerimport buildbot_globals
3531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgimport gm_json
36f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
37f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# Imports from local dir
387498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org#
39e5481eca7e66fd194c3b897fa3b8911cfe420958epoger# pylint: disable=C0301
407498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org# Note: we import results under a different name, to avoid confusion with the
417498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org# Server.results() property. See discussion at
427498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org# https://codereview.chromium.org/195943004/diff/1/gm/rebaseline_server/server.py#newcode44
43e5481eca7e66fd194c3b897fa3b8911cfe420958epoger# pylint: enable=C0301
4431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgimport compare_configs
450b7127635d8245de7ac704080d722d06e47621d0epogerimport compare_rendered_pictures
46b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.orgimport compare_to_expectations
47b144271179aaf82cb1151e9dfd8e866747402594epogerimport download_actuals
486132b436d8723beaf06d1a8a0880f4f1535908a0epogerimport imagediffdb
4916f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgimport imagepairset
507498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.orgimport results as results_mod
512c4352bb9db02db70b829f96f397faa80fade220epogerimport writable_expectations as writable_expectations_mod
522c4352bb9db02db70b829f96f397faa80fade220epoger
53f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
54f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comPATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
55f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
56f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# A simple dictionary of file name extensions to MIME types. The empty string
57f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# entry is used as the default when no extension was given or if the extension
58f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com# has no entry in this dictionary.
59f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comMIME_TYPE_MAP = {'': 'application/octet-stream',
60f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                 'html': 'text/html',
61f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                 'css': 'text/css',
62f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                 'png': 'image/png',
63f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                 'js': 'application/javascript',
64f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                 'json': 'application/json'
65f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                 }
66f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
6716f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org# Keys that server.py uses to create the toplevel content header.
6816f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org# NOTE: Keep these in sync with static/constants.js
6916f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgKEY__EDITS__MODIFICATIONS = 'modifications'
7016f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgKEY__EDITS__OLD_RESULTS_HASH = 'oldResultsHash'
7116f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.orgKEY__EDITS__OLD_RESULTS_TYPE = 'oldResultsType'
722c4352bb9db02db70b829f96f397faa80fade220epogerKEY__LIVE_EDITS__MODIFICATIONS = 'modifications'
732c4352bb9db02db70b829f96f397faa80fade220epogerKEY__LIVE_EDITS__SET_A_DESCRIPTIONS = 'setA'
742c4352bb9db02db70b829f96f397faa80fade220epogerKEY__LIVE_EDITS__SET_B_DESCRIPTIONS = 'setB'
757498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org
7631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgDEFAULT_ACTUALS_DIR = results_mod.DEFAULT_ACTUALS_DIR
77b144271179aaf82cb1151e9dfd8e866747402594epogerDEFAULT_GM_SUMMARIES_BUCKET = download_actuals.GM_SUMMARIES_BUCKET
78b144271179aaf82cb1151e9dfd8e866747402594epogerDEFAULT_JSON_FILENAME = download_actuals.DEFAULT_JSON_FILENAME
79f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comDEFAULT_PORT = 8888
80f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
81b144271179aaf82cb1151e9dfd8e866747402594epogerPARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
82b144271179aaf82cb1151e9dfd8e866747402594epogerTRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY))
833facc7c87d9f81c352c9d37b1b46340b9e745578epoger
8431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org# Directory, relative to PARENT_DIRECTORY, within which the server will serve
8531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org# out static files.
8631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgSTATIC_CONTENTS_SUBDIR = 'static'
8731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org# All of the GENERATED_*_SUBDIRS are relative to STATIC_CONTENTS_SUBDIR
8831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgGENERATED_HTML_SUBDIR = 'generated-html'
8931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgGENERATED_IMAGES_SUBDIR = 'generated-images'
9031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgGENERATED_JSON_SUBDIR = 'generated-json'
91579942387bed9259b03419c593503022e1399931commit-bot@chromium.org
923facc7c87d9f81c352c9d37b1b46340b9e745578epoger# Directives associated with various HTTP GET requests.
933facc7c87d9f81c352c9d37b1b46340b9e745578epogerGET__LIVE_RESULTS = 'live-results'
943facc7c87d9f81c352c9d37b1b46340b9e745578epogerGET__PRECOMPUTED_RESULTS = 'results'
953facc7c87d9f81c352c9d37b1b46340b9e745578epogerGET__PREFETCH_RESULTS = 'prefetch'
963facc7c87d9f81c352c9d37b1b46340b9e745578epogerGET__STATIC_CONTENTS = 'static'
973facc7c87d9f81c352c9d37b1b46340b9e745578epoger
98e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger# Parameters we use within do_GET_live_results() and do_GET_prefetch_results()
99e73cd5ab0f693ae0c81f5f1ca76045363b6f999depogerLIVE_PARAM__DOWNLOAD_ONLY_DIFFERING = 'downloadOnlyDifferingImages'
1000b7127635d8245de7ac704080d722d06e47621d0epogerLIVE_PARAM__SET_A_DIR = 'setADir'
101e73cd5ab0f693ae0c81f5f1ca76045363b6f999depogerLIVE_PARAM__SET_A_SECTION = 'setASection'
1020b7127635d8245de7ac704080d722d06e47621d0epogerLIVE_PARAM__SET_B_DIR = 'setBDir'
103e73cd5ab0f693ae0c81f5f1ca76045363b6f999depogerLIVE_PARAM__SET_B_SECTION = 'setBSection'
1043facc7c87d9f81c352c9d37b1b46340b9e745578epoger
1052682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com# How often (in seconds) clients should reload while waiting for initial
1062682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com# results to load.
1072682c90860655f6c25c61f97c8d8db309d03087aepoger@google.comRELOAD_INTERVAL_UNTIL_READY = 10
1082682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com
109fb261109ec2c33e9236667faf09c13574146e627epoger_GM_SUMMARY_TYPES = [
11031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    results_mod.KEY__HEADER__RESULTS_FAILURES,
11131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    results_mod.KEY__HEADER__RESULTS_ALL,
11231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org]
11331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org# If --compare-configs is specified, compare these configs.
11431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgCONFIG_PAIRS_TO_COMPARE = [('8888', 'gpu')]
11531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
116fb261109ec2c33e9236667faf09c13574146e627epoger# SKP results that are available to compare.
117fb261109ec2c33e9236667faf09c13574146e627epoger#
118fb261109ec2c33e9236667faf09c13574146e627epoger# TODO(stephana): We don't actually want to maintain this list of platforms.
119fb261109ec2c33e9236667faf09c13574146e627epoger# We are just putting them in here for now, as "convenience" links for testing
120fb261109ec2c33e9236667faf09c13574146e627epoger# SKP diffs.
121fb261109ec2c33e9236667faf09c13574146e627epoger# Ultimately, we will depend on buildbot steps linking to their own diffs on
122fb261109ec2c33e9236667faf09c13574146e627epoger# the shared rebaseline_server instance.
123fb261109ec2c33e9236667faf09c13574146e627epoger_SKP_BASE_GS_URL = 'gs://' + buildbot_globals.Get('skp_summaries_bucket')
1245c4b137592a7937b4d02d72a976fa783f8d3675aepoger_SKP_BASE_REPO_URL = (
1255c4b137592a7937b4d02d72a976fa783f8d3675aepoger    compare_rendered_pictures.REPO_URL_PREFIX + posixpath.join(
1265c4b137592a7937b4d02d72a976fa783f8d3675aepoger        'expectations', 'skp'))
127fb261109ec2c33e9236667faf09c13574146e627epoger_SKP_PLATFORMS = [
128fb261109ec2c33e9236667faf09c13574146e627epoger    'Test-Mac10.8-MacMini4.1-GeForce320M-x86_64-Debug',
129fb261109ec2c33e9236667faf09c13574146e627epoger    'Test-Ubuntu12-ShuttleA-GTX660-x86-Release',
130fb261109ec2c33e9236667faf09c13574146e627epoger]
131fb261109ec2c33e9236667faf09c13574146e627epoger
132eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com_HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
133eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com_HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
134eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
135f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com_SERVER = None   # This gets filled in by main()
136f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
137d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
138d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.comdef _run_command(args, directory):
139d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  """Runs a command and returns stdout as a single string.
140d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
141d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  Args:
142d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    args: the command to run, as a list of arguments
143d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    directory: directory within which to run the command
144d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
145d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  Returns: stdout, as a string
146d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
147d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  Raises an Exception if the command failed (exited with nonzero return code).
148d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  """
149d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  logging.debug('_run_command: %s in directory %s' % (args, directory))
150d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  proc = subprocess.Popen(args, cwd=directory,
151d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                          stdout=subprocess.PIPE,
152d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                          stderr=subprocess.PIPE)
153d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  (stdout, stderr) = proc.communicate()
154d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  if proc.returncode is not 0:
155d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    raise Exception('command "%s" failed in dir "%s": %s' %
156d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                    (args, directory, stderr))
157d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  return stdout
158d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
159d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
160591469b1e93f72172cef13a2f0675699994d7848epoger@google.comdef _get_routable_ip_address():
161b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com  """Returns routable IP address of this host (the IP address of its network
162b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com     interface that would be used for most traffic, not its localhost
163b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com     interface).  See http://stackoverflow.com/a/166589 """
164b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
165b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com  sock.connect(('8.8.8.8', 80))
166b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com  host = sock.getsockname()[0]
167b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com  sock.close()
168b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com  return host
169b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com
170d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
17131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.orgdef _create_index(file_path, config_pairs):
17231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  """Creates an index file linking to all results available from this server.
17331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
17431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  Prior to https://codereview.chromium.org/215503002 , we had a static
17531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  index.html within our repo.  But now that the results may or may not include
17631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  config comparisons, index.html needs to be generated differently depending
17731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  on which results are included.
17831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
17931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  TODO(epoger): Instead of including raw HTML within the Python code,
18031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  consider restoring the index.html file as a template and using django (or
18131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  similar) to fill in dynamic content.
18231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
18331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  Args:
18431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    file_path: path on local disk to write index to; any directory components
18531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org               of this path that do not already exist will be created
18631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    config_pairs: what pairs of configs (if any) we compare actual results of
18731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  """
18831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  dir_path = os.path.dirname(file_path)
18931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  if not os.path.isdir(dir_path):
19031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    os.makedirs(dir_path)
19131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  with open(file_path, 'w') as file_handle:
19231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    file_handle.write(
19331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        '<!DOCTYPE html><html>'
19431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        '<head><title>rebaseline_server</title></head>'
19531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        '<body><ul>')
196fb261109ec2c33e9236667faf09c13574146e627epoger
197fb261109ec2c33e9236667faf09c13574146e627epoger    if _GM_SUMMARY_TYPES:
198fb261109ec2c33e9236667faf09c13574146e627epoger      file_handle.write('<li>GM Expectations vs Actuals</li><ul>')
199fb261109ec2c33e9236667faf09c13574146e627epoger      for summary_type in _GM_SUMMARY_TYPES:
20031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        file_handle.write(
201fb261109ec2c33e9236667faf09c13574146e627epoger            '\n<li><a href="/{static_directive}/view.html#/view.html?'
2023facc7c87d9f81c352c9d37b1b46340b9e745578epoger            'resultsToLoad=/{results_directive}/{summary_type}">'
203e5481eca7e66fd194c3b897fa3b8911cfe420958epoger            '{summary_type}</a></li>'.format(
2043facc7c87d9f81c352c9d37b1b46340b9e745578epoger                results_directive=GET__PRECOMPUTED_RESULTS,
2053facc7c87d9f81c352c9d37b1b46340b9e745578epoger                static_directive=GET__STATIC_CONTENTS,
20646e51e10ae9a061456a5135a9973755f0b64547cepoger                summary_type=summary_type))
20731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      file_handle.write('</ul>')
208fb261109ec2c33e9236667faf09c13574146e627epoger
20931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    if config_pairs:
210fb261109ec2c33e9236667faf09c13574146e627epoger      file_handle.write(
211fb261109ec2c33e9236667faf09c13574146e627epoger          '\n<li>Comparing configs within actual GM results</li><ul>')
21231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      for config_pair in config_pairs:
21331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        file_handle.write('<li>%s vs %s:' % config_pair)
214fb261109ec2c33e9236667faf09c13574146e627epoger        for summary_type in _GM_SUMMARY_TYPES:
21531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org          file_handle.write(
21631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org              ' <a href="/%s/view.html#/view.html?'
21731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org              'resultsToLoad=/%s/%s/%s-vs-%s_%s.json">%s</a>' % (
2183facc7c87d9f81c352c9d37b1b46340b9e745578epoger                  GET__STATIC_CONTENTS, GET__STATIC_CONTENTS,
21931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                  GENERATED_JSON_SUBDIR, config_pair[0], config_pair[1],
22031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                  summary_type, summary_type))
22131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        file_handle.write('</li>')
22231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      file_handle.write('</ul>')
223fb261109ec2c33e9236667faf09c13574146e627epoger
224fb261109ec2c33e9236667faf09c13574146e627epoger    if _SKP_PLATFORMS:
225fb261109ec2c33e9236667faf09c13574146e627epoger      file_handle.write('\n<li>Rendered SKPs:<ul>')
226fb261109ec2c33e9236667faf09c13574146e627epoger      for builder in _SKP_PLATFORMS:
227fb261109ec2c33e9236667faf09c13574146e627epoger        file_handle.write(
228fb261109ec2c33e9236667faf09c13574146e627epoger            '\n<li><a href="../live-view.html#live-view.html?%s">' %
229fb261109ec2c33e9236667faf09c13574146e627epoger            urllib.urlencode({
230fb261109ec2c33e9236667faf09c13574146e627epoger                LIVE_PARAM__SET_A_SECTION:
231fb261109ec2c33e9236667faf09c13574146e627epoger                    gm_json.JSONKEY_EXPECTEDRESULTS,
232fb261109ec2c33e9236667faf09c13574146e627epoger                LIVE_PARAM__SET_A_DIR:
2335c4b137592a7937b4d02d72a976fa783f8d3675aepoger                    posixpath.join(_SKP_BASE_REPO_URL, builder),
234fb261109ec2c33e9236667faf09c13574146e627epoger                LIVE_PARAM__SET_B_SECTION:
235fb261109ec2c33e9236667faf09c13574146e627epoger                    gm_json.JSONKEY_ACTUALRESULTS,
236fb261109ec2c33e9236667faf09c13574146e627epoger                LIVE_PARAM__SET_B_DIR:
237fb261109ec2c33e9236667faf09c13574146e627epoger                    posixpath.join(_SKP_BASE_GS_URL, builder),
238fb261109ec2c33e9236667faf09c13574146e627epoger            }))
239fb261109ec2c33e9236667faf09c13574146e627epoger        file_handle.write('expected vs actuals on %s</a></li>' % builder)
240fb261109ec2c33e9236667faf09c13574146e627epoger      file_handle.write(
241fb261109ec2c33e9236667faf09c13574146e627epoger          '\n<li><a href="../live-view.html#live-view.html?%s">' %
242fb261109ec2c33e9236667faf09c13574146e627epoger          urllib.urlencode({
243fb261109ec2c33e9236667faf09c13574146e627epoger              LIVE_PARAM__SET_A_SECTION:
244fb261109ec2c33e9236667faf09c13574146e627epoger                  gm_json.JSONKEY_ACTUALRESULTS,
245fb261109ec2c33e9236667faf09c13574146e627epoger              LIVE_PARAM__SET_A_DIR:
246fb261109ec2c33e9236667faf09c13574146e627epoger                  posixpath.join(_SKP_BASE_GS_URL, _SKP_PLATFORMS[0]),
247fb261109ec2c33e9236667faf09c13574146e627epoger              LIVE_PARAM__SET_B_SECTION:
248fb261109ec2c33e9236667faf09c13574146e627epoger                  gm_json.JSONKEY_ACTUALRESULTS,
249fb261109ec2c33e9236667faf09c13574146e627epoger              LIVE_PARAM__SET_B_DIR:
250fb261109ec2c33e9236667faf09c13574146e627epoger                  posixpath.join(_SKP_BASE_GS_URL, _SKP_PLATFORMS[1]),
251fb261109ec2c33e9236667faf09c13574146e627epoger          }))
252fb261109ec2c33e9236667faf09c13574146e627epoger      file_handle.write('actuals on %s vs %s</a></li>' % (
253fb261109ec2c33e9236667faf09c13574146e627epoger          _SKP_PLATFORMS[0], _SKP_PLATFORMS[1]))
254fb261109ec2c33e9236667faf09c13574146e627epoger      file_handle.write('</li>')
255fb261109ec2c33e9236667faf09c13574146e627epoger
256fb261109ec2c33e9236667faf09c13574146e627epoger    file_handle.write('\n</ul></body></html>')
25731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
25831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
259f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comclass Server(object):
2609fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com  """ HTTP server for our HTML rebaseline viewer. """
2619fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com
262f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  def __init__(self,
263f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com               actuals_dir=DEFAULT_ACTUALS_DIR,
264b144271179aaf82cb1151e9dfd8e866747402594epoger               json_filename=DEFAULT_JSON_FILENAME,
265b144271179aaf82cb1151e9dfd8e866747402594epoger               gm_summaries_bucket=DEFAULT_GM_SUMMARIES_BUCKET,
266542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com               port=DEFAULT_PORT, export=False, editable=True,
2670b7127635d8245de7ac704080d722d06e47621d0epoger               reload_seconds=0, config_pairs=None, builder_regex_list=None,
2680b7127635d8245de7ac704080d722d06e47621d0epoger               boto_file_path=None,
2690b7127635d8245de7ac704080d722d06e47621d0epoger               imagediffdb_threads=imagediffdb.DEFAULT_NUM_WORKER_THREADS):
2709fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    """
2719fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    Args:
2729fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com      actuals_dir: directory under which we will check out the latest actual
273c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org          GM results
274b144271179aaf82cb1151e9dfd8e866747402594epoger      json_filename: basename of the JSON summary file to load for each builder
275b144271179aaf82cb1151e9dfd8e866747402594epoger      gm_summaries_bucket: Google Storage bucket to download json_filename
276b144271179aaf82cb1151e9dfd8e866747402594epoger          files from; if None or '', don't fetch new actual-results files
277b144271179aaf82cb1151e9dfd8e866747402594epoger          at all, just compare to whatever files are already in actuals_dir
2789fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com      port: which TCP port to listen on for HTTP requests
2799fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com      export: whether to allow HTTP clients on other hosts to access this server
2804c808253f4f41f61309cf4449476e6bbbbd86631epoger      editable: whether HTTP clients are allowed to submit new GM baselines
2814c808253f4f41f61309cf4449476e6bbbbd86631epoger          (SKP baseline modifications are performed using an entirely different
2824c808253f4f41f61309cf4449476e6bbbbd86631epoger          mechanism, not affected by this parameter)
283542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com      reload_seconds: polling interval with which to check for new results;
284c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org          if 0, don't check for new results at all
28531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      config_pairs: List of (string, string) tuples; for each tuple, compare
28631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org          actual results of these two configs.  If None or empty,
28731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org          don't compare configs at all.
288defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org      builder_regex_list: List of regular expressions specifying which builders
289defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org          we will process. If None, process all builders.
2900b7127635d8245de7ac704080d722d06e47621d0epoger      boto_file_path: Path to .boto file giving us credentials to access
2910b7127635d8245de7ac704080d722d06e47621d0epoger          Google Storage buckets; if None, we will only be able to access
2920b7127635d8245de7ac704080d722d06e47621d0epoger          public GS buckets.
2930b7127635d8245de7ac704080d722d06e47621d0epoger      imagediffdb_threads: How many threads to spin up within imagediffdb.
2949fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    """
295f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    self._actuals_dir = actuals_dir
296b144271179aaf82cb1151e9dfd8e866747402594epoger    self._json_filename = json_filename
297b144271179aaf82cb1151e9dfd8e866747402594epoger    self._gm_summaries_bucket = gm_summaries_bucket
298f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    self._port = port
299f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    self._export = export
300542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com    self._editable = editable
301542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com    self._reload_seconds = reload_seconds
30231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    self._config_pairs = config_pairs or []
303defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org    self._builder_regex_list = builder_regex_list
3040b7127635d8245de7ac704080d722d06e47621d0epoger    self.truncate_results = False
3050b7127635d8245de7ac704080d722d06e47621d0epoger
3060b7127635d8245de7ac704080d722d06e47621d0epoger    if boto_file_path:
3070b7127635d8245de7ac704080d722d06e47621d0epoger      self._gs = gs_utils.GSUtils(boto_file_path=boto_file_path)
3080b7127635d8245de7ac704080d722d06e47621d0epoger    else:
3090b7127635d8245de7ac704080d722d06e47621d0epoger      self._gs = gs_utils.GSUtils()
3100b7127635d8245de7ac704080d722d06e47621d0epoger
31131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    _create_index(
31231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        file_path=os.path.join(
31331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org            PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_HTML_SUBDIR,
31431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org            "index.html"),
31531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        config_pairs=config_pairs)
316591469b1e93f72172cef13a2f0675699994d7848epoger@google.com
317d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # Reentrant lock that must be held whenever updating EITHER of:
318d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # 1. self._results
319d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # 2. the expected or actual results on local disk
320d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    self.results_rlock = threading.RLock()
3216132b436d8723beaf06d1a8a0880f4f1535908a0epoger
3220b7127635d8245de7ac704080d722d06e47621d0epoger    # Create a single ImageDiffDB instance that is used by all our differs.
3230b7127635d8245de7ac704080d722d06e47621d0epoger    self._image_diff_db = imagediffdb.ImageDiffDB(
3240b7127635d8245de7ac704080d722d06e47621d0epoger        gs=self._gs,
3250b7127635d8245de7ac704080d722d06e47621d0epoger        storage_root=os.path.join(
3260b7127635d8245de7ac704080d722d06e47621d0epoger            PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR,
3270b7127635d8245de7ac704080d722d06e47621d0epoger            GENERATED_IMAGES_SUBDIR),
3280b7127635d8245de7ac704080d722d06e47621d0epoger        num_worker_threads=imagediffdb_threads)
3290b7127635d8245de7ac704080d722d06e47621d0epoger
3300b7127635d8245de7ac704080d722d06e47621d0epoger    # This will be filled in by calls to update_results()
331d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    self._results = None
332d7255b6ae43f1931d5ec086fe4e92a664e28ab6fepoger@google.com
333d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  @property
334d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  def results(self):
33550ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org    """ Returns the most recently generated results, or None if we don't have
33650ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org    any valid results (update_results() has not completed yet). """
337d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    return self._results
338d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
339d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  @property
3400b7127635d8245de7ac704080d722d06e47621d0epoger  def image_diff_db(self):
3410b7127635d8245de7ac704080d722d06e47621d0epoger    """ Returns reference to our ImageDiffDB object."""
3420b7127635d8245de7ac704080d722d06e47621d0epoger    return self._image_diff_db
3430b7127635d8245de7ac704080d722d06e47621d0epoger
3440b7127635d8245de7ac704080d722d06e47621d0epoger  @property
3450b7127635d8245de7ac704080d722d06e47621d0epoger  def gs(self):
3460b7127635d8245de7ac704080d722d06e47621d0epoger    """ Returns reference to our GSUtils object."""
3470b7127635d8245de7ac704080d722d06e47621d0epoger    return self._gs
3480b7127635d8245de7ac704080d722d06e47621d0epoger
3490b7127635d8245de7ac704080d722d06e47621d0epoger  @property
3509fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com  def is_exported(self):
3519fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    """ Returns true iff HTTP clients on other hosts are allowed to access
3529fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    this server. """
3539fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    return self._export
3549fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com
355d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  @property
356542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com  def is_editable(self):
3579f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    """ True iff HTTP clients are allowed to submit new GM baselines.
3589f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger
3599f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    TODO(epoger): This only pertains to GM baselines; SKP baselines are
3609f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    editable whenever expectations vs actuals are shown.
3619f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    Once we move the GM baselines to use the same code as the SKP baselines,
3629f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    we can delete this property.
3639f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    """
364542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com    return self._editable
365542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com
366d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com  @property
367542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com  def reload_seconds(self):
368542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com    """ Returns the result reload period in seconds, or 0 if we don't reload
369542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com    results. """
370542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com    return self._reload_seconds
371f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
37250ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org  def update_results(self, invalidate=False):
3737498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org    """ Create or update self._results, based on the latest expectations and
3747498d95bde16eeaa4b20643fb930b6a3be7face1commit-bot@chromium.org    actuals.
375d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
376d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    We hold self.results_rlock while we do this, to guarantee that no other
377d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    thread attempts to update either self._results or the underlying files at
378d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    the same time.
37950ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org
38050ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org    Args:
38150ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org      invalidate: if True, invalidate self._results immediately upon entry;
38250ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org                  otherwise, we will let readers see those results until we
38350ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org                  replace them
384f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    """
385d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    with self.results_rlock:
38650ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org      if invalidate:
38750ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org        self._results = None
388b144271179aaf82cb1151e9dfd8e866747402594epoger      if self._gm_summaries_bucket:
389c0df2fb5d0421a649d1dff9133874e440300fa7ccommit-bot@chromium.org        logging.info(
390b144271179aaf82cb1151e9dfd8e866747402594epoger            'Updating GM result summaries in %s from gm_summaries_bucket %s ...'
391b144271179aaf82cb1151e9dfd8e866747402594epoger            % (self._actuals_dir, self._gm_summaries_bucket))
392b144271179aaf82cb1151e9dfd8e866747402594epoger
393b144271179aaf82cb1151e9dfd8e866747402594epoger        # Clean out actuals_dir first, in case some builders have gone away
394b144271179aaf82cb1151e9dfd8e866747402594epoger        # since we last ran.
395b144271179aaf82cb1151e9dfd8e866747402594epoger        if os.path.isdir(self._actuals_dir):
396b144271179aaf82cb1151e9dfd8e866747402594epoger          shutil.rmtree(self._actuals_dir)
397b144271179aaf82cb1151e9dfd8e866747402594epoger
398b144271179aaf82cb1151e9dfd8e866747402594epoger        # Get the list of builders we care about.
399b144271179aaf82cb1151e9dfd8e866747402594epoger        all_builders = download_actuals.get_builders_list(
400b144271179aaf82cb1151e9dfd8e866747402594epoger            summaries_bucket=self._gm_summaries_bucket)
401b144271179aaf82cb1151e9dfd8e866747402594epoger        if self._builder_regex_list:
402b144271179aaf82cb1151e9dfd8e866747402594epoger          matching_builders = []
403b144271179aaf82cb1151e9dfd8e866747402594epoger          for builder in all_builders:
404b144271179aaf82cb1151e9dfd8e866747402594epoger            for regex in self._builder_regex_list:
405b144271179aaf82cb1151e9dfd8e866747402594epoger              if re.match(regex, builder):
406b144271179aaf82cb1151e9dfd8e866747402594epoger                matching_builders.append(builder)
407b144271179aaf82cb1151e9dfd8e866747402594epoger                break  # go on to the next builder, no need to try more regexes
408b144271179aaf82cb1151e9dfd8e866747402594epoger        else:
409b144271179aaf82cb1151e9dfd8e866747402594epoger          matching_builders = all_builders
410b144271179aaf82cb1151e9dfd8e866747402594epoger
411b144271179aaf82cb1151e9dfd8e866747402594epoger        # Download the JSON file for each builder we care about.
412b144271179aaf82cb1151e9dfd8e866747402594epoger        #
413b144271179aaf82cb1151e9dfd8e866747402594epoger        # TODO(epoger): When this is a large number of builders, we would be
414b144271179aaf82cb1151e9dfd8e866747402594epoger        # better off downloading them in parallel!
415b144271179aaf82cb1151e9dfd8e866747402594epoger        for builder in matching_builders:
41603f3db02c10d82ccbb4e3d3bd12fac689ce91fc6epoger          self._gs.download_file(
417b144271179aaf82cb1151e9dfd8e866747402594epoger              source_bucket=self._gm_summaries_bucket,
418b144271179aaf82cb1151e9dfd8e866747402594epoger              source_path=posixpath.join(builder, self._json_filename),
419b144271179aaf82cb1151e9dfd8e866747402594epoger              dest_path=os.path.join(self._actuals_dir, builder,
420b144271179aaf82cb1151e9dfd8e866747402594epoger                                     self._json_filename),
421b144271179aaf82cb1151e9dfd8e866747402594epoger              create_subdirs_if_needed=True)
422d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
423d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      # We only update the expectations dir if the server was run with a
424d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      # nonzero --reload argument; otherwise, we expect the user to maintain
425d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      # her own expectations as she sees fit.
426d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      #
427b144271179aaf82cb1151e9dfd8e866747402594epoger      # Because the Skia repo is hosted using git, and git does not
428d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      # support updating a single directory tree, we have to update the entire
429d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      # repo checkout.
430d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      #
431d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      # Because Skia uses depot_tools, we have to update using "gclient sync"
432b144271179aaf82cb1151e9dfd8e866747402594epoger      # instead of raw git commands.
433b144271179aaf82cb1151e9dfd8e866747402594epoger      #
434b144271179aaf82cb1151e9dfd8e866747402594epoger      # TODO(epoger): Fetch latest expectations in some other way.
435b144271179aaf82cb1151e9dfd8e866747402594epoger      # Eric points out that our official documentation recommends an
436b144271179aaf82cb1151e9dfd8e866747402594epoger      # unmanaged Skia checkout, so "gclient sync" will not bring down updated
437b144271179aaf82cb1151e9dfd8e866747402594epoger      # expectations from origin/master-- you'd have to do a "git pull" of
438b144271179aaf82cb1151e9dfd8e866747402594epoger      # some sort instead.
439b144271179aaf82cb1151e9dfd8e866747402594epoger      # However, the live rebaseline_server at
440b144271179aaf82cb1151e9dfd8e866747402594epoger      # http://skia-tree-status.appspot.com/redirect/rebaseline-server (which
441b144271179aaf82cb1151e9dfd8e866747402594epoger      # is probably the only user of the --reload flag!) uses a managed
442b144271179aaf82cb1151e9dfd8e866747402594epoger      # checkout, so "gclient sync" works in that case.
443b144271179aaf82cb1151e9dfd8e866747402594epoger      # Probably the best idea is to avoid all of this nonsense by fetching
444b144271179aaf82cb1151e9dfd8e866747402594epoger      # updated expectations into a temp directory, and leaving the rest of
445b144271179aaf82cb1151e9dfd8e866747402594epoger      # the checkout alone.  This could be done using "git show", or by
446b144271179aaf82cb1151e9dfd8e866747402594epoger      # downloading individual expectation JSON files from
447b144271179aaf82cb1151e9dfd8e866747402594epoger      # skia.googlesource.com .
448d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      if self._reload_seconds:
449d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com        logging.info(
450d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com            'Updating expected GM results in %s by syncing Skia repo ...' %
451b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.org            compare_to_expectations.DEFAULT_EXPECTATIONS_DIR)
452d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com        _run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
453591469b1e93f72172cef13a2f0675699994d7848epoger@google.com
45431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      self._results = compare_to_expectations.ExpectationComparisons(
4556132b436d8723beaf06d1a8a0880f4f1535908a0epoger          image_diff_db=self._image_diff_db,
456579942387bed9259b03419c593503022e1399931commit-bot@chromium.org          actuals_root=self._actuals_dir,
457a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org          diff_base_url=posixpath.join(
458defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org              os.pardir, STATIC_CONTENTS_SUBDIR, GENERATED_IMAGES_SUBDIR),
459defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org          builder_regex_list=self._builder_regex_list)
460f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
46131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      json_dir = os.path.join(
46231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org          PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR, GENERATED_JSON_SUBDIR)
46331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      if not os.path.isdir(json_dir):
464e5481eca7e66fd194c3b897fa3b8911cfe420958epoger        os.makedirs(json_dir)
46531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
46631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org      for config_pair in self._config_pairs:
46731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        config_comparisons = compare_configs.ConfigComparisons(
46831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org            configs=config_pair,
46931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org            actuals_root=self._actuals_dir,
47031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org            generated_images_root=os.path.join(
47131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR,
47231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                GENERATED_IMAGES_SUBDIR),
47331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org            diff_base_url=posixpath.join(
474defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org                os.pardir, GENERATED_IMAGES_SUBDIR),
475defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org            builder_regex_list=self._builder_regex_list)
476fb261109ec2c33e9236667faf09c13574146e627epoger        for summary_type in _GM_SUMMARY_TYPES:
47731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org          gm_json.WriteToFile(
47831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org              config_comparisons.get_packaged_results_of_type(
47931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                  results_type=summary_type),
48031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org              os.path.join(
48131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                  json_dir, '%s-vs-%s_%s.json' % (
48231d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                      config_pair[0], config_pair[1], summary_type)))
48331d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
4842682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com  def _result_loader(self, reload_seconds=0):
4852682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    """ Call self.update_results(), either once or periodically.
4862682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com
4872682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    Params:
4882682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com      reload_seconds: integer; if nonzero, reload results at this interval
4892682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com          (in which case, this method will never return!)
490542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com    """
4912682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    self.update_results()
4922682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    logging.info('Initial results loaded. Ready for requests on %s' % self._url)
4932682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    if reload_seconds:
4942682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com      while True:
4952682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com        time.sleep(reload_seconds)
4962682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com        self.update_results()
497542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com
498f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  def run(self):
4992682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    arg_tuple = (self._reload_seconds,)  # start_new_thread needs a tuple,
5002682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com                                         # even though it holds just one param
5012682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    thread.start_new_thread(self._result_loader, arg_tuple)
502542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com
503f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    if self._export:
504f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com      server_address = ('', self._port)
505591469b1e93f72172cef13a2f0675699994d7848epoger@google.com      host = _get_routable_ip_address()
506542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com      if self._editable:
507542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com        logging.warning('Running with combination of "export" and "editable" '
508542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                        'flags.  Users on other machines will '
509542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                        'be able to modify your GM expectations!')
510f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    else:
511b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com      host = '127.0.0.1'
512b08c707847be4b0c94adf592912b4e7073f71ecbepoger@google.com      server_address = (host, self._port)
513e5481eca7e66fd194c3b897fa3b8911cfe420958epoger    # pylint: disable=W0201
514f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler)
5152682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    self._url = 'http://%s:%d' % (host, http_server.server_port)
5162682c90860655f6c25c61f97c8d8db309d03087aepoger@google.com    logging.info('Listening for requests on %s' % self._url)
517f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    http_server.serve_forever()
518f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
519f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
520f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comclass HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
521f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  """ HTTP request handlers for various types of queries this server knows
522f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com      how to handle (static HTML and Javascript, expected/actual results, etc.)
523f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  """
524f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  def do_GET(self):
525e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    """
526e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    Handles all GET requests, forwarding them to the appropriate
527e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    do_GET_* dispatcher.
528f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
529e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    If we see any Exceptions, return a 404.  This fixes http://skbug.com/2147
530e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    """
531e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    try:
532e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      logging.debug('do_GET: path="%s"' % self.path)
533e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      if self.path == '' or self.path == '/' or self.path == '/index.html' :
53431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org        self.redirect_to('/%s/%s/index.html' % (
5353facc7c87d9f81c352c9d37b1b46340b9e745578epoger            GET__STATIC_CONTENTS, GENERATED_HTML_SUBDIR))
536e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org        return
537e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      if self.path == '/favicon.ico' :
5383facc7c87d9f81c352c9d37b1b46340b9e745578epoger        self.redirect_to('/%s/favicon.ico' % GET__STATIC_CONTENTS)
539e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org        return
540e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org
541e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      # All requests must be of this form:
542e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      #   /dispatcher/remainder
543e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      # where 'dispatcher' indicates which do_GET_* dispatcher to run
544e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      # and 'remainder' is the remaining path sent to that dispatcher.
545c8d8a604bf995856d3f147dc3a1b010d8e4efa06epoger      (dispatcher_name, remainder) = PATHSPLIT_RE.match(self.path).groups()
546e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      dispatchers = {
5473facc7c87d9f81c352c9d37b1b46340b9e745578epoger          GET__LIVE_RESULTS: self.do_GET_live_results,
5483facc7c87d9f81c352c9d37b1b46340b9e745578epoger          GET__PRECOMPUTED_RESULTS: self.do_GET_precomputed_results,
5493facc7c87d9f81c352c9d37b1b46340b9e745578epoger          GET__PREFETCH_RESULTS: self.do_GET_prefetch_results,
5503facc7c87d9f81c352c9d37b1b46340b9e745578epoger          GET__STATIC_CONTENTS: self.do_GET_static,
551e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      }
552e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      dispatcher = dispatchers[dispatcher_name]
553e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      dispatcher(remainder)
554e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    except:
555e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      self.send_error(404)
556e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org      raise
557f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
5580b7127635d8245de7ac704080d722d06e47621d0epoger  def do_GET_precomputed_results(self, results_type):
5590b7127635d8245de7ac704080d722d06e47621d0epoger    """ Handle a GET request for part of the precomputed _SERVER.results object.
560a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org
561a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    Args:
562a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org      results_type: string indicating which set of results to return;
563a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org            must be one of the results_mod.RESULTS_* constants
564a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    """
5650b7127635d8245de7ac704080d722d06e47621d0epoger    logging.debug('do_GET_precomputed_results: sending results of type "%s"' %
5660b7127635d8245de7ac704080d722d06e47621d0epoger                  results_type)
56731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    # Since we must make multiple calls to the ExpectationComparisons object,
56831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    # grab a reference to it in case it is updated to point at a new
56931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    # ExpectationComparisons object within another thread.
570a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    #
571a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    # TODO(epoger): Rather than using a global variable for the handler
572a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    # to refer to the Server object, make Server a subclass of
573a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    # HTTPServer, and then it could be available to the handler via
574a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    # the handler's .server instance variable.
575a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    results_obj = _SERVER.results
576a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    if results_obj:
577a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org      response_dict = results_obj.get_packaged_results_of_type(
578a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org          results_type=results_type, reload_seconds=_SERVER.reload_seconds,
579a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org          is_editable=_SERVER.is_editable, is_exported=_SERVER.is_exported)
580a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    else:
581a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org      now = int(time.time())
582a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org      response_dict = {
58368a3815401f461976f76891d0477cb1440fa0abacommit-bot@chromium.org          imagepairset.KEY__ROOT__HEADER: {
584a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org              results_mod.KEY__HEADER__SCHEMA_VERSION: (
58568a3815401f461976f76891d0477cb1440fa0abacommit-bot@chromium.org                  results_mod.VALUE__HEADER__SCHEMA_VERSION),
586a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org              results_mod.KEY__HEADER__IS_STILL_LOADING: True,
587a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org              results_mod.KEY__HEADER__TIME_UPDATED: now,
588a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org              results_mod.KEY__HEADER__TIME_NEXT_UPDATE_AVAILABLE: (
589a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org                  now + RELOAD_INTERVAL_UNTIL_READY),
590a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org          },
591a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org      }
592a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    self.send_json_dict(response_dict)
593a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org
5944c808253f4f41f61309cf4449476e6bbbbd86631epoger  def _get_live_results_or_prefetch(self, url_remainder, prefetch_only=False):
5954c808253f4f41f61309cf4449476e6bbbbd86631epoger    """ Handle a GET request for live-generated image diff data.
5964c808253f4f41f61309cf4449476e6bbbbd86631epoger
5974c808253f4f41f61309cf4449476e6bbbbd86631epoger    Args:
5984c808253f4f41f61309cf4449476e6bbbbd86631epoger      url_remainder: string indicating which image diffs to generate
5994c808253f4f41f61309cf4449476e6bbbbd86631epoger      prefetch_only: if True, the user isn't waiting around for results
6004c808253f4f41f61309cf4449476e6bbbbd86631epoger    """
6014c808253f4f41f61309cf4449476e6bbbbd86631epoger    param_dict = urlparse.parse_qs(url_remainder)
6024c808253f4f41f61309cf4449476e6bbbbd86631epoger    download_all_images = (
6034c808253f4f41f61309cf4449476e6bbbbd86631epoger        param_dict.get(LIVE_PARAM__DOWNLOAD_ONLY_DIFFERING, [''])[0].lower()
6044c808253f4f41f61309cf4449476e6bbbbd86631epoger        not in ['1', 'true'])
605caa80b9a46ea00a7b582d96d73302939aa21b5eeepoger    setA_dir = param_dict[LIVE_PARAM__SET_A_DIR][0]
606caa80b9a46ea00a7b582d96d73302939aa21b5eeepoger    setB_dir = param_dict[LIVE_PARAM__SET_B_DIR][0]
6074c808253f4f41f61309cf4449476e6bbbbd86631epoger    setA_section = self._validate_summary_section(
6084c808253f4f41f61309cf4449476e6bbbbd86631epoger        param_dict.get(LIVE_PARAM__SET_A_SECTION, [None])[0])
6094c808253f4f41f61309cf4449476e6bbbbd86631epoger    setB_section = self._validate_summary_section(
6104c808253f4f41f61309cf4449476e6bbbbd86631epoger        param_dict.get(LIVE_PARAM__SET_B_SECTION, [None])[0])
6119f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger
6129f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    # If the sets show expectations vs actuals, always show expectations on
6139f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    # the left (setA).
6149f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    if ((setA_section == gm_json.JSONKEY_ACTUALRESULTS) and
6159f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger        (setB_section == gm_json.JSONKEY_EXPECTEDRESULTS)):
616caa80b9a46ea00a7b582d96d73302939aa21b5eeepoger      setA_dir, setB_dir = setB_dir, setA_dir
6179f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger      setA_section, setB_section = setB_section, setA_section
6189f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger
6199f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    # Are we comparing some actuals against expectations stored in the repo?
6209f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    # If so, we can allow the user to submit new baselines.
6219f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger    is_editable = (
6229f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger        (setA_section == gm_json.JSONKEY_EXPECTEDRESULTS) and
623caa80b9a46ea00a7b582d96d73302939aa21b5eeepoger        (setA_dir.startswith(compare_rendered_pictures.REPO_URL_PREFIX)) and
6249f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger        (setB_section == gm_json.JSONKEY_ACTUALRESULTS))
6259f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger
6264c808253f4f41f61309cf4449476e6bbbbd86631epoger    results_obj = compare_rendered_pictures.RenderedPicturesComparisons(
627caa80b9a46ea00a7b582d96d73302939aa21b5eeepoger        setA_dir=setA_dir, setB_dir=setB_dir,
6284c808253f4f41f61309cf4449476e6bbbbd86631epoger        setA_section=setA_section, setB_section=setB_section,
6294c808253f4f41f61309cf4449476e6bbbbd86631epoger        image_diff_db=_SERVER.image_diff_db,
6304c808253f4f41f61309cf4449476e6bbbbd86631epoger        diff_base_url='/static/generated-images',
6314c808253f4f41f61309cf4449476e6bbbbd86631epoger        gs=_SERVER.gs, truncate_results=_SERVER.truncate_results,
6324c808253f4f41f61309cf4449476e6bbbbd86631epoger        prefetch_only=prefetch_only, download_all_images=download_all_images)
6334c808253f4f41f61309cf4449476e6bbbbd86631epoger    if prefetch_only:
6344c808253f4f41f61309cf4449476e6bbbbd86631epoger      self.send_response(200)
6354c808253f4f41f61309cf4449476e6bbbbd86631epoger    else:
6364c808253f4f41f61309cf4449476e6bbbbd86631epoger      self.send_json_dict(results_obj.get_packaged_results_of_type(
6379f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger          results_type=results_mod.KEY__HEADER__RESULTS_ALL,
6389f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger          is_editable=is_editable))
6394c808253f4f41f61309cf4449476e6bbbbd86631epoger
6400b7127635d8245de7ac704080d722d06e47621d0epoger  def do_GET_live_results(self, url_remainder):
6410b7127635d8245de7ac704080d722d06e47621d0epoger    """ Handle a GET request for live-generated image diff data.
6420b7127635d8245de7ac704080d722d06e47621d0epoger
6430b7127635d8245de7ac704080d722d06e47621d0epoger    Args:
6440b7127635d8245de7ac704080d722d06e47621d0epoger      url_remainder: string indicating which image diffs to generate
6450b7127635d8245de7ac704080d722d06e47621d0epoger    """
6460b7127635d8245de7ac704080d722d06e47621d0epoger    logging.debug('do_GET_live_results: url_remainder="%s"' % url_remainder)
6474c808253f4f41f61309cf4449476e6bbbbd86631epoger    self._get_live_results_or_prefetch(
6484c808253f4f41f61309cf4449476e6bbbbd86631epoger        url_remainder=url_remainder, prefetch_only=False)
6490b7127635d8245de7ac704080d722d06e47621d0epoger
6503facc7c87d9f81c352c9d37b1b46340b9e745578epoger  def do_GET_prefetch_results(self, url_remainder):
6513facc7c87d9f81c352c9d37b1b46340b9e745578epoger    """ Prefetch image diff data for a future do_GET_live_results() call.
6523facc7c87d9f81c352c9d37b1b46340b9e745578epoger
6533facc7c87d9f81c352c9d37b1b46340b9e745578epoger    Args:
6543facc7c87d9f81c352c9d37b1b46340b9e745578epoger      url_remainder: string indicating which image diffs to generate
6553facc7c87d9f81c352c9d37b1b46340b9e745578epoger    """
6563facc7c87d9f81c352c9d37b1b46340b9e745578epoger    logging.debug('do_GET_prefetch_results: url_remainder="%s"' % url_remainder)
6574c808253f4f41f61309cf4449476e6bbbbd86631epoger    self._get_live_results_or_prefetch(
6584c808253f4f41f61309cf4449476e6bbbbd86631epoger        url_remainder=url_remainder, prefetch_only=True)
6593facc7c87d9f81c352c9d37b1b46340b9e745578epoger
660f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  def do_GET_static(self, path):
661a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    """ Handle a GET request for a file under STATIC_CONTENTS_SUBDIR .
662a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    Only allow serving of files within STATIC_CONTENTS_SUBDIR that is a
6639fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    filesystem sibling of this script.
6649fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com
6659fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    Args:
666a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org      path: path to file (within STATIC_CONTENTS_SUBDIR) to retrieve
6679fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    """
668dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com    # Strip arguments ('?resultsToLoad=all') from the path
669dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com    path = urlparse.urlparse(path).path
670dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com
671dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com    logging.debug('do_GET_static: sending file "%s"' % path)
672a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    static_dir = os.path.realpath(os.path.join(
673a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org        PARENT_DIRECTORY, STATIC_CONTENTS_SUBDIR))
674a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    full_path = os.path.realpath(os.path.join(static_dir, path))
675a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    if full_path.startswith(static_dir):
676cb55f11a8f8ad66cdfc7643298a8772837cc35dfepoger@google.com      self.send_file(full_path)
677cb55f11a8f8ad66cdfc7643298a8772837cc35dfepoger@google.com    else:
678dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com      logging.error(
679dcb4e65998913bfb2cc7e331ffacf0965bdee0eaepoger@google.com          'Attempted do_GET_static() of path [%s] outside of static dir [%s]'
680a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org          % (full_path, static_dir))
681cb55f11a8f8ad66cdfc7643298a8772837cc35dfepoger@google.com      self.send_error(404)
682f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
683eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com  def do_POST(self):
684eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    """ Handles all POST requests, forwarding them to the appropriate
685eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com        do_POST_* dispatcher. """
686eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    # All requests must be of this form:
687eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    #   /dispatcher
688eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    # where 'dispatcher' indicates which do_POST_* dispatcher to run.
689e6af4fb34bd780ff24e0e967a8dc73639ccc2a9dcommit-bot@chromium.org    logging.debug('do_POST: path="%s"' % self.path)
690eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    normpath = posixpath.normpath(self.path)
691eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    dispatchers = {
692eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      '/edits': self.do_POST_edits,
6932c4352bb9db02db70b829f96f397faa80fade220epoger      '/live-edits': self.do_POST_live_edits,
694eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    }
695eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    try:
696eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      dispatcher = dispatchers[normpath]
697eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      dispatcher()
698eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    except:
699eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      self.send_error(404)
700eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      raise
701eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
702eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com  def do_POST_edits(self):
703eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    """ Handle a POST request with modifications to GM expectations, in this
704eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    format:
705eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
706eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    {
70716f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org      KEY__EDITS__OLD_RESULTS_TYPE: 'all',  # type of results that the client
70816f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org                                            # loaded and then made
70916f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org                                            # modifications to
71016f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org      KEY__EDITS__OLD_RESULTS_HASH: 39850913, # hash of results when the client
71116f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org                                              # loaded them (ensures that the
71216f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org                                              # client and server apply
71316f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org                                              # modifications to the same base)
71416f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org      KEY__EDITS__MODIFICATIONS: [
715b463d5668a498b672b80047f09901981afe513edcommit-bot@chromium.org        # as needed by compare_to_expectations.edit_expectations()
716eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com        ...
717eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      ],
718eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    }
719eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
720eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    Raises an Exception if there were any problems.
721eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    """
722d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    if not _SERVER.is_editable:
723eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      raise Exception('this server is not running in --editable mode')
724eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
725eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
726eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    if content_type != 'application/json;charset=UTF-8':
727eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com      raise Exception('unsupported %s [%s]' % (
728eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com          _HTTP_HEADER_CONTENT_TYPE, content_type))
729eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
730eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH])
731eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    json_data = self.rfile.read(content_length)
732eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    data = json.loads(json_data)
733eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com    logging.debug('do_POST_edits: received new GM expectations data [%s]' %
734eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com                  data)
735eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
736d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # Update the results on disk with the information we received from the
737d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # client.
738d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # We must hold _SERVER.results_rlock while we do this, to guarantee that
739d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # no other thread updates expectations (from the Skia repo) while we are
740d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    # updating them (using the info we received from the client).
741d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com    with _SERVER.results_rlock:
74216f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org      oldResultsType = data[KEY__EDITS__OLD_RESULTS_TYPE]
743d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com      oldResults = _SERVER.results.get_results_of_type(oldResultsType)
74468a3815401f461976f76891d0477cb1440fa0abacommit-bot@chromium.org      oldResultsHash = str(hash(repr(
74568a3815401f461976f76891d0477cb1440fa0abacommit-bot@chromium.org          oldResults[imagepairset.KEY__ROOT__IMAGEPAIRS])))
74616f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org      if oldResultsHash != data[KEY__EDITS__OLD_RESULTS_HASH]:
747d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com        raise Exception('results of type "%s" changed while the client was '
748d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                        'making modifications. The client should reload the '
749d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                        'results and submit the modifications again.' %
750d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                        oldResultsType)
75116f418080ff6751e15e0193263149412de9c848acommit-bot@chromium.org      _SERVER.results.edit_expectations(data[KEY__EDITS__MODIFICATIONS])
75250ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org
75350ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org    # Read the updated results back from disk.
75450ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org    # We can do this in a separate thread; we should return our success message
75550ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org    # to the UI as soon as possible.
75650ad8e4d8efcd04f8a2c34cc32f6fecb3985a1a4commit-bot@chromium.org    thread.start_new_thread(_SERVER.update_results, (True,))
7572c4352bb9db02db70b829f96f397faa80fade220epoger    self.send_response(200)
7582c4352bb9db02db70b829f96f397faa80fade220epoger
7592c4352bb9db02db70b829f96f397faa80fade220epoger  def do_POST_live_edits(self):
7602c4352bb9db02db70b829f96f397faa80fade220epoger    """ Handle a POST request with modifications to SKP expectations, in this
7612c4352bb9db02db70b829f96f397faa80fade220epoger    format:
7622c4352bb9db02db70b829f96f397faa80fade220epoger
7632c4352bb9db02db70b829f96f397faa80fade220epoger    {
7642c4352bb9db02db70b829f96f397faa80fade220epoger      KEY__LIVE_EDITS__SET_A_DESCRIPTIONS: {
7652c4352bb9db02db70b829f96f397faa80fade220epoger        # setA descriptions from the original data
7662c4352bb9db02db70b829f96f397faa80fade220epoger      },
7672c4352bb9db02db70b829f96f397faa80fade220epoger      KEY__LIVE_EDITS__SET_B_DESCRIPTIONS: {
7682c4352bb9db02db70b829f96f397faa80fade220epoger        # setB descriptions from the original data
7692c4352bb9db02db70b829f96f397faa80fade220epoger      },
7702c4352bb9db02db70b829f96f397faa80fade220epoger      KEY__LIVE_EDITS__MODIFICATIONS: [
7712c4352bb9db02db70b829f96f397faa80fade220epoger        # as needed by writable_expectations.modify()
7722c4352bb9db02db70b829f96f397faa80fade220epoger      ],
7732c4352bb9db02db70b829f96f397faa80fade220epoger    }
7742c4352bb9db02db70b829f96f397faa80fade220epoger
7752c4352bb9db02db70b829f96f397faa80fade220epoger    Raises an Exception if there were any problems.
7762c4352bb9db02db70b829f96f397faa80fade220epoger    """
7772c4352bb9db02db70b829f96f397faa80fade220epoger    content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
7782c4352bb9db02db70b829f96f397faa80fade220epoger    if content_type != 'application/json;charset=UTF-8':
7792c4352bb9db02db70b829f96f397faa80fade220epoger      raise Exception('unsupported %s [%s]' % (
7802c4352bb9db02db70b829f96f397faa80fade220epoger          _HTTP_HEADER_CONTENT_TYPE, content_type))
7812c4352bb9db02db70b829f96f397faa80fade220epoger
7822c4352bb9db02db70b829f96f397faa80fade220epoger    content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH])
7832c4352bb9db02db70b829f96f397faa80fade220epoger    json_data = self.rfile.read(content_length)
7842c4352bb9db02db70b829f96f397faa80fade220epoger    data = json.loads(json_data)
7852c4352bb9db02db70b829f96f397faa80fade220epoger    logging.debug('do_POST_live_edits: received new GM expectations data [%s]' %
7862c4352bb9db02db70b829f96f397faa80fade220epoger                  data)
7872c4352bb9db02db70b829f96f397faa80fade220epoger    with writable_expectations_mod.WritableExpectations(
7882c4352bb9db02db70b829f96f397faa80fade220epoger        data[KEY__LIVE_EDITS__SET_A_DESCRIPTIONS]) as writable_expectations:
7892c4352bb9db02db70b829f96f397faa80fade220epoger      writable_expectations.modify(data[KEY__LIVE_EDITS__MODIFICATIONS])
7902c4352bb9db02db70b829f96f397faa80fade220epoger      diffs = writable_expectations.get_diffs()
7912c4352bb9db02db70b829f96f397faa80fade220epoger      # TODO(stephana): Move to a simpler web framework so we don't have to
7922c4352bb9db02db70b829f96f397faa80fade220epoger      # call these functions.  See http://skbug.com/2856 ('rebaseline_server:
7932c4352bb9db02db70b829f96f397faa80fade220epoger      # Refactor server to use a simple web framework')
7942c4352bb9db02db70b829f96f397faa80fade220epoger      self.send_response(200)
7952c4352bb9db02db70b829f96f397faa80fade220epoger      self.send_header('Content-type', 'text/plain')
7962c4352bb9db02db70b829f96f397faa80fade220epoger      self.end_headers()
7972c4352bb9db02db70b829f96f397faa80fade220epoger      self.wfile.write(diffs)
798eb832599b631a09b180ea3615347adba6bd5e363epoger@google.com
799f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  def redirect_to(self, url):
8009fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    """ Redirect the HTTP client to a different url.
8019fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com
8029fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    Args:
8039fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com      url: URL to redirect the HTTP client to
8049fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    """
805f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    self.send_response(301)
806f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    self.send_header('Location', url)
807f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    self.end_headers()
808f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
809f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  def send_file(self, path):
810f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    """ Send the contents of the file at this path, with a mimetype based
8119fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com        on the filename extension.
8129fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com
8139fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    Args:
8149fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com      path: path of file whose contents to send to the HTTP client
8159fb6c8ac9ce7dd5d3319b4e3affd5f1e051162a2epoger@google.com    """
816f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    # Grab the extension if there is one
817f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    extension = os.path.splitext(path)[1]
818f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    if len(extension) >= 1:
819f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com      extension = extension[1:]
820f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
821f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    # Determine the MIME type of the file from its extension
822f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP[''])
823f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
824f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    # Open the file and send it over HTTP
825f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    if os.path.isfile(path):
826f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com      with open(path, 'rb') as sending_file:
827f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        self.send_response(200)
828f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        self.send_header('Content-type', mime_type)
829f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        self.end_headers()
830f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com        self.wfile.write(sending_file.read())
831f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com    else:
832f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com      self.send_error(404)
833f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
834a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org  def send_json_dict(self, json_dict):
835a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    """ Send the contents of this dictionary in JSON format, with a JSON
836a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org        mimetype.
837a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org
838a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    Args:
839a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org      json_dict: dictionary to send
840a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    """
841a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    self.send_response(200)
842a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    self.send_header('Content-type', 'application/json')
843a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    self.end_headers()
844a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org    json.dump(json_dict, self.wfile)
845a25c4e4fefd10980e2a4701607e494513a6c60b5commit-bot@chromium.org
846e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger  def _validate_summary_section(self, section_name):
847e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger    """Validates the section we have been requested to read within JSON summary.
848e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger
849e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger    Args:
850e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger      section_name: which section of the JSON summary file has been requested
851e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger
852e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger    Returns: the validated section name
853e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger
854e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger    Raises: Exception if an invalid section_name was requested.
855e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger    """
856e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger    if section_name not in compare_rendered_pictures.ALLOWED_SECTION_NAMES:
857e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger      raise Exception('requested section name "%s" not in allowed list %s' % (
858e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger          section_name, compare_rendered_pictures.ALLOWED_SECTION_NAMES))
859e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger    return section_name
860e73cd5ab0f693ae0c81f5f1ca76045363b6f999depoger
861f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
862f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comdef main():
863a6ecbb8414b017fcb262e645fffc5a3129ecdf43commit-bot@chromium.org  logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
864a6ecbb8414b017fcb262e645fffc5a3129ecdf43commit-bot@chromium.org                      datefmt='%m/%d/%Y %H:%M:%S',
865a6ecbb8414b017fcb262e645fffc5a3129ecdf43commit-bot@chromium.org                      level=logging.INFO)
866f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  parser = argparse.ArgumentParser()
867f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  parser.add_argument('--actuals-dir',
868f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                    help=('Directory into which we will check out the latest '
869f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                          'actual GM results. If this directory does not '
870f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                          'exist, it will be created. Defaults to %(default)s'),
871f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                    default=DEFAULT_ACTUALS_DIR)
8720b7127635d8245de7ac704080d722d06e47621d0epoger  parser.add_argument('--boto',
8730b7127635d8245de7ac704080d722d06e47621d0epoger                    help=('Path to .boto file giving us credentials to access '
8740b7127635d8245de7ac704080d722d06e47621d0epoger                          'Google Storage buckets. If not specified, we will '
8750b7127635d8245de7ac704080d722d06e47621d0epoger                          'only be able to access public GS buckets (and thus '
8760b7127635d8245de7ac704080d722d06e47621d0epoger                          'won\'t be able to download SKP images).'),
8770b7127635d8245de7ac704080d722d06e47621d0epoger                    default='')
878b144271179aaf82cb1151e9dfd8e866747402594epoger  # TODO(epoger): Before https://codereview.chromium.org/310093003 ,
879b144271179aaf82cb1151e9dfd8e866747402594epoger  # when this tool downloaded the JSON summaries from skia-autogen,
880b144271179aaf82cb1151e9dfd8e866747402594epoger  # it had an --actuals-revision the caller could specify to download
881b144271179aaf82cb1151e9dfd8e866747402594epoger  # actual results as of a specific point in time.  We should add similar
882b144271179aaf82cb1151e9dfd8e866747402594epoger  # functionality when retrieving the summaries from Google Storage.
883defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org  parser.add_argument('--builders', metavar='BUILDER_REGEX', nargs='+',
884defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org                      help=('Only process builders matching these regular '
885defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org                            'expressions.  If unspecified, process all '
886defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org                            'builders.'))
88731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  parser.add_argument('--compare-configs', action='store_true',
88831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                      help=('In addition to generating differences between '
88931d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                            'expectations and actuals, also generate '
89031d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                            'differences between these config pairs: '
89131d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org                            + str(CONFIG_PAIRS_TO_COMPARE)))
892542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com  parser.add_argument('--editable', action='store_true',
8939f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger                      help=('Allow HTTP clients to submit new GM baselines; '
8949f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger                            'SKP baselines can be edited regardless of this '
8959f1ae465c89bb47f4cbf972bc946f661d6d50b4eepoger                            'setting.'))
896f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  parser.add_argument('--export', action='store_true',
897f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                      help=('Instead of only allowing access from HTTP clients '
898f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                            'on localhost, allow HTTP clients on other hosts '
899f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                            'to access this server.  WARNING: doing so will '
900f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com                            'allow users on other hosts to modify your '
901542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                            'GM expectations, if combined with --editable.'))
902b144271179aaf82cb1151e9dfd8e866747402594epoger  parser.add_argument('--gm-summaries-bucket',
903b144271179aaf82cb1151e9dfd8e866747402594epoger                    help=('Google Cloud Storage bucket to download '
904b144271179aaf82cb1151e9dfd8e866747402594epoger                          'JSON_FILENAME files from. '
905b144271179aaf82cb1151e9dfd8e866747402594epoger                          'Defaults to %(default)s ; if set to '
906b144271179aaf82cb1151e9dfd8e866747402594epoger                          'empty string, just compare to actual-results '
907b144271179aaf82cb1151e9dfd8e866747402594epoger                          'already found in ACTUALS_DIR.'),
908b144271179aaf82cb1151e9dfd8e866747402594epoger                    default=DEFAULT_GM_SUMMARIES_BUCKET)
909b144271179aaf82cb1151e9dfd8e866747402594epoger  parser.add_argument('--json-filename',
910b144271179aaf82cb1151e9dfd8e866747402594epoger                    help=('JSON summary filename to read for each builder; '
911b144271179aaf82cb1151e9dfd8e866747402594epoger                          'defaults to %(default)s.'),
912b144271179aaf82cb1151e9dfd8e866747402594epoger                    default=DEFAULT_JSON_FILENAME)
913afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com  parser.add_argument('--port', type=int,
914afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com                      help=('Which TCP port to listen on for HTTP requests; '
915afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com                            'defaults to %(default)s'),
916afaad3dd7002feb6e69983ddc4e5148d7803baedepoger@google.com                      default=DEFAULT_PORT)
917542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com  parser.add_argument('--reload', type=int,
918542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                      help=('How often (a period in seconds) to update the '
919b063e13428cef96abda9a80b273c92e2ed9c8287epoger@google.com                            'results.  If specified, both expected and actual '
920d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                            'results will be updated by running "gclient sync" '
921d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com                            'on your Skia checkout as a whole.  '
922542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                            'By default, we do not reload at all, and you '
923542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                            'must restart the server to pick up new data.'),
924542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                      default=0)
9250b7127635d8245de7ac704080d722d06e47621d0epoger  parser.add_argument('--threads', type=int,
9260b7127635d8245de7ac704080d722d06e47621d0epoger                      help=('How many parallel threads we use to download '
9270b7127635d8245de7ac704080d722d06e47621d0epoger                            'images and generate diffs; defaults to '
9280b7127635d8245de7ac704080d722d06e47621d0epoger                            '%(default)s'),
9290b7127635d8245de7ac704080d722d06e47621d0epoger                      default=imagediffdb.DEFAULT_NUM_WORKER_THREADS)
9300b7127635d8245de7ac704080d722d06e47621d0epoger  parser.add_argument('--truncate', action='store_true',
9310b7127635d8245de7ac704080d722d06e47621d0epoger                      help=('FOR TESTING ONLY: truncate the set of images we '
9320b7127635d8245de7ac704080d722d06e47621d0epoger                            'process, to speed up testing.'))
933f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  args = parser.parse_args()
93431d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  if args.compare_configs:
93531d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    config_pairs = CONFIG_PAIRS_TO_COMPARE
93631d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org  else:
93731d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org    config_pairs = None
93831d0b3d806a1aa86b7edaa442b3821f5d548e184commit-bot@chromium.org
939f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  global _SERVER
940542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com  _SERVER = Server(actuals_dir=args.actuals_dir,
941b144271179aaf82cb1151e9dfd8e866747402594epoger                   json_filename=args.json_filename,
942b144271179aaf82cb1151e9dfd8e866747402594epoger                   gm_summaries_bucket=args.gm_summaries_bucket,
943542b65f2347f571d1361df5a71844ebe24f1351cepoger@google.com                   port=args.port, export=args.export, editable=args.editable,
944defe6fdbc8edb2df0887c007450a8d8cc446f420commit-bot@chromium.org                   reload_seconds=args.reload, config_pairs=config_pairs,
9450b7127635d8245de7ac704080d722d06e47621d0epoger                   builder_regex_list=args.builders, boto_file_path=args.boto,
9460b7127635d8245de7ac704080d722d06e47621d0epoger                   imagediffdb_threads=args.threads)
9470b7127635d8245de7ac704080d722d06e47621d0epoger  if args.truncate:
9480b7127635d8245de7ac704080d722d06e47621d0epoger    _SERVER.truncate_results = True
949f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  _SERVER.run()
950f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com
951d6bab0238655dbab24dfe92bd0b16b464310a8c7rmistry@google.com
952f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.comif __name__ == '__main__':
953f9d134da93b8c78e7127efd84c9a26f99a73527eepoger@google.com  main()
954