Repository: numba/numba

Issue:
Write tests for numba.test.support
The `TestCase` class in `numba.tests.support` is starting to be non-trivial, especially the `assertPreciseEqual` method. It would be useful to test it does what we want it to do.



Discussion:

Resolving patch:
diff --git a/numba/targets/builtins.py b/numba/targets/builtins.py
--- a/numba/targets/builtins.py
+++ b/numba/targets/builtins.py
@@ -740,11 +740,10 @@ def complex_imag_impl(context, builder, typ, value):
 @builtin
 @implement("complex.conjugate", types.Kind(types.Complex))
 def complex_conjugate_impl(context, builder, sig, args):
+    from . import mathimpl
     cplx_cls = context.make_complex(sig.args[0])
     z = cplx_cls(context, builder, args[0])
-    imag = z.imag
-    zero = cgutils.get_null_value(imag.type)
-    z.imag = builder.fsub(zero, imag)
+    z.imag = mathimpl.negate_real(builder, z.imag)
     return z._getvalue()
 
 def real_real_impl(context, builder, typ, value):
diff --git a/numba/targets/cmathimpl.py b/numba/targets/cmathimpl.py
--- a/numba/targets/cmathimpl.py
+++ b/numba/targets/cmathimpl.py
@@ -75,14 +75,23 @@ def rect(r, phi, phi_is_finite):
         if not phi_is_finite:
             if not r:
                 # cmath.rect(0, phi={inf, nan}) = 0
-                return complex(r, r)
+                return abs(r)
             if math.isinf(r):
                 # cmath.rect(inf, phi={inf, nan}) = inf + j phi
                 return complex(r, phi)
-        if not phi:
-            # cmath.rect(r, 0) = r
-            return complex(r, phi)
-        return r * complex(math.cos(phi), math.sin(phi))
+        real = math.cos(phi)
+        imag = math.sin(phi)
+        if real == 0. and math.isinf(r):
+            # 0 * inf would return NaN, we want to keep 0 but xor the sign
+            real /= r
+        else:
+            real *= r
+        if imag == 0. and math.isinf(r):
+            # ditto
+            imag /= r
+        else:
+            imag *= r
+        return complex(real, imag)
 
     inner_sig = signature(sig.return_type, *sig.args + (types.boolean,))
     return context.compile_internal(builder, rect, inner_sig,
@@ -132,9 +141,15 @@ def exp_impl(x, y, x_is_finite, y_is_finite):
     elif x > 0.0:
         # x == +inf
         if y_is_finite:
-            c = math.cos(y)
-            s = math.sin(y)
-            return complex(x * c, y * s)
+            real = math.cos(y)
+            imag = math.sin(y)
+            # Avoid NaNs if math.cos(y) or math.sin(y) == 0
+            # (e.g. cmath.exp(inf + 0j) == inf + 0j)
+            if real != 0:
+                real *= x
+            if imag != 0:
+                imag *= x
+            return complex(real, imag)
         else:
             return complex(x, NAN)
     else:
@@ -267,19 +282,17 @@ def cosh_impl(z):
         x = z.real
         y = z.imag
         if math.isinf(x):
-            real = abs(x)
-            if y == 0.0:
-                # x = +inf, y = 0 => cmath.cosh(x + y j) = inf + 0j
-                imag = y
-            elif math.isnan(y):
+            if math.isnan(y):
                 # x = +inf, y = NaN => cmath.cosh(x + y j) = inf + Nan * j
+                real = abs(x)
+                imag = y
+            elif y == 0.0:
+                # x = +inf, y = 0 => cmath.cosh(x + y j) = inf + 0j
+                real = abs(x)
                 imag = y
-            elif y < 0.0:
-                # x = +inf, y < 0 => cmath.cosh(x + y j) = inf - inf * j
-                imag = -real
             else:
-                # x = +inf, y > 0 => cmath.cosh(x + y j) = inf + inf * j
-                imag = real
+                real = math.copysign(x, math.cos(y))
+                imag = math.copysign(x, math.sin(y))
             if x < 0.0:
                 # x = -inf => negate imaginary part of result
                 imag = -imag
@@ -308,19 +321,17 @@ def sinh_impl(z):
         x = z.real
         y = z.imag
         if math.isinf(x):
-            real = x
-            if y == 0.0:
-                # x = +/-inf, y = 0 => cmath.sinh(x + y j) = x + y * j
-                imag = y
-            elif math.isnan(y):
+            if math.isnan(y):
                 # x = +/-inf, y = NaN => cmath.sinh(x + y j) = x + NaN * j
+                real = x
                 imag = y
-            elif y < 0.0:
-                # x = +/-inf, y < 0 => cmath.cosh(x + y j) = x - inf * j
-                imag = -abs(x)
             else:
-                # x = +/-inf, y > 0 => cmath.cosh(x + y j) = x + inf * j
-                imag = abs(x)
+                real = math.cos(y)
+                imag = math.sin(y)
+                if real != 0.:
+                    real *= x
+                if imag != 0.:
+                    imag *= abs(x)
             return complex(real, imag)
         return complex(math.cos(y) * math.sinh(x),
                        math.sin(y) * math.cosh(x))
@@ -347,7 +358,10 @@ def tanh_impl(z):
         y = z.imag
         if math.isinf(x):
             real = math.copysign(1., x)
-            imag = math.copysign(0., y)
+            if math.isinf(y):
+                imag = 0.
+            else:
+                imag = math.copysign(0., math.sin(2. * y))
             return complex(real, imag)
         # This is CPython's algorithm (see c_tanh() in cmathmodule.c).
         # XXX how to force float constants into single precision?
@@ -455,7 +469,11 @@ def atan_impl(context, builder, sig, args):
     def atan_impl(z):
         """cmath.atan(z) = -j * cmath.atanh(z j)"""
         r = cmath.atanh(complex(-z.imag, z.real))
-        return complex(r.imag, -r.real)
+        if math.isinf(z.real) and math.isnan(z.imag):
+            # XXX this is odd but necessary
+            return complex(r.imag, r.real)
+        else:
+            return complex(r.imag, -r.real)
 
     return context.compile_internal(builder, atan_impl, sig, args)
 


Test patch:
diff --git a/numba/tests/support.py b/numba/tests/support.py
--- a/numba/tests/support.py
+++ b/numba/tests/support.py
@@ -92,6 +92,8 @@ def assertTypingError(self):
     _exact_typesets = [(bool, np.bool_), utils.INT_TYPES, (str,), (utils.text_type),]
     _approx_typesets = [(float,), (complex,), (np.inexact)]
     _sequence_typesets = [(tuple,)]
+    _float_types = (float, np.floating)
+    _complex_types = (complex, np.complexfloating)
 
     def assertPreciseEqual(self, first, second, prec='exact', ulps=1,
                            msg=None):
@@ -109,6 +111,37 @@ def assertPreciseEqual(self, first, second, prec='exact', ulps=1,
         Any value of *prec* other than 'exact', 'single' or 'double'
         will raise an error.
         """
+        try:
+            self._assertPreciseEqual(first, second, prec, ulps, msg)
+        except AssertionError as exc:
+            failure_msg = str(exc)
+            # Fall off of the 'except' scope to avoid Python 3 exception
+            # chaining.
+            self.fail("when comparing %s and %s: %s" % (first, second, failure_msg))
+        else:
+            return
+        # Decorate the failure message with more information
+        self.fail("when comparing %s and %s: %s" % (first, second, failure_msg))
+
+    def _assertPreciseEqual(self, first, second, prec='exact', ulps=1,
+                            msg=None):
+        """Recursive workhorse for assertPreciseEqual()."""
+
+        def _assertNumberEqual(first, second, delta=None):
+            if (delta is None or first == second == 0.0
+                or math.isinf(first) or math.isinf(second)):
+                self.assertEqual(first, second, msg=msg)
+                # For signed zeros
+                try:
+                    if math.copysign(1, first) != math.copysign(1, second):
+                        self.fail(
+                            self._formatMessage(msg,
+                                                "%s != %s" % (first, second)))
+                except TypeError:
+                    pass
+            else:
+                self.assertAlmostEqual(first, second, delta=delta, msg=msg)
+
         for tp in self._sequence_typesets:
             # For recognized sequences, recurse
             if isinstance(first, tp) or isinstance(second, tp):
@@ -116,7 +149,7 @@ def assertPreciseEqual(self, first, second, prec='exact', ulps=1,
                 self.assertIsInstance(second, tp)
                 self.assertEqual(len(first), len(second), msg=msg)
                 for a, b in zip(first, second):
-                    self.assertPreciseEqual(a, b, prec, ulps, msg)
+                    self._assertPreciseEqual(a, b, prec, ulps, msg)
                 return
         for tp in self._exact_typesets:
             # One or another could be the expected, the other the actual;
@@ -152,16 +185,9 @@ def assertPreciseEqual(self, first, second, prec='exact', ulps=1,
             # Not floats.
             pass
 
+        exact_comparison = exact_comparison or prec == 'exact'
+
         if not exact_comparison and prec != 'exact':
-            if (isinstance(first, complex)
-                and cmath.isinf(first) and cmath.isinf(second)):
-                # For infinite complex numbers, recurse on real
-                # and imaginary parts.
-                self.assertPreciseEqual(first.real, second.real,
-                                        prec, ulps, msg)
-                self.assertPreciseEqual(first.imag, second.imag,
-                                        prec, ulps, msg)
-                return
             if prec == 'single':
                 bits = 24
             elif prec == 'double':
@@ -170,9 +196,13 @@ def assertPreciseEqual(self, first, second, prec='exact', ulps=1,
                 raise ValueError("unsupported precision %r" % (prec,))
             k = 2 ** (ulps - bits - 1)
             delta = k * (abs(first) + abs(second))
-            self.assertAlmostEqual(first, second, delta=delta, msg=msg)
         else:
-            self.assertEqual(first, second, msg=msg)
+            delta = None
+        if isinstance(first, self._complex_types):
+            _assertNumberEqual(first.real, second.real, delta)
+            _assertNumberEqual(first.imag, second.imag, delta)
+        else:
+            _assertNumberEqual(first, second, delta)
 
     def run_nullary_func(self, pyfunc, flags):
         """
diff --git a/numba/tests/test_complex.py b/numba/tests/test_complex.py
--- a/numba/tests/test_complex.py
+++ b/numba/tests/test_complex.py
@@ -107,10 +107,15 @@ def basic_values(self):
         return [complex(x, y) for x, y in itertools.product(reals, reals)]
 
     def more_values(self):
-        reals = [-0.0, +0.0, 1, -1, +1.5, -3.5, -math.pi, +math.pi,
+        reals = [-0.0, +0.0, 1, -1, -math.pi, +math.pi,
                  float('-inf'), float('+inf'), float('nan')]
         return [complex(x, y) for x, y in itertools.product(reals, reals)]
 
+    def non_nan_values(self):
+        reals = [-0.0, +0.0, 1, -1, -math.pi, +math.pi,
+                 float('inf'), float('-inf')]
+        return [complex(x, y) for x, y in itertools.product(reals, reals)]
+
     def run_unary(self, pyfunc, x_types, x_values, ulps=1,
                   flags=enable_pyobj_flags):
         for tx in x_types:
@@ -209,10 +214,11 @@ def check_predicate_func(self, pyfunc, flags):
                        self.basic_values(), flags=flags)
 
     def check_unary_func(self, pyfunc, flags, ulps=1, values=None):
-        if not values:
-            values = self.more_values()
-        self.run_unary(pyfunc, [types.complex128, types.complex64],
-                       values, flags=flags, ulps=ulps)
+        self.run_unary(pyfunc, [types.complex128],
+                       values or self.more_values(), flags=flags, ulps=ulps)
+        # Avoid discontinuities around pi when in single precision.
+        self.run_unary(pyfunc, [types.complex64],
+                       values or self.basic_values(), flags=flags, ulps=ulps)
 
     # Conversions
 
@@ -229,11 +235,13 @@ def test_polar_npm(self):
         self.check_unary_func(polar_usecase, no_pyobj_flags)
 
     def test_rect(self, flags=enable_pyobj_flags):
-        values = [(z.real, z.imag) for z in self.more_values()
-                  if not math.isinf(z.imag) or z.real == 0]
-        value_types = [(types.float64, types.float64),
-                       (types.float32, types.float32)]
-        self.run_binary(rect_usecase, value_types, values, flags=flags)
+        def do_test(tp, seed_values):
+            values = [(z.real, z.imag) for z in seed_values
+                      if not math.isinf(z.imag) or z.real == 0]
+            self.run_binary(rect_usecase, [(tp, tp)], values, flags=flags)
+        do_test(types.float64, self.more_values())
+        # Avoid discontinuities around pi when in single precision.
+        do_test(types.float32, self.basic_values())
 
     def test_rect_npm(self):
         self.test_rect(flags=no_pyobj_flags)
@@ -279,7 +287,7 @@ def test_log_base(self, flags=enable_pyobj_flags):
         value_types = [(types.complex128, types.complex128),
                        (types.complex64, types.complex64)]
         self.run_binary(log_base_usecase, value_types, values, flags=flags,
-                        ulps=2)
+                        ulps=3)
 
     def test_log_base_npm(self):
         self.test_log_base(flags=no_pyobj_flags)
@@ -311,10 +319,12 @@ def test_asin_npm(self):
         self.check_unary_func(asin_usecase, no_pyobj_flags, ulps=2)
 
     def test_atan(self):
-        self.check_unary_func(atan_usecase, enable_pyobj_flags, ulps=2)
+        self.check_unary_func(atan_usecase, enable_pyobj_flags, ulps=2,)
+                              #values=self.non_infinite_values())
 
     def test_atan_npm(self):
-        self.check_unary_func(atan_usecase, no_pyobj_flags, ulps=2)
+        self.check_unary_func(atan_usecase, no_pyobj_flags, ulps=2,)
+                              #values=self.non_infinite_values())
 
     def test_cos(self):
         self.check_unary_func(cos_usecase, enable_pyobj_flags, ulps=2)
@@ -324,20 +334,16 @@ def test_cos_npm(self):
 
     def test_sin(self):
         # See test_sinh.
-        self.check_unary_func(sin_usecase, enable_pyobj_flags,
-                              values=self.basic_values())
+        self.check_unary_func(sin_usecase, enable_pyobj_flags)
 
     def test_sin_npm(self):
-        self.check_unary_func(sin_usecase, no_pyobj_flags,
-                              values=self.basic_values())
+        self.check_unary_func(sin_usecase, no_pyobj_flags)
 
     def test_tan(self):
-        self.check_unary_func(tan_usecase, enable_pyobj_flags, ulps=2,
-                              values=self.basic_values())
+        self.check_unary_func(tan_usecase, enable_pyobj_flags, ulps=2)
 
     def test_tan_npm(self):
-        self.check_unary_func(tan_usecase, enable_pyobj_flags, ulps=2,
-                              values=self.basic_values())
+        self.check_unary_func(tan_usecase, enable_pyobj_flags, ulps=2)
 
     # Hyperbolic functions
 
@@ -365,25 +371,17 @@ def test_cosh(self):
     def test_cosh_npm(self):
         self.check_unary_func(cosh_usecase, no_pyobj_flags, ulps=2)
 
-    # sinh() and tanh() results around math.pi are too different in single
-    # precision and double precision, which would fail the test;
-    # hence basic_values().
-
     def test_sinh(self):
-        self.check_unary_func(sinh_usecase, enable_pyobj_flags,
-                              values=self.basic_values())
+        self.check_unary_func(sinh_usecase, enable_pyobj_flags)
 
     def test_sinh_npm(self):
-        self.check_unary_func(sinh_usecase, no_pyobj_flags,
-                              values=self.basic_values())
+        self.check_unary_func(sinh_usecase, no_pyobj_flags)
 
     def test_tanh(self):
-        self.check_unary_func(tanh_usecase, enable_pyobj_flags, ulps=2,
-                              values=self.basic_values())
+        self.check_unary_func(tanh_usecase, enable_pyobj_flags, ulps=2)
 
     def test_tanh_npm(self):
-        self.check_unary_func(tanh_usecase, enable_pyobj_flags, ulps=2,
-                              values=self.basic_values())
+        self.check_unary_func(tanh_usecase, enable_pyobj_flags, ulps=2)
 
 
 if __name__ == '__main__':
diff --git a/numba/tests/test_support.py b/numba/tests/test_support.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_support.py
@@ -0,0 +1,233 @@
+from __future__ import print_function, absolute_import, division
+
+import itertools
+
+import numpy as np
+
+from numba import utils
+from numba import unittest_support as unittest
+from .support import TestCase
+
+
+DBL_EPSILON = 2**-52
+FLT_EPSILON = 2**-23
+
+INF = float('inf')
+NAN = float('nan')
+
+
+class TestAssertPreciseEqual(TestCase):
+    """
+    Tests for TestCase.assertPreciseEqual().
+    """
+
+    int_types = [int]
+    if utils.PYVERSION < (3,):
+        int_types.append(long)
+    np_float_types = [np.float32, np.float64]
+    float_types = [float] + np_float_types
+    np_complex_types = [np.complex64, np.complex128]
+    complex_types = [complex] + np_complex_types
+
+    def eq(self, left, right, **kwargs):
+        def assert_succeed(left, right):
+            self.assertPreciseEqual(left, right, **kwargs)
+            self.assertPreciseEqual(right, left, **kwargs)
+        assert_succeed(left, right)
+        assert_succeed((left, left), (right, right))
+
+    def ne(self, left, right, **kwargs):
+        def assert_fail(left, right):
+            try:
+                self.assertPreciseEqual(left, right, **kwargs)
+            except AssertionError:
+                pass
+            else:
+                self.fail("%s and %s unexpectedly considered equal" % (left, right))
+        assert_fail(left, right)
+        assert_fail(right, left)
+        assert_fail((left, left), (right, right))
+        assert_fail((right, right), (left, left))
+
+    def test_types(self):
+        # assertPreciseEqual() should test for type compatibility
+        # int-like, float-like, complex-like are not compatible
+        for i, f, c in itertools.product(self.int_types, self.float_types,
+                                         self.complex_types):
+            self.ne(i(1), f(1))
+            self.ne(f(1), c(1))
+            self.ne(i(1), c(1))
+        # int and long are compatible between each other
+        for u, v in itertools.product(self.int_types, self.int_types):
+            self.eq(u(1), v(1))
+        # NumPy float types are not compatible between each other
+        for u, v in itertools.product(self.np_float_types, self.np_float_types):
+            if u is v:
+                self.eq(u(1), v(1))
+            else:
+                self.ne(u(1), v(1))
+        # NumPy complex types are not compatible between each other
+        for u, v in itertools.product(self.np_complex_types, self.np_complex_types):
+            if u is v:
+                self.eq(u(1), v(1))
+            else:
+                self.ne(u(1), v(1))
+
+    def test_int_values(self):
+        for tp in self.int_types:
+            for prec in ['exact', 'single', 'double']:
+                self.eq(tp(0), tp(0), prec=prec)
+                self.ne(tp(0), tp(1), prec=prec)
+                self.ne(tp(-1), tp(1), prec=prec)
+                self.ne(tp(2**80), tp(1+2**80), prec=prec)
+
+    def test_float_values(self):
+        for tp in self.float_types:
+            for prec in ['exact', 'single', 'double']:
+                self.eq(tp(1.5), tp(1.5), prec=prec)
+                # Signed zeros
+                self.eq(tp(0.0), tp(0.0), prec=prec)
+                self.eq(tp(-0.0), tp(-0.0), prec=prec)
+                self.ne(tp(0.0), tp(-0.0), prec=prec)
+                # Infinities
+                self.eq(tp(INF), tp(INF), prec=prec)
+                self.ne(tp(INF), tp(1e38), prec=prec)
+                self.eq(tp(-INF), tp(-INF), prec=prec)
+                self.ne(tp(INF), tp(-INF), prec=prec)
+                # NaNs
+                self.eq(tp(NAN), tp(NAN), prec=prec)
+                self.ne(tp(NAN), tp(0), prec=prec)
+                self.ne(tp(NAN), tp(INF), prec=prec)
+                self.ne(tp(NAN), tp(-INF), prec=prec)
+
+
+    def test_float64_values(self):
+        for tp in [float, np.float64]:
+            self.ne(tp(1.0 + DBL_EPSILON), tp(1.0))
+
+    def test_float32_values(self):
+        tp = np.float32
+        self.ne(tp(1.0 + FLT_EPSILON), tp(1.0))
+
+    def test_float64_values_inexact(self):
+        for tp in [float, np.float64]:
+            for scale in [1.0, -2**3, 2**-4, -2**-20]:
+                a = scale * 1.0
+                b = scale * (1.0 + DBL_EPSILON)
+                c = scale * (1.0 + DBL_EPSILON * 2)
+                d = scale * (1.0 + DBL_EPSILON * 4)
+                self.ne(tp(a), tp(b))
+                self.ne(tp(a), tp(b), prec='exact')
+                self.eq(tp(a), tp(b), prec='double')
+                self.eq(tp(a), tp(b), prec='double', ulps=1)
+                self.ne(tp(a), tp(c), prec='double')
+                self.eq(tp(a), tp(c), prec='double', ulps=2)
+                self.ne(tp(a), tp(d), prec='double', ulps=2)
+                self.eq(tp(a), tp(c), prec='double', ulps=3)
+                self.eq(tp(a), tp(d), prec='double', ulps=3)
+
+    def test_float32_values_inexact(self):
+        tp = np.float32
+        for scale in [1.0, -2**3, 2**-4, -2**-20]:
+            # About the choice of 0.9: there seem to be issues when
+            # converting
+            a = scale * 1.0
+            b = scale * (1.0 + FLT_EPSILON)
+            c = scale * (1.0 + FLT_EPSILON * 2)
+            d = scale * (1.0 + FLT_EPSILON * 4)
+            self.ne(tp(a), tp(b))
+            self.ne(tp(a), tp(b), prec='exact')
+            self.ne(tp(a), tp(b), prec='double')
+            self.eq(tp(a), tp(b), prec='single')
+            self.ne(tp(a), tp(c), prec='single')
+            self.eq(tp(a), tp(c), prec='single', ulps=2)
+            self.ne(tp(a), tp(d), prec='single', ulps=2)
+            self.eq(tp(a), tp(c), prec='single', ulps=3)
+            self.eq(tp(a), tp(d), prec='single', ulps=3)
+
+    def test_complex_values(self):
+        # Complex literals with signed zeros are confusing, better use
+        # the explicit constructor.
+        c_pp, c_pn, c_np, c_nn = [complex(0.0, 0.0), complex(0.0, -0.0),
+                                  complex(-0.0, 0.0), complex(-0.0, -0.0)]
+        for tp in self.complex_types:
+            for prec in ['exact', 'single', 'double']:
+                self.eq(tp(1 + 2j), tp(1 + 2j), prec=prec)
+                self.ne(tp(1 + 1j), tp(1 + 2j), prec=prec)
+                self.ne(tp(2 + 2j), tp(1 + 2j), prec=prec)
+                # Signed zeros
+                self.eq(tp(c_pp), tp(c_pp), prec=prec)
+                self.eq(tp(c_np), tp(c_np), prec=prec)
+                self.eq(tp(c_nn), tp(c_nn), prec=prec)
+                self.ne(tp(c_pp), tp(c_pn), prec=prec)
+                self.ne(tp(c_pn), tp(c_nn), prec=prec)
+                # Infinities
+                self.eq(tp(complex(INF, INF)), tp(complex(INF, INF)), prec=prec)
+                self.eq(tp(complex(INF, -INF)), tp(complex(INF, -INF)), prec=prec)
+                self.eq(tp(complex(-INF, -INF)), tp(complex(-INF, -INF)), prec=prec)
+                self.ne(tp(complex(INF, INF)), tp(complex(INF, -INF)), prec=prec)
+                self.ne(tp(complex(INF, INF)), tp(complex(-INF, INF)), prec=prec)
+                self.eq(tp(complex(INF, 0)), tp(complex(INF, 0)), prec=prec)
+                # NaNs
+                self.eq(tp(complex(NAN, 0)), tp(complex(NAN, 0)), prec=prec)
+                self.eq(tp(complex(0, NAN)), tp(complex(0, NAN)), prec=prec)
+                self.eq(tp(complex(NAN, NAN)), tp(complex(NAN, NAN)), prec=prec)
+                self.eq(tp(complex(INF, NAN)), tp(complex(INF, NAN)), prec=prec)
+                self.eq(tp(complex(NAN, -INF)), tp(complex(NAN, -INF)), prec=prec)
+                # FIXME
+                #self.ne(tp(complex(NAN, INF)), tp(complex(NAN, -INF)))
+                #self.ne(tp(complex(NAN, 0)), tp(complex(NAN, 1)))
+                #self.ne(tp(complex(INF, NAN)), tp(complex(-INF, NAN)))
+                #self.ne(tp(complex(0, NAN)), tp(complex(1, NAN)))
+                #self.ne(tp(complex(NAN, 0)), tp(complex(0, NAN)))
+            # XXX should work with other precisions as well?
+            self.ne(tp(complex(INF, 0)), tp(complex(INF, 1)), prec='exact')
+
+    def test_complex128_values_inexact(self):
+        for tp in [complex, np.complex128]:
+            for scale in [1.0, -2**3, 2**-4, -2**-20]:
+                a = scale * 1.0
+                b = scale * (1.0 + DBL_EPSILON)
+                c = scale * (1.0 + DBL_EPSILON * 2)
+                aa = tp(complex(a, a))
+                ab = tp(complex(a, b))
+                bb = tp(complex(b, b))
+                self.ne(tp(aa), tp(ab))
+                self.eq(tp(aa), tp(ab), prec='double')
+                self.eq(tp(ab), tp(bb), prec='double')
+                self.eq(tp(aa), tp(bb), prec='double')
+                ac = tp(complex(a, c))
+                cc = tp(complex(c, c))
+                self.ne(tp(aa), tp(ac), prec='double')
+                self.ne(tp(ac), tp(cc), prec='double')
+                self.eq(tp(aa), tp(ac), prec='double', ulps=2)
+                self.eq(tp(ac), tp(cc), prec='double', ulps=2)
+                self.eq(tp(aa), tp(cc), prec='double', ulps=2)
+                self.eq(tp(aa), tp(cc), prec='single')
+
+    def test_complex64_values_inexact(self):
+        tp = np.complex64
+        for scale in [1.0, -2**3, 2**-4, -2**-20]:
+            a = scale * 1.0
+            b = scale * (1.0 + FLT_EPSILON)
+            c = scale * (1.0 + FLT_EPSILON * 2)
+            aa = tp(complex(a, a))
+            ab = tp(complex(a, b))
+            bb = tp(complex(b, b))
+            self.ne(tp(aa), tp(ab))
+            self.ne(tp(aa), tp(ab), prec='double')
+            self.eq(tp(aa), tp(ab), prec='single')
+            self.eq(tp(ab), tp(bb), prec='single')
+            self.eq(tp(aa), tp(bb), prec='single')
+            ac = tp(complex(a, c))
+            cc = tp(complex(c, c))
+            self.ne(tp(aa), tp(ac), prec='single')
+            self.ne(tp(ac), tp(cc), prec='single')
+            self.eq(tp(aa), tp(ac), prec='single', ulps=2)
+            self.eq(tp(ac), tp(cc), prec='single', ulps=2)
+            self.eq(tp(aa), tp(cc), prec='single', ulps=2)
+
+
+if __name__ == '__main__':
+    unittest.main()
+