New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement a fake resolver, for compatibility with Twisted 17.1.0 and higher #150
Conversation
See: twisted/twisted@cd75dd8 , which is part of this pull request to Twisted: twisted/twisted#624 |
See also: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. This basically looks good to me. A few small changes requested inline, along with one slightly more significant request to ensure we retain good test coverage. Sorry about the review delay here, I've been entirely occupied offline recently. Thanks again.
def resolveHostName(self, resolutionReceiver, hostName, *args, **kwargs): | ||
portNumber = kwargs.pop('portNumber') | ||
r = Resolution() | ||
r.name = hostName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about Resolution(name=hostName)
? Your choice of pyrsistent
or attrs
for defining Resolution
to make this possible. :)
from twisted.internet.protocol import Factory | ||
from twisted.web.http_headers import Headers | ||
from twisted.test.iosim import ConnectionCompleter | ||
from twisted.test.proto_helpers import AccumulatingProtocol, MemoryReactorClock | ||
|
||
from zope.interface import implementer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this up above the Twisted imports? (because Twisted depends on zope.interface)
r.name = hostName | ||
|
||
resolutionReceiver.resolutionBegan(r) | ||
resolutionReceiver.addressResolved(IPv4Address('TCP', 'example.invalid.', portNumber)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose that since there's now an explicit resolution step going on (whereas previously the hostname would be passed to connectTCP
), the result here should be an IP address instead of a hostname? The hostname and the IP address are both sloppily represented as native strings but they're pretty distinctly different things.
Pick an address from one of the TEST-NET
ranges, probably: https://en.wikipedia.org/wiki/IPv4#Special-use_addresses.
Also, there should be some kind of assertion made about the value of hostName
somewhere. It is currently essentially ignored, as far as I can tell. Previously, each test asserted that "example.invalid." was being connected to (a value taken from an environment variable). Now no matter what hostname is passed in, it will be resolved to the hard-coded value passed to this addressResolved
method. So the coverage of the hostname being selected properly is being removed.
A well-behaved fake resolver, I suppose, would know about just the relevant fake hostnames and return a resolution error for any others (in whatever way is correct for the resolver interface now, I haven't checked, but I guess there's some other method).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So resolveHostname
https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IHostnameResolver.html#resolveHostName returns an IResolutionReceiver
:
https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IResolutionReceiver.html
not an IP address.
I'm new to all these interfaces, but let me see what I can do to clean this up.
setup.py
Outdated
@@ -32,7 +32,7 @@ | |||
"incremental", | |||
# See https://github.com/twisted/treq/issues/167 | |||
# And https://twistedmatrix.com/trac/ticket/9032 | |||
"twisted[tls]!=17.1.0", | |||
"twisted[tls]>=17.9.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want this change here or in #151? Also, I'm not sure the comments above are now relevant. The constraint is now primarily due to the name resolver behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want the change in #151 . I just pulled it into this branch because it was interrelated. I wanted to get your feedback separately on bumping the Twisted version in #151 because at the time I wasn't sure how many versions of Twisted you wanted to maintain compatibility with. We had a short conversion on IRC later where you mentioned you were OK with bumping the Twisted version.
If you can, please review #151 separately,and once we beat that into shape, I will pull that into this branch.
I've improved the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This looks really good now. Just a couple very minor additional changes and then I'm happy to see it merged.
str(result.value), | ||
Equals('DNS lookup failed: no results for ' | ||
'hostname lookup: doesnotresolve.') | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't necessarily assert that the Deferred fails. It might just never fire. It's a good idea to use failureResultOf
for this reason.
@@ -68,11 +76,48 @@ | |||
-----END CERTIFICATE----- | |||
""" | |||
|
|||
# Let hostname u"example.invalid" map to an | |||
# IPv4 address in the TEST-NET range. | |||
_hostMap = { u"example.invalid." : "192.0.2.2" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's some extra whitespace inside this dict. Also, as a global, it should probably be something more like HOST_MAP
. It probably doesn't need a leading underscore because test modules are automatically not public APIs.
resolutionReceiver.addressResolved( | ||
IPv4Address('TCP', _hostMap[hostName], portNumber)) | ||
resolutionReceiver.resolutionComplete() | ||
return r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. This seems to be the right way to get the desired behavior from the new resolver interface. I'm not quite sure I entirely understand how the interface means to deal with errors - but not calling addressResolved
seems to be how _resolver.py
in Twisted deals with errors, so it makes sense to do the same here.
@exarkun I've updated the patch, please look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! A few more minor comments, sorry I didn't notice them earlier. But more importantly, I misled you with the failureResultOf
suggestion. Correct idea in a comment inline. Sorry about that. Thanks again for your efforts here.
""" | ||
d = self._authorized_request(token="test", headers=Headers({}), | ||
kubernetesHost=b"doesnotresolve") | ||
result = SynchronousTestCase().failureResultOf(d, DNSLookupError) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. :( I forgot that txkube tests don't use trial's TestCase
. I don't think instantiating SynchronousTestCase
to call methods on it is a great idea. It probably won't break but normal usage of is not for application code to instantiate this kind of thing.
So, instead, what I should have asked for in my previous review was use of testtools.twistedsupport.failed
. I think it'll look something like this:
has_lookup_failure = failed(
AfterPreprocessing(lambda f: str(f.value),
Equals("DNS lookup failed: ..."),
)
self.assertThat(some_deferred, has_lookup_failure)
Sorry about steering you wrong with failureResultOf
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
|
||
d = agent.request(b"GET", b"http://" + kubernetesHost, headers) | ||
if isinstance(d.result, Failure): | ||
return d |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should use the testtools.twistedsupport.succeeded
(and testtools.matchers.Always
) matcher:
self.assertThat(d, succeeded(Always())
The Always()
just matches any result after succeeded
matches on a successful deferred. This has the benefit of the nice reporting that you get from testtools matches and avoiding using Deferred.result
directly (which looks public but it's really a bad idea to ever use it directly).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried self.assertThat(d, succeeded(Always()))
and got:
===============================================================================
[FAIL]
Traceback (most recent call last):
Failure: testtools.testresult.real._StringException: Traceback (most recent call last):
File "/Users/crodrigues/txkube2/src/txkube/test/test_authentication.py", line 262, in test_other_headers_preserved
request_bytes = self._authorized_request(token=token, headers=headers)
File "/Users/crodrigues/txkube2/src/txkube/test/test_authentication.py", line 159, in _authorized_request
self.assertThat(d, succeeded(Always()))
File "/Users/crodrigues/venv-2.7/lib/python2.7/site-packages/testtools/testcase.py", line 498, in assertThat
raise mismatch_error
testtools.matchers._impl.MismatchError: Success result expected on <Deferred at 0x1120f98c0 waiting on Deferred at 0x1120f9b00>, found no result instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, huh. I guess the point of the code is that if the Deferred
has failed, fail quickly. But expected behavior is for it to have no result at all yet.
I wonder if assertThat(d, Not(failed(Always())))
would work... I expect probably not. Peeking at an in-progress Deferred
is kind of a sneaky thing. I'm going to sleep on this, maybe I'll remember some better trick.
class AuthenticateWithServiceAccountTests(TestCase): | ||
""" | ||
Tests for ``authenticate_with_serviceaccount``. | ||
""" | ||
def _authorized_request(self, token, headers): | ||
def _authorized_request(self, token, headers, | ||
kubernetesHost=b"example.invalid."): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this should be kubernetes_host
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
u"example.invalid.": "192.0.2.2" | ||
} | ||
|
||
def createReactor(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this should be create_reactor
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
@exarkun please take a look, I've updated the patch. I used @defer.inlineCallbacks. I hope you don't mind. |
|
||
yield agent.request(b"GET", b"http://" + kubernetes_host, headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this change. The agent isn't doing real network operations, is it? Shouldn't it return an already-fired Deferred
? If so, self.successResultOf
seems more appropriate - and there's no need to turn _authorized_request
into a Deferred
-returning API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change was the easiest way for me to test the failure path by trying to resolve an invalid host against the FakeResolver, and the success path.
I didn't have good luck with successResultOf.
Use assertNoResult instead of inlineCallbacks
This is a forward port of part of the patch in #135 .
I couldn't find any existing test classes in Twisted which worked to get this test working.