diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000..51113017d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,31 @@ +Checks: >- + boost-*, + bugprone-*, + cert-*, + clang-analyzer-*, + cppcoreguidelines-*, + google-*, + hicpp-*, + llvm-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -cppcoreguidelines-avoid-const-or-ref-data-members, + -google-readability-todo, + -readability-avoid-const-params-in-decls, + -readability-identifier-length, + -llvm-header-guard, + -llvm-include-order, + -*-use-trailing-return-type, + -*-named-parameter, +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: '90' + - key: readability-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;5;8;10;16;20;32;60;64;100;128;256;500;512;1000' +WarningsAsErrors: '*' +HeaderFilterRegex: '.*\.hpp' +AnalyzeTemporaryDtors: false +FormatStyle: file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cace2941..920c084cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,7 +62,7 @@ if (${CMAKE_CXX_PLATFORM_ID} STREQUAL "Linux") endif() # Don't normalize deviance: if CMAKE_TOOLCHAIN_FILE is not set then provide -# an initalized default to display in the status thus avoiding a warning about +# an initialized default to display in the status thus avoiding a warning about # using an uninitialized variable. if (NOT CMAKE_TOOLCHAIN_FILE) set(LOCAL_CMAKE_TOOLCHAIN_FILE "(not set)") diff --git a/cmake/modules/Findcetl.cmake b/cmake/modules/Findcetl.cmake index 94659983e..383261d8e 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 "886a0d227a043511eed6b252ea0f788590c50e75") +set(cetl_GIT_TAG "10fbb2b7b89473d68e73db7235848b0692169e5a") FetchContent_Declare( cetl diff --git a/docs/doxygen.ini b/docs/doxygen.ini index 7530e18a9..bbca28ed5 100644 --- a/docs/doxygen.ini +++ b/docs/doxygen.ini @@ -1071,7 +1071,7 @@ EXCLUDE_PATTERNS = # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -EXCLUDE_SYMBOLS = +EXCLUDE_SYMBOLS = *::detail::* # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include diff --git a/include/libcyphal/transport/can/delegate.hpp b/include/libcyphal/transport/can/delegate.hpp index 9b371d6ef..69f162a2f 100644 --- a/include/libcyphal/transport/can/delegate.hpp +++ b/include/libcyphal/transport/can/delegate.hpp @@ -6,6 +6,7 @@ #ifndef LIBCYPHAL_TRANSPORT_CAN_DELEGATE_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_CAN_DELEGATE_HPP_INCLUDED +#include #include #include @@ -29,7 +30,7 @@ namespace detail /// This internal transport delegate class serves the following purposes: /// 1. It provides memory management functions for the Canard library. /// 2. It provides a way to convert Canard error codes to `AnyError` type. -/// 3. It provides interface to access the transport from various session classes. +/// 3. It provides an interface to access the transport from various session classes. /// class TransportDelegate { @@ -40,7 +41,7 @@ class TransportDelegate public: /// @brief RAII class to manage memory allocated by Canard library. /// - class CanardMemory final : public cetl::rtti_helper + class CanardMemory final : public cetl::rtti_helper { public: CanardMemory(TransportDelegate& delegate, void* const buffer, const std::size_t payload_size) @@ -70,7 +71,7 @@ class TransportDelegate CanardMemory& operator=(const CanardMemory&) = delete; CanardMemory& operator=(CanardMemory&&) noexcept = delete; - // MARK: ScatteredBuffer::Interface + // MARK: ScatteredBuffer::IStorage CETL_NODISCARD std::size_t size() const noexcept final { @@ -161,10 +162,9 @@ class TransportDelegate /// /// Internal method which is in use by TX session implementations to delegate actual sending to transport. /// - CETL_NODISCARD virtual cetl::optional sendTransfer(const CanardMicrosecond deadline, + CETL_NODISCARD virtual cetl::optional sendTransfer(const TimePoint deadline, const CanardTransferMetadata& metadata, - const void* const payload, - const std::size_t payload_size) = 0; + const PayloadFragments payload_fragments) = 0; protected: ~TransportDelegate() = default; @@ -230,26 +230,28 @@ class TransportDelegate }; // TransportDelegate -/// This internal session delegate class serves the following purpose: it provides interface -/// to access a session from transport (by casting canard's `user_reference` member to this class). +// MARK: - + +/// This internal session delegate class serves the following purpose: it provides an interface (aka gateway) +/// to access RX session from transport (by casting canard's `user_reference` member to this class). /// -class SessionDelegate +class IRxSessionDelegate { public: - SessionDelegate(const SessionDelegate&) = delete; - SessionDelegate(SessionDelegate&&) noexcept = delete; - SessionDelegate& operator=(const SessionDelegate&) = delete; - SessionDelegate& operator=(SessionDelegate&&) noexcept = delete; + 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 CanardRxTransfer& transfer) = 0; protected: - SessionDelegate() = default; - ~SessionDelegate() = default; + IRxSessionDelegate() = default; + ~IRxSessionDelegate() = default; -}; // SessionDelegate +}; // IRxSessionDelegate } // namespace detail } // namespace can diff --git a/include/libcyphal/transport/can/media.hpp b/include/libcyphal/transport/can/media.hpp index 94a2f1c0b..f092c72cc 100644 --- a/include/libcyphal/transport/can/media.hpp +++ b/include/libcyphal/transport/can/media.hpp @@ -50,7 +50,7 @@ class IMedia /// 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. /// - CETL_NODISCARD virtual std::size_t getMtu() const noexcept = 0; + virtual std::size_t getMtu() const noexcept = 0; /// @brief Set the filters for the CAN bus. /// diff --git a/include/libcyphal/transport/can/msg_rx_session.hpp b/include/libcyphal/transport/can/msg_rx_session.hpp index d0c70b1ee..294339d5e 100644 --- a/include/libcyphal/transport/can/msg_rx_session.hpp +++ b/include/libcyphal/transport/can/msg_rx_session.hpp @@ -25,22 +25,33 @@ namespace can /// namespace detail { -class MessageRxSession final : public IMessageRxSession, private SessionDelegate + +/// @brief A class to represent a message subscriber RX session. +/// +class MessageRxSession final : public IMessageRxSession, private IRxSessionDelegate { - // In use to disable public construction. - // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ - struct Tag + /// @brief Defines specification for making interface unique ptr. + /// + struct Spec { - explicit Tag() = default; using Interface = IMessageRxSession; using Concrete = MessageRxSession; + + // In use to disable public construction. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; }; public: CETL_NODISCARD static Expected, AnyError> make(TransportDelegate& delegate, const MessageRxParams& params) { - auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Tag{}, delegate, params); + if (params.subject_id > CANARD_SUBJECT_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); if (session == nullptr) { return MemoryError{}; @@ -49,7 +60,7 @@ class MessageRxSession final : public IMessageRxSession, private SessionDelegate return session; } - MessageRxSession(Tag, TransportDelegate& delegate, const MessageRxParams& params) + MessageRxSession(Spec, TransportDelegate& delegate, const MessageRxParams& params) : delegate_{delegate} , params_{params} , subscription_{} @@ -65,14 +76,17 @@ class MessageRxSession final : public IMessageRxSession, private SessionDelegate 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); + subscription_.user_reference = static_cast(this); } ~MessageRxSession() final { - ::canardRxUnsubscribe(&delegate_.canard_instance(), - CanardTransferKindMessage, - static_cast(params_.subject_id)); + const int8_t result = ::canardRxUnsubscribe(&delegate_.canard_instance(), + CanardTransferKindMessage, + static_cast(params_.subject_id)); + (void) result; + 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."); } private: @@ -108,7 +122,7 @@ class MessageRxSession final : public IMessageRxSession, private SessionDelegate // Nothing to do here currently. } - // MARK: SessionDelegate + // MARK: IRxSessionDelegate void acceptRxTransfer(const CanardRxTransfer& transfer) final { @@ -129,9 +143,8 @@ class MessageRxSession final : public IMessageRxSession, private SessionDelegate // MARK: Data members: - TransportDelegate& delegate_; - const MessageRxParams params_; - + TransportDelegate& delegate_; + const MessageRxParams params_; CanardRxSubscription subscription_; cetl::optional last_rx_transfer_; diff --git a/include/libcyphal/transport/can/msg_tx_session.hpp b/include/libcyphal/transport/can/msg_tx_session.hpp index 420162104..f52dd6f12 100644 --- a/include/libcyphal/transport/can/msg_tx_session.hpp +++ b/include/libcyphal/transport/can/msg_tx_session.hpp @@ -26,22 +26,31 @@ namespace can /// namespace detail { + class MessageTxSession final : public IMessageTxSession { - // In use to disable public construction. - // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ - struct Tag + /// @brief Defines specification for making interface unique ptr. + /// + struct Spec { - explicit Tag() = default; using Interface = IMessageTxSession; using Concrete = MessageTxSession; + + // In use to disable public construction. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; }; public: CETL_NODISCARD static Expected, AnyError> make(TransportDelegate& delegate, const MessageTxParams& params) { - auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Tag{}, delegate, params); + if (params.subject_id > CANARD_SUBJECT_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); if (session == nullptr) { return MemoryError{}; @@ -50,7 +59,7 @@ class MessageTxSession final : public IMessageTxSession return session; } - MessageTxSession(Tag, TransportDelegate& delegate, const MessageTxParams& params) + MessageTxSession(Spec, TransportDelegate& delegate, const MessageTxParams& params) : delegate_{delegate} , params_{params} , send_timeout_{std::chrono::seconds{1}} @@ -75,29 +84,13 @@ class MessageTxSession final : public IMessageTxSession CETL_NODISCARD cetl::optional send(const TransferMetadata& metadata, const PayloadFragments payload_fragments) final { - // libcanard currently does not support fragmented payloads (at `canardTxPush`). - // so we need to concatenate them when there are more than one non-empty fragment. - // See https://github.com/OpenCyphal/libcanard/issues/223 - // - const transport::detail::ContiguousPayload contiguous_payload{delegate_.memory(), payload_fragments}; - if ((contiguous_payload.data() == nullptr) && (contiguous_payload.size() > 0)) - { - return MemoryError{}; - } - - const TimePoint deadline = metadata.timestamp + send_timeout_; - const auto deadline_us = std::chrono::duration_cast(deadline.time_since_epoch()); - const auto canard_metadata = CanardTransferMetadata{static_cast(metadata.priority), CanardTransferKindMessage, static_cast(params_.subject_id), CANARD_NODE_ID_UNSET, static_cast(metadata.transfer_id)}; - return delegate_.sendTransfer(static_cast(deadline_us.count()), - canard_metadata, - contiguous_payload.data(), - contiguous_payload.size()); + return delegate_.sendTransfer(metadata.timestamp + send_timeout_, canard_metadata, payload_fragments); } // MARK: IRunnable diff --git a/include/libcyphal/transport/can/svc_rx_sessions.hpp b/include/libcyphal/transport/can/svc_rx_sessions.hpp new file mode 100644 index 000000000..d289f46d7 --- /dev/null +++ b/include/libcyphal/transport/can/svc_rx_sessions.hpp @@ -0,0 +1,172 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_CAN_SVC_RX_SESSIONS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_CAN_SVC_RX_SESSIONS_HPP_INCLUDED + +#include "delegate.hpp" +#include "libcyphal/transport/svc_sessions.hpp" + +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace can +{ + +/// Internal implementation details of the CAN 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`. +/// @tparam TransferKind Kind of the service transfer. +/// Could be either `CanardTransferKindRequest` or `CanardTransferKindResponse`. +/// +template +class SvcRxSession final : public Interface_, private IRxSessionDelegate +{ + /// @brief Defines specification for making interface unique ptr. + /// + struct Spec + { + using Interface = Interface_; + using Concrete = SvcRxSession; + + // In use to disable public construction. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(TransportDelegate& delegate, + const Params& params) + { + if (params.service_id > CANARD_SERVICE_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + SvcRxSession(Spec, TransportDelegate& delegate, const Params& params) + : delegate_{delegate} + , params_{params} + , subscription_{} + , last_rx_transfer_{} + { + const int8_t result = ::canardRxSubscribe(&delegate.canard_instance(), + TransferKind, + static_cast(params_.service_id), + static_cast(params_.extent_bytes), + CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, + &subscription_); + (void) result; + 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); + } + + ~SvcRxSession() final + { + const int8_t result = ::canardRxUnsubscribe(&delegate_.canard_instance(), + TransferKind, + static_cast(params_.service_id)); + (void) result; + 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."); + } + +private: + // MARK: Interface + + CETL_NODISCARD Params getParams() const noexcept final + { + return params_; + } + + CETL_NODISCARD cetl::optional receive() final + { + cetl::optional result{}; + result.swap(last_rx_transfer_); + return result; + } + + // MARK: IRxSession + + void setTransferIdTimeout(const Duration timeout) final + { + const auto timeout_us = std::chrono::duration_cast(timeout); + if (timeout_us.count() > 0) + { + subscription_.transfer_id_timeout_usec = static_cast(timeout_us.count()); + } + } + + // MARK: IRunnable + + void run(const TimePoint) final + { + // Nothing to do here currently. + } + + // MARK: IRxSessionDelegate + + void acceptRxTransfer(const CanardRxTransfer& transfer) final + { + const auto priority = static_cast(transfer.metadata.priority); + const auto remote_node_id = static_cast(transfer.metadata.remote_node_id); + 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_, transfer.payload, transfer.payload_size}; + + last_rx_transfer_.emplace(ServiceRxTransfer{meta, ScatteredBuffer{std::move(canard_memory)}}); + } + + // MARK: Data members: + + TransportDelegate& delegate_; + const Params params_; + CanardRxSubscription subscription_; + 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 can +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_CAN_SVC_RX_SESSIONS_HPP_INCLUDED diff --git a/include/libcyphal/transport/can/svc_tx_sessions.hpp b/include/libcyphal/transport/can/svc_tx_sessions.hpp new file mode 100644 index 000000000..9348492d0 --- /dev/null +++ b/include/libcyphal/transport/can/svc_tx_sessions.hpp @@ -0,0 +1,223 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_CAN_SVC_TX_SESSIONS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_CAN_SVC_TX_SESSIONS_HPP_INCLUDED + +#include "delegate.hpp" +#include "libcyphal/transport/svc_sessions.hpp" +#include "libcyphal/transport/contiguous_payload.hpp" + +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace can +{ + +/// Internal implementation details of the CAN 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 specification for making interface unique ptr. + /// + struct Spec + { + using Interface = IRequestTxSession; + using Concrete = SvcRequestTxSession; + + // In use to disable public construction. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(TransportDelegate& delegate, + const RequestTxParams& params) + { + if ((params.service_id > CANARD_SERVICE_ID_MAX) || (params.server_node_id > CANARD_NODE_ID_MAX)) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + SvcRequestTxSession(Spec, TransportDelegate& delegate, const RequestTxParams& params) + : delegate_{delegate} + , params_{params} + , send_timeout_{std::chrono::seconds{1}} + { + } + +private: + // MARK: ITxSession + + void setSendTimeout(const Duration timeout) final + { + send_timeout_ = timeout; + } + + // MARK: IRequestTxSession + + CETL_NODISCARD RequestTxParams getParams() const noexcept final + { + return params_; + } + + CETL_NODISCARD cetl::optional send(const TransferMetadata& metadata, + const PayloadFragments payload_fragments) final + { + // 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. + // + const CanardNodeID local_node_id = delegate_.canard_instance().node_id; + if (local_node_id > CANARD_NODE_ID_MAX) + { + return ArgumentError{}; + } + + const auto canard_metadata = CanardTransferMetadata{static_cast(metadata.priority), + CanardTransferKindRequest, + static_cast(params_.service_id), + static_cast(params_.server_node_id), + static_cast(metadata.transfer_id)}; + + return delegate_.sendTransfer(metadata.timestamp + send_timeout_, canard_metadata, payload_fragments); + } + + // MARK: IRunnable + + void run(const TimePoint) final + { + // Nothing to do here currently. + } + + // 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 specification for making interface unique ptr. + /// + struct Spec + { + using Interface = IResponseTxSession; + using Concrete = SvcResponseTxSession; + + // In use to disable public construction. + // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ + explicit Spec() = default; + }; + +public: + CETL_NODISCARD static Expected, AnyError> make(TransportDelegate& delegate, + const ResponseTxParams& params) + { + if (params.service_id > CANARD_SERVICE_ID_MAX) + { + return ArgumentError{}; + } + + auto session = libcyphal::detail::makeUniquePtr(delegate.memory(), Spec{}, delegate, params); + if (session == nullptr) + { + return MemoryError{}; + } + + return session; + } + + SvcResponseTxSession(Spec, TransportDelegate& delegate, const ResponseTxParams& params) + : delegate_{delegate} + , params_{params} + , send_timeout_{std::chrono::seconds{1}} + { + } + +private: + // MARK: ITxSession + + void setSendTimeout(const Duration timeout) final + { + send_timeout_ = timeout; + } + + // MARK: IResponseTxSession + + CETL_NODISCARD ResponseTxParams getParams() const noexcept final + { + return params_; + } + + CETL_NODISCARD cetl::optional send(const ServiceTransferMetadata& metadata, + const PayloadFragments payload_fragments) final + { + // 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. + // + 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)) + { + return ArgumentError{}; + } + + const auto canard_metadata = CanardTransferMetadata{static_cast(metadata.priority), + CanardTransferKindResponse, + static_cast(params_.service_id), + static_cast(metadata.remote_node_id), + static_cast(metadata.transfer_id)}; + + return delegate_.sendTransfer(metadata.timestamp + send_timeout_, canard_metadata, payload_fragments); + } + + // MARK: IRunnable + + void run(const TimePoint) final + { + // Nothing to do here currently. + } + + // MARK: Data members: + + TransportDelegate& delegate_; + const ResponseTxParams params_; + Duration send_timeout_; + +}; // SvcResponseTxSession + +} // namespace detail +} // namespace can +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_CAN_SVC_TX_SESSIONS_HPP_INCLUDED diff --git a/include/libcyphal/transport/can/transport.hpp b/include/libcyphal/transport/can/transport.hpp index 562b2c67e..1804f290f 100644 --- a/include/libcyphal/transport/can/transport.hpp +++ b/include/libcyphal/transport/can/transport.hpp @@ -10,6 +10,8 @@ #include "delegate.hpp" #include "msg_rx_session.hpp" #include "msg_tx_session.hpp" +#include "svc_rx_sessions.hpp" +#include "svc_tx_sessions.hpp" #include "libcyphal/transport/transport.hpp" #include "libcyphal/transport/multiplexer.hpp" @@ -42,13 +44,14 @@ class TransportImpl final : public ICanTransport, private TransportDelegate { using Interface = ICanTransport; using Concrete = TransportImpl; + + // In use to disable public construction. + // 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. /// - /// B/c it's private, it also disables public construction of `TransportImpl`. - /// See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ - /// class Media final { public: @@ -101,8 +104,12 @@ class TransportImpl final : public ICanTransport, private TransportDelegate const auto canard_node_id = static_cast(local_node_id.value_or(CANARD_NODE_ID_UNSET)); - auto transport = - libcyphal::detail::makeUniquePtr(memory, memory, multiplexer, std::move(media_array), canard_node_id); + auto transport = libcyphal::detail::makeUniquePtr(memory, + Spec{}, + memory, + multiplexer, + std::move(media_array), + canard_node_id); if (transport == nullptr) { return MemoryError{}; @@ -111,7 +118,8 @@ class TransportImpl final : public ICanTransport, private TransportDelegate return transport; } - TransportImpl(cetl::pmr::memory_resource& memory, + TransportImpl(Spec, + cetl::pmr::memory_resource& memory, IMultiplexer& multiplexer, MediaArray&& media_array, const CanardNodeID canard_node_id) @@ -181,8 +189,7 @@ class TransportImpl final : public ICanTransport, private TransportDelegate CETL_NODISCARD Expected, AnyError> makeMessageRxSession( const MessageRxParams& params) final { - const cetl::optional any_error = - ensureNewSessionFor(CanardTransferKindMessage, params.subject_id, CANARD_SUBJECT_ID_MAX); + const cetl::optional any_error = ensureNewSessionFor(CanardTransferKindMessage, params.subject_id); if (any_error.has_value()) { return any_error.value(); @@ -200,38 +207,37 @@ class TransportImpl final : public ICanTransport, private TransportDelegate CETL_NODISCARD Expected, AnyError> makeRequestRxSession( const RequestRxParams& params) final { - const cetl::optional any_error = - ensureNewSessionFor(CanardTransferKindRequest, params.service_id, CANARD_SERVICE_ID_MAX); + const cetl::optional any_error = ensureNewSessionFor(CanardTransferKindRequest, params.service_id); if (any_error.has_value()) { return any_error.value(); } - return NotImplementedError{}; + return SvcRequestRxSession::make(asDelegate(), params); } - CETL_NODISCARD Expected, AnyError> makeRequestTxSession(const RequestTxParams&) final + CETL_NODISCARD Expected, AnyError> makeRequestTxSession( + const RequestTxParams& params) final { - return NotImplementedError{}; + return SvcRequestTxSession::make(asDelegate(), params); } CETL_NODISCARD Expected, AnyError> makeResponseRxSession( const ResponseRxParams& params) final { - const cetl::optional any_error = - ensureNewSessionFor(CanardTransferKindResponse, params.service_id, CANARD_SERVICE_ID_MAX); + const cetl::optional any_error = ensureNewSessionFor(CanardTransferKindResponse, params.service_id); if (any_error.has_value()) { return any_error.value(); } - return NotImplementedError{}; + return SvcResponseRxSession::make(asDelegate(), params); } CETL_NODISCARD Expected, AnyError> makeResponseTxSession( - const ResponseTxParams&) final + const ResponseTxParams& params) final { - return NotImplementedError{}; + return SvcResponseTxSession::make(asDelegate(), params); } // MARK: IRunnable @@ -249,11 +255,22 @@ class TransportImpl final : public ICanTransport, private TransportDelegate return *this; } - CETL_NODISCARD cetl::optional sendTransfer(const CanardMicrosecond deadline, + CETL_NODISCARD cetl::optional sendTransfer(const TimePoint deadline, const CanardTransferMetadata& metadata, - const void* const payload, - const std::size_t payload_size) final + const PayloadFragments payload_fragments) final { + // libcanard currently does not support fragmented payloads (at `canardTxPush`). + // so we need to concatenate them when there are more than one non-empty fragment. + // See https://github.com/OpenCyphal/libcanard/issues/223 + // + const transport::detail::ContiguousPayload payload{memory(), payload_fragments}; + if ((payload.data() == nullptr) && (payload.size() > 0)) + { + return MemoryError{}; + } + + const auto deadline_us = std::chrono::duration_cast(deadline.time_since_epoch()); + // TODO: Rework error handling strategy. // Currently, we return the last error encountered, but we should consider all errors somehow. // @@ -263,8 +280,12 @@ class TransportImpl final : public ICanTransport, private TransportDelegate { media.canard_tx_queue.mtu_bytes = media.interface.getMtu(); - const std::int32_t result = - ::canardTxPush(&media.canard_tx_queue, &canard_instance(), deadline, &metadata, payload_size, payload); + const std::int32_t result = ::canardTxPush(&media.canard_tx_queue, + &canard_instance(), + static_cast(deadline_us.count()), + &metadata, + payload.size(), + payload.data()); if (result < 0) { maybe_error = TransportDelegate::anyErrorFromCanard(result); @@ -288,14 +309,8 @@ class TransportImpl final : public ICanTransport, private TransportDelegate } CETL_NODISCARD cetl::optional ensureNewSessionFor(const CanardTransferKind transfer_kind, - const PortId port_id, - const PortId max_port_id) noexcept + const PortId port_id) noexcept { - if (port_id > max_port_id) - { - return ArgumentError{}; - } - const std::int8_t hasSubscription = ::canardRxHasSubscription(&canard_instance(), transfer_kind, port_id); CETL_DEBUG_ASSERT(hasSubscription >= 0, "There is no way currently to get an error here."); if (hasSubscription > 0) @@ -376,7 +391,7 @@ class TransportImpl final : public ICanTransport, private TransportDelegate CETL_DEBUG_ASSERT(out_subscription != nullptr, "Expected subscription."); CETL_DEBUG_ASSERT(out_subscription->user_reference != nullptr, "Expected session delegate."); - const auto delegate = static_cast(out_subscription->user_reference); + const auto delegate = static_cast(out_subscription->user_reference); delegate->acceptRxTransfer(out_transfer); } } diff --git a/include/libcyphal/transport/scattered_buffer.hpp b/include/libcyphal/transport/scattered_buffer.hpp index 8d89a3daa..6c2772e91 100644 --- a/include/libcyphal/transport/scattered_buffer.hpp +++ b/include/libcyphal/transport/scattered_buffer.hpp @@ -6,7 +6,7 @@ #ifndef LIBCYPHAL_TRANSPORT_SCATTERED_BUFFER_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_SCATTERED_BUFFER_HPP_INCLUDED -#include +#include #include @@ -15,53 +15,99 @@ namespace libcyphal namespace transport { +/// @brief Represents a buffer that could be scattered across multiple memory regions of an abstract storage. +/// /// The buffer is movable but not copyable because copying the contents of a buffer is considered wasteful. /// The buffer behaves as if it's empty if the underlying implementation is moved away. /// class ScatteredBuffer final { // 91C1B109-F90E-45BE-95CF-6ED02AC3FFAA - using InterfaceTypeIdType = cetl:: + using IStorageTypeIdType = cetl:: type_id_type<0x91, 0xC1, 0xB1, 0x09, 0xF9, 0x0E, 0x45, 0xBE, 0x95, 0xCF, 0x6E, 0xD0, 0x2A, 0xC3, 0xFF, 0xAA>; public: - static constexpr std::size_t ImplementationFootprint = sizeof(void*) * 8; + /// @brief Defines maximum size (aka footprint) of the storage variant. + /// + static constexpr std::size_t StorageVariantFootprint = sizeof(void*) * 8; - class Interface : public cetl::rtti_helper + /// @brief Defines storage interface for the scattered buffer. + /// + /// @see ScatteredBuffer::ScatteredBuffer(AnyStorage&& any_storage) + /// + class IStorage : public cetl::rtti_helper { public: - Interface(const Interface&) = delete; - Interface& operator=(const Interface&) = delete; - - CETL_NODISCARD virtual std::size_t size() const noexcept = 0; + // No copying, but move only! + IStorage(const IStorage&) = delete; + IStorage& operator=(const IStorage&) = delete; + + /// @brief Gets the total number of bytes stored in the buffer. + /// + /// The storage could be possibly scattered, but this is hidden from the user. + /// + CETL_NODISCARD virtual std::size_t size() const noexcept = 0; + + /// @brief Copies a fragment of the specified size at the specified offset out of the storage. + /// + /// The request `[offset, offset+length)` range is truncated to prevent out-of-range memory access. + /// The storage memory could be possibly scattered, but this is hidden from the user. + /// + /// @param offset_bytes The offset in bytes from the beginning of the storage. + /// @param destination The pointer to the destination buffer. Should be at least `length_bytes` long. + /// Could be `nullptr` if `length_bytes` is zero. + /// @param length_bytes The number of bytes to copy. + /// @return The number of bytes copied. + /// CETL_NODISCARD virtual std::size_t copy(const std::size_t offset_bytes, void* const destination, const std::size_t length_bytes) const = 0; protected: - Interface() = default; - ~Interface() override = default; - Interface(Interface&&) noexcept = default; - Interface& operator=(Interface&&) noexcept = default; + IStorage() = default; + ~IStorage() override = default; + IStorage(IStorage&&) noexcept = default; + IStorage& operator=(IStorage&&) noexcept = default; + + }; // IStorage + + /// @brief Default constructor of empty buffer with no storage attached. + /// + /// `copy()` method will do no operation, and returns zero (as `size()` does). + /// + ScatteredBuffer() + : storage_{} + { + } - }; // Interface + // No copying, but move only! + ScatteredBuffer(const ScatteredBuffer& other) = delete; + ScatteredBuffer& operator=(const ScatteredBuffer& other) = delete; - ScatteredBuffer() = default; - ScatteredBuffer(const ScatteredBuffer& other) = delete; + /// @brief Moves other buffer into this new scattered buffer instance. + /// + /// The buffer is moved by moving the internal storage variant. + /// + /// @param other The other buffer to move into. + /// ScatteredBuffer(ScatteredBuffer&& other) noexcept { - storage_ = std::move(other.storage_); + storage_variant_ = std::move(other.storage_variant_); - other.interface_ = nullptr; - interface_ = cetl::any_cast(&storage_); + other.storage_ = nullptr; + storage_ = cetl::get_if(&storage_variant_); } - /// @brief Accepts a Lizard-specific implementation of `Interface` and moves it into the internal storage. + /// @brief Constructs buffer by accepting a Lizard-specific implementation of `IStorage` + /// and moving it into the internal storage variant. + /// + /// @tparam AnyStorage The type of the storage implementation. Should fit into \ref StorageVariantFootprint. + /// @param any_storage The storage to move into the buffer. /// - template ::value>> - explicit ScatteredBuffer(T&& source) noexcept - : storage_(std::forward(source)) - , interface_{cetl::any_cast(&storage_)} + template ::value>> + explicit ScatteredBuffer(AnyStorage&& any_storage) noexcept + : storage_variant_(std::forward(any_storage)) + , storage_{cetl::get_if(&storage_variant_)} { } @@ -70,21 +116,28 @@ class ScatteredBuffer final reset(); } - ScatteredBuffer& operator=(const ScatteredBuffer& other) = delete; + /// @brief Assigns other scattered buffer by moving it into the this one. + /// + /// @param other The other buffer to move into. + /// ScatteredBuffer& operator=(ScatteredBuffer&& other) noexcept { - storage_ = std::move(other.storage_); + storage_variant_ = std::move(other.storage_variant_); - other.interface_ = nullptr; - interface_ = cetl::any_cast(&storage_); + other.storage_ = nullptr; + storage_ = cetl::get_if(&storage_variant_); return *this; } + /// @brief Resets the buffer by releasing its internal source. + /// + /// Has similar effect as if moved away. Has no effect if the buffer is moved away already. + /// void reset() noexcept { - storage_.reset(); - interface_ = nullptr; + storage_variant_.reset(); + storage_ = nullptr; } /// @brief Gets the number of bytes stored in the buffer (possibly scattered, but this is hidden from the user). @@ -93,25 +146,30 @@ class ScatteredBuffer final /// CETL_NODISCARD std::size_t size() const noexcept { - return interface_ ? interface_->size() : 0; + return storage_ ? storage_->size() : 0; } /// @brief Copies a fragment of the specified size at the specified offset out of the buffer. /// - /// The request is truncated to prevent out-of-range memory access. - /// Returns the number of bytes copied. + /// The request `[offset, offset+length)` range is truncated to prevent out-of-range memory access. /// Does nothing and returns zero if the instance has been moved away. /// + /// @param offset_bytes The offset in bytes from the beginning of the buffer. + /// @param destination The pointer to the destination buffer. Should be at least `length_bytes` long. + /// Could be `nullptr` if `length_bytes` is zero. + /// @param length_bytes The number of bytes to copy. + /// @return The number of bytes copied. + /// CETL_NODISCARD std::size_t copy(const std::size_t offset_bytes, void* const destination, const std::size_t length_bytes) const { - return interface_ ? interface_->copy(offset_bytes, destination, length_bytes) : 0; + return storage_ ? storage_->copy(offset_bytes, destination, length_bytes) : 0; } private: - cetl::any storage_; - const Interface* interface_ = nullptr; + cetl::unbounded_variant storage_variant_; + const IStorage* storage_; }; // ScatteredBuffer diff --git a/include/libcyphal/transport/session.hpp b/include/libcyphal/transport/session.hpp index 7a94d4bfb..3cf618cae 100644 --- a/include/libcyphal/transport/session.hpp +++ b/include/libcyphal/transport/session.hpp @@ -31,7 +31,7 @@ class ITxSession : public ISession /// @brief Sets the timeout for a transmission. /// /// The value is added to the original transfer timestamp to determine its deadline. - /// Any transfer that exceeded this deadline would be dropped by the transport. + /// Any transfer that exceeded this deadline would be dropped. /// /// @param timeout - Positive duration for transmission timeout. Default value is 1 second. /// diff --git a/include/libcyphal/transport/transport.hpp b/include/libcyphal/transport/transport.hpp index 779eaa805..b3444724a 100644 --- a/include/libcyphal/transport/transport.hpp +++ b/include/libcyphal/transport/transport.hpp @@ -15,9 +15,20 @@ namespace libcyphal namespace transport { +/// @brief Interface for a transport layer. +/// class ITransport : public IRunnable { public: + /// @brief Gets the protocol parameters. + /// + /// @return Almost the same parameters as they were passed to the corresponding transport layer factory. + /// The only difference is that the `mtu_bytes` is calculated at run-time as current maximum for + /// all media interfaces (see f.e. `can::IMedia::getMtu` method). + /// + /// @see can::IMedia::getMtu() + /// @see udp::IMedia::getMtu() + /// CETL_NODISCARD virtual ProtocolParams getProtocolParams() const noexcept = 0; /// @brief Gets the local node ID (if any). diff --git a/include/libcyphal/transport/types.hpp b/include/libcyphal/transport/types.hpp index f171770e6..62a414834 100644 --- a/include/libcyphal/transport/types.hpp +++ b/include/libcyphal/transport/types.hpp @@ -20,12 +20,12 @@ namespace transport /// using NodeId = std::uint16_t; -/// @brief `PortId` is a 16-bit unsigned integer that represents a port (subject & service) in a Cyphal network. +/// @brief `PortId` is a 16-bit unsigned integer that represents a port (subject or service) in a Cyphal network. /// using PortId = std::uint16_t; -/// @brief `TransferId` is a 64-bit unsigned integer that represents a service transfer (request & response) -/// in a Cyphal network. +/// @brief TransferId is a 64-bit unsigned integer that represents a message +/// or service transfer (request & response) in a Cyphal network. /// using TransferId = std::uint64_t; @@ -58,7 +58,7 @@ struct TransferMetadata // AUTOSAR A11-0-2 exception: we just enhance the base metadata with additional information. struct MessageTransferMetadata final : TransferMetadata { - MessageTransferMetadata(const TransferMetadata& transfer_metadata, cetl::optional _publisher_node_id) + MessageTransferMetadata(const TransferMetadata& transfer_metadata, const cetl::optional& _publisher_node_id) : TransferMetadata{transfer_metadata} , publisher_node_id{_publisher_node_id} { @@ -70,6 +70,12 @@ struct MessageTransferMetadata final : TransferMetadata // AUTOSAR A11-0-2 exception: we just enhance the base metadata with additional information. struct ServiceTransferMetadata final : TransferMetadata { + ServiceTransferMetadata(const TransferMetadata& transfer_metadata, const NodeId _remote_node_id) + : TransferMetadata{transfer_metadata} + , remote_node_id{_remote_node_id} + { + } + NodeId remote_node_id; }; diff --git a/include/libcyphal/transport/udp/media.hpp b/include/libcyphal/transport/udp/media.hpp index f85899752..9f890517d 100644 --- a/include/libcyphal/transport/udp/media.hpp +++ b/include/libcyphal/transport/udp/media.hpp @@ -6,6 +6,8 @@ #ifndef LIBCYPHAL_TRANSPORT_UDP_MEDIA_HPP_INCLUDED #define LIBCYPHAL_TRANSPORT_UDP_MEDIA_HPP_INCLUDED +#include "libcyphal/types.hpp" + namespace libcyphal { namespace transport @@ -25,7 +27,12 @@ class IMedia IMedia& operator=(const IMedia&) = delete; IMedia& operator=(IMedia&&) noexcept = delete; - // TODO: Add methods here + /// @brief Get the maximum transmission unit (MTU) of the UDP 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 std::size_t getMtu() const noexcept = 0; protected: IMedia() = default; diff --git a/include/libcyphal/transport/udp/transport.hpp b/include/libcyphal/transport/udp/transport.hpp index 0b9078eeb..441719dc5 100644 --- a/include/libcyphal/transport/udp/transport.hpp +++ b/include/libcyphal/transport/udp/transport.hpp @@ -30,17 +30,20 @@ namespace detail class TransportImpl final : public IUdpTransport { - // In use to disable public construction. - // See https://seanmiddleditch.github.io/enabling-make-unique-with-private-constructors/ - struct Tag + /// @brief Defines specification for making interface unique ptr. + /// + struct Spec { - explicit Tag() = default; 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(Tag, + TransportImpl(Spec, cetl::pmr::memory_resource& memory, IMultiplexer& multiplexer, libcyphal::detail::VarArray&& media_array, diff --git a/include/libcyphal/types.hpp b/include/libcyphal/types.hpp index fae42e2d0..56bca75f7 100644 --- a/include/libcyphal/types.hpp +++ b/include/libcyphal/types.hpp @@ -65,14 +65,14 @@ using PmrAllocator = cetl::pmr::polymorphic_allocator; template using VarArray = cetl::VariableLengthArray>; -template -CETL_NODISCARD UniquePtr makeUniquePtr(cetl::pmr::memory_resource& memory, Args&&... args) +template +CETL_NODISCARD UniquePtr makeUniquePtr(cetl::pmr::memory_resource& memory, Args&&... args) { - PmrAllocator allocator{&memory}; - auto interface_deleter = typename UniquePtr::deleter_type{allocator, 1}; + PmrAllocator allocator{&memory}; + auto interface_deleter = typename UniquePtr::deleter_type{allocator, 1}; auto concrete = cetl::pmr::Factory::make_unique(allocator, std::forward(args)...); - auto interface = UniquePtr{concrete.release(), interface_deleter}; + auto interface = UniquePtr{concrete.release(), interface_deleter}; return interface; } diff --git a/test/unittest/gtest_helpers.hpp b/test/unittest/gtest_helpers.hpp index 3b8cb7cc4..9ea90cbf6 100644 --- a/test/unittest/gtest_helpers.hpp +++ b/test/unittest/gtest_helpers.hpp @@ -67,8 +67,8 @@ namespace libcyphal inline void PrintTo(const Duration duration, std::ostream* os) { - auto locale = os->imbue(std::locale("en_US")); - *os << std::chrono::duration_cast(duration).count() << "_us"; + auto locale = os->imbue(std::locale("")); + *os << std::chrono::duration_cast(duration).count() << " us"; os->imbue(locale); } @@ -216,6 +216,42 @@ inline testing::Matcher SubjectOfCanIdEq(PortId subject_id) return SubjectOfCanIdMatcher(subject_id); } +class ServiceOfCanIdMatcher +{ +public: + using is_gtest_matcher = void; + + explicit ServiceOfCanIdMatcher(PortId service_id) + : service_id_{service_id} + { + } + + bool MatchAndExplain(const CanId& can_id, std::ostream* os) const + { + const auto service_id = (can_id >> 14) & CANARD_SERVICE_ID_MAX; + if (os) + { + *os << "service_id=" << service_id; + } + return service_id == service_id_; + } + void DescribeTo(std::ostream* os) const + { + *os << "service_id==" << service_id_; + } + void DescribeNegationTo(std::ostream* os) const + { + *os << "service_id!=" << service_id_; + } + +private: + const PortId service_id_; +}; +inline testing::Matcher ServiceOfCanIdEq(PortId service_id) +{ + return ServiceOfCanIdMatcher(service_id); +} + class SourceNodeCanIdMatcher { public: @@ -252,6 +288,42 @@ inline testing::Matcher SourceNodeOfCanIdEq(NodeId node_id) return SourceNodeCanIdMatcher(node_id); } +class DestinationNodeCanIdMatcher +{ +public: + using is_gtest_matcher = void; + + explicit DestinationNodeCanIdMatcher(NodeId node_id) + : node_id_{node_id} + { + } + + bool MatchAndExplain(const CanId& can_id, std::ostream* os) const + { + const auto node_id = (can_id >> 7) & CANARD_NODE_ID_MAX; + if (os) + { + *os << "node_id=" << node_id; + } + return node_id == node_id_; + } + void DescribeTo(std::ostream* os) const + { + *os << "node_id==" << node_id_; + } + void DescribeNegationTo(std::ostream* os) const + { + *os << "node_id!=" << node_id_; + } + +private: + const NodeId node_id_; +}; +inline testing::Matcher DestinationNodeOfCanIdEq(NodeId node_id) +{ + return DestinationNodeCanIdMatcher(node_id); +} + class TailByteMatcher { public: diff --git a/test/unittest/transport/can/test_can_delegate.cpp b/test/unittest/transport/can/test_can_delegate.cpp index 65334573f..134f8a268 100644 --- a/test/unittest/transport/can/test_can_delegate.cpp +++ b/test/unittest/transport/can/test_can_delegate.cpp @@ -41,10 +41,9 @@ class TestCanDelegate : public testing::Test MOCK_METHOD((cetl::optional), sendTransfer, - (const CanardMicrosecond deadline, - const CanardTransferMetadata& metadata, - const void* const payload, - const std::size_t payload_size)); + (const libcyphal::TimePoint deadline, + const CanardTransferMetadata& metadata, + const PayloadFragments payload_fragments)); }; void TearDown() override diff --git a/test/unittest/transport/can/test_can_msg_rx_session.cpp b/test/unittest/transport/can/test_can_msg_rx_session.cpp index ed0223b0d..eee3a440f 100644 --- a/test/unittest/transport/can/test_can_msg_rx_session.cpp +++ b/test/unittest/transport/can/test_can_msg_rx_session.cpp @@ -81,9 +81,8 @@ TEST_F(TestCanMsgRxSession, make_setTransferIdTimeout) auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageRxSession({42, 123}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); EXPECT_THAT(session->getParams().extent_bytes, 42); EXPECT_THAT(session->getParams().subject_id, 123); @@ -106,26 +105,35 @@ TEST_F(TestCanMsgRxSession, make_no_memory) EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); } +TEST_F(TestCanMsgRxSession, make_fails_due_to_argument_error) +{ + auto transport = makeTransport(mr_); + + // Try invalid subject id + auto maybe_session = transport->makeMessageRxSession({64, CANARD_SUBJECT_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + TEST_F(TestCanMsgRxSession, run_and_receive) { auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageRxSession({4, 0x23}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); - // 1-st iteration: one frame available @ 1s { + 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 payload) { - EXPECT_THAT(payload.size(), CANARD_MTU_MAX); - - payload[0] = b(42); - payload[1] = b(147); - payload[2] = b(0xED); + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + p[0] = b('0'); + p[1] = b('1'); + p[2] = b(0b111'01101); return RxMetadata{rx_timestamp, 0x0C'60'23'45, 3}; }); @@ -141,18 +149,20 @@ TEST_F(TestCanMsgRxSession, run_and_receive) EXPECT_THAT(rx_transfer.metadata.priority, Priority::High); EXPECT_THAT(rx_transfer.metadata.publisher_node_id, Optional(0x45)); - 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)); + 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')); } - - // 2-nd iteration: no frames available { + 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(payload.size(), CANARD_MTU_MAX); + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); return cetl::nullopt; }); @@ -162,18 +172,18 @@ TEST_F(TestCanMsgRxSession, run_and_receive) const auto maybe_rx_transfer = session->receive(); EXPECT_THAT(maybe_rx_transfer, Eq(cetl::nullopt)); } - - // 3-rd iteration: one anonymous frame available @ 3s { + SCOPED_TRACE("3-rd iteration: one anonymous frame available @ 3s"); + scheduler_.setNow(TimePoint{3s}); const auto rx_timestamp = now(); - EXPECT_CALL(media_mock_, pop(_)).WillOnce([=](auto payload) { - EXPECT_THAT(payload.size(), CANARD_MTU_MAX); - - payload[0] = b(42); - payload[1] = b(147); - payload[2] = b(0xEE); + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + p[0] = b('1'); + p[1] = b('2'); + p[2] = b(0b111'01110); return RxMetadata{rx_timestamp, 0x01'60'23'13, 3}; }); @@ -189,10 +199,10 @@ TEST_F(TestCanMsgRxSession, run_and_receive) EXPECT_THAT(rx_transfer.metadata.priority, Priority::Exceptional); EXPECT_THAT(rx_transfer.metadata.publisher_node_id, Eq(cetl::nullopt)); - 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)); + 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')); } } diff --git a/test/unittest/transport/can/test_can_msg_tx_session.cpp b/test/unittest/transport/can/test_can_msg_tx_session.cpp index d3cd17660..79fb71ee0 100644 --- a/test/unittest/transport/can/test_can_msg_tx_session.cpp +++ b/test/unittest/transport/can/test_can_msg_tx_session.cpp @@ -59,10 +59,9 @@ class TestCanMsgTxSession : public testing::Test } CETL_NODISCARD UniquePtr makeTransport(cetl::pmr::memory_resource& mr, - IMedia* extra_media = nullptr, const std::size_t tx_capacity = 16) { - std::array media_array{&media_mock_, extra_media}; + std::array media_array{&media_mock_}; auto maybe_transport = can::makeTransport(mr, mux_mock_, media_array, tx_capacity, {}); EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); @@ -84,9 +83,8 @@ TEST_F(TestCanMsgTxSession, make) auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageTxSession({123}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); EXPECT_THAT(session->getParams().subject_id, 123); } @@ -105,14 +103,22 @@ TEST_F(TestCanMsgTxSession, make_no_memory) EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); } +TEST_F(TestCanMsgTxSession, make_fails_due_to_argument_error) +{ + auto transport = makeTransport(mr_); + + // Try invalid subject id + auto maybe_session = transport->makeMessageTxSession({CANARD_SUBJECT_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + TEST_F(TestCanMsgTxSession, send_empty_payload_and_no_transport_run) { auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageTxSession({123}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); const PayloadFragments empty_payload{}; const TransferMetadata metadata{0x1AF52, {}, Priority::Low}; @@ -134,9 +140,8 @@ TEST_F(TestCanMsgTxSession, send_empty_payload) auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageTxSession({123}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); scheduler_.runNow(+10s); const auto send_time = now(); @@ -170,9 +175,8 @@ TEST_F(TestCanMsgTxSession, send_empty_expired_payload) auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageTxSession({123}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); scheduler_.runNow(+10s); const auto send_time = now(); @@ -198,9 +202,8 @@ TEST_F(TestCanMsgTxSession, send_7bytes_payload_with_500ms_timeout) auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageTxSession({17}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); const auto timeout = 500ms; session->setSendTimeout(timeout); @@ -246,9 +249,8 @@ TEST_F(TestCanMsgTxSession, send_when_no_memory_for_contiguous_payload) EXPECT_CALL(mr_mock, do_allocate(sizeof(payload1) + sizeof(payload2), _)).WillOnce(Return(nullptr)); auto maybe_session = transport->makeMessageTxSession({17}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); scheduler_.runNow(+10s); const auto send_time = now(); diff --git a/test/unittest/transport/can/test_can_svc_rx_sessions.cpp b/test/unittest/transport/can/test_can_svc_rx_sessions.cpp new file mode 100644 index 000000000..2d3a15402 --- /dev/null +++ b/test/unittest/transport/can/test_can_svc_rx_sessions.cpp @@ -0,0 +1,242 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include + +#include "media_mock.hpp" +#include "../multiplexer_mock.hpp" +#include "../../gtest_helpers.hpp" +#include "../../test_utilities.hpp" +#include "../../memory_resource_mock.hpp" +#include "../../virtual_time_scheduler.hpp" +#include "../../tracking_memory_resource.hpp" + +#include + +namespace +{ +using byte = cetl::byte; + +using namespace libcyphal; +using namespace libcyphal::transport; +using namespace libcyphal::transport::can; +using namespace libcyphal::test_utilities; + +using testing::_; +using testing::Eq; +using testing::Return; +using testing::IsNull; +using testing::IsEmpty; +using testing::NotNull; +using testing::Optional; +using testing::InSequence; +using testing::StrictMock; +using testing::ElementsAre; +using testing::VariantWith; + +using namespace std::chrono_literals; + +class TestCanSvcRxSessions : public testing::Test +{ +protected: + void SetUp() override + { + EXPECT_CALL(media_mock_, getMtu()).WillRepeatedly(Return(CANARD_MTU_CAN_CLASSIC)); + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + // TODO: Uncomment this when PMR deleter is fixed. + // EXPECT_EQ(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + TimePoint now() const + { + return scheduler_.now(); + } + + CETL_NODISCARD UniquePtr makeTransport(cetl::pmr::memory_resource& mr, const NodeId local_node_id) + { + std::array media_array{&media_mock_}; + + // TODO: `local_node_id` could be just passed to `can::makeTransport` as an argument, + // but it's not possible due to CETL issue https://github.com/OpenCyphal/CETL/issues/119. + const auto opt_local_node_id = cetl::optional{local_node_id}; + + auto maybe_transport = can::makeTransport(mr, mux_mock_, media_array, 0, opt_local_node_id); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + return cetl::get>(std::move(maybe_transport)); + } + + // MARK: Data members: + + VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock media_mock_{}; + StrictMock mux_mock_{}; +}; + +// MARK: Tests: + +TEST_F(TestCanSvcRxSessions, 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(TestCanSvcRxSessions, 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(can::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(TestCanSvcRxSessions, make_request_fails_due_to_argument_error) +{ + auto transport = makeTransport(mr_, 0x31); + + // Try invalid subject id + auto maybe_session = transport->makeRequestRxSession({64, CANARD_SERVICE_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestCanSvcRxSessions, 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(), CANARD_MTU_MAX); + 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}; + }); + + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { session->run(now()); }); + + const auto maybe_rx_transfer = session->receive(); + ASSERT_THAT(maybe_rx_transfer, Optional(_)); + 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(), CANARD_MTU_MAX); + return cetl::nullopt; + }); + + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { session->run(now()); }); + + const auto maybe_rx_transfer = session->receive(); + EXPECT_THAT(maybe_rx_transfer, Eq(cetl::nullopt)); + } + { + SCOPED_TRACE("3-rd iteration: 2 frames available @ 3s"); + + scheduler_.setNow(TimePoint{3s}); + const auto rx_timestamp = now(); + { + InSequence seq; + + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 10ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + 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_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx_timestamp + 30ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + 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, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { session->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { session->run(now()); }); + + const auto maybe_rx_transfer = session->receive(); + ASSERT_THAT(maybe_rx_transfer, Optional(_)); + 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')); + } +} + +} // namespace diff --git a/test/unittest/transport/can/test_can_svc_tx_sessions.cpp b/test/unittest/transport/can/test_can_svc_tx_sessions.cpp new file mode 100644 index 000000000..b1b5e62a1 --- /dev/null +++ b/test/unittest/transport/can/test_can_svc_tx_sessions.cpp @@ -0,0 +1,343 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include + +#include "media_mock.hpp" +#include "../multiplexer_mock.hpp" +#include "../../gtest_helpers.hpp" +#include "../../test_utilities.hpp" +#include "../../memory_resource_mock.hpp" +#include "../../virtual_time_scheduler.hpp" +#include "../../tracking_memory_resource.hpp" + +#include + +namespace +{ +using byte = cetl::byte; + +using namespace libcyphal; +using namespace libcyphal::transport; +using namespace libcyphal::transport::can; +using namespace libcyphal::test_utilities; + +using testing::_; +using testing::Eq; +using testing::Return; +using testing::IsNull; +using testing::IsEmpty; +using testing::NotNull; +using testing::Optional; +using testing::InSequence; +using testing::StrictMock; +using testing::ElementsAre; +using testing::VariantWith; + +using namespace std::chrono_literals; + +class TestCanSvcTxSessions : public testing::Test +{ +protected: + void SetUp() override + { + EXPECT_CALL(media_mock_, getMtu()).WillRepeatedly(Return(CANARD_MTU_CAN_CLASSIC)); + } + + void TearDown() override + { + EXPECT_THAT(mr_.allocations, IsEmpty()); + // TODO: Uncomment this when PMR deleter is fixed. + // EXPECT_EQ(mr_.total_allocated_bytes, mr_.total_deallocated_bytes); + } + + TimePoint now() const + { + return scheduler_.now(); + } + + CETL_NODISCARD UniquePtr makeTransport(cetl::pmr::memory_resource& mr, + const NodeId local_node_id, + const std::size_t tx_capacity = 16) + { + std::array media_array{&media_mock_}; + + // TODO: `local_node_id` could be just passed to `can::makeTransport` as an argument, + // but it's not possible due to CETL issue https://github.com/OpenCyphal/CETL/issues/119. + const auto opt_local_node_id = cetl::optional{local_node_id}; + + auto maybe_transport = can::makeTransport(mr, mux_mock_, media_array, tx_capacity, opt_local_node_id); + EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + return cetl::get>(std::move(maybe_transport)); + } + + // MARK: Data members: + + VirtualTimeScheduler scheduler_{}; + TrackingMemoryResource mr_; + StrictMock mux_mock_{}; + StrictMock media_mock_{}; +}; + +// MARK: Tests: + +TEST_F(TestCanSvcTxSessions, make_request_session) +{ + auto transport = makeTransport(mr_, 0); + + auto maybe_session = transport->makeRequestTxSession({123, CANARD_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, CANARD_NODE_ID_MAX); + + session->run(now()); +} + +TEST_F(TestCanSvcTxSessions, make_request_fails_due_to_argument_error) +{ + auto transport = makeTransport(mr_, 0); + + // Try invalid service id + { + auto maybe_session = transport->makeRequestTxSession({CANARD_SERVICE_ID_MAX + 1, 0}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); + } + + // Try invalid server node id + { + auto maybe_session = transport->makeRequestTxSession({0, CANARD_NODE_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); + } +} + +TEST_F(TestCanSvcTxSessions, 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(can::detail::SvcRequestTxSession), _)).WillOnce(Return(nullptr)); + + auto transport = makeTransport(mr_mock, CANARD_NODE_ID_MAX); + + auto maybe_session = transport->makeRequestTxSession({0x23, 0}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestCanSvcTxSessions, send_request) +{ + EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + + auto transport = makeTransport(mr_, 13); + + auto maybe_session = transport->makeRequestTxSession({123, 31}); + 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(media_mock_, push(_, _, _)).WillOnce([&](auto deadline, auto can_id, auto payload) { + EXPECT_THAT(now(), send_time + 10ms); + EXPECT_THAT(deadline, send_time + timeout); + EXPECT_THAT(can_id, ServiceOfCanIdEq(123)); + EXPECT_THAT(can_id, AllOf(SourceNodeOfCanIdEq(13), DestinationNodeOfCanIdEq(31))); + EXPECT_THAT(can_id, AllOf(PriorityOfCanIdEq(metadata.priority), IsServiceCanId())); + + auto tbm = TailByteEq(metadata.transfer_id); + EXPECT_THAT(payload, ElementsAre(tbm)); + return true; + }); + + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); +} + +TEST_F(TestCanSvcTxSessions, send_request_with_argument_error) +{ + EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + + // Make initially anonymous node transport. + // + std::array media_array{&media_mock_}; + auto maybe_transport = can::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, 31}); + 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, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + } + + // 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(media_mock_, push(_, _, _)).WillOnce([&](auto deadline, auto can_id, auto payload) { + EXPECT_THAT(now(), send_time + 10ms); + EXPECT_THAT(deadline, transfer_time + timeout); + EXPECT_THAT(can_id, ServiceOfCanIdEq(123)); + EXPECT_THAT(can_id, AllOf(SourceNodeOfCanIdEq(13), DestinationNodeOfCanIdEq(31))); + EXPECT_THAT(can_id, AllOf(PriorityOfCanIdEq(metadata.priority), IsServiceCanId())); + + auto tbm = TailByteEq(metadata.transfer_id); + EXPECT_THAT(payload, ElementsAre(tbm)); + return true; + }); + + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + } +} + +TEST_F(TestCanSvcTxSessions, make_response_session) +{ + auto transport = makeTransport(mr_, CANARD_NODE_ID_MAX, 2); + + 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); + + session->run(now()); +} + +TEST_F(TestCanSvcTxSessions, make_response_fails_due_to_argument_error) +{ + auto transport = makeTransport(mr_, 0); + + // Try invalid service id + auto maybe_session = transport->makeResponseTxSession({CANARD_SERVICE_ID_MAX + 1}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestCanSvcTxSessions, 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(can::detail::SvcRequestTxSession), _)).WillOnce(Return(nullptr)); + + auto transport = makeTransport(mr_mock, CANARD_NODE_ID_MAX); + + auto maybe_session = transport->makeResponseTxSession({0x23}); + EXPECT_THAT(maybe_session, VariantWith(VariantWith(_))); +} + +TEST_F(TestCanSvcTxSessions, send_respose) +{ + EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + + auto transport = makeTransport(mr_, 31); + + 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}, 13}; + + auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Eq(cetl::nullopt)); + + EXPECT_CALL(media_mock_, push(_, _, _)).WillOnce([&](auto deadline, auto can_id, auto payload) { + EXPECT_THAT(now(), send_time + 10ms); + EXPECT_THAT(deadline, send_time + timeout); + EXPECT_THAT(can_id, ServiceOfCanIdEq(123)); + EXPECT_THAT(can_id, AllOf(SourceNodeOfCanIdEq(31), DestinationNodeOfCanIdEq(13))); + EXPECT_THAT(can_id, AllOf(PriorityOfCanIdEq(metadata.priority), IsServiceCanId())); + + auto tbm = TailByteEq(metadata.transfer_id); + EXPECT_THAT(payload, ElementsAre(tbm)); + return true; + }); + + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); +} + +TEST_F(TestCanSvcTxSessions, send_respose_with_argument_error) +{ + EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + + // Make initially anonymous node transport. + // + std::array media_array{&media_mock_}; + auto maybe_transport = can::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, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + } + + // Fix anonymous node, but break remote node id. + { + scheduler_.setNow(TimePoint{200ms}); + + EXPECT_THAT(transport->setLocalNodeId(31), Eq(cetl::nullopt)); + metadata.remote_node_id = CANARD_NODE_ID_MAX + 1; + const auto maybe_error = session->send(metadata, empty_payload); + EXPECT_THAT(maybe_error, Optional(VariantWith(_))); + + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + } +} + +} // namespace diff --git a/test/unittest/transport/can/test_can_transport.cpp b/test/unittest/transport/can/test_can_transport.cpp index 1d41a94ac..08cd7b4e0 100644 --- a/test/unittest/transport/can/test_can_transport.cpp +++ b/test/unittest/transport/can/test_can_transport.cpp @@ -32,6 +32,7 @@ using testing::NotNull; using testing::Optional; using testing::InSequence; using testing::StrictMock; +using testing::ElementsAre; using testing::VariantWith; using namespace std::chrono_literals; @@ -122,7 +123,7 @@ TEST_F(TestCanTransport, makeTransport_getLocalNodeId) { std::array media_array{&media_mock_}; auto maybe_transport = can::makeTransport(mr_, mux_mock_, media_array, 0, {}); - EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); auto transport = cetl::get>(std::move(maybe_transport)); EXPECT_THAT(transport->getLocalNodeId(), Eq(cetl::nullopt)); @@ -134,7 +135,7 @@ TEST_F(TestCanTransport, makeTransport_getLocalNodeId) std::array media_array{&media_mock_}; auto maybe_transport = can::makeTransport(mr_, mux_mock_, media_array, 0, node_id); - EXPECT_THAT(maybe_transport, VariantWith>(NotNull())); + ASSERT_THAT(maybe_transport, VariantWith>(NotNull())); auto transport = cetl::get>(std::move(maybe_transport)); EXPECT_THAT(transport->getLocalNodeId(), Optional(42)); @@ -252,7 +253,7 @@ TEST_F(TestCanTransport, makeMessageRxSession) auto transport = makeTransport(mr_); auto maybe_rx_session = transport->makeMessageRxSession({42, 123}); - EXPECT_THAT(maybe_rx_session, VariantWith>(NotNull())); + ASSERT_THAT(maybe_rx_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_rx_session)); EXPECT_THAT(session->getParams().extent_bytes, 42); @@ -274,18 +275,44 @@ TEST_F(TestCanTransport, makeMessageRxSession_invalid_resubscription) const PortId test_subject_id = 111; auto maybe_rx_session1 = transport->makeMessageRxSession({0, test_subject_id}); - EXPECT_THAT(maybe_rx_session1, VariantWith>(NotNull())); + ASSERT_THAT(maybe_rx_session1, VariantWith>(NotNull())); auto maybe_rx_session2 = transport->makeMessageRxSession({0, test_subject_id}); EXPECT_THAT(maybe_rx_session2, VariantWith(VariantWith(_))); } +TEST_F(TestCanTransport, makeRequestRxSession_invalid_resubscription) +{ + auto transport = makeTransport(mr_); + + const PortId test_subject_id = 111; + + auto maybe_rx_session1 = transport->makeRequestRxSession({0, test_subject_id}); + ASSERT_THAT(maybe_rx_session1, VariantWith>(NotNull())); + + auto maybe_rx_session2 = transport->makeRequestRxSession({0, test_subject_id}); + EXPECT_THAT(maybe_rx_session2, VariantWith(VariantWith(_))); +} + +TEST_F(TestCanTransport, makeResponseRxSession_invalid_resubscription) +{ + auto transport = makeTransport(mr_); + + const PortId test_subject_id = 111; + + auto maybe_rx_session1 = transport->makeResponseRxSession({0, test_subject_id, 0x31}); + ASSERT_THAT(maybe_rx_session1, VariantWith>(NotNull())); + + auto maybe_rx_session2 = transport->makeResponseRxSession({0, test_subject_id, 0x31}); + EXPECT_THAT(maybe_rx_session2, VariantWith(VariantWith(_))); +} + TEST_F(TestCanTransport, makeMessageTxSession) { auto transport = makeTransport(mr_); auto maybe_tx_session = transport->makeMessageTxSession({123}); - EXPECT_THAT(maybe_tx_session, VariantWith>(NotNull())); + ASSERT_THAT(maybe_tx_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_tx_session)); EXPECT_THAT(session->getParams().subject_id, 123); @@ -298,9 +325,8 @@ TEST_F(TestCanTransport, sending_multiframe_payload_should_fail_for_anonymous) auto transport = makeTransport(mr_); auto maybe_session = transport->makeMessageTxSession({7}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); scheduler_.runNow(+10s); const auto send_time = now(); @@ -323,9 +349,8 @@ TEST_F(TestCanTransport, sending_multiframe_payload_for_non_anonymous) EXPECT_THAT(transport->setLocalNodeId(0x45), Eq(cetl::nullopt)); auto maybe_session = transport->makeMessageTxSession({7}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); scheduler_.runNow(+10s); const auto timeout = 1s; @@ -377,9 +402,8 @@ TEST_F(TestCanTransport, send_multiframe_payload_to_redundant_not_ready_media) EXPECT_THAT(transport->setLocalNodeId(0x45), Eq(cetl::nullopt)); auto maybe_session = transport->makeMessageTxSession({7}); - EXPECT_THAT(maybe_session, VariantWith>(_)); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); auto session = cetl::get>(std::move(maybe_session)); - EXPECT_THAT(session, NotNull()); scheduler_.runNow(+10s); const auto timeout = 1s; @@ -412,7 +436,7 @@ TEST_F(TestCanTransport, send_multiframe_payload_to_redundant_not_ready_media) EXPECT_THAT(can_id, AllOf(PriorityOfCanIdEq(metadata.priority), IsMessageCanId())) << ctx; auto tbm = TailByteEq(metadata.transfer_id, false, true, false); - EXPECT_THAT(payload, ElementsAre(b('7'), b('8'), b('9'), _, _ /* CRC bytes */, tbm)) << ctx; + EXPECT_THAT(payload, ElementsAre(b('7'), b('8'), b('9'), b(0x7D), b(0x61) /* CRC bytes */, tbm)) << ctx; return true; }); }; @@ -433,4 +457,106 @@ TEST_F(TestCanTransport, send_multiframe_payload_to_redundant_not_ready_media) scheduler_.runNow(+10us, [&] { transport->run(now()); }); } +TEST_F(TestCanTransport, run_and_receive_svc_responses_from_redundant_media) +{ + StrictMock media_mock2{}; + EXPECT_CALL(media_mock_, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + EXPECT_CALL(media_mock2, pop(_)).WillRepeatedly(Return(cetl::nullopt)); + EXPECT_CALL(media_mock2, getMtu()).WillRepeatedly(Return(CANARD_MTU_CAN_CLASSIC)); + + auto transport = makeTransport(mr_, &media_mock2); + EXPECT_THAT(transport->setLocalNodeId(0x13), Eq(cetl::nullopt)); + + auto maybe_session = transport->makeResponseRxSession({64, 0x17B, 0x31}); + ASSERT_THAT(maybe_session, VariantWith>(NotNull())); + auto session = cetl::get>(std::move(maybe_session)); + + const auto params = session->getParams(); + EXPECT_THAT(params.extent_bytes, 64); + EXPECT_THAT(params.service_id, 0x17B); + EXPECT_THAT(params.server_node_id, 0x31); + + const auto timeout = 200ms; + session->setTransferIdTimeout(timeout); + + const auto epoch = TimePoint{10s}; + scheduler_.setNow(epoch); + const auto rx1_timestamp = epoch; + const auto rx2_timestamp = epoch + 2 * timeout; + { + InSequence seq; + + // 1. Emulate that only one 1st frame came from the 1st media interface (@ rx1_timestamp+10ms)... + // + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx1_timestamp + 10ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + 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'11101); + return RxMetadata{rx1_timestamp, 0b111'1'0'0'101111011'0010011'0110001, 8}; + }); + EXPECT_CALL(media_mock2, pop(_)).WillOnce([&](auto) { + EXPECT_THAT(now(), rx1_timestamp + 10ms); + return cetl::nullopt; + }); + // 2. And then 2nd media delivered all frames ones again after timeout (@ rx2_timestamp+10ms). + // + EXPECT_CALL(media_mock_, pop(_)).WillOnce([&](auto) { + EXPECT_THAT(now(), rx2_timestamp + 10ms); + return cetl::nullopt; + }); + EXPECT_CALL(media_mock2, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx2_timestamp + 10ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + 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{rx2_timestamp, 0b111'1'0'0'101111011'0010011'0110001, 8}; + }); + EXPECT_CALL(media_mock2, pop(_)).WillOnce([&](auto p) { + EXPECT_THAT(now(), rx2_timestamp + 30ms); + EXPECT_THAT(p.size(), CANARD_MTU_MAX); + 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{rx2_timestamp + 1ms, 0b111'1'0'0'101111011'0010011'0110001, 6}; + }); + } + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { session->run(now()); }); + scheduler_.setNow(rx2_timestamp); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { session->run(now()); }); + scheduler_.runNow(+10ms, [&] { transport->run(now()); }); + scheduler_.runNow(+10ms, [&] { session->run(now()); }); + + const auto maybe_rx_transfer = session->receive(); + ASSERT_THAT(maybe_rx_transfer, Optional(_)); + const auto& rx_transfer = maybe_rx_transfer.value(); + + EXPECT_THAT(rx_transfer.metadata.timestamp, rx2_timestamp); + EXPECT_THAT(rx_transfer.metadata.transfer_id, 0x1E); + EXPECT_THAT(rx_transfer.metadata.priority, Priority::Optional); + EXPECT_THAT(rx_transfer.metadata.remote_node_id, 0x31); + + 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', '8', '9')); +} + } // namespace diff --git a/test/unittest/transport/test_scattered_buffer.cpp b/test/unittest/transport/test_scattered_buffer.cpp index 10a05cd96..3e289195b 100644 --- a/test/unittest/transport/test_scattered_buffer.cpp +++ b/test/unittest/transport/test_scattered_buffer.cpp @@ -18,10 +18,10 @@ using testing::Return; using testing::StrictMock; // Just random id: 277C3545-564C-4617-993D-27B1043ECEBA -using TestTypeIdType = +using StorageWrapperTypeIdType = cetl::type_id_type<0x27, 0x7C, 0x35, 0x45, 0x56, 0x4C, 0x46, 0x17, 0x99, 0x3D, 0x27, 0xB1, 0x04, 0x3E, 0xCE, 0xBA>; -class InterfaceMock : public ScatteredBuffer::Interface +class StorageMock : public ScatteredBuffer::IStorage { public: MOCK_METHOD(void, moved, ()); @@ -30,23 +30,23 @@ class InterfaceMock : public ScatteredBuffer::Interface MOCK_METHOD(std::size_t, size, (), (const, noexcept, override)); MOCK_METHOD(std::size_t, copy, (const std::size_t, void* const, const std::size_t), (const, override)); }; -class InterfaceWrapper final : public rtti_helper +class StorageWrapper final : public rtti_helper { public: - explicit InterfaceWrapper(InterfaceMock* mock) + explicit StorageWrapper(StorageMock* mock) : mock_{mock} { } - InterfaceWrapper(InterfaceWrapper&& other) noexcept + StorageWrapper(StorageWrapper&& other) noexcept { move_from(std::move(other)); } - InterfaceWrapper& operator=(InterfaceWrapper&& other) noexcept + StorageWrapper& operator=(StorageWrapper&& other) noexcept { move_from(std::move(other)); return *this; } - ~InterfaceWrapper() override + ~StorageWrapper() final { if (mock_ != nullptr) { @@ -55,23 +55,23 @@ class InterfaceWrapper final : public rtti_helpersize() : 0; } - CETL_NODISCARD std::size_t copy(const std::size_t offset_bytes, - void* const destination, - const std::size_t length_bytes) const override + std::size_t copy(const std::size_t offset_bytes, + void* const destination, + const std::size_t length_bytes) const final { return mock_ ? mock_->copy(offset_bytes, destination, length_bytes) : 0; } private: - InterfaceMock* mock_ = nullptr; + StorageMock* mock_ = nullptr; - void move_from(InterfaceWrapper&& other) + void move_from(StorageWrapper&& other) { mock_ = other.mock_; other.mock_ = nullptr; @@ -82,18 +82,18 @@ class InterfaceWrapper final : public rtti_helper interface_mock{}; - EXPECT_CALL(interface_mock, deinit()).Times(1); - EXPECT_CALL(interface_mock, moved()).Times(1 + 2 + 2); - EXPECT_CALL(interface_mock, size()).Times(3).WillRepeatedly(Return(42)); + StrictMock storage_mock{}; + EXPECT_CALL(storage_mock, deinit()).Times(1); + EXPECT_CALL(storage_mock, moved()).Times(1 + 2 + 2); + EXPECT_CALL(storage_mock, size()).Times(3).WillRepeatedly(Return(42)); { - ScatteredBuffer src{InterfaceWrapper{&interface_mock}}; //< +1 move + ScatteredBuffer src{StorageWrapper{&storage_mock}}; //< +1 move EXPECT_THAT(src.size(), 42); ScatteredBuffer dst{std::move(src)}; //< +2 moves b/c of `cetl::any` specifics (via swap with tmp) @@ -110,12 +110,12 @@ TEST(TestScatteredBuffer, copy_reset) { std::array test_dst{}; - StrictMock interface_mock{}; - EXPECT_CALL(interface_mock, deinit()).Times(1); - EXPECT_CALL(interface_mock, moved()).Times(1); - EXPECT_CALL(interface_mock, copy(13, test_dst.data(), test_dst.size())).WillOnce(Return(7)); + StrictMock storage_mock{}; + EXPECT_CALL(storage_mock, deinit()).Times(1); + EXPECT_CALL(storage_mock, moved()).Times(1); + EXPECT_CALL(storage_mock, copy(13, test_dst.data(), test_dst.size())).WillOnce(Return(7)); { - ScatteredBuffer buffer{InterfaceWrapper{&interface_mock}}; + ScatteredBuffer buffer{StorageWrapper{&storage_mock}}; auto copied_bytes = buffer.copy(13, test_dst.data(), test_dst.size()); EXPECT_THAT(copied_bytes, 7); diff --git a/test/unittest/transport/udp/media_mock.hpp b/test/unittest/transport/udp/media_mock.hpp index 2a981463e..686ce8b68 100644 --- a/test/unittest/transport/udp/media_mock.hpp +++ b/test/unittest/transport/udp/media_mock.hpp @@ -23,6 +23,8 @@ class MediaMock : public IMedia MediaMock() = default; virtual ~MediaMock() = default; + MOCK_METHOD(std::size_t, getMtu, (), (const, noexcept, override)); + }; // MediaMock } // namespace udp