gh-130317: Fix test_pack_unpack_roundtrip() and add docs (#133204)

* Skip sNaN's testing in 32-bit mode.
* Drop float_set_snan() helper.
* Use memcpy() workaround for sNaN's in PyFloat_Unpack4().
* Document, that sNaN's may not be preserved by PyFloat_Pack/Unpack API.
This commit is contained in:
Sergey B Kirpichev 2025-05-01 17:20:36 +03:00 committed by GitHub
parent ed039b801d
commit ad2f0884b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 18 additions and 62 deletions

View file

@ -96,6 +96,9 @@ NaNs (if such things exist on the platform) isn't handled correctly, and
attempting to unpack a bytes string containing an IEEE INF or NaN will raise an
exception.
Note that NaNs type may not be preserved on IEEE platforms (silent NaN become
quiet), for example on x86 systems in 32-bit mode.
On non-IEEE platforms with more precision, or larger dynamic range, than IEEE
754 supports, not all values can be packed; on non-IEEE platforms with less
precision, or smaller dynamic range, not all values can be unpacked. What

View file

@ -180,12 +180,6 @@ class CAPIFloatTest(unittest.TestCase):
self.assertEqual(value2, value)
@unittest.skipUnless(HAVE_IEEE_754, "requires IEEE 754")
# Skip on x86 (32-bit), since these tests fail. The problem is that sNaN
# doubles become qNaN doubles just by the C calling convention, there is no
# way to preserve sNaN doubles between C function calls. But tests pass
# on Windows x86.
@unittest.skipIf((sys.maxsize == 2147483647) and not(sys.platform == 'win32'),
'test fails on x86 (32-bit)')
def test_pack_unpack_roundtrip_for_nans(self):
pack = _testcapi.float_pack
unpack = _testcapi.float_unpack
@ -193,7 +187,16 @@ class CAPIFloatTest(unittest.TestCase):
for _ in range(10):
for size in (2, 4, 8):
sign = random.randint(0, 1)
signaling = random.randint(0, 1)
if sys.maxsize != 2147483647: # not it 32-bit mode
signaling = random.randint(0, 1)
else:
# Skip sNaN's on x86 (32-bit). The problem is that sNaN
# doubles become qNaN doubles just by the C calling
# convention, there is no way to preserve sNaN doubles
# between C function calls with the current
# PyFloat_Pack/Unpack*() API. See also gh-130317 and
# e.g. https://developercommunity.visualstudio.com/t/155064
signaling = 0
quiet = int(not signaling)
if size == 8:
payload = random.randint(signaling, 1 << 50)
@ -209,12 +212,6 @@ class CAPIFloatTest(unittest.TestCase):
with self.subTest(data=data, size=size, endian=endian):
data1 = data if endian == BIG_ENDIAN else data[::-1]
value = unpack(data1, endian)
if signaling and sys.platform == 'win32':
# On Windows x86, sNaN becomes qNaN when returned
# from function. That's a known bug, e.g.
# https://developercommunity.visualstudio.com/t/155064
# (see also gh-130317).
value = _testcapi.float_set_snan(value)
data2 = pack(size, value, endian)
self.assertTrue(math.isnan(value))
self.assertEqual(data1, data2)

View file

@ -81,13 +81,4 @@ _testcapi_float_unpack(PyObject *module, PyObject *const *args, Py_ssize_t nargs
exit:
return return_value;
}
PyDoc_STRVAR(_testcapi_float_set_snan__doc__,
"float_set_snan($module, obj, /)\n"
"--\n"
"\n"
"Make a signaling NaN.");
#define _TESTCAPI_FLOAT_SET_SNAN_METHODDEF \
{"float_set_snan", (PyCFunction)_testcapi_float_set_snan, METH_O, _testcapi_float_set_snan__doc__},
/*[clinic end generated code: output=1b0e9b05e1f50712 input=a9049054013a1b77]*/
/*[clinic end generated code: output=b43dfd3a77fe04ba input=a9049054013a1b77]*/

View file

@ -157,42 +157,9 @@ test_string_to_double(PyObject *self, PyObject *Py_UNUSED(ignored))
}
/*[clinic input]
_testcapi.float_set_snan
obj: object
/
Make a signaling NaN.
[clinic start generated code]*/
static PyObject *
_testcapi_float_set_snan(PyObject *module, PyObject *obj)
/*[clinic end generated code: output=f43778a70f60aa4b input=c1269b0f88ef27ac]*/
{
if (!PyFloat_Check(obj)) {
PyErr_SetString(PyExc_ValueError, "float-point number expected");
return NULL;
}
double d = ((PyFloatObject *)obj)->ob_fval;
if (!isnan(d)) {
PyErr_SetString(PyExc_ValueError, "nan expected");
return NULL;
}
uint64_t v;
memcpy(&v, &d, 8);
v &= ~(1ULL << 51); /* make sNaN */
// gh-130317: memcpy() is needed to preserve the sNaN flag on x86 (32-bit)
PyObject *res = PyFloat_FromDouble(0.0);
memcpy(&((PyFloatObject *)res)->ob_fval, &v, 8);
return res;
}
static PyMethodDef test_methods[] = {
_TESTCAPI_FLOAT_PACK_METHODDEF
_TESTCAPI_FLOAT_UNPACK_METHODDEF
_TESTCAPI_FLOAT_SET_SNAN_METHODDEF
{"test_string_to_double", test_string_to_double, METH_NOARGS},
{NULL},
};

View file

@ -2495,12 +2495,10 @@ PyFloat_Unpack4(const char *data, int le)
if ((v & (1 << 22)) == 0) {
double y = x; /* will make qNaN double */
union double_val {
double d;
uint64_t u64;
} *py = (union double_val *)&y;
py->u64 &= ~(1ULL << 51); /* make sNaN */
uint64_t u64;
memcpy(&u64, &y, 8);
u64 &= ~(1ULL << 51); /* make sNaN */
memcpy(&y, &u64, 8);
return y;
}
}