blob: c79d79002f7f09e8aa2b49daddc147f38cf62690 [file] [log] [blame]
# Copyright (c) 2010 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Starts a local HTTP server which displays layout test failures (given a test
results directory), provides comparisons of expected and actual results (both
images and text) and allows one-click rebaselining of tests."""
from __future__ import with_statement
import codecs
import datetime
import fnmatch
import mimetypes
import os
import os.path
import shutil
import threading
import time
import urlparse
import BaseHTTPServer
from optparse import make_option
from wsgiref.handlers import format_date_time
from webkitpy.common import system
from webkitpy.layout_tests.layout_package import json_results_generator
from webkitpy.layout_tests.port import factory
from webkitpy.layout_tests.port.webkit import WebKitPort
from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand
from webkitpy.thirdparty import simplejson
STATE_NEEDS_REBASELINE = 'needs_rebaseline'
STATE_REBASELINE_FAILED = 'rebaseline_failed'
STATE_REBASELINE_SUCCEEDED = 'rebaseline_succeeded'
class RebaselineHTTPServer(BaseHTTPServer.HTTPServer):
def __init__(self, httpd_port, test_config, results_json, platforms_json):
BaseHTTPServer.HTTPServer.__init__(self, ("", httpd_port), RebaselineHTTPRequestHandler)
self.test_config = test_config
self.results_json = results_json
self.platforms_json = platforms_json
class RebaselineHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
STATIC_FILE_NAMES = frozenset([
"index.html",
"loupe.js",
"main.js",
"main.css",
"queue.js",
"util.js",
])
STATIC_FILE_DIRECTORY = os.path.join(
os.path.dirname(__file__), "data", "rebaselineserver")
def do_GET(self):
self._handle_request()
def do_POST(self):
self._handle_request()
def _handle_request(self):
# Parse input.
if "?" in self.path:
path, query_string = self.path.split("?", 1)
self.query = urlparse.parse_qs(query_string)
else:
path = self.path
self.query = {}
function_or_file_name = path[1:] or "index.html"
# See if a static file matches.
if function_or_file_name in RebaselineHTTPRequestHandler.STATIC_FILE_NAMES:
self._serve_static_file(function_or_file_name)
return
# See if a class method matches.
function_name = function_or_file_name.replace(".", "_")
if not hasattr(self, function_name):
self.send_error(404, "Unknown function %s" % function_name)
return
if function_name[0] == "_":
self.send_error(
401, "Not allowed to invoke private or protected methods")
return
function = getattr(self, function_name)
function()
def _serve_static_file(self, static_path):
self._serve_file(os.path.join(
RebaselineHTTPRequestHandler.STATIC_FILE_DIRECTORY, static_path))
def rebaseline(self):
test = self.query['test'][0]
baseline_target = self.query['baseline-target'][0]
baseline_move_to = self.query['baseline-move-to'][0]
test_json = self.server.results_json['tests'][test]
if test_json['state'] != STATE_NEEDS_REBASELINE:
self.send_error(400, "Test %s is in unexpected state: %s" %
(test, test_json["state"]))
return
log = []
success = _rebaseline_test(
test,
baseline_target,
baseline_move_to,
self.server.test_config,
log=lambda l: log.append(l))
if success:
test_json['state'] = STATE_REBASELINE_SUCCEEDED
self.send_response(200)
else:
test_json['state'] = STATE_REBASELINE_FAILED
self.send_response(500)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write('\n'.join(log))
def quitquitquit(self):
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write("Quit.\n")
# Shutdown has to happen on another thread from the server's thread,
# otherwise there's a deadlock
threading.Thread(target=lambda: self.server.shutdown()).start()
def test_result(self):
test_name, _ = os.path.splitext(self.query['test'][0])
mode = self.query['mode'][0]
if mode == 'expected-image':
file_name = test_name + '-expected.png'
elif mode == 'actual-image':
file_name = test_name + '-actual.png'
if mode == 'expected-checksum':
file_name = test_name + '-expected.checksum'
elif mode == 'actual-checksum':
file_name = test_name + '-actual.checksum'
elif mode == 'diff-image':
file_name = test_name + '-diff.png'
if mode == 'expected-text':
file_name = test_name + '-expected.txt'
elif mode == 'actual-text':
file_name = test_name + '-actual.txt'
elif mode == 'diff-text':
file_name = test_name + '-diff.txt'
elif mode == 'diff-text-pretty':
file_name = test_name + '-pretty-diff.html'
file_path = os.path.join(self.server.test_config.results_directory, file_name)
# Let results be cached for 60 seconds, so that they can be pre-fetched
# by the UI
self._serve_file(file_path, cacheable_seconds=60)
def results_json(self):
self._serve_json(self.server.results_json)
def platforms_json(self):
self._serve_json(self.server.platforms_json)
def _serve_json(self, json):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
simplejson.dump(json, self.wfile)
def _serve_file(self, file_path, cacheable_seconds=0):
if not os.path.exists(file_path):
self.send_error(404, "File not found")
return
with codecs.open(file_path, "rb") as static_file:
self.send_response(200)
self.send_header("Content-Length", os.path.getsize(file_path))
mime_type, encoding = mimetypes.guess_type(file_path)
if mime_type:
self.send_header("Content-type", mime_type)
if cacheable_seconds:
expires_time = (datetime.datetime.now() +
datetime.timedelta(0, cacheable_seconds))
expires_formatted = format_date_time(
time.mktime(expires_time.timetuple()))
self.send_header("Expires", expires_formatted)
self.end_headers()
shutil.copyfileobj(static_file, self.wfile)
class TestConfig(object):
def __init__(self, test_port, layout_tests_directory, results_directory, platforms, filesystem, scm):
self.test_port = test_port
self.layout_tests_directory = layout_tests_directory
self.results_directory = results_directory
self.platforms = platforms
self.filesystem = filesystem
self.scm = scm
def _get_actual_result_files(test_file, test_config):
test_name, _ = os.path.splitext(test_file)
test_directory = os.path.dirname(test_file)
test_results_directory = test_config.filesystem.join(
test_config.results_directory, test_directory)
actual_pattern = os.path.basename(test_name) + '-actual.*'
actual_files = []
for filename in test_config.filesystem.listdir(test_results_directory):
if fnmatch.fnmatch(filename, actual_pattern):
actual_files.append(filename)
actual_files.sort()
return tuple(actual_files)
def _rebaseline_test(test_file, baseline_target, baseline_move_to, test_config, log):
test_name, _ = os.path.splitext(test_file)
test_directory = os.path.dirname(test_name)
log('Rebaselining %s...' % test_name)
actual_result_files = _get_actual_result_files(test_file, test_config)
filesystem = test_config.filesystem
scm = test_config.scm
layout_tests_directory = test_config.layout_tests_directory
results_directory = test_config.results_directory
target_expectations_directory = filesystem.join(
layout_tests_directory, 'platform', baseline_target, test_directory)
test_results_directory = test_config.filesystem.join(
test_config.results_directory, test_directory)
# If requested, move current baselines out
current_baselines = _get_test_baselines(test_file, test_config)
if baseline_target in current_baselines and baseline_move_to != 'none':
log(' Moving current %s baselines to %s' %
(baseline_target, baseline_move_to))
# See which ones we need to move (only those that are about to be
# updated), and make sure we're not clobbering any files in the
# destination.
current_extensions = set(current_baselines[baseline_target].keys())
actual_result_extensions = [
os.path.splitext(f)[1] for f in actual_result_files]
extensions_to_move = current_extensions.intersection(
actual_result_extensions)
if extensions_to_move.intersection(
current_baselines.get(baseline_move_to, {}).keys()):
log(' Already had baselines in %s, could not move existing '
'%s ones' % (baseline_move_to, baseline_target))
return False
# Do the actual move.
if extensions_to_move:
if not _move_test_baselines(
test_file,
list(extensions_to_move),
baseline_target,
baseline_move_to,
test_config,
log):
return False
else:
log(' No current baselines to move')
log(' Updating baselines for %s' % baseline_target)
filesystem.maybe_make_directory(target_expectations_directory)
for source_file in actual_result_files:
source_path = filesystem.join(test_results_directory, source_file)
destination_file = source_file.replace('-actual', '-expected')
destination_path = filesystem.join(
target_expectations_directory, destination_file)
filesystem.copyfile(source_path, destination_path)
exit_code = scm.add(destination_path, return_exit_code=True)
if exit_code:
log(' Could not update %s in SCM, exit code %d' %
(destination_file, exit_code))
return False
else:
log(' Updated %s' % destination_file)
return True
def _move_test_baselines(test_file, extensions_to_move, source_platform, destination_platform, test_config, log):
test_file_name = os.path.splitext(os.path.basename(test_file))[0]
test_directory = os.path.dirname(test_file)
filesystem = test_config.filesystem
# Want predictable output order for unit tests.
extensions_to_move.sort()
source_directory = os.path.join(
test_config.layout_tests_directory,
'platform',
source_platform,
test_directory)
destination_directory = os.path.join(
test_config.layout_tests_directory,
'platform',
destination_platform,
test_directory)
filesystem.maybe_make_directory(destination_directory)
for extension in extensions_to_move:
file_name = test_file_name + '-expected' + extension
source_path = filesystem.join(source_directory, file_name)
destination_path = filesystem.join(destination_directory, file_name)
filesystem.copyfile(source_path, destination_path)
exit_code = test_config.scm.add(destination_path, return_exit_code=True)
if exit_code:
log(' Could not update %s in SCM, exit code %d' %
(file_name, exit_code))
return False
else:
log(' Moved %s' % file_name)
return True
def _get_test_baselines(test_file, test_config):
class AllPlatformsPort(WebKitPort):
def __init__(self):
WebKitPort.__init__(self, filesystem=test_config.filesystem)
self._platforms_by_directory = dict(
[(self._webkit_baseline_path(p), p) for p in test_config.platforms])
def baseline_search_path(self):
return self._platforms_by_directory.keys()
def platform_from_directory(self, directory):
return self._platforms_by_directory[directory]
test_path = test_config.filesystem.join(
test_config.layout_tests_directory, test_file)
all_platforms_port = AllPlatformsPort()
all_test_baselines = {}
for baseline_extension in ('.txt', '.checksum', '.png'):
test_baselines = test_config.test_port.expected_baselines(
test_path, baseline_extension)
baselines = all_platforms_port.expected_baselines(
test_path, baseline_extension, all_baselines=True)
for platform_directory, expected_filename in baselines:
if not platform_directory:
continue
if platform_directory == test_config.layout_tests_directory:
platform = 'base'
else:
platform = all_platforms_port.platform_from_directory(
platform_directory)
platform_baselines = all_test_baselines.setdefault(platform, {})
was_used_for_test = (
platform_directory, expected_filename) in test_baselines
platform_baselines[baseline_extension] = was_used_for_test
return all_test_baselines
class RebaselineServer(AbstractDeclarativeCommand):
name = "rebaseline-server"
help_text = __doc__
argument_names = "/path/to/results/directory"
def __init__(self):
options = [
make_option("--httpd-port", action="store", type="int", default=8127, help="Port to use for the the rebaseline HTTP server"),
]
AbstractDeclarativeCommand.__init__(self, options=options)
def execute(self, options, args, tool):
results_directory = args[0]
filesystem = system.filesystem.FileSystem()
scm = self._tool.scm()
if options.dry_run:
def no_op_copyfile(src, dest):
pass
def no_op_add(path, return_exit_code=False):
if return_exit_code:
return 0
filesystem.copyfile = no_op_copyfile
scm.add = no_op_add
print 'Parsing unexpected_results.json...'
results_json_path = filesystem.join(results_directory, 'unexpected_results.json')
results_json = json_results_generator.load_json(filesystem, results_json_path)
port = factory.get()
layout_tests_directory = port.layout_tests_dir()
platforms = filesystem.listdir(
filesystem.join(layout_tests_directory, 'platform'))
test_config = TestConfig(
port,
layout_tests_directory,
results_directory,
platforms,
filesystem,
scm)
print 'Gathering current baselines...'
for test_file, test_json in results_json['tests'].items():
test_json['state'] = STATE_NEEDS_REBASELINE
test_path = filesystem.join(layout_tests_directory, test_file)
test_json['baselines'] = _get_test_baselines(test_file, test_config)
server_url = "http://localhost:%d/" % options.httpd_port
print "Starting server at %s" % server_url
print ("Use the 'Exit' link in the UI, %squitquitquit "
"or Ctrl-C to stop") % server_url
threading.Timer(
.1, lambda: self._tool.user.open_url(server_url)).start()
httpd = RebaselineHTTPServer(
httpd_port=options.httpd_port,
test_config=test_config,
results_json=results_json,
platforms_json={
'platforms': platforms,
'defaultPlatform': port.name(),
})
httpd.serve_forever()