Skip to content

Commit

Permalink
Add contains option to checks. (doctest#620)
Browse files Browse the repository at this point in the history
* Add contains otion to checks.

* Add test for teh contains option of CHECK_THROWS_WITH.

* Adress comments Saalvage: move compare function to Contains, add operator== and make m_exception_string a union."

* Add new tests.

* Attempt to fix windows specific warning about unions.

* Attempt 2 to fix windows specific warning about unions.

* Attempt 3 to fix windows specific warning about unions.

* Attempt 4 to fix windows specific warning about unions.

* Return union to a struct.

* Fixing and refactoring

* Docs

Co-authored-by: Salvage <[email protected]>
  • Loading branch information
MFraters and Saalvage authored Mar 5, 2022
1 parent bef1965 commit 8de4cf7
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 64 deletions.
13 changes: 13 additions & 0 deletions doc/markdown/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ Checkout the [**example**](../../examples/all_features/asserts_used_outside_of_t

Currently [**logging macros**](logging.md) cannot be used for extra context for asserts outside of a test run. That means that the ```_MESSAGE``` variants of asserts are also not usable - since they are just a packed ```INFO()``` with an assert right after it.

## String containment

```doctest::Contains``` can be used to check whether the string passed to its constructor is contained within the string it is compared with. Here's a simple example:

```c++
REQUIRE("foobar" == doctest::Contains("foo"));
```
It can also be used with the ```THROWS_WITH``` family of assertion macros to check whether the thrown exception [translated to a string](stringification.md#translating-exceptions) contains the provided string. Here's another example:
```c++
REQUIRE_THROWS_WITH(func(), doctest::Contains("Oopsie"));
```

## Floating point comparisons

When comparing floating point numbers - especially if at least one of them has been computed - great care must be taken to allow for rounding errors and inexact representations.
Expand Down
108 changes: 81 additions & 27 deletions doctest/doctest.h
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,22 @@ DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs);

DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in);

class DOCTEST_INTERFACE Contains {
public:
explicit Contains(const String& string);

bool checkWith(const String& other) const;

String string;
};

DOCTEST_INTERFACE String toString(const Contains& in);

DOCTEST_INTERFACE bool operator==(const String& lhs, const Contains& rhs);
DOCTEST_INTERFACE bool operator==(const Contains& lhs, const String& rhs);
DOCTEST_INTERFACE bool operator!=(const String& lhs, const Contains& rhs);
DOCTEST_INTERFACE bool operator!=(const Contains& lhs, const String& rhs);

namespace Color {
enum Enum
{
Expand Down Expand Up @@ -744,9 +760,27 @@ struct DOCTEST_INTERFACE AssertData
String m_decomp;

// for specific exception-related asserts
bool m_threw_as;
const char* m_exception_type;
const char* m_exception_string;
bool m_threw_as;
const char* m_exception_type;
class DOCTEST_INTERFACE StringContains {
private:
Contains content;
bool isContains;

public:
StringContains() : content(String()), isContains(false) { }
StringContains(const String& str) : content(str), isContains(false) { }
StringContains(const Contains& cntn) : content(cntn), isContains(true) { }

bool check(const String& str) { return isContains ? (content == str) : (content.string == str); }

operator const String&() const { return content.string; }

const char* c_str() const { return content.string.c_str(); }
} m_exception_string;

AssertData(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const StringContains& exception_string);
};

struct DOCTEST_INTERFACE MessageData
Expand Down Expand Up @@ -1519,7 +1553,10 @@ DOCTEST_CLANG_SUPPRESS_WARNING_POP
struct DOCTEST_INTERFACE ResultBuilder : public AssertData
{
ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type = "", const char* exception_string = "");
const char* exception_type = "", const String& exception_string = "");

ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const Contains& exception_string);

void setResult(const Result& res);

Expand Down Expand Up @@ -3643,17 +3680,30 @@ int String::compare(const String& other, bool no_case) const {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
String operator+(const String& lhs, const String& rhs) { return String(lhs) += rhs; }

// clang-format off
bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; }
bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; }
bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; }
bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; }
bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; }
bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; }
// clang-format on

std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); }

Contains::Contains(const String& str) : string(str) { }

bool Contains::checkWith(const String& full_string) const {
return strstr(full_string.c_str(), string.c_str()) != nullptr;
}

String toString(const Contains& in) {
return "Contains( " + in.string + " )";
}

bool operator==(const String& lhs, const Contains& rhs) { return rhs.checkWith(lhs); }
bool operator==(const Contains& lhs, const String& rhs) { return lhs.checkWith(rhs); }
bool operator!=(const String& lhs, const Contains& rhs) { return !rhs.checkWith(lhs); }
bool operator!=(const Contains& lhs, const String& rhs) { return !lhs.checkWith(rhs); }

namespace {
void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;)
} // namespace
Expand Down Expand Up @@ -4706,24 +4756,26 @@ namespace {
}
#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH
} // namespace
namespace detail {
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;
m_at = at;
m_file = file;
m_line = line;
m_expr = expr;
m_failed = true;
m_threw = false;
m_threw_as = false;
m_exception_type = exception_type;
m_exception_string = exception_string;

AssertData::AssertData(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const StringContains& exception_string)
: m_test_case(g_cs->currentTest), m_at(at), m_file(file), m_line(line), m_expr(expr),
m_failed(true), m_threw(false), m_threw_as(false), m_exception_type(exception_type),
m_exception_string(exception_string) {
#if DOCTEST_MSVC
if(m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC
++m_expr;
if (m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC
++m_expr;
#endif // MSVC
}
}

namespace detail {
ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const String& exception_string)
: AssertData(at, file, line, expr, exception_type, exception_string) { }

ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const Contains& exception_string)
: AssertData(at, file, line, expr, exception_type, exception_string) { }

void ResultBuilder::setResult(const Result& res) {
m_decomp = res.m_decomp;
Expand All @@ -4739,11 +4791,11 @@ namespace detail {
if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional
m_failed = !m_threw;
} else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT
m_failed = !m_threw_as || (m_exception != m_exception_string);
m_failed = !m_threw_as || !m_exception_string.check(m_exception);
} else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional
m_failed = !m_threw_as;
} else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional
m_failed = m_exception != m_exception_string;
m_failed = !m_exception_string.check(m_exception);
} else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional
m_failed = m_threw;
}
Expand Down Expand Up @@ -5416,7 +5468,7 @@ namespace {
if(rb.m_at & assertType::is_throws_as)
xml.scopedElement("ExpectedException").writeText(rb.m_exception_type);
if(rb.m_at & assertType::is_throws_with)
xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string);
xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string.c_str());
if((rb.m_at & assertType::is_normal) && !rb.m_threw)
xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str());

Expand Down Expand Up @@ -5462,7 +5514,8 @@ namespace {
} else if((rb.m_at & assertType::is_throws_as) &&
(rb.m_at & assertType::is_throws_with)) { //!OCLINT
s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \""
<< rb.m_exception_string << "\", " << rb.m_exception_type << " ) " << Color::None;
<< rb.m_exception_string.c_str()
<< "\", " << rb.m_exception_type << " ) " << Color::None;
if(rb.m_threw) {
if(!rb.m_failed) {
s << "threw as expected!\n";
Expand All @@ -5483,7 +5536,8 @@ namespace {
} else if(rb.m_at &
assertType::is_throws_with) { //!OCLINT bitwise operator in conditional
s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \""
<< rb.m_exception_string << "\" ) " << Color::None
<< rb.m_exception_string.c_str()
<< "\" ) " << Color::None
<< (rb.m_threw ? (!rb.m_failed ? "threw as expected!" :
"threw a DIFFERENT exception: ") :
"did NOT throw at all!")
Expand Down
63 changes: 40 additions & 23 deletions doctest/parts/doctest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -673,17 +673,30 @@ int String::compare(const String& other, bool no_case) const {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
String operator+(const String& lhs, const String& rhs) { return String(lhs) += rhs; }

// clang-format off
bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; }
bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; }
bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; }
bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; }
bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; }
bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; }
// clang-format on

std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); }

Contains::Contains(const String& str) : string(str) { }

bool Contains::checkWith(const String& full_string) const {
return strstr(full_string.c_str(), string.c_str()) != nullptr;
}

String toString(const Contains& in) {
return "Contains( " + in.string + " )";
}

bool operator==(const String& lhs, const Contains& rhs) { return rhs.checkWith(lhs); }
bool operator==(const Contains& lhs, const String& rhs) { return lhs.checkWith(rhs); }
bool operator!=(const String& lhs, const Contains& rhs) { return !rhs.checkWith(lhs); }
bool operator!=(const Contains& lhs, const String& rhs) { return !lhs.checkWith(rhs); }

namespace {
void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;)
} // namespace
Expand Down Expand Up @@ -1736,24 +1749,26 @@ namespace {
}
#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH
} // namespace
namespace detail {
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;
m_at = at;
m_file = file;
m_line = line;
m_expr = expr;
m_failed = true;
m_threw = false;
m_threw_as = false;
m_exception_type = exception_type;
m_exception_string = exception_string;

AssertData::AssertData(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const StringContains& exception_string)
: m_test_case(g_cs->currentTest), m_at(at), m_file(file), m_line(line), m_expr(expr),
m_failed(true), m_threw(false), m_threw_as(false), m_exception_type(exception_type),
m_exception_string(exception_string) {
#if DOCTEST_MSVC
if(m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC
++m_expr;
if (m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC
++m_expr;
#endif // MSVC
}
}

namespace detail {
ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const String& exception_string)
: AssertData(at, file, line, expr, exception_type, exception_string) { }

ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const Contains& exception_string)
: AssertData(at, file, line, expr, exception_type, exception_string) { }

void ResultBuilder::setResult(const Result& res) {
m_decomp = res.m_decomp;
Expand All @@ -1769,11 +1784,11 @@ namespace detail {
if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional
m_failed = !m_threw;
} else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT
m_failed = !m_threw_as || (m_exception != m_exception_string);
m_failed = !m_threw_as || !m_exception_string.check(m_exception);
} else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional
m_failed = !m_threw_as;
} else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional
m_failed = m_exception != m_exception_string;
m_failed = !m_exception_string.check(m_exception);
} else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional
m_failed = m_threw;
}
Expand Down Expand Up @@ -2446,7 +2461,7 @@ namespace {
if(rb.m_at & assertType::is_throws_as)
xml.scopedElement("ExpectedException").writeText(rb.m_exception_type);
if(rb.m_at & assertType::is_throws_with)
xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string);
xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string.c_str());
if((rb.m_at & assertType::is_normal) && !rb.m_threw)
xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str());

Expand Down Expand Up @@ -2492,7 +2507,8 @@ namespace {
} else if((rb.m_at & assertType::is_throws_as) &&
(rb.m_at & assertType::is_throws_with)) { //!OCLINT
s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \""
<< rb.m_exception_string << "\", " << rb.m_exception_type << " ) " << Color::None;
<< rb.m_exception_string.c_str()
<< "\", " << rb.m_exception_type << " ) " << Color::None;
if(rb.m_threw) {
if(!rb.m_failed) {
s << "threw as expected!\n";
Expand All @@ -2513,7 +2529,8 @@ namespace {
} else if(rb.m_at &
assertType::is_throws_with) { //!OCLINT bitwise operator in conditional
s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \""
<< rb.m_exception_string << "\" ) " << Color::None
<< rb.m_exception_string.c_str()
<< "\" ) " << Color::None
<< (rb.m_threw ? (!rb.m_failed ? "threw as expected!" :
"threw a DIFFERENT exception: ") :
"did NOT throw at all!")
Expand Down
45 changes: 41 additions & 4 deletions doctest/parts/doctest_fwd.h
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,22 @@ DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs);

DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in);

class DOCTEST_INTERFACE Contains {
public:
explicit Contains(const String& string);

bool checkWith(const String& other) const;

String string;
};

DOCTEST_INTERFACE String toString(const Contains& in);

DOCTEST_INTERFACE bool operator==(const String& lhs, const Contains& rhs);
DOCTEST_INTERFACE bool operator==(const Contains& lhs, const String& rhs);
DOCTEST_INTERFACE bool operator!=(const String& lhs, const Contains& rhs);
DOCTEST_INTERFACE bool operator!=(const Contains& lhs, const String& rhs);

namespace Color {
enum Enum
{
Expand Down Expand Up @@ -741,9 +757,27 @@ struct DOCTEST_INTERFACE AssertData
String m_decomp;

// for specific exception-related asserts
bool m_threw_as;
const char* m_exception_type;
const char* m_exception_string;
bool m_threw_as;
const char* m_exception_type;
class DOCTEST_INTERFACE StringContains {
private:
Contains content;
bool isContains;

public:
StringContains() : content(String()), isContains(false) { }
StringContains(const String& str) : content(str), isContains(false) { }
StringContains(const Contains& cntn) : content(cntn), isContains(true) { }

bool check(const String& str) { return isContains ? (content == str) : (content.string == str); }

operator const String&() const { return content.string; }

const char* c_str() const { return content.string.c_str(); }
} m_exception_string;

AssertData(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const StringContains& exception_string);
};

struct DOCTEST_INTERFACE MessageData
Expand Down Expand Up @@ -1516,7 +1550,10 @@ DOCTEST_CLANG_SUPPRESS_WARNING_POP
struct DOCTEST_INTERFACE ResultBuilder : public AssertData
{
ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type = "", const char* exception_string = "");
const char* exception_type = "", const String& exception_string = "");

ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
const char* exception_type, const Contains& exception_string);

void setResult(const Result& res);

Expand Down
Loading

0 comments on commit 8de4cf7

Please sign in to comment.