gh-121039: add Floats/ComplexesAreIdenticalMixin to test.support.testcase (GH-121071)

This commit is contained in:
Sergey B Kirpichev 2024-09-08 16:01:54 +03:00 committed by GitHub
parent beee91cdcc
commit 8ef8354ef1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 65 additions and 120 deletions

View File

@ -1,3 +1,6 @@
from math import copysign, isnan
class ExceptionIsLikeMixin: class ExceptionIsLikeMixin:
def assertExceptionIsLike(self, exc, template): def assertExceptionIsLike(self, exc, template):
""" """
@ -23,3 +26,40 @@ class ExceptionIsLikeMixin:
self.assertEqual(len(exc.exceptions), len(template.exceptions)) self.assertEqual(len(exc.exceptions), len(template.exceptions))
for e, t in zip(exc.exceptions, template.exceptions): for e, t in zip(exc.exceptions, template.exceptions):
self.assertExceptionIsLike(e, t) 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)

View File

@ -6,6 +6,7 @@ from test import support
from test.support import import_helper from test.support import import_helper
from test.support import script_helper from test.support import script_helper
from test.support import warnings_helper from test.support import warnings_helper
from test.support.testcase import FloatsAreIdenticalMixin
# Skip this test if the _testcapi module isn't available. # Skip this test if the _testcapi module isn't available.
_testcapi = import_helper.import_module('_testcapi') _testcapi = import_helper.import_module('_testcapi')
from _testcapi import getargs_keywords, getargs_keyword_only from _testcapi import getargs_keywords, getargs_keyword_only
@ -436,11 +437,7 @@ class LongLong_TestCase(unittest.TestCase):
self.assertEqual(VERY_LARGE & ULLONG_MAX, getargs_K(VERY_LARGE)) self.assertEqual(VERY_LARGE & ULLONG_MAX, getargs_K(VERY_LARGE))
class Float_TestCase(unittest.TestCase): class Float_TestCase(unittest.TestCase, FloatsAreIdenticalMixin):
def assertEqualWithSign(self, actual, expected):
self.assertEqual(actual, expected)
self.assertEqual(math.copysign(1, actual), math.copysign(1, expected))
def test_f(self): def test_f(self):
from _testcapi import getargs_f from _testcapi import getargs_f
self.assertEqual(getargs_f(4.25), 4.25) self.assertEqual(getargs_f(4.25), 4.25)
@ -462,10 +459,10 @@ class Float_TestCase(unittest.TestCase):
self.assertEqual(getargs_f(DBL_MAX), INF) self.assertEqual(getargs_f(DBL_MAX), INF)
self.assertEqual(getargs_f(-DBL_MAX), -INF) self.assertEqual(getargs_f(-DBL_MAX), -INF)
if FLT_MIN > DBL_MIN: if FLT_MIN > DBL_MIN:
self.assertEqualWithSign(getargs_f(DBL_MIN), 0.0) self.assertFloatsAreIdentical(getargs_f(DBL_MIN), 0.0)
self.assertEqualWithSign(getargs_f(-DBL_MIN), -0.0) self.assertFloatsAreIdentical(getargs_f(-DBL_MIN), -0.0)
self.assertEqualWithSign(getargs_f(0.0), 0.0) self.assertFloatsAreIdentical(getargs_f(0.0), 0.0)
self.assertEqualWithSign(getargs_f(-0.0), -0.0) self.assertFloatsAreIdentical(getargs_f(-0.0), -0.0)
r = getargs_f(NAN) r = getargs_f(NAN)
self.assertNotEqual(r, r) self.assertNotEqual(r, r)
@ -494,8 +491,8 @@ class Float_TestCase(unittest.TestCase):
self.assertEqual(getargs_d(x), x) 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.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.assertFloatsAreIdentical(getargs_d(0.0), 0.0)
self.assertEqualWithSign(getargs_d(-0.0), -0.0) self.assertFloatsAreIdentical(getargs_d(-0.0), -0.0)
r = getargs_d(NAN) r = getargs_d(NAN)
self.assertNotEqual(r, r) self.assertNotEqual(r, r)
@ -519,10 +516,10 @@ class Float_TestCase(unittest.TestCase):
self.assertEqual(getargs_D(c), c) self.assertEqual(getargs_D(c), c)
c = complex(1.0, x) c = complex(1.0, x)
self.assertEqual(getargs_D(c), c) self.assertEqual(getargs_D(c), c)
self.assertEqualWithSign(getargs_D(complex(0.0, 1.0)).real, 0.0) self.assertFloatsAreIdentical(getargs_D(complex(0.0, 1.0)).real, 0.0)
self.assertEqualWithSign(getargs_D(complex(-0.0, 1.0)).real, -0.0) self.assertFloatsAreIdentical(getargs_D(complex(-0.0, 1.0)).real, -0.0)
self.assertEqualWithSign(getargs_D(complex(1.0, 0.0)).imag, 0.0) self.assertFloatsAreIdentical(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(1.0, -0.0)).imag, -0.0)
class Paradox: class Paradox:

View File

@ -1,4 +1,5 @@
from test.support import requires_IEEE_754, cpython_only, import_helper 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 from test.test_math import parse_testfile, test_file
import test.test_math as test_math import test.test_math as test_math
import unittest import unittest
@ -49,7 +50,7 @@ complex_nans = [complex(x, y) for x, y in [
(INF, NAN) (INF, NAN)
]] ]]
class CMathTests(unittest.TestCase): class CMathTests(ComplexesAreIdenticalMixin, unittest.TestCase):
# list of all functions in cmath # list of all functions in cmath
test_functions = [getattr(cmath, fname) for fname in [ test_functions = [getattr(cmath, fname) for fname in [
'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh',
@ -65,39 +66,6 @@ class CMathTests(unittest.TestCase):
def tearDown(self): def tearDown(self):
self.test_values.close() 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, def rAssertAlmostEqual(self, a, b, rel_err = 2e-15, abs_err = 5e-323,
msg=None): msg=None):
"""Fail if the two floating-point numbers are not almost equal. """Fail if the two floating-point numbers are not almost equal.
@ -555,7 +523,7 @@ class CMathTests(unittest.TestCase):
@requires_IEEE_754 @requires_IEEE_754
def testTanhSign(self): def testTanhSign(self):
for z in complex_zeros: 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 # The algorithm used for atan and atanh makes use of the system
# log1p function; If that system function doesn't respect the sign # log1p function; If that system function doesn't respect the sign
@ -564,12 +532,12 @@ class CMathTests(unittest.TestCase):
@requires_IEEE_754 @requires_IEEE_754
def testAtanSign(self): def testAtanSign(self):
for z in complex_zeros: for z in complex_zeros:
self.assertComplexIdentical(cmath.atan(z), z) self.assertComplexesAreIdentical(cmath.atan(z), z)
@requires_IEEE_754 @requires_IEEE_754
def testAtanhSign(self): def testAtanhSign(self):
for z in complex_zeros: for z in complex_zeros:
self.assertComplexIdentical(cmath.atanh(z), z) self.assertComplexesAreIdentical(cmath.atanh(z), z)
class IsCloseTests(test_math.IsCloseTests): class IsCloseTests(test_math.IsCloseTests):

View File

@ -1,6 +1,7 @@
import unittest import unittest
import sys import sys
from test import support from test import support
from test.support.testcase import ComplexesAreIdenticalMixin
from test.test_grammar import (VALID_UNDERSCORE_LITERALS, from test.test_grammar import (VALID_UNDERSCORE_LITERALS,
INVALID_UNDERSCORE_LITERALS) INVALID_UNDERSCORE_LITERALS)
@ -52,7 +53,7 @@ class WithComplex:
def __complex__(self): def __complex__(self):
return self.value return self.value
class ComplexTest(unittest.TestCase): class ComplexTest(ComplexesAreIdenticalMixin, unittest.TestCase):
def assertAlmostEqual(self, a, b): def assertAlmostEqual(self, a, b):
if isinstance(a, complex): if isinstance(a, complex):
@ -81,33 +82,6 @@ class ComplexTest(unittest.TestCase):
# check that relative difference < eps # check that relative difference < eps
self.assertTrue(abs((x-y)/y) < 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 assertComplexesAreIdentical(self, x, y):
self.assertFloatsAreIdentical(x.real, y.real)
self.assertFloatsAreIdentical(x.imag, y.imag)
def assertClose(self, x, y, eps=1e-9): def assertClose(self, x, y, eps=1e-9):
"""Return true iff complexes x and y "are close".""" """Return true iff complexes x and y "are close"."""
self.assertCloseAbs(x.real, y.real, eps) self.assertCloseAbs(x.real, y.real, eps)
@ -829,8 +803,7 @@ class ComplexTest(unittest.TestCase):
for y in vals: for y in vals:
z = complex(x, y) z = complex(x, y)
roundtrip = complex(repr(z)) roundtrip = complex(repr(z))
self.assertFloatsAreIdentical(z.real, roundtrip.real) self.assertComplexesAreIdentical(z, roundtrip)
self.assertFloatsAreIdentical(z.imag, roundtrip.imag)
# if we predefine some constants, then eval(repr(z)) should # if we predefine some constants, then eval(repr(z)) should
# also work, except that it might change the sign of zeros # also work, except that it might change the sign of zeros

View File

@ -4,12 +4,12 @@ import struct
import sys import sys
import unittest import unittest
from itertools import combinations from itertools import combinations
from math import copysign, isnan
from operator import truth from operator import truth
from ctypes import (byref, sizeof, alignment, from ctypes import (byref, sizeof, alignment,
c_char, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, c_char, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint,
c_long, c_ulong, c_longlong, c_ulonglong, c_long, c_ulong, c_longlong, c_ulonglong,
c_float, c_double, c_longdouble, c_bool) c_float, c_double, c_longdouble, c_bool)
from test.support.testcase import ComplexesAreIdenticalMixin
def valid_ranges(*types): def valid_ranges(*types):
@ -62,34 +62,7 @@ INF = float("inf")
NAN = float("nan") NAN = float("nan")
class NumberTestCase(unittest.TestCase): class NumberTestCase(unittest.TestCase, ComplexesAreIdenticalMixin):
# from Lib/test/test_complex.py
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 assertComplexesAreIdentical(self, x, y):
self.assertFloatsAreIdentical(x.real, y.real)
self.assertFloatsAreIdentical(x.imag, y.imag)
def test_default_init(self): def test_default_init(self):
# default values are set to zero # default values are set to zero

View File

@ -8,6 +8,7 @@ import time
import unittest import unittest
from test import support from test import support
from test.support.testcase import FloatsAreIdenticalMixin
from test.test_grammar import (VALID_UNDERSCORE_LITERALS, from test.test_grammar import (VALID_UNDERSCORE_LITERALS,
INVALID_UNDERSCORE_LITERALS) INVALID_UNDERSCORE_LITERALS)
from math import isinf, isnan, copysign, ldexp from math import isinf, isnan, copysign, ldexp
@ -1093,21 +1094,14 @@ class InfNanTest(unittest.TestCase):
fromHex = float.fromhex fromHex = float.fromhex
toHex = float.hex toHex = float.hex
class HexFloatTestCase(unittest.TestCase): class HexFloatTestCase(FloatsAreIdenticalMixin, unittest.TestCase):
MAX = fromHex('0x.fffffffffffff8p+1024') # max normal MAX = fromHex('0x.fffffffffffff8p+1024') # max normal
MIN = fromHex('0x1p-1022') # min normal MIN = fromHex('0x1p-1022') # min normal
TINY = fromHex('0x0.0000000000001p-1022') # min subnormal TINY = fromHex('0x0.0000000000001p-1022') # min subnormal
EPS = fromHex('0x0.0000000000001p0') # diff between 1.0 and next float up EPS = fromHex('0x0.0000000000001p0') # diff between 1.0 and next float up
def identical(self, x, y): def identical(self, x, y):
# check that floats x and y are identical, or that both self.assertFloatsAreIdentical(x, y)
# 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))
def test_ends(self): def test_ends(self):
self.identical(self.MIN, ldexp(1.0, -1022)) self.identical(self.MIN, ldexp(1.0, -1022))