2009-11-27 Adam Barth <abarth@webkit.org>
authorabarth@webkit.org <abarth@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 27 Nov 2009 08:02:05 +0000 (08:02 +0000)
committerabarth@webkit.org <abarth@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 27 Nov 2009 08:02:05 +0000 (08:02 +0000)
        Reviewed by Eric Seidel.

        [bzt] Unit test upload commands
        https://bugs.webkit.org/show_bug.cgi?id=31903

        Adds unit tests for all but two of the upload commands.  The two
        remaining ones are more difficult.  I'll return to them later.  The
        goal of these tests is just to run the commands.  We can test more
        detailed behavior later.

        * Scripts/modules/commands/commandtest.py:
        * Scripts/modules/commands/upload.py:
        * Scripts/modules/commands/upload_unittest.py:
        * Scripts/modules/mock.py: Added.
        * Scripts/modules/mock_bugzillatool.py:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@51436 268f45cc-cd09-0410-ab3c-d52691b4dbfc

WebKitTools/ChangeLog
WebKitTools/Scripts/modules/commands/commandtest.py
WebKitTools/Scripts/modules/commands/upload.py
WebKitTools/Scripts/modules/commands/upload_unittest.py
WebKitTools/Scripts/modules/mock.py [new file with mode: 0644]
WebKitTools/Scripts/modules/mock_bugzillatool.py

index 81ff87f92318e59344b86b9ef99e59d255fc8d90..f9f85b27392b2c039474d64effc7c23fc051b9d9 100644 (file)
@@ -1,3 +1,21 @@
+2009-11-27  Adam Barth  <abarth@webkit.org>
+
+        Reviewed by Eric Seidel.
+
+        [bzt] Unit test upload commands
+        https://bugs.webkit.org/show_bug.cgi?id=31903
+
+        Adds unit tests for all but two of the upload commands.  The two
+        remaining ones are more difficult.  I'll return to them later.  The
+        goal of these tests is just to run the commands.  We can test more
+        detailed behavior later.
+
+        * Scripts/modules/commands/commandtest.py:
+        * Scripts/modules/commands/upload.py:
+        * Scripts/modules/commands/upload_unittest.py:
+        * Scripts/modules/mock.py: Added.
+        * Scripts/modules/mock_bugzillatool.py:
+
 2009-11-26  Adam Barth  <abarth@webkit.org>
 
         Reviewed by Eric Seidel.
index 5532b2661b85a176ae2a7907925cee964570d970..618a51724598262dc37edd50e8354e28e5c045d0 100644 (file)
 
 import unittest
 
+from modules.mock import Mock
 from modules.mock_bugzillatool import MockBugzillaTool
 from modules.outputcapture import OutputCapture
 
 class CommandsTest(unittest.TestCase):
-    def assert_execute_outputs(self, command, command_args, expected_stdout, expected_stderr=""):
+    def assert_execute_outputs(self, command, args, expected_stdout="", expected_stderr="", options=Mock(), tool=MockBugzillaTool()):
         capture = OutputCapture()
         capture.capture_output()
-        command.execute(None, command_args, MockBugzillaTool())
+        command.execute(options, args, tool)
         (stdout_string, stderr_string) = capture.restore_output()
         self.assertEqual(stdout_string, expected_stdout)
         self.assertEqual(expected_stderr, expected_stderr)
index d261ddc4bc09ace81a96d7f17eabc517bcb40a56..e88562336f9895bc8a3486afe9fb3e3c139bae84 100644 (file)
@@ -54,6 +54,7 @@ from modules.webkitlandingscripts import WebKitLandingScripts, commit_message_fo
 from modules.webkitport import WebKitPort
 from modules.workqueue import WorkQueue, WorkQueueDelegate
 
+# FIXME: Requires unit test.  Blocking issue: commit_message_for_this_commit.
 class CommitMessageForCurrentDiff(Command):
     name = "commit-message"
     show_in_main_help = False
@@ -180,6 +181,7 @@ class MarkFixed(Command):
         tool.bugs.close_bug_as_fixed(args[0], args[1])
 
 
+# FIXME: Requires unit test.  Blocking issue: too complex for now.
 class CreateBug(Command):
     name = "create-bug"
     show_in_main_help = True
index 093ebe307611785d7cea847723e37fe0d3be3fa2..4d3f85c1d5e97705ebcd21afc566da80eb49e402 100644 (file)
@@ -33,4 +33,10 @@ from modules.commands.upload import *
 
 class UploadCommandsTest(CommandsTest):
     def test_mark_fixed(self):
-        self.assert_execute_outputs(MarkFixed(), [43, "Test comment"], "", "")
+        self.assert_execute_outputs(MarkFixed(), [43, "Test comment"])
+
+    def test_obsolete_attachments(self):
+        self.assert_execute_outputs(ObsoleteAttachments(), [42])
+
+    def test_post_diff(self):
+        self.assert_execute_outputs(PostDiff(), [42])
diff --git a/WebKitTools/Scripts/modules/mock.py b/WebKitTools/Scripts/modules/mock.py
new file mode 100644 (file)
index 0000000..f6f328e
--- /dev/null
@@ -0,0 +1,309 @@
+# mock.py\r
+# Test tools for mocking and patching.\r
+# Copyright (C) 2007-2009 Michael Foord\r
+# E-mail: fuzzyman AT voidspace DOT org DOT uk\r
+\r
+# mock 0.6.0\r
+# http://www.voidspace.org.uk/python/mock/\r
+\r
+# Released subject to the BSD License\r
+# Please see http://www.voidspace.org.uk/python/license.shtml\r
+\r
+# 2009-11-25: Licence downloaded from above URL.\r
+# BEGIN DOWNLOADED LICENSE\r
+#\r
+# Copyright (c) 2003-2009, Michael Foord\r
+# All rights reserved.\r
+# E-mail : fuzzyman AT voidspace DOT org DOT uk\r
+# \r
+# Redistribution and use in source and binary forms, with or without\r
+# modification, are permitted provided that the following conditions are\r
+# met:\r
+# \r
+# \r
+#     * Redistributions of source code must retain the above copyright\r
+#       notice, this list of conditions and the following disclaimer.\r
+# \r
+#     * Redistributions in binary form must reproduce the above\r
+#       copyright notice, this list of conditions and the following\r
+#       disclaimer in the documentation and/or other materials provided\r
+#       with the distribution.\r
+# \r
+#     * Neither the name of Michael Foord nor the name of Voidspace\r
+#       may be used to endorse or promote products derived from this\r
+#       software without specific prior written permission.\r
+# \r
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\r
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\r
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\r
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\r
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\r
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\r
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\r
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+#\r
+# END DOWNLOADED LICENSE\r
+\r
+# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml\r
+# Comments, suggestions and bug reports welcome.\r
+\r
+\r
+__all__ = (\r
+    'Mock',\r
+    'patch',\r
+    'patch_object',\r
+    'sentinel',\r
+    'DEFAULT'\r
+)\r
+\r
+__version__ = '0.6.0'\r
+\r
+class SentinelObject(object):\r
+    def __init__(self, name):\r
+        self.name = name\r
+        \r
+    def __repr__(self):\r
+        return '<SentinelObject "%s">' % self.name\r
+\r
+\r
+class Sentinel(object):\r
+    def __init__(self):\r
+        self._sentinels = {}\r
+        \r
+    def __getattr__(self, name):\r
+        return self._sentinels.setdefault(name, SentinelObject(name))\r
+    \r
+    \r
+sentinel = Sentinel()\r
+\r
+DEFAULT = sentinel.DEFAULT\r
+\r
+class OldStyleClass:\r
+    pass\r
+ClassType = type(OldStyleClass)\r
+\r
+def _is_magic(name):\r
+    return '__%s__' % name[2:-2] == name\r
+\r
+def _copy(value):\r
+    if type(value) in (dict, list, tuple, set):\r
+        return type(value)(value)\r
+    return value\r
+\r
+\r
+class Mock(object):\r
+\r
+    def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, \r
+                 name=None, parent=None, wraps=None):\r
+        self._parent = parent\r
+        self._name = name\r
+        if spec is not None and not isinstance(spec, list):\r
+            spec = [member for member in dir(spec) if not _is_magic(member)]\r
+        \r
+        self._methods = spec\r
+        self._children = {}\r
+        self._return_value = return_value\r
+        self.side_effect = side_effect\r
+        self._wraps = wraps\r
+        \r
+        self.reset_mock()\r
+        \r
+\r
+    def reset_mock(self):\r
+        self.called = False\r
+        self.call_args = None\r
+        self.call_count = 0\r
+        self.call_args_list = []\r
+        self.method_calls = []\r
+        for child in self._children.itervalues():\r
+            child.reset_mock()\r
+        if isinstance(self._return_value, Mock):\r
+            self._return_value.reset_mock()\r
+        \r
+    \r
+    def __get_return_value(self):\r
+        if self._return_value is DEFAULT:\r
+            self._return_value = Mock()\r
+        return self._return_value\r
+    \r
+    def __set_return_value(self, value):\r
+        self._return_value = value\r
+        \r
+    return_value = property(__get_return_value, __set_return_value)\r
+\r
+\r
+    def __call__(self, *args, **kwargs):\r
+        self.called = True\r
+        self.call_count += 1\r
+        self.call_args = (args, kwargs)\r
+        self.call_args_list.append((args, kwargs))\r
+        \r
+        parent = self._parent\r
+        name = self._name\r
+        while parent is not None:\r
+            parent.method_calls.append((name, args, kwargs))\r
+            if parent._parent is None:\r
+                break\r
+            name = parent._name + '.' + name\r
+            parent = parent._parent\r
+        \r
+        ret_val = DEFAULT\r
+        if self.side_effect is not None:\r
+            if (isinstance(self.side_effect, Exception) or \r
+                isinstance(self.side_effect, (type, ClassType)) and\r
+                issubclass(self.side_effect, Exception)):\r
+                raise self.side_effect\r
+            \r
+            ret_val = self.side_effect(*args, **kwargs)\r
+            if ret_val is DEFAULT:\r
+                ret_val = self.return_value\r
+        \r
+        if self._wraps is not None and self._return_value is DEFAULT:\r
+            return self._wraps(*args, **kwargs)\r
+        if ret_val is DEFAULT:\r
+            ret_val = self.return_value\r
+        return ret_val\r
+    \r
+    \r
+    def __getattr__(self, name):\r
+        if self._methods is not None:\r
+            if name not in self._methods:\r
+                raise AttributeError("Mock object has no attribute '%s'" % name)\r
+        elif _is_magic(name):\r
+            raise AttributeError(name)\r
+        \r
+        if name not in self._children:\r
+            wraps = None\r
+            if self._wraps is not None:\r
+                wraps = getattr(self._wraps, name)\r
+            self._children[name] = Mock(parent=self, name=name, wraps=wraps)\r
+            \r
+        return self._children[name]\r
+    \r
+    \r
+    def assert_called_with(self, *args, **kwargs):\r
+        assert self.call_args == (args, kwargs), 'Expected: %s\nCalled with: %s' % ((args, kwargs), self.call_args)\r
+        \r
+\r
+def _dot_lookup(thing, comp, import_path):\r
+    try:\r
+        return getattr(thing, comp)\r
+    except AttributeError:\r
+        __import__(import_path)\r
+        return getattr(thing, comp)\r
+\r
+\r
+def _importer(target):\r
+    components = target.split('.')\r
+    import_path = components.pop(0)\r
+    thing = __import__(import_path)\r
+\r
+    for comp in components:\r
+        import_path += ".%s" % comp\r
+        thing = _dot_lookup(thing, comp, import_path)\r
+    return thing\r
+\r
+\r
+class _patch(object):\r
+    def __init__(self, target, attribute, new, spec, create):\r
+        self.target = target\r
+        self.attribute = attribute\r
+        self.new = new\r
+        self.spec = spec\r
+        self.create = create\r
+        self.has_local = False\r
+\r
+\r
+    def __call__(self, func):\r
+        if hasattr(func, 'patchings'):\r
+            func.patchings.append(self)\r
+            return func\r
+\r
+        def patched(*args, **keywargs):\r
+            # don't use a with here (backwards compatability with 2.5)\r
+            extra_args = []\r
+            for patching in patched.patchings:\r
+                arg = patching.__enter__()\r
+                if patching.new is DEFAULT:\r
+                    extra_args.append(arg)\r
+            args += tuple(extra_args)\r
+            try:\r
+                return func(*args, **keywargs)\r
+            finally:\r
+                for patching in getattr(patched, 'patchings', []):\r
+                    patching.__exit__()\r
+\r
+        patched.patchings = [self]\r
+        patched.__name__ = func.__name__ \r
+        patched.compat_co_firstlineno = getattr(func, "compat_co_firstlineno", \r
+                                                func.func_code.co_firstlineno)\r
+        return patched\r
+\r
+\r
+    def get_original(self):\r
+        target = self.target\r
+        name = self.attribute\r
+        create = self.create\r
+        \r
+        original = DEFAULT\r
+        if _has_local_attr(target, name):\r
+            try:\r
+                original = target.__dict__[name]\r
+            except AttributeError:\r
+                # for instances of classes with slots, they have no __dict__\r
+                original = getattr(target, name)\r
+        elif not create and not hasattr(target, name):\r
+            raise AttributeError("%s does not have the attribute %r" % (target, name))\r
+        return original\r
+\r
+    \r
+    def __enter__(self):\r
+        new, spec, = self.new, self.spec\r
+        original = self.get_original()\r
+        if new is DEFAULT:\r
+            # XXXX what if original is DEFAULT - shouldn't use it as a spec\r
+            inherit = False\r
+            if spec == True:\r
+                # set spec to the object we are replacing\r
+                spec = original\r
+                if isinstance(spec, (type, ClassType)):\r
+                    inherit = True\r
+            new = Mock(spec=spec)\r
+            if inherit:\r
+                new.return_value = Mock(spec=spec)\r
+        self.temp_original = original\r
+        setattr(self.target, self.attribute, new)\r
+        return new\r
+\r
+\r
+    def __exit__(self, *_):\r
+        if self.temp_original is not DEFAULT:\r
+            setattr(self.target, self.attribute, self.temp_original)\r
+        else:\r
+            delattr(self.target, self.attribute)\r
+        del self.temp_original\r
+            \r
+                \r
+def patch_object(target, attribute, new=DEFAULT, spec=None, create=False):\r
+    return _patch(target, attribute, new, spec, create)\r
+\r
+\r
+def patch(target, new=DEFAULT, spec=None, create=False):\r
+    try:\r
+        target, attribute = target.rsplit('.', 1)    \r
+    except (TypeError, ValueError):\r
+        raise TypeError("Need a valid target to patch. You supplied: %r" % (target,))\r
+    target = _importer(target)\r
+    return _patch(target, attribute, new, spec, create)\r
+\r
+\r
+\r
+def _has_local_attr(obj, name):\r
+    try:\r
+        return name in vars(obj)\r
+    except TypeError:\r
+        # objects without a __dict__\r
+        return hasattr(obj, name)\r
index 4399763f848390ed29c611ce7d9efff78a4c351d..8015f1021ca4656c3451a18e5d99d2984be8cdf1 100644 (file)
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+from modules.scm import CommitMessage
+
 class MockBugzilla():
-    patch1 = { "id": 197, "bug_id": 42, "url": "http://example.com/197" }
-    patch2 = { "id": 128, "bug_id": 42, "url": "http://example.com/128" }
+    patch1 = { "id": 197, "bug_id": 42, "url": "http://example.com/197", "is_obsolete": False }
+    patch2 = { "id": 128, "bug_id": 42, "url": "http://example.com/128", "is_obsolete": False }
 
     def fetch_bug_ids_from_commit_queue(self):
         return [42, 75]
@@ -41,9 +43,25 @@ class MockBugzilla():
             return [self.patch1, self.patch2]
         return None
 
+    def fetch_attachments_from_bug(self, bug_id):
+        if bug_id == 42:
+            return [self.patch1, self.patch2]
+        return None
+
+    def fetch_patches_from_bug(self, bug_id):
+        if bug_id == 42:
+            return [self.patch1, self.patch2]
+        return None
+
     def close_bug_as_fixed(self, bug_id, comment_text=None):
         pass
 
+    def obsolete_attachment(self, attachment_id, comment_text=None):
+        pass
+
+    def add_patch_to_bug(self, bug_id, patch_file_object, description, comment_text=None, mark_for_review=False, mark_for_commit_queue=False):
+        pass
+
 
 class MockBuildBot():
     def builder_statuses(self):
@@ -56,6 +74,32 @@ class MockBuildBot():
         }]
 
 
+class MockSCM():
+    def create_patch(self):
+        return "Patch1"
+
+    def commit_ids_from_commitish_arguments(self, args):
+        return ["Commitish1", "Commitish2"]
+
+    def commit_message_for_local_commit(self, commit_id):
+        if commit_id == "Commitish1":
+            return CommitMessage("CommitMessage1\nhttps://bugs.example.org/show_bug.cgi?id=42\n")
+        if commit_id == "Commitish2":
+            return CommitMessage("CommitMessage2\nhttps://bugs.example.org/show_bug.cgi?id=75\n")
+        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
+
+    def create_patch_from_local_commit(self, commit_id):
+        if commit_id == "Commitish1":
+            return "Patch1"
+        if commit_id == "Commitish2":
+            return "Patch2"
+        raise Exception("Bogus commit_id in commit_message_for_local_commit.")
+
+
 class MockBugzillaTool():
     bugs = MockBugzilla()
     buildbot = MockBuildBot()
+
+    _scm = MockSCM()
+    def scm(self):
+        return self._scm