diff --git a/cmake/modules/Findcetl.cmake b/cmake/modules/Findcetl.cmake index 3c64e4d04..c489e0e39 100644 --- a/cmake/modules/Findcetl.cmake +++ b/cmake/modules/Findcetl.cmake @@ -6,7 +6,7 @@ include(FetchContent) set(cetl_GIT_REPOSITORY "https://github.com/OpenCyphal/cetl.git") -set(cetl_GIT_TAG "5aa0a5b62427547b63cbbf966bcc4cde2abe396e") +set(cetl_GIT_TAG "a1ba92512924e4851ce95dd68b4525a6f9d4fc75") FetchContent_Declare( cetl diff --git a/include/libcyphal/runnable.hpp b/include/libcyphal/runnable.hpp index 5ed899ed3..ed1944fab 100644 --- a/include/libcyphal/runnable.hpp +++ b/include/libcyphal/runnable.hpp @@ -6,7 +6,6 @@ #ifndef LIBCYPHAL_RUNNABLE_HPP_INCLUDED #define LIBCYPHAL_RUNNABLE_HPP_INCLUDED -#include "transport/errors.hpp" #include "types.hpp" #include diff --git a/include/libcyphal/transport/can/can_transport.hpp b/include/libcyphal/transport/can/can_transport.hpp index f53b2d282..095bf9220 100644 --- a/include/libcyphal/transport/can/can_transport.hpp +++ b/include/libcyphal/transport/can/can_transport.hpp @@ -6,9 +6,9 @@ #ifndef LIBCYPHAL_TRANSPORT_CAN_TRANSPORT_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_CAN_TRANSPORT_HPP_INCLUDED -#include "libcyphal/transport/can/media.hpp" #include "libcyphal/transport/errors.hpp" #include "libcyphal/transport/transport.hpp" +#include "media.hpp" #include #include diff --git a/include/libcyphal/transport/can/can_transport_impl.hpp b/include/libcyphal/transport/can/can_transport_impl.hpp index 6423e638e..3b2fe9c64 100644 --- a/include/libcyphal/transport/can/can_transport_impl.hpp +++ b/include/libcyphal/transport/can/can_transport_impl.hpp @@ -15,6 +15,7 @@ #include "svc_tx_sessions.hpp" #include "libcyphal/runnable.hpp" +#include "libcyphal/transport/common/tools.hpp" #include "libcyphal/transport/contiguous_payload.hpp" #include "libcyphal/transport/errors.hpp" #include "libcyphal/transport/msg_sessions.hpp" @@ -51,23 +52,20 @@ namespace detail /// @brief Represents final implementation class of the CAN transport. /// /// NOSONAR cpp:S4963 for below `class TransportImpl` - we do directly handle resources here; -/// namely: in destructor we have to unsubscribe, as well as let delegate to know this fact. +/// namely: in destructor we have to flush TX queues (otherwise there will be memory leaks). /// class TransportImpl final : private TransportDelegate, public ICanTransport // NOSONAR cpp:S4963 { - /// @brief Defines specification for making interface unique ptr. + /// @brief Defines private specification for making interface unique ptr. /// - struct Spec + struct Spec : libcyphal::detail::UniquePtrSpec { - using Interface = ICanTransport; - using Concrete = TransportImpl; - - // In use to disable public construction. + // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ explicit Spec() = default; }; - /// @brief Internal (private) storage of a media index, its interface and TX queue. + /// @brief Defines private storage of a media index, its interface and TX queue. /// struct Media final { @@ -113,10 +111,9 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // { // Verify input arguments: // - At least one media interface must be provided, but no more than the maximum allowed (255). - // - If a local node ID is provided, it must be within the valid range. // - const auto media_count = - static_cast(std::count_if(media.begin(), media.end(), [](const IMedia* const media_ptr) { + const auto media_count = static_cast( + std::count_if(media.begin(), media.end(), [](const IMedia* const media_ptr) -> bool { return media_ptr != nullptr; })); if ((media_count == 0) || (media_count > std::numeric_limits::max())) @@ -124,13 +121,15 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // return ArgumentError{}; } - const MediaArray media_array{make_media_array(memory, media_count, media, tx_capacity)}; + // False positive of clang-tidy - we move `media_array` to the `transport` instance, so can't make it const. + // NOLINTNEXTLINE(misc-const-correctness) + MediaArray media_array = makeMediaArray(memory, media_count, media, tx_capacity); if (media_array.size() != media_count) { return MemoryError{}; } - auto transport = libcyphal::detail::makeUniquePtr(memory, Spec{}, memory, media_array); + auto transport = libcyphal::detail::makeUniquePtr(memory, Spec{}, memory, std::move(media_array)); if (transport == nullptr) { return MemoryError{}; @@ -139,7 +138,7 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // return transport; } - TransportImpl(Spec, cetl::pmr::memory_resource& memory, MediaArray media_array) + TransportImpl(const Spec, cetl::pmr::memory_resource& memory, MediaArray&& media_array) : TransportDelegate{memory} , media_array_{std::move(media_array)} , should_reconfigure_filters_{false} @@ -176,34 +175,32 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // CETL_NODISCARD cetl::optional getLocalNodeId() const noexcept override { - if (canard_instance().node_id > CANARD_NODE_ID_MAX) + if (node_id() > CANARD_NODE_ID_MAX) { return cetl::nullopt; } - return cetl::make_optional(static_cast(canard_instance().node_id)); + return cetl::make_optional(node_id()); } - CETL_NODISCARD cetl::optional setLocalNodeId(const NodeId node_id) noexcept override + CETL_NODISCARD cetl::optional setLocalNodeId(const NodeId new_node_id) noexcept override { - if (node_id > CANARD_NODE_ID_MAX) + if (new_node_id > CANARD_NODE_ID_MAX) { return ArgumentError{}; } // Allow setting the same node ID multiple times, but only once otherwise. // - CanardInstance& ins = canard_instance(); - if (ins.node_id == node_id) + if (node_id() == new_node_id) { return cetl::nullopt; } - if (ins.node_id != CANARD_NODE_ID_UNSET) + if (node_id() != CANARD_NODE_ID_UNSET) { return ArgumentError{}; } - - ins.node_id = static_cast(node_id); + canard_node_id() = static_cast(new_node_id); // We just became non-anonymous node, so we might need to reconfigure media filters // in case we have at least one service RX port. @@ -343,14 +340,15 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // { media.propagateMtuToTxQueue(); + // No Sonar `cpp:S5356` b/c we need to pass payload as a raw data to the libcanard. const std::int32_t result = ::canardTxPush(&media.canard_tx_queue(), &canard_instance(), static_cast(deadline_us.count()), &metadata, payload.size(), - payload.data()); + payload.data()); // NOSONAR cpp:S5356 - opt_any_error = tryHandleTransientCanardResult(result, media); + opt_any_error = tryHandleTransientCanardResult(media, result); if (opt_any_error.has_value()) { // The handler (if any) just said that it's NOT fine to continue with pushing to other media TX queues, @@ -362,53 +360,62 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // return opt_any_error; } - void triggerUpdateOfFilters(const FiltersUpdateCondition condition) noexcept override + void triggerUpdateOfFilters(const FiltersUpdate::Variant& update_var) override { - switch (condition) - { - case FiltersUpdateCondition::SubjectPortAdded: { - ++total_message_ports_; - break; - } - case FiltersUpdateCondition::SubjectPortRemoved: { - // We are not going to allow negative number of ports. - CETL_DEBUG_ASSERT(total_message_ports_ > 0, ""); - total_message_ports_ -= std::min(static_cast(1), total_message_ports_); - break; - } - case FiltersUpdateCondition::ServicePortAdded: { - ++total_service_ports_; - break; - } - case FiltersUpdateCondition::ServicePortRemoved: { - // We are not going to allow negative number of ports. - CETL_DEBUG_ASSERT(total_service_ports_ > 0, ""); - total_service_ports_ -= std::min(static_cast(1), total_service_ports_); - break; - } - default: { - // NOLINTNEXTLINE(cert-dcl03-c,hicpp-static-assert,misc-static-assert) - CETL_DEBUG_ASSERT(false, "Unexpected condition."); - return; - } - } + FiltersUpdateHandler handler_with{*this}; + cetl::visit(handler_with, update_var); should_reconfigure_filters_ = true; } // MARK: Privates: - template - CETL_NODISCARD static AnyError anyErrorFromVariant(ErrorVariant&& other_error_var) + using Self = TransportImpl; + + struct FiltersUpdateHandler { - return cetl::visit([](auto&& error) -> AnyError { return std::forward(error); }, - std::forward(other_error_var)); - } + explicit FiltersUpdateHandler(Self& self) + : self_{self} + { + } + + void operator()(const FiltersUpdate::SubjectPort& port) const + { + if (port.is_added) + { + ++self_.total_message_ports_; + } + else + { + // We are not going to allow negative number of ports. + CETL_DEBUG_ASSERT(self_.total_message_ports_ > 0, ""); + self_.total_message_ports_ -= std::min(static_cast(1), self_.total_message_ports_); + } + } + + void operator()(const FiltersUpdate::ServicePort& port) const + { + if (port.is_added) + { + ++self_.total_service_ports_; + } + else + { + // We are not going to allow negative number of ports. + CETL_DEBUG_ASSERT(self_.total_service_ports_ > 0, ""); + self_.total_service_ports_ -= std::min(static_cast(1), self_.total_service_ports_); + } + } + + private: + Self& self_; + + }; // FiltersUpdateHandler template - CETL_NODISCARD cetl::optional tryHandleTransientMediaError(MediaError&& media_error, const Media& media) + CETL_NODISCARD cetl::optional tryHandleTransientMediaError(const Media& media, MediaError&& error) { - AnyError any_error = anyErrorFromVariant(std::move(media_error)); + AnyError any_error = common::detail::anyErrorFromVariant(std::move(error)); if (!transient_error_handler_) { return any_error; @@ -419,8 +426,8 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // } template - CETL_NODISCARD cetl::optional tryHandleTransientCanardResult(const std::int32_t result, - const Media& media) + CETL_NODISCARD cetl::optional tryHandleTransientCanardResult(const Media& media, + const std::int32_t result) { cetl::optional opt_any_error = optAnyErrorFromCanard(result); if (opt_any_error.has_value() && transient_error_handler_) @@ -446,10 +453,10 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // return cetl::nullopt; } - CETL_NODISCARD static MediaArray make_media_array(cetl::pmr::memory_resource& memory, - const std::size_t media_count, - const cetl::span media_interfaces, - const std::size_t tx_capacity) + CETL_NODISCARD static MediaArray makeMediaArray(cetl::pmr::memory_resource& memory, + const std::size_t media_count, + const cetl::span media_interfaces, + const std::size_t tx_capacity) { MediaArray media_array{media_count, &memory}; @@ -475,12 +482,14 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // return media_array; } - void flushCanardTxQueue(CanardTxQueue& canard_tx_queue) + void flushCanardTxQueue(CanardTxQueue& canard_tx_queue) const { while (const CanardTxQueueItem* const maybe_item = ::canardTxPeek(&canard_tx_queue)) { CanardTxQueueItem* const item = ::canardTxPop(&canard_tx_queue, maybe_item); - freeCanardMemory(item); + + // No Sonar `cpp:S5356` b/c we need to free tx item allocated by libcanard as a raw memory. + freeCanardMemory(item); // NOSONAR cpp:S5356 } } @@ -505,17 +514,17 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // std::array payload{}; Expected, MediaError> pop_result = media.interface().pop(payload); - if (auto* media_error = cetl::get_if(&pop_result)) + if (auto* const error = cetl::get_if(&pop_result)) { - return tryHandleTransientMediaError(std::move(*media_error), media); + return tryHandleTransientMediaError(media, std::move(*error)); } - const auto* const opt_rx_meta = cetl::get_if>(&pop_result); - if ((opt_rx_meta == nullptr) || !opt_rx_meta->has_value()) + const auto& opt_rx_meta = cetl::get>(pop_result); + if (!opt_rx_meta.has_value()) { return cetl::nullopt; } - const RxMetadata& rx_meta = opt_rx_meta->value(); + const RxMetadata& rx_meta = opt_rx_meta.value(); const auto timestamp_us = std::chrono::duration_cast(rx_meta.timestamp.time_since_epoch()); @@ -524,20 +533,24 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // CanardRxTransfer out_transfer{}; CanardRxSubscription* out_subscription{}; - const std::int8_t result = ::canardRxAccept(&canard_instance(), + const std::int8_t result = ::canardRxAccept(&canard_instance(), static_cast(timestamp_us.count()), &canard_frame, media.index(), &out_transfer, &out_subscription); + cetl::optional opt_any_error = - tryHandleTransientCanardResult(result, media); - if (!opt_any_error.has_value() && (result > 0)) + tryHandleTransientCanardResult(media, result); + if ((!opt_any_error.has_value()) && (result > 0)) { CETL_DEBUG_ASSERT(out_subscription != nullptr, "Expected subscription."); CETL_DEBUG_ASSERT(out_subscription->user_reference != nullptr, "Expected session delegate."); - auto* const delegate = static_cast(out_subscription->user_reference); + // No Sonar `cpp:S5357` b/c the raw `user_reference` is part of libcanard api, + // and it was set by us at a RX session constructor (see f.e. `MessageRxSession` ctor). + auto* const delegate = + static_cast(out_subscription->user_reference); // NOSONAR cpp:S5357 delegate->acceptRxTransfer(out_transfer); } @@ -570,13 +583,6 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // { while (const CanardTxQueueItem* const tx_item = ::canardTxPeek(&media.canard_tx_queue())) { - // Prepare a lambda to pop and free the TX queue item, but not just yet. - // In case of media not being ready to push this item, we will need to retry it on next `run`. - // - auto popAndFreeCanardTxQueueItem = [this, tx_queue = &media.canard_tx_queue(), tx_item]() { - freeCanardMemory(::canardTxPop(tx_queue, tx_item)); - }; - // We are dropping any TX item that has expired. // Otherwise, we would send it to the media interface. // We use strictly `>=` (instead of `>`) to give this frame a chance (one extra 1us) at media level. @@ -584,13 +590,20 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // const auto deadline = TimePoint{std::chrono::microseconds{tx_item->tx_deadline_usec}}; if (now >= deadline) { - popAndFreeCanardTxQueueItem(); - continue; + // Release whole expired transfer b/c possible next frames of the same transfer are also expired. + popAndFreeCanardTxQueueItem(&media.canard_tx_queue(), tx_item, true /*whole transfer*/); + + // No Sonar `cpp:S909` b/c it make sense to use `continue` statement here - the corner case of + // "early" (by deadline) transfer drop. Using `if` would make the code less readable and more nested. + continue; // NOSONAR cpp:S909 } - const cetl::span payload{static_cast(tx_item->frame.payload), - tx_item->frame.payload_size}; - Expected maybe_pushed = + // No Sonar `cpp:S5356` and `cpp:S5357` b/c we integrate here with C libcanard API. + const auto* const buffer = + static_cast(tx_item->frame.payload); // NOSONAR cpp:S5356 cpp:S5357 + const cetl::span payload{buffer, tx_item->frame.payload_size}; + + Expected maybe_pushed = media.interface().push(deadline, tx_item->frame.extended_can_id, payload); // In case of media push error we are going to drop this problematic frame @@ -599,34 +612,36 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // // Note that media not being ready/able to push a frame just yet (aka temporary) // is not reported as an error (see `is_pushed` below). // - if (auto* media_error = cetl::get_if(&maybe_pushed)) + if (auto* const error = cetl::get_if(&maybe_pushed)) { - // Release problematic frame from the TX queue, so that other frames in TX queue have their chance. + // Release whole problematic transfer from the TX queue, + // so that other transfers in TX queue have their chance. // Otherwise, we would be stuck in a run loop trying to push the same frame. - popAndFreeCanardTxQueueItem(); + popAndFreeCanardTxQueueItem(&media.canard_tx_queue(), tx_item, true /*whole transfer*/); cetl::optional opt_any_error = - tryHandleTransientMediaError(std::move(*media_error), media); - if (!opt_any_error.has_value()) + tryHandleTransientMediaError(media, std::move(*error)); + if (opt_any_error.has_value()) { - // The handler (if any) just said that it's fine to continue with pushing other frames - // and ignore such a transient media error (and don't propagate it outside). - continue; + return opt_any_error; } - return opt_any_error; + // The handler just said that it's fine to continue with pushing other frames + // and ignore such a transient media error (and don't propagate it outside). } - - const auto* const is_pushed = cetl::get_if(&maybe_pushed); - if ((is_pushed != nullptr) && !*is_pushed) + else { - // Media interface is busy, so we are done with this media for now, - // and will just try again with it later (on next `run`). - // Note, we are NOT releasing this item from the queue, so it will be retried on next `run`. - break; - } + const auto is_pushed = cetl::get(maybe_pushed); + if (!is_pushed) + { + // Media interface is busy, so we are done with this media for now, + // and will just try again with it later (on next `run`). + // Note, we are NOT releasing this item from the queue, so it will be retried on next `run`. + break; + } - popAndFreeCanardTxQueueItem(); + popAndFreeCanardTxQueueItem(&media.canard_tx_queue(), tx_item, false /*single frame*/); + } } // for each frame @@ -667,14 +682,13 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // bool was_error = false; for (const Media& media : media_array_) { - cetl::optional media_error = media.interface().setFilters({filters.data(), filters.size()}); - if (media_error.has_value()) + cetl::optional error = media.interface().setFilters({filters.data(), filters.size()}); + if (error.has_value()) { was_error = true; cetl::optional opt_any_error = - tryHandleTransientMediaError(std::move(media_error.value()), - media); + tryHandleTransientMediaError(media, std::move(error.value())); if (opt_any_error.has_value()) { // The handler (if any) just said that it's NOT fine to continue with configuring other media, @@ -704,7 +718,7 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // // Total "active" RX ports depends on the local node ID. For anonymous nodes, // we don't account for service ports (b/c they don't work while being anonymous). // - const CanardNodeID local_node_id = canard_instance().node_id; + const CanardNodeID local_node_id = canard_node_id(); const auto is_anonymous = local_node_id > CANARD_NODE_ID_MAX; const std::size_t total_active_ports = total_message_ports_ + (is_anonymous ? 0 : total_service_ports_); if (total_active_ports == 0) @@ -740,7 +754,7 @@ class TransportImpl final : private TransportDelegate, public ICanTransport // // No need to make service filters if we don't have a local node ID. // - if ((total_service_ports_ > 0) && !is_anonymous) + if ((total_service_ports_ > 0) && (!is_anonymous)) { const auto svc_visitor = [&filters, local_node_id](RxSubscription& rx_subscription) { // Make and store a single service filter. diff --git a/include/libcyphal/transport/can/delegate.hpp b/include/libcyphal/transport/can/delegate.hpp index f49f2273b..e63bd81d8 100644 --- a/include/libcyphal/transport/can/delegate.hpp +++ b/include/libcyphal/transport/can/delegate.hpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace libcyphal { @@ -73,7 +74,8 @@ class TransportDelegate { if (buffer_ != nullptr) { - delegate_.freeCanardMemory(buffer_); + // No Sonar `cpp:S5356` b/c we integrate here with C libcanard memory management. + delegate_.freeCanardMemory(buffer_); // NOSONAR cpp:S5356 } } @@ -101,8 +103,9 @@ class TransportDelegate const std::size_t bytes_to_copy = std::min(length_bytes, payload_size_ - offset_bytes); // Next nolint is unavoidable: we need offset from the beginning of the buffer. + // No Sonar `cpp:S5356` b/c we integrate here with libcanard raw C buffers. // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - (void) std::memmove(destination, buffer_ + offset_bytes, bytes_to_copy); + (void) std::memmove(destination, buffer_ + offset_bytes, bytes_to_copy); // NOSONAR cpp:S5356 return bytes_to_copy; } @@ -127,7 +130,8 @@ class TransportDelegate /// Recursion goes first to the left child, then to the current node, and finally to the right child. /// B/c AVL tree is balanced, the total complexity is `O(n)` and call stack depth should not be deeper than /// ~O(log(N)) (`ceil(1.44 * log2(N + 2) - 0.328)` to be precise, or 19 in case of 8192 ports), - /// where `N` is the total number of tree nodes. Hence, the `NOLINTNEXTLINE(misc-no-recursion)` exception. + /// where `N` is the total number of tree nodes. Hence, the `NOLINTNEXTLINE(misc-no-recursion)` + /// and `NOSONAR cpp:S925` exceptions. /// /// @tparam Visitor Type of the visitor callable. /// @param node (sub-)root node of the AVL tree. Could be `nullptr`. @@ -146,45 +150,50 @@ class TransportDelegate // Initial `1` is for the current node. std::size_t count = 1; - count += visitCounting(node->lr[0], visitor); + count += visitCounting(node->lr[0], visitor); // NOSONAR cpp:S925 // Next nolint & NOSONAR are unavoidable: this is integration with low-level C code of Canard AVL trees. // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) visitor(*reinterpret_cast(node)); // NOSONAR cpp:S3630 - count += visitCounting(node->lr[1], visitor); + count += visitCounting(node->lr[1], visitor); // NOSONAR cpp:S925 return count; } }; // CanardConcreteTree - enum class FiltersUpdateCondition : std::uint8_t + struct FiltersUpdate { - SubjectPortAdded, - SubjectPortRemoved, - ServicePortAdded, - ServicePortRemoved - }; + struct SubjectPort + { + bool is_added; + }; + struct ServicePort + { + bool is_added; + }; - explicit TransportDelegate(cetl::pmr::memory_resource& memory) - : memory_{memory} - , canard_instance_{::canardInit(allocateMemoryForCanard, freeCanardMemory)} - { - canard_instance().user_reference = this; - } + using Variant = cetl::variant; + + }; // FiltersUpdate TransportDelegate(const TransportDelegate&) = delete; TransportDelegate(TransportDelegate&&) noexcept = delete; TransportDelegate& operator=(const TransportDelegate&) = delete; TransportDelegate& operator=(TransportDelegate&&) noexcept = delete; - CETL_NODISCARD CanardInstance& canard_instance() noexcept + CETL_NODISCARD NodeId node_id() const noexcept { - return canard_instance_; + return canard_instance_.node_id; } - CETL_NODISCARD const CanardInstance& canard_instance() const noexcept + CETL_NODISCARD CanardNodeID& canard_node_id() noexcept + { + return canard_instance_.node_id; + } + + CETL_NODISCARD CanardInstance& canard_instance() noexcept { return canard_instance_; } @@ -213,21 +222,46 @@ class TransportDelegate /// @brief Releases memory allocated for canard (by previous `allocateMemoryForCanard` call). /// - /// NOSONAR cpp:S5008 b/s it is unavoidable: this is integration with low-level C code of Canard memory management. + /// No Sonar `cpp:S5008` and `cpp:S5356` b/c they are unavoidable - + /// this is integration with low-level C code of Canard memory management. /// - void freeCanardMemory(void* const pointer) // NOSONAR cpp:S5008 + void freeCanardMemory(void* const pointer) const // NOSONAR cpp:S5008 { if (pointer == nullptr) { return; } - auto* memory_header = static_cast(pointer); + auto* memory_header = static_cast(pointer); // NOSONAR cpp:S5356 // Next nolint is unavoidable: this is integration with C code of Canard memory management. // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) --memory_header; - memory_.deallocate(memory_header, memory_header->size); + memory_.deallocate(memory_header, memory_header->size); // NOSONAR cpp:S5356 + } + + /// Pops and frees Canard TX queue item(s). + /// + /// @param tx_queue The TX queue from which the item should be popped. + /// @param tx_item The TX queue item to be popped and freed. + /// @param whole_transfer If `true` then whole transfer should be released from the queue. + /// + void popAndFreeCanardTxQueueItem(CanardTxQueue* const tx_queue, + const CanardTxQueueItem* tx_item, + const bool whole_transfer) const + { + while (CanardTxQueueItem* const mut_tx_item = ::canardTxPop(tx_queue, tx_item)) + { + tx_item = tx_item->next_in_transfer; + + // No Sonar `cpp:S5356` b/c we need to free tx item allocated by libcanard as a raw memory. + freeCanardMemory(mut_tx_item); // NOSONAR cpp:S5356 + + if (!whole_transfer) + { + break; + } + } } /// @brief Sends transfer to each media canard TX queue of the transport. @@ -242,12 +276,20 @@ class TransportDelegate /// /// Actual update will be done on next `run` of transport. /// - /// @param condition Describes condition in which the RX ports change has happened. Allows to distinguish - /// between subject and service ports, and between adding and removing a port. + /// @param update_var Describes variant of which the RX ports update has happened. + /// Allows to distinguish between subject and service ports. /// - virtual void triggerUpdateOfFilters(const FiltersUpdateCondition condition) noexcept = 0; + virtual void triggerUpdateOfFilters(const FiltersUpdate::Variant& update_var) = 0; protected: + explicit TransportDelegate(cetl::pmr::memory_resource& memory) + : memory_{memory} + , canard_instance_{::canardInit(allocateMemoryForCanard, freeCanardMemory)} + { + // No Sonar `cpp:S5356` b/c we integrate here with C libcanard API. + canard_instance().user_reference = this; // NOSONAR cpp:S5356 + } + ~TransportDelegate() = default; private: @@ -270,7 +312,9 @@ class TransportDelegate CETL_DEBUG_ASSERT(ins != nullptr, "Expected canard instance."); CETL_DEBUG_ASSERT(ins->user_reference != nullptr, "Expected `this` transport as user reference."); - return *static_cast(ins->user_reference); + // No Sonar `cpp:S5357` b/c the raw `user_reference` is part of libcanard api, + // and it was set by us at this delegate constructor (see `TransportDelegate` ctor). + return *static_cast(ins->user_reference); // NOSONAR cpp:S5357 } /// @brief Allocates memory for canard instance. @@ -285,8 +329,11 @@ class TransportDelegate { TransportDelegate& self = getSelfFrom(ins); - const std::size_t memory_size = sizeof(CanardMemoryHeader) + amount; - auto* memory_header = static_cast(self.memory_.allocate(memory_size)); + const std::size_t memory_size = sizeof(CanardMemoryHeader) + amount; + + // No Sonar `cpp:S5356` and `cpp:S5357` b/c we integrate here with C libcanard memory management. + auto* memory_header = + static_cast(self.memory_.allocate(memory_size)); // NOSONAR cpp:S5356 cpp:S5357 if (memory_header == nullptr) { return nullptr; @@ -296,9 +343,10 @@ class TransportDelegate // The size is used in `canardMemoryFree` to deallocate the memory. // memory_header->size = memory_size; - // Next nolint is unavoidable: this is integration with C code of Canard memory management. + // Next nolint and no Sonar `cpp:S5356` are unavoidable - + // this is integration with C code of Canard memory management. // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) - return ++memory_header; + return ++memory_header; // NOSONAR cpp:S5356 } /// @brief Releases memory allocated for canard instance (by previous `allocateMemoryForCanard` call). @@ -309,7 +357,7 @@ class TransportDelegate static void freeCanardMemory(CanardInstance* ins, // NOSONAR cpp:S995 void* pointer) // NOSONAR cpp:S5008 { - TransportDelegate& self = getSelfFrom(ins); + const TransportDelegate& self = getSelfFrom(ins); self.freeCanardMemory(pointer); } diff --git a/include/libcyphal/transport/can/media.hpp b/include/libcyphal/transport/can/media.hpp index eccb0b677..286e44141 100644 --- a/include/libcyphal/transport/can/media.hpp +++ b/include/libcyphal/transport/can/media.hpp @@ -72,6 +72,9 @@ class IMedia /// @brief Schedules the frame for transmission asynchronously and return immediately. /// + /// @param deadline The deadline for the push operation. Media implementation should drop the payload + /// if the deadline is exceeded (aka `now > deadline`). + /// @param can_id The destination CAN ID of the frame. /// @return `true` if the frame is accepted or already timed out; /// `false` to try again later (f.e. b/c output TX queue is currently full). /// If any media error occurred, the frame will be dropped by transport. diff --git a/include/libcyphal/transport/can/msg_rx_session.hpp b/include/libcyphal/transport/can/msg_rx_session.hpp index a3b605f6f..0b10e3950 100644 --- a/include/libcyphal/transport/can/msg_rx_session.hpp +++ b/include/libcyphal/transport/can/msg_rx_session.hpp @@ -43,14 +43,11 @@ namespace detail /// class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSession // NOSONAR cpp:S4963 { - /// @brief Defines specification for making interface unique ptr. + /// @brief Defines private specification for making interface unique ptr. /// - struct Spec + struct Spec : libcyphal::detail::UniquePtrSpec { - using Interface = IMessageRxSession; - using Concrete = MessageRxSession; - - // In use to disable public construction. + // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ explicit Spec() = default; }; @@ -73,7 +70,7 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess return session; } - MessageRxSession(Spec, TransportDelegate& delegate, const MessageRxParams& params) + MessageRxSession(const Spec, TransportDelegate& delegate, const MessageRxParams& params) : delegate_{delegate} , params_{params} , subscription_{} @@ -88,9 +85,10 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); CETL_DEBUG_ASSERT(result > 0, "New subscription supposed to be made."); - subscription_.user_reference = static_cast(this); + // No Sonar `cpp:S5356` b/c we integrate here with C libcanard API. + subscription_.user_reference = static_cast(this); // NOSONAR cpp:S5356 - delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdateCondition::SubjectPortAdded); + delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdate::SubjectPort{true}); } MessageRxSession(const MessageRxSession&) = delete; @@ -106,7 +104,7 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); CETL_DEBUG_ASSERT(result > 0, "Subscription supposed to be made at constructor."); - delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdateCondition::SubjectPortRemoved); + delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdate::SubjectPort{false}); } private: @@ -119,9 +117,7 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess CETL_NODISCARD cetl::optional receive() override { - cetl::optional result{}; - result.swap(last_rx_transfer_); - return result; + return std::exchange(last_rx_transfer_, cetl::nullopt); } // MARK: IRxSession @@ -156,11 +152,11 @@ class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSess ? cetl::nullopt : cetl::make_optional(transfer.metadata.remote_node_id); - const MessageTransferMetadata meta{transfer_id, timestamp, priority, publisher_node_id}; - TransportDelegate::CanardMemory canard_memory{delegate_, - static_cast(transfer.payload), - transfer.payload_size}; + // No Sonar `cpp:S5356` and `cpp:S5357` b/c we need to pass raw data from C libcanard api. + auto* const buffer = static_cast(transfer.payload); // NOSONAR cpp:S5356 cpp:S5357 + TransportDelegate::CanardMemory canard_memory{delegate_, buffer, transfer.payload_size}; + const MessageTransferMetadata meta{transfer_id, timestamp, priority, publisher_node_id}; (void) last_rx_transfer_.emplace(MessageRxTransfer{meta, ScatteredBuffer{std::move(canard_memory)}}); } diff --git a/include/libcyphal/transport/can/msg_tx_session.hpp b/include/libcyphal/transport/can/msg_tx_session.hpp index a12758b23..d4ed88ccf 100644 --- a/include/libcyphal/transport/can/msg_tx_session.hpp +++ b/include/libcyphal/transport/can/msg_tx_session.hpp @@ -33,14 +33,11 @@ namespace detail class MessageTxSession final : public IMessageTxSession { - /// @brief Defines specification for making interface unique ptr. + /// @brief Defines private specification for making interface unique ptr. /// - struct Spec + struct Spec : libcyphal::detail::UniquePtrSpec { - using Interface = IMessageTxSession; - using Concrete = MessageTxSession; - - // In use to disable public construction. + // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ explicit Spec() = default; }; @@ -63,7 +60,7 @@ class MessageTxSession final : public IMessageTxSession return session; } - MessageTxSession(Spec, TransportDelegate& delegate, const MessageTxParams& params) + MessageTxSession(const Spec, TransportDelegate& delegate, const MessageTxParams& params) : delegate_{delegate} , params_{params} , send_timeout_{std::chrono::seconds{1}} diff --git a/include/libcyphal/transport/can/svc_rx_sessions.hpp b/include/libcyphal/transport/can/svc_rx_sessions.hpp index e9363721c..39ea649d5 100644 --- a/include/libcyphal/transport/can/svc_rx_sessions.hpp +++ b/include/libcyphal/transport/can/svc_rx_sessions.hpp @@ -19,7 +19,6 @@ #include #include -#include #include #include @@ -51,14 +50,11 @@ namespace detail template class SvcRxSession final : private IRxSessionDelegate, public Interface_ // NOSONAR cpp:S4963 { - /// @brief Defines specification for making interface unique ptr. + /// @brief Defines private specification for making interface unique ptr. /// - struct Spec + struct Spec : libcyphal::detail::UniquePtrSpec { - using Interface = Interface_; - using Concrete = SvcRxSession; - - // In use to disable public construction. + // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ explicit Spec() = default; }; @@ -81,7 +77,7 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ // NOS return session; } - SvcRxSession(Spec, TransportDelegate& delegate, const Params& params) + SvcRxSession(const Spec, TransportDelegate& delegate, const Params& params) : delegate_{delegate} , params_{params} , subscription_{} @@ -96,9 +92,10 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ // NOS CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); CETL_DEBUG_ASSERT(result > 0, "New subscription supposed to be made."); - subscription_.user_reference = static_cast(this); + // No Sonar `cpp:S5356` b/c we integrate here with C libcanard API. + subscription_.user_reference = static_cast(this); // NOSONAR cpp:S5356 - delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdateCondition::ServicePortAdded); + delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdate::ServicePort{true}); } SvcRxSession(const SvcRxSession&) = delete; @@ -113,7 +110,7 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ // NOS CETL_DEBUG_ASSERT(result >= 0, "There is no way currently to get an error here."); CETL_DEBUG_ASSERT(result > 0, "Subscription supposed to be made at constructor."); - delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdateCondition::ServicePortRemoved); + delegate_.triggerUpdateOfFilters(TransportDelegate::FiltersUpdate::ServicePort{false}); } private: @@ -126,9 +123,7 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ // NOS CETL_NODISCARD cetl::optional receive() override { - cetl::optional result{}; - result.swap(last_rx_transfer_); - return result; + return std::exchange(last_rx_transfer_, cetl::nullopt); } // MARK: IRxSession @@ -159,11 +154,11 @@ class SvcRxSession final : private IRxSessionDelegate, public Interface_ // NOS const auto transfer_id = static_cast(transfer.metadata.transfer_id); const auto timestamp = TimePoint{std::chrono::microseconds{transfer.timestamp_usec}}; - const ServiceTransferMetadata meta{transfer_id, timestamp, priority, remote_node_id}; - TransportDelegate::CanardMemory canard_memory{delegate_, - static_cast(transfer.payload), - transfer.payload_size}; + // No Sonar `cpp:S5356` and `cpp:S5357` b/c we need to pass raw data from C libcanard api. + auto* const buffer = static_cast(transfer.payload); // NOSONAR cpp:S5356 cpp:S5357 + TransportDelegate::CanardMemory canard_memory{delegate_, buffer, transfer.payload_size}; + const ServiceTransferMetadata meta{transfer_id, timestamp, priority, remote_node_id}; (void) last_rx_transfer_.emplace(ServiceRxTransfer{meta, ScatteredBuffer{std::move(canard_memory)}}); } diff --git a/include/libcyphal/transport/can/svc_tx_sessions.hpp b/include/libcyphal/transport/can/svc_tx_sessions.hpp index 9ad7297b6..19e482476 100644 --- a/include/libcyphal/transport/can/svc_tx_sessions.hpp +++ b/include/libcyphal/transport/can/svc_tx_sessions.hpp @@ -35,14 +35,11 @@ namespace detail /// class SvcRequestTxSession final : public IRequestTxSession { - /// @brief Defines specification for making interface unique ptr. + /// @brief Defines private specification for making interface unique ptr. /// - struct Spec + struct Spec : libcyphal::detail::UniquePtrSpec { - using Interface = IRequestTxSession; - using Concrete = SvcRequestTxSession; - - // In use to disable public construction. + // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ explicit Spec() = default; }; @@ -65,7 +62,7 @@ class SvcRequestTxSession final : public IRequestTxSession return session; } - SvcRequestTxSession(Spec, TransportDelegate& delegate, const RequestTxParams& params) + SvcRequestTxSession(const Spec, TransportDelegate& delegate, const RequestTxParams& params) : delegate_{delegate} , params_{params} , send_timeout_{std::chrono::seconds{1}} @@ -94,8 +91,7 @@ class SvcRequestTxSession final : public IRequestTxSession // Otherwise, transport may do some work (like possible payload allocation/copying, // media enumeration and pushing into their TX queues) doomed to fail with argument error. // - const CanardNodeID local_node_id = delegate_.canard_instance().node_id; - if (local_node_id > CANARD_NODE_ID_MAX) + if (delegate_.node_id() > CANARD_NODE_ID_MAX) { return ArgumentError{}; } @@ -131,14 +127,11 @@ class SvcRequestTxSession final : public IRequestTxSession /// class SvcResponseTxSession final : public IResponseTxSession { - /// @brief Defines specification for making interface unique ptr. + /// @brief Defines private specification for making interface unique ptr. /// - struct Spec + struct Spec : libcyphal::detail::UniquePtrSpec { - using Interface = IResponseTxSession; - using Concrete = SvcResponseTxSession; - - // In use to disable public construction. + // `explicit` here is in use to disable public construction of derived private `Spec` structs. // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ explicit Spec() = default; }; @@ -161,7 +154,7 @@ class SvcResponseTxSession final : public IResponseTxSession return session; } - SvcResponseTxSession(Spec, TransportDelegate& delegate, const ResponseTxParams& params) + SvcResponseTxSession(const Spec, TransportDelegate& delegate, const ResponseTxParams& params) : delegate_{delegate} , params_{params} , send_timeout_{std::chrono::seconds{1}} @@ -190,8 +183,7 @@ class SvcResponseTxSession final : public IResponseTxSession // Otherwise, transport may do some work (like possible payload allocation/copying, // media enumeration and pushing into their TX queues) doomed to fail with argument error. // - const CanardNodeID local_node_id = delegate_.canard_instance().node_id; - if ((local_node_id > CANARD_NODE_ID_MAX) || (metadata.remote_node_id > CANARD_NODE_ID_MAX)) + if ((delegate_.node_id() > CANARD_NODE_ID_MAX) || (metadata.remote_node_id > CANARD_NODE_ID_MAX)) { return ArgumentError{}; } diff --git a/include/libcyphal/transport/common/tools.hpp b/include/libcyphal/transport/common/tools.hpp new file mode 100644 index 000000000..a4c48f750 --- /dev/null +++ b/include/libcyphal/transport/common/tools.hpp @@ -0,0 +1,37 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_COMMON_TOOLS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_COMMON_TOOLS_HPP_INCLUDED + +#include "libcyphal/transport/errors.hpp" + +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace common +{ +namespace detail +{ + +/// @brief Converts a compatible (aka subset) error variant to transport's `AnyError` one. +/// +template +CETL_NODISCARD static AnyError anyErrorFromVariant(ErrorVariant&& error_var) +{ + return cetl::visit([](auto&& error) -> AnyError { return std::forward(error); }, + std::forward(error_var)); +} + +} // namespace detail +} // namespace common +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_COMMON_TOOLS_HPP_INCLUDED diff --git a/include/libcyphal/transport/svc_sessions.hpp b/include/libcyphal/transport/svc_sessions.hpp index 7eff12e7b..ce6f4c68f 100644 --- a/include/libcyphal/transport/svc_sessions.hpp +++ b/include/libcyphal/transport/svc_sessions.hpp @@ -6,6 +6,7 @@ #ifndef LIBCYPHAL_TRANSPORT_SVC_SESSION_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_SVC_SESSION_HPP_INCLUDED +#include "errors.hpp" #include "session.hpp" #include "types.hpp" diff --git a/include/libcyphal/transport/transport.hpp b/include/libcyphal/transport/transport.hpp index 5ff7ed1a6..c8a97f055 100644 --- a/include/libcyphal/transport/transport.hpp +++ b/include/libcyphal/transport/transport.hpp @@ -15,10 +15,6 @@ #include "libcyphal/types.hpp" #include -#include -#include - -#include namespace libcyphal { @@ -65,11 +61,11 @@ class ITransport : public IRunnable /// - an UDP transport may have a range of 0...65534 node ids (see `UDPARD_NODE_ID_MAX` in `udpard.h`) /// - a CAN bus transport may have a range of 0...127 node ids (see `CANARD_NODE_ID_MAX` in `canard.h`) /// - /// @param node_id Specific node ID to be assigned to this transport interface. + /// @param new_node_id Specific node ID to be assigned to this transport interface. /// @return `nullopt` on successful set (or when node ID is the same). /// Otherwise an `ArgumentError` in case of the subsequent calls or ID out of range. /// - virtual cetl::optional setLocalNodeId(const NodeId node_id) noexcept = 0; + virtual cetl::optional setLocalNodeId(const NodeId new_node_id) noexcept = 0; /// @brief Makes a message receive (RX) session. /// diff --git a/include/libcyphal/transport/udp/delegate.hpp b/include/libcyphal/transport/udp/delegate.hpp new file mode 100644 index 000000000..730d4fb43 --- /dev/null +++ b/include/libcyphal/transport/udp/delegate.hpp @@ -0,0 +1,259 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_DELEGATE_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_DELEGATE_HPP_INCLUDED + +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include +#include + +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +struct AnyUdpardTxMetadata +{ + struct Publish + { + UdpardMicrosecond deadline_us; + UdpardPriority priority; + UdpardPortID subject_id; + UdpardTransferID transfer_id; + }; + struct Request + { + UdpardMicrosecond deadline_us; + UdpardPriority priority; + UdpardPortID service_id; + UdpardNodeID server_node_id; + UdpardTransferID transfer_id; + }; + struct Respond + { + UdpardMicrosecond deadline_us; + UdpardPriority priority; + UdpardPortID service_id; + UdpardNodeID client_node_id; + UdpardTransferID transfer_id; + }; + + /// Defines variant of all possible transient error reports. + /// + using Variant = cetl::variant; + +}; // AnyUdpardTxMetadata + +/// This internal transport delegate class serves the following purposes: +/// 1. It provides memory management functions for the Udpard library. +/// 2. It provides a way to convert Udpard error codes to `AnyError` type. +/// 3. It provides an interface to access the transport from various session classes. +/// +class TransportDelegate +{ +public: + TransportDelegate(const TransportDelegate&) = delete; + TransportDelegate(TransportDelegate&&) noexcept = delete; + TransportDelegate& operator=(const TransportDelegate&) = delete; + TransportDelegate& operator=(TransportDelegate&&) noexcept = delete; + + CETL_NODISCARD NodeId node_id() const noexcept + { + return udpard_node_id_; + } + + CETL_NODISCARD UdpardNodeID& udpard_node_id() noexcept + { + return udpard_node_id_; + } + + static cetl::optional optAnyErrorFromUdpard(const std::int32_t result) + { + // Udpard error results are negative, so we need to negate them to get the error code. + const std::int32_t canard_error = -result; + + if (canard_error == UDPARD_ERROR_ARGUMENT) + { + return ArgumentError{}; + } + if (canard_error == UDPARD_ERROR_MEMORY) + { + return MemoryError{}; + } + if (canard_error == UDPARD_ERROR_CAPACITY) + { + return CapacityError{}; + } + if (canard_error == UDPARD_ERROR_ANONYMOUS) + { + return AnonymousError{}; + } + + return cetl::nullopt; + } + + /// Pops and frees Udpard TX queue item(s). + /// + /// @param tx_queue The TX queue from which the item should be popped. + /// @param tx_item The TX queue item to be popped and freed. + /// @param whole_transfer If `true` then whole transfer should be released from the queue. + /// + static void popAndFreeUdpardTxItem(UdpardTx* const tx_queue, const UdpardTxItem* tx_item, const bool whole_transfer) + { + while (UdpardTxItem* const mut_tx_item = ::udpardTxPop(tx_queue, tx_item)) + { + tx_item = tx_item->next_in_transfer; + + ::udpardTxFree(tx_queue->memory, mut_tx_item); + + if (!whole_transfer) + { + break; + } + } + } + + /// @brief Sends transfer to each media udpard TX queue of the transport. + /// + /// Internal method which is in use by TX session implementations to delegate actual sending to transport. + /// + CETL_NODISCARD virtual cetl::optional sendAnyTransfer(const AnyUdpardTxMetadata::Variant& tx_metadata_var, + const PayloadFragments payload_fragments) = 0; + +protected: + /// @brief Defines internal set of memory resources used by the UDP transport. + /// + struct MemoryResources + { + /// The general purpose memory resource is used to provide memory for the libcyphal library. + /// It is NOT used for any Udpard TX or RX transfers, payload (de)fragmentation or transient handles, + /// but only for the libcyphal internal needs (like `make*[Rx|Tx]Session` factory calls). + cetl::pmr::memory_resource& general; + + /// The session memory resource is used to provide memory for the Udpard session instances. + /// Each instance is fixed-size, so a trivial zero-fragmentation block allocator is sufficient. + UdpardMemoryResource session; + + /// The fragment handles are allocated per payload fragment; each handle contains a pointer to its fragment. + /// Each instance is of a very small fixed size, so a trivial zero-fragmentation block allocator is sufficient. + UdpardMemoryResource fragment; + + /// The library never allocates payload buffers itself, as they are handed over by the application via + /// receive calls. Once a buffer is handed over, the library may choose to keep it if it is deemed to be + /// necessary to complete a transfer reassembly, or to discard it if it is deemed to be unnecessary. + /// Discarded payload buffers are freed using this memory resource. + UdpardMemoryDeleter payload; + }; + + explicit TransportDelegate(const MemoryResources& memory_resources) + : udpard_node_id_{UDPARD_NODE_ID_UNSET} + , memory_resources_{memory_resources} + { + } + + ~TransportDelegate() = default; + + CETL_NODISCARD const MemoryResources& memoryResources() const noexcept + { + return memory_resources_; + } + + static UdpardMemoryResource makeUdpardMemoryResource(cetl::pmr::memory_resource* const custom, + cetl::pmr::memory_resource& general) + { + // No Sonar `cpp:S5356` b/c the raw `user_reference` is part of libudpard api. + void* const user_reference = (custom != nullptr) ? custom : &general; // NOSONAR cpp:S5356 + return UdpardMemoryResource{user_reference, deallocateMemoryForUdpard, allocateMemoryForUdpard}; + } + + static UdpardMemoryDeleter makeUdpardMemoryDeleter(cetl::pmr::memory_resource* const custom, + cetl::pmr::memory_resource& general) + { + // No Sonar `cpp:S5356` b/c the raw `user_reference` is part of libudpard api. + void* const user_reference = (custom != nullptr) ? custom : &general; // NOSONAR cpp:S5356 + return UdpardMemoryDeleter{user_reference, deallocateMemoryForUdpard}; + } + +private: + /// @brief Allocates memory for udpard. + /// + /// NOSONAR cpp:S5008 is unavoidable: this is integration with low-level C code of Udpard memory management. + /// + static void* allocateMemoryForUdpard(void* const user_reference, const size_t size) // NOSONAR cpp:S5008 + { + // No Sonar `cpp:S5356` and `cpp:S5357` b/c the raw `user_reference` is part of libudpard api, + // and it was set by us at `makeUdpardMemoryResource` call. + auto* const mr = static_cast(user_reference); // NOSONAR cpp:S5356 cpp:S5357 + CETL_DEBUG_ASSERT(mr != nullptr, "Memory resource should not be null."); + return mr->allocate(size); + } + + /// @brief Releases memory allocated for udpard (by previous `allocateMemoryForUdpard` call). + /// + /// NOSONAR cpp:S5008 is unavoidable: this is integration with low-level C code of Udpard memory management. + /// + static void deallocateMemoryForUdpard(void* const user_reference, // NOSONAR cpp:S5008 + const size_t size, + void* const pointer) // NOSONAR cpp:S5008 + { + // No Sonar `cpp:S5356` and `cpp:S5357` b/c the raw `user_reference` is part of libudpard api, + // and it was set by us at `makeUdpardMemoryResource` call. + auto* const mr = static_cast(user_reference); // NOSONAR cpp:S5356 cpp:S5357 + CETL_DEBUG_ASSERT(mr != nullptr, "Memory resource should not be null."); + mr->deallocate(pointer, size); + } + + // MARK: Data members: + + UdpardNodeID udpard_node_id_; + const MemoryResources memory_resources_; + +}; // TransportDelegate + +// MARK: - + +/// This internal session delegate class serves the following purpose: it provides an interface (aka gateway) +/// to access RX session from transport (by casting udpard `user_reference` member to this class). +/// +class IRxSessionDelegate +{ +public: + IRxSessionDelegate(const IRxSessionDelegate&) = delete; + IRxSessionDelegate(IRxSessionDelegate&&) noexcept = delete; + IRxSessionDelegate& operator=(const IRxSessionDelegate&) = delete; + IRxSessionDelegate& operator=(IRxSessionDelegate&&) noexcept = delete; + + /// @brief Accepts a received transfer from the transport dedicated to this RX session. + /// + virtual void acceptRxTransfer(const UdpardRxTransfer& transfer) = 0; + +protected: + IRxSessionDelegate() = default; + ~IRxSessionDelegate() = default; + +}; // IRxSessionDelegate + +} // namespace detail +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_DELEGATE_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/media.hpp b/include/libcyphal/transport/udp/media.hpp index ef041f166..c50ec4b98 100644 --- a/include/libcyphal/transport/udp/media.hpp +++ b/include/libcyphal/transport/udp/media.hpp @@ -6,7 +6,11 @@ #ifndef LIBCYPHAL_TRANSPORT_UDP_MEDIA_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_UDP_MEDIA_HPP_INCLUDED -#include +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/types.hpp" +#include "tx_rx_sockets.hpp" + +#include namespace libcyphal { @@ -27,12 +31,14 @@ class IMedia IMedia& operator=(const IMedia&) = delete; IMedia& operator=(IMedia&&) noexcept = delete; - /// @brief Get the maximum transmission unit (MTU) of the UDP media. + /// Constructs a new TX socket bound to this media. /// - /// This value may change arbitrarily at runtime. The transport implementation will query it before every - /// transmission on the port. This value has no effect on the reception pipeline as it can accept arbitrary MTU. + virtual Expected, cetl::variant> makeTxSocket() = 0; + + /// Constructs a new RX socket bound to the specified multicast group endpoint. /// - virtual std::size_t getMtu() const noexcept = 0; + virtual Expected, cetl::variant> makeRxSocket( + const IpEndpoint& multicast_endpoint) = 0; protected: IMedia() = default; diff --git a/include/libcyphal/transport/udp/msg_rx_session.hpp b/include/libcyphal/transport/udp/msg_rx_session.hpp new file mode 100644 index 000000000..30e823541 --- /dev/null +++ b/include/libcyphal/transport/udp/msg_rx_session.hpp @@ -0,0 +1,143 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_MSG_RX_SESSION_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_MSG_RX_SESSION_HPP_INCLUDED + +#include "delegate.hpp" + +#include "libcyphal/runnable.hpp" +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/msg_sessions.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include +#include + +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +/// @brief A class to represent a message subscriber RX session. +/// +/// NOSONAR cpp:S4963 for below `class MessageRxSession` - we do directly handle resources here; +/// namely: in destructor we have to unsubscribe, as well as let delegate to know this fact. +/// +class MessageRxSession final : private IRxSessionDelegate, public IMessageRxSession // NOSONAR cpp:S4963 +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec + { + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const MessageRxParams& params) + { + if (params.subject_id > UDPARD_SUBJECT_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + MessageRxSession(const Spec, TransportDelegate& delegate, const MessageRxParams& params) + : delegate_{delegate} + , params_{params} + { + // TODO: Implement! + (void) delegate_; + } + + MessageRxSession(const MessageRxSession&) = delete; + MessageRxSession(MessageRxSession&&) noexcept = delete; + MessageRxSession& operator=(const MessageRxSession&) = delete; + MessageRxSession& operator=(MessageRxSession&&) noexcept = delete; + + ~MessageRxSession() + { + // TODO: Implement! + (void) 0; + } + +private: + // MARK: IMessageRxSession + + CETL_NODISCARD MessageRxParams getParams() const noexcept override + { + return params_; + } + + CETL_NODISCARD cetl::optional receive() override + { + return std::exchange(last_rx_transfer_, cetl::nullopt); + } + + // MARK: IRxSession + + void setTransferIdTimeout(const Duration timeout) override + { + const auto timeout_us = std::chrono::duration_cast(timeout); + if (timeout_us.count() > 0) + { + // TODO: Implement! + } + } + + // MARK: IRunnable + + IRunnable::MaybeError run(const TimePoint) override + { + // Nothing to do here currently. + return {}; + } + + // MARK: IRxSessionDelegate + + void acceptRxTransfer(const UdpardRxTransfer&) override + { + // TODO: Implement! + } + + // MARK: Data members: + + TransportDelegate& delegate_; + const MessageRxParams params_; + cetl::optional last_rx_transfer_; + +}; // MessageRxSession + +} // namespace detail +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_MSG_RX_SESSION_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/msg_tx_session.hpp b/include/libcyphal/transport/udp/msg_tx_session.hpp new file mode 100644 index 000000000..0c2382f18 --- /dev/null +++ b/include/libcyphal/transport/udp/msg_tx_session.hpp @@ -0,0 +1,123 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_MSG_TX_SESSION_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_MSG_TX_SESSION_HPP_INCLUDED + +#include "delegate.hpp" + +#include "libcyphal/runnable.hpp" +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/msg_sessions.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +class MessageTxSession final : public IMessageTxSession +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec + { + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const MessageTxParams& params) + { + if (params.subject_id > UDPARD_SUBJECT_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + MessageTxSession(const Spec, TransportDelegate& delegate, const MessageTxParams& params) + : delegate_{delegate} + , params_{params} + , send_timeout_{std::chrono::seconds{1}} + { + } + +private: + // MARK: ITxSession + + void setSendTimeout(const Duration timeout) override + { + send_timeout_ = timeout; + } + + // MARK: IMessageTxSession + + CETL_NODISCARD MessageTxParams getParams() const noexcept override + { + return params_; + } + + CETL_NODISCARD cetl::optional send(const TransferMetadata& metadata, + const PayloadFragments payload_fragments) override + { + const auto deadline_us = std::chrono::duration_cast( + (metadata.timestamp + send_timeout_).time_since_epoch()); + + const auto tx_metadata = AnyUdpardTxMetadata::Publish{static_cast(deadline_us.count()), + static_cast(metadata.priority), + params_.subject_id, + metadata.transfer_id}; + + return delegate_.sendAnyTransfer(tx_metadata, payload_fragments); + } + + // MARK: IRunnable + + MaybeError run(const TimePoint) override + { + // Nothing to do here currently. + return {}; + } + + // MARK: Data members: + + TransportDelegate& delegate_; + const MessageTxParams params_; + Duration send_timeout_; + +}; // MessageTxSession + +} // namespace detail +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_MSG_TX_SESSION_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/svc_rx_sessions.hpp b/include/libcyphal/transport/udp/svc_rx_sessions.hpp new file mode 100644 index 000000000..4c3803a0a --- /dev/null +++ b/include/libcyphal/transport/udp/svc_rx_sessions.hpp @@ -0,0 +1,159 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_SVC_RX_SESSIONS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_SVC_RX_SESSIONS_HPP_INCLUDED + +#include "delegate.hpp" + +#include "libcyphal/runnable.hpp" +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/svc_sessions.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include +#include + +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +/// @brief A template class to represent a service request/response RX session (both for server and client sides). +/// +/// @tparam Interface_ Type of the session interface. +/// Could be either `IRequestRxSession` or `IResponseRxSession`. +/// @tparam Params Type of the session parameters. +/// Could be either `RequestRxParams` or `ResponseRxParams`. +/// +/// NOSONAR cpp:S4963 for below `class SvcRxSession` - we do directly handle resources here; +/// namely: in destructor we have to unsubscribe, as well as let delegate to know this fact. +/// +template +class SvcRxSession final : private IRxSessionDelegate, public Interface_ // NOSONAR cpp:S4963 +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec + { + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const Params& params) + { + if (params.service_id > UDPARD_SERVICE_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + SvcRxSession(const Spec, TransportDelegate& delegate, const Params& params) + : delegate_{delegate} + , params_{params} + { + // TODO: Implement! + (void) delegate_; + } + + SvcRxSession(const SvcRxSession&) = delete; + SvcRxSession(SvcRxSession&&) noexcept = delete; + SvcRxSession& operator=(const SvcRxSession&) = delete; + SvcRxSession& operator=(SvcRxSession&&) noexcept = delete; + + ~SvcRxSession() + { + // TODO: Implement! + (void) 0; + } + +private: + // MARK: Interface + + CETL_NODISCARD Params getParams() const noexcept override + { + return params_; + } + + CETL_NODISCARD cetl::optional receive() override + { + return std::exchange(last_rx_transfer_, cetl::nullopt); + } + + // MARK: IRxSession + + void setTransferIdTimeout(const Duration timeout) override + { + const auto timeout_us = std::chrono::duration_cast(timeout); + if (timeout_us.count() > 0) + { + // TODO: Implement! + } + } + + // MARK: IRunnable + + IRunnable::MaybeError run(const TimePoint) override + { + // Nothing to do here currently. + return {}; + } + + // MARK: IRxSessionDelegate + + void acceptRxTransfer(const UdpardRxTransfer&) override + { + // TODO: Implement! + } + + // MARK: Data members: + + TransportDelegate& delegate_; + const Params params_; + cetl::optional last_rx_transfer_; + +}; // SvcRxSession + +// MARK: - + +/// @brief A concrete class to represent a service request RX session (aka server side). +/// +using SvcRequestRxSession = SvcRxSession; + +/// @brief A concrete class to represent a service response RX session (aka client side). +/// +using SvcResponseRxSession = SvcRxSession; + +} // namespace detail +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_SVC_RX_SESSIONS_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/svc_tx_sessions.hpp b/include/libcyphal/transport/udp/svc_tx_sessions.hpp new file mode 100644 index 000000000..b7552b83c --- /dev/null +++ b/include/libcyphal/transport/udp/svc_tx_sessions.hpp @@ -0,0 +1,231 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_SVC_TX_SESSIONS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_SVC_TX_SESSIONS_HPP_INCLUDED + +#include "delegate.hpp" + +#include "libcyphal/runnable.hpp" +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/svc_sessions.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +/// @brief A class to represent a service request TX session (aka client side). +/// +class SvcRequestTxSession final : public IRequestTxSession +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec + { + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const RequestTxParams& params) + { + if ((params.service_id > UDPARD_SERVICE_ID_MAX) || (params.server_node_id > UDPARD_NODE_ID_MAX)) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + SvcRequestTxSession(const Spec, TransportDelegate& delegate, const RequestTxParams& params) + : delegate_{delegate} + , params_{params} + , send_timeout_{std::chrono::seconds{1}} + { + } + +private: + // MARK: ITxSession + + void setSendTimeout(const Duration timeout) override + { + send_timeout_ = timeout; + } + + // MARK: IRequestTxSession + + CETL_NODISCARD RequestTxParams getParams() const noexcept override + { + return params_; + } + + CETL_NODISCARD cetl::optional send(const TransferMetadata& metadata, + const PayloadFragments payload_fragments) override + { + // Before delegating to transport it makes sense to do some sanity checks. + // Otherwise, transport may do some work (like possible payload allocation/copying, + // media enumeration and pushing into their TX queues) doomed to fail with argument error. + // + if (delegate_.node_id() > UDPARD_NODE_ID_MAX) + { + return ArgumentError{}; + } + + const auto deadline_us = std::chrono::duration_cast( + (metadata.timestamp + send_timeout_).time_since_epoch()); + + const auto tx_metadata = AnyUdpardTxMetadata::Request{static_cast(deadline_us.count()), + static_cast(metadata.priority), + params_.service_id, + params_.server_node_id, + metadata.transfer_id}; + + return delegate_.sendAnyTransfer(tx_metadata, payload_fragments); + } + + // MARK: IRunnable + + IRunnable::MaybeError run(const TimePoint) override + { + // Nothing to do here currently. + return {}; + } + + // MARK: Data members: + + TransportDelegate& delegate_; + const RequestTxParams params_; + Duration send_timeout_; + +}; // SvcRequestTxSession + +// MARK: - + +/// @brief A class to represent a service response TX session (aka server side). +/// +class SvcResponseTxSession final : public IResponseTxSession +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec + { + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(cetl::pmr::memory_resource& memory, + TransportDelegate& delegate, + const ResponseTxParams& params) + { + if (params.service_id > UDPARD_SERVICE_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(memory, Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + SvcResponseTxSession(const Spec, TransportDelegate& delegate, const ResponseTxParams& params) + : delegate_{delegate} + , params_{params} + , send_timeout_{std::chrono::seconds{1}} + { + } + +private: + // MARK: ITxSession + + void setSendTimeout(const Duration timeout) override + { + send_timeout_ = timeout; + } + + // MARK: IResponseTxSession + + CETL_NODISCARD ResponseTxParams getParams() const noexcept override + { + return params_; + } + + CETL_NODISCARD cetl::optional send(const ServiceTransferMetadata& metadata, + const PayloadFragments payload_fragments) override + { + // Before delegating to transport it makes sense to do some sanity checks. + // Otherwise, transport may do some work (like possible payload allocation/copying, + // media enumeration and pushing into their TX queues) doomed to fail with argument error. + // + if ((delegate_.node_id() > UDPARD_NODE_ID_MAX) || (metadata.remote_node_id > UDPARD_NODE_ID_MAX)) + { + return ArgumentError{}; + } + + const auto deadline_us = std::chrono::duration_cast( + (metadata.timestamp + send_timeout_).time_since_epoch()); + + const auto tx_metadata = AnyUdpardTxMetadata::Respond{static_cast(deadline_us.count()), + static_cast(metadata.priority), + params_.service_id, + metadata.remote_node_id, + metadata.transfer_id}; + + return delegate_.sendAnyTransfer(tx_metadata, payload_fragments); + } + + // MARK: IRunnable + + IRunnable::MaybeError run(const TimePoint) override + { + // Nothing to do here currently. + return {}; + } + + // MARK: Data members: + + TransportDelegate& delegate_; + const ResponseTxParams params_; + Duration send_timeout_; + +}; // SvcResponseTxSession + +} // namespace detail +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_SVC_TX_SESSIONS_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/transport.hpp b/include/libcyphal/transport/udp/transport.hpp deleted file mode 100644 index 266cba03e..000000000 --- a/include/libcyphal/transport/udp/transport.hpp +++ /dev/null @@ -1,176 +0,0 @@ -/// @copyright -/// Copyright (C) OpenCyphal Development Team -/// Copyright Amazon.com Inc. or its affiliates. -/// SPDX-License-Identifier: MIT - -#ifndef LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_HPP_INCLUDED -#define LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_HPP_INCLUDED - -#include "media.hpp" - -#include "libcyphal/runnable.hpp" -#include "libcyphal/transport/errors.hpp" -#include "libcyphal/transport/msg_sessions.hpp" -#include "libcyphal/transport/multiplexer.hpp" -#include "libcyphal/transport/svc_sessions.hpp" -#include "libcyphal/transport/transport.hpp" -#include "libcyphal/transport/types.hpp" -#include "libcyphal/types.hpp" - -#include -#include -#include -#include - -namespace libcyphal -{ -namespace transport -{ -namespace udp -{ - -/// @brief Defines interface of UDP transport layer. -/// -class IUdpTransport : public ITransport -{ -public: - IUdpTransport(const IUdpTransport&) = delete; - IUdpTransport(IUdpTransport&&) noexcept = delete; - IUdpTransport& operator=(const IUdpTransport&) = delete; - IUdpTransport& operator=(IUdpTransport&&) noexcept = delete; - -protected: - IUdpTransport() = default; - ~IUdpTransport() = default; -}; - -/// Internal implementation details of the UDP transport. -/// Not supposed to be used directly by the users of the library. -/// -namespace detail -{ - -class TransportImpl final : public IUdpTransport -{ - /// @brief Defines specification for making interface unique ptr. - /// - struct Spec - { - using Interface = IUdpTransport; - using Concrete = TransportImpl; - - // In use to disable public construction. - // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ - explicit Spec() = default; - }; - -public: - TransportImpl(Spec, - cetl::pmr::memory_resource& memory, - IMultiplexer& multiplexer, - libcyphal::detail::VarArray&& media_array, // NOLINT - const UdpardNodeID udpard_node_id) - { - // TODO: Use them! - (void) memory; - (void) multiplexer; - (void) media_array; - (void) udpard_node_id; - } - -private: - // MARK: ITransport - - CETL_NODISCARD cetl::optional getLocalNodeId() const noexcept override - { - return cetl::nullopt; - } - - CETL_NODISCARD cetl::optional setLocalNodeId(const NodeId node_id) noexcept override - { - if (node_id > UDPARD_NODE_ID_MAX) - { - return ArgumentError{}; - } - - // TODO: Implement! - return cetl::nullopt; - } - - CETL_NODISCARD ProtocolParams getProtocolParams() const noexcept override - { - return ProtocolParams{}; - } - - CETL_NODISCARD Expected, AnyError> makeMessageRxSession( - const MessageRxParams&) override - { - return NotImplementedError{}; - } - CETL_NODISCARD Expected, AnyError> makeMessageTxSession( - const MessageTxParams&) override - { - return NotImplementedError{}; - } - CETL_NODISCARD Expected, AnyError> makeRequestRxSession( - const RequestRxParams&) override - { - return NotImplementedError{}; - } - CETL_NODISCARD Expected, AnyError> makeRequestTxSession( - const RequestTxParams&) override - { - return NotImplementedError{}; - } - CETL_NODISCARD Expected, AnyError> makeResponseRxSession( - const ResponseRxParams&) override - { - return NotImplementedError{}; - } - CETL_NODISCARD Expected, AnyError> makeResponseTxSession( - const ResponseTxParams&) override - { - return NotImplementedError{}; - } - - // MARK: IRunnable - - CETL_NODISCARD IRunnable::MaybeError run(const TimePoint) override - { - return AnyError{NotImplementedError{}}; - } - - // MARK: Data members: - -}; // TransportImpl - -} // namespace detail - -/// @brief Makes a new UDP transport instance. -/// -/// NB! Lifetime of the transport instance must never outlive `memory`, `media` and `multiplexer` instances. -/// -/// @param memory Reference to a polymorphic memory resource to use for all allocations. -/// @param multiplexer Interface of the multiplexer to use. -/// @param media Collection of redundant media interfaces to use. -/// @param local_node_id Optional id of the local node. Could be set (once!) later by `setLocalNodeId` call. -/// @return Unique pointer to the new UDP transport instance or an error. -/// -inline Expected, FactoryError> makeTransport(cetl::pmr::memory_resource& memory, - IMultiplexer& multiplexer, - const cetl::span media, - const cetl::optional local_node_id) -{ - // TODO: Use these! - (void) multiplexer; - (void) media; - (void) memory; - (void) local_node_id; - - return NotImplementedError{}; -} -} // namespace udp -} // namespace transport -} // namespace libcyphal - -#endif // LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/tx_rx_sockets.hpp b/include/libcyphal/transport/udp/tx_rx_sockets.hpp new file mode 100644 index 000000000..f9265992d --- /dev/null +++ b/include/libcyphal/transport/udp/tx_rx_sockets.hpp @@ -0,0 +1,109 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_TX_RX_SOCKETS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_TX_RX_SOCKETS_HPP_INCLUDED + +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include + +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// @brief Defines IP endpoint information in use for multicast transmissions. +/// +struct IpEndpoint final +{ + std::uint32_t ip_address; + std::uint16_t udp_port; +}; + +/// @brief Defines interface to a custom UDP media TX socket implementation. +/// +/// Implementation is supposed to be provided by an user of the library. +/// +class ITxSocket +{ +public: + ITxSocket(const ITxSocket&) = delete; + ITxSocket(ITxSocket&&) noexcept = delete; + ITxSocket& operator=(const ITxSocket&) = delete; + ITxSocket& operator=(ITxSocket&&) noexcept = delete; + + /// @brief Get the maximum transmission unit (MTU) of the UDP TX socket. + /// + /// To guarantee a single frame transfer, the maximum payload size shall be 4 bytes less to accommodate the CRC. + /// This value may change arbitrarily at runtime. The transport implementation will query it before every + /// transmission on the socket. + /// + virtual std::size_t getMtu() const noexcept + { + return DefaultMtu; + }; + + /// The default MTU is derived as: + /// 1500B Ethernet MTU (RFC 894) - 60B IPv4 max header - 8B UDP Header - 24B Cyphal header. + /// + static constexpr std::size_t DefaultMtu = UDPARD_MTU_DEFAULT; + + /// @brief Sends payload fragments to this socket. + /// + /// The payload may be fragmented to minimize data copying in the user space, + /// allowing the implementation to use vectorized I/O (iov). + /// + /// @param deadline The deadline for the send operation. Socket implementation should drop the payload + /// if the deadline is exceeded (aka `now > deadline`). + /// @param multicast_endpoint The multicast endpoint to send the payload to. + /// @param dscp The Differentiated Services Code Point (DSCP) to set in the IP header. + /// @param payload_fragments Fragments of the payload to send. + /// @return `true` if the payload has been accepted successfully, `false` if the socket is not ready for writing. + /// In case of failure, an error is returned. + /// + virtual Expected> send( + const TimePoint deadline, + const IpEndpoint multicast_endpoint, + const std::uint8_t dscp, + const PayloadFragments payload_fragments) = 0; + +protected: + ITxSocket() = default; + ~ITxSocket() = default; + +}; // ITxSocket + +/// @brief Defines interface to a custom UDP media RX socket implementation. +/// +/// Implementation is supposed to be provided by an user of the library. +/// +class IRxSocket +{ +public: + IRxSocket(const IRxSocket&) = delete; + IRxSocket(IRxSocket&&) noexcept = delete; + IRxSocket& operator=(const IRxSocket&) = delete; + IRxSocket& operator=(IRxSocket&&) noexcept = delete; + +protected: + IRxSocket() = default; + ~IRxSocket() = default; + +}; // IRxSocket + +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_TX_RX_SOCKETS_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/udp_transport.hpp b/include/libcyphal/transport/udp/udp_transport.hpp new file mode 100644 index 000000000..d3a63626f --- /dev/null +++ b/include/libcyphal/transport/udp/udp_transport.hpp @@ -0,0 +1,171 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_HPP_INCLUDED + +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/transport.hpp" +#include "media.hpp" +#include "tx_rx_sockets.hpp" + +#include +#include +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// @brief Defines interface of UDP transport layer. +/// +class IUdpTransport : public ITransport +{ +public: + /// Defines structure for reporting transient transport errors to the user's handler. + /// + /// In addition to the error itself, it provides: + /// - Index of media interface related to this error. + /// This index is the same as the index of the (not `nullptr`!) media interface + /// pointer in the `media` span argument used at the `makeTransport()` factory method. + /// - A reference to the entity that has caused this error. + /// + struct TransientErrorReport + { + /// @brief Error report about publishing a message to a TX session. + struct UdpardTxPublish + { + AnyError error; + std::uint8_t media_index; + UdpardTx& culprit; + }; + + /// @brief Error report about pushing a service request to a TX session. + struct UdpardTxRequest + { + AnyError error; + std::uint8_t media_index; + UdpardTx& culprit; + }; + + /// @brief Error report about pushing a service respond to a TX session. + struct UdpardTxRespond + { + AnyError error; + std::uint8_t media_index; + UdpardTx& culprit; + }; + + /// @brief Error report about making TX socket by the media interface. + struct MediaMakeTxSocket + { + AnyError error; + std::uint8_t media_index; + IMedia& culprit; + }; + + /// @brief Error report about sending a frame to the media TX socket interface. + struct MediaTxSocketSend + { + AnyError error; + std::uint8_t media_index; + ITxSocket& culprit; + }; + + /// Defines variant of all possible transient error reports. + /// + using Variant = + cetl::variant; + + }; // TransientErrorReport + + /// @brief Defines signature of a transient error handler. + /// + /// If set, this handler is called by the transport layer when a transient media related error occurs during + /// transport's (or any of its sessions) `run` method. A TX session `send` method may also trigger this handler. + /// + /// Note that there is a limited set of things that can be done within this handler, f.e.: + /// - it's not allowed to call transport's (or its session's) `run` method from within this handler; + /// - it's not allowed to call a TX session `send` or RX session `receive` methods from within this handler; + /// - main purpose of the handler: + /// - is to log/report/stat the error; + /// - potentially modify state of some "culprit" media related component; + /// - return an optional (maybe different) error back to the transport. + /// - result error from the handler affects: + /// - whether or not other redundant media of this transport will continue to be processed + /// as part of this current "problematic" run (see description of return below), + /// - propagation of the error up to the original user's call (result of the `run` or `send` methods). + /// + /// @param report The error report to be handled. It's made as non-const ref to allow the handler modify it, + /// and f.e. reuse original `.error` field value by moving it as is to return result. + /// @return An optional (maybe different) error back to the transport. + /// - If `cetl::nullopt` is returned, the original error (in the `report`) is considered as handled + /// and insignificant for the transport. Transport will continue its current process (effectively + /// either ignoring such transient failure, or retrying the process later on its next run). + /// - If an error is returned, the transport will immediately stop current process, won't process any + /// other media (if any), and propagate the returned error to the user (as result of `run` or etc). + /// + using TransientErrorHandler = + cetl::pmr::function(TransientErrorReport::Variant& report_var), sizeof(void*) * 3>; + + IUdpTransport(const IUdpTransport&) = delete; + IUdpTransport(IUdpTransport&&) noexcept = delete; + IUdpTransport& operator=(const IUdpTransport&) = delete; + IUdpTransport& operator=(IUdpTransport&&) noexcept = delete; + + /// Sets new transient error handler. + /// + /// - If the handler is set, it will be called by the transport layer when a transient media related error occurs, + /// and it's up to the handler to decide what to do with the error, and whether to continue or stop the process. + /// - If the handler is not set (default mode), the transport will treat this transient error as "serious" one, + /// and immediately stop its current process (its `run` or TX session's `send` method) and propagate the error. + /// See \ref TransientErrorHandler for more details. + /// + virtual void setTransientErrorHandler(TransientErrorHandler handler) = 0; + +protected: + IUdpTransport() = default; + ~IUdpTransport() = default; + +}; // IUdpTransport + +/// @brief Specifies set of memory resources used by the UDP transport. +/// +struct MemoryResourcesSpec +{ + /// The general purpose memory resource is used to provide memory for the libcyphal library. + /// It is NOT used for any Udpard TX or RX transfers, payload (de)fragmentation or transient handles, + /// but only for the libcyphal internal needs (like `make*[Rx|Tx]Session` factory calls). + cetl::pmr::memory_resource& general; + + /// The session memory resource is used to provide memory for the Udpard session instances. + /// Each instance is fixed-size, so a trivial zero-fragmentation block allocator is sufficient. + /// If `nullptr` then the `.general` memory resource will be used instead. + cetl::pmr::memory_resource* session{nullptr}; + + /// The fragment handles are allocated per payload fragment; each handle contains a pointer to its fragment. + /// Each instance is of a very small fixed size, so a trivial zero-fragmentation block allocator is sufficient. + /// If `nullptr` then the `.general` memory resource will be used instead. + cetl::pmr::memory_resource* fragment{nullptr}; + + /// The library never allocates payload buffers itself, as they are handed over by the application via + /// receive calls. Once a buffer is handed over, the library may choose to keep it if it is deemed to be + /// necessary to complete a transfer reassembly, or to discard it if it is deemed to be unnecessary. + /// Discarded payload buffers are freed using this memory resource. + /// If `nullptr` then the `.general` memory resource will be used instead. + cetl::pmr::memory_resource* payload{nullptr}; + +}; // MemoryResourcesSpec + +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/udp_transport_impl.hpp b/include/libcyphal/transport/udp/udp_transport_impl.hpp new file mode 100644 index 000000000..602db476a --- /dev/null +++ b/include/libcyphal/transport/udp/udp_transport_impl.hpp @@ -0,0 +1,708 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_IMPL_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_IMPL_HPP_INCLUDED + +#include "delegate.hpp" +#include "media.hpp" +#include "msg_rx_session.hpp" +#include "msg_tx_session.hpp" +#include "svc_rx_sessions.hpp" +#include "svc_tx_sessions.hpp" +#include "tx_rx_sockets.hpp" +#include "udp_transport.hpp" + +#include "libcyphal/runnable.hpp" +#include "libcyphal/transport/common/tools.hpp" +#include "libcyphal/transport/contiguous_payload.hpp" +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/msg_sessions.hpp" +#include "libcyphal/transport/multiplexer.hpp" +#include "libcyphal/transport/svc_sessions.hpp" +#include "libcyphal/transport/types.hpp" +#include "libcyphal/types.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +/// Internal implementation details of the UDP transport. +/// Not supposed to be used directly by the users of the library. +/// +namespace detail +{ + +/// @brief Represents final implementation class of the UDP transport. +/// +/// NOSONAR cpp:S4963 for below `class TransportImpl` - we do directly handle resources here; +/// namely: in destructor we have to flush TX queues (otherwise there will be memory leaks). +/// +class TransportImpl final : private TransportDelegate, public IUdpTransport // NOSONAR cpp:S4963 +{ + /// @brief Defines private specification for making interface unique ptr. + /// + struct Spec : libcyphal::detail::UniquePtrSpec + { + // `explicit` here is in use to disable public construction of derived private `Spec` structs. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + + /// @brief Defines private storage of a media index, its interface, TX queue and socket. + /// + struct Media final + { + public: + Media(const std::size_t index, + IMedia& interface, + const UdpardNodeID* const local_node_id, + const std::size_t tx_capacity, + const struct UdpardMemoryResource udp_mem_res) + : index_{static_cast(index)} + , interface_{interface} + , udpard_tx_{} + { + const std::int8_t result = ::udpardTxInit(&udpard_tx_, local_node_id, tx_capacity, udp_mem_res); + CETL_DEBUG_ASSERT(result == 0, "There should be no path for an error here."); + (void) result; + } + + std::uint8_t index() const + { + return index_; + } + + IMedia& interface() const + { + return interface_; + } + + UdpardTx& udpard_tx() + { + return udpard_tx_; + } + + UniquePtr& tx_socket_ptr() + { + return tx_socket_ptr_; + } + + std::size_t getTxSocketMtu() const noexcept + { + return tx_socket_ptr_ ? tx_socket_ptr_->getMtu() : ITxSocket::DefaultMtu; + } + + private: + const std::uint8_t index_; + IMedia& interface_; + UdpardTx udpard_tx_; + UniquePtr tx_socket_ptr_; + }; + using MediaArray = libcyphal::detail::VarArray; + +public: + CETL_NODISCARD static Expected, FactoryError> make(const MemoryResourcesSpec& mem_res_spec, + IMultiplexer& multiplexer, + const cetl::span media, + const std::size_t tx_capacity) + { + // Verify input arguments: + // - At least one media interface must be provided, but no more than the maximum allowed (3). + // + const auto media_count = static_cast( + std::count_if(media.begin(), media.end(), [](const IMedia* const media_ptr) -> bool { + return media_ptr != nullptr; + })); + if ((media_count == 0) || (media_count > UDPARD_NETWORK_INTERFACE_COUNT_MAX)) + { + return ArgumentError{}; + } + + const MemoryResources memory_resources{mem_res_spec.general, + makeUdpardMemoryResource(mem_res_spec.session, mem_res_spec.general), + makeUdpardMemoryResource(mem_res_spec.fragment, mem_res_spec.general), + makeUdpardMemoryDeleter(mem_res_spec.payload, mem_res_spec.general)}; + + const UdpardNodeID unset_node_id = UDPARD_NODE_ID_UNSET; + + // False positive of clang-tidy - we move `media_array` to the `transport` instance, so can't make it const. + // NOLINTNEXTLINE(misc-const-correctness) + MediaArray media_array = makeMediaArray(mem_res_spec.general, + media_count, + media, + &unset_node_id, + tx_capacity, + memory_resources.fragment); + if (media_array.size() != media_count) + { + return MemoryError{}; + } + + auto transport = libcyphal::detail::makeUniquePtr(memory_resources.general, + Spec{}, + memory_resources, + multiplexer, + std::move(media_array)); + if (transport == nullptr) + { + return MemoryError{}; + } + + return transport; + } + + TransportImpl(const Spec, + const MemoryResources& memory_resources, + IMultiplexer& multiplexer, + MediaArray&& media_array) + : TransportDelegate{memory_resources} + , media_array_{std::move(media_array)} + { + for (auto& media : media_array_) + { + media.udpard_tx().local_node_id = &udpard_node_id(); + } + + // TODO: Use it! + (void) multiplexer; + } + + TransportImpl(const TransportImpl&) = delete; + TransportImpl(TransportImpl&&) noexcept = delete; + TransportImpl& operator=(const TransportImpl&) = delete; + TransportImpl& operator=(TransportImpl&&) noexcept = delete; + + ~TransportImpl() + { + for (Media& media : media_array_) + { + flushUdpardTxQueue(media.udpard_tx()); + } + } + +private: + // MARK: IUdpTransport + + void setTransientErrorHandler(TransientErrorHandler handler) override + { + transient_error_handler_ = std::move(handler); + } + + // MARK: ITransport + + CETL_NODISCARD cetl::optional getLocalNodeId() const noexcept override + { + if (node_id() > UDPARD_NODE_ID_MAX) + { + return cetl::nullopt; + } + + return cetl::make_optional(node_id()); + } + + CETL_NODISCARD cetl::optional setLocalNodeId(const NodeId new_node_id) noexcept override + { + if (new_node_id > UDPARD_NODE_ID_MAX) + { + return ArgumentError{}; + } + + // Allow setting the same node ID multiple times, but only once otherwise. + // + if (node_id() == new_node_id) + { + return cetl::nullopt; + } + if (node_id() != UDPARD_NODE_ID_UNSET) + { + return ArgumentError{}; + } + udpard_node_id() = new_node_id; + + return cetl::nullopt; + } + + CETL_NODISCARD ProtocolParams getProtocolParams() const noexcept override + { + std::size_t min_mtu = std::numeric_limits::max(); + for (const Media& media : media_array_) + { + min_mtu = std::min(min_mtu, media.getTxSocketMtu()); + } + + return ProtocolParams{std::numeric_limits::max(), min_mtu, UDPARD_NODE_ID_MAX + 1}; + } + + CETL_NODISCARD Expected, AnyError> makeMessageRxSession( + const MessageRxParams& params) override + { + // TODO: Uncomment! + // const cetl::optional any_error = ensureNewSessionFor(CanardTransferKindMessage, + // params.subject_id); if (any_error.has_value()) + // { + // return any_error.value(); + // } + + return MessageRxSession::make(memoryResources().general, asDelegate(), params); + } + + CETL_NODISCARD Expected, AnyError> makeMessageTxSession( + const MessageTxParams& params) override + { + auto any_error = ensureMediaTxSockets(); + if (any_error.has_value()) + { + return any_error.value(); + } + + return MessageTxSession::make(memoryResources().general, asDelegate(), params); + } + + CETL_NODISCARD Expected, AnyError> makeRequestRxSession( + const RequestRxParams& params) override + { + // TODO: Uncomment! + // const cetl::optional any_error = ensureNewSessionFor(CanardTransferKindRequest, + // params.service_id); if (any_error.has_value()) + // { + // return any_error.value(); + // } + + return SvcRequestRxSession::make(memoryResources().general, asDelegate(), params); + } + + CETL_NODISCARD Expected, AnyError> makeRequestTxSession( + const RequestTxParams& params) override + { + auto any_error = ensureMediaTxSockets(); + if (any_error.has_value()) + { + return any_error.value(); + } + + return SvcRequestTxSession::make(memoryResources().general, asDelegate(), params); + } + + CETL_NODISCARD Expected, AnyError> makeResponseRxSession( + const ResponseRxParams& params) override + { + // TODO: Uncomment! + // const cetl::optional any_error = ensureNewSessionFor(CanardTransferKindResponse, + // params.service_id); if (any_error.has_value()) + // { + // return any_error.value(); + // } + + return SvcResponseRxSession::make(memoryResources().general, asDelegate(), params); + } + + CETL_NODISCARD Expected, AnyError> makeResponseTxSession( + const ResponseTxParams& params) override + { + auto any_error = ensureMediaTxSockets(); + if (any_error.has_value()) + { + return any_error.value(); + } + + return SvcResponseTxSession::make(memoryResources().general, asDelegate(), params); + } + + // MARK: IRunnable + + CETL_NODISCARD IRunnable::MaybeError run(const TimePoint now) override + { + cetl::optional any_error{}; + + // We deliberately first run TX as much as possible, and only then running RX - + // transmission will release resources (like TX queue items) and make room for new incoming frames. + // + any_error = runMediaTransmit(now); + if (any_error.has_value()) + { + return any_error.value(); + } + + return {}; + } + + // MARK: TransportDelegate + + CETL_NODISCARD TransportDelegate& asDelegate() + { + return *this; + } + + CETL_NODISCARD cetl::optional sendAnyTransfer(const AnyUdpardTxMetadata::Variant& tx_metadata_var, + const PayloadFragments payload_fragments) override + { + // Udpard currently does not support fragmented payloads (at `udpardTx[Publish|Request|Respond]`). + // so we need to concatenate them when there are more than one non-empty fragment. + // TODO: Make similar issue but for Udpard repo. + // See https://github.com/OpenCyphal/libcanard/issues/223 + // + const ContiguousPayload payload{memoryResources().general, payload_fragments}; + if ((payload.data() == nullptr) && (payload.size() > 0)) + { + return MemoryError{}; + } + + cetl::optional opt_any_error; + + for (Media& some_media : media_array_) + { + opt_any_error = withEnsureMediaTxSocket(some_media, + [this, &tx_metadata_var, &payload](auto& media, auto& tx_socket) + -> cetl::optional { + media.udpard_tx().mtu = tx_socket.getMtu(); + + const TxTransferHandler transfer_handler{*this, media, payload}; + return cetl::visit(transfer_handler, tx_metadata_var); + }); + if (opt_any_error.has_value()) + { + // The handler (if any) just said that it's NOT fine to continue with transferring to + // other media TX queues, and the error should not be ignored but propagated outside. + break; + } + } + + return opt_any_error; + } + + // MARK: Privates: + + using Self = TransportImpl; + using ContiguousPayload = transport::detail::ContiguousPayload; + + struct TxTransferHandler + { + // No Sonar `cpp:S5356` b/c we integrate here with libudpard raw C buffers. + TxTransferHandler(const Self& self, Media& media, const ContiguousPayload& cont_payload) + : self_{self} + , media_{media} + , payload_{cont_payload.size(), cont_payload.data()} // NOSONAR cpp:S5356 + { + } + + cetl::optional operator()(const AnyUdpardTxMetadata::Publish& tx_metadata) const + { + const std::int32_t result = ::udpardTxPublish(&media_.udpard_tx(), + tx_metadata.deadline_us, + tx_metadata.priority, + tx_metadata.subject_id, + tx_metadata.transfer_id, + payload_, + nullptr); + + return self_.tryHandleTransientUdpardResult(media_, result); + } + + cetl::optional operator()(const AnyUdpardTxMetadata::Request& tx_metadata) const + { + const std::int32_t result = ::udpardTxRequest(&media_.udpard_tx(), + tx_metadata.deadline_us, + tx_metadata.priority, + tx_metadata.service_id, + tx_metadata.server_node_id, + tx_metadata.transfer_id, + payload_, + nullptr); + + return self_.tryHandleTransientUdpardResult(media_, result); + } + + cetl::optional operator()(const AnyUdpardTxMetadata::Respond& tx_metadata) const + { + const std::int32_t result = ::udpardTxRespond(&media_.udpard_tx(), + tx_metadata.deadline_us, + tx_metadata.priority, + tx_metadata.service_id, + tx_metadata.client_node_id, + tx_metadata.transfer_id, + payload_, + nullptr); + + return self_.tryHandleTransientUdpardResult(media_, result); + } + + private: + const Self& self_; + Media& media_; + const struct UdpardPayload payload_; + + }; // TxTransferHandler + + template + CETL_NODISCARD cetl::optional tryHandleTransientMediaError(const Media& media, + ErrorVariant&& error_var, + Culprit&& culprit) + { + AnyError any_error = common::detail::anyErrorFromVariant(std::forward(error_var)); + if (!transient_error_handler_) + { + return any_error; + } + + TransientErrorReport::Variant report_var{ + Report{std::move(any_error), media.index(), std::forward(culprit)}}; + + return transient_error_handler_(report_var); + } + + template + CETL_NODISCARD cetl::optional tryHandleTransientUdpardResult(Media& media, + const std::int32_t result) const + { + cetl::optional opt_any_error = optAnyErrorFromUdpard(result); + if (opt_any_error.has_value() && transient_error_handler_) + { + TransientErrorReport::Variant report_var{ + Report{std::move(opt_any_error.value()), media.index(), media.udpard_tx()}}; + + opt_any_error = transient_error_handler_(report_var); + } + return opt_any_error; + } + + CETL_NODISCARD static MediaArray makeMediaArray(cetl::pmr::memory_resource& memory, + const std::size_t media_count, + const cetl::span media_interfaces, + const UdpardNodeID* const local_node_id_, + const std::size_t tx_capacity, + const struct UdpardMemoryResource udp_mem_res) + { + MediaArray media_array{media_count, &memory}; + + // Reserve the space for the whole array (to avoid reallocations). + // Capacity will be less than requested in case of out of memory. + media_array.reserve(media_count); + if (media_array.capacity() >= media_count) + { + std::size_t index = 0; + for (IMedia* const media_interface : media_interfaces) + { + if (media_interface != nullptr) + { + IMedia& media = *media_interface; + media_array.emplace_back(index, media, local_node_id_, tx_capacity, udp_mem_res); + index++; + } + } + CETL_DEBUG_ASSERT(index == media_count, ""); + CETL_DEBUG_ASSERT(media_array.size() == media_count, ""); + } + + return media_array; + } + + /// @brief Tries to run an action with media and its TX socket (the latter one is made on demand if necessary). + /// + template + CETL_NODISCARD cetl::optional withEnsureMediaTxSocket(Media& media, Action&& action) + { + using ErrorReport = TransientErrorReport::MediaMakeTxSocket; + + if (!media.tx_socket_ptr()) + { + auto maybe_tx_socket = media.interface().makeTxSocket(); + if (auto* const error = cetl::get_if>(&maybe_tx_socket)) + { + return tryHandleTransientMediaError(media, std::move(*error), media.interface()); + } + + media.tx_socket_ptr() = cetl::get>(std::move(maybe_tx_socket)); + if (!media.tx_socket_ptr()) + { + return tryHandleTransientMediaError>(media, + MemoryError{}, + media.interface()); + } + } + + return std::forward(action)(media, *media.tx_socket_ptr()); + } + + CETL_NODISCARD cetl::optional ensureMediaTxSockets() + { + for (Media& media : media_array_) + { + cetl::optional any_error = + withEnsureMediaTxSocket(media, [](auto&, auto&) -> cetl::nullopt_t { return cetl::nullopt; }); + if (any_error.has_value()) + { + return any_error; + } + } + + return cetl::nullopt; + } + + void flushUdpardTxQueue(UdpardTx& udpard_tx) const + { + while (const UdpardTxItem* const maybe_item = ::udpardTxPeek(&udpard_tx)) + { + UdpardTxItem* const item = ::udpardTxPop(&udpard_tx, maybe_item); + ::udpardTxFree(memoryResources().fragment, item); + } + } + + /// @brief Runs transmission loop for each redundant media interface. + /// + CETL_NODISCARD cetl::optional runMediaTransmit(const TimePoint now) + { + cetl::optional opt_any_error{}; + + for (Media& some_media : media_array_) + { + opt_any_error = + withEnsureMediaTxSocket(some_media, + [this, now](Media& media, ITxSocket& tx_socket) -> cetl::optional { + return runSingleMediaTransmit(media, tx_socket, now); + }); + + if (opt_any_error.has_value()) + { + break; + } + } + + return opt_any_error; + } + + /// @brief Runs transmission loop for a single media interface and its TX socket. + /// + /// Transmits as much as possible frames that are ready to be sent by the media TX socket interface. + /// + CETL_NODISCARD cetl::optional runSingleMediaTransmit(Media& media, + ITxSocket& tx_socket, + const TimePoint now) + { + using PayloadFragment = cetl::span; + + while (const UdpardTxItem* const tx_item = ::udpardTxPeek(&media.udpard_tx())) + { + // We are dropping any TX item that has expired. + // Otherwise, we would send it to the media TX socket interface. + // We use strictly `>=` (instead of `>`) to give this frame a chance (one extra 1us) at the socket. + // + const auto deadline = TimePoint{std::chrono::microseconds{tx_item->deadline_usec}}; + if (now >= deadline) + { + // Release whole expired transfer b/c possible next frames of the same transfer are also expired. + popAndFreeUdpardTxItem(&media.udpard_tx(), tx_item, true /*whole transfer*/); + + // No Sonar `cpp:S909` b/c it make sense to use `continue` statement here - the corner case of + // "early" (by deadline) transfer drop. Using `if` would make the code less readable and more nested. + continue; // NOSONAR cpp:S909 + } + + // No Sonar `cpp:S5356` and `cpp:S5357` b/c we integrate here with C libudpard API. + const auto* const buffer = + static_cast(tx_item->datagram_payload.data); // NOSONAR cpp:S5356 cpp:S5357 + const std::array single_payload_fragment{ + PayloadFragment{buffer, tx_item->datagram_payload.size}}; + + Expected> maybe_sent = + tx_socket.send(deadline, + {tx_item->destination.ip_address, tx_item->destination.udp_port}, + tx_item->dscp, + single_payload_fragment); + + // In case of socket send error we are going to drop this problematic frame + // (b/c it looks like media TX socket can't handle this frame), + // but we will continue to process with other frames if transient error handler says so. + // Note that socket not being ready/able to send a frame just yet (aka temporary) + // is not reported as an error (see `is_sent` below). + // + if (auto* const error = cetl::get_if>(&maybe_sent)) + { + // Release whole problematic transfer from the TX queue, + // so that other transfers in TX queue have their chance. + // Otherwise, we would be stuck in a run loop trying to send the same frame. + popAndFreeUdpardTxItem(&media.udpard_tx(), tx_item, true /*whole transfer*/); + + cetl::optional opt_any_error = + tryHandleTransientMediaError(media, + std::move(*error), + tx_socket); + if (opt_any_error.has_value()) + { + return opt_any_error; + } + + // The handler just said that it's fine to continue with sending other frames + // and ignore such a transient media error (and don't propagate it outside). + } + else + { + const auto is_sent = cetl::get(maybe_sent); + if (!is_sent) + { + // TX socket interface is busy, so we are done with this media for now, + // and will just try again with it later (on next `run`). + // Note, we are NOT releasing this item from the queue, so it will be retried on next `run`. + break; + + // TODO: It seems that `Multiplexer` interface would be used here + // but it is not yet implemented, so for now just `break`. + } + + popAndFreeUdpardTxItem(&media.udpard_tx(), tx_item, false /*single frame*/); + } + + } // for each frame + + return cetl::nullopt; + } + + // MARK: Data members: + + MediaArray media_array_; + TransientErrorHandler transient_error_handler_; + +}; // TransportImpl + +} // namespace detail + +/// @brief Makes a new UDP transport instance. +/// +/// NB! Lifetime of the transport instance must never outlive memory resources, `media` and `multiplexer` instances. +/// +/// @param mem_res_spec Specification of polymorphic memory resources to use for all allocations. +/// @param multiplexer Interface of the multiplexer to use. +/// @param media Collection of redundant media interfaces to use. +/// @param tx_capacity Total number of frames that can be queued for transmission per `IMedia` instance. +/// @return Unique pointer to the new UDP transport instance or an error. +/// +inline Expected, FactoryError> makeTransport(const MemoryResourcesSpec& mem_res_spec, + IMultiplexer& multiplexer, + const cetl::span media, + const std::size_t tx_capacity) +{ + return detail::TransportImpl::make(mem_res_spec, multiplexer, media, tx_capacity); +} + +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_TRANSPORT_IMPL_HPP_INCLUDED diff --git a/include/libcyphal/types.hpp b/include/libcyphal/types.hpp index 1cd42abd7..b971caf6e 100644 --- a/include/libcyphal/types.hpp +++ b/include/libcyphal/types.hpp @@ -103,21 +103,55 @@ class ImplementationCell final namespace detail { -template -using PmrAllocator = cetl::pmr::polymorphic_allocator; +template +using PmrAllocator = cetl::pmr::polymorphic_allocator; template using VarArray = cetl::VariableLengthArray>; -template -CETL_NODISCARD UniquePtr makeUniquePtr(cetl::pmr::memory_resource& memory, Args&&... args) +template +struct UniquePtrSpec +{ + using Interface = I; + using Concrete = C; +}; + +template +CETL_NODISCARD UniquePtr makeUniquePtr(cetl::pmr::memory_resource& memory, + Args&&... args) { return cetl::pmr::InterfaceFactory::make_unique< - typename Spec::Interface>(PmrAllocator{&memory}, std::forward(args)...); + typename UniquePtrSpec::Interface>(PmrAllocator{&memory}, + std::forward(args)...); } } // namespace detail +/// @brief Helper function which creates a new `Concrete` type object in a dynamically allocated memory from the given +/// Polymorphic Memory Resource (PMR). The object's pointer is wrapped into libcyphal compatible/expected `UniquePtr`. +/// The result `Interface`-typed smart-pointer (aka `std::unique_ptr`) has the concrete-type-erased PMR deleter. +/// +/// Internally it uses `cetl::pmr::InterfaceFactory` factory which in turn uses: +/// - `PmrAllocator = cetl::pmr::polymorphic_allocator` as the PMR (de)allocator type; +/// - `cetl::pmr::PmrInterfaceDeleter` as the concrete-type-erased PMR deleter type. +/// +/// @tparam Interface The interface template type - should be base of `Concrete` template type. +/// Such interface type could be without exposed destructor (protected or private). +/// @tparam Concrete The concrete type the object to be created. Its `~Concrete()` destructor have to be public. +/// The result smart pointer deleter will use the destructor to de-initialize memory of the object. +/// @tparam Args The types of arguments to be passed to the constructor of the `Concrete` object. +/// @param memory The PMR resource to be used for memory allocation and de-allocation. +/// NB! It's captured by reference inside of the deleter, so smart pointer should not outlive +/// the PMR resource (or use `reset` to release the object earlier). +/// @param args The arguments to be forwarded to the constructor of the `Concrete` object. +/// +template +CETL_NODISCARD UniquePtr makeUniquePtr(cetl::pmr::memory_resource& memory, Args&&... args) +{ + using Spec = detail::UniquePtrSpec; + return makeUniquePtr(memory, std::forward(args)...); +} + } // namespace libcyphal #endif // LIBCYPHAL_TYPES_HPP_INCLUDED diff --git a/test/unittest/sonar.cpp b/test/unittest/sonar.cpp index 38b491b37..bdb355eee 100644 --- a/test/unittest/sonar.cpp +++ b/test/unittest/sonar.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -21,8 +22,15 @@ #include #include #include +#include #include -#include +#include +#include +#include +#include +#include +#include "libcyphal/transport/udp/udp_transport.hpp" +#include "libcyphal/transport/udp/udp_transport_impl.hpp" #include int main() diff --git a/test/unittest/transport/can/test_can_delegate.cpp b/test/unittest/transport/can/test_can_delegate.cpp index c75d5f7e5..0a948b8ef 100644 --- a/test/unittest/transport/can/test_can_delegate.cpp +++ b/test/unittest/transport/can/test_can_delegate.cpp @@ -58,14 +58,15 @@ class TestCanDelegate : public testing::Test { } + // MARK: TransportDelegate + MOCK_METHOD((cetl::optional), sendTransfer, (const libcyphal::TimePoint deadline, const CanardTransferMetadata& metadata, const PayloadFragments payload_fragments), (override)); - // NOLINTNEXTLINE(bugprone-exception-escape) - MOCK_METHOD(void, triggerUpdateOfFilters, (const FiltersUpdateCondition condition), (noexcept, override)); + MOCK_METHOD(void, triggerUpdateOfFilters, (const FiltersUpdate::Variant& update_var), (override)); }; void TearDown() override diff --git a/test/unittest/transport/can/test_can_svc_tx_sessions.cpp b/test/unittest/transport/can/test_can_svc_tx_sessions.cpp index c4ad25eb4..c8f47e338 100644 --- a/test/unittest/transport/can/test_can_svc_tx_sessions.cpp +++ b/test/unittest/transport/can/test_can_svc_tx_sessions.cpp @@ -275,7 +275,7 @@ TEST_F(TestCanSvcTxSessions, make_response_fails_due_to_no_memory) EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); } -TEST_F(TestCanSvcTxSessions, send_respose) +TEST_F(TestCanSvcTxSessions, send_response) { EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); @@ -312,7 +312,7 @@ TEST_F(TestCanSvcTxSessions, send_respose) scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); } -TEST_F(TestCanSvcTxSessions, send_respose_with_argument_error) +TEST_F(TestCanSvcTxSessions, send_response_with_argument_error) { EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); diff --git a/test/unittest/transport/can/transient_error_handler_mock.hpp b/test/unittest/transport/can/transient_error_handler_mock.hpp index 633edd0da..29aedbb92 100644 --- a/test/unittest/transport/can/transient_error_handler_mock.hpp +++ b/test/unittest/transport/can/transient_error_handler_mock.hpp @@ -7,7 +7,7 @@ #define LIBCYPHAL_TRANSPORT_CAN_TRANSIENT_ERROR_HANDLER_MOCK_HPP_INCLUDED #include -#include +#include #include #include diff --git a/test/unittest/transport/udp/media_mock.hpp b/test/unittest/transport/udp/media_mock.hpp index 59f5bb683..75d654df5 100644 --- a/test/unittest/transport/udp/media_mock.hpp +++ b/test/unittest/transport/udp/media_mock.hpp @@ -6,7 +6,11 @@ #ifndef LIBCYPHAL_TRANSPORT_UDP_MEDIA_MOCK_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_UDP_MEDIA_MOCK_HPP_INCLUDED +#include +#include #include +#include +#include #include @@ -30,8 +34,15 @@ class MediaMock : public IMedia MediaMock& operator=(const MediaMock&) = delete; MediaMock& operator=(MediaMock&&) noexcept = delete; - // NOLINTNEXTLINE(bugprone-exception-escape) - MOCK_METHOD(std::size_t, getMtu, (), (const, noexcept, override)); + MOCK_METHOD((Expected, cetl::variant>), + makeTxSocket, + (), + (override)); + + MOCK_METHOD((Expected, cetl::variant>), + makeRxSocket, + (const IpEndpoint& multicast_endpoint), + (override)); }; // MediaMock diff --git a/test/unittest/transport/udp/test_udp_delegate.cpp b/test/unittest/transport/udp/test_udp_delegate.cpp new file mode 100644 index 000000000..05e324505 --- /dev/null +++ b/test/unittest/transport/udp/test_udp_delegate.cpp @@ -0,0 +1,156 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "../../memory_resource_mock.hpp" +#include "../../tracking_memory_resource.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + +using libcyphal::TimePoint; +using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. +using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. + +using testing::_; +using testing::Eq; +using testing::Return; +using testing::IsNull; +using testing::NotNull; +using testing::IsEmpty; +using testing::Optional; +using testing::StrictMock; +using testing::VariantWith; + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +class TestUdpDelegate : public testing::Test +{ +protected: + class TransportDelegateImpl final : public udp::detail::TransportDelegate + { + public: + using udp::detail::TransportDelegate::memoryResources; + using udp::detail::TransportDelegate::makeUdpardMemoryDeleter; + using udp::detail::TransportDelegate::makeUdpardMemoryResource; + + explicit TransportDelegateImpl(cetl::pmr::memory_resource& general_mr) + : udp::detail::TransportDelegate{MemoryResources{general_mr, + makeUdpardMemoryResource(nullptr, general_mr), + makeUdpardMemoryResource(nullptr, general_mr), + makeUdpardMemoryDeleter(nullptr, general_mr)}} + { + } + + // MARK: TransportDelegate + + MOCK_METHOD((cetl::optional), + sendAnyTransfer, + (const udp::detail::AnyUdpardTxMetadata::Variant& tx_metadata_var, + const PayloadFragments payload_fragments), + (override)); + + }; // TransportDelegateImpl + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_EQ(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + // MARK: Data members: + + // NOLINTBEGIN + TrackingMemoryResource mr_; + // NOLINTEND +}; + +// MARK: Tests: + +TEST_F(TestUdpDelegate, optAnyErrorFromUdpard) +{ + EXPECT_THAT(udp::detail::TransportDelegate::optAnyErrorFromUdpard(-UDPARD_ERROR_MEMORY), + Optional(VariantWith(_))); + + EXPECT_THAT(udp::detail::TransportDelegate::optAnyErrorFromUdpard(-UDPARD_ERROR_ARGUMENT), + Optional(VariantWith(_))); + + EXPECT_THAT(udp::detail::TransportDelegate::optAnyErrorFromUdpard(-UDPARD_ERROR_CAPACITY), + Optional(VariantWith(_))); + + EXPECT_THAT(udp::detail::TransportDelegate::optAnyErrorFromUdpard(-UDPARD_ERROR_ANONYMOUS), + Optional(VariantWith(_))); + + EXPECT_THAT(udp::detail::TransportDelegate::optAnyErrorFromUdpard(0), Eq(cetl::nullopt)); + EXPECT_THAT(udp::detail::TransportDelegate::optAnyErrorFromUdpard(1), Eq(cetl::nullopt)); + EXPECT_THAT(udp::detail::TransportDelegate::optAnyErrorFromUdpard(-1), Eq(cetl::nullopt)); +} + +TEST_F(TestUdpDelegate, makeUdpardMemoryResource) +{ + const auto udp_mem_res1 = TransportDelegateImpl::makeUdpardMemoryResource(nullptr, mr_); + EXPECT_THAT(udp_mem_res1.user_reference, &mr_); + EXPECT_THAT(udp_mem_res1.allocate, NotNull()); + EXPECT_THAT(udp_mem_res1.deallocate, NotNull()); + + StrictMock mr_mock{}; + const auto udp_mem_res2 = TransportDelegateImpl::makeUdpardMemoryResource(&mr_mock, mr_); + EXPECT_THAT(udp_mem_res2.user_reference, &mr_mock); + EXPECT_THAT(udp_mem_res2.allocate, NotNull()); + EXPECT_THAT(udp_mem_res2.deallocate, NotNull()); +} + +TEST_F(TestUdpDelegate, makeUdpardMemoryDeleter) +{ + const auto udp_mr_del1 = TransportDelegateImpl::makeUdpardMemoryDeleter(nullptr, mr_); + EXPECT_THAT(udp_mr_del1.user_reference, &mr_); + EXPECT_THAT(udp_mr_del1.deallocate, NotNull()); + + StrictMock mr_mock{}; + const auto udp_mr_del2 = TransportDelegateImpl::makeUdpardMemoryDeleter(&mr_mock, mr_); + EXPECT_THAT(udp_mr_del2.user_reference, &mr_mock); + EXPECT_THAT(udp_mr_del2.deallocate, NotNull()); +} + +TEST_F(TestUdpDelegate, allocateMemoryForUdpard_deallocateMemoryForUdpard) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + const TransportDelegateImpl delegate{mr_mock}; + + const auto& fragment_mr = delegate.memoryResources().fragment; + + auto* mem_ptr = fragment_mr.allocate(fragment_mr.user_reference, 1); + EXPECT_THAT(mem_ptr, NotNull()); + + fragment_mr.deallocate(fragment_mr.user_reference, 1, mem_ptr); +} + +TEST_F(TestUdpDelegate, allocateMemoryForUdpard_no_memory) +{ + StrictMock mr_mock{}; + + const TransportDelegateImpl delegate{mr_mock}; + + // Emulate that there is no memory at all. + EXPECT_CALL(mr_mock, do_allocate(1, _)).WillOnce(Return(nullptr)); + + const auto& session_mr = delegate.memoryResources().session; + EXPECT_THAT(session_mr.allocate(session_mr.user_reference, 1), IsNull()); +} + +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +} // namespace diff --git a/test/unittest/transport/udp/test_udp_msg_rx_session.cpp b/test/unittest/transport/udp/test_udp_msg_rx_session.cpp new file mode 100644 index 000000000..ef6305995 --- /dev/null +++ b/test/unittest/transport/udp/test_udp_msg_rx_session.cpp @@ -0,0 +1,273 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "../../memory_resource_mock.hpp" +#include "../../tracking_memory_resource.hpp" +#include "../../virtual_time_scheduler.hpp" +#include "../multiplexer_mock.hpp" +#include "media_mock.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace +{ + +using libcyphal::TimePoint; +using libcyphal::UniquePtr; +using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. +using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. + +using cetl::byte; + +using testing::_; +using testing::Return; +using testing::IsEmpty; +using testing::NotNull; +using testing::StrictMock; +using testing::VariantWith; + +// https://github.com/llvm/llvm-project/issues/53444 +// NOLINTBEGIN(misc-unused-using-decls, misc-include-cleaner) +using std::literals::chrono_literals::operator""s; +using std::literals::chrono_literals::operator""ms; +// NOLINTEND(misc-unused-using-decls, misc-include-cleaner) + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +class TestUdpMsgRxSession : public testing::Test +{ +protected: + void SetUp() override + { + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + TimePoint now() const + { + return scheduler_.now(); + } + + UniquePtr makeTransport(const MemoryResourcesSpec& mem_res_spec) + { + std::array media_array{&media_mock_}; + + auto maybe_transport = udp::makeTransport(mem_res_spec, mux_mock_, media_array, 0); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + return cetl::get>(std::move(maybe_transport)); + } + + // MARK: Data members: + + // NOLINTBEGIN + libcyphal::VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock mux_mock_{}; + StrictMock media_mock_{}; + // NOLINTEND +}; + +// MARK: Tests: + +TEST_F(TestUdpMsgRxSession, make_setTransferIdTimeout) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageRxSession({42, 123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + EXPECT_THAT(session->getParams().extent_bytes, 42); + EXPECT_THAT(session->getParams().subject_id, 123); + + session->setTransferIdTimeout(0s); + session->setTransferIdTimeout(500ms); +} + +TEST_F(TestUdpMsgRxSession, make_no_memory) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::MessageRxSession), _)).WillOnce(Return(nullptr)); + + auto transport = makeTransport({mr_mock}); + + auto maybe_session = transport->makeMessageRxSession({64, 0x23}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestUdpMsgRxSession, make_fails_due_to_argument_error) +{ + auto transport = makeTransport({mr_}); + + // Try invalid subject id + auto maybe_session = transport->makeMessageRxSession({64, UDPARD_SUBJECT_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +// TODO: Uncomment gradually as the implementation progresses. +/* +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(TestUdpMsgRxSession, run_and_receive) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageRxSession({4, 0x23}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + { + SCOPED_TRACE("1-st iteration: one frame available @ 1s"); + + scheduler_.setNow(TimePoint{1s}); + const auto rx_timestamp = now(); + + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), UDPARD_MTU_DEFAULT); + p[0] = b('0'); + p[1] = b('1'); + p[2] = b(0b111'01101); + return RxMetadata{rx_timestamp, 0x0C'60'23'45, 3}; + }); + EXPECT_CALL(media_mock_, setFilters(SizeIs(1))).WillOnce([&](Filters filters) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(filters, Contains(FilterEq({0x2300, 0x21FFF80}))); + return cetl::nullopt; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); + + const auto maybe_rx_transfer = session->receive(); + ASSERT_THAT(maybe_rx_transfer, Optional(_)); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + const auto& rx_transfer = maybe_rx_transfer.value(); + + EXPECT_THAT(rx_transfer.metadata.timestamp, rx_timestamp); + EXPECT_THAT(rx_transfer.metadata.transfer_id, 0x0D); + EXPECT_THAT(rx_transfer.metadata.priority, Priority::High); + EXPECT_THAT(rx_transfer.metadata.publisher_node_id, Optional(0x45)); + + std::array buffer{}; + ASSERT_THAT(rx_transfer.payload.size(), buffer.size()); + EXPECT_THAT(rx_transfer.payload.copy(0, buffer.data(), buffer.size()), buffer.size()); + EXPECT_THAT(buffer, ElementsAre('0', '1')); + } + { + SCOPED_TRACE("2-nd iteration: no frames available @ 2s"); + + scheduler_.setNow(TimePoint{2s}); + const auto rx_timestamp = now(); + + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), UDPARD_MTU_DEFAULT); + return cetl::nullopt; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); + + const auto maybe_rx_transfer = session->receive(); + EXPECT_THAT(maybe_rx_transfer, Eq(cetl::nullopt)); + } +} + +TEST_F(TestUdpMsgRxSession, run_and_receive_one_anonymous_frame) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageRxSession({4, 0x23}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.setNow(TimePoint{1s}); + const auto rx_timestamp = now(); + + { + const InSequence seq; + + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), UDPARD_MTU_DEFAULT); + p[0] = b('1'); + p[1] = b('2'); + p[2] = b(0b111'01110); + return RxMetadata{rx_timestamp, 0x01'60'23'13, 3}; + }); + EXPECT_CALL(media_mock_, setFilters(SizeIs(1))).WillOnce([&](Filters filters) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(filters, Contains(FilterEq({0x2300, 0x21FFF80}))); + return cetl::nullopt; + }); + } + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); + + const auto maybe_rx_transfer = session->receive(); + ASSERT_THAT(maybe_rx_transfer, Optional(_)); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + const auto& rx_transfer = maybe_rx_transfer.value(); + + EXPECT_THAT(rx_transfer.metadata.timestamp, rx_timestamp); + EXPECT_THAT(rx_transfer.metadata.transfer_id, 0x0E); + EXPECT_THAT(rx_transfer.metadata.priority, Priority::Exceptional); + EXPECT_THAT(rx_transfer.metadata.publisher_node_id, Eq(cetl::nullopt)); + + std::array buffer{}; + ASSERT_THAT(rx_transfer.payload.size(), buffer.size()); + EXPECT_THAT(rx_transfer.payload.copy(0, buffer.data(), buffer.size()), buffer.size()); + EXPECT_THAT(buffer, ElementsAre('1', '2')); +} + +TEST_F(TestUdpMsgRxSession, unsubscribe_and_run) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageRxSession({4, 0x23}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.setNow(TimePoint{1s}); + const auto reset_time = now(); + + EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + EXPECT_CALL(media_mock_, setFilters(IsEmpty())).WillOnce([&](Filters) { + EXPECT_THAT(now(), reset_time + 10ms); + return cetl::nullopt; + }); + + session.reset(); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} +*/ + +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +} // namespace diff --git a/test/unittest/transport/udp/test_udp_msg_tx_session.cpp b/test/unittest/transport/udp/test_udp_msg_tx_session.cpp new file mode 100644 index 000000000..e5c40684d --- /dev/null +++ b/test/unittest/transport/udp/test_udp_msg_tx_session.cpp @@ -0,0 +1,349 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "../../cetl_gtest_helpers.hpp" +#include "../../memory_resource_mock.hpp" +#include "../../tracking_memory_resource.hpp" +#include "../../verification_utilities.hpp" +#include "../../virtual_time_scheduler.hpp" +#include "../multiplexer_mock.hpp" +#include "media_mock.hpp" +#include "transient_error_handler_mock.hpp" +#include "tx_rx_sockets_mock.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + +using libcyphal::TimePoint; +using libcyphal::UniquePtr; +using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. +using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. + +using cetl::byte; +using libcyphal::verification_utilities::b; +using libcyphal::verification_utilities::makeIotaArray; +using libcyphal::verification_utilities::makeSpansFrom; + +using testing::_; +using testing::Eq; +using testing::Truly; +using testing::Invoke; +using testing::Return; +using testing::SizeIs; +using testing::IsEmpty; +using testing::NotNull; +using testing::StrictMock; +using testing::VariantWith; + +// https://github.com/llvm/llvm-project/issues/53444 +// NOLINTBEGIN(misc-unused-using-decls, misc-include-cleaner) +using std::literals::chrono_literals::operator""s; +using std::literals::chrono_literals::operator""ms; +using std::literals::chrono_literals::operator""us; +// NOLINTEND(misc-unused-using-decls, misc-include-cleaner) + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +class TestUdpMsgTxSession : public testing::Test +{ +protected: + void SetUp() override + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillRepeatedly(Invoke([this]() { + return libcyphal::detail::makeUniquePtr(mr_, tx_socket_mock_); + })); + EXPECT_CALL(tx_socket_mock_, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT)); + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + TimePoint now() const + { + return scheduler_.now(); + } + + UniquePtr makeTransport(const MemoryResourcesSpec& mem_res_spec) + { + std::array media_array{&media_mock_}; + + auto maybe_transport = udp::makeTransport(mem_res_spec, mux_mock_, media_array, 16); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + return cetl::get>(std::move(maybe_transport)); + } + + // MARK: Data members: + + // NOLINTBEGIN + libcyphal::VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock mux_mock_{}; + StrictMock media_mock_{}; + StrictMock tx_socket_mock_{"S1"}; + // NOLINTEND +}; + +// MARK: Tests: + +TEST_F(TestUdpMsgTxSession, make) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageTxSession({123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + EXPECT_THAT(session->getParams().subject_id, 123); +} + +TEST_F(TestUdpMsgTxSession, make_no_memory) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::MessageTxSession), _)).WillOnce(Return(nullptr)); + + auto transport = makeTransport({mr_mock}); + + auto maybe_session = transport->makeMessageTxSession({0x23}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestUdpMsgTxSession, make_fails_due_to_argument_error) +{ + auto transport = makeTransport({mr_}); + + // Try invalid subject id + auto maybe_session = transport->makeMessageTxSession({UDPARD_SUBJECT_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestUdpMsgTxSession, make_fails_due_to_media_socket) +{ + using MakeSocketReport = IUdpTransport::TransientErrorReport::MediaMakeTxSocket; + + auto transport = makeTransport({mr_}); + + // 1. Transport will fail to make msg TX session b/c media fails to create a TX socket. + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillOnce(Return(MemoryError{})); + + auto maybe_tx_session = transport->makeMessageTxSession({123}); + EXPECT_THAT(maybe_tx_session, VariantWith(VariantWith(_))); + } + + // 2. Transport will succeed to make TX session despite the media fails to create a TX socket. + // This is b/c transient error handler will be set and will handle the error. + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillOnce(Return(MemoryError{})); + + StrictMock handler_mock{}; + transport->setTransientErrorHandler(std::ref(handler_mock)); + EXPECT_CALL(handler_mock, invoke(VariantWith(Truly([&](auto& report) { + EXPECT_THAT(report.error, VariantWith(_)); + EXPECT_THAT(report.media_index, 0); + EXPECT_THAT(report.culprit, Ref(media_mock_)); + return true; + })))) + .WillOnce(Return(cetl::nullopt)); + + auto maybe_tx_session = transport->makeMessageTxSession({123}); + ASSERT_THAT(maybe_tx_session, VariantWith>(NotNull())); + + auto session = cetl::get>(std::move(maybe_tx_session)); + EXPECT_THAT(session->getParams().subject_id, 123); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, ITxSocket::DefaultMtu); + } +} + +TEST_F(TestUdpMsgTxSession, send_empty_payload_and_no_transport_run) +{ + StrictMock fragment_mr_mock{}; + fragment_mr_mock.redirectExpectedCallsTo(mr_); + + auto transport = makeTransport({mr_, nullptr, &fragment_mr_mock}); + + auto maybe_session = transport->makeMessageTxSession({123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + const PayloadFragments empty_payload{}; + const TransferMetadata metadata{0x1AF52, {}, Priority::Low}; + + // TX item for our payload to send is expected to be de/allocated on the *fragment* memory resource. + // + EXPECT_CALL(fragment_mr_mock, do_allocate(_, _)) + .WillOnce([&](std::size_t size_bytes, std::size_t alignment) -> void* { + return mr_.allocate(size_bytes, alignment); + }); + EXPECT_CALL(fragment_mr_mock, do_deallocate(_, _, _)) + .WillOnce( + [&](void* p, std::size_t size_bytes, std::size_t alignment) { mr_.deallocate(p, size_bytes, alignment); }); + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + scheduler_.runNow(+10ms, [&] { session->run(scheduler_.now()); }); + + // Payload still inside udpard TX queue (b/c there was no `transport->run` call deliberately), + // but there will be no memory leak b/c we expect that it should be deallocated when the transport is destroyed. + // See `EXPECT_THAT(mr_.allocations, IsEmpty());` at the `TearDown` method. +} + +TEST_F(TestUdpMsgTxSession, send_empty_payload) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageTxSession({0x7B}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto send_time = now(); + const auto timeout = 1s; + + const PayloadFragments empty_payload{}; + const TransferMetadata metadata{0x3AF52, send_time, Priority::Low}; + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)) + .WillOnce([&](auto deadline, auto endpoint, auto dscp, auto fragments) { + EXPECT_THAT(now(), send_time + 10ms); + EXPECT_THAT(deadline, send_time + timeout); + EXPECT_THAT(endpoint.ip_address, 0xEF00007B); + EXPECT_THAT(endpoint.udp_port, 9382); + EXPECT_THAT(dscp, 0x0); + EXPECT_THAT(fragments, SizeIs(1)); + EXPECT_THAT(fragments[0], SizeIs(24 + 4)); + return true; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} + +TEST_F(TestUdpMsgTxSession, send_empty_expired_payload) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageTxSession({123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto send_time = now(); + const auto timeout = 1s; + + const PayloadFragments empty_payload{}; + const TransferMetadata metadata{0x11, send_time, Priority::Low}; + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + // Emulate run calls just on the very edge of the default 1s timeout (exactly at the deadline). + // Payload should NOT be sent but dropped instead. + // + scheduler_.runNow(+timeout + 1us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+1us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} + +TEST_F(TestUdpMsgTxSession, send_single_frame_payload_with_500ms_timeout) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageTxSession({0x17}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + const auto timeout = 500ms; + session->setSendTimeout(timeout); + + scheduler_.runNow(+10s); + const auto send_time = now(); + + const auto payload = makeIotaArray(b('1')); + const TransferMetadata metadata{0x03, send_time, Priority::High}; + + auto maybe_error = session->send(metadata, makeSpansFrom(payload)); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + // Emulate run calls just on the very edge of the 500ms deadline. + // Payload should be sent successfully. + // + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)).WillOnce([&](auto, auto endpoint, auto dscp, auto fragments) { + EXPECT_THAT(now(), send_time + timeout - 1us); + EXPECT_THAT(endpoint.ip_address, 0xEF000017); + EXPECT_THAT(endpoint.udp_port, 9382); + EXPECT_THAT(dscp, 0x0); + EXPECT_THAT(fragments, SizeIs(1)); + EXPECT_THAT(fragments[0], SizeIs(24 + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 4)); + EXPECT_THAT(fragments[0][24 + 0], b('1')); + EXPECT_THAT(fragments[0][24 + 1], b('2')); + EXPECT_THAT(fragments[0][24 + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME - 1], + b(static_cast('1' + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME - 1))); + return true; + }); + // + scheduler_.runNow(timeout - 1us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+0us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} + +TEST_F(TestUdpMsgTxSession, send_when_no_memory_for_contiguous_payload) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + auto transport = makeTransport({mr_mock}); + + // Emulate that there is no memory available for the expected contiguous payload. + const auto payload1 = makeIotaArray<1>(b('0')); + const auto payload2 = makeIotaArray<2>(b('1')); + EXPECT_CALL(mr_mock, do_allocate(sizeof(payload1) + sizeof(payload2), _)).WillOnce(Return(nullptr)); + + auto maybe_session = transport->makeMessageTxSession({17}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto send_time = now(); + + const TransferMetadata metadata{0x03, send_time, Priority::Optional}; + + auto maybe_error = session->send(metadata, makeSpansFrom(payload1, payload2)); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + + scheduler_.runNow(+10ms, [&] { transport->run(scheduler_.now()); }); +} + +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +} // namespace diff --git a/test/unittest/transport/udp/test_udp_svc_rx_sessions.cpp b/test/unittest/transport/udp/test_udp_svc_rx_sessions.cpp new file mode 100644 index 000000000..24a844b7a --- /dev/null +++ b/test/unittest/transport/udp/test_udp_svc_rx_sessions.cpp @@ -0,0 +1,307 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "../../memory_resource_mock.hpp" +#include "../../tracking_memory_resource.hpp" +#include "../../virtual_time_scheduler.hpp" +#include "../multiplexer_mock.hpp" +#include "media_mock.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace +{ + +using libcyphal::TimePoint; +using libcyphal::UniquePtr; +using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. +using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. + +using cetl::byte; + +using testing::_; +using testing::Return; +using testing::IsEmpty; +using testing::NotNull; +using testing::StrictMock; +using testing::VariantWith; + +// https://github.com/llvm/llvm-project/issues/53444 +// NOLINTBEGIN(misc-unused-using-decls, misc-include-cleaner) +using std::literals::chrono_literals::operator""s; +using std::literals::chrono_literals::operator""ms; +// NOLINTEND(misc-unused-using-decls, misc-include-cleaner) + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +class TestUdpSvcRxSessions : public testing::Test +{ +protected: + void SetUp() override + { + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + TimePoint now() const + { + return scheduler_.now(); + } + + UniquePtr makeTransport(const MemoryResourcesSpec& mem_res_spec, const NodeId local_node_id) + { + std::array media_array{&media_mock_}; + + auto maybe_transport = udp::makeTransport(mem_res_spec, mux_mock_, media_array, 0); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + auto transport = cetl::get>(std::move(maybe_transport)); + + transport->setLocalNodeId(local_node_id); + + return transport; + } + + // MARK: Data members: + + // NOLINTBEGIN + libcyphal::VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock mux_mock_{}; + StrictMock media_mock_{}; + // NOLINTEND +}; + +// MARK: Tests: + +TEST_F(TestUdpSvcRxSessions, make_request_setTransferIdTimeout) +{ + auto transport = makeTransport({mr_}, 0x31); + + auto maybe_session = transport->makeRequestRxSession({42, 123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + EXPECT_THAT(session->getParams().extent_bytes, 42); + EXPECT_THAT(session->getParams().service_id, 123); + + session->setTransferIdTimeout(0s); + session->setTransferIdTimeout(500ms); +} + +TEST_F(TestUdpSvcRxSessions, make_resposnse_no_memory) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::SvcResponseRxSession), _)).WillOnce(Return(nullptr)); + + auto transport = makeTransport({mr_mock}, 0x13); + + auto maybe_session = transport->makeResponseRxSession({64, 0x23, 0x45}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestUdpSvcRxSessions, make_request_fails_due_to_argument_error) +{ + auto transport = makeTransport({mr_}, 0x31); + + // Try invalid subject id + auto maybe_session = transport->makeRequestRxSession({64, UDPARD_SERVICE_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +// TODO: Uncomment gradually as the implementation progresses. +/* +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(TestUdpSvcRxSessions, run_and_receive_requests) +{ + auto transport = makeTransport({mr_}, 0x31); + + const std::size_t extent_bytes = 8; + auto maybe_session = transport->makeRequestRxSession({extent_bytes, 0x17B}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + const auto params = session->getParams(); + EXPECT_THAT(params.extent_bytes, extent_bytes); + EXPECT_THAT(params.service_id, 0x17B); + + const auto timeout = 200ms; + session->setTransferIdTimeout(timeout); + + { + SCOPED_TRACE("1-st iteration: one frame available @ 1s"); + + scheduler_.setNow(TimePoint{1s}); + const auto rx_timestamp = now(); + + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), UDPARD_MTU_DEFAULT); + p[0] = b(42); + p[1] = b(147); + p[2] = b(0b111'11101); + return RxMetadata{rx_timestamp, 0b011'1'1'0'101111011'0110001'0010011, 3}; + }); + EXPECT_CALL(media_mock_, setFilters(SizeIs(1))).WillOnce([&](Filters filters) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(filters[0].id, 0b1'0'0'101111011'0110001'0000000); + EXPECT_THAT(filters[0].mask, 0b1'0'1'111111111'1111111'0000000); + return cetl::nullopt; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); + + const auto maybe_rx_transfer = session->receive(); + ASSERT_THAT(maybe_rx_transfer, Optional(_)); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + const auto& rx_transfer = maybe_rx_transfer.value(); + + EXPECT_THAT(rx_transfer.metadata.timestamp, rx_timestamp); + EXPECT_THAT(rx_transfer.metadata.transfer_id, 0x1D); + EXPECT_THAT(rx_transfer.metadata.priority, Priority::High); + EXPECT_THAT(rx_transfer.metadata.remote_node_id, 0x13); + + std::array buffer{}; + EXPECT_THAT(rx_transfer.payload.size(), 2); + EXPECT_THAT(rx_transfer.payload.copy(0, buffer.data(), buffer.size()), 2); + EXPECT_THAT(buffer, ElementsAre(42, 147)); + } + { + SCOPED_TRACE("2-nd iteration: no frames available @ 2s"); + + scheduler_.setNow(TimePoint{2s}); + const auto rx_timestamp = now(); + + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto payload) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(payload.size(), UDPARD_MTU_DEFAULT); + return cetl::nullopt; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); + + const auto maybe_rx_transfer = session->receive(); + EXPECT_THAT(maybe_rx_transfer, Eq(cetl::nullopt)); + } +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(TestUdpSvcRxSessions, run_and_receive_two_frame) +{ + auto transport = makeTransport({mr_}, 0x31); + + const std::size_t extent_bytes = 8; + auto maybe_session = transport->makeRequestRxSession({extent_bytes, 0x17B}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.setNow(TimePoint{1s}); + const auto rx_timestamp = now(); + { + const InSequence seq; + + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), UDPARD_MTU_DEFAULT); + p[0] = b('0'); + p[1] = b('1'); + p[2] = b('2'); + p[3] = b('3'); + p[4] = b('4'); + p[5] = b('5'); + p[6] = b('6'); + p[7] = b(0b101'11110); + return RxMetadata{rx_timestamp, 0b000'1'1'0'101111011'0110001'0010011, 8}; + }); + EXPECT_CALL(media_mock_, setFilters(SizeIs(1))).WillOnce([&](Filters filters) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(filters[0].id, 0b1'0'0'101111011'0110001'0000000); + EXPECT_THAT(filters[0].mask, 0b1'0'1'111111111'1111111'0000000); + return cetl::nullopt; + }); + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 30ms); + EXPECT_THAT(p.size(), UDPARD_MTU_DEFAULT); + p[0] = b('7'); + p[1] = b('8'); + p[2] = b('9'); + p[3] = b(0x7D); + p[4] = b(0x61); // expected 16-bit CRC + p[5] = b(0b010'11110); + return RxMetadata{rx_timestamp, 0b000'1'1'0'101111011'0110001'0010011, 6}; + }); + } + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); + + const auto maybe_rx_transfer = session->receive(); + ASSERT_THAT(maybe_rx_transfer, Optional(_)); + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + const auto& rx_transfer = maybe_rx_transfer.value(); + + EXPECT_THAT(rx_transfer.metadata.timestamp, rx_timestamp); + EXPECT_THAT(rx_transfer.metadata.transfer_id, 0x1E); + EXPECT_THAT(rx_transfer.metadata.priority, Priority::Exceptional); + EXPECT_THAT(rx_transfer.metadata.remote_node_id, 0x13); + + std::array buffer{}; + EXPECT_THAT(rx_transfer.payload.size(), buffer.size()); + EXPECT_THAT(rx_transfer.payload.copy(0, buffer.data(), buffer.size()), buffer.size()); + EXPECT_THAT(buffer, ElementsAre('0', '1', '2', '3', '4', '5', '6', '7')); +} + +TEST_F(TestUdpSvcRxSessions, unsubscribe_and_run) +{ + auto transport = makeTransport({mr_}, 0x31); + + const std::size_t extent_bytes = 8; + auto maybe_session = transport->makeRequestRxSession({extent_bytes, 0x17B}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.setNow(TimePoint{1s}); + const auto reset_time = now(); + + EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + EXPECT_CALL(media_mock_, setFilters(IsEmpty())).WillOnce([&](Filters) { + EXPECT_THAT(now(), reset_time + 10ms); + return cetl::nullopt; + }); + + session.reset(); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} +*/ + +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +} // namespace diff --git a/test/unittest/transport/udp/test_udp_svc_tx_sessions.cpp b/test/unittest/transport/udp/test_udp_svc_tx_sessions.cpp new file mode 100644 index 000000000..282cdf0ce --- /dev/null +++ b/test/unittest/transport/udp/test_udp_svc_tx_sessions.cpp @@ -0,0 +1,530 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "../../cetl_gtest_helpers.hpp" +#include "../../memory_resource_mock.hpp" +#include "../../tracking_memory_resource.hpp" +#include "../../virtual_time_scheduler.hpp" +#include "../multiplexer_mock.hpp" +#include "media_mock.hpp" +#include "transient_error_handler_mock.hpp" +#include "tx_rx_sockets_mock.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace +{ + +using libcyphal::TimePoint; +using libcyphal::UniquePtr; +using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. +using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. + +using cetl::byte; + +using testing::_; +using testing::Eq; +using testing::Truly; +using testing::Invoke; +using testing::Return; +using testing::SizeIs; +using testing::IsEmpty; +using testing::NotNull; +using testing::StrictMock; +using testing::VariantWith; + +// https://github.com/llvm/llvm-project/issues/53444 +// NOLINTBEGIN(misc-unused-using-decls, misc-include-cleaner) +using std::literals::chrono_literals::operator""s; +using std::literals::chrono_literals::operator""ms; +// NOLINTEND(misc-unused-using-decls, misc-include-cleaner) + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +class TestUdpSvcTxSessions : public testing::Test +{ +protected: + void SetUp() override + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillRepeatedly(Invoke([this]() { + return libcyphal::detail::makeUniquePtr(mr_, tx_socket_mock_); + })); + EXPECT_CALL(tx_socket_mock_, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT)); + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + TimePoint now() const + { + return scheduler_.now(); + } + + UniquePtr makeTransport(const MemoryResourcesSpec& mem_res_spec, + const cetl::optional local_node_id = cetl::nullopt) + { + std::array media_array{&media_mock_}; + + auto maybe_transport = udp::makeTransport(mem_res_spec, mux_mock_, media_array, 16); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + auto transport = cetl::get>(std::move(maybe_transport)); + + if (local_node_id.has_value()) + { + transport->setLocalNodeId(local_node_id.value()); + } + + return transport; + } + + // MARK: Data members: + + // NOLINTBEGIN + libcyphal::VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock mux_mock_{}; + StrictMock media_mock_{}; + StrictMock tx_socket_mock_{"S1"}; + // NOLINTEND +}; + +// MARK: Tests: + +TEST_F(TestUdpSvcTxSessions, make_request_session) +{ + auto transport = makeTransport({mr_}, static_cast(0)); + + auto maybe_session = transport->makeRequestTxSession({123, UDPARD_NODE_ID_MAX}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + EXPECT_THAT(session->getParams().service_id, 123); + EXPECT_THAT(session->getParams().server_node_id, UDPARD_NODE_ID_MAX); + + EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); +} + +TEST_F(TestUdpSvcTxSessions, make_request_fails_due_to_argument_error) +{ + auto transport = makeTransport({mr_}, static_cast(0)); + + // Try invalid service id + { + auto maybe_session = transport->makeRequestTxSession({UDPARD_SERVICE_ID_MAX + 1, 0}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); + } + + // Try invalid server node id + { + auto maybe_session = transport->makeRequestTxSession({0, UDPARD_NODE_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); + } +} + +TEST_F(TestUdpSvcTxSessions, make_request_fails_due_to_no_memory) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::SvcRequestTxSession), _)).WillOnce(Return(nullptr)); + + auto transport = makeTransport({mr_mock}, static_cast(UDPARD_NODE_ID_MAX)); + + auto maybe_session = transport->makeRequestTxSession({0x23, 0}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestUdpSvcTxSessions, make_request_fails_due_to_media_socket) +{ + using MakeSocketReport = IUdpTransport::TransientErrorReport::MediaMakeTxSocket; + + auto transport = makeTransport({mr_}); + + // 1. Transport will fail to make msg TX session b/c media fails to create a TX socket. + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillOnce(Return(MemoryError{})); + + auto maybe_tx_session = transport->makeRequestTxSession({0x23, 0}); + EXPECT_THAT(maybe_tx_session, VariantWith(VariantWith(_))); + } + + // 2. Transport will succeed to make TX session despite the media fails to create a TX socket. + // This is b/c transient error handler will be set and will handle the error. + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillOnce(Return(MemoryError{})); + + StrictMock handler_mock{}; + transport->setTransientErrorHandler(std::ref(handler_mock)); + EXPECT_CALL(handler_mock, invoke(VariantWith(Truly([&](auto& report) { + EXPECT_THAT(report.error, VariantWith(_)); + EXPECT_THAT(report.media_index, 0); + EXPECT_THAT(report.culprit, Ref(media_mock_)); + return true; + })))) + .WillOnce(Return(cetl::nullopt)); + + auto maybe_tx_session = transport->makeRequestTxSession({0x23, 0}); + ASSERT_THAT(maybe_tx_session, VariantWith>(NotNull())); + + auto session = cetl::get>(std::move(maybe_tx_session)); + EXPECT_THAT(session->getParams().service_id, 0x23); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, ITxSocket::DefaultMtu); + } +} + +TEST_F(TestUdpSvcTxSessions, send_empty_payload_request_and_no_transport_run) +{ + StrictMock fragment_mr_mock{}; + fragment_mr_mock.redirectExpectedCallsTo(mr_); + + auto transport = makeTransport({mr_, nullptr, &fragment_mr_mock}); + + auto maybe_session = transport->makeRequestTxSession({0x23, 0}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + const PayloadFragments empty_payload{}; + const TransferMetadata metadata{0x1AF52, {}, Priority::Low}; + + // 1st try anonymous node - should fail without even trying to allocate & send payload. + { + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + } + + // 2nd. Try again but now with a valid node id. + { + EXPECT_THAT(transport->setLocalNodeId(0x13), Eq(cetl::nullopt)); + + // TX item for our payload to send is expected to be de/allocated on the *fragment* memory resource. + // + EXPECT_CALL(fragment_mr_mock, do_allocate(_, _)) + .WillOnce([&](std::size_t size_bytes, std::size_t alignment) -> void* { + return mr_.allocate(size_bytes, alignment); + }); + EXPECT_CALL(fragment_mr_mock, do_deallocate(_, _, _)) + .WillOnce([&](void* p, std::size_t size_bytes, std::size_t alignment) { + mr_.deallocate(p, size_bytes, alignment); + }); + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + scheduler_.runNow(+10ms, [&] { session->run(scheduler_.now()); }); + } + + // Payload still inside udpard TX queue (b/c there was no `transport->run` call deliberately), + // but there will be no memory leak b/c we expect that it should be deallocated when the transport is destroyed. + // See `EXPECT_THAT(mr_.allocations, IsEmpty());` at the `TearDown` method. +} + +TEST_F(TestUdpSvcTxSessions, send_empty_payload_responce_and_no_transport_run) +{ + StrictMock fragment_mr_mock{}; + fragment_mr_mock.redirectExpectedCallsTo(mr_); + + auto transport = makeTransport({mr_, nullptr, &fragment_mr_mock}); + + auto maybe_session = transport->makeResponseTxSession({0x23}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + const PayloadFragments empty_payload{}; + const ServiceTransferMetadata metadata{0x1AF52, {}, Priority::Low, 0x31}; + + // 1st try anonymous node - should fail without even trying to allocate & send payload. + { + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + } + + // 2nd. Try again but now with a valid node id. + { + EXPECT_THAT(transport->setLocalNodeId(0x13), Eq(cetl::nullopt)); + + // TX item for our payload to send is expected to be de/allocated on the *fragment* memory resource. + // + EXPECT_CALL(fragment_mr_mock, do_allocate(_, _)) + .WillOnce([&](std::size_t size_bytes, std::size_t alignment) -> void* { + return mr_.allocate(size_bytes, alignment); + }); + EXPECT_CALL(fragment_mr_mock, do_deallocate(_, _, _)) + .WillOnce([&](void* p, std::size_t size_bytes, std::size_t alignment) { + mr_.deallocate(p, size_bytes, alignment); + }); + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + scheduler_.runNow(+10ms, [&] { session->run(scheduler_.now()); }); + } + + // Payload still inside udpard TX queue (b/c there was no `transport->run` call deliberately), + // but there will be no memory leak b/c we expect that it should be deallocated when the transport is destroyed. + // See `EXPECT_THAT(mr_.allocations, IsEmpty());` at the `TearDown` method. +} + +TEST_F(TestUdpSvcTxSessions, send_request) +{ + auto transport = makeTransport({mr_}, NodeId{13}); + + auto maybe_session = transport->makeRequestTxSession({0x7B, 0x1F}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto send_time = now(); + const auto timeout = 100ms; + session->setSendTimeout(timeout); + + const PayloadFragments empty_payload{}; + const TransferMetadata metadata{0x66, send_time, Priority::Slow}; + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)) + .WillOnce([&](auto deadline, auto endpoint, auto dscp, auto fragments) { + EXPECT_THAT(now(), send_time + 10ms); + EXPECT_THAT(deadline, send_time + timeout); + EXPECT_THAT(endpoint.ip_address, 0xEF01001F); + EXPECT_THAT(endpoint.udp_port, 9382); + EXPECT_THAT(dscp, 0x0); + EXPECT_THAT(fragments, SizeIs(1)); + EXPECT_THAT(fragments[0], SizeIs(24 + 4)); + return true; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} + +TEST_F(TestUdpSvcTxSessions, send_request_with_argument_error) +{ + // Make initially anonymous node transport. + // + std::array media_array{&media_mock_}; + auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, media_array, 2); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); + auto transport = cetl::get>(std::move(maybe_transport)); + + auto maybe_session = transport->makeRequestTxSession({123, 0x1F}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+100ms); + const auto timeout = 1s; + const auto transfer_time = now(); + + const PayloadFragments empty_payload{}; + const TransferMetadata metadata{0x66, transfer_time, Priority::Immediate}; + + // Should fail due to anonymous node. + { + scheduler_.setNow(TimePoint{200ms}); + + const auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + } + + // Fix anonymous node + { + scheduler_.setNow(TimePoint{300ms}); + const auto send_time = now(); + + EXPECT_THAT(transport->setLocalNodeId(13), Eq(cetl::nullopt)); + const auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)).WillOnce([&](auto deadline, auto endpoint, auto, auto) { + EXPECT_THAT(now(), send_time + 10ms); + EXPECT_THAT(deadline, transfer_time + timeout); + EXPECT_THAT(endpoint.ip_address, 0xEF01001F); + return true; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + } +} + +TEST_F(TestUdpSvcTxSessions, make_response_session) +{ + auto transport = makeTransport({mr_}, NodeId{UDPARD_NODE_ID_MAX}); + + auto maybe_session = transport->makeResponseTxSession({123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + EXPECT_THAT(session->getParams().service_id, 123); + + EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); +} + +TEST_F(TestUdpSvcTxSessions, make_response_fails_due_to_argument_error) +{ + auto transport = makeTransport({mr_}, NodeId{0}); + + // Try invalid service id + auto maybe_session = transport->makeResponseTxSession({UDPARD_SERVICE_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestUdpSvcTxSessions, make_response_fails_due_to_no_memory) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the message session. + EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::SvcRequestTxSession), _)).WillOnce(Return(nullptr)); + + auto transport = makeTransport({mr_mock}, NodeId{UDPARD_NODE_ID_MAX}); + + auto maybe_session = transport->makeResponseTxSession({0x23}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestUdpSvcTxSessions, make_response_fails_due_to_media_socket) +{ + using MakeSocketReport = IUdpTransport::TransientErrorReport::MediaMakeTxSocket; + + auto transport = makeTransport({mr_}); + + // 1. Transport will fail to make msg TX session b/c media fails to create a TX socket. + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillOnce(Return(MemoryError{})); + + auto maybe_tx_session = transport->makeResponseTxSession({123}); + EXPECT_THAT(maybe_tx_session, VariantWith(VariantWith(_))); + } + + // 2. Transport will succeed to make TX session despite the media fails to create a TX socket. + // This is b/c transient error handler will be set and will handle the error. + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillOnce(Return(MemoryError{})); + + StrictMock handler_mock{}; + transport->setTransientErrorHandler(std::ref(handler_mock)); + EXPECT_CALL(handler_mock, invoke(VariantWith(Truly([&](auto& report) { + EXPECT_THAT(report.error, VariantWith(_)); + EXPECT_THAT(report.media_index, 0); + EXPECT_THAT(report.culprit, Ref(media_mock_)); + return true; + })))) + .WillOnce(Return(cetl::nullopt)); + + auto maybe_tx_session = transport->makeResponseTxSession({123}); + ASSERT_THAT(maybe_tx_session, VariantWith>(NotNull())); + + auto session = cetl::get>(std::move(maybe_tx_session)); + EXPECT_THAT(session->getParams().service_id, 123); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, ITxSocket::DefaultMtu); + } +} + +TEST_F(TestUdpSvcTxSessions, send_response) +{ + auto transport = makeTransport({mr_}, NodeId{0x1F}); + + auto maybe_session = transport->makeResponseTxSession({123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto send_time = now(); + const auto timeout = 100ms; + session->setSendTimeout(timeout); + + const PayloadFragments empty_payload{}; + const ServiceTransferMetadata metadata{0x66, send_time, Priority::Fast, 0x0D}; + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)) + .WillOnce([&](auto deadline, auto endpoint, auto dscp, auto fragments) { + EXPECT_THAT(now(), send_time + 10ms); + EXPECT_THAT(deadline, send_time + timeout); + EXPECT_THAT(endpoint.ip_address, 0xEF01000D); + EXPECT_THAT(endpoint.udp_port, 9382); + EXPECT_THAT(dscp, 0x0); + EXPECT_THAT(fragments, SizeIs(1)); + EXPECT_THAT(fragments[0], SizeIs(24 + 4)); + return true; + }); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} + +TEST_F(TestUdpSvcTxSessions, send_response_with_argument_error) +{ + // Make initially anonymous node transport. + // + std::array media_array{&media_mock_}; + auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, media_array, 2); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); + auto transport = cetl::get>(std::move(maybe_transport)); + + auto maybe_session = transport->makeResponseTxSession({123}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + const PayloadFragments empty_payload{}; + ServiceTransferMetadata metadata{0x66, now(), Priority::Immediate, 13}; + + // Should fail due to anonymous node. + { + scheduler_.setNow(TimePoint{100ms}); + + const auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + } + + // Fix anonymous node, but break remote node id. + { + scheduler_.setNow(TimePoint{200ms}); + + EXPECT_THAT(transport->setLocalNodeId(31), Eq(cetl::nullopt)); + metadata.remote_node_id = UDPARD_NODE_ID_MAX + 1; + const auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10ms, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + } +} + +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +} // namespace diff --git a/test/unittest/transport/udp/test_udp_transport.cpp b/test/unittest/transport/udp/test_udp_transport.cpp index 7da3ac857..665ff91e2 100644 --- a/test/unittest/transport/udp/test_udp_transport.cpp +++ b/test/unittest/transport/udp/test_udp_transport.cpp @@ -3,58 +3,543 @@ /// Copyright Amazon.com Inc. or its affiliates. /// SPDX-License-Identifier: MIT +#include "../../cetl_gtest_helpers.hpp" +#include "../../memory_resource_mock.hpp" #include "../../tracking_memory_resource.hpp" +#include "../../verification_utilities.hpp" +#include "../../virtual_time_scheduler.hpp" #include "../multiplexer_mock.hpp" #include "media_mock.hpp" +#include "transient_error_handler_mock.hpp" +#include "tx_rx_sockets_mock.hpp" +#include +#include #include +#include +#include #include -#include +#include +#include +#include +#include +#include #include #include +#include #include +#include +#include +#include +#include +#include +#include namespace { +using libcyphal::TimePoint; +using libcyphal::UniquePtr; using namespace libcyphal::transport; // NOLINT This our main concern here in the unit tests. using namespace libcyphal::transport::udp; // NOLINT This our main concern here in the unit tests. +using libcyphal::verification_utilities::b; +using libcyphal::verification_utilities::makeIotaArray; +using libcyphal::verification_utilities::makeSpansFrom; + using testing::_; +using testing::Eq; +using testing::Truly; +using testing::Invoke; +using testing::Return; +using testing::SizeIs; using testing::IsEmpty; +using testing::NotNull; +using testing::Optional; +using testing::InSequence; using testing::StrictMock; using testing::VariantWith; +// https://github.com/llvm/llvm-project/issues/53444 +// NOLINTBEGIN(misc-unused-using-decls, misc-include-cleaner) +using std::literals::chrono_literals::operator""s; +using std::literals::chrono_literals::operator""us; +// NOLINTEND(misc-unused-using-decls, misc-include-cleaner) + +// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +class MyPlatformError final : public libcyphal::transport::IPlatformError +{ +public: + explicit MyPlatformError(std::uint32_t code) + : code_{code} + { + } + virtual ~MyPlatformError() noexcept = default; + MyPlatformError(const MyPlatformError&) = default; + MyPlatformError(MyPlatformError&&) noexcept = default; + MyPlatformError& operator=(const MyPlatformError&) = default; + MyPlatformError& operator=(MyPlatformError&&) noexcept = default; + + // MARK: IPlatformError + + std::uint32_t code() const noexcept override + { + return code_; + } + +private: + std::uint32_t code_; + +}; // MyPlatformError + class TestUpdTransport : public testing::Test { protected: + void SetUp() override + { + EXPECT_CALL(media_mock_, makeTxSocket()).WillRepeatedly(Invoke([this]() { + return libcyphal::detail::makeUniquePtr(mr_, tx_socket_mock_); + })); + EXPECT_CALL(tx_socket_mock_, getMtu()).WillRepeatedly(Invoke(&tx_socket_mock_, &TxSocketMock::getBaseMtu)); + } + void TearDown() override { EXPECT_THAT(mr_.allocations, IsEmpty()); EXPECT_THAT(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); } + TimePoint now() const + { + return scheduler_.now(); + } + + UniquePtr makeTransport(const MemoryResourcesSpec& mem_res_spec, + IMedia* extra_media = nullptr, + const std::size_t tx_capacity = 16) + { + std::array media_array{&media_mock_, extra_media}; + + auto maybe_transport = udp::makeTransport(mem_res_spec, mux_mock_, media_array, tx_capacity); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + return cetl::get>(std::move(maybe_transport)); + } + // MARK: Data members: // NOLINTBEGIN - TrackingMemoryResource mr_; - StrictMock mux_mock_{}; - StrictMock media_mock_{}; + libcyphal::VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock mux_mock_{}; + StrictMock media_mock_{}; + StrictMock tx_socket_mock_{"S1"}; // NOLINTEND }; // MARK: Tests: -TEST_F(TestUpdTransport, makeTransport) +TEST_F(TestUpdTransport, makeTransport_no_memory_at_all) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory at all (even for initial array of media). + EXPECT_CALL(mr_mock, do_allocate(_, _)).WillRepeatedly(Return(nullptr)); +#if (__cplusplus < CETL_CPP_STANDARD_17) + EXPECT_CALL(mr_mock, do_reallocate(nullptr, 0, _, _)).WillRepeatedly(Return(nullptr)); +#endif + + std::array media_array{&media_mock_}; + auto maybe_transport = udp::makeTransport({mr_mock}, mux_mock_, media_array, 0); + EXPECT_THAT(maybe_transport, VariantWith(VariantWith(_))); +} + +TEST_F(TestUpdTransport, makeTransport_no_memory_for_impl) +{ + StrictMock mr_mock{}; + mr_mock.redirectExpectedCallsTo(mr_); + + // Emulate that there is no memory available for the transport. + EXPECT_CALL(mr_mock, do_allocate(sizeof(udp::detail::TransportImpl), _)).WillOnce(Return(nullptr)); + + std::array media_array{&media_mock_}; + auto maybe_transport = udp::makeTransport({mr_mock}, mux_mock_, media_array, 0); + EXPECT_THAT(maybe_transport, VariantWith(VariantWith(_))); +} + +TEST_F(TestUpdTransport, makeTransport_too_many_media) +{ + std::array media_array{}; + std::fill(media_array.begin(), media_array.end(), &media_mock_); + + auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, media_array, 0); + EXPECT_THAT(maybe_transport, VariantWith(VariantWith(_))); +} + +TEST_F(TestUpdTransport, makeTransport_getLocalNodeId) { // Anonymous node { std::array media_array{&media_mock_}; - auto maybe_transport = udp::makeTransport(mr_, mux_mock_, media_array, {}); - EXPECT_THAT(maybe_transport, VariantWith(VariantWith(_))); + auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, media_array, 0); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); + + auto transport = cetl::get>(std::move(maybe_transport)); + EXPECT_THAT(transport->getLocalNodeId(), Eq(cetl::nullopt)); + } + + // Node with ID + { + std::array media_array{&media_mock_}; + auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, media_array, 0); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); + + auto transport = cetl::get>(std::move(maybe_transport)); + transport->setLocalNodeId(42); + + EXPECT_THAT(transport->getLocalNodeId(), Optional(42)); + } + + // Two media interfaces + { + StrictMock media_mock2; + + std::array media_array{&media_mock_, nullptr, &media_mock2}; + auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, media_array, 0); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); } + + // All 3 maximum number of media interfaces + { + StrictMock media_mock2{}; + StrictMock media_mock3{}; + + std::array media_array{&media_mock_, &media_mock2, &media_mock3}; + auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, media_array, 0); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + } +} + +TEST_F(TestUpdTransport, setLocalNodeId) +{ + // EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + + auto transport = makeTransport({mr_}); + + EXPECT_THAT(transport->setLocalNodeId(UDPARD_NODE_ID_MAX + 1), Optional(testing::A())); + EXPECT_THAT(transport->getLocalNodeId(), Eq(cetl::nullopt)); + + EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); + + EXPECT_THAT(transport->setLocalNodeId(UDPARD_NODE_ID_MAX), Eq(cetl::nullopt)); + EXPECT_THAT(transport->getLocalNodeId(), Optional(UDPARD_NODE_ID_MAX)); + + EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); + + EXPECT_THAT(transport->setLocalNodeId(UDPARD_NODE_ID_MAX), Eq(cetl::nullopt)); + EXPECT_THAT(transport->getLocalNodeId(), Optional(UDPARD_NODE_ID_MAX)); + + EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); + + EXPECT_THAT(transport->setLocalNodeId(0), Optional(testing::A())); + EXPECT_THAT(transport->getLocalNodeId(), Optional(UDPARD_NODE_ID_MAX)); + + EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); +} + +TEST_F(TestUpdTransport, makeTransport_with_invalid_arguments) +{ + // No media + const auto maybe_transport = udp::makeTransport({mr_}, mux_mock_, {}, 0); + EXPECT_THAT(maybe_transport, VariantWith(VariantWith(_))); } +TEST_F(TestUpdTransport, getProtocolParams) +{ + StrictMock media_mock2{}; + + std::array media_array{&media_mock_, &media_mock2}; + auto transport = cetl::get>(udp::makeTransport({mr_}, mux_mock_, media_array, 0)); + + const auto params = transport->getProtocolParams(); + EXPECT_THAT(params.transfer_id_modulo, std::numeric_limits::max()); + EXPECT_THAT(params.max_nodes, UDPARD_NODE_ID_MAX + 1); + EXPECT_THAT(params.mtu_bytes, UDPARD_MTU_DEFAULT); + + StrictMock tx_socket_mock2{"S2"}; + EXPECT_CALL(media_mock2, makeTxSocket()).WillRepeatedly(Invoke([&]() { + return libcyphal::detail::makeUniquePtr(mr_, tx_socket_mock2); + })); + EXPECT_CALL(tx_socket_mock2, getMtu()).WillRepeatedly(Return(ITxSocket::DefaultMtu)); + + auto maybe_tx_session = transport->makeMessageTxSession({123}); + ASSERT_THAT(maybe_tx_session, VariantWith>(NotNull())); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, UDPARD_MTU_DEFAULT); + + EXPECT_CALL(tx_socket_mock_, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT)); + EXPECT_CALL(tx_socket_mock2, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT - 256)); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, UDPARD_MTU_DEFAULT - 256); + + // Manipulate MTU values on fly + { + EXPECT_CALL(tx_socket_mock2, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT)); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, UDPARD_MTU_DEFAULT); + + EXPECT_CALL(tx_socket_mock_, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT - 256)); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, UDPARD_MTU_DEFAULT - 256); + + EXPECT_CALL(tx_socket_mock2, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT - 256)); + EXPECT_THAT(transport->getProtocolParams().mtu_bytes, UDPARD_MTU_DEFAULT - 256); + } +} + +TEST_F(TestUpdTransport, makeMessageTxSession) +{ + auto transport = makeTransport({mr_}); + + auto maybe_tx_session = transport->makeMessageTxSession({123}); + ASSERT_THAT(maybe_tx_session, VariantWith>(NotNull())); + + auto session = cetl::get>(std::move(maybe_tx_session)); + EXPECT_THAT(session->getParams().subject_id, 123); +} + +TEST_F(TestUpdTransport, sending_multiframe_payload_should_fail_for_anonymous) +{ + auto transport = makeTransport({mr_}); + + auto maybe_session = transport->makeMessageTxSession({7}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto send_time = now(); + + const auto payload = makeIotaArray(b('0')); + const TransferMetadata metadata{0x13, send_time, Priority::Nominal}; + + auto maybe_error = session->send(metadata, makeSpansFrom(payload)); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(10us, [&] { EXPECT_THAT(session->run(now()), UbVariantWithoutValue()); }); +} + +TEST_F(TestUpdTransport, sending_multiframe_payload_for_non_anonymous) +{ + auto transport = makeTransport({mr_}); + EXPECT_THAT(transport->setLocalNodeId(0x45), Eq(cetl::nullopt)); + + auto maybe_session = transport->makeMessageTxSession({7}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto timeout = 1s; + const auto send_time = now(); + + const auto payload = makeIotaArray(b('0')); + const TransferMetadata metadata{0x13, send_time, Priority::Nominal}; + + auto maybe_error = session->send(metadata, makeSpansFrom(payload)); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + { + const InSequence s; + + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)) + .WillOnce([&](auto deadline, auto endpoint, auto, auto fragments) { + EXPECT_THAT(now(), send_time + 10us); + EXPECT_THAT(deadline, send_time + timeout); + EXPECT_THAT(endpoint.ip_address, 0xEF000007); + EXPECT_THAT(fragments, SizeIs(1)); + EXPECT_THAT(fragments[0], SizeIs(24 + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 4)); + return true; + }); + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)) + .WillOnce([&](auto deadline, auto endpoint, auto, auto fragments) { + EXPECT_THAT(now(), send_time + 10us); + EXPECT_THAT(deadline, send_time + timeout); + EXPECT_THAT(endpoint.ip_address, 0xEF000007); + EXPECT_THAT(fragments, SizeIs(1)); + // NB! No `+4` here b/c CRC was in the start frame. + EXPECT_THAT(fragments[0], SizeIs(24 + 1)); + return true; + }); + } + + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} + +// NOLINTNEXTLINE(readability-function-cognitive-complexity) +TEST_F(TestUpdTransport, send_multiframe_payload_to_redundant_not_ready_media) +{ + StrictMock media_mock2{}; + StrictMock tx_socket_mock2{"S2"}; + EXPECT_CALL(tx_socket_mock2, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT)); + EXPECT_CALL(media_mock2, makeTxSocket()).WillRepeatedly(Invoke([this, &tx_socket_mock2]() { + return libcyphal::detail::makeUniquePtr(mr_, tx_socket_mock2); + })); + + auto transport = makeTransport({mr_}, &media_mock2); + EXPECT_THAT(transport->setLocalNodeId(0x45), Eq(cetl::nullopt)); + + auto maybe_session = transport->makeMessageTxSession({7}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto timeout = 1s; + const auto send_time = now(); + + const auto payload = makeIotaArray(b('0')); + const TransferMetadata metadata{0x13, send_time, Priority::Nominal}; + + auto maybe_error = session->send(metadata, makeSpansFrom(payload)); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + { + const InSequence s; + + auto expectSocketCalls = [&](TxSocketMock& tx_socket_mock, const std::string& ctx, TimePoint when) { + EXPECT_CALL(tx_socket_mock, send(_, _, _, _)) + .WillOnce([&, ctx, when](auto deadline, auto endpoint, auto, auto fragments) { + EXPECT_THAT(now(), when) << ctx; + EXPECT_THAT(deadline, send_time + timeout) << ctx; + EXPECT_THAT(endpoint.ip_address, 0xEF000007); + EXPECT_THAT(fragments, SizeIs(1)); + EXPECT_THAT(fragments[0], SizeIs(24 + UDPARD_MTU_DEFAULT_MAX_SINGLE_FRAME + 4)); + return true; + }); + EXPECT_CALL(tx_socket_mock, send(_, _, _, _)) + .WillOnce([&, ctx, when](auto deadline, auto endpoint, auto, auto fragments) { + EXPECT_THAT(now(), when) << ctx; + EXPECT_THAT(deadline, send_time + timeout) << ctx; + EXPECT_THAT(endpoint.ip_address, 0xEF000007); + EXPECT_THAT(fragments, SizeIs(1)); + EXPECT_THAT(fragments[0], SizeIs(24 + 4)); + return true; + }); + }; + + // Emulate once that the first media is not ready to send fragment (@10us). So transport will + // switch to the second media, and only on the next run will (@20us) retry with the first media again. + // + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)).WillOnce([&](auto, auto, auto, auto) { + EXPECT_THAT(now(), send_time + 10us); + return false; + }); + expectSocketCalls(tx_socket_mock2, "M#2", send_time + 10us); + expectSocketCalls(tx_socket_mock_, "M#1", send_time + 20us); + } + + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); +} + +TEST_F(TestUpdTransport, send_payload_to_redundant_fallible_media) +{ + using SocketSendReport = IUdpTransport::TransientErrorReport::MediaTxSocketSend; + + StrictMock media_mock2{}; + StrictMock tx_socket_mock2{"S2"}; + EXPECT_CALL(tx_socket_mock2, getMtu()).WillRepeatedly(Return(UDPARD_MTU_DEFAULT)); + EXPECT_CALL(media_mock2, makeTxSocket()).WillRepeatedly(Invoke([&]() { + return libcyphal::detail::makeUniquePtr(mr_, tx_socket_mock2); + })); + + auto transport = makeTransport({mr_}, &media_mock2); + EXPECT_THAT(transport->setLocalNodeId(0x45), Eq(cetl::nullopt)); + + auto maybe_session = transport->makeMessageTxSession({7}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + scheduler_.runNow(+10s); + const auto send_time = now(); + + const auto payload = makeIotaArray<6>(b('0')); + const TransferMetadata metadata{0x13, send_time, Priority::Nominal}; + + // First attempt to send payload. + auto maybe_error = session->send(metadata, makeSpansFrom(payload)); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + // 1st run: media #0 and there is no transient error handler; its frame should be dropped. + { + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)).WillOnce(Return(ArgumentError{})); + + scheduler_.runNow(+10us, [&] { + EXPECT_THAT(transport->run(now()), UbVariantWith(VariantWith(_))); + }); + } + // 2nd run: media #1 and transient error handler have failed; its frame should be dropped. + { + StrictMock handler_mock{}; + transport->setTransientErrorHandler(std::ref(handler_mock)); + EXPECT_CALL(handler_mock, invoke(VariantWith(Truly([&](auto& report) { + EXPECT_THAT(report.error, VariantWith(_)); + EXPECT_THAT(report.media_index, 1); + auto culprit = static_cast(report.culprit); + EXPECT_THAT(culprit.tx_socket_mock(), Ref(tx_socket_mock2)); + return true; + })))) + .WillOnce(Return(PlatformError{MyPlatformError{13}})); + + EXPECT_CALL(tx_socket_mock2, send(_, _, _, _)).WillOnce(Return(ArgumentError{})); + + scheduler_.runNow(+10us, [&] { + EXPECT_THAT(transport->run(now()), UbVariantWith(VariantWith(_))); + }); + + // No frames should be left in the session. + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + } + + // Second attempt to send payload. + EXPECT_THAT(session->send(metadata, makeSpansFrom(payload)), Eq(cetl::nullopt)); + + // 3rd run: media #0 has failed but transient error handler succeeded. + { + StrictMock handler_mock{}; + transport->setTransientErrorHandler(std::ref(handler_mock)); + EXPECT_CALL(handler_mock, invoke(VariantWith(Truly([&](auto& report) { + EXPECT_THAT(report.error, VariantWith(_)); + EXPECT_THAT(report.media_index, 0); + auto culprit = static_cast(report.culprit); + EXPECT_THAT(culprit.tx_socket_mock(), Ref(tx_socket_mock_)); + return true; + })))) + .WillOnce(Return(cetl::nullopt)); + + EXPECT_CALL(tx_socket_mock_, send(_, _, _, _)).WillOnce(Return(ArgumentError{})); + EXPECT_CALL(tx_socket_mock2, send(_, _, _, _)).WillOnce(Return(true)); + + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + + // No frames should be left in the session. + scheduler_.runNow(+10us, [&] { EXPECT_THAT(transport->run(now()), UbVariantWithoutValue()); }); + } +} + +// NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + } // namespace + +namespace cetl +{ + +// Just random id: 96B4BFC0-FCDD-4804-9C0E-97566FD9BE42 +template <> +constexpr type_id type_id_getter() noexcept +{ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + return {0x96, 0xB4, 0xBF, 0xC0, 0xFC, 0xDD, 0x48, 0x04, 0x9C, 0x0E, 0x97, 0x56, 0x6F, 0xD9, 0xBE, 0x42}; +} + +} // namespace cetl diff --git a/test/unittest/transport/udp/transient_error_handler_mock.hpp b/test/unittest/transport/udp/transient_error_handler_mock.hpp new file mode 100644 index 000000000..18b7d64d7 --- /dev/null +++ b/test/unittest/transport/udp/transient_error_handler_mock.hpp @@ -0,0 +1,37 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_CAN_TRANSIENT_ERROR_HANDLER_MOCK_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_CAN_TRANSIENT_ERROR_HANDLER_MOCK_HPP_INCLUDED + +#include +#include +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +class TransientErrorHandlerMock +{ +public: + cetl::optional operator()(IUdpTransport::TransientErrorReport::Variant& report_var) + { + return invoke(report_var); + } + + MOCK_METHOD(cetl::optional, invoke, (IUdpTransport::TransientErrorReport::Variant& report_var)); +}; + +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_CAN_TRANSIENT_ERROR_HANDLER_MOCK_HPP_INCLUDED diff --git a/test/unittest/transport/udp/tx_rx_sockets_mock.hpp b/test/unittest/transport/udp/tx_rx_sockets_mock.hpp new file mode 100644 index 000000000..740269f54 --- /dev/null +++ b/test/unittest/transport/udp/tx_rx_sockets_mock.hpp @@ -0,0 +1,133 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_TX_RX_SOCKETS_MOCK_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_TX_RX_SOCKETS_MOCK_HPP_INCLUDED + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +class TxSocketMock : public ITxSocket +{ +public: + struct ReferenceWrapper : ITxSocket + { + struct Spec : libcyphal::detail::UniquePtrSpec + {}; + + explicit ReferenceWrapper(TxSocketMock& tx_socket_mock) + : tx_socket_mock_{tx_socket_mock} + { + } + ReferenceWrapper(const ReferenceWrapper& other) + : tx_socket_mock_{other.tx_socket_mock_} + { + } + + virtual ~ReferenceWrapper() = default; + ReferenceWrapper(ReferenceWrapper&&) noexcept = delete; + ReferenceWrapper& operator=(const ReferenceWrapper&) = delete; + ReferenceWrapper& operator=(ReferenceWrapper&&) noexcept = delete; + + TxSocketMock& tx_socket_mock() + { + return tx_socket_mock_; + } + + // MARK: ITxSocket + + std::size_t getMtu() const noexcept override + { + return tx_socket_mock_.getMtu(); + } + + libcyphal::Expected> send( + const TimePoint deadline, + const IpEndpoint multicast_endpoint, + const std::uint8_t dscp, + const PayloadFragments payload_fragments) override + { + return tx_socket_mock_.send(deadline, multicast_endpoint, dscp, payload_fragments); + } + + private: + TxSocketMock& tx_socket_mock_; + + }; // ReferenceWrapper + + explicit TxSocketMock(std::string name) + : name_{std::move(name)} {}; + + virtual ~TxSocketMock() = default; + TxSocketMock(const TxSocketMock&) = delete; + TxSocketMock(TxSocketMock&&) noexcept = delete; + TxSocketMock& operator=(const TxSocketMock&) = delete; + TxSocketMock& operator=(TxSocketMock&&) noexcept = delete; + + std::string getMockName() const + { + return name_; + } + + std::size_t getBaseMtu() const noexcept + { + return ITxSocket::getMtu(); + } + + // MARK: ITxSocket + + // NOLINTNEXTLINE(bugprone-exception-escape) + MOCK_METHOD(std::size_t, getMtu, (), (const, noexcept, override)); + + MOCK_METHOD((Expected>), + send, + (const TimePoint deadline, + const IpEndpoint multicast_endpoint, + const std::uint8_t dscp, + const PayloadFragments payload_fragments), + (override)); + +private: + const std::string name_; + +}; // TxSocketMock + +// MARK: - + +class RxSocketMock : public IRxSocket +{ +public: + RxSocketMock() = default; + virtual ~RxSocketMock() = default; + + RxSocketMock(const RxSocketMock&) = delete; + RxSocketMock(RxSocketMock&&) noexcept = delete; + RxSocketMock& operator=(const RxSocketMock&) = delete; + RxSocketMock& operator=(RxSocketMock&&) noexcept = delete; + +}; // RxSocketMock + +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_TX_RX_SOCKETS_MOCK_HPP_INCLUDED