forked from boostorg/mqtt5
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:
1 parent
e3eb408c98
commit
2f6751d162
27 files changed
+884
-855
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user