Skip to content
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

[3.12] gh-121039: add Floats/ComplexesAreIdenticalMixin to test.support.testcase (GH-121071) #123841

Merged
merged 2 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Lib/test/support/testcase.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from math import copysign, isnan


class ExceptionIsLikeMixin:
def assertExceptionIsLike(self, exc, template):
"""
Expand All @@ -23,3 +26,40 @@ def assertExceptionIsLike(self, exc, template):
self.assertEqual(len(exc.exceptions), len(template.exceptions))
for e, t in zip(exc.exceptions, template.exceptions):
self.assertExceptionIsLike(e, t)


class FloatsAreIdenticalMixin:
def assertFloatsAreIdentical(self, x, y):
"""Fail unless floats x and y are identical, in the sense that:
(1) both x and y are nans, or
(2) both x and y are infinities, with the same sign, or
(3) both x and y are zeros, with the same sign, or
(4) x and y are both finite and nonzero, and x == y

"""
msg = 'floats {!r} and {!r} are not identical'

if isnan(x) or isnan(y):
if isnan(x) and isnan(y):
return
elif x == y:
if x != 0.0:
return
# both zero; check that signs match
elif copysign(1.0, x) == copysign(1.0, y):
return
else:
msg += ': zeros have different signs'
self.fail(msg.format(x, y))


class ComplexesAreIdenticalMixin(FloatsAreIdenticalMixin):
def assertComplexesAreIdentical(self, x, y):
"""Fail unless complex numbers x and y have equal values and signs.

In particular, if x and y both have real (or imaginary) part
zero, but the zeros have different signs, this test will fail.

"""
self.assertFloatsAreIdentical(x.real, y.real)
self.assertFloatsAreIdentical(x.imag, y.imag)
27 changes: 12 additions & 15 deletions Lib/test/test_capi/test_getargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from test.support import import_helper
from test.support import script_helper
from test.support import warnings_helper
from test.support.testcase import FloatsAreIdenticalMixin
# Skip this test if the _testcapi module isn't available.
_testcapi = import_helper.import_module('_testcapi')
from _testcapi import getargs_keywords, getargs_keyword_only
Expand Down Expand Up @@ -436,11 +437,7 @@ def test_K(self):
self.assertEqual(VERY_LARGE & ULLONG_MAX, getargs_K(VERY_LARGE))


class Float_TestCase(unittest.TestCase):
def assertEqualWithSign(self, actual, expected):
self.assertEqual(actual, expected)
self.assertEqual(math.copysign(1, actual), math.copysign(1, expected))

class Float_TestCase(unittest.TestCase, FloatsAreIdenticalMixin):
def test_f(self):
from _testcapi import getargs_f
self.assertEqual(getargs_f(4.25), 4.25)
Expand All @@ -462,10 +459,10 @@ def test_f(self):
self.assertEqual(getargs_f(DBL_MAX), INF)
self.assertEqual(getargs_f(-DBL_MAX), -INF)
if FLT_MIN > DBL_MIN:
self.assertEqualWithSign(getargs_f(DBL_MIN), 0.0)
self.assertEqualWithSign(getargs_f(-DBL_MIN), -0.0)
self.assertEqualWithSign(getargs_f(0.0), 0.0)
self.assertEqualWithSign(getargs_f(-0.0), -0.0)
self.assertFloatsAreIdentical(getargs_f(DBL_MIN), 0.0)
self.assertFloatsAreIdentical(getargs_f(-DBL_MIN), -0.0)
self.assertFloatsAreIdentical(getargs_f(0.0), 0.0)
self.assertFloatsAreIdentical(getargs_f(-0.0), -0.0)
r = getargs_f(NAN)
self.assertNotEqual(r, r)

Expand Down Expand Up @@ -494,8 +491,8 @@ def test_d(self):
self.assertEqual(getargs_d(x), x)
self.assertRaises(OverflowError, getargs_d, 1<<DBL_MAX_EXP)
self.assertRaises(OverflowError, getargs_d, -1<<DBL_MAX_EXP)
self.assertEqualWithSign(getargs_d(0.0), 0.0)
self.assertEqualWithSign(getargs_d(-0.0), -0.0)
self.assertFloatsAreIdentical(getargs_d(0.0), 0.0)
self.assertFloatsAreIdentical(getargs_d(-0.0), -0.0)
r = getargs_d(NAN)
self.assertNotEqual(r, r)

Expand All @@ -519,10 +516,10 @@ def test_D(self):
self.assertEqual(getargs_D(c), c)
c = complex(1.0, x)
self.assertEqual(getargs_D(c), c)
self.assertEqualWithSign(getargs_D(complex(0.0, 1.0)).real, 0.0)
self.assertEqualWithSign(getargs_D(complex(-0.0, 1.0)).real, -0.0)
self.assertEqualWithSign(getargs_D(complex(1.0, 0.0)).imag, 0.0)
self.assertEqualWithSign(getargs_D(complex(1.0, -0.0)).imag, -0.0)
self.assertFloatsAreIdentical(getargs_D(complex(0.0, 1.0)).real, 0.0)
self.assertFloatsAreIdentical(getargs_D(complex(-0.0, 1.0)).real, -0.0)
self.assertFloatsAreIdentical(getargs_D(complex(1.0, 0.0)).imag, 0.0)
self.assertFloatsAreIdentical(getargs_D(complex(1.0, -0.0)).imag, -0.0)


class Paradox:
Expand Down
42 changes: 5 additions & 37 deletions Lib/test/test_cmath.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from test.support import requires_IEEE_754, cpython_only, import_helper
from test.support.testcase import ComplexesAreIdenticalMixin
from test.test_math import parse_testfile, test_file
import test.test_math as test_math
import unittest
Expand Down Expand Up @@ -49,7 +50,7 @@
(INF, NAN)
]]

class CMathTests(unittest.TestCase):
class CMathTests(ComplexesAreIdenticalMixin, unittest.TestCase):
# list of all functions in cmath
test_functions = [getattr(cmath, fname) for fname in [
'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh',
Expand All @@ -65,39 +66,6 @@ def setUp(self):
def tearDown(self):
self.test_values.close()

def assertFloatIdentical(self, x, y):
"""Fail unless floats x and y are identical, in the sense that:
(1) both x and y are nans, or
(2) both x and y are infinities, with the same sign, or
(3) both x and y are zeros, with the same sign, or
(4) x and y are both finite and nonzero, and x == y

"""
msg = 'floats {!r} and {!r} are not identical'

if math.isnan(x) or math.isnan(y):
if math.isnan(x) and math.isnan(y):
return
elif x == y:
if x != 0.0:
return
# both zero; check that signs match
elif math.copysign(1.0, x) == math.copysign(1.0, y):
return
else:
msg += ': zeros have different signs'
self.fail(msg.format(x, y))

def assertComplexIdentical(self, x, y):
"""Fail unless complex numbers x and y have equal values and signs.

In particular, if x and y both have real (or imaginary) part
zero, but the zeros have different signs, this test will fail.

"""
self.assertFloatIdentical(x.real, y.real)
self.assertFloatIdentical(x.imag, y.imag)

def rAssertAlmostEqual(self, a, b, rel_err = 2e-15, abs_err = 5e-323,
msg=None):
"""Fail if the two floating-point numbers are not almost equal.
Expand Down Expand Up @@ -555,7 +523,7 @@ def test_isinf(self):
@requires_IEEE_754
def testTanhSign(self):
for z in complex_zeros:
self.assertComplexIdentical(cmath.tanh(z), z)
self.assertComplexesAreIdentical(cmath.tanh(z), z)

# The algorithm used for atan and atanh makes use of the system
# log1p function; If that system function doesn't respect the sign
Expand All @@ -564,12 +532,12 @@ def testTanhSign(self):
@requires_IEEE_754
def testAtanSign(self):
for z in complex_zeros:
self.assertComplexIdentical(cmath.atan(z), z)
self.assertComplexesAreIdentical(cmath.atan(z), z)

@requires_IEEE_754
def testAtanhSign(self):
for z in complex_zeros:
self.assertComplexIdentical(cmath.atanh(z), z)
self.assertComplexesAreIdentical(cmath.atanh(z), z)


class IsCloseTests(test_math.IsCloseTests):
Expand Down
29 changes: 3 additions & 26 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest
import sys
from test import support
from test.support.testcase import ComplexesAreIdenticalMixin
from test.test_grammar import (VALID_UNDERSCORE_LITERALS,
INVALID_UNDERSCORE_LITERALS)

Expand Down Expand Up @@ -41,7 +42,7 @@ def __init__(self, value):
def __complex__(self):
return self.value

class ComplexTest(unittest.TestCase):
class ComplexTest(ComplexesAreIdenticalMixin, unittest.TestCase):

def assertAlmostEqual(self, a, b):
if isinstance(a, complex):
Expand Down Expand Up @@ -70,29 +71,6 @@ def assertCloseAbs(self, x, y, eps=1e-9):
# check that relative difference < eps
self.assertTrue(abs((x-y)/y) < eps)

def assertFloatsAreIdentical(self, x, y):
"""assert that floats x and y are identical, in the sense that:
(1) both x and y are nans, or
(2) both x and y are infinities, with the same sign, or
(3) both x and y are zeros, with the same sign, or
(4) x and y are both finite and nonzero, and x == y

"""
msg = 'floats {!r} and {!r} are not identical'

if isnan(x) or isnan(y):
if isnan(x) and isnan(y):
return
elif x == y:
if x != 0.0:
return
# both zero; check that signs match
elif copysign(1.0, x) == copysign(1.0, y):
return
else:
msg += ': zeros have different signs'
self.fail(msg.format(x, y))

def assertClose(self, x, y, eps=1e-9):
"""Return true iff complexes x and y "are close"."""
self.assertCloseAbs(x.real, y.real, eps)
Expand Down Expand Up @@ -728,8 +706,7 @@ def test_repr_roundtrip(self):
for y in vals:
z = complex(x, y)
roundtrip = complex(repr(z))
self.assertFloatsAreIdentical(z.real, roundtrip.real)
self.assertFloatsAreIdentical(z.imag, roundtrip.imag)
self.assertComplexesAreIdentical(z, roundtrip)

# if we predefine some constants, then eval(repr(z)) should
# also work, except that it might change the sign of zeros
Expand Down
12 changes: 3 additions & 9 deletions Lib/test/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import unittest

from test import support
from test.support.testcase import FloatsAreIdenticalMixin
from test.test_grammar import (VALID_UNDERSCORE_LITERALS,
INVALID_UNDERSCORE_LITERALS)
from math import isinf, isnan, copysign, ldexp
Expand Down Expand Up @@ -1052,21 +1053,14 @@ def test_nan_signs(self):

fromHex = float.fromhex
toHex = float.hex
class HexFloatTestCase(unittest.TestCase):
class HexFloatTestCase(FloatsAreIdenticalMixin, unittest.TestCase):
MAX = fromHex('0x.fffffffffffff8p+1024') # max normal
MIN = fromHex('0x1p-1022') # min normal
TINY = fromHex('0x0.0000000000001p-1022') # min subnormal
EPS = fromHex('0x0.0000000000001p0') # diff between 1.0 and next float up

def identical(self, x, y):
# check that floats x and y are identical, or that both
# are NaNs
if isnan(x) or isnan(y):
if isnan(x) == isnan(y):
return
elif x == y and (x != 0.0 or copysign(1.0, x) == copysign(1.0, y)):
return
self.fail('%r not identical to %r' % (x, y))
self.assertFloatsAreIdentical(x, y)

def test_ends(self):
self.identical(self.MIN, ldexp(1.0, -1022))
Expand Down
Loading