Commit 4878481a authored by Jason Madden's avatar Jason Madden Committed by GitHub

Dns test love (#908)

* Resolve the DNS test differences.

Also document some of the known ways that c-ares is different as a
result of these test fixes.

Fixes #774.

* spurious assert statement.

* disable the ipv6 dual stack test even without ares, it has the same issue on travis ci. must have a different resolver configuration than I do.

* more dns tweaks for travis

* One small failure on travis.
parent c15b77d3
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
1.2b1 (unreleased) 1.2b1 (unreleased)
================== ==================
- TBD - The c-ares DNS resolver ignores bad flags to getnameinfo, like the
system resolver does. Discovered when cleaning up the DNS resolver
tests to produce more reliable results. See :issue:`774`.
1.2a2 (Dec 9, 2017) 1.2a2 (Dec 9, 2017)
=================== ===================
......
...@@ -446,4 +446,9 @@ cdef public class channel [object PyGeventAresChannelObject, type PyGeventAresCh ...@@ -446,4 +446,9 @@ cdef public class channel [object PyGeventAresChannelObject, type PyGeventAresCh
cares.ares_getnameinfo(self.channel, x, length, flags, <void*>gevent_ares_nameinfo_callback, <void*>arg) cares.ares_getnameinfo(self.channel, x, length, flags, <void*>gevent_ares_nameinfo_callback, <void*>arg)
def getnameinfo(self, object callback, tuple sockaddr, int flags): def getnameinfo(self, object callback, tuple sockaddr, int flags):
return self._getnameinfo(callback, sockaddr, _convert_cares_flags(flags)) try:
flags = _convert_cares_flags(flags)
except gaierror:
# The stdlib just ignores bad flags
flags = 0
return self._getnameinfo(callback, sockaddr, flags)
...@@ -31,6 +31,25 @@ class Resolver(object): ...@@ -31,6 +31,25 @@ class Resolver(object):
the threaded resolver). However, because it does not use threads, the threaded resolver). However, because it does not use threads,
it may scale better for applications that make many lookups. it may scale better for applications that make many lookups.
There are some known differences from the system resolver:
- ``gethostbyname_ex`` and ``gethostbyaddr`` may return different
for the ``aliaslist`` tuple member. (Sometimes the same,
sometimes in a different order, sometimes a different alias
altogether.)
- ``gethostbyname_ex`` may return the ``ipaddrlist`` in a different order.
- ``getaddrinfo`` does not return ``SOCK_RAW`` results.
- ``getaddrinfo`` may return results in a different order.
- Handling of ``.local`` (mDNS) names may be different, even if they are listed in
the hosts file.
- c-ares will not resolve ``broadcasthost``, even if listed in the hosts file.
- This implementation may raise ``gaierror(4)`` where the system implementation would raise
``herror(1)``.
- The results for ``localhost`` may be different. In particular, some system
resolvers will return more results from ``getaddrinfo`` than c-ares does,
such as SOCK_DGRAM results, and c-ares may report more ips on a multi-homed
host.
.. caution:: This module is considered extremely experimental on PyPy, and .. caution:: This module is considered extremely experimental on PyPy, and
due to its implementation in cython, it may be slower. It may also lead to due to its implementation in cython, it may be slower. It may also lead to
interpreter crashes. interpreter crashes.
......
...@@ -36,22 +36,6 @@ FAILING_TESTS = [ ...@@ -36,22 +36,6 @@ FAILING_TESTS = [
] ]
if os.environ.get('GEVENT_RESOLVER') == 'ares' or LEAKTEST:
# XXX fix this
FAILING_TESTS += [
'FLAKY test__socket_dns.py',
'FLAKY test__socket_dns6.py',
]
else:
FAILING_TESTS += [
# A number of the host names hardcoded have multiple, load
# balanced DNS entries. Therefore, multiple sequential calls
# of the resolution function, whether gevent or stdlib, can
# return non-equal results, possibly dependent on the host
# dns configuration
'FLAKY test__socket_dns6.py',
]
if sys.platform == 'win32': if sys.platform == 'win32':
# other Windows-related issues (need investigating) # other Windows-related issues (need investigating)
FAILING_TESTS += [ FAILING_TESTS += [
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import _six as six import _six as six
import re import re
import greentest import greentest
import unittest
import socket import socket
from time import time from time import time
import gevent import gevent
...@@ -18,6 +19,8 @@ if getattr(resolver, 'pool', None) is not None: ...@@ -18,6 +19,8 @@ if getattr(resolver, 'pool', None) is not None:
resolver.pool.size = 1 resolver.pool.size = 1
RESOLVER_IS_ARES = 'ares' in gevent.get_hub().resolver_class.__module__
assert gevent_socket.gaierror is socket.gaierror assert gevent_socket.gaierror is socket.gaierror
assert gevent_socket.error is socket.error assert gevent_socket.error is socket.error
...@@ -155,18 +158,22 @@ def add(klass, hostname, name=None): ...@@ -155,18 +158,22 @@ def add(klass, hostname, name=None):
call = callable(hostname) call = callable(hostname)
def _setattr(k, n, v):
if not hasattr(k, n):
setattr(k, n, v)
if name is None: if name is None:
if call: if call:
name = hostname.__name__ name = hostname.__name__
else: else:
name = re.sub('[^\w]+', '_', repr(hostname)) name = re.sub(r'[^\w]+', '_', repr(hostname))
assert name, repr(hostname) assert name, repr(hostname)
def test1(self): def test1(self):
x = hostname() if call else hostname x = hostname() if call else hostname
self._test('getaddrinfo', x, 'http') self._test('getaddrinfo', x, 'http')
test1.__name__ = 'test_%s_getaddrinfo' % name test1.__name__ = 'test_%s_getaddrinfo' % name
setattr(klass, test1.__name__, test1) _setattr(klass, test1.__name__, test1)
def test2(self): def test2(self):
x = hostname() if call else hostname x = hostname() if call else hostname
...@@ -174,33 +181,37 @@ def add(klass, hostname, name=None): ...@@ -174,33 +181,37 @@ def add(klass, hostname, name=None):
if not isinstance(ipaddr, Exception): if not isinstance(ipaddr, Exception):
self._test('gethostbyaddr', ipaddr) self._test('gethostbyaddr', ipaddr)
test2.__name__ = 'test_%s_gethostbyname' % name test2.__name__ = 'test_%s_gethostbyname' % name
setattr(klass, test2.__name__, test2) _setattr(klass, test2.__name__, test2)
def test3(self): def test3(self):
x = hostname() if call else hostname x = hostname() if call else hostname
self._test('gethostbyname_ex', x) self._test('gethostbyname_ex', x)
test3.__name__ = 'test_%s_gethostbyname_ex' % name test3.__name__ = 'test_%s_gethostbyname_ex' % name
setattr(klass, test3.__name__, test3) _setattr(klass, test3.__name__, test3)
def test4(self): def test4(self):
x = hostname() if call else hostname x = hostname() if call else hostname
self._test('gethostbyaddr', x) self._test('gethostbyaddr', x)
test4.__name__ = 'test_%s_gethostbyaddr' % name test4.__name__ = 'test_%s_gethostbyaddr' % name
setattr(klass, test4.__name__, test4) _setattr(klass, test4.__name__, test4)
def test5(self): def test5(self):
x = hostname() if call else hostname x = hostname() if call else hostname
self._test('getnameinfo', (x, 80), 0) self._test('getnameinfo', (x, 80), 0)
test5.__name__ = 'test_%s_getnameinfo' % name test5.__name__ = 'test_%s_getnameinfo' % name
setattr(klass, test5.__name__, test5) _setattr(klass, test5.__name__, test5)
class TestCase(greentest.TestCase): class TestCase(greentest.TestCase):
__timeout__ = 30 __timeout__ = 30
switch_expected = None switch_expected = None
verbose_dns = False
def should_log_results(self, result1, result2): def should_log_results(self, result1, result2):
if not self.verbose_dns:
return False
if isinstance(result1, BaseException) and isinstance(result2, BaseException): if isinstance(result1, BaseException) and isinstance(result2, BaseException):
return type(result1) is not type(result2) return type(result1) is not type(result2)
return repr(result1) != repr(result2) return repr(result1) != repr(result2)
...@@ -214,9 +225,9 @@ class TestCase(greentest.TestCase): ...@@ -214,9 +225,9 @@ class TestCase(greentest.TestCase):
log('') log('')
log_call(real_result, time_real, real_func, *args) log_call(real_result, time_real, real_func, *args)
log_call(gevent_result, time_gevent, gevent_func, *args) log_call(gevent_result, time_gevent, gevent_func, *args)
self.assertEqualResults(real_result, gevent_result, func, args) self.assertEqualResults(real_result, gevent_result, func)
if time_gevent > time_real + 0.01 and time_gevent > 0.02: if self.verbose_dns and time_gevent > time_real + 0.01 and time_gevent > 0.02:
msg = 'gevent:%s%s took %dms versus %dms stdlib' % (func, args, time_gevent * 1000.0, time_real * 1000.0) msg = 'gevent:%s%s took %dms versus %dms stdlib' % (func, args, time_gevent * 1000.0, time_real * 1000.0)
if time_gevent > time_real + 1: if time_gevent > time_real + 1:
...@@ -228,18 +239,71 @@ class TestCase(greentest.TestCase): ...@@ -228,18 +239,71 @@ class TestCase(greentest.TestCase):
return gevent_result return gevent_result
def _normalize_result(self, result): def _normalize_result(self, result, func_name):
norm_name = '_normalize_result_' + func_name
if hasattr(self, norm_name):
return getattr(self, norm_name)(result)
return result
def _normalize_result_gethostbyname_ex(self, result):
# Often the second and third part of the tuple (hostname, aliaslist, ipaddrlist)
# can be in different orders if we're hitting different servers,
# or using the native and ares resolvers due to load-balancing techniques.
# We sort them.
if not RESOLVER_IS_ARES or isinstance(result, BaseException):
return result
# result[1].sort() # we wind up discarding this
# On Py2 in test_russion_gethostbyname_ex, this
# is actually an integer, for some reason. In TestLocalhost.tets__ip6_localhost,
# the result isn't this long (maybe an error?).
try:
result[2].sort()
except AttributeError:
pass
except IndexError:
return result
# On some systems, a random alias is found in the aliaslist
# by the system resolver, but not by cares, and vice versa. We deem the aliaslist
# unimportant and discard it.
# On some systems (Travis CI), the ipaddrlist for 'localhost' can come back
# with two entries 127.0.0.1 (presumably two interfaces?) for c-ares
ips = result[2]
if ips == ['127.0.0.1', '127.0.0.1']:
ips = ['127.0.0.1']
return (result[0], [], ips)
def _normalize_result_getaddrinfo(self, result):
if not RESOLVER_IS_ARES:
return result
# On Python 3, the builtin resolver can return SOCK_RAW results, but
# c-ares doesn't do that. So we remove those if we find them.
if hasattr(socket, 'SOCK_RAW') and isinstance(result, list):
result = [x for x in result if x[1] != socket.SOCK_RAW]
if isinstance(result, list):
result.sort()
return result
def _normalize_result_gethostbyaddr(self, result):
if not RESOLVER_IS_ARES:
return result
if isinstance(result, tuple):
# On some systems, a random alias is found in the aliaslist
# by the system resolver, but not by cares and vice versa. We deem the aliaslist
# unimportant and discard it.
return (result[0], [], result[2])
return result return result
def assertEqualResults(self, real_result, gevent_result, func, args): def assertEqualResults(self, real_result, gevent_result, func):
errors = (socket.gaierror, socket.herror, TypeError) errors = (socket.gaierror, socket.herror, TypeError)
if isinstance(real_result, errors) and isinstance(gevent_result, errors): if isinstance(real_result, errors) and isinstance(gevent_result, errors):
if type(real_result) is not type(gevent_result): if type(real_result) is not type(gevent_result):
log('WARNING: error type mismatch: %r (gevent) != %r (stdlib)', gevent_result, real_result) log('WARNING: error type mismatch: %r (gevent) != %r (stdlib)', gevent_result, real_result)
return return
real_result = self._normalize_result(real_result) real_result = self._normalize_result(real_result, func)
gevent_result = self._normalize_result(gevent_result) gevent_result = self._normalize_result(gevent_result, func)
real_result_repr = repr(real_result) real_result_repr = repr(real_result)
gevent_result_repr = repr(gevent_result) gevent_result_repr = repr(gevent_result)
...@@ -248,11 +312,18 @@ class TestCase(greentest.TestCase): ...@@ -248,11 +312,18 @@ class TestCase(greentest.TestCase):
if relaxed_is_equal(gevent_result, real_result): if relaxed_is_equal(gevent_result, real_result):
return return
# If we're using the ares resolver, allow the real resolver to generate an
# error that the ares resolver actually gets an answer to.
if (RESOLVER_IS_ARES
and isinstance(real_result, errors)
and not isinstance(gevent_result, errors)):
return
# From 2.7 on, assertEqual does a better job highlighting the results than we would # From 2.7 on, assertEqual does a better job highlighting the results than we would
# because it calls assertSequenceEqual, which highlights the exact # because it calls assertSequenceEqual, which highlights the exact
# difference in the tuple # difference in the tuple
msg = format_call(func, args) self.assertEqual(real_result, gevent_result)
self.assertEqual((msg, gevent_result), (msg, real_result))
class TestTypeError(TestCase): class TestTypeError(TestCase):
...@@ -271,11 +342,23 @@ add(TestHostname, socket.gethostname) ...@@ -271,11 +342,23 @@ add(TestHostname, socket.gethostname)
class TestLocalhost(TestCase): class TestLocalhost(TestCase):
# certain tests in test_patched_socket.py only work if getaddrinfo('localhost') does not switch # certain tests in test_patched_socket.py only work if getaddrinfo('localhost') does not switch
# (e.g. NetworkConnectionAttributesTest.testSourceAddress) # (e.g. NetworkConnectionAttributesTest.testSourceAddress)
pass
#switch_expected = False #switch_expected = False
# XXX: The above has been commented out for some time. Apparently this isn't the case
# anymore.
def _normalize_result_getaddrinfo(self, result):
if RESOLVER_IS_ARES:
# We see that some impls (OS X) return extra results
# like DGRAM that ares does not.
return ()
return super(TestLocalhost, self)._normalize_result_getaddrinfo(result)
add(TestLocalhost, 'localhost') add(TestLocalhost, 'localhost')
add(TestLocalhost, 'ip6-localhost') if not greentest.RUNNING_ON_TRAVIS:
# ares fails here, for some reason, presumably a badly
# configured /etc/hosts
add(TestLocalhost, 'ip6-localhost')
class TestNonexistent(TestCase): class TestNonexistent(TestCase):
...@@ -299,6 +382,14 @@ add(Test127001, '127.0.0.1') ...@@ -299,6 +382,14 @@ add(Test127001, '127.0.0.1')
class TestBroadcast(TestCase): class TestBroadcast(TestCase):
switch_expected = False switch_expected = False
if RESOLVER_IS_ARES:
# ares raises errors for broadcasthost/255.255.255.255
@unittest.skip('ares raises errors for broadcasthost/255.255.255.255')
def test__broadcast__gethostbyaddr(self):
return
test__broadcast__gethostbyname = test__broadcast__gethostbyaddr
add(TestBroadcast, '<broadcast>') add(TestBroadcast, '<broadcast>')
...@@ -307,11 +398,25 @@ class TestEtcHosts(TestCase): ...@@ -307,11 +398,25 @@ class TestEtcHosts(TestCase):
pass pass
try: try:
etc_hosts = open('/etc/hosts').read() with open('/etc/hosts') as f:
etc_hosts = f.read()
except IOError: except IOError:
etc_hosts = '' etc_hosts = ''
for ip, host in re.findall(r'^\s*(\d+\.\d+\.\d+\.\d+)\s+([^\s]+)', etc_hosts, re.M)[:10]: for ip, host in re.findall(r'^\s*(\d+\.\d+\.\d+\.\d+)\s+([^\s]+)', etc_hosts, re.M)[:10]:
if (RESOLVER_IS_ARES
and (host.endswith('local') # ignore bonjour, ares can't find them
# ignore common aliases that ares can't find
or ip == '255.255.255.255'
or host == 'broadcasthost'
# We get extra results from some impls, like OS X
# it returns DGRAM results
or host == 'localhost')):
continue
if host.endswith('local'):
# These can only be found if bonjour is running,
# and are very slow to do so with the system resolver on OS X
continue
add(TestEtcHosts, host) add(TestEtcHosts, host)
add(TestEtcHosts, ip) add(TestEtcHosts, ip)
del host, ip del host, ip
...@@ -353,10 +458,14 @@ class TestFamily(TestCase): ...@@ -353,10 +458,14 @@ class TestFamily(TestCase):
raise raise
def test_inet(self): def test_inet(self):
self.assertEqual(gevent_socket.getaddrinfo(TestGeventOrg.HOSTNAME, None, socket.AF_INET), self.getresult()) self.assertEqualResults(self.getresult(),
gevent_socket.getaddrinfo(TestGeventOrg.HOSTNAME, None, socket.AF_INET),
'getaddrinfo')
def test_unspec(self): def test_unspec(self):
self.assertEqual(gevent_socket.getaddrinfo(TestGeventOrg.HOSTNAME, None, socket.AF_UNSPEC), self.getresult()) self.assertEqualResults(self.getresult(),
gevent_socket.getaddrinfo(TestGeventOrg.HOSTNAME, None, socket.AF_UNSPEC),
'getaddrinfo')
def test_badvalue(self): def test_badvalue(self):
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, 255) self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, 255)
...@@ -407,8 +516,15 @@ add(TestInternational, u'президент.рф', 'russian') ...@@ -407,8 +516,15 @@ add(TestInternational, u'президент.рф', 'russian')
add(TestInternational, u'президент.рф'.encode('idna'), 'idna') add(TestInternational, u'президент.рф'.encode('idna'), 'idna')
class TestInterrupted_gethostbyname(greentest.GenericWaitTestCase): class TestInterrupted_gethostbyname(greentest.GenericWaitTestCase):
# There are refs to a Waiter in the C code that don't go
# away yet; one gc may or may not do it.
@greentest.ignores_leakcheck
def test_returns_none_after_timeout(self):
super(TestInterrupted_gethostbyname, self).test_returns_none_after_timeout()
def wait(self, timeout): def wait(self, timeout):
with gevent.Timeout(timeout, False): with gevent.Timeout(timeout, False):
for index in xrange(1000000): for index in xrange(1000000):
...@@ -488,7 +604,7 @@ class Test_getnameinfo_fail(TestCase): ...@@ -488,7 +604,7 @@ class Test_getnameinfo_fail(TestCase):
self._test('getnameinfo', ('www.gevent.org', 'http'), 0) self._test('getnameinfo', ('www.gevent.org', 'http'), 0)
def test_bad_flags(self): def test_bad_flags(self):
self._test('getnameinfo', ('127.0.0.1', 80), 55555555) self._test('getnameinfo', ('localhost', 80), 55555555)
class TestInvalidPort(TestCase): class TestInvalidPort(TestCase):
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import greentest import greentest
import socket import socket
from test__socket_dns import TestCase, add from test__socket_dns import TestCase, add, RESOLVER_IS_ARES
class Test6(TestCase): class Test6(TestCase):
...@@ -26,15 +26,34 @@ class Test6(TestCase): ...@@ -26,15 +26,34 @@ class Test6(TestCase):
class Test6_google(Test6): class Test6_google(Test6):
host = 'ipv6.google.com' host = 'ipv6.google.com'
def _normalize_result_getnameinfo(self, result):
if greentest.RUNNING_ON_CI and RESOLVER_IS_ARES:
# Disabled, there are multiple possibilities
# and we can get different ones, rarely.
return ()
return result
class Test6_ds(Test6): add(Test6, Test6.host)
add(Test6_google, Test6_google.host)
if not greentest.RUNNING_ON_CI:
# We can't control the DNS servers we use there
# for the system. This works best with the google DNS servers
# The getnameinfo test can fail on CI
class Test6_ds(Test6):
# host that has both A and AAAA records # host that has both A and AAAA records
host = 'ds.test-ipv6.com' host = 'ds.test-ipv6.com'
def _normalize_result_gethostbyaddr(self, result):
# This test is effectively disabled. There are multiple address
# that resolve and which ones you get depend on the settings
# of the system and ares. They don't match exactly.
return ()
add(Test6, Test6.host) _normalize_result_gethostbyname = _normalize_result_gethostbyaddr
add(Test6_google, Test6_google.host)
add(Test6_ds, Test6_ds.host) add(Test6_ds, Test6_ds.host)
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment