Make tst_QNumeric more systematic about checking float as well as double

Do this by templating the floating-point tests, which removes some
existing duplication as well as avoiding new duplication. Did some
renaming in the process. Added some tests of fuzzyCompare that come
closer to its boundary. Increased number of tests from 69 to 97. Use
std::numeric_limits to replace assorted hard-coded constants and old
C-library boundary-value macros.

It turns out MSVC's float conflates quiet and signaling NaN (although
MinGW's doesn't); and WebAssembly's old fastcomp compiler conflates
NaNs for both float and double; so XFAIL the test for distinct NaNs in
those cases.

Change-Id: I0a1c0d2f68f75d51b8cda9e3ddfe7fa9c190a3e2
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Erik Verbruggen <erik.verbruggen@me.com>
This commit is contained in:
Edward Welbourne 2019-09-23 16:25:26 +02:00
parent d157292f16
commit d05ca484cf

View File

@ -35,59 +35,106 @@
#include <math.h>
#include <float.h>
namespace {
template <typename F> struct Fuzzy {};
/* Data taken from qglobal.h's implementation of qFuzzyCompare:
* qFuzzyCompare conflates values with fractional difference up to (and
* including) the given scale.
*/
template <> struct Fuzzy<double> { constexpr static double scale = 1e12; };
template <> struct Fuzzy<float> { constexpr static float scale = 1e5f; };
}
class tst_QNumeric: public QObject
{
Q_OBJECT
private slots:
void fuzzyCompare_data();
void fuzzyCompare();
void rawNaN_data();
void rawNaN();
// Support for floating-point:
template<typename F> inline void fuzzyCompare_data();
template<typename F> inline void fuzzyCompare();
template<typename F> inline void checkNaN(F nan);
template<typename F> inline void rawNaN_data();
template<typename F> inline void rawNaN();
#if QT_CONFIG(signaling_nan)
void distinctNaN();
template<typename F> inline void distinctNaN();
#endif
void generalNaN_data();
void generalNaN();
void infinity();
void classifyfp();
void floatDistance_data();
void floatDistance();
void floatDistance_double_data();
void floatDistance_double();
template<typename F, typename Whole> inline void generalNaN_data();
template<typename F, typename Whole> inline void generalNaN();
template<typename F> inline void infinity();
template<typename F> inline void classifyfp();
template<typename F, typename Count> inline void distance_data();
template<typename F, typename Count> inline void distance();
private slots:
// Floating-point tests:
void fuzzyCompareF_data() { fuzzyCompare_data<float>(); }
void fuzzyCompareF() { fuzzyCompare<float>(); }
void fuzzyCompareD_data() { fuzzyCompare_data<double>(); }
void fuzzyCompareD() { fuzzyCompare<double>(); }
void rawNaNF_data() { rawNaN_data<float>(); }
void rawNaNF() { rawNaN<float>(); }
void rawNaND_data() { rawNaN_data<double>(); }
void rawNaND() { rawNaN<double>(); }
#if QT_CONFIG(signaling_nan)
void distinctNaNF();
void distinctNaND() { distinctNaN<double>(); }
#endif
void generalNaNd_data() { generalNaN_data<double, quint64>(); }
void generalNaNd() { generalNaN<double, quint64>(); }
void generalNaNf_data() { generalNaN_data<float, quint32>(); }
void generalNaNf() { generalNaN<float, quint32>(); }
void infinityF() { infinity<float>(); }
void infinityD() { infinity<double>(); }
void classifyF() { classifyfp<float>(); }
void classifyD() { classifyfp<double>(); }
void floatDistance_data() { distance_data<float, quint32>(); }
void floatDistance() { distance<float, quint32>(); }
void doubleDistance_data() { distance_data<double, quint64>(); }
void doubleDistance() { distance<double, quint64>(); }
// Whole number tests:
void addOverflow_data();
void addOverflow();
void mulOverflow_data();
void mulOverflow();
void signedOverflow();
private:
void checkNaN(double nan);
};
// Floating-point tests:
template<typename F>
void tst_QNumeric::fuzzyCompare_data()
{
QTest::addColumn<double>("val1");
QTest::addColumn<double>("val2");
QTest::addColumn<F>("val1");
QTest::addColumn<F>("val2");
QTest::addColumn<bool>("isEqual");
const F zero(0), one(1), ten(10);
const F huge = Fuzzy<F>::scale, tiny = one / huge;
const F deci(.1), giga(1e9), nano(1e-9), big(1e7), small(1e-10);
QTest::newRow("zero") << 0.0 << 0.0 << true;
QTest::newRow("ten") << 10.0 << 10.0 << true;
QTest::newRow("large") << 1000000000.0 << 1000000000.0 << true;
QTest::newRow("small") << 0.00000000001 << 0.00000000001 << true;
QTest::newRow("eps") << 10.000000000000001 << 10.00000000000002 << true;
QTest::newRow("eps2") << 10.000000000000001 << 10.000000000000009 << true;
QTest::newRow("zero") << zero << zero << true;
QTest::newRow("ten") << ten << ten << true;
QTest::newRow("large") << giga << giga << true;
QTest::newRow("small") << small << small << true;
QTest::newRow("10+9*tiny==10") << (ten + 9 * tiny) << ten << true;
QTest::newRow("huge+.9==huge") << (huge + 9 * deci) << huge << true;
QTest::newRow("eps2") << (ten + tiny) << (ten + 2 * tiny) << true;
QTest::newRow("eps9") << (ten + tiny) << (ten + 9 * tiny) << true;
QTest::newRow("mis1") << 0.0 << 1.0 << false;
QTest::newRow("mis2") << 0.0 << 10000000.0 << false;
QTest::newRow("mis3") << 0.0 << 0.000000001 << false;
QTest::newRow("mis4") << 100000000.0 << 0.000000001 << false;
QTest::newRow("mis5") << 0.0000000001 << 0.000000001 << false;
QTest::newRow("0!=1") << zero << one << false;
QTest::newRow("0!=big") << zero << big << false;
QTest::newRow("0!=nano") << zero << nano << false;
QTest::newRow("giga!=nano") << giga << nano << false;
QTest::newRow("small!=nano") << small << nano << false;
QTest::newRow("huge+1.1!=huge") << (huge + 1 + deci) << huge << false;
QTest::newRow("1+1.1*tiny!=1") << (one + tiny * (one + deci)) << one << false;
}
template<typename F>
void tst_QNumeric::fuzzyCompare()
{
QFETCH(double, val1);
QFETCH(double, val2);
QFETCH(F, val1);
QFETCH(F, val2);
QFETCH(bool, isEqual);
QCOMPARE(::qFuzzyCompare(val1, val2), isEqual);
@ -101,11 +148,12 @@ void tst_QNumeric::fuzzyCompare()
# pragma GCC optimize "no-fast-math"
#endif
void tst_QNumeric::checkNaN(double nan)
template<typename F>
void tst_QNumeric::checkNaN(F nan)
{
#define CHECKNAN(value) \
do { \
const double v = (value); \
const F v = (value); \
QCOMPARE(qFpClassify(v), FP_NAN); \
QVERIFY(qIsNaN(v)); \
QVERIFY(!qIsFinite(v)); \
@ -134,211 +182,197 @@ void tst_QNumeric::checkNaN(double nan)
#undef CHECKNAN
}
template<typename F>
void tst_QNumeric::rawNaN_data()
{
#if defined __FAST_MATH__ && (__GNUC__ * 100 + __GNUC_MINOR__ < 404)
QSKIP("Non-conformant fast math mode is enabled, cannot run test");
#endif
QTest::addColumn<double>("nan");
QTest::addColumn<F>("nan");
QTest::newRow("quiet") << qQNaN();
QTest::newRow("quiet") << F(qQNaN());
#if QT_CONFIG(signaling_nan)
QTest::newRow("signaling") << qSNaN();
QTest::newRow("signaling") << F(qSNaN());
#endif
}
template<typename F>
void tst_QNumeric::rawNaN()
{
QFETCH(double, nan);
QFETCH(F, nan);
#ifdef Q_OS_WASM
# ifdef __asmjs
QEXPECT_FAIL("", "Fastcomp conflates quiet and signaling NaNs", Continue);
# endif // but the modern clang compiler handls it fine.
#endif
checkNaN(nan);
}
#if QT_CONFIG(signaling_nan)
template<typename F>
void tst_QNumeric::distinctNaN()
{
const double qnan = qQNaN();
const double snan = qSNaN();
QVERIFY(memcmp(&qnan, &snan, sizeof(double)) != 0);
const F qnan = qQNaN();
const F snan = qSNaN();
QVERIFY(memcmp(&qnan, &snan, sizeof(F)) != 0);
}
#endif
void tst_QNumeric::distinctNaNF() {
#ifdef Q_CC_MSVC
QEXPECT_FAIL("", "MSVC's float conflates quiet and signaling NaNs", Continue);
#endif
distinctNaN<float>();
}
#endif // signaling_nan
template<typename F, typename Whole>
void tst_QNumeric::generalNaN_data()
{
QTest::addColumn<int>("most");
QTest::addColumn<int>("next");
QTest::addColumn<int>("least");
Q_STATIC_ASSERT(sizeof(F) == sizeof(Whole));
QTest::addColumn<Whole>("whole");
// Every value with every bit of the exponent set is a NaN.
// Sign and mantissa can be anything without interfering with that.
// The 0x7f bits of most and the 0xf0 bits of next are the exponent.
using Bounds = std::numeric_limits<F>;
// Bounds::digits is one more than the number of bits used to encode the mantissa:
const int mantissaBits = Bounds::digits - 1;
// One bit for sign, the rest are mantissa and exponent:
const int exponentBits = sizeof(F) * CHAR_BIT - 1 - mantissaBits;
QTest::newRow("lowload") << 0x7f << 0xf0 << 1;
QTest::newRow("sign-lowload") << 0xff << 0xf0 << 1;
QTest::newRow("highload") << 0x7f << 0xf1 << 0;
QTest::newRow("sign-highload") << 0xff << 0xf1 << 0;
const Whole exponent = ((Whole(1) << exponentBits) - 1) << mantissaBits;
const Whole sign = Whole(1) << (exponentBits + mantissaBits);
const Whole mantissaTop = Whole(1) << (mantissaBits - 1);
QTest::newRow("lowload") << (exponent | 1);
QTest::newRow("sign-lowload") << (sign | exponent | 1);
QTest::newRow("highload") << (exponent | mantissaTop);
QTest::newRow("sign-highload") << (sign | exponent | mantissaTop);
}
template<typename F, typename Whole>
void tst_QNumeric::generalNaN()
{
QFETCH(int, most);
QFETCH(int, next);
QFETCH(int, least);
double nan;
Q_STATIC_ASSERT(sizeof(double) == 8);
#ifdef Q_LITTLE_ENDIAN
const uchar bytes[] = { uchar(least), 0, 0, 0, 0, 0, uchar(next), uchar(most) };
#else
const uchar bytes[] = { uchar(most), uchar(next), 0, 0, 0, 0, 0, uchar(least) };
#endif
memcpy(&nan, bytes, 8);
Q_STATIC_ASSERT(sizeof(F) == sizeof(Whole));
QFETCH(const Whole, whole);
F nan;
memcpy(&nan, &whole, sizeof(F));
checkNaN(nan);
}
template<typename F>
void tst_QNumeric::infinity()
{
const double inf = qInf();
QVERIFY(inf > 0);
QVERIFY(-inf < 0);
const F inf = qInf();
const F zero(0), one(1), two(2);
QVERIFY(inf > zero);
QVERIFY(-inf < zero);
QVERIFY(qIsInf(inf));
QCOMPARE(inf, inf);
QCOMPARE(-inf, -inf);
QVERIFY(qIsInf(-inf));
QVERIFY(qIsInf(inf + 1));
QVERIFY(qIsInf(inf - 1));
QVERIFY(qIsInf(-inf - 1));
QVERIFY(qIsInf(-inf + 1));
QVERIFY(qIsInf(inf * 2.0));
QVERIFY(qIsInf(-inf * 2.0));
QVERIFY(qIsInf(inf / 2.0));
QVERIFY(qIsInf(-inf / 2.0));
QVERIFY(qFuzzyCompare(1.0 / inf, 0.0));
QVERIFY(qIsInf(inf + one));
QVERIFY(qIsInf(inf - one));
QVERIFY(qIsInf(-inf - one));
QVERIFY(qIsInf(-inf + one));
QVERIFY(qIsInf(inf * two));
QVERIFY(qIsInf(-inf * two));
QVERIFY(qIsInf(inf / two));
QVERIFY(qIsInf(-inf / two));
QVERIFY(qFuzzyCompare(one / inf, zero));
QCOMPARE(1.0 / inf, 0.0);
QVERIFY(qFuzzyCompare(1.0 / -inf, 0.0));
QCOMPARE(1.0 / -inf, 0.0);
QVERIFY(qIsNaN(0.0 * inf));
QVERIFY(qIsNaN(0.0 * -inf));
QVERIFY(qFuzzyCompare(one / -inf, zero));
QCOMPARE(one / -inf, zero);
QVERIFY(qIsNaN(zero * inf));
QVERIFY(qIsNaN(zero * -inf));
}
template<typename F>
void tst_QNumeric::classifyfp()
{
using Bounds = std::numeric_limits<F>;
const F huge = Bounds::max();
const F tiny = Bounds::min();
// NaNs already handled, see checkNaN()'s callers.
const F one(1), two(2), inf(qInf());
QCOMPARE(qFpClassify(qInf()), FP_INFINITE);
QCOMPARE(qFpClassify(-qInf()), FP_INFINITE);
QCOMPARE(qFpClassify(DBL_MAX * 2.0), FP_INFINITE);
QCOMPARE(qFpClassify(FLT_MAX * 2.f), FP_INFINITE);
QCOMPARE(qFpClassify(DBL_MAX * -2.0), FP_INFINITE);
QCOMPARE(qFpClassify(FLT_MAX * -2.f), FP_INFINITE);
QCOMPARE(qFpClassify(inf), FP_INFINITE);
QCOMPARE(qFpClassify(-inf), FP_INFINITE);
QCOMPARE(qFpClassify(huge * two), FP_INFINITE);
QCOMPARE(qFpClassify(huge * -two), FP_INFINITE);
QCOMPARE(qFpClassify(1.0), FP_NORMAL);
QCOMPARE(qFpClassify(DBL_MAX), FP_NORMAL);
QCOMPARE(qFpClassify(-DBL_MAX), FP_NORMAL);
QCOMPARE(qFpClassify(DBL_MIN), FP_NORMAL);
QCOMPARE(qFpClassify(-DBL_MIN), FP_NORMAL);
QCOMPARE(qFpClassify(DBL_MIN / 2.0), FP_SUBNORMAL);
QCOMPARE(qFpClassify(DBL_MIN / -2.0), FP_SUBNORMAL);
QCOMPARE(qFpClassify(1.f), FP_NORMAL);
QCOMPARE(qFpClassify(FLT_MAX), FP_NORMAL);
QCOMPARE(qFpClassify(-FLT_MAX), FP_NORMAL);
QCOMPARE(qFpClassify(FLT_MIN), FP_NORMAL);
QCOMPARE(qFpClassify(-FLT_MIN), FP_NORMAL);
QCOMPARE(qFpClassify(FLT_MIN / 2.f), FP_SUBNORMAL);
QCOMPARE(qFpClassify(FLT_MIN / -2.f), FP_SUBNORMAL);
QCOMPARE(qFpClassify(one), FP_NORMAL);
QCOMPARE(qFpClassify(huge), FP_NORMAL);
QCOMPARE(qFpClassify(-huge), FP_NORMAL);
QCOMPARE(qFpClassify(tiny), FP_NORMAL);
QCOMPARE(qFpClassify(-tiny), FP_NORMAL);
if (Bounds::has_denorm == std::denorm_present) {
QCOMPARE(qFpClassify(tiny / two), FP_SUBNORMAL);
QCOMPARE(qFpClassify(tiny / -two), FP_SUBNORMAL);
}
}
void tst_QNumeric::floatDistance_data()
template<typename F, typename Count>
void tst_QNumeric::distance_data()
{
QTest::addColumn<float>("val1");
QTest::addColumn<float>("val2");
QTest::addColumn<quint32>("expectedDistance");
using Bounds = std::numeric_limits<F>;
const F huge = Bounds::max();
const F tiny = Bounds::min();
// exponent: 8 bits
// mantissa: 23 bits
const quint32 number_of_denormals = (1 << 23) - 1; // Set to 0 if denormals are not included
QTest::addColumn<F>("from");
QTest::addColumn<F>("stop");
QTest::addColumn<Count>("expectedDistance");
quint32 _0_to_1 = quint32((1 << 23) * 126 + 1 + number_of_denormals); // We need +1 to include the 0
quint32 _1_to_2 = quint32(1 << 23);
using Bounds = std::numeric_limits<F>;
const int mantissaBits = Bounds::digits - 1;
const int exponentBits = sizeof(F) * CHAR_BIT - 1 - mantissaBits;
// We don't need +1 because FLT_MAX has all bits set in the mantissa. (Thus mantissa
// Set to 1 and 0 if denormals are not included:
const Count count_0_to_tiny = Count(1) << mantissaBits;
const Count count_denormals = count_0_to_tiny - 1;
// We need +1 to include the 0:
const Count count_0_to_1
= (Count(1) << mantissaBits) * ((Count(1) << (exponentBits - 1)) - 2)
+ 1 + count_denormals;
const Count count_1_to_2 = Count(1) << mantissaBits;
// We don't need +1 because huge has all bits set in the mantissa. (Thus mantissa
// have not wrapped back to 0, which would be the case for 1 in _0_to_1
quint32 _0_to_FLT_MAX = quint32((1 << 23) * 254) + number_of_denormals;
const Count count_0_to_huge
= (Count(1) << mantissaBits) * ((Count(1) << exponentBits) - 2)
+ count_denormals;
quint32 _0_to_FLT_MIN = 1 + number_of_denormals;
QTest::newRow("[0,FLT_MIN]") << 0.F << FLT_MIN << _0_to_FLT_MIN;
QTest::newRow("[0,FLT_MAX]") << 0.F << FLT_MAX << _0_to_FLT_MAX;
QTest::newRow("[1,1.5]") << 1.0F << 1.5F << quint32(1 << 22);
QTest::newRow("[0,1]") << 0.F << 1.0F << _0_to_1;
QTest::newRow("[0.5,1]") << 0.5F << 1.0F << quint32(1 << 23);
QTest::newRow("[1,2]") << 1.F << 2.0F << _1_to_2;
QTest::newRow("[-1,+1]") << -1.F << +1.0F << 2 * _0_to_1;
QTest::newRow("[-1,0]") << -1.F << 0.0F << _0_to_1;
QTest::newRow("[-1,FLT_MAX]") << -1.F << FLT_MAX << _0_to_1 + _0_to_FLT_MAX;
QTest::newRow("[-2,-1") << -2.F << -1.F << _1_to_2;
QTest::newRow("[-1,-2") << -1.F << -2.F << _1_to_2;
QTest::newRow("[FLT_MIN,FLT_MAX]") << FLT_MIN << FLT_MAX << _0_to_FLT_MAX - _0_to_FLT_MIN;
QTest::newRow("[-FLT_MAX,FLT_MAX]") << -FLT_MAX << FLT_MAX << (2*_0_to_FLT_MAX);
float denormal = FLT_MIN;
denormal/=2.0F;
QTest::newRow("denormal") << 0.F << denormal << _0_to_FLT_MIN/2;
const F zero(0), half(.5), one(1), sesqui(1.5), two(2);
const F denormal = tiny / two;
QTest::newRow("[0,tiny]") << zero << tiny << count_0_to_tiny;
QTest::newRow("[0,huge]") << zero << huge << count_0_to_huge;
QTest::newRow("[1,1.5]") << one << sesqui << (Count(1) << (mantissaBits - 1));
QTest::newRow("[0,1]") << zero << one << count_0_to_1;
QTest::newRow("[0.5,1]") << half << one << (Count(1) << mantissaBits);
QTest::newRow("[1,2]") << one << two << count_1_to_2;
QTest::newRow("[-1,+1]") << -one << +one << 2 * count_0_to_1;
QTest::newRow("[-1,0]") << -one << zero << count_0_to_1;
QTest::newRow("[-1,huge]") << -one << huge << count_0_to_1 + count_0_to_huge;
QTest::newRow("[-2,-1") << -two << -one << count_1_to_2;
QTest::newRow("[-1,-2") << -one << -two << count_1_to_2;
QTest::newRow("[tiny,huge]") << tiny << huge << count_0_to_huge - count_0_to_tiny;
QTest::newRow("[-huge,huge]") << -huge << huge << (2 * count_0_to_huge);
QTest::newRow("denormal") << zero << denormal << count_0_to_tiny / 2;
}
void tst_QNumeric::floatDistance()
template<typename F, typename Count>
void tst_QNumeric::distance()
{
QFETCH(float, val1);
QFETCH(float, val2);
QFETCH(quint32, expectedDistance);
QFETCH(F, from);
QFETCH(F, stop);
QFETCH(Count, expectedDistance);
#ifdef Q_OS_QNX
QEXPECT_FAIL("denormal", "See QTBUG-37094", Continue);
#endif
QCOMPARE(qFloatDistance(val1, val2), expectedDistance);
QCOMPARE(qFloatDistance(from, stop), expectedDistance);
}
void tst_QNumeric::floatDistance_double_data()
{
QTest::addColumn<double>("val1");
QTest::addColumn<double>("val2");
QTest::addColumn<quint64>("expectedDistance");
// exponent: 11 bits
// mantissa: 52 bits
const quint64 number_of_denormals = (Q_UINT64_C(1) << 52) - 1; // Set to 0 if denormals are not included
quint64 _0_to_1 = (Q_UINT64_C(1) << 52) * ((1 << (11-1)) - 2) + 1 + number_of_denormals; // We need +1 to include the 0
quint64 _1_to_2 = Q_UINT64_C(1) << 52;
// We don't need +1 because DBL_MAX has all bits set in the mantissa. (Thus mantissa
// have not wrapped back to 0, which would be the case for 1 in _0_to_1
quint64 _0_to_DBL_MAX = quint64((Q_UINT64_C(1) << 52) * ((1 << 11) - 2)) + number_of_denormals;
quint64 _0_to_DBL_MIN = 1 + number_of_denormals;
QTest::newRow("[0,DBL_MIN]") << 0.0 << DBL_MIN << _0_to_DBL_MIN;
QTest::newRow("[0,DBL_MAX]") << 0.0 << DBL_MAX << _0_to_DBL_MAX;
QTest::newRow("[1,1.5]") << 1.0 << 1.5 << (Q_UINT64_C(1) << 51);
QTest::newRow("[0,1]") << 0.0 << 1.0 << _0_to_1;
QTest::newRow("[0.5,1]") << 0.5 << 1.0 << (Q_UINT64_C(1) << 52);
QTest::newRow("[1,2]") << 1.0 << 2.0 << _1_to_2;
QTest::newRow("[-1,+1]") << -1.0 << +1.0 << 2 * _0_to_1;
QTest::newRow("[-1,0]") << -1.0 << 0.0 << _0_to_1;
QTest::newRow("[-1,DBL_MAX]") << -1.0 << DBL_MAX << _0_to_1 + _0_to_DBL_MAX;
QTest::newRow("[-2,-1") << -2.0 << -1.0 << _1_to_2;
QTest::newRow("[-1,-2") << -1.0 << -2.0 << _1_to_2;
QTest::newRow("[DBL_MIN,DBL_MAX]") << DBL_MIN << DBL_MAX << _0_to_DBL_MAX - _0_to_DBL_MIN;
QTest::newRow("[-DBL_MAX,DBL_MAX]") << -DBL_MAX << DBL_MAX << (2*_0_to_DBL_MAX);
double denormal = DBL_MIN;
denormal/=2.0;
QTest::newRow("denormal") << 0.0 << denormal << _0_to_DBL_MIN/2;
}
void tst_QNumeric::floatDistance_double()
{
QFETCH(double, val1);
QFETCH(double, val2);
QFETCH(quint64, expectedDistance);
#ifdef Q_OS_QNX
QEXPECT_FAIL("denormal", "See QTBUG-37094", Continue);
#endif
QCOMPARE(qFloatDistance(val1, val2), expectedDistance);
}
// Whole number tests:
void tst_QNumeric::addOverflow_data()
{