From 28040489d744e0c5d475a88663056c9040ed5320 Mon Sep 17 00:00:00 2001 From: Teng-Hui Zhu Date: Wed, 10 Nov 2010 15:31:59 -0800 Subject: Merge WebKit at r71558: Initial merge by git. Change-Id: Ib345578fa29df7e4bc72b4f00e4a6fddcb754c4c --- WebKitTools/Scripts/check-webkit-style | 5 +- WebKitTools/Scripts/old-run-webkit-tests | 9 +- .../Scripts/webkitpy/common/checkout/scm.py | 2 +- .../Scripts/webkitpy/common/config/committers.py | 4 +- .../Scripts/webkitpy/common/net/bugzilla.py | 4 +- .../Scripts/webkitpy/common/system/executive.py | 109 ++++++++++--- .../webkitpy/common/system/executive_unittest.py | 77 ++++++--- .../Scripts/webkitpy/common/system/filesystem.py | 117 ++++++++++++++ .../webkitpy/common/system/filesystem_unittest.py | 157 ++++++++++++++++++ .../Scripts/webkitpy/common/system/fileutils.py | 33 ++++ WebKitTools/Scripts/webkitpy/common/system/path.py | 8 +- .../layout_package/dump_render_tree_thread.py | 39 +++-- .../Scripts/webkitpy/layout_tests/port/base.py | 21 ++- .../webkitpy/layout_tests/port/chromium_gpu.py | 11 ++ .../layout_tests/port/chromium_gpu_unittest.py | 6 +- .../Scripts/webkitpy/layout_tests/port/config.py | 154 ++++++++++++++++++ .../webkitpy/layout_tests/port/config_unittest.py | 176 +++++++++++++++++++++ .../webkitpy/layout_tests/port/http_lock.py | 47 ++++-- .../Scripts/webkitpy/layout_tests/port/webkit.py | 9 +- .../rebaseline_chromium_webkit_tests.py | 31 ++-- .../rebaseline_chromium_webkit_tests_unittest.py | 9 ++ .../webkitpy/layout_tests/run_webkit_tests.py | 57 +++---- .../layout_tests/run_webkit_tests_unittest.py | 146 ++++++++++++----- WebKitTools/Scripts/webkitpy/style/checker.py | 8 +- WebKitTools/Scripts/webkitpy/style/optparser.py | 15 +- .../Scripts/webkitpy/style/optparser_unittest.py | 8 +- WebKitTools/Scripts/webkitpy/style_references.py | 6 +- WebKitTools/Scripts/webkitpy/test/cat.py | 42 +++++ WebKitTools/Scripts/webkitpy/test/cat_unittest.py | 52 ++++++ WebKitTools/Scripts/webkitpy/test/echo.py | 52 ++++++ WebKitTools/Scripts/webkitpy/test/echo_unittest.py | 64 ++++++++ WebKitTools/Scripts/webkitpy/test/skip.py | 52 ++++++ WebKitTools/Scripts/webkitpy/test/skip_unittest.py | 77 +++++++++ .../Scripts/webkitpy/tool/commands/__init__.py | 1 + .../tool/commands/data/rebaselineserver/index.html | 40 +++++ .../tool/commands/data/rebaselineserver/main.css | 56 +++++++ .../tool/commands/data/rebaselineserver/main.js | 36 +++++ .../webkitpy/tool/commands/rebaselineserver.py | 157 ++++++++++++++++++ .../Scripts/webkitpy/tool/commands/upload.py | 26 +++ WebKitTools/Scripts/webkitpy/tool/mocktool.py | 3 + .../Scripts/webkitpy/tool/steps/checkstyle.py | 3 + 41 files changed, 1735 insertions(+), 194 deletions(-) create mode 100644 WebKitTools/Scripts/webkitpy/common/system/filesystem.py create mode 100644 WebKitTools/Scripts/webkitpy/common/system/filesystem_unittest.py create mode 100644 WebKitTools/Scripts/webkitpy/common/system/fileutils.py create mode 100644 WebKitTools/Scripts/webkitpy/layout_tests/port/config.py create mode 100644 WebKitTools/Scripts/webkitpy/layout_tests/port/config_unittest.py create mode 100644 WebKitTools/Scripts/webkitpy/test/cat.py create mode 100644 WebKitTools/Scripts/webkitpy/test/cat_unittest.py create mode 100644 WebKitTools/Scripts/webkitpy/test/echo.py create mode 100644 WebKitTools/Scripts/webkitpy/test/echo_unittest.py create mode 100644 WebKitTools/Scripts/webkitpy/test/skip.py create mode 100644 WebKitTools/Scripts/webkitpy/test/skip_unittest.py create mode 100644 WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html create mode 100644 WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.css create mode 100644 WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.js create mode 100644 WebKitTools/Scripts/webkitpy/tool/commands/rebaselineserver.py (limited to 'WebKitTools/Scripts') diff --git a/WebKitTools/Scripts/check-webkit-style b/WebKitTools/Scripts/check-webkit-style index e29d4b1..076c712 100755 --- a/WebKitTools/Scripts/check-webkit-style +++ b/WebKitTools/Scripts/check-webkit-style @@ -112,10 +112,11 @@ def main(): file_reader = TextFileReader(style_processor) - if paths: + if paths and not options.diff_files: file_reader.process_paths(paths) else: - patch = checkout.create_patch(options.git_commit) + changed_files = paths if options.diff_files else None + patch = checkout.create_patch(options.git_commit, changed_files=changed_files) patch_checker = PatchReader(file_reader) patch_checker.check(patch) diff --git a/WebKitTools/Scripts/old-run-webkit-tests b/WebKitTools/Scripts/old-run-webkit-tests index e40b3ad..2cef18b 100755 --- a/WebKitTools/Scripts/old-run-webkit-tests +++ b/WebKitTools/Scripts/old-run-webkit-tests @@ -373,6 +373,8 @@ if ($useWebKitTestRunner) { } } +$timeoutSeconds *= 10 if $guardMalloc; + $stripEditingCallbacks = isCygwin() unless defined $stripEditingCallbacks; my $ignoreSkipped = $treatSkipped eq "ignore"; @@ -1483,6 +1485,12 @@ sub openDumpTool() unshift @args, "valgrind", "--suppressions=$platformBaseDirectory/qt/SuppressedValgrindErrors"; } + if ($useWebKitTestRunner) { + # Make WebKitTestRunner use a similar timeout. We don't use the exact same timeout to avoid + # race conditions. + push @args, "--timeout", $timeoutSeconds - 5; + } + $CLEAN_ENV{MallocStackLogging} = 1 if $shouldCheckLeaks; $dumpToolPID = open3(\*OUT, \*IN, \*ERROR, launchWithEnv(@args, %CLEAN_ENV)) or die "Failed to start tool: $dumpTool\n"; @@ -2089,7 +2097,6 @@ sub readFromDumpToolWithTimer(**) setFileHandleNonBlocking($fhError, 1); my $maximumSecondsWithoutOutput = $timeoutSeconds; - $maximumSecondsWithoutOutput *= 10 if $guardMalloc; my $microsecondsToWaitBeforeReadingAgain = 1000; my $timeOfLastSuccessfulRead = time; diff --git a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py index 9b602c3..8aadcb8 100644 --- a/WebKitTools/Scripts/webkitpy/common/checkout/scm.py +++ b/WebKitTools/Scripts/webkitpy/common/checkout/scm.py @@ -64,7 +64,7 @@ def default_scm(): cwd = os.getcwd() scm_system = detect_scm_system(cwd) if not scm_system: - script_directory = os.path.abspath(sys.path[0]) + script_directory = os.path.dirname(os.path.abspath(__file__)) scm_system = detect_scm_system(script_directory) if scm_system: log("The current directory (%s) is not a WebKit checkout, using %s" % (cwd, scm_system.checkout_root)) diff --git a/WebKitTools/Scripts/webkitpy/common/config/committers.py b/WebKitTools/Scripts/webkitpy/common/config/committers.py index 71d764c..446c2b1 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/committers.py +++ b/WebKitTools/Scripts/webkitpy/common/config/committers.py @@ -112,6 +112,7 @@ committers_unable_to_review = [ Committer("Girish Ramakrishnan", ["girish@forwardbias.in", "ramakrishnan.girish@gmail.com"]), Committer("Graham Dennis", ["Graham.Dennis@gmail.com", "gdennis@webkit.org"]), Committer("Greg Bolsinga", "bolsinga@apple.com"), + Committer("Gyuyoung Kim", ["gyuyoung.kim@samsung.com", "gyuyoung@gmail.com", "gyuyoung@webkit.org"], "gyuyoung"), Committer("Hans Wennborg", "hans@chromium.org", "hwennborg"), Committer("Hayato Ito", "hayato@chromium.org", "hayato"), Committer("Hin-Chung Lam", ["hclam@google.com", "hclam@chromium.org"]), @@ -147,7 +148,7 @@ committers_unable_to_review = [ Committer("Luiz Agostini", ["luiz@webkit.org", "luiz.agostini@openbossa.org"], "lca"), Committer("Mads Ager", "ager@chromium.org"), Committer("Marcus Voltis Bulach", "bulach@chromium.org"), - Committer("Mario Sanchez Prada", ["msanchez@igalia.com", "mario@webkit.org"]), + Committer("Mario Sanchez Prada", ["msanchez@igalia.com", "mario@webkit.org"], "msanchez"), Committer("Matt Delaney", "mdelaney@apple.com"), Committer("Matt Lilek", ["webkit@mattlilek.com", "pewtermoose@webkit.org"]), Committer("Matt Perry", "mpcomplete@chromium.org"), @@ -171,6 +172,7 @@ committers_unable_to_review = [ Committer("Philippe Normand", ["pnormand@igalia.com", "philn@webkit.org"], "philn-tp"), Committer("Pierre d'Herbemont", ["pdherbemont@free.fr", "pdherbemont@apple.com"], "pdherbemont"), Committer("Pierre-Olivier Latour", "pol@apple.com", "pol"), + Committer("Renata Hodovan", "reni@webkit.org", "reni"), Committer("Robert Hogan", ["robert@webkit.org", "robert@roberthogan.net", "lists@roberthogan.net"], "mwenge"), Committer("Roland Steiner", "rolandsteiner@chromium.org"), Committer("Ryosuke Niwa", "rniwa@webkit.org", "rniwa"), diff --git a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py index 1cc8e2e..a7dc1b7 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py +++ b/WebKitTools/Scripts/webkitpy/common/net/bugzilla.py @@ -281,7 +281,7 @@ class BugzillaQueries(object): return sum([self._fetch_bug(bug_id).commit_queued_patches() for bug_id in self.fetch_bug_ids_from_commit_queue()], []) - def _fetch_bug_ids_from_review_queue(self): + def fetch_bug_ids_from_review_queue(self): review_queue_url = "buglist.cgi?query_format=advanced&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&field0-0-0=flagtypes.name&type0-0-0=equals&value0-0-0=review?" return self._fetch_bug_ids_advanced_query(review_queue_url) @@ -289,7 +289,7 @@ class BugzillaQueries(object): def fetch_patches_from_review_queue(self, limit=None): # [:None] returns the whole array. return sum([self._fetch_bug(bug_id).unreviewed_patches() - for bug_id in self._fetch_bug_ids_from_review_queue()[:limit]], []) + for bug_id in self.fetch_bug_ids_from_review_queue()[:limit]], []) # NOTE: This is the only client of _fetch_attachment_ids_request_query # This method only makes one request to bugzilla. diff --git a/WebKitTools/Scripts/webkitpy/common/system/executive.py b/WebKitTools/Scripts/webkitpy/common/system/executive.py index 216cf58..37f4e53 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/executive.py +++ b/WebKitTools/Scripts/webkitpy/common/system/executive.py @@ -33,6 +33,7 @@ try: except ImportError: multiprocessing = None +import ctypes import errno import logging import os @@ -44,6 +45,7 @@ import sys import time from webkitpy.common.system.deprecated_logging import tee +from webkitpy.python24 import versioning _log = logging.getLogger("webkitpy.common.system") @@ -103,13 +105,8 @@ class Executive(object): def _run_command_with_teed_output(self, args, teed_output): args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) - if sys.platform == 'cygwin': - # Cygwin's Python's os.execv doesn't support unicode command - # arguments, and neither does Cygwin's execv itself. - # FIXME: Using UTF-8 here will confuse Windows-native commands - # which will expect arguments to be encoded using the current code - # page. - args = [arg.encode('utf-8') for arg in args] + args = map(self._encode_argument_if_needed, args) + child_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -148,9 +145,8 @@ class Executive(object): child_output = child_out_file.getvalue() child_out_file.close() - # We assume the child process output utf-8 if decode_output: - child_output = child_output.decode("utf-8") + child_output = child_output.decode(self._child_process_encoding()) if exit_code: raise ScriptError(script_args=args, @@ -205,6 +201,55 @@ class Executive(object): return raise + def _win32_check_running_pid(self): + + class PROCESSENTRY32(ctypes.Structure): + _fields_ = [("dwSize", ctypes.c_ulong), + ("cntUsage", ctypes.c_ulong), + ("th32ProcessID", ctypes.c_ulong), + ("th32DefaultHeapID", ctypes.c_ulong), + ("th32ModuleID", ctypes.c_ulong), + ("cntThreads", ctypes.c_ulong), + ("th32ParentProcessID", ctypes.c_ulong), + ("pcPriClassBase", ctypes.c_ulong), + ("dwFlags", ctypes.c_ulong), + ("szExeFile", ctypes.c_char * 260)] + + CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot + Process32First = ctypes.windll.kernel32.Process32First + Process32Next = ctypes.windll.kernel32.Process32Next + CloseHandle = ctypes.windll.kernel32.CloseHandle + TH32CS_SNAPPROCESS = 0x00000002 # win32 magic number + hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) + pe32 = PROCESSENTRY32() + pe32.dwSize = ctypes.sizeof(PROCESSENTRY32) + result = False + if not Process32First(hProcessSnap, ctypes.byref(pe32)): + _log.debug("Failed getting first process.") + CloseHandle(hProcessSnap) + return result + while True: + if pe32.th32ProcessID == pid: + result = True + break + if not Process32Next(hProcessSnap, ctypes.byref(pe32)): + break + CloseHandle(hProcessSnap) + return result + + def check_running_pid(self, pid): + """Return True if pid is alive, otherwise return False.""" + if sys.platform in ('darwin', 'linux2', 'cygwin'): + try: + os.kill(pid, 0) + return True + except OSError: + return False + elif sys.platform == 'win32': + return self._win32_check_running_pid() + + assert(False) + def _windows_image_name(self, process_name): name, extension = os.path.splitext(process_name) if not extension: @@ -260,7 +305,7 @@ class Executive(object): # for an example of a regresion caused by passing a unicode string directly. # FIXME: We may need to encode differently on different platforms. if isinstance(input, unicode): - input = input.encode("utf-8") + input = input.encode(self._child_process_encoding()) return (subprocess.PIPE, input) def _command_for_printing(self, args): @@ -288,13 +333,8 @@ class Executive(object): assert(isinstance(args, list) or isinstance(args, tuple)) start_time = time.time() args = map(unicode, args) # Popen will throw an exception if args are non-strings (like int()) - if sys.platform == 'cygwin': - # Cygwin's Python's os.execv doesn't support unicode command - # arguments, and neither does Cygwin's execv itself. - # FIXME: Using UTF-8 here will confuse Windows-native commands - # which will expect arguments to be encoded using the current code - # page. - args = [arg.encode('utf-8') for arg in args] + args = map(self._encode_argument_if_needed, args) + stdin, string_to_communicate = self._compute_stdin(input) stderr = subprocess.STDOUT if return_stderr else None @@ -305,9 +345,11 @@ class Executive(object): cwd=cwd, close_fds=self._should_close_fds()) output = process.communicate(string_to_communicate)[0] + # run_command automatically decodes to unicode() unless explicitly told not to. if decode_output: - output = output.decode("utf-8") + output = output.decode(self._child_process_encoding()) + # wait() is not threadsafe and can throw OSError due to: # http://bugs.python.org/issue1731717 exit_code = process.wait() @@ -324,3 +366,34 @@ class Executive(object): cwd=cwd) (error_handler or self.default_error_handler)(script_error) return output + + def _child_process_encoding(self): + # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW + # to launch subprocesses, so we have to encode arguments using the + # current code page. + if sys.platform == 'win32' and versioning.compare_version(sys, '3.0')[0] < 0: + return 'mbcs' + # All other platforms use UTF-8. + # FIXME: Using UTF-8 on Cygwin will confuse Windows-native commands + # which will expect arguments to be encoded using the current code + # page. + return 'utf-8' + + def _should_encode_child_process_arguments(self): + # Cygwin's Python's os.execv doesn't support unicode command + # arguments, and neither does Cygwin's execv itself. + if sys.platform == 'cygwin': + return True + + # Win32 Python 2.x uses CreateProcessA rather than CreateProcessW + # to launch subprocesses, so we have to encode arguments using the + # current code page. + if sys.platform == 'win32' and versioning.compare_version(sys, '3.0')[0] < 0: + return True + + return False + + def _encode_argument_if_needed(self, argument): + if not self._should_encode_child_process_arguments(): + return argument + return argument.encode(self._child_process_encoding()) diff --git a/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py index 32f8f51..b8fd82e 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/system/executive_unittest.py @@ -27,12 +27,23 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import os import signal import subprocess import sys import unittest from webkitpy.common.system.executive import Executive, run_command, ScriptError +from webkitpy.test import cat, echo + + +def never_ending_command(): + """Arguments for a command that will never end (useful for testing process + killing). It should be a process that is unlikely to already be running + because all instances will be killed.""" + if sys.platform == 'win32': + return ['wmic'] + return ['yes'] class ExecutiveTest(unittest.TestCase): @@ -46,46 +57,56 @@ class ExecutiveTest(unittest.TestCase): executive = Executive() self.assertRaises(AssertionError, executive.run_command, "echo") self.assertRaises(AssertionError, executive.run_command, u"echo") - executive.run_command(["echo", "foo"]) - executive.run_command(("echo", "foo")) + executive.run_command(echo.command_arguments('foo')) + executive.run_command(tuple(echo.command_arguments('foo'))) def test_run_command_with_unicode(self): """Validate that it is safe to pass unicode() objects to Executive.run* methods, and they will return unicode() objects by default unless decode_output=False""" + unicode_tor_input = u"WebKit \u2661 Tor Arne Vestb\u00F8!" + if sys.platform == 'win32': + encoding = 'mbcs' + else: + encoding = 'utf-8' + encoded_tor = unicode_tor_input.encode(encoding) + # On Windows, we expect the unicode->mbcs->unicode roundtrip to be + # lossy. On other platforms, we expect a lossless roundtrip. + if sys.platform == 'win32': + unicode_tor_output = encoded_tor.decode(encoding) + else: + unicode_tor_output = unicode_tor_input + executive = Executive() - unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!" - utf8_tor = unicode_tor.encode("utf-8") - output = executive.run_command(["cat"], input=unicode_tor) - self.assertEquals(output, unicode_tor) + output = executive.run_command(cat.command_arguments(), input=unicode_tor_input) + self.assertEquals(output, unicode_tor_output) - output = executive.run_command(["echo", "-n", unicode_tor]) - self.assertEquals(output, unicode_tor) + output = executive.run_command(echo.command_arguments("-n", unicode_tor_input)) + self.assertEquals(output, unicode_tor_output) - output = executive.run_command(["echo", "-n", unicode_tor], decode_output=False) - self.assertEquals(output, utf8_tor) + output = executive.run_command(echo.command_arguments("-n", unicode_tor_input), decode_output=False) + self.assertEquals(output, encoded_tor) # Make sure that str() input also works. - output = executive.run_command(["cat"], input=utf8_tor, decode_output=False) - self.assertEquals(output, utf8_tor) + output = executive.run_command(cat.command_arguments(), input=encoded_tor, decode_output=False) + self.assertEquals(output, encoded_tor) # FIXME: We should only have one run* method to test - output = executive.run_and_throw_if_fail(["echo", "-n", unicode_tor], quiet=True) - self.assertEquals(output, unicode_tor) + output = executive.run_and_throw_if_fail(echo.command_arguments("-n", unicode_tor_input), quiet=True) + self.assertEquals(output, unicode_tor_output) - output = executive.run_and_throw_if_fail(["echo", "-n", unicode_tor], quiet=True, decode_output=False) - self.assertEquals(output, utf8_tor) + output = executive.run_and_throw_if_fail(echo.command_arguments("-n", unicode_tor_input), quiet=True, decode_output=False) + self.assertEquals(output, encoded_tor) def test_kill_process(self): executive = Executive() - # We use "yes" because it loops forever. - process = subprocess.Popen(["yes"], stdout=subprocess.PIPE) + process = subprocess.Popen(never_ending_command(), stdout=subprocess.PIPE) self.assertEqual(process.poll(), None) # Process is running executive.kill_process(process.pid) # Note: Can't use a ternary since signal.SIGKILL is undefined for sys.platform == "win32" if sys.platform == "win32": - expected_exit_code = 0 # taskkill.exe results in exit(0) + expected_exit_code = 1 else: expected_exit_code = -signal.SIGKILL self.assertEqual(process.wait(), expected_exit_code) @@ -109,14 +130,22 @@ class ExecutiveTest(unittest.TestCase): def test_kill_all(self): executive = Executive() # We use "yes" because it loops forever. - process = subprocess.Popen(["yes"], stdout=subprocess.PIPE) + process = subprocess.Popen(never_ending_command(), stdout=subprocess.PIPE) self.assertEqual(process.poll(), None) # Process is running - executive.kill_all("yes") + executive.kill_all(never_ending_command()[0]) # Note: Can't use a ternary since signal.SIGTERM is undefined for sys.platform == "win32" - if sys.platform in ("win32", "cygwin"): - expected_exit_code = 0 # taskkill.exe results in exit(0) + if sys.platform == "cygwin": + expected_exit_code = 0 # os.kill results in exit(0) for this process. + elif sys.platform == "win32": + expected_exit_code = 1 else: expected_exit_code = -signal.SIGTERM self.assertEqual(process.wait(), expected_exit_code) # Killing again should fail silently. - executive.kill_all("yes") + executive.kill_all(never_ending_command()[0]) + + def test_check_running_pid(self): + executive = Executive() + self.assertTrue(executive.check_running_pid(os.getpid())) + # Maximum pid number on Linux is 32768 by default + self.assertFalse(executive.check_running_pid(100000)) diff --git a/WebKitTools/Scripts/webkitpy/common/system/filesystem.py b/WebKitTools/Scripts/webkitpy/common/system/filesystem.py new file mode 100644 index 0000000..c7efde3 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/system/filesystem.py @@ -0,0 +1,117 @@ +# 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. + +"""Wrapper object for the file system / source tree.""" + +from __future__ import with_statement + +import codecs +import errno +import os +import tempfile + + +class FileSystem(object): + """FileSystem interface for webkitpy. + + Unless otherwise noted, all paths are allowed to be either absolute + or relative.""" + + def exists(self, path): + """Return whether the path exists in the filesystem.""" + return os.path.exists(path) + + def isdir(self, path): + """Return whether the path refers to a directory.""" + return os.path.isdir(path) + + def join(self, *comps): + """Return the path formed by joining the components.""" + return os.path.join(*comps) + + def listdir(self, path): + """Return the contents of the directory pointed to by path.""" + return os.listdir(path) + + def mkdtemp(self, **kwargs): + """Create and return a uniquely named directory. + + This is like tempfile.mkdtemp, but if used in a with statement + the directory will self-delete at the end of the block (if the + directory is empty; non-empty directories raise errors). The + directory can be safely deleted inside the block as well, if so + desired.""" + class TemporaryDirectory(object): + def __init__(self, **kwargs): + self._kwargs = kwargs + self._directory_path = None + + def __enter__(self): + self._directory_path = tempfile.mkdtemp(**self._kwargs) + return self._directory_path + + def __exit__(self, type, value, traceback): + # Only self-delete if necessary. + + # FIXME: Should we delete non-empty directories? + if os.path.exists(self._directory_path): + os.rmdir(self._directory_path) + + return TemporaryDirectory(**kwargs) + + def maybe_make_directory(self, *path): + """Create the specified directory if it doesn't already exist.""" + try: + os.makedirs(os.path.join(*path)) + except OSError, e: + if e.errno != errno.EEXIST: + raise + + def read_binary_file(self, path): + """Return the contents of the file at the given path as a byte string.""" + with file(path, 'rb') as f: + return f.read() + + def read_text_file(self, path): + """Return the contents of the file at the given path as a Unicode string. + + The file is read assuming it is a UTF-8 encoded file with no BOM.""" + with codecs.open(path, 'r', 'utf8') as f: + return f.read() + + def write_binary_file(self, path, contents): + """Write the contents to the file at the given location.""" + with file(path, 'wb') as f: + f.write(contents) + + def write_text_file(self, path, contents): + """Write the contents to the file at the given location. + + The file is written encoded as UTF-8 with no BOM.""" + with codecs.open(path, 'w', 'utf8') as f: + f.write(contents) diff --git a/WebKitTools/Scripts/webkitpy/common/system/filesystem_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/filesystem_unittest.py new file mode 100644 index 0000000..95684b7 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/system/filesystem_unittest.py @@ -0,0 +1,157 @@ +# vim: set fileencoding=utf-8 : +# 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. + +# NOTE: The fileencoding comment on the first line of the file is +# important; without it, Python will choke while trying to parse the file, +# since it includes non-ASCII characters. + +from __future__ import with_statement + +import os +import stat +import sys +import tempfile +import unittest + +from filesystem import FileSystem + + +class FileSystemTest(unittest.TestCase): + def setUp(self): + self._this_dir = os.path.dirname(os.path.abspath(__file__)) + self._missing_file = os.path.join(self._this_dir, 'missing_file.py') + self._this_file = os.path.join(self._this_dir, 'filesystem_unittest.py') + + def test_exists__true(self): + fs = FileSystem() + self.assertTrue(fs.exists(self._this_file)) + + def test_exists__false(self): + fs = FileSystem() + self.assertFalse(fs.exists(self._missing_file)) + + def test_isdir__true(self): + fs = FileSystem() + self.assertTrue(fs.isdir(self._this_dir)) + + def test_isdir__false(self): + fs = FileSystem() + self.assertFalse(fs.isdir(self._this_file)) + + def test_join(self): + fs = FileSystem() + self.assertEqual(fs.join('foo', 'bar'), + os.path.join('foo', 'bar')) + + def test_listdir(self): + fs = FileSystem() + with fs.mkdtemp(prefix='filesystem_unittest_') as d: + self.assertEqual(fs.listdir(d), []) + new_file = os.path.join(d, 'foo') + fs.write_text_file(new_file, u'foo') + self.assertEqual(fs.listdir(d), ['foo']) + os.remove(new_file) + + def test_maybe_make_directory__success(self): + fs = FileSystem() + + with fs.mkdtemp(prefix='filesystem_unittest_') as base_path: + sub_path = os.path.join(base_path, "newdir") + self.assertFalse(os.path.exists(sub_path)) + self.assertFalse(fs.isdir(sub_path)) + + fs.maybe_make_directory(sub_path) + self.assertTrue(os.path.exists(sub_path)) + self.assertTrue(fs.isdir(sub_path)) + + # Make sure we can re-create it. + fs.maybe_make_directory(sub_path) + self.assertTrue(os.path.exists(sub_path)) + self.assertTrue(fs.isdir(sub_path)) + + # Clean up. + os.rmdir(sub_path) + + self.assertFalse(os.path.exists(base_path)) + self.assertFalse(fs.isdir(base_path)) + + def test_maybe_make_directory__failure(self): + # FIXME: os.chmod() doesn't work on Windows to set directories + # as readonly, so we skip this test for now. + if sys.platform in ('win32', 'cygwin'): + return + + fs = FileSystem() + with fs.mkdtemp(prefix='filesystem_unittest_') as d: + # Remove write permissions on the parent directory. + os.chmod(d, stat.S_IRUSR) + + # Now try to create a sub directory - should fail. + sub_dir = fs.join(d, 'subdir') + self.assertRaises(OSError, fs.maybe_make_directory, sub_dir) + + # Clean up in case the test failed and we did create the + # directory. + if os.path.exists(sub_dir): + os.rmdir(sub_dir) + + def test_read_and_write_file(self): + fs = FileSystem() + text_path = None + binary_path = None + + unicode_text_string = u'Ūnĭcōde̽' + hex_equivalent = '\xC5\xAA\x6E\xC4\xAD\x63\xC5\x8D\x64\x65\xCC\xBD' + try: + text_path = tempfile.mktemp(prefix='tree_unittest_') + binary_path = tempfile.mktemp(prefix='tree_unittest_') + fs.write_text_file(text_path, unicode_text_string) + contents = fs.read_binary_file(text_path) + self.assertEqual(contents, hex_equivalent) + + fs.write_text_file(binary_path, hex_equivalent) + text_contents = fs.read_text_file(binary_path) + self.assertEqual(text_contents, unicode_text_string) + except: + if text_path: + os.remove(text_path) + if binary_path: + os.remove(binary_path) + + def test_read_binary_file__missing(self): + fs = FileSystem() + self.assertRaises(IOError, fs.read_binary_file, self._missing_file) + + def test_read_text_file__missing(self): + fs = FileSystem() + self.assertRaises(IOError, fs.read_text_file, self._missing_file) + + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/common/system/fileutils.py b/WebKitTools/Scripts/webkitpy/common/system/fileutils.py new file mode 100644 index 0000000..55821f8 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/system/fileutils.py @@ -0,0 +1,33 @@ +# Copyright (C) 2010 Apple 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + +import sys + + +def make_stdout_binary(): + """Puts sys.stdout into binary mode (on platforms that have a distinction + between text and binary mode).""" + if sys.platform != 'win32' or not hasattr(sys.stdout, 'fileno'): + return + import msvcrt + import os + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) diff --git a/WebKitTools/Scripts/webkitpy/common/system/path.py b/WebKitTools/Scripts/webkitpy/common/system/path.py index 43c6410..09787d7 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/path.py +++ b/WebKitTools/Scripts/webkitpy/common/system/path.py @@ -44,7 +44,7 @@ def abspath_to_uri(path, platform=None): def cygpath(path): - """Converts a cygwin path to Windows path.""" + """Converts an absolute cygwin path to an absolute Windows path.""" return _CygPath.convert_using_singleton(path) @@ -103,7 +103,11 @@ class _CygPath(object): self.start() self._child_process.stdin.write("%s\r\n" % path) self._child_process.stdin.flush() - return self._child_process.stdout.readline().rstrip() + windows_path = self._child_process.stdout.readline().rstrip() + # Some versions of cygpath use lowercase drive letters while others + # use uppercase. We always convert to uppercase for consistency. + windows_path = '%s%s' % (windows_path[0].upper(), windows_path[1:]) + return windows_path def _escape(path): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py index 3e3ba0b..9f2de7e 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/layout_package/dump_render_tree_thread.py @@ -341,7 +341,7 @@ class TestShellThread(WatchableThread): def cancel(self): """Clean up http lock and set a flag telling this thread to quit.""" - self._stop_http_lock() + self._stop_servers_with_lock() WatchableThread.cancel(self) def next_timeout(self): @@ -389,23 +389,15 @@ class TestShellThread(WatchableThread): self._current_group, self._filename_list = \ self._filename_list_queue.get_nowait() except Queue.Empty: - self._stop_http_lock() + self._stop_servers_with_lock() self._kill_dump_render_tree() tests_run_file.close() return - if self._options.wait_for_httpd: - if self._current_group == "tests_to_http_lock": - self._http_lock_wait_begin = time.time() - self._port.acquire_http_lock() - - self._port.start_http_server() - self._port.start_websocket_server() - - self._have_http_lock = True - self._http_lock_wait_end = time.time() - elif self._have_http_lock: - self._stop_http_lock() + if self._current_group == "tests_to_http_lock": + self._start_servers_with_lock() + elif self._have_http_lock: + self._stop_servers_with_lock() self._num_tests_in_current_group = len(self._filename_list) self._current_group_start_time = time.time() @@ -440,7 +432,7 @@ class TestShellThread(WatchableThread): self._port.relative_test_filename(filename))) self._result_queue.put(result.dumps()) - if batch_size > 0 and batch_count > batch_size: + if batch_size > 0 and batch_count >= batch_size: # Bounce the shell and reset count. self._kill_dump_render_tree() batch_count = 0 @@ -545,11 +537,26 @@ class TestShellThread(WatchableThread): self._options) self._driver.start() - def _stop_http_lock(self): + def _start_servers_with_lock(self): + """Acquire http lock and start the servers.""" + self._http_lock_wait_begin = time.time() + _log.debug('Acquire http lock ...') + self._port.acquire_http_lock() + _log.debug('Starting HTTP server ...') + self._port.start_http_server() + _log.debug('Starting WebSocket server ...') + self._port.start_websocket_server() + self._http_lock_wait_end = time.time() + self._have_http_lock = True + + def _stop_servers_with_lock(self): """Stop the servers and release http lock.""" if self._have_http_lock: + _log.debug('Stopping HTTP server ...') self._port.stop_http_server() + _log.debug('Stopping WebSocket server ...') self._port.stop_websocket_server() + _log.debug('Release http lock ...') self._port.release_http_lock() self._have_http_lock = False diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py index cd7d663..a98b858 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/base.py @@ -47,6 +47,7 @@ import http_server import test_files import websocket_server +from webkitpy.common.memoized import memoized from webkitpy.common.system import logutils from webkitpy.common.system.executive import Executive, ScriptError from webkitpy.common.system.path import abspath_to_uri @@ -725,13 +726,25 @@ class Port(object): e.message_with_output())) return self._pretty_patch_error_html - def _webkit_build_directory(self, args): - args = ["perl", self.script_path("webkit-build-directory")] + args + def _webkit_build_directory_command(self, args): + return ["perl", self.script_path("webkit-build-directory")] + args + + @memoized + def _webkit_top_level_build_directory(self, top_level=True): + """This directory is above where products are built to and contains things like the Configuration file.""" + args = self._webkit_build_directory_command(["--top-level"]) + return self._executive.run_command(args).rstrip() + + @memoized + def _webkit_configuration_build_directory(self, configuration=None): + """This is where products are normally built to.""" + if not configuration: + configuration = self.flag_from_configuration(self.get_option('configuration')) + args = self._webkit_build_directory_command(["--configuration", configuration]) return self._executive.run_command(args).rstrip() def _configuration_file_path(self): - build_root = self._webkit_build_directory(["--top-level"]) - return os.path.join(build_root, "Configuration") + return os.path.join(self._webkit_top_level_build_directory(), "Configuration") # Easy override for unit tests def _open_configuration_file(self): diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py index 5d28fae..57b6989 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu.py @@ -71,6 +71,8 @@ def _set_gpu_options(options): options.accelerated_2d_canvas = True if options.builder_name is not None: options.builder_name = options.builder_name + ' - GPU' + if options.use_drt is None: + options.use_drt = True def _gpu_overrides(port): @@ -96,6 +98,9 @@ class ChromiumGpuLinuxPort(chromium_linux.ChromiumLinuxPort): return (map(self._webkit_baseline_path, ['chromium-gpu-linux', 'chromium-gpu-win', 'chromium-gpu']) + chromium_linux.ChromiumLinuxPort.baseline_search_path(self)) + def default_child_processes(self): + return 1 + def path_to_test_expectations_file(self): return self.path_from_webkit_base('LayoutTests', 'platform', 'chromium-gpu', 'test_expectations.txt') @@ -114,6 +119,9 @@ class ChromiumGpuMacPort(chromium_mac.ChromiumMacPort): return (map(self._webkit_baseline_path, ['chromium-gpu-mac', 'chromium-gpu']) + chromium_mac.ChromiumMacPort.baseline_search_path(self)) + def default_child_processes(self): + return 1 + def path_to_test_expectations_file(self): return self.path_from_webkit_base('LayoutTests', 'platform', 'chromium-gpu', 'test_expectations.txt') @@ -132,6 +140,9 @@ class ChromiumGpuWinPort(chromium_win.ChromiumWinPort): return (map(self._webkit_baseline_path, ['chromium-gpu-win', 'chromium-gpu']) + chromium_win.ChromiumWinPort.baseline_search_path(self)) + def default_child_processes(self): + return 1 + def path_to_test_expectations_file(self): return self.path_from_webkit_base('LayoutTests', 'platform', 'chromium-gpu', 'test_expectations.txt') diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py index 88524fc..03bc98a 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/chromium_gpu_unittest.py @@ -45,10 +45,14 @@ class ChromiumGpuTest(unittest.TestCase): # test that we got the right port mock_options = mocktool.MockOptions(accelerated_compositing=None, accelerated_2d_canvas=None, - builder_name='foo') + builder_name='foo', + use_drt=None, + child_processes=None) port = chromium_gpu.get(port_name=port_name, options=mock_options) self.assertTrue(port._options.accelerated_compositing) self.assertTrue(port._options.accelerated_2d_canvas) + self.assertTrue(port._options.use_drt) + self.assertEqual(port.default_child_processes(), 1) self.assertEqual(port._options.builder_name, 'foo - GPU') # we use startswith() instead of Equal to gloss over platform versions. diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/config.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/config.py new file mode 100644 index 0000000..2364098 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/config.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# 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. + +"""Wrapper objects for WebKit-specific utility routines.""" + +# FIXME: This file needs to be unified with common/checkout/scm.py and +# common/config/ports.py . + +from __future__ import with_statement + +import codecs +import os + +from webkitpy.common.checkout import scm +from webkitpy.common.system import logutils + + +_log = logutils.get_logger(__file__) + +# +# This is used to record if we've already hit the filesystem to look +# for a default configuration. We cache this to speed up the unit tests, +# but this can be reset with clear_cached_configuration(). +# +_determined_configuration = None + + +def clear_cached_configuration(): + global _determined_configuration + _determined_configuration = -1 + + +class Config(object): + _FLAGS_FROM_CONFIGURATIONS = { + "Debug": "--debug", + "Release": "--release", + } + + def __init__(self, executive): + self._executive = executive + self._webkit_base_dir = None + self._default_configuration = None + self._build_directories = {} + + def build_directory(self, configuration): + """Returns the path to the build directory for the configuration.""" + if configuration: + flags = ["--configuration", + self._FLAGS_FROM_CONFIGURATIONS[configuration]] + configuration = "" + else: + configuration = "" + flags = ["--top-level"] + + if not self._build_directories.get(configuration): + args = ["perl", self._script_path("webkit-build-directory")] + flags + self._build_directories[configuration] = ( + self._executive.run_command(args).rstrip()) + + return self._build_directories[configuration] + + def build_dumprendertree(self, configuration): + """Builds DRT in the given configuration. + + Returns True if the build was successful and up-to-date.""" + flag = self._FLAGS_FROM_CONFIGURATIONS[configuration] + exit_code = self._executive.run_command([ + self._script_path("build-dumprendertree"), flag], + return_exit_code=True) + if exit_code != 0: + _log.error("Failed to build DumpRenderTree") + return False + return True + + def default_configuration(self): + """Returns the default configuration for the user. + + Returns the value set by 'set-webkit-configuration', or "Release" + if that has not been set. This mirrors the logic in webkitdirs.pm.""" + if not self._default_configuration: + self._default_configuration = self._determine_configuration() + if not self._default_configuration: + self._default_configuration = 'Release' + if self._default_configuration not in self._FLAGS_FROM_CONFIGURATIONS: + _log.warn("Configuration \"%s\" is not a recognized value.\n" % + self._default_configuration) + _log.warn("Scripts may fail. " + "See 'set-webkit-configuration --help'.") + return self._default_configuration + + def path_from_webkit_base(self, *comps): + return os.path.join(self.webkit_base_dir(), *comps) + + def webkit_base_dir(self): + """Returns the absolute path to the top of the WebKit tree. + + Raises an AssertionError if the top dir can't be determined.""" + # FIXME: Consider determining this independently of scm in order + # to be able to run in a bare tree. + if not self._webkit_base_dir: + self._webkit_base_dir = scm.find_checkout_root() + assert self._webkit_base_dir, "Could not determine the top of the WebKit checkout" + return self._webkit_base_dir + + def _script_path(self, script_name): + return os.path.join(self.webkit_base_dir(), "WebKitTools", + "Scripts", script_name) + + def _determine_configuration(self): + # This mirrors the logic in webkitdirs.pm:determineConfiguration(). + global _determined_configuration + if _determined_configuration == -1: + contents = self._read_configuration() + if contents == "Deployment": + contents = "Release" + if contents == "Development": + contents = "Debug" + _determined_configuration = contents + return _determined_configuration + + def _read_configuration(self): + configuration_path = os.path.join(self.build_directory(None), + "Configuration") + if not os.path.exists(configuration_path): + return None + + with codecs.open(configuration_path, "r", "utf-8") as fh: + return fh.read().rstrip() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/config_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/config_unittest.py new file mode 100644 index 0000000..4674cba --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/config_unittest.py @@ -0,0 +1,176 @@ +# 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. + +import codecs +import os +import StringIO +import unittest + +from webkitpy.common.system import outputcapture + +import config + + +# FIXME: This makes StringIO objects work with "with". Remove +# when we upgrade to 2.6. +class NewStringIO(StringIO.StringIO): + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + pass + + +class MockExecutive(object): + def __init__(self, output='', exit_code=0): + self._output = output + self._exit_code = exit_code + + def run_command(self, arg_list, return_exit_code=False, + decode_output=False): + if return_exit_code: + return self._exit_code + return self._output + + +class ConfigTest(unittest.TestCase): + def tearDown(self): + config.clear_cached_configuration() + + def assertConfiguration(self, contents, expected): + # This tests that a configuration file containing + # _contents_ endsd up being interpreted as _expected_. + # + # FIXME: rewrite this when we have a filesystem abstraction we + # can mock out more easily. + config.clear_cached_configuration() + orig_open = codecs.open + + def wrap_open(contents): + def open_wrapper(path, mode, encoding): + return NewStringIO(contents) + return open_wrapper + + try: + orig_exists = os.path.exists + os.path.exists = lambda p: True + codecs.open = wrap_open(contents) + + e = MockExecutive(output='foo') + c = config.Config(e) + self.assertEqual(c.default_configuration(), expected) + finally: + os.path.exists = orig_exists + codecs.open = orig_open + + def test_build_directory_toplevel(self): + e = MockExecutive(output="toplevel") + c = config.Config(e) + self.assertEqual(c.build_directory(None), 'toplevel') + + # Test again to check caching + self.assertEqual(c.build_directory(None), 'toplevel') + + def test_build_directory__release(self): + e = MockExecutive(output="release") + c = config.Config(e) + self.assertEqual(c.build_directory('Release'), 'release') + + def test_build_directory__debug(self): + e = MockExecutive(output="debug") + c = config.Config(e) + self.assertEqual(c.build_directory('Debug'), 'debug') + + def test_build_directory__unknown(self): + e = MockExecutive(output="unknown") + c = config.Config(e) + self.assertRaises(KeyError, c.build_directory, 'Unknown') + + def test_build_dumprendertree__success(self): + e = MockExecutive(exit_code=0) + c = config.Config(e) + self.assertTrue(c.build_dumprendertree("Debug")) + self.assertTrue(c.build_dumprendertree("Release")) + self.assertRaises(KeyError, c.build_dumprendertree, "Unknown") + + def test_build_dumprendertree__failure(self): + e = MockExecutive(exit_code=-1) + c = config.Config(e) + + oc = outputcapture.OutputCapture() + oc.capture_output() + self.assertFalse(c.build_dumprendertree('Debug')) + (out, err) = oc.restore_output() + + oc.capture_output() + self.assertFalse(c.build_dumprendertree('Release')) + oc.restore_output() + + def test_default_configuration__release(self): + self.assertConfiguration('Release', 'Release') + + def test_default_configuration__debug(self): + self.assertConfiguration('Debug', 'Debug') + + def test_default_configuration__deployment(self): + self.assertConfiguration('Deployment', 'Release') + + def test_default_configuration__development(self): + self.assertConfiguration('Development', 'Debug') + + def test_default_configuration__notfound(self): + # This tests what happens if the default configuration file + # doesn't exist. + config.clear_cached_configuration() + try: + orig_exists = os.path.exists + os.path.exists = lambda p: False + e = MockExecutive(output="foo") + c = config.Config(e) + self.assertEqual(c.default_configuration(), "Release") + finally: + os.path.exists = orig_exists + + def test_default_configuration__unknown(self): + # Ignore the warning about an unknown configuration value. + oc = outputcapture.OutputCapture() + oc.capture_output() + self.assertConfiguration('Unknown', 'Unknown') + oc.restore_output() + + def test_path_from_webkit_base(self, *comps): + c = config.Config(None) + self.assertTrue(c.path_from_webkit_base('foo')) + + def test_webkit_base_dir(self): + c = config.Config(None) + self.assertTrue(c.webkit_base_dir()) + + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock.py index b2615a3..d65801d 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/http_lock.py @@ -29,27 +29,38 @@ http and websocket tests in a same time.""" import glob +import logging import os import sys import tempfile import time +from webkitpy.common.system.executive import Executive + + +_log = logging.getLogger("webkitpy.layout_tests.port.http_lock") + class HttpLock(object): def __init__(self, lock_path, lock_file_prefix="WebKitHttpd.lock.", guard_lock="WebKit.lock"): - if not lock_path: + self._lock_path = lock_path + if not self._lock_path: self._lock_path = tempfile.gettempdir() self._lock_file_prefix = lock_file_prefix self._lock_file_path_prefix = os.path.join(self._lock_path, self._lock_file_prefix) self._guard_lock_file = os.path.join(self._lock_path, guard_lock) self._process_lock_file_name = "" + self._executive = Executive() + # maximum wait time for the lock creation + self._guard_lock_max_wait = 1 * 60 def cleanup_http_lock(self): """Delete the lock file if exists.""" if os.path.exists(self._process_lock_file_name): + _log.debug("Removing lock file: %s" % self._process_lock_file_name) os.unlink(self._process_lock_file_name) def _extract_lock_number(self, lock_file_name): @@ -70,17 +81,6 @@ class HttpLock(object): return 0 return self._extract_lock_number(lock_list[-1]) + 1 - def _check_pid(self, current_pid): - """Return True if pid is alive, otherwise return False. - FIXME: os.kill() doesn't work on Windows for checking if - a pid is alive, so always return True""" - if sys.platform in ('darwin', 'linux2'): - try: - os.kill(current_pid, 0) - except OSError: - return False - return True - def _curent_lock_pid(self): """Return with the current lock pid. If the lock is not valid it deletes the lock file.""" @@ -91,7 +91,8 @@ class HttpLock(object): current_lock_file = open(lock_list[0], 'r') current_pid = current_lock_file.readline() current_lock_file.close() - if not (current_pid and self._check_pid(int(current_pid))): + if not (current_pid and self._executive.check_running_pid(int(current_pid))): + _log.debug("Removing stuck lock file: %s" % lock_list[0]) os.unlink(lock_list[0]) return except IOError, OSError: @@ -102,22 +103,34 @@ class HttpLock(object): """The lock files are used to schedule the running test sessions in first come first served order. The sequential guard lock ensures that the lock numbers are sequential.""" + if not os.path.exists(self._lock_path): + _log.debug("Lock directory does not exist: %s" % self._lock_path) + return False + + start_time = time.time() while(True): try: sequential_guard_lock = os.open(self._guard_lock_file, os.O_CREAT | os.O_EXCL) self._process_lock_file_name = (self._lock_file_path_prefix + str(self._next_lock_number())) lock_file = open(self._process_lock_file_name, 'w') + _log.debug("Creating lock file: %s" % self._process_lock_file_name) lock_file.write(str(os.getpid())) lock_file.close() os.close(sequential_guard_lock) os.unlink(self._guard_lock_file) - break + return True except OSError: - pass + if time.time() - start_time > self._guard_lock_max_wait: + _log.debug("Lock does not created: %s" % str(sys.exc_info())) + return False def wait_for_httpd_lock(self): - """Create a lock file and wait until it's turn comes.""" - self._create_lock_file() + """Create a lock file and wait until it's turn comes. If something goes wrong + it wont do any locking.""" + if not self._create_lock_file(): + _log.debug("Warning, http locking failed!") + return + while self._curent_lock_pid() != os.getpid(): time.sleep(1) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py index 0d0d3e0..0b324f5 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/port/webkit.py @@ -60,7 +60,6 @@ class WebKitPort(base.Port): def __init__(self, **kwargs): base.Port.__init__(self, **kwargs) - self._cached_build_root = None self._cached_apache_path = None # FIXME: disable pixel tests until they are run by default on the @@ -358,12 +357,8 @@ class WebKitPort(base.Port): 'mac-tiger', 'mac-leopard', 'mac-snowleopard') def _build_path(self, *comps): - if not self._cached_build_root: - self._cached_build_root = self._webkit_build_directory([ - "--configuration", - self.flag_from_configuration(self.get_option('configuration')), - ]) - return os.path.join(self._cached_build_root, *comps) + build_root = self._webkit_configuration_build_directory() + return os.path.join(build_root, *comps) def _path_to_driver(self): return self._build_path('DumpRenderTree') diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py index 434058e..55c4558 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests.py @@ -70,11 +70,11 @@ _log = logging.getLogger("webkitpy.layout_tests." BASELINE_SUFFIXES = ['.txt', '.png', '.checksum'] REBASELINE_PLATFORM_ORDER = ['mac', 'win', 'win-xp', 'win-vista', 'linux'] -ARCHIVE_DIR_NAME_DICT = {'win': 'webkit-rel', +ARCHIVE_DIR_NAME_DICT = {'win': 'Webkit_Win', 'win-vista': 'webkit-dbg-vista', - 'win-xp': 'webkit-rel', - 'mac': 'webkit-rel-mac5', - 'linux': 'webkit-rel-linux', + 'win-xp': 'Webkit_Win', + 'mac': 'Webkit_Mac10_5', + 'linux': 'webkit-rel-linux64', 'win-canary': 'webkit-rel-webkit-org', 'win-vista-canary': 'webkit-dbg-vista', 'win-xp-canary': 'webkit-rel-webkit-org', @@ -819,9 +819,8 @@ def get_host_port_object(options): return port_obj -def main(executive=Executive()): - """Main function to produce new baselines.""" - +def parse_options(args): + """Parse options and return a pair of host options and target options.""" option_parser = optparse.OptionParser() option_parser.add_option('-v', '--verbose', action='store_true', @@ -874,7 +873,20 @@ def main(executive=Executive()): help=('The target platform to rebaseline ' '("mac", "chromium", "qt", etc.). Defaults ' 'to "chromium".')) - options = option_parser.parse_args()[0] + options = option_parser.parse_args(args)[0] + + target_options = copy.copy(options) + if options.target_platform == 'chromium': + target_options.chromium = True + options.tolerance = 0 + + return (options, target_options) + + +def main(executive=Executive()): + """Main function to produce new baselines.""" + + (options, target_options) = parse_options(sys.argv[1:]) # We need to create three different Port objects over the life of this # script. |target_port_obj| is used to determine configuration information: @@ -882,9 +894,6 @@ def main(executive=Executive()): # |port_obj| is used for runtime functionality like actually diffing # Then we create a rebaselining port to actual find and manage the # baselines. - target_options = copy.copy(options) - if options.target_platform == 'chromium': - target_options.chromium = True target_port_obj = port.get(None, target_options) # Set up our logging format. diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py index 8db31a6..7c55b94 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/rebaseline_chromium_webkit_tests_unittest.py @@ -95,6 +95,15 @@ class TestRebaseliner(unittest.TestCase): return rebaseline_chromium_webkit_tests.Rebaseliner( host_port_obj, target_port_obj, platform, options) + def test_parse_options(self): + (options, target_options) = rebaseline_chromium_webkit_tests.parse_options([]) + self.assertTrue(target_options.chromium) + self.assertEqual(options.tolerance, 0) + + (options, target_options) = rebaseline_chromium_webkit_tests.parse_options(['--target-platform', 'qt']) + self.assertFalse(hasattr(target_options, 'chromium')) + self.assertEqual(options.tolerance, 0) + def test_noop(self): # this method tests that was can at least instantiate an object, even # if there is nothing to do. diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py index 704180c..f360adc 100755 --- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests.py @@ -85,6 +85,8 @@ _log = logging.getLogger("webkitpy.layout_tests.run_webkit_tests") # Builder base URL where we have the archived test results. BUILDER_BASE_URL = "http://build.chromium.org/buildbot/layout_test_results/" +LAYOUT_TESTS_DIRECTORY = "LayoutTests" + os.sep + TestExpectationsFile = test_expectations.TestExpectationsFile @@ -283,12 +285,17 @@ class TestRunner: last_unexpected_results: list of unexpected results to retest, if any """ - paths = [arg for arg in args if arg and arg != ''] + paths = [self._strip_test_dir_prefix(arg) for arg in args if arg and arg != ''] paths += last_unexpected_results if self._options.test_list: paths += read_test_files(self._options.test_list) self._test_files = self._port.tests(paths) + def _strip_test_dir_prefix(self, path): + if path.startswith(LAYOUT_TESTS_DIRECTORY): + return path[len(LAYOUT_TESTS_DIRECTORY):] + return path + def lint(self): # Creating the expecations for each platform/configuration pair does # all the test list parsing and ensures it's correct syntax (e.g. no @@ -404,9 +411,8 @@ class TestRunner: # If we reached the end and we don't have enough tests, we run some # from the beginning. - if (self._options.run_chunk and - (slice_end - slice_start < chunk_len)): - extra = 1 + chunk_len - (slice_end - slice_start) + if slice_end - slice_start < chunk_len: + extra = chunk_len - (slice_end - slice_start) extra_msg = (' last chunk is partial, appending [0:%d]' % extra) self._printer.print_expected(extra_msg) @@ -470,9 +476,9 @@ class TestRunner: def _get_dir_for_test_file(self, test_file): """Returns the highest-level directory by which to shard the given test file.""" - index = test_file.rfind(os.sep + 'LayoutTests' + os.sep) + index = test_file.rfind(os.sep + LAYOUT_TESTS_DIRECTORY) - test_file = test_file[index + len('LayoutTests/'):] + test_file = test_file[index + len(LAYOUT_TESTS_DIRECTORY):] test_file_parts = test_file.split(os.sep, 1) directory = test_file_parts[0] test_file = test_file_parts[1] @@ -741,17 +747,6 @@ class TestRunner: if not result_summary: return None - # Do not start when http locking is enabled. - if not self._options.wait_for_httpd: - if self.needs_http(): - self._printer.print_update('Starting HTTP server ...') - self._port.start_http_server() - - if self.needs_websocket(): - self._printer.print_update('Starting WebSocket server ...') - self._port.start_websocket_server() - # self._websocket_secure_server.Start() - return result_summary def run(self, result_summary): @@ -841,11 +836,6 @@ class TestRunner: sys.stdout.flush() _log.debug("flushing stderr") sys.stderr.flush() - if not self._options.wait_for_httpd: - _log.debug("stopping http server") - self._port.stop_http_server() - _log.debug("stopping websocket server") - self._port.stop_websocket_server() _log.debug("stopping helper") self._port.stop_helper() @@ -948,14 +938,15 @@ class TestRunner: if not self._options.test_results_server: return + if not self._options.master_name: + _log.error("--test-results-server was set, but --master-name was not. Not uploading JSON files.") + return + _log.info("Uploading JSON files for builder: %s", self._options.builder_name) - attrs = [("builder", self._options.builder_name), ("testtype", "layout-tests")] - # FIXME: master_name should be required if test_results_server is set. - # Throw an error if master_name isn't set. - if self._options.master_name: - attrs.append(("master", self._options.master_name)) + attrs = [("builder", self._options.builder_name), ("testtype", "layout-tests"), + ("master", self._options.master_name)] json_files = ["expectations.json"] if self._options.upload_full_results: @@ -966,13 +957,6 @@ class TestRunner: files = [(file, os.path.join(self._options.results_directory, file)) for file in json_files] - # FIXME: Remove this. This is temporary debug logging. - if self._options.builder_name.startswith("Webkit Linux"): - for filename in files: - _log.debug(filename[1]) - with codecs.open(filename[1], "r") as results_file: - _log.debug("%s:\n%s" % (filename[0], results_file.read())) - uploader = test_results_uploader.TestResultsUploader( self._options.test_results_server) try: @@ -1530,7 +1514,7 @@ def parse_args(args=None): default=False, help="Don't check the system dependencies (themes)"), optparse.make_option("--use-drt", action="store_true", - default=False, + default=None, help="Use DumpRenderTree instead of test_shell"), optparse.make_option("--accelerated-compositing", action="store_true", @@ -1612,9 +1596,6 @@ def parse_args(args=None): optparse.make_option("--no-record-results", action="store_false", default=True, dest="record_results", help="Don't record the results."), - optparse.make_option("--wait-for-httpd", action="store_true", - default=False, dest="wait_for_httpd", - help="Wait for http locks."), # old-run-webkit-tests also has HTTP toggle options: # --[no-]http Run (or do not run) http tests # (default: run) diff --git a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py index 0f09ffa..f21e7a5 100644 --- a/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py +++ b/WebKitTools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py @@ -31,6 +31,7 @@ """Unit tests for run_webkit_tests.""" import codecs +import itertools import logging import os import Queue @@ -48,7 +49,9 @@ from webkitpy.common.system import user from webkitpy.layout_tests import port from webkitpy.layout_tests import run_webkit_tests from webkitpy.layout_tests.layout_package import dump_render_tree_thread -from webkitpy.layout_tests.port.test import TestPort +from webkitpy.layout_tests.port.test import TestPort, TestDriver +from webkitpy.python24.versioning import compare_version +from webkitpy.test.skip import skip_if from webkitpy.thirdparty.mock import Mock @@ -61,41 +64,44 @@ class MockUser(): self.url = url -def passing_run(args=[], port_obj=None, record_results=False, +def passing_run(extra_args=None, port_obj=None, record_results=False, tests_included=False): - new_args = ['--print', 'nothing'] - if not '--platform' in args: - new_args.extend(['--platform', 'test']) + extra_args = extra_args or [] + args = ['--print', 'nothing'] + if not '--platform' in extra_args: + args.extend(['--platform', 'test']) if not record_results: - new_args.append('--no-record-results') - new_args.extend(args) + args.append('--no-record-results') + args.extend(extra_args) if not tests_included: # We use the glob to test that globbing works. - new_args.extend(['passes', - 'http/tests', - 'http/tests/websocket/tests', - 'failures/expected/*']) - options, parsed_args = run_webkit_tests.parse_args(new_args) - if port_obj is None: + args.extend(['passes', + 'http/tests', + 'http/tests/websocket/tests', + 'failures/expected/*']) + options, parsed_args = run_webkit_tests.parse_args(args) + if not port_obj: port_obj = port.get(port_name=options.platform, options=options, user=MockUser()) res = run_webkit_tests.run(port_obj, options, parsed_args) return res == 0 -def logging_run(args=[], tests_included=False): - new_args = ['--no-record-results'] - if not '--platform' in args: - new_args.extend(['--platform', 'test']) - new_args.extend(args) +def logging_run(extra_args=None, port_obj=None, tests_included=False): + extra_args = extra_args or [] + args = ['--no-record-results'] + if not '--platform' in extra_args: + args.extend(['--platform', 'test']) + args.extend(extra_args) if not tests_included: - new_args.extend(['passes', - 'http/tests', - 'http/tests/websocket/tests', - 'failures/expected/*']) - options, parsed_args = run_webkit_tests.parse_args(new_args) + args.extend(['passes', + 'http/tests', + 'http/tests/websocket/tests', + 'failures/expected/*']) + options, parsed_args = run_webkit_tests.parse_args(args) user = MockUser() - port_obj = port.get(port_name=options.platform, options=options, user=user) + if not port_obj: + port_obj = port.get(port_name=options.platform, options=options, user=user) buildbot_output = array_stream.ArrayStream() regular_output = array_stream.ArrayStream() res = run_webkit_tests.run(port_obj, options, parsed_args, @@ -104,6 +110,54 @@ def logging_run(args=[], tests_included=False): return (res, buildbot_output, regular_output, user) +def get_tests_run(extra_args=None, tests_included=False, flatten_batches=False): + extra_args = extra_args or [] + args = [ + '--print', 'nothing', + '--platform', 'test', + '--no-record-results', + '--child-processes', '1'] + args.extend(extra_args) + if not tests_included: + # Not including http tests since they get run out of order (that + # behavior has its own test, see test_get_test_file_queue) + args.extend(['passes', 'failures']) + options, parsed_args = run_webkit_tests.parse_args(args) + user = MockUser() + + test_batches = [] + + class RecordingTestDriver(TestDriver): + def __init__(self, port, image_path, options): + TestDriver.__init__(self, port, image_path, options, executive=None) + self._current_test_batch = None + + def poll(self): + # So that we don't create a new driver for every test + return None + + def stop(self): + self._current_test_batch = None + + def run_test(self, uri, timeoutms, image_hash): + if self._current_test_batch is None: + self._current_test_batch = [] + test_batches.append(self._current_test_batch) + self._current_test_batch.append(self._port.uri_to_test_name(uri)) + return TestDriver.run_test(self, uri, timeoutms, image_hash) + + class RecordingTestPort(TestPort): + def create_driver(self, image_path, options): + return RecordingTestDriver(self, image_path, options) + + recording_port = RecordingTestPort(options=options, user=user) + logging_run(extra_args=args, port_obj=recording_port, tests_included=True) + + if flatten_batches: + return list(itertools.chain(*test_batches)) + + return test_batches + class MainTest(unittest.TestCase): def test_accelerated_compositing(self): # This just tests that we recognize the command line args @@ -119,8 +173,9 @@ class MainTest(unittest.TestCase): self.assertTrue(passing_run()) def test_batch_size(self): - # FIXME: verify # of tests run - self.assertTrue(passing_run(['--batch-size', '2'])) + batch_tests_run = get_tests_run(['--batch-size', '2']) + for batch in batch_tests_run: + self.assertTrue(len(batch) <= 2, '%s had too many tests' % ', '.join(batch)) def test_child_process_1(self): (res, buildbot_output, regular_output, user) = logging_run( @@ -194,8 +249,15 @@ class MainTest(unittest.TestCase): self.assertTrue(passing_run(['--randomize-order'])) def test_run_chunk(self): - # FIXME: verify # of tests run - self.assertTrue(passing_run(['--run-chunk', '1:4'])) + # Test that we actually select the right chunk + all_tests_run = get_tests_run(flatten_batches=True) + chunk_tests_run = get_tests_run(['--run-chunk', '1:4'], flatten_batches=True) + self.assertEquals(all_tests_run[4:8], chunk_tests_run) + + # Test that we wrap around if the number of tests is not evenly divisible by the chunk size + tests_to_run = ['passes/error.html', 'passes/image.html', 'passes/platform_image.html', 'passes/text.html'] + chunk_tests_run = get_tests_run(['--run-chunk', '1:3'] + tests_to_run, tests_included=True, flatten_batches=True) + self.assertEquals(['passes/text.html', 'passes/error.html', 'passes/image.html'], chunk_tests_run) def test_run_force(self): # This raises an exception because we run @@ -203,23 +265,33 @@ class MainTest(unittest.TestCase): self.assertRaises(ValueError, logging_run, ['--force']) def test_run_part(self): - # FIXME: verify # of tests run - self.assertTrue(passing_run(['--run-part', '1:2'])) + # Test that we actually select the right part + tests_to_run = ['passes/error.html', 'passes/image.html', 'passes/platform_image.html', 'passes/text.html'] + tests_run = get_tests_run(['--run-part', '1:2'] + tests_to_run, tests_included=True, flatten_batches=True) + self.assertEquals(['passes/error.html', 'passes/image.html'], tests_run) + + # Test that we wrap around if the number of tests is not evenly divisible by the chunk size + # (here we end up with 3 parts, each with 2 tests, and we only have 4 tests total, so the + # last part repeats the first two tests). + chunk_tests_run = get_tests_run(['--run-part', '3:3'] + tests_to_run, tests_included=True, flatten_batches=True) + self.assertEquals(['passes/error.html', 'passes/image.html'], chunk_tests_run) def test_run_singly(self): - self.assertTrue(passing_run(['--run-singly'])) + batch_tests_run = get_tests_run(['--run-singly']) + for batch in batch_tests_run: + self.assertEquals(len(batch), 1, '%s had too many tests' % ', '.join(batch)) def test_single_file(self): - # FIXME: verify # of tests run - self.assertTrue(passing_run(['passes/text.html'], tests_included=True)) + tests_run = get_tests_run(['passes/text.html'], tests_included=True, flatten_batches=True) + self.assertEquals(['passes/text.html'], tests_run) def test_test_list(self): filename = tempfile.mktemp() tmpfile = file(filename, mode='w+') tmpfile.write('passes/text.html') tmpfile.close() - self.assertTrue(passing_run(['--test-list=%s' % filename], - tests_included=True)) + tests_run = get_tests_run(['--test-list=%s' % filename], tests_included=True, flatten_batches=True) + self.assertEquals(['passes/text.html'], tests_run) os.remove(filename) res, out, err, user = logging_run(['--test-list=%s' % filename], tests_included=True) @@ -271,7 +343,7 @@ class MainTest(unittest.TestCase): def get_port_for_run(args): options, parsed_args = run_webkit_tests.parse_args(args) test_port = ImageDiffTestPort(options=options, user=MockUser()) - passing_run(args=args, port_obj=test_port, tests_included=True) + passing_run(args, port_obj=test_port, tests_included=True) return test_port base_args = ['--pixel-tests', 'failures/expected/*'] @@ -287,6 +359,8 @@ class MainTest(unittest.TestCase): test_port = get_port_for_run(base_args) self.assertEqual(None, test_port.tolerance_used_for_diff_image) +MainTest = skip_if(MainTest, sys.platform == 'cygwin' and compare_version(sys, '2.6')[0] < 0, 'new-run-webkit-tests tests hang on Cygwin Python 2.5.2') + def _mocked_open(original_open, file_list): def _wrapper(name, mode, encoding): diff --git a/WebKitTools/Scripts/webkitpy/style/checker.py b/WebKitTools/Scripts/webkitpy/style/checker.py index 11e3e33..fb93eb9 100644 --- a/WebKitTools/Scripts/webkitpy/style/checker.py +++ b/WebKitTools/Scripts/webkitpy/style/checker.py @@ -115,7 +115,13 @@ _PATH_RULES_SPECIFIER = [ # Files in these directories are consumers of the WebKit # API and therefore do not follow the same header including # discipline as WebCore. - (["WebKitTools/WebKitAPITest/", + + ([# TestNetscapePlugIn has no config.h and uses funny names like + # NPP_SetWindow. + "WebKitTools/DumpRenderTree/TestNetscapePlugIn/", + # The API test harnesses have no config.h and use funny macros like + # TEST_CLASS_NAME. + "WebKitTools/WebKitAPITest/", "WebKitTools/TestWebKitAPI/"], ["-build/include", "-readability/naming"]), diff --git a/WebKitTools/Scripts/webkitpy/style/optparser.py b/WebKitTools/Scripts/webkitpy/style/optparser.py index 3ba0fae..f4e9923 100644 --- a/WebKitTools/Scripts/webkitpy/style/optparser.py +++ b/WebKitTools/Scripts/webkitpy/style/optparser.py @@ -145,6 +145,7 @@ class CommandOptionValues(object): def __init__(self, filter_rules=None, git_commit=None, + diff_files=None, is_verbose=False, min_confidence=1, output_format="emacs"): @@ -163,6 +164,7 @@ class CommandOptionValues(object): self.filter_rules = filter_rules self.git_commit = git_commit + self.diff_files = diff_files self.is_verbose = is_verbose self.min_confidence = min_confidence self.output_format = output_format @@ -174,6 +176,8 @@ class CommandOptionValues(object): return False if self.git_commit != other.git_commit: return False + if self.diff_files != other.diff_files: + return False if self.is_verbose != other.is_verbose: return False if self.min_confidence != other.min_confidence: @@ -214,6 +218,8 @@ class ArgumentPrinter(object): flags['filter'] = ",".join(filter_rules) if options.git_commit: flags['git-commit'] = options.git_commit + if options.diff_files: + flags['diff_files'] = options.diff_files flag_string = '' # Alphabetizing lets us unit test this method. @@ -308,6 +314,9 @@ class ArgumentParser(object): parser.add_option("-g", "--git-diff", "--git-commit", metavar="COMMIT", dest="git_commit", help=git_commit_help,) + diff_files_help = "diff the files passed on the command line rather than checking the style of every line" + parser.add_option("--diff-files", action="store_true", dest="diff_files", default=False, help=diff_files_help) + min_confidence_help = ("set the minimum confidence of style errors " "to report. Can be an integer 1-5, with 1 " "displaying all errors. Defaults to %default.") @@ -409,6 +418,7 @@ class ArgumentParser(object): filter_value = options.filter_value git_commit = options.git_commit + diff_files = options.diff_files is_verbose = options.is_verbose min_confidence = options.min_confidence output_format = options.output_format @@ -420,10 +430,6 @@ class ArgumentParser(object): # Validate user-provided values. - if paths and git_commit: - self._parse_error('You cannot provide both paths and a git ' - 'commit at the same time.') - min_confidence = int(min_confidence) if (min_confidence < 1) or (min_confidence > 5): self._parse_error('option --min-confidence: invalid integer: ' @@ -442,6 +448,7 @@ class ArgumentParser(object): options = CommandOptionValues(filter_rules=filter_rules, git_commit=git_commit, + diff_files=diff_files, is_verbose=is_verbose, min_confidence=min_confidence, output_format=output_format) diff --git a/WebKitTools/Scripts/webkitpy/style/optparser_unittest.py b/WebKitTools/Scripts/webkitpy/style/optparser_unittest.py index b7e3eda..a6b64da 100644 --- a/WebKitTools/Scripts/webkitpy/style/optparser_unittest.py +++ b/WebKitTools/Scripts/webkitpy/style/optparser_unittest.py @@ -136,11 +136,6 @@ class ArgumentParserTest(LoggingTestCase): self.assertLog(['ERROR: Invalid filter rule "build": ' 'every rule must start with + or -.\n']) parse(['--filter=+build']) # works - # Pass files and git-commit at the same time. - self.assertRaises(SystemExit, parse, ['--git-commit=committish', - 'file.txt']) - self.assertLog(['ERROR: You cannot provide both paths and ' - 'a git commit at the same time.\n']) def test_parse_default_arguments(self): parse = self._parse @@ -151,6 +146,7 @@ class ArgumentParserTest(LoggingTestCase): self.assertEquals(options.filter_rules, []) self.assertEquals(options.git_commit, None) + self.assertEquals(options.diff_files, False) self.assertEquals(options.is_verbose, False) self.assertEquals(options.min_confidence, 3) self.assertEquals(options.output_format, 'vs7') @@ -171,6 +167,8 @@ class ArgumentParserTest(LoggingTestCase): self.assertEquals(options.git_commit, 'commit') (files, options) = parse(['--verbose']) self.assertEquals(options.is_verbose, True) + (files, options) = parse(['--diff-files', 'file.txt']) + self.assertEquals(options.diff_files, True) # Pass user_rules. (files, options) = parse(['--filter=+build,-whitespace']) diff --git a/WebKitTools/Scripts/webkitpy/style_references.py b/WebKitTools/Scripts/webkitpy/style_references.py index 34f3bff..a21e931 100644 --- a/WebKitTools/Scripts/webkitpy/style_references.py +++ b/WebKitTools/Scripts/webkitpy/style_references.py @@ -69,6 +69,6 @@ class WebKitCheckout(object): """Return the checkout root as an absolute path.""" return self._scm.checkout_root - def create_patch(self, git_commit): - return self._scm.create_patch(git_commit) - + def create_patch(self, git_commit, changed_files=None): + # FIXME: SCM.create_patch should understand how to handle None. + return self._scm.create_patch(git_commit, changed_files=changed_files or []) diff --git a/WebKitTools/Scripts/webkitpy/test/cat.py b/WebKitTools/Scripts/webkitpy/test/cat.py new file mode 100644 index 0000000..ae1e143 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/test/cat.py @@ -0,0 +1,42 @@ +# Copyright (C) 2010 Apple 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + +import os.path +import sys + +# Add WebKitTools/Scripts to the path to ensure we can find webkitpy. +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +from webkitpy.common.system import fileutils + + +def command_arguments(*args): + return ['python', __file__] + list(args) + + +def main(): + fileutils.make_stdout_binary() + sys.stdout.write(sys.stdin.read()) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/WebKitTools/Scripts/webkitpy/test/cat_unittest.py b/WebKitTools/Scripts/webkitpy/test/cat_unittest.py new file mode 100644 index 0000000..4ed1f67 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/test/cat_unittest.py @@ -0,0 +1,52 @@ +# Copyright (C) 2010 Apple 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + +import StringIO +import os.path +import sys +import unittest + +from webkitpy.common.system import executive, outputcapture +from webkitpy.test import cat + + +class CatTest(outputcapture.OutputCaptureTestCaseBase): + def assert_cat(self, input): + saved_stdin = sys.stdin + sys.stdin = StringIO.StringIO(input) + cat.main() + self.assertStdout(input) + sys.stdin = saved_stdin + + def test_basic(self): + self.assert_cat('foo bar baz\n') + + def test_no_newline(self): + self.assert_cat('foo bar baz') + + def test_unicode(self): + self.assert_cat(u'WebKit \u2661 Tor Arne Vestb\u00F8!') + + def test_as_command(self): + input = 'foo bar baz\n' + output = executive.Executive().run_command(cat.command_arguments(), input=input) + self.assertEqual(input, output) diff --git a/WebKitTools/Scripts/webkitpy/test/echo.py b/WebKitTools/Scripts/webkitpy/test/echo.py new file mode 100644 index 0000000..f7468f7 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/test/echo.py @@ -0,0 +1,52 @@ +# Copyright (C) 2010 Apple 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + +import os.path +import sys + +# Add WebKitTools/Scripts to the path to ensure we can find webkitpy. +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +from webkitpy.common.system import fileutils + + +def command_arguments(*args): + return ['python', __file__] + list(args) + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + fileutils.make_stdout_binary() + + print_newline = True + if len(args) and args[0] == '-n': + print_newline = False + del args[0] + sys.stdout.write(' '.join(args)) + if print_newline: + sys.stdout.write('\n') + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/WebKitTools/Scripts/webkitpy/test/echo_unittest.py b/WebKitTools/Scripts/webkitpy/test/echo_unittest.py new file mode 100644 index 0000000..bc13b5e --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/test/echo_unittest.py @@ -0,0 +1,64 @@ +# Copyright (C) 2010 Apple 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + +import os.path +import sys +import unittest + +from webkitpy.common.system import executive, outputcapture +from webkitpy.test import echo + + +class EchoTest(outputcapture.OutputCaptureTestCaseBase): + def test_basic(self): + echo.main(['foo', 'bar', 'baz']) + self.assertStdout('foo bar baz\n') + + def test_no_newline(self): + echo.main(['-n', 'foo', 'bar', 'baz']) + self.assertStdout('foo bar baz') + + def test_unicode(self): + echo.main([u'WebKit \u2661', 'Tor Arne', u'Vestb\u00F8!']) + self.assertStdout(u'WebKit \u2661 Tor Arne Vestb\u00F8!\n') + + def test_argument_order(self): + echo.main(['foo', '-n', 'bar']) + self.assertStdout('foo -n bar\n') + + def test_empty_arguments(self): + old_argv = sys.argv + sys.argv = ['echo.py', 'foo', 'bar', 'baz'] + echo.main([]) + self.assertStdout('\n') + sys.argv = old_argv + + def test_no_arguments(self): + old_argv = sys.argv + sys.argv = ['echo.py', 'foo', 'bar', 'baz'] + echo.main() + self.assertStdout('foo bar baz\n') + sys.argv = old_argv + + def test_as_command(self): + output = executive.Executive().run_command(echo.command_arguments('foo', 'bar', 'baz')) + self.assertEqual(output, 'foo bar baz\n') diff --git a/WebKitTools/Scripts/webkitpy/test/skip.py b/WebKitTools/Scripts/webkitpy/test/skip.py new file mode 100644 index 0000000..8587d56 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/test/skip.py @@ -0,0 +1,52 @@ +# Copyright (C) 2010 Apple 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + +import logging + +_log = logging.getLogger(__name__) + + +def skip_if(klass, condition, message=None, logger=None): + """Makes all test_* methods in a given class no-ops if the given condition + is False. Backported from Python 3.1+'s unittest.skipIf decorator.""" + if not logger: + logger = _log + if not condition: + return klass + for name in dir(klass): + attr = getattr(klass, name) + if not callable(attr): + continue + if not name.startswith('test_'): + continue + setattr(klass, name, _skipped_method(attr, message, logger)) + klass._printed_skipped_message = False + return klass + + +def _skipped_method(method, message, logger): + def _skip(*args): + if method.im_class._printed_skipped_message: + return + method.im_class._printed_skipped_message = True + logger.info('Skipping %s.%s: %s' % (method.__module__, method.im_class.__name__, message)) + return _skip diff --git a/WebKitTools/Scripts/webkitpy/test/skip_unittest.py b/WebKitTools/Scripts/webkitpy/test/skip_unittest.py new file mode 100644 index 0000000..f61a1bb --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/test/skip_unittest.py @@ -0,0 +1,77 @@ +# Copyright (C) 2010 Apple 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: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. + +import StringIO +import logging +import unittest + +from webkitpy.test.skip import skip_if + + +class SkipTest(unittest.TestCase): + def setUp(self): + self.logger = logging.getLogger(__name__) + + self.old_level = self.logger.level + self.logger.setLevel(logging.INFO) + + self.old_propagate = self.logger.propagate + self.logger.propagate = False + + self.log_stream = StringIO.StringIO() + self.handler = logging.StreamHandler(self.log_stream) + self.logger.addHandler(self.handler) + + self.foo_was_called = False + + def tearDown(self): + self.logger.removeHandler(self.handler) + self.propagate = self.old_propagate + self.logger.setLevel(self.old_level) + + def create_fixture_class(self): + class TestSkipFixture(object): + def __init__(self, callback): + self.callback = callback + + def test_foo(self): + self.callback() + + return TestSkipFixture + + def foo_callback(self): + self.foo_was_called = True + + def test_skip_if_false(self): + klass = skip_if(self.create_fixture_class(), False, 'Should not see this message.', logger=self.logger) + klass(self.foo_callback).test_foo() + self.assertEqual(self.log_stream.getvalue(), '') + self.assertTrue(self.foo_was_called) + + def test_skip_if_true(self): + klass = skip_if(self.create_fixture_class(), True, 'Should see this message.', logger=self.logger) + klass(self.foo_callback).test_foo() + self.assertEqual(self.log_stream.getvalue(), 'Skipping webkitpy.test.skip_unittest.TestSkipFixture: Should see this message.\n') + self.assertFalse(self.foo_was_called) + +if __name__ == '__main__': + unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/__init__.py b/WebKitTools/Scripts/webkitpy/tool/commands/__init__.py index 9bdec8f..a070324 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/__init__.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/__init__.py @@ -2,4 +2,5 @@ from webkitpy.tool.commands.prettydiff import PrettyDiff from webkitpy.tool.commands.rebaseline import Rebaseline +from webkitpy.tool.commands.rebaselineserver import RebaselineServer # FIXME: Add the rest of the commands here. diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html b/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html new file mode 100644 index 0000000..5667cd2 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/index.html @@ -0,0 +1,40 @@ + + + + + Layout Test Rebaseline Server + + + + + Exit + + diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.css b/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.css new file mode 100644 index 0000000..35bd6a5 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.css @@ -0,0 +1,56 @@ +/* + * 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. + */ + +body { + font-size: 12px; + font-family: Helvetica, Arial, sans-serif; + padding: 0; + margin: 0; +} + +.loading { + opacity: 0.5; +} + +div { + margin: 0; +} + +a, .link { + color: #aaf; + text-decoration: underline; + cursor: pointer; +} + +.link.selected { + color: #fff; + font-weight: bold; + text-decoration: none; +} diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.js b/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.js new file mode 100644 index 0000000..55f19a4 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/commands/data/rebaselineserver/main.js @@ -0,0 +1,36 @@ +/* + * 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. + */ + +function main() +{ + document.body.classList.remove('loading'); +} + +window.addEventListener('DOMContentLoaded', main); diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/rebaselineserver.py b/WebKitTools/Scripts/webkitpy/tool/commands/rebaselineserver.py new file mode 100644 index 0000000..0a37677 --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/tool/commands/rebaselineserver.py @@ -0,0 +1,157 @@ +# 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 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.tool.multicommandtool import AbstractDeclarativeCommand + + +class RebaselineHTTPServer(BaseHTTPServer.HTTPServer): + def __init__(self, httpd_port, results_directory): + BaseHTTPServer.HTTPServer.__init__(self, ("", httpd_port), RebaselineHTTPRequestHandler) + self.results_directory = results_directory + + +class RebaselineHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + STATIC_FILE_NAMES = frozenset([ + "index.html", + "main.js", + "main.css", + ]) + + 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 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 _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 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] + + print "Starting server at http://localhost:%d/" % options.httpd_port + print ("Use the 'Exit' link in the UI, http://localhost:%d/" + "quitquitquit or Ctrl-C to stop") % options.httpd_port + + httpd = RebaselineHTTPServer( + httpd_port=options.httpd_port, + results_directory=results_directory) + httpd.serve_forever() diff --git a/WebKitTools/Scripts/webkitpy/tool/commands/upload.py b/WebKitTools/Scripts/webkitpy/tool/commands/upload.py index ed91f5a..e12c8e2 100644 --- a/WebKitTools/Scripts/webkitpy/tool/commands/upload.py +++ b/WebKitTools/Scripts/webkitpy/tool/commands/upload.py @@ -92,6 +92,32 @@ class CleanPendingCommit(AbstractDeclarativeCommand): self._tool.bugs.obsolete_attachment(patch.id(), message) +# FIXME: This should be share more logic with AssignToCommitter and CleanPendingCommit +class CleanReviewQueue(AbstractDeclarativeCommand): + name = "clean-review-queue" + help_text = "Clear r? on obsolete patches so they do not appear in the pending-commit list." + + def execute(self, options, args, tool): + queue_url = "http://webkit.org/pending-review" + # We do this inefficient dance to be more like webkit.org/pending-review + # bugs.queries.fetch_bug_ids_from_review_queue() doesn't return + # closed bugs, but folks using /pending-review will see them. :( + for patch_id in tool.bugs.queries.fetch_attachment_ids_from_review_queue(): + patch = self._tool.bugs.fetch_attachment(patch_id) + if not patch.review() == "?": + continue + attachment_obsolete_modifier = "" + if patch.is_obsolete(): + attachment_obsolete_modifier = "obsolete " + elif patch.bug().is_closed(): + bug_closed_explanation = " If you would like this patch reviewed, please attach it to a new bug (or re-open this bug before marking it for review again)." + else: + # Neither the patch was obsolete or the bug was closed, next patch... + continue + message = "Cleared review? from %sattachment %s so that this bug does not appear in %s.%s" % (attachment_obsolete_modifier, patch.id(), queue_url, bug_closed_explanation) + self._tool.bugs.obsolete_attachment(patch.id(), message) + + class AssignToCommitter(AbstractDeclarativeCommand): name = "assign-to-committer" help_text = "Assign bug to whoever attached the most recent r+'d patch" diff --git a/WebKitTools/Scripts/webkitpy/tool/mocktool.py b/WebKitTools/Scripts/webkitpy/tool/mocktool.py index af232d9..b6ee95f 100644 --- a/WebKitTools/Scripts/webkitpy/tool/mocktool.py +++ b/WebKitTools/Scripts/webkitpy/tool/mocktool.py @@ -424,6 +424,9 @@ class MockSCM(Mock): # will actually be the root. Since getcwd() is wrong, use a globally fake root for now. self.checkout_root = self.fake_checkout_root + def changed_files(self, git_commit=None): + return ["MockFile1"] + def create_patch(self, git_commit, changed_files=None): return "Patch1" diff --git a/WebKitTools/Scripts/webkitpy/tool/steps/checkstyle.py b/WebKitTools/Scripts/webkitpy/tool/steps/checkstyle.py index af38214..af66c50 100644 --- a/WebKitTools/Scripts/webkitpy/tool/steps/checkstyle.py +++ b/WebKitTools/Scripts/webkitpy/tool/steps/checkstyle.py @@ -52,6 +52,9 @@ class CheckStyle(AbstractStep): args.append("--git-commit") args.append(self._options.git_commit) + args.append("--diff-files") + args.extend(self._changed_files(state)) + try: self._run_script("check-webkit-style", args) except ScriptError, e: -- cgit v1.1