diff options
Diffstat (limited to 'WebKitTools/Scripts/webkitpy/common')
9 files changed, 283 insertions, 78 deletions
diff --git a/WebKitTools/Scripts/webkitpy/common/config/committers.py b/WebKitTools/Scripts/webkitpy/common/config/committers.py index 113131f..2d07158 100644 --- a/WebKitTools/Scripts/webkitpy/common/config/committers.py +++ b/WebKitTools/Scripts/webkitpy/common/config/committers.py @@ -111,6 +111,7 @@ committers_unable_to_review = [ Committer("Graham Dennis", ["Graham.Dennis@gmail.com", "gdennis@webkit.org"]), Committer("Greg Bolsinga", "bolsinga@apple.com"), Committer("Hans Wennborg", "hans@chromium.org", "hwennborg"), + Committer("Hayato Ito", "hayato@chromium.org", "hayato"), Committer("Hin-Chung Lam", ["hclam@google.com", "hclam@chromium.org"]), Committer("Ilya Tikhonovsky", "loislo@chromium.org", "loislo"), Committer("Jakob Petsovits", ["jpetsovits@rim.com", "jpetso@gmx.at"], "jpetso"), @@ -136,6 +137,7 @@ committers_unable_to_review = [ Committer("Kent Hansen", "kent.hansen@nokia.com", "khansen"), Committer("Kinuko Yasuda", "kinuko@chromium.org", "kinuko"), Committer("Krzysztof Kowalczyk", "kkowalczyk@gmail.com"), + Committer("Kwang Yul Seo", ["kwangyul.seo@gmail.com", "skyul@company100.net", "kseo@webkit.org"], "kwangseo"), Committer("Leandro Pereira", ["leandro@profusion.mobi", "leandro@webkit.org"], "acidx"), Committer("Levi Weintraub", "lweintraub@apple.com"), Committer("Lucas De Marchi", ["lucas.demarchi@profusion.mobi", "demarchi@webkit.org"], "demarchi"), @@ -158,6 +160,7 @@ committers_unable_to_review = [ Committer("Noam Rosenthal", "noam.rosenthal@nokia.com", "noamr"), Committer("Pam Greene", "pam@chromium.org", "pamg"), Committer("Patrick Gansterer", ["paroga@paroga.com", "paroga@webkit.org"], "paroga"), + Committer("Pavel Podivilov", "podivilov@chromium.org", "podivilov"), Committer("Peter Kasting", ["pkasting@google.com", "pkasting@chromium.org"], "pkasting"), Committer("Philippe Normand", ["pnormand@igalia.com", "philn@webkit.org"], "philn-tp"), Committer("Pierre d'Herbemont", ["pdherbemont@free.fr", "pdherbemont@apple.com"], "pdherbemont"), diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py index 593ebc1..17f6c7a 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot.py @@ -34,6 +34,8 @@ import urllib import urllib2 import xmlrpclib +from webkitpy.common.net.failuremap import FailureMap +from webkitpy.common.net.regressionwindow import RegressionWindow from webkitpy.common.system.logutils import get_logger from webkitpy.thirdparty.autoinstalled.mechanize import Browser from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup @@ -145,9 +147,9 @@ class Builder(object): ) return build - def find_failure_transition(self, red_build, look_back_limit=30): + def find_regression_window(self, red_build, look_back_limit=30): if not red_build or red_build.is_green(): - return (None, None) + return RegressionWindow(None, None) common_failures = None current_build = red_build build_after_current_build = None @@ -172,34 +174,25 @@ class Builder(object): break look_back_count += 1 if look_back_count > look_back_limit: - return (None, current_build) + return RegressionWindow(None, current_build, common_failures=common_failures) build_after_current_build = current_build current_build = current_build.previous_build() # We must iterate at least once because red_build is red. assert(build_after_current_build) # Current build must either be green or have no failures in common # with red build, so we've found our failure transition. - return (current_build, build_after_current_build) + return RegressionWindow(current_build, build_after_current_build, common_failures=common_failures) - # FIXME: This likely does not belong on Builder - def suspect_revisions_for_transition(self, last_good_build, first_bad_build): - suspect_revisions = range(first_bad_build.revision(), - last_good_build.revision(), - -1) - suspect_revisions.reverse() - return suspect_revisions - - def blameworthy_revisions(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True): + def find_blameworthy_regression_window(self, red_build_number, look_back_limit=30, avoid_flakey_tests=True): red_build = self.build(red_build_number) - (last_good_build, first_bad_build) = \ - self.find_failure_transition(red_build, look_back_limit) - if not last_good_build: - return [] # We ran off the limit of our search + regression_window = self.find_regression_window(red_build, look_back_limit) + if not regression_window.build_before_failure(): + return None # We ran off the limit of our search # If avoid_flakey_tests, require at least 2 bad builds before we # suspect a real failure transition. - if avoid_flakey_tests and first_bad_build == red_build: - return [] - return self.suspect_revisions_for_transition(last_good_build, first_bad_build) + if avoid_flakey_tests and regression_window.failing_build() == red_build: + return None + return regression_window # FIXME: This should be unified with all the layout test results code in the layout_tests package @@ -414,20 +407,27 @@ class BuildBot(object): build_status_url = "http://%s/one_box_per_builder" % self.buildbot_host return urllib2.urlopen(build_status_url) + def _file_cell_text(self, file_cell): + """Traverses down through firstChild elements until one containing a string is found, then returns that string""" + element = file_cell + while element.string is None and element.contents: + element = element.contents[0] + return element.string + def _parse_twisted_file_row(self, file_row): - string_or_empty = lambda soup: unicode(soup.string) if soup.string else u"" + string_or_empty = lambda string: unicode(string) if string else u"" file_cells = file_row.findAll('td') return { - "filename": string_or_empty(file_cells[0].find("a")), - "size": string_or_empty(file_cells[1]), - "type": string_or_empty(file_cells[2]), - "encoding": string_or_empty(file_cells[3]), + "filename": string_or_empty(self._file_cell_text(file_cells[0])), + "size": string_or_empty(self._file_cell_text(file_cells[1])), + "type": string_or_empty(self._file_cell_text(file_cells[2])), + "encoding": string_or_empty(self._file_cell_text(file_cells[3])), } def _parse_twisted_directory_listing(self, page): soup = BeautifulSoup(page) # HACK: Match only table rows with a class to ignore twisted header/footer rows. - file_rows = soup.find('table').findAll('tr', { "class" : True }) + file_rows = soup.find('table').findAll('tr', {'class': re.compile(r'\b(?:directory|file)\b')}) return [self._parse_twisted_file_row(file_row) for file_row in file_rows] # FIXME: There should be a better way to get this information directly from twisted. @@ -452,19 +452,17 @@ class BuildBot(object): self._builder_by_name[name] = builder return builder - def revisions_causing_failures(self, only_core_builders=True): + def failure_map(self, only_core_builders=True): builder_statuses = self.core_builder_statuses() if only_core_builders else self.builder_statuses() + failure_map = FailureMap() revision_to_failing_bots = {} for builder_status in builder_statuses: if builder_status["is_green"]: continue builder = self.builder_with_name(builder_status["name"]) - revisions = builder.blameworthy_revisions(builder_status["build_number"]) - for revision in revisions: - failing_bots = revision_to_failing_bots.get(revision, []) - failing_bots.append(builder) - revision_to_failing_bots[revision] = failing_bots - return revision_to_failing_bots + regression_window = builder.find_blameworthy_regression_window(builder_status["build_number"]) + failure_map.add_regression_window(builder, regression_window) + return failure_map # This makes fewer requests than calling Builder.latest_build would. It grabs all builder # statuses in one request using self.builder_statuses (fetching /one_box_per_builder instead of builder pages). diff --git a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py index b48f0e4..c99ab32 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/buildbot_unittest.py @@ -54,53 +54,53 @@ class BuilderTest(unittest.TestCase): self.builder = Builder(u"Test Builder \u2661", self.buildbot) self._install_fetch_build(lambda build_number: ["test1", "test2"]) - def test_find_failure_transition(self): - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1003) - self.assertEqual(red_build.revision(), 1004) + def test_find_regression_window(self): + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1003) + self.assertEqual(regression_window.failing_build().revision(), 1004) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10), look_back_limit=2) - self.assertEqual(green_build, None) - self.assertEqual(red_build.revision(), 1008) + regression_window = self.builder.find_regression_window(self.builder.build(10), look_back_limit=2) + self.assertEqual(regression_window.build_before_failure(), None) + self.assertEqual(regression_window.failing_build().revision(), 1008) def test_none_build(self): self.builder._fetch_build = lambda build_number: None - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build, None) - self.assertEqual(red_build, None) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure(), None) + self.assertEqual(regression_window.failing_build(), None) def test_flaky_tests(self): self._install_fetch_build(lambda build_number: ["test1"] if build_number % 2 else ["test2"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1009) - self.assertEqual(red_build.revision(), 1010) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1009) + self.assertEqual(regression_window.failing_build().revision(), 1010) def test_failure_and_flaky(self): self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1003) - self.assertEqual(red_build.revision(), 1004) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1003) + self.assertEqual(regression_window.failing_build().revision(), 1004) def test_no_results(self): self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number % 2 else ["test2"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1003) - self.assertEqual(red_build.revision(), 1004) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1003) + self.assertEqual(regression_window.failing_build().revision(), 1004) def test_failure_after_flaky(self): self._install_fetch_build(lambda build_number: ["test1", "test2"] if build_number > 6 else ["test3"]) - (green_build, red_build) = self.builder.find_failure_transition(self.builder.build(10)) - self.assertEqual(green_build.revision(), 1006) - self.assertEqual(red_build.revision(), 1007) + regression_window = self.builder.find_regression_window(self.builder.build(10)) + self.assertEqual(regression_window.build_before_failure().revision(), 1006) + self.assertEqual(regression_window.failing_build().revision(), 1007) - def test_blameworthy_revisions(self): - self.assertEqual(self.builder.blameworthy_revisions(10), [1004]) - self.assertEqual(self.builder.blameworthy_revisions(10, look_back_limit=2), []) + def test_find_blameworthy_regression_window(self): + self.assertEqual(self.builder.find_blameworthy_regression_window(10).revisions(), [1004]) + self.assertEqual(self.builder.find_blameworthy_regression_window(10, look_back_limit=2), None) # Flakey test avoidance requires at least 2 red builds: - self.assertEqual(self.builder.blameworthy_revisions(4), []) - self.assertEqual(self.builder.blameworthy_revisions(4, avoid_flakey_tests=False), [1004]) + self.assertEqual(self.builder.find_blameworthy_regression_window(4), None) + self.assertEqual(self.builder.find_blameworthy_regression_window(4, avoid_flakey_tests=False).revisions(), [1004]) # Green builder: - self.assertEqual(self.builder.blameworthy_revisions(3), []) + self.assertEqual(self.builder.find_blameworthy_regression_window(3), None) def test_build_caching(self): self.assertEqual(self.builder.build(10), self.builder.build(10)) @@ -361,22 +361,19 @@ class BuildBotTest(unittest.TestCase): <h1>Directory listing for /results/SnowLeopard Intel Leaks/</h1> <table> - <thead> - <tr> + <tr class="alt"> <th>Filename</th> <th>Size</th> <th>Content type</th> <th>Content encoding</th> </tr> - </thead> - <tbody> -<tr class="odd"> - <td><a href="r47483%20%281%29/">r47483 (1)/</a></td> - <td></td> - <td>[Directory]</td> - <td></td> +<tr class="directory "> + <td><a href="r47483%20%281%29/"><b>r47483 (1)/</b></a></td> + <td><b></b></td> + <td><b>[Directory]</b></td> + <td><b></b></td> </tr> -<tr class="odd"> +<tr class="file alt"> <td><a href="r47484%20%282%29.zip">r47484 (2).zip</a></td> <td>89K</td> <td>[application/zip]</td> diff --git a/WebKitTools/Scripts/webkitpy/common/net/credentials.py b/WebKitTools/Scripts/webkitpy/common/net/credentials.py index 1d5f83d..1c3e6c0 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/credentials.py +++ b/WebKitTools/Scripts/webkitpy/common/net/credentials.py @@ -39,14 +39,23 @@ from webkitpy.common.system.executive import Executive, ScriptError from webkitpy.common.system.user import User from webkitpy.common.system.deprecated_logging import log +try: + # Use keyring, a cross platform keyring interface, as a fallback: + # http://pypi.python.org/pypi/keyring + import keyring +except ImportError: + keyring = None + class Credentials(object): - def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd()): + def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd(), + keyring=keyring): self.host = host self.git_prefix = "%s." % git_prefix if git_prefix else "" self.executive = executive or Executive() self.cwd = cwd + self._keyring = keyring def _credentials_from_git(self): return [Git.read_git_config(self.git_prefix + "username"), @@ -117,10 +126,19 @@ class Credentials(object): if not username or not password: (username, password) = self._credentials_from_keychain(username) + if username and not password and self._keyring: + password = self._keyring.get_password(self.host, username) + if not username: username = User.prompt("%s login: " % self.host) if not password: password = getpass.getpass("%s password for %s: " % (self.host, username)) + if self._keyring: + store_password = User().confirm( + "Store password in system keyring?", User.DEFAULT_NO) + if store_password: + self._keyring.set_password(self.host, username, password) + return [username, password] diff --git a/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py b/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py index 9a42bdd..d30291b 100644 --- a/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/net/credentials_unittest.py @@ -113,5 +113,28 @@ password: "SECRETSAUCE" self.assertEqual(credentials.read_credentials(), ["test@webkit.org", "SECRETSAUCE"]) os.rmdir(temp_dir_path) + def test_keyring_without_git_repo(self): + class MockKeyring(object): + def get_password(self, host, username): + return "NOMNOMNOM" + + class FakeCredentials(Credentials): + def __init__(self, cwd): + Credentials.__init__(self, "fake.hostname", cwd=cwd, + keyring=MockKeyring()) + + def _is_mac_os_x(self): + return True + + def _credentials_from_keychain(self, username): + return ("test@webkit.org", None) + + temp_dir_path = tempfile.mkdtemp(suffix="not_a_git_repo") + credentials = FakeCredentials(temp_dir_path) + try: + self.assertEqual(credentials.read_credentials(), ["test@webkit.org", "NOMNOMNOM"]) + finally: + os.rmdir(temp_dir_path) + if __name__ == '__main__': unittest.main() diff --git a/WebKitTools/Scripts/webkitpy/common/net/failuremap.py b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py new file mode 100644 index 0000000..98e4b8f --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/failuremap.py @@ -0,0 +1,48 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +class FailureMap(object): + def __init__(self): + self._failures = [] + + def add_regression_window(self, builder, regression_window): + self._failures.append({ + 'builder': builder, + 'regression_window': regression_window, + }) + + def revisions_causing_failures(self): + revision_to_failing_bots = {} + for failure_info in self._failures: + revisions = failure_info['regression_window'].revisions() + for revision in revisions: + failing_bots = revision_to_failing_bots.get(revision, []) + failing_bots.append(failure_info['builder']) + revision_to_failing_bots[revision] = failing_bots + return revision_to_failing_bots diff --git a/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py new file mode 100644 index 0000000..231459f --- /dev/null +++ b/WebKitTools/Scripts/webkitpy/common/net/regressionwindow.py @@ -0,0 +1,48 @@ +# Copyright (C) 2010 Google Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +class RegressionWindow(object): + def __init__(self, build_before_failure, failing_build, common_failures=None): + self._build_before_failure = build_before_failure + self._failing_build = failing_build + self._common_failures = common_failures + + def build_before_failure(self): + return self._build_before_failure + + def failing_build(self): + return self._failing_build + + def common_failures(self): + return self._common_failures + + def revisions(self): + revisions = range(self._failing_build.revision(), self._build_before_failure.revision(), -1) + revisions.reverse() + return revisions diff --git a/WebKitTools/Scripts/webkitpy/common/system/user.py b/WebKitTools/Scripts/webkitpy/common/system/user.py index 9444c00..240b67b 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user.py @@ -28,6 +28,7 @@ import logging import os +import re import shlex import subprocess import sys @@ -51,6 +52,9 @@ except ImportError: class User(object): + DEFAULT_NO = 'n' + DEFAULT_YES = 'y' + # FIXME: These are @classmethods because bugzilla.py doesn't have a Tool object (thus no User instance). @classmethod def prompt(cls, message, repeat=1, raw_input=raw_input): @@ -61,14 +65,30 @@ class User(object): return response @classmethod - def prompt_with_list(cls, list_title, list_items): + def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input): print list_title i = 0 for item in list_items: i += 1 print "%2d. %s" % (i, item) - result = int(cls.prompt("Enter a number: ")) - 1 - return list_items[result] + + # Loop until we get valid input + while True: + if can_choose_multiple: + response = cls.prompt("Enter one or more numbers (comma-separated), or \"all\": ", raw_input=raw_input) + if not response.strip() or response == "all": + return list_items + try: + indices = [int(r) - 1 for r in re.split("\s*,\s*", response)] + except ValueError, err: + continue + return [list_items[i] for i in indices] + else: + try: + result = int(cls.prompt("Enter a number: ", raw_input=raw_input)) - 1 + except ValueError, err: + continue + return list_items[result] def edit(self, files): editor = os.environ.get("EDITOR") or "vi" @@ -98,11 +118,14 @@ class User(object): except IOError, e: pass - def confirm(self, message=None): + def confirm(self, message=None, default=DEFAULT_YES, raw_input=raw_input): if not message: message = "Continue?" - response = raw_input("%s [Y/n]: " % message) - return not response or response.lower() == "y" + choice = {'y': 'Y/n', 'n': 'y/N'}[default] + response = raw_input("%s [%s]: " % (message, choice)) + if not response: + response = default + return response.lower() == 'y' def can_open_url(self): try: diff --git a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py index dadead3..ae1bad5 100644 --- a/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py +++ b/WebKitTools/Scripts/webkitpy/common/system/user_unittest.py @@ -28,6 +28,7 @@ import unittest +from webkitpy.common.system.outputcapture import OutputCapture from webkitpy.common.system.user import User class UserTest(unittest.TestCase): @@ -50,5 +51,51 @@ class UserTest(unittest.TestCase): return None self.assertEqual(User.prompt("input", repeat=self.repeatsRemaining, raw_input=mock_raw_input), None) + def test_prompt_with_list(self): + def run_prompt_test(inputs, expected_result, can_choose_multiple=False): + def mock_raw_input(message): + return inputs.pop(0) + output_capture = OutputCapture() + actual_result = output_capture.assert_outputs( + self, + User.prompt_with_list, + args=["title", ["foo", "bar"]], + kwargs={"can_choose_multiple": can_choose_multiple, "raw_input": mock_raw_input}, + expected_stdout="title\n 1. foo\n 2. bar\n") + self.assertEqual(actual_result, expected_result) + self.assertEqual(len(inputs), 0) + + run_prompt_test(["1"], "foo") + run_prompt_test(["badinput", "2"], "bar") + + run_prompt_test(["1,2"], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test([" 1, 2 "], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test(["all"], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test([""], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test([" "], ["foo", "bar"], can_choose_multiple=True) + run_prompt_test(["badinput", "all"], ["foo", "bar"], can_choose_multiple=True) + + def test_confirm(self): + test_cases = ( + (("Continue? [Y/n]: ", True), (User.DEFAULT_YES, 'y')), + (("Continue? [Y/n]: ", False), (User.DEFAULT_YES, 'n')), + (("Continue? [Y/n]: ", True), (User.DEFAULT_YES, '')), + (("Continue? [Y/n]: ", False), (User.DEFAULT_YES, 'q')), + (("Continue? [y/N]: ", True), (User.DEFAULT_NO, 'y')), + (("Continue? [y/N]: ", False), (User.DEFAULT_NO, 'n')), + (("Continue? [y/N]: ", False), (User.DEFAULT_NO, '')), + (("Continue? [y/N]: ", False), (User.DEFAULT_NO, 'q')), + ) + for test_case in test_cases: + expected, inputs = test_case + + def mock_raw_input(message): + self.assertEquals(expected[0], message) + return inputs[1] + + result = User().confirm(default=inputs[0], + raw_input=mock_raw_input) + self.assertEquals(expected[1], result) + if __name__ == '__main__': unittest.main() |