Skip to content

Commit be805a8

Browse files
Add header providing timed asserts for unit tests
<level>_WITHIN asserts if the given condition becomes true within the given timeout. <level>_EDGE_WITHIN aserts if the given condition becomes true no sooner than time 1 but not after time 2. <level>_DONE_WITHIN asserts the execution time of the given expression is under the given duration. <level>_DONE_BETWEEN asserts the execution time of the given expression is between the given durations.
1 parent d799275 commit be805a8

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed

test/test-timedasserts.hpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
2+
3+
#pragma once
4+
5+
#include <BoostTestTargetConfig.h>
6+
#include <boost/test/test_tools.hpp>
7+
#include <chrono>
8+
#include <thread>
9+
10+
#define ASSERT_CONDITION_WITHIN_TIMEOUT(condition, timeout, level) \
11+
/* NOLINTNEXTLINE */ \
12+
do { \
13+
/* NOLINTNEXTLINE */ \
14+
auto pred = [&, this]() { return static_cast<bool>(condition); }; \
15+
BOOST_##level(AssertWithTimeout(pred, timeout, #condition)); \
16+
} while (0)
17+
18+
#define REQUIRE_WITHIN(condition, timeout) ASSERT_CONDITION_WITHIN_TIMEOUT(condition, timeout, REQUIRE)
19+
#define CHECK_WITHIN(condition, timeout) ASSERT_CONDITION_WITHIN_TIMEOUT(condition, timeout, CHECK)
20+
#define WARN_WITHIN(condition, timeout) ASSERT_CONDITION_WITHIN_TIMEOUT(condition, timeout, WARN)
21+
22+
#define ASSERT_CONDITION_EDGE_WITHIN_TIMEOUT(cond, time1, time2, level) \
23+
/* NOLINTNEXTLINE */ \
24+
do { \
25+
/* NOLINTNEXTLINE */ \
26+
auto pred = [&, this]() { return static_cast<bool>(cond); }; \
27+
BOOST_##level(AssertEdgeWithinTimeout(pred, time1, time2, #cond)); \
28+
} while (0)
29+
30+
#define REQUIRE_EDGE_WITHIN(cond, time1, time2) ASSERT_CONDITION_EDGE_WITHIN_TIMEOUT(cond, time1, time2, REQUIRE)
31+
#define CHECK_EDGE_WITHIN(cond, time1, time2) ASSERT_CONDITION_EDGE_WITHIN_TIMEOUT(cond, time1, time2, CHECK)
32+
#define WARN_EDGE_WITHIN(cond, time1, time2) ASSERT_CONDITION_EDGE_WITHIN_TIMEOUT(cond, time1, time2, WARN)
33+
34+
#define ASSERT_DONE_WITHIN_TIMEOUT(expr, time1, time2, level) \
35+
/* NOLINTNEXTLINE */ \
36+
do { \
37+
/* NOLINTNEXTLINE */ \
38+
auto task = [&, this]() { expr; }; \
39+
BOOST_##level(AssertDoneWithin(task, time1, time2, #expr)); \
40+
} while (0)
41+
42+
#define REQUIRE_DONE_BETWEEN(expr, time1, time2) ASSERT_DONE_WITHIN_TIMEOUT(expr, time1, time2, REQUIRE)
43+
#define CHECK_DONE_BETWEEN(expr, time1, time2) ASSERT_DONE_WITHIN_TIMEOUT(expr, time1, time2, CHECK)
44+
#define WARN_DONE_BETWEEN(expr, time1, time2) ASSERT_DONE_WITHIN_TIMEOUT(expr, time1, time2, WARN)
45+
46+
#define REQUIRE_DONE_WITHIN(expr, time1) ASSERT_DONE_WITHIN_TIMEOUT(expr, 0s, time1, REQUIRE)
47+
#define CHECK_DONE_WITHIN(expr, time1) ASSERT_DONE_WITHIN_TIMEOUT(expr, 0s, time1, CHECK)
48+
#define WARN_DONE_WITHIN(expr, time1) ASSERT_DONE_WITHIN_TIMEOUT(expr, 0s, time1, WARN)
49+
50+
namespace icinga {
51+
52+
using namespace std::chrono_literals;
53+
54+
/**
55+
* Assert that the predicate `fn` will switch from false to true in the given time window
56+
*
57+
* @param fn The predicate to check
58+
* @param timeout The duration in which the predicate is expected to return true
59+
* @param cond A string representing the condition for use in error messages
60+
*
61+
* @return a boost assertion result.
62+
*/
63+
template<class Rep, class Period>
64+
static boost::test_tools::assertion_result AssertWithTimeout(const std::function<bool()>& fn,
65+
const std::chrono::duration<Rep, Period>& timeout, std::string_view cond)
66+
{
67+
std::size_t iterations = timeout / 1ms;
68+
for (std::size_t i = 0; i < iterations && !fn(); i++) {
69+
std::this_thread::sleep_for(1ms);
70+
}
71+
boost::test_tools::assertion_result retVal{fn()};
72+
retVal.message() << "Condition (" << cond << ") not true within " << std::chrono::duration<double>(timeout).count()
73+
<< "s";
74+
return retVal;
75+
}
76+
77+
/**
78+
* Assert that the predicate `fn` will switch from false to true in the given time window
79+
*
80+
* @param fn The predicate to check
81+
* @param falseUntil The duration for which the predicate is expected to be false
82+
* @param trueWithin The duration in which the predicate is expected to return true
83+
* @param cond A string representing the condition for use in error messages
84+
*
85+
* @return a boost assertion result.
86+
*/
87+
template<class RepStart, class PeriodStart, class RepTimeout, class PeriodTimeout>
88+
static boost::test_tools::assertion_result AssertEdgeWithinTimeout(const std::function<bool()>& fn,
89+
const std::chrono::duration<RepStart, PeriodStart>& falseUntil,
90+
const std::chrono::duration<RepTimeout, PeriodTimeout>& trueWithin, std::string_view cond)
91+
{
92+
// TODO: just use ms
93+
std::size_t iterations = falseUntil / 1ms;
94+
for (std::size_t i = 0; i < iterations && !fn(); i++) {
95+
std::this_thread::sleep_for(1ms);
96+
}
97+
if (fn()) {
98+
boost::test_tools::assertion_result retVal{false};
99+
retVal.message() << "Condtion (" << cond << ") was true before "
100+
<< std::chrono::duration<double>(falseUntil).count() << "s";
101+
return retVal;
102+
}
103+
return AssertWithTimeout(fn, trueWithin, cond);
104+
}
105+
106+
/**
107+
* Assert that the given function takes a duration between lower and upper to complete.
108+
*
109+
* @param fn The function to execute
110+
* @param lower the lower bound to compare the duration against
111+
* @param upper the upper bound to compare the duration against
112+
*
113+
* @return a boost assertion result.
114+
*/
115+
template<class RepStart, class PeriodStart, class RepTimeout, class PeriodTimeout>
116+
static boost::test_tools::assertion_result AssertDoneWithin(const std::function<void()>& fn,
117+
const std::chrono::duration<RepStart, PeriodStart>& lower,
118+
const std::chrono::duration<RepTimeout, PeriodTimeout>& upper, std::string_view fnString)
119+
{
120+
auto start = std::chrono::steady_clock::now();
121+
fn();
122+
auto duration = std::chrono::steady_clock::now() - start;
123+
boost::test_tools::assertion_result retVal{duration > lower && duration < upper};
124+
retVal.message() << fnString << " took " << std::chrono::duration<double>(duration).count() << "s";
125+
return retVal;
126+
}
127+
128+
} // namespace icinga

0 commit comments

Comments
 (0)