Commit 86d0fb7e authored by Dominik Charousset's avatar Dominik Charousset

More op::buffer coverage; make test code private

parent 335d3a2d
...@@ -145,6 +145,18 @@ public: ...@@ -145,6 +145,18 @@ public:
int compare(uint8_t code, type_id_t category) const noexcept; int compare(uint8_t code, type_id_t category) const noexcept;
/// Returns a copy of `this` if `!empty()` or else returns a new error from
/// given arguments.
template <class Enum, class... Ts>
error or_else(Enum code, Ts&&... args) const {
if (!empty())
return *this;
if constexpr (sizeof...(Ts) > 0)
return error{code, make_message(std::forward<Ts>(args)...)};
else
return error{code};
}
// -- modifiers -------------------------------------------------------------- // -- modifiers --------------------------------------------------------------
/// Reverts this error to "not an error" as if calling `*this = error{}`. /// Reverts this error to "not an error" as if calling `*this = error{}`.
......
...@@ -522,133 +522,4 @@ private: ...@@ -522,133 +522,4 @@ private:
Token token_; Token token_;
}; };
/// An observer with minimal internal logic. Useful for writing unit tests.
template <class T>
class passive_observer : public observer_impl_base<T> {
public:
// -- implementation of observer_impl<T> -------------------------------------
void on_complete() override {
if (sub) {
sub.dispose();
sub = nullptr;
}
state = observer_state::completed;
}
void on_error(const error& what) override {
if (sub) {
sub.dispose();
sub = nullptr;
}
err = what;
state = observer_state::aborted;
}
void on_subscribe(subscription new_sub) override {
if (state == observer_state::idle) {
CAF_ASSERT(!sub);
sub = std::move(new_sub);
state = observer_state::subscribed;
} else {
new_sub.dispose();
}
}
void on_next(const T& item) override {
buf.emplace_back(item);
}
// -- convenience functions --------------------------------------------------
bool request(size_t demand) {
if (sub) {
sub.request(demand);
return true;
} else {
return false;
}
}
void unsubscribe() {
if (sub) {
sub.dispose();
state = observer_state::idle;
}
}
bool idle() const noexcept {
return state == observer_state::idle;
}
bool subscribed() const noexcept {
return state == observer_state::subscribed;
}
bool completed() const noexcept {
return state == observer_state::completed;
}
bool aborted() const noexcept {
return state == observer_state::aborted;
}
std::vector<T> sorted_buf() const {
auto result = buf;
std::sort(result.begin(), result.end());
return result;
}
// -- member variables -------------------------------------------------------
/// The subscription for requesting additional items.
subscription sub;
/// Default-constructed unless on_error was called.
error err;
/// Represents the current state of this observer.
observer_state state = observer_state::idle;
/// Stores all items received via `on_next`.
std::vector<T> buf;
};
/// @relates passive_observer
template <class T>
intrusive_ptr<passive_observer<T>> make_passive_observer() {
return make_counted<passive_observer<T>>();
}
/// Similar to @ref passive_observer but automatically requests items until
/// completed. Useful for writing unit tests.
template <class T>
class auto_observer : public passive_observer<T> {
public:
// -- implementation of observer_impl<T> -------------------------------------
void on_subscribe(subscription new_sub) override {
if (this->state == observer_state::idle) {
CAF_ASSERT(!this->sub);
this->sub = std::move(new_sub);
this->state = observer_state::subscribed;
this->sub.request(64);
} else {
new_sub.dispose();
}
}
void on_next(const T& item) override {
this->buf.emplace_back(item);
if (this->sub)
this->sub.request(1);
}
};
/// @relates auto_observer
template <class T>
intrusive_ptr<auto_observer<T>> make_auto_observer() {
return make_counted<auto_observer<T>>();
}
} // namespace caf::flow } // namespace caf::flow
...@@ -69,6 +69,24 @@ public: ...@@ -69,6 +69,24 @@ public:
// nop // nop
} }
// -- properties -------------------------------------------------------------
bool running() const noexcept {
return state_ == state::running;
}
const error& err() const noexcept {
return err_;
}
size_t pending() const noexcept {
return buf_.size();
}
bool can_emit() const noexcept {
return buf_.size() == max_buf_size_ || has_shut_down(state_);
}
// -- callbacks for the parent ----------------------------------------------- // -- callbacks for the parent -----------------------------------------------
void init(observable<input_type> vals, observable<select_token_type> ctrl) { void init(observable<input_type> vals, observable<select_token_type> ctrl) {
...@@ -76,21 +94,22 @@ public: ...@@ -76,21 +94,22 @@ public:
using ctrl_fwd_t = forwarder<select_token_type, buffer_sub, buffer_emit_t>; using ctrl_fwd_t = forwarder<select_token_type, buffer_sub, buffer_emit_t>;
vals.subscribe( vals.subscribe(
make_counted<val_fwd_t>(this, buffer_input_t{})->as_observer()); make_counted<val_fwd_t>(this, buffer_input_t{})->as_observer());
ctrl.subscribe( // Note: the previous subscribe might call on_error, in which case we don't
make_counted<ctrl_fwd_t>(this, buffer_emit_t{})->as_observer()); // need to try to subscribe to the control observable.
if (running())
ctrl.subscribe(
make_counted<ctrl_fwd_t>(this, buffer_emit_t{})->as_observer());
} }
// -- callbacks for the forwarders ------------------------------------------- // -- callbacks for the forwarders -------------------------------------------
void fwd_on_subscribe(buffer_input_t, subscription sub) { void fwd_on_subscribe(buffer_input_t, subscription sub) {
if (state_ != state::idle || value_sub_ || !out_) { if (!running() || value_sub_ || !out_) {
sub.dispose(); sub.dispose();
return; return;
} }
value_sub_ = std::move(sub); value_sub_ = std::move(sub);
value_sub_.request(max_buf_size_); value_sub_.request(max_buf_size_);
if (control_sub_)
state_ = state::running;
} }
void fwd_on_complete(buffer_input_t) { void fwd_on_complete(buffer_input_t) {
...@@ -105,27 +124,20 @@ public: ...@@ -105,27 +124,20 @@ public:
} }
void fwd_on_next(buffer_input_t, const input_type& item) { void fwd_on_next(buffer_input_t, const input_type& item) {
switch (state_) { if (running()) {
case state::idle: buf_.push_back(item);
case state::running: if (buf_.size() == max_buf_size_)
buf_.push_back(item); do_emit();
if (buf_.size() == max_buf_size_)
do_emit();
break;
default:
break;
} }
} }
void fwd_on_subscribe(buffer_emit_t, subscription sub) { void fwd_on_subscribe(buffer_emit_t, subscription sub) {
if (state_ != state::idle || control_sub_ || !out_) { if (!running() || control_sub_ || !out_) {
sub.dispose(); sub.dispose();
return; return;
} }
control_sub_ = std::move(sub); control_sub_ = std::move(sub);
control_sub_.request(1); control_sub_.request(1);
if (value_sub_)
state_ = state::running;
} }
void fwd_on_complete(buffer_emit_t) { void fwd_on_complete(buffer_emit_t) {
...@@ -177,15 +189,10 @@ public: ...@@ -177,15 +189,10 @@ public:
} }
private: private:
bool can_emit() const noexcept {
return buf_.size() == max_buf_size_ || has_shut_down(state_);
}
void shutdown() { void shutdown() {
value_sub_.dispose(); value_sub_.dispose();
control_sub_.dispose(); control_sub_.dispose();
switch (state_) { switch (state_) {
case state::idle:
case state::running: case state::running:
if (!buf_.empty()) { if (!buf_.empty()) {
if (demand_ == 0) { if (demand_ == 0) {
...@@ -211,33 +218,27 @@ private: ...@@ -211,33 +218,27 @@ private:
void on_request() { void on_request() {
if (demand_ == 0 || !can_emit()) if (demand_ == 0 || !can_emit())
return; return;
switch (state_) { if (running()) {
case state::idle: CAF_ASSERT(buf_.size() == max_buf_size_);
case state::running: do_emit();
CAF_ASSERT(buf_.size() == max_buf_size_); return;
do_emit();
break;
case state::completed:
case state::aborted:
if (!buf_.empty())
do_emit();
if (err_)
out_.on_error(err_);
else
out_.on_complete();
out_ = nullptr;
break;
default:
break;
} }
if (!buf_.empty())
do_emit();
if (err_)
out_.on_error(err_);
else
out_.on_complete();
out_ = nullptr;
} }
void do_emit() { void do_emit() {
CAF_ASSERT(demand_ > 0); if (demand_ == 0)
return;
Trait f; Trait f;
--demand_; --demand_;
out_.on_next(f(buf_));
auto buffered = buf_.size(); auto buffered = buf_.size();
out_.on_next(f(buf_));
buf_.clear(); buf_.clear();
if (value_sub_ && buffered > 0) if (value_sub_ && buffered > 0)
value_sub_.request(buffered); value_sub_.request(buffered);
...@@ -277,12 +278,11 @@ private: ...@@ -277,12 +278,11 @@ private:
size_t demand_ = 0; size_t demand_ = 0;
/// Our current state. /// Our current state.
/// - idle: until we have received both subscriptions. /// - running: alive and ready to emit batches.
/// - running: emitting batches.
/// - completed: on_complete was called but some data is still buffered. /// - completed: on_complete was called but some data is still buffered.
/// - aborted: on_error was called but some data is still buffered. /// - aborted: on_error was called but some data is still buffered.
/// - disposed: inactive. /// - disposed: inactive.
state state_ = state::idle; state state_ = state::running;
/// Caches the abort reason. /// Caches the abort reason.
error err_; error err_;
...@@ -318,6 +318,12 @@ public: ...@@ -318,6 +318,12 @@ public:
disposable subscribe(observer<output_type> out) override { disposable subscribe(observer<output_type> out) override {
auto ptr = make_counted<buffer_sub<Trait>>(super::ctx_, max_items_, out); auto ptr = make_counted<buffer_sub<Trait>>(super::ctx_, max_items_, out);
ptr->init(in_, select_); ptr->init(in_, select_);
if (!ptr->running()) {
auto err = ptr->err().or_else(sec::runtime_error,
"failed to initialize buffer subscription");
out.on_error(err);
return {};
}
out.on_subscribe(subscription{ptr}); out.on_subscribe(subscription{ptr});
return ptr->as_disposable(); return ptr->as_disposable();
} }
......
...@@ -4,6 +4,67 @@ ...@@ -4,6 +4,67 @@
#include "core-test.hpp" #include "core-test.hpp"
#include <atomic>
namespace {
/// A trivial disposable with an atomic flag.
class trivial_impl : public caf::ref_counted, public caf::disposable::impl {
public:
trivial_impl() : flag_(false) {
// nop
}
void dispose() override {
flag_ = true;
}
bool disposed() const noexcept override {
return flag_.load();
}
void ref_disposable() const noexcept override {
ref();
}
void deref_disposable() const noexcept override {
deref();
}
friend void intrusive_ptr_add_ref(const trivial_impl* ptr) noexcept {
ptr->ref();
}
friend void intrusive_ptr_release(const trivial_impl* ptr) noexcept {
ptr->deref();
}
private:
std::atomic<bool> flag_;
};
} // namespace
namespace caf::flow {
disposable make_trivial_disposable() {
return disposable{make_counted<trivial_impl>()};
}
void passive_subscription_impl::request(size_t n) {
demand += n;
}
void passive_subscription_impl::dispose() {
disposed_flag = true;
}
bool passive_subscription_impl::disposed() const noexcept {
return disposed_flag;
}
} // namespace caf::flow
std::string to_string(level lvl) { std::string to_string(level lvl) {
switch (lvl) { switch (lvl) {
case level::all: case level::all:
......
...@@ -16,6 +16,227 @@ ...@@ -16,6 +16,227 @@
#include <string> #include <string>
#include <utility> #include <utility>
// -- utility for testing flows ------------------------------------------------
namespace caf::flow {
/// Returns a trivial disposable that wraps an atomic flag.
disposable make_trivial_disposable();
/// An observer with minimal internal logic. Useful for writing unit tests.
template <class T>
class passive_observer : public observer_impl_base<T> {
public:
// -- implementation of observer_impl<T> -------------------------------------
void on_complete() override {
if (sub) {
sub.dispose();
sub = nullptr;
}
state = observer_state::completed;
}
void on_error(const error& what) override {
if (sub) {
sub.dispose();
sub = nullptr;
}
err = what;
state = observer_state::aborted;
}
void on_subscribe(subscription new_sub) override {
if (state == observer_state::idle) {
CAF_ASSERT(!sub);
sub = std::move(new_sub);
state = observer_state::subscribed;
} else {
new_sub.dispose();
}
}
void on_next(const T& item) override {
buf.emplace_back(item);
}
// -- convenience functions --------------------------------------------------
bool request(size_t demand) {
if (sub) {
sub.request(demand);
return true;
} else {
return false;
}
}
void unsubscribe() {
if (sub) {
sub.dispose();
state = observer_state::idle;
}
}
bool idle() const noexcept {
return state == observer_state::idle;
}
bool subscribed() const noexcept {
return state == observer_state::subscribed;
}
bool completed() const noexcept {
return state == observer_state::completed;
}
bool aborted() const noexcept {
return state == observer_state::aborted;
}
std::vector<T> sorted_buf() const {
auto result = buf;
std::sort(result.begin(), result.end());
return result;
}
// -- member variables -------------------------------------------------------
/// The subscription for requesting additional items.
subscription sub;
/// Default-constructed unless on_error was called.
error err;
/// Represents the current state of this observer.
observer_state state = observer_state::idle;
/// Stores all items received via `on_next`.
std::vector<T> buf;
};
/// @relates passive_observer
template <class T>
intrusive_ptr<passive_observer<T>> make_passive_observer() {
return make_counted<passive_observer<T>>();
}
/// Similar to @ref passive_observer but automatically requests items until
/// completed. Useful for writing unit tests.
template <class T>
class auto_observer : public passive_observer<T> {
public:
// -- implementation of observer_impl<T> -------------------------------------
void on_subscribe(subscription new_sub) override {
if (this->state == observer_state::idle) {
CAF_ASSERT(!this->sub);
this->sub = std::move(new_sub);
this->state = observer_state::subscribed;
this->sub.request(64);
} else {
new_sub.dispose();
}
}
void on_next(const T& item) override {
this->buf.emplace_back(item);
if (this->sub)
this->sub.request(1);
}
};
/// @relates auto_observer
template <class T>
intrusive_ptr<auto_observer<T>> make_auto_observer() {
return make_counted<auto_observer<T>>();
}
/// A subscription implementation without internal logic.
class passive_subscription_impl final : public subscription::impl_base {
public:
/// Incremented by `request`.
size_t demand = 0;
/// Flipped by `dispose`.
bool disposed_flag = false;
void request(size_t n) override;
void dispose() override;
bool disposed() const noexcept override;
};
namespace op {
/// An observable that does nothing when subscribed except returning a trivial
/// disposable. Allows tests to call on_subscribe some time later.
template <class T>
class nil_observable : public op::cold<T> {
public:
using super = op::cold<T>;
using shared_count = std::shared_ptr<size_t>;
nil_observable(coordinator* ctx, shared_count subscribe_count)
: super(ctx), subscribe_count_(std::move(subscribe_count)) {
// nop
}
disposable subscribe(observer<T>) override {
if (subscribe_count_)
*subscribe_count_ += 1;
return make_trivial_disposable();
}
shared_count subscribe_count_;
};
/// An observable that passes a trivial disposable to any observer.
template <class T>
class trivial_observable : public op::cold<T> {
public:
using super = op::cold<T>;
using shared_count = std::shared_ptr<size_t>;
trivial_observable(coordinator* ctx, shared_count subscribe_count)
: super(ctx), subscribe_count_(std::move(subscribe_count)) {
// nop
}
disposable subscribe(observer<T> out) override {
if (subscribe_count_)
*subscribe_count_ += 1;
auto ptr = make_counted<passive_subscription_impl>();
out.on_subscribe(subscription{ptr});
return make_trivial_disposable();
}
shared_count subscribe_count_;
};
} // namespace op
template <class T>
observable<T>
make_nil_observable(coordinator* ctx,
std::shared_ptr<size_t> subscribe_count = nullptr) {
auto ptr = make_counted<op::nil_observable<T>>(ctx, subscribe_count);
return observable<T>{std::move(ptr)};
}
template <class T>
observable<T>
make_trivial_observable(coordinator* ctx,
std::shared_ptr<size_t> subscribe_count = nullptr) {
auto ptr = make_counted<op::trivial_observable<T>>(ctx, subscribe_count);
return observable<T>{std::move(ptr)};
}
} // namespace caf::flow
// -- utility for testing serialization round-trips ---------------------------- // -- utility for testing serialization round-trips ----------------------------
template <class T> template <class T>
......
This diff is collapsed.
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