Commit 5d233fae authored by Dominik Charousset's avatar Dominik Charousset

Fix and improve some issues with the testing DSL

parent 4bbbb5a2
...@@ -5,10 +5,34 @@ is based on [Keep a Changelog](https://keepachangelog.com). ...@@ -5,10 +5,34 @@ is based on [Keep a Changelog](https://keepachangelog.com).
## [Unreleased] ## [Unreleased]
### Added
- When adding CAF with exceptions enabled (default), the unit test framework now
offers new check macros:
- `CAF_CHECK_NOTHROW(expr)`
- `CAF_CHECK_THROWS_AS(expr, type)`
- `CAF_CHECK_THROWS_WITH(expr, str)`
- `CAF_CHECK_THROWS_WITH_AS(expr, str, type)`
### Fixed
- The DSL function `run_until` miscounted the number executed events, also
causing `run_once` to report a wrong value. Both functions now return the
correct result.
- Using `allow(...).with(...)` in unit tests without a matching message crashed
the program. By adding a missing NULL-check, `allow` is now always safe to
use.
### Changed ### Changed
- Since support of Qt 5 expired, we have ported the Qt examples to version 6. - Since support of Qt 5 expired, we have ported the Qt examples to version 6.
Hence, building the Qt examples now requires Qt in version 6. Hence, building the Qt examples now requires Qt in version 6.
- When compiling CAF with exceptions enabled (default), `REQUIRE*` macros,
`expect` and `disallow` no longer call `abort()`. Instead, they throw an
exception that only stops the current test instead of stopping the entire test
program.
- Reporting of several unit test macros has been improved to show correct line
numbers and provide better diagnostic of test errors.
## [0.18.5] - 2021-07-16 ## [0.18.5] - 2021-07-16
......
...@@ -270,6 +270,7 @@ caf_add_component( ...@@ -270,6 +270,7 @@ caf_add_component(
detail.unique_function detail.unique_function
detail.unordered_flat_map detail.unordered_flat_map
dictionary dictionary
dsl
dynamic_spawn dynamic_spawn
error error
expected expected
......
...@@ -46,14 +46,18 @@ public: ...@@ -46,14 +46,18 @@ public:
} }
/// Peeks into the mailbox of `next_job<scheduled_actor>()`. /// Peeks into the mailbox of `next_job<scheduled_actor>()`.
template <class T> template <class... Ts>
const T& peek() { decltype(auto) peek() {
auto ptr = next_job<scheduled_actor>().mailbox().peek(); auto ptr = next_job<scheduled_actor>().mailbox().peek();
CAF_ASSERT(ptr != nullptr); CAF_ASSERT(ptr != nullptr);
auto view = make_typed_message_view<T>(ptr->content()); if (auto view = make_const_typed_message_view<Ts...>(ptr->payload)) {
if (!view) if constexpr (sizeof...(Ts) == 1)
CAF_RAISE_ERROR("Mailbox element does not match T."); return get<0>(view);
return get<0>(view); else
return to_tuple(view);
} else {
CAF_RAISE_ERROR("Mailbox element does not match.");
}
} }
/// Puts `x` at the front of the queue unless it cannot be found in the queue. /// Puts `x` at the front of the queue unless it cannot be found in the queue.
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
// This test suite checks the testing DSL itself.
#define CAF_SUITE dsl
#include "core-test.hpp"
using namespace caf;
namespace {
struct testee_state {
behavior make_behavior() {
return {
[](add_atom, int32_t x, int32_t y) { return x + y; },
[](sub_atom, int32_t x, int32_t y) { return x - y; },
};
}
};
using testee_actor = stateful_actor<testee_state>;
struct bool_list {
std::vector<bool> values;
auto begin() {
return values.begin();
}
auto end() {
return values.end();
}
};
bool_list& operator<<(bool_list& ls, bool value) {
ls.values.push_back(value);
return ls;
}
} // namespace
BEGIN_FIXTURE_SCOPE(test_coordinator_fixture<>)
SCENARIO("successful checks increment the 'good' counter") {
GIVEN("a unit test") {
auto this_test = test::engine::current_test();
WHEN("using any CHECK macro with a true statement") {
THEN("the 'good' counter increments by one per check") {
bool_list results;
results << CHECK_EQ(this_test->good(), 0u);
results << CHECK_EQ(this_test->good(), 1u);
results << CHECK_EQ(this_test->good(), 2u);
results << CHECK(true);
results << CHECK_NE(1, 2);
results << CHECK_LT(1, 2);
results << CHECK_LE(1, 2);
results << CHECK_GT(2, 1);
results << CHECK_GE(2, 1);
results << CHECK_EQ(this_test->good(), 9u);
results << CHECK_EQ(this_test->bad(), 0u);
auto is_true = [](bool x) { return x; };
CHECK(std::all_of(results.begin(), results.end(), is_true));
}
}
}
}
SCENARIO("unsuccessful checks increment the 'bad' counter") {
GIVEN("a unit test") {
auto this_test = test::engine::current_test();
WHEN("using any CHECK macro with a false statement") {
THEN("the 'bad' counter increments by one per check") {
// Run the known-to-fail tests with suppressed output.
auto bk = caf::test::logger::instance().make_quiet();
bool_list results;
results << CHECK_EQ(this_test->good(), 1u);
results << CHECK(false);
results << CHECK_NE(1, 1);
results << CHECK_LT(2, 1);
results << CHECK_LE(2, 1);
results << CHECK_GT(1, 2);
results << CHECK_GE(1, 2);
caf::test::logger::instance().levels(bk);
auto failed_checks = this_test->bad();
this_test->reset(); // Prevent the unit test from actually failing.
CHECK_EQ(failed_checks, 7u);
REQUIRE_EQ(results.values.size(), 7u);
auto is_false = [](bool x) { return !x; };
CHECK(std::all_of(results.begin(), results.end(), is_false));
}
}
}
}
SCENARIO("the test scheduler allows manipulating the control flow") {
GIVEN("some event-based actors") {
auto aut1 = sys.spawn<testee_actor>();
auto aut2 = sys.spawn<testee_actor>();
auto aut3 = sys.spawn<testee_actor>();
run();
WHEN("sending messages to event-based actors") {
THEN("the actors become jobs in the scheduler") {
CHECK(!sched.has_job());
self->send(aut1, add_atom_v, 1, 2);
CHECK(sched.has_job());
CHECK_EQ(sched.jobs.size(), 1u);
self->send(aut2, add_atom_v, 2, 3);
CHECK_EQ(sched.jobs.size(), 2u);
self->send(aut3, add_atom_v, 3, 4);
CHECK_EQ(sched.jobs.size(), 3u);
}
AND("prioritize allows moving actors to the head of the job queue") {
auto ptr = [](const actor& hdl) {
auto actor_ptr = actor_cast<caf::abstract_actor*>(hdl);
return dynamic_cast<resumable*>(actor_ptr);
};
CHECK_EQ(&sched.next_job(), ptr(aut1));
CHECK(sched.prioritize(aut2));
CHECK_EQ(&sched.next_job(), ptr(aut2));
CHECK(sched.prioritize(aut3));
CHECK_EQ(&sched.next_job(), ptr(aut3));
CHECK(sched.prioritize(aut1));
CHECK_EQ(&sched.next_job(), ptr(aut1));
}
AND("peek allows inspecting the mailbox of the next job") {
using std::make_tuple;
auto peek = [this] { return sched.peek<add_atom, int, int>(); };
CHECK_EQ(peek(), make_tuple(add_atom_v, 1, 2));
CHECK(sched.prioritize(aut2));
CHECK_EQ(peek(), make_tuple(add_atom_v, 2, 3));
CHECK(sched.prioritize(aut3));
CHECK_EQ(peek(), make_tuple(add_atom_v, 3, 4));
}
AND("run_until and run_once allows executing jobs selectively") {
CHECK_EQ(run_until([this] { return sched.jobs.size() == 1; }), 2u);
CHECK(run_once());
CHECK(!sched.has_job());
CHECK(!run_once());
}
}
}
}
SCENARIO("allow() turns into a no-op on mismatch") {
GIVEN("an event-based actor") {
auto aut = sys.spawn<testee_actor>();
run();
WHEN("allow()-ing a message if no message is waiting in any mailbox") {
THEN("allow() becomes a no-op and returns false") {
CHECK(!(allow((add_atom, int32_t, int32_t), from(self).to(aut))));
CHECK(!(allow((add_atom, int32_t, int32_t),
from(self).to(aut).with(_, _, _))));
}
}
WHEN("allow()-ing a message but a different message is waiting") {
THEN("allow() becomes a no-op and returns false") {
self->send(aut, sub_atom_v, 4, 3);
auto fake_sender = sys.spawn<testee_actor>();
// Wrong type.
CHECK(!(allow((add_atom, int32_t, int32_t), from(self).to(aut))));
// Wrong type plus .with() check.
CHECK(!(allow((add_atom, int32_t, int32_t),
from(self).to(aut).with(_, 4, 3))));
// Correct type but .with() check expects different values.
CHECK(!(allow((sub_atom, int32_t, int32_t),
from(self).to(aut).with(_, 1, 2))));
// Correct type and matching .with() but wrong sender.
CHECK(!(allow((sub_atom, int32_t, int32_t),
from(fake_sender).to(aut).with(_, 4, 3))));
// Message must still wait in the mailbox.
auto& aut_dref = sched.next_job<testee_actor>();
REQUIRE_EQ(actor_cast<abstract_actor*>(aut), &aut_dref);
auto msg_ptr = aut_dref.peek_at_next_mailbox_element();
REQUIRE_NE(msg_ptr, nullptr);
CHECK(msg_ptr->payload.matches(sub_atom_v, 4, 3));
// Drop test message.
run();
while (!self->mailbox().empty())
self->dequeue();
}
}
WHEN("allow()-ing and a matching message arrives") {
THEN("the actor processes the message and allow() returns true") {
self->send(aut, sub_atom_v, 4, 3);
CHECK(sched.has_job());
CHECK(allow((sub_atom, int32_t, int32_t),
from(self).to(aut).with(_, 4, 3)));
CHECK(!sched.has_job());
CHECK(allow((int32_t), from(aut).to(self).with(1)));
}
}
}
}
#ifdef CAF_ENABLE_EXCEPTIONS
SCENARIO("tests may check for exceptions") {
GIVEN("a unit test") {
auto this_test = test::engine::current_test();
WHEN("using any CHECK_THROWS macro with a matching exception") {
THEN("the 'good' counter increments by one per check") {
auto f = [] { throw std::runtime_error("foo"); };
CHECK_THROWS_AS(f(), std::runtime_error);
CHECK_THROWS_WITH(f(), "foo");
CHECK_THROWS_WITH_AS(f(), "foo", std::runtime_error);
CHECK_NOTHROW([] {}());
}
}
WHEN("using any CHECK_THROWS macro with a unexpected exception") {
THEN("the 'bad' counter increments by one per check") {
auto f = [] { throw std::logic_error("bar"); };
auto bk = caf::test::logger::instance().make_quiet();
CHECK_THROWS_AS(f(), std::runtime_error);
CHECK_THROWS_WITH(f(), "foo");
CHECK_THROWS_WITH_AS(f(), "foo", std::runtime_error);
CHECK_THROWS_WITH_AS(f(), "foo", std::logic_error);
CHECK_NOTHROW(f());
caf::test::logger::instance().levels(bk);
CHECK_EQ(this_test->bad(), 5u);
this_test->reset_bad();
}
}
}
}
SCENARIO("passing requirements increment the 'good' counter") {
GIVEN("a unit test") {
auto this_test = test::engine::current_test();
WHEN("using any REQUIRE macro with a true statement") {
THEN("the 'good' counter increments by one per requirement") {
REQUIRE_EQ(this_test->good(), 0u);
REQUIRE_EQ(this_test->good(), 1u);
REQUIRE_EQ(this_test->good(), 2u);
REQUIRE(true);
REQUIRE_NE(1, 2);
REQUIRE_LT(1, 2);
REQUIRE_LE(1, 2);
REQUIRE_GT(2, 1);
REQUIRE_GE(2, 1);
REQUIRE_EQ(this_test->good(), 9u);
REQUIRE_EQ(this_test->bad(), 0u);
}
}
}
}
# define CHECK_FAILS(expr) \
if (true) { \
auto silent_expr = [&] { \
auto bk = caf::test::logger::instance().make_quiet(); \
auto guard = caf::detail::make_scope_guard( \
[bk] { caf::test::logger::instance().levels(bk); }); \
expr; \
}; \
CHECK_THROWS_AS(silent_expr(), test::requirement_error); \
} \
if (CHECK_EQ(this_test->bad(), 1u)) \
this_test->reset_bad()
SCENARIO("failing requirements increment the 'bad' counter and throw") {
GIVEN("a unit test") {
auto this_test = test::engine::current_test();
WHEN("using any REQUIRE macro with a true statement") {
THEN("the 'good' counter increments by one per requirement") {
CHECK_FAILS(REQUIRE_EQ(1, 2));
CHECK_FAILS(REQUIRE_EQ(this_test->good(), 42u));
CHECK_FAILS(REQUIRE(false));
CHECK_FAILS(REQUIRE_NE(1, 1));
CHECK_FAILS(REQUIRE_LT(2, 1));
CHECK_FAILS(REQUIRE_LE(2, 1));
CHECK_FAILS(REQUIRE_GT(1, 2));
CHECK_FAILS(REQUIRE_GE(1, 2));
}
}
}
}
SCENARIO("disallow() throws when finding a prohibited message") {
GIVEN("an event-based actor") {
auto aut = sys.spawn<testee_actor>();
run();
WHEN("disallow()-ing a message if no message is waiting in any mailbox") {
THEN("disallow() becomes a no-op") {
CHECK_NOTHROW(disallow((add_atom, int32_t, int32_t), //
from(self).to(aut)));
CHECK_NOTHROW(disallow((add_atom, int32_t, int32_t),
from(self).to(aut).with(_, _, _)));
}
}
WHEN("disallow()-ing a message if no matching message exists") {
THEN("disallow() becomes a no-op") {
self->send(aut, sub_atom_v, 4, 3);
auto fake_sender = sys.spawn<testee_actor>();
CHECK_NOTHROW(disallow((add_atom, int32_t, int32_t), to(aut)));
CHECK_NOTHROW(disallow((add_atom, int32_t, int32_t), //
to(aut).with(_, _, _)));
CHECK_NOTHROW(disallow((add_atom, int32_t, int32_t), //
from(self).to(aut)));
CHECK_NOTHROW(disallow((add_atom, int32_t, int32_t),
from(self).to(aut).with(_, _, _)));
CHECK_NOTHROW(disallow((sub_atom, int32_t, int32_t),
from(self).to(aut).with(_, 1, 2)));
CHECK_NOTHROW(disallow((sub_atom, int32_t, int32_t), //
from(fake_sender).to(aut)));
CHECK_NOTHROW(disallow((sub_atom, int32_t, int32_t), //
from(fake_sender).to(aut).with(_, 4, 3)));
// Drop test message.
run();
while (!self->mailbox().empty())
self->dequeue();
}
}
WHEN("disallow()-ing an existing message") {
THEN("disallow() throws and increment the 'bad' counter") {
auto this_test = test::engine::current_test();
self->send(aut, sub_atom_v, 4, 3);
CHECK_FAILS(disallow((sub_atom, int32_t, int32_t), to(aut)));
CHECK_FAILS(disallow((sub_atom, int32_t, int32_t), //
to(aut).with(_, _, _)));
CHECK_FAILS(disallow((sub_atom, int32_t, int32_t), from(self).to(aut)));
CHECK_FAILS(disallow((sub_atom, int32_t, int32_t), //
from(self).to(aut).with(_, _, _)));
}
}
}
}
SCENARIO("expect() throws when not finding the required message") {
GIVEN("an event-based actor") {
auto this_test = test::engine::current_test();
auto aut = sys.spawn<testee_actor>();
run();
WHEN("expect()-ing a message if no message is waiting in any mailbox") {
THEN("expect() throws and increment the 'bad' counter") {
CHECK_FAILS(expect((add_atom, int32_t, int32_t), to(aut)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t), //
to(aut).with(_, _, _)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t), //
from(self).to(aut)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t),
from(self).to(aut).with(_, _, _)));
auto aut2 = sys.spawn<testee_actor>();
CHECK_FAILS(expect((add_atom, int32_t, int32_t), to(aut2)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t), //
to(aut2).with(_, _, _)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t), //
from(self).to(aut2)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t),
from(self).to(aut2).with(_, _, _)));
}
}
WHEN("expect()-ing a message if no matching message exists") {
THEN("expect() throws and increment the 'bad' counter") {
self->send(aut, sub_atom_v, 4, 3);
auto fake_sender = sys.spawn<testee_actor>();
CHECK_FAILS(expect((add_atom, int32_t, int32_t), to(aut)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t), //
to(aut).with(_, _, _)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t), //
from(self).to(aut)));
CHECK_FAILS(expect((add_atom, int32_t, int32_t),
from(self).to(aut).with(_, _, _)));
CHECK_FAILS(expect((sub_atom, int32_t, int32_t),
from(self).to(aut).with(_, 1, 2)));
CHECK_FAILS(expect((sub_atom, int32_t, int32_t), //
from(fake_sender).to(aut)));
CHECK_FAILS(expect((sub_atom, int32_t, int32_t), //
from(fake_sender).to(aut).with(_, 4, 3)));
// Drop test message.
run();
while (!self->mailbox().empty())
self->dequeue();
}
}
WHEN("expect()-ing an existing message") {
THEN("expect() processes the message") {
self->send(aut, add_atom_v, 4, 3);
CHECK_NOTHROW(expect((add_atom, int32_t, int32_t), to(aut)));
CHECK_NOTHROW(expect((int32_t), to(self)));
self->send(aut, add_atom_v, 4, 3);
CHECK_NOTHROW(expect((add_atom, int32_t, int32_t), //
to(aut).with(_, _, _)));
CHECK_NOTHROW(expect((int32_t), to(self).with(7)));
self->send(aut, add_atom_v, 4, 3);
CHECK_NOTHROW(expect((add_atom, int32_t, int32_t), from(self).to(aut)));
CHECK_NOTHROW(expect((int32_t), from(aut).to(self)));
self->send(aut, add_atom_v, 4, 3);
CHECK_NOTHROW(expect((add_atom, int32_t, int32_t), //
from(self).to(aut).with(_, _, _)));
CHECK_NOTHROW(expect((int32_t), from(aut).to(self).with(7)));
}
}
}
}
#endif
END_FIXTURE_SCOPE()
...@@ -37,6 +37,16 @@ ...@@ -37,6 +37,16 @@
#define CHECK_GT(lhs, rhs) CAF_CHECK_GREATER(lhs, rhs) #define CHECK_GT(lhs, rhs) CAF_CHECK_GREATER(lhs, rhs)
#define CHECK_GE(lhs, rhs) CAF_CHECK_GREATER_OR_EQUAL(lhs, rhs) #define CHECK_GE(lhs, rhs) CAF_CHECK_GREATER_OR_EQUAL(lhs, rhs)
#ifdef CAF_ENABLE_EXCEPTIONS
# define CHECK_NOTHROW(expr) CAF_CHECK_NOTHROW(expr)
# define CHECK_THROWS_AS(expr, type) CAF_CHECK_THROWS_AS(expr, type)
# define CHECK_THROWS_WITH(expr, msg) CAF_CHECK_THROWS_WITH(expr, msg)
# define CHECK_THROWS_WITH_AS(expr, msg, type) \
CAF_CHECK_THROWS_WITH_AS(expr, msg, type)
#endif // CAF_ENABLE_EXCEPTIONS
#define REQUIRE(what) CAF_REQUIRE(what) #define REQUIRE(what) CAF_REQUIRE(what)
#define REQUIRE_EQ(lhs, rhs) CAF_REQUIRE_EQUAL(lhs, rhs) #define REQUIRE_EQ(lhs, rhs) CAF_REQUIRE_EQUAL(lhs, rhs)
#define REQUIRE_NE(lhs, rhs) CAF_REQUIRE_NOT_EQUAL(lhs, rhs) #define REQUIRE_NE(lhs, rhs) CAF_REQUIRE_NOT_EQUAL(lhs, rhs)
......
...@@ -86,48 +86,42 @@ T get(const has_outer_type<U>&); ...@@ -86,48 +86,42 @@ T get(const has_outer_type<U>&);
template <class T, class U> template <class T, class U>
bool is(const has_outer_type<U>&); bool is(const has_outer_type<U>&);
template <class Tup> template <class Tuple>
class elementwise_compare_inspector { class elementwise_compare_inspector {
public: public:
using result_type = bool; using result_type = bool;
template <size_t X> static constexpr size_t num_values = std::tuple_size_v<Tuple>;
using pos = std::integral_constant<size_t, X>;
explicit elementwise_compare_inspector(const Tup& xs) : xs_(xs) { explicit elementwise_compare_inspector(const Tuple& xs) : xs_(xs) {
// nop // nop
} }
template <class... Ts> template <class... Ts>
bool operator()(const Ts&... xs) { bool operator()(const Ts&... xs) const {
return iterate(pos<0>{}, xs...); static_assert(sizeof...(Ts) == num_values);
return apply(std::forward_as_tuple(xs...),
std::index_sequence_for<Ts...>{});
} }
private: private:
template <size_t X> template <class OtherTuple, size_t... Is>
bool iterate(pos<X>) { bool apply(OtherTuple values, std::index_sequence<Is...>) const {
// end of recursion using std::get;
return true; return (check(get<Is>(values), get<Is>(xs_)) && ...);
}
template <size_t X, class T, class... Ts>
bool iterate(pos<X>, const T& y, const Ts&... ys) {
std::integral_constant<size_t, X + 1> next;
check(y, get<X>(xs_));
return iterate(next, ys...);
} }
template <class T, class U> template <class T, class U>
static void check(const T& x, const U& y) { static bool check(const T& x, const U& y) {
CAF_CHECK_EQUAL(x, y); return x == y;
} }
template <class T> template <class T>
static void check(const T&, const wildcard&) { static bool check(const T&, const wildcard&) {
// nop return true;
} }
const Tup& xs_; const Tuple& xs_;
}; };
// -- unified access to all actor handles in CAF ------------------------------- // -- unified access to all actor handles in CAF -------------------------------
...@@ -257,16 +251,18 @@ caf::optional<std::tuple<T, Ts...>> try_extract(caf_handle x) { ...@@ -257,16 +251,18 @@ caf::optional<std::tuple<T, Ts...>> try_extract(caf_handle x) {
/// the mailbox. Fails on an empty mailbox or if the content of the next /// the mailbox. Fails on an empty mailbox or if the content of the next
/// element does not match `<T, Ts...>`. /// element does not match `<T, Ts...>`.
template <class T, class... Ts> template <class T, class... Ts>
std::tuple<T, Ts...> extract(caf_handle x) { std::tuple<T, Ts...> extract(caf_handle x, int src_line) {
auto result = try_extract<T, Ts...>(x); if (auto result = try_extract<T, Ts...>(x); result != caf::none) {
if (result == caf::none) { return std::move(*result);
} else {
auto ptr = x->peek_at_next_mailbox_element(); auto ptr = x->peek_at_next_mailbox_element();
if (ptr == nullptr) if (ptr == nullptr)
CAF_FAIL("Mailbox is empty"); CAF_FAIL("cannot peek at the next message: mailbox is empty", src_line);
CAF_FAIL( else
"Message does not match expected pattern: " << to_string(ptr->content())); CAF_FAIL("message does not match expected types: "
<< to_string(ptr->content()),
src_line);
} }
return std::move(*result);
} }
template <class T, class... Ts> template <class T, class... Ts>
...@@ -277,22 +273,25 @@ bool received(caf_handle x) { ...@@ -277,22 +273,25 @@ bool received(caf_handle x) {
template <class... Ts> template <class... Ts>
class expect_clause { class expect_clause {
public: public:
explicit expect_clause(caf::scheduler::test_coordinator& sched) explicit expect_clause(caf::scheduler::test_coordinator& sched, int src_line)
: sched_(sched), dest_(nullptr) { : sched_(sched), src_line_(src_line) {
peek_ = [=] { peek_ = [this] {
/// The extractor will call CAF_FAIL on a type mismatch, essentially /// The extractor will call CAF_FAIL on a type mismatch, essentially
/// performing a type check when ignoring the result. /// performing a type check when ignoring the result.
extract<Ts...>(dest_); extract<Ts...>(dest_, src_line_);
}; };
} }
expect_clause(expect_clause&& other) = default; expect_clause(expect_clause&& other) = default;
~expect_clause() { void eval(const char* type_str, const char* fields_str) {
if (peek_ != nullptr) { using namespace caf;
peek_(); test::logger::instance().verbose()
run_once(); << term::yellow << " -> " << term::reset
} << test::logger::stream::reset_flags_t{} << "expect " << type_str << "."
<< fields_str << " [line " << src_line_ << "]\n";
peek_();
run_once();
} }
expect_clause& from(const wildcard&) { expect_clause& from(const wildcard&) {
...@@ -307,12 +306,15 @@ public: ...@@ -307,12 +306,15 @@ public:
template <class Handle> template <class Handle>
expect_clause& to(const Handle& whom) { expect_clause& to(const Handle& whom) {
CAF_REQUIRE(sched_.prioritize(whom)); if (!sched_.prioritize(whom))
CAF_FAIL("there is no message for the designated receiver", src_line_);
dest_ = &sched_.next_job<caf::abstract_actor>(); dest_ = &sched_.next_job<caf::abstract_actor>();
auto ptr = dest_->peek_at_next_mailbox_element(); auto ptr = dest_->peek_at_next_mailbox_element();
CAF_REQUIRE(ptr != nullptr); if (ptr == nullptr)
if (src_) CAF_FAIL("the designated receiver has no message in its mailbox",
CAF_REQUIRE_EQUAL(ptr->sender, src_); src_line_);
if (src_ && ptr->sender != src_)
CAF_FAIL("the found message is not from the expected sender", src_line_);
return *this; return *this;
} }
...@@ -322,14 +324,17 @@ public: ...@@ -322,14 +324,17 @@ public:
} }
template <class... Us> template <class... Us>
void with(Us&&... xs) { expect_clause& with(Us&&... xs) {
peek_ = [this, tmp = std::make_tuple(std::forward<Us>(xs)...)] { peek_ = [this, tmp = std::make_tuple(std::forward<Us>(xs)...)] {
using namespace caf::detail; auto inspector = elementwise_compare_inspector<decltype(tmp)>{tmp};
elementwise_compare_inspector<decltype(tmp)> inspector{tmp}; auto content = extract<Ts...>(dest_, src_line_);
auto ys = extract<Ts...>(dest_); if (!std::apply(inspector, content))
auto ys_indices = get_indices(ys); CAF_FAIL("message does not match expected content: "
CAF_REQUIRE(apply_args(inspector, ys_indices, ys)); << caf::deep_to_string(tmp) << " vs "
<< caf::deep_to_string(content),
src_line_);
}; };
return *this;
} }
protected: protected:
...@@ -343,26 +348,33 @@ protected: ...@@ -343,26 +348,33 @@ protected:
caf::scheduler::test_coordinator& sched_; caf::scheduler::test_coordinator& sched_;
caf::strong_actor_ptr src_; caf::strong_actor_ptr src_;
caf::abstract_actor* dest_; caf::abstract_actor* dest_ = nullptr;
std::function<void()> peek_; std::function<void()> peek_;
int src_line_;
}; };
template <> template <>
class expect_clause<void> { class expect_clause<void> {
public: public:
explicit expect_clause(caf::scheduler::test_coordinator& sched) explicit expect_clause(caf::scheduler::test_coordinator& sched, int src_line)
: sched_(sched), dest_(nullptr) { : sched_(sched), src_line_(src_line) {
// nop // nop
} }
expect_clause(expect_clause&& other) = default; expect_clause(expect_clause&& other) = default;
~expect_clause() { void eval(const char* type_str, const char* fields_str) {
using namespace caf;
test::logger::instance().verbose()
<< term::yellow << " -> " << term::reset
<< test::logger::stream::reset_flags_t{} << "expect(void)." << fields_str
<< " [line " << src_line_ << "]\n";
auto ptr = dest_->peek_at_next_mailbox_element(); auto ptr = dest_->peek_at_next_mailbox_element();
if (ptr == nullptr) if (ptr == nullptr)
CAF_FAIL("no message found"); CAF_FAIL("no message found", src_line_);
if (!ptr->content().empty()) if (!ptr->content().empty())
CAF_FAIL("non-empty message found: " << to_string(ptr->content())); CAF_FAIL("non-empty message found: " << to_string(ptr->content()),
src_line_);
run_once(); run_once();
} }
...@@ -403,14 +415,15 @@ protected: ...@@ -403,14 +415,15 @@ protected:
caf::scheduler::test_coordinator& sched_; caf::scheduler::test_coordinator& sched_;
caf::strong_actor_ptr src_; caf::strong_actor_ptr src_;
caf::abstract_actor* dest_; caf::abstract_actor* dest_ = nullptr;
int src_line_;
}; };
template <class... Ts> template <class... Ts>
class inject_clause { class inject_clause {
public: public:
explicit inject_clause(caf::scheduler::test_coordinator& sched) explicit inject_clause(caf::scheduler::test_coordinator& sched, int src_line)
: sched_(sched), dest_(nullptr) { : sched_(sched), src_line_(src_line) {
// nop // nop
} }
...@@ -428,27 +441,39 @@ public: ...@@ -428,27 +441,39 @@ public:
return *this; return *this;
} }
void with(Ts... xs) { inject_clause& with(Ts... xs) {
msg_ = caf::make_message(std::move(xs)...);
return *this;
}
void eval(const char* type_str, const char* fields_str) {
using namespace caf;
test::logger::instance().verbose()
<< term::yellow << " -> " << term::reset
<< test::logger::stream::reset_flags_t{} << "inject" << type_str << "."
<< fields_str << " [line " << src_line_ << "]\n";
if (dest_ == nullptr) if (dest_ == nullptr)
CAF_FAIL("missing .to() in inject() statement"); CAF_FAIL("missing .to() in inject() statement", src_line_);
auto msg = caf::make_message(std::move(xs)...); else if (src_ == nullptr)
if (src_ == nullptr) caf::anon_send(caf::actor_cast<caf::actor>(dest_), msg_);
caf::anon_send(caf::actor_cast<caf::actor>(dest_), msg);
else else
caf::send_as(caf::actor_cast<caf::actor>(src_), caf::send_as(caf::actor_cast<caf::actor>(src_),
caf::actor_cast<caf::actor>(dest_), msg); caf::actor_cast<caf::actor>(dest_), msg_);
if (!sched_.prioritize(dest_)) if (!sched_.prioritize(dest_))
CAF_FAIL("inject: failed to schedule destination actor"); CAF_FAIL("inject: failed to schedule destination actor", src_line_);
auto dest_ptr = &sched_.next_job<caf::abstract_actor>(); auto dest_ptr = &sched_.next_job<caf::abstract_actor>();
auto ptr = dest_ptr->peek_at_next_mailbox_element(); auto ptr = dest_ptr->peek_at_next_mailbox_element();
if (ptr == nullptr) if (ptr == nullptr)
CAF_FAIL("inject: failed to get next message from destination actor"); CAF_FAIL("inject: failed to get next message from destination actor",
src_line_);
if (ptr->sender != src_) if (ptr->sender != src_)
CAF_FAIL("inject: found unexpected sender for the next message"); CAF_FAIL("inject: found unexpected sender for the next message",
if (ptr->payload.cptr() != msg.cptr()) src_line_);
if (ptr->payload.cptr() != msg_.cptr())
CAF_FAIL("inject: found unexpected message => " << ptr->payload << " !! " CAF_FAIL("inject: found unexpected message => " << ptr->payload << " !! "
<< msg); << msg_,
msg.reset(); // drop local reference before running the actor src_line_);
msg_.reset(); // drop local reference before running the actor
run_once(); run_once();
} }
...@@ -465,26 +490,25 @@ protected: ...@@ -465,26 +490,25 @@ protected:
caf::scheduler::test_coordinator& sched_; caf::scheduler::test_coordinator& sched_;
caf::strong_actor_ptr src_; caf::strong_actor_ptr src_;
caf::strong_actor_ptr dest_; caf::strong_actor_ptr dest_;
caf::message msg_;
int src_line_;
}; };
template <class... Ts> template <class... Ts>
class allow_clause { class allow_clause {
public: public:
explicit allow_clause(caf::scheduler::test_coordinator& sched) explicit allow_clause(caf::scheduler::test_coordinator& sched, int src_line)
: sched_(sched), dest_(nullptr) { : sched_(sched), src_line_(src_line) {
peek_ = [=] { peek_ = [this] {
if (dest_ != nullptr) if (dest_ != nullptr)
return try_extract<Ts...>(dest_) != caf::none; return try_extract<Ts...>(dest_) != caf::none;
return false; else
return false;
}; };
} }
allow_clause(allow_clause&& other) = default; allow_clause(allow_clause&& other) = default;
~allow_clause() {
eval();
}
allow_clause& from(const wildcard&) { allow_clause& from(const wildcard&) {
return *this; return *this;
} }
...@@ -499,31 +523,46 @@ public: ...@@ -499,31 +523,46 @@ public:
allow_clause& to(const Handle& whom) { allow_clause& to(const Handle& whom) {
if (sched_.prioritize(whom)) if (sched_.prioritize(whom))
dest_ = &sched_.next_job<caf::abstract_actor>(); dest_ = &sched_.next_job<caf::abstract_actor>();
else if (auto ptr = caf::actor_cast<caf::abstract_actor*>(whom))
dest_ = dynamic_cast<caf::blocking_actor*>(ptr);
return *this; return *this;
} }
template <class... Us> template <class... Us>
void with(Us&&... xs) { allow_clause& with(Us&&... xs) {
peek_ = [this, tmp = std::make_tuple(std::forward<Us>(xs)...)] { peek_ = [this, tmp = std::make_tuple(std::forward<Us>(xs)...)] {
using namespace caf::detail; using namespace caf::detail;
elementwise_compare_inspector<decltype(tmp)> inspector{tmp}; if (dest_ != nullptr) {
auto ys = try_extract<Ts...>(dest_); if (auto ys = try_extract<Ts...>(dest_); ys != caf::none) {
if (ys != caf::none) { elementwise_compare_inspector<decltype(tmp)> inspector{tmp};
auto ys_indices = get_indices(*ys); auto ys_indices = get_indices(*ys);
return apply_args(inspector, ys_indices, *ys); return apply_args(inspector, ys_indices, *ys);
}
} }
return false; return false;
}; };
return *this;
} }
bool eval() { bool eval(const char* type_str, const char* fields_str) {
if (peek_ != nullptr) { using namespace caf;
if (peek_()) { test::logger::instance().verbose()
run_once(); << term::yellow << " -> " << term::reset
return true; << test::logger::stream::reset_flags_t{} << "allow" << type_str << "."
} << fields_str << " [line " << src_line_ << "]\n";
if (!dest_) {
return false;
}
if (auto msg_ptr = dest_->peek_at_next_mailbox_element(); !msg_ptr) {
return false;
} else if (src_ && msg_ptr->sender != src_) {
return false;
} else if (peek_()) {
run_once();
return true;
} else {
return false;
} }
return false;
} }
protected: protected:
...@@ -537,15 +576,16 @@ protected: ...@@ -537,15 +576,16 @@ protected:
caf::scheduler::test_coordinator& sched_; caf::scheduler::test_coordinator& sched_;
caf::strong_actor_ptr src_; caf::strong_actor_ptr src_;
caf::abstract_actor* dest_; caf::abstract_actor* dest_ = nullptr;
std::function<bool()> peek_; std::function<bool()> peek_;
int src_line_;
}; };
template <class... Ts> template <class... Ts>
class disallow_clause { class disallow_clause {
public: public:
disallow_clause() { disallow_clause(int src_line) : src_line_(src_line) {
check_ = [=] { check_ = [this] {
auto ptr = dest_->peek_at_next_mailbox_element(); auto ptr = dest_->peek_at_next_mailbox_element();
if (ptr == nullptr) if (ptr == nullptr)
return; return;
...@@ -553,17 +593,13 @@ public: ...@@ -553,17 +593,13 @@ public:
return; return;
auto res = try_extract<Ts...>(dest_); auto res = try_extract<Ts...>(dest_);
if (res != caf::none) if (res != caf::none)
CAF_FAIL("received disallowed message: " << caf::deep_to_string(*ptr)); CAF_FAIL("received disallowed message: " << caf::deep_to_string(*ptr),
src_line_);
}; };
} }
disallow_clause(disallow_clause&& other) = default; disallow_clause(disallow_clause&& other) = default;
~disallow_clause() {
if (check_ != nullptr)
check_();
}
disallow_clause& from(const wildcard&) { disallow_clause& from(const wildcard&) {
return *this; return *this;
} }
...@@ -579,7 +615,7 @@ public: ...@@ -579,7 +615,7 @@ public:
} }
template <class... Us> template <class... Us>
void with(Us&&... xs) { disallow_clause& with(Us&&... xs) {
check_ = [this, tmp = std::make_tuple(std::forward<Us>(xs)...)] { check_ = [this, tmp = std::make_tuple(std::forward<Us>(xs)...)] {
auto ptr = dest_->peek_at_next_mailbox_element(); auto ptr = dest_->peek_at_next_mailbox_element();
if (ptr == nullptr) if (ptr == nullptr)
...@@ -593,15 +629,26 @@ public: ...@@ -593,15 +629,26 @@ public:
auto& ys = *res; auto& ys = *res;
auto ys_indices = get_indices(ys); auto ys_indices = get_indices(ys);
if (apply_args(inspector, ys_indices, ys)) if (apply_args(inspector, ys_indices, ys))
CAF_FAIL("received disallowed message: " << CAF_ARG(*res)); CAF_FAIL("received disallowed message: " << CAF_ARG(*res), src_line_);
} }
}; };
return *this;
}
void eval(const char* type_str, const char* fields_str) {
using namespace caf;
test::logger::instance().verbose()
<< term::yellow << " -> " << term::reset
<< test::logger::stream::reset_flags_t{} << "disallow" << type_str << "."
<< fields_str << " [line " << src_line_ << "]\n";
check_();
} }
protected: protected:
caf_handle src_; caf_handle src_;
caf_handle dest_; caf_handle dest_;
std::function<void()> check_; std::function<void()> check_;
int src_line_;
}; };
template <class... Ts> template <class... Ts>
...@@ -747,6 +794,7 @@ public: ...@@ -747,6 +794,7 @@ public:
size_t progress = 0; size_t progress = 0;
while (consume_message()) { while (consume_message()) {
++progress; ++progress;
++events;
if (predicate()) { if (predicate()) {
CAF_LOG_DEBUG("stop due to predicate:" << CAF_ARG(events)); CAF_LOG_DEBUG("stop due to predicate:" << CAF_ARG(events));
return events; return events;
...@@ -754,18 +802,20 @@ public: ...@@ -754,18 +802,20 @@ public:
} }
while (handle_io_event()) { while (handle_io_event()) {
++progress; ++progress;
++events;
if (predicate()) { if (predicate()) {
CAF_LOG_DEBUG("stop due to predicate:" << CAF_ARG(events)); CAF_LOG_DEBUG("stop due to predicate:" << CAF_ARG(events));
return events; return events;
} }
} }
if (trigger_timeout()) if (trigger_timeout()) {
++progress; ++progress;
++events;
}
if (progress == 0) { if (progress == 0) {
CAF_LOG_DEBUG("no activity left:" << CAF_ARG(events)); CAF_LOG_DEBUG("no activity left:" << CAF_ARG(events));
return events; return events;
} }
events += progress;
} }
} }
...@@ -875,32 +925,22 @@ T unbox(T* x) { ...@@ -875,32 +925,22 @@ T unbox(T* x) {
/// Convenience macro for defining expect clauses. /// Convenience macro for defining expect clauses.
#define expect(types, fields) \ #define expect(types, fields) \
do { \ (expect_clause<CAF_EXPAND(CAF_DSL_LIST types)>{sched, __LINE__}.fields.eval( \
CAF_MESSAGE("expect" << #types << "." << #fields); \ #types, #fields))
expect_clause<CAF_EXPAND(CAF_DSL_LIST types)>{sched}.fields; \
} while (false)
#define inject(types, fields) \ #define inject(types, fields) \
do { \ (inject_clause<CAF_EXPAND(CAF_DSL_LIST types)>{sched, __LINE__}.fields.eval( \
CAF_MESSAGE("inject" << #types << "." << #fields); \ #types, #fields))
inject_clause<CAF_EXPAND(CAF_DSL_LIST types)>{sched}.fields; \
} while (false)
/// Convenience macro for defining allow clauses. /// Convenience macro for defining allow clauses.
#define allow(types, fields) \ #define allow(types, fields) \
([&] { \ (allow_clause<CAF_EXPAND(CAF_DSL_LIST types)>{sched, __LINE__}.fields.eval( \
CAF_MESSAGE("allow" << #types << "." << #fields); \ #types, #fields))
allow_clause<CAF_EXPAND(CAF_DSL_LIST types)> x{sched}; \
x.fields; \
return x.eval(); \
})()
/// Convenience macro for defining disallow clauses. /// Convenience macro for defining disallow clauses.
#define disallow(types, fields) \ #define disallow(types, fields) \
do { \ (disallow_clause<CAF_EXPAND(CAF_DSL_LIST types)>{__LINE__}.fields.eval( \
CAF_MESSAGE("disallow" << #types << "." << #fields); \ #types, #fields))
disallow_clause<CAF_EXPAND(CAF_DSL_LIST types)>{}.fields; \
} while (false)
/// Defines the required base type for testee states in the current namespace. /// Defines the required base type for testee states in the current namespace.
#define TESTEE_SETUP() \ #define TESTEE_SETUP() \
......
...@@ -55,9 +55,9 @@ public: ...@@ -55,9 +55,9 @@ public:
/// @param fun A function object for delegating to the parent's `exec_all`. /// @param fun A function object for delegating to the parent's `exec_all`.
test_node_fixture(run_all_nodes_fun fun) test_node_fixture(run_all_nodes_fun fun)
: mm(this->sys.middleman()), : mm(this->sys.middleman()),
mpx(dynamic_cast<caf::io::network::test_multiplexer&>(mm.backend())), mpx(dynamic_cast<caf::io::network::test_multiplexer&>(mm.backend())),
run_all_nodes(std::move(fun)) { run_all_nodes(std::move(fun)) {
bb = mm.named_broker<caf::io::basp_broker>("BASP"); bb = mm.named_broker<caf::io::basp_broker>("BASP");
} }
...@@ -124,9 +124,7 @@ void exec_all_fixtures(Iterator first, Iterator last) { ...@@ -124,9 +124,7 @@ void exec_all_fixtures(Iterator first, Iterator last) {
return x->sched.try_run_once() || x->mpx.read_data() return x->sched.try_run_once() || x->mpx.read_data()
|| x->mpx.try_exec_runnable() || x->mpx.try_accept_connection(); || x->mpx.try_exec_runnable() || x->mpx.try_accept_connection();
}; };
auto trigger_timeouts = [](fixture_ptr x) { auto trigger_timeouts = [](fixture_ptr x) { x->sched.trigger_timeouts(); };
x->sched.trigger_timeouts();
};
for (;;) { for (;;) {
// Exhaust all messages in the system. // Exhaust all messages in the system.
while (std::any_of(first, last, advance)) while (std::any_of(first, last, advance))
...@@ -167,9 +165,8 @@ public: ...@@ -167,9 +165,8 @@ public:
/// (calls `publish`). /// (calls `publish`).
/// @returns randomly picked connection handles for the server and the client. /// @returns randomly picked connection handles for the server and the client.
std::pair<connection_handle, connection_handle> std::pair<connection_handle, connection_handle>
prepare_connection(PlanetType& server, PlanetType& client, prepare_connection(PlanetType& server, PlanetType& client, std::string host,
std::string host, uint16_t port, uint16_t port, accept_handle server_accept_hdl) {
accept_handle server_accept_hdl) {
auto server_hdl = next_connection_handle(); auto server_hdl = next_connection_handle();
auto client_hdl = next_connection_handle(); auto client_hdl = next_connection_handle();
server.mpx.prepare_connection(server_accept_hdl, server_hdl, client.mpx, server.mpx.prepare_connection(server_accept_hdl, server_hdl, client.mpx,
...@@ -181,8 +178,8 @@ public: ...@@ -181,8 +178,8 @@ public:
/// (calls `publish`). /// (calls `publish`).
/// @returns randomly picked connection handles for the server and the client. /// @returns randomly picked connection handles for the server and the client.
std::pair<connection_handle, connection_handle> std::pair<connection_handle, connection_handle>
prepare_connection(PlanetType& server, PlanetType& client, prepare_connection(PlanetType& server, PlanetType& client, std::string host,
std::string host, uint16_t port) { uint16_t port) {
return prepare_connection(server, client, std::move(host), port, return prepare_connection(server, client, std::move(host), port,
next_accept_handle()); next_accept_handle());
} }
...@@ -206,7 +203,7 @@ public: ...@@ -206,7 +203,7 @@ public:
} }
/// Type-erased callback for calling `exec_all`. /// Type-erased callback for calling `exec_all`.
std::function<void ()> exec_all_callback() { std::function<void()> exec_all_callback() {
return [&] { exec_all(); }; return [&] { exec_all(); };
} }
...@@ -273,13 +270,9 @@ public: ...@@ -273,13 +270,9 @@ public:
}; };
#define expect_on(where, types, fields) \ #define expect_on(where, types, fields) \
do { \ (expect_clause<CAF_EXPAND(CAF_DSL_LIST types)>{where.sched, __LINE__} \
CAF_MESSAGE(#where << ": expect" << #types << "." << #fields); \ .fields.eval(#types, #fields))
expect_clause<CAF_EXPAND(CAF_DSL_LIST types)>{where.sched}.fields; \
} while (false)
#define disallow_on(where, types, fields) \ #define disallow_on(where, types, fields) \
do { \ (disallow_clause<CAF_EXPAND(CAF_DSL_LIST types)>{where.sched, __LINE__} \
CAF_MESSAGE(#where << ": disallow" << #types << "." << #fields); \ .fields.eval(#types, #fields))
disallow_clause<CAF_EXPAND(CAF_DSL_LIST types)>{where.sched}.fields; \
} while (false)
...@@ -15,10 +15,12 @@ ...@@ -15,10 +15,12 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "caf/config.hpp"
#include "caf/deep_to_string.hpp" #include "caf/deep_to_string.hpp"
#include "caf/fwd.hpp" #include "caf/fwd.hpp"
#include "caf/logger.hpp" #include "caf/logger.hpp"
#include "caf/optional.hpp" #include "caf/optional.hpp"
#include "caf/raise_error.hpp"
#include "caf/term.hpp" #include "caf/term.hpp"
#include "caf/variant.hpp" #include "caf/variant.hpp"
...@@ -27,6 +29,20 @@ ...@@ -27,6 +29,20 @@
namespace caf::test { namespace caf::test {
#ifdef CAF_ENABLE_EXCEPTIONS
class requirement_error : public std::exception {
public:
requirement_error(std::string msg);
const char* what() const noexcept override;
private:
std::string what_;
};
#endif // CAF_ENABLE_EXCEPTIONS
// -- Function objects for implementing CAF_CHECK_* macros --------------------- // -- Function objects for implementing CAF_CHECK_* macros ---------------------
template <class F> template <class F>
...@@ -235,6 +251,16 @@ public: ...@@ -235,6 +251,16 @@ public:
return bad_; return bad_;
} }
void reset() {
expected_failures_ = 0;
good_ = 0;
bad_ = 0;
}
void reset_bad() {
bad_ = 0;
}
bool disabled() const noexcept { bool disabled() const noexcept {
return disabled_; return disabled_;
} }
...@@ -339,6 +365,25 @@ public: ...@@ -339,6 +365,25 @@ public:
level lvl_; level lvl_;
}; };
auto levels() {
return std::make_pair(level_console_, level_file_);
}
void levels(std::pair<level, level> values) {
std::tie(level_console_, level_file_) = values;
}
void levels(level console_lvl, level file_lvl) {
level_console_ = console_lvl;
level_file_ = file_lvl;
}
auto make_quiet() {
auto res = levels();
levels(level::quiet, level::quiet);
return res;
}
stream error(); stream error();
stream info(); stream info();
stream verbose(); stream verbose();
...@@ -474,21 +519,42 @@ bool check_un(bool result, const char* file, size_t line, const char* expr); ...@@ -474,21 +519,42 @@ bool check_un(bool result, const char* file, size_t line, const char* expr);
bool check_bin(bool result, const char* file, size_t line, const char* expr, bool check_bin(bool result, const char* file, size_t line, const char* expr,
const std::string& lhs, const std::string& rhs); const std::string& lhs, const std::string& rhs);
void require_un(bool result, const char* file, size_t line, const char* expr);
void require_bin(bool result, const char* file, size_t line, const char* expr,
const std::string& lhs, const std::string& rhs);
} // namespace detail } // namespace detail
} // namespace caf::test } // namespace caf::test
// on the global namespace so that it can hidden via namespace-scoping // on the global namespace so that it can hidden via namespace-scoping
using caf_test_case_auto_fixture = caf::test::dummy_fixture; using caf_test_case_auto_fixture = caf::test::dummy_fixture;
#define CAF_TEST_PRINT(level, msg, colorcode) \ #define CAF_TEST_LOG_MSG_4(level, colorcode, msg, line) \
(::caf::test::logger::instance().level() \ (::caf::test::logger::instance().level() \
<< ::caf::term::colorcode << " -> " << ::caf::term::reset \ << ::caf::term::colorcode << " -> " << ::caf::term::reset \
<< ::caf::test::logger::stream::reset_flags_t{} << msg << " [line " \ << ::caf::test::logger::stream::reset_flags_t{} << msg << " [line " << line \
<< __LINE__ << "]\n") << "]\n")
#define CAF_TEST_LOG_MSG_3(level, colorcode, msg) \
CAF_TEST_LOG_MSG_4(level, colorcode, msg, __LINE__)
#ifdef CAF_MSVC
# define CAF_TEST_LOG_MSG(...) \
CAF_PP_CAT(CAF_PP_OVERLOAD(CAF_TEST_LOG_MSG_, __VA_ARGS__)(__VA_ARGS__), \
CAF_PP_EMPTY())
#else
# define CAF_TEST_LOG_MSG(...) \
CAF_PP_OVERLOAD(CAF_TEST_LOG_MSG_, __VA_ARGS__)(__VA_ARGS__)
#endif
#define CAF_TEST_PRINT_ERROR(...) CAF_TEST_LOG_MSG(error, red, __VA_ARGS__)
#define CAF_TEST_PRINT_INFO(...) CAF_TEST_LOG_MSG(info, yellow, __VA_ARGS__)
#define CAF_TEST_PRINT_VERBOSE(...) \
CAF_TEST_LOG_MSG(verbose, yellow, __VA_ARGS__)
#define CAF_TEST_PRINT_ERROR(msg) CAF_TEST_PRINT(info, msg, red) #define CAF_TEST_PRINT(level, msg, colorcode) \
#define CAF_TEST_PRINT_INFO(msg) CAF_TEST_PRINT(info, msg, yellow) CAF_TEST_LOG_MSG(level, colorcode, msg)
#define CAF_TEST_PRINT_VERBOSE(msg) CAF_TEST_PRINT(verbose, msg, yellow)
#define CAF_PASTE_CONCAT(lhs, rhs) lhs##rhs #define CAF_PASTE_CONCAT(lhs, rhs) lhs##rhs
...@@ -514,54 +580,21 @@ using caf_test_case_auto_fixture = caf::test::dummy_fixture; ...@@ -514,54 +580,21 @@ using caf_test_case_auto_fixture = caf::test::dummy_fixture;
::caf::test::engine::last_check_line(__LINE__); \ ::caf::test::engine::last_check_line(__LINE__); \
} while (false) } while (false)
#define CAF_CHECK(...) \ #define CAF_CHECK(expr) \
[](bool expr_result) { \ ::caf::test::detail::check_un(static_cast<bool>(expr), __FILE__, __LINE__, \
return ::caf::test::detail::check_un(expr_result, __FILE__, __LINE__, \ #expr)
#__VA_ARGS__); \
}(static_cast<bool>(__VA_ARGS__)) #define CAF_REQUIRE(expr) \
::caf::test::detail::require_un(static_cast<bool>(expr), __FILE__, __LINE__, \
#define CAF_CHECK_FUNC(func, x_expr, y_expr) \ #expr)
[](auto&& x_val, auto&& y_val) { \
func comparator; \
auto caf_check_res \
= ::caf::test::detail::check(::caf::test::engine::current_test(), \
__FILE__, __LINE__, \
CAF_FUNC_EXPR(func, x_expr, y_expr), false, \
comparator(x_val, y_val), x_val, y_val); \
return caf_check_res; \
}(x_expr, y_expr)
#define CAF_FAIL(msg) \ #define CAF_FAIL(...) \
do { \ do { \
CAF_TEST_PRINT_ERROR(msg); \ CAF_TEST_PRINT_ERROR(__VA_ARGS__); \
::caf::test::engine::current_test()->fail(false); \ ::caf::test::engine::current_test()->fail(false); \
::caf::test::detail::requirement_failed("test failure"); \ ::caf::test::detail::requirement_failed("test failure"); \
} while (false) } while (false)
#define CAF_REQUIRE(...) \
do { \
auto CAF_UNIQUE(__result) \
= ::caf::test::detail::check(::caf::test::engine::current_test(), \
__FILE__, __LINE__, #__VA_ARGS__, false, \
static_cast<bool>(__VA_ARGS__)); \
if (!CAF_UNIQUE(__result)) \
::caf::test::detail::requirement_failed(#__VA_ARGS__); \
} while (false)
#define CAF_REQUIRE_FUNC(func, x_expr, y_expr) \
do { \
func comparator; \
auto&& x_val___ = x_expr; \
auto&& y_val___ = y_expr; \
auto CAF_UNIQUE(__result) = ::caf::test::detail::check( \
::caf::test::engine::current_test(), __FILE__, __LINE__, \
CAF_FUNC_EXPR(func, x_expr, y_expr), false, \
comparator(x_val___, y_val___), x_val___, y_val___); \
if (!CAF_UNIQUE(__result)) \
::caf::test::detail::requirement_failed( \
CAF_FUNC_EXPR(func, x_expr, y_expr)); \
} while (false)
#define CAF_TEST_IMPL(name, disabled_by_default) \ #define CAF_TEST_IMPL(name, disabled_by_default) \
namespace { \ namespace { \
struct CAF_UNIQUE(test) : caf_test_case_auto_fixture { \ struct CAF_UNIQUE(test) : caf_test_case_auto_fixture { \
...@@ -590,7 +623,7 @@ using caf_test_case_auto_fixture = caf::test::dummy_fixture; ...@@ -590,7 +623,7 @@ using caf_test_case_auto_fixture = caf::test::dummy_fixture;
CAF_TEST_PRINT_VERBOSE(msg); \ CAF_TEST_PRINT_VERBOSE(msg); \
} while (false) } while (false)
// -- CAF_CHECK* predicate family ---------------------------------------------- // -- CAF_CHECK* macro family --------------------------------------------------
#define CAF_CHECK_EQUAL(x_expr, y_expr) \ #define CAF_CHECK_EQUAL(x_expr, y_expr) \
[](const auto& x_val, const auto& y_val) { \ [](const auto& x_val, const auto& y_val) { \
...@@ -672,33 +705,139 @@ using caf_test_case_auto_fixture = caf::test::dummy_fixture; ...@@ -672,33 +705,139 @@ using caf_test_case_auto_fixture = caf::test::dummy_fixture;
caf::detail::stringification_inspector::render(y_val)); \ caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr) }(x_expr, y_expr)
// -- CAF_CHECK* predicate family ---------------------------------------------- #ifdef CAF_ENABLE_EXCEPTIONS
#define CAF_REQUIRE_EQUAL(x, y) CAF_REQUIRE_FUNC(::caf::test::equal_to, x, y) # define CAF_CHECK_NOTHROW(expr) \
[&] { \
bool got_exception = false; \
try { \
static_cast<void>(expr); \
} catch (...) { \
got_exception = true; \
} \
::caf::test::detail::check_un(!got_exception, __FILE__, __LINE__, \
#expr " does not throw"); \
return !got_exception; \
}()
# define CAF_CHECK_THROWS_AS(expr, type) \
[&] { \
bool got_exception = false; \
try { \
static_cast<void>(expr); \
} catch (type&) { \
got_exception = true; \
} catch (...) { \
} \
::caf::test::detail::check_un(got_exception, __FILE__, __LINE__, \
#expr " throws " #type); \
return got_exception; \
}()
# define CAF_CHECK_THROWS_WITH(expr, msg) \
[&] { \
std::string ex_what = "EX-NOT-FOUND"; \
try { \
static_cast<void>(expr); \
} catch (std::exception & ex) { \
ex_what = ex.what(); \
} catch (...) { \
} \
return CHECK_EQ(ex_what, msg); \
}()
# define CAF_CHECK_THROWS_WITH_AS(expr, msg, type) \
[&] { \
std::string ex_what = "EX-NOT-FOUND"; \
try { \
static_cast<void>(expr); \
} catch (type & ex) { \
ex_what = ex.what(); \
} catch (...) { \
} \
return CHECK_EQ(ex_what, msg); \
}()
#endif // CAF_ENABLE_EXCEPTIONS
// -- CAF_REQUIRE* macro family ------------------------------------------------
#define CAF_REQUIRE_EQUAL(x_expr, y_expr) \
[](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
x_val == y_val, __FILE__, __LINE__, #x_expr " == " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_NOT_EQUAL(x, y) \ #define CAF_REQUIRE_NOT_EQUAL(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::not_equal_to, x, y) [](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
x_val != y_val, __FILE__, __LINE__, #x_expr " != " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_LESS(x, y) CAF_REQUIRE_FUNC(::caf::test::less_than, x, y) #define CAF_REQUIRE_LESS(x_expr, y_expr) \
[](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
x_val < y_val, __FILE__, __LINE__, #x_expr " < " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_NOT_LESS(x, y) \ #define CAF_REQUIRE_NOT_LESS(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::negated<::caf::test::less_than>, x, y) [](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
!(x_val < y_val), __FILE__, __LINE__, "not " #x_expr " < " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_LESS_OR_EQUAL(x, y) \ #define CAF_REQUIRE_LESS_OR_EQUAL(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::less_than_or_equal, x, y) [](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
x_val <= y_val, __FILE__, __LINE__, #x_expr " <= " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_NOT_LESS_OR_EQUAL(x, y) \ #define CAF_REQUIRE_NOT_LESS_OR_EQUAL(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::negated<::caf::test::less_than_or_equal>, x, y) [](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
!(x_val <= y_val), __FILE__, __LINE__, "not " #x_expr " <= " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_GREATER(x, y) \ #define CAF_REQUIRE_GREATER(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::greater_than, x, y) [](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
x_val > y_val, __FILE__, __LINE__, #x_expr " > " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_NOT_GREATER(x, y) \ #define CAF_REQUIRE_NOT_GREATER(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::negated<::caf::test::greater_than>, x, y) [](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
!(x_val > y_val), __FILE__, __LINE__, "not " #x_expr " > " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_GREATER_OR_EQUAL(x, y) \ #define CAF_REQUIRE_GREATER_OR_EQUAL(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::greater_than_or_equal, x, y) [](const auto& x_val, const auto& y_val) { \
return ::caf::test::detail::require_bin( \
x_val >= y_val, __FILE__, __LINE__, #x_expr " >= " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
#define CAF_REQUIRE_NOT_GREATER_OR_EQUAL(x, y) \ #define CAF_REQUIRE_NOT_GREATER_OR_EQUAL(x_expr, y_expr) \
CAF_REQUIRE_FUNC(::caf::test::negated<::caf::test::greater_than_or_equal>, \ [](const auto& x_val, const auto& y_val) { \
x, y) return ::caf::test::detail::require_bin( \
!(x_val >= y_val), __FILE__, __LINE__, "not " #x_expr " >= " #y_expr, \
caf::detail::stringification_inspector::render(x_val), \
caf::detail::stringification_inspector::render(y_val)); \
}(x_expr, y_expr)
...@@ -29,6 +29,14 @@ ...@@ -29,6 +29,14 @@
namespace caf::test { namespace caf::test {
requirement_error::requirement_error(std::string msg) : what_(std::move(msg)) {
// nop
}
const char* requirement_error::what() const noexcept {
return what_.c_str();
}
class watchdog { class watchdog {
public: public:
static void start(int secs); static void start(int secs);
...@@ -121,6 +129,7 @@ namespace detail { ...@@ -121,6 +129,7 @@ namespace detail {
<< term::yellow << ":" << term::cyan << engine::last_check_line() << term::yellow << ":" << term::cyan << engine::last_check_line()
<< term::reset << detail::fill(engine::last_check_line()) << term::reset << detail::fill(engine::last_check_line())
<< "had last successful check" << '\n'; << "had last successful check" << '\n';
CAF_RAISE_ERROR(requirement_error, msg.c_str());
abort(); abort();
} }
...@@ -200,6 +209,59 @@ bool check_bin(bool result, const char* file, size_t line, const char* expr, ...@@ -200,6 +209,59 @@ bool check_bin(bool result, const char* file, size_t line, const char* expr,
return result; return result;
} }
void require_un(bool result, const char* file, size_t line, const char* expr) {
string_view rel_up = "../";
while (strncmp(file, rel_up.data(), rel_up.size()) == 0)
file += rel_up.size();
auto parent = engine::current_test();
auto out = logger::instance().massive();
if (result) {
out << term::green << "** " << term::blue << file << term::yellow << ":"
<< term::blue << line << fill(line) << term::reset << "passed" << '\n';
parent->pass();
engine::last_check_file(file);
engine::last_check_line(line);
} else {
out << term::red << "!! " << term::blue << file << term::yellow << ":"
<< term::blue << line << fill(line) << term::red
<< "requirement failed: " << expr << term::reset << '\n';
parent->fail(false);
std::string msg = "requirement failed in ";
msg += file;
msg += " line ";
msg += std::to_string(line);
requirement_failed(std::move(msg));
}
}
void require_bin(bool result, const char* file, size_t line, const char* expr,
const std::string& lhs, const std::string& rhs) {
string_view rel_up = "../";
while (strncmp(file, rel_up.data(), rel_up.size()) == 0)
file += rel_up.size();
auto parent = engine::current_test();
auto out = logger::instance().massive();
if (result) {
out << term::green << "** " << term::blue << file << term::yellow << ":"
<< term::blue << line << fill(line) << term::reset << "passed" << '\n';
parent->pass();
engine::last_check_file(file);
engine::last_check_line(line);
} else {
out << term::red << "!! " << term::blue << file << term::yellow << ":"
<< term::blue << line << fill(line) << term::red
<< "requirement failed: " << expr << term::reset << '\n'
<< " lhs: " << lhs << '\n'
<< " rhs: " << rhs << '\n';
parent->fail(false);
std::string msg = "requirement failed in ";
msg += file;
msg += " line ";
msg += std::to_string(line);
requirement_failed(std::move(msg));
}
}
} // namespace detail } // namespace detail
logger::stream::stream(logger& parent, logger::level lvl) logger::stream::stream(logger& parent, logger::level lvl)
...@@ -319,36 +381,6 @@ bool engine::run(bool colorize, const std::string& log_file, ...@@ -319,36 +381,6 @@ bool engine::run(bool colorize, const std::string& log_file,
size_t total_bad = 0; size_t total_bad = 0;
size_t total_bad_expected = 0; size_t total_bad_expected = 0;
auto bar = '+' + std::string(70, '-') + '+'; auto bar = '+' + std::string(70, '-') + '+';
#if (!defined(__clang__) && defined(__GNUC__) && __GNUC__ == 4 \
&& __GNUC_MINOR__ < 9) \
|| (defined(__clang__) && !defined(_LIBCPP_VERSION))
// regex implementation is broken prior to 4.9
using strvec = std::vector<std::string>;
using whitelist_type = strvec;
using blacklist_type = strvec;
auto from_psv = [](const std::string& psv) -> strvec {
// psv == pipe-separated-values
strvec result;
if (psv != ".*") {
split(result, psv, "|", token_compress_on);
std::sort(result.begin(), result.end());
}
return result;
};
auto suites = from_psv(suites_str);
auto not_suites = from_psv(not_suites_str);
auto tests = from_psv(tests_str);
auto not_tests = from_psv(not_tests_str);
auto enabled = [](const strvec& whitelist, const strvec& blacklist,
const std::string& x) {
// an empty whitelist means original input was ".*", i.e., enable all
return !std::binary_search(blacklist.begin(), blacklist.end(), x)
&& (whitelist.empty()
|| std::binary_search(whitelist.begin(), whitelist.end(), x));
};
#else
using whitelist_type = std::regex;
using blacklist_type = std::regex;
std::regex suites{suites_str}; std::regex suites{suites_str};
std::regex tests{tests_str}; std::regex tests{tests_str};
std::regex not_suites; std::regex not_suites;
...@@ -358,18 +390,16 @@ bool engine::run(bool colorize, const std::string& log_file, ...@@ -358,18 +390,16 @@ bool engine::run(bool colorize, const std::string& log_file,
not_suites.assign(not_suites_str); not_suites.assign(not_suites_str);
if (!not_tests_str.empty()) if (!not_tests_str.empty())
not_tests.assign(not_tests_str); not_tests.assign(not_tests_str);
auto enabled = [](const std::regex& whitelist, const std::regex& blacklist, auto enabled = [](const std::regex& selected, const std::regex& blocked,
const std::string& x) { const std::string& x) {
// an empty whitelist means original input was "*", i.e., enable all return std::regex_search(x, selected) && !std::regex_search(x, blocked);
return std::regex_search(x, whitelist) && !std::regex_search(x, blacklist);
}; };
#endif auto test_enabled = [&](const std::regex& selected, const std::regex& blocked,
auto test_enabled = [&](const whitelist_type& whitelist, const test& x) {
const blacklist_type& blacklist, const test& x) {
// Disabled tests run if explicitly requested by the user, i.e., // Disabled tests run if explicitly requested by the user, i.e.,
// tests_str is not the ".*" catch-all default. // tests_str is not the ".*" catch-all default.
return (!x.disabled() || tests_str != ".*") return (!x.disabled() || tests_str != ".*")
&& enabled(whitelist, blacklist, x.name()); && enabled(selected, blocked, x.name());
}; };
std::vector<std::string> failed_tests; std::vector<std::string> failed_tests;
for (auto& p : instance().suites_) { for (auto& p : instance().suites_) {
...@@ -393,7 +423,23 @@ bool engine::run(bool colorize, const std::string& log_file, ...@@ -393,7 +423,23 @@ bool engine::run(bool colorize, const std::string& log_file,
log.verbose() << term::yellow << "- " << term::reset << t->name() << '\n'; log.verbose() << term::yellow << "- " << term::reset << t->name() << '\n';
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
watchdog::start(max_runtime()); watchdog::start(max_runtime());
#ifdef CAF_ENABLE_EXCEPTIONS
try {
t->run_test_impl();
} catch (requirement_error&) {
// nop
} catch (std::exception& ex) {
t->fail(false);
log.error() << term::red << "!! uncaught exception, what: " << ex.what()
<< term::reset_endl;
} catch (...) {
t->fail(false);
log.error() << term::red << "!! uncaught exception of unknown type"
<< term::reset_endl;
}
#else
t->run_test_impl(); t->run_test_impl();
#endif // CAF_ENABLE_EXCEPTIONS
watchdog::stop(); watchdog::stop();
auto stop = std::chrono::high_resolution_clock::now(); auto stop = std::chrono::high_resolution_clock::now();
auto elapsed auto elapsed
...@@ -515,8 +561,8 @@ std::string engine::render(std::chrono::microseconds t) { ...@@ -515,8 +561,8 @@ std::string engine::render(std::chrono::microseconds t) {
return t.count() > 1000000 return t.count() > 1000000
? (std::to_string(t.count() / 1000000) + '.' ? (std::to_string(t.count() / 1000000) + '.'
+ std::to_string((t.count() % 1000000) / 10000) + " s") + std::to_string((t.count() % 1000000) / 10000) + " s")
: t.count() > 1000 ? (std::to_string(t.count() / 1000) + " ms") : t.count() > 1000 ? (std::to_string(t.count() / 1000) + " ms")
: (std::to_string(t.count()) + " us"); : (std::to_string(t.count()) + " us");
} }
int main(int argc, char** argv) { int main(int argc, char** argv) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment