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 @@
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)
===================
......
......@@ -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)
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):
the threaded resolver). However, because it does not use threads,
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
due to its implementation in cython, it may be slower. It may also lead to
interpreter crashes.
......
......@@ -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':
# other Windows-related issues (need investigating)
FAILING_TESTS += [
......
......@@ -3,6 +3,7 @@
import _six as six
import re
import greentest
import unittest
import socket
from time import time
import gevent
......@@ -18,6 +19,8 @@ if getattr(resolver, 'pool', None) is not None:
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.error is socket.error
......@@ -155,18 +158,22 @@ def add(klass, hostname, name=None):
call = callable(hostname)
def _setattr(k, n, v):
if not hasattr(k, n):
setattr(k, n, v)
if name is None:
if call:
name = hostname.__name__
else:
name = re.sub('[^\w]+', '_', repr(hostname))
name = re.sub(r'[^\w]+', '_', repr(hostname))
assert name, repr(hostname)
def test1(self):
x = hostname() if call else hostname
self._test('getaddrinfo', x, 'http')
test1.__name__ = 'test_%s_getaddrinfo' % name
setattr(klass, test1.__name__, test1)
_setattr(klass, test1.__name__, test1)
def test2(self):
x = hostname() if call else hostname
......@@ -174,33 +181,37 @@ def add(klass, hostname, name=None):
if not isinstance(ipaddr, Exception):
self._test('gethostbyaddr', ipaddr)
test2.__name__ = 'test_%s_gethostbyname' % name
setattr(klass, test2.__name__, test2)
_setattr(klass, test2.__name__, test2)
def test3(self):
x = hostname() if call else hostname
self._test('gethostbyname_ex', x)
test3.__name__ = 'test_%s_gethostbyname_ex' % name
setattr(klass, test3.__name__, test3)
_setattr(klass, test3.__name__, test3)
def test4(self):
x = hostname() if call else hostname
self._test('gethostbyaddr', x)
test4.__name__ = 'test_%s_gethostbyaddr' % name
setattr(klass, test4.__name__, test4)
_setattr(klass, test4.__name__, test4)
def test5(self):
x = hostname() if call else hostname
self._test('getnameinfo', (x, 80), 0)
test5.__name__ = 'test_%s_getnameinfo' % name
setattr(klass, test5.__name__, test5)
_setattr(klass, test5.__name__, test5)
class TestCase(greentest.TestCase):
__timeout__ = 30
switch_expected = None
verbose_dns = False
def should_log_results(self, result1, result2):
if not self.verbose_dns:
return False
if isinstance(result1, BaseException) and isinstance(result2, BaseException):
return type(result1) is not type(result2)
return repr(result1) != repr(result2)
......@@ -214,9 +225,9 @@ class TestCase(greentest.TestCase):
log('')
log_call(real_result, time_real, real_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)
if time_gevent > time_real + 1:
......@@ -228,18 +239,71 @@ class TestCase(greentest.TestCase):
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
def assertEqualResults(self, real_result, gevent_result, func, args):
def assertEqualResults(self, real_result, gevent_result, func):
errors = (socket.gaierror, socket.herror, TypeError)
if isinstance(real_result, errors) and isinstance(gevent_result, errors):
if type(real_result) is not type(gevent_result):
log('WARNING: error type mismatch: %r (gevent) != %r (stdlib)', gevent_result, real_result)
return
real_result = self._normalize_result(real_result)
gevent_result = self._normalize_result(gevent_result)
real_result = self._normalize_result(real_result, func)
gevent_result = self._normalize_result(gevent_result, func)
real_result_repr = repr(real_result)
gevent_result_repr = repr(gevent_result)
......@@ -248,11 +312,18 @@ class TestCase(greentest.TestCase):
if relaxed_is_equal(gevent_result, real_result):
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
# because it calls assertSequenceEqual, which highlights the exact
# difference in the tuple
msg = format_call(func, args)
self.assertEqual((msg, gevent_result), (msg, real_result))
self.assertEqual(real_result, gevent_result)
class TestTypeError(TestCase):
......@@ -271,11 +342,23 @@ add(TestHostname, socket.gethostname)
class TestLocalhost(TestCase):
# certain tests in test_patched_socket.py only work if getaddrinfo('localhost') does not switch
# (e.g. NetworkConnectionAttributesTest.testSourceAddress)
pass
#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, '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):
......@@ -299,6 +382,14 @@ add(Test127001, '127.0.0.1')
class TestBroadcast(TestCase):
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>')
......@@ -307,11 +398,25 @@ class TestEtcHosts(TestCase):
pass
try:
etc_hosts = open('/etc/hosts').read()
with open('/etc/hosts') as f:
etc_hosts = f.read()
except IOError:
etc_hosts = ''
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, ip)
del host, ip
......@@ -353,10 +458,14 @@ class TestFamily(TestCase):
raise
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):
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):
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, 255)
......@@ -407,8 +516,15 @@ add(TestInternational, u'президент.рф', 'russian')
add(TestInternational, u'президент.рф'.encode('idna'), 'idna')
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):
with gevent.Timeout(timeout, False):
for index in xrange(1000000):
......@@ -488,7 +604,7 @@ class Test_getnameinfo_fail(TestCase):
self._test('getnameinfo', ('www.gevent.org', 'http'), 0)
def test_bad_flags(self):
self._test('getnameinfo', ('127.0.0.1', 80), 55555555)
self._test('getnameinfo', ('localhost', 80), 55555555)
class TestInvalidPort(TestCase):
......
......@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
import greentest
import socket
from test__socket_dns import TestCase, add
from test__socket_dns import TestCase, add, RESOLVER_IS_ARES
class Test6(TestCase):
......@@ -26,15 +26,34 @@ class Test6(TestCase):
class Test6_google(Test6):
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 = '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)
add(Test6_google, Test6_google.host)
add(Test6_ds, Test6_ds.host)
_normalize_result_gethostbyname = _normalize_result_gethostbyaddr
add(Test6_ds, Test6_ds.host)
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