← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/git-hosting-utility into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-hosting-utility into lp:launchpad.

Commit message:
Turn GitHostingClient into a utility.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-hosting-utility/+merge/261868

Turn GitHostingClient into a utility.  It's much simpler to use ZopeUtilityFixture for testability than to pass hosting clients around everywhere in instance attributes and method parameters.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-hosting-utility into lp:launchpad.
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2015-06-06 08:49:54 +0000
+++ lib/lp/code/configure.zcml	2015-06-12 16:28:49 +0000
@@ -938,6 +938,13 @@
   <adapter factory="lp.code.model.gitlookup.DistributionGitTraversable" />
   <adapter factory="lp.code.model.gitlookup.DistributionSourcePackageGitTraversable" />
 
+  <!-- Git hosting -->
+  <securedutility
+      component="lp.code.model.githosting.GitHostingClient"
+      provides="lp.code.interfaces.githosting.IGitHostingClient">
+    <allow interface="lp.code.interfaces.githosting.IGitHostingClient" />
+  </securedutility>
+
   <!-- Git-related jobs -->
   <class class="lp.code.model.gitjob.GitJob">
     <allow interface="lp.code.interfaces.gitjob.IGitJob" />

=== added file 'lib/lp/code/interfaces/githosting.py'
--- lib/lp/code/interfaces/githosting.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/githosting.py	2015-06-12 16:28:49 +0000
@@ -0,0 +1,63 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interface for communication with the Git hosting service."""
+
+__metaclass__ = type
+__all__ = [
+    'IGitHostingClient',
+    ]
+
+from zope.interface import Interface
+
+
+class IGitHostingClient(Interface):
+    """Interface for the internal API provided by the Git hosting service."""
+
+    def create(path, clone_from=None):
+        """Create a Git repository.
+
+        :param path: Physical path of the new repository on the hosting
+            service.
+        :param clone_from: If not None, clone the new repository from this
+            other physical path.
+        """
+
+    def getRefs(path):
+        """Get all refs in this repository.
+
+        :param path: Physical path of the repository on the hosting service.
+        :return: A dict mapping ref paths to dicts representing the objects
+            they point to.
+        """
+
+    def getCommits(path, commit_oids, logger=None):
+        """Get details of a list of commits.
+
+        :param path: Physical path of the repository on the hosting service.
+        :param commit_oids: A list of commit OIDs.
+        :param logger: An optional logger.
+        :return: A list of dicts each of which represents one of the
+            requested commits.  Non-existent commits will be omitted.
+        """
+
+    def getMergeDiff(path, base, head, logger=None):
+        """Get the merge preview diff between two commits.
+
+        :param path: Physical path of the repository on the hosting service.
+        :param base: The OID of the base commit.
+        :param head: The OID of the commit that we want to merge into
+            'base'.
+        :param logger: An optional logger.
+        :return: A dict mapping 'commits' to a list of commits between
+            'base' and 'head' (formatted as with `getCommits`), 'patch' to
+            the text of the diff between 'base' and 'head', and 'conflicts'
+            to a list of conflicted paths.
+        """
+
+    def delete(path, logger=None):
+        """Delete a repository.
+
+        :param path: Physical path of the repository on the hosting service.
+        :param logger: An optional logger.
+        """

=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py	2015-06-12 08:20:17 +0000
+++ lib/lp/code/interfaces/gitrepository.py	2015-06-12 16:28:49 +0000
@@ -266,10 +266,9 @@
         :params paths: An iterable of paths.
         """
 
-    def planRefChanges(hosting_client, hosting_path, logger=None):
+    def planRefChanges(hosting_path, logger=None):
         """Plan ref changes based on information from the hosting service.
 
-        :param hosting_client: A `GitHostingClient`.
         :param hosting_path: A path on the hosting service.
         :param logger: An optional logger.
 
@@ -278,10 +277,9 @@
             paths to remove.
         """
 
-    def fetchRefCommits(hosting_client, hosting_path, refs, logger=None):
+    def fetchRefCommits(hosting_path, refs, logger=None):
         """Fetch commit information from the hosting service for a set of refs.
 
-        :param hosting_client: A `GitHostingClient`.
         :param hosting_path: A path on the hosting service.
         :param refs: A dict mapping ref paths to dictionaries of their
             fields; the field dictionaries will be updated with any detailed

=== modified file 'lib/lp/code/model/diff.py'
--- lib/lp/code/model/diff.py	2015-04-30 22:06:11 +0000
+++ lib/lp/code/model/diff.py	2015-06-12 16:28:49 +0000
@@ -42,12 +42,12 @@
 from zope.interface import implements
 
 from lp.app.errors import NotFoundError
-from lp.code.githosting import GitHostingClient
 from lp.code.interfaces.diff import (
     IDiff,
     IIncrementalDiff,
     IPreviewDiff,
     )
+from lp.code.interfaces.githosting import IGitHostingClient
 from lp.codehosting.bzrutils import read_locked
 from lp.services.config import config
 from lp.services.database.constants import UTC_NOW
@@ -417,10 +417,6 @@
     def has_conflicts(self):
         return self.conflicts is not None and self.conflicts != ''
 
-    @staticmethod
-    def getGitHostingClient():
-        return GitHostingClient(config.codehosting.internal_git_api_endpoint)
-
     @classmethod
     def fromBranchMergeProposal(cls, bmp):
         """Create a `PreviewDiff` from a `BranchMergeProposal`.
@@ -449,7 +445,6 @@
             preview.conflicts = u''.join(
                 unicode(conflict) + '\n' for conflict in conflicts)
         else:
-            hosting_client = cls.getGitHostingClient()
             source_repository = bmp.source_git_repository
             target_repository = bmp.target_git_repository
             if source_repository == target_repository:
@@ -460,7 +455,7 @@
                     source_repository.getInternalPath())
             # XXX cjwatson 2015-04-30: Add prerequisite handling once turnip
             # supports it.
-            response = hosting_client.getMergeDiff(
+            response = getUtility(IGitHostingClient).getMergeDiff(
                 path, bmp.target_git_ref.commit_sha1,
                 bmp.source_git_ref.commit_sha1)
             source_revision_id = bmp.source_git_ref.commit_sha1

=== renamed file 'lib/lp/code/githosting.py' => 'lib/lp/code/model/githosting.py'
--- lib/lp/code/githosting.py	2015-05-19 11:29:24 +0000
+++ lib/lp/code/model/githosting.py	2015-06-12 16:28:49 +0000
@@ -12,19 +12,24 @@
 from urlparse import urljoin
 
 import requests
+from zope.interface import implements
 
 from lp.code.errors import (
     GitRepositoryCreationFault,
     GitRepositoryDeletionFault,
     GitRepositoryScanFault,
     )
+from lp.code.interfaces.githosting import IGitHostingClient
+from lp.services.config import config
 
 
 class GitHostingClient:
     """A client for the internal API provided by the Git hosting system."""
 
-    def __init__(self, endpoint):
-        self.endpoint = endpoint
+    implements(IGitHostingClient)
+
+    def __init__(self):
+        self.endpoint = config.codehosting.internal_git_api_endpoint
 
     def _makeSession(self):
         session = requests.Session()

=== modified file 'lib/lp/code/model/gitjob.py'
--- lib/lp/code/model/gitjob.py	2015-05-26 13:55:10 +0000
+++ lib/lp/code/model/gitjob.py	2015-06-12 16:28:49 +0000
@@ -23,13 +23,14 @@
     SQL,
     Store,
     )
+from zope.component import getUtility
 from zope.interface import (
     classProvides,
     implements,
     )
 
 from lp.app.errors import NotFoundError
-from lp.code.githosting import GitHostingClient
+from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitjob import (
     IGitJob,
     IGitRefScanJob,
@@ -198,11 +199,6 @@
         job.celeryRunOnCommit()
         return job
 
-    def __init__(self, git_job):
-        super(GitRefScanJob, self).__init__(git_job)
-        self._hosting_client = GitHostingClient(
-            config.codehosting.internal_git_api_endpoint)
-
     def run(self):
         """See `IGitRefScanJob`."""
         try:
@@ -211,11 +207,9 @@
                     Store.of(self.repository)):
                 hosting_path = self.repository.getInternalPath()
                 refs_to_upsert, refs_to_remove = (
-                    self.repository.planRefChanges(
-                        self._hosting_client, hosting_path, logger=log))
+                    self.repository.planRefChanges(hosting_path, logger=log))
                 self.repository.fetchRefCommits(
-                    self._hosting_client, hosting_path, refs_to_upsert,
-                    logger=log)
+                    hosting_path, refs_to_upsert, logger=log)
                 self.repository.synchroniseRefs(refs_to_upsert, refs_to_remove)
         except LostObjectError:
             log.info(
@@ -250,14 +244,9 @@
         job.celeryRunOnCommit()
         return job
 
-    def __init__(self, git_job):
-        super(ReclaimGitRepositorySpaceJob, self).__init__(git_job)
-        self._hosting_client = GitHostingClient(
-            config.codehosting.internal_git_api_endpoint)
-
     @property
     def repository_path(self):
         return self.metadata["repository_path"]
 
     def run(self):
-        self._hosting_client.delete(self.repository_path, logger=log)
+        getUtility(IGitHostingClient).delete(self.repository_path, logger=log)

=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py	2015-06-12 08:20:17 +0000
+++ lib/lp/code/model/gitrepository.py	2015-06-12 16:28:49 +0000
@@ -76,6 +76,7 @@
     IAllGitRepositories,
     IGitCollection,
     )
+from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitlookup import IGitLookup
 from lp.code.interfaces.gitnamespace import (
     get_git_namespace,
@@ -519,8 +520,9 @@
             GitRef.repository == self, GitRef.path.is_in(paths)).remove()
         self.date_last_modified = UTC_NOW
 
-    def planRefChanges(self, hosting_client, hosting_path, logger=None):
+    def planRefChanges(self, hosting_path, logger=None):
         """See `IGitRepository`."""
+        hosting_client = getUtility(IGitHostingClient)
         new_refs = {}
         for path, info in hosting_client.getRefs(hosting_path).items():
             try:
@@ -550,12 +552,12 @@
         return refs_to_upsert, refs_to_remove
 
     @staticmethod
-    def fetchRefCommits(hosting_client, hosting_path, refs, logger=None):
+    def fetchRefCommits(hosting_path, refs, logger=None):
         """See `IGitRepository`."""
         oids = sorted(set(info["sha1"] for info in refs.values()))
         commits = {
             commit.get("sha1"): commit
-            for commit in hosting_client.getCommits(
+            for commit in getUtility(IGitHostingClient).getCommits(
                 hosting_path, oids, logger=logger)}
         authors_to_acquire = []
         committers_to_acquire = []

=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py	2015-05-14 13:57:51 +0000
+++ lib/lp/code/model/tests/test_diff.py	2015-06-12 16:28:49 +0000
@@ -16,8 +16,8 @@
     parse_patches,
     RemoveLine,
     )
-from fixtures import MonkeyPatch
 import transaction
+from zope.interface import implements
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.errors import NotFoundError
@@ -26,6 +26,7 @@
     IIncrementalDiff,
     IPreviewDiff,
     )
+from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.model.diff import (
     Diff,
     PreviewDiff,
@@ -43,6 +44,7 @@
     verifyObject,
     )
 from lp.testing.fakemethod import FakeMethod
+from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
@@ -90,7 +92,8 @@
 
 
 class FakeGitHostingClient:
-    pass
+
+    implements(IGitHostingClient)
 
 
 class DiffTestCase(TestCaseWithFactory):
@@ -139,9 +142,7 @@
         hosting_client = FakeGitHostingClient()
         hosting_client.getMergeDiff = FakeMethod(
             result=result, failure=failure)
-        self.useFixture(MonkeyPatch(
-            "lp.code.model.diff.PreviewDiff.getGitHostingClient",
-            staticmethod(lambda: hosting_client)))
+        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
 
     def createExampleGitMerge(self):
         """Create an example Git-based merge scenario.

=== modified file 'lib/lp/code/model/tests/test_gitjob.py'
--- lib/lp/code/model/tests/test_gitjob.py	2015-05-26 13:35:50 +0000
+++ lib/lp/code/model/tests/test_gitjob.py	2015-06-12 16:28:49 +0000
@@ -16,9 +16,11 @@
     MatchesSetwise,
     MatchesStructure,
     )
+from zope.interface import implements
 from zope.security.proxy import removeSecurityProxy
 
 from lp.code.enums import GitObjectType
+from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitjob import (
     IGitJob,
     IGitRefScanJob,
@@ -39,12 +41,18 @@
     )
 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,
     )
 
 
+class FakeGitHostingClient:
+
+    implements(IGitHostingClient)
+
+
 class TestGitJob(TestCaseWithFactory):
     """Tests for `GitJob`."""
 
@@ -129,26 +137,28 @@
 
     def test_run(self):
         # Ensure the job scans the repository.
+        hosting_client = FakeGitHostingClient()
+        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
         repository = self.factory.makeGitRepository()
         job = GitRefScanJob.create(repository)
         paths = (u"refs/heads/master", u"refs/tags/1.0")
-        job._hosting_client.getRefs = FakeMethod(
-            result=self.makeFakeRefs(paths))
+        hosting_client.getRefs = FakeMethod(result=self.makeFakeRefs(paths))
         author = repository.owner
         author_date_start = datetime(2015, 01, 01, tzinfo=pytz.UTC)
         author_date_gen = time_counter(author_date_start, timedelta(days=1))
-        job._hosting_client.getCommits = FakeMethod(
+        hosting_client.getCommits = FakeMethod(
             result=self.makeFakeCommits(author, author_date_gen, paths))
         with dbuser("branchscanner"):
             JobRunner([job]).runAll()
         self.assertRefsMatch(repository.refs, repository, paths)
 
     def test_logs_bad_ref_info(self):
+        hosting_client = FakeGitHostingClient()
+        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
         repository = self.factory.makeGitRepository()
         job = GitRefScanJob.create(repository)
-        job._hosting_client.getRefs = FakeMethod(
-            result={u"refs/heads/master": {}})
-        job._hosting_client.getCommits = FakeMethod(result=[])
+        hosting_client.getRefs = FakeMethod(result={u"refs/heads/master": {}})
+        hosting_client.getCommits = FakeMethod(result=[])
         expected_message = (
             'Unconvertible ref refs/heads/master {}: '
             'ref info does not contain "object" key')
@@ -206,15 +216,17 @@
     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))
         name = "/~owner/+git/gone"
         path = "1"
         job = ReclaimGitRepositorySpaceJob.create(name, path)
         self.makeJobReady(job)
         [job] = list(ReclaimGitRepositorySpaceJob.iterReady())
         with dbuser("branchscanner"):
-            job._hosting_client.delete = FakeMethod()
+            hosting_client.delete = FakeMethod()
             JobRunner([job]).runAll()
-        self.assertEqual([(path,)], job._hosting_client.delete.extract_args())
+        self.assertEqual([(path,)], hosting_client.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_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py	2015-06-12 08:20:17 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py	2015-06-12 16:28:49 +0000
@@ -14,15 +14,15 @@
 from bzrlib import urlutils
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
+import pytz
 from sqlobject import SQLObjectNotFound
 from storm.store import Store
-import transaction
-import pytz
 from testtools.matchers import (
     EndsWith,
     MatchesSetwise,
     MatchesStructure,
     )
+import transaction
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import providedBy
@@ -53,6 +53,7 @@
     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
 from lp.code.interfaces.gitlookup import IGitLookup
 from lp.code.interfaces.gitnamespace import (
@@ -118,6 +119,7 @@
     )
 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,
@@ -1052,8 +1054,8 @@
                     },
                 },
             })
-        refs_to_upsert, refs_to_remove = repository.planRefChanges(
-            hosting_client, "dummy")
+        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        refs_to_upsert, refs_to_remove = repository.planRefChanges("dummy")
 
         expected_upsert = {
             u"refs/heads/master": {
@@ -1094,8 +1096,8 @@
                     },
                 },
             })
-        self.assertEqual(
-            ({}, set()), repository.planRefChanges(hosting_client, "dummy"))
+        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
+        self.assertEqual(({}, set()), repository.planRefChanges("dummy"))
 
     def test_fetchRefCommits(self):
         # fetchRefCommits fetches detailed tip commit metadata for the
@@ -1126,6 +1128,7 @@
                 u"parents": [],
                 u"tree": unicode(hashlib.sha1("").hexdigest()),
                 }])
+        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
         refs = {
             u"refs/heads/master": {
                 u"sha1": master_sha1,
@@ -1136,7 +1139,7 @@
                 u"type": GitObjectType.COMMIT,
                 },
             }
-        GitRepository.fetchRefCommits(hosting_client, "dummy", refs)
+        GitRepository.fetchRefCommits("dummy", refs)
 
         expected_oids = [master_sha1, foo_sha1]
         [(_, observed_oids)] = hosting_client.getCommits.extract_args()

=== modified file 'lib/lp/code/xmlrpc/git.py'
--- lib/lp/code/xmlrpc/git.py	2015-05-26 10:54:35 +0000
+++ lib/lp/code/xmlrpc/git.py	2015-06-12 16:28:49 +0000
@@ -22,15 +22,16 @@
 from lp.app.validators import LaunchpadValidationError
 from lp.code.errors import (
     GitRepositoryCreationException,
+    GitRepositoryCreationFault,
     GitRepositoryCreationForbidden,
-    GitRepositoryCreationFault,
     GitRepositoryExists,
     GitTargetError,
     InvalidNamespace,
     )
-from lp.code.githosting import GitHostingClient
 from lp.code.interfaces.codehosting import LAUNCHPAD_ANONYMOUS
 from lp.code.interfaces.gitapi import IGitAPI
+from lp.code.interfaces.githosting import IGitHostingClient
+from lp.code.interfaces.gitjob import IGitRefScanJobSource
 from lp.code.interfaces.gitlookup import (
     IGitLookup,
     IGitTraverser,
@@ -40,7 +41,6 @@
     split_git_unique_name,
     )
 from lp.code.interfaces.gitrepository import IGitRepositorySet
-from lp.code.interfaces.gitjob import IGitRefScanJobSource
 from lp.code.xmlrpc.codehosting import run_with_login
 from lp.registry.errors import (
     InvalidName,
@@ -52,7 +52,6 @@
     NoSuchProduct,
     )
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
-from lp.services.config import config
 from lp.services.webapp import LaunchpadXMLRPCView
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.errorlog import ScriptRequest
@@ -67,8 +66,6 @@
 
     def __init__(self, *args, **kwargs):
         super(GitAPI, self).__init__(*args, **kwargs)
-        self.hosting_client = GitHostingClient(
-            config.codehosting.internal_git_api_endpoint)
         self.repository_set = getUtility(IGitRepositorySet)
 
     def _performLookup(self, path):
@@ -211,15 +208,16 @@
                 else:
                     default = self.repository_set.getDefaultRepositoryForOwner(
                         repository.owner, repository.target)
-                    if default is not None and default.visibleByUser(requester):
+                    if (default is not None and
+                            default.visibleByUser(requester)):
                         target_path = default.getInternalPath()
             except GitTargetError:
                 pass  # Ignore Personal repositories.
 
             hosting_path = repository.getInternalPath()
             try:
-                self.hosting_client.create(hosting_path,
-                                           clone_from=target_path)
+                getUtility(IGitHostingClient).create(
+                    hosting_path, clone_from=target_path)
             except GitRepositoryCreationFault as e:
                 # The hosting service failed.  Log an OOPS for investigation.
                 self._reportError(path, e, hosting_path=hosting_path)

=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
--- lib/lp/code/xmlrpc/tests/test_git.py	2015-05-26 10:54:35 +0000
+++ lib/lp/code/xmlrpc/tests/test_git.py	2015-06-12 16:28:49 +0000
@@ -6,6 +6,7 @@
 __metaclass__ = type
 
 from zope.component import getUtility
+from zope.interface import implements
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
@@ -15,6 +16,7 @@
     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,
@@ -30,6 +32,7 @@
     person_logged_in,
     TestCaseWithFactory,
     )
+from lp.testing.fixture import ZopeUtilityFixture
 from lp.testing.layers import (
     AppServerLayer,
     LaunchpadFunctionalLayer,
@@ -40,6 +43,8 @@
 class FakeGitHostingClient:
     """A GitHostingClient lookalike that just logs calls."""
 
+    implements(IGitHostingClient)
+
     def __init__(self):
         self.calls = []
 
@@ -50,6 +55,8 @@
 class BrokenGitHostingClient:
     """A GitHostingClient lookalike that pretends the remote end is down."""
 
+    implements(IGitHostingClient)
+
     def create(self, path, clone_from=None):
         raise GitRepositoryCreationFault("nothing here")
 
@@ -60,7 +67,9 @@
     def setUp(self):
         super(TestGitAPIMixin, self).setUp()
         self.git_api = GitAPI(None, None)
-        self.git_api.hosting_client = FakeGitHostingClient()
+        self.hosting_client = FakeGitHostingClient()
+        self.useFixture(
+            ZopeUtilityFixture(self.hosting_client, IGitHostingClient))
         self.repository_set = getUtility(IGitRepositorySet)
 
     def assertPathTranslationError(self, requester, path, permission="read",
@@ -167,15 +176,14 @@
             translation)
         self.assertEqual(
             ("create", repository.getInternalPath()),
-            self.git_api.hosting_client.calls[0][0:2])
+            self.hosting_client.calls[0][0:2])
         return repository
 
     def assertCreatesFromClone(self, requester, path, cloned_from,
                                can_authenticate=False):
         self.assertCreates(requester, path, can_authenticate)
         self.assertEqual(
-            cloned_from.getInternalPath(),
-            self.git_api.hosting_client.calls[0][2])
+            cloned_from.getInternalPath(), self.hosting_client.calls[0][2])
 
     def test_translatePath_private_repository(self):
         requester = self.factory.makePerson()
@@ -603,7 +611,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.
-        self.git_api.hosting_client = BrokenGitHostingClient()
+        hosting_client = BrokenGitHostingClient()
+        self.useFixture(ZopeUtilityFixture(hosting_client, IGitHostingClient))
         requester = self.factory.makePerson()
         initial_count = getUtility(IAllGitRepositories).count()
         oops_id = self.assertOopsOccurred(

=== modified file 'lib/lp/testing/fixture.py'
--- lib/lp/testing/fixture.py	2013-06-20 05:50:00 +0000
+++ lib/lp/testing/fixture.py	2015-06-12 16:28:49 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Launchpad test fixtures that have no better home."""
@@ -228,7 +228,7 @@
 class ZopeUtilityFixture(Fixture):
     """A fixture that temporarily registers a different utility."""
 
-    def __init__(self, component, intf, name):
+    def __init__(self, component, intf, name=""):
         """Construct a new fixture.
 
         :param component: An instance of a class that provides this


Follow ups