Skip to content

Commit

Permalink
Feature: Better NaN (doctest#584)
Browse files Browse the repository at this point in the history
* matcher-like nan check

* Remove superfluous extern template declarations

* Add explicit template parameters

* Correct template instantiation

* Fix test includes

* class -> struct

* Correctly instantiate

* Oops

* Try fix interface

* Add MinGW exception

* Add info regarding interface decl and def

* Adjust docs

* Remove accidental paste in comment

* Remove redundant macro definition
  • Loading branch information
Saalvage authored Jan 13, 2022
1 parent 1a62e75 commit 58f3ec8
Show file tree
Hide file tree
Showing 23 changed files with 181 additions and 335 deletions.
25 changes: 12 additions & 13 deletions doc/markdown/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,6 @@ Note that these asserts also have a ```_MESSAGE``` form - like ```CHECK_THROWS_M

One may use the [**```DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS```**](configuration.md#doctest_config_void_cast_expressions) config identifier to cast the expression in these asserts to void to avoid warnings or other issues - for example nodiscard statements whose result isn't checked. This will however limit the ability to write entire ```{}``` blocks of code as the expression (or multiple statements) but in that case a simple lambda can be used. This should have been the default behavior from day 1 of the framework...

## NaN checking

```<LEVEL>``` is one of 3 possible: ```REQUIRE```/```CHECK```/```WARN```.

- ```<LEVEL>_NAN(expression)```
- ```<LEVEL>_NOT_NAN(expression)```

These utility macros check if a floating point value is or is not NaN respectively.

They capture the actual float value on assertion failure.

These macros are unaffected by `DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED`.

## Using asserts out of a testing context

Asserts can be used outside of a testing context (in code not called from a ```TEST_CASE()```) instead of [```assert()```](https://en.cppreference.com/w/cpp/error/assert).
Expand Down Expand Up @@ -182,6 +169,18 @@ REQUIRE(22.0/7 == doctest::Approx(3.141).epsilon(0.01)); // allow for a 1% error
When dealing with very large or very small numbers it can be useful to specify a scale, which can be achieved by calling the ```scale()``` method on the ```doctest::Approx``` instance.
## NaN checking
Two NaN floating point numbers do not compare equal to each other. This makes it quite inconvenient to check for NaN while capturing the value.
```c++
CHECK(std::isnan(performComputation()); // does not capture the result of the call
```

**doctest** provides `doctest::IsNaN` which can be used in assertions to check if a float (or any other floating point fundamental type) is indeed NaN, outputting the actual value if it is not.
```c++
CHECK(doctest::IsNaN(performComputation()); // captures the result!
```
--------
- Check out the [**example**](../../examples/all_features/assertion_macros.cpp) which shows many of these macros
Expand Down
143 changes: 42 additions & 101 deletions doctest/doctest.h
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,16 @@ DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly define
#define DOCTEST_INTERFACE
#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL

// needed for extern template instantiations
// see https://github.com/fmtlib/fmt/issues/2228
#if DOCTEST_MSVC
#define DOCTEST_INTERFACE_DECL
#define DOCTEST_INTERFACE_DEF DOCTEST_INTERFACE
#else // DOCTEST_MSVC
#define DOCTEST_INTERFACE_DECL DOCTEST_INTERFACE
#define DOCTEST_INTERFACE_DEF
#endif // DOCTEST_MSVC

#define DOCTEST_EMPTY

#if DOCTEST_MSVC
Expand Down Expand Up @@ -445,7 +455,6 @@ DOCTEST_GCC_SUPPRESS_WARNING_POP
#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
#include <cmath>
#include <cstddef>
#include <ostream>
#include <istream>
Expand Down Expand Up @@ -633,8 +642,6 @@ namespace assertType {
is_ge = 2 * is_gt,
is_le = 2 * is_ge,

is_nan = 2 * is_le,

// macro types

DT_WARN = is_normal | is_warn,
Expand Down Expand Up @@ -696,14 +703,6 @@ namespace assertType {
DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn,
DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check,
DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require,

DT_WARN_NAN = is_normal | is_nan | is_warn,
DT_CHECK_NAN = is_normal | is_nan | is_check,
DT_REQUIRE_NAN = is_normal | is_nan | is_require,

DT_WARN_NOT_NAN = is_normal | is_nan | is_false | is_warn,
DT_CHECK_NOT_NAN = is_normal | is_nan | is_false | is_check,
DT_REQUIRE_NOT_NAN = is_normal | is_nan | is_false | is_require,
};
} // namespace assertType

Expand Down Expand Up @@ -1134,6 +1133,22 @@ DOCTEST_INTERFACE String toString(const Approx& in);

DOCTEST_INTERFACE const ContextOptions* getContextOptions();

template <typename F>
struct DOCTEST_INTERFACE_DECL IsNaN
{
F val;
IsNaN(F f) : val(f) { }
operator bool() const;
};
#ifndef __MINGW32__
extern template struct DOCTEST_INTERFACE_DECL IsNaN<float>;
extern template struct DOCTEST_INTERFACE_DECL IsNaN<double>;
extern template struct DOCTEST_INTERFACE_DECL IsNaN<long double>;
#endif
DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& out, IsNaN<float> nanCheck);
DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& out, IsNaN<double> nanCheck);
DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& out, IsNaN<long double> nanCheck);

#ifndef DOCTEST_CONFIG_DISABLE

namespace detail {
Expand Down Expand Up @@ -1501,12 +1516,6 @@ DOCTEST_CLANG_SUPPRESS_WARNING_POP
DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge)
DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le)

template <typename T>
bool is_nan(T);
extern template bool is_nan(float);
extern template bool is_nan(double);
extern template bool is_nan(long double);

struct DOCTEST_INTERFACE ResultBuilder : public AssertData
{
ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
Expand Down Expand Up @@ -1536,19 +1545,6 @@ DOCTEST_CLANG_SUPPRESS_WARNING_POP
return !m_failed;
}

template <typename L>
DOCTEST_NOINLINE bool nan_assert(L val) {
m_failed = !is_nan(val);

if(m_at & assertType::is_false) //!OCLINT bitwise operator in conditional
m_failed = !m_failed;

if(m_failed || getContextOptions()->success)
m_decomp = toString(val);

return !m_failed;
}

void translateException();

bool log();
Expand Down Expand Up @@ -1628,23 +1624,6 @@ DOCTEST_CLANG_SUPPRESS_WARNING_POP
return !failed;
}

template <typename L>
DOCTEST_NOINLINE bool nan_assert(assertType::Enum at, const char* file, int line,
const char* expr, L val) {
bool failed = !is_nan(val);

if (at & assertType::is_false) //!OCLINT bitwise operator in conditional
failed = !failed;

// ###################################################################################
// IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT
// THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED
// ###################################################################################
DOCTEST_ASSERT_OUT_OF_TESTS(toString(val));
DOCTEST_ASSERT_IN_TESTS(toString(val));
return !failed;
}

struct DOCTEST_INTERFACE IExceptionTranslator
{
IExceptionTranslator();
Expand Down Expand Up @@ -2430,32 +2409,6 @@ int registerReporter(const char* name, int priority, bool isReporter) {
#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__)
#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__)

#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS

#define DOCTEST_NAN_ASSERT(assert_type, ...) \
[&] { \
doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \
__LINE__, #__VA_ARGS__); \
DOCTEST_WRAP_IN_TRY( \
DOCTEST_RB.nan_assert(__VA_ARGS__)) \
DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \
}()

#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS

#define DOCTEST_NAN_ASSERT(assert_type, ...) \
doctest::detail::nan_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \
#__VA_ARGS__, __VA_ARGS__)

#endif

#define DOCTEST_WARN_NAN(...) DOCTEST_NAN_ASSERT(DT_WARN_NAN, __VA_ARGS__)
#define DOCTEST_CHECK_NAN(...) DOCTEST_NAN_ASSERT(DT_CHECK_NAN, __VA_ARGS__)
#define DOCTEST_REQUIRE_NAN(...) DOCTEST_NAN_ASSERT(DT_REQUIRE_NAN, __VA_ARGS__)
#define DOCTEST_WARN_NOT_NAN(...) DOCTEST_NAN_ASSERT(DT_WARN_NOT_NAN, __VA_ARGS__)
#define DOCTEST_CHECK_NOT_NAN(...) DOCTEST_NAN_ASSERT(DT_CHECK_NOT_NAN, __VA_ARGS__)
#define DOCTEST_REQUIRE_NOT_NAN(...) DOCTEST_NAN_ASSERT(DT_REQUIRE_NOT_NAN, __VA_ARGS__)

#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS

#undef DOCTEST_WARN_THROWS
Expand Down Expand Up @@ -2759,13 +2712,6 @@ namespace detail {

#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED

#define DOCTEST_WARN_NAN(...) [] { return false; }()
#define DOCTEST_CHECK_NAN(...) [] { return false; }()
#define DOCTEST_REQUIRE_NAN(...) [] { return false; }()
#define DOCTEST_WARN_NOT_NAN(...) [] { return false; }()
#define DOCTEST_CHECK_NOT_NAN(...) [] { return false; }()
#define DOCTEST_REQUIRE_NOT_NAN(...) [] { return false; }()

#define DOCTEST_WARN_THROWS_WITH(expr, ...) [] { return false; }()
#define DOCTEST_CHECK_THROWS_WITH(expr, ...) [] { return false; }()
#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) [] { return false; }()
Expand Down Expand Up @@ -2934,13 +2880,6 @@ namespace detail {
#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__)
#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__)

#define WARN_NAN(...) DOCTEST_WARN_NAN(__VA_ARGS__)
#define CHECK_NAN(...) DOCTEST_CHECK_NAN(__VA_ARGS__)
#define REQUIRE_NAN(...) DOCTEST_REQUIRE_NAN(__VA_ARGS__)
#define WARN_NOT_NAN(...) DOCTEST_WARN_NOT_NAN(__VA_ARGS__)
#define CHECK_NOT_NAN(...) DOCTEST_CHECK_NOT_NAN(__VA_ARGS__)
#define REQUIRE_NOT_NAN(...) DOCTEST_REQUIRE_NOT_NAN(__VA_ARGS__)

// KEPT FOR BACKWARDS COMPATIBILITY
#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__)
#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__)
Expand Down Expand Up @@ -3745,9 +3684,6 @@ const char* assertString(assertType::Enum at) {
DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY);
DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY_FALSE);

DOCTEST_GENERATE_ASSERT_TYPE_CASES(NAN);
DOCTEST_GENERATE_ASSERT_TYPE_CASES(NOT_NAN);

default: DOCTEST_INTERNAL_ERROR("Tried stringifying invalid assert type!");
}
DOCTEST_MSVC_SUPPRESS_WARNING_POP
Expand Down Expand Up @@ -3875,6 +3811,22 @@ String toString(const Approx& in) {
}
const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); }

DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738)
template <typename F>
IsNaN<F>::operator bool() const {
return std::isnan(val);
}
DOCTEST_MSVC_SUPPRESS_WARNING_POP
template struct DOCTEST_INTERFACE_DEF IsNaN<float>;
template struct DOCTEST_INTERFACE_DEF IsNaN<double>;
template struct DOCTEST_INTERFACE_DEF IsNaN<long double>;
std::ostream& operator<<(std::ostream& out, IsNaN<float> nanCheck)
{ out << nanCheck.val; return out; }
std::ostream& operator<<(std::ostream& out, IsNaN<double> nanCheck)
{ out << nanCheck.val; return out; }
std::ostream& operator<<(std::ostream& out, IsNaN<long double> nanCheck)
{ out << nanCheck.val; return out; }

} // namespace doctest

#ifdef DOCTEST_CONFIG_DISABLE
Expand Down Expand Up @@ -4739,17 +4691,6 @@ namespace {
#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH
} // namespace
namespace detail {

DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738)
template <typename T>
bool is_nan(T t) {
return std::isnan(t);
}
DOCTEST_MSVC_SUPPRESS_WARNING_POP
template bool is_nan(float);
template bool is_nan(double);
template bool is_nan(long double);

ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const char* exception_string) {
m_test_case = g_cs->currentTest;
Expand Down
30 changes: 16 additions & 14 deletions doctest/parts/doctest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -730,9 +730,6 @@ const char* assertString(assertType::Enum at) {
DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY);
DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY_FALSE);

DOCTEST_GENERATE_ASSERT_TYPE_CASES(NAN);
DOCTEST_GENERATE_ASSERT_TYPE_CASES(NOT_NAN);

default: DOCTEST_INTERNAL_ERROR("Tried stringifying invalid assert type!");
}
DOCTEST_MSVC_SUPPRESS_WARNING_POP
Expand Down Expand Up @@ -860,6 +857,22 @@ String toString(const Approx& in) {
}
const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); }

DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738)
template <typename F>
IsNaN<F>::operator bool() const {
return std::isnan(val);
}
DOCTEST_MSVC_SUPPRESS_WARNING_POP
template struct DOCTEST_INTERFACE_DEF IsNaN<float>;
template struct DOCTEST_INTERFACE_DEF IsNaN<double>;
template struct DOCTEST_INTERFACE_DEF IsNaN<long double>;
std::ostream& operator<<(std::ostream& out, IsNaN<float> nanCheck)
{ out << nanCheck.val; return out; }
std::ostream& operator<<(std::ostream& out, IsNaN<double> nanCheck)
{ out << nanCheck.val; return out; }
std::ostream& operator<<(std::ostream& out, IsNaN<long double> nanCheck)
{ out << nanCheck.val; return out; }

} // namespace doctest

#ifdef DOCTEST_CONFIG_DISABLE
Expand Down Expand Up @@ -1724,17 +1737,6 @@ namespace {
#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH
} // namespace
namespace detail {

DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738)
template <typename T>
bool is_nan(T t) {
return std::isnan(t);
}
DOCTEST_MSVC_SUPPRESS_WARNING_POP
template bool is_nan(float);
template bool is_nan(double);
template bool is_nan(long double);

ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const char* exception_string) {
m_test_case = g_cs->currentTest;
Expand Down
Loading

0 comments on commit 58f3ec8

Please sign in to comment.