← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/fix-git-update-related-bugs into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/fix-git-update-related-bugs into lp:launchpad.

Commit message:
Only update related bugs for Git-based MPs.  Use new GitHostingFixture and MemcacheFixture to handle the plethora of tests that create such MPs.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/fix-git-update-related-bugs/+merge/304941

Only update related bugs for Git-based MPs.  Use new GitHostingFixture and MemcacheFixture to handle the plethora of tests that create such MPs.

This is a large diff, but the boring bulk of it is in a separate commit that only touches tests, so if necessary you can review the interesting bits commit-by-commit.

I've been meaning to tidy up all this fixture setup for a while.  As a bonus, using MemcacheFixture allows us to drop LaunchpadLayer from several test suites, generally speeding things up.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/fix-git-update-related-bugs into lp:launchpad.
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py	2016-07-29 16:13:36 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py	2016-09-05 16:03:58 +0000
@@ -66,10 +66,10 @@
     IMergeProposalNeedsReviewEmailJobSource,
     IMergeProposalUpdatedEmailJobSource,
     )
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.model.diff import PreviewDiff
 from lp.code.tests.helpers import (
     add_revision_to_branch,
+    GitHostingFixture,
     make_merge_proposal_without_reviewers,
     )
 from lp.code.xmlrpc.git import GitAPI
@@ -97,8 +97,6 @@
     time_counter,
     verifyObject,
     )
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
@@ -117,10 +115,7 @@
 
     def setUp(self):
         super(GitHostingClientMixin, self).setUp()
-        self.hosting_client = FakeMethod()
-        self.hosting_client.getLog = FakeMethod(result=[])
-        self.useFixture(
-            ZopeUtilityFixture(self.hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture())
 
 
 class TestBranchMergeProposalContextMenu(TestCaseWithFactory):
@@ -247,8 +242,6 @@
     BrowserTestCase):
     """Tests for `BranchMergeProposalMergedView` for Git."""
 
-    layer = LaunchpadFunctionalLayer
-
     arbitrary_revisions = ("0" * 40, "1" * 40, "2" * 40)
     merged_revision_text = 'Merged Revision ID'
 
@@ -819,7 +812,8 @@
 
 
 class TestRegisterBranchMergeProposalViewGit(
-    TestRegisterBranchMergeProposalViewMixin, BrowserTestCase):
+    TestRegisterBranchMergeProposalViewMixin, GitHostingClientMixin,
+    BrowserTestCase):
     """Test the merge proposal registration view for Git."""
 
     def _makeBranch(self):
@@ -1046,8 +1040,6 @@
     BrowserTestCase):
     """Test `BranchMergeProposalRequestReviewView` for Git."""
 
-    layer = LaunchpadFunctionalLayer
-
     def makeBranchMergeProposal(self):
         return self.factory.makeBranchMergeProposalForGit()
 
@@ -1206,7 +1198,8 @@
 
 
 class TestBranchMergeProposalResubmitViewGit(
-    TestBranchMergeProposalResubmitViewMixin, TestCaseWithFactory):
+    TestBranchMergeProposalResubmitViewMixin, GitHostingClientMixin,
+    TestCaseWithFactory):
     """Test BranchMergeProposalResubmitView for Git."""
 
     def _makeBranchMergeProposal(self):
@@ -1278,7 +1271,7 @@
 class TestResubmitBrowserGit(GitHostingClientMixin, BrowserTestCase):
     """Browser tests for resubmitting branch merge proposals for Git."""
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def test_resubmit_text(self):
         """The text of the resubmit page is as expected."""
@@ -1424,6 +1417,7 @@
     def test_linked_bugtasks_includes_direct_links(self):
         # linked_bugtasks includes bugs that are linked directly to the
         # merge proposal, as is the case for Git-based MPs.
+        self.useFixture(GitHostingFixture())
         bug = self.factory.makeBug()
         bmp = self.factory.makeBranchMergeProposalForGit(registrant=self.user)
         bmp.linkBug(bug, bmp.registrant)
@@ -1486,10 +1480,7 @@
         epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
         review_date = self.factory.getUniqueDate()
         commit_date = self.factory.getUniqueDate()
-        bmp = self.factory.makeBranchMergeProposalForGit(
-            date_created=review_date)
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[
+        self.useFixture(GitHostingFixture(log=[
             {
                 u'sha1': unicode(hashlib.sha1(b'0').hexdigest()),
                 u'message': u'0',
@@ -1499,8 +1490,9 @@
                     u'time': int((commit_date - epoch).total_seconds()),
                     },
                 }
-            ])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+            ]))
+        bmp = self.factory.makeBranchMergeProposalForGit(
+            date_created=review_date)
 
         view = create_initialized_view(bmp, '+index')
         new_commits = view.conversation.comments[0]
@@ -1533,6 +1525,7 @@
         self.assertTrue(view.pending_diff)
 
     def test_pending_diff_with_pending_git_repository(self):
+        self.useFixture(GitHostingFixture())
         bmp = self.factory.makeBranchMergeProposalForGit()
         bmp.next_preview_diff_job.start()
         bmp.next_preview_diff_job.fail()
@@ -1582,12 +1575,10 @@
     def test_unmerged_commits_from_deleted_git_ref(self):
         # Even if the source Git ref has been deleted, we still know its tip
         # SHA-1 and can ask the repository for its unmerged commits.
-        bmp = self.factory.makeBranchMergeProposalForGit()
         sha1 = unicode(hashlib.sha1(b'0').hexdigest())
         epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
         commit_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[
+        self.useFixture(GitHostingFixture(log=[
             {
                 u'sha1': sha1,
                 u'message': u'Sample message',
@@ -1597,9 +1588,9 @@
                     u'time': int((commit_date - epoch).total_seconds()),
                     },
                 }
-            ])
+            ]))
+        bmp = self.factory.makeBranchMergeProposalForGit()
         bmp.source_git_repository.removeRefs([bmp.source_git_path])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
         browser = self.getUserBrowser(canonical_url(bmp, rootsite='code'))
         tag = first_tag_by_class(browser.contents, 'commit-details')
         self.assertEqual(
@@ -1607,10 +1598,9 @@
             "on 2015-01-01" % sha1, extract_text(tag))
 
 
-class TestBranchMergeProposalBrowserView(
-    GitHostingClientMixin, BrowserTestCase):
+class TestBranchMergeProposalBrowserView(BrowserTestCase):
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def test_prerequisite_bzr(self):
         # A prerequisite branch is rendered in the Bazaar case.
@@ -1623,6 +1613,7 @@
 
     def test_prerequisite_git(self):
         # A prerequisite reference is rendered in the Git case.
+        self.useFixture(GitHostingFixture())
         [ref] = self.factory.makeGitRefs()
         identity = ref.identity
         bmp = self.factory.makeBranchMergeProposalForGit(prerequisite_ref=ref)
@@ -1634,9 +1625,10 @@
         # Rendering a Git-based merge proposal makes the correct calls to
         # the hosting service, including requesting cross-repository
         # information.
+        hosting_fixture = self.useFixture(GitHostingFixture())
         bmp = self.factory.makeBranchMergeProposalForGit()
         self.getMainText(bmp, '+index')
-        self.assertThat(self.hosting_client.getLog.calls, MatchesSetwise(
+        self.assertThat(hosting_fixture.getLog.calls, MatchesSetwise(
             # _getNewerRevisions
             MatchesListwise([
                 Equals((
@@ -1679,6 +1671,7 @@
         login_person(self.user)
         self.proposal = self.factory.makeBranchMergeProposal(
             registrant=self.user)
+        self.useFixture(GitHostingFixture())
 
     def _createView(self, form=None):
         # Construct the view and initialize it.
@@ -1857,7 +1850,7 @@
         self.assertEqual('Eric on 2008-09-10', view.status_title)
 
 
-class TestBranchMergeProposal(GitHostingClientMixin, BrowserTestCase):
+class TestBranchMergeProposal(BrowserTestCase):
 
     layer = LaunchpadFunctionalLayer
 
@@ -1943,10 +1936,11 @@
     def test_timeout(self):
         """The page renders even if fetching revisions times out."""
         self.useFixture(FakeLogger())
+        hosting_fixture = self.useFixture(GitHostingFixture())
         bmp = self.factory.makeBranchMergeProposalForGit()
         comment = self.factory.makeCodeReviewComment(
             body='x y' * 100, merge_proposal=bmp)
-        self.hosting_client.getLog.failure = TimeoutError
+        hosting_fixture.getLog.failure = TimeoutError
         browser = self.getViewBrowser(comment.branch_merge_proposal)
         self.assertIn('x y' * 100, browser.contents)
 
@@ -2008,7 +2002,8 @@
 
 
 class TestLatestProposalsForEachBranchGit(
-    TestLatestProposalsForEachBranchMixin, TestCaseWithFactory):
+    TestLatestProposalsForEachBranchMixin, GitHostingClientMixin,
+    TestCaseWithFactory):
     """Confirm that the latest branch is returned for Bazaar."""
 
     def _makeBranchMergeProposal(self, merge_target=None, **kwargs):
@@ -2191,7 +2186,5 @@
     BrowserTestCase):
     """Test the BranchMergeProposal deletion view for Git."""
 
-    layer = LaunchpadFunctionalLayer
-
     def _makeBranchMergeProposal(self, **kwargs):
         return self.factory.makeBranchMergeProposalForGit(**kwargs)

=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py'
--- lib/lp/code/browser/tests/test_branchmergeproposallisting.py	2015-10-14 12:50:55 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py	2016-09-05 16:03:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for BranchMergeProposal listing views."""
@@ -27,6 +27,7 @@
     )
 from lp.code.interfaces.gitref import IGitRef
 from lp.code.interfaces.gitrepository import IGitRepositorySet
+from lp.code.tests.helpers import GitHostingFixture
 from lp.registry.enums import SharingPermission
 from lp.registry.model.personproduct import PersonProduct
 from lp.services.database.sqlbase import flush_database_caches
@@ -80,6 +81,10 @@
 class GitMixin:
     """Mixin for Git-based tests."""
 
+    def setUp(self, *args, **kwargs):
+        super(GitMixin, self).setUp(*args, **kwargs)
+        self.useFixture(GitHostingFixture())
+
     def _makeBranch(self, **kwargs):
         return self.factory.makeGitRefs(**kwargs)[0]
 
@@ -254,6 +259,7 @@
         self.assertIn(identity, view.contents)
 
     def test_product_git(self):
+        self.useFixture(GitHostingFixture())
         [target] = self.factory.makeGitRefs()
         with person_logged_in(target.target.owner):
             getUtility(IGitRepositorySet).setDefaultRepository(
@@ -327,6 +333,7 @@
         """The merges view should be enabled for the target."""
         if not self.supports_git:
             self.skipTest("Context doesn't support Git repositories.")
+        self.useFixture(GitHostingFixture())
         with admin_logged_in():
             bmp = self.makeGitMergeProposal()
             url = canonical_url(bmp, force_local_path=True)
@@ -351,6 +358,7 @@
     def test_query_count_git(self):
         if not self.supports_git:
             self.skipTest("Context doesn't support Git repositories.")
+        self.useFixture(GitHostingFixture())
         with admin_logged_in():
             for i in range(7):
                 self.makeGitMergeProposal()

=== modified file 'lib/lp/code/browser/tests/test_codereviewcomment.py'
--- lib/lp/code/browser/tests/test_codereviewcomment.py	2016-01-21 01:56:21 +0000
+++ lib/lp/code/browser/tests/test_codereviewcomment.py	2016-09-05 16:03:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for CodeReviewComments."""
@@ -22,6 +22,7 @@
 from lp.code.interfaces.codereviewinlinecomment import (
     ICodeReviewInlineCommentSet,
     )
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.webapp import canonical_url
 from lp.testing import (
     BrowserTestCase,
@@ -187,5 +188,9 @@
 class TestCodeReviewCommentHtmlGit(
     TestCodeReviewCommentHtmlMixin, BrowserTestCase):
 
+    def setUp(self):
+        super(TestCodeReviewCommentHtmlGit, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def makeCodeReviewComment(self, **kwargs):
         return self.factory.makeCodeReviewComment(git=True, **kwargs)

=== modified file 'lib/lp/code/browser/tests/test_gitref.py'
--- lib/lp/code/browser/tests/test_gitref.py	2016-05-18 11:49:31 +0000
+++ lib/lp/code/browser/tests/test_gitref.py	2016-09-05 16:03:58 +0000
@@ -15,9 +15,9 @@
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitjob import IGitRefScanJobSource
 from lp.code.interfaces.gitrepository import IGitRepositorySet
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.job.runner import JobRunner
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import (
@@ -25,8 +25,6 @@
     BrowserTestCase,
     )
 from lp.testing.dbuser import dbuser
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import LaunchpadFunctionalLayer
 from lp.testing.pages import (
     extract_text,
@@ -44,10 +42,7 @@
 
     def setUp(self):
         super(TestGitRefView, self).setUp()
-        self.hosting_client = FakeMethod()
-        self.hosting_client.getLog = FakeMethod(result=[])
-        self.useFixture(
-            ZopeUtilityFixture(self.hosting_client, IGitHostingClient))
+        self.hosting_fixture = self.useFixture(GitHostingFixture())
 
     def test_rendering(self):
         repository = self.factory.makeGitRepository(
@@ -121,13 +116,13 @@
             for i in range(5)]
 
     def scanRef(self, ref, tip):
-        self.hosting_client.getRefs = FakeMethod(
-            result={
-                ref.path: {"object": {"sha1": tip["sha1"], "type": "commit"}},
-                })
-        self.hosting_client.getCommits = FakeMethod(result=[tip])
-        self.hosting_client.getProperties = FakeMethod(
-            result={"default_branch": ref.path})
+        self.hosting_fixture.getRefs.result = {
+            ref.path: {"object": {"sha1": tip["sha1"], "type": "commit"}},
+            }
+        self.hosting_fixture.getCommits.result = [tip]
+        self.hosting_fixture.getProperties.result = {
+            "default_branch": ref.path,
+            }
         job = getUtility(IGitRefScanJobSource).create(
             removeSecurityProxy(ref.repository))
         with dbuser("branchscanner"):
@@ -136,7 +131,7 @@
     def test_recent_commits(self):
         [ref] = self.factory.makeGitRefs(paths=[u"refs/heads/branch"])
         log = self.makeCommitLog()
-        self.hosting_client.getLog.result = list(reversed(log))
+        self.hosting_fixture.getLog.result = list(reversed(log))
         self.scanRef(ref, log[-1])
         view = create_initialized_view(ref, "+index")
         expected_texts = list(reversed([
@@ -156,7 +151,7 @@
     def test_recent_commits_with_merge(self):
         [ref] = self.factory.makeGitRefs(paths=[u"refs/heads/branch"])
         log = self.makeCommitLog()
-        self.hosting_client.getLog.result = list(reversed(log))
+        self.hosting_fixture.getLog.result = list(reversed(log))
         self.scanRef(ref, log[-1])
         mp = self.factory.makeBranchMergeProposalForGit(target_ref=ref)
         merged_tip = dict(log[-1])
@@ -182,7 +177,7 @@
     def test_recent_commits_with_merge_from_deleted_ref(self):
         [ref] = self.factory.makeGitRefs(paths=[u"refs/heads/branch"])
         log = self.makeCommitLog()
-        self.hosting_client.getLog.result = list(reversed(log))
+        self.hosting_fixture.getLog.result = list(reversed(log))
         self.scanRef(ref, log[-1])
         mp = self.factory.makeBranchMergeProposalForGit(target_ref=ref)
         merged_tip = dict(log[-1])
@@ -209,7 +204,7 @@
     def test_all_commits_link(self):
         [ref] = self.factory.makeGitRefs(paths=[u"refs/heads/branch"])
         log = self.makeCommitLog()
-        self.hosting_client.getLog.result = list(reversed(log))
+        self.hosting_fixture.getLog.result = list(reversed(log))
         self.scanRef(ref, log[-1])
         view = create_initialized_view(ref, "+index")
         recent_commits_tag = soupmatchers.Tag(

=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
--- lib/lp/code/browser/tests/test_gitrepository.py	2016-05-19 11:52:16 +0000
+++ lib/lp/code/browser/tests/test_gitrepository.py	2016-09-05 16:03:58 +0000
@@ -22,8 +22,8 @@
 from lp.app.enums import InformationType
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.interfaces.services import IService
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.revision import IRevisionSet
+from lp.code.tests.helpers import GitHostingFixture
 from lp.registry.enums import BranchSharingPolicy
 from lp.registry.interfaces.accesspolicy import IAccessPolicySource
 from lp.registry.interfaces.person import PersonVisibility
@@ -39,8 +39,6 @@
     record_two_runs,
     TestCaseWithFactory,
     )
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import DatabaseFunctionalLayer
 from lp.testing.matchers import (
     Contains,
@@ -648,9 +646,7 @@
     def test_change_default_branch(self):
         # An authorised user can change the default branch to one that
         # exists.  They may omit "refs/heads/".
-        hosting_client = FakeMethod()
-        hosting_client.setProperties = FakeMethod()
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        hosting_fixture = self.useFixture(GitHostingFixture())
         person = self.factory.makePerson()
         repository = self.factory.makeGitRepository(owner=person)
         master, new = self.factory.makeGitRefs(
@@ -665,7 +661,7 @@
             self.assertEqual(
                 [((repository.getInternalPath(),),
                  {u"default_branch": u"refs/heads/new"})],
-                hosting_client.setProperties.calls)
+                hosting_fixture.setProperties.calls)
             self.assertEqual(u"refs/heads/new", repository.default_branch)
 
     def test_change_default_branch_nonexistent(self):
@@ -762,10 +758,9 @@
     layer = DatabaseFunctionalLayer
 
     def test_render(self):
-        hosting_client = FakeMethod()
         diff = u"A fake diff\n"
-        hosting_client.getDiff = FakeMethod(result={"patch": diff})
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        hosting_fixture = self.useFixture(GitHostingFixture(
+            diff={"patch": diff}))
         person = self.factory.makePerson()
         repository = self.factory.makeGitRepository(owner=person)
         browser = self.getUserBrowser(
@@ -773,22 +768,20 @@
         with person_logged_in(person):
             self.assertEqual(
                 [((repository.getInternalPath(), "0123456^", "0123456"), {})],
-                hosting_client.getDiff.calls)
+                hosting_fixture.getDiff.calls)
         self.assertEqual(
             'text/x-patch;charset=UTF-8', browser.headers["Content-Type"])
         self.assertEqual(str(len(diff)), browser.headers["Content-Length"])
         self.assertEqual(
             "attachment; filename=0123456^_0123456.diff",
             browser.headers["Content-Disposition"])
-        self.assertEqual("A fake diff\n", browser.contents)
+        self.assertEqual(diff, browser.contents)
 
     def test_security(self):
         # A user who can see a private repository can fetch diffs from it,
         # but other users cannot.
-        hosting_client = FakeMethod()
         diff = u"A fake diff\n"
-        hosting_client.getDiff = FakeMethod(result={"patch": diff})
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(diff={"patch": diff}))
         person = self.factory.makePerson()
         project = self.factory.makeProduct(
             owner=person, information_type=InformationType.PROPRIETARY)
@@ -799,7 +792,7 @@
             repository_url = canonical_url(repository)
         browser = self.getUserBrowser(
             repository_url + "/+diff/0123456/0123456^", user=person)
-        self.assertEqual("A fake diff\n", browser.contents)
+        self.assertEqual(diff, browser.contents)
         self.useFixture(FakeLogger())
         self.assertRaises(
             Unauthorized, self.getUserBrowser,

=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2016-08-12 12:56:41 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2016-09-05 16:03:58 +0000
@@ -35,13 +35,15 @@
 from lp.code.browser.sourcepackagerecipebuild import (
     SourcePackageRecipeBuildView,
     )
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.sourcepackagerecipe import (
     GIT_RECIPES_FEATURE_FLAG,
     MINIMAL_RECIPE_TEXT_BZR,
     MINIMAL_RECIPE_TEXT_GIT,
     )
-from lp.code.tests.helpers import recipe_parser_newest_version
+from lp.code.tests.helpers import (
+    GitHostingFixture,
+    recipe_parser_newest_version,
+    )
 from lp.registry.interfaces.person import TeamMembershipPolicy
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
@@ -64,8 +66,6 @@
     time_counter,
     )
 from lp.testing.deprecated import LaunchpadFormHarness
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
@@ -857,9 +857,7 @@
 
     def setUp(self):
         super(TestSourcePackageRecipeAddViewGit, self).setUp()
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture())
 
     def makeBranchAndPackage(self):
         product = self.factory.makeProduct(

=== modified file 'lib/lp/code/mail/tests/test_branch.py'
--- lib/lp/code/mail/tests/test_branch.py	2015-09-11 12:20:23 +0000
+++ lib/lp/code/mail/tests/test_branch.py	2016-09-05 16:03:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for Branch-related mailings"""
@@ -14,6 +14,7 @@
     )
 from lp.code.model.branch import Branch
 from lp.code.model.gitref import GitRef
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.config import config
 from lp.testing import (
     login_person,
@@ -169,6 +170,10 @@
 class TestRecipientReasonGit(TestRecipientReasonMixin, TestCaseWithFactory):
     """Test RecipientReason for Git references."""
 
+    def setUp(self):
+        super(TestRecipientReasonGit, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def makeProposalWithSubscription(self, subscriber=None):
         """Test fixture."""
         if subscriber is None:

=== modified file 'lib/lp/code/mail/tests/test_codehandler.py'
--- lib/lp/code/mail/tests/test_codehandler.py	2015-10-06 16:09:06 +0000
+++ lib/lp/code/mail/tests/test_codehandler.py	2016-09-05 16:03:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Testing the CodeHandler."""
@@ -34,7 +34,10 @@
     BranchMergeProposalJob,
     BranchMergeProposalJobType,
     )
-from lp.code.tests.helpers import make_merge_proposal_without_reviewers
+from lp.code.tests.helpers import (
+    GitHostingFixture,
+    make_merge_proposal_without_reviewers,
+    )
 from lp.services.config import config
 from lp.services.mail.handlers import mail_handlers
 from lp.services.mail.interfaces import EmailProcessingError
@@ -169,6 +172,7 @@
 
     def test_process_git(self):
         """Processing an email related to a Git-based merge proposal works."""
+        self.useFixture(GitHostingFixture())
         mail = self.factory.makeSignedMessage('<my-id>')
         bmp = self.factory.makeBranchMergeProposalForGit()
         email_addr = bmp.address

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py	2016-08-24 13:51:36 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py	2016-09-05 16:03:58 +0000
@@ -60,7 +60,6 @@
     IBranchMergeProposalGetter,
     notify_modified,
     )
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.model.branchmergeproposal import (
     BranchMergeProposalGetter,
     is_valid_transition,
@@ -72,6 +71,7 @@
     )
 from lp.code.tests.helpers import (
     add_revision_to_branch,
+    GitHostingFixture,
     make_merge_proposal_without_reviewers,
     )
 from lp.registry.enums import TeamMembershipPolicy
@@ -80,6 +80,7 @@
 from lp.services.config import config
 from lp.services.database.constants import UTC_NOW
 from lp.services.features.testing import FeatureFixture
+from lp.services.memcache.testing import MemcacheFixture
 from lp.services.webapp import canonical_url
 from lp.services.xref.interfaces import IXRefSet
 from lp.testing import (
@@ -95,8 +96,6 @@
     )
 from lp.testing.dbuser import dbuser
 from lp.testing.factory import LaunchpadObjectFactory
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
@@ -147,6 +146,10 @@
 class TestBranchMergeProposalCanonicalUrlGit(
     TestBranchMergeProposalCanonicalUrlMixin, TestCaseWithFactory):
 
+    def setUp(self):
+        super(TestBranchMergeProposalCanonicalUrlGit, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def _makeBranchMergeProposal(self):
         return self.factory.makeBranchMergeProposalForGit()
 
@@ -1073,6 +1076,10 @@
 class TestMergeProposalWebhooksGit(
     TestMergeProposalWebhooksMixin, TestCaseWithFactory):
 
+    def setUp(self):
+        super(TestMergeProposalWebhooksGit, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def makeBranch(self, same_target_as=None):
         kwargs = {}
         if same_target_as is not None:
@@ -1460,12 +1467,9 @@
 
     def setUp(self):
         super(TestBranchMergeProposalBugsGit, self).setUp()
-        # Disable GitRef._getLog's use of memcache; we don't need it here,
-        # it requires more time-consuming test setup, and it makes it harder
-        # to repeatedly run updateRelatedBugsFromSource with different log
-        # responses.
-        self.useFixture(FeatureFixture(
-            {u"code.git.log.disable_memcache": u"on"}))
+        self.hosting_fixture = self.useFixture(GitHostingFixture(
+            disable_memcache=False))
+        self.memcache_fixture = self.useFixture(MemcacheFixture())
 
     def _makeBranchMergeProposal(self):
         return self.factory.makeBranchMergeProposalForGit()
@@ -1474,11 +1478,9 @@
         # _fetchRelatedBugIDsFromSource makes a reasonable backend call and
         # parses commit messages.
         bugs = [self.factory.makeBug() for _ in range(3)]
-        bmp = self._makeBranchMergeProposal()
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[
+        self.hosting_fixture.getLog.result = [
             {
-                u"sha1": bmp.source_git_commit_sha1,
+                u"sha1": unicode(hashlib.sha1("0").hexdigest()),
                 u"message": u"Commit 1\n\nLP: #%d" % bugs[0].id,
                 },
             {
@@ -1495,8 +1497,8 @@
                 # Non-existent bug ID will not be returned.
                 u"message": u"Non-existent bug; LP: #%d" % (bugs[2].id + 100),
                 },
-            ])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+            ]
+        bmp = self._makeBranchMergeProposal()
         related_bugs = bmp._fetchRelatedBugIDsFromSource()
         path = "%s:%s" % (
             bmp.target_git_repository.getInternalPath(),
@@ -1505,28 +1507,24 @@
             [((path, bmp.source_git_commit_sha1),
               {"limit": 10, "stop": bmp.target_git_commit_sha1,
                "logger": None})],
-            hosting_client.getLog.calls)
+            self.hosting_fixture.getLog.calls)
         self.assertContentEqual([bugs[0].id, bugs[2].id], related_bugs)
 
     def _setUpLog(self, bugs):
         """Set up a fake log response referring to the given bugs."""
-        if getattr(self, "hosting_client", None) is None:
-            self.hosting_client = FakeMethod()
-            self.hosting_client.getLog = FakeMethod()
-            self.useFixture(
-                ZopeUtilityFixture(self.hosting_client, IGitHostingClient))
-        self.hosting_client.getLog = FakeMethod(result=[
+        self.hosting_fixture.getLog.result = [
             {
                 u"sha1": unicode(hashlib.sha1(str(i)).hexdigest()),
                 u"message": u"LP: #%d" % bug.id,
                 }
-            for i, bug in enumerate(bugs)])
+            for i, bug in enumerate(bugs)]
+        self.memcache_fixture.clear()
 
     def test_updateRelatedBugsFromSource_no_links(self):
         # updateRelatedBugsFromSource does nothing if there are no related
         # bugs in either the database or the source branch.
+        self._setUpLog([])
         bmp = self._makeBranchMergeProposal()
-        self._setUpLog([])
         bmp.updateRelatedBugsFromSource()
         self.assertEqual([], bmp.bugs)
 
@@ -1534,9 +1532,8 @@
         # If the source branch has related bugs not yet reflected in the
         # database, updateRelatedBugsFromSource creates the links.
         bugs = [self.factory.makeBug() for _ in range(3)]
+        self._setUpLog([bugs[0]])
         bmp = self._makeBranchMergeProposal()
-        self._setUpLog([bugs[0]])
-        bmp.updateRelatedBugsFromSource()
         self.assertEqual([bugs[0]], bmp.bugs)
         self._setUpLog(bugs)
         bmp.updateRelatedBugsFromSource()
@@ -1546,9 +1543,8 @@
         # If the database and the source branch list the same related bugs,
         # updateRelatedBugsFromSource does nothing.
         bug = self.factory.makeBug()
+        self._setUpLog([bug])
         bmp = self._makeBranchMergeProposal()
-        self._setUpLog([bug])
-        bmp.updateRelatedBugsFromSource()
         self.assertEqual([bug], bmp.bugs)
         # The second run is a no-op.
         bmp.updateRelatedBugsFromSource()
@@ -1559,9 +1555,8 @@
         # that is no longer listed by the source branch,
         # updateRelatedBugsFromSource removes the link.
         bug = self.factory.makeBug()
+        self._setUpLog([bug])
         bmp = self._makeBranchMergeProposal()
-        self._setUpLog([bug])
-        bmp.updateRelatedBugsFromSource()
         self.assertEqual([bug], bmp.bugs)
         self._setUpLog([])
         bmp.updateRelatedBugsFromSource()
@@ -1572,9 +1567,9 @@
         # updateRelatedBugsFromSource leaves the link alone regardless of
         # whether it is listed by the source branch.
         bug = self.factory.makeBug()
+        self._setUpLog([])
         bmp = self._makeBranchMergeProposal()
         bmp.linkBug(bug)
-        self._setUpLog([])
         bmp.updateRelatedBugsFromSource()
         self.assertEqual([bug], bmp.bugs)
         matches_expected_xref = MatchesDict(
@@ -1595,9 +1590,8 @@
         # OOPS.
         self.pushConfig("codehosting", related_bugs_from_source_limit=3)
         bugs = [self.factory.makeBug() for _ in range(5)]
+        self._setUpLog([bugs[0]])
         bmp = self._makeBranchMergeProposal()
-        self._setUpLog([bugs[0]])
-        bmp.updateRelatedBugsFromSource()
         self.assertEqual([bugs[0]], bmp.bugs)
         self.assertEqual([], self.oopses)
         self._setUpLog(bugs)
@@ -2524,6 +2518,7 @@
 
     def test_getRelatedBugTasks_git(self):
         """Test the getRelatedBugTasks API for Git."""
+        self.useFixture(GitHostingFixture())
         db_bmp = self.factory.makeBranchMergeProposalForGit()
         db_bug = self.factory.makeBug()
         db_bmp.linkBug(db_bug, db_bmp.registrant)

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py'
--- lib/lp/code/model/tests/test_branchmergeproposaljobs.py	2015-10-27 23:35:50 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py	2016-09-05 16:03:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for branch merge proposal jobs."""
@@ -58,6 +58,7 @@
     )
 from lp.code.model.tests.test_diff import DiffTestCase
 from lp.code.subscribers.branchmergeproposal import merge_proposal_modified
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.config import config
 from lp.services.features.testing import FeatureFixture
 from lp.services.job.interfaces.job import JobStatus
@@ -95,6 +96,10 @@
 class GitMixin:
     """Mixin for Git-based tests."""
 
+    def setUp(self):
+        super(GitMixin, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def makeBranchMergeProposal(self, merge_source=None, merge_target=None,
                                 **kwargs):
         return self.factory.makeBranchMergeProposalForGit(
@@ -594,6 +599,7 @@
         # iterReady supports merge proposals based on Git.  (These are
         # currently considered ready regardless of scanning, since the hard
         # work is done by the backend.)
+        self.useFixture(GitHostingFixture())
         bmp = self.factory.makeBranchMergeProposalForGit()
         [job] = self.job_source.iterReady()
         self.assertEqual(bmp, job.branch_merge_proposal)

=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py	2016-07-01 20:07:09 +0000
+++ lib/lp/code/model/tests/test_diff.py	2016-09-05 16:03:58 +0000
@@ -17,7 +17,6 @@
     RemoveLine,
     )
 import transaction
-from zope.interface import implementer
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.errors import NotFoundError
@@ -26,12 +25,12 @@
     IIncrementalDiff,
     IPreviewDiff,
     )
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.model.diff import (
     Diff,
     PreviewDiff,
     )
 from lp.code.model.directbranchcommit import DirectBranchCommit
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.librarian.interfaces.client import (
     LIBRARIAN_SERVER_DEFAULT_TIMEOUT,
     )
@@ -52,7 +51,6 @@
     verifyObject,
     )
 from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
@@ -99,12 +97,6 @@
     return bmp, source_rev_id, target_rev_id
 
 
-@implementer(IGitHostingClient)
-class FakeGitHostingClient:
-
-    pass
-
-
 class DiffTestCase(TestCaseWithFactory):
 
     def createExampleBzrMerge(self):
@@ -147,12 +139,6 @@
         return (source_bzr, source_rev_id, target_bzr, prerequisite_bzr,
                 prerequisite)
 
-    def installFakeGitMergeDiff(self, result=None, failure=None):
-        hosting_client = FakeGitHostingClient()
-        hosting_client.getMergeDiff = FakeMethod(
-            result=result, failure=failure)
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
-
     def createExampleGitMerge(self):
         """Create an example Git-based merge scenario.
 
@@ -160,6 +146,7 @@
         fixture as yet, so for the moment we just install a fake hosting
         client that will return a plausible-looking diff.
         """
+        hosting_fixture = self.useFixture(GitHostingFixture())
         bmp = self.factory.makeBranchMergeProposalForGit()
         source_sha1 = bmp.source_git_ref.commit_sha1
         target_sha1 = bmp.target_git_ref.commit_sha1
@@ -177,10 +164,10 @@
              a
             +b
             """) % (target_sha1[:7], source_sha1[:7])
-        self.installFakeGitMergeDiff(result={
+        hosting_fixture.getMergeDiff.result = {
             "patch": patch,
             "conflicts": ["foo"],
-            })
+            }
         return bmp, source_sha1, target_sha1, patch
 
 
@@ -420,6 +407,7 @@
 
     def _createGitProposalWithPreviewDiff(self, merge_prerequisite=None,
                                           content=None):
+        self.useFixture(GitHostingFixture())
         mp = self.factory.makeBranchMergeProposalForGit(
             prerequisite_ref=merge_prerequisite)
         login_person(mp.registrant)

=== modified file 'lib/lp/code/model/tests/test_gitcollection.py'
--- lib/lp/code/model/tests/test_gitcollection.py	2016-06-24 23:35:24 +0000
+++ lib/lp/code/model/tests/test_gitcollection.py	2016-09-05 16:03:58 +0000
@@ -37,6 +37,7 @@
 from lp.code.interfaces.gitrepository import IGitRepositorySet
 from lp.code.model.gitcollection import GenericGitCollection
 from lp.code.model.gitrepository import GitRepository
+from lp.code.tests.helpers import GitHostingFixture
 from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.person import TeamMembershipPolicy
 from lp.registry.model.persondistributionsourcepackage import (
@@ -417,6 +418,7 @@
 
     def test_targetedBy(self):
         # Only repositories that are merge targets are returned.
+        self.useFixture(GitHostingFixture())
         [target_ref] = self.factory.makeGitRefs()
         registrant = self.factory.makePerson()
         self.factory.makeBranchMergeProposalForGit(
@@ -429,6 +431,7 @@
 
     def test_targetedBy_since(self):
         # Ignore proposals created before 'since'.
+        self.useFixture(GitHostingFixture())
         bmp = self.factory.makeBranchMergeProposalForGit()
         date_created = self.factory.getUniqueDate()
         removeSecurityProxy(bmp).date_created = date_created
@@ -450,6 +453,7 @@
     def setUp(self):
         TestCaseWithFactory.setUp(self)
         self.all_repositories = getUtility(IAllGitRepositories)
+        self.useFixture(GitHostingFixture())
 
     def test_empty_branch_merge_proposals(self):
         proposals = self.all_repositories.getMergeProposals()
@@ -650,6 +654,7 @@
         # nominate reviewer in this test.
         TestCaseWithFactory.setUp(self, 'admin@xxxxxxxxxxxxx')
         self.all_repositories = getUtility(IAllGitRepositories)
+        self.useFixture(GitHostingFixture())
 
     def test_getProposalsForReviewer(self):
         reviewer = self.factory.makePerson()

=== modified file 'lib/lp/code/model/tests/test_gitjob.py'
--- lib/lp/code/model/tests/test_gitjob.py	2016-06-25 08:07:19 +0000
+++ lib/lp/code/model/tests/test_gitjob.py	2016-09-05 16:03:58 +0000
@@ -19,14 +19,12 @@
     MatchesSetwise,
     MatchesStructure,
     )
-from zope.interface import implementer
 from zope.security.proxy import removeSecurityProxy
 
 from lp.code.enums import GitObjectType
 from lp.code.interfaces.branchmergeproposal import (
     BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,
     )
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitjob import (
     IGitJob,
     IGitRefScanJob,
@@ -39,6 +37,7 @@
     GitRefScanJob,
     ReclaimGitRepositorySpaceJob,
     )
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.config import config
 from lp.services.database.constants import UTC_NOW
 from lp.services.features.testing import FeatureFixture
@@ -49,32 +48,12 @@
     time_counter,
     )
 from lp.testing.dbuser import dbuser
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
-    LaunchpadZopelessLayer,
+    ZopelessDatabaseLayer,
     )
 
 
-@implementer(IGitHostingClient)
-class FakeGitHostingClient:
-
-    def __init__(self, refs, commits, default_branch=u"refs/heads/master"):
-        self._refs = refs
-        self._commits = commits
-        self._default_branch = default_branch
-
-    def getRefs(self, paths):
-        return self._refs
-
-    def getCommits(self, path, commit_oids, logger=None):
-        return self._commits
-
-    def getProperties(self, path):
-        return {u"default_branch": self._default_branch}
-
-
 class TestGitJob(TestCaseWithFactory):
     """Tests for `GitJob`."""
 
@@ -90,7 +69,7 @@
 class TestGitJobDerived(TestCaseWithFactory):
     """Tests for `GitJobDerived`."""
 
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
 
     def test_getOopsMailController(self):
         """By default, no mail is sent about failed BranchJobs."""
@@ -103,7 +82,7 @@
 class TestGitRefScanJob(TestCaseWithFactory):
     """Tests for `GitRefScanJob`."""
 
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
 
     @staticmethod
     def makeFakeRefs(paths):
@@ -165,10 +144,9 @@
         author = repository.owner
         author_date_start = datetime(2015, 1, 1, tzinfo=pytz.UTC)
         author_date_gen = time_counter(author_date_start, timedelta(days=1))
-        hosting_client = FakeGitHostingClient(
-            self.makeFakeRefs(paths),
-            self.makeFakeCommits(author, author_date_gen, paths))
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(
+            refs=self.makeFakeRefs(paths),
+            commits=self.makeFakeCommits(author, author_date_gen, paths)))
         with dbuser("branchscanner"):
             JobRunner([job]).runAll()
         self.assertRefsMatch(repository.refs, repository, paths)
@@ -177,8 +155,7 @@
     def test_logs_bad_ref_info(self):
         repository = self.factory.makeGitRepository()
         job = GitRefScanJob.create(repository)
-        hosting_client = FakeGitHostingClient({u"refs/heads/master": {}}, [])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(refs={u"refs/heads/master": {}}))
         expected_message = (
             'Unconvertible ref refs/heads/master {}: '
             'ref info does not contain "object" key')
@@ -197,8 +174,7 @@
             target=repository, event_types=['git:push:0.1'])
         job = GitRefScanJob.create(repository)
         paths = (u'refs/heads/master', u'refs/tags/2.0')
-        hosting_client = FakeGitHostingClient(self.makeFakeRefs(paths), [])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(refs=self.makeFakeRefs(paths)))
         with dbuser('branchscanner'):
             JobRunner([job]).runAll()
         delivery = hook.deliveries.one()
@@ -230,11 +206,7 @@
         repository = self.factory.makeGitRepository()
         target, source = self.factory.makeGitRefs(
             repository, paths=[u'refs/heads/target', u'refs/heads/source'])
-        bmp = self.factory.makeBranchMergeProposalForGit(
-            target_ref=target, source_ref=source)
-        hook = self.factory.makeWebhook(
-            target=repository, event_types=['merge-proposal:0.1'])
-        new_refs = {
+        refs = {
             target.path: {'object': {
                 'sha1': u'0' * 40,
                 'type': 'commit',
@@ -244,11 +216,12 @@
                 'type': 'commit',
                 }},
             }
-        hosting_client = FakeGitHostingClient(new_refs, [])
-        hosting_client.getLog = FakeMethod(result=[])
-        hosting_client.detectMerges = FakeMethod(
-            result={source.commit_sha1: u'0' * 40})
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        merges = {source.commit_sha1: u'0' * 40}
+        self.useFixture(GitHostingFixture(refs=refs, merges=merges))
+        bmp = self.factory.makeBranchMergeProposalForGit(
+            target_ref=target, source_ref=source)
+        hook = self.factory.makeWebhook(
+            target=repository, event_types=['merge-proposal:0.1'])
         job = GitRefScanJob.create(repository)
         with dbuser('branchscanner'):
             JobRunner([job]).runAll()
@@ -307,7 +280,7 @@
 class TestReclaimGitRepositorySpaceJob(TestCaseWithFactory):
     """Tests for `ReclaimGitRepositorySpaceJob`."""
 
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
 
     def test_provides_interface(self):
         # `ReclaimGitRepositorySpaceJob` objects provide
@@ -352,17 +325,15 @@
     def test_run(self):
         # Running a job to reclaim space sends a request to the hosting
         # service.
-        hosting_client = FakeGitHostingClient({}, [])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        hosting_fixture = self.useFixture(GitHostingFixture())
         name = "/~owner/+git/gone"
         path = "1"
         job = ReclaimGitRepositorySpaceJob.create(name, path)
         self.makeJobReady(job)
         [job] = list(ReclaimGitRepositorySpaceJob.iterReady())
         with dbuser("branchscanner"):
-            hosting_client.delete = FakeMethod()
             JobRunner([job]).runAll()
-        self.assertEqual([(path,)], hosting_client.delete.extract_args())
+        self.assertEqual([(path,)], hosting_fixture.delete.extract_args())
 
 
 # XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,

=== modified file 'lib/lp/code/model/tests/test_gitref.py'
--- lib/lp/code/model/tests/test_gitref.py	2016-09-02 14:36:28 +0000
+++ lib/lp/code/model/tests/test_gitref.py	2016-09-05 16:03:58 +0000
@@ -27,7 +27,7 @@
 from lp.app.interfaces.informationtype import IInformationType
 from lp.app.interfaces.launchpad import IPrivacy
 from lp.code.errors import InvalidBranchMergeProposal
-from lp.code.interfaces.githosting import IGitHostingClient
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.features.testing import FeatureFixture
 from lp.services.memcache.interfaces import IMemcacheClient
 from lp.services.webapp.interfaces import OAuthPermission
@@ -39,8 +39,6 @@
     TestCaseWithFactory,
     verifyObject,
     )
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
@@ -61,12 +59,14 @@
             [ref.display_name for ref in (master, personal)])
 
     def test_getMergeProposals(self):
+        self.useFixture(GitHostingFixture())
         [target_ref] = self.factory.makeGitRefs()
         bmp = self.factory.makeBranchMergeProposalForGit(target_ref=target_ref)
         self.factory.makeBranchMergeProposalForGit()
         self.assertEqual([bmp], list(target_ref.getMergeProposals()))
 
     def test_getDependentMergeProposals(self):
+        self.useFixture(GitHostingFixture())
         [prerequisite_ref] = self.factory.makeGitRefs()
         bmp = self.factory.makeBranchMergeProposalForGit(
             prerequisite_ref=prerequisite_ref)
@@ -142,10 +142,7 @@
                 u"tree": unicode(hashlib.sha1("").hexdigest()),
                 },
             ]
-        self.hosting_client = FakeMethod()
-        self.hosting_client.getLog = FakeMethod(result=self.log)
-        self.useFixture(
-            ZopeUtilityFixture(self.hosting_client, IGitHostingClient))
+        self.hosting_fixture = self.useFixture(GitHostingFixture(log=self.log))
 
     def test_basic(self):
         commits = self.ref.getCommits(self.sha1_tip)
@@ -153,7 +150,7 @@
         self.assertEqual(
             [((path, self.sha1_tip),
               {"limit": None, "stop": None, "logger": None})],
-            self.hosting_client.getLog.calls)
+            self.hosting_fixture.getLog.calls)
         self.assertThat(commits, MatchesListwise([
             ContainsDict({
                 "sha1": Equals(self.sha1_tip),
@@ -189,7 +186,7 @@
                 "commit_message": Is(None),
                 }),
             ]))
-        self.assertEqual([], self.hosting_client.getLog.calls)
+        self.assertEqual([], self.hosting_fixture.getLog.calls)
         path = self.ref.repository.getInternalPath()
         key = u"git.launchpad.dev:git-log:%s:%s" % (path, self.sha1_tip)
         self.assertIsNone(getUtility(IMemcacheClient).get(key.encode("UTF-8")))
@@ -210,7 +207,7 @@
         self.assertEqual(
             [((path, self.sha1_tip),
               {"limit": 10, "stop": self.sha1_root, "logger": None})],
-            self.hosting_client.getLog.calls)
+            self.hosting_fixture.getLog.calls)
         key = u"git.launchpad.dev:git-log:%s:%s:limit=10:stop=%s" % (
             path, self.sha1_tip, self.sha1_root)
         self.assertEqual(
@@ -228,7 +225,7 @@
         self.assertEqual(
             [((path, self.sha1_tip),
               {"limit": None, "stop": self.sha1_root, "logger": None})],
-            self.hosting_client.getLog.calls)
+            self.hosting_fixture.getLog.calls)
         key = u"git.launchpad.dev:git-log:%s:%s:stop=%s" % (
             path, self.sha1_tip, self.sha1_root)
         self.assertEqual(
@@ -289,10 +286,7 @@
             [self.prerequisite] = self.factory.makeGitRefs(
                 name=u"prerequisite", owner=self.user, target=self.project)
         self.log = []
-        self.hosting_client = FakeMethod()
-        self.hosting_client.getLog = FakeMethod(result=self.log)
-        self.useFixture(
-            ZopeUtilityFixture(self.hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(log=self.log))
 
     def assertOnePendingReview(self, proposal, reviewer, review_type=None):
         # There should be one pending vote for the reviewer with the specified
@@ -450,6 +444,10 @@
 
     layer = DatabaseFunctionalLayer
 
+    def setUp(self):
+        super(TestGitRefWebservice, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def test_attributes(self):
         [master] = self.factory.makeGitRefs(paths=[u"refs/heads/master"])
         webservice = webservice_for_person(

=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py	2016-09-02 18:21:07 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py	2016-09-05 16:03:58 +0000
@@ -56,7 +56,6 @@
     BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES,
     )
 from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitjob import (
     IGitRefScanJobSource,
     IGitRepositoryModifiedMailJobSource,
@@ -91,6 +90,7 @@
     DeletionOperation,
     GitRepository,
     )
+from lp.code.tests.helpers import GitHostingFixture
 from lp.code.xmlrpc.git import GitAPI
 from lp.registry.enums import (
     BranchSharingPolicy,
@@ -116,6 +116,7 @@
 from lp.services.job.model.job import Job
 from lp.services.job.runner import JobRunner
 from lp.services.mail import stub
+from lp.services.memcache.testing import MemcacheFixture
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.interfaces import OAuthPermission
 from lp.testing import (
@@ -130,12 +131,9 @@
     verifyObject,
     )
 from lp.testing.dbuser import dbuser
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
-    LaunchpadZopelessLayer,
     ZopelessDatabaseLayer,
     )
 from lp.testing.mail_helpers import pop_notifications
@@ -400,6 +398,7 @@
 
     def test_landing_target_disables_deletion(self):
         # A repository with a landing target cannot be deleted.
+        self.useFixture(GitHostingFixture())
         [merge_target] = self.factory.makeGitRefs(
             name=u"landing-target", owner=self.user, target=self.project)
         self.ref.addLandingTarget(self.user, merge_target)
@@ -411,6 +410,7 @@
 
     def test_landing_candidate_disables_deletion(self):
         # A repository with a landing candidate cannot be deleted.
+        self.useFixture(GitHostingFixture())
         [merge_source] = self.factory.makeGitRefs(
             name=u"landing-candidate", owner=self.user, target=self.project)
         merge_source.addLandingTarget(self.user, self.ref)
@@ -422,6 +422,7 @@
 
     def test_prerequisite_repository_disables_deletion(self):
         # A repository that is a prerequisite repository cannot be deleted.
+        self.useFixture(GitHostingFixture())
         [merge_source] = self.factory.makeGitRefs(
             name=u"landing-candidate", owner=self.user, target=self.project)
         [merge_target] = self.factory.makeGitRefs(
@@ -475,6 +476,7 @@
     def test_destroySelf_with_inline_comments_draft(self):
         # Draft inline comments related to a deleted repository (source or
         # target MP repository) also get removed.
+        self.useFixture(GitHostingFixture())
         merge_proposal = self.factory.makeBranchMergeProposalForGit(
             registrant=self.user, target_ref=self.ref)
         preview_diff = self.factory.makePreviewDiff(
@@ -489,6 +491,7 @@
     def test_destroySelf_with_inline_comments_published(self):
         # Published inline comments related to a deleted repository (source
         # or target MP repository) also get removed.
+        self.useFixture(GitHostingFixture())
         merge_proposal = self.factory.makeBranchMergeProposalForGit(
             registrant=self.user, target_ref=self.ref)
         preview_diff = self.factory.makePreviewDiff(
@@ -514,7 +517,7 @@
     """Test determination and application of repository deletion
     consequences."""
 
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
 
     def setUp(self):
         super(TestGitRepositoryDeletionConsequences, self).setUp(
@@ -526,6 +529,7 @@
         # unsubscribe the repository owner here.
         self.repository.unsubscribe(
             self.repository.owner, self.repository.owner)
+        self.useFixture(GitHostingFixture())
 
     def test_plain_repository(self):
         # A fresh repository has no deletion requirements.
@@ -1107,9 +1111,7 @@
         return [UpdatePreviewDiffJob(job) for job in jobs]
 
     def test_update_schedules_diff_update(self):
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture())
         repository = self.factory.makeGitRepository()
         [ref] = self.factory.makeGitRefs(repository=repository)
         self.assertRefsMatch(repository.refs, repository, [ref.path])
@@ -1144,8 +1146,7 @@
         self.assertRefsMatch(repository.refs, repository, paths)
         master_sha1 = repository.getRefByPath(u"refs/heads/master").commit_sha1
         foo_sha1 = repository.getRefByPath(u"refs/heads/foo").commit_sha1
-        hosting_client = FakeMethod()
-        hosting_client.getRefs = FakeMethod(result={
+        self.useFixture(GitHostingFixture(refs={
             u"refs/heads/master": {
                 u"object": {
                     u"sha1": u"1111111111111111111111111111111111111111",
@@ -1164,8 +1165,7 @@
                     u"type": u"commit",
                     },
                 },
-            })
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+            }))
         refs_to_upsert, refs_to_remove = repository.planRefChanges("dummy")
 
         expected_upsert = {
@@ -1198,16 +1198,14 @@
                 },
             }
         repository.createOrUpdateRefs(refs_info)
-        hosting_client = FakeMethod()
-        hosting_client.getRefs = FakeMethod(result={
+        self.useFixture(GitHostingFixture(refs={
             u"refs/heads/blob": {
                 u"object": {
                     u"sha1": blob_sha1,
                     u"type": u"blob",
                     },
                 },
-            })
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+            }))
         self.assertEqual(({}, set()), repository.planRefChanges("dummy"))
 
     def test_fetchRefCommits(self):
@@ -1221,8 +1219,7 @@
         epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
         author_date = datetime(2015, 1, 1, tzinfo=pytz.UTC)
         committer_date = datetime(2015, 1, 2, tzinfo=pytz.UTC)
-        hosting_client = FakeMethod()
-        hosting_client.getCommits = FakeMethod(result=[
+        hosting_fixture = self.useFixture(GitHostingFixture(commits=[
             {
                 u"sha1": master_sha1,
                 u"message": u"tip of master",
@@ -1238,8 +1235,7 @@
                     },
                 u"parents": [],
                 u"tree": unicode(hashlib.sha1("").hexdigest()),
-                }])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+                }]))
         refs = {
             u"refs/heads/master": {
                 u"sha1": master_sha1,
@@ -1253,7 +1249,7 @@
         GitRepository.fetchRefCommits("dummy", refs)
 
         expected_oids = [master_sha1, foo_sha1]
-        [(_, observed_oids)] = hosting_client.getCommits.extract_args()
+        [(_, observed_oids)] = hosting_fixture.getCommits.extract_args()
         self.assertContentEqual(expected_oids, observed_oids)
         expected_author_addr = u"%s <%s>" % (author.displayname, author_email)
         [expected_author] = getUtility(IRevisionSet).acquireRevisionAuthors(
@@ -1323,9 +1319,7 @@
         self.assertThat(repository.refs, MatchesSetwise(*matchers))
 
     def test_set_default_branch(self):
-        hosting_client = FakeMethod()
-        hosting_client.setProperties = FakeMethod()
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        hosting_fixture = self.useFixture(GitHostingFixture())
         repository = self.factory.makeGitRepository()
         self.factory.makeGitRefs(
             repository=repository,
@@ -1336,20 +1330,18 @@
         self.assertEqual(
             [((repository.getInternalPath(),),
              {u"default_branch": u"refs/heads/new"})],
-            hosting_client.setProperties.calls)
+            hosting_fixture.setProperties.calls)
         self.assertEqual(u"refs/heads/new", repository.default_branch)
 
     def test_set_default_branch_unchanged(self):
-        hosting_client = FakeMethod()
-        hosting_client.setProperties = FakeMethod()
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        hosting_fixture = self.useFixture(GitHostingFixture())
         repository = self.factory.makeGitRepository()
         self.factory.makeGitRefs(
             repository=repository, paths=[u"refs/heads/master"])
         removeSecurityProxy(repository)._default_branch = u"refs/heads/master"
         with person_logged_in(repository.owner):
             repository.default_branch = u"master"
-        self.assertEqual([], hosting_client.setProperties.calls)
+        self.assertEqual([], hosting_fixture.setProperties.calls)
         self.assertEqual(u"refs/heads/master", repository.default_branch)
 
 
@@ -1763,6 +1755,10 @@
 
     layer = DatabaseFunctionalLayer
 
+    def setUp(self):
+        super(TestGitRepositoryUpdateMergeCommitIDs, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def test_updates_proposals(self):
         # `updateMergeCommitIDs` updates proposals for specified refs.
         repository = self.factory.makeGitRepository()
@@ -1821,31 +1817,31 @@
 
 class TestGitRepositoryUpdateLandingTargets(TestCaseWithFactory):
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def test_updates_related_bugs(self):
         """All non-final merge proposals have their related bugs updated."""
+        hosting_fixture = self.useFixture(GitHostingFixture(
+            disable_memcache=False))
+        memcache_fixture = self.useFixture(MemcacheFixture())
         bug = self.factory.makeBug()
         bmp = self.factory.makeBranchMergeProposalForGit()
         self.assertEqual([], bmp.bugs)
         self.assertEqual([], bug.linked_merge_proposals)
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[
+        hosting_fixture.getLog.result = [
             {
                 u"sha1": bmp.source_git_commit_sha1,
                 u"message": u"Fix upside-down messages\n\nLP: #%d" % bug.id,
                 },
-            ])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+            ]
+        memcache_fixture.clear()
         bmp.source_git_repository.updateLandingTargets([bmp.source_git_path])
         self.assertEqual([bug], bmp.bugs)
         self.assertEqual([bmp], bug.linked_merge_proposals)
 
     def test_schedules_diff_updates(self):
         """Create jobs for all merge proposals."""
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture())
         bmp1 = self.factory.makeBranchMergeProposalForGit()
         bmp2 = self.factory.makeBranchMergeProposalForGit(
             source_ref=bmp1.source_git_ref)
@@ -1858,6 +1854,7 @@
 
     def test_ignores_final(self):
         """Diffs for proposals in final states aren't updated."""
+        self.useFixture(GitHostingFixture())
         [source_ref] = self.factory.makeGitRefs()
         for state in FINAL_STATES:
             bmp = self.factory.makeBranchMergeProposalForGit(
@@ -2003,7 +2000,11 @@
 
 class TestGitRepositoryDetectMerges(TestCaseWithFactory):
 
-    layer = LaunchpadZopelessLayer
+    layer = ZopelessDatabaseLayer
+
+    def setUp(self):
+        super(TestGitRepositoryDetectMerges, self).setUp()
+        self.useFixture(GitHostingFixture())
 
     def test_markProposalMerged(self):
         # A merge proposal that is merged is marked as such.
@@ -2050,10 +2051,8 @@
             target_ref=target_1, source_ref=source_2)
         bmp3 = self.factory.makeBranchMergeProposalForGit(
             target_ref=target_2, source_ref=source_1)
-        hosting_client = FakeMethod()
-        hosting_client.detectMerges = FakeMethod(
-            result={source_1.commit_sha1: u"0" * 40})
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        hosting_fixture = self.useFixture(GitHostingFixture(
+            merges={source_1.commit_sha1: u"0" * 40}))
         refs_info = {
             u"refs/heads/target-1": {
                 u"sha1": u"0" * 40,
@@ -2075,7 +2074,7 @@
              set([source_1.commit_sha1])),
             ]
         self.assertContentEqual(
-            expected_args, hosting_client.detectMerges.extract_args())
+            expected_args, hosting_fixture.detectMerges.extract_args())
         self.assertEqual(BranchMergeProposalStatus.MERGED, bmp1.queue_status)
         self.assertEqual(u"0" * 40, bmp1.merged_revision_id)
         self.assertEqual(
@@ -2100,17 +2099,13 @@
 
     def test_getBlob_with_default_rev(self):
         repository = self.factory.makeGitRepository()
-        hosting_client = FakeMethod()
-        hosting_client.getBlob = FakeMethod(result='Some text')
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(blob='Some text'))
         ret = repository.getBlob('src/README.txt')
         self.assertEqual('Some text', ret)
 
     def test_getBlob_with_rev(self):
         repository = self.factory.makeGitRepository()
-        hosting_client = FakeMethod()
-        hosting_client.getBlob = FakeMethod(result='Some text')
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(blob='Some text'))
         ret = repository.getBlob('src/README.txt', 'some-rev')
         self.assertEqual('Some text', ret)
 
@@ -2485,6 +2480,10 @@
 
     layer = DatabaseFunctionalLayer
 
+    def setUp(self):
+        super(TestGitRepositoryWebservice, self).setUp()
+        self.useFixture(GitHostingFixture())
+
     def test_urls(self):
         owner_db = self.factory.makePerson(name="person")
         project_db = self.factory.makeProduct(name="project")

=== modified file 'lib/lp/code/stories/branches/xx-code-review-comments.txt'
--- lib/lp/code/stories/branches/xx-code-review-comments.txt	2016-05-13 17:59:35 +0000
+++ lib/lp/code/stories/branches/xx-code-review-comments.txt	2016-09-05 16:03:58 +0000
@@ -191,41 +191,41 @@
 The same thing works for Git.  Note that the hosting client returns newest
 log entries first.
 
-    >>> from lp.code.interfaces.githosting import IGitHostingClient
-    >>> from lp.testing.fakemethod import FakeMethod
-    >>> from lp.testing.fixture import ZopeUtilityFixture
+    >>> from lp.code.tests.helpers import GitHostingFixture
 
     >>> login('admin@xxxxxxxxxxxxx')
-    >>> bmp = factory.makeBranchMergeProposalForGit()
-    >>> bmp.requestReview(review_date)
+    >>> registrant = factory.makePerson()
     >>> epoch = datetime.fromtimestamp(0, tz=pytz.UTC)
     >>> commit_date = review_date + timedelta(days=1)
-    >>> hosting_client = FakeMethod()
-    >>> hosting_client.getLog = FakeMethod(result=[])
+    >>> hosting_fixture = GitHostingFixture()
     >>> for i in range(2):
-    ...     hosting_client.getLog.result.insert(0, {
+    ...     hosting_fixture.getLog.result.insert(0, {
     ...         u'sha1': unicode(i * 2) * 40,
     ...         u'message': u'Testing commits in conversation',
     ...         u'author': {
-    ...             u'name': bmp.registrant.display_name,
-    ...             u'email': bmp.registrant.preferredemail.email,
+    ...             u'name': registrant.display_name,
+    ...             u'email': registrant.preferredemail.email,
     ...             u'time': int((commit_date - epoch).total_seconds()),
     ...             },
     ...         })
-    ...     hosting_client.getLog.result.insert(0, {
+    ...     hosting_fixture.getLog.result.insert(0, {
     ...         u'sha1': unicode(i * 2 + 1) * 40,
     ...         u'message': u'and it works!',
     ...         u'author': {
-    ...             u'name': bmp.registrant.display_name,
-    ...             u'email': bmp.registrant.preferredemail.email,
+    ...             u'name': registrant.display_name,
+    ...             u'email': registrant.preferredemail.email,
     ...             u'time': int((commit_date - epoch).total_seconds()),
     ...             },
     ...         })
     ...     commit_date += timedelta(days=1)
-    >>> url = canonical_url(bmp)
     >>> logout()
 
-    >>> with ZopeUtilityFixture(hosting_client, IGitHostingClient):
+    >>> with hosting_fixture:
+    ...     login('admin@xxxxxxxxxxxxx')
+    ...     bmp = factory.makeBranchMergeProposalForGit(registrant=registrant)
+    ...     bmp.requestReview(review_date)
+    ...     url = canonical_url(bmp)
+    ...     logout()
     ...     browser.open(url)
     >>> print_tag_with_id(browser.contents, 'conversation')
     ~.../+git/...:... updated on 2009-09-12 ...

=== modified file 'lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt'
--- lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt	2016-05-13 21:12:59 +0000
+++ lib/lp/code/stories/sourcepackagerecipes/xx-recipe-listings.txt	2016-09-05 16:03:58 +0000
@@ -125,13 +125,9 @@
 If we start from one of the branches instead, then only two recipes are
 listed.
 
-    >>> from lp.code.interfaces.githosting import IGitHostingClient
-    >>> from lp.testing.fakemethod import FakeMethod
-    >>> from lp.testing.fixture import ZopeUtilityFixture
+    >>> from lp.code.tests.helpers import GitHostingFixture
 
-    >>> hosting_client = FakeMethod()
-    >>> hosting_client.getLog = FakeMethod(result=[])
-    >>> with ZopeUtilityFixture(hosting_client, IGitHostingClient):
+    >>> with GitHostingFixture():
     ...     nopriv_browser.open(ref1_url)
     >>> nopriv_browser.getLink('2 recipes').click()
     >>> print nopriv_browser.url

=== modified file 'lib/lp/code/subscribers/branchmergeproposal.py'
--- lib/lp/code/subscribers/branchmergeproposal.py	2016-09-02 14:36:28 +0000
+++ lib/lp/code/subscribers/branchmergeproposal.py	2016-09-05 16:03:58 +0000
@@ -66,7 +66,8 @@
     Update related bug links based on commits in the source branch; create a
     job to update the diff for the merge proposal; trigger webhooks.
     """
-    merge_proposal.updateRelatedBugsFromSource()
+    if merge_proposal.source_git_ref is not None:
+        merge_proposal.updateRelatedBugsFromSource()
     getUtility(IUpdatePreviewDiffJobSource).create(merge_proposal)
     if getFeatureFlag(BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG):
         payload = {

=== modified file 'lib/lp/code/tests/helpers.py'
--- lib/lp/code/tests/helpers.py	2015-09-28 17:38:45 +0000
+++ lib/lp/code/tests/helpers.py	2016-09-05 16:03:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Helper functions for code testing live here."""
@@ -23,6 +23,7 @@
 from itertools import count
 
 from bzrlib.plugins.builder.recipe import RecipeParser
+import fixtures
 import transaction
 from zope.component import getUtility
 from zope.security.proxy import (
@@ -34,6 +35,7 @@
 from lp.code.interfaces.branchmergeproposal import (
     IBranchMergeProposalJobSource,
     )
+from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
 from lp.code.interfaces.revision import IRevisionSet
 from lp.code.model.seriessourcepackagebranch import (
@@ -42,10 +44,13 @@
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.database.sqlbase import cursor
+from lp.services.memcache.testing import MemcacheFixture
 from lp.testing import (
     run_with_login,
     time_counter,
     )
+from lp.testing.fakemethod import FakeMethod
+from lp.testing.fixture import ZopeUtilityFixture
 
 
 def mark_all_merge_proposal_jobs_done():
@@ -291,3 +296,38 @@
     c.execute('delete from codeimportjob')
     c.execute('delete from codeimport')
     c.execute('delete from branch')
+
+
+class GitHostingFixture(fixtures.Fixture):
+    """A fixture that temporarily registers a fake Git hosting client."""
+
+    def __init__(self, default_branch=u"refs/heads/master",
+                 refs=None, commits=None, log=None, diff=None, merge_diff=None,
+                 merges=None, blob=None, disable_memcache=True):
+        self.create = FakeMethod()
+        self.getProperties = FakeMethod(
+            result={u"default_branch": default_branch})
+        self.setProperties = FakeMethod()
+        self.getRefs = FakeMethod(result=({} if refs is None else refs))
+        self.getCommits = FakeMethod(
+            result=([] if commits is None else commits))
+        self.getLog = FakeMethod(result=([] if log is None else log))
+        self.getDiff = FakeMethod(result=({} if diff is None else diff))
+        self.getMergeDiff = FakeMethod(
+            result={} if merge_diff is None else merge_diff)
+        self.detectMerges = FakeMethod(
+            result=({} if merges is None else merges))
+        self.getBlob = FakeMethod(result=blob)
+        self.delete = FakeMethod()
+        self.disable_memcache = disable_memcache
+
+    def setUp(self):
+        super(GitHostingFixture, self).setUp()
+        self.useFixture(ZopeUtilityFixture(self, IGitHostingClient))
+        if self.disable_memcache:
+            # Most tests that involve GitRef._getLog don't want to cache the
+            # result: doing so requires more time-consuming test setup and
+            # makes it awkward to repeat the same call with different log
+            # responses.  For convenience, we make it easy to disable that
+            # here.
+            self.useFixture(MemcacheFixture())

=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
--- lib/lp/code/xmlrpc/tests/test_git.py	2015-07-10 04:51:21 +0000
+++ lib/lp/code/xmlrpc/tests/test_git.py	2016-09-05 16:03:58 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the internal Git API."""
@@ -6,7 +6,6 @@
 __metaclass__ = type
 
 from zope.component import getUtility
-from zope.interface import implementer
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
@@ -16,12 +15,12 @@
     LAUNCHPAD_SERVICES,
     )
 from lp.code.interfaces.gitcollection import IAllGitRepositories
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitjob import IGitRefScanJobSource
 from lp.code.interfaces.gitrepository import (
     GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE,
     IGitRepositorySet,
     )
+from lp.code.tests.helpers import GitHostingFixture
 from lp.code.xmlrpc.git import GitAPI
 from lp.registry.enums import TeamMembershipPolicy
 from lp.services.webapp.escaping import html_escape
@@ -32,7 +31,6 @@
     person_logged_in,
     TestCaseWithFactory,
     )
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     AppServerLayer,
     LaunchpadFunctionalLayer,
@@ -40,34 +38,13 @@
 from lp.xmlrpc import faults
 
 
-@implementer(IGitHostingClient)
-class FakeGitHostingClient:
-    """A GitHostingClient lookalike that just logs calls."""
-
-    def __init__(self):
-        self.calls = []
-
-    def create(self, path, clone_from=None):
-        self.calls.append(("create", path, clone_from))
-
-
-@implementer(IGitHostingClient)
-class BrokenGitHostingClient:
-    """A GitHostingClient lookalike that pretends the remote end is down."""
-
-    def create(self, path, clone_from=None):
-        raise GitRepositoryCreationFault("nothing here")
-
-
 class TestGitAPIMixin:
     """Helper methods for `IGitAPI` tests, and security-relevant tests."""
 
     def setUp(self):
         super(TestGitAPIMixin, self).setUp()
         self.git_api = GitAPI(None, None)
-        self.hosting_client = FakeGitHostingClient()
-        self.useFixture(
-            ZopeUtilityFixture(self.hosting_client, IGitHostingClient))
+        self.hosting_fixture = self.useFixture(GitHostingFixture())
         self.repository_set = getUtility(IGitRepositorySet)
 
     def assertGitRepositoryNotFound(self, requester, path, permission="read",
@@ -174,15 +151,16 @@
              "trailing": "", "private": private},
             translation)
         self.assertEqual(
-            ("create", repository.getInternalPath()),
-            self.hosting_client.calls[0][0:2])
+            (repository.getInternalPath(),),
+            self.hosting_fixture.create.extract_args()[0])
         return repository
 
     def assertCreatesFromClone(self, requester, path, cloned_from,
                                can_authenticate=False):
         self.assertCreates(requester, path, can_authenticate)
         self.assertEqual(
-            cloned_from.getInternalPath(), self.hosting_client.calls[0][2])
+            {"clone_from": cloned_from.getInternalPath()},
+            self.hosting_fixture.create.extract_kwargs()[0])
 
     def test_translatePath_private_repository(self):
         requester = self.factory.makePerson()
@@ -610,8 +588,8 @@
     def test_translatePath_create_broken_hosting_service(self):
         # If the hosting service is down, trying to create a repository
         # fails and doesn't leave junk around in the Launchpad database.
-        hosting_client = BrokenGitHostingClient()
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.hosting_fixture.create.failure = GitRepositoryCreationFault(
+            "nothing here")
         requester = self.factory.makePerson()
         initial_count = getUtility(IAllGitRepositories).count()
         oops_id = self.assertOopsOccurred(

=== added file 'lib/lp/services/memcache/testing.py'
--- lib/lp/services/memcache/testing.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/memcache/testing.py	2016-09-05 16:03:58 +0000
@@ -0,0 +1,37 @@
+# Copyright 2016 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+    'MemcacheFixture',
+    ]
+
+import fixtures
+
+from lp.services.memcache.interfaces import IMemcacheClient
+from lp.testing.fixture import ZopeUtilityFixture
+
+
+class MemcacheFixture(fixtures.Fixture):
+    """A trivial in-process memcache fixture."""
+
+    def __init__(self):
+        self._cache = {}
+
+    def setUp(self):
+        super(MemcacheFixture, self).setUp()
+        self.useFixture(ZopeUtilityFixture(self, IMemcacheClient))
+
+    def get(self, key):
+        return self._cache.get(key)
+
+    def set(self, key, val):
+        self._cache[key] = val
+        return 1
+
+    def delete(self, key):
+        self._cache.pop(key, None)
+        return 1
+
+    def clear(self):
+        self._cache = {}

=== modified file 'lib/lp/snappy/browser/tests/test_hassnaps.py'
--- lib/lp/snappy/browser/tests/test_hassnaps.py	2016-07-18 13:48:42 +0000
+++ lib/lp/snappy/browser/tests/test_hassnaps.py	2016-09-05 16:03:58 +0000
@@ -12,16 +12,11 @@
     )
 
 from lp.code.interfaces.branch import IBranch
-from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitrepository import IGitRepository
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.webapp import canonical_url
 from lp.testing import TestCaseWithFactory
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
-from lp.testing.layers import (
-    DatabaseFunctionalLayer,
-    LaunchpadFunctionalLayer,
-    )
+from lp.testing.layers import DatabaseFunctionalLayer
 from lp.testing.views import create_initialized_view
 
 
@@ -96,27 +91,23 @@
 
 class TestHasSnapsMenu(WithScenarios, TestCaseWithFactory):
 
-    needs_hosting_client = False
+    layer = DatabaseFunctionalLayer
+    needs_hosting_fixture = False
 
     scenarios = [
         ("Branch", {
-            "layer": DatabaseFunctionalLayer,
             "context_factory": make_branch,
             }),
         ("GitRef", {
-            "layer": LaunchpadFunctionalLayer,
             "context_factory": make_git_ref,
-            "needs_hosting_client": True,
+            "needs_hosting_fixture": True,
             }),
         ]
 
     def setUp(self):
         super(TestHasSnapsMenu, self).setUp()
-        if self.needs_hosting_client:
-            hosting_client = FakeMethod()
-            hosting_client.getLog = FakeMethod(result=[])
-            self.useFixture(
-                ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        if self.needs_hosting_fixture:
+            self.useFixture(GitHostingFixture())
 
     def makeSnap(self, context):
         if IBranch.providedBy(context):

=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py	2016-08-12 12:56:41 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py	2016-09-05 16:03:58 +0000
@@ -41,7 +41,7 @@
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.processor import IProcessorSet
 from lp.code.errors import GitRepositoryScanFault
-from lp.code.interfaces.githosting import IGitHostingClient
+from lp.code.tests.helpers import GitHostingFixture
 from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
@@ -251,9 +251,7 @@
             MatchesTagText(content, "store_upload"))
 
     def test_create_new_snap_git(self):
-        hosting_client = FakeMethod()
-        hosting_client.getBlob = FakeMethod(result="")
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture(blob=""))
         [git_ref] = self.factory.makeGitRefs()
         source_display = git_ref.display_name
         browser = self.getViewBrowser(

=== modified file 'lib/lp/snappy/browser/tests/test_snaplisting.py'
--- lib/lp/snappy/browser/tests/test_snaplisting.py	2016-06-30 17:25:25 +0000
+++ lib/lp/snappy/browser/tests/test_snaplisting.py	2016-09-05 16:03:58 +0000
@@ -8,7 +8,7 @@
 import soupmatchers
 from testtools.matchers import Not
 
-from lp.code.interfaces.githosting import IGitHostingClient
+from lp.code.tests.helpers import GitHostingFixture
 from lp.services.database.constants import (
     ONE_DAY_AGO,
     UTC_NOW,
@@ -21,8 +21,6 @@
     person_logged_in,
     record_two_runs,
     )
-from lp.testing.fakemethod import FakeMethod
-from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import LaunchpadFunctionalLayer
 from lp.testing.matchers import HasQueryCount
 
@@ -57,9 +55,7 @@
         self.assertSnapsLink(repository, "2 snap packages", git_ref=ref)
 
     def test_git_ref_links_to_snaps(self):
-        hosting_client = FakeMethod()
-        hosting_client.getLog = FakeMethod(result=[])
-        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.useFixture(GitHostingFixture())
         [ref] = self.factory.makeGitRefs()
         self.assertSnapsLink(ref, "2 snap packages", git_ref=ref)
 


Follow ups