diff --git a/lager/detail/no_value.hpp b/lager/detail/no_value.hpp new file mode 100644 index 00000000..a80309c3 --- /dev/null +++ b/lager/detail/no_value.hpp @@ -0,0 +1,43 @@ +// +// lager - library for functional interactive c++ programs +// Copyright (C) 2017 Juan Pedro Bolivar Puente +// +// This file is part of lager. +// +// lager is free software: you can redistribute it and/or modify +// it under the terms of the MIT License, as detailed in the LICENSE +// file located at the root of this source code distribution, +// or here: +// + +#pragma once + +#include + +#include + +namespace lager { + +/*! + * Raised by the view when it produces no value yet. This can happen + * when the reducing function that it uses is filtering some values. + */ +struct no_value_error : std::exception +{ + const char* what() const noexcept override; +}; + +namespace detail { + +struct no_value : zug::meta::bottom +{ + template + operator T() + { + throw no_value_error{}; + } +}; + +} // namespace detail + +} // namespace lager diff --git a/lager/detail/root_signals.hpp b/lager/detail/root_signals.hpp new file mode 100644 index 00000000..a7262055 --- /dev/null +++ b/lager/detail/root_signals.hpp @@ -0,0 +1,86 @@ +// +// lager - library for functional interactive c++ programs +// Copyright (C) 2017 Juan Pedro Bolivar Puente +// +// This file is part of lager. +// +// lager is free software: you can redistribute it and/or modify +// it under the terms of the MIT License, as detailed in the LICENSE +// file located at the root of this source code distribution, +// or here: +// + +#pragma once + +#include + +namespace lager { + +namespace detail { + +/*! + * Add signal that will always send the current value returned by a + * given function. + */ +template +class sensor_down_signal + : public down_signal>> +{ + using base_t = down_signal>>; + +public: + sensor_down_signal(SensorFnT sensor) + : base_t(sensor()) + , sensor_(std::move(sensor)) + {} + + void recompute() final { this->push_down(sensor_()); } + +private: + SensorFnT sensor_; +}; + +/*! + * Make a StateUpDownSignal with deduced types. + */ +template +auto make_sensor_signal(SensorFnT&& fn) + -> std::shared_ptr>> +{ + return std::make_shared>>( + std::forward(fn)); +} + +/*! + * Root signal that serves as state. + */ +template +class state_up_down_signal : public up_down_signal +{ +public: + using value_type = T; + + using up_down_signal::up_down_signal; + + void send_up(const value_type& value) final { this->push_down(value); } + + void send_up(value_type&& value) final + { + this->push_down(std::move(value)); + } +}; + +/*! + * Make a StateUpDownSignal with deduced types. + */ +template +auto make_state_signal(T&& value) + -> std::shared_ptr>> +{ + return std::make_shared>>( + std::forward(value)); +} + +} // namespace detail + +} // namespace lager diff --git a/lager/detail/signals.hpp b/lager/detail/signals.hpp new file mode 100644 index 00000000..071ecace --- /dev/null +++ b/lager/detail/signals.hpp @@ -0,0 +1,220 @@ +// +// lager - library for functional interactive c++ programs +// Copyright (C) 2017 Juan Pedro Bolivar Puente +// +// This file is part of lager. +// +// lager is free software: you can redistribute it and/or modify +// it under the terms of the MIT License, as detailed in the LICENSE +// file located at the root of this source code distribution, +// or here: +// + +/*! + * @file + * + * This module implements the cursor signal flow in Lager. Signals can be + * connected forming two superinposed directed acyclical graphs, where + * signals flow *down* or *up*. Signals are derived from eachother + * using transducers. + * + * The APIs for flowing up and down are asymmetric because of the way + * the graph is constructed and the semantics of information flow. + * + * - An up-down-signal can be constructed from a up-down-signal. + * - A down-signal can be constructed from a up-down-signa or + * a down-signal. + * - A signal can be appended children, but can not be appended + * parents. + * - Information propagates upwardes immediately, but propagates + * upwards in two fases. + * + * In general, sucessors know a lot about their predecessors, but + * sucessors need to know very little or nothing from their sucessors. + * + * @todo We could eventually flatten signals when the sucessors knows + * the transducer of its predecessor, this could be done heuristically. + */ + +#pragma once + +#include + +#include +#include +#include + +namespace lager { + +namespace detail { + +/*! + * Allows comparing shared and weak pointers based on their owner. + */ +constexpr struct +{ + template + bool operator()(const T1& a, const T2& b) + { + return !a.owner_before(b) && !b.owner_before(a); + } +} owner_equals{}; + +/*! + * Interface for children of a signal and is used to propagate + * notifications. The notifications are propagated in two steps, + * `send_down()` and `notify()`, not ensure that the outside world sees a + * consistent state when it receives notifications. + */ +struct down_signal_base +{ + virtual ~down_signal_base() = default; + virtual void send_down() = 0; + virtual void notify() = 0; +}; + +/*! + * Interface for signals that can send values back to their parents. + */ +template +struct up_signal_base +{ + virtual ~up_signal_base() = default; + virtual void send_up(const T&) = 0; + virtual void send_up(T&&) = 0; +}; + +/*! + * Base class for the various signal types. Provides basic + * functionality for setting values and propagating them to children. + */ +template +class down_signal + : public std::enable_shared_from_this> + , public down_signal_base +{ +public: + using value_type = T; + + down_signal(down_signal&&) = default; + down_signal(const down_signal&) = delete; + down_signal& operator=(down_signal&&) = default; + down_signal& operator=(const down_signal&) = delete; + + down_signal(T value) + : current_(std::move(value)) + , last_(current_) + , last_notified_(current_) + {} + + virtual void recompute() {} + virtual void recompute_deep() {} + + const value_type& current() const { return current_; } + const value_type& last() const { return last_; } + + void link(std::weak_ptr child) + { + using namespace std; + using std::placeholders::_1; + assert(find_if(begin(children_), + end(children_), + bind(owner_equals, child, _1)) == end(children_) && + "Child signal must not be linked twice"); + children_.push_back(child); + } + + template + void push_down(U&& value) + { + if (value != current_) { + current_ = std::forward(value); + needs_send_down_ = true; + } + } + + void send_down() final + { + recompute(); + if (needs_send_down_) { + last_ = current_; + needs_send_down_ = false; + needs_notify_ = true; + + for (auto& wchild : children_) { + if (auto child = wchild.lock()) { + child->send_down(); + } + } + } + } + + void notify() final + { + using namespace std; + if (!needs_send_down_ && needs_notify_) { + needs_notify_ = false; + observers_(last_notified_, last_); + last_notified_ = last_; + + auto garbage = false; + for (std::size_t i = 0, size = children_.size(); i < size; ++i) { + if (auto child = children_[i].lock()) { + child->notify(); + } else { + garbage = true; + } + } + + if (garbage) { + collect(); + } + } + } + + template + auto observe(Fn&& f) -> boost::signals2::connection + { + return observers_.connect(std::forward(f)); + } + + auto observers() + -> boost::signals2::signal& + { + return observers_; + } + +private: + void collect() + { + using namespace std; + children_.erase(remove_if(begin(children_), + end(children_), + mem_fn(&weak_ptr::expired)), + end(children_)); + } + + bool needs_send_down_ = false; + bool needs_notify_ = false; + value_type current_; + value_type last_; + value_type last_notified_; + std::vector> children_; + boost::signals2::signal + observers_; +}; + +/*! + * Base class for signals that can send values up the signal chain. + */ +template +class up_down_signal + : public down_signal + , public up_signal_base +{ + using down_signal::down_signal; +}; + +} // namespace detail + +} // namespace lager diff --git a/lager/detail/xform_signals.hpp b/lager/detail/xform_signals.hpp new file mode 100644 index 00000000..9827925a --- /dev/null +++ b/lager/detail/xform_signals.hpp @@ -0,0 +1,333 @@ +// +// Copyright (C) 2014, 2015 Ableton AG, Berlin. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +/*! + * @file + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace lager { + +namespace detail { + +ZUG_INLINE_CONSTEXPR struct send_down_t +{ + template + auto operator()(DownSignalPtr s, Inputs&&... is) const -> DownSignalPtr + { + s->push_down(zug::tuplify(std::forward(is)...)); + return s; + } + + template + auto operator()(DownSignalPtr s) const -> DownSignalPtr + { + return s; + } +} send_down{}; + +template +auto default_construct_or_throw() + -> std::enable_if_t::value, T> +{ + return T(); +} + +template +auto default_construct_or_throw() + -> std::enable_if_t::value, T> +{ + throw Err(); +} + +/*! + * Implementation of a signal with a transducer. + */ +template , + template class Base = down_signal> +class xform_down_signal; + +template class Base> +class xform_down_signal, Base> + : public Base...>> +{ + using base_t = + Base...>>; + using down_rf_t = decltype(std::declval()(send_down)); + + std::tuple...> parents_; + +public: + using value_type = typename base_t::value_type; + + xform_down_signal(xform_down_signal&&) = default; + xform_down_signal(const xform_down_signal&) = delete; + xform_down_signal& operator=(xform_down_signal&&) = default; + xform_down_signal& operator=(const xform_down_signal&) = delete; + + template + xform_down_signal(XForm2&& xform, std::shared_ptr... parents) + : base_t([&]() -> value_type { + try { + return xform(zug::last)(detail::no_value{}, + parents->current()...); + } catch (const no_value_error&) { + return default_construct_or_throw(); + } + }()) + , parents_(std::move(parents)...) + , down_step_(xform(send_down)) + {} + + void recompute() /* final */ + { + recompute(std::make_index_sequence{}); + } + + void recompute_deep() /* final */ + { + recompute_deep(std::make_index_sequence{}); + } + + std::tuple...>& parents() { return parents_; } + const std::tuple...>& parents() const + { + return parents_; + } + +private: + template + void recompute(std::index_sequence) + { + down_step_(this, std::get(parents_)->current()...); + } + + template + void recompute_deep(std::index_sequence) + { + noop((std::get(parents_)->recompute_deep(), 0)...); + recompute(); + } + + down_rf_t down_step_; +}; + +/*! + * Reducing function that pushes the received values into the signal + * that is passed as pointer as an accumulator. + */ +constexpr struct +{ + template + auto operator()(UpSignalPtr s, Inputs&&... is) const -> UpSignalPtr + { + s->push_up(zug::tuplify(std::forward(is)...)); + return s; + } +} send_up_rf{}; + +/*! + * @see update() + */ +template +decltype(auto) peek_parents(XformUpSignalPtr s, std::index_sequence) +{ + s->recompute_deep(); + return zug::tuplify(std::get(s->parents())->current()...); +} + +/*! + * Returns a transducer for updating the parent values via a + * up-signal. It processes the input with the function `mapping`, + * passing to it a value or tuple containing the values of the parents + * of the signal as first parameter, and the input as second. This + * mapping can thus return an *updated* version of the values in the + * parents with the new input. + * + * @note This transducer should only be used for the setter of + * output signals. + */ +template +auto update(UpdateT&& updater) +{ + return [=](auto&& step) { + return [=](auto s, auto&&... is) mutable { + auto indices = std::make_index_sequence< + std::tuple_sizeparents())>>::value>{}; + return step(s, updater(peek_parents(s, indices), ZUG_FWD(is)...)); + }; + }; +} + +/*! + * Implementation of a signal with a transducer + */ +template , + template class Base = up_down_signal> +class xform_up_down_signal; + +template + class Base> +class xform_up_down_signal, Base> + : public xform_down_signal, Base> +{ + using base_t = xform_down_signal, Base>; + using up_rf_t = decltype(std::declval()(send_up_rf)); + +public: + using value_type = typename base_t::value_type; + + xform_up_down_signal(xform_up_down_signal&&) = default; + xform_up_down_signal(const xform_up_down_signal&) = delete; + xform_up_down_signal& operator=(xform_up_down_signal&&) = default; + xform_up_down_signal& operator=(const xform_up_down_signal&) = delete; + + template + xform_up_down_signal(XForm2&& xform, + SetXForm2&& set_xform, + std::shared_ptr... parents) + : base_t(std::forward(xform), std::move(parents)...) + , up_step_(set_xform(send_up_rf)) + {} + + void send_up(const value_type& value) final + { + send_up(value, std::make_index_sequence{}); + } + + void send_up(value_type&& value) final + { + send_up(std::move(value), + std::make_index_sequence{}); + } + + template + void push_up(T&& value) + { + push_up(std::forward(value), + std::make_index_sequence{}); + } + +private: + template + void send_up(T&& x, std::index_sequence) + { + up_step_(this, std::forward(x)); + } + + template + void push_up(T&& value, std::index_sequence) + { + auto& parents = this->parents(); + noop((std::get(parents)->send_up( + std::get(std::forward(value))), + 0)...); + } + + template + void push_up(T&& value, std::index_sequence<0>) + { + std::get<0>(this->parents())->send_up(std::forward(value)); + } + + up_rf_t up_step_; +}; + +/*! + * Links a signal to its parents and returns it. + */ +template +auto link_to_parents(std::shared_ptr signal) + -> std::shared_ptr +{ + return link_to_parents( + std::move(signal), + std::make_index_sequenceparents())>>::value>{}); +} + +template +auto link_to_parents(std::shared_ptr signal, + std::index_sequence) + -> std::shared_ptr +{ + auto& parents = signal->parents(); + noop((std::get(parents)->link(signal), 0)...); + return signal; +} + +/*! + * Make a xform_down_signal with deduced types. + */ +template +auto make_xform_down_signal(XForm&& xform, std::shared_ptr... parents) + -> std::shared_ptr< + xform_down_signal, zug::meta::pack>> +{ + using signal_t = + xform_down_signal, zug::meta::pack>; + return link_to_parents(std::make_shared( + std::forward(xform), std::move(parents)...)); +} + +/*! + * Make a xform_down_signal with deduced types. + */ +template +auto make_xform_up_down_signal(XForm&& xform, + SetXForm&& set_xform, + std::shared_ptr... parents) + -> std::shared_ptr, + std::decay_t, + zug::meta::pack>> +{ + using signal_t = xform_up_down_signal, + std::decay_t, + zug::meta::pack>; + return link_to_parents( + std::make_shared(std::forward(xform), + std::forward(set_xform), + std::move(parents)...)); +} + +} // namespace detail + +} // namespace lager diff --git a/lager/util.hpp b/lager/util.hpp index 662e151c..1f62a99d 100644 --- a/lager/util.hpp +++ b/lager/util.hpp @@ -46,7 +46,12 @@ visitor(Ts...)->visitor; //! @{ //! Function that takes any argument and does nothing -constexpr auto noop = [](auto&&...) {}; +constexpr struct noop_t +{ + template + void operator()(T&&...) const + {} +} noop{}; //! Function that returns its first arguemnt constexpr auto identity = [](auto&& x) { return std::forward(x); }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index eea562b8..253ae312 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,7 +23,8 @@ add_dependencies(check tests) file(GLOB_RECURSE lager_unit_tests "*.cpp") set(lager_unit_tests core.cpp - deps.cpp) + deps.cpp + detail/signals.cpp) if(lager_BUILD_DEBUGGER) list(APPEND lager_unit_tests diff --git a/test/detail/signals.cpp b/test/detail/signals.cpp new file mode 100644 index 00000000..b2e55b88 --- /dev/null +++ b/test/detail/signals.cpp @@ -0,0 +1,282 @@ +// +// lager - library for functional interactive c++ programs +// Copyright (C) 2017 Juan Pedro Bolivar Puente +// +// This file is part of lager. +// +// lager is free software: you can redistribute it and/or modify +// it under the terms of the MIT License, as detailed in the LICENSE +// file located at the root of this source code distribution, +// or here: +// + +#include + +#include "../spies.hpp" + +#include +#include +#include + +#include + +#include + +using namespace zug; +using namespace lager::detail; + +TEST_CASE("signal, instantiate down signal") +{ + make_xform_down_signal(identity); +} + +TEST_CASE("signal, instantiate state") { make_state_signal(0); } + +TEST_CASE("signal, last_value is not visible") +{ + auto x = make_state_signal(0); + x->send_up(12); + CHECK(0 == x->last()); + x->send_up(42); + CHECK(0 == x->last()); +} + +TEST_CASE("signal, last value becomes visible") +{ + auto x = make_state_signal(0); + + x->send_up(12); + x->send_down(); + CHECK(12 == x->last()); + + x->send_up(42); + x->send_down(); + CHECK(42 == x->last()); +} + +TEST_CASE("signal, sending down") +{ + auto x = make_state_signal(5); + auto y = make_xform_down_signal(identity, x); + CHECK(5 == y->last()); + + x->send_up(12); + x->send_down(); + CHECK(12 == y->last()); + + x->send_up(42); + x->send_down(); + CHECK(42 == y->last()); +} + +TEST_CASE("signal, notifies new and previous value after send down") +{ + auto x = make_state_signal(5); + auto s = testing::spy([](int last, int next) { + CHECK(5 == last); + CHECK(42 == next); + }); + x->observe(s); + + x->send_up(42); + CHECK(0 == s.count()); + + x->notify(); + CHECK(0 == s.count()); + + x->send_down(); + x->notify(); + CHECK(1 == s.count()); +} + +TEST_CASE("signal, lifetime of observer") +{ + auto x = make_state_signal(5); + auto s = testing::spy(); + auto c = boost::signals2::connection{}; + { + auto y = make_xform_down_signal(identity, x); + c = y->observe(s); + CHECK(c.connected()); + + x->push_down(56); + x->send_down(); + x->notify(); + CHECK(1 == s.count()); + } + CHECK(!c.connected()); + + x->push_down(26); + x->send_down(); + x->notify(); + CHECK(1 == s.count()); +} + +TEST_CASE("signal, notify idempotence") +{ + auto x = make_state_signal(5); + auto s = testing::spy(); + x->observe(s); + + x->send_up(42); + CHECK(0 == s.count()); + + x->notify(); + x->notify(); + x->notify(); + CHECK(0 == s.count()); + + x->send_down(); + x->notify(); + x->notify(); + x->notify(); + CHECK(1 == s.count()); +} + +TEST_CASE("signal, observing is consistent") +{ + auto x = make_state_signal(5); + auto y = make_xform_down_signal(identity, x); + auto z = make_xform_down_signal(identity, x); + auto w = make_xform_down_signal(identity, y); + + auto s = testing::spy([&](int last_value, int new_value) { + CHECK(5 == last_value); + CHECK(42 == new_value); + CHECK(42 == x->last()); + CHECK(42 == y->last()); + CHECK(42 == z->last()); + CHECK(42 == w->last()); + }); + + x->observe(s); + y->observe(s); + z->observe(s); + w->observe(s); + + x->send_up(42); + x->send_down(); + CHECK(0 == s.count()); + + x->notify(); + CHECK(4 == s.count()); +} + +TEST_CASE("signal, bidirectional signal sends values up") +{ + auto x = make_state_signal(5); + auto y = make_xform_up_down_signal(identity, identity, x); + + y->send_up(42); + CHECK(5 == x->last()); + CHECK(5 == y->last()); + + x->send_down(); + CHECK(42 == x->last()); + CHECK(42 == y->last()); +} + +TEST_CASE("signal, bidirectional mapping") +{ + auto inc = [](int x) { return ++x; }; + auto dec = [](int x) { return --x; }; + auto x = make_state_signal(5); + auto y = make_xform_up_down_signal(map(inc), map(dec), x); + + CHECK(5 == x->last()); + CHECK(6 == y->last()); + + y->send_up(42); + x->send_down(); + CHECK(41 == x->last()); + CHECK(42 == y->last()); + + x->send_up(42); + x->send_down(); + CHECK(42 == x->last()); + CHECK(43 == y->last()); +} + +TEST_CASE("signal, bidirectiona update is consistent") +{ + using arr = std::array; + auto x = make_state_signal(arr{{5, 13}}); + auto y = make_xform_up_down_signal(map([](const arr& a) { return a[0]; }), + update([](arr a, int v) { + a[0] = v; + return a; + }), + x); + auto z = make_xform_up_down_signal(map([](const arr& a) { return a[1]; }), + update([](arr a, int v) { + a[1] = v; + return a; + }), + x); + + CHECK((arr{{5, 13}}) == x->last()); + CHECK(5 == y->last()); + CHECK(13 == z->last()); + + z->send_up(42); + y->send_up(69); + CHECK((arr{{5, 13}}) == x->last()); + CHECK(5 == y->last()); + CHECK(13 == z->last()); + + x->send_down(); + CHECK((arr{{69, 42}}) == x->last()); + CHECK(69 == y->last()); + CHECK(42 == z->last()); +} + +TEST_CASE("signal, sensors signals reevaluate on send down") +{ + int count = 0; + auto x = make_sensor_signal([&count] { return count++; }); + CHECK(0 == x->last()); + x->send_down(); + CHECK(1 == x->last()); + x->send_down(); + CHECK(2 == x->last()); +} + +TEST_CASE("signal, one signal two parents") +{ + int count = 0; + auto x = make_sensor_signal([&count] { return count++; }); + auto y = make_state_signal(12); + auto z = + make_xform_down_signal(map([](int a, int b) { return a + b; }), x, y); + auto s = + testing::spy([&](int, int r) { CHECK(r == x->last() + y->last()); }); + z->observe(s); + CHECK(12 == z->last()); + + // Commit first root individually + x->send_down(); + CHECK(13 == z->last()); + CHECK(0 == s.count()); + x->notify(); + CHECK(1 == s.count()); + y->notify(); + CHECK(1 == s.count()); + + // Commit second root individually + y->push_down(3); + y->send_down(); + CHECK(4 == z->last()); + y->notify(); + CHECK(2 == s.count()); + x->notify(); + CHECK(2 == s.count()); + + // Commit both roots together + x->send_down(); + y->push_down(69); + y->send_down(); + x->notify(); + y->notify(); + CHECK(71 == z->last()); + CHECK(3 == s.count()); +} diff --git a/test/spies.hpp b/test/spies.hpp new file mode 100644 index 00000000..75cd5ccf --- /dev/null +++ b/test/spies.hpp @@ -0,0 +1,232 @@ +// +// lager - library for functional interactive c++ programs +// Copyright (C) 2017 Juan Pedro Bolivar Puente +// +// This file is part of lager. +// +// lager is free software: you can redistribute it and/or modify +// it under the terms of the MIT License, as detailed in the LICENSE +// file located at the root of this source code distribution, +// or here: +// + +#include +#include +#include +#include + +namespace testing { +namespace mocks { + +template +struct defaulting +{ + template + T operator()(Args&&...) + { + return T(); + } +}; + +template +struct returning +{ + returning() = default; + + template + returning(const Fn& mock) + : mock_(mock) + {} + + template + T operator()(Args&&...) + { + return mock_(); + } + +private: + std::function mock_; +}; + +} // namespace mocks + +namespace detail { + +class spy_base +{ +public: + std::size_t count() const { return *count_; } + + void called() { ++*count_; } + +private: + std::shared_ptr count_ = std::make_shared(0); +}; + +} // namespace detail + +/*! + * Functor that counts the number of times it was called. + * + * @todo Support comparing the actual arguments. Keep generic + * interface using boost::any + */ +template > +class spy_fn : public detail::spy_base +{ + MockT mock_; + +public: + spy_fn() = default; + + template + spy_fn(MockT2 mock) + : mock_(std::move(mock)) + {} + + template + spy_fn(MockT2 mock, const spy_base& spy) + : spy_base(spy) + , mock_(std::move(mock)) + {} + + template + decltype(auto) operator()(Args&&... args) + { + called(); + return this->mock_(std::forward(args)...); + } +}; + +/*! + * Returns a spy object that uses fn as mock implementation. + */ +template +inline auto spy(const Fn& fn) -> spy_fn +{ + return spy_fn(fn); +} + +/*! + * Returns a spy object with a no-op mock implementation. + */ +inline auto spy() -> spy_fn<> { return spy_fn<>(); } + +namespace detail { + +template +class scoped_intruder +{ + MockT* mock_; + MockT original_; + +public: + scoped_intruder& operator=(const scoped_intruder&) = delete; + scoped_intruder(const scoped_intruder&) = delete; + + scoped_intruder(scoped_intruder&& other) { swap(*this, other); } + + scoped_intruder& operator=(scoped_intruder&& other) + { + if (this != &other) { + swap(*this, other); + } + } + + scoped_intruder(MockT& mock, MockT replacement) + : mock_(&mock) + , original_(mock) + { + *mock_ = replacement; + } + + ~scoped_intruder() + { + if (mock_) { + *mock_ = original_; + } + } + + template + auto operator()(Args&&... args) + -> decltype((*mock_)(std::forward(args)...)) + { + assert(mock_ && "must not call intruder after having moved from it"); + return (*mock_)(std::forward(args)...); + } + + friend void swap(scoped_intruder& a, scoped_intruder& b) + { + using std::swap; + swap(a.mock_, b.mock_); + swap(a.original_, b.original_); + } +}; + +} // namespace detail + +/*! + * Given a functor object `mock` of a general functor with type erasure + * (e.g. std::function or boost::function) installs a spy that counts + * the calls and returns such a spy. + */ +template +inline auto spy_on(MockT& mock) -> spy_fn> +{ + auto s = spy(mock); + return {detail::scoped_intruder(mock, s), s}; +} + +/*! + * Like @a spy_on(), but it installs the `replacement` function instead + * of keeping the original one. The spy is uninstalled on + * destruction, and it is not copyable. + */ +template +inline auto spy_on(MockT& mock, const FnT& replacement) + -> spy_fn> +{ + auto s = spy(replacement); + return {detail::scoped_intruder(mock, s), s}; +} + +namespace detail { + +struct default_copy_spy_base_t +{}; + +} // namespace detail + +/*! + * Utility for testing how many times an object is copied. + */ +template +struct copy_spy : BaseT +{ + using base_t = BaseT; + + spy_fn<> copied; + + copy_spy(BaseT base = {}) + : BaseT{std::move(base)} + {} + copy_spy(copy_spy&&) = default; + copy_spy& operator=(copy_spy&&) = default; + + copy_spy(const copy_spy& x) + : base_t(x) + , copied(x.copied) + { + copied(); + } + + copy_spy& operator=(const copy_spy& x) + { + base_t::operator=(x); + copied = x.copied; + copied(); + return *this; + } +}; + +} // namespace testing