Skip to content

Commit f0e8191

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 5f3a8c6 commit f0e8191

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed

test/test-timedasserts.hpp

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
static boost::test_tools::assertion_result AssertWithTimeout(
64+
const std::function<bool()>& fn,
65+
const std::chrono::duration<double>& timeout,
66+
std::string_view cond
67+
)
68+
{
69+
std::size_t iterations = timeout / 1ms;
70+
auto stepDur = timeout / iterations;
71+
for (std::size_t i = 0; i < iterations && !fn(); i++) {
72+
std::this_thread::sleep_for(stepDur);
73+
}
74+
boost::test_tools::assertion_result retVal{fn()};
75+
retVal.message() << "Condition (" << cond << ") not true within " << timeout.count() << "s";
76+
return retVal;
77+
}
78+
79+
/**
80+
* Assert that the predicate `fn` will switch from false to true in the given time window
81+
*
82+
* @param fn The predicate to check
83+
* @param falseUntil The duration for which the predicate is expected to be false
84+
* @param trueWithin The duration in which the predicate is expected to return true
85+
* @param cond A string representing the condition for use in error messages
86+
*
87+
* @return a boost assertion result.
88+
*/
89+
static boost::test_tools::assertion_result AssertEdgeWithinTimeout(
90+
const std::function<bool()>& fn,
91+
const std::chrono::duration<double>& falseUntil,
92+
const std::chrono::duration<double>& trueWithin,
93+
std::string_view cond
94+
)
95+
{
96+
std::size_t iterations = falseUntil / 1ms;
97+
auto stepDur = falseUntil / iterations;
98+
for (std::size_t i = 0; i < iterations && !fn(); i++) {
99+
std::this_thread::sleep_for(stepDur);
100+
}
101+
if (fn()) {
102+
boost::test_tools::assertion_result retVal{false};
103+
retVal.message() << "Condition (" << cond << ") was true before " << falseUntil.count() << "s";
104+
return retVal;
105+
}
106+
return AssertWithTimeout(fn, trueWithin, cond);
107+
}
108+
109+
/**
110+
* Assert that the given function takes a duration between lower and upper to complete.
111+
*
112+
* @param fn The function to execute
113+
* @param lower the lower bound to compare the duration against
114+
* @param upper the upper bound to compare the duration against
115+
*
116+
* @return a boost assertion result.
117+
*/
118+
template<class RepStart, class PeriodStart, class RepTimeout, class PeriodTimeout>
119+
static boost::test_tools::assertion_result AssertDoneWithin(
120+
const std::function<void()>& fn,
121+
const std::chrono::duration<RepStart, PeriodStart>& lower,
122+
const std::chrono::duration<RepTimeout, PeriodTimeout>& upper,
123+
std::string_view fnString
124+
)
125+
{
126+
auto start = std::chrono::steady_clock::now();
127+
fn();
128+
auto duration = std::chrono::steady_clock::now() - start;
129+
boost::test_tools::assertion_result retVal{duration > lower && duration < upper};
130+
retVal.message() << fnString << " took " << std::chrono::duration<double>(duration).count() << "s";
131+
return retVal;
132+
}
133+
134+
} // namespace icinga

0 commit comments

Comments
 (0)