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:
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 --------------------------------------------------------------
/// Reverts this error to "not an error" as if calling `*this = error{}`.
......
......@@ -522,133 +522,4 @@ private:
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
......@@ -69,6 +69,24 @@ public:
// 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 -----------------------------------------------
void init(observable<input_type> vals, observable<select_token_type> ctrl) {
......@@ -76,21 +94,22 @@ public:
using ctrl_fwd_t = forwarder<select_token_type, buffer_sub, buffer_emit_t>;
vals.subscribe(
make_counted<val_fwd_t>(this, buffer_input_t{})->as_observer());
ctrl.subscribe(
make_counted<ctrl_fwd_t>(this, buffer_emit_t{})->as_observer());
// Note: the previous subscribe might call on_error, in which case we don't
// 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 -------------------------------------------
void fwd_on_subscribe(buffer_input_t, subscription sub) {
if (state_ != state::idle || value_sub_ || !out_) {
if (!running() || value_sub_ || !out_) {
sub.dispose();
return;
}
value_sub_ = std::move(sub);
value_sub_.request(max_buf_size_);
if (control_sub_)
state_ = state::running;
}
void fwd_on_complete(buffer_input_t) {
......@@ -105,27 +124,20 @@ public:
}
void fwd_on_next(buffer_input_t, const input_type& item) {
switch (state_) {
case state::idle:
case state::running:
buf_.push_back(item);
if (buf_.size() == max_buf_size_)
do_emit();
break;
default:
break;
if (running()) {
buf_.push_back(item);
if (buf_.size() == max_buf_size_)
do_emit();
}
}
void fwd_on_subscribe(buffer_emit_t, subscription sub) {
if (state_ != state::idle || control_sub_ || !out_) {
if (!running() || control_sub_ || !out_) {
sub.dispose();
return;
}
control_sub_ = std::move(sub);
control_sub_.request(1);
if (value_sub_)
state_ = state::running;
}
void fwd_on_complete(buffer_emit_t) {
......@@ -177,15 +189,10 @@ public:
}
private:
bool can_emit() const noexcept {
return buf_.size() == max_buf_size_ || has_shut_down(state_);
}
void shutdown() {
value_sub_.dispose();
control_sub_.dispose();
switch (state_) {
case state::idle:
case state::running:
if (!buf_.empty()) {
if (demand_ == 0) {
......@@ -211,33 +218,27 @@ private:
void on_request() {
if (demand_ == 0 || !can_emit())
return;
switch (state_) {
case state::idle:
case state::running:
CAF_ASSERT(buf_.size() == max_buf_size_);
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 (running()) {
CAF_ASSERT(buf_.size() == max_buf_size_);
do_emit();
return;
}
if (!buf_.empty())
do_emit();
if (err_)
out_.on_error(err_);
else
out_.on_complete();
out_ = nullptr;
}
void do_emit() {
CAF_ASSERT(demand_ > 0);
if (demand_ == 0)
return;
Trait f;
--demand_;
out_.on_next(f(buf_));
auto buffered = buf_.size();
out_.on_next(f(buf_));
buf_.clear();
if (value_sub_ && buffered > 0)
value_sub_.request(buffered);
......@@ -277,12 +278,11 @@ private:
size_t demand_ = 0;
/// Our current state.
/// - idle: until we have received both subscriptions.
/// - running: emitting batches.
/// - running: alive and ready to emit batches.
/// - completed: on_complete was called but some data is still buffered.
/// - aborted: on_error was called but some data is still buffered.
/// - disposed: inactive.
state state_ = state::idle;
state state_ = state::running;
/// Caches the abort reason.
error err_;
......@@ -318,6 +318,12 @@ public:
disposable subscribe(observer<output_type> out) override {
auto ptr = make_counted<buffer_sub<Trait>>(super::ctx_, max_items_, out);
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});
return ptr->as_disposable();
}
......
......@@ -4,6 +4,67 @@
#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) {
switch (lvl) {
case level::all:
......
......@@ -16,6 +16,227 @@
#include <string>
#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 ----------------------------
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