Separate tests into unit & integration

Summary:
- Separate tests into unit & integration

- Update paths in workflows

- Cut test execution time

Reviewers: ivica

Reviewed By: ivica

Subscribers: miljen, iljazovic

Differential Revision: https://repo.mireo.local/D27374
This commit is contained in:
Korina Šimičević committed 2024-01-16 11:45:23 +01:00
1 parent e3eb408c98
commit 2f6751d162
27 files changed
+884 -855

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
#ifndef ASYNC_MQTT5_TEST_DELAYED_OP_HPP
#define ASYNC_MQTT5_TEST_DELAYED_OP_HPP
#include <chrono>
#include <boost/asio/append.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/prepend.hpp>
#include <boost/asio/recycling_allocator.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/type_traits/remove_cv_ref.hpp>
namespace async_mqtt5::test {
namespace asio = boost::asio;
using error_code = boost::system::error_code;
using time_stamp = std::chrono::time_point<std::chrono::steady_clock>;
using duration = time_stamp::duration;
template <typename ...BoundArgs>
class delayed_op {
struct on_timer {};
std::unique_ptr<asio::steady_timer> _timer;
time_stamp::duration _delay;
asio::cancellation_slot _cancel_slot;
std::tuple<BoundArgs...> _args;
public:
template <typename Executor, typename ...Args>
delayed_op(
const Executor& ex, time_stamp::duration delay, Args&& ...args
) :
_timer(new asio::steady_timer(ex)), _delay(delay),
_args(std::move(args)...)
{}
delayed_op(delayed_op&&) noexcept = default;
delayed_op(const delayed_op&) = delete;
using executor_type = asio::steady_timer::executor_type;
executor_type get_executor() const noexcept {
return _timer->get_executor();
}
using allocator_type = asio::recycling_allocator<void>;
allocator_type get_allocator() const noexcept {
return allocator_type {};
}
using cancellation_slot_type = asio::cancellation_slot;
asio::cancellation_slot get_cancellation_slot() const noexcept {
return _cancel_slot;
}
template <typename CompletionHandler>
void perform(CompletionHandler&& handler) {
_cancel_slot = asio::get_associated_cancellation_slot(handler);
_timer->expires_from_now(_delay);
_timer->async_wait(
asio::prepend(std::move(*this), on_timer {}, std::move(handler))
);
}
template <typename CompletionHandler>
void operator()(on_timer, CompletionHandler&& h, error_code ec) {
get_cancellation_slot().clear();
auto bh = std::apply(
[h = std::move(h)](auto&&... args) mutable {
return asio::append(std::move(h), std::move(args)...);
},
_args
);
asio::dispatch(asio::prepend(std::move(bh), ec));
}
};
template <typename CompletionToken, typename ...BoundArgs>
decltype(auto) async_delay(
asio::cancellation_slot cancel_slot,
delayed_op<BoundArgs...>&& op,
CompletionToken&& token
) {
using Signature = void (error_code, boost::remove_cv_ref_t<BoundArgs>...);
auto initiation = [](
auto handler, asio::cancellation_slot cancel_slot,
delayed_op<BoundArgs...> op
) {
op.perform(
asio::bind_cancellation_slot(cancel_slot, std::move(handler))
);
};
return asio::async_initiate<CompletionToken, Signature>(
std::move(initiation), token, cancel_slot, std::move(op)
);
}
} // end namespace async_mqtt5::test
#endif // ASYNC_MQTT5_TEST_DELAYED_OP_HPP
@@ -0,0 +1,278 @@
#ifndef ASYNC_MQTT5_TEST_MESSAGE_EXCHANGE_HPP
#define ASYNC_MQTT5_TEST_MESSAGE_EXCHANGE_HPP
#include <chrono>
#include <deque>
#include <optional>
#include <string>
#include <vector>
#include <boost/type_traits/remove_cv_ref.hpp>
#include "test_common/delayed_op.hpp"
namespace async_mqtt5::test {
using error_code = boost::system::error_code;
using time_stamp = std::chrono::time_point<std::chrono::steady_clock>;
using duration = time_stamp::duration;
class msg_exchange;
class broker_message;
inline duration after(duration d) { return d; }
using namespace std::chrono_literals;
namespace detail {
class stream_message {
error_code _ec;
duration _after { 0 };
std::vector<uint8_t> _content;
public:
template <typename ...Args>
stream_message(error_code ec, duration after, Args&& ...args) :
_ec(ec), _after(after)
{
(_content.insert(_content.end(), args.begin(), args.end()), ...);
}
stream_message(const stream_message&) = delete;
stream_message(stream_message&&) = default;
template <typename Executor>
auto to_operation(const Executor& ex) {
return delayed_op<error_code, std::vector<uint8_t>> {
ex, _after, _ec, std::move(_content)
};
}
};
} // end namespace detail
class client_message {
msg_exchange* _owner;
error_code _write_ec;
duration _complete_after { 0 };
std::vector<std::string> _expected_packets;
std::vector<detail::stream_message> _replies;
public:
template <typename ...Args>
client_message(msg_exchange* owner, Args&&... args) :
_owner(owner),
_expected_packets({ std::forward<Args>(args)... })
{}
client_message(const client_message&) = delete;
client_message(client_message&&) = default;
client_message& complete_with(error_code ec, duration af) {
_write_ec = ec;
_complete_after = af;
return *this;
}
template <typename ...Args>
client_message& reply_with(Args&& ...args) {
return reply_with_dur(std::make_tuple(std::forward<Args>(args)...));
}
template <typename ...Args>
client_message& expect(Args&& ...args);
template <typename ...Args>
broker_message& send(Args&& ...args);
template <typename Executor>
decltype(auto) write_completion(const Executor& ex) const {
return delayed_op<error_code>(ex, _complete_after, _write_ec);
}
template <typename Executor>
decltype(auto) pop_reply_ops(const Executor& ex) {
std::vector<delayed_op<error_code, std::vector<uint8_t>>> ret;
std::transform(
_replies.begin(), _replies.end(), std::back_inserter(ret),
[&ex](auto& r) { return r.to_operation(ex); }
);
_replies.clear();
return ret;
}
decltype(auto) expected_packets() {
return _expected_packets;
}
private:
template <typename Tuple>
client_message& reply_with_dur(const Tuple& t) {
using indices = std::make_index_sequence<std::tuple_size_v<Tuple> - 1>;
return reply_with_dur(t, indices {});
}
template<typename Tuple, size_t ...I>
client_message& reply_with_dur(const Tuple& t, std::index_sequence<I...>) {
return reply_with_impl(
std::get<std::tuple_size_v<Tuple> - 1>(t),
std::get<I>(t)...
);
}
template <
typename ...Args,
std::enable_if_t<
(std::is_same_v<boost::remove_cv_ref_t<Args>, std::string> && ...),
bool
> = true
>
client_message& reply_with_impl(duration af, Args&& ...args) {
_replies.emplace_back(
error_code {}, af, std::forward<Args>(args)...
);
return *this;
}
client_message& reply_with_impl(duration af, error_code ec) {
_replies.emplace_back(ec, af);
return *this;
}
};
class broker_message {
msg_exchange* _owner;
detail::stream_message _message;
public:
template <typename ...Args>
broker_message(
msg_exchange* owner, error_code ec, duration af, Args&&... args
) :
_owner(owner), _message(ec, af, std::forward<Args>(args) ...)
{}
broker_message(const broker_message&) = delete;
broker_message(broker_message&&) = default;
template <typename ...Args>
client_message& expect(Args&& ...args);
template <typename ...Args>
broker_message& send(Args&& ...args);
template <typename Executor>
decltype(auto) pop_send_op(const Executor& ex) {
return _message.to_operation(ex);
}
};
class msg_exchange {
std::deque<client_message> _to_broker;
std::vector<broker_message> _from_broker;
public:
template <
typename ...Args,
std::enable_if_t<
(std::is_same_v<boost::remove_cv_ref_t<Args>, std::string> && ...),
bool
> = true
>
client_message& expect(Args&& ...args) {
_to_broker.emplace_back(this, std::forward<Args>(args)...);
return _to_broker.back();
}
template <typename ...Args>
broker_message& send(Args&& ...args) {
return send_with_dur(std::make_tuple(std::forward<Args>(args)...));
}
std::optional<client_message> pop_reply_action() {
if (_to_broker.empty())
return std::nullopt;
auto rv = std::move(_to_broker.front());
_to_broker.pop_front();
return rv;
}
template <typename Executor>
auto pop_broker_ops(const Executor& ex) {
std::vector<delayed_op<error_code, std::vector<uint8_t>>> ret;
std::transform(
_from_broker.begin(), _from_broker.end(), std::back_inserter(ret),
[&ex](auto& s) { return s.pop_send_op(ex); }
);
_from_broker.clear();
return ret;
}
private:
template <typename Tuple>
broker_message& send_with_dur(const Tuple& t) {
using indices = std::make_index_sequence<std::tuple_size_v<Tuple> - 1>;
return send_with_dur(t, indices {});
}
template<typename Tuple, size_t ...I>
broker_message& send_with_dur(const Tuple& t, std::index_sequence<I...>) {
return send_impl(
std::get<std::tuple_size_v<Tuple> - 1>(t),
std::get<I>(t)...
);
}
template <
typename ...Args,
std::enable_if_t<
(std::is_same_v<boost::remove_cv_ref_t<Args>, std::string> && ...),
bool
> = true
>
broker_message& send_impl(duration after, Args&& ...args) {
_from_broker.emplace_back(
this, error_code {}, after, std::forward<Args>(args)...
);
return _from_broker.back();
}
broker_message& send_impl(duration after, error_code ec) {
_from_broker.emplace_back(this, ec, after);
return _from_broker.back();
}
};
template <typename ...Args>
client_message& client_message::expect(Args&& ...args) {
return _owner->expect(std::forward<Args>(args)...);
}
template <typename ...Args>
broker_message& client_message::send(Args&& ...args) {
return _owner->send(std::forward<Args>(args)...);
}
template <typename ...Args>
client_message& broker_message::expect(Args&& ...args) {
return _owner->expect(std::forward<Args>(args)...);
}
template <typename ...Args>
broker_message& broker_message::send(Args&& ...args) {
return _owner->send(std::forward<Args>(args)...);
}
} // end namespace async_mqtt5::test
#endif // ASYNC_MQTT5_TEST_MESSAGE_EXCHANGE_HPP
+121
View File
@@ -0,0 +1,121 @@
#ifndef ASYNC_MQTT5_TEST_PACKET_UTIL_HPP
#define ASYNC_MQTT5_TEST_PACKET_UTIL_HPP
#include <string>
#include <async_mqtt5/detail/control_packet.hpp>
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
#include <async_mqtt5/impl/codecs/message_encoders.hpp>
namespace async_mqtt5::test {
using error_code = boost::system::error_code;
inline qos_e extract_qos(uint8_t flags) {
auto byte = (flags & 0b0110) >> 1;
return qos_e(byte);
}
inline control_code_e extract_code(uint8_t control_byte) {
constexpr uint8_t mask = 0b11110000;
constexpr uint8_t publish_bits = 0b0011;
constexpr uint8_t special_mask = 0b00000010;
constexpr control_code_e codes_with_non_zero_end[] = {
control_code_e::pubrel, control_code_e::subscribe,
control_code_e::unsubscribe
};
if ((control_byte >> 4) == publish_bits)
return control_code_e::publish;
if ((control_byte & mask) == control_byte)
return control_code_e(control_byte & mask);
for (const auto& special_code : codes_with_non_zero_end)
if (control_byte == (uint8_t(special_code) | special_mask))
return special_code;
return control_code_e::no_packet;
}
inline std::string_view code_to_str(control_code_e code) {
switch (code) {
case control_code_e::connect: return "CONNECT";
case control_code_e::connack: return "CONNACK";
case control_code_e::publish: return "PUBLISH";
case control_code_e::puback: return "PUBACK";
case control_code_e::pubrec: return "PUBREC";
case control_code_e::pubrel: return "PUBREL";
case control_code_e::pubcomp: return "PUBCOMP";
case control_code_e::subscribe: return "SUBSCRIBE";
case control_code_e::suback: return "SUBACK";
case control_code_e::unsubscribe: return "UNSUBSCRIBE";
case control_code_e::unsuback: return "UNSUBACK";
case control_code_e::auth: return "AUTH";
case control_code_e::disconnect: return "DISCONNECT";
case control_code_e::pingreq: return "PINGREQ";
case control_code_e::pingresp: return "PINGRESP";
default: return "NO PACKET";
}
}
inline std::string to_readable_packet(std::string packet) {
auto control_byte = uint8_t(*packet.data());
auto code = extract_code(control_byte);
if (code == control_code_e::no_packet)
return {};
std::ostringstream stream;
if (
code == control_code_e::connect || code == control_code_e::connack ||
code == control_code_e::disconnect
) {
stream << code_to_str(code);
return stream.str();
}
auto begin = ++packet.cbegin();
auto varlen = decoders::type_parse(
begin, packet.cend(), decoders::basic::varint_
);
if (code == control_code_e::publish) {
auto publish = decoders::decode_publish(
control_byte, *varlen, begin
);
auto& [topic, packet_id, flags, props, payload] = *publish;
stream << code_to_str(code);
stream << (packet_id ? " " + std::to_string(*packet_id) : "");
stream << "flags: " << flags;
return stream.str();
}
const auto packet_id = decoders::decode_packet_id(begin).value();
stream << code_to_str(code) << " " << packet_id;
return stream.str();
}
template <typename ConstBufferSequence>
std::vector<std::string> to_packets(const ConstBufferSequence& buffers) {
std::vector<std::string> content;
for (const auto& buff : buffers) {
auto control_byte = *(const uint8_t*) buff.data();
auto code = extract_code(control_byte);
if (code == control_code_e::pingreq)
continue;
content.push_back({ (const char*)buff.data(), buff.size() });
}
return content;
}
} // end namespace async_mqtt5::test
#endif // ASYNC_MQTT5_TEST_PACKET_UTIL_HPP
@@ -0,0 +1,21 @@
#ifndef ASYNC_MQTT5_TEST_PROTOCOL_LOGGING_HPP
#define ASYNC_MQTT5_TEST_PROTOCOL_LOGGING_HPP
#include <string>
namespace async_mqtt5::test {
inline bool& logging_enabled() {
static bool enabled = false;
return enabled;
}
inline void log(const std::string& message) {
if (logging_enabled())
fprintf(stderr, "%s\n", message.c_str());
}
} // end namespace async_mqtt5::test
#endif // ASYNC_MQTT5_TEST_PROTOCOL_LOGGING_HPP
+291
View File
@@ -0,0 +1,291 @@
#ifndef ASYNC_MQTT5_TEST_TEST_BROKER_HPP
#define ASYNC_MQTT5_TEST_TEST_BROKER_HPP
#include <memory>
#include <numeric>
#include <string>
#include <vector>
#include <boost/asio/any_completion_handler.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/prepend.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <async_mqtt5/impl/codecs/message_decoders.hpp>
#include "test_common/message_exchange.hpp"
#include "test_common/packet_util.hpp"
#include "test_common/protocol_logging.hpp"
namespace async_mqtt5::test {
namespace asio = boost::asio;
using error_code = boost::system::error_code;
class pending_read {
void* _buffer_data { nullptr };
size_t _buffer_size { 0 };
asio::any_completion_handler<void (error_code, size_t)> _handler {};
public:
template <typename MutableBuffer, typename Handler>
pending_read(const MutableBuffer& buffer, Handler&& handler) :
_buffer_data(asio::buffer_cast<void*>(buffer)),
_buffer_size(buffer.size()),
_handler(std::move(handler))
{}
pending_read() = default;
pending_read(pending_read&&) = default;
pending_read& operator=(pending_read&&) = default;
size_t consume(const std::vector<uint8_t>& data) {
size_t num_bytes = std::min(_buffer_size, data.size());
std::memcpy(_buffer_data, data.data(), num_bytes);
return num_bytes;
}
template <typename Executor>
void complete(const Executor& ex, error_code ec, size_t bytes_read) {
if (empty())
return;
if (ec || bytes_read || _buffer_size == 0)
asio::post(ex, asio::prepend(std::move(_handler), ec, bytes_read));
}
constexpr bool empty() const {
return !_handler;
}
};
class test_broker : public asio::execution_context::service {
public:
using executor_type = asio::any_io_executor;
using protocol_type = asio::ip::tcp;
using endpoint_type = asio::ip::tcp::endpoint;
static inline asio::execution_context::id id {};
private:
using base = asio::execution_context::service;
struct on_receive {};
struct on_delayed_complete {};
struct broker_data {
error_code ec;
std::vector<uint8_t> bytes;
};
executor_type _ex;
std::deque<broker_data> _broker_data;
pending_read _pending_read;
msg_exchange _broker_side;
std::vector<std::unique_ptr<asio::cancellation_signal>> _cancel_signals;
public:
test_broker(
asio::execution_context& context,
asio::any_io_executor ex = {}, msg_exchange broker_side = {}
) :
base(context), _ex(std::move(ex)), _broker_side(std::move(broker_side))
{
launch_broker_ops();
}
test_broker(const test_broker&) = delete;
void operator=(const test_broker&) = delete;
executor_type get_executor() const noexcept {
return _ex;
}
void close_connection() {
_pending_read.complete(
get_executor(), asio::error::operation_aborted, 0
);
for (auto& cs : _cancel_signals)
cs->emit(asio::cancellation_type::terminal);
_broker_data.clear();
}
bool received_all_expected() {
return !_broker_side.pop_reply_action().has_value();
}
template <typename ConstBufferSequence, typename WriteToken>
decltype(auto) write_to_network(
const ConstBufferSequence& buffers,
WriteToken&& token
) {
auto initiation = [this](
auto handler, const ConstBufferSequence& buffers
) {
auto reply_action = _broker_side.pop_reply_action();
size_t bytes_written = std::accumulate(
std::begin(buffers), std::end(buffers), size_t(0),
[](size_t a, const auto& b) { return a + b.size(); }
);
executor_type ex = get_executor();
if (reply_action) {
const auto& expected = reply_action->expected_packets();
size_t buffers_size = std::distance(
buffers.begin(), buffers.end()
);
BOOST_CHECK_EQUAL(buffers_size, expected.size());
size_t num_packets = std::min(buffers_size, expected.size());
auto it = buffers.begin();
for (size_t i = 0; i < num_packets; ++i, ++it) {
BOOST_CHECK_EQUAL(it->size(), expected[i].size());
size_t len = std::min(it->size(), expected[i].size());
if (memcmp(it->data(), expected[i].data(), len)) {
std::ostringstream stream;
stream << "Packet mismatch! Expected: "
<< to_readable_packet(expected[i])
<< " Received: "
<< to_readable_packet(std::string((const char*)it->data(), len));
log(stream.str());
}
}
}
auto complete_op = reply_action ?
reply_action->write_completion(ex) :
delayed_op<error_code>(ex, 0ms, error_code {});
async_delay(
make_cancel_slot(), std::move(complete_op),
asio::prepend(
std::ref(*this), on_delayed_complete {},
std::move(handler), bytes_written
)
);
if (!reply_action.has_value())
return;
for (auto& op : reply_action->pop_reply_ops(ex))
async_delay(
make_cancel_slot(), std::move(op),
asio::prepend(std::ref(*this), on_receive {})
);
};
return asio::async_initiate<WriteToken, void (error_code, size_t)>(
std::move(initiation), token, buffers
);
}
template <typename MutableBuffer, typename ReadToken>
decltype(auto) read_from_network(
const MutableBuffer& buffer, ReadToken&& token
) {
auto initiation = [this](
auto handler, const MutableBuffer& buffer
) {
_pending_read = pending_read(buffer, std::move(handler));
complete_read();
};
return asio::async_initiate<ReadToken, void (error_code, size_t)>(
std::move(initiation), token, buffer
);
}
void operator()(
on_receive, error_code delay_ec,
error_code ec, std::vector<uint8_t> bytes
) {
remove_cancel_signal();
if (delay_ec) // asio::operation_aborted
return;
_broker_data.push_back({ ec, std::move(bytes) });
complete_read();
}
template <typename Handler>
void operator()(
on_delayed_complete, Handler handler, size_t bytes,
error_code delay_ec, error_code ec
) {
remove_cancel_signal();
if (delay_ec) { // asio::operation_aborted
ec = delay_ec;
bytes = 0;
}
asio::dispatch(asio::prepend(std::move(handler), ec, bytes));
}
private:
void shutdown() override { }
void launch_broker_ops() {
for (auto& op: _broker_side.pop_broker_ops(get_executor())) {
async_delay(
asio::cancellation_slot {},
std::move(op),
asio::prepend(std::ref(*this), on_receive {})
);
}
}
void complete_read() {
if (_pending_read.empty())
return;
error_code ec = {};
size_t bytes_read = 0;
if (!_broker_data.empty()) {
auto& [read_ec, bytes] = _broker_data.front();
ec = read_ec;
bytes_read = _pending_read.consume(bytes);
if (bytes_read == bytes.size())
_broker_data.pop_front();
else
bytes.erase(bytes.begin(), bytes.begin() + bytes_read);
}
_pending_read.complete(get_executor(), ec, bytes_read);
}
asio::cancellation_slot make_cancel_slot() {
_cancel_signals.push_back(
std::make_unique<asio::cancellation_signal>()
);
return _cancel_signals.back()->slot();
}
void remove_cancel_signal() {
_cancel_signals.erase(
std::remove_if(
_cancel_signals.begin(), _cancel_signals.end(),
[](auto& sig_ptr) { return !sig_ptr->slot().has_handler(); }
),
_cancel_signals.end()
);
}
};
} // end namespace async_mqtt5::test
#endif // ASYNC_MQTT5_TEST_TEST_BROKER_HPP
+82
View File
@@ -0,0 +1,82 @@
#ifndef ASYNC_MQTT5_TEST_TEST_SERVICE_HPP
#define ASYNC_MQTT5_TEST_TEST_SERVICE_HPP
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/prepend.hpp>
#include <async_mqtt5/types.hpp>
#include <async_mqtt5/impl/client_service.hpp>
namespace async_mqtt5::test {
namespace asio = boost::asio;
template <
typename StreamType,
typename TlsContext = std::monostate
>
class test_service : public async_mqtt5::detail::client_service<StreamType, TlsContext> {
using error_code = boost::system::error_code;
using base = async_mqtt5::detail::client_service<StreamType, TlsContext>;
asio::any_io_executor _ex;
connack_props _test_props;
public:
test_service(const asio::any_io_executor ex)
: base(ex, {}), _ex(ex)
{}
test_service(const asio::any_io_executor ex, connack_props props)
: base(ex, {}), _ex(ex), _test_props(std::move(props))
{}
template <typename BufferType, typename CompletionToken>
decltype(auto) async_send(
const BufferType&, uint32_t, unsigned,
CompletionToken&& token
) {
auto initiation = [this](auto handler) {
asio::post(_ex,
asio::prepend(std::move(handler), error_code {})
);
};
return asio::async_initiate<
CompletionToken, void (error_code)
> (std::move(initiation), token);
}
template <typename Prop>
const auto& connack_property(Prop p) const {
return _test_props[p];
}
const auto& connack_properties() {
return _test_props;
}
};
template <
typename StreamType,
typename TlsContext = std::monostate
>
class overrun_client : public async_mqtt5::detail::client_service<StreamType, TlsContext> {
public:
overrun_client(const asio::any_io_executor& ex, const std::string& cnf) :
async_mqtt5::detail::client_service<StreamType, TlsContext>(ex, cnf)
{}
uint16_t allocate_pid() {
return 0;
}
};
} // end namespace async_mqtt5::test
#endif // ASYNC_MQTT5_TEST_TEST_SERVICE_HPP
+336
View File
@@ -0,0 +1,336 @@
#ifndef ASYNC_MQTT5_TEST_TEST_STREAM_HPP
#define ASYNC_MQTT5_TEST_TEST_STREAM_HPP
#include <boost/asio/dispatch.hpp>
#include <boost/asio/execution_context.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/prepend.hpp>
#include <boost/asio/recycling_allocator.hpp>
#include <boost/asio/ip/tcp.hpp>
#include "test_common/test_broker.hpp"
namespace async_mqtt5::test {
namespace asio = boost::asio;
using error_code = boost::system::error_code;
using time_stamp = std::chrono::time_point<std::chrono::steady_clock>;
using duration = time_stamp::duration;
namespace detail {
class test_stream_impl {
public:
using executor_type = test_broker::executor_type;
using protocol_type = test_broker::protocol_type;
using endpoint_type = test_broker::endpoint_type;
private:
executor_type _ex;
test_broker* _test_broker { nullptr };
endpoint_type _remote_ep;
template <typename Handler>
friend class read_op;
template <typename Handler>
friend class write_op;
public:
test_stream_impl(executor_type ex) : _ex(std::move(ex)) {}
executor_type get_executor() const noexcept {
return _ex;
}
void open(const protocol_type&, error_code& ec) {
ec = {};
_test_broker = &asio::use_service<test_broker>(_ex.context());
}
void close(error_code& ec) {
disconnect();
ec = {};
_test_broker = nullptr;
}
void shutdown(asio::ip::tcp::socket::shutdown_type, error_code& ec) {
ec = {};
}
void connect(const endpoint_type& ep, error_code& ec) {
ec = {};
_remote_ep = ep;
}
void disconnect() {
_remote_ep = {};
if (_test_broker)
_test_broker->close_connection();
}
endpoint_type remote_endpoint(error_code& ec) {
if (_remote_ep == endpoint_type {})
ec = asio::error::not_connected;
else
ec = {};
return _remote_ep;
}
bool is_open() const {
return _test_broker != nullptr;
}
bool is_connected() const {
return _remote_ep != endpoint_type {};
}
};
template <typename Handler>
class read_op {
struct on_read {};
std::shared_ptr<test_stream_impl> _stream_impl;
Handler _handler;
public:
read_op(
std::shared_ptr<test_stream_impl> stream_impl, Handler handler
) :
_stream_impl(std::move(stream_impl)),
_handler(std::move(handler))
{}
read_op(read_op&&) noexcept = default;
read_op(const read_op&) = delete;
using executor_type = test_stream_impl::executor_type;
executor_type get_executor() const noexcept {
return _stream_impl->get_executor();
}
using allocator_type = asio::recycling_allocator<void>;
allocator_type get_allocator() const noexcept {
return allocator_type {};
}
template <typename BufferType>
void perform(const BufferType& buffer) {
if (!_stream_impl->is_open() || !_stream_impl->is_connected())
return complete_post(asio::error::not_connected, 0);
_stream_impl->_test_broker->read_from_network(
buffer,
asio::prepend(std::move(*this), on_read {})
);
}
void operator()(on_read, error_code ec, size_t bytes_read) {
if (ec)
_stream_impl->disconnect();
complete(ec, bytes_read);
}
private:
void complete_post(error_code ec, size_t bytes_read) {
asio::post(
get_executor(),
asio::prepend(std::move(_handler), ec, bytes_read)
);
}
void complete(error_code ec, size_t bytes_read) {
asio::dispatch(
get_executor(),
asio::prepend(std::move(_handler), ec, bytes_read)
);
}
};
template <typename Handler>
class write_op {
struct on_write {};
std::shared_ptr<test_stream_impl> _stream_impl;
Handler _handler;
public:
write_op(
std::shared_ptr<test_stream_impl> stream_impl, Handler handler
) :
_stream_impl(std::move(stream_impl)),
_handler(std::move(handler))
{}
write_op(write_op&&) noexcept = default;
write_op(const write_op&) = delete;
using executor_type = test_stream_impl::executor_type;
executor_type get_executor() const noexcept {
return _stream_impl->get_executor();
}
using allocator_type = asio::recycling_allocator<void>;
allocator_type get_allocator() const noexcept {
return allocator_type {};
}
template <typename BufferType>
void perform(const BufferType& buffers) {
if (!_stream_impl->is_open() || !_stream_impl->is_connected())
return complete_post(asio::error::not_connected, 0);
_stream_impl->_test_broker->write_to_network(
buffers,
asio::prepend(std::move(*this), on_write {})
);
}
void operator()(on_write, error_code ec, size_t bytes_written) {
if (ec)
_stream_impl->disconnect();
complete(ec, bytes_written);
}
private:
void complete_post(error_code ec, size_t bytes_written) {
asio::post(
get_executor(),
asio::prepend(std::move(_handler), ec, bytes_written)
);
}
void complete(error_code ec, size_t bytes_written) {
asio::dispatch(
get_executor(),
asio::prepend(std::move(_handler), ec, bytes_written)
);
}
};
} // end namespace detail
class test_stream {
public:
using executor_type = test_broker::executor_type;
using protocol_type = test_broker::protocol_type;
using endpoint_type = test_broker::endpoint_type;
private:
std::shared_ptr<detail::test_stream_impl> _impl;
public:
test_stream(executor_type ex) :
_impl(std::make_shared<detail::test_stream_impl>(std::move(ex)))
{}
test_stream(const test_stream&) = delete;
~test_stream() {
error_code ec;
close(ec); // cancel() would be more appropriate
}
executor_type get_executor() const noexcept {
return _impl->get_executor();
}
void open(const protocol_type& p, error_code& ec) {
_impl->open(p, ec);
}
void close(error_code& ec) {
_impl->close(ec);
}
void connect(const endpoint_type& ep, error_code& ec) {
_impl->connect(ep, ec);
}
void disconnect() {
_impl->disconnect();
}
bool is_open() const {
return _impl->is_open();
}
bool is_connected() const {
return _impl->is_connected();
}
void shutdown(asio::ip::tcp::socket::shutdown_type st, error_code& ec) {
return _impl->shutdown(st, ec);
}
endpoint_type remote_endpoint(error_code& ec) {
return _impl->remote_endpoint(ec);
}
template<typename SettableSocketOption>
void set_option(const SettableSocketOption&, error_code&) {}
template <typename ConnectToken>
decltype(auto) async_connect(
const endpoint_type& ep, ConnectToken&& token
) {
auto initiation = [this](auto handler, const endpoint_type& ep) {
error_code ec;
open(asio::ip::tcp::v4(), ec);
if (!ec)
connect(ep, ec);
asio::post(get_executor(), asio::prepend(std::move(handler), ec));
};
return asio::async_initiate<ConnectToken, void (error_code)>(
std::move(initiation), token, ep
);
}
template<typename ConstBufferSequence, typename WriteToken>
decltype(auto) async_write_some(
const ConstBufferSequence& buffers, WriteToken&& token
) {
using Signature = void (error_code, size_t);
auto initiation = [this](
auto handler, const ConstBufferSequence& buffers
) {
detail::write_op { _impl, std::move(handler) }.perform(buffers);
};
return asio::async_initiate<WriteToken, Signature>(
std::move(initiation), token, buffers
);
}
template<typename MutableBufferSequence, typename ReadToken>
decltype(auto) async_read_some(
const MutableBufferSequence& buffers,
ReadToken&& token
) {
using Signature = void (error_code, size_t);
auto initiation = [this](
auto handler, const MutableBufferSequence& buffers
) {
detail::read_op { _impl, std::move(handler) }.perform(buffers);
};
return asio::async_initiate<ReadToken, Signature>(
std::move(initiation), token, buffers
);
}
};
} // end namespace async_mqtt5::test
#endif // ASYNC_MQTT5_TEST_TEST_STREAM_HPP