diff options
Diffstat (limited to 'Tools/Scripts/webkitpy')
88 files changed, 1849 insertions, 1115 deletions
diff --git a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py index 82a637f08..b70e5d06f 100644 --- a/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py +++ b/Tools/Scripts/webkitpy/common/checkout/baselineoptimizer_unittest.py @@ -78,13 +78,13 @@ class BaselineOptimizerTest(unittest.TestCase): def test_no_add_mac_future(self): self._assertOptimization({ 'LayoutTests/platform/mac': '29a1715a6470d5dd9486a142f609708de84cdac8', - 'LayoutTests/platform/win': '453e67177a75b2e79905154ece0efba6e5bfb65d', - 'LayoutTests/platform/mac-snowleopard': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f', + 'LayoutTests/platform/win-xp': '453e67177a75b2e79905154ece0efba6e5bfb65d', + 'LayoutTests/platform/mac-lion': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f', 'LayoutTests/platform/chromium-mac': 'a9ba153c700a94ae1b206d8e4a75a621a89b4554', }, { 'LayoutTests/platform/mac': '29a1715a6470d5dd9486a142f609708de84cdac8', - 'LayoutTests/platform/win': '453e67177a75b2e79905154ece0efba6e5bfb65d', - 'LayoutTests/platform/mac-snowleopard': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f', + 'LayoutTests/platform/win-xp': '453e67177a75b2e79905154ece0efba6e5bfb65d', + 'LayoutTests/platform/mac-lion': 'c43eaeb358f49d5e835236ae23b7e49d7f2b089f', 'LayoutTests/platform/chromium-mac': 'a9ba153c700a94ae1b206d8e4a75a621a89b4554', }) @@ -107,9 +107,9 @@ class BaselineOptimizerTest(unittest.TestCase): def test_mac_future(self): self._assertOptimization({ - 'LayoutTests/platform/mac-snowleopard': '462d03b9c025db1b0392d7453310dbee5f9a9e74', + 'LayoutTests/platform/mac-lion': '462d03b9c025db1b0392d7453310dbee5f9a9e74', }, { - 'LayoutTests/platform/mac-snowleopard': '462d03b9c025db1b0392d7453310dbee5f9a9e74', + 'LayoutTests/platform/mac-lion': '462d03b9c025db1b0392d7453310dbee5f9a9e74', }) def test_qt_unknown(self): @@ -154,17 +154,17 @@ class BaselineOptimizerTest(unittest.TestCase): 'LayoutTests/platform/mac': '5daa78e55f05d9f0d1bb1f32b0cd1bc3a01e9364', 'LayoutTests/platform/chromium-win-xp': '462d03b9c025db1b0392d7453310dbee5f9a9e74', 'LayoutTests/platform/chromium-mac-leopard': '65e7d42f8b4882b29d46dc77bb879dd41bc074dc', - 'LayoutTests/platform/mac-leopard': '7ad045ece7c030e2283c5d21d9587be22bcba56e', + 'LayoutTests/platform/mac-lion': '7ad045ece7c030e2283c5d21d9587be22bcba56e', 'LayoutTests/platform/chromium-win-vista': 'f83af9732ce74f702b8c9c4a3d9a4c6636b8d3bd', - 'LayoutTests/platform/win': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7', + 'LayoutTests/platform/win-xp': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7', 'LayoutTests/platform/chromium-linux': 'f52fcdde9e4be8bd5142171cd859230bd4471036', }, { 'LayoutTests/platform/chromium-win': '462d03b9c025db1b0392d7453310dbee5f9a9e74', 'LayoutTests/platform/mac': '5daa78e55f05d9f0d1bb1f32b0cd1bc3a01e9364', 'LayoutTests/platform/chromium-win-xp': '462d03b9c025db1b0392d7453310dbee5f9a9e74', 'LayoutTests/platform/chromium-mac-leopard': '65e7d42f8b4882b29d46dc77bb879dd41bc074dc', - 'LayoutTests/platform/mac-leopard': '7ad045ece7c030e2283c5d21d9587be22bcba56e', + 'LayoutTests/platform/mac-lion': '7ad045ece7c030e2283c5d21d9587be22bcba56e', 'LayoutTests/platform/chromium-win-vista': 'f83af9732ce74f702b8c9c4a3d9a4c6636b8d3bd', - 'LayoutTests/platform/win': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7', + 'LayoutTests/platform/win-xp': '5b1253ef4d5094530d5f1bc6cdb95c90b446bec7', 'LayoutTests/platform/chromium-linux': 'f52fcdde9e4be8bd5142171cd859230bd4471036' }) diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/git.py b/Tools/Scripts/webkitpy/common/checkout/scm/git.py index 43fdb9c00..802d81db0 100644 --- a/Tools/Scripts/webkitpy/common/checkout/scm/git.py +++ b/Tools/Scripts/webkitpy/common/checkout/scm/git.py @@ -61,6 +61,8 @@ class Git(SCM, SVNRepository): # 1 or 128, mostly. ERROR_FILE_IS_MISSING = 128 + executable_name = 'git' + def __init__(self, cwd, **kwargs): SCM.__init__(self, cwd, **kwargs) self._check_git_architecture() @@ -89,7 +91,7 @@ class Git(SCM, SVNRepository): # We could path-search entirely in python or with # which.py (http://code.google.com/p/which), but this is easier: - git_path = self.run(['which', 'git']).rstrip() + git_path = self.run(['which', self.executable_name]).rstrip() if self._executable_is_64bit(git_path): return @@ -100,14 +102,14 @@ class Git(SCM, SVNRepository): def in_working_directory(cls, path): try: # FIXME: This should use an Executive. - return run_command(['git', 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true" + return run_command([cls.executable_name, 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true" except OSError, e: # The Windows bots seem to through a WindowsError when git isn't installed. return False def find_checkout_root(self, path): # "git rev-parse --show-cdup" would be another way to get to the root - git_output = self._executive.run_command(['git', 'rev-parse', '--git-dir'], cwd=(path or "./")) + git_output = self._executive.run_command([self.executable_name, 'rev-parse', '--git-dir'], cwd=(path or "./")) (checkout_root, dot_git) = self._filesystem.split(git_output) if not self._filesystem.isabs(checkout_root): # Sometimes git returns relative paths checkout_root = self._filesystem.join(path, checkout_root) @@ -125,7 +127,7 @@ class Git(SCM, SVNRepository): # Pass --get-all for cases where the config has multiple values # Pass the cwd if provided so that we can handle the case of running webkit-patch outside of the working directory. # FIXME: This should use an Executive. - return run_command(["git", "config", "--get-all", key], error_handler=Executive.ignore_error, cwd=cwd).rstrip('\n') + return run_command([cls.executable_name, "config", "--get-all", key], error_handler=Executive.ignore_error, cwd=cwd).rstrip('\n') @staticmethod def commit_success_regexp(): @@ -133,48 +135,48 @@ class Git(SCM, SVNRepository): def discard_local_commits(self): # FIXME: This should probably use cwd=self.checkout_root - self.run(['git', 'reset', '--hard', self.remote_branch_ref()]) + self.run([self.executable_name, 'reset', '--hard', self.remote_branch_ref()]) def local_commits(self): - return self.run(['git', 'log', '--pretty=oneline', 'HEAD...' + self.remote_branch_ref()], cwd=self.checkout_root).splitlines() + return self.run([self.executable_name, 'log', '--pretty=oneline', 'HEAD...' + self.remote_branch_ref()], cwd=self.checkout_root).splitlines() def rebase_in_progress(self): return self._filesystem.exists(self.absolute_path(self._filesystem.join('.git', 'rebase-apply'))) def working_directory_is_clean(self): - return self.run(['git', 'diff', 'HEAD', '--no-renames', '--name-only'], cwd=self.checkout_root) == "" + return self.run([self.executable_name, 'diff', 'HEAD', '--no-renames', '--name-only'], cwd=self.checkout_root) == "" def clean_working_directory(self): # FIXME: These should probably use cwd=self.checkout_root. # Could run git clean here too, but that wouldn't match working_directory_is_clean - self.run(['git', 'reset', '--hard', 'HEAD']) + self.run([self.executable_name, 'reset', '--hard', 'HEAD']) # Aborting rebase even though this does not match working_directory_is_clean if self.rebase_in_progress(): - self.run(['git', 'rebase', '--abort']) + self.run([self.executable_name, 'rebase', '--abort']) def status_command(self): # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead. # No file contents printed, thus utf-8 autodecoding in self.run is fine. - return ["git", "diff", "--name-status", "--no-renames", "HEAD"] + return [self.executable_name, "diff", "--name-status", "--no-renames", "HEAD"] def _status_regexp(self, expected_types): return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types def add_list(self, paths, return_exit_code=False): - return self.run(["git", "add"] + paths, return_exit_code=return_exit_code) + return self.run([self.executable_name, "add"] + paths, return_exit_code=return_exit_code) def delete_list(self, paths): - return self.run(["git", "rm", "-f"] + paths) + return self.run([self.executable_name, "rm", "-f"] + paths) def exists(self, path): - return_code = self.run(["git", "show", "HEAD:%s" % path], return_exit_code=True, decode_output=False) + return_code = self.run([self.executable_name, "show", "HEAD:%s" % path], return_exit_code=True, decode_output=False) return return_code != self.ERROR_FILE_IS_MISSING def _branch_from_ref(self, ref): return ref.replace('refs/heads/', '') def _current_branch(self): - return self._branch_from_ref(self.run(['git', 'symbolic-ref', '-q', 'HEAD'], cwd=self.checkout_root).strip()) + return self._branch_from_ref(self.run([self.executable_name, 'symbolic-ref', '-q', 'HEAD'], cwd=self.checkout_root).strip()) def _upstream_branch(self): current_branch = self._current_branch() @@ -201,14 +203,14 @@ class Git(SCM, SVNRepository): def changed_files(self, git_commit=None): # FIXME: --diff-filter could be used to avoid the "extract_filenames" step. - status_command = ['git', 'diff', '-r', '--name-status', "--no-renames", "--no-ext-diff", "--full-index", self.merge_base(git_commit)] + status_command = [self.executable_name, 'diff', '-r', '--name-status', "--no-renames", "--no-ext-diff", "--full-index", self.merge_base(git_commit)] # FIXME: I'm not sure we're returning the same set of files that SVN.changed_files is. # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R) return self.run_status_and_extract_filenames(status_command, self._status_regexp("ADM")) def _changes_files_for_commit(self, git_commit): # --pretty="format:" makes git show not print the commit log header, - changed_files = self.run(["git", "show", "--pretty=format:", "--name-only", git_commit]).splitlines() + changed_files = self.run([self.executable_name, "show", "--pretty=format:", "--name-only", git_commit]).splitlines() # instead it just prints a blank line at the top, so we skip the blank line: return changed_files[1:] @@ -222,13 +224,13 @@ class Git(SCM, SVNRepository): raise ScriptError(message="Path %s does not exist." % path) # git rev-list head --remove-empty --limit=5 -- path would be equivalent. - commit_ids = self.run(["git", "log", "--remove-empty", "--pretty=format:%H", "-%s" % limit, "--", path]).splitlines() + commit_ids = self.run([self.executable_name, "log", "--remove-empty", "--pretty=format:%H", "-%s" % limit, "--", path]).splitlines() return filter(lambda revision: revision, map(self.svn_revision_from_git_commit, commit_ids)) def conflicted_files(self): # We do not need to pass decode_output for this diff command # as we're passing --name-status which does not output any data. - status_command = ['git', 'diff', '--name-status', '--no-renames', '--diff-filter=U'] + status_command = [self.executable_name, 'diff', '--name-status', '--no-renames', '--diff-filter=U'] return self.run_status_and_extract_filenames(status_command, self._status_regexp("U")) def added_files(self): @@ -246,7 +248,7 @@ class Git(SCM, SVNRepository): def svn_revision(self, path): _log.debug('Running git.head_svn_revision... (Temporary logging message)') - git_log = self.run(['git', 'log', '-25', path]) + git_log = self.run([self.executable_name, 'log', '-25', path]) match = re.search("^\s*git-svn-id:.*@(?P<svn_revision>\d+)\ ", git_log, re.MULTILINE) if not match: return "" @@ -272,14 +274,14 @@ class Git(SCM, SVNRepository): if self._filesystem.exists(order_file): order = "-O%s" % order_file - command = ['git', 'diff', '--binary', "--no-ext-diff", "--full-index", "--no-renames", order, self.merge_base(git_commit), "--"] + command = [self.executable_name, 'diff', '--binary', "--no-ext-diff", "--full-index", "--no-renames", order, self.merge_base(git_commit), "--"] if changed_files: command += changed_files return self.prepend_svn_revision(self.run(command, decode_output=False, cwd=self.checkout_root)) def _run_git_svn_find_rev(self, arg): # git svn find-rev always exits 0, even when the revision or commit is not found. - return self.run(['git', 'svn', 'find-rev', arg], cwd=self.checkout_root).rstrip() + return self.run([self.executable_name, 'svn', 'find-rev', arg], cwd=self.checkout_root).rstrip() def _string_to_int_or_none(self, string): try: @@ -303,21 +305,21 @@ class Git(SCM, SVNRepository): def contents_at_revision(self, path, revision): """Returns a byte array (str()) containing the contents of path @ revision in the repository.""" - return self.run(["git", "show", "%s:%s" % (self.git_commit_from_svn_revision(revision), path)], decode_output=False) + return self.run([self.executable_name, "show", "%s:%s" % (self.git_commit_from_svn_revision(revision), path)], decode_output=False) def diff_for_revision(self, revision): git_commit = self.git_commit_from_svn_revision(revision) return self.create_patch(git_commit) def diff_for_file(self, path, log=None): - return self.run(['git', 'diff', 'HEAD', '--no-renames', '--', path], cwd=self.checkout_root) + return self.run([self.executable_name, 'diff', 'HEAD', '--no-renames', '--', path], cwd=self.checkout_root) def show_head(self, path): - return self.run(['git', 'show', 'HEAD:' + self.to_object_name(path)], decode_output=False) + return self.run([self.executable_name, 'show', 'HEAD:' + self.to_object_name(path)], decode_output=False) def committer_email_for_revision(self, revision): git_commit = self.git_commit_from_svn_revision(revision) - committer_email = self.run(["git", "log", "-1", "--pretty=format:%ce", git_commit]) + committer_email = self.run([self.executable_name, "log", "-1", "--pretty=format:%ce", git_commit]) # Git adds an extra @repository_hash to the end of every committer email, remove it: return committer_email.rsplit("@", 1)[0] @@ -325,10 +327,10 @@ class Git(SCM, SVNRepository): # Assume the revision is an svn revision. git_commit = self.git_commit_from_svn_revision(revision) # I think this will always fail due to ChangeLogs. - self.run(['git', 'revert', '--no-commit', git_commit], error_handler=Executive.ignore_error) + self.run([self.executable_name, 'revert', '--no-commit', git_commit], error_handler=Executive.ignore_error) def revert_files(self, file_paths): - self.run(['git', 'checkout', 'HEAD'] + file_paths) + self.run([self.executable_name, 'checkout', 'HEAD'] + file_paths) def _assert_can_squash(self, working_directory_is_clean): squash = Git.read_git_config('webkit-patch.commit-should-always-squash', cwd=self.checkout_root) @@ -362,7 +364,7 @@ class Git(SCM, SVNRepository): if not force_squash: self._assert_can_squash(working_directory_is_clean) - self.run(['git', 'reset', '--soft', self.remote_merge_base()], cwd=self.checkout_root) + self.run([self.executable_name, 'reset', '--soft', self.remote_merge_base()], cwd=self.checkout_root) self.commit_locally_with_message(message) return self.push_local_commits_to_server(username=username, password=password) @@ -385,16 +387,16 @@ class Git(SCM, SVNRepository): # We wrap in a try...finally block so if anything goes wrong, we clean up the branches. commit_succeeded = True try: - self.run(['git', 'checkout', '-q', '-b', MERGE_BRANCH_NAME, self.remote_branch_ref()]) + self.run([self.executable_name, 'checkout', '-q', '-b', MERGE_BRANCH_NAME, self.remote_branch_ref()]) for commit in commit_ids: # We're on a different branch now, so convert "head" to the branch name. commit = re.sub(r'(?i)head', branch_name, commit) # FIXME: Once changed_files and create_patch are modified to separately handle each # commit in a commit range, commit each cherry pick so they'll get dcommitted separately. - self.run(['git', 'cherry-pick', '--no-commit', commit]) + self.run([self.executable_name, 'cherry-pick', '--no-commit', commit]) - self.run(['git', 'commit', '-m', message]) + self.run([self.executable_name, 'commit', '-m', message]) output = self.push_local_commits_to_server(username=username, password=password) except Exception, e: log("COMMIT FAILED: " + str(e)) @@ -403,31 +405,31 @@ class Git(SCM, SVNRepository): finally: # And then swap back to the original branch and clean up. self.clean_working_directory() - self.run(['git', 'checkout', '-q', branch_name]) + self.run([self.executable_name, 'checkout', '-q', branch_name]) self.delete_branch(MERGE_BRANCH_NAME) return output def svn_commit_log(self, svn_revision): svn_revision = self.strip_r_from_svn_revision(svn_revision) - return self.run(['git', 'svn', 'log', '-r', svn_revision]) + return self.run([self.executable_name, 'svn', 'log', '-r', svn_revision]) def last_svn_commit_log(self): - return self.run(['git', 'svn', 'log', '--limit=1']) + return self.run([self.executable_name, 'svn', 'log', '--limit=1']) def svn_blame(self, path): - return self.run(['git', 'svn', 'blame', path]) + return self.run([self.executable_name, 'svn', 'blame', path]) # Git-specific methods: def _branch_ref_exists(self, branch_ref): - return self.run(['git', 'show-ref', '--quiet', '--verify', branch_ref], return_exit_code=True) == 0 + return self.run([self.executable_name, 'show-ref', '--quiet', '--verify', branch_ref], return_exit_code=True) == 0 def delete_branch(self, branch_name): if self._branch_ref_exists('refs/heads/' + branch_name): - self.run(['git', 'branch', '-D', branch_name]) + self.run([self.executable_name, 'branch', '-D', branch_name]) def remote_merge_base(self): - return self.run(['git', 'merge-base', self.remote_branch_ref(), 'HEAD'], cwd=self.checkout_root).strip() + return self.run([self.executable_name, 'merge-base', self.remote_branch_ref(), 'HEAD'], cwd=self.checkout_root).strip() def remote_branch_ref(self): # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists. @@ -444,10 +446,10 @@ class Git(SCM, SVNRepository): return first_remote_branch_ref.split(':')[1] def commit_locally_with_message(self, message): - self.run(['git', 'commit', '--all', '-F', '-'], input=message, cwd=self.checkout_root) + self.run([self.executable_name, 'commit', '--all', '-F', '-'], input=message, cwd=self.checkout_root) def push_local_commits_to_server(self, username=None, password=None): - dcommit_command = ['git', 'svn', 'dcommit'] + dcommit_command = [self.executable_name, 'svn', 'dcommit'] if (not username or not password) and not self.has_authorization_for_realm(SVN.svn_server_realm): raise AuthenticationError(SVN.svn_server_host, prompt_for_password=True) if username: @@ -469,14 +471,14 @@ class Git(SCM, SVNRepository): if '...' in commitish: raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish) elif '..' in commitish: - commit_ids += reversed(self.run(['git', 'rev-list', commitish]).splitlines()) + commit_ids += reversed(self.run([self.executable_name, 'rev-list', commitish]).splitlines()) else: # Turn single commits or branch or tag names into commit ids. - commit_ids += self.run(['git', 'rev-parse', '--revs-only', commitish]).splitlines() + commit_ids += self.run([self.executable_name, 'rev-parse', '--revs-only', commitish]).splitlines() return commit_ids def commit_message_for_local_commit(self, commit_id): - commit_lines = self.run(['git', 'cat-file', 'commit', commit_id]).splitlines() + commit_lines = self.run([self.executable_name, 'cat-file', 'commit', commit_id]).splitlines() # Skip the git headers. first_line_after_headers = 0 @@ -487,4 +489,4 @@ class Git(SCM, SVNRepository): return CommitMessage(commit_lines[first_line_after_headers:]) def files_changed_summary_for_commit(self, commit_id): - return self.run(['git', 'diff-tree', '--shortstat', '--no-renames', '--no-commit-id', commit_id]) + return self.run([self.executable_name, 'diff-tree', '--shortstat', '--no-renames', '--no-commit-id', commit_id]) diff --git a/Tools/Scripts/webkitpy/common/checkout/scm/svn.py b/Tools/Scripts/webkitpy/common/checkout/scm/svn.py index 3c269175c..a22063f0e 100644 --- a/Tools/Scripts/webkitpy/common/checkout/scm/svn.py +++ b/Tools/Scripts/webkitpy/common/checkout/scm/svn.py @@ -172,10 +172,6 @@ class SVN(SCM, SVNRepository): """Does 'svn add' to the path and its parents.""" if self.in_working_directory(path): return - dirname = os.path.dirname(path) - # We have dirname directry - ensure it added. - if dirname != path: - self._add_parent_directories(dirname) self.add(path) def add_list(self, paths, return_exit_code=False): @@ -189,9 +185,6 @@ class SVN(SCM, SVNRepository): if set(os.listdir(path)) - self._svn_metadata_files: return # Directory has non-trivial files in it. self.delete(path) - dirname = os.path.dirname(path) - if dirname != path: - self._delete_parent_directories(dirname) def delete_list(self, paths): for path in paths: diff --git a/Tools/Scripts/webkitpy/common/config/committers.py b/Tools/Scripts/webkitpy/common/config/committers.py index 673406c33..835b048bc 100644 --- a/Tools/Scripts/webkitpy/common/config/committers.py +++ b/Tools/Scripts/webkitpy/common/config/committers.py @@ -160,6 +160,7 @@ contributors_who_are_not_committers = [ Contributor("Tom Hudson", "tomhudson@google.com"), Contributor("WebKit Review Bot", "webkit.review.bot@gmail.com", "sheriff-bot"), Contributor("Wyatt Carss", ["wcarss@chromium.org", "wcarss@google.com"], "wcarss"), + Contributor("Zeev Lieber", "zlieber@chromium.org"), Contributor("Zoltan Arvai", "zarvai@inf.u-szeged.hu", "azbest_hu"), Contributor("Zsolt Feher", "feherzs@inf.u-szeged.hu", "Smith"), ] @@ -205,6 +206,7 @@ committers_unable_to_review = [ Committer("Benjamin C Meyer", ["ben@meyerhome.net", "ben@webkit.org", "bmeyer@rim.com"], "icefox"), Committer("Benjamin Kalman", ["kalman@chromium.org", "kalman@google.com"], "kalman"), Committer("Benjamin Otte", ["otte@gnome.org", "otte@webkit.org"], "otte"), + Committer("Bill Budge", ["bbudge@chromium.org", "bbudge@gmail.com"], "bbudge"), Committer("Brett Wilson", "brettw@chromium.org", "brettx"), Committer("Caio Marcelo de Oliveira Filho", ["cmarcelo@webkit.org", "caio.oliveira@openbossa.org"], "cmarcelo"), Committer("Cameron McCormack", ["cam@mcc.id.au", "cam@webkit.org"], "heycam"), @@ -252,7 +254,7 @@ committers_unable_to_review = [ Committer("Hans Wennborg", "hans@chromium.org", "hwennborg"), Committer("Hayato Ito", "hayato@chromium.org", "hayato"), Committer("Hironori Bono", "hbono@chromium.org", "hbono"), - Committer("Helder Correia", "helder@sencha.com", "helder"), + Committer("Helder Correia", "helder.correia@nokia.com", "helder"), Committer("Hin-Chung Lam", ["hclam@google.com", "hclam@chromium.org"]), Committer("Igor Trindade Oliveira", ["igor.oliveira@webkit.org", "igor.o@sisa.samsung.com"], "igoroliveira"), Committer("Ilya Sherman", "isherman@chromium.org", "isherman"), @@ -280,7 +282,7 @@ committers_unable_to_review = [ Committer("John Knottenbelt", "jknotten@chromium.org", "jknotten"), Committer("Johnny Ding", ["jnd@chromium.org", "johnnyding.webkit@gmail.com"], "johnnyding"), Committer("Jon Lee", "jonlee@apple.com", "jonlee"), - Committer("Joone Hur", "joone@webkit.org", "joone"), + Committer("Joone Hur", ["joone@webkit.org", "joone.hur@intel.com"], "joone"), Committer("Joost de Valk", ["joost@webkit.org", "webkit-dev@joostdevalk.nl"], "Altha"), Committer("Joshua Bell", ["jsbell@chromium.org", "jsbell@google.com"], "jsbell"), Committer("Julie Parent", ["jparent@google.com", "jparent@chromium.org"], "jparent"), @@ -296,6 +298,7 @@ committers_unable_to_review = [ Committer(u"Kim Gr\u00f6nholm", "kim.1.gronholm@nokia.com"), Committer("Kimmo Kinnunen", ["kimmo.t.kinnunen@nokia.com", "kimmok@iki.fi", "ktkinnun@webkit.org"], "kimmok"), Committer("Kinuko Yasuda", "kinuko@chromium.org", "kinuko"), + Committer("Konrad Piascik", "kpiascik@rim.com", "kpiascik"), Committer("Kristof Kosztyo", "kkristof@inf.u-szeged.hu", "kkristof"), Committer("Krzysztof Kowalczyk", "kkowalczyk@gmail.com"), Committer("Kwang Yul Seo", ["kwangyul.seo@gmail.com", "skyul@company100.net", "kseo@webkit.org"], "kwangseo"), @@ -341,7 +344,7 @@ committers_unable_to_review = [ Committer("Pierre-Olivier Latour", "pol@apple.com", "pol"), Committer("Pierre Rossi", "pierre.rossi@gmail.com", "elproxy"), Committer("Pratik Solanki", "psolanki@apple.com", "psolanki"), - Committer("Qi Zhang", ["qi.2.zhang@nokia.com", "qi.zhang02180@gmail.com"], "qi"), + Committer("Qi Zhang", "qi.zhang02180@gmail.com", "qi"), Committer("Rafael Antognolli", "antognolli@profusion.mobi", "antognolli"), Committer("Rafael Brandao", "rafael.lobo@openbossa.org", "rafaelbrandao"), Committer("Rafael Weinstein", "rafaelw@chromium.org", "rafaelw"), @@ -349,6 +352,7 @@ committers_unable_to_review = [ Committer("Ravi Kasibhatla", "ravi.kasibhatla@motorola.com", "kphanee"), Committer("Renata Hodovan", "reni@webkit.org", "reni"), Committer("Robert Hogan", ["robert@webkit.org", "robert@roberthogan.net", "lists@roberthogan.net"], "mwenge"), + Committer("Robert Kroeger", "rjkroege@chromium.org", "rjkroege"), Committer("Roland Steiner", "rolandsteiner@chromium.org"), Committer("Ryuan Choi", "ryuan.choi@samsung.com", "ryuan"), Committer("Satish Sampath", "satish@chromium.org"), @@ -379,7 +383,7 @@ committers_unable_to_review = [ Committer("Yael Aharon", "yael.aharon@nokia.com", "yael"), Committer("Yaar Schnitman", ["yaar@chromium.org", "yaar@google.com"]), Committer("Yi Shen", ["yi.4.shen@nokia.com", "shenyi2006@gmail.com"]), - Committer("Yong Li", ["yong.li.webkit@gmail.com", "yong.li@torchmobile.com", "yoli@rim.com"], "yong"), + Committer("Yong Li", ["yoli@rim.com", "yong.li.webkit@gmail.com"], "yoli"), Committer("Yongjun Zhang", ["yongjun.zhang@nokia.com", "yongjun_zhang@apple.com"]), Committer("Yoshifumi Inoue", "yosin@chromium.org", "yosin"), Committer("Yuqiang Xian", "yuqiang.xian@intel.com"), diff --git a/Tools/Scripts/webkitpy/common/host.py b/Tools/Scripts/webkitpy/common/host.py index ffc13bf98..083120227 100644 --- a/Tools/Scripts/webkitpy/common/host.py +++ b/Tools/Scripts/webkitpy/common/host.py @@ -104,6 +104,26 @@ class Host(SystemHost): SVN.executable_name = 'svn.bat' except OSError, e: _log.debug('Failed to engage svn.bat Windows hack.') + try: + self.executive.run_command(['git', 'help']) + except OSError, e: + try: + self.executive.run_command(['git.bat', 'help']) + # Chromium Win uses the depot_tools package, which contains a number + # of development tools, including Python and git. Instead of using a + # real git executable, depot_tools indirects via a batch file, called + # git.bat. This batch file allows depot_tools to auto-update the real + # git executable, which is contained in a subdirectory. + # + # That's all fine and good, except that subprocess.popen can detect + # the difference between a real git executable and batch file when we + # don't provide use shell=True. Rather than use shell=True on Windows, + # We hack the git.bat name into the SVN class. + _log.debug('Engaging git.bat Windows hack.') + from webkitpy.common.checkout.scm.git import Git + Git.executable_name = 'git.bat' + except OSError, e: + _log.debug('Failed to engage git.bat Windows hack.') def _initialize_scm(self, patch_directories=None): if sys.platform == "win32": diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py index 1afa287de..58d497dc6 100644 --- a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py +++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla.py @@ -209,12 +209,10 @@ class BugzillaQueries(object): # Currently this returns all bugs across all components. # In the future we may wish to extend this API to construct more restricted searches. - def fetch_bugs_matching_search(self, search_string, author_email=None): + def fetch_bugs_matching_search(self, search_string): query = "buglist.cgi?query_format=advanced" if search_string: query += "&short_desc_type=allwordssubstr&short_desc=%s" % urllib.quote(search_string) - if author_email: - query += "&emailreporter1=1&emailtype1=substring&email1=%s" % urllib.quote(search_string) return self._fetch_bugs_from_advanced_query(query) def fetch_patches_from_pending_commit_list(self): @@ -515,10 +513,21 @@ class Bugzilla(object): self.authenticated = True self.username = username + # FIXME: Use enum instead of two booleans def _commit_queue_flag(self, mark_for_landing, mark_for_commit_queue): if mark_for_landing: + user = self.committers.account_by_email(self.username) + mark_for_commit_queue = True + if not user: + log("Your Bugzilla login is not listed in committers.py. Uploading with cq? instead of cq+") + mark_for_landing = False + elif not user.can_commit: + log("You're not a committer yet or haven't updated committers.py yet. Uploading with cq? instead of cq+") + mark_for_landing = False + + if mark_for_landing: return '+' - elif mark_for_commit_queue: + if mark_for_commit_queue: return '?' return 'X' diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py index 36b465920..4e50189a1 100644 --- a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py +++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_mock.py @@ -269,7 +269,7 @@ class MockBugzillaQueries(object): def fetch_patches_from_pending_commit_list(self): return sum([bug.reviewed_patches() for bug in self._all_bugs()], []) - def fetch_bugs_matching_search(self, search_string, author_email=None): + def fetch_bugs_matching_search(self, search_string): return [self._bugzilla.fetch_bug(50004), self._bugzilla.fetch_bug(50003)] def fetch_bugs_matching_quicksearch(self, search_string): diff --git a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py index 986ce3bac..6108b5e8a 100644 --- a/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py +++ b/Tools/Scripts/webkitpy/common/net/bugzilla/bugzilla_unittest.py @@ -33,6 +33,7 @@ import StringIO from .bugzilla import Bugzilla, BugzillaQueries, EditUsersParser from webkitpy.common.config import urls +from webkitpy.common.config.committers import Reviewer, Committer, Contributor, CommitterList from webkitpy.common.system.outputcapture import OutputCapture from webkitpy.common.net.web_mock import MockBrowser from webkitpy.thirdparty.mock import Mock @@ -299,6 +300,43 @@ Ignore this bug. Just for testing failure modes of webkit-patch and the commit- filename = bugzilla._filename_for_upload(StringIO.StringIO(), 1234, extension="patch", timestamp=mock_timestamp) self.assertEqual(filename, "bug-1234-now.patch") + def test_commit_queue_flag(self): + bugzilla = Bugzilla() + + bugzilla.committers = CommitterList(reviewers=[Reviewer("WebKit Reviewer", "reviewer@webkit.org")], + committers=[Committer("WebKit Committer", "committer@webkit.org")], + contributors=[Contributor("WebKit Contributor", "contributor@webkit.org")], + watchers=[]) + + def assert_commit_queue_flag(mark_for_landing, mark_for_commit_queue, expected, username=None): + bugzilla.username = username + capture = OutputCapture() + capture.capture_output() + try: + self.assertEqual(bugzilla._commit_queue_flag(mark_for_landing=mark_for_landing, mark_for_commit_queue=mark_for_commit_queue), expected) + finally: + capture.restore_output() + + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='unknown@webkit.org') + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org') + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org') + assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org') + + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='contributor@webkit.org') + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='contributor@webkit.org') + assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='?', username='contributor@webkit.org') + assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='contributor@webkit.org') + + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='committer@webkit.org') + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='committer@webkit.org') + assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='committer@webkit.org') + assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='committer@webkit.org') + + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='reviewer@webkit.org') + assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='reviewer@webkit.org') + assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='reviewer@webkit.org') + assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='reviewer@webkit.org') + class BugzillaQueriesTest(unittest.TestCase): _sample_request_page = """ diff --git a/Tools/Scripts/webkitpy/common/system/executive.py b/Tools/Scripts/webkitpy/common/system/executive.py index 8759c719a..cb36b5dc0 100644 --- a/Tools/Scripts/webkitpy/common/system/executive.py +++ b/Tools/Scripts/webkitpy/common/system/executive.py @@ -454,7 +454,13 @@ class Executive(object): def run_in_parallel(self, command_lines_and_cwds, processes=None): """Runs a list of (cmd_line list, cwd string) tuples in parallel and returns a list of (retcode, stdout, stderr) tuples.""" - return multiprocessing.Pool(processes=processes).map(_run_command_thunk, command_lines_and_cwds) + if sys.platform in ('cygwin', 'win32'): + return map(_run_command_thunk, command_lines_and_cwds) + pool = multiprocessing.Pool(processes=processes) + results = pool.map(_run_command_thunk, command_lines_and_cwds) + pool.close() + pool.join() + return results def _run_command_thunk(cmd_line_and_cwd): diff --git a/Tools/Scripts/webkitpy/common/system/executive_unittest.py b/Tools/Scripts/webkitpy/common/system/executive_unittest.py index 466e2ec02..212896a4a 100644 --- a/Tools/Scripts/webkitpy/common/system/executive_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/executive_unittest.py @@ -218,6 +218,10 @@ class ExecutiveTest(unittest.TestCase): self.assertTrue(os.getpid() in pids) def test_run_in_parallel(self): + if sys.platform in ("win32", "cygwin"): + return # This function isn't implemented properly on windows yet. + import multiprocessing + NUM_PROCESSES = 4 DELAY_SECS = 0.25 cmd_line = [sys.executable, '-c', 'import time; time.sleep(%f); print "hello"' % DELAY_SECS] @@ -228,6 +232,7 @@ class ExecutiveTest(unittest.TestCase): done = time.time() self.assertTrue(done - start < NUM_PROCESSES * DELAY_SECS) self.assertEquals([output[1] for output in command_outputs], ["hello\n"] * NUM_PROCESSES) + self.assertEquals([], multiprocessing.active_children()) def main(platform, stdin, stdout, cmd, args): diff --git a/Tools/Scripts/webkitpy/common/system/outputcapture.py b/Tools/Scripts/webkitpy/common/system/outputcapture.py index 66188c0cb..4f931b7d1 100644 --- a/Tools/Scripts/webkitpy/common/system/outputcapture.py +++ b/Tools/Scripts/webkitpy/common/system/outputcapture.py @@ -3,7 +3,7 @@ # 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 @@ -13,7 +13,7 @@ # * 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 @@ -35,6 +35,13 @@ from StringIO import StringIO class OutputCapture(object): + # By default we capture the output to a stream. Other modules may override + # this function in order to do things like pass through the output. See + # webkitpy.test.main for an example. + @staticmethod + def stream_wrapper(stream): + return StringIO() + def __init__(self): self.saved_outputs = dict() self._log_level = logging.INFO @@ -45,8 +52,9 @@ class OutputCapture(object): self._logs_handler.setLevel(self._log_level) def _capture_output_with_name(self, output_name): - self.saved_outputs[output_name] = getattr(sys, output_name) - captured_output = StringIO() + stream = getattr(sys, output_name) + captured_output = self.stream_wrapper(stream) + self.saved_outputs[output_name] = stream setattr(sys, output_name, captured_output) return captured_output diff --git a/Tools/Scripts/webkitpy/common/system/path.py b/Tools/Scripts/webkitpy/common/system/path.py index b7ad3723a..e5a66bf87 100644 --- a/Tools/Scripts/webkitpy/common/system/path.py +++ b/Tools/Scripts/webkitpy/common/system/path.py @@ -35,11 +35,9 @@ import threading import urllib -def abspath_to_uri(path, platform=None): +def abspath_to_uri(platform, path): """Converts a platform-specific absolute path to a file: URL.""" - if platform is None: - platform = sys.platform - return "file:" + _escape(_convert_path(path, platform)) + return "file:" + _escape(_convert_path(platform, path)) def cygpath(path): @@ -118,12 +116,12 @@ def _escape(path): return urllib.quote(path, safe='/+:') -def _convert_path(path, platform): +def _convert_path(platform, path): """Handles any os-specific path separators, mappings, etc.""" - if platform == 'win32': - return _winpath_to_uri(path) - if platform == 'cygwin': + if platform.is_cygwin(): return _winpath_to_uri(cygpath(path)) + if platform.is_win(): + return _winpath_to_uri(path) return _unixypath_to_uri(path) diff --git a/Tools/Scripts/webkitpy/common/system/path_unittest.py b/Tools/Scripts/webkitpy/common/system/path_unittest.py index e08212a2c..954d32d71 100644 --- a/Tools/Scripts/webkitpy/common/system/path_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/path_unittest.py @@ -29,69 +29,41 @@ import unittest import sys -import path +from webkitpy.common.system.systemhost import SystemHost +from webkitpy.common.system.platforminfo import PlatformInfo +from webkitpy.common.system.platforminfo_mock import MockPlatformInfo +from webkitpy.common.system import path class AbspathTest(unittest.TestCase): - def assertMatch(self, test_path, expected_uri, - platform=None): - if platform == 'cygwin' and sys.platform != 'cygwin': - return - self.assertEqual(path.abspath_to_uri(test_path, platform=platform), - expected_uri) + def platforminfo(self): + return SystemHost().platform def test_abspath_to_uri_cygwin(self): if sys.platform != 'cygwin': return + self.assertEquals(path.abspath_to_uri(self.platforminfo(), '/cygdrive/c/foo/bar.html'), + 'file:///C:/foo/bar.html') - self.assertMatch('/cygdrive/c/foo/bar.html', - 'file:///C:/foo/bar.html', - platform='cygwin') - self.assertEqual(path.abspath_to_uri('/cygdrive/c/foo/bar.html', - platform='cygwin'), - 'file:///C:/foo/bar.html') - - def test_abspath_to_uri_darwin(self): - self.assertMatch('/foo/bar.html', - 'file:///foo/bar.html', - platform='darwin') - self.assertEqual(path.abspath_to_uri("/foo/bar.html", - platform='darwin'), - "file:///foo/bar.html") - - def test_abspath_to_uri_linux2(self): - self.assertMatch('/foo/bar.html', - 'file:///foo/bar.html', - platform='darwin') - self.assertEqual(path.abspath_to_uri("/foo/bar.html", - platform='linux2'), - "file:///foo/bar.html") - self.assertEqual(path.abspath_to_uri("/foo/bar.html", - platform='linux3'), - "file:///foo/bar.html") + def test_abspath_to_uri_unixy(self): + self.assertEquals(path.abspath_to_uri(MockPlatformInfo(), "/foo/bar.html"), + 'file:///foo/bar.html') def test_abspath_to_uri_win(self): - self.assertMatch('c:\\foo\\bar.html', - 'file:///c:/foo/bar.html', - platform='win32') - self.assertEqual(path.abspath_to_uri("c:\\foo\\bar.html", - platform='win32'), - "file:///c:/foo/bar.html") + if sys.platform != 'win32': + return + self.assertEquals(path.abspath_to_uri(self.platforminfo(), 'c:\\foo\\bar.html'), + 'file:///c:/foo/bar.html') - def test_abspath_to_uri_escaping(self): - self.assertMatch('/foo/bar + baz%?.html', - 'file:///foo/bar%20+%20baz%25%3F.html', - platform='darwin') - self.assertMatch('/foo/bar + baz%?.html', - 'file:///foo/bar%20+%20baz%25%3F.html', - platform='linux2') - self.assertMatch('/foo/bar + baz%?.html', - 'file:///foo/bar%20+%20baz%25%3F.html', - platform='linux3') + def test_abspath_to_uri_escaping_unixy(self): + self.assertEquals(path.abspath_to_uri(MockPlatformInfo(), '/foo/bar + baz%?.html'), + 'file:///foo/bar%20+%20baz%25%3F.html') # Note that you can't have '?' in a filename on windows. - self.assertMatch('/cygdrive/c/foo/bar + baz%.html', - 'file:///C:/foo/bar%20+%20baz%25.html', - platform='cygwin') + def test_abspath_to_uri_escaping_cygwin(self): + if sys.platform != 'cygwin': + return + self.assertEquals(path.abspath_to_uri(self.platforminfo(), '/cygdrive/c/foo/bar + baz%.html'), + 'file:///C:/foo/bar%20+%20baz%25.html') def test_stop_cygpath_subprocess(self): if sys.platform != 'cygwin': @@ -106,6 +78,3 @@ class AbspathTest(unittest.TestCase): # Ensure that it is stopped. self.assertFalse(path._CygPath._singleton.is_running()) - -if __name__ == '__main__': - unittest.main() diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo.py b/Tools/Scripts/webkitpy/common/system/platforminfo.py index 22cafbbee..74cff5412 100644 --- a/Tools/Scripts/webkitpy/common/system/platforminfo.py +++ b/Tools/Scripts/webkitpy/common/system/platforminfo.py @@ -54,6 +54,7 @@ class PlatformInfo(object): self.os_version = self._determine_mac_version(platform_module.mac_ver()[0]) if self.os_name.startswith('win'): self.os_version = self._determine_win_version(self._win_version_tuple(sys_module)) + self._is_cygwin = sys_module.platform == 'cygwin' def is_mac(self): return self.os_name == 'mac' @@ -61,6 +62,9 @@ class PlatformInfo(object): def is_win(self): return self.os_name == 'win' + def is_cygwin(self): + return self._is_cygwin + def is_linux(self): return self.os_name == 'linux' @@ -77,18 +81,9 @@ class PlatformInfo(object): # Windows-2008ServerR2-6.1.7600 return self._platform_module.platform() - def free_bytes_memory(self): - if self.is_mac(): - vm_stat_output = self._executive.run_command(["vm_stat"]) - free_bytes = self._compute_bytes_from_vm_stat_output("Pages free", vm_stat_output) - # Per https://bugs.webkit.org/show_bug.cgi?id=74650 include inactive memory since the OS is lazy about freeing memory. - free_bytes += self._compute_bytes_from_vm_stat_output("Pages inactive", vm_stat_output) - return free_bytes - return None - def total_bytes_memory(self): if self.is_mac(): - return int(self._executive.run_command(["sysctl", "-n", "hw.memsize"])) + return long(self._executive.run_command(["sysctl", "-n", "hw.memsize"])) return None def _determine_os_name(self, sys_platform): @@ -137,14 +132,3 @@ class PlatformInfo(object): match_object = re.search(r'(?P<major>\d)\.(?P<minor>\d)\.(?P<build>\d+)', ver_output) assert match_object, 'cmd returned an unexpected version string: ' + ver_output return tuple(map(int, match_object.groups())) - - def _compute_bytes_from_vm_stat_output(self, label_text, vm_stat_output): - page_size_match = re.search(r"page size of (\d+) bytes", vm_stat_output) - free_pages_match = re.search(r"%s:\s+(\d+)." % label_text, vm_stat_output) - - # Fail hard if vmstat's output isn't what we expect. - assert(page_size_match and free_pages_match) - - free_page_count = int(free_pages_match.group(1)) - page_size = int(page_size_match.group(1)) - return free_page_count * page_size diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo_mock.py b/Tools/Scripts/webkitpy/common/system/platforminfo_mock.py index c953aa185..34fa97fb4 100644 --- a/Tools/Scripts/webkitpy/common/system/platforminfo_mock.py +++ b/Tools/Scripts/webkitpy/common/system/platforminfo_mock.py @@ -41,6 +41,9 @@ class MockPlatformInfo(object): def is_win(self): return self.os_name == 'win' + def is_cygwin(self): + return False + def is_freebsd(self): return self.os_name == 'freebsd' @@ -48,7 +51,4 @@ class MockPlatformInfo(object): return "MockPlatform 1.0" def total_bytes_memory(self): - return 2 * 1024 * 1024 * 1024 # 2GB is a reasonable amount of ram to mock. - - def free_bytes_memory(self): - return 1 * 1024 * 1024 * 1024 # 1GB is a reasonable amount of ram to mock as free. + return 3 * 1024 * 1024 * 1024 # 3GB is a reasonable amount of ram to mock. diff --git a/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py b/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py index 5a1f85fc3..8fc961b08 100644 --- a/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py +++ b/Tools/Scripts/webkitpy/common/system/platforminfo_unittest.py @@ -82,10 +82,8 @@ class TestPlatformInfo(unittest.TestCase): if info.is_mac(): self.assertTrue(info.total_bytes_memory() > 0) - self.assertTrue(info.free_bytes_memory() > 0) else: self.assertEquals(info.total_bytes_memory(), None) - self.assertEquals(info.free_bytes_memory(), None) def test_os_name_and_wrappers(self): info = self.make_info(fake_sys('linux2')) @@ -180,22 +178,6 @@ class TestPlatformInfo(unittest.TestCase): info = self.make_info(fake_sys('freebsd9')) self.assertEquals(info.total_bytes_memory(), None) - def test_free_bytes_memory(self): - vmstat_output = ("Mach Virtual Memory Statistics: (page size of 4096 bytes)\n" - "Pages free: 1.\n" - "Pages inactive: 1.\n") - info = self.make_info(fake_sys('darwin'), fake_platform('10.6.3'), fake_executive(vmstat_output)) - self.assertEquals(info.free_bytes_memory(), 8192) - - info = self.make_info(fake_sys('win32', tuple([6, 1, 7600]))) - self.assertEquals(info.free_bytes_memory(), None) - - info = self.make_info(fake_sys('linux2')) - self.assertEquals(info.free_bytes_memory(), None) - - info = self.make_info(fake_sys('freebsd9')) - self.assertEquals(info.free_bytes_memory(), None) - if __name__ == '__main__': unittest.main() diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py b/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py index e762cfda0..3189563d2 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py @@ -236,10 +236,13 @@ def summarize_results(port_obj, expectations, result_summary, retry_summary, tes results['layout_tests_dir'] = port_obj.layout_tests_dir() results['has_wdiff'] = port_obj.wdiff_available() results['has_pretty_patch'] = port_obj.pretty_patch_available() + results['pixel_tests_enabled'] = port_obj.get_option('pixel_tests') + try: # We only use the svn revision for using trac links in the results.html file, # Don't do this by default since it takes >100ms. if use_trac_links_in_results_html(port_obj): + port_obj.host._initialize_scm() results['revision'] = port_obj.host.scm().head_svn_revision() except Exception, e: _log.warn("Failed to determine svn revision for checkout (cwd: %s, webkit_base: %s), leaving 'revision' key blank in full_results.json.\n%s" % (port_obj._filesystem.getcwd(), port_obj.path_from_webkit_base(), e)) @@ -316,6 +319,8 @@ class Manager(object): # a set of test files, and the same tests as a list + self._paths = set() + # FIXME: Rename to test_names. self._test_files = set() self._test_files_list = None @@ -340,6 +345,7 @@ class Manager(object): paths = self._strip_test_dir_prefixes(args) if self._options.test_list: paths += self._strip_test_dir_prefixes(read_test_files(self._filesystem, self._options.test_list, self._port.TEST_PATH_SEPARATOR)) + self._paths = set(paths) self._test_files = self._port.tests(paths) def _strip_test_dir_prefixes(self, paths): @@ -360,6 +366,9 @@ class Manager(object): def _http_tests(self): return set(test for test in self._test_files if self._is_http_test(test)) + def _websocket_tests(self): + return set(test for test in self._test_files if self.WEBSOCKET_SUBDIR in test) + def _is_perf_test(self, test): return self.PERF_SUBDIR == test or (self.PERF_SUBDIR + self._port.TEST_PATH_SEPARATOR) in test @@ -456,24 +465,40 @@ class Manager(object): # Remove skipped - both fixable and ignored - files from the # top-level list of files to test. + found_test_files = set(self._test_files) num_all_test_files = len(self._test_files) - self._printer.print_expected("Found: %d tests" % (len(self._test_files))) - if not num_all_test_files: - _log.critical('No tests to run.') - return None - - skipped = set() + skipped = self._expectations.get_tests_with_result_type(test_expectations.SKIP) if not self._options.http: - skipped = skipped.union(self._http_tests()) + skipped.update(set(self._http_tests())) + + if self._options.skipped == 'only': + self._test_files = self._test_files.intersection(skipped) + elif self._options.skipped == 'default': + self._test_files -= skipped + elif self._options.skipped == 'ignore': + pass # just to be clear that we're ignoring the skip list. - if num_all_test_files > 1 and not self._options.force: - skipped.update(self._expectations.get_tests_with_result_type(test_expectations.SKIP)) - if self._options.skip_failing_tests: - skipped.update(self._expectations.get_tests_with_result_type(test_expectations.FAIL)) - skipped.update(self._expectations.get_tests_with_result_type(test_expectations.FLAKY)) + if self._options.skip_failing_tests: + self._test_files -= self._expectations.get_tests_with_result_type(test_expectations.FAIL) + self._test_files -= self._expectations.get_tests_with_result_type(test_expectations.FLAKY) - self._test_files -= skipped + # now make sure we're explicitly running any tests passed on the command line. + self._test_files.update(found_test_files.intersection(self._paths)) + + if not num_all_test_files: + _log.critical('No tests to run.') + return None + + num_skipped = num_all_test_files - len(self._test_files) + if num_skipped: + self._printer.print_expected("Running %s (found %d, skipping %d)." % ( + grammar.pluralize('test', num_all_test_files - num_skipped), + num_all_test_files, num_skipped)) + elif len(self._test_files) > 1: + self._printer.print_expected("Running all %d tests." % len(self._test_files)) + else: + self._printer.print_expected("Running %1 test.") # Create a sorted list of test files so the subset chunk, # if used, contains alphabetically consecutive tests. @@ -504,9 +529,7 @@ class Manager(object): self._print_expected_results_of_type(result_summary, test_expectations.FLAKY, "flaky") self._print_expected_results_of_type(result_summary, test_expectations.SKIP, "skipped") - if self._options.force: - self._printer.print_expected('Running all tests, including skips (--force)') - else: + if self._options.skipped != 'ignore': # Note that we don't actually run the skipped tests (they were # subtracted out of self._test_files, above), but we stub out the # results here so the statistics can remain accurate. @@ -742,7 +765,7 @@ class Manager(object): all_shards = locked_shards + unlocked_shards self._remaining_locked_shards = locked_shards - if locked_shards and self._options.http: + if self._options.http and (self._http_tests() or self._websocket_tests()): self.start_servers_with_lock() num_workers = min(num_workers, len(all_shards)) @@ -797,7 +820,8 @@ class Manager(object): _log.error('Worker %d did not exit in time.' % worker_state.number) except KeyboardInterrupt: - self._printer.print_update('Interrupted, exiting ...') + self._printer.flush() + self._printer.write('Interrupted, exiting ...') self.cancel_workers() keyboard_interrupted = True except TestRunInterruptedException, e: @@ -946,21 +970,24 @@ class Manager(object): return self._port.exit_code_from_summarized_results(unexpected_results) def start_servers_with_lock(self): - assert(self._options.http) self._printer.print_update('Acquiring http lock ...') self._port.acquire_http_lock() - self._printer.print_update('Starting HTTP server ...') - self._port.start_http_server() - self._printer.print_update('Starting WebSocket server ...') - self._port.start_websocket_server() + if self._http_tests(): + self._printer.print_update('Starting HTTP server ...') + self._port.start_http_server() + if self._websocket_tests(): + self._printer.print_update('Starting WebSocket server ...') + self._port.start_websocket_server() self._has_http_lock = True def stop_servers_with_lock(self): if self._has_http_lock: - self._printer.print_update('Stopping HTTP server ...') - self._port.stop_http_server() - self._printer.print_update('Stopping WebSocket server ...') - self._port.stop_websocket_server() + if self._http_tests(): + self._printer.print_update('Stopping HTTP server ...') + self._port.stop_http_server() + if self._websocket_tests(): + self._printer.print_update('Stopping WebSocket server ...') + self._port.stop_websocket_server() self._printer.print_update('Releasing server lock ...') self._port.release_http_lock() self._has_http_lock = False @@ -1445,9 +1472,10 @@ class Manager(object): worker_state.current_test_name = test_info.test_name worker_state.next_timeout = time.time() + hang_timeout - def handle_done(self, source): + def handle_done(self, source, log_messages=None): worker_state = self._worker_states[source] worker_state.done = True + self._log_messages(log_messages) def handle_exception(self, source, exception_type, exception_value, stack): if exception_type in (KeyboardInterrupt, TestRunInterruptedException): @@ -1474,16 +1502,21 @@ class Manager(object): if not self._remaining_locked_shards: self.stop_servers_with_lock() - def handle_finished_test(self, source, result, elapsed_time): + def handle_finished_test(self, source, result, elapsed_time, log_messages=None): worker_state = self._worker_states[source] worker_state.next_timeout = None worker_state.current_test_name = None worker_state.stats['total_time'] += elapsed_time worker_state.stats['num_tests'] += 1 + self._log_messages(log_messages) self._all_results.append(result) self._update_summary_with_result(self._current_result_summary, result) + def _log_messages(self, messages): + for message in messages: + self._printer.writeln(*message) + def _log_worker_stack(self, stack): webkitpydir = self._port.path_from_webkit_base('Tools', 'Scripts', 'webkitpy') + self._filesystem.sep for filename, line_number, function_name, text in stack: diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py index 3ddab8ec3..13011883f 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py @@ -280,7 +280,7 @@ class ManagerTest(unittest.TestCase): host = MockHost() port = host.port_factory.get('test-win-xp') test = 'failures/expected/reftest.html' - port.test_expectations = lambda: 'WONTFIX : failures/expected/reftest.html = IMAGE' + port.expectations_dict = lambda: {'': 'WONTFIX : failures/expected/reftest.html = IMAGE'} expectations = TestExpectations(port, tests=[test]) # Reftests expected to be image mismatch should be respected when pixel_tests=False. manager = Manager(port=port, options=MockOptions(pixel_tests=False, exit_after_n_failures=None, exit_after_n_crashes_or_timeouts=None), printer=Mock()) @@ -342,6 +342,58 @@ class ManagerTest(unittest.TestCase): manager = get_manager_with_tests(tests) manager._look_for_new_crash_logs(rs, time.time()) + def test_servers_started(self): + + def start_http_server(): + self.http_started = True + + def start_websocket_server(): + self.websocket_started = True + + def stop_http_server(): + self.http_stopped = True + + def stop_websocket_server(): + self.websocket_stopped = True + + host = MockHost() + port = host.port_factory.get('test-mac-leopard') + port.start_http_server = start_http_server + port.start_websocket_server = start_websocket_server + port.stop_http_server = stop_http_server + port.stop_websocket_server = stop_websocket_server + + self.http_started = self.http_stopped = self.websocket_started = self.websocket_stopped = False + manager = Manager(port=port, options=MockOptions(http=True), printer=Mock()) + manager._test_files = ['http/tests/pass.txt'] + manager.start_servers_with_lock() + self.assertEquals(self.http_started, True) + self.assertEquals(self.websocket_started, False) + manager.stop_servers_with_lock() + self.assertEquals(self.http_stopped, True) + self.assertEquals(self.websocket_stopped, False) + + self.http_started = self.http_stopped = self.websocket_started = self.websocket_stopped = False + manager = Manager(port=port, options=MockOptions(http=True), printer=Mock()) + manager._test_files = ['websocket/pass.txt'] + manager.start_servers_with_lock() + self.assertEquals(self.http_started, True) + self.assertEquals(self.websocket_started, True) + manager.stop_servers_with_lock() + self.assertEquals(self.http_stopped, True) + self.assertEquals(self.websocket_stopped, True) + + self.http_started = self.http_stopped = self.websocket_started = self.websocket_stopped = False + manager = Manager(port=port, options=MockOptions(http=True), printer=Mock()) + manager._test_files = ['perf/foo/test.html'] + manager.start_servers_with_lock() + self.assertEquals(self.http_started, False) + self.assertEquals(self.websocket_started, False) + manager.stop_servers_with_lock() + self.assertEquals(self.http_stopped, False) + self.assertEquals(self.websocket_stopped, False) + + class NaturalCompareTest(unittest.TestCase): def assert_cmp(self, x, y, result): @@ -422,7 +474,7 @@ class ResultSummaryTest(unittest.TestCase): return test_results.TestResult(test_name, failures=failures, test_run_time=run_time) def get_result_summary(self, port, test_names, expectations_str): - port.test_expectations = lambda: expectations_str + port.expectations_dict = lambda: {'': expectations_str} expectations = test_expectations.TestExpectations(port, test_names) return test_names, result_summary.ResultSummary(expectations, test_names), expectations @@ -481,7 +533,7 @@ class ResultSummaryTest(unittest.TestCase): port = host.port_factory.get('test') port._options.builder_name = 'dummy builder' port._filesystem.write_text_file(port._filesystem.join(port.layout_tests_dir(), "failures/expected/wontfix.html"), "Dummy test contents") - expected_results, unexpected_results = self.summarized_results(port, expected=False, passing=False, flaky=False, extra_tests=['failures/expected/wontfix.html'], extra_expectations='BUGX WONTFIX : failures/expected/wontfix.html = FAIL\n') + expected_results, unexpected_results = self.summarized_results(port, expected=False, passing=False, flaky=False, extra_tests=['failures/expected/wontfix.html'], extra_expectations='BUGX WONTFIX : failures/expected/wontfix.html = TEXT\n') self.assertTrue(expected_results['tests']['failures']['expected']['wontfix.html']['wontfix']) if __name__ == '__main__': diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/manager_worker_broker_unittest.py b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_worker_broker_unittest.py index 93806e7d8..046425664 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/manager_worker_broker_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/manager_worker_broker_unittest.py @@ -215,14 +215,14 @@ class InterfaceTest(unittest.TestCase): def test_managerconnection_is_abstract(self): # Test that all the base class methods are abstract and have the # signature we expect. - broker = make_broker(self, 'inline') + broker = make_broker(self, 1) obj = manager_worker_broker._ManagerConnection(broker._broker, self, None) self.assertRaises(NotImplementedError, obj.start_worker) def test_workerconnection_is_abstract(self): # Test that all the base class methods are abstract and have the # signature we expect. - broker = make_broker(self, 'inline') + broker = make_broker(self, 1) obj = manager_worker_broker._WorkerConnection(broker._broker, _TestWorker, None) self.assertRaises(NotImplementedError, obj.cancel) self.assertRaises(NotImplementedError, obj.is_alive) diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py b/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py index 529aea222..fa07e88ff 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py @@ -46,7 +46,8 @@ def run_single_test(port, options, test_input, driver, worker_name): return runner.run() -class SingleTestRunner: +class SingleTestRunner(object): + (ALONGSIDE_TEST, PLATFORM_DIR, VERSION_DIR, UPDATE) = ('alongside', 'platform', 'version', 'update') def __init__(self, options, port, driver, test_input, worker_name): self._options = options @@ -94,12 +95,12 @@ class SingleTestRunner: def run(self): if self._reference_files: - if self._port.get_option('no_ref_tests') or self._options.new_baseline or self._options.reset_results: + if self._port.get_option('no_ref_tests') or self._options.reset_results: result = TestResult(self._test_name) result.type = test_expectations.SKIP return result return self._run_reftest() - if self._options.new_baseline or self._options.reset_results: + if self._options.reset_results: return self._run_rebaseline() return self._run_compare_test() @@ -121,7 +122,7 @@ class SingleTestRunner: driver_output = self._driver.run_test(self._driver_input()) failures = self._handle_error(driver_output) test_result_writer.write_test_result(self._filesystem, self._port, self._test_name, driver_output, None, failures) - # FIXME: It the test crashed or timed out, it might be bettter to avoid + # FIXME: It the test crashed or timed out, it might be better to avoid # to write new baselines. self._overwrite_baselines(driver_output) return TestResult(self._test_name, failures, driver_output.test_time, driver_output.has_stderr()) @@ -131,49 +132,50 @@ class SingleTestRunner: def _add_missing_baselines(self, test_result, driver_output): missingImage = test_result.has_failure_matching_types(test_failures.FailureMissingImage, test_failures.FailureMissingImageHash) if test_result.has_failure_matching_types(test_failures.FailureMissingResult): - self._save_baseline_data(driver_output.text, ".txt", SingleTestRunner._render_tree_dump_pattern.match(driver_output.text)) + self._save_baseline_data(driver_output.text, '.txt', self._location_for_new_baseline(driver_output.text, '.txt')) if test_result.has_failure_matching_types(test_failures.FailureMissingAudio): - self._save_baseline_data(driver_output.audio, ".wav", generate_new_baseline=False) + self._save_baseline_data(driver_output.audio, '.wav', self._location_for_new_baseline(driver_output.audio, '.wav')) if missingImage: - self._save_baseline_data(driver_output.image, ".png", generate_new_baseline=True) + self._save_baseline_data(driver_output.image, '.png', self._location_for_new_baseline(driver_output.image, '.png')) + + def _location_for_new_baseline(self, data, extension): + if self._options.add_platform_exceptions: + return self.VERSION_DIR + if extension == '.png': + return self.PLATFORM_DIR + if extension == '.wav': + return self.ALONGSIDE_TEST + if extension == '.txt' and self._render_tree_dump_pattern.match(data): + return self.PLATFORM_DIR + return self.ALONGSIDE_TEST def _overwrite_baselines(self, driver_output): - # Although all DumpRenderTree output should be utf-8, - # we do not ever decode it inside run-webkit-tests. For some tests - # DumpRenderTree may not output utf-8 text (e.g. webarchives). - self._save_baseline_data(driver_output.text, ".txt", generate_new_baseline=self._options.new_baseline) - self._save_baseline_data(driver_output.audio, ".wav", generate_new_baseline=self._options.new_baseline) + location = self.VERSION_DIR if self._options.add_platform_exceptions else self.UPDATE + self._save_baseline_data(driver_output.text, '.txt', location) + self._save_baseline_data(driver_output.audio, '.wav', location) if self._options.pixel_tests: - self._save_baseline_data(driver_output.image, ".png", generate_new_baseline=self._options.new_baseline) + self._save_baseline_data(driver_output.image, '.png', location) - def _save_baseline_data(self, data, modifier, generate_new_baseline=True): - """Saves a new baseline file into the port's baseline directory. - - The file will be named simply "<test>-expected<modifier>", suitable for - use as the expected results in a later run. - - Args: - data: result to be saved as the new baseline - modifier: type of the result file, e.g. ".txt" or ".png" - generate_new_baseline: whether to enerate a new, platform-specific - baseline, or update the existing one - """ + def _save_baseline_data(self, data, extension, location): if data is None: return port = self._port fs = self._filesystem - if generate_new_baseline: - relative_dir = fs.dirname(self._test_name) - baseline_path = port.baseline_path() - output_dir = fs.join(baseline_path, relative_dir) - output_file = fs.basename(fs.splitext(self._test_name)[0] + "-expected" + modifier) - fs.maybe_make_directory(output_dir) - output_path = fs.join(output_dir, output_file) + if location == self.ALONGSIDE_TEST: + output_dir = fs.dirname(port.abspath_for_test(self._test_name)) + elif location == self.VERSION_DIR: + output_dir = fs.join(port.baseline_version_dir(), fs.dirname(self._test_name)) + elif location == self.PLATFORM_DIR: + output_dir = fs.join(port.baseline_platform_dir(), fs.dirname(self._test_name)) + elif location == self.UPDATE: + output_dir = fs.dirname(port.expected_filename(self._test_name, extension)) else: - output_path = port.expected_filename(self._test_name, modifier) + raise AssertionError('unrecognized baseline location: %s' % location) - result_name = fs.relpath(output_path, port.layout_tests_dir()) - _log.info('Writing new expected result "%s"' % result_name) + fs.maybe_make_directory(output_dir) + output_basename = fs.basename(fs.splitext(self._test_name)[0] + "-expected" + extension) + output_path = fs.join(output_dir, output_basename) + _log.info('Writing new expected result "%s"' % port.relative_test_filename(output_path)) port.update_baseline(output_path, data) def _handle_error(self, driver_output, reference_filename=None): diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/test_expectations_editor_unittest.py b/Tools/Scripts/webkitpy/layout_tests/controllers/test_expectations_editor_unittest.py index 31029a35f..09f9670a0 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/test_expectations_editor_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/test_expectations_editor_unittest.py @@ -77,7 +77,7 @@ class TestExpectationEditorTests(unittest.TestCase): def make_parsed_expectation_lines(self, in_string): parser = TestExpectationParser(self.test_port, self.full_test_list, allow_rebaseline_modifier=False) - expectation_lines = parser.parse(in_string) + expectation_lines = parser.parse('path', in_string) for expectation_line in expectation_lines: self.assertFalse(expectation_line.is_invalid()) return expectation_lines @@ -164,8 +164,8 @@ BUGX2 XP DEBUG : failures/expected/keyboard.html = IMAGE""", 'failures/expected/ BUGX2 XP DEBUG : failures/expected/keyboard.html = IMAGE""") self.assert_remove_roundtrip(""" -BUGX1 WIN : failures/expected = FAIL""", 'failures/expected/keyboard.html', """ -BUGX1 WIN : failures/expected = FAIL""") +BUGX1 WIN : failures/expected = TEXT""", 'failures/expected/keyboard.html', """ +BUGX1 WIN : failures/expected = TEXT""") self.assert_remove_roundtrip(""" BUGX1 XP RELEASE : failures/expected/keyboard.html = IMAGE PASS @@ -326,11 +326,11 @@ BUG_UPDATE2 XP DEBUG : failures/expected/keyboard.html = TEXT BUG_UPDATE3 WIN RELEASE : failures/expected/keyboard.html = CRASH BUGX2 WIN : failures/expected/audio.html = IMAGE""") - editor.update_expectation(test, self.RELEASE_CONFIGS, set([FAIL]), ['BUG_UPDATE4']) + editor.update_expectation(test, self.RELEASE_CONFIGS, set([IMAGE_PLUS_TEXT]), ['BUG_UPDATE4']) self.assertEquals(TestExpectationSerializer.list_to_string(expectation_lines, converter), """ BUGX1 VISTA WIN7 DEBUG : failures/expected/keyboard.html = IMAGE BUG_UPDATE2 XP DEBUG : failures/expected/keyboard.html = TEXT -BUG_UPDATE4 RELEASE : failures/expected/keyboard.html = FAIL +BUG_UPDATE4 RELEASE : failures/expected/keyboard.html = IMAGE+TEXT BUGX2 WIN : failures/expected/audio.html = IMAGE""") editor.update_expectation(test, set(self.test_port.all_test_configurations()), set([TIMEOUT]), ['BUG_UPDATE5']) diff --git a/Tools/Scripts/webkitpy/layout_tests/controllers/worker.py b/Tools/Scripts/webkitpy/layout_tests/controllers/worker.py index a1e3bee70..d321b5a41 100644 --- a/Tools/Scripts/webkitpy/layout_tests/controllers/worker.py +++ b/Tools/Scripts/webkitpy/layout_tests/controllers/worker.py @@ -29,6 +29,7 @@ """Handle messages from the Manager and executes actual tests.""" import logging +import os import sys import threading import time @@ -65,7 +66,9 @@ class Worker(manager_worker_broker.AbstractWorker): self._driver = None self._tests_run_file = None self._tests_run_filename = None - self._meter = None + self._log_messages = [] + self._logger = None + self._log_handler = None def __del__(self): self.cleanup() @@ -83,29 +86,19 @@ class Worker(manager_worker_broker.AbstractWorker): self._tests_run_file = self._filesystem.open_text_file_for_writing(tests_run_filename) def _set_up_logging(self): + self._logger = logging.getLogger() + # The unix multiprocessing implementation clones the MeteredStream log handler - # into the child process, so we need to remove it before we can - # add a new one to get the correct pid logged. - root_logger = logging.getLogger() - handler_to_remove = None - for h in root_logger.handlers: + # into the child process, so we need to remove it to avoid duplicate logging. + for h in self._logger.handlers: # log handlers don't have names until python 2.7. if getattr(h, 'name', '') == metered_stream.LOG_HANDLER_NAME: - handler_to_remove = h + self._logger.removeHandler(h) break - if handler_to_remove: - root_logger.removeHandler(handler_to_remove) - - # FIXME: This won't work if the calling process is logging - # somewhere other than sys.stderr, but I'm not sure - # if this will be an issue in practice. Also, it would be - # nice if we trapped all of the messages for a given test - # and sent them back in finished_test() rather than logging - # them to stderr. - if not root_logger.handlers: - options = self._options - root_logger.setLevel(logging.DEBUG if options.verbose else logging.INFO) - self._meter = metered_stream.MeteredStream(sys.stderr, options.verbose, logger=root_logger) + + self._logger.setLevel(logging.DEBUG if self._options.verbose else logging.INFO) + self._log_handler = _WorkerLogHandler(self) + self._logger.addHandler(self._log_handler) def _set_up_host_and_port(self): options = self._options @@ -132,9 +125,9 @@ class Worker(manager_worker_broker.AbstractWorker): super(Worker, self).run() finally: self.kill_driver() - self._worker_connection.post_message('done') _log.debug("%s exiting" % self._name) self.cleanup() + self._worker_connection.post_message('done', self._log_messages) def handle_test_list(self, src, list_name, test_list): start_time = time.time() @@ -171,7 +164,9 @@ class Worker(manager_worker_broker.AbstractWorker): result = self.run_test_with_timeout(test_input, test_timeout_sec) elapsed_time = time.time() - start - self._worker_connection.post_message('finished_test', result, elapsed_time) + log_messages = self._log_messages + self._log_messages = [] + self._worker_connection.post_message('finished_test', result, elapsed_time, log_messages) self.clean_up_after_test(test_input, result) @@ -181,9 +176,10 @@ class Worker(manager_worker_broker.AbstractWorker): if self._tests_run_file: self._tests_run_file.close() self._tests_run_file = None - if self._meter: - self._meter.cleanup() - self._meter = None + if self._log_handler and self._logger: + self._logger.removeHandler(self._log_handler) + self._log_handler = None + self._logger = None def timeout(self, test_input): """Compute the appropriate timeout value for a test.""" @@ -300,3 +296,13 @@ class Worker(manager_worker_broker.AbstractWorker): def run_single_test(self, driver, test_input): return single_test_runner.run_single_test(self._port, self._options, test_input, driver, self._name) + + +class _WorkerLogHandler(logging.Handler): + def __init__(self, worker): + logging.Handler.__init__(self) + self._worker = worker + self._pid = os.getpid() + + def emit(self, record): + self._worker._log_messages.append(tuple([record.getMessage(), record.created, self._pid])) diff --git a/Tools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py index 0ff0d2f19..af36b2733 100644 --- a/Tools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py +++ b/Tools/Scripts/webkitpy/layout_tests/layout_package/json_layout_results_generator.py @@ -42,7 +42,6 @@ class JSONLayoutResultsGenerator(json_results_generator.JSONResultsGeneratorBase FAILURE_TO_CHAR = {test_expectations.PASS: json_results_generator.JSONResultsGeneratorBase.PASS_RESULT, test_expectations.SKIP: json_results_generator.JSONResultsGeneratorBase.SKIP_RESULT, - test_expectations.FAIL: "Y", test_expectations.CRASH: "C", test_expectations.TIMEOUT: "T", test_expectations.IMAGE: "I", diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py index c18452421..93ef517f4 100644 --- a/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py +++ b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py @@ -57,8 +57,6 @@ def result_was_expected(result, expected_results, test_needs_rebaselining, test_ test_is_skipped: whether test was marked as SKIP""" if result in expected_results: return True - if result in (IMAGE, TEXT, IMAGE_PLUS_TEXT) and FAIL in expected_results: - return True if result == MISSING and test_needs_rebaselining: return True if result == SKIP and test_is_skipped: @@ -85,6 +83,21 @@ def has_pixel_failures(actual_results): return IMAGE in actual_results or IMAGE_PLUS_TEXT in actual_results +# FIXME: Perhas these two routines should be part of the Port instead? +BASELINE_SUFFIX_LIST = ('png', 'wav', 'txt') + + +def suffixes_for_expectations(expectations): + suffixes = set() + if expectations.intersection(set([TEXT, IMAGE_PLUS_TEXT])): + suffixes.add('txt') + if expectations.intersection(set([IMAGE, IMAGE_PLUS_TEXT])): + suffixes.add('png') + if AUDIO in expectations: + suffixes.add('wav') + return set(suffixes) + + # FIXME: This method is no longer used here in this module. Remove remaining callsite in manager.py and this method. def strip_comments(line): """Strips comments from a line and return None if the line is empty @@ -210,8 +223,8 @@ class TestExpectationParser(object): self._full_test_list = full_test_list self._allow_rebaseline_modifier = allow_rebaseline_modifier - def parse(self, expectations_string): - expectations = TestExpectationParser._tokenize_list(expectations_string) + def parse(self, filename, expectations_string): + expectations = TestExpectationParser._tokenize_list(filename, expectations_string) for expectation_line in expectations: self._parse_line(expectation_line) return expectations @@ -221,6 +234,9 @@ class TestExpectationParser(object): expectation_line.original_string = test_name expectation_line.modifiers = [TestExpectationParser.DUMMY_BUG_MODIFIER, TestExpectationParser.SKIP_MODIFIER] expectation_line.name = test_name + # FIXME: we should pass in a more descriptive string here. + expectation_line.filename = '<Skipped file>' + expectation_line.line_number = 0 expectation_line.expectations = [TestExpectationParser.PASS_EXPECTATION] self._parse_line(expectation_line) return expectation_line @@ -292,7 +308,7 @@ class TestExpectationParser(object): if (not self._port.test_exists(expectation_line.name) and not self._port.test_exists(expectation_line.name + '-disabled')): # Log a warning here since you hit this case any - # time you update test_expectations.txt without syncing + # time you update TestExpectations without syncing # the LayoutTests directory expectation_line.warnings.append('Path does not exist.') return True @@ -322,8 +338,8 @@ class TestExpectationParser(object): expectation_line.matching_tests.append(expectation_line.path) @classmethod - def _tokenize(cls, expectation_string, line_number=None): - """Tokenizes a line from test_expectations.txt and returns an unparsed TestExpectationLine instance. + def _tokenize(cls, filename, expectation_string, line_number): + """Tokenizes a line from TestExpectations and returns an unparsed TestExpectationLine instance. The format of a test expectation line is: @@ -335,6 +351,7 @@ class TestExpectationParser(object): expectation_line = TestExpectationLine() expectation_line.original_string = expectation_string expectation_line.line_number = line_number + expectation_line.filename = filename comment_index = expectation_string.find("//") if comment_index == -1: comment_index = len(expectation_string) @@ -361,13 +378,13 @@ class TestExpectationParser(object): return expectation_line @classmethod - def _tokenize_list(cls, expectations_string): + def _tokenize_list(cls, filename, expectations_string): """Returns a list of TestExpectationLines, one for each line in expectations_string.""" expectation_lines = [] line_number = 0 for line in expectations_string.split("\n"): line_number += 1 - expectation_lines.append(cls._tokenize(line, line_number)) + expectation_lines.append(cls._tokenize(filename, line, line_number)) return expectation_lines @classmethod @@ -377,7 +394,7 @@ class TestExpectationParser(object): return [part.strip().lower() for part in space_separated_string.strip().split(' ')] -class TestExpectationLine: +class TestExpectationLine(object): """Represents a line in test expectations file.""" def __init__(self): @@ -417,7 +434,7 @@ class TestExpectationLine: class TestExpectationsModel(object): """Represents relational store of all expectations and provides CRUD semantics to manage it.""" - def __init__(self): + def __init__(self, shorten_filename=None): # Maps a test to its list of expectations. self._test_to_expectations = {} @@ -427,18 +444,13 @@ class TestExpectationsModel(object): # Maps a test to a TestExpectationLine instance. self._test_to_expectation_line = {} - # List of tests that are in the overrides file (used for checking for - # duplicates inside the overrides file itself). Note that just because - # a test is in this set doesn't mean it's necessarily overridding a - # expectation in the regular expectations; the test might not be - # mentioned in the regular expectations file at all. - self._overridding_tests = set() - self._modifier_to_tests = self._dict_of_sets(TestExpectations.MODIFIERS) self._expectation_to_tests = self._dict_of_sets(TestExpectations.EXPECTATIONS) self._timeline_to_tests = self._dict_of_sets(TestExpectations.TIMELINES) self._result_type_to_tests = self._dict_of_sets(TestExpectations.RESULT_TYPES) + self._shorten_filename = shorten_filename or (lambda x: x) + def _dict_of_sets(self, strings_to_constants): """Takes a dict of strings->constants and returns a dict mapping each constant to an empty set.""" @@ -503,31 +515,26 @@ class TestExpectationsModel(object): def get_expectations(self, test): return self._test_to_expectations[test] - def add_expectation_line(self, expectation_line, in_overrides=False, in_skipped=False): + def add_expectation_line(self, expectation_line, in_skipped=False): """Returns a list of warnings encountered while matching modifiers.""" if expectation_line.is_invalid(): return for test in expectation_line.matching_tests: - if not in_skipped and self._already_seen_better_match(test, expectation_line, in_overrides): + if not in_skipped and self._already_seen_better_match(test, expectation_line): continue self._clear_expectations_for_test(test, expectation_line) self._test_to_expectation_line[test] = expectation_line - self._add_test(test, expectation_line, in_overrides) + self._add_test(test, expectation_line) - def _add_test(self, test, expectation_line, in_overrides): + def _add_test(self, test, expectation_line): """Sets the expected state for a given test. This routine assumes the test has not been added before. If it has, use _clear_expectations_for_test() to reset the state prior to - calling this. - - Args: - test: test to add - expectation_line: expectation to add - in_overrides: whether we're parsing the regular expectations or the overridding expectations""" + calling this.""" self._test_to_expectations[test] = expectation_line.parsed_expectations for expectation in expectation_line.parsed_expectations: self._expectation_to_tests[expectation].add(test) @@ -549,11 +556,9 @@ class TestExpectationsModel(object): elif expectation_line.is_flaky(): self._result_type_to_tests[FLAKY].add(test) else: + # FIXME: What is this? self._result_type_to_tests[FAIL].add(test) - if in_overrides: - self._overridding_tests.add(test) - def _clear_expectations_for_test(self, test, expectation_line): """Remove prexisting expectations for this test. This happens if we are seeing a more precise path @@ -576,7 +581,7 @@ class TestExpectationsModel(object): if test in set_of_tests: set_of_tests.remove(test) - def _already_seen_better_match(self, test, expectation_line, in_overrides): + def _already_seen_better_match(self, test, expectation_line): """Returns whether we've seen a better match already in the file. Returns True if we've already seen a expectation_line.name that matches more of the test @@ -589,6 +594,10 @@ class TestExpectationsModel(object): prev_expectation_line = self._test_to_expectation_line[test] + if prev_expectation_line.filename != expectation_line.filename: + # We've moved on to a new expectation file, which overrides older ones. + return False + if len(prev_expectation_line.path) > len(expectation_line.path): # The previous path matched more of the test. return True @@ -597,20 +606,9 @@ class TestExpectationsModel(object): # This path matches more of the test. return False - if in_overrides and test not in self._overridding_tests: - # We have seen this path, but that's okay because it is - # in the overrides and the earlier path was in the - # expectations (not the overrides). - return False - # At this point we know we have seen a previous exact match on this # base path, so we need to check the two sets of modifiers. - if in_overrides: - expectation_source = "override" - else: - expectation_source = "expectation" - # FIXME: This code was originally designed to allow lines that matched # more modifiers to override lines that matched fewer modifiers. # However, we currently view these as errors. @@ -619,20 +617,28 @@ class TestExpectationsModel(object): # to be warnings and return False". if prev_expectation_line.matching_configurations == expectation_line.matching_configurations: - expectation_line.warnings.append('Duplicate or ambiguous %s.' % expectation_source) + expectation_line.warnings.append('Duplicate or ambiguous entry for %s on lines %s:%d and %s:%d.' % (expectation_line.name, + self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number, + self._shorten_filename(expectation_line.filename), expectation_line.line_number)) return True if prev_expectation_line.matching_configurations >= expectation_line.matching_configurations: - expectation_line.warnings.append('More specific entry on line %d overrides line %d' % (expectation_line.line_number, prev_expectation_line.line_number)) + expectation_line.warnings.append('More specific entry for %s on line %s:%d overrides line %s:%d.' % (expectation_line.name, + self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number, + self._shorten_filename(expectation_line.filename), expectation_line.line_number)) # FIXME: return False if we want more specific to win. return True if prev_expectation_line.matching_configurations <= expectation_line.matching_configurations: - expectation_line.warnings.append('More specific entry on line %d overrides line %d' % (prev_expectation_line.line_number, expectation_line.line_number)) + expectation_line.warnings.append('More specific entry for %s on line %s:%d overrides line %s:%d.' % (expectation_line.name, + self._shorten_filename(expectation_line.filename), expectation_line.line_number, + self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number)) return True if prev_expectation_line.matching_configurations & expectation_line.matching_configurations: - expectation_line.warnings.append('Entries on line %d and line %d match overlapping sets of configurations' % (prev_expectation_line.line_number, expectation_line.line_number)) + expectation_line.warnings.append('Entries for %s on lines %s:%d and %s:%d match overlapping sets of configurations.' % (expectation_line.name, + self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number, + self._shorten_filename(expectation_line.filename), expectation_line.line_number)) return True # Configuration sets are disjoint, then. @@ -645,9 +651,9 @@ class TestExpectations(object): in which case the expectations apply to all test cases in that directory and any subdirectory. The format is along the lines of: - LayoutTests/fast/js/fixme.js = FAIL - LayoutTests/fast/js/flaky.js = FAIL PASS - LayoutTests/fast/js/crash.js = CRASH TIMEOUT FAIL PASS + LayoutTests/fast/js/fixme.js = TEXT + LayoutTests/fast/js/flaky.js = TEXT PASS + LayoutTests/fast/js/crash.js = CRASH TIMEOUT TEXT PASS ... To add modifiers: @@ -663,20 +669,13 @@ class TestExpectations(object): Notes: -A test cannot be both SLOW and TIMEOUT - -A test should only be one of IMAGE, TEXT, IMAGE+TEXT, AUDIO, or FAIL. - FAIL is a legacy value that currently means either IMAGE, - TEXT, or IMAGE+TEXT. Once we have finished migrating the expectations, - we should change FAIL to have the meaning of IMAGE+TEXT and remove the - IMAGE+TEXT identifier. + -A test should only be one of IMAGE, TEXT, IMAGE+TEXT, or AUDIO. -A test can be included twice, but not via the same path. -If a test is included twice, then the more precise path wins. -CRASH tests cannot be WONTFIX """ - TEST_LIST = "test_expectations.txt" - EXPECTATIONS = {'pass': PASS, - 'fail': FAIL, 'text': TEXT, 'image': IMAGE, 'image+text': IMAGE_PLUS_TEXT, @@ -687,7 +686,6 @@ class TestExpectations(object): EXPECTATION_DESCRIPTIONS = {SKIP: ('skipped', 'skipped'), PASS: ('pass', 'passes'), - FAIL: ('failure', 'failures'), TEXT: ('text diff mismatch', 'text diff mismatch'), IMAGE: ('image mismatch', 'image mismatch'), @@ -699,7 +697,7 @@ class TestExpectations(object): MISSING: ('no expected result found', 'no expected results found')} - EXPECTATION_ORDER = (PASS, CRASH, TIMEOUT, MISSING, IMAGE_PLUS_TEXT, TEXT, IMAGE, AUDIO, FAIL, SKIP) + EXPECTATION_ORDER = (PASS, CRASH, TIMEOUT, MISSING, IMAGE_PLUS_TEXT, TEXT, IMAGE, AUDIO, SKIP) BUILD_TYPES = ('debug', 'release') @@ -722,23 +720,24 @@ class TestExpectations(object): assert(' ' not in string) # This only handles one expectation at a time. return cls.EXPECTATIONS.get(string.lower()) - def __init__(self, port, tests=None, is_lint_mode=False): + def __init__(self, port, tests=None, is_lint_mode=False, include_overrides=True): self._full_test_list = tests self._test_config = port.test_configuration() self._is_lint_mode = is_lint_mode - self._model = TestExpectationsModel() + self._model = TestExpectationsModel(self._shorten_filename) self._parser = TestExpectationParser(port, tests, is_lint_mode) self._port = port self._skipped_tests_warnings = [] - self._expectations = self._parser.parse(port.test_expectations()) - self._add_expectations(self._expectations, in_overrides=False) + expectations_dict = port.expectations_dict() + self._expectations = self._parser.parse(expectations_dict.keys()[0], expectations_dict.values()[0]) + self._add_expectations(self._expectations) - overrides = port.test_expectations_overrides() - if overrides: - overrides_expectations = self._parser.parse(overrides) - self._add_expectations(overrides_expectations, in_overrides=True) - self._expectations += overrides_expectations + if len(expectations_dict) > 1 and include_overrides: + for name in expectations_dict.keys()[1:]: + expectations = self._parser.parse(name, expectations_dict[name]) + self._add_expectations(expectations) + self._expectations += expectations # FIXME: move ignore_tests into port.skipped_layout_tests() self._add_skipped_tests(port.skipped_layout_tests(tests).union(set(port.get_option('ignore_tests', [])))) @@ -753,8 +752,7 @@ class TestExpectations(object): return self._model def get_rebaselining_failures(self): - return (self._model.get_test_set(REBASELINE, FAIL) | - self._model.get_test_set(REBASELINE, IMAGE) | + return (self._model.get_test_set(REBASELINE, IMAGE) | self._model.get_test_set(REBASELINE, TEXT) | self._model.get_test_set(REBASELINE, IMAGE_PLUS_TEXT) | self._model.get_test_set(REBASELINE, AUDIO)) @@ -813,17 +811,16 @@ class TestExpectations(object): def is_rebaselining(self, test): return self._model.has_modifier(test, REBASELINE) + def _shorten_filename(self, filename): + if filename.startswith(self._port.path_from_webkit_base()): + return self._port.host.filesystem.relpath(filename, self._port.path_from_webkit_base()) + return filename + def _report_warnings(self): warnings = [] - test_expectation_path = self._port.path_to_test_expectations_file() - if test_expectation_path.startswith(self._port.path_from_webkit_base()): - test_expectation_path = self._port.host.filesystem.relpath(test_expectation_path, self._port.path_from_webkit_base()) for expectation in self._expectations: for warning in expectation.warnings: - warnings.append('%s:%d %s %s' % (test_expectation_path, expectation.line_number, warning, expectation.name if expectation.expectations else expectation.original_string)) - - for warning in self._skipped_tests_warnings: - warnings.append('%s%s' % (test_expectation_path, warning)) + warnings.append('%s:%d %s %s' % (self._shorten_filename(expectation.filename), expectation.line_number, warning, expectation.name if expectation.expectations else expectation.original_string)) if warnings: self._has_warnings = True @@ -846,7 +843,7 @@ class TestExpectations(object): for expectation in self._expectations: if expectation.name != test or expectation.is_flaky() or not expectation.parsed_expectations: continue - if iter(expectation.parsed_expectations).next() not in (FAIL, TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO): + if iter(expectation.parsed_expectations).next() not in (TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO): continue if test_configuration not in expectation.matching_configurations: continue @@ -869,20 +866,20 @@ class TestExpectations(object): return TestExpectationSerializer.list_to_string(filter(without_rebaseline_modifier, self._expectations)) - def _add_expectations(self, expectation_list, in_overrides): + def _add_expectations(self, expectation_list): for expectation_line in expectation_list: if not expectation_line.expectations: continue if self._is_lint_mode or self._test_config in expectation_line.matching_configurations: - self._model.add_expectation_line(expectation_line, in_overrides) + self._model.add_expectation_line(expectation_line) def _add_skipped_tests(self, tests_to_skip): if not tests_to_skip: return - for index, test in enumerate(self._expectations, start=1): + for test in self._expectations: if test.name and test.name in tests_to_skip: - self._skipped_tests_warnings.append(':%d %s is also in a Skipped file.' % (index, test.name)) + test.warnings.append('%s:%d %s is also in a Skipped file.' % (test.filename, test.line_number, test.name)) for test_name in tests_to_skip: expectation_line = self._parser.expectation_for_skipped_test(test_name) diff --git a/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py index 0453a0818..7b589b501 100644 --- a/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py @@ -35,6 +35,12 @@ from webkitpy.layout_tests.models.test_configuration import * from webkitpy.layout_tests.models.test_expectations import * from webkitpy.layout_tests.models.test_configuration import * +try: + from collections import OrderedDict +except ImportError: + # Needed for Python < 2.7 + from webkitpy.thirdparty.ordered_dict import OrderedDict + class MockBugManager(object): def close_bug(self, bug_id, reference_bug_id=None): @@ -52,16 +58,6 @@ class FunctionsTest(unittest.TestCase): self.assertEquals(result_was_expected(TEXT, set([PASS]), False, False), False) - # test handling of FAIL expectations - self.assertEquals(result_was_expected(IMAGE_PLUS_TEXT, set([FAIL]), - False, False), True) - self.assertEquals(result_was_expected(IMAGE, set([FAIL]), - False, False), True) - self.assertEquals(result_was_expected(TEXT, set([FAIL]), - False, False), True) - self.assertEquals(result_was_expected(CRASH, set([FAIL]), - False, False), False) - # test handling of SKIPped tests and results self.assertEquals(result_was_expected(SKIP, set([CRASH]), False, True), True) @@ -86,6 +82,14 @@ class FunctionsTest(unittest.TestCase): self.assertEquals(remove_pixel_failures(set([PASS, IMAGE, CRASH])), set([PASS, CRASH])) + def test_suffixes_for_expectations(self): + self.assertEquals(suffixes_for_expectations(set([TEXT])), set(['txt'])) + self.assertEquals(suffixes_for_expectations(set([IMAGE_PLUS_TEXT])), set(['txt', 'png'])) + self.assertEquals(suffixes_for_expectations(set([IMAGE])), set(['png'])) + self.assertEquals(suffixes_for_expectations(set([AUDIO])), set(['wav'])) + self.assertEquals(suffixes_for_expectations(set([TEXT, IMAGE, CRASH])), set(['txt', 'png'])) + self.assertEquals(suffixes_for_expectations(set()), set()) + class Base(unittest.TestCase): # Note that all of these tests are written assuming the configuration @@ -119,8 +123,11 @@ BUG_TEST WONTFIX MAC : failures/expected/image.html = IMAGE """ def parse_exp(self, expectations, overrides=None, is_lint_mode=False): - self._port.test_expectations = lambda: expectations - self._port.test_expectations_overrides = lambda: overrides + self._expectations_dict = OrderedDict() + self._expectations_dict['expectations'] = expectations + if overrides: + self._expectations_dict['overrides'] = overrides + self._port.expectations_dict = lambda: self._expectations_dict self._exp = TestExpectations(self._port, self.get_basic_tests(), is_lint_mode) def assert_exp(self, test, result): @@ -197,17 +204,17 @@ BUGX WONTFIX : failures/expected = IMAGE SKIP : failures/expected/image.html""", is_lint_mode=True) self.assertFalse(True, "ParseError wasn't raised") except ParseError, e: - warnings = [u":1 Test lacks BUG modifier. failures/expected/text.html", - u":1 Unrecognized modifier 'foo' failures/expected/text.html", - u":2 Missing expectations SKIP : failures/expected/image.html"] - self.assertEqual(str(e), '\n'.join(self._port.path_to_test_expectations_file() + str(warning) for warning in warnings)) + warnings = ("expectations:1 Test lacks BUG modifier. failures/expected/text.html\n" + "expectations:1 Unrecognized modifier 'foo' failures/expected/text.html\n" + "expectations:2 Missing expectations SKIP : failures/expected/image.html") + self.assertEqual(str(e), warnings) try: self.parse_exp('SKIP : failures/expected/text.html = TEXT', is_lint_mode=True) self.assertFalse(True, "ParseError wasn't raised") except ParseError, e: - warnings = [u':1 Test lacks BUG modifier. failures/expected/text.html'] - self.assertEqual(str(e), '\n'.join(self._port.path_to_test_expectations_file() + str(warning) for warning in warnings)) + warnings = u'expectations:1 Test lacks BUG modifier. failures/expected/text.html' + self.assertEqual(str(e), warnings) def test_error_on_different_platform(self): # parse_exp uses a Windows port. Assert errors on Mac show up in lint mode. @@ -226,6 +233,12 @@ SKIP : failures/expected/image.html""", is_lint_mode=True) "BUG_OVERRIDE : failures/expected/text.html = IMAGE") self.assert_exp('failures/expected/text.html', IMAGE) + def test_overrides__directory(self): + self.parse_exp("BUG_EXP: failures/expected/text.html = TEXT", + "BUG_OVERRIDE: failures/expected = CRASH") + self.assert_exp('failures/expected/text.html', CRASH) + self.assert_exp('failures/expected/image.html', CRASH) + def test_overrides__duplicate(self): self.assert_bad_expectations("BUG_EXP: failures/expected/text.html = TEXT", "BUG_OVERRIDE : failures/expected/text.html = IMAGE\n" @@ -261,8 +274,11 @@ class SkippedTests(Base): def check(self, expectations, overrides, skips, lint=False): port = MockHost().port_factory.get('qt') port._filesystem.write_text_file(port._filesystem.join(port.layout_tests_dir(), 'failures/expected/text.html'), 'foo') - port.test_expectations = lambda: expectations - port.test_expectations_overrides = lambda: overrides + self._expectations_dict = OrderedDict() + self._expectations_dict['expectations'] = expectations + if overrides: + self._expectations_dict['overrides'] = overrides + port.expectations_dict = lambda: self._expectations_dict port.skipped_layout_tests = lambda tests: set(skips) exp = TestExpectations(port, ['failures/expected/text.html'], lint) @@ -407,9 +423,9 @@ class RemoveConfigurationsTest(Base): test_port.test_isfile = lambda test: True test_config = test_port.test_configuration() - test_port.test_expectations = lambda: """BUGX LINUX WIN RELEASE : failures/expected/foo.html = TEXT + test_port.expectations_dict = lambda: {"expectations": """BUGX LINUX WIN RELEASE : failures/expected/foo.html = TEXT BUGY WIN MAC DEBUG : failures/expected/foo.html = CRASH -""" +"""} expectations = TestExpectations(test_port, self.get_basic_tests()) actual_expectations = expectations.remove_configuration_from_test('failures/expected/foo.html', test_config) @@ -425,9 +441,9 @@ BUGY WIN MAC DEBUG : failures/expected/foo.html = CRASH test_port.test_isfile = lambda test: True test_config = test_port.test_configuration() - test_port.test_expectations = lambda: """BUGX WIN RELEASE : failures/expected/foo.html = TEXT + test_port.expectations_dict = lambda: {'expectations': """BUGX WIN RELEASE : failures/expected/foo.html = TEXT BUGY WIN DEBUG : failures/expected/foo.html = CRASH -""" +"""} expectations = TestExpectations(test_port) actual_expectations = expectations.remove_configuration_from_test('failures/expected/foo.html', test_config) @@ -459,51 +475,54 @@ class RebaseliningTest(Base): class TestExpectationParserTests(unittest.TestCase): + def _tokenize(self, line): + return TestExpectationParser._tokenize('path', line, 0) + def test_tokenize_blank(self): - expectation = TestExpectationParser._tokenize('') + expectation = self._tokenize('') self.assertEqual(expectation.comment, None) self.assertEqual(len(expectation.warnings), 0) def test_tokenize_missing_colon(self): - expectation = TestExpectationParser._tokenize('Qux.') + expectation = self._tokenize('Qux.') self.assertEqual(str(expectation.warnings), '["Missing a \':\'"]') def test_tokenize_extra_colon(self): - expectation = TestExpectationParser._tokenize('FOO : : bar') + expectation = self._tokenize('FOO : : bar') self.assertEqual(str(expectation.warnings), '["Extraneous \':\'"]') def test_tokenize_empty_comment(self): - expectation = TestExpectationParser._tokenize('//') + expectation = self._tokenize('//') self.assertEqual(expectation.comment, '') self.assertEqual(len(expectation.warnings), 0) def test_tokenize_comment(self): - expectation = TestExpectationParser._tokenize('//Qux.') + expectation = self._tokenize('//Qux.') self.assertEqual(expectation.comment, 'Qux.') self.assertEqual(len(expectation.warnings), 0) def test_tokenize_missing_equal(self): - expectation = TestExpectationParser._tokenize('FOO : bar') + expectation = self._tokenize('FOO : bar') self.assertEqual(str(expectation.warnings), "['Missing expectations\']") def test_tokenize_extra_equal(self): - expectation = TestExpectationParser._tokenize('FOO : bar = BAZ = Qux.') + expectation = self._tokenize('FOO : bar = BAZ = Qux.') self.assertEqual(str(expectation.warnings), '["Extraneous \'=\'"]') def test_tokenize_valid(self): - expectation = TestExpectationParser._tokenize('FOO : bar = BAZ') + expectation = self._tokenize('FOO : bar = BAZ') self.assertEqual(expectation.comment, None) self.assertEqual(len(expectation.warnings), 0) def test_tokenize_valid_with_comment(self): - expectation = TestExpectationParser._tokenize('FOO : bar = BAZ //Qux.') + expectation = self._tokenize('FOO : bar = BAZ //Qux.') self.assertEqual(expectation.comment, 'Qux.') self.assertEqual(str(expectation.modifiers), '[\'foo\']') self.assertEqual(str(expectation.expectations), '[\'baz\']') self.assertEqual(len(expectation.warnings), 0) def test_tokenize_valid_with_multiple_modifiers(self): - expectation = TestExpectationParser._tokenize('FOO1 FOO2 : bar = BAZ //Qux.') + expectation = self._tokenize('FOO1 FOO2 : bar = BAZ //Qux.') self.assertEqual(expectation.comment, 'Qux.') self.assertEqual(str(expectation.modifiers), '[\'foo1\', \'foo2\']') self.assertEqual(str(expectation.expectations), '[\'baz\']') @@ -515,7 +534,7 @@ class TestExpectationParserTests(unittest.TestCase): test_port.test_exists = lambda test: True test_config = test_port.test_configuration() full_test_list = [] - expectation_line = TestExpectationParser._tokenize('') + expectation_line = self._tokenize('') parser = TestExpectationParser(test_port, full_test_list, allow_rebaseline_modifier=False) parser._parse_line(expectation_line) self.assertFalse(expectation_line.is_invalid()) @@ -529,14 +548,17 @@ class TestExpectationSerializerTests(unittest.TestCase): self._serializer = TestExpectationSerializer(self._converter) unittest.TestCase.__init__(self, testFunc) + def _tokenize(self, line): + return TestExpectationParser._tokenize('path', line, 0) + def assert_round_trip(self, in_string, expected_string=None): - expectation = TestExpectationParser._tokenize(in_string) + expectation = self._tokenize(in_string) if expected_string is None: expected_string = in_string self.assertEqual(expected_string, self._serializer.to_string(expectation)) def assert_list_round_trip(self, in_string, expected_string=None): - expectations = TestExpectationParser._tokenize_list(in_string) + expectations = TestExpectationParser._tokenize_list('path', in_string) if expected_string is None: expected_string = in_string self.assertEqual(expected_string, TestExpectationSerializer.list_to_string(expectations, self._converter)) @@ -588,10 +610,10 @@ class TestExpectationSerializerTests(unittest.TestCase): self.assertEqual(self._serializer._parsed_expectations_string(expectation_line), '') expectation_line.parsed_expectations = set([IMAGE_PLUS_TEXT]) self.assertEqual(self._serializer._parsed_expectations_string(expectation_line), 'image+text') - expectation_line.parsed_expectations = set([PASS, FAIL]) - self.assertEqual(self._serializer._parsed_expectations_string(expectation_line), 'pass fail') - expectation_line.parsed_expectations = set([FAIL, PASS]) - self.assertEqual(self._serializer._parsed_expectations_string(expectation_line), 'pass fail') + expectation_line.parsed_expectations = set([PASS, IMAGE]) + self.assertEqual(self._serializer._parsed_expectations_string(expectation_line), 'pass image') + expectation_line.parsed_expectations = set([TEXT, PASS]) + self.assertEqual(self._serializer._parsed_expectations_string(expectation_line), 'pass text') def test_parsed_modifier_string(self): expectation_line = TestExpectationLine() diff --git a/Tools/Scripts/webkitpy/layout_tests/port/apple.py b/Tools/Scripts/webkitpy/layout_tests/port/apple.py index b6e3b1d2e..8807d40d7 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/apple.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/apple.py @@ -90,6 +90,6 @@ class ApplePort(WebKitPort): version = self.FUTURE_VERSION for build_type in self.ALL_BUILD_TYPES: - # But at some later point we may need to make these configurable by the MacPort and WinPort subclasses. - configurations.append(TestConfiguration(version=version, architecture='x86', build_type=build_type)) + for architecture in self.ARCHITECTURES: + configurations.append(TestConfiguration(version=version, architecture=architecture, build_type=build_type)) return configurations diff --git a/Tools/Scripts/webkitpy/layout_tests/port/base.py b/Tools/Scripts/webkitpy/layout_tests/port/base.py index c0e13dfcb..9e73b2c55 100755 --- a/Tools/Scripts/webkitpy/layout_tests/port/base.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/base.py @@ -34,8 +34,16 @@ import cgi import difflib import errno import os +import optparse import re +try: + from collections import OrderedDict +except ImportError: + # Needed for Python < 2.7 + from webkitpy.thirdparty.ordered_dict import OrderedDict + + from webkitpy.common import find_files from webkitpy.common import read_checksum_from_png from webkitpy.common.memoized import memoized @@ -54,19 +62,6 @@ from webkitpy.layout_tests.servers import websocket_server _log = logutils.get_logger(__file__) -class DummyOptions(object): - """Fake implementation of optparse.Values. Cloned from webkitpy.tool.mocktool.MockOptions.""" - - def __init__(self, *args, **kwargs): - # The caller can set option values using keyword arguments. We don't - # set any values by default because we don't know how this - # object will be used. Generally speaking unit tests should - # subclass this or provider wrapper functions that set a common - # set of options. - for key, value in kwargs.items(): - self.__dict__[key] = value - - # FIXME: This class should merge with WebKitPort now that Chromium behaves mostly like other webkit ports. class Port(object): """Abstract class for Port-specific hooks for the layout_test package.""" @@ -104,7 +99,7 @@ class Port(object): # FIXME: Ideally we'd have a package-wide way to get a # well-formed options object that had all of the necessary # options defined on it. - self._options = options or DummyOptions() + self._options = options or optparse.Values() self.host = host self._executive = host.executive @@ -159,17 +154,7 @@ class Port(object): def default_child_processes(self): """Return the number of DumpRenderTree instances to use for this port.""" - cpu_count = self._executive.cpu_count() - # Make sure we have enough ram to support that many instances: - free_memory = self.host.platform.free_bytes_memory() - if free_memory: - bytes_per_drt = 200 * 1024 * 1024 # Assume each DRT needs 200MB to run. - supportable_instances = max(free_memory / bytes_per_drt, 1) # Always use one process, even if we don't have space for it. - if supportable_instances < cpu_count: - # FIXME: The Printer isn't initialized when this is called, so using _log would just show an unitialized logger error. - print "This machine could support %s child processes, but only has enough memory for %s." % (cpu_count, supportable_instances) - return min(supportable_instances, cpu_count) - return cpu_count + return self._executive.cpu_count() def worker_startup_delay_secs(self): # FIXME: If we start workers up too quickly, DumpRenderTree appears @@ -180,6 +165,15 @@ class Port(object): def baseline_path(self): """Return the absolute path to the directory to store new baselines in for this port.""" + # FIXME: remove once all callers are calling either baseline_version_dir() or baseline_platform_dir() + return self.baseline_version_dir() + + def baseline_platform_dir(self): + """Return the absolute path to the default (version-independent) platform-specific results.""" + return self._filesystem.join(self.layout_tests_dir(), 'platform', self.port_name) + + def baseline_version_dir(self): + """Return the absolute path to the platform-and-version-specific results.""" baseline_search_paths = self.get_option('additional_platform_directory', []) + self.baseline_search_path() return baseline_search_paths[0] @@ -489,8 +483,22 @@ class Port(object): return reftest_list.get(self._filesystem.join(self.layout_tests_dir(), test_name), []) def tests(self, paths): - """Return the list of tests found.""" - return self._real_tests(paths).union(self._virtual_tests(paths, self.populated_virtual_test_suites())) + """Return the list of tests found. Both generic and platform-specific tests matching paths should be returned.""" + expanded_paths = self._expanded_paths(paths) + return self._real_tests(expanded_paths).union(self._virtual_tests(expanded_paths, self.populated_virtual_test_suites())) + + def _expanded_paths(self, paths): + expanded_paths = [] + fs = self._filesystem + all_platform_dirs = [path for path in fs.glob(fs.join(self.layout_tests_dir(), 'platform', '*')) if fs.isdir(path)] + for path in paths: + expanded_paths.append(path) + if self.test_isdir(path) and not path.startswith('platform'): + for platform_dir in all_platform_dirs: + if fs.isdir(fs.join(platform_dir, path)): + expanded_paths.append(self.relative_test_filename(fs.join(platform_dir, path))) + + return expanded_paths def _real_tests(self, paths): # When collecting test cases, skip these directories @@ -657,30 +665,31 @@ class Port(object): return self._architecture def get_option(self, name, default_value=None): - # FIXME: Eventually we should not have to do a test for - # hasattr(), and we should be able to just do - # self.options.value. See additional FIXME in the constructor. - if hasattr(self._options, name): - return getattr(self._options, name) - return default_value + return getattr(self._options, name, default_value) def set_option_default(self, name, default_value): - # FIXME: Callers could also use optparse_parser.Values.ensure_value, - # since this should always be a optparse_parser.Values object. - if not hasattr(self._options, name) or getattr(self._options, name) is None: - return setattr(self._options, name, default_value) + return self._options.ensure_value(name, default_value) def path_from_webkit_base(self, *comps): """Returns the full path to path made by joining the top of the WebKit source tree and the list of path components in |*comps|.""" return self._config.path_from_webkit_base(*comps) + @memoized def path_to_test_expectations_file(self): """Update the test expectations to the passed-in string. This is used by the rebaselining tool. Raises NotImplementedError if the port does not use expectations files.""" - raise NotImplementedError('Port.path_to_test_expectations_file') + + # FIXME: We need to remove this when we make rebaselining work with multiple files and just generalize expectations_files(). + + # test_expectations are always in mac/ not mac-leopard/ by convention, hence we use port_name instead of name(). + port_name = self.port_name + if port_name.startswith('chromium') or port_name.startswith('google-chrome'): + port_name = 'chromium' + + return self._filesystem.join(self._webkit_baseline_path(port_name), 'TestExpectations') def relative_test_filename(self, filename): """Returns a test_name a realtive unix-style path for a filename under the LayoutTests @@ -765,7 +774,7 @@ class Port(object): def show_results_html_file(self, results_filename): """This routine should display the HTML file pointed at by results_filename in a users' browser.""" - return self.host.user.open_url(path.abspath_to_uri(results_filename)) + return self.host.user.open_url(path.abspath_to_uri(self.host.platform, results_filename)) def create_driver(self, worker_number, no_timeout=False): """Return a newly created Driver subclass for starting/stopping the test driver.""" @@ -876,28 +885,32 @@ class Port(object): # some ports have Skipped files which are returned as part of test_expectations(). return self._filesystem.exists(self.path_to_test_expectations_file()) - def test_expectations(self): - """Returns the test expectations for this port. - - Basically this string should contain the equivalent of a - test_expectations file. See test_expectations.py for more details.""" - return self._filesystem.read_text_file(self.path_to_test_expectations_file()) + def expectations_dict(self): + """Returns an OrderedDict of name -> expectations strings. + The names are expected to be (but not required to be) paths in the filesystem. + If the name is a path, the file can be considered updatable for things like rebaselining, + so don't use names that are paths if they're not paths. + Generally speaking the ordering should be files in the filesystem in cascade order + (TestExpectations followed by Skipped, if the port honors both formats), + then any built-in expectations (e.g., from compile-time exclusions), then --additional-expectations options.""" + # FIXME: rename this to test_expectations() once all the callers are updated to know about the ordered dict. + expectations = OrderedDict() - def test_expectations_overrides(self): - """Returns an optional set of overrides for the test_expectations. + for path in self.expectations_files(): + expectations[path] = self._filesystem.read_text_file(path) - This is used by ports that have code in two repositories, and where - it is possible that you might need "downstream" expectations that - temporarily override the "upstream" expectations until the port can - sync up the two repos.""" - overrides = '' for path in self.get_option('additional_expectations', []): - if self._filesystem.exists(self._filesystem.expanduser(path)): + expanded_path = self._filesystem.expanduser(path) + if self._filesystem.exists(expanded_path): _log.debug("reading additional_expectations from path '%s'" % path) - overrides += self._filesystem.read_text_file(self._filesystem.expanduser(path)) + expectations[path] = self._filesystem.read_text_file(expanded_path) else: _log.warning("additional_expectations path '%s' does not exist" % path) - return overrides or None + return expectations + + def expectations_files(self): + # FIXME: see comment in path_to_expectations_file(). + return [self.path_to_test_expectations_file()] def repository_paths(self): """Returns a list of (repository_name, repository_path) tuples of its depending code base. diff --git a/Tools/Scripts/webkitpy/layout_tests/port/base_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/base_unittest.py index 92edfe232..f7afb7b22 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/base_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/base_unittest.py @@ -60,21 +60,7 @@ class PortTest(unittest.TestCase): def test_default_child_processes(self): port = self.make_port() - # Even though the MockPlatformInfo shows 1GB free memory (enough for 5 DRT instances) - # we're still limited by the 2 mock cores we have: - self.assertEqual(port.default_child_processes(), 2) - bytes_for_drt = 200 * 1024 * 1024 - - port.host.platform.free_bytes_memory = lambda: bytes_for_drt - expected_stdout = "This machine could support 2 child processes, but only has enough memory for 1.\n" - child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stdout=expected_stdout) - self.assertEqual(child_processes, 1) - - # Make sure that we always use one process, even if we don't have the memory for it. - port.host.platform.free_bytes_memory = lambda: bytes_for_drt - 1 - expected_stdout = "This machine could support 2 child processes, but only has enough memory for 1.\n" - child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stdout=expected_stdout) - self.assertEqual(child_processes, 1) + self.assertNotEquals(port.default_child_processes(), None) def test_format_wdiff_output_as_html(self): output = "OUTPUT %s %s %s" % (Port._WDIFF_DEL, Port._WDIFF_ADD, Port._WDIFF_END) @@ -280,31 +266,33 @@ class PortTest(unittest.TestCase): def test_additional_expectations(self): port = self.make_port(port_name='foo') - + port.port_name = 'foo' + port._filesystem.write_text_file('/mock-checkout/LayoutTests/platform/foo/TestExpectations', '') port._filesystem.write_text_file( '/tmp/additional-expectations-1.txt', 'content1\n') port._filesystem.write_text_file( '/tmp/additional-expectations-2.txt', 'content2\n') - self.assertEquals(None, port.test_expectations_overrides()) + self.assertEquals('\n'.join(port.expectations_dict().values()), '') port._options.additional_expectations = [ '/tmp/additional-expectations-1.txt'] - self.assertEquals('content1\n', port.test_expectations_overrides()) + self.assertEquals('\n'.join(port.expectations_dict().values()), '\ncontent1\n') port._options.additional_expectations = [ '/tmp/nonexistent-file', '/tmp/additional-expectations-1.txt'] - self.assertEquals('content1\n', port.test_expectations_overrides()) + self.assertEquals('\n'.join(port.expectations_dict().values()), '\ncontent1\n') port._options.additional_expectations = [ '/tmp/additional-expectations-1.txt', '/tmp/additional-expectations-2.txt'] - self.assertEquals('content1\ncontent2\n', port.test_expectations_overrides()) + self.assertEquals('\n'.join(port.expectations_dict().values()), '\ncontent1\n\ncontent2\n') def test_uses_test_expectations_file(self): port = self.make_port(port_name='foo') - port.path_to_test_expectations_file = lambda: '/mock-results/test_expectations.txt' + port.port_name = 'foo' + port.path_to_test_expectations_file = lambda: '/mock-results/TestExpectations' self.assertFalse(port.uses_test_expectations_file()) - port._filesystem = MockFileSystem({'/mock-results/test_expectations.txt': ''}) + port._filesystem = MockFileSystem({'/mock-results/TestExpectations': ''}) self.assertTrue(port.uses_test_expectations_file()) def test_find_no_paths_specified(self): @@ -406,9 +394,7 @@ class PortTest(unittest.TestCase): self.assertVirtual(port.check_image_diff) self.assertVirtual(port.create_driver, 0) self.assertVirtual(port.diff_image, None, None) - self.assertVirtual(port.path_to_test_expectations_file) self.assertVirtual(port.default_results_directory) - self.assertVirtual(port.test_expectations) self.assertVirtual(port._path_to_apache) self.assertVirtual(port._path_to_apache_config_file) self.assertVirtual(port._path_to_driver) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/builders.py b/Tools/Scripts/webkitpy/layout_tests/port/builders.py index 1249bb543..cc0fa6932 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/builders.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/builders.py @@ -53,13 +53,16 @@ _exact_matches = { "Webkit Mac10.7": {"port_name": "chromium-mac-lion", "specifiers": set(["lion"])}, # These builders are on build.webkit.org. + "Apple Lion Release WK1 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion"])}, + "Apple Lion Debug WK1 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion", "debug"])}, + "Apple Lion Release WK2 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion", "wk2"])}, + "Apple Lion Debug WK2 (Tests)": {"port_name": "mac-lion", "specifiers": set(["lion", "wk2", "debug"])}, + + "Apple Win XP Debug (Tests)": {"port_name": "win-xp", "specifiers": set(["win", "debug"])}, + "Apple Win 7 Release (Tests)": {"port_name": "win-xp", "specifiers": set(["win"])}, + "GTK Linux 32-bit Debug": {"port_name": "gtk", "specifiers": set(["gtk"])}, - "Leopard Intel Debug (Tests)": {"port_name": "mac-leopard", "specifiers": set(["leopard"])}, - "SnowLeopard Intel Release (Tests)": {"port_name": "mac-snowleopard", "specifiers": set(["snowleopard"])}, - "SnowLeopard Intel Release (WebKit2 Tests)": {"port_name": "mac-snowleopard-wk2", "specifiers": set(["wk2"])}, "Qt Linux Release": {"port_name": "qt-linux", "specifiers": set(["win", "linux", "mac"])}, - "Windows XP Debug (Tests)": {"port_name": "win-xp", "specifiers": set(["win"])}, - "Windows 7 Release (WebKit2 Tests)": {"port_name": "win-future-wk2", "specifiers": set(["wk2"])}, "EFL Linux Release": {"port_name": "efl", "specifiers": set(["efl"])}, } @@ -67,7 +70,7 @@ _exact_matches = { _fuzzy_matches = { # These builders are on build.webkit.org. r"SnowLeopard": "mac-snowleopard", - r"Lion": "mac-lion", + r"Apple Lion": "mac-lion", r"Leopard": "mac-leopard", r"Windows": "win", r"GTK": "gtk", diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium.py index 9db4cb570..2c1911072 100755 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium.py @@ -37,20 +37,12 @@ import signal import subprocess import sys import time -import webbrowser -from webkitpy.common.config import urls from webkitpy.common.system import executive -from webkitpy.common.system.path import cygpath -from webkitpy.layout_tests.controllers.manager import Manager -from webkitpy.layout_tests.models import test_expectations from webkitpy.layout_tests.models.test_configuration import TestConfiguration from webkitpy.layout_tests.port.base import Port, VirtualTestSuite from webkitpy.layout_tests.port.driver import DriverOutput from webkitpy.layout_tests.port.webkit import WebKitDriver -from webkitpy.layout_tests.port import builders -from webkitpy.layout_tests.servers import http_server -from webkitpy.layout_tests.servers import websocket_server _log = logging.getLogger(__name__) @@ -85,6 +77,26 @@ class ChromiumPort(Port): 'android': ['icecreamsandwich'], } + DEFAULT_BUILD_DIRECTORIES = ('out',) + + @classmethod + def _static_build_path(cls, filesystem, build_directory, chromium_base, webkit_base, *comps): + if build_directory: + return filesystem.join(build_directory, *comps) + + for directory in cls.DEFAULT_BUILD_DIRECTORIES: + base_dir = filesystem.join(chromium_base, directory) + if filesystem.exists(base_dir): + return filesystem.join(base_dir, *comps) + + for directory in cls.DEFAULT_BUILD_DIRECTORIES: + base_dir = filesystem.join(webkit_base, directory) + if filesystem.exists(base_dir): + return filesystem.join(base_dir, *comps) + + # We have to default to something, so pick the last one. + return filesystem.join(base_dir, *comps) + @classmethod def _chromium_base_dir(cls, filesystem): module_path = filesystem.path_to_module(cls.__module__) @@ -232,9 +244,6 @@ class ChromiumPort(Port): self._chromium_base_dir_path = self._chromium_base_dir(self._filesystem) return self._filesystem.join(self._chromium_base_dir_path, *comps) - def path_to_test_expectations_file(self): - return self.path_from_webkit_base('LayoutTests', 'platform', 'chromium', 'test_expectations.txt') - def setup_environ_for_server(self, server_name=None): clean_env = super(ChromiumPort, self).setup_environ_for_server(server_name) # Webkit Linux (valgrind layout) bot needs these envvars. @@ -293,14 +302,6 @@ class ChromiumPort(Port): def all_baseline_variants(self): return self.ALL_BASELINE_VARIANTS - def test_expectations(self): - """Returns the test expectations for this port. - - Basically this string should contain the equivalent of a - test_expectations file. See test_expectations.py for more details.""" - expectations_path = self.path_to_test_expectations_file() - return self._filesystem.read_text_file(expectations_path) - def _generate_all_test_configurations(self): """Returns a sequence of the TestConfigurations the port supports.""" # By default, we assume we want to test every graphics type in @@ -320,37 +321,12 @@ class ChromiumPort(Port): 'win_layout_rel', ]) - def _expectations_file_contents(self, filetype, filepath): - if self._filesystem.exists(filepath): - _log.debug( - "reading %s test_expectations overrides from file '%s'" % - (filetype, filepath)) - return (self._filesystem.read_text_file(filepath) or '') - else: - # FIXME: This allows this to work with mock checkouts; - # This probably shouldn't be used with a mock checkout, though. - if not 'mock-checkout' in filepath: - _log.warning( - "%s test_expectations overrides file '%s' does not exist" % - (filetype, filepath)) - return '' - - def test_expectations_overrides(self): - combined_overrides = '' - combined_overrides += self._expectations_file_contents( - 'skia', self.path_from_chromium_base( - 'skia', 'skia_test_expectations.txt')) - # FIXME: It seems bad that run_webkit_tests.py uses a hardcoded dummy - # builder string instead of just using None. + def expectations_files(self): + paths = [self.path_to_test_expectations_file(), self.path_from_chromium_base('skia', 'skia_test_expectations.txt')] builder_name = self.get_option('builder_name', 'DUMMY_BUILDER_NAME') if builder_name == 'DUMMY_BUILDER_NAME' or '(deps)' in builder_name or builder_name in self.try_builder_names: - combined_overrides += self._expectations_file_contents( - 'chromium', self.path_from_chromium_base( - 'webkit', 'tools', 'layout_tests', 'test_expectations.txt')) - - base_overrides = super(ChromiumPort, self).test_expectations_overrides() - combined_overrides += (base_overrides or '') - return combined_overrides + paths.append(self.path_from_chromium_base('webkit', 'tools', 'layout_tests', 'test_expectations.txt')) + return paths def repository_paths(self): repos = super(ChromiumPort, self).repository_paths() @@ -387,6 +363,9 @@ class ChromiumPort(Port): # or any subclasses. # + def _build_path(self, *comps): + return self._static_build_path(self._filesystem, self.get_option('build_directory'), self.path_from_chromium_base(), self.path_from_webkit_base(), *comps) + def _check_driver_build_up_to_date(self, configuration): if configuration in ('Debug', 'Release'): try: @@ -667,6 +646,9 @@ class ChromiumDriver(WebKitDriver): test_time=run_time, timeout=timeout, error=error) def start(self, pixel_tests, per_test_args): + if not self._test_shell: + return super(ChromiumDriver, self).start(pixel_tests, per_test_args) + if not self._proc: self._start(pixel_tests, per_test_args) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_android.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_android.py index c701fbfb3..6568692e8 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_android.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_android.py @@ -152,6 +152,12 @@ class ChromiumAndroidPort(chromium.ChromiumPort): def __init__(self, host, port_name, **kwargs): chromium.ChromiumPort.__init__(self, host, port_name, **kwargs) + # FIXME: Stop using test_shell mode: https://bugs.webkit.org/show_bug.cgi?id=88542 + if not hasattr(self._options, 'additional_drt_flag'): + self._options.additional_drt_flag = [] + if not '--test-shell' in self._options.additional_drt_flag: + self._options.additional_drt_flag.append('--test-shell') + # The Chromium port for Android always uses the hardware GPU path. self._options.enable_hardware_gpu = True @@ -209,7 +215,7 @@ class ChromiumAndroidPort(chromium.ChromiumPort): # chromium-android. # FIXME: This is a temporary measure to reduce the manual work when # updating WebKit. This method should be removed when we merge - # test_expectations_android.txt into test_expectations.txt. + # test_expectations_android.txt into TestExpectations. expectations = chromium.ChromiumPort.test_expectations(self) return expectations.replace('LINUX ', 'LINUX ANDROID ') @@ -267,7 +273,7 @@ class ChromiumAndroidPort(chromium.ChromiumPort): def _path_to_driver(self, configuration=None): if not configuration: configuration = self.get_option('configuration') - return self._build_path(configuration, 'DumpRenderTree_apk/ChromeNativeTests-debug.apk') + return self._build_path(configuration, 'DumpRenderTree_apk/DumpRenderTree-debug.apk') def _path_to_helper(self): return self._build_path(self.get_option('configuration'), 'forwarder') @@ -477,9 +483,23 @@ class ChromiumAndroidDriver(chromium.ChromiumDriver): ChromiumAndroidDriver._started_driver = self + retries = 0 + while not self._start_once(pixel_tests, per_test_args): + _log.error('Failed to start DumpRenderTree application. Log:\n' + self._port._get_logcat()) + retries += 1 + if retries >= 3: + raise AssertionError('Failed to start DumpRenderTree application multiple times. Give up.') + self.stop() + time.sleep(2) + + def _start_once(self, pixel_tests, per_test_args): self._port._run_adb_command(['logcat', '-c']) self._port._run_adb_command(['shell', 'echo'] + self.cmd_line(pixel_tests, per_test_args) + ['>', COMMAND_LINE_FILE]) - self._port._run_adb_command(['shell', 'am', 'start', '-n', DRT_ACTIVITY_FULL_NAME]) + start_result = self._port._run_adb_command(['shell', 'am', 'start', '-n', DRT_ACTIVITY_FULL_NAME]) + if start_result.find('Exception') != -1: + _log.error('Failed to start DumpRenderTree application. Exception:\n' + start_result) + return False + seconds = 0 while (not self._file_exists_on_device(self._in_fifo_path) or not self._file_exists_on_device(self._out_fifo_path) or @@ -487,8 +507,7 @@ class ChromiumAndroidDriver(chromium.ChromiumDriver): time.sleep(1) seconds += 1 if seconds >= DRT_START_STOP_TIMEOUT_SECS: - _log.error('Failed to start DumpRenderTreeApplication. Log:\n' + self._port._get_logcat()) - raise AssertionError('Failed to start DumpRenderTree application.') + return False shell_cmd = self._port._adb_command + ['shell'] executive = self._port._executive @@ -531,7 +550,7 @@ class ChromiumAndroidDriver(chromium.ChromiumDriver): else: # Inform the deadlock detector that the startup is successful without deadlock. normal_startup_event.set() - return + return True def run_test(self, driver_input): driver_output = chromium.ChromiumDriver.run_test(self, driver_input) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux.py index 3e07a6a73..05ee62dd6 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux.py @@ -57,6 +57,8 @@ class ChromiumLinuxPort(chromium.ChromiumPort): ], } + DEFAULT_BUILD_DIRECTORIES = ('sconsbuild', 'out') + @classmethod def _determine_driver_path_statically(cls, host, options): config_object = config.Config(host.executive, host.filesystem) @@ -70,18 +72,6 @@ class ChromiumLinuxPort(chromium.ChromiumPort): return cls._static_build_path(host.filesystem, build_directory, chromium_base, webkit_base, configuration, 'DumpRenderTree') @staticmethod - def _static_build_path(filesystem, build_directory, chromium_base, webkit_base, *comps): - if build_directory: - return filesystem.join(build_directory, *comps) - if filesystem.exists(filesystem.join(chromium_base, 'sconsbuild')): - return filesystem.join(chromium_base, 'sconsbuild', *comps) - if filesystem.exists(filesystem.join(chromium_base, 'out')): - return filesystem.join(chromium_base, 'out', *comps) - if filesystem.exists(filesystem.join(webkit_base, 'sconsbuild')): - return filesystem.join(webkit_base, 'sconsbuild', *comps) - return filesystem.join(webkit_base, 'out', *comps) - - @staticmethod def _determine_architecture(filesystem, executive, driver_path): file_output = '' if filesystem.exists(driver_path): @@ -137,9 +127,6 @@ class ChromiumLinuxPort(chromium.ChromiumPort): # PROTECTED METHODS # - def _build_path(self, *comps): - return self._static_build_path(self._filesystem, self.get_option('build_directory'), self.path_from_chromium_base(), self.path_from_webkit_base(), *comps) - def _check_apache_install(self): result = self._check_file_exists(self._path_to_apache(), "apache2") result = self._check_file_exists(self._path_to_apache_config_file(), "apache2 config file") and result diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux_unittest.py index 647596227..e0ef728bd 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_linux_unittest.py @@ -30,6 +30,7 @@ import unittest from webkitpy.common.system import executive_mock from webkitpy.common.system.systemhost_mock import MockSystemHost +from webkitpy.tool.mocktool import MockOptions from webkitpy.layout_tests.port import chromium_linux from webkitpy.layout_tests.port import port_testcase @@ -89,6 +90,29 @@ class ChromiumLinuxPortTest(port_testcase.PortTestCase): def test_operating_system(self): self.assertEqual('linux', self.make_port().operating_system()) + def test_build_path(self): + # Test that optional paths are used regardless of whether they exist. + options = MockOptions(configuration='Release', build_directory='/foo') + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out'], '/foo') + + # Test that optional relative paths are returned unmodified. + options = MockOptions(configuration='Release', build_directory='foo') + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out'], 'foo') + + # Test that we look in a chromium directory before the webkit directory. + options = MockOptions(configuration='Release', build_directory=None) + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out', '/mock-checkout/out'], '/mock-checkout/Source/WebKit/chromium/out') + + # Test that we prefer the legacy dir over the new dir. + options = MockOptions(configuration='Release', build_directory=None) + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/sconsbuild', '/mock-checkout/Source/WebKit/chromium/out'], '/mock-checkout/Source/WebKit/chromium/sconsbuild') + + def test_driver_name_option(self): + self.assertTrue(self.make_port()._path_to_driver().endswith('DumpRenderTree')) + self.assertTrue(self.make_port(options=MockOptions(driver_name='OtherDriver'))._path_to_driver().endswith('OtherDriver')) + + def test_path_to_image_diff(self): + self.assertEquals(self.make_port()._path_to_image_diff(), '/mock-checkout/out/Release/ImageDiff') if __name__ == '__main__': port_testcase.main() diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac.py index e1cafccfd..698db7266 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac.py @@ -68,6 +68,8 @@ class ChromiumMacPort(chromium.ChromiumPort): ], } + DEFAULT_BUILD_DIRECTORIES = ('xcodebuild', 'out') + @classmethod def determine_full_port_name(cls, host, options, port_name): if port_name.endswith('-mac'): @@ -99,40 +101,10 @@ class ChromiumMacPort(chromium.ChromiumPort): def operating_system(self): return 'mac' - def default_child_processes(self): - # FIXME: As a temporary workaround while we figure out what's going - # on with https://bugs.webkit.org/show_bug.cgi?id=83076, reduce by - # half the # of workers we run by default on bigger machines. - default_count = super(ChromiumMacPort, self).default_child_processes() - if default_count >= 8: - cpu_count = self._executive.cpu_count() - return max(1, min(default_count, int(cpu_count / 2))) - return default_count - # # PROTECTED METHODS # - def _build_path(self, *comps): - if self.get_option('build_directory'): - return self._filesystem.join(self.get_option('build_directory'), - *comps) - base = self.path_from_chromium_base() - path = self._filesystem.join(base, 'out', *comps) - if self._filesystem.exists(path): - return path - - path = self._filesystem.join(base, 'xcodebuild', *comps) - if self._filesystem.exists(path): - return path - - base = self.path_from_webkit_base() - path = self._filesystem.join(base, 'out', *comps) - if self._filesystem.exists(path): - return path - - return self._filesystem.join(base, 'xcodebuild', *comps) - def check_wdiff(self, logging=True): try: # We're ignoring the return and always returning True diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac_unittest.py index 51948eceb..a85beca74 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_mac_unittest.py @@ -30,6 +30,7 @@ import unittest from webkitpy.layout_tests.port import chromium_mac from webkitpy.layout_tests.port import port_testcase +from webkitpy.tool.mocktool import MockOptions class ChromiumMacPortTest(port_testcase.PortTestCase): @@ -81,6 +82,30 @@ class ChromiumMacPortTest(port_testcase.PortTestCase): def test_operating_system(self): self.assertEqual('mac', self.make_port().operating_system()) + def test_build_path(self): + # Test that optional paths are used regardless of whether they exist. + options = MockOptions(configuration='Release', build_directory='/foo') + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out'], '/foo') + + # Test that optional relative paths are returned unmodified. + options = MockOptions(configuration='Release', build_directory='foo') + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out'], 'foo') + + # Test that we look in a chromium directory before the webkit directory. + options = MockOptions(configuration='Release', build_directory=None) + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out', '/mock-checkout/out'], '/mock-checkout/Source/WebKit/chromium/out') + + # Test that we prefer the legacy dir over the new dir. + options = MockOptions(configuration='Release', build_directory=None) + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/xcodebuild', '/mock-checkout/Source/WebKit/chromium/out'], '/mock-checkout/Source/WebKit/chromium/xcodebuild') + + def test_driver_name_option(self): + self.assertTrue(self.make_port()._path_to_driver().endswith('DumpRenderTree')) + self.assertTrue(self.make_port(options=MockOptions(driver_name='OtherDriver'))._path_to_driver().endswith('OtherDriver')) + + def test_path_to_image_diff(self): + self.assertEquals(self.make_port()._path_to_image_diff(), '/mock-checkout/out/Release/ImageDiff') + if __name__ == '__main__': port_testcase.main() diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py index 16275d70e..532843a9f 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_unittest.py @@ -160,6 +160,21 @@ class ChromiumDriverTest(unittest.TestCase): driver2.stop() self.assertTrue(time.time() - start_time < 20) + def test_stop_cleans_up_properly(self): + self.driver._test_shell = False + self.driver.start(True, []) + last_tmpdir = self.port._filesystem.last_tmpdir + self.assertNotEquals(last_tmpdir, None) + self.driver.stop() + self.assertFalse(self.port._filesystem.isdir(last_tmpdir)) + + def test_two_starts_cleans_up_properly(self): + # clone the WebKitDriverTest tests here since we override start() and stop() + self.driver._test_shell = False + self.driver.start(True, []) + last_tmpdir = self.port._filesystem.last_tmpdir + self.driver._start(True, []) + self.assertFalse(self.port._filesystem.isdir(last_tmpdir)) class ChromiumPortTest(port_testcase.PortTestCase): port_name = 'chromium-mac' @@ -237,12 +252,6 @@ class ChromiumPortTest(port_testcase.PortTestCase): self.default_configuration_called = True return 'default' - def test_path_to_image_diff(self): - # FIXME: These don't need to use endswith now that the port uses a MockFileSystem. - self.assertTrue(ChromiumPortTest.TestLinuxPort()._path_to_image_diff().endswith('/out/default/ImageDiff')) - self.assertTrue(ChromiumPortTest.TestMacPort()._path_to_image_diff().endswith('/xcodebuild/default/ImageDiff')) - self.assertTrue(ChromiumPortTest.TestWinPort()._path_to_image_diff().endswith('/default/ImageDiff.exe')) - def test_default_configuration(self): mock_options = MockOptions() port = ChromiumPortTest.TestLinuxPort(options=mock_options) @@ -283,62 +292,30 @@ class ChromiumPortTest(port_testcase.PortTestCase): exception_raised = True self.assertFalse(exception_raised) - def test_overrides_and_builder_names(self): + def test_expectations_files(self): port = self.make_port() + port.port_name = 'chromium' - filesystem = MockFileSystem() - port._filesystem = filesystem - port.path_from_chromium_base = lambda *comps: '/' + '/'.join(comps) - + expectations_path = port.path_to_test_expectations_file() chromium_overrides_path = port.path_from_chromium_base( 'webkit', 'tools', 'layout_tests', 'test_expectations.txt') - CHROMIUM_OVERRIDES = 'contents of %s\n' % chromium_overrides_path - filesystem.write_text_file(chromium_overrides_path, CHROMIUM_OVERRIDES) skia_overrides_path = port.path_from_chromium_base( 'skia', 'skia_test_expectations.txt') - SKIA_OVERRIDES = 'contents of %s\n' % skia_overrides_path - filesystem.write_text_file(skia_overrides_path, SKIA_OVERRIDES) - - additional_expectations_path = port.path_from_chromium_base( - 'additional_expectations.txt') - ADDITIONAL_EXPECTATIONS = 'contents of %s\n' % additional_expectations_path - filesystem.write_text_file(additional_expectations_path, ADDITIONAL_EXPECTATIONS) port._options.builder_name = 'DUMMY_BUILDER_NAME' - port._options.additional_expectations = [] - self.assertEquals(port.test_expectations_overrides(), - SKIA_OVERRIDES + CHROMIUM_OVERRIDES) - port._options.additional_expectations = [additional_expectations_path] - self.assertEquals(port.test_expectations_overrides(), - SKIA_OVERRIDES + CHROMIUM_OVERRIDES + ADDITIONAL_EXPECTATIONS) + self.assertEquals(port.expectations_files(), [expectations_path, skia_overrides_path, chromium_overrides_path]) port._options.builder_name = 'builder (deps)' - port._options.additional_expectations = [] - self.assertEquals(port.test_expectations_overrides(), - SKIA_OVERRIDES + CHROMIUM_OVERRIDES) - port._options.additional_expectations = [additional_expectations_path] - self.assertEquals(port.test_expectations_overrides(), - SKIA_OVERRIDES + CHROMIUM_OVERRIDES + ADDITIONAL_EXPECTATIONS) + self.assertEquals(port.expectations_files(), [expectations_path, skia_overrides_path, chromium_overrides_path]) # A builder which does NOT observe the Chromium test_expectations, # but still observes the Skia test_expectations... port._options.builder_name = 'builder' - port._options.additional_expectations = [] - self.assertEquals(port.test_expectations_overrides(), - SKIA_OVERRIDES) - port._options.additional_expectations = [additional_expectations_path] - self.assertEquals(port.test_expectations_overrides(), - SKIA_OVERRIDES + ADDITIONAL_EXPECTATIONS) - - def test_driver_name_option(self): - self.assertTrue(ChromiumPortTest.TestLinuxPort()._path_to_driver().endswith('/out/default/DumpRenderTree')) - self.assertTrue(ChromiumPortTest.TestMacPort()._path_to_driver().endswith('/xcodebuild/default/DumpRenderTree.app/Contents/MacOS/DumpRenderTree')) - self.assertTrue(ChromiumPortTest.TestWinPort()._path_to_driver().endswith('/default/DumpRenderTree.exe')) - - options = MockOptions(driver_name='OtherDriver') - self.assertTrue(ChromiumPortTest.TestLinuxPort(options)._path_to_driver().endswith('/out/default/OtherDriver')) - self.assertTrue(ChromiumPortTest.TestMacPort(options)._path_to_driver().endswith('/xcodebuild/default/OtherDriver.app/Contents/MacOS/OtherDriver')) - self.assertTrue(ChromiumPortTest.TestWinPort(options)._path_to_driver().endswith('/default/OtherDriver.exe')) + self.assertEquals(port.expectations_files(), [expectations_path, skia_overrides_path]) + + def test_expectations_ordering(self): + # since we don't implement self.port_name in ChromiumPort. + pass class ChromiumPortLoggingTest(logtesting.LoggingTestCase): diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_win.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win.py index b06fe9135..b76f7ee68 100755 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_win.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win.py @@ -66,6 +66,8 @@ class ChromiumWinPort(chromium.ChromiumPort): ], } + DEFAULT_BUILD_DIRECTORIES = ('build', 'out') + @classmethod def determine_full_port_name(cls, host, options, port_name): if port_name.endswith('-win'): @@ -128,14 +130,6 @@ class ChromiumWinPort(chromium.ChromiumPort): # # PROTECTED ROUTINES # - def _build_path(self, *comps): - if self.get_option('build_directory'): - return self._filesystem.join(self.get_option('build_directory'), *comps) - - p = self.path_from_chromium_base('build', *comps) - if self._filesystem.exists(p): - return p - return self._filesystem.join(self.path_from_webkit_base(), 'Source', 'WebKit', 'chromium', 'build', *comps) def _uses_apache(self): return False diff --git a/Tools/Scripts/webkitpy/layout_tests/port/chromium_win_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win_unittest.py index 5bff7e129..759bc7e1d 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/chromium_win_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/chromium_win_unittest.py @@ -34,14 +34,10 @@ from webkitpy.common.system.executive_mock import MockExecutive from webkitpy.common.system.filesystem_mock import MockFileSystem from webkitpy.layout_tests.port import chromium_win from webkitpy.layout_tests.port import port_testcase +from webkitpy.tool.mocktool import MockOptions class ChromiumWinTest(port_testcase.PortTestCase): - class RegisterCygwinOption(object): - def __init__(self): - self.register_cygwin = True - self.results_directory = '/' - port_name = 'chromium-win' port_maker = chromium_win.ChromiumWinPort os_name = 'win' @@ -66,7 +62,7 @@ class ChromiumWinTest(port_testcase.PortTestCase): self.assertEquals(env['CYGWIN_PATH'], '/mock-checkout/Source/WebKit/chromium/third_party/cygwin/bin') def test_setup_environ_for_server_register_cygwin(self): - port = self.make_port(options=ChromiumWinTest.RegisterCygwinOption()) + port = self.make_port(options=MockOptions(register_cygwin=True, results_directory='/')) port._executive = MockExecutive(should_log=True) expected_stderr = "MOCK run_command: ['/mock-checkout/Source/WebKit/chromium/third_party/cygwin/setup_mount.bat'], cwd=None\n" output = outputcapture.OutputCapture() @@ -111,16 +107,28 @@ class ChromiumWinTest(port_testcase.PortTestCase): self.assertEquals(port.baseline_path(), port._webkit_baseline_path('chromium-win')) def test_build_path(self): - port = self.make_port() - port._filesystem.files = { - '/mock-checkout/Source/WebKit/chromium/build/Release/DumpRenderTree.exe': 'exe', - } - self.assertEquals( - '/mock-checkout/Source/WebKit/chromium/build/Release/DumpRenderTree.exe', - port._path_to_driver('Release')) - self.assertEquals( - '/mock-checkout/Source/WebKit/chromium/build/Debug/DumpRenderTree.exe', - port._path_to_driver('Debug')) + # Test that optional paths are used regardless of whether they exist. + options = MockOptions(configuration='Release', build_directory='/foo') + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out'], '/foo') + + # Test that optional relative paths are returned unmodified. + options = MockOptions(configuration='Release', build_directory='foo') + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out'], 'foo') + + # Test that we look in a chromium directory before the webkit directory. + options = MockOptions(configuration='Release', build_directory=None) + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/out', '/mock-checkout/out'], '/mock-checkout/Source/WebKit/chromium/out') + + # Test that we prefer the legacy dir over the new dir. + options = MockOptions(configuration='Release', build_directory=None) + self.assert_build_path(options, ['/mock-checkout/Source/WebKit/chromium/build', '/mock-checkout/Source/WebKit/chromium/out'], '/mock-checkout/Source/WebKit/chromium/build') def test_operating_system(self): self.assertEqual('win', self.make_port().operating_system()) + + def test_driver_name_option(self): + self.assertTrue(self.make_port()._path_to_driver().endswith('DumpRenderTree.exe')) + self.assertTrue(self.make_port(options=MockOptions(driver_name='OtherDriver'))._path_to_driver().endswith('OtherDriver.exe')) + + def test_path_to_image_diff(self): + self.assertEquals(self.make_port()._path_to_image_diff(), '/mock-checkout/out/Release/ImageDiff.exe') diff --git a/Tools/Scripts/webkitpy/layout_tests/port/config.py b/Tools/Scripts/webkitpy/layout_tests/port/config.py index 6a1045f12..9297b8872 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/config.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/config.py @@ -78,7 +78,7 @@ class Config(object): if not self._build_directories.get(configuration): args = ["perl", self.script_path("webkit-build-directory")] + flags - output = self._executive.run_command(args, cwd=self.webkit_base_dir()).rstrip() + output = self._executive.run_command(args, cwd=self.webkit_base_dir(), return_stderr=False).rstrip() parts = output.split("\n") self._build_directories[configuration] = parts[0] diff --git a/Tools/Scripts/webkitpy/layout_tests/port/config_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/config_unittest.py index 08b9af16c..c7d22c966 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/config_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/config_unittest.py @@ -46,8 +46,8 @@ class ConfigTest(unittest.TestCase): def tearDown(self): config.clear_cached_configuration() - def make_config(self, output='', files=None, exit_code=0, exception=None, run_command_fn=None): - e = MockExecutive2(output=output, exit_code=exit_code, exception=exception, run_command_fn=run_command_fn) + def make_config(self, output='', files=None, exit_code=0, exception=None, run_command_fn=None, stderr=''): + e = MockExecutive2(output=output, exit_code=exit_code, exception=exception, run_command_fn=run_command_fn, stderr=stderr) fs = MockFileSystem(files) return config.Config(e, fs) @@ -87,6 +87,10 @@ class ConfigTest(unittest.TestCase): self.assertTrue(c.build_directory('Debug').endswith('/Debug')) self.assertRaises(KeyError, c.build_directory, 'Unknown') + # Test that stderr output from webkit-build-directory won't mangle the build dir + c = self.make_config(output='/WebKitBuild/', stderr="mock stderr output from webkit-build-directory") + self.assertEqual(c.build_directory(None), '/WebKitBuild/') + def test_default_configuration__release(self): self.assert_configuration('Release', 'Release') diff --git a/Tools/Scripts/webkitpy/layout_tests/port/driver.py b/Tools/Scripts/webkitpy/layout_tests/port/driver.py index a113da3a8..f65db2f7b 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/driver.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/driver.py @@ -132,7 +132,7 @@ class Driver(object): def test_to_uri(self, test_name): """Convert a test name to a URI.""" if not self.is_http_test(test_name): - return path.abspath_to_uri(self._port.abspath_for_test(test_name)) + return path.abspath_to_uri(self._port.host.platform, self._port.abspath_for_test(test_name)) relative_path = test_name[len(self.HTTP_DIR):] @@ -150,7 +150,10 @@ class Driver(object): """ if uri.startswith("file:///"): - return uri[len(path.abspath_to_uri(self._port.layout_tests_dir()) + "/"):] + prefix = path.abspath_to_uri(self._port.host.platform, self._port.layout_tests_dir()) + if not prefix.endswith('/'): + prefix += '/' + return uri[len(prefix):] if uri.startswith("http://"): return uri.replace('http://127.0.0.1:8000/', self.HTTP_DIR) if uri.startswith("https://"): diff --git a/Tools/Scripts/webkitpy/layout_tests/port/driver_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/driver_unittest.py index 08d4d9882..00a42f42f 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/driver_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/driver_unittest.py @@ -29,7 +29,6 @@ import sys import unittest -from webkitpy.common.system.path import abspath_to_uri from webkitpy.common.system.systemhost_mock import MockSystemHost from webkitpy.layout_tests.port import Port, Driver, DriverOutput @@ -105,20 +104,14 @@ class DriverTest(unittest.TestCase): def test_test_to_uri(self): port = self.make_port() driver = Driver(port, None, pixel_tests=False) - if sys.platform in ('cygwin', 'win32'): - self.assertEqual(driver.test_to_uri('foo/bar.html'), 'file:///%s/foo/bar.html' % port.layout_tests_dir()) - else: - self.assertEqual(driver.test_to_uri('foo/bar.html'), 'file://%s/foo/bar.html' % port.layout_tests_dir()) + self.assertEqual(driver.test_to_uri('foo/bar.html'), 'file://%s/foo/bar.html' % port.layout_tests_dir()) self.assertEqual(driver.test_to_uri('http/tests/foo.html'), 'http://127.0.0.1:8000/foo.html') self.assertEqual(driver.test_to_uri('http/tests/ssl/bar.html'), 'https://127.0.0.1:8443/ssl/bar.html') def test_uri_to_test(self): port = self.make_port() driver = Driver(port, None, pixel_tests=False) - if sys.platform in ('cygwin', 'win32'): - self.assertEqual(driver.uri_to_test('file:///%s/foo/bar.html' % port.layout_tests_dir()), 'foo/bar.html') - else: - self.assertEqual(driver.uri_to_test('file://%s/foo/bar.html' % port.layout_tests_dir()), 'foo/bar.html') + self.assertEqual(driver.uri_to_test('file://%s/foo/bar.html' % port.layout_tests_dir()), 'foo/bar.html') self.assertEqual(driver.uri_to_test('http://127.0.0.1:8000/foo.html'), 'http/tests/foo.html') self.assertEqual(driver.uri_to_test('https://127.0.0.1:8443/ssl/bar.html'), 'http/tests/ssl/bar.html') diff --git a/Tools/Scripts/webkitpy/layout_tests/port/factory.py b/Tools/Scripts/webkitpy/layout_tests/port/factory.py index 9f5df7c7e..881a80bec 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/factory.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/factory.py @@ -53,13 +53,15 @@ def port_options(**help_strings): optparse.make_option('--gtk', action='store_const', const='gtk', dest="platform", help='Alias for --platform=gtk'), optparse.make_option('--qt', action='store_const', const='qt', dest="platform", - help='Alias for --platform=qt')] + help='Alias for --platform=qt'), + optparse.make_option('--32-bit', action='store_const', const='x86', default=None, dest="architecture", + help='use 32-bit binaries by default (x86 instead of x86_64)')] -class BuilderOptions(object): - def __init__(self, builder_name): - self.configuration = "Debug" if re.search(r"[d|D](ebu|b)g", builder_name) else "Release" - self.builder_name = builder_name +def _builder_options(builder_name): + configuration = "Debug" if re.search(r"[d|D](ebu|b)g", builder_name) else "Release" + builder_name = builder_name + return optparse.Values({'builder_name': builder_name, 'configuration': configuration}) class PortFactory(object): @@ -127,6 +129,6 @@ class PortFactory(object): def get_from_builder_name(self, builder_name): port_name = builders.port_name_for_builder_name(builder_name) assert(port_name) # Need to update port_name_for_builder_name - port = self.get(port_name, BuilderOptions(builder_name)) + port = self.get(port_name, _builder_options(builder_name)) assert(port) # Need to update port_name_for_builder_name return port diff --git a/Tools/Scripts/webkitpy/layout_tests/port/factory_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/factory_unittest.py index 9fc20582f..700399e28 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/factory_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/factory_unittest.py @@ -120,6 +120,10 @@ class FactoryTest(unittest.TestCase): def test_unknown_default(self): self.assertRaises(NotImplementedError, factory.PortFactory(MockSystemHost(os_name='vms')).get) + def test_get_from_builder_name(self): + self.assertEquals(factory.PortFactory(MockSystemHost()).get_from_builder_name('Webkit Mac10.7').name(), + 'chromium-mac-lion') + if __name__ == '__main__': unittest.main() diff --git a/Tools/Scripts/webkitpy/layout_tests/port/google_chrome.py b/Tools/Scripts/webkitpy/layout_tests/port/google_chrome.py index 6d5d9276b..8d2df0ee9 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/google_chrome.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/google_chrome.py @@ -29,21 +29,8 @@ import chromium_mac import chromium_win -def _test_expectations_overrides(port, super): - # The chrome ports use the regular overrides plus anything in the - # official test_expectations as well. Hopefully we don't get collisions. - chromium_overrides = super.test_expectations_overrides(port) - - # FIXME: It used to be that AssertionError would get raised by - # path_from_chromium_base() if we weren't in a Chromium checkout, but - # this changed in r60427. This should probably be changed back. - overrides_path = port.path_from_chromium_base('webkit', 'tools', - 'layout_tests', 'test_expectations_chrome.txt') - if not port._filesystem.exists(overrides_path): - return chromium_overrides - - chromium_overrides = chromium_overrides or '' - return chromium_overrides + port._filesystem.read_text_file(overrides_path) +def _expectations_files(port, super): + return super.expectations_files(port) + [port.path_from_chromium_base('webkit', 'tools', 'layout_tests', 'test_expectations_chrome.txt')] class GoogleChromeLinux32Port(chromium_linux.ChromiumLinuxPort): @@ -59,8 +46,8 @@ class GoogleChromeLinux32Port(chromium_linux.ChromiumLinuxPort): paths.insert(0, self._webkit_baseline_path('google-chrome-linux32')) return paths - def test_expectations_overrides(self): - return _test_expectations_overrides(self, chromium_linux.ChromiumLinuxPort) + def expectations_files(self): + return _expectations_files(self, chromium_linux.ChromiumLinuxPort) def architecture(self): return 'x86' @@ -79,8 +66,8 @@ class GoogleChromeLinux64Port(chromium_linux.ChromiumLinuxPort): paths.insert(0, self._webkit_baseline_path('google-chrome-linux64')) return paths - def test_expectations_overrides(self): - return _test_expectations_overrides(self, chromium_linux.ChromiumLinuxPort) + def expectations_files(self): + return _expectations_files(self, chromium_linux.ChromiumLinuxPort) def architecture(self): return 'x86_64' @@ -99,8 +86,8 @@ class GoogleChromeMacPort(chromium_mac.ChromiumMacPort): paths.insert(0, self._webkit_baseline_path('google-chrome-mac')) return paths - def test_expectations_overrides(self): - return _test_expectations_overrides(self, chromium_mac.ChromiumMacPort) + def expectations_files(self): + return _expectations_files(self, chromium_mac.ChromiumMacPort) class GoogleChromeWinPort(chromium_win.ChromiumWinPort): @@ -116,5 +103,5 @@ class GoogleChromeWinPort(chromium_win.ChromiumWinPort): paths.insert(0, self._webkit_baseline_path('google-chrome-win')) return paths - def test_expectations_overrides(self): - return _test_expectations_overrides(self, chromium_win.ChromiumWinPort) + def expectations_files(self): + return _expectations_files(self, chromium_win.ChromiumWinPort) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py index 9d3653ae5..6d3c1a7dd 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/google_chrome_unittest.py @@ -38,23 +38,10 @@ class TestGoogleChromePort(unittest.TestCase): def _verify_expectations_overrides(self, port_name): host = MockSystemHost() - chromium_port = PortFactory(host).get("chromium-mac-leopard") - chromium_base = chromium_port.path_from_chromium_base() port = PortFactory(host).get(port_name=port_name, options=None) - - expected_chromium_overrides = '// chromium overrides\n' - expected_chrome_overrides = '// chrome overrides\n' - chromium_path = host.filesystem.join(chromium_base, 'webkit', 'tools', 'layout_tests', 'test_expectations.txt') - chrome_path = host.filesystem.join(chromium_base, 'webkit', 'tools', 'layout_tests', 'test_expectations_chrome.txt') - - host.filesystem.files[chromium_path] = expected_chromium_overrides - host.filesystem.files[chrome_path] = None - actual_chrome_overrides = port.test_expectations_overrides() - self.assertEqual(expected_chromium_overrides, actual_chrome_overrides) - - host.filesystem.files[chrome_path] = expected_chrome_overrides - actual_chrome_overrides = port.test_expectations_overrides() - self.assertEqual(actual_chrome_overrides, expected_chromium_overrides + expected_chrome_overrides) + self.assertTrue('TestExpectations' in port.expectations_files()[0]) + self.assertTrue('skia_test_expectations.txt' in port.expectations_files()[1]) + self.assertTrue('test_expectations_chrome.txt' in port.expectations_files()[-1]) def test_get_google_chrome_port(self): self._verify_baseline_search_path_startswith('google-chrome-linux32', ['google-chrome-linux32', 'chromium-linux-x86']) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/mac.py b/Tools/Scripts/webkitpy/layout_tests/port/mac.py index fa57cc3f7..a3ec6a4b9 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/mac.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/mac.py @@ -31,6 +31,7 @@ import logging import os import re import subprocess +import sys import time from webkitpy.common.system.crashlogs import CrashLogs @@ -49,14 +50,24 @@ class MacPort(ApplePort): # and the order of fallback between them. Matches ORWT. VERSION_FALLBACK_ORDER = ["mac-leopard", "mac-snowleopard", "mac-lion", "mac"] + ARCHITECTURES = ['x86_64', 'x86'] + def __init__(self, host, port_name, **kwargs): ApplePort.__init__(self, host, port_name, **kwargs) + self._architecture = self.get_option('architecture') + + if not self._architecture: + self._architecture = 'x86_64' + self._leak_detector = LeakDetector(self) if self.get_option("leaks"): # DumpRenderTree slows down noticably if we run more than about 1000 tests in a batch # with MallocStackLogging enabled. self.set_option_default("batch_size", 1000) + def _build_driver_flags(self): + return ['ARCHS=i386'] if self.architecture() == 'x86' else [] + def _most_recent_version(self): # This represents the most recently-shipping version of the operating system. return self.VERSION_FALLBACK_ORDER[-2] @@ -107,18 +118,22 @@ class MacPort(ApplePort): return self._version == "lion" def default_child_processes(self): + # FIXME: The Printer isn't initialized when this is called, so using _log would just show an unitialized logger error. + if self.is_snowleopard(): - _log.warn("Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525.") + print >> sys.stderr, "Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525." return 1 - # FIXME: As a temporary workaround while we figure out what's going - # on with https://bugs.webkit.org/show_bug.cgi?id=83076, reduce by - # half the # of workers we run by default on bigger machines. default_count = super(MacPort, self).default_child_processes() - if default_count >= 8: - cpu_count = self._executive.cpu_count() - return max(1, min(default_count, int(cpu_count / 2))) - return default_count + + # Make sure we have enough ram to support that many instances: + total_memory = self.host.platform.total_bytes_memory() + bytes_per_drt = 256 * 1024 * 1024 # Assume each DRT needs 256MB to run. + overhead = 2048 * 1024 * 1024 # Assume we need 2GB free for the O/S + supportable_instances = max((total_memory - overhead) / bytes_per_drt, 1) # Always use one process, even if we don't have space for it. + if supportable_instances < default_count: + print >> sys.stderr, "This machine could support %s child processes, but only has enough memory for %s." % (default_count, supportable_instances) + return min(supportable_instances, default_count) def _build_java_test_support(self): java_tests_path = self._filesystem.join(self.layout_tests_dir(), "java") diff --git a/Tools/Scripts/webkitpy/layout_tests/port/mac_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/mac_unittest.py index 0b9bca978..2ebf255ae 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/mac_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/mac_unittest.py @@ -183,10 +183,22 @@ java/ # MockPlatformInfo only has 2 mock cores. The important part is that 2 > 1. self.assertEqual(port.default_child_processes(), 2) + bytes_for_drt = 200 * 1024 * 1024 + port.host.platform.total_bytes_memory = lambda: bytes_for_drt + expected_stderr = "This machine could support 2 child processes, but only has enough memory for 1.\n" + child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stderr=expected_stderr) + self.assertEqual(child_processes, 1) + + # Make sure that we always use one process, even if we don't have the memory for it. + port.host.platform.total_bytes_memory = lambda: bytes_for_drt - 1 + expected_stderr = "This machine could support 2 child processes, but only has enough memory for 1.\n" + child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stderr=expected_stderr) + self.assertEqual(child_processes, 1) + # SnowLeopard has a CFNetwork bug which causes crashes if we execute more than one copy of DRT at once. port = self.make_port(port_name='mac-snowleopard') - expected_logs = "Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525.\n" - child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_logs=expected_logs) + expected_stderr = "Cannot run tests in parallel on Snow Leopard due to rdar://problem/10621525.\n" + child_processes = OutputCapture().assert_outputs(self, port.default_child_processes, (), expected_stderr=expected_stderr) self.assertEqual(child_processes, 1) def test_get_crash_log(self): @@ -253,3 +265,26 @@ java/ port = self.make_port() port._executive = MockExecutive2(run_command_fn=throwing_run_command) OutputCapture().assert_outputs(self, port.sample_process, args=['test', 42]) + + def test_32bit(self): + port = self.make_port(options=MockOptions(architecture='x86')) + + def run_script(script, args=None, env=None): + self.args = args + + port._run_script = run_script + self.assertEquals(port.architecture(), 'x86') + port._build_driver() + self.assertEquals(self.args, ['ARCHS=i386']) + + def test_64bit(self): + # Apple Mac port is 64-bit by default + port = self.make_port() + self.assertEquals(port.architecture(), 'x86_64') + + def run_script(script, args=None, env=None): + self.args = args + + port._run_script = run_script + port._build_driver() + self.assertEquals(self.args, []) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/mock_drt_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/mock_drt_unittest.py index 605071805..570a9e4ad 100755 --- a/Tools/Scripts/webkitpy/layout_tests/port/mock_drt_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/mock_drt_unittest.py @@ -49,11 +49,6 @@ class MockDRTPortTest(port_testcase.PortTestCase): def make_port(self, options=mock_options): host = MockSystemHost() test.add_unit_tests_to_mock_filesystem(host.filesystem) - if sys.platform == 'win32': - # We use this because the 'win' port doesn't work yet. - host.platform.os_name = 'win' - host.platform.os_version = 'xp' - return mock_drt.MockDRTPort(host, port_name='mock-chromium-win', options=options) return mock_drt.MockDRTPort(host, port_name='mock-mac', options=options) def test_port_name_in_constructor(self): @@ -93,10 +88,7 @@ class MockDRTTest(unittest.TestCase): def input_line(self, port, test_name, checksum=None): url = port.create_driver(0).test_to_uri(test_name) if url.startswith('file://'): - if sys.platform == 'win32': - url = url[len('file:///'):] - else: - url = url[len('file://'):] + url = url[len('file://'):] if checksum: return url + "'" + checksum + '\n' @@ -247,9 +239,6 @@ class MockChromiumDRTTest(MockDRTTest): def test_pixeltest__fails(self): host = MockSystemHost() url = '#URL:file://' - if sys.platform == 'win32': - host = MockSystemHost(os_name='win', os_version='xp') - url = '#URL:file:///' url = url + '%s/failures/expected/image_checksum.html' % PortFactory(host).get('test').layout_tests_dir() self.assertTest('failures/expected/image_checksum.html', pixel_tests=True, expected_checksum='image_checksum', diff --git a/Tools/Scripts/webkitpy/layout_tests/port/port_testcase.py b/Tools/Scripts/webkitpy/layout_tests/port/port_testcase.py index 4bdc3ed13..f37273f78 100755 --- a/Tools/Scripts/webkitpy/layout_tests/port/port_testcase.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/port_testcase.py @@ -333,6 +333,30 @@ class PortTestCase(unittest.TestCase): u'STDOUT: foo\ufffdbar\n' u'STDERR: foo\ufffdbar\n')) + def assert_build_path(self, options, dirs, expected_path): + port = self.make_port(options=options) + for directory in dirs: + port.host.filesystem.maybe_make_directory(directory) + self.assertEquals(port._build_path(), expected_path) + + def test_expectations_ordering(self): + port = self.make_port() + for path in port.expectations_files(): + port._filesystem.write_text_file(path, '') + ordered_dict = port.expectations_dict() + self.assertEquals(port.path_to_test_expectations_file(), ordered_dict.keys()[0]) + + options = MockOptions(additional_expectations=['/tmp/foo', '/tmp/bar']) + port = self.make_port(options=options) + for path in port.expectations_files(): + port._filesystem.write_text_file(path, '') + port._filesystem.write_text_file('/tmp/foo', 'foo') + port._filesystem.write_text_file('/tmp/bar', 'bar') + ordered_dict = port.expectations_dict() + self.assertEquals(ordered_dict.keys()[-2:], options.additional_expectations) + self.assertEquals(ordered_dict.values()[-2:], ['foo', 'bar']) + + # FIXME: This class and main() should be merged into test-webkitpy. class EnhancedTestLoader(unittest.TestLoader): integration_tests = False diff --git a/Tools/Scripts/webkitpy/layout_tests/port/qt.py b/Tools/Scripts/webkitpy/layout_tests/port/qt.py index 520575952..2488936fe 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/qt.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/qt.py @@ -81,9 +81,6 @@ class QtPort(WebKitPort): # The Qt port builds DRT as part of the main build step return True - def _driver_class(self): - return XvfbDriver - def _path_to_driver(self): return self._build_path('bin/%s' % self.driver_name()) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/server_process_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/server_process_unittest.py index 4257d5455..b5ce05391 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/server_process_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/server_process_unittest.py @@ -100,11 +100,15 @@ class TestServerProcess(unittest.TestCase): line = proc.read_stdout_line(now - 1) self.assertEquals(line, None) + # FIXME: This part appears to be flaky. line should always be non-None. + # FIXME: https://bugs.webkit.org/show_bug.cgi?id=88280 line = proc.read_stdout_line(now + 1.0) - self.assertEquals(line.strip(), "stdout") + if line: + self.assertEquals(line.strip(), "stdout") line = proc.read_stderr_line(now + 1.0) - self.assertEquals(line.strip(), "stderr") + if line: + self.assertEquals(line.strip(), "stderr") proc.stop() diff --git a/Tools/Scripts/webkitpy/layout_tests/port/test.py b/Tools/Scripts/webkitpy/layout_tests/port/test.py index 8abd81235..25c6a13be 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/test.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/test.py @@ -174,6 +174,7 @@ layer at (0,0) size 800x34 tests.add('passes/checksum_in_image.html', expected_checksum=None, expected_image='tEXtchecksum\x00checksum_in_image-checksum') + tests.add('passes/skipped/skip.html') # Note that here the checksums don't match but the images do, so this test passes "unexpectedly". # See https://bugs.webkit.org/show_bug.cgi?id=69444 . @@ -233,12 +234,8 @@ layer at (0,0) size 800x34 # this works. The path contains a '.' in the name because we've seen bugs # related to this before. -if sys.platform == 'win32': - LAYOUT_TEST_DIR = 'c:/test.checkout/LayoutTests' - PERF_TEST_DIR = 'c:/test.checkout/PerformanceTests' -else: - LAYOUT_TEST_DIR = '/test.checkout/LayoutTests' - PERF_TEST_DIR = '/test.checkout/PerformanceTests' +LAYOUT_TEST_DIR = '/test.checkout/LayoutTests' +PERF_TEST_DIR = '/test.checkout/PerformanceTests' # Here we synthesize an in-memory filesystem from the test list @@ -247,8 +244,8 @@ else: def add_unit_tests_to_mock_filesystem(filesystem): # Add the test_expectations file. filesystem.maybe_make_directory(LAYOUT_TEST_DIR + '/platform/test') - if not filesystem.exists(LAYOUT_TEST_DIR + '/platform/test/test_expectations.txt'): - filesystem.write_text_file(LAYOUT_TEST_DIR + '/platform/test/test_expectations.txt', """ + if not filesystem.exists(LAYOUT_TEST_DIR + '/platform/test/TestExpectations'): + filesystem.write_text_file(LAYOUT_TEST_DIR + '/platform/test/TestExpectations', """ WONTFIX : failures/expected/crash.html = CRASH WONTFIX : failures/expected/image.html = IMAGE WONTFIX : failures/expected/audio.html = AUDIO @@ -267,6 +264,7 @@ WONTFIX : failures/expected/timeout.html = TIMEOUT WONTFIX SKIP : failures/expected/hang.html = TIMEOUT WONTFIX SKIP : failures/expected/keyboard.html = CRASH WONTFIX SKIP : failures/expected/exception.html = CRASH +WONTFIX SKIP : passes/skipped/skip.html = PASS """) filesystem.maybe_make_directory(LAYOUT_TEST_DIR + '/reftests/foo') @@ -340,7 +338,7 @@ class TestPort(Port): Port.__init__(self, host, port_name, **kwargs) self._tests = unit_test_list() self._flakes = set() - self._expectations_path = LAYOUT_TEST_DIR + '/platform/test/test_expectations.txt' + self._expectations_path = LAYOUT_TEST_DIR + '/platform/test/TestExpectations' self._results_directory = None self._operating_system = 'mac' diff --git a/Tools/Scripts/webkitpy/layout_tests/port/webkit.py b/Tools/Scripts/webkitpy/layout_tests/port/webkit.py index 879c79abf..ab35a6463 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/webkit.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/webkit.py @@ -77,10 +77,6 @@ class WebKitPort(Port): search_paths.append(self.port_name) return map(self._webkit_baseline_path, search_paths) - def path_to_test_expectations_file(self): - # test_expectations are always in mac/ not mac-leopard/ by convention, hence we use port_name instead of name(). - return self._filesystem.join(self._webkit_baseline_path(self.port_name), 'test_expectations.txt') - def _port_flag_for_scripts(self): # This is overrriden by ports which need a flag passed to scripts to distinguish the use of that port. # For example --qt on linux, since a user might have both Gtk and Qt libraries installed. @@ -118,14 +114,17 @@ class WebKitPort(Port): # These two projects should be factored out into their own # projects. try: - self._run_script("build-dumprendertree", env=env) + self._run_script("build-dumprendertree", args=self._build_driver_flags(), env=env) if self.get_option('webkit_test_runner'): - self._run_script("build-webkittestrunner", env=env) + self._run_script("build-webkittestrunner", args=self._build_driver_flags(), env=env) except ScriptError, e: _log.error(e.message_with_output(output_limit=None)) return False return True + def _build_driver_flags(self): + return [] + def _check_driver(self): driver_path = self._path_to_driver() if not self._filesystem.exists(driver_path): @@ -360,15 +359,6 @@ class WebKitPort(Port): return search_paths - def test_expectations(self): - # This allows ports to use a combination of test_expectations.txt files and Skipped lists. - expectations = '' - expectations_path = self.path_to_test_expectations_file() - if self._filesystem.exists(expectations_path): - _log.debug("Using test_expectations.txt: %s" % expectations_path) - expectations = self._filesystem.read_text_file(expectations_path) - return expectations - def skipped_layout_tests(self, test_list): tests_to_skip = set(self._expectations_from_skipped_files(self._skipped_file_search_paths())) tests_to_skip.update(self._tests_for_other_platforms()) @@ -462,8 +452,7 @@ class WebKitDriver(Driver): self._server_process = None def __del__(self): - assert(self._server_process is None) - assert(self._driver_tempdir is None) + self.stop() def cmd_line(self, pixel_tests, per_test_args): cmd = self._command_wrapper(self._port.get_option('wrapper')) @@ -488,6 +477,7 @@ class WebKitDriver(Driver): return cmd def _start(self, pixel_tests, per_test_args): + self.stop() self._driver_tempdir = self._port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name()) server_name = self._port.driver_name() environment = self._port.setup_environ_for_server(server_name) @@ -565,8 +555,7 @@ class WebKitDriver(Driver): def run_test(self, driver_input): start_time = time.time() - if not self._server_process: - self._start(driver_input.should_run_pixel_test, driver_input.args) + self.start(driver_input.should_run_pixel_test, driver_input.args) self.error_from_test = str() self.err_seen_eof = False diff --git a/Tools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py b/Tools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py index 33e422866..f7e9525d6 100755 --- a/Tools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/webkit_unittest.py @@ -94,9 +94,16 @@ class WebKitPortTest(port_testcase.PortTestCase): def test_path_to_test_expectations_file(self): port = TestWebKitPort() port._options = MockOptions(webkit_test_runner=False) - self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/test_expectations.txt') + self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations') + + port = TestWebKitPort() port._options = MockOptions(webkit_test_runner=True) - self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/test_expectations.txt') + self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations') + + port = TestWebKitPort() + port.host.filesystem.files['/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations'] = 'some content' + port._options = MockOptions(webkit_test_runner=False) + self.assertEqual(port.path_to_test_expectations_file(), '/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations') def test_skipped_directories_for_symbols(self): # This first test confirms that the commonly found symbols result in the expected skipped directories. @@ -165,10 +172,10 @@ class WebKitPortTest(port_testcase.PortTestCase): def test_test_expectations(self): # Check that we read the expectations file host = MockSystemHost() - host.filesystem.write_text_file('/mock-checkout/LayoutTests/platform/testwebkitport/test_expectations.txt', - 'BUG_TESTEXPECTATIONS SKIP : fast/html/article-element.html = FAIL\n') + host.filesystem.write_text_file('/mock-checkout/LayoutTests/platform/testwebkitport/TestExpectations', + 'BUG_TESTEXPECTATIONS SKIP : fast/html/article-element.html = TEXT\n') port = TestWebKitPort(host=host) - self.assertEqual(port.test_expectations(), 'BUG_TESTEXPECTATIONS SKIP : fast/html/article-element.html = FAIL\n') + self.assertEqual(''.join(port.expectations_dict().values()), 'BUG_TESTEXPECTATIONS SKIP : fast/html/article-element.html = TEXT\n') def test_build_driver(self): output = OutputCapture() @@ -349,7 +356,7 @@ class WebKitDriverTest(unittest.TestCase): driver._server_process = FakeServerProcess(False) driver._subprocess_was_unresponsive = False assert_crash(driver, '#CRASHED - WebProcess (pid 8675)\n', True, 'WebProcess', 8675) - + driver._crashed_process_name = None driver._crashed_pid = None driver._server_process = FakeServerProcess(False) @@ -376,3 +383,11 @@ class WebKitDriverTest(unittest.TestCase): self.assertNotEquals(last_tmpdir, None) driver.stop() self.assertFalse(port._filesystem.isdir(last_tmpdir)) + + def test_two_starts_cleans_up_properly(self): + port = TestWebKitPort() + driver = WebKitDriver(port, 0, pixel_tests=True) + driver.start(True, []) + last_tmpdir = port._filesystem.last_tmpdir + driver._start(True, []) + self.assertFalse(port._filesystem.isdir(last_tmpdir)) diff --git a/Tools/Scripts/webkitpy/layout_tests/port/win.py b/Tools/Scripts/webkitpy/layout_tests/port/win.py index da3209ead..420f4db04 100644 --- a/Tools/Scripts/webkitpy/layout_tests/port/win.py +++ b/Tools/Scripts/webkitpy/layout_tests/port/win.py @@ -30,6 +30,7 @@ import logging import re import sys +from webkitpy.common.system.systemhost import SystemHost from webkitpy.common.system.executive import ScriptError, Executive from webkitpy.common.system.path import abspath_to_uri from webkitpy.layout_tests.port.apple import ApplePort @@ -45,6 +46,8 @@ class WinPort(ApplePort): # and the order of fallback between them. Matches ORWT. VERSION_FALLBACK_ORDER = ["win-xp", "win-vista", "win-7sp0", "win"] + ARCHITECTURES = ['x86'] + def do_text_results_differ(self, expected_text, actual_text): # Sanity was restored in WK2, so we don't need this hack there. if self.get_option('webkit_test_runner'): @@ -75,7 +78,7 @@ class WinPort(ApplePort): return 'win' def show_results_html_file(self, results_filename): - self._run_script('run-safari', [abspath_to_uri(results_filename)]) + self._run_script('run-safari', [abspath_to_uri(SystemHost().platform, results_filename)]) # FIXME: webkitperl/httpd.pm installs /usr/lib/apache/libphp4.dll on cycwin automatically # as part of running old-run-webkit-tests. That's bad design, but we may need some similar hack. diff --git a/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py index d4eccb0fa..3d7b20f41 100755 --- a/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py +++ b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests.py @@ -157,8 +157,8 @@ def _set_up_derived_options(port, options): normalized_platform_directories.append(port.host.filesystem.normpath(path)) options.additional_platform_directory = normalized_platform_directories - if not options.http and options.force: - warnings.append("--no-http is ignored since --force is also provided") + if not options.http and options.skipped in ('ignore', 'only'): + warnings.append("--force/--skipped=%s overrides --no-http." % (options.skipped)) options.http = True if options.skip_pixel_test_if_no_baseline and not options.pixel_tests: @@ -167,6 +167,10 @@ def _set_up_derived_options(port, options): if options.ignore_metrics and (options.new_baseline or options.reset_results): warnings.append("--ignore-metrics has no effect with --new-baselines or with --reset-results") + if options.new_baseline: + options.reset_results = True + options.add_platform_exceptions = True + return warnings @@ -277,10 +281,12 @@ def parse_args(args=None): optparse.make_option("--results-directory", help="Location of test results"), optparse.make_option("--build-directory", help="Path to the directory under which build files are kept (should not include configuration)"), + optparse.make_option("--add-platform-exceptions", action="store_true", default=False, + help="Save generated results into the *most-specific-platform* directory rather than the *generic-platform* directory"), optparse.make_option("--new-baseline", action="store_true", default=False, help="Save generated results as new baselines " - "into the *platform* directory, overwriting whatever's " - "already there."), + "into the *most-specific-platform* directory, overwriting whatever's " + "already there. Equivalent to --reset-results --add-platform-exceptions"), optparse.make_option("--reset-results", action="store_true", default=False, help="Reset expectations to the " "generated results in their existing location."), @@ -347,17 +353,14 @@ def parse_args(args=None): help="wrapper command to insert before invocations of " "DumpRenderTree; option is split on whitespace before " "running. (Example: --wrapper='valgrind --smc-check=all')"), - # old-run-webkit-tests: - # -i|--ignore-tests Comma-separated list of directories - # or tests to ignore optparse.make_option("-i", "--ignore-tests", action="append", default=[], help="directories or test to ignore (may specify multiple times)"), optparse.make_option("--test-list", action="append", help="read list of tests to run from file", metavar="FILE"), - # old-run-webkit-tests uses --skipped==[default|ignore|only] - # instead of --force: - optparse.make_option("--force", action="store_true", default=False, - help="Run all tests, even those marked SKIP in the test list"), + optparse.make_option("--skipped", action="store", default="default", + help="control how tests marked SKIP are run. 'default' == Skip, 'ignore' == Run them anyway, 'only' == only run the SKIP tests."), + optparse.make_option("--force", dest="skipped", action="store_const", const='ignore', + help="Run all tests, even those marked SKIP in the test list (same as --skipped=ignore)"), optparse.make_option("--time-out-ms", help="Set the timeout for each test"), # old-run-webkit-tests calls --randomize-order --random: @@ -383,11 +386,11 @@ def parse_args(args=None): # FIXME: Display default number of child processes that will run. optparse.make_option("-f", "--fully-parallel", action="store_true", help="run all tests in parallel"), - optparse.make_option("--exit-after-n-failures", type="int", default=500, + optparse.make_option("--exit-after-n-failures", type="int", default=None, help="Exit after the first N failures instead of running all " "tests"), optparse.make_option("--exit-after-n-crashes-or-timeouts", type="int", - default=20, help="Exit after the first N crashes instead of " + default=None, help="Exit after the first N crashes instead of " "running all tests"), optparse.make_option("--iterations", type="int", help="Number of times to run the set of tests (e.g. ABCABCABC)"), optparse.make_option("--repeat-each", type="int", help="Number of times to run each test (e.g. AAABBBCCC)"), @@ -443,7 +446,6 @@ def main(): host = MockHost() else: host = Host() - host._initialize_scm() port = host.port_factory.get(options.platform, options) logging.getLogger().setLevel(logging.DEBUG if options.verbose else logging.INFO) return run(port, options, args) diff --git a/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_integrationtest.py b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_integrationtest.py index 96fb6853d..bfdb55cc9 100755 --- a/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_integrationtest.py +++ b/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_integrationtest.py @@ -197,18 +197,15 @@ class LintTest(unittest.TestCase, StreamTestingMixin): self.name = name self.path = path - def test_expectations(self): - self.host.ports_parsed.append(self.name) - return '' - def path_to_test_expectations_file(self): return self.path def test_configuration(self): return None - def test_expectations_overrides(self): - return None + def expectations_dict(self): + self.host.ports_parsed.append(self.name) + return {self.path: ''} def skipped_layout_tests(self, tests): return set([]) @@ -261,7 +258,7 @@ class LintTest(unittest.TestCase, StreamTestingMixin): options, parsed_args = parse_args(['--lint-test-files']) host = MockHost() port_obj = host.port_factory.get(options.platform, options=options) - port_obj.test_expectations = lambda: "# syntax error" + port_obj.expectations_dict = lambda: {'': '# syntax error'} res, out, err = run_and_capture(port_obj, options, parsed_args) self.assertEqual(res, -1) @@ -404,19 +401,29 @@ class MainTest(unittest.TestCase, StreamTestingMixin): tests_run = get_tests_run(['--skip-pixel-test-if-no-baseline'] + tests_to_run, tests_included=True, flatten_batches=True) self.assertEquals(tests_run, ['passes/image.html', 'passes/text.html']) - def test_ignore_tests(self): - def assert_ignored(args, tests_expected_to_run): - tests_to_run = ['failures/expected/image.html', 'passes/image.html'] - tests_run = get_tests_run(args + tests_to_run, tests_included=True, flatten_batches=True) - self.assertEquals(tests_run, tests_expected_to_run) + def test_ignore_flag(self): + # Note that passes/image.html is expected to be run since we specified it directly. + tests_run = get_tests_run(['-i', 'passes', 'passes/image.html'], flatten_batches=True, tests_included=True) + self.assertFalse('passes/text.html' in tests_run) + self.assertTrue('passes/image.html' in tests_run) + + def test_skipped_flag(self): + tests_run = get_tests_run(['passes'], tests_included=True, flatten_batches=True) + self.assertFalse('passes/skipped/skip.html' in tests_run) + num_tests_run_by_default = len(tests_run) - assert_ignored(['-i', 'failures/expected/image.html'], ['passes/image.html']) - assert_ignored(['-i', 'passes'], ['failures/expected/image.html']) + # Check that nothing changes when we specify skipped=default. + self.assertEquals(len(get_tests_run(['--skipped=default', 'passes'], tests_included=True, flatten_batches=True)), + num_tests_run_by_default) - # Note here that there is an expectation for failures/expected/image.html already, but - # it is overriden by the command line arg. This might be counter-intuitive. - # FIXME: This isn't currently working ... - # assert_ignored(['-i', 'failures/expected'], ['passes/image.html']) + # Now check that we run one more test (the skipped one). + tests_run = get_tests_run(['--skipped=ignore', 'passes'], tests_included=True, flatten_batches=True) + self.assertTrue('passes/skipped/skip.html' in tests_run) + self.assertEquals(len(tests_run), num_tests_run_by_default + 1) + + # Now check that we only run the skipped test. + self.assertEquals(get_tests_run(['--skipped=only', 'passes'], tests_included=True, flatten_batches=True), + ['passes/skipped/skip.html']) def test_iterations(self): tests_to_run = ['passes/image.html', 'passes/text.html'] @@ -537,7 +544,7 @@ class MainTest(unittest.TestCase, StreamTestingMixin): self.assertEqual(res, unexpected_tests_count) self.assertNotEmpty(out) self.assertNotEmpty(err) - self.assertEqual(user.opened_urls, [path.abspath_to_uri('/tmp/layout-test-results/results.html')]) + self.assertEqual(user.opened_urls, [path.abspath_to_uri(MockHost().platform, '/tmp/layout-test-results/results.html')]) def test_missing_and_unexpected_results(self): # Test that we update expectations in place. If the expectation @@ -715,7 +722,7 @@ class MainTest(unittest.TestCase, StreamTestingMixin): with host.filesystem.mkdtemp() as tmpdir: res, out, err, user = logging_run(['--results-directory=' + str(tmpdir)], tests_included=True, host=host) - self.assertEqual(user.opened_urls, [path.abspath_to_uri(host.filesystem.join(tmpdir, 'results.html'))]) + self.assertEqual(user.opened_urls, [path.abspath_to_uri(host.platform, host.filesystem.join(tmpdir, 'results.html'))]) def test_results_directory_default(self): # We run a configuration that should fail, to generate output, then @@ -723,7 +730,7 @@ class MainTest(unittest.TestCase, StreamTestingMixin): # This is the default location. res, out, err, user = logging_run(tests_included=True) - self.assertEqual(user.opened_urls, [path.abspath_to_uri('/tmp/layout-test-results/results.html')]) + self.assertEqual(user.opened_urls, [path.abspath_to_uri(MockHost().platform, '/tmp/layout-test-results/results.html')]) def test_results_directory_relative(self): # We run a configuration that should fail, to generate output, then @@ -733,7 +740,7 @@ class MainTest(unittest.TestCase, StreamTestingMixin): host.filesystem.chdir('/tmp/cwd') res, out, err, user = logging_run(['--results-directory=foo'], tests_included=True, host=host) - self.assertEqual(user.opened_urls, [path.abspath_to_uri('/tmp/cwd/foo/results.html')]) + self.assertEqual(user.opened_urls, [path.abspath_to_uri(host.platform, '/tmp/cwd/foo/results.html')]) def test_retrying_and_flaky_tests(self): host = MockHost() @@ -874,6 +881,10 @@ class MainTest(unittest.TestCase, StreamTestingMixin): self.assertTrue(MainTest.has_test_of_type(batch_tests_run_http, 'http')) self.assertTrue(MainTest.has_test_of_type(batch_tests_run_http, 'websocket')) + def test_platform_tests_are_found(self): + tests_run = get_tests_run(['http'], tests_included=True, flatten_batches=True) + self.assertTrue('platform/test-snow-leopard/http/test.html' in tests_run) + class EndToEndTest(unittest.TestCase): def parse_full_results(self, full_results_text): @@ -919,7 +930,7 @@ class RebaselineTest(unittest.TestCase, StreamTestingMixin): "assert that the file_list contains the baselines.""" for ext in extensions: baseline = file + "-expected" + ext - baseline_msg = 'Writing new expected result "%s"\n' % baseline[1:] + baseline_msg = 'Writing new expected result "%s"\n' % baseline self.assertTrue(any(f.find(baseline) != -1 for f in file_list)) self.assertContainsLine(err, baseline_msg) @@ -940,8 +951,8 @@ class RebaselineTest(unittest.TestCase, StreamTestingMixin): self.assertEquals(res, 0) self.assertEmpty(out) self.assertEqual(len(file_list), 4) - self.assertBaselines(file_list, "/passes/image", [".txt", ".png"], err) - self.assertBaselines(file_list, "/failures/expected/missing_image", [".txt", ".png"], err) + self.assertBaselines(file_list, "passes/image", [".txt", ".png"], err) + self.assertBaselines(file_list, "failures/expected/missing_image", [".txt", ".png"], err) def test_missing_results(self): # Test that we update expectations in place. If the expectation @@ -958,13 +969,13 @@ class RebaselineTest(unittest.TestCase, StreamTestingMixin): self.assertEquals(res, 0) self.assertNotEmpty(out) self.assertEqual(len(file_list), 6) - self.assertBaselines(file_list, "/failures/unexpected/missing_text", [".txt"], err) - self.assertBaselines(file_list, "/platform/test-mac-leopard/failures/unexpected/missing_image", [".png"], err) - self.assertBaselines(file_list, "/platform/test-mac-leopard/failures/unexpected/missing_render_tree_dump", [".txt"], err) + self.assertBaselines(file_list, "failures/unexpected/missing_text", [".txt"], err) + self.assertBaselines(file_list, "platform/test/failures/unexpected/missing_image", [".png"], err) + self.assertBaselines(file_list, "platform/test/failures/unexpected/missing_render_tree_dump", [".txt"], err) def test_new_baseline(self): - # Test that we update the platform expectations. If the expectation - # is mssing, then create a new expectation in the platform dir. + # Test that we update the platform expectations in the version-specific directories + # for both existing and new baselines. host = MockHost() res, out, err, _ = logging_run(['--pixel-tests', '--new-baseline', @@ -977,9 +988,9 @@ class RebaselineTest(unittest.TestCase, StreamTestingMixin): self.assertEmpty(out) self.assertEqual(len(file_list), 4) self.assertBaselines(file_list, - "/platform/test-mac-leopard/passes/image", [".txt", ".png"], err) + "platform/test-mac-leopard/passes/image", [".txt", ".png"], err) self.assertBaselines(file_list, - "/platform/test-mac-leopard/failures/expected/missing_image", [".txt", ".png"], err) + "platform/test-mac-leopard/failures/expected/missing_image", [".txt", ".png"], err) if __name__ == '__main__': diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/http_server_unittest.py b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_unittest.py index a037a37dd..7a14526d1 100644 --- a/Tools/Scripts/webkitpy/layout_tests/servers/http_server_unittest.py +++ b/Tools/Scripts/webkitpy/layout_tests/servers/http_server_unittest.py @@ -28,6 +28,7 @@ import unittest import re +import sys from webkitpy.common.host_mock import MockHost from webkitpy.layout_tests.port import test @@ -37,6 +38,10 @@ from webkitpy.layout_tests.servers.http_server_base import ServerError class TestHttpServer(unittest.TestCase): def test_start_cmd(self): + # Fails on win - see https://bugs.webkit.org/show_bug.cgi?id=84726 + if sys.platform in ('cygwin', 'win32'): + return + host = MockHost() test_port = test.TestPort(host) host.filesystem.write_text_file( diff --git a/Tools/Scripts/webkitpy/layout_tests/servers/websocket_server.py b/Tools/Scripts/webkitpy/layout_tests/servers/websocket_server.py index 296ab9882..93747f690 100644 --- a/Tools/Scripts/webkitpy/layout_tests/servers/websocket_server.py +++ b/Tools/Scripts/webkitpy/layout_tests/servers/websocket_server.py @@ -51,6 +51,7 @@ _DEFAULT_WSS_PORT = 9323 class PyWebSocket(http_server.Lighttpd): def __init__(self, port_obj, output_dir, port=_DEFAULT_WS_PORT, root=None, use_tls=False, + private_key=None, certificate=None, ca_certificate=None, pidfile=None): """Args: output_dir: the absolute path to the layout test result directory @@ -70,8 +71,15 @@ class PyWebSocket(http_server.Lighttpd): if self._use_tls: self._name = 'pywebsocket_secure' - self._private_key = self._pem_file - self._certificate = self._pem_file + if private_key: + self._private_key = private_key + else: + self._private_key = self._pem_file + if certificate: + self._certificate = certificate + else: + self._certificate = self._pem_file + self._ca_certificate = ca_certificate if self._port: self._port = int(self._port) self._wsin = None @@ -137,6 +145,9 @@ class PyWebSocket(http_server.Lighttpd): if self._use_tls: start_cmd.extend(['-t', '-k', self._private_key, '-c', self._certificate]) + if self._ca_certificate: + start_cmd.append('--ca-certificate') + start_cmd.append(self._ca_certificate) self._start_cmd = start_cmd server_name = self._filesystem.basename(pywebsocket_script) diff --git a/Tools/Scripts/webkitpy/layout_tests/views/metered_stream.py b/Tools/Scripts/webkitpy/layout_tests/views/metered_stream.py index 49a507ed6..814437780 100644 --- a/Tools/Scripts/webkitpy/layout_tests/views/metered_stream.py +++ b/Tools/Scripts/webkitpy/layout_tests/views/metered_stream.py @@ -91,14 +91,15 @@ class MeteredStream(object): if self._erasing: self._last_partial_line = txt[txt.rfind('\n') + 1:] - def write(self, txt, now=None): + def write(self, txt, now=None, pid=None): now = now or self._time_fn() + pid = pid or self._pid self._last_write_time = now if self._last_partial_line: self._erase_last_partial_line() if self._verbose: now_tuple = time.localtime(now) - msg = '%02d:%02d:%02d.%03d %d %s' % (now_tuple.tm_hour, now_tuple.tm_min, now_tuple.tm_sec, int((now * 1000) % 1000), self._pid, self._ensure_newline(txt)) + msg = '%02d:%02d:%02d.%03d %d %s' % (now_tuple.tm_hour, now_tuple.tm_min, now_tuple.tm_sec, int((now * 1000) % 1000), pid, self._ensure_newline(txt)) elif self._isatty: msg = txt else: @@ -106,14 +107,19 @@ class MeteredStream(object): self._stream.write(msg) - def writeln(self, txt, now=None): - self.write(self._ensure_newline(txt), now) + def writeln(self, txt, now=None, pid=None): + self.write(self._ensure_newline(txt), now, pid) def _erase_last_partial_line(self): num_chars = len(self._last_partial_line) self._stream.write(self._erasure(self._last_partial_line)) self._last_partial_line = '' + def flush(self): + if self._last_partial_line: + self._stream.write('\n') + self._last_partial_line = '' + self._stream.flush() class _LogHandler(logging.Handler): def __init__(self, meter): diff --git a/Tools/Scripts/webkitpy/layout_tests/views/printing.py b/Tools/Scripts/webkitpy/layout_tests/views/printing.py index 3df2956c1..3d98c6c59 100644 --- a/Tools/Scripts/webkitpy/layout_tests/views/printing.py +++ b/Tools/Scripts/webkitpy/layout_tests/views/printing.py @@ -407,5 +407,11 @@ class Printer(object): return self._write(msg) + def writeln(self, *args, **kwargs): + self._meter.writeln(*args, **kwargs) + def _write(self, msg): self._meter.writeln(msg) + + def flush(self): + self._meter.flush() diff --git a/Tools/Scripts/webkitpy/performance_tests/perftest.py b/Tools/Scripts/webkitpy/performance_tests/perftest.py index 20d3d5838..896bad4fa 100644 --- a/Tools/Scripts/webkitpy/performance_tests/perftest.py +++ b/Tools/Scripts/webkitpy/performance_tests/perftest.py @@ -36,10 +36,13 @@ import os import signal import socket import subprocess +import sys import time # Import for auto-install -import webkitpy.thirdparty.autoinstalled.webpagereplay.replay +if sys.platform != 'win32': + # FIXME: webpagereplay doesn't work on win32. See https://bugs.webkit.org/show_bug.cgi?id=88279. + import webkitpy.thirdparty.autoinstalled.webpagereplay.replay from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter from webkitpy.layout_tests.port.driver import DriverInput @@ -216,10 +219,10 @@ class ReplayServer(object): def __init__(self, archive, record): self._process = None - # FIXME: Should error if local proxy isn't set to forward requests to localhost:8080 and localhost:8413 + # FIXME: Should error if local proxy isn't set to forward requests to localhost:8080 and localhost:8443 replay_path = webkitpy.thirdparty.autoinstalled.webpagereplay.replay.__file__ - args = ['python', replay_path, '--no-dns_forwarding', '--port', '8080', '--ssl_port', '8413', '--use_closest_match', '--log_level', 'warning'] + args = ['python', replay_path, '--no-dns_forwarding', '--port', '8080', '--ssl_port', '8443', '--use_closest_match', '--log_level', 'warning'] if record: args.append('--record') args.append(archive) diff --git a/Tools/Scripts/webkitpy/style/checker.py b/Tools/Scripts/webkitpy/style/checker.py index e72a025f4..dff790ec1 100644 --- a/Tools/Scripts/webkitpy/style/checker.py +++ b/Tools/Scripts/webkitpy/style/checker.py @@ -140,11 +140,17 @@ _PATH_RULES_SPECIFIER = [ # Qt code uses '_' in some places (such as private slots # and on test xxx_data methos on tests) "Source/JavaScriptCore/qt/", - "Source/WebKit/qt/Api/", "Source/WebKit/qt/tests/", "Source/WebKit/qt/declarative/", "Source/WebKit/qt/examples/"], ["-readability/naming"]), + + ([# The Qt APIs use Qt declaration style, it puts the * to + # the variable name, not to the class. + "Source/WebKit/qt/Api/"], + ["-readability/naming", + "-whitespace/declaration"]), + ([# Qt's MiniBrowser has no config.h "Tools/MiniBrowser/qt"], ["-build/include"]), @@ -517,15 +523,14 @@ class CheckerDispatcher(object): # Since "LayoutTests" is in _SKIPPED_FILES_WITHOUT_WARNING, make # an exception to prevent files like "LayoutTests/ChangeLog" and # "LayoutTests/ChangeLog-2009-06-16" from being skipped. - # Files like 'test_expectations.txt' and 'drt_expectations.txt' - # are also should not be skipped. + # Files like 'TestExpectations' are also should not be skipped. # # FIXME: Figure out a good way to avoid having to add special logic # for this special case. basename = os.path.basename(file_path) if basename.startswith('ChangeLog'): return False - elif basename == 'test_expectations.txt' or basename == 'drt_expectations.txt': + elif basename == 'TestExpectations': return False for skipped_file in _SKIPPED_FILES_WITHOUT_WARNING: if self._should_skip_file_path(file_path, skipped_file): @@ -593,7 +598,7 @@ class CheckerDispatcher(object): checker = PNGChecker(file_path, handle_style_error) elif file_type == FileType.TEXT: basename = os.path.basename(file_path) - if basename == 'test_expectations.txt' or basename == 'drt_expectations.txt': + if basename == 'TestExpectations': checker = TestExpectationsChecker(file_path, handle_style_error) else: checker = TextChecker(file_path, handle_style_error) diff --git a/Tools/Scripts/webkitpy/style/checkers/cpp.py b/Tools/Scripts/webkitpy/style/checkers/cpp.py index 1eea4973f..aaad85d5c 100644 --- a/Tools/Scripts/webkitpy/style/checkers/cpp.py +++ b/Tools/Scripts/webkitpy/style/checkers/cpp.py @@ -2632,7 +2632,7 @@ def _classify_include(filename, include, is_system, include_state): """ # If it is a system header we know it is classified as _OTHER_HEADER. - if is_system: + if is_system and not include.startswith('public/'): return _OTHER_HEADER # If the include is named config.h then this is WebCore/config.h. diff --git a/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py b/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py index cba917108..1675c7010 100644 --- a/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py +++ b/Tools/Scripts/webkitpy/style/checkers/cpp_unittest.py @@ -2696,6 +2696,30 @@ class OrderOfIncludesTest(CppStyleTestBase): os.path.isfile = self.os_path_isfile_orig + def test_public_primary_header(self): + # System header is not considered a primary header. + self.assert_language_rules_check('foo.cpp', + '#include "config.h"\n' + '#include <other/foo.h>\n' + '\n' + '#include "a.h"\n', + 'Alphabetical sorting problem. [build/include_order] [4]') + + # ...except that it starts with public/. + self.assert_language_rules_check('foo.cpp', + '#include "config.h"\n' + '#include <public/foo.h>\n' + '\n' + '#include "a.h"\n', + '') + + # Even if it starts with public/ its base part must match with the source file name. + self.assert_language_rules_check('foo.cpp', + '#include "config.h"\n' + '#include <public/foop.h>\n' + '\n' + '#include "a.h"\n', + 'Alphabetical sorting problem. [build/include_order] [4]') def test_check_wtf_includes(self): self.assert_language_rules_check('foo.cpp', @@ -2751,6 +2775,19 @@ class OrderOfIncludesTest(CppStyleTestBase): classify_include('foo.cpp', 'moc_foo.cpp', False, include_state)) + # <public/foo.h> must be considered as primary even if is_system is True. + self.assertEqual(cpp_style._PRIMARY_HEADER, + classify_include('foo/foo.cpp', + 'public/foo.h', + True, include_state)) + self.assertEqual(cpp_style._OTHER_HEADER, + classify_include('foo.cpp', + 'foo.h', + True, include_state)) + self.assertEqual(cpp_style._OTHER_HEADER, + classify_include('foo.cpp', + 'public/foop.h', + True, include_state)) # Qt private APIs use _p.h suffix. self.assertEqual(cpp_style._PRIMARY_HEADER, classify_include('foo.cpp', diff --git a/Tools/Scripts/webkitpy/style/checkers/test_expectations.py b/Tools/Scripts/webkitpy/style/checkers/test_expectations.py index 616959f76..c95c4120f 100644 --- a/Tools/Scripts/webkitpy/style/checkers/test_expectations.py +++ b/Tools/Scripts/webkitpy/style/checkers/test_expectations.py @@ -29,27 +29,27 @@ """Checks WebKit style for test_expectations files.""" import logging +import optparse import os import re import sys from common import TabChecker from webkitpy.common.host import Host -from webkitpy.layout_tests.models import test_expectations -from webkitpy.layout_tests.port.base import DummyOptions +from webkitpy.layout_tests.models.test_expectations import TestExpectationParser _log = logging.getLogger(__name__) class TestExpectationsChecker(object): - """Processes test_expectations.txt lines for validating the syntax.""" + """Processes TestExpectations lines for validating the syntax.""" categories = set(['test/expectations']) def _determine_port_from_expectations_path(self, host, expectations_path): # Pass a configuration to avoid calling default_configuration() when initializing the port (takes 0.5 seconds on a Mac Pro!). - options = DummyOptions(configuration='Release') + options = optparse.Values({'configuration': 'Release'}) for port_name in host.port_factory.all_port_names(): port = host.port_factory.get(port_name, options=options) if port.path_to_test_expectations_file().replace(port.path_from_webkit_base() + host.filesystem.sep, '') == expectations_path: @@ -61,7 +61,6 @@ class TestExpectationsChecker(object): self._handle_style_error = handle_style_error self._handle_style_error.turn_off_line_filtering() self._tab_checker = TabChecker(file_path, handle_style_error) - self._output_regex = re.compile('.*test_expectations.txt:(?P<line>\d+)\s*(?P<message>.+)') # FIXME: host should be a required parameter, not an optional one. host = host or Host() @@ -77,32 +76,17 @@ class TestExpectationsChecker(object): pass def check_test_expectations(self, expectations_str, tests=None, overrides=None): - err = None - expectations = None - # FIXME: We need to rework how we lint strings so that we can do it independently of what a - # port's existing expectations are. Linting should probably just call the parser directly. - # For now we override the port hooks. This will also need to be reworked when expectations - # can cascade arbitrarily, rather than just have expectations and overrides. - orig_expectations = self._port_obj.test_expectations - orig_overrides = self._port_obj.test_expectations_overrides - try: - self._port_obj.test_expectations = lambda: expectations_str - self._port_obj.test_expectations_overrides = lambda: overrides - expectations = test_expectations.TestExpectations(self._port_obj, tests, True) - except test_expectations.ParseError, error: - err = error - finally: - self._port_obj.text_expectations = orig_expectations - self._port_obj.text_expectations_overrides = orig_overrides - - if err: - level = 5 - for warning in err.warnings: - matched = self._output_regex.match(warning) - if matched: - lineno, message = matched.group('line', 'message') - self._handle_style_error(int(lineno), 'test/expectations', level, message) - + # FIXME: we should pass in the filenames here if possible, and ensure + # that this works with with cascading expectations files and remove the overrides param. + parser = TestExpectationParser(self._port_obj, tests, False) + expectations = parser.parse('expectations', expectations_str) + if overrides: + expectations += parser.parse('overrides', overrides) + + level = 5 + for expectation_line in expectations: + for warning in expectation_line.warnings: + self._handle_style_error(expectation_line.line_number, 'test/expectations', level, warning) def check_tabs(self, lines): self._tab_checker.check(lines) diff --git a/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py b/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py index b6e3595fa..8cc91b1ac 100644 --- a/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py +++ b/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py @@ -75,16 +75,16 @@ class TestExpectationsTestCase(unittest.TestCase): def test_determine_port_from_expectations_path(self): self._expect_port_for_expectations_path(None, '/') - self._expect_port_for_expectations_path(None, 'LayoutTests/chromium-mac/test_expectations.txt') - self._expect_port_for_expectations_path('chromium', 'LayoutTests/platform/chromium/test_expectations.txt') - self._expect_port_for_expectations_path(None, '/mock-checkout/LayoutTests/platform/win/test_expectations.txt') - self._expect_port_for_expectations_path('win', 'LayoutTests/platform/win/test_expectations.txt') + self._expect_port_for_expectations_path(None, 'LayoutTests/chromium-mac/TestExpectations') + self._expect_port_for_expectations_path('chromium', 'LayoutTests/platform/chromium/TestExpectations') + self._expect_port_for_expectations_path(None, '/mock-checkout/LayoutTests/platform/win/TestExpectations') + self._expect_port_for_expectations_path('win', 'LayoutTests/platform/win/TestExpectations') def assert_lines_lint(self, lines, should_pass, expected_output=None): self._error_collector.reset_errors() host = MockHost() - checker = TestExpectationsChecker('test/test_expectations.txt', + checker = TestExpectationsChecker('test/TestExpectations', self._error_collector, host=host) # We should have failed to find a valid port object for that path. @@ -105,7 +105,7 @@ class TestExpectationsTestCase(unittest.TestCase): self.assertTrue(self._error_collector.turned_off_filtering) def test_valid_expectations(self): - self.assert_lines_lint(["BUGCR1234 MAC : passes/text.html = PASS FAIL"], should_pass=True) + self.assert_lines_lint(["BUGCR1234 MAC : passes/text.html = PASS TEXT"], should_pass=True) def test_invalid_expectations(self): self.assert_lines_lint(["BUG1234 : passes/text.html = GIVE UP"], should_pass=False) diff --git a/Tools/Scripts/webkitpy/test/main.py b/Tools/Scripts/webkitpy/test/main.py index af3123a01..c5dc39433 100644 --- a/Tools/Scripts/webkitpy/test/main.py +++ b/Tools/Scripts/webkitpy/test/main.py @@ -25,12 +25,14 @@ import logging import optparse +import os import StringIO import sys import traceback import unittest from webkitpy.common.system.filesystem import FileSystem +from webkitpy.common.system import outputcapture from webkitpy.test.test_finder import TestFinder from webkitpy.test.runner import TestRunner @@ -59,6 +61,8 @@ class Tester(object): help='verbose output (specify once for individual test results, twice for debug messages)') parser.add_option('--skip-integrationtests', action='store_true', default=False, help='do not run the integration tests') + parser.add_option('-p', '--pass-through', action='store_true', default=False, + help='be debugger friendly by passing captured output through to the system') parser.epilog = ('[args...] is an optional list of modules, test_classes, or individual tests. ' 'If no args are given, all the tests will be run.') @@ -135,6 +139,10 @@ class Tester(object): self.finder.clean_trees() names = self.finder.find_names(args, self._options.skip_integrationtests, self._options.all) + if not names: + _log.error('No tests to run') + return False + return self._run_tests(names) def _run_tests(self, names): @@ -172,6 +180,8 @@ class Tester(object): test_runner = TestRunner(self.stream, self._options, loader) _log.debug("Running the tests.") + if self._options.pass_through: + outputcapture.OutputCapture.stream_wrapper = _CaptureAndPassThroughStream result = test_runner.run(test_suite) if self._options.coverage: cov.stop() @@ -184,3 +194,32 @@ class Tester(object): traceback.print_exc(file=s) for l in s.buflist: _log.error(' ' + l.rstrip()) + + +class _CaptureAndPassThroughStream(object): + def __init__(self, stream): + self._buffer = StringIO.StringIO() + self._stream = stream + + def write(self, msg): + self._stream.write(msg) + + # Note that we don't want to capture any output generated by the debugger + # because that could cause the results of capture_output() to be invalid. + if not self._message_is_from_pdb(): + self._buffer.write(msg) + + def _message_is_from_pdb(self): + # We will assume that if the pdb module is in the stack then the output + # is being generated by the python debugger (or the user calling something + # from inside the debugger). + import inspect + import pdb + stack = inspect.stack() + return any(frame[1] == pdb.__file__.replace('.pyc', '.py') for frame in stack) + + def flush(self): + self._stream.flush() + + def getvalue(self): + return self._buffer.getvalue() diff --git a/Tools/Scripts/webkitpy/test/main_unittest.py b/Tools/Scripts/webkitpy/test/main_unittest.py new file mode 100644 index 000000000..1a60beef3 --- /dev/null +++ b/Tools/Scripts/webkitpy/test/main_unittest.py @@ -0,0 +1,54 @@ +# Copyright (C) 2012 Google, Inc. +# +# 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 +import unittest +import StringIO + +from webkitpy.common.system.outputcapture import OutputCapture +from webkitpy.test.main import Tester + + +class TesterTest(unittest.TestCase): + + def test_no_tests_found(self): + tester = Tester() + errors = StringIO.StringIO() + + # Here we need to remove any existing log handlers so that they + # don't log the messages webkitpy.test while we're testing it. + root_logger = logging.getLogger() + root_handlers = root_logger.handlers + root_logger.handlers = [] + + tester.stream = errors + tester.finder.find_names = lambda args, skip_integration, run_all: [] + oc = OutputCapture() + try: + oc.capture_output() + self.assertFalse(tester.run()) + finally: + out, err, logs = oc.restore_output() + root_logger.handlers = root_handlers + + self.assertTrue('No tests to run' in errors.getvalue()) + self.assertTrue('No tests to run' in logs) diff --git a/Tools/Scripts/webkitpy/test/test_finder.py b/Tools/Scripts/webkitpy/test/test_finder.py index b2671e917..3a90197e9 100644 --- a/Tools/Scripts/webkitpy/test/test_finder.py +++ b/Tools/Scripts/webkitpy/test/test_finder.py @@ -166,9 +166,6 @@ class TestFinder(object): 'webkitpy.tool') self._exclude(modules, win32_blacklist, 'fail horribly on win32', 54526) - win32_blacklist_84726 = ('webkitpy.layout_tests.servers.http_server_unittest.TestHttpServer.test_start_cmd') - self._exclude(modules, win32_blacklist_84726, 'fails on win32', 84726) - return modules def _exclude(self, modules, module_prefixes, reason, bugid): diff --git a/Tools/Scripts/webkitpy/thirdparty/__init__.py b/Tools/Scripts/webkitpy/thirdparty/__init__.py index 26245bacc..078e18041 100644 --- a/Tools/Scripts/webkitpy/thirdparty/__init__.py +++ b/Tools/Scripts/webkitpy/thirdparty/__init__.py @@ -106,7 +106,12 @@ class AutoinstallImportHook(object): installer.install(url="http://pypi.python.org/packages/source/J/Jinja2/Jinja2-2.6.tar.gz#md5=1c49a8825c993bfdcf55bb36897d28a2", url_subpath="Jinja2-2.6/jinja2") - self._install("http://pypi.python.org/packages/source/b/buildbot/buildbot-0.8.4p2.tar.gz#md5=7597d945724c80c0ab476e833a1026cb", "buildbot-0.8.4p2/buildbot") + SQLAlchemy_dir = self._fs.join(_AUTOINSTALLED_DIR, "sqlalchemy") + installer = AutoInstaller(append_to_search_path=True, target_dir=SQLAlchemy_dir) + installer.install(url="http://pypi.python.org/packages/source/S/SQLAlchemy/SQLAlchemy-0.7.7.tar.gz#md5=ddf6df7e014cea318fa981364f3f93b9", + url_subpath="SQLAlchemy-0.7.7/lib/sqlalchemy") + + self._install("http://pypi.python.org/packages/source/b/buildbot/buildbot-0.8.6p1.tar.gz#md5=b6727d2810c692062c657492bcbeac6a", "buildbot-0.8.6p1/buildbot") def _install_coverage(self): installer = AutoInstaller(target_dir=_AUTOINSTALLED_DIR) @@ -130,8 +135,8 @@ class AutoinstallImportHook(object): def _install_webpagereplay(self): if not self._fs.exists(self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay")): - self._install("http://web-page-replay.googlecode.com/files/webpagereplay-1.1.1.tar.gz", "webpagereplay-1.1.1") - self._fs.move(self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay-1.1.1"), self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay")) + self._install("http://web-page-replay.googlecode.com/files/webpagereplay-1.1.2.tar.gz", "webpagereplay-1.1.2") + self._fs.move(self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay-1.1.2"), self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay")) init_path = self._fs.join(_AUTOINSTALLED_DIR, "webpagereplay", "__init__.py") if not self._fs.exists(init_path): diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/COPYING b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/COPYING index ab9d52ddf..989d02e4c 100644 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/COPYING +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/COPYING @@ -1,4 +1,4 @@ -Copyright 2009, Google Inc. +Copyright 2012, Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hybi.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hybi.py index c00f5692b..34fa7a60e 100644 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hybi.py +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/_stream_hybi.py @@ -39,6 +39,7 @@ http://tools.ietf.org/html/rfc6455 from collections import deque import os import struct +import time from mod_pywebsocket import common from mod_pywebsocket import util @@ -208,7 +209,17 @@ class FragmentedFrameBuilder(object): def _create_control_frame(opcode, body, mask, frame_filters): frame = Frame(opcode=opcode, payload=body) - return _filter_and_format_frame_object(frame, mask, frame_filters) + for frame_filter in frame_filters: + frame_filter.filter(frame) + + if len(frame.payload) > 125: + raise BadOperationException( + 'Payload data size of control frames must be 125 bytes or less') + + header = create_header( + frame.opcode, len(frame.payload), frame.fin, + frame.rsv1, frame.rsv2, frame.rsv3, mask) + return _build_frame(header, frame.payload, mask) def create_ping_frame(body, mask=False, frame_filters=[]): @@ -286,6 +297,9 @@ class Stream(StreamBase): InvalidFrameException: when the frame contains invalid data. """ + self._logger.log(common.LOGLEVEL_FINE, + 'Receive the first 2 octets of a frame') + received = self.receive_bytes(2) first_byte = ord(received[0]) @@ -299,6 +313,11 @@ class Stream(StreamBase): mask = (second_byte >> 7) & 1 payload_length = second_byte & 0x7f + self._logger.log(common.LOGLEVEL_FINE, + 'FIN=%s, RSV1=%s, RSV2=%s, RSV3=%s, opcode=%s, ' + 'Mask=%s, Payload_length=%s', + fin, rsv1, rsv2, rsv3, opcode, mask, payload_length) + if (mask == 1) != self._options.unmask_receive: raise InvalidFrameException( 'Mask bit on the received frame did\'nt match masking ' @@ -310,6 +329,9 @@ class Stream(StreamBase): valid_length_encoding = True length_encoding_bytes = 1 if payload_length == 127: + self._logger.log(common.LOGLEVEL_FINE, + 'Receive 8-octet extended payload length') + extended_payload_length = self.receive_bytes(8) payload_length = struct.unpack( '!Q', extended_payload_length)[0] @@ -319,7 +341,13 @@ class Stream(StreamBase): if self._request.ws_version >= 13 and payload_length < 0x10000: valid_length_encoding = False length_encoding_bytes = 8 + + self._logger.log(common.LOGLEVEL_FINE, + 'Decoded_payload_length=%s', payload_length) elif payload_length == 126: + self._logger.log(common.LOGLEVEL_FINE, + 'Receive 2-octet extended payload length') + extended_payload_length = self.receive_bytes(2) payload_length = struct.unpack( '!H', extended_payload_length)[0] @@ -327,6 +355,9 @@ class Stream(StreamBase): valid_length_encoding = False length_encoding_bytes = 2 + self._logger.log(common.LOGLEVEL_FINE, + 'Decoded_payload_length=%s', payload_length) + if not valid_length_encoding: self._logger.warning( 'Payload length is not encoded using the minimal number of ' @@ -335,12 +366,38 @@ class Stream(StreamBase): length_encoding_bytes) if mask == 1: + self._logger.log(common.LOGLEVEL_FINE, 'Receive mask') + masking_nonce = self.receive_bytes(4) masker = util.RepeatedXorMasker(masking_nonce) + + self._logger.log(common.LOGLEVEL_FINE, 'Mask=%r', masking_nonce) else: masker = _NOOP_MASKER - bytes = masker.mask(self.receive_bytes(payload_length)) + self._logger.log(common.LOGLEVEL_FINE, 'Receive payload data') + if self._logger.isEnabledFor(common.LOGLEVEL_FINE): + receive_start = time.time() + + raw_payload_bytes = self.receive_bytes(payload_length) + + if self._logger.isEnabledFor(common.LOGLEVEL_FINE): + self._logger.log( + common.LOGLEVEL_FINE, + 'Done receiving payload data at %s MB/s', + payload_length / (time.time() - receive_start) / 1000 / 1000) + self._logger.log(common.LOGLEVEL_FINE, 'Unmask payload data') + + if self._logger.isEnabledFor(common.LOGLEVEL_FINE): + unmask_start = time.time() + + bytes = masker.mask(raw_payload_bytes) + + if self._logger.isEnabledFor(common.LOGLEVEL_FINE): + self._logger.log( + common.LOGLEVEL_FINE, + 'Done unmasking payload data at %s MB/s', + payload_length / (time.time() - unmask_start) / 1000 / 1000) return opcode, bytes, fin, rsv1, rsv2, rsv3 @@ -359,8 +416,8 @@ class Stream(StreamBase): Raises: BadOperationException: when called on a server-terminated - connection or called with inconsistent message type or binary - parameter. + connection or called with inconsistent message type or + binary parameter. """ if self._request.server_terminated: @@ -408,6 +465,15 @@ class Stream(StreamBase): frame = self._receive_frame_as_frame_object() + # Check the constraint on the payload size for control frames + # before extension processes the frame. + # See also http://tools.ietf.org/html/rfc6455#section-5.5 + if (common.is_control_opcode(frame.opcode) and + len(frame.payload) > 125): + raise InvalidFrameException( + 'Payload data size of control frames must be 125 bytes or ' + 'less') + for frame_filter in self._options.incoming_frame_filters: frame_filter.filter(frame) @@ -450,12 +516,6 @@ class Stream(StreamBase): if frame.fin: # Unfragmented frame - if (common.is_control_opcode(frame.opcode) and - len(frame.payload) > 125): - raise InvalidFrameException( - 'Application data size of control frames must be ' - '125 bytes or less') - self._original_opcode = frame.opcode message = frame.payload else: @@ -488,7 +548,11 @@ class Stream(StreamBase): # - no application data: no code no reason # - 2 octet of application data: has code but no reason # - 3 or more octet of application data: both code and reason - if len(message) == 1: + if len(message) == 0: + self._logger.debug('Received close frame (empty body)') + self._request.ws_close_code = ( + common.STATUS_NO_STATUS_RECEIVED) + elif len(message) == 1: raise InvalidFrameException( 'If a close frame has status code, the length of ' 'status code must be 2 octet') @@ -507,8 +571,7 @@ class Stream(StreamBase): if self._request.server_terminated: self._logger.debug( - 'Received ack for server-initiated closing ' - 'handshake') + 'Received ack for server-initiated closing handshake') return None self._logger.debug( @@ -520,9 +583,16 @@ class Stream(StreamBase): dispatcher = self._request._dispatcher code, reason = dispatcher.passive_closing_handshake( self._request) + if code is None and reason is not None and len(reason) > 0: + self._logger.warning( + 'Handler specified reason despite code being None') + reason = '' + if reason is None: + reason = '' self._send_closing_handshake(code, reason) self._logger.debug( - 'Sent ack for client-initiated closing handshake') + 'Sent ack for client-initiated closing handshake ' + '(code=%r, reason=%r)', code, reason) return None elif self._original_opcode == common.OPCODE_PING: try: @@ -571,17 +641,21 @@ class Stream(StreamBase): 'Opcode %d is not supported' % self._original_opcode) def _send_closing_handshake(self, code, reason): - if code >= (1 << 16) or code < 0: - raise BadOperationException('Status code is out of range') - - encoded_reason = reason.encode('utf-8') - if len(encoded_reason) + 2 > 125: - raise BadOperationException( - 'Application data size of close frames must be 125 bytes or ' - 'less') + body = '' + if code is not None: + if (code > common.STATUS_USER_PRIVATE_MAX or + code < common.STATUS_NORMAL_CLOSURE): + raise BadOperationException('Status code is out of range') + if (code == common.STATUS_NO_STATUS_RECEIVED or + code == common.STATUS_ABNORMAL_CLOSURE or + code == common.STATUS_TLS_HANDSHAKE): + raise BadOperationException('Status code is reserved pseudo ' + 'code') + encoded_reason = reason.encode('utf-8') + body = struct.pack('!H', code) + encoded_reason frame = create_close_frame( - struct.pack('!H', code) + encoded_reason, + body, self._options.mask_send, self._options.outgoing_frame_filters) @@ -590,15 +664,36 @@ class Stream(StreamBase): self._write(frame) def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''): - """Closes a WebSocket connection.""" + """Closes a WebSocket connection. + + Args: + code: Status code for close frame. If code is None, a close + frame with empty body will be sent. + reason: string representing close reason. + Raises: + BadOperationException: when reason is specified with code None + or reason is not an instance of both str and unicode. + """ if self._request.server_terminated: self._logger.debug( 'Requested close_connection but server is already terminated') return + if code is None: + if reason is not None and len(reason) > 0: + raise BadOperationException( + 'close reason must not be specified if code is None') + reason = '' + else: + if not isinstance(reason, str) and not isinstance(reason, unicode): + raise BadOperationException( + 'close reason must be an instance of str or unicode') + self._send_closing_handshake(code, reason) - self._logger.debug('Sent server-initiated closing handshake') + self._logger.debug( + 'Sent server-initiated closing handshake (code=%r, reason=%r)', + code, reason) if (code == common.STATUS_GOING_AWAY or code == common.STATUS_PROTOCOL_ERROR): @@ -621,10 +716,6 @@ class Stream(StreamBase): # note: mod_python Connection (mp_conn) doesn't have close method. def send_ping(self, body=''): - if len(body) > 125: - raise ValueError( - 'Application data size of control frames must be 125 bytes or ' - 'less') frame = create_ping_frame( body, self._options.mask_send, @@ -634,10 +725,6 @@ class Stream(StreamBase): self._ping_queue.append(body) def _send_pong(self, body): - if len(body) > 125: - raise ValueError( - 'Application data size of control frames must be 125 bytes or ' - 'less') frame = create_pong_frame( body, self._options.mask_send, diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/common.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/common.py index ba670bb42..710967c80 100644 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/common.py +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/common.py @@ -28,6 +28,16 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""This file must not depend on any module specific to the WebSocket protocol. +""" + + +from mod_pywebsocket import http_header_util + + +# Additional log level definitions. +LOGLEVEL_FINE = 9 + # Constants indicating WebSocket protocol version. VERSION_HIXIE75 = -1 VERSION_HYBI00 = 0 @@ -93,6 +103,7 @@ SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location' # Extensions DEFLATE_STREAM_EXTENSION = 'deflate-stream' DEFLATE_FRAME_EXTENSION = 'deflate-frame' +PERFRAME_COMPRESSION_EXTENSION = 'perframe-compress' X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame' # Status codes @@ -176,4 +187,118 @@ class ExtensionParameter(object): return param_value +class ExtensionParsingException(Exception): + def __init__(self, name): + super(ExtensionParsingException, self).__init__(name) + + +def _parse_extension_param(state, definition, allow_quoted_string): + param_name = http_header_util.consume_token(state) + + if param_name is None: + raise ExtensionParsingException('No valid parameter name found') + + http_header_util.consume_lwses(state) + + if not http_header_util.consume_string(state, '='): + definition.add_parameter(param_name, None) + return + + http_header_util.consume_lwses(state) + + if allow_quoted_string: + # TODO(toyoshim): Add code to validate that parsed param_value is token + param_value = http_header_util.consume_token_or_quoted_string(state) + else: + param_value = http_header_util.consume_token(state) + if param_value is None: + raise ExtensionParsingException( + 'No valid parameter value found on the right-hand side of ' + 'parameter %r' % param_name) + + definition.add_parameter(param_name, param_value) + + +def _parse_extension(state, allow_quoted_string): + extension_token = http_header_util.consume_token(state) + if extension_token is None: + return None + + extension = ExtensionParameter(extension_token) + + while True: + http_header_util.consume_lwses(state) + + if not http_header_util.consume_string(state, ';'): + break + + http_header_util.consume_lwses(state) + + try: + _parse_extension_param(state, extension, allow_quoted_string) + except ExtensionParsingException, e: + raise ExtensionParsingException( + 'Failed to parse parameter for %r (%r)' % + (extension_token, e)) + + return extension + + +def parse_extensions(data, allow_quoted_string=False): + """Parses Sec-WebSocket-Extensions header value returns a list of + ExtensionParameter objects. + + Leading LWSes must be trimmed. + """ + + state = http_header_util.ParsingState(data) + + extension_list = [] + while True: + extension = _parse_extension(state, allow_quoted_string) + if extension is not None: + extension_list.append(extension) + + http_header_util.consume_lwses(state) + + if http_header_util.peek(state) is None: + break + + if not http_header_util.consume_string(state, ','): + raise ExtensionParsingException( + 'Failed to parse Sec-WebSocket-Extensions header: ' + 'Expected a comma but found %r' % + http_header_util.peek(state)) + + http_header_util.consume_lwses(state) + + if len(extension_list) == 0: + raise ExtensionParsingException( + 'No valid extension entry found') + + return extension_list + + +def format_extension(extension): + """Formats an ExtensionParameter object.""" + + formatted_params = [extension.name()] + for param_name, param_value in extension.get_parameters(): + if param_value is None: + formatted_params.append(param_name) + else: + quoted_value = http_header_util.quote_if_necessary(param_value) + formatted_params.append('%s=%s' % (param_name, quoted_value)) + return '; '.join(formatted_params) + + +def format_extensions(extension_list): + """Formats a list of ExtensionParameter objects.""" + + formatted_extension_list = [] + for extension in extension_list: + formatted_extension_list.append(format_extension(extension)) + return ', '.join(formatted_extension_list) + + # vi:sts=4 sw=4 et diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/extensions.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/extensions.py index ce3784658..52b7a4a19 100644 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/extensions.py +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/extensions.py @@ -1,4 +1,4 @@ -# Copyright 2011, Google Inc. +# Copyright 2012, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -30,6 +30,7 @@ from mod_pywebsocket import common from mod_pywebsocket import util +from mod_pywebsocket.http_header_util import quote_if_necessary _available_processors = {} @@ -254,6 +255,96 @@ _available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = ( DeflateFrameExtensionProcessor) +def _parse_compression_method(data): + """Parses the value of "method" extension parameter.""" + + return common.parse_extensions(data, allow_quoted_string=True) + + +def _create_accepted_method_desc(method_name, method_params): + """Creates accepted-method-desc from given method name and parameters""" + + extension = common.ExtensionParameter(method_name) + for name, value in method_params: + extension.add_parameter(name, value) + return common.format_extension(extension) + + +class PerFrameCompressionExtensionProcessor(ExtensionProcessorInterface): + """WebSocket Per-frame compression extension processor.""" + + _METHOD_PARAM = 'method' + _DEFLATE_METHOD = 'deflate' + + def __init__(self, request): + self._logger = util.get_class_logger(self) + self._request = request + self._compression_method_name = None + self._compression_processor = None + + def _lookup_compression_processor(self, method_desc): + if method_desc.name() == self._DEFLATE_METHOD: + return DeflateFrameExtensionProcessor(method_desc) + return None + + def _get_compression_processor_response(self): + """Looks up the compression processor based on the self._request and + returns the compression processor's response. + """ + + method_list = self._request.get_parameter_value(self._METHOD_PARAM) + if method_list is None: + return None + methods = _parse_compression_method(method_list) + if methods is None: + return None + comression_processor = None + # The current implementation tries only the first method that matches + # supported algorithm. Following methods aren't tried even if the + # first one is rejected. + # TODO(bashi): Need to clarify this behavior. + for method_desc in methods: + compression_processor = self._lookup_compression_processor( + method_desc) + if compression_processor is not None: + self._compression_method_name = method_desc.name() + break + if compression_processor is None: + return None + processor_response = compression_processor.get_extension_response() + if processor_response is None: + return None + self._compression_processor = compression_processor + return processor_response + + def get_extension_response(self): + processor_response = self._get_compression_processor_response() + if processor_response is None: + return None + + response = common.ExtensionParameter(self._request.name()) + accepted_method_desc = _create_accepted_method_desc( + self._compression_method_name, + processor_response.get_parameters()) + response.add_parameter(self._METHOD_PARAM, accepted_method_desc) + self._logger.debug( + 'Enable %s extension (method: %s)' % + (self._request.name(), self._compression_method_name)) + return response + + def setup_stream_options(self, stream_options): + if self._compression_processor is None: + return + self._compression_processor.setup_stream_options(stream_options) + + def get_compression_processor(self): + return self._compression_processor + + +_available_processors[common.PERFRAME_COMPRESSION_EXTENSION] = ( + PerFrameCompressionExtensionProcessor) + + def get_extension_processor(extension_request): global _available_processors processor_class = _available_processors.get(extension_request.name()) diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/_base.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/_base.py index 4d7c32e35..bc095b129 100644 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/_base.py +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/_base.py @@ -1,4 +1,4 @@ -# Copyright 2011, Google Inc. +# Copyright 2012, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -216,108 +216,4 @@ def parse_token_list(data): return token_list -def _parse_extension_param(state, definition, allow_quoted_string): - param_name = http_header_util.consume_token(state) - - if param_name is None: - raise HandshakeException('No valid parameter name found') - - http_header_util.consume_lwses(state) - - if not http_header_util.consume_string(state, '='): - definition.add_parameter(param_name, None) - return - - http_header_util.consume_lwses(state) - - if allow_quoted_string: - # TODO(toyoshim): Add code to validate that parsed param_value is token - param_value = http_header_util.consume_token_or_quoted_string(state) - else: - param_value = http_header_util.consume_token(state) - if param_value is None: - raise HandshakeException( - 'No valid parameter value found on the right-hand side of ' - 'parameter %r' % param_name) - - definition.add_parameter(param_name, param_value) - - -def _parse_extension(state, allow_quoted_string): - extension_token = http_header_util.consume_token(state) - if extension_token is None: - return None - - extension = common.ExtensionParameter(extension_token) - - while True: - http_header_util.consume_lwses(state) - - if not http_header_util.consume_string(state, ';'): - break - - http_header_util.consume_lwses(state) - - try: - _parse_extension_param(state, extension, allow_quoted_string) - except HandshakeException, e: - raise HandshakeException( - 'Failed to parse Sec-WebSocket-Extensions header: ' - 'Failed to parse parameter for %r (%r)' % - (extension_token, e)) - - return extension - - -def parse_extensions(data, allow_quoted_string=False): - """Parses Sec-WebSocket-Extensions header value returns a list of - common.ExtensionParameter objects. - - Leading LWSes must be trimmed. - """ - - state = http_header_util.ParsingState(data) - - extension_list = [] - while True: - extension = _parse_extension(state, allow_quoted_string) - if extension is not None: - extension_list.append(extension) - - http_header_util.consume_lwses(state) - - if http_header_util.peek(state) is None: - break - - if not http_header_util.consume_string(state, ','): - raise HandshakeException( - 'Failed to parse Sec-WebSocket-Extensions header: ' - 'Expected a comma but found %r' % - http_header_util.peek(state)) - - http_header_util.consume_lwses(state) - - if len(extension_list) == 0: - raise HandshakeException( - 'Sec-WebSocket-Extensions header contains no valid extension') - - return extension_list - - -def format_extensions(extension_list): - formatted_extension_list = [] - for extension in extension_list: - formatted_params = [extension.name()] - for param_name, param_value in extension.get_parameters(): - if param_value is None: - formatted_params.append(param_name) - else: - quoted_value = http_header_util.quote_if_necessary(param_value) - formatted_params.append('%s=%s' % (param_name, quoted_value)) - - formatted_extension_list.append('; '.join(formatted_params)) - - return ', '.join(formatted_extension_list) - - # vi:sts=4 sw=4 et diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi.py index 3bc84bd26..2883acbf8 100644 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi.py +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/handshake/hybi.py @@ -1,4 +1,4 @@ -# Copyright 2011, Google Inc. +# Copyright 2012, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -50,11 +50,9 @@ import re from mod_pywebsocket import common from mod_pywebsocket.extensions import get_extension_processor from mod_pywebsocket.handshake._base import check_request_line -from mod_pywebsocket.handshake._base import format_extensions from mod_pywebsocket.handshake._base import format_header from mod_pywebsocket.handshake._base import get_mandatory_header from mod_pywebsocket.handshake._base import HandshakeException -from mod_pywebsocket.handshake._base import parse_extensions from mod_pywebsocket.handshake._base import parse_token_list from mod_pywebsocket.handshake._base import validate_mandatory_header from mod_pywebsocket.handshake._base import validate_subprotocol @@ -290,8 +288,12 @@ class Handshaker(object): allow_quoted_string=False else: allow_quoted_string=True - self._request.ws_requested_extensions = parse_extensions( - extensions_header, allow_quoted_string=allow_quoted_string) + try: + self._request.ws_requested_extensions = common.parse_extensions( + extensions_header, allow_quoted_string=allow_quoted_string) + except common.ExtensionParsingException, e: + raise HandshakeException( + 'Failed to parse Sec-WebSocket-Extensions header: %r' % e) self._logger.debug( 'Extensions requested: %r', @@ -358,7 +360,7 @@ class Handshaker(object): len(self._request.ws_extensions) != 0): response.append(format_header( common.SEC_WEBSOCKET_EXTENSIONS_HEADER, - format_extensions(self._request.ws_extensions))) + common.format_extensions(self._request.ws_extensions))) response.append('\r\n') raw_response = ''.join(response) diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/standalone.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/standalone.py index dc143ea00..850aa5cd4 100755 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/standalone.py +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/standalone.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2011, Google Inc. +# Copyright 2012, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -55,6 +55,20 @@ handlers. If this path is relative, <document_root> is used as the base. handlers under scan_dir are scanned. This is useful in saving scan time. +SUPPORTING TLS + +To support TLS, run standalone.py with -t, -k, and -c options. + + +SUPPORTING CLIENT AUTHENTICATION + +To support client authentication with TLS, run standalone.py with -t, -k, -c, +and --ca-certificate options. + +E.g., $./standalone.py -d ../example -p 10443 -t -c ../test/cert/cert.pem -k +../test/cert/key.pem --ca-certificate=../test/cert/cacert.pem + + CONFIGURATION FILE You can also write a configuration file and use it by specifying the path to @@ -311,10 +325,16 @@ class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): continue if self.websocket_server_options.use_tls: if _HAS_SSL: + if self.websocket_server_options.ca_certificate: + client_cert_ = ssl.CERT_REQUIRED + else: + client_cert_ = ssl.CERT_NONE socket_ = ssl.wrap_socket(socket_, keyfile=self.websocket_server_options.private_key, certfile=self.websocket_server_options.certificate, - ssl_version=ssl.PROTOCOL_SSLv23) + ssl_version=ssl.PROTOCOL_SSLv23, + ca_certs=self.websocket_server_options.ca_certificate, + cert_reqs=client_cert_) if _HAS_OPEN_SSL: ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) ctx.use_privatekey_file( @@ -601,7 +621,13 @@ class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): return False +def _get_logger_from_class(c): + return logging.getLogger('%s.%s' % (c.__module__, c.__name__)) + + def _configure_logging(options): + logging.addLevelName(common.LOGLEVEL_FINE, 'FINE') + logger = logging.getLogger() logger.setLevel(logging.getLevelName(options.log_level.upper())) if options.log_file: @@ -614,6 +640,13 @@ def _configure_logging(options): handler.setFormatter(formatter) logger.addHandler(handler) + deflate_log_level_name = logging.getLevelName( + options.deflate_log_level.upper()) + _get_logger_from_class(util._Deflater).setLevel( + deflate_log_level_name) + _get_logger_from_class(util._Inflater).setLevel( + deflate_log_level_name) + def _alias_handlers(dispatcher, websock_handlers_map_file): """Set aliases specified in websock_handler_map_file in dispatcher. @@ -702,13 +735,25 @@ def _build_option_parser(): default='', help='TLS private key file.') parser.add_option('-c', '--certificate', dest='certificate', default='', help='TLS certificate file.') + parser.add_option('--ca-certificate', dest='ca_certificate', default='', + help=('TLS CA certificate file for client ' + 'authentication.')) parser.add_option('-l', '--log-file', '--log_file', dest='log_file', default='', help='Log file.') + # Custom log level: + # - FINE: Prints status of each frame processing step parser.add_option('--log-level', '--log_level', type='choice', dest='log_level', default='warn', - choices=['debug', 'info', 'warning', 'warn', 'error', + choices=['fine', + 'debug', 'info', 'warning', 'warn', 'error', 'critical'], help='Log level.') + parser.add_option('--deflate-log-level', '--deflate_log_level', + type='choice', + dest='deflate_log_level', default='warn', + choices=['debug', 'info', 'warning', 'warn', 'error', + 'critical'], + help='Log level for _Deflater and _Inflater.') parser.add_option('--thread-monitor-interval-in-sec', '--thread_monitor_interval_in_sec', dest='thread_monitor_interval_in_sec', @@ -825,13 +870,20 @@ def _main(args=None): if options.use_tls: if not (_HAS_SSL or _HAS_OPEN_SSL): - logging.critical('TLS support requires ssl or pyOpenSSL.') + logging.critical('TLS support requires ssl or pyOpenSSL module.') sys.exit(1) if not options.private_key or not options.certificate: logging.critical( 'To use TLS, specify private_key and certificate.') sys.exit(1) + if options.ca_certificate: + if not options.use_tls: + logging.critical('TLS must be enabled for client authentication.') + sys.exit(1) + if not _HAS_SSL: + logging.critical('Client authentication requires ssl module.') + if not options.scan_dir: options.scan_dir = options.websock_handlers diff --git a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/util.py b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/util.py index 9a0ab5de6..6146e052f 100644 --- a/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/util.py +++ b/Tools/Scripts/webkitpy/thirdparty/mod_pywebsocket/util.py @@ -177,9 +177,16 @@ class RepeatedXorMasker(object): def mask(self, s): result = array.array('B') result.fromstring(s) + # Use temporary local variables to eliminate the cost to access + # attributes + count = self._count + mask = self._mask + mask_size = self._mask_size for i in xrange(len(result)): - result[i] ^= self._mask[self._count] - self._count = (self._count + 1) % self._mask_size + result[i] ^= mask[count] + count = (count + 1) % mask_size + self._count = count + return result.tostring() diff --git a/Tools/Scripts/webkitpy/thirdparty/ordered_dict.py b/Tools/Scripts/webkitpy/thirdparty/ordered_dict.py new file mode 100644 index 000000000..3dc735a4a --- /dev/null +++ b/Tools/Scripts/webkitpy/thirdparty/ordered_dict.py @@ -0,0 +1,89 @@ +# Copyright (c) 2009 Raymond Hettinger. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# This code is obtained from http://code.activestate.com/recipes/576669/ + +from collections import MutableMapping + +class OrderedDict(dict, MutableMapping): + + # Methods with direct access to underlying attributes + + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at 1 argument, got %d', len(args)) + if not hasattr(self, '_keys'): + self._keys = [] + self.update(*args, **kwds) + + def clear(self): + del self._keys[:] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + self._keys.append(key) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + self._keys.remove(key) + + def __iter__(self): + return iter(self._keys) + + def __reversed__(self): + return reversed(self._keys) + + def popitem(self): + if not self: + raise KeyError + key = self._keys.pop() + value = dict.pop(self, key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + inst_dict.pop('_keys', None) + return (self.__class__, (items,), inst_dict) + + # Methods with indirect access via the above methods + + setdefault = MutableMapping.setdefault + update = MutableMapping.update + pop = MutableMapping.pop + keys = MutableMapping.keys + values = MutableMapping.values + items = MutableMapping.items + + def __repr__(self): + pairs = ', '.join(map('%r: %r'.__mod__, self.items())) + return '%s({%s})' % (self.__class__.__name__, pairs) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d diff --git a/Tools/Scripts/webkitpy/tool/commands/queries.py b/Tools/Scripts/webkitpy/tool/commands/queries.py index ab9db4aca..f69971e59 100644 --- a/Tools/Scripts/webkitpy/tool/commands/queries.py +++ b/Tools/Scripts/webkitpy/tool/commands/queries.py @@ -395,7 +395,7 @@ class PrintExpectations(AbstractDeclarativeCommand): make_option('--csv', action='store_true', default=False, help='Print a CSV-style report that includes the port name, modifiers, tests, and expectations'), make_option('-f', '--full', action='store_true', default=False, - help='Print a full test_expectations.txt-style line for every match'), + help='Print a full TestExpectations-style line for every match'), ] + port_options(platform='port/platform to use. Use glob-style wildcards for multiple ports (implies --csv)') AbstractDeclarativeCommand.__init__(self, options=options) diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py index 5a184f19e..bd890cc8e 100644 --- a/Tools/Scripts/webkitpy/tool/commands/rebaseline.py +++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline.py @@ -43,16 +43,12 @@ from webkitpy.common.system.user import User from webkitpy.layout_tests.controllers.test_result_writer import TestResultWriter from webkitpy.layout_tests.models import test_failures from webkitpy.layout_tests.models.test_configuration import TestConfiguration -from webkitpy.layout_tests.models.test_expectations import TestExpectations +from webkitpy.layout_tests.models.test_expectations import TestExpectations, suffixes_for_expectations, BASELINE_SUFFIX_LIST from webkitpy.layout_tests.port import builders from webkitpy.tool.grammar import pluralize from webkitpy.tool.multicommandtool import AbstractDeclarativeCommand -# FIXME: Pull this from Port.baseline_extensions(). -_baseline_suffix_list = ['png', 'wav', 'txt'] - - _log = logging.getLogger(__name__) # FIXME: Should TestResultWriter know how to compute this string? @@ -64,10 +60,10 @@ class AbstractRebaseliningCommand(AbstractDeclarativeCommand): def __init__(self, options=None): options = options or [] options.extend([ - optparse.make_option('--suffixes', default=','.join(_baseline_suffix_list), action='store', + optparse.make_option('--suffixes', default=','.join(BASELINE_SUFFIX_LIST), action='store', help='file types to rebaseline')]) AbstractDeclarativeCommand.__init__(self, options=options) - self._baseline_suffix_list = _baseline_suffix_list + self._baseline_suffix_list = BASELINE_SUFFIX_LIST class RebaselineTest(AbstractRebaseliningCommand): @@ -140,7 +136,7 @@ class RebaselineTest(AbstractRebaseliningCommand): def _update_expectations_file(self, builder_name, test_name): port = self._tool.port_factory.get_from_builder_name(builder_name) - expectations = TestExpectations(port) + expectations = TestExpectations(port, include_overrides=False) for test_configuration in port.all_test_configurations(): if test_configuration.version == port.test_configuration().version: @@ -239,7 +235,7 @@ class AnalyzeBaselines(AbstractRebaseliningCommand): class RebaselineExpectations(AbstractDeclarativeCommand): name = "rebaseline-expectations" - help_text = "Rebaselines the tests indicated in test_expectations.txt." + help_text = "Rebaselines the tests indicated in TestExpectations." def __init__(self): options = [ @@ -260,19 +256,25 @@ class RebaselineExpectations(AbstractDeclarativeCommand): # FIXME: Support non-Chromium ports. return port_name.startswith('chromium-') - def _expectations(self, port): - return TestExpectations(port) - def _update_expectations_file(self, port_name): if not self._is_supported_port(port_name): return port = self._tool.port_factory.get(port_name) - expectations = self._expectations(port) + + # FIXME: This will intentionally skip over any REBASELINE expectations that were in an overrides file. + # This is not good, but avoids having the overrides getting written into the main file. + # See https://bugs.webkit.org/show_bug.cgi?id=88456 for context. This will no longer be needed + # once we properly support cascading expectations files. + expectations = TestExpectations(port, include_overrides=False) path = port.path_to_test_expectations_file() self._tool.filesystem.write_text_file(path, expectations.remove_rebaselined_tests(expectations.get_rebaselining_failures())) def _tests_to_rebaseline(self, port): - return self._expectations(port).get_rebaselining_failures() + tests_to_rebaseline = {} + expectations = TestExpectations(port, include_overrides=True) + for test in expectations.get_rebaselining_failures(): + tests_to_rebaseline[test] = suffixes_for_expectations(expectations.get_expectations(test)) + return tests_to_rebaseline def _rebaseline_port(self, port_name): if not self._is_supported_port(port_name): @@ -281,23 +283,23 @@ class RebaselineExpectations(AbstractDeclarativeCommand): if not builder_name: return _log.info("Retrieving results for %s from %s." % (port_name, builder_name)) - for test_name in self._tests_to_rebaseline(self._tool.port_factory.get(port_name)): - self._touched_test_names.add(test_name) - _log.info(" %s" % test_name) - # FIXME: need to extract the correct list of suffixes here. - self._run_webkit_patch(['rebaseline-test', builder_name, test_name]) + for test_name, suffixes in self._tests_to_rebaseline(self._tool.port_factory.get(port_name)).iteritems(): + self._touched_tests.setdefault(test_name, set()).update(set(suffixes)) + _log.info(" %s (%s)" % (test_name, ','.join(suffixes))) + # FIXME: we should use executive.run_in_parallel() to speed this up. + self._run_webkit_patch(['rebaseline-test', '--suffixes', ','.join(suffixes), builder_name, test_name]) def execute(self, options, args, tool): - self._touched_test_names = set([]) + self._touched_tests = {} for port_name in tool.port_factory.all_port_names(): self._rebaseline_port(port_name) for port_name in tool.port_factory.all_port_names(): self._update_expectations_file(port_name) if not options.optimize: return - for test_name in self._touched_test_names: - _log.info("Optimizing baselines for %s." % test_name) - self._run_webkit_patch(['optimize-baselines', test_name]) + for test_name, suffixes in self._touched_tests.iteritems(): + _log.info("Optimizing baselines for %s (%s)." % (test_name, ','.join(suffixes))) + self._run_webkit_patch(['optimize-baselines', '--suffixes', ','.join(suffixes), test_name]) class Rebaseline(AbstractDeclarativeCommand): diff --git a/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py index f8d9dfc1b..b5a043ff8 100644 --- a/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py +++ b/Tools/Scripts/webkitpy/tool/commands/rebaseline_unittest.py @@ -48,6 +48,8 @@ class TestRebaseline(unittest.TestCase): command.bind_to_tool(tool) lion_port = tool.port_factory.get_from_builder_name("Webkit Mac10.7") + for path in lion_port.expectations_files(): + tool.filesystem.write_text_file(path, '') tool.filesystem.write_text_file(lion_port.path_to_test_expectations_file(), """BUGB MAC LINUX XP DEBUG : fast/dom/Window/window-postmessage-clone-really-deep-array.html = PASS BUGA DEBUG : fast/css/large-list-of-rules-crash.html = TEXT """) @@ -72,6 +74,7 @@ BUGA DEBUG : fast/css/large-list-of-rules-crash.html = TEXT command.bind_to_tool(tool) lion_port = tool.port_factory.get_from_builder_name("Webkit Mac10.7") + tool.filesystem.write_text_file(lion_port.path_from_chromium_base('skia', 'skia_test_expectations.txt'), '') tool.filesystem.write_text_file(lion_port.path_to_test_expectations_file(), "BUGX MAC : userscripts/another-test.html = IMAGE\nBUGZ LINUX : userscripts/another-test.html = IMAGE\n") tool.filesystem.write_text_file(os.path.join(lion_port.layout_tests_dir(), "userscripts/another-test.html"), "Dummy test contents") @@ -84,6 +87,26 @@ Retrieving http://example.com/f/builders/Webkit Mac10.7/results/layout-test-resu new_expectations = tool.filesystem.read_text_file(lion_port.path_to_test_expectations_file()) self.assertEqual(new_expectations, "BUGX LEOPARD SNOWLEOPARD : userscripts/another-test.html = IMAGE\nBUGZ LINUX : userscripts/another-test.html = IMAGE\n") + def test_rebaseline_does_not_include_overrides(self): + command = RebaselineTest() + tool = MockTool() + command.bind_to_tool(tool) + + lion_port = tool.port_factory.get_from_builder_name("Webkit Mac10.7") + tool.filesystem.write_text_file(lion_port.path_from_chromium_base('skia', 'skia_test_expectations.txt'), '') + tool.filesystem.write_text_file(lion_port.path_to_test_expectations_file(), "BUGX MAC : userscripts/another-test.html = IMAGE\nBUGZ LINUX : userscripts/another-test.html = IMAGE\n") + tool.filesystem.write_text_file(lion_port.path_from_chromium_base('skia', 'skia_test_expectations.txt'), "BUGY MAC : other-test.html = TEXT\n") + tool.filesystem.write_text_file(os.path.join(lion_port.layout_tests_dir(), "userscripts/another-test.html"), "Dummy test contents") + + expected_logs = """Retrieving http://example.com/f/builders/Webkit Mac10.7/results/layout-test-results/userscripts/another-test-actual.png. +Retrieving http://example.com/f/builders/Webkit Mac10.7/results/layout-test-results/userscripts/another-test-actual.wav. +Retrieving http://example.com/f/builders/Webkit Mac10.7/results/layout-test-results/userscripts/another-test-actual.txt. +""" + OutputCapture().assert_outputs(self, command._rebaseline_test_and_update_expectations, ["Webkit Mac10.7", "userscripts/another-test.html", None], expected_logs=expected_logs) + + new_expectations = tool.filesystem.read_text_file(lion_port.path_to_test_expectations_file()) + self.assertEqual(new_expectations, "BUGX LEOPARD SNOWLEOPARD : userscripts/another-test.html = IMAGE\nBUGZ LINUX : userscripts/another-test.html = IMAGE\n") + def test_rebaseline_test(self): command = RebaselineTest() command.bind_to_tool(MockTool()) @@ -160,64 +183,87 @@ Retrieving http://example.com/f/builders/Webkit Mac10.7/results/layout-test-resu for port_name in tool.port_factory.all_port_names(): port = tool.port_factory.get(port_name) - tool.filesystem.write_text_file(port.path_to_test_expectations_file(), '') + for path in port.expectations_files(): + tool.filesystem.write_text_file(path, '') # Don't enable logging until after we create the mock expectation files as some Port.__init__'s run subcommands. tool.executive = MockExecutive(should_log=True) expected_logs = """Retrieving results for chromium-linux-x86 from Webkit Linux 32. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) Retrieving results for chromium-linux-x86_64 from Webkit Linux. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) Retrieving results for chromium-mac-leopard from Webkit Mac10.5. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) Retrieving results for chromium-mac-lion from Webkit Mac10.7. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) Retrieving results for chromium-mac-snowleopard from Webkit Mac10.6. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) Retrieving results for chromium-win-vista from Webkit Vista. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) Retrieving results for chromium-win-win7 from Webkit Win7. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) Retrieving results for chromium-win-xp from Webkit Win. - userscripts/another-test.html - userscripts/images.svg + userscripts/another-test.html (txt) + userscripts/images.svg (png) """ - expected_stderr = """MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux 32', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux 32', 'userscripts/images.svg'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Linux', 'userscripts/images.svg'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.5', 'userscripts/images.svg'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.7', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.7', 'userscripts/images.svg'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Mac10.6', 'userscripts/images.svg'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Vista', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Vista', 'userscripts/images.svg'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win7', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win7', 'userscripts/images.svg'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win', 'userscripts/another-test.html'], cwd=/mock-checkout -MOCK run_command: ['echo', 'rebaseline-test', 'Webkit Win', 'userscripts/images.svg'], cwd=/mock-checkout + expected_stderr = """MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Linux 32', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Linux 32', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Linux', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Linux', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Mac10.5', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Mac10.5', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Mac10.7', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Mac10.7', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Mac10.6', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Mac10.6', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Vista', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Vista', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Win7', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Win7', 'userscripts/images.svg'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'txt', 'Webkit Win', 'userscripts/another-test.html'], cwd=/mock-checkout +MOCK run_command: ['echo', 'rebaseline-test', '--suffixes', 'png', 'Webkit Win', 'userscripts/images.svg'], cwd=/mock-checkout """ - command._tests_to_rebaseline = lambda port: ['userscripts/another-test.html', 'userscripts/images.svg'] + command._tests_to_rebaseline = lambda port: {'userscripts/another-test.html': set(['txt']), 'userscripts/images.svg': set(['png'])} OutputCapture().assert_outputs(self, command.execute, [MockOptions(optimize=False), [], tool], expected_logs=expected_logs, expected_stderr=expected_stderr) expected_logs_with_optimize = expected_logs + ( - "Optimizing baselines for userscripts/another-test.html.\n" - "Optimizing baselines for userscripts/images.svg.\n") + "Optimizing baselines for userscripts/another-test.html (txt).\n" + "Optimizing baselines for userscripts/images.svg (png).\n") expected_stderr_with_optimize = expected_stderr + ( - "MOCK run_command: ['echo', 'optimize-baselines', 'userscripts/another-test.html'], cwd=/mock-checkout\n" - "MOCK run_command: ['echo', 'optimize-baselines', 'userscripts/images.svg'], cwd=/mock-checkout\n") + "MOCK run_command: ['echo', 'optimize-baselines', '--suffixes', 'txt', 'userscripts/another-test.html'], cwd=/mock-checkout\n" + "MOCK run_command: ['echo', 'optimize-baselines', '--suffixes', 'png', 'userscripts/images.svg'], cwd=/mock-checkout\n") - command._tests_to_rebaseline = lambda port: ['userscripts/another-test.html', 'userscripts/images.svg'] + command._tests_to_rebaseline = lambda port: {'userscripts/another-test.html': set(['txt']), 'userscripts/images.svg': set(['png'])} OutputCapture().assert_outputs(self, command.execute, [MockOptions(optimize=True), [], tool], expected_logs=expected_logs_with_optimize, expected_stderr=expected_stderr_with_optimize) + + def test_overrides_are_included_correctly(self): + command = RebaselineExpectations() + tool = MockTool() + command.bind_to_tool(tool) + port = tool.port_factory.get('chromium-mac-lion') + + # This tests that the any tests marked as REBASELINE in the overrides are found, but + # that the overrides do not get written into the main file. + expectations_path = port.expectations_files()[0] + expectations_contents = '' + port._filesystem.write_text_file(expectations_path, expectations_contents) + port.expectations_dict = lambda: { + expectations_path: expectations_contents, + 'overrides': ('BUGX REBASELINE : userscripts/another-test.html = TEXT\n' + 'BUGY : userscripts/test.html = CRASH\n')} + + for path in port.expectations_files(): + port._filesystem.write_text_file(path, '') + port._filesystem.write_text_file(port.layout_tests_dir() + '/userscripts/another-test.html', '') + self.assertEquals(command._tests_to_rebaseline(port), {'userscripts/another-test.html': set(['txt'])}) + self.assertEquals(port._filesystem.read_text_file(expectations_path), expectations_contents) diff --git a/Tools/Scripts/webkitpy/tool/mocktool.py b/Tools/Scripts/webkitpy/tool/mocktool.py index 55fde64ad..21ee91fc9 100644 --- a/Tools/Scripts/webkitpy/tool/mocktool.py +++ b/Tools/Scripts/webkitpy/tool/mocktool.py @@ -36,8 +36,7 @@ from webkitpy.common.net.irc.irc_mock import MockIRC from webkitpy.common.config.ports_mock import MockPort -# FIXME: This should be moved somewhere in common and renamed -# something without Mock in the name. +# FIXME: We should just replace this with optparse.Values(default=kwargs) class MockOptions(object): """Mock implementation of optparse.Values.""" @@ -53,6 +52,11 @@ class MockOptions(object): self.__dict__.update(**kwargs) return self + def ensure_value(self, key, value): + if getattr(self, key, None) == None: + self.__dict__[key] = value + return self.__dict__[key] + # FIXME: This should be renamed MockWebKitPatch. class MockTool(MockHost): diff --git a/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py b/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py index 369070982..bfe003fe9 100644 --- a/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py +++ b/Tools/Scripts/webkitpy/tool/servers/gardeningserver.py @@ -69,7 +69,7 @@ class GardeningExpectationsUpdater(BugManager): return "BUG_NEW" def update_expectations(self, failure_info_list): - expectation_lines = self._parser.parse(self._tool.filesystem.read_text_file(self._path_to_test_expectations_file)) + expectation_lines = self._parser.parse(self._path_to_test_expectations_file, self._tool.filesystem.read_text_file(self._path_to_test_expectations_file)) editor = TestExpectationsEditor(expectation_lines, self) updated_expectation_lines = [] # FIXME: Group failures by testName+failureTypeList. diff --git a/Tools/Scripts/webkitpy/tool/steps/commit.py b/Tools/Scripts/webkitpy/tool/steps/commit.py index 2f245e024..0e5ca9157 100644 --- a/Tools/Scripts/webkitpy/tool/steps/commit.py +++ b/Tools/Scripts/webkitpy/tool/steps/commit.py @@ -54,11 +54,7 @@ class Commit(AbstractStep): error.num_local_commits, working_directory_message)) def _check_test_expectations(self, changed_files): - test_expectations_files = [] - for filename in changed_files: - if filename.endswith('test_expectations.txt'): - test_expectations_files.append(filename) - + test_expectations_files = [filename for filename in changed_files if filename.endswith('TestExpectations')] if not test_expectations_files: return diff --git a/Tools/Scripts/webkitpy/tool/steps/commit_unittest.py b/Tools/Scripts/webkitpy/tool/steps/commit_unittest.py index 50dfaea2b..25d9b61a1 100644 --- a/Tools/Scripts/webkitpy/tool/steps/commit_unittest.py +++ b/Tools/Scripts/webkitpy/tool/steps/commit_unittest.py @@ -36,7 +36,7 @@ from webkitpy.tool.steps.commit import Commit class CommitTest(unittest.TestCase): - def test_check_test_expectations(self): + def _test_check_test_expectations(self, filename): capture = OutputCapture() options = MockOptions() options.git_commit = "" @@ -46,16 +46,20 @@ class CommitTest(unittest.TestCase): tool.user = None # Will cause any access of tool.user to raise an exception. step = Commit(tool, options) state = { - "changed_files": ["test_expectations.txtXXX"], + "changed_files": [filename + "XXX"], } tool.executive = MockExecutive(should_log=True, should_throw_when_run=False) capture.assert_outputs(self, step.run, [state], expected_stderr="Committed r49824: <http://trac.webkit.org/changeset/49824>\n") state = { - "changed_files": ["platform/chromium/test_expectations.txt"], + "changed_files": ["platform/chromium/" + filename], } - capture.assert_outputs(self, step.run, [state], expected_stderr="MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--diff-files', 'platform/chromium/test_expectations.txt'], cwd=/mock-checkout\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\n") + capture.assert_outputs(self, step.run, [state], expected_stderr="MOCK run_and_throw_if_fail: ['mock-check-webkit-style', '--diff-files', 'platform/chromium/" + + filename + "'], cwd=/mock-checkout\nCommitted r49824: <http://trac.webkit.org/changeset/49824>\n") - tool.executive = MockExecutive(should_log=True, should_throw_when_run=set(["platform/chromium/test_expectations.txt"])) + tool.executive = MockExecutive(should_log=True, should_throw_when_run=set(["platform/chromium/" + filename])) self.assertRaises(ScriptError, capture.assert_outputs, self, step.run, [state]) + + def test_check_test_expectations(self): + self._test_check_test_expectations('TestExpectations') |