← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/treq into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/treq into lp:launchpad.

Commit message:
Port from the deprecated twisted.web.client.getPage to treq.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/treq/+merge/348808

twisted.web.client.getPage was deprecated in Twisted 17.1.0, so we should stop using it.

treq is disappointingly more verbose in places, though I've done my best to keep that to a minimum.  That said, it definitely has its good points (e.g. decent JSON handling), and the changes to the SnapBuildBehaviour tests make them clearly better by using a double for the proxy authentication API rather than just mocking the client.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/treq into lp:launchpad.
=== modified file 'constraints.txt'
--- constraints.txt	2018-06-06 12:46:56 +0000
+++ constraints.txt	2018-07-02 00:28:14 +0000
@@ -359,6 +359,7 @@
 testresources==0.2.7
 testscenarios==0.4
 timeline==0.0.3
+treq==18.6.0
 Twisted[conch,tls]==17.9.0
 txAMQP==0.6.2
 txfixtures==0.4.2

=== modified file 'lib/lp/buildmaster/tests/test_interactor.py'
--- lib/lp/buildmaster/tests/test_interactor.py	2018-06-06 08:47:46 +0000
+++ lib/lp/buildmaster/tests/test_interactor.py	2018-07-02 00:28:14 +0000
@@ -29,13 +29,13 @@
     AsynchronousDeferredRunTestForBrokenTwisted,
     SynchronousDeferredRunTest,
     )
+import treq
 from twisted.internet import (
     defer,
     reactor as default_reactor,
     )
 from twisted.internet.task import Clock
 from twisted.python.failure import Failure
-from twisted.web.client import getPage
 from zope.security.proxy import removeSecurityProxy
 
 from lp.buildmaster.enums import (
@@ -66,6 +66,8 @@
     WaitingSlave,
     )
 from lp.services.config import config
+from lp.services.twistedsupport.testing import TReqFixture
+from lp.services.twistedsupport.treq import check_status
 from lp.soyuz.model.binarypackagebuildbehaviour import (
     BinaryPackageBuildBehaviour,
     )
@@ -763,10 +765,12 @@
         d.addCallback(self.assertEqual, [True, 'Download'])
         return d
 
+    @defer.inlineCallbacks
     def test_retrieve_files_from_filecache(self):
         # Files that are present on the slave can be downloaded with a
         # filename made from the sha1 of the content underneath the
         # 'filecache' directory.
+        from twisted.internet import reactor
         content = "Hello World"
         lf = self.factory.makeLibraryFileAlias(
             'HelloWorld.txt', content=content)
@@ -775,13 +779,11 @@
             self.slave_helper.base_url, lf.content.sha1)
         self.slave_helper.getServerSlave()
         slave = self.slave_helper.getClientSlave()
-        d = slave.ensurepresent(lf.content.sha1, lf.http_url, "", "")
-
-        def check_file(ignored):
-            d = getPage(expected_url.encode('utf8'))
-            return d.addCallback(self.assertEqual, content)
-
-        return d.addCallback(check_file)
+        yield slave.ensurepresent(lf.content.sha1, lf.http_url, "", "")
+        client = self.useFixture(TReqFixture(reactor)).client
+        response = yield client.get(expected_url).addCallback(check_status)
+        got_content = yield treq.content(response)
+        self.assertEqual(content, got_content)
 
     def test_getFiles(self):
         # Test BuilderSlave.getFiles().

=== added file 'lib/lp/services/twistedsupport/testing.py'
--- lib/lp/services/twistedsupport/testing.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/twistedsupport/testing.py	2018-07-02 00:28:14 +0000
@@ -0,0 +1,33 @@
+# Copyright 2018 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test-specific Twisted utilities."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+    'TReqFixture',
+    ]
+
+from fixtures import Fixture
+from treq.client import HTTPClient
+from twisted.web.client import (
+    Agent,
+    HTTPConnectionPool,
+    )
+
+
+class TReqFixture(Fixture):
+    """A `treq` client that handles test cleanup."""
+
+    def __init__(self, reactor, pool=None):
+        super(TReqFixture, self).__init__()
+        self.reactor = reactor
+        if pool is None:
+            pool = HTTPConnectionPool(reactor, persistent=False)
+        self.pool = pool
+
+    def _setUp(self):
+        self.client = HTTPClient(Agent(self.reactor, pool=self.pool))
+        self.addCleanup(self.pool.closeCachedConnections)

=== added file 'lib/lp/services/twistedsupport/treq.py'
--- lib/lp/services/twistedsupport/treq.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/twistedsupport/treq.py	2018-07-02 00:28:14 +0000
@@ -0,0 +1,37 @@
+# Copyright 2018 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Utilities for HTTP request handling with Twisted and treq."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+    'check_status',
+    ]
+
+import treq
+from twisted.internet import defer
+from twisted.web.error import Error
+
+
+# This can be removed once something like
+# https://github.com/twisted/treq/pull/159 is merged into treq.
+def check_status(response):
+    """Fail with an error if the response has an error status.
+
+    An error status is a 4xx or 5xx code (RFC 7231).
+
+    :rtype: A `Deferred` that fires with the response itself if the status
+        is a known non-error code, or that fails it with
+        :exc:`twisted.web.error.Error` otherwise.
+    """
+    if 100 <= response.code < 400:
+        d = defer.succeed(response)
+    else:
+        def raise_error(body):
+            raise Error(response.code, response=body)
+
+        d = treq.content(response).addCallback(raise_error)
+
+    return d

=== modified file 'lib/lp/snappy/model/snapbuildbehaviour.py'
--- lib/lp/snappy/model/snapbuildbehaviour.py	2018-05-02 12:45:12 +0000
+++ lib/lp/snappy/model/snapbuildbehaviour.py	2018-07-02 00:28:14 +0000
@@ -12,11 +12,10 @@
     ]
 
 import base64
-import json
 import time
 
+import treq
 from twisted.internet import defer
-from twisted.web.client import getPage
 from zope.component import adapter
 from zope.interface import implementer
 from zope.security.proxy import removeSecurityProxy
@@ -30,6 +29,7 @@
     )
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.config import config
+from lp.services.twistedsupport.treq import check_status
 from lp.snappy.interfaces.snap import SnapBuildArchiveOwnerMismatch
 from lp.snappy.interfaces.snapbuild import ISnapBuild
 from lp.soyuz.adapters.archivedependencies import (
@@ -152,18 +152,14 @@
             build_id=self.build.build_cookie,
             timestamp=timestamp)
         auth_string = '{}:{}'.format(admin_username, secret).strip()
-        auth_header = 'Basic ' + base64.b64encode(auth_string)
-        data = json.dumps({'username': proxy_username})
+        auth_header = b'Basic ' + base64.b64encode(auth_string)
 
-        result = yield getPage(
-            url,
-            method='POST',
-            postdata=data,
-            headers={
-                'Authorization': auth_header,
-                'Content-Type': 'application/json'}
-            )
-        token = json.loads(result)
+        response = yield treq.post(
+            url, headers={'Authorization': auth_header},
+            json={'username': proxy_username},
+            reactor=self._slave.reactor,
+            pool=self._slave.pool).addCallback(check_status)
+        token = yield treq.json_content(response)
         defer.returnValue(token)
 
     def verifySuccessfulBuild(self):

=== modified file 'lib/lp/snappy/tests/test_snapbuildbehaviour.py'
--- lib/lp/snappy/tests/test_snapbuildbehaviour.py	2018-05-02 13:22:17 +0000
+++ lib/lp/snappy/tests/test_snapbuildbehaviour.py	2018-07-02 00:28:14 +0000
@@ -12,24 +12,40 @@
 import json
 import os.path
 from textwrap import dedent
+import time
 import uuid
 
 import fixtures
-from mock import patch
 from pymacaroons import Macaroon
+from six.moves.urllib_parse import urlsplit
 from testtools import ExpectedException
 from testtools.matchers import (
     AfterPreprocessing,
+    ContainsDict,
     Equals,
+    HasLength,
+    Is,
     IsInstance,
     MatchesDict,
     MatchesListwise,
+    MatchesStructure,
     StartsWith,
     )
-from testtools.twistedsupport import AsynchronousDeferredRunTest
+from testtools.twistedsupport import (
+    AsynchronousDeferredRunTestForBrokenTwisted,
+    )
 import transaction
-from twisted.internet import defer
+from twisted.internet import (
+    defer,
+    endpoints,
+    reactor,
+    )
+from twisted.python.compat import nativeString
 from twisted.trial.unittest import TestCase as TrialTestCase
+from twisted.web import (
+    resource,
+    server,
+    )
 from zope.component import getUtility
 from zope.proxy import isProxy
 from zope.security.proxy import removeSecurityProxy
@@ -46,6 +62,7 @@
 from lp.buildmaster.tests.mock_slaves import (
     MockBuilder,
     OkSlave,
+    SlaveTestHelpers,
     )
 from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
     TestGetUploadMethodsMixin,
@@ -66,12 +83,74 @@
 from lp.soyuz.interfaces.archive import ArchiveDisabled
 from lp.soyuz.tests.soyuz import Base64KeyMatches
 from lp.testing import TestCaseWithFactory
-from lp.testing.fakemethod import FakeMethod
 from lp.testing.gpgkeys import gpgkeysdir
 from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import LaunchpadZopelessLayer
 
 
+class ProxyAuthAPITokensResource(resource.Resource):
+    """A test tokens resource for the proxy authentication API."""
+
+    isLeaf = True
+
+    def __init__(self):
+        resource.Resource.__init__(self)
+        self.requests = []
+
+    def render_POST(self, request):
+        content = request.content.read()
+        self.requests.append({
+            "method": request.method,
+            "uri": request.uri,
+            "headers": dict(request.requestHeaders.getAllRawHeaders()),
+            "content": content,
+            })
+        username = json.loads(content)["username"]
+        return json.dumps({
+            "username": username,
+            "secret": uuid.uuid4().hex,
+            "timestamp": datetime.utcnow().isoformat(),
+            })
+
+
+class InProcessProxyAuthAPIFixture(fixtures.Fixture):
+    """A fixture that pretends to be the proxy authentication API.
+
+    Users of this fixture must call the `start` method, which returns a
+    `Deferred`, and arrange for that to get back to the reactor.  This is
+    necessary because the basic fixture API does not allow `setUp` to return
+    anything.  For example:
+
+        class TestSomething(TestCase):
+
+            run_tests_with = AsynchronousDeferredRunTest.make_factory(
+                timeout=10)
+
+            @defer.inlineCallbacks
+            def setUp(self):
+                super(TestSomething, self).setUp()
+                yield self.useFixture(InProcessProxyAuthAPIFixture()).start()
+    """
+
+    @defer.inlineCallbacks
+    def start(self):
+        root = resource.Resource()
+        self.tokens = ProxyAuthAPITokensResource()
+        root.putChild("tokens", self.tokens)
+        endpoint = endpoints.serverFromString(reactor, nativeString("tcp:0"))
+        site = server.Site(self.tokens)
+        self.addCleanup(site.stopFactory)
+        port = yield endpoint.listen(site)
+        self.addCleanup(port.stopListening)
+        config.push("in-process-proxy-auth-api-fixture", dedent("""
+            [snappy]
+            builder_proxy_auth_api_admin_secret: admin-secret
+            builder_proxy_auth_api_endpoint: http://%s:%s/tokens
+            """) %
+            (port.getHost().host, port.getHost().port))
+        self.addCleanup(config.pop, "in-process-proxy-auth-api-fixture")
+
+
 class TestSnapBuildBehaviourBase(TestCaseWithFactory):
     layer = LaunchpadZopelessLayer
 
@@ -80,7 +159,7 @@
         self.pushConfig("snappy", tools_source=None, tools_fingerprint=None)
 
     def makeJob(self, archive=None, pocket=PackagePublishingPocket.UPDATES,
-                with_builder=False, **kwargs):
+                **kwargs):
         """Create a sample `ISnapBuildBehaviour`."""
         if archive is None:
             distribution = self.factory.makeDistribution(name="distro")
@@ -95,12 +174,7 @@
         build = self.factory.makeSnapBuild(
             archive=archive, distroarchseries=distroarchseries, pocket=pocket,
             name="test-snap", **kwargs)
-        job = IBuildFarmJobBehaviour(build)
-        if with_builder:
-            builder = MockBuilder()
-            builder.processor = processor
-            job.setBuilder(builder, None)
-        return job
+        return IBuildFarmJobBehaviour(build)
 
 
 class TestSnapBuildBehaviour(TestSnapBuildBehaviourBase):
@@ -197,8 +271,10 @@
 
 
 class TestAsyncSnapBuildBehaviour(TestSnapBuildBehaviourBase):
-    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
+    run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
+        timeout=10)
 
+    @defer.inlineCallbacks
     def setUp(self):
         super(TestAsyncSnapBuildBehaviour, self).setUp()
         build_username = 'SNAPBUILD-1'
@@ -211,23 +287,41 @@
                               password=self.token['secret'],
                               host=config.snappy.builder_proxy_host,
                               port=config.snappy.builder_proxy_port))
-        self.revocation_endpoint = "{endpoint}/{username}".format(
-            endpoint=config.snappy.builder_proxy_auth_api_endpoint,
-            username=build_username)
-        self.api_admin_secret = "admin-secret"
-        self.pushConfig(
-            "snappy",
-            builder_proxy_auth_api_admin_secret=self.api_admin_secret)
-        self.mock_proxy_api = FakeMethod(
-            result=defer.succeed(json.dumps(self.token)))
-        patcher = patch(
-            "lp.snappy.model.snapbuildbehaviour.getPage", self.mock_proxy_api)
-        patcher.start()
-        self.addCleanup(patcher.stop)
+        self.proxy_api = self.useFixture(InProcessProxyAuthAPIFixture())
+        yield self.proxy_api.start()
+        self.now = time.time()
+        self.useFixture(fixtures.MockPatch(
+            "time.time", return_value=self.now))
+
+    def makeJob(self, **kwargs):
+        # We need a builder slave in these tests, in order that requesting a
+        # proxy token can piggyback on its reactor and pool.
+        job = super(TestAsyncSnapBuildBehaviour, self).makeJob(**kwargs)
+        builder = MockBuilder()
+        builder.processor = job.build.processor
+        slave = self.useFixture(SlaveTestHelpers()).getClientSlave()
+        job.setBuilder(builder, slave)
+        self.addCleanup(slave.pool.closeCachedConnections)
+        return job
+
+    def getProxyURLMatcher(self, job):
+        return AfterPreprocessing(urlsplit, MatchesStructure(
+            scheme=Equals("http"),
+            username=Equals("{}-{}".format(
+                job.build.build_cookie, int(self.now))),
+            password=HasLength(32),
+            hostname=Equals(config.snappy.builder_proxy_host),
+            port=Equals(config.snappy.builder_proxy_port),
+            path=Equals("")))
+
+    def getRevocationEndpointMatcher(self, job):
+        return Equals("{}/{}-{}".format(
+            config.snappy.builder_proxy_auth_api_endpoint,
+            job.build.build_cookie, int(self.now)))
 
     @defer.inlineCallbacks
     def test_composeBuildRequest(self):
-        job = self.makeJob(with_builder=True)
+        job = self.makeJob()
         lfa = self.factory.makeLibraryFileAlias(db_only=True)
         job.build.distro_arch_series.addOrUpdateChroot(lfa)
         build_request = yield job.composeBuildRequest(None)
@@ -238,7 +332,7 @@
     def test_requestProxyToken_unconfigured(self):
         self.pushConfig("snappy", builder_proxy_auth_api_admin_secret=None)
         branch = self.factory.makeBranch()
-        job = self.makeJob(branch=branch, with_builder=True)
+        job = self.makeJob(branch=branch)
         expected_exception_msg = (
             "builder_proxy_auth_api_admin_secret is not configured.")
         with ExpectedException(CannotBuild, expected_exception_msg):
@@ -247,25 +341,25 @@
     @defer.inlineCallbacks
     def test_requestProxyToken(self):
         branch = self.factory.makeBranch()
-        job = self.makeJob(branch=branch, with_builder=True)
+        job = self.makeJob(branch=branch)
         yield job.extraBuildArgs()
-        self.assertThat(self.mock_proxy_api.calls, MatchesListwise([
-            MatchesListwise([
-                MatchesListwise([
-                    Equals(config.snappy.builder_proxy_auth_api_endpoint),
-                    ]),
-                MatchesDict({
-                    "method": Equals(b"POST"),
-                    "postdata": AfterPreprocessing(json.loads, MatchesDict({
-                        "username": StartsWith(job.build.build_cookie + "-"),
-                        })),
-                    "headers": MatchesDict({
-                        b"Authorization": Equals(b"Basic " + base64.b64encode(
-                            b"admin-launchpad.dev:admin-secret")),
-                        b"Content-Type": Equals(b"application/json"),
-                        }),
+        self.assertThat(self.proxy_api.tokens.requests, MatchesListwise([
+            MatchesDict({
+                "method": Equals("POST"),
+                "uri": Equals(urlsplit(
+                    config.snappy.builder_proxy_auth_api_endpoint).path),
+                "headers": ContainsDict({
+                    b"Authorization": MatchesListwise([
+                        Equals(b"Basic " + base64.b64encode(
+                            b"admin-launchpad.dev:admin-secret"))]),
+                    b"Content-Type": MatchesListwise([
+                        Equals(b"application/json; charset=UTF-8"),
+                        ]),
                     }),
-                ]),
+                "content": AfterPreprocessing(json.loads, MatchesDict({
+                    "username": StartsWith(job.build.build_cookie + "-"),
+                    })),
+                }),
             ]))
 
     @defer.inlineCallbacks
@@ -273,51 +367,51 @@
         # extraBuildArgs returns appropriate arguments if asked to build a
         # job for a Bazaar branch.
         branch = self.factory.makeBranch()
-        job = self.makeJob(branch=branch, with_builder=True)
+        job = self.makeJob(branch=branch)
         expected_archives, expected_trusted_keys = (
             yield get_sources_list_for_building(
                 job.build, job.build.distro_arch_series, None))
         args = yield job.extraBuildArgs()
-        self.assertEqual({
-            "archive_private": False,
-            "archives": expected_archives,
-            "arch_tag": "i386",
-            "branch": branch.bzr_identity,
-            "build_source_tarball": False,
-            "build_url": canonical_url(job.build),
-            "fast_cleanup": True,
-            "name": "test-snap",
-            "proxy_url": self.proxy_url,
-            "revocation_endpoint": self.revocation_endpoint,
-            "series": "unstable",
-            "trusted_keys": expected_trusted_keys,
-            }, args)
+        self.assertThat(args, MatchesDict({
+            "archive_private": Is(False),
+            "archives": Equals(expected_archives),
+            "arch_tag": Equals("i386"),
+            "branch": Equals(branch.bzr_identity),
+            "build_source_tarball": Is(False),
+            "build_url": Equals(canonical_url(job.build)),
+            "fast_cleanup": Is(True),
+            "name": Equals("test-snap"),
+            "proxy_url": self.getProxyURLMatcher(job),
+            "revocation_endpoint": self.getRevocationEndpointMatcher(job),
+            "series": Equals("unstable"),
+            "trusted_keys": Equals(expected_trusted_keys),
+            }))
 
     @defer.inlineCallbacks
     def test_extraBuildArgs_git(self):
         # extraBuildArgs returns appropriate arguments if asked to build a
         # job for a Git branch.
         [ref] = self.factory.makeGitRefs()
-        job = self.makeJob(git_ref=ref, with_builder=True)
+        job = self.makeJob(git_ref=ref)
         expected_archives, expected_trusted_keys = (
             yield get_sources_list_for_building(
                 job.build, job.build.distro_arch_series, None))
         args = yield job.extraBuildArgs()
-        self.assertEqual({
-            "archive_private": False,
-            "archives": expected_archives,
-            "arch_tag": "i386",
-            "build_source_tarball": False,
-            "build_url": canonical_url(job.build),
-            "fast_cleanup": True,
-            "git_repository": ref.repository.git_https_url,
-            "git_path": ref.name,
-            "name": "test-snap",
-            "proxy_url": self.proxy_url,
-            "revocation_endpoint": self.revocation_endpoint,
-            "series": "unstable",
-            "trusted_keys": expected_trusted_keys,
-            }, args)
+        self.assertThat(args, MatchesDict({
+            "archive_private": Is(False),
+            "archives": Equals(expected_archives),
+            "arch_tag": Equals("i386"),
+            "build_source_tarball": Is(False),
+            "build_url": Equals(canonical_url(job.build)),
+            "fast_cleanup": Is(True),
+            "git_repository": Equals(ref.repository.git_https_url),
+            "git_path": Equals(ref.name),
+            "name": Equals("test-snap"),
+            "proxy_url": self.getProxyURLMatcher(job),
+            "revocation_endpoint": self.getRevocationEndpointMatcher(job),
+            "series": Equals("unstable"),
+            "trusted_keys": Equals(expected_trusted_keys),
+            }))
 
     @defer.inlineCallbacks
     def test_extraBuildArgs_git_HEAD(self):
@@ -325,26 +419,25 @@
         # job for the default branch in a Launchpad-hosted Git repository.
         [ref] = self.factory.makeGitRefs()
         removeSecurityProxy(ref.repository)._default_branch = ref.path
-        job = self.makeJob(
-            git_ref=ref.repository.getRefByPath("HEAD"), with_builder=True)
+        job = self.makeJob(git_ref=ref.repository.getRefByPath("HEAD"))
         expected_archives, expected_trusted_keys = (
             yield get_sources_list_for_building(
                 job.build, job.build.distro_arch_series, None))
         args = yield job.extraBuildArgs()
-        self.assertEqual({
-            "archive_private": False,
-            "archives": expected_archives,
-            "arch_tag": "i386",
-            "build_source_tarball": False,
-            "build_url": canonical_url(job.build),
-            "fast_cleanup": True,
-            "git_repository": ref.repository.git_https_url,
-            "name": "test-snap",
-            "proxy_url": self.proxy_url,
-            "revocation_endpoint": self.revocation_endpoint,
-            "series": "unstable",
-            "trusted_keys": expected_trusted_keys,
-            }, args)
+        self.assertThat(args, MatchesDict({
+            "archive_private": Is(False),
+            "archives": Equals(expected_archives),
+            "arch_tag": Equals("i386"),
+            "build_source_tarball": Is(False),
+            "build_url": Equals(canonical_url(job.build)),
+            "fast_cleanup": Is(True),
+            "git_repository": Equals(ref.repository.git_https_url),
+            "name": Equals("test-snap"),
+            "proxy_url": self.getProxyURLMatcher(job),
+            "revocation_endpoint": self.getRevocationEndpointMatcher(job),
+            "series": Equals("unstable"),
+            "trusted_keys": Equals(expected_trusted_keys),
+            }))
 
     @defer.inlineCallbacks
     def test_extraBuildArgs_git_url(self):
@@ -353,26 +446,26 @@
         url = "https://git.example.org/foo";
         ref = self.factory.makeGitRefRemote(
             repository_url=url, path="refs/heads/master")
-        job = self.makeJob(git_ref=ref, with_builder=True)
+        job = self.makeJob(git_ref=ref)
         expected_archives, expected_trusted_keys = (
             yield get_sources_list_for_building(
                 job.build, job.build.distro_arch_series, None))
         args = yield job.extraBuildArgs()
-        self.assertEqual({
-            "archive_private": False,
-            "archives": expected_archives,
-            "arch_tag": "i386",
-            "build_source_tarball": False,
-            "build_url": canonical_url(job.build),
-            "fast_cleanup": True,
-            "git_repository": url,
-            "git_path": "master",
-            "name": "test-snap",
-            "proxy_url": self.proxy_url,
-            "revocation_endpoint": self.revocation_endpoint,
-            "series": "unstable",
-            "trusted_keys": expected_trusted_keys,
-            }, args)
+        self.assertThat(args, MatchesDict({
+            "archive_private": Is(False),
+            "archives": Equals(expected_archives),
+            "arch_tag": Equals("i386"),
+            "build_source_tarball": Is(False),
+            "build_url": Equals(canonical_url(job.build)),
+            "fast_cleanup": Is(True),
+            "git_repository": Equals(url),
+            "git_path": Equals("master"),
+            "name": Equals("test-snap"),
+            "proxy_url": self.getProxyURLMatcher(job),
+            "revocation_endpoint": self.getRevocationEndpointMatcher(job),
+            "series": Equals("unstable"),
+            "trusted_keys": Equals(expected_trusted_keys),
+            }))
 
     @defer.inlineCallbacks
     def test_extraBuildArgs_git_url_HEAD(self):
@@ -380,31 +473,31 @@
         # job for the default branch in an external Git repository.
         url = "https://git.example.org/foo";
         ref = self.factory.makeGitRefRemote(repository_url=url, path="HEAD")
-        job = self.makeJob(git_ref=ref, with_builder=True)
+        job = self.makeJob(git_ref=ref)
         expected_archives, expected_trusted_keys = (
             yield get_sources_list_for_building(
                 job.build, job.build.distro_arch_series, None))
         args = yield job.extraBuildArgs()
-        self.assertEqual({
-            "archive_private": False,
-            "archives": expected_archives,
-            "arch_tag": "i386",
-            "build_source_tarball": False,
-            "build_url": canonical_url(job.build),
-            "fast_cleanup": True,
-            "git_repository": url,
-            "name": "test-snap",
-            "proxy_url": self.proxy_url,
-            "revocation_endpoint": self.revocation_endpoint,
-            "series": "unstable",
-            "trusted_keys": expected_trusted_keys,
-            }, args)
+        self.assertThat(args, MatchesDict({
+            "archive_private": Is(False),
+            "archives": Equals(expected_archives),
+            "arch_tag": Equals("i386"),
+            "build_source_tarball": Is(False),
+            "build_url": Equals(canonical_url(job.build)),
+            "fast_cleanup": Is(True),
+            "git_repository": Equals(url),
+            "name": Equals("test-snap"),
+            "proxy_url": self.getProxyURLMatcher(job),
+            "revocation_endpoint": self.getRevocationEndpointMatcher(job),
+            "series": Equals("unstable"),
+            "trusted_keys": Equals(expected_trusted_keys),
+            }))
 
     @defer.inlineCallbacks
     def test_extraBuildArgs_prefers_store_name(self):
         # For the "name" argument, extraBuildArgs prefers Snap.store_name
         # over Snap.name if the former is set.
-        job = self.makeJob(store_name="something-else", with_builder=True)
+        job = self.makeJob(store_name="something-else")
         args = yield job.extraBuildArgs()
         self.assertEqual("something-else", args["name"])
 
@@ -416,7 +509,7 @@
         key_path = os.path.join(gpgkeysdir, "ppa-sample@xxxxxxxxxxxxxxxxx")
         yield IArchiveSigningKey(archive).setSigningKey(
             key_path, async_keyserver=True)
-        job = self.makeJob(archive=archive, with_builder=True)
+        job = self.makeJob(archive=archive)
         self.factory.makeBinaryPackagePublishingHistory(
             distroarchseries=job.build.distro_arch_series,
             pocket=job.build.pocket, archive=archive,
@@ -429,7 +522,7 @@
     @defer.inlineCallbacks
     def test_extraBuildArgs_channels(self):
         # If the build needs particular channels, extraBuildArgs sends them.
-        job = self.makeJob(channels={"snapcraft": "edge"}, with_builder=True)
+        job = self.makeJob(channels={"snapcraft": "edge"})
         expected_archives, expected_trusted_keys = (
             yield get_sources_list_for_building(
                 job.build, job.build.distro_arch_series, None))
@@ -441,7 +534,7 @@
     def test_extraBuildArgs_disallow_internet(self):
         # If external network access is not allowed for the snap,
         # extraBuildArgs does not dispatch a proxy token.
-        job = self.makeJob(allow_internet=False, with_builder=True)
+        job = self.makeJob(allow_internet=False)
         args = yield job.extraBuildArgs()
         self.assertNotIn("proxy_url", args)
         self.assertNotIn("revocation_endpoint", args)
@@ -450,21 +543,16 @@
     def test_extraBuildArgs_build_source_tarball(self):
         # If the snap requests building of a source tarball, extraBuildArgs
         # sends the appropriate arguments.
-        job = self.makeJob(build_source_tarball=True, with_builder=True)
+        job = self.makeJob(build_source_tarball=True)
         args = yield job.extraBuildArgs()
         self.assertTrue(args["build_source_tarball"])
 
     @defer.inlineCallbacks
     def test_composeBuildRequest_proxy_url_set(self):
-        job = self.makeJob(with_builder=True)
+        job = self.makeJob()
         build_request = yield job.composeBuildRequest(None)
-        proxy_url = ("http://{username}:{password}";
-                     "@{host}:{port}".format(
-                         username=self.token['username'],
-                         password=self.token['secret'],
-                         host=config.snappy.builder_proxy_host,
-                         port=config.snappy.builder_proxy_port))
-        self.assertEqual(proxy_url, build_request[3]['proxy_url'])
+        self.assertThat(
+            build_request[3]["proxy_url"], self.getProxyURLMatcher(job))
 
     @defer.inlineCallbacks
     def test_composeBuildRequest_deleted(self):
@@ -472,8 +560,7 @@
         # composeBuildRequest raises CannotBuild.
         branch = self.factory.makeBranch()
         owner = self.factory.makePerson(name="snap-owner")
-        job = self.makeJob(
-            registrant=owner, owner=owner, branch=branch, with_builder=True)
+        job = self.makeJob(registrant=owner, owner=owner, branch=branch)
         branch.destroySelf(break_references=True)
         self.assertIsNone(job.build.snap.branch)
         self.assertIsNone(job.build.snap.git_repository)
@@ -489,8 +576,7 @@
         repository = self.factory.makeGitRepository()
         [ref] = self.factory.makeGitRefs(repository=repository)
         owner = self.factory.makePerson(name="snap-owner")
-        job = self.makeJob(
-            registrant=owner, owner=owner, git_ref=ref, with_builder=True)
+        job = self.makeJob(registrant=owner, owner=owner, git_ref=ref)
         repository.removeRefs([ref.path])
         self.assertIsNone(job.build.snap.git_ref)
         expected_exception_msg = ("Source branch/repository for "

=== modified file 'lib/lp/testing/keyserver/tests/test_inprocess.py'
--- lib/lp/testing/keyserver/tests/test_inprocess.py	2017-12-19 17:22:44 +0000
+++ lib/lp/testing/keyserver/tests/test_inprocess.py	2018-07-02 00:28:14 +0000
@@ -1,4 +1,4 @@
-# Copyright 2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2017-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """In-process keyserver fixture tests."""
@@ -10,10 +10,12 @@
 from testtools.twistedsupport import (
     AsynchronousDeferredRunTestForBrokenTwisted,
     )
+import treq
 from twisted.internet import defer
-from twisted.web.client import getPage
 
 from lp.services.config import config
+from lp.services.twistedsupport.testing import TReqFixture
+from lp.services.twistedsupport.treq import check_status
 from lp.testing import TestCase
 from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.keyserver.web import GREETING
@@ -38,7 +40,10 @@
     @defer.inlineCallbacks
     def test_starts_properly(self):
         # The fixture starts properly and we can load the page.
+        from twisted.internet import reactor
         fixture = self.useFixture(InProcessKeyServerFixture())
         yield fixture.start()
-        content = yield getPage(fixture.url)
+        client = self.useFixture(TReqFixture(reactor)).client
+        response = yield client.get(fixture.url).addCallback(check_status)
+        content = yield treq.content(response)
         self.assertEqual(GREETING, content)

=== modified file 'lib/lp/testing/keyserver/tests/test_web.py'
--- lib/lp/testing/keyserver/tests/test_web.py	2017-12-19 17:22:44 +0000
+++ lib/lp/testing/keyserver/tests/test_web.py	2018-07-02 00:28:14 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2011-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the web resources of the testkeyserver."""
@@ -9,11 +9,13 @@
 import shutil
 
 from testtools.twistedsupport import AsynchronousDeferredRunTest
+import treq
 from twisted.internet.endpoints import serverFromString
 from twisted.python.failure import Failure
-from twisted.web.client import getPage
 from twisted.web.server import Site
 
+from lp.services.twistedsupport.testing import TReqFixture
+from lp.services.twistedsupport.treq import check_status
 from lp.testing import TestCase
 from lp.testing.keyserver.harness import KEYS_DIR
 from lp.testing.keyserver.web import KeyServerResource
@@ -46,10 +48,13 @@
     def fetchResource(self, listening_port, path):
         """GET the content at 'path' from the web server at 'listening_port'.
         """
+        from twisted.internet import reactor
         url = 'http://localhost:%s/%s' % (
             listening_port.getHost().port,
             path.lstrip('/'))
-        return getPage(url)
+        client = self.useFixture(TReqFixture(reactor)).client
+        return client.get(url).addCallback(check_status).addCallback(
+            treq.content)
 
     def getURL(self, path):
         """Start a test key server and get the content at 'path'."""

=== modified file 'setup.py'
--- setup.py	2018-06-26 10:59:34 +0000
+++ setup.py	2018-07-02 00:28:14 +0000
@@ -224,6 +224,7 @@
         'testtools',
         'timeline',
         'transaction',
+        'treq',
         'Twisted[conch,tls]',
         'txfixtures',
         'txlongpoll',


Follow ups