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

Fix ULP matcher #2152

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion CMakeLists.txt
Expand Up @@ -9,6 +9,7 @@ endif()
option(CATCH_INSTALL_DOCS "Install documentation alongside library" ON)
option(CATCH_INSTALL_EXTRAS "Install extras alongside library" ON)
option(CATCH_DEVELOPMENT_BUILD "Build tests, enable warnings, enable Werror, etc" OFF)
option(CATCH_FORCE_COLORED_OUTPUT "Always produce ANSI-colored output (GNU/Clang only)" OFF)

include(CMakeDependentOption)
cmake_dependent_option(CATCH_BUILD_TESTING "Build the SelfTest project" ON "CATCH_DEVELOPMENT_BUILD" OFF)
Expand All @@ -33,6 +34,14 @@ endif()

project(Catch2 LANGUAGES CXX VERSION 3.0.0)

if (${CATCH_FORCE_COLORED_OUTPUT})
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
add_compile_options (-fdiagnostics-color=always)
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options (-fcolor-diagnostics)
endif ()
endif()

# Provide path for scripts
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMake")
include(GNUInstallDirs)
Expand Down Expand Up @@ -158,7 +167,7 @@ if (NOT_SUBPROJECT)
DESTINATION
${CATCH_CMAKE_CONFIG_DESTINATION}
)

# Install debugger helpers
install(
FILES
Expand Down
61 changes: 50 additions & 11 deletions src/catch2/matchers/catch_matchers_floating_point.cpp
Expand Up @@ -38,23 +38,62 @@ namespace {
return i;
}

// Calculates the ULP distance between two floating point numbers
// That is, the number of valid IEEE-754 floating point representations
// between the two values. In the general case we can say:
// * if nextafter(a, INFINITY) == b, then ulpDistance(a, b) == 1
// * if a == nextafter(b, INFINITY), then ulpDistance(a, b) == -1
// however, as an exception, the distance between positive and negative
// zero is considered to always have a value of zero
// There is an argument to be made that this distance should be one, since
// nextafter(-0f, INFINITY) == +0f
// nextafter(+0f, -INFINITY) == -0f
// However, the above exception was chosen to ensure that a == b implies
// ulpDistance(a, b) == 0
// and that
// ulpDistance(-x, x) == ulpDistance(0, x)*2
// Denormalized normals are counted normally in distance calculations.
// See also: boost/math/special_functions/next.hpp
template <typename FP>
int64_t ulpDistance(FP a, FP b) {
// Smallest value greater than zero
constexpr FP EPSILON_0 = std::numeric_limits<FP>::denorm_min();
// Largest possible distance we can return
constexpr int64_t INFINITE_DISTANCE = std::numeric_limits<int64_t>::max();
if (Catch::isnan(a) || Catch::isnan(b)) // Early out for NaNs
return INFINITE_DISTANCE;
if (!std::isfinite(a) || !std::isfinite(b)) // Early out for infinity
return INFINITE_DISTANCE;
if(a > b) // Ensure a < b
return -ulpDistance(b, a);
if(a == b) // This also ensures ulpDistance(-0f, +0f) == 0
return 0;
if(a == 0) // Ensure a != 0
return 1 + std::abs(ulpDistance((b < 0) ? -EPSILON_0 : EPSILON_0, b));
if(b == 0) // Ensure b != 0
return 1 + std::abs(ulpDistance((a < 0) ? -EPSILON_0 : EPSILON_0, a));
if((a < 0) != (b < 0)) // Ensure a and b have the same sign
return 2 + std::abs(ulpDistance((b < 0) ? -EPSILON_0 : EPSILON_0, b))
+ std::abs(ulpDistance((a < 0) ? -EPSILON_0 : EPSILON_0, a));
if(a < 0) // Ensure a and b are positive
return ulpDistance(-b, -a);
assert(a >= 0);
assert(a < b);
int64_t ac = convert(a);
int64_t bc = convert(b);
return bc - ac; // ULP distance, assuming IEEE-754 floating point numbers
}

template <typename FP>
bool almostEqualUlps(FP lhs, FP rhs, uint64_t maxUlpDiff) {
// Comparison with NaN should always be false.
// This way we can rule it out before getting into the ugly details
if (Catch::isnan(lhs) || Catch::isnan(rhs)) {
if (Catch::isnan(lhs) || Catch::isnan(rhs))
return false;
}

auto lc = convert(lhs);
auto rc = convert(rhs);

if ((lc < 0) != (rc < 0)) {
// Potentially we can have +0 and -0
if (!std::isfinite(lhs) || !std::isfinite(rhs))
return lhs == rhs;
}

auto ulpDiff = std::abs(lc - rc);
auto ulpDiff = std::abs(ulpDistance(lhs, rhs));
return static_cast<uint64_t>(ulpDiff) <= maxUlpDiff;
}

Expand Down Expand Up @@ -128,7 +167,7 @@ namespace Detail {
WithinUlpsMatcher::WithinUlpsMatcher(double target, uint64_t ulps, Detail::FloatingPointKind baseType)
:m_target{ target }, m_ulps{ ulps }, m_type{ baseType } {
CATCH_ENFORCE(m_type == Detail::FloatingPointKind::Double
|| m_ulps < (std::numeric_limits<uint32_t>::max)(),
|| m_ulps <= (std::numeric_limits<uint32_t>::max)(),
"Provided ULP is impossibly large for a float comparison.");
}

Expand Down
92 changes: 80 additions & 12 deletions tests/SelfTest/UsageTests/Matchers.tests.cpp
Expand Up @@ -447,15 +447,45 @@ namespace { namespace MatchersTests {
REQUIRE_THAT(-10.f, WithinAbs(-9.6f, 0.5f));
}
SECTION("ULPs") {
REQUIRE_THAT(1.f, WithinULP(1.f, 0));

REQUIRE_THAT(nextafter(1.f, 2.f), WithinULP(1.f, 1));
REQUIRE_THAT(0.f, WithinULP(nextafter(0.f, 1.f), 1));
REQUIRE_THAT(1.f, WithinULP(nextafter(1.f, 0.f), 1));
REQUIRE_THAT(1.f, !WithinULP(nextafter(1.f, 2.f), 0));
const float ONE_EPSILON_0 = nextafter(0.f, 1.f);
const float TWO_EPSILON_0 = nextafter(ONE_EPSILON_0, 1.f);
const float NEG_ONE_EPSILON_1 = nextafter(1.f, 0.f);
const float POS_ONE_EPSILON_1 = nextafter(1.f, 2.f);
const uint32_t ULP_DISTANCE_0_1 = 1065353216;

REQUIRE_THAT(1.f, WithinULP(1.f, 0));
REQUIRE_THAT(-1.f, WithinULP(-1.f, 0));
REQUIRE_THAT(-1.f, !WithinULP(1.f, 0));
REQUIRE_THAT(1.f, !WithinULP(-1.f, 0));
REQUIRE_THAT(-0.f, WithinULP(0.f, 0));
REQUIRE_THAT(0.f, WithinULP(-0.f, 0));
REQUIRE_THAT(999e10f, WithinULP(999e10f, 0));
REQUIRE_THAT(-999e10f, WithinULP(-999e10f, 0));
REQUIRE_THAT(999e10f, !WithinULP(-999e10f, 0));
REQUIRE_THAT(-999e10f, !WithinULP(999e10f, 0));
REQUIRE_THAT(0.f, !WithinULP(1.f, 1));
REQUIRE_THAT(1.f, !WithinULP(0.f, 1));

REQUIRE_THAT(0.f, WithinULP(ONE_EPSILON_0, 1));
REQUIRE_THAT(-ONE_EPSILON_0, WithinULP(ONE_EPSILON_0, 2));
REQUIRE_THAT(0.f, WithinULP(1.f, ULP_DISTANCE_0_1));
REQUIRE_THAT(0.f, WithinULP(-1.f, ULP_DISTANCE_0_1));
REQUIRE_THAT(1.f, WithinULP(0.f, ULP_DISTANCE_0_1));
REQUIRE_THAT(-1.f, WithinULP(0.f, ULP_DISTANCE_0_1));
REQUIRE_THAT(-1.f, WithinULP(1.f, 2*ULP_DISTANCE_0_1));
REQUIRE_THAT(1.f, WithinULP(-1.f, 2*ULP_DISTANCE_0_1));

REQUIRE_THAT(0.f, WithinULP(ONE_EPSILON_0, 1));
REQUIRE_THAT(0.f, WithinULP(TWO_EPSILON_0, 2));
REQUIRE_THAT(1.f, WithinULP(NEG_ONE_EPSILON_1, 1));
REQUIRE_THAT(1.f, WithinULP(POS_ONE_EPSILON_1, 1));
REQUIRE_THAT(POS_ONE_EPSILON_1, WithinULP(1.f, 1));
REQUIRE_THAT(1.f, !WithinULP(NEG_ONE_EPSILON_1, 0));
REQUIRE_THAT(1.f, !WithinULP(POS_ONE_EPSILON_1, 0));

REQUIRE_THAT(0.f, WithinULP(TWO_EPSILON_0, 2));
REQUIRE_THAT(ONE_EPSILON_0, WithinULP(TWO_EPSILON_0, 1));
REQUIRE_THAT(POS_ONE_EPSILON_1, WithinULP(NEG_ONE_EPSILON_1, 2));
}
SECTION("Composed") {
REQUIRE_THAT(1.f, WithinAbs(1.f, 0.5) || WithinULP(1.f, 1));
Expand Down Expand Up @@ -503,15 +533,45 @@ namespace { namespace MatchersTests {
REQUIRE_THAT(-10., WithinAbs(-9.6, 0.5));
}
SECTION("ULPs") {
REQUIRE_THAT(1., WithinULP(1., 0));

REQUIRE_THAT(nextafter(1., 2.), WithinULP(1., 1));
REQUIRE_THAT(0., WithinULP(nextafter(0., 1.), 1));
REQUIRE_THAT(1., WithinULP(nextafter(1., 0.), 1));
REQUIRE_THAT(1., !WithinULP(nextafter(1., 2.), 0));
const double ONE_EPSILON_0 = nextafter(0., 1.);
const double TWO_EPSILON_0 = nextafter(ONE_EPSILON_0, 1.);
const double NEG_ONE_EPSILON_1 = nextafter(1., 0.);
const double POS_ONE_EPSILON_1 = nextafter(1., 2.);
const uint64_t ULP_DISTANCE_0_1 = 4607182418800017408;

REQUIRE_THAT(1., WithinULP(1., 0));
REQUIRE_THAT(-1., WithinULP(-1., 0));
REQUIRE_THAT(-1., !WithinULP(1., 0));
REQUIRE_THAT(1., !WithinULP(-1., 0));
REQUIRE_THAT(-0., WithinULP(0., 0));
REQUIRE_THAT(0., WithinULP(-0., 0));
REQUIRE_THAT(999e10, WithinULP(999e10, 0));
REQUIRE_THAT(-999e10, WithinULP(-999e10, 0));
REQUIRE_THAT(999e10, !WithinULP(-999e10, 0));
REQUIRE_THAT(-999e10, !WithinULP(999e10, 0));
REQUIRE_THAT(0., !WithinULP(1., 1));
REQUIRE_THAT(1., !WithinULP(0., 1));

REQUIRE_THAT(0., WithinULP(ONE_EPSILON_0, 1));
REQUIRE_THAT(-ONE_EPSILON_0, WithinULP(ONE_EPSILON_0, 2));
REQUIRE_THAT(0., WithinULP(1., ULP_DISTANCE_0_1));
REQUIRE_THAT(0., WithinULP(-1., ULP_DISTANCE_0_1));
REQUIRE_THAT(1., WithinULP(0., ULP_DISTANCE_0_1));
REQUIRE_THAT(-1., WithinULP(0., ULP_DISTANCE_0_1));
REQUIRE_THAT(-1., WithinULP(1., 2*ULP_DISTANCE_0_1));
REQUIRE_THAT(1., WithinULP(-1., 2*ULP_DISTANCE_0_1));

REQUIRE_THAT(0., WithinULP(ONE_EPSILON_0, 1));
REQUIRE_THAT(0., WithinULP(TWO_EPSILON_0, 2));
REQUIRE_THAT(1., WithinULP(NEG_ONE_EPSILON_1, 1));
REQUIRE_THAT(1., WithinULP(POS_ONE_EPSILON_1, 1));
REQUIRE_THAT(POS_ONE_EPSILON_1, WithinULP(1., 1));
REQUIRE_THAT(1., !WithinULP(NEG_ONE_EPSILON_1, 0));
REQUIRE_THAT(1., !WithinULP(POS_ONE_EPSILON_1, 0));

REQUIRE_THAT(0., WithinULP(TWO_EPSILON_0, 2));
REQUIRE_THAT(ONE_EPSILON_0, WithinULP(TWO_EPSILON_0, 1));
REQUIRE_THAT(POS_ONE_EPSILON_1, WithinULP(NEG_ONE_EPSILON_1, 2));
}
SECTION("Composed") {
REQUIRE_THAT(1., WithinAbs(1., 0.5) || WithinULP(2., 1));
Expand Down Expand Up @@ -541,6 +601,14 @@ namespace { namespace MatchersTests {
REQUIRE_THAT(NAN, !WithinRel(NAN));
REQUIRE_THAT(1., !WithinRel(NAN));
REQUIRE_THAT(NAN, !WithinRel(1.));
REQUIRE_THAT(INFINITY, WithinULP(INFINITY, 0));
REQUIRE_THAT(-INFINITY, WithinULP(-INFINITY, 0));
REQUIRE_THAT(INFINITY, !WithinULP(-INFINITY, 0));
REQUIRE_THAT(-INFINITY, !WithinULP(INFINITY, 0));
REQUIRE_THAT(-INFINITY, !WithinULP(static_cast<float>(INFINITY), std::numeric_limits<uint32_t>::max()));
REQUIRE_THAT(INFINITY, !WithinULP(static_cast<float>(-INFINITY), std::numeric_limits<uint32_t>::max()));
REQUIRE_THAT(1., !WithinULP(INFINITY, std::numeric_limits<uint32_t>::max()));
REQUIRE_THAT(1., !WithinULP(-INFINITY, std::numeric_limits<uint32_t>::max()));
}

TEST_CASE("Arbitrary predicate matcher", "[matchers][generic]") {
Expand Down