Commit cd8363aa authored by Dominik Charousset's avatar Dominik Charousset

Merge pull request #1518

parents c677bf5b faaaf3d5
...@@ -38,7 +38,7 @@ is based on [Keep a Changelog](https://keepachangelog.com). ...@@ -38,7 +38,7 @@ is based on [Keep a Changelog](https://keepachangelog.com).
- We renamed `caf::flow::item_publisher` to `caf::flow::multicaster` to better - We renamed `caf::flow::item_publisher` to `caf::flow::multicaster` to better
reflect its purpose and to avoid confusion with the new reflect its purpose and to avoid confusion with the new
`caf::async::publisher`. `caf::async::publisher`.
- When failing to deserialize a request, the sender will receive an error of - When failing to deserialize a request, the sender will receive an error of
kind `sec::malformed_message`. kind `sec::malformed_message`.
### Fixed ### Fixed
...@@ -52,6 +52,8 @@ is based on [Keep a Changelog](https://keepachangelog.com). ...@@ -52,6 +52,8 @@ is based on [Keep a Changelog](https://keepachangelog.com).
`on_complete` on its subscriber on the first activity of the source `on_complete` on its subscriber on the first activity of the source
observable. Previously, the created observable would never reach its threshold observable. Previously, the created observable would never reach its threshold
and attempt to buffer all values indefinitely. and attempt to buffer all values indefinitely.
- The comparison operator of `intrusive_ptr` no longer accidentally creates new
`intrusive_ptr` instances when comparing to raw pointers.
## [0.19.2] - 2023-06-13 ## [0.19.2] - 2023-06-13
......
...@@ -541,4 +541,8 @@ void actor_system::release_private_thread(detail::private_thread* ptr) { ...@@ -541,4 +541,8 @@ void actor_system::release_private_thread(detail::private_thread* ptr) {
private_threads_.release(ptr); private_threads_.release(ptr);
} }
detail::mailbox_factory* actor_system::mailbox_factory() {
return cfg_.mailbox_factory();
}
} // namespace caf } // namespace caf
...@@ -398,6 +398,7 @@ public: ...@@ -398,6 +398,7 @@ public:
infer_handle_from_class_t<C> spawn(Ts&&... xs) { infer_handle_from_class_t<C> spawn(Ts&&... xs) {
check_invariants<C>(); check_invariants<C>();
actor_config cfg; actor_config cfg;
cfg.mbox_factory = mailbox_factory();
return spawn_impl<C, Os>(cfg, detail::spawn_fwd<Ts>(xs)...); return spawn_impl<C, Os>(cfg, detail::spawn_fwd<Ts>(xs)...);
} }
...@@ -436,6 +437,7 @@ public: ...@@ -436,6 +437,7 @@ public:
static_assert(spawnable, static_assert(spawnable,
"cannot spawn function-based actor with given arguments"); "cannot spawn function-based actor with given arguments");
actor_config cfg; actor_config cfg;
cfg.mbox_factory = mailbox_factory();
return spawn_functor<Os>(detail::bool_token<spawnable>{}, cfg, fun, return spawn_functor<Os>(detail::bool_token<spawnable>{}, cfg, fun,
std::forward<Ts>(xs)...); std::forward<Ts>(xs)...);
} }
...@@ -500,6 +502,7 @@ public: ...@@ -500,6 +502,7 @@ public:
infer_handle_from_fun_t<F> infer_handle_from_fun_t<F>
spawn_in_groups(std::initializer_list<group> gs, F fun, Ts&&... xs) { spawn_in_groups(std::initializer_list<group> gs, F fun, Ts&&... xs) {
actor_config cfg; actor_config cfg;
cfg.mbox_factory = mailbox_factory();
return spawn_fun_in_groups<Os>(cfg, gs.begin(), gs.end(), fun, return spawn_fun_in_groups<Os>(cfg, gs.begin(), gs.end(), fun,
std::forward<Ts>(xs)...); std::forward<Ts>(xs)...);
} }
...@@ -508,6 +511,7 @@ public: ...@@ -508,6 +511,7 @@ public:
template <spawn_options Os = no_spawn_options, class Gs, class F, class... Ts> template <spawn_options Os = no_spawn_options, class Gs, class F, class... Ts>
infer_handle_from_fun_t<F> spawn_in_groups(const Gs& gs, F fun, Ts&&... xs) { infer_handle_from_fun_t<F> spawn_in_groups(const Gs& gs, F fun, Ts&&... xs) {
actor_config cfg; actor_config cfg;
cfg.mbox_factory = mailbox_factory();
return spawn_fun_in_groups<Os>(cfg, gs.begin(), gs.end(), fun, return spawn_fun_in_groups<Os>(cfg, gs.begin(), gs.end(), fun,
std::forward<Ts>(xs)...); std::forward<Ts>(xs)...);
} }
...@@ -524,6 +528,7 @@ public: ...@@ -524,6 +528,7 @@ public:
infer_handle_from_class_t<T> infer_handle_from_class_t<T>
spawn_in_groups(std::initializer_list<group> gs, Ts&&... xs) { spawn_in_groups(std::initializer_list<group> gs, Ts&&... xs) {
actor_config cfg; actor_config cfg;
cfg.mbox_factory = mailbox_factory();
return spawn_class_in_groups<T, Os>(cfg, gs.begin(), gs.end(), return spawn_class_in_groups<T, Os>(cfg, gs.begin(), gs.end(),
std::forward<Ts>(xs)...); std::forward<Ts>(xs)...);
} }
...@@ -532,6 +537,7 @@ public: ...@@ -532,6 +537,7 @@ public:
template <class T, spawn_options Os = no_spawn_options, class Gs, class... Ts> template <class T, spawn_options Os = no_spawn_options, class Gs, class... Ts>
infer_handle_from_class_t<T> spawn_in_groups(const Gs& gs, Ts&&... xs) { infer_handle_from_class_t<T> spawn_in_groups(const Gs& gs, Ts&&... xs) {
actor_config cfg; actor_config cfg;
cfg.mbox_factory = mailbox_factory();
return spawn_class_in_groups<T, Os>(cfg, gs.begin(), gs.end(), return spawn_class_in_groups<T, Os>(cfg, gs.begin(), gs.end(),
std::forward<Ts>(xs)...); std::forward<Ts>(xs)...);
} }
...@@ -696,6 +702,7 @@ public: ...@@ -696,6 +702,7 @@ public:
"only scheduled actors may get spawned inactively"); "only scheduled actors may get spawned inactively");
CAF_SET_LOGGER_SYS(this); CAF_SET_LOGGER_SYS(this);
actor_config cfg{dummy_execution_unit(), nullptr}; actor_config cfg{dummy_execution_unit(), nullptr};
cfg.mbox_factory = mailbox_factory();
auto res = make_actor<Impl>(next_actor_id(), node(), this, cfg, auto res = make_actor<Impl>(next_actor_id(), node(), this, cfg,
std::forward<Ts>(xs)...); std::forward<Ts>(xs)...);
auto ptr = static_cast<Impl*>(actor_cast<abstract_actor*>(res)); auto ptr = static_cast<Impl*>(actor_cast<abstract_actor*>(res));
...@@ -790,6 +797,8 @@ private: ...@@ -790,6 +797,8 @@ private:
config_serv_ = std::move(x); config_serv_ = std::move(x);
} }
detail::mailbox_factory* mailbox_factory();
// -- member variables ------------------------------------------------------- // -- member variables -------------------------------------------------------
/// Provides system-wide callbacks for several actor operations. /// Provides system-wide callbacks for several actor operations.
......
...@@ -478,6 +478,10 @@ actor_system_config::extract_config_file_path(string_list& args) { ...@@ -478,6 +478,10 @@ actor_system_config::extract_config_file_path(string_list& args) {
} }
} }
detail::mailbox_factory* actor_system_config::mailbox_factory() {
return nullptr;
}
const settings& content(const actor_system_config& cfg) { const settings& content(const actor_system_config& cfg) {
return cfg.content; return cfg.content;
} }
......
...@@ -32,6 +32,10 @@ namespace caf { ...@@ -32,6 +32,10 @@ namespace caf {
/// Configures an `actor_system` on startup. /// Configures an `actor_system` on startup.
class CAF_CORE_EXPORT actor_system_config { class CAF_CORE_EXPORT actor_system_config {
public: public:
// -- friends ----------------------------------------------------------------
friend class actor_system;
// -- member types ----------------------------------------------------------- // -- member types -----------------------------------------------------------
using hook_factory = std::function<io::hook*(actor_system&)>; using hook_factory = std::function<io::hook*(actor_system&)>;
...@@ -303,6 +307,8 @@ protected: ...@@ -303,6 +307,8 @@ protected:
config_option_set custom_options_; config_option_set custom_options_;
private: private:
virtual detail::mailbox_factory* mailbox_factory();
void set_remainder(string_list args); void set_remainder(string_list args);
mutable std::vector<char*> c_args_remainder_; mutable std::vector<char*> c_args_remainder_;
......
...@@ -175,6 +175,9 @@ public: ...@@ -175,6 +175,9 @@ public:
static constexpr bool value = std::is_same_v<bool, result_type>; static constexpr bool value = std::is_same_v<bool, result_type>;
}; };
template <class T1, class T2>
constexpr bool is_comparable_v = is_comparable<T1, T2>::value;
/// Checks whether `T` behaves like a forward iterator. /// Checks whether `T` behaves like a forward iterator.
template <class T> template <class T>
class is_forward_iterator { class is_forward_iterator {
......
...@@ -231,27 +231,31 @@ bool operator!=(std::nullptr_t, const intrusive_ptr<T>& x) { ...@@ -231,27 +231,31 @@ bool operator!=(std::nullptr_t, const intrusive_ptr<T>& x) {
// -- comparison to raw pointer ------------------------------------------------ // -- comparison to raw pointer ------------------------------------------------
/// @relates intrusive_ptr /// @relates intrusive_ptr
template <class T> template <class T, class U>
bool operator==(const intrusive_ptr<T>& x, const T* y) { detail::enable_if_t<detail::is_comparable<T*, U*>::value, bool>
return x.get() == y; operator==(const intrusive_ptr<T>& lhs, const U* rhs) {
return lhs.get() == rhs;
} }
/// @relates intrusive_ptr /// @relates intrusive_ptr
template <class T> template <class T, class U>
bool operator==(const T* x, const intrusive_ptr<T>& y) { detail::enable_if_t<detail::is_comparable<T*, U*>::value, bool>
return x == y.get(); operator==(const T* lhs, const intrusive_ptr<U>& rhs) {
return lhs == rhs.get();
} }
/// @relates intrusive_ptr /// @relates intrusive_ptr
template <class T> template <class T, class U>
bool operator!=(const intrusive_ptr<T>& x, const T* y) { detail::enable_if_t<detail::is_comparable<T*, U*>::value, bool>
return x.get() != y; operator!=(const intrusive_ptr<T>& lhs, const U* rhs) {
return lhs.get() != rhs;
} }
/// @relates intrusive_ptr /// @relates intrusive_ptr
template <class T> template <class T, class U>
bool operator!=(const T* x, const intrusive_ptr<T>& y) { detail::enable_if_t<detail::is_comparable<T*, U*>::value, bool>
return x != y.get(); operator!=(const T* lhs, const intrusive_ptr<U>& rhs) {
return lhs != rhs.get();
} }
// -- comparison to intrusive_pointer ------------------------------------------ // -- comparison to intrusive_pointer ------------------------------------------
......
...@@ -241,6 +241,11 @@ public: ...@@ -241,6 +241,11 @@ public:
return *mailbox_; return *mailbox_;
} }
/// Checks whether this actor is fully initialized.
bool initialized() const noexcept {
return getf(is_initialized_flag);
}
// -- event handlers --------------------------------------------------------- // -- event handlers ---------------------------------------------------------
/// Sets a custom handler for unexpected messages. /// Sets a custom handler for unexpected messages.
......
...@@ -22,6 +22,8 @@ caf_add_component( ...@@ -22,6 +22,8 @@ caf_add_component(
caf/test/block.cpp caf/test/block.cpp
caf/test/context.cpp caf/test/context.cpp
caf/test/factory.cpp caf/test/factory.cpp
caf/test/fixture/deterministic.cpp
caf/test/fixture/deterministic.test.cpp
caf/test/fixture/flow.cpp caf/test/fixture/flow.cpp
caf/test/fixture/flow.test.cpp caf/test/fixture/flow.test.cpp
caf/test/given.cpp caf/test/given.cpp
......
...@@ -15,13 +15,13 @@ void context::on_enter(block* ptr) { ...@@ -15,13 +15,13 @@ void context::on_enter(block* ptr) {
call_stack.push_back(ptr); call_stack.push_back(ptr);
unwind_stack.clear(); unwind_stack.clear();
path.push_back(ptr); path.push_back(ptr);
reporter::instance->begin_step(ptr); reporter::instance().begin_step(ptr);
} }
void context::on_leave(block* ptr) { void context::on_leave(block* ptr) {
call_stack.pop_back(); call_stack.pop_back();
unwind_stack.push_back(ptr); unwind_stack.push_back(ptr);
reporter::instance->end_step(ptr); reporter::instance().end_step(ptr);
} }
bool context::activated(block* ptr) const noexcept { bool context::activated(block* ptr) const noexcept {
......
// 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.
#include "caf/test/fixture/deterministic.hpp"
#include "caf/actor_control_block.hpp"
#include "caf/actor_system.hpp"
#include "caf/actor_system_config.hpp"
#include "caf/detail/mailbox_factory.hpp"
#include "caf/detail/source_location.hpp"
#include "caf/detail/sync_request_bouncer.hpp"
#include "caf/detail/test_export.hpp"
#include "caf/scheduled_actor.hpp"
namespace caf::test::fixture {
// -- mailbox ------------------------------------------------------------------
class deterministic::mailbox_impl : public ref_counted,
public abstract_mailbox {
public:
mailbox_impl(deterministic* fix, scheduled_actor* owner)
: fix_(fix), owner_(owner) {
}
intrusive::inbox_result push_back(mailbox_element_ptr ptr) override {
if (closed_) {
detail::sync_request_bouncer bouncer{close_reason_};
bouncer(*ptr);
return intrusive::inbox_result::queue_closed;
}
using event_t = deterministic::scheduling_event;
auto unblocked = fix_->mail_count(owner_) == 0;
auto event = std::make_unique<event_t>(owner_, std::move(ptr));
fix_->events_.push_back(std::move(event));
return unblocked ? intrusive::inbox_result::unblocked_reader
: intrusive::inbox_result::success;
}
void push_front(mailbox_element_ptr ptr) override {
using event_t = deterministic::scheduling_event;
auto event = std::make_unique<event_t>(owner_, std::move(ptr));
fix_->events_.emplace_front(std::move(event));
}
mailbox_element_ptr pop_front() override {
return fix_->pop_msg_impl(owner_);
}
bool closed() const noexcept override {
return closed_;
}
bool blocked() const noexcept override {
return blocked_;
}
bool try_block() override {
blocked_ = true;
return true;
}
bool try_unblock() override {
if (!blocked_)
return false;
blocked_ = false;
return true;
}
size_t close(const error& reason) override {
closed_ = true;
close_reason_ = reason;
auto result = size_t{0};
auto envelope = fix_->pop_msg_impl(owner_);
while (envelope != nullptr) {
detail::sync_request_bouncer bouncer{reason};
++result;
envelope = fix_->pop_msg_impl(owner_);
}
return result;
}
size_t size() override {
return fix_->mail_count(owner_);
}
void ref_mailbox() noexcept override {
ref();
}
void deref_mailbox() noexcept override {
deref();
}
mailbox_element* peek(message_id) override {
// Note: this function only exists for backwards compatibility with the old
// unit testing framework. It is not used by the new test runner and thus
// not implemented.
CAF_RAISE_ERROR(std::logic_error, "peek not supported by this mailbox");
}
private:
bool blocked_ = false;
bool closed_ = false;
error close_reason_;
deterministic* fix_;
scheduled_actor* owner_;
};
class deterministic::mailbox_factory_impl : public detail::mailbox_factory {
public:
explicit mailbox_factory_impl(deterministic* fix) : fix_(fix) {
// nop
}
abstract_mailbox* make(scheduled_actor* owner) override {
return new deterministic::mailbox_impl(fix_, owner);
}
abstract_mailbox* make(blocking_actor*) override {
return nullptr;
}
private:
deterministic* fix_;
};
// -- scheduler ----------------------------------------------------------------
class deterministic::scheduler_impl : public scheduler::abstract_coordinator {
public:
using super = caf::scheduler::abstract_coordinator;
scheduler_impl(actor_system& sys, deterministic* fix)
: super(sys), fix_(fix) {
// nop
}
bool detaches_utility_actors() const override {
return false;
}
detail::test_actor_clock& clock() noexcept override {
return clock_;
}
protected:
void start() override {
// nop
}
void stop() override {
fix_->drop_events();
}
void enqueue(resumable* ptr) override {
using event_t = deterministic::scheduling_event;
using subtype_t = resumable::subtype_t;
switch (ptr->subtype()) {
case subtype_t::scheduled_actor:
case subtype_t::io_actor: {
// Actors put their messages into events_ directly. However, we do run
// them right away if they aren't initialized yet.
auto dptr = static_cast<scheduled_actor*>(ptr);
if (!dptr->initialized())
dptr->resume(system_.dummy_execution_unit(), 0);
break;
}
default:
fix_->events_.push_back(std::make_unique<event_t>(ptr, nullptr));
}
// Before calling this function, CAF *always* bumps the reference count.
// Hence, we need to release one reference count here.
intrusive_ptr_release(ptr);
}
private:
/// The fixture this scheduler belongs to.
deterministic* fix_;
/// Allows users to fake time at will.
detail::test_actor_clock clock_;
};
// -- config -------------------------------------------------------------------
deterministic::config::config(deterministic* fix) {
factory_ = std::make_unique<mailbox_factory_impl>(fix);
module_factories.push_back([fix](actor_system& sys) -> actor_system::module* {
return new scheduler_impl(sys, fix);
});
}
deterministic::config::~config() {
// nop
}
detail::mailbox_factory* deterministic::config::mailbox_factory() {
return factory_.get();
}
// -- abstract_message_predicate -----------------------------------------------
deterministic::abstract_message_predicate::~abstract_message_predicate() {
// nop
}
// -- fixture ------------------------------------------------------------------
deterministic::deterministic() : cfg(this), sys(cfg) {
// nop
}
deterministic::~deterministic() {
// Note: we need clean up all remaining messages manually. This in turn may
// clean up actors as unreachable if the test did not consume all
// messages. Otherwise, the destructor of `sys` will wait for all
// actors, potentially waiting forever.
drop_events();
}
void deterministic::drop_events() {
// Note: We cannot just call `events_.clear()`, because that would potentially
// cause an actor to become unreachable and close its mailbox. This
// could call `pop_msg_impl` in turn, which then tries to alter the list
// while we're clearing it.
while (!events_.empty()) {
std::list<std::unique_ptr<scheduling_event>> tmp;
tmp.splice(tmp.end(), events_);
}
}
bool deterministic::prepone_event_impl(const strong_actor_ptr& receiver) {
actor_predicate any_sender{std::ignore};
message_predicate any_payload{std::ignore};
return prepone_event_impl(receiver, any_sender, any_payload);
}
bool deterministic::prepone_event_impl(
const strong_actor_ptr& receiver, actor_predicate& sender_pred,
abstract_message_predicate& payload_pred) {
if (events_.empty() || !receiver)
return false;
auto first = events_.begin();
auto last = events_.end();
auto i = std::find_if(first, last, [&](const auto& event) {
auto self = actor_cast<abstract_actor*>(receiver);
return event->target == static_cast<scheduled_actor*>(self)
&& sender_pred(event->item->sender)
&& payload_pred(event->item->payload);
});
if (i == last)
return false;
if (i != first) {
auto ptr = std::move(*i);
events_.erase(i);
events_.insert(events_.begin(), std::move(ptr));
}
return true;
}
deterministic::scheduling_event*
deterministic::find_event_impl(const strong_actor_ptr& receiver) {
if (events_.empty() || !receiver)
return nullptr;
auto last = events_.end();
auto i = std::find_if(events_.begin(), last, [&](const auto& event) {
auto raw_ptr = actor_cast<abstract_actor*>(receiver);
return event->target == static_cast<scheduled_actor*>(raw_ptr);
});
if (i != last)
return i->get();
return nullptr;
}
mailbox_element_ptr deterministic::pop_msg_impl(scheduled_actor* receiver) {
auto pred = [&](const auto& event) { return event->target == receiver; };
auto first = events_.begin();
auto last = events_.end();
auto i = std::find_if(first, last, pred);
if (i == last)
return nullptr;
auto result = std::move((*i)->item);
events_.erase(i);
return result;
}
size_t deterministic::mail_count(scheduled_actor* receiver) {
if (receiver == nullptr)
return 0;
auto pred = [&](const auto& event) { return event->target == receiver; };
return std::count_if(events_.begin(), events_.end(), pred);
}
size_t deterministic::mail_count(const strong_actor_ptr& receiver) {
auto raw_ptr = actor_cast<abstract_actor*>(receiver);
return mail_count(dynamic_cast<scheduled_actor*>(raw_ptr));
}
bool deterministic::dispatch_message() {
if (events_.empty())
return false;
if (events_.front()->item == nullptr) {
// Regular resumable.
auto ev = std::move(events_.front());
events_.pop_front();
ev->target->resume(sys.dummy_execution_unit(), 0);
return true;
}
// Actor: we simply resume the next actor and it will pick up its message.
auto next = events_.front()->target;
next->resume(sys.dummy_execution_unit(), 1);
return true;
}
size_t deterministic::dispatch_messages() {
size_t result = 0;
while (dispatch_message())
++result;
return result;
}
} // namespace caf::test
// 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.
#pragma once
#include "caf/test/runnable.hpp"
#include "caf/actor_system.hpp"
#include "caf/actor_system_config.hpp"
#include "caf/detail/source_location.hpp"
#include "caf/detail/test_actor_clock.hpp"
#include "caf/detail/test_export.hpp"
#include "caf/detail/type_traits.hpp"
#include "caf/scheduler/abstract_coordinator.hpp"
#include <list>
#include <memory>
namespace caf::test::fixture {
/// A fixture for writing unit tests that require deterministic scheduling. The
/// fixture equips tests with an actor system that uses a deterministic
/// scheduler and provides a DSL for writing high-level tests for message
/// passing between actors.
class CAF_TEST_EXPORT deterministic {
private:
// -- private member types (implementation details) --------------------------
class mailbox_impl;
class scheduler_impl;
class mailbox_factory_impl;
/// Wraps a resumable pointer and a mailbox element pointer.
struct scheduling_event {
scheduling_event(resumable* target, mailbox_element_ptr payload)
: target(target), item(std::move(payload)) {
// nop
}
/// The target of the event.
intrusive_ptr<resumable> target;
/// The message for the event or `nullptr` if the target is not an actor.
mailbox_element_ptr item;
};
/// A predicate for checking of single value. When constructing from a
/// reference wrapper, the predicate assigns the found value to the reference
/// instead of checking it.
template <class T>
class value_predicate {
public:
value_predicate() {
predicate_ = [](const T&) { return true; };
}
explicit value_predicate(decltype(std::ignore)) : value_predicate() {
// nop
}
template <class U>
explicit value_predicate(
U value, std::enable_if_t<detail::is_comparable_v<T, U>>* = nullptr) {
predicate_ = [value](const T& found) { return found == value; };
}
explicit value_predicate(std::reference_wrapper<T> ref) {
predicate_ = [ref](const T& found) {
ref.get() = found;
return true;
};
}
template <
class Predicate,
class = std::enable_if_t<std::is_same_v<
bool, decltype(std::declval<Predicate>()(std::declval<const T&>()))>>>
explicit value_predicate(Predicate predicate) {
predicate_ = std::move(predicate);
}
value_predicate(value_predicate&&) = default;
value_predicate(const value_predicate&) = default;
value_predicate& operator=(value_predicate&&) = default;
value_predicate& operator=(const value_predicate&) = default;
bool operator()(const T& value) {
return predicate_(value);
}
private:
std::function<bool(const T&)> predicate_;
};
/// Convenience alias for predicates on strong actor pointers.
using actor_predicate = value_predicate<strong_actor_ptr>;
/// Abstract base type for message predicates.
class abstract_message_predicate {
public:
virtual ~abstract_message_predicate();
virtual bool operator()(const message&) = 0;
};
/// A predicate for checking type and (optionally) content of a message.
template <class... Ts>
class message_predicate : public abstract_message_predicate {
public:
template <class U, class... Us>
explicit message_predicate(U x, Us... xs) {
predicates_ = std::make_shared<predicates_tuple>(std::move(x),
std::move(xs)...);
}
explicit message_predicate(decltype(std::ignore)) {
// nop
}
message_predicate() {
predicates_ = std::make_shared<predicates_tuple>();
}
message_predicate(message_predicate&&) = default;
message_predicate(const message_predicate&) = default;
message_predicate& operator=(message_predicate&&) = default;
message_predicate& operator=(const message_predicate&) = default;
bool operator()(const message& msg) {
if (!predicates_)
return true;
if (auto view = make_const_typed_message_view<Ts...>(msg))
return do_check(view, std::index_sequence_for<Ts...>{});
return false;
}
private:
template <size_t... Is>
bool do_check([[maybe_unused]] const_typed_message_view<Ts...> view, //
std::index_sequence<Is...>) {
return (((std::get<Is>(*predicates_))(get<Is>(view))) && ...);
}
using predicates_tuple = std::tuple<value_predicate<Ts>...>;
std::shared_ptr<predicates_tuple> predicates_;
};
public:
// -- public member types ----------------------------------------------------
/// The configuration type for this fixture.
class config : public actor_system_config {
public:
config(deterministic* fix);
~config() override;
private:
detail::mailbox_factory* mailbox_factory() override;
std::unique_ptr<detail::mailbox_factory> factory_;
};
/// Configures the algorithm to evaluate for an `evaluator` instances.
enum class evaluator_algorithm {
expect,
allow,
prepone,
prepone_and_expect,
prepone_and_allow
};
/// Provides a fluent interface for matching messages. The `evaluator` allows
/// setting `from` and `with` parameters for an algorithm that matches
/// messages against a predicate. When setting the only mandatory parameter
/// `to`, the `evaluator` evaluates the predicate against the next message
/// in the mailbox of the target actor.
template <class... Ts>
class evaluator {
public:
evaluator(deterministic* fix, detail::source_location loc,
evaluator_algorithm algorithm)
: fix_(fix), loc_(loc), algo_(algorithm) {
// nop
}
evaluator() = delete;
evaluator(evaluator&&) noexcept = default;
evaluator& operator=(evaluator&&) noexcept = default;
evaluator(const evaluator&) = delete;
evaluator& operator=(const evaluator&) = delete;
/// Matches the values of the message. The evaluator will match a message
/// only if all individual values match the corresponding predicate.
///
/// The template parameter pack `xs...` contains a list of match expressions
/// that all must evaluate to true for a message to match. For each match
/// expression:
///
/// - Passing a value creates a predicate that matches the value exactly.
/// - Passing a predicate (a function object taking one argument and
/// returning `bool`) will match any value for which the predicate returns
/// `true`.
/// - Passing `std::ignore` accepts any value at that position.
/// - Passing a `std::reference_wrapper<T>` will match any value and stores
/// the value in the reference wrapper.
template <class... Us>
evaluator&& with(Us... xs) && {
static_assert(sizeof...(Ts) == sizeof...(Us));
static_assert((std::is_constructible_v<value_predicate<Ts>, Us> && ...));
with_ = message_predicate<Ts...>(std::move(xs)...);
return std::move(*this);
}
/// Adds a predicate for the sender of the next message that matches only if
/// the sender is `src`.
evaluator&& from(const strong_actor_ptr& src) && {
from_ = value_predicate<strong_actor_ptr>{std::move(src)};
return std::move(*this);
}
/// Adds a predicate for the sender of the next message that matches only if
/// the sender is `src`.
evaluator&& from(const actor& src) && {
from_ = value_predicate<strong_actor_ptr>{std::move(src)};
return std::move(*this);
}
/// Adds a predicate for the sender of the next message that matches only if
/// the sender is `src`.
template <class... Us>
evaluator&& from(const typed_actor<Us...>& src) && {
from_ = value_predicate<strong_actor_ptr>{std::move(src)};
return std::move(*this);
}
/// Adds a predicate for the sender of the next message that matches only
/// anonymous messages, i.e., messages without a sender.
evaluator&& from(std::nullptr_t) && {
return std::move(*this).from(strong_actor_ptr{});
}
/// Causes the evaluator to store the sender of a matched message in `src`.
evaluator&& from(std::reference_wrapper<strong_actor_ptr> src) && {
from_ = value_predicate<strong_actor_ptr>{src};
return std::move(*this);
}
/// Sets the target actor for this evaluator and evaluate the predicate.
template <class T>
bool to(const T& dst) && {
auto dst_ptr = actor_cast<strong_actor_ptr>(dst);
switch (algo_) {
case evaluator_algorithm::expect:
return eval_dispatch(dst_ptr, true);
case evaluator_algorithm::allow:
return eval_dispatch(dst_ptr, false);
case evaluator_algorithm::prepone:
return eval_prepone(dst_ptr);
case evaluator_algorithm::prepone_and_expect:
eval_prepone(dst_ptr);
return eval_dispatch(dst_ptr, true);
case evaluator_algorithm::prepone_and_allow:
return eval_prepone(dst_ptr) && eval_dispatch(dst_ptr, false);
default:
CAF_RAISE_ERROR(std::logic_error, "invalid algorithm");
}
}
private:
using predicates = std::tuple<value_predicate<Ts>...>;
bool eval_dispatch(const strong_actor_ptr& dst, bool fail_on_mismatch) {
auto& ctx = runnable::current();
auto* event = fix_->find_event_impl(dst);
if (!event) {
if (fail_on_mismatch)
ctx.fail({"no matching message found", loc_});
return false;
}
if (!from_(event->item->sender) || !with_(event->item->payload)) {
if (fail_on_mismatch)
ctx.fail({"no matching message found", loc_});
return false;
}
fix_->prepone_event_impl(dst);
if (fail_on_mismatch) {
if (!fix_->dispatch_message())
ctx.fail({"failed to dispatch message", loc_});
reporter::instance().pass(loc_);
return true;
}
return fix_->dispatch_message();
}
bool eval_prepone(const strong_actor_ptr& dst) {
return fix_->prepone_event_impl(dst, from_, with_);
}
deterministic* fix_;
detail::source_location loc_;
evaluator_algorithm algo_;
actor_predicate from_;
message_predicate<Ts...> with_;
};
// -- friends ----------------------------------------------------------------
friend class mailbox_impl;
friend class scheduler_impl;
template <class... Ts>
friend class evaluator;
// -- constructors, destructors, and assignment operators --------------------
deterministic();
~deterministic();
// -- properties -------------------------------------------------------------
/// Returns the number of pending messages for `receiver`.
size_t mail_count(scheduled_actor* receiver);
/// Returns the number of pending messages for `receiver`.
size_t mail_count(const strong_actor_ptr& receiver);
/// Returns the number of pending messages for `receiver`.
size_t mail_count(const actor& receiver) {
return mail_count(actor_cast<strong_actor_ptr>(receiver));
}
/// Returns the number of pending messages for `receiver`.
template <class... Ts>
size_t mail_count(const typed_actor<Ts...>& receiver) {
return mail_count(actor_cast<strong_actor_ptr>(receiver));
}
// -- control flow -----------------------------------------------------------
/// Tries to dispatch a single message.
bool dispatch_message();
/// Dispatches all pending messages.
size_t dispatch_messages();
// -- message-based predicates -----------------------------------------------
/// Expects a message with types `Ts...` as the next message in the mailbox of
/// the receiver and aborts the test if the message is missing. Otherwise
/// executes the message.
template <class... Ts>
auto expect(const detail::source_location& loc
= detail::source_location::current()) {
return evaluator<Ts...>{this, loc, evaluator_algorithm::expect};
}
/// Tries to match a message with types `Ts...` and executes it if it is the
/// next message in the mailbox of the receiver.
template <class... Ts>
auto allow(const detail::source_location& loc
= detail::source_location::current()) {
return evaluator<Ts...>{this, loc, evaluator_algorithm::allow};
}
/// Tries to prepone a message, i.e., reorders the messages in the mailbox of
/// the receiver such that the next call to `dispatch_message` will run it.
/// @returns `true` if the message could be preponed, `false` otherwise.
template <class... Ts>
auto prepone(const detail::source_location& loc
= detail::source_location::current()) {
return evaluator<Ts...>{this, loc, evaluator_algorithm::prepone};
}
/// Shortcut for calling `prepone` and then `expect` with the same arguments.
template <class... Ts>
auto prepone_and_expect(const detail::source_location& loc
= detail::source_location::current()) {
return evaluator<Ts...>{this, loc, evaluator_algorithm::prepone_and_expect};
}
/// Shortcut for calling `prepone` and then `allow` with the same arguments.
template <class... Ts>
auto prepone_and_allow(const detail::source_location& loc
= detail::source_location::current()) {
return evaluator<Ts...>{this, loc, evaluator_algorithm::prepone_and_allow};
}
// -- member variables -------------------------------------------------------
private:
// Note: this is put here because this member variable must be destroyed
// *after* the actor system (and thus must come before `sys` in
// the declaration order).
/// Stores all pending messages of scheduled actors.
std::list<std::unique_ptr<scheduling_event>> events_;
public:
/// Configures the actor system with deterministic scheduling.
config cfg;
/// The actor system instance for the tests.
actor_system sys;
private:
/// Removes all events from the queue.
void drop_events();
/// Tries find a message in `events_` that matches the given predicate and
/// moves it to the front of the queue.
bool prepone_event_impl(const strong_actor_ptr& receiver);
/// Tries find a message in `events_` that matches the given predicates and
/// moves it to the front of the queue.
bool prepone_event_impl(const strong_actor_ptr& receiver,
actor_predicate& sender_pred,
abstract_message_predicate& payload_pred);
/// Returns the next event for `receiver` or `nullptr` if there is none.
scheduling_event* find_event_impl(const strong_actor_ptr& receiver);
/// Removes the next message for `receiver` from the queue and returns it.
mailbox_element_ptr pop_msg_impl(scheduled_actor* receiver);
};
} // namespace caf::test
// 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.
#include "caf/test/fixture/deterministic.hpp"
#include "caf/test/caf_test_main.hpp"
#include "caf/test/test.hpp"
#include "caf/event_based_actor.hpp"
#include "caf/scoped_actor.hpp"
#include "caf/send.hpp"
namespace fixture = caf::test::fixture;
struct my_int {
int value;
};
template <class Inspector>
auto inspect(Inspector& f, my_int& x) {
return f.object(x).fields(f.field("value", x.value));
}
bool operator==(my_int lhs, my_int rhs) noexcept {
return lhs.value == rhs.value;
}
bool operator==(my_int lhs, int rhs) noexcept {
return lhs.value == rhs;
}
bool operator==(int lhs, my_int rhs) noexcept {
return lhs == rhs.value;
}
bool operator!=(my_int lhs, my_int rhs) noexcept {
return lhs.value != rhs.value;
}
CAF_BEGIN_TYPE_ID_BLOCK(test, caf::first_custom_type_id)
CAF_ADD_TYPE_ID(test, (my_int))
CAF_END_TYPE_ID_BLOCK(test)
WITH_FIXTURE(fixture::deterministic) {
TEST("the deterministic fixture provides a deterministic scheduler") {
auto initialized = std::make_shared<bool>(false);
auto count = std::make_shared<int32_t>(0);
auto worker = sys.spawn([initialized, count] {
*initialized = true;
return caf::behavior{
[count](int32_t value) { *count += value; },
[count](const std::string& str) {
if (auto ival = caf::get_as<int32_t>(caf::config_value{str}))
*count += *ival;
},
};
});
caf::scoped_actor self{sys};
check(*initialized);
check_eq(mail_count(worker), 0u);
anon_send(worker, 1);
check_eq(mail_count(worker), 1u);
self->send(worker, 2);
check_eq(mail_count(worker), 2u);
anon_send(worker, 3);
check_eq(mail_count(worker), 3u);
SECTION("calling dispatch_message processes a single message") {
check(dispatch_message());
check_eq(mail_count(worker), 2u);
check_eq(*count, 1);
check(dispatch_message());
check_eq(mail_count(worker), 1u);
check_eq(*count, 3);
check(dispatch_message());
check_eq(mail_count(worker), 0u);
check_eq(*count, 6);
check(!dispatch_message());
}
SECTION("calling dispatch_messages processes all messages") {
check_eq(dispatch_messages(), 3u);
check_eq(*count, 6);
}
#ifdef CAF_ENABLE_EXCEPTIONS
SECTION("expect() checks for required messages") {
expect<int32_t>().to(worker);
check_eq(mail_count(worker), 2u);
check_eq(*count, 1);
expect<int32_t>().to(worker);
check_eq(mail_count(worker), 1u);
check_eq(*count, 3);
expect<int32_t>().to(worker);
check_eq(mail_count(worker), 0u);
check_eq(*count, 6);
should_fail_with_exception([this, worker] { //
expect<int32_t>().with(3).to(worker);
});
}
SECTION("expect() matches the types of the next message") {
anon_send(worker, "4");
should_fail_with_exception([this, worker] { //
expect<std::string>().to(worker);
});
should_fail_with_exception([this, worker] { //
expect<int32_t, int32_t>().to(worker);
});
check_eq(*count, 0);
check_eq(dispatch_messages(), 4u);
check_eq(*count, 10);
}
SECTION("expect() optionally matches the content of the next message") {
should_fail_with_exception([this, worker] { //
expect<int32_t>().with(3).to(worker);
});
check_eq(*count, 0);
expect<int32_t>().with(1).to(worker);
expect<int32_t>().with(2).to(worker);
expect<int32_t>().with(3).to(worker);
check_eq(*count, 6);
}
SECTION("expect() optionally matches the sender of the next message") {
// First message has no sender.
should_fail_with_exception([this, worker, &self] { //
expect<int32_t>().from(self).to(worker);
});
check_eq(*count, 0);
expect<int32_t>().from(nullptr).to(worker);
check_eq(*count, 1);
// Second message is from self.
should_fail_with_exception([this, worker] { //
expect<int32_t>().from(nullptr).to(worker);
});
check_eq(*count, 1);
expect<int32_t>().from(self).to(worker);
check_eq(*count, 3);
}
SECTION("prepone_and_expect() processes out of order based on types") {
anon_send(worker, "4");
prepone_and_expect<std::string>().to(worker);
check_eq(*count, 4);
should_fail_with_exception([this, worker] { //
prepone_and_expect<std::string>().to(worker);
});
check_eq(*count, 4);
check_eq(dispatch_messages(), 3u);
check_eq(*count, 10);
}
SECTION("prepone_and_expect() processes out of order based on content") {
prepone_and_expect<int32_t>().with(3).to(worker);
check_eq(mail_count(worker), 2u);
check_eq(*count, 3);
should_fail_with_exception([this, worker] { //
prepone_and_expect<int32_t>().with(3).to(worker);
});
check_eq(mail_count(worker), 2u);
check_eq(*count, 3);
prepone_and_expect<int32_t>().with(1).to(worker);
check_eq(*count, 4);
prepone_and_expect<int32_t>().with(2).to(worker);
check_eq(*count, 6);
check(!dispatch_message());
}
SECTION("prepone_and_expect() processes out of order based on senders") {
prepone_and_expect<int32_t>().from(self).to(worker);
check_eq(mail_count(worker), 2u);
check_eq(*count, 2);
should_fail_with_exception([this, worker, &self] { //
prepone_and_expect<int32_t>().from(self).to(worker);
});
check_eq(mail_count(worker), 2u);
check_eq(*count, 2);
prepone_and_expect<int32_t>().from(nullptr).to(worker);
check_eq(*count, 3);
prepone_and_expect<int32_t>().from(nullptr).to(worker);
check_eq(*count, 6);
check(!dispatch_message());
}
#endif // CAF_ENABLE_EXCEPTIONS
SECTION("allow() checks for optional messages") {
check(allow<int32_t>().to(worker));
check_eq(mail_count(worker), 2u);
check_eq(*count, 1);
check(allow<int32_t>().to(worker));
check_eq(mail_count(worker), 1u);
check_eq(*count, 3);
check(allow<int32_t>().to(worker));
check_eq(mail_count(worker), 0u);
check_eq(*count, 6);
check(!allow<int32_t>().with(3).to(worker));
}
SECTION("allow() matches the types of the next message") {
anon_send(worker, "4");
check(!allow<std::string>().to(worker));
check(!allow<int32_t, int32_t>().to(worker));
check_eq(*count, 0);
check_eq(dispatch_messages(), 4u);
check_eq(*count, 10);
}
SECTION("allow() optionally matches the content of the next message") {
check(!allow<int32_t>().with(3).to(worker));
check_eq(*count, 0);
check(allow<int32_t>().with(1).to(worker));
check(allow<int32_t>().with(2).to(worker));
check(allow<int32_t>().with(3).to(worker));
check_eq(*count, 6);
}
SECTION("allow() optionally matches the sender of the next message") {
// First message has no sender.
check(!allow<int32_t>().from(self).to(worker));
check_eq(*count, 0);
check(allow<int32_t>().from(nullptr).to(worker));
check_eq(*count, 1);
// Second message is from self.
check(!allow<int32_t>().from(nullptr).to(worker));
check_eq(*count, 1);
check(allow<int32_t>().from(self).to(worker));
check_eq(*count, 3);
}
SECTION("prepone_and_allow() checks for optional messages") {
check(prepone_and_allow<int32_t>().to(worker));
check_eq(mail_count(worker), 2u);
check_eq(*count, 1);
check(prepone_and_allow<int32_t>().to(worker));
check_eq(mail_count(worker), 1u);
check_eq(*count, 3);
check(prepone_and_allow<int32_t>().to(worker));
check_eq(mail_count(worker), 0u);
check_eq(*count, 6);
check(!prepone_and_allow<int32_t>().with(3).to(worker));
}
SECTION("prepone_and_allow() processes out of order based on types") {
anon_send(worker, "4");
check(prepone_and_allow<std::string>().to(worker));
check(!prepone_and_allow<std::string>().to(worker));
check_eq(*count, 4);
check_eq(dispatch_messages(), 3u);
check_eq(*count, 10);
}
SECTION("prepone_and_allow() processes out of order based on content") {
check(prepone_and_allow<int32_t>().with(3).to(worker));
check_eq(mail_count(worker), 2u);
check_eq(*count, 3);
check(!prepone_and_allow<int32_t>().with(3).to(worker));
check_eq(mail_count(worker), 2u);
check_eq(*count, 3);
check(prepone_and_allow<int32_t>().with(1).to(worker));
check_eq(*count, 4);
check(prepone_and_allow<int32_t>().with(2).to(worker));
check_eq(*count, 6);
check(!dispatch_message());
}
SECTION("prepone_and_allow() processes out of order based on senders") {
check(prepone_and_allow<int32_t>().from(self).to(worker));
check_eq(mail_count(worker), 2u);
check_eq(*count, 2);
check(!prepone_and_allow<int32_t>().from(self).to(worker));
check_eq(mail_count(worker), 2u);
check_eq(*count, 2);
check(prepone_and_allow<int32_t>().from(nullptr).to(worker));
check_eq(*count, 3);
check(prepone_and_allow<int32_t>().from(nullptr).to(worker));
check_eq(*count, 6);
check(!dispatch_message());
}
SECTION("prepone_and_allow() ignores non-existent combinations") {
// There is no message with content (4).
check(!prepone_and_allow<int32_t>().with(4).from(self).to(worker));
// There is no message with content (3) from self.
check(!prepone_and_allow<int32_t>().with(3).from(self).to(worker));
// The original order should be preserved.
check(dispatch_message());
check_eq(*count, 1);
check(dispatch_message());
check_eq(*count, 3);
check(dispatch_message());
check_eq(*count, 6);
}
}
TEST("evaluator expressions can check or extract individual values") {
auto worker = sys.spawn([](caf::event_based_actor* self) -> caf::behavior {
self->set_default_handler(caf::drop);
return {
[](int32_t) {},
};
});
SECTION("omitting with() matches on the types only") {
anon_send(worker, 1);
check(!allow<std::string>().to(worker));
check(allow<int>().to(worker));
anon_send(worker, 1, "two", 3.0);
check(!allow<int>().to(worker));
check(allow<int, std::string, double>().to(worker));
anon_send(worker, 42, "hello world", 7.7);
check(allow<int, std::string, double>().to(worker));
}
SECTION("reference wrappers turn evaluators into extractors") {
auto tmp = 0;
anon_send(worker, 1);
check(allow<int>().with(std::ref(tmp)).to(worker));
check_eq(tmp, 1);
}
SECTION("std::ignore matches any value") {
anon_send(worker, 1);
check(allow<int>().with(std::ignore).to(worker));
anon_send(worker, 2);
check(allow<int>().with(std::ignore).to(worker));
anon_send(worker, 3);
check(allow<int>().with(std::ignore).to(worker));
anon_send(worker, 1, 2, 3);
check(!allow<int, int, int>().with(1, std::ignore, 4).to(worker));
check(!allow<int, int, int>().with(2, std::ignore, 3).to(worker));
check(allow<int, int, int>().with(1, std::ignore, 3).to(worker));
}
SECTION("value predicates allow user-defined types") {
anon_send(worker, my_int{1});
check(allow<my_int>().to(worker));
anon_send(worker, my_int{1});
check(allow<my_int>().with(std::ignore).to(worker));
anon_send(worker, my_int{1});
check(!allow<my_int>().with(my_int{2}).to(worker));
check(allow<my_int>().with(my_int{1}).to(worker));
anon_send(worker, my_int{1});
check(allow<my_int>().with(1).to(worker));
auto tmp = my_int{0};
anon_send(worker, my_int{42});
check(allow<my_int>().with(std::ref(tmp)).to(worker));
check_eq(tmp.value, 42);
}
SECTION("value predicates allow user-defined predicates") {
auto le2 = [](my_int x) { return x.value <= 2; };
anon_send(worker, my_int{1});
check(allow<my_int>().with(le2).to(worker));
anon_send(worker, my_int{2});
check(allow<my_int>().with(le2).to(worker));
anon_send(worker, my_int{3});
check(!allow<my_int>().with(le2).to(worker));
}
}
} // WITH_FIXTURE(fixture::deterministic)
CAF_TEST_MAIN(caf::id_block::test)
...@@ -22,8 +22,6 @@ reporter::~reporter() { ...@@ -22,8 +22,6 @@ reporter::~reporter() {
// nop // nop
} }
reporter* reporter::instance;
namespace { namespace {
/// Implements a mini-DSL for colored output: /// Implements a mini-DSL for colored output:
...@@ -334,8 +332,8 @@ public: ...@@ -334,8 +332,8 @@ public:
set_live(); set_live();
format_to(colored(), format_to(colored(),
"{0:{1}}$R(error): check failed\n" "{0:{1}}$R(error): check failed\n"
"{0:{1}} loc: $C({3}):$Y({4})$0\n" "{0:{1}} loc: $C({2}):$Y({3})$0\n"
"{0:{1}} check: {5}\n", "{0:{1}} check: {4}\n",
' ', indent_, location.file_name(), location.line(), arg); ' ', indent_, location.file_name(), location.line(), arg);
} }
...@@ -388,8 +386,10 @@ public: ...@@ -388,8 +386,10 @@ public:
' ', indent_, location.file_name(), location.line(), msg); ' ', indent_, location.file_name(), location.line(), msg);
} }
void verbosity(unsigned level) override { unsigned verbosity(unsigned level) override {
auto result = level_;
level_ = level; level_ = level;
return result;
} }
void no_colors(bool new_value) override { void no_colors(bool new_value) override {
...@@ -470,8 +470,20 @@ private: ...@@ -470,8 +470,20 @@ private:
context_ptr current_ctx_; context_ptr current_ctx_;
}; };
reporter* global_instance;
} // namespace } // namespace
reporter& reporter::instance() {
if (global_instance == nullptr)
CAF_RAISE_ERROR("no reporter instance available");
return *global_instance;
}
void reporter::instance(reporter* ptr) {
global_instance = ptr;
}
std::unique_ptr<reporter> reporter::make_default() { std::unique_ptr<reporter> reporter::make_default() {
return std::make_unique<default_reporter>(); return std::make_unique<default_reporter>();
} }
......
...@@ -75,7 +75,8 @@ public: ...@@ -75,7 +75,8 @@ public:
info(std::string_view msg, const detail::source_location& location) info(std::string_view msg, const detail::source_location& location)
= 0; = 0;
virtual void verbosity(unsigned level) = 0; /// Sets the verbosity level of the reporter and returns the previous value.
virtual unsigned verbosity(unsigned level) = 0;
/// Sets whether the reporter disables colored output even when writing to a /// Sets whether the reporter disables colored output even when writing to a
/// TTY. /// TTY.
...@@ -93,8 +94,11 @@ public: ...@@ -93,8 +94,11 @@ public:
/// Returns statistics for the entire run. /// Returns statistics for the entire run.
virtual stats total_stats() = 0; virtual stats total_stats() = 0;
/// Stores a pointer to the currently active reporter. /// Returns the registered reporter instance.
static reporter* instance; static reporter& instance();
/// Sets the reporter instance for the current test run.
static void instance(reporter* ptr);
/// Creates a default reporter that writes to the standard output. /// Creates a default reporter that writes to the standard output.
static std::unique_ptr<reporter> make_default(); static std::unique_ptr<reporter> make_default();
......
...@@ -10,13 +10,23 @@ ...@@ -10,13 +10,23 @@
#include "caf/test/scope.hpp" #include "caf/test/scope.hpp"
#include "caf/test/test.hpp" #include "caf/test/test.hpp"
#include "caf/detail/scope_guard.hpp"
namespace caf::test { namespace caf::test {
namespace {
thread_local runnable* current_runnable;
} // namespace
runnable::~runnable() { runnable::~runnable() {
// nop // nop
} }
void runnable::run() { void runnable::run() {
current_runnable = this;
auto guard = detail::make_scope_guard([] { current_runnable = nullptr; });
switch (root_type_) { switch (root_type_) {
case block_type::scenario: case block_type::scenario:
if (auto guard = ctx_->get<scenario>(0, description_, loc_)->commit()) { if (auto guard = ctx_->get<scenario>(0, description_, loc_)->commit()) {
...@@ -41,13 +51,20 @@ void runnable::run() { ...@@ -41,13 +51,20 @@ void runnable::run() {
bool runnable::check(bool value, const detail::source_location& location) { bool runnable::check(bool value, const detail::source_location& location) {
if (value) { if (value) {
reporter::instance->pass(location); reporter::instance().pass(location);
} else { } else {
reporter::instance->fail("should be true", location); reporter::instance().fail("should be true", location);
} }
return value; return value;
} }
runnable& runnable::current() {
auto ptr = current_runnable;
if (!ptr)
CAF_RAISE_ERROR(std::logic_error, "no current runnable");
return *ptr;
}
block& runnable::current_block() { block& runnable::current_block() {
if (ctx_->call_stack.empty()) if (ctx_->call_stack.empty())
CAF_RAISE_ERROR(std::logic_error, "no current block"); CAF_RAISE_ERROR(std::logic_error, "no current block");
......
...@@ -12,8 +12,10 @@ ...@@ -12,8 +12,10 @@
#include "caf/config.hpp" #include "caf/config.hpp"
#include "caf/deep_to_string.hpp" #include "caf/deep_to_string.hpp"
#include "caf/detail/format.hpp" #include "caf/detail/format.hpp"
#include "caf/detail/scope_guard.hpp"
#include "caf/detail/source_location.hpp" #include "caf/detail/source_location.hpp"
#include "caf/detail/test_export.hpp" #include "caf/detail/test_export.hpp"
#include "caf/raise_error.hpp"
#include <string_view> #include <string_view>
...@@ -42,14 +44,26 @@ public: ...@@ -42,14 +44,26 @@ public:
/// Runs the next branch of the test. /// Runs the next branch of the test.
void run(); void run();
/// Generates a message with the INFO severity level.
template <class... Ts>
[[noreturn]] void fail(detail::format_string_with_location fwl, Ts&&... xs) {
if constexpr (sizeof...(Ts) > 0) {
auto msg = detail::format(fwl.value, std::forward<Ts>(xs)...);
reporter::instance().fail(msg, fwl.location);
} else {
reporter::instance().fail(fwl.value, fwl.location);
}
CAF_RAISE_ERROR(std::logic_error, "requirement failed: abort test");
}
/// Generates a message with the INFO severity level. /// Generates a message with the INFO severity level.
template <class... Ts> template <class... Ts>
void info(detail::format_string_with_location fwl, Ts&&... xs) { void info(detail::format_string_with_location fwl, Ts&&... xs) {
if constexpr (sizeof...(Ts) > 0) { if constexpr (sizeof...(Ts) > 0) {
auto msg = detail::format(fwl.value, std::forward<Ts>(xs)...); auto msg = detail::format(fwl.value, std::forward<Ts>(xs)...);
reporter::instance->info(msg, fwl.location); reporter::instance().info(msg, fwl.location);
} else { } else {
reporter::instance->info(fwl.value, fwl.location); reporter::instance().info(fwl.value, fwl.location);
} }
} }
...@@ -60,11 +74,11 @@ public: ...@@ -60,11 +74,11 @@ public:
= detail::source_location::current()) { = detail::source_location::current()) {
assert_save_comparison<T0, T1>(); assert_save_comparison<T0, T1>();
if (lhs == rhs) { if (lhs == rhs) {
reporter::instance->pass(location); reporter::instance().pass(location);
return true; return true;
} }
reporter::instance->fail(binary_predicate::eq, stringify(lhs), reporter::instance().fail(binary_predicate::eq, stringify(lhs),
stringify(rhs), location); stringify(rhs), location);
return false; return false;
} }
...@@ -75,11 +89,11 @@ public: ...@@ -75,11 +89,11 @@ public:
= detail::source_location::current()) { = detail::source_location::current()) {
assert_save_comparison<T0, T1>(); assert_save_comparison<T0, T1>();
if (lhs != rhs) { if (lhs != rhs) {
reporter::instance->pass(location); reporter::instance().pass(location);
return true; return true;
} }
reporter::instance->fail(binary_predicate::ne, stringify(lhs), reporter::instance().fail(binary_predicate::ne, stringify(lhs),
stringify(rhs), location); stringify(rhs), location);
return false; return false;
} }
...@@ -90,11 +104,11 @@ public: ...@@ -90,11 +104,11 @@ public:
= detail::source_location::current()) { = detail::source_location::current()) {
assert_save_comparison<T0, T1>(); assert_save_comparison<T0, T1>();
if (lhs < rhs) { if (lhs < rhs) {
reporter::instance->pass(location); reporter::instance().pass(location);
return true; return true;
} }
reporter::instance->fail(binary_predicate::lt, stringify(lhs), reporter::instance().fail(binary_predicate::lt, stringify(lhs),
stringify(rhs), location); stringify(rhs), location);
return false; return false;
} }
...@@ -105,11 +119,11 @@ public: ...@@ -105,11 +119,11 @@ public:
= detail::source_location::current()) { = detail::source_location::current()) {
assert_save_comparison<T0, T1>(); assert_save_comparison<T0, T1>();
if (lhs <= rhs) { if (lhs <= rhs) {
reporter::instance->pass(location); reporter::instance().pass(location);
return true; return true;
} }
reporter::instance->fail(binary_predicate::le, stringify(lhs), reporter::instance().fail(binary_predicate::le, stringify(lhs),
stringify(rhs), location); stringify(rhs), location);
return false; return false;
} }
...@@ -120,11 +134,11 @@ public: ...@@ -120,11 +134,11 @@ public:
= detail::source_location::current()) { = detail::source_location::current()) {
assert_save_comparison<T0, T1>(); assert_save_comparison<T0, T1>();
if (lhs > rhs) { if (lhs > rhs) {
reporter::instance->pass(location); reporter::instance().pass(location);
return true; return true;
} }
reporter::instance->fail(binary_predicate::gt, stringify(lhs), reporter::instance().fail(binary_predicate::gt, stringify(lhs),
stringify(rhs), location); stringify(rhs), location);
return false; return false;
} }
...@@ -135,58 +149,118 @@ public: ...@@ -135,58 +149,118 @@ public:
= detail::source_location::current()) { = detail::source_location::current()) {
assert_save_comparison<T0, T1>(); assert_save_comparison<T0, T1>();
if (lhs >= rhs) { if (lhs >= rhs) {
reporter::instance->pass(location); reporter::instance().pass(location);
return true; return true;
} }
reporter::instance->fail(binary_predicate::ge, stringify(lhs), reporter::instance().fail(binary_predicate::ge, stringify(lhs),
stringify(rhs), location); stringify(rhs), location);
return false; return false;
} }
/// Checks whether `value` is `true`.
bool check(bool value, const detail::source_location& location bool check(bool value, const detail::source_location& location
= detail::source_location::current()); = detail::source_location::current());
/// Returns the `runnable` instance that is currently running.
static runnable& current();
block& current_block(); block& current_block();
template <class Expr>
void should_fail(Expr&& expr, const caf::detail::source_location& location
= caf::detail::source_location::current()) {
auto& rep = reporter::instance();
auto lvl = rep.verbosity(CAF_LOG_LEVEL_QUIET);
auto before = rep.test_stats();
{
auto lvl_guard = detail::make_scope_guard([&] { rep.verbosity(lvl); });
expr();
}
auto after = rep.test_stats();
auto passed_count_ok = before.passed == after.passed;
auto failed_count_ok = before.failed + 1 == after.failed;
if (passed_count_ok && failed_count_ok) {
reporter::instance().pass(location);
rep.test_stats({before.passed + 1, before.failed});
} else {
reporter::instance().fail("nested check should fail", location);
rep.test_stats({before.passed, before.failed + 1});
}
}
#ifdef CAF_ENABLE_EXCEPTIONS #ifdef CAF_ENABLE_EXCEPTIONS
template <class Exception = void, class CodeBlock>
void check_throws(CodeBlock&& fn, const detail::source_location& location /// Checks whether `expr()` throws an exception of type `Exception`.
= detail::source_location::current()) { template <class Exception = void, class Expr>
void check_throws(Expr&& expr, const detail::source_location& location
= detail::source_location::current()) {
if constexpr (std::is_same_v<Exception, void>) { if constexpr (std::is_same_v<Exception, void>) {
try { try {
fn(); expr();
} catch (...) { } catch (...) {
reporter::instance->pass(location); reporter::instance().pass(location);
return; return;
} }
reporter::instance->fail("throws", location); reporter::instance().fail("throws", location);
} else { } else {
try { try {
fn(); expr();
} catch (const Exception&) { } catch (const Exception&) {
reporter::instance->pass(location); reporter::instance().pass(location);
return; return;
} catch (...) { } catch (...) {
reporter::instance->fail("throws Exception", location); reporter::instance().fail("throws Exception", location);
return; return;
} }
reporter::instance->fail("throws Exception", location); reporter::instance().fail("throws Exception", location);
} }
} }
#endif
template <class Expr> /// Checks whether `expr()` throws an exception of type `Exception` and
void should_fail(Expr&& expr, const caf::detail::source_location& location /// increases the failure count.
= caf::detail::source_location::current()) { template <class Exception = void, class Expr>
auto* rep = reporter::instance; void should_fail_with_exception(Expr&& expr,
auto before = rep->test_stats(); const caf::detail::source_location& location
expr(); = caf::detail::source_location::current()) {
auto after = rep->test_stats(); auto& rep = reporter::instance();
check_eq(before.passed, after.passed, location); auto before = rep.test_stats();
if (check_eq(before.failed + 1, after.failed, location)) auto lvl = rep.verbosity(CAF_LOG_LEVEL_QUIET);
rep->test_stats({before.passed + 1, before.failed}); auto caught = false;
if constexpr (std::is_same_v<Exception, void>) {
try {
expr();
} catch (...) {
caught = true;
}
} else {
try {
expr();
} catch (const Exception&) {
caught = true;
} catch (...) {
// TODO: print error message
}
}
rep.verbosity(lvl);
auto after = rep.test_stats();
auto passed_count_ok = before.passed == after.passed;
auto failed_count_ok = before.failed + 1 == after.failed;
if (caught && passed_count_ok && failed_count_ok) {
reporter::instance().pass(location);
rep.test_stats({before.passed + 1, before.failed});
} else {
if (!caught) {
reporter::instance().fail("nested check should throw an Exception",
location);
} else if (!passed_count_ok || !failed_count_ok) {
reporter::instance().fail("nested check should fail", location);
}
rep.test_stats({before.passed, before.failed + 1});
}
} }
#endif
protected: protected:
context_ptr ctx_; context_ptr ctx_;
std::string_view description_; std::string_view description_;
......
...@@ -131,7 +131,7 @@ runner::runner() : suites_(caf::test::registry::suites()) { ...@@ -131,7 +131,7 @@ runner::runner() : suites_(caf::test::registry::suites()) {
int runner::run(int argc, char** argv) { int runner::run(int argc, char** argv) {
auto default_reporter = reporter::make_default(); auto default_reporter = reporter::make_default();
reporter::instance = default_reporter.get(); reporter::instance(default_reporter.get());
if (auto [ok, help_printed] = parse_cli(argc, argv); !ok) { if (auto [ok, help_printed] = parse_cli(argc, argv); !ok) {
return EXIT_FAILURE; return EXIT_FAILURE;
} else if (help_printed) { } else if (help_printed) {
...@@ -240,7 +240,7 @@ runner::parse_cli_result runner::parse_cli(int argc, char** argv) { ...@@ -240,7 +240,7 @@ runner::parse_cli_result runner::parse_cli(int argc, char** argv) {
*verbosity); *verbosity);
return {false, true}; return {false, true};
} }
reporter::instance->verbosity(*level); reporter::instance().verbosity(*level);
} }
return {true, false}; return {true, false};
} }
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
using caf::test::block_type; using caf::test::block_type;
TEST("tests can contain different types of checks") { TEST("tests can contain different types of checks") {
auto* rep = caf::test::reporter::instance; auto& rep = caf::test::reporter::instance();
auto stats = rep->test_stats(); auto stats = rep.test_stats();
SECTION("check_ne checks for inequality") { SECTION("check_ne checks for inequality") {
check_ne(0, 1); check_ne(0, 1);
should_fail([this]() { check_ne(0, 0); }); should_fail([this]() { check_ne(0, 0); });
...@@ -38,16 +38,16 @@ TEST("tests can contain different types of checks") { ...@@ -38,16 +38,16 @@ TEST("tests can contain different types of checks") {
should_fail([this]() { check_lt(1, 1); }); should_fail([this]() { check_lt(1, 1); });
should_fail([this]() { check_lt(2, 1); }); should_fail([this]() { check_lt(2, 1); });
} }
info("this test had {} checks", rep->test_stats().total()); info("this test had {} checks", rep.test_stats().total());
} }
TEST("failed checks increment the failed counter") { TEST("failed checks increment the failed counter") {
check_eq(1, 2); check_eq(1, 2);
auto stats = caf::test::reporter::instance->test_stats(); auto stats = caf::test::reporter::instance().test_stats();
check_eq(stats.passed, 0u); check_eq(stats.passed, 0u);
check_eq(stats.failed, 1u); check_eq(stats.failed, 1u);
info("reset error count to not fail the test"); info("reset error count to not fail the test");
caf::test::reporter::instance->test_stats({2, 0}); caf::test::reporter::instance().test_stats({2, 0});
} }
TEST("each run starts with fresh local variables") { TEST("each run starts with fresh local variables") {
......
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