From ee5f1e236f5e4627de2798339982214b96a3ad34 Mon Sep 17 00:00:00 2001 From: Sergei Date: Thu, 25 Apr 2024 16:38:06 +0300 Subject: [PATCH] Issue/336 transport iterfaces (#343) First draft of transport interfaces Added: - `transport::ISession` - `transport::IMessage[Rx|Ts]Session` - `transport::I[Request|Response][Rx|Ts]Session` Also: - add "std: [14, 17, 20]" to the build matrix - add hash tag triggering by #verification #docs tags - strip repo absolute path prefix from doxygen file paths --------- Co-authored-by: Sergei Shirokov --- .clang-format | 1 - .github/workflows/tests.yml | 76 ++++--- CONTRIBUTING.md | 22 ++ cmake/modules/Findcetl.cmake | 2 +- docs/CMakeLists.txt | 3 +- docs/design/readme.md | 48 ++-- docs/doxygen.ini | 2 +- docs/examples/CMakeLists.txt | 6 + docs/examples/example_01_hello_world.cpp | 2 +- include/libcyphal/libcyphal.hpp | 16 -- include/libcyphal/runnable.hpp | 31 +++ include/libcyphal/transport/can/media.hpp | 88 ++++++++ include/libcyphal/transport/can/transport.hpp | 212 ++++++++++++++++++ include/libcyphal/transport/defines.hpp | 90 ++++++++ include/libcyphal/transport/errors.hpp | 56 +++++ include/libcyphal/transport/msg_sessions.hpp | 59 +++++ include/libcyphal/transport/multiplexer.hpp | 33 +++ .../libcyphal/transport/scattered_buffer.hpp | 120 ++++++++++ include/libcyphal/transport/session.hpp | 31 +++ include/libcyphal/transport/svc_sessions.hpp | 97 ++++++++ include/libcyphal/transport/transport.hpp | 42 ++++ include/libcyphal/transport/udp/media.hpp | 36 +++ include/libcyphal/transport/udp/transport.hpp | 99 ++++++++ include/libcyphal/types.hpp | 72 ++++++ test/unittest/CMakeLists.txt | 8 +- test/unittest/test_libcyphal.cpp | 9 +- test/unittest/transport/can/media_mock.hpp | 43 ++++ .../transport/can/test_can_transport.cpp | 147 ++++++++++++ test/unittest/transport/multiplexer_mock.hpp | 28 +++ .../transport/test_scattered_buffer.cpp | 127 +++++++++++ test/unittest/transport/udp/media_mock.hpp | 32 +++ .../transport/udp/test_udp_transport.cpp | 37 +++ 32 files changed, 1593 insertions(+), 82 deletions(-) delete mode 100644 include/libcyphal/libcyphal.hpp create mode 100644 include/libcyphal/runnable.hpp create mode 100644 include/libcyphal/transport/can/media.hpp create mode 100644 include/libcyphal/transport/can/transport.hpp create mode 100644 include/libcyphal/transport/defines.hpp create mode 100644 include/libcyphal/transport/errors.hpp create mode 100644 include/libcyphal/transport/msg_sessions.hpp create mode 100644 include/libcyphal/transport/multiplexer.hpp create mode 100644 include/libcyphal/transport/scattered_buffer.hpp create mode 100644 include/libcyphal/transport/session.hpp create mode 100644 include/libcyphal/transport/svc_sessions.hpp create mode 100644 include/libcyphal/transport/transport.hpp create mode 100644 include/libcyphal/transport/udp/media.hpp create mode 100644 include/libcyphal/transport/udp/transport.hpp create mode 100644 include/libcyphal/types.hpp create mode 100644 test/unittest/transport/can/media_mock.hpp create mode 100644 test/unittest/transport/can/test_can_transport.cpp create mode 100644 test/unittest/transport/multiplexer_mock.hpp create mode 100644 test/unittest/transport/test_scattered_buffer.cpp create mode 100644 test/unittest/transport/udp/media_mock.hpp create mode 100644 test/unittest/transport/udp/test_udp_transport.cpp diff --git a/.clang-format b/.clang-format index f5021158e..ba36fb92a 100644 --- a/.clang-format +++ b/.clang-format @@ -28,7 +28,6 @@ BraceWrapping: AfterStruct: true AfterClass: true AfterControlStatement: true - AfterEnum: true AfterFunction: true AfterUnion: true AfterNamespace: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c3d8eabbe..c097c523e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,36 +1,25 @@ name: "libcyphal_test" on: - push: - branches: - - main - - 'issue/*' - # todo: temp - remove this later! - - 'sshirokov/CI_*' + push: # Further filtering is done in the jobs. pull_request: branches: - main - 'issue/*' - # todo: temp - remove this later! - - 'sshirokov/CI_*' -# To test use https://github.com/nektos/act and specify the event as "act" -# For example: -# -# act act -j verification --bind --reuse -# -# That command will run the verification job locally and bind the current directory -# into the container (You'll probably need to delete any existing build directory -# before running act). jobs: warmup: + if: > + contains(github.event.head_commit.message, '#verification') || + contains(github.event.head_commit.message, '#docs') || + contains(github.ref, '/main') || + contains(github.ref, '/issue/') || + (github.event_name == 'pull_request') runs-on: ubuntu-latest container: ghcr.io/opencyphal/toolshed:ts22.4.3 steps: - uses: actions/checkout@v4 - if: ${{ github.event_name != 'act' }} - name: Cache ext modules - if: ${{ github.event_name != 'act' }} id: libcyphal-ext uses: actions/cache@v4 env: @@ -52,18 +41,26 @@ jobs: --build-flavor Debug clean-configure verification: + if: > + contains(github.event.head_commit.message, '#verification') || + contains(github.ref, '/main') || + contains(github.ref, '/issue/') || + (github.event_name == 'pull_request') runs-on: ubuntu-latest container: ghcr.io/opencyphal/toolshed:ts22.4.3 needs: [warmup] strategy: matrix: - flavor: [Release, Debug] + build_flavor: [Release, Debug] + std: [14, 17, 20] toolchain: [gcc, clang] + include: + - build_flavor: Coverage + std: 14 + toolchain: gcc steps: - uses: actions/checkout@v4 - if: ${{ github.event_name != 'act' }} - name: Cache ext modules - if: ${{ github.event_name != 'act' }} id: libcyphal-ext uses: actions/cache@v4 env: @@ -74,24 +71,43 @@ jobs: - name: get nunavut run: > pip install nunavut - - name: release + - name: run tests + env: + GTEST_COLOR: yes run: > ./build-tools/bin/verify.py --verbose --asserts - --cpp-standard 14 - --build-flavor ${{ matrix.flavor }} + --cpp-standard ${{ matrix.std }} + --build-flavor ${{ matrix.build_flavor }} --toolchain ${{ matrix.toolchain }} - clean-release + test + - name: debug output + if: ${{ runner.debug == '1' }} + run: ls -lAhR build/ + - name: upload-artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.build_flavor }}-${{ matrix.std }}-${{ matrix.toolchain }} + path: | + build/compile_commands.json + build/*/**/coverage.xml + build/*/**/*-sonarqube.xml + build/*/**/gcovr_html/*.* + if-no-files-found: error + docs: + if: > + contains(github.event.head_commit.message, '#docs') || + contains(github.ref, '/main') || + contains(github.ref, '/issue/') || + (github.event_name == 'pull_request') runs-on: ubuntu-latest container: ghcr.io/opencyphal/toolshed:ts22.4.3 needs: [warmup] steps: - uses: actions/checkout@v4 - if: ${{ github.event_name != 'act' }} - name: Cache ext modules - if: ${{ github.event_name != 'act' }} id: libcyphal-ext uses: actions/cache@v4 env: @@ -111,15 +127,15 @@ jobs: --build-flavor Debug build-docs - name: Setup Pages - if: ${{ github.event_name != 'pull_request' && github.event_name != 'act' }} + if: ${{ github.event_name != 'pull_request' }} uses: actions/configure-pages@v5 - name: Upload docs - if: ${{ github.event_name != 'pull_request' && github.event_name != 'act' }} + if: ${{ github.event_name != 'pull_request' }} uses: actions/upload-pages-artifact@v3 with: path: "build/docs/html/" - name: upload-pr-docs - if: ${{ github.event_name == 'pull_request' && github.event_name != 'act' }} + if: ${{ github.event_name == 'pull_request' }} uses: actions/upload-artifact@v4 with: name: pr-docs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a571d0dc..6ef7368c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,6 +193,28 @@ Reviewers, please check the following items when reviewing a pull-request: * Are the tests maintainable? * Is the code in the right namespace/class/function? +### Format the sources + +Clang-Format may format the sources differently depending on the version used. +To ensure that the formatting matches the expectations of the CI suite, +invoke Clang-Format of the correct version from the container (be sure to use the correct image tag): + +``` +docker run --rm -v ${PWD}:/repo ghcr.io/opencyphal/toolshed:ts22.4.3 ./build-tools/bin/verify.py build-danger-danger-repo-clang-format-in-place +``` + +### `issue/*` and hashtag-based CI triggering + +Normally, the CI will only run on pull requests (PR), releases, and perhaps some other special occasions on `main` branch. +Often, however, you will want to run it on your branch before proposing the changes to ensure all checks are +green and test coverage is adequate - to do that: +- either target your PR to any `issue/NN_LABEL` branch, where `NN` is the issue number and `LABEL` is a small title giving context (like `issue/83_any`) +- or add a hashtag with the name of the workflow you need to run to the head commit; +for example, making a commit with a message like `Add feature such and such #verification #docs #sonar` +will force the CI to execute jobs named `verification`, `docs`, and `sonar`. + +Note that if the job you requested is dependent on other jobs that are not triggered, it will not run; +for example, if `sonar` requires `docs`, pushing a commit with `#sonar` alone will not make it run. ## CAN bus Physical Layer Notes diff --git a/cmake/modules/Findcetl.cmake b/cmake/modules/Findcetl.cmake index 2c2a574f6..edad54c7d 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 "c1c2ae21ed446a7b25394d0067f3f4bec43a881b") +set(cetl_GIT_TAG "884f3b38e7857daa790ed6ba7031f7037fb81a2e") FetchContent_Declare( cetl diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index f472df504..d6ca46ceb 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -64,6 +64,7 @@ function (create_docs_target ARG_DOCS_DOXY_ROOT set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) set(DOXYGEN_CONFIG_FILE ${DOXYGEN_OUTPUT_DIRECTORY}/doxygen.config) set(DOXYGEN_EXAMPLE_PATH ${ARG_EXAMPLES_PATH}) + set(DOXYGEN_STRIP_FROM_PATH ${ARG_DOCS_DOXY_ROOT}/../include/libcyphal) list(APPEND DOXYGEN_INPUT_LIST ${ARG_INPUT_LIST}) list(JOIN DOXYGEN_INPUT_LIST "\\\n " DOXYGEN_INPUT ) @@ -129,7 +130,7 @@ endfunction(create_docs_target) file(GLOB_RECURSE DOXYGEN_INPUT_LIST LIST_DIRECTORIES false CONFIGURE_DEPENDS - ${LIBCYPHAL_ROOT}/include/libcyphal/**/*.hpp + ${LIBCYPHAL_ROOT}/include/**/*.hpp ) get_property(LOCAL_EXAMPLES DIRECTORY "examples" PROPERTY IN_BUILD_TESTS) diff --git a/docs/design/readme.md b/docs/design/readme.md index 9944d2f17..4a6c216b6 100644 --- a/docs/design/readme.md +++ b/docs/design/readme.md @@ -316,8 +316,8 @@ class IRunnable { +run(now: TimePoint) } -%% DYNAMIC BUFFER -class DynamicBuffer { +%% SCATTERED BUFFER +class ScatteredBuffer { +copy(offset: std::size_t, destination: void*, size_bytes: std::size_t) std::size_t } @@ -334,12 +334,12 @@ TransferMetadata <|-- ServiceTransferMetadata class MessageRxTransfer { +meta: TransferMetadata +publisher_node_id: cetl::optional~u16~ - +payload: DynamicBuffer + +payload: ScatteredBuffer } TransferMetadata o-- MessageRxTransfer class ServiceRxTransfer { +meta: ServiceTransferMetadata - +payload: DynamicBuffer + +payload: ScatteredBuffer } ServiceTransferMetadata o-- ServiceRxTransfer @@ -612,22 +612,22 @@ The `send` method can either accept the transfer for transmission in its entiret The transfer timestamp given to the `send` method signifies the transmission deadline for the transport frames originating from this transfer; transport frames whose transmission could not be completed prior to the expiration of their deadline will be dropped (dequeued without transmission) by the transport. Such occurrences should normally be recorded for diagnostic purposes via the appropriate statistical counters, but this feature is currently outside of the scope of this proposal. -Rx sessions operate like sampling ports, storing one most recently received transfer internally as an instance of `DynamicBuffer` (see below). The buffer is passed over to the application upon the next call to `receive`. A sampling port is similar to a FIFO queue of depth 1 unit, so in the future this design can be trivially generalized to support variable-depth FIFO queues inside Rx session instances that are set to the depth of 1 unit by default. Shall the application fail to collect the received transfer(s) before the FIFO queue is full, the least recently received transfer is dropped, and the new one is pushed to the opposite end of the queue. Such occurrences should normally be recorded for diagnostic purposes via the appropriate statistical counters, but this feature is currently outside of the scope of this proposal. +Rx sessions operate like sampling ports, storing one most recently received transfer internally as an instance of `ScatteredBuffer` (see below). The buffer is passed over to the application upon the next call to `receive`. A sampling port is similar to a FIFO queue of depth 1 unit, so in the future this design can be trivially generalized to support variable-depth FIFO queues inside Rx session instances that are set to the depth of 1 unit by default. Shall the application fail to collect the received transfer(s) before the FIFO queue is full, the least recently received transfer is dropped, and the new one is pushed to the opposite end of the queue. Such occurrences should normally be recorded for diagnostic purposes via the appropriate statistical counters, but this feature is currently outside of the scope of this proposal. -#### `DynamicBuffer` +#### `ScatteredBuffer` -Lizards operate on raw serialized binary blobs of data rather than high-level message representations. Transmission is performed by enqueueing serialized transfers into a private transmission queue managed by the lizard, which is easy to abstract from the application. Reception is a more complicated case because it requires a lizard to return memory to the application that is owned by the lizard, requiring the latter to free it after use in a lizard-specific manner; further and more importantly, such memory may or may not be fragmented in a gather-scatter buffer. To hide the specifics of such memory management from the application, a new abstraction is introduced, represented by the class named `DynamicBuffer`. +Lizards operate on raw serialized binary blobs of data rather than high-level message representations. Transmission is performed by enqueueing serialized transfers into a private transmission queue managed by the lizard, which is easy to abstract from the application. Reception is a more complicated case because it requires a lizard to return memory to the application that is owned by the lizard, requiring the latter to free it after use in a lizard-specific manner; further and more importantly, such memory may or may not be fragmented in a gather-scatter buffer. To hide the specifics of such memory management from the application, a new abstraction is introduced, represented by the class named `ScatteredBuffer`. Examples of lizard-specific management of the returned memory can be found in and . -The `DynamicBuffer` provides a uniform API for dealing with the Cyphal transfer payload returned by a lizard and also implements the movable/non-copyable RAII semantics for freeing the memory allocated for the buffer once the dynamic buffer instance is disposed of. The interface hides the gather-scatter nature of the buffer, providing a simplified linearized view. The definition of the class is approximately as follows: +The `ScatteredBuffer` provides a uniform API for dealing with the Cyphal transfer payload returned by a lizard and also implements the movable/non-copyable RAII semantics for freeing the memory allocated for the buffer once the scattered buffer instance is disposed of. The interface hides the gather-scatter nature of the buffer, providing a simplified linearized view. The definition of the class is approximately as follows: ```c++ /// 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 DynamicBuffer final +class ScatteredBuffer final { public: static constexpr std::size_t ImplementationFootprint = sizeof(void*) * 8; @@ -651,7 +651,7 @@ public: /// Accepts a Lizard-specific implementation of Iface and moves it into the internal storage. template::value>> - explicit DynamicBuffer(T&& source) : impl_(std::move(source)) {} + explicit ScatteredBuffer(T&& source) : impl_(std::move(source)) {} /// 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. @@ -791,7 +791,7 @@ class IRxSocket public: /// Payload is returned as a pointer to the heap. The buffer is allocated using the allocator given to the media /// instance. We use heap allocation here because LibUDPard takes ownership of the payload and then transfers it - /// to the upper layers without copying via DynamicBuffer. + /// to the upper layers without copying via ScatteredBuffer. virtual [[nodiscard]] std::expected>>, std::variant> @@ -916,7 +916,7 @@ public: // If the message is of type cetl::VariableLengthArray, where Allocator may be arbitrary, // a specialized subscriber is constructed that does not deserialize messages but returns the serialzied // representation as-is after copying it into a new instance of the specified array type. - // Note that we can't just deliver DynamicBuffer to the application because it is non-copyable. + // Note that we can't just deliver ScatteredBuffer to the application because it is non-copyable. // See the specialization for usage details. template [[nodiscard]] std::expected, Error> makeSubscriber(const std::uint16_t subject_id); @@ -931,7 +931,7 @@ public: // state is not lost by creating a dummy client instance whose only purpose is to keep the counter alive. // // If the service is of type void, a specialized client is constructed that does not serialize requests nor - // deserializes responses, but accepts raw pre-serialized requests and returns responses contained in DynamicBuffer + // deserializes responses, but accepts raw pre-serialized requests and returns responses contained in ScatteredBuffer // as received from the transport layer. See the specialization for usage details. template [[nodiscard]] std::expected, Error> makeClient(const std::uint16_t service_id, @@ -1073,13 +1073,13 @@ protected: explicit SubscriberBase(const cetl::shared_ptr& impl); [[nodiscard]] virtual nunavut::support::SerializeResult doAccept(const Metadata& meta, - const DynamicBuffer& data) noexcept = 0; + const ScatteredBuffer& data) noexcept = 0; private: /// This is invoked from SubscriberImpl when a new transfer for this subscription is received. /// The concrete subscriber implements this by invoking the auto-generated deserialization function. [[nodiscard]] nunavut::support::SerializeResult accept(const Metadata& meta, - const DynamicBuffer& data) noexcept; + const ScatteredBuffer& data) noexcept; cetl::shared_ptr impl_; }; @@ -1105,7 +1105,7 @@ public: private: [[nodiscard]] nunavut::support::SerializeResult doAccept(const Metadata& meta, - const DynamicBuffer& data) noexcept override + const ScatteredBuffer& data) noexcept override { Message msg; if (const auto res = Message::deserialize(msg, data); !res) @@ -1143,7 +1143,7 @@ public: private: [[nodiscard]] nunavut::support::SerializeResult doAccept(const Metadata& meta, - const DynamicBuffer& data) noexcept override + const ScatteredBuffer& data) noexcept override { Message msg; msg.resize(std::min(data.size(), msg.max_max_size())); // Excess will be truncated per Cyphal spec. @@ -1161,11 +1161,11 @@ private: }; ``` -The message RX session object is managed by `SubscriberImpl`, which also implements `IRunnable`. The `run` method polls the RX session, and if there is a transfer available, it is consumed, and then the `accept` method of each living `SubscriberBase` is invoked sequentially. Each `Subscriber` deserializes its own copy of the message and stores it for consumption by the application in the FIFO queue. The `SubscriberImpl` then destroys the `DynamicBuffer` containing the received transfer. +The message RX session object is managed by `SubscriberImpl`, which also implements `IRunnable`. The `run` method polls the RX session, and if there is a transfer available, it is consumed, and then the `accept` method of each living `SubscriberBase` is invoked sequentially. Each `Subscriber` deserializes its own copy of the message and stores it for consumption by the application in the FIFO queue. The `SubscriberImpl` then destroys the `ScatteredBuffer` containing the received transfer. It is easy to see that there is a certain redundancy involved, as each `Subscriber` performs deserialization independently of each other, resulting in duplicate work. This is avoidable but is somewhat convoluted, as the type of the message is not known to `SubscriberImpl`; one approach is to use the first instance of `Subscriber` to perform deserialization and then to deliver the deserialized message object to each of its siblings by value (unless we are comfortable using shared pointers here, which we are probably not). This may need to be revised in a later version; however, one should keep in mind that the work duplication only becomes a problem for large messages that are expensive to deserialize, and the application is arguably less likely to have more than one subscriber for such expensive messages. -Another issue to be aware of on is the deep copying inherent to the C++ deserialization code generated by Nunavut. This creates potentially significant issues for large messages (imagery, point clouds, radar samples) that can be avoided with more careful buffer memory management. For example, the Python codegen implemented in Nunavut heavily leverages shared pointers and aliasing for this purpose (resorting to NumPy-aliased arrays for large blobs) instead of the slow byte-by-byte copying implemented for C++. This approach is difficult to recreate directly in LibCyphal because the presentation layer receives transfers in `DynamicBuffer`, which is fragmented as it receives the data from the underlying media layer as a sequence of byte spans pointing directly into the memory allocated for the network frames (like UDP datagrams). As the MTU is typically small, large data blobs where it's worth worrying about zero-copy deserialization will invariably end up being fragmented; hence, whatever solution is chosen for the zero-copy deserialization needs to be able to accept scattered buffers and present them to the application with a contiguous API. This leads to another problem: many (all known to me) APIs designed for imagery and point cloud manipulation expect the imagery data to be arranged in contiguous memory chunks, which suggests that at least one deep copy will be needed *somewhere* along the stack. The current revision does it in `Subscriber::doAccept` but there is no implication that this choice is optimal. +Another issue to be aware of on is the deep copying inherent to the C++ deserialization code generated by Nunavut. This creates potentially significant issues for large messages (imagery, point clouds, radar samples) that can be avoided with more careful buffer memory management. For example, the Python codegen implemented in Nunavut heavily leverages shared pointers and aliasing for this purpose (resorting to NumPy-aliased arrays for large blobs) instead of the slow byte-by-byte copying implemented for C++. This approach is difficult to recreate directly in LibCyphal because the presentation layer receives transfers in `ScatteredBuffer`, which is fragmented as it receives the data from the underlying media layer as a sequence of byte spans pointing directly into the memory allocated for the network frames (like UDP datagrams). As the MTU is typically small, large data blobs where it's worth worrying about zero-copy deserialization will invariably end up being fragmented; hence, whatever solution is chosen for the zero-copy deserialization needs to be able to accept scattered buffers and present them to the application with a contiguous API. This leads to another problem: many (all known to me) APIs designed for imagery and point cloud manipulation expect the imagery data to be arranged in contiguous memory chunks, which suggests that at least one deep copy will be needed *somewhere* along the stack. The current revision does it in `Subscriber::doAccept` but there is no implication that this choice is optimal. The proposal intentionally excludes statistical counters; such auxiliary features are to be retrofitted at a later stage. @@ -1184,7 +1184,7 @@ public: [[nodiscard]] TimePoint getRequestTimestamp() const noexcept; protected: - [[nodiscard]] std::optional> getRaw() noexcept; + [[nodiscard]] std::optional> getRaw() noexcept; }; template @@ -1244,7 +1244,7 @@ public: [[nodiscard]] std::expected, Error> request(const Service::Request& req); }; -/// The non-typed specialization that accepts the request as a raw blob and returns the response as a raw DynamicBuffer. +/// The non-typed specialization that accepts the request as a raw blob and returns the response as a raw ScatteredBuffer. template <> class Client final : public ClientBase { @@ -1256,7 +1256,7 @@ public: TooManyPendingRequestsError>; /// Sends the request and returns a promise that will be materialized when the response is successfully received. - [[nodiscard]] std::expected, Error> request( + [[nodiscard]] std::expected, Error> request( const std::span> req); }; ``` @@ -1307,7 +1307,7 @@ private: std::optional> callback_; }; -/// The non-typed specialization that presents incoming requests as a raw DynamicBuffer and accepts responses as raw blobs. +/// The non-typed specialization that presents incoming requests as a raw ScatteredBuffer and accepts responses as raw blobs. template <> class Server final : public ServerBase { @@ -1316,7 +1316,7 @@ public: { using Continuation = cetl::function(const std::span>)>; - DynamicBuffer request; + ScatteredBuffer request; Metadata meta; Continuation continuation; ///< The continuation can be used at most once. }; diff --git a/docs/doxygen.ini b/docs/doxygen.ini index 4e8e799b5..814093a88 100644 --- a/docs/doxygen.ini +++ b/docs/doxygen.ini @@ -184,7 +184,7 @@ FULL_PATH_NAMES = YES # will be relative from the directory where doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. -STRIP_FROM_PATH = +STRIP_FROM_PATH = @DOXYGEN_STRIP_FROM_PATH@ # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the # path mentioned in the documentation of a class, which tells the reader which diff --git a/docs/examples/CMakeLists.txt b/docs/examples/CMakeLists.txt index 3c6a26022..c709c737f 100644 --- a/docs/examples/CMakeLists.txt +++ b/docs/examples/CMakeLists.txt @@ -115,3 +115,9 @@ add_custom_target( set_directory_properties(PROPERTIES IN_BUILD_TESTS "${ALL_EXAMPLE_RUNS}" ) + +if (CMAKE_BUILD_TYPE STREQUAL "Coverage") + enable_coverage_report(COVERAGE_REPORT_FORMATS sonarqube html + ROOT_DIRECTORY ${LIBCYPHAL_ROOT} + ) +endif() diff --git a/docs/examples/example_01_hello_world.cpp b/docs/examples/example_01_hello_world.cpp index 1669a7a59..1f0ecd3b0 100644 --- a/docs/examples/example_01_hello_world.cpp +++ b/docs/examples/example_01_hello_world.cpp @@ -7,7 +7,7 @@ /// SPDX-License-Identifier: MIT /// -#include "libcyphal/libcyphal.hpp" +#include "libcyphal/types.hpp" // TODO: Uncomment this when we have a real example to test. // diff --git a/include/libcyphal/libcyphal.hpp b/include/libcyphal/libcyphal.hpp deleted file mode 100644 index 0dd58360e..000000000 --- a/include/libcyphal/libcyphal.hpp +++ /dev/null @@ -1,16 +0,0 @@ -/// @file -/// libcyphal common header. -/// -/// @copyright -/// Copyright (C) OpenCyphal Development Team -/// Copyright Amazon.com Inc. or its affiliates. -/// SPDX-License-Identifier: MIT - -#ifndef LIBCYPHAL_LIBCYPHAL_HPP_INCLUDED -#define LIBCYPHAL_LIBCYPHAL_HPP_INCLUDED - -namespace libcyphal -{ -} // namespace libcyphal - -#endif // LIBCYPHAL_LIBCYPHAL_HPP_INCLUDED diff --git a/include/libcyphal/runnable.hpp b/include/libcyphal/runnable.hpp new file mode 100644 index 000000000..6e68d6401 --- /dev/null +++ b/include/libcyphal/runnable.hpp @@ -0,0 +1,31 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_RUNNABLE_HPP_INCLUDED +#define LIBCYPHAL_RUNNABLE_HPP_INCLUDED + +#include "types.hpp" + +namespace libcyphal +{ + +class IRunnable +{ +public: + IRunnable(IRunnable&&) = delete; + IRunnable(const IRunnable&) = delete; + IRunnable& operator=(IRunnable&&) = delete; + IRunnable& operator=(const IRunnable&) = delete; + + virtual void run(const TimePoint now) = 0; + +protected: + IRunnable() = default; + virtual ~IRunnable() = default; +}; + +} // namespace libcyphal + +#endif // LIBCYPHAL_RUNNABLE_HPP_INCLUDED diff --git a/include/libcyphal/transport/can/media.hpp b/include/libcyphal/transport/can/media.hpp new file mode 100644 index 000000000..e4d2b0b25 --- /dev/null +++ b/include/libcyphal/transport/can/media.hpp @@ -0,0 +1,88 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_CAN_MEDIA_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_CAN_MEDIA_HPP_INCLUDED + +#include "libcyphal/types.hpp" +#include "libcyphal/transport/errors.hpp" +#include "libcyphal/transport/defines.hpp" + +namespace libcyphal +{ +namespace transport +{ +namespace can +{ + +using CanId = std::uint32_t; + +struct Filter final +{ + CanId id; + CanId mask; +}; +using Filters = cetl::span; + +struct RxMetadata final +{ + TimePoint timestamp; + CanId can_id; + std::size_t payload_size; +}; + +class IMedia +{ +public: + IMedia(IMedia&&) = delete; + IMedia(const IMedia&) = delete; + IMedia& operator=(IMedia&&) = delete; + IMedia& operator=(const IMedia&) = delete; + + /// @brief Get the maximum transmission unit (MTU) of the CAN bus. + /// + /// 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; + + /// @brief Set the filters for the CAN bus. + /// + /// If there are fewer hardware filters available than requested, the configuration will be coalesced as described + /// in the Cyphal/CAN Specification. If zero filters are requested, all incoming traffic will be rejected. + /// While reconfiguration is in progress, incoming frames may be lost and/or unwanted frames may be received. + /// The lifetime of the filter array may end upon return (no references retained). + /// + /// @return Returns `nullopt` on success; otherwise some `MediaError` in case of a low-level error. + /// + CETL_NODISCARD virtual cetl::optional setFilters(const Filters filters) noexcept = 0; + + /// @brief Schedules the frame for transmission asynchronously and return immediately. + /// + /// @return Returns `true` if accepted or already timed out; `false` to try again later. + /// + CETL_NODISCARD virtual Expected push(const TimePoint deadline, + const CanId can_id, + const cetl::span payload) noexcept = 0; + + /// @brief Takes the next payload fragment (aka CAN frame) from the reception queue unless it's empty. + /// + /// @param payload_buffer The payload of the frame will be written into the mutable `payload_buffer` (aka span). + /// @return Description of a received fragment if available; otherwise an empty optional is returned immediately. + /// + CETL_NODISCARD virtual Expected, MediaError> pop( + const cetl::span payload_buffer) noexcept = 0; + +protected: + IMedia() = default; + ~IMedia() = default; + +}; // IMedia + +} // namespace can +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_CAN_MEDIA_HPP_INCLUDED diff --git a/include/libcyphal/transport/can/transport.hpp b/include/libcyphal/transport/can/transport.hpp new file mode 100644 index 000000000..187bd9af4 --- /dev/null +++ b/include/libcyphal/transport/can/transport.hpp @@ -0,0 +1,212 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_CAN_TRANSPORT_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_CAN_TRANSPORT_HPP_INCLUDED + +#include "media.hpp" +#include "libcyphal/transport/transport.hpp" +#include "libcyphal/transport/multiplexer.hpp" + +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace can +{ + +class ICanTransport : public ITransport +{}; + +namespace detail +{ +class TransportImpl final : public ICanTransport +{ +public: + TransportImpl(cetl::pmr::memory_resource& memory, + libcyphal::detail::VarArray&& media_array, + const CanardNodeID canard_node_id) + : memory_{memory} + , media_array_{std::move(media_array)} + , canard_instance_{canardInit(canardMemoryAllocate, canardMemoryFree)} + { + canard_instance_.user_reference = this; + canard_instance_.node_id = canard_node_id; + } + + // MARK: ICanTransport + + // MARK: ITransport + + CETL_NODISCARD cetl::optional getLocalNodeId() const noexcept override + { + return canard_instance_.node_id > CANARD_NODE_ID_MAX ? cetl::nullopt + : cetl::make_optional(canard_instance_.node_id); + } + CETL_NODISCARD ProtocolParams getProtocolParams() const noexcept override + { + const auto min_mtu = + reduceMediaInto(std::numeric_limits::max(), + [](std::size_t&& mtu, IMedia& media) { mtu = std::min(mtu, media.getMtu()); }); + return ProtocolParams{1 << CANARD_TRANSFER_ID_BIT_LENGTH, min_mtu, CANARD_NODE_ID_MAX + 1}; + } + + 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 + + void run(const TimePoint) override {} + +private: + // MARK: Privates: + + // Until "canardMemFree must provide size" issue #216 is resolved, + // we need to store the size of the memory allocated. + // TODO: Remove this workaround when the issue is resolved. + // see https://github.com/OpenCyphal/libcanard/issues/216 + // + struct CanardMemory final + { + alignas(std::max_align_t) std::size_t size; + }; + + CETL_NODISCARD static inline TransportImpl& getSelfFrom(const CanardInstance* const ins) + { + 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); + } + + CETL_NODISCARD static void* canardMemoryAllocate(CanardInstance* ins, size_t amount) + { + auto& self = getSelfFrom(ins); + + const std::size_t memory_size = sizeof(CanardMemory) + amount; + auto canard_memory = static_cast(self.memory_.allocate(memory_size)); + if (canard_memory == nullptr) + { + return nullptr; + } + + // Return the memory after the `CanardMemory` struct (containing its size). + // The size is used in `canardMemoryFree` to deallocate the memory. + // + canard_memory->size = memory_size; + return ++canard_memory; + } + + static void canardMemoryFree(CanardInstance* ins, void* pointer) + { + if (pointer == nullptr) + { + return; + } + + auto canard_memory = static_cast(pointer); + --canard_memory; + + auto& self = getSelfFrom(ins); + self.memory_.deallocate(canard_memory, canard_memory->size); + } + + template + T reduceMediaInto(T&& init, Reducer reducer) const + { + for (IMedia* const media : media_array_) + { + CETL_DEBUG_ASSERT(media != nullptr, "Expected media interface."); + reducer(std::forward(init), std::ref(*media)); + } + return init; + } + + // MARK: Data members: + + cetl::pmr::memory_resource& memory_; + const libcyphal::detail::VarArray media_array_; + CanardInstance canard_instance_; + +}; // TransportImpl + +} // namespace detail + +CETL_NODISCARD inline Expected, FactoryError> makeTransport( + cetl::pmr::memory_resource& memory, + IMultiplexer& multiplexer, + const std::array& media, // TODO: replace with `cetl::span` + const cetl::optional local_node_id) +{ + // TODO: Use these! + (void) multiplexer; + + // Verify input arguments: + // - At least one media interface must be provided. + // - If a local node ID is provided, it must be within the valid range. + // + const auto media_count = static_cast( + std::count_if(media.cbegin(), media.cend(), [](IMedia* const m) { return m != nullptr; })); + if (media_count == 0) + { + return ArgumentError{}; + } + if (local_node_id.has_value() && local_node_id.value() > CANARD_NODE_ID_MAX) + { + return ArgumentError{}; + } + + libcyphal::detail::VarArray media_array{MaxMediaInterfaces, &memory}; + media_array.reserve(media_count); + std::copy_if(media.cbegin(), media.cend(), std::back_inserter(media_array), [](IMedia* const m) { + return m != nullptr; + }); + CETL_DEBUG_ASSERT(!media_array.empty() && (media_array.size() == media_count), ""); + + const auto canard_node_id = static_cast(local_node_id.value_or(CANARD_NODE_ID_UNSET)); + + libcyphal::detail::PmrAllocator allocator{&memory}; + auto transport = cetl::pmr::Factory::make_unique(allocator, memory, std::move(media_array), canard_node_id); + + return UniquePtr{transport.release(), UniquePtr::deleter_type{allocator, 1}}; +} + +} // namespace can +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_CAN_TRANSPORT_HPP_INCLUDED diff --git a/include/libcyphal/transport/defines.hpp b/include/libcyphal/transport/defines.hpp new file mode 100644 index 000000000..c137f21a1 --- /dev/null +++ b/include/libcyphal/transport/defines.hpp @@ -0,0 +1,90 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_DEFINES_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_DEFINES_HPP_INCLUDED + +#include "scattered_buffer.hpp" + +namespace libcyphal +{ +namespace transport +{ + +/// @brief `NodeId` is a 16-bit unsigned integer that represents a node in a Cyphal network. +/// +/// Anonymity is represented by an empty `cetl::optional` (see `cetl::nullopt`). +/// +using NodeId = std::uint16_t; + +/// @brief `PortId` is a 16-bit unsigned integer that represents a port (subject & 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. +/// +using TransferId = std::uint64_t; + +enum class Priority +{ + + Exceptional = 0, + Immediate = 1, + Fast = 2, + High = 3, + Nominal = 4, ///< Nominal priority level should be the default. + Low = 5, + Slow = 6, + Optional = 7, +}; + +struct ProtocolParams final +{ + TransferId transfer_id_modulo; + std::size_t mtu_bytes; + NodeId max_nodes; +}; + +struct TransferMetadata +{ + TransferId transfer_id; + TimePoint timestamp; + Priority priority; +}; + +struct MessageTransferMetadata final : TransferMetadata +{ + cetl::optional publisher_node_id; +}; + +struct ServiceTransferMetadata final : TransferMetadata +{ + NodeId remote_node_id; +}; + +/// @brief Defines a span of immutable fragments of payload. +using PayloadFragments = cetl::span>; + +struct MessageRxTransfer final +{ + MessageTransferMetadata metadata; + ScatteredBuffer payload; +}; + +struct ServiceRxTransfer final +{ + ServiceTransferMetadata metadata; + ScatteredBuffer payload; +}; + +/// @brief Defines maximum number of media interfaces that can be used in a Cyphal transport. +/// TODO: This is a temporary constant and will be replaced by `cetl::span::size()` (see `makeTransport`). +constexpr std::size_t MaxMediaInterfaces = 3; + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_DEFINES_HPP_INCLUDED diff --git a/include/libcyphal/transport/errors.hpp b/include/libcyphal/transport/errors.hpp new file mode 100644 index 000000000..9b3cf3530 --- /dev/null +++ b/include/libcyphal/transport/errors.hpp @@ -0,0 +1,56 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_ERRORS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_ERRORS_HPP_INCLUDED + +#include "libcyphal/types.hpp" + +namespace libcyphal +{ +namespace transport +{ + +struct StateError +{}; + +struct AnonymousError +{}; + +struct ArgumentError +{}; + +struct MemoryError +{}; + +struct CapacityError +{}; + +struct PlatformError +{ + std::uint32_t code; +}; + +// TODO: Delete it when everything is implemented. +struct NotImplementedError +{}; + +/// @brief Defines any possible error at Cyphal transport layer. +/// +using AnyError = cetl:: + variant; + +/// @brief Defines any possible factory error at Cyphal transport layer. +/// +using FactoryError = cetl::variant; + +/// @brief Defines any possible error at Cyphal media layer. +/// +using MediaError = cetl::variant; + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_ERRORS_HPP_INCLUDED diff --git a/include/libcyphal/transport/msg_sessions.hpp b/include/libcyphal/transport/msg_sessions.hpp new file mode 100644 index 000000000..358d27272 --- /dev/null +++ b/include/libcyphal/transport/msg_sessions.hpp @@ -0,0 +1,59 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_MSG_SESSIONS_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_MSG_SESSIONS_HPP_INCLUDED + +#include "session.hpp" + +namespace libcyphal +{ +namespace transport +{ + +struct MessageRxParams final +{ + std::size_t extent_bytes; + PortId subject_id; +}; + +struct MessageTxParams final +{ + PortId subject_id; +}; + +class IMessageRxSession : public IRxSession +{ +public: + CETL_NODISCARD virtual MessageRxParams getParams() const noexcept = 0; + + /// @brief Receives a message from the transport layer. + /// + /// Method is not blocking, and will return immediately if no message is available. + /// + /// @return A message transfer if available; otherwise an empty optional. + /// + CETL_NODISCARD virtual cetl::optional receive() = 0; +}; + +class IMessageTxSession : public IRunnable +{ +public: + CETL_NODISCARD virtual MessageTxParams getParams() const noexcept = 0; + + /// @brief Sends a message to the transport layer. + /// + /// @param metadata Additional metadata associated with the message. + /// @param payload_fragments Segments of the message payload. + /// @return `nullopt` in case of success; otherwise a transport error. + /// + CETL_NODISCARD virtual cetl::optional send(const TransferMetadata& metadata, + const PayloadFragments payload_fragments) = 0; +}; + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_MSG_SESSIONS_HPP_INCLUDED diff --git a/include/libcyphal/transport/multiplexer.hpp b/include/libcyphal/transport/multiplexer.hpp new file mode 100644 index 000000000..d2f49950c --- /dev/null +++ b/include/libcyphal/transport/multiplexer.hpp @@ -0,0 +1,33 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_MULTIPLEXER_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_MULTIPLEXER_HPP_INCLUDED + +namespace libcyphal +{ +namespace transport +{ + +class IMultiplexer +{ +public: + IMultiplexer(IMultiplexer&&) = delete; + IMultiplexer(const IMultiplexer&) = delete; + IMultiplexer& operator=(IMultiplexer&&) = delete; + IMultiplexer& operator=(const IMultiplexer&) = delete; + + // TODO: Add methods here + +protected: + IMultiplexer() = default; + ~IMultiplexer() = default; + +}; // IMultiplexer + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_MULTIPLEXER_HPP_INCLUDED diff --git a/include/libcyphal/transport/scattered_buffer.hpp b/include/libcyphal/transport/scattered_buffer.hpp new file mode 100644 index 000000000..354db0f03 --- /dev/null +++ b/include/libcyphal/transport/scattered_buffer.hpp @@ -0,0 +1,120 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_SCATTERED_BUFFER_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_SCATTERED_BUFFER_HPP_INCLUDED + +#include + +#include + +namespace libcyphal +{ +namespace transport +{ + +/// 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:: + 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; + + class Interface : public cetl::rtti_helper + { + public: + Interface(const Interface&) = delete; + Interface& operator=(const Interface&) = delete; + + CETL_NODISCARD virtual std::size_t size() const noexcept = 0; + 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(Interface&&) noexcept = default; + Interface& operator=(Interface&&) noexcept = default; + + }; // Interface + + ScatteredBuffer() = default; + ScatteredBuffer(const ScatteredBuffer& other) = delete; + ScatteredBuffer(ScatteredBuffer&& other) noexcept + { + storage_ = std::move(other.storage_); + + other.interface_ = nullptr; + interface_ = cetl::any_cast(&storage_); + } + + /// @brief Accepts a Lizard-specific implementation of `Interface` and moves it into the internal storage. + /// + template ::value>> + explicit ScatteredBuffer(T&& source) noexcept + : storage_(std::forward(source)) + , interface_{cetl::any_cast(&storage_)} + { + } + + ~ScatteredBuffer() + { + reset(); + } + + ScatteredBuffer& operator=(const ScatteredBuffer& other) = delete; + ScatteredBuffer& operator=(ScatteredBuffer&& other) noexcept + { + storage_ = std::move(other.storage_); + + other.interface_ = nullptr; + interface_ = cetl::any_cast(&storage_); + + return *this; + } + + void reset() noexcept + { + storage_.reset(); + interface_ = nullptr; + } + + /// @brief Gets the number of bytes stored in the buffer (possibly scattered, but this is hidden from the user). + /// + /// @return Returns zero if the buffer is moved away. + /// + CETL_NODISCARD std::size_t size() const noexcept + { + return interface_ ? interface_->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. + /// Does nothing and returns zero if the instance has been moved away. + /// + 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; + } + +private: + cetl::any storage_; + const Interface* interface_ = nullptr; + +}; // ScatteredBuffer + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_SCATTERED_BUFFER_HPP_INCLUDED diff --git a/include/libcyphal/transport/session.hpp b/include/libcyphal/transport/session.hpp new file mode 100644 index 000000000..a85672acd --- /dev/null +++ b/include/libcyphal/transport/session.hpp @@ -0,0 +1,31 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_SESSION_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_SESSION_HPP_INCLUDED + +#include "libcyphal/runnable.hpp" +#include "defines.hpp" + +namespace libcyphal +{ +namespace transport +{ + +class ISession : public IRunnable +{ +public: +}; + +class IRxSession : public ISession +{ +public: + virtual void setTransferIdTimeout(const Duration timeout) = 0; +}; + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_SESSION_HPP_INCLUDED diff --git a/include/libcyphal/transport/svc_sessions.hpp b/include/libcyphal/transport/svc_sessions.hpp new file mode 100644 index 000000000..462f2f07e --- /dev/null +++ b/include/libcyphal/transport/svc_sessions.hpp @@ -0,0 +1,97 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_SVC_SESSION_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_SVC_SESSION_HPP_INCLUDED + +#include "session.hpp" + +namespace libcyphal +{ +namespace transport +{ + +struct RequestRxParams final +{ + std::size_t extent_bytes; + PortId service_id; +}; + +struct RequestTxParams final +{ + PortId service_id; + NodeId server_node_id; +}; + +struct ResponseRxParams final +{ + std::size_t extent_bytes; + PortId service_id; + NodeId server_node_id; +}; + +struct ResponseTxParams final +{ + PortId service_id; +}; + +class ISvcRxSession : public IRxSession +{ +public: + /// @brief Receives a service transfer (request or response) from the transport layer. + /// + /// Method is not blocking, and will return immediately if no transfer is available. + /// + /// @return A service transfer if available; otherwise an empty optional. + /// + CETL_NODISCARD virtual cetl::optional receive() = 0; +}; + +class IRequestRxSession : public ISvcRxSession +{ +public: + CETL_NODISCARD virtual RequestRxParams getParams() const noexcept = 0; +}; + +class IRequestTxSession : public ISession +{ +public: + CETL_NODISCARD virtual RequestTxParams getParams() const noexcept = 0; + + /// @brief Sends a service request to the transport layer. + /// + /// @param metadata Additional metadata associated with the request. + /// @param payload_fragments Segments of the request payload. + /// @return `nullopt` in case of success; otherwise a transport error. + /// + CETL_NODISCARD virtual cetl::optional send(const TransferMetadata& metadata, + const PayloadFragments payload_fragments) = 0; +}; + +class IResponseRxSession : public ISvcRxSession +{ +public: + CETL_NODISCARD virtual ResponseRxParams getParams() const noexcept = 0; +}; + +class IResponseTxSession : public ISession +{ +public: + CETL_NODISCARD virtual ResponseTxParams getParams() const noexcept = 0; + + /// @brief Sends a service response to the transport layer. + /// + /// @param metadata Additional metadata associated with the response. + /// @param payload_fragments Segments of the response payload. + /// @return `nullopt` in case of success; otherwise a transport error. + /// + CETL_NODISCARD virtual cetl::optional send(const ServiceTransferMetadata& metadata, + const PayloadFragments payload_fragments) = 0; +}; + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_SVC_SESSION_HPP_INCLUDED diff --git a/include/libcyphal/transport/transport.hpp b/include/libcyphal/transport/transport.hpp new file mode 100644 index 000000000..6df2f6cbd --- /dev/null +++ b/include/libcyphal/transport/transport.hpp @@ -0,0 +1,42 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_TRANSPORT_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_TRANSPORT_HPP_INCLUDED + +#include "errors.hpp" +#include "msg_sessions.hpp" +#include "svc_sessions.hpp" + +namespace libcyphal +{ +namespace transport +{ + +class ITransport : public IRunnable +{ +public: + CETL_NODISCARD virtual cetl::optional getLocalNodeId() const noexcept = 0; + CETL_NODISCARD virtual ProtocolParams getProtocolParams() const noexcept = 0; + + CETL_NODISCARD virtual Expected, AnyError> makeMessageRxSession( + const MessageRxParams& params) = 0; + CETL_NODISCARD virtual Expected, AnyError> makeMessageTxSession( + const MessageTxParams& params) = 0; + CETL_NODISCARD virtual Expected, AnyError> makeRequestRxSession( + const RequestRxParams& params) = 0; + CETL_NODISCARD virtual Expected, AnyError> makeRequestTxSession( + const RequestTxParams& params) = 0; + CETL_NODISCARD virtual Expected, AnyError> makeResponseRxSession( + const ResponseRxParams& params) = 0; + CETL_NODISCARD virtual Expected, AnyError> makeResponseTxSession( + const ResponseTxParams& params) = 0; + +}; // ITransport + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_TRANSPORT_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/media.hpp b/include/libcyphal/transport/udp/media.hpp new file mode 100644 index 000000000..d68b9d0ea --- /dev/null +++ b/include/libcyphal/transport/udp/media.hpp @@ -0,0 +1,36 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_MEDIA_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_MEDIA_HPP_INCLUDED + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +class IMedia +{ +public: + IMedia(IMedia&&) = delete; + IMedia(const IMedia&) = delete; + IMedia& operator=(IMedia&&) = delete; + IMedia& operator=(const IMedia&) = delete; + + // TODO: Add methods here + +protected: + IMedia() = default; + ~IMedia() = default; + +}; // IMedia + +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_MEDIA_HPP_INCLUDED diff --git a/include/libcyphal/transport/udp/transport.hpp b/include/libcyphal/transport/udp/transport.hpp new file mode 100644 index 000000000..d7e6d83e4 --- /dev/null +++ b/include/libcyphal/transport/udp/transport.hpp @@ -0,0 +1,99 @@ +/// @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/transport/transport.hpp" +#include "libcyphal/transport/multiplexer.hpp" + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +class IUdpTransport : public ITransport +{}; + +namespace detail +{ + +class TransportImpl final : public IUdpTransport +{ +public: + // MARK: IUpdTransport + + // MARK: ITransport + + CETL_NODISCARD cetl::optional getLocalNodeId() const noexcept override + { + 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 + + void run(const TimePoint) override {} + +}; // TransportImpl + +} // namespace detail + +CETL_NODISCARD inline Expected, FactoryError> makeTransport( + cetl::pmr::memory_resource& memory, + IMultiplexer& multiplexer, + const std::array media, // TODO: replace with `cetl::span` + 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/types.hpp b/include/libcyphal/types.hpp new file mode 100644 index 000000000..1f10daed5 --- /dev/null +++ b/include/libcyphal/types.hpp @@ -0,0 +1,72 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TYPES_HPP_INCLUDED +#define LIBCYPHAL_TYPES_HPP_INCLUDED + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace libcyphal +{ + +/// @brief The internal time representation is in microseconds. +/// +/// This is in line with the lizards that use `uint64_t`-typed microsecond counters throughout. +/// +struct MonotonicClock final +{ + using rep = std::int64_t; + using period = std::micro; + using duration = std::chrono::duration; + using time_point = std::chrono::time_point; + + static constexpr bool is_steady = true; + + /// @brief Gets the current time point. + /// + /// Method is NOT implemented by the library; the user code is expected to provide a suitable implementation + /// instead depending on the requirements of the application. + /// A possible implementation on a POSIX-like platform is: + /// ``` + /// MonotonicClock::time_point MonotonicClock::now() noexcept + /// { + /// return std::chrono::time_point_cast(std::chrono::steady_clock::now()); + /// } + /// ``` + CETL_NODISCARD static time_point now() noexcept; + +}; // MonotonicClock + +using TimePoint = MonotonicClock::time_point; +using Duration = MonotonicClock::duration; + +template +using UniquePtr = cetl::pmr::Factory::unique_ptr_t>; + +// TODO: Maybe introduce `cetl::expected` at CETL repo. +template +using Expected = cetl::variant; + +namespace detail +{ +template +using PmrAllocator = cetl::pmr::polymorphic_allocator; + +template +using VarArray = cetl::VariableLengthArray>; + +} // namespace detail + +} // namespace libcyphal + +#endif // LIBCYPHAL_TYPES_HPP_INCLUDED diff --git a/test/unittest/CMakeLists.txt b/test/unittest/CMakeLists.txt index 55480ec46..163f0b78b 100644 --- a/test/unittest/CMakeLists.txt +++ b/test/unittest/CMakeLists.txt @@ -18,11 +18,11 @@ endif() # We generate individual test binaires so we can record which test generated # what coverage. We also allow test authors to generate coverage reports for # just one test allowing for faster iteration. -file(GLOB NATIVE_TESTS +file(GLOB_RECURSE NATIVE_TESTS LIST_DIRECTORIES false CONFIGURE_DEPENDS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} - test_*.cpp + test_*.cpp **/test_*.cpp ) set(ALL_TESTS_BUILD "") @@ -43,12 +43,16 @@ foreach(NATIVE_TEST ${NATIVE_TESTS}) list(APPEND ALL_TESTS_BUILD ${LOCAL_TEST_TARGET}) list(APPEND ALL_TESTS_RUN ${LOCAL_TEST_REPORT}) + # We need to exclude the "external" directory from coverage reports. + cmake_path(APPEND LIBCYPHAL_ROOT "external" OUTPUT_VARIABLE LIBCYPHAL_EXTERNAL_PATH) + if (CMAKE_BUILD_TYPE STREQUAL "Coverage") define_gcovr_tracefile_target( TARGET ${LOCAL_TEST_TARGET} ROOT_DIRECTORY ${LIBCYPHAL_ROOT} TARGET_EXECUTION_DEPENDS ${LOCAL_TEST_REPORT} OBJECT_LIBRARY ${LOCAL_TEST_LIB} + EXCLUDE_PATHS ${LIBCYPHAL_EXTERNAL_PATH} EXCLUDE_TEST_FRAMEWORKS EXCLUDE_TARGET ENABLE_INSTRUMENTATION diff --git a/test/unittest/test_libcyphal.cpp b/test/unittest/test_libcyphal.cpp index 5171145bd..913759561 100644 --- a/test/unittest/test_libcyphal.cpp +++ b/test/unittest/test_libcyphal.cpp @@ -6,15 +6,14 @@ /// Copyright Amazon.com Inc. or its affiliates. /// SPDX-License-Identifier: MIT -#include "libcyphal/libcyphal.hpp" +#include #include -namespace { +namespace +{ // TODO: Add tests here -TEST(test_libcyphal, rename_me) -{ -} +TEST(test_libcyphal, rename_me) {} } // namespace diff --git a/test/unittest/transport/can/media_mock.hpp b/test/unittest/transport/can/media_mock.hpp new file mode 100644 index 000000000..d1f74ed9e --- /dev/null +++ b/test/unittest/transport/can/media_mock.hpp @@ -0,0 +1,43 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_CAN_MEDIA_MOCK_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_CAN_MEDIA_MOCK_HPP_INCLUDED + +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace can +{ + +class MediaMock : public IMedia +{ +public: + MediaMock() = default; + virtual ~MediaMock() = default; + + MOCK_METHOD(std::size_t, getMtu, (), (const, noexcept, override)); + MOCK_METHOD(cetl::optional, setFilters, (const Filters filters), (noexcept, override)); + MOCK_METHOD((Expected), + push, + (const TimePoint deadline, const CanId can_id, const cetl::span payload), + (noexcept, override)); + MOCK_METHOD((Expected, MediaError>), + pop, + (const cetl::span payload_buffer), + (noexcept, override)); + +}; // MediaMock + +} // namespace can +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_CAN_MEDIA_MOCK_HPP_INCLUDED diff --git a/test/unittest/transport/can/test_can_transport.cpp b/test/unittest/transport/can/test_can_transport.cpp new file mode 100644 index 000000000..43f809798 --- /dev/null +++ b/test/unittest/transport/can/test_can_transport.cpp @@ -0,0 +1,147 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "media_mock.hpp" +#include "../multiplexer_mock.hpp" +#include + +#include + +#include +#include + +namespace +{ +using namespace libcyphal; +using namespace libcyphal::transport; +using namespace libcyphal::transport::can; + +using testing::StrictMock; + +TEST(test_can_transport, makeTransport_getLocalNodeId) +{ + auto mr = cetl::pmr::new_delete_resource(); + + StrictMock mux_mock{}; + StrictMock media_mock{}; + + // Anonymous node + { + auto maybe_transport = makeTransport(*mr, mux_mock, {&media_mock}, {}); + EXPECT_FALSE(cetl::get_if(&maybe_transport)); + + auto transport = cetl::get>(std::move(maybe_transport)); + EXPECT_TRUE(transport); + EXPECT_EQ(cetl::nullopt, transport->getLocalNodeId()); + } + + // Node with ID + { + const auto node_id = cetl::make_optional(static_cast(42)); + + auto maybe_transport = makeTransport(*mr, mux_mock, {&media_mock}, node_id); + EXPECT_FALSE(cetl::get_if(&maybe_transport)); + + auto transport = cetl::get>(std::move(maybe_transport)); + EXPECT_TRUE(transport); + EXPECT_EQ(42, transport->getLocalNodeId().value()); + } + + // Two media interfaces + { + StrictMock media_mock2; + + auto maybe_transport = makeTransport(*mr, mux_mock, {&media_mock, nullptr, &media_mock2}, {}); + EXPECT_FALSE(cetl::get_if(&maybe_transport)); + + auto transport = cetl::get>(std::move(maybe_transport)); + EXPECT_TRUE(transport); + } + + // All 3 maximum number of media interfaces + { + StrictMock media_mock2{}, media_mock3{}; + + auto maybe_transport = makeTransport(*mr, mux_mock, {&media_mock, &media_mock2, &media_mock3}, {}); + EXPECT_FALSE(cetl::get_if(&maybe_transport)); + + auto transport = cetl::get>(std::move(maybe_transport)); + EXPECT_TRUE(transport); + } +} + +TEST(test_can_transport, makeTransport_with_invalid_arguments) +{ + auto mr = cetl::pmr::new_delete_resource(); + + StrictMock mux_mock{}; + StrictMock media_mock{}; + + // No media + { + const auto node_id = cetl::make_optional(static_cast(CANARD_NODE_ID_MAX)); + + const auto maybe_transport = makeTransport(*mr, mux_mock, {}, node_id); + EXPECT_TRUE(cetl::get_if(cetl::get_if(&maybe_transport))); + } + + // try just a bit bigger than max canard id (aka 128) + { + const auto node_id = cetl::make_optional(static_cast(CANARD_NODE_ID_MAX + 1)); + + const auto maybe_transport = makeTransport(*mr, mux_mock, {&media_mock}, node_id); + EXPECT_TRUE(cetl::get_if(cetl::get_if(&maybe_transport))); + } + + // magic 255 number (aka CANARD_NODE_ID_UNSET) can't be used as well + { + const auto node_id = cetl::make_optional(static_cast(CANARD_NODE_ID_UNSET)); + + const auto maybe_transport = makeTransport(*mr, mux_mock, {&media_mock}, node_id); + EXPECT_TRUE(cetl::get_if(cetl::get_if(&maybe_transport))); + } + + // just in case try 0x100 (aka overflow) + { + const NodeId too_big = static_cast(std::numeric_limits::max()) + 1; + const auto node_id = cetl::make_optional(too_big); + + const auto maybe_transport = makeTransport(*mr, mux_mock, {&media_mock}, node_id); + EXPECT_TRUE(cetl::get_if(cetl::get_if(&maybe_transport))); + } +} + +TEST(test_can_transport, getProtocolParams) +{ + auto mr = cetl::pmr::new_delete_resource(); + + StrictMock mux_mock{}; + StrictMock media_mock1{}, media_mock2{}; + + auto transport = + cetl::get>(makeTransport(*mr, mux_mock, {&media_mock1, &media_mock2}, {})); + + EXPECT_CALL(media_mock1, getMtu()).WillRepeatedly(testing::Return(CANARD_MTU_CAN_FD)); + EXPECT_CALL(media_mock2, getMtu()).WillRepeatedly(testing::Return(CANARD_MTU_CAN_CLASSIC)); + + auto params = transport->getProtocolParams(); + EXPECT_EQ(1 << CANARD_TRANSFER_ID_BIT_LENGTH, params.transfer_id_modulo); + EXPECT_EQ(CANARD_NODE_ID_MAX + 1, params.max_nodes); + EXPECT_EQ(CANARD_MTU_CAN_CLASSIC, params.mtu_bytes); + + // Manipulate MTU values on fly + { + EXPECT_CALL(media_mock2, getMtu()).WillRepeatedly(testing::Return(CANARD_MTU_CAN_FD)); + EXPECT_EQ(CANARD_MTU_CAN_FD, transport->getProtocolParams().mtu_bytes); + + EXPECT_CALL(media_mock1, getMtu()).WillRepeatedly(testing::Return(CANARD_MTU_CAN_CLASSIC)); + EXPECT_EQ(CANARD_MTU_CAN_CLASSIC, transport->getProtocolParams().mtu_bytes); + + EXPECT_CALL(media_mock2, getMtu()).WillRepeatedly(testing::Return(CANARD_MTU_CAN_CLASSIC)); + EXPECT_EQ(CANARD_MTU_CAN_CLASSIC, transport->getProtocolParams().mtu_bytes); + } +} + +} // namespace diff --git a/test/unittest/transport/multiplexer_mock.hpp b/test/unittest/transport/multiplexer_mock.hpp new file mode 100644 index 000000000..88ba9c5a6 --- /dev/null +++ b/test/unittest/transport/multiplexer_mock.hpp @@ -0,0 +1,28 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_MULTIPLEXER_MOCK_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_MULTIPLEXER_MOCK_HPP_INCLUDED + +#include + +#include + +namespace libcyphal +{ +namespace transport +{ + +class MultiplexerMock : public IMultiplexer +{ +public: + MultiplexerMock() = default; + virtual ~MultiplexerMock() = default; +}; + +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_MULTIPLEXER_MOCK_HPP_INCLUDED diff --git a/test/unittest/transport/test_scattered_buffer.cpp b/test/unittest/transport/test_scattered_buffer.cpp new file mode 100644 index 000000000..cf989c68b --- /dev/null +++ b/test/unittest/transport/test_scattered_buffer.cpp @@ -0,0 +1,127 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include + +#include + +namespace +{ +using cetl::type_id_type; +using cetl::rtti_helper; + +using ScatteredBuffer = libcyphal::transport::ScatteredBuffer; + +using testing::Return; +using testing::StrictMock; + +// Just random id: 277C3545-564C-4617-993D-27B1043ECEBA +using TestTypeIdType = + 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 +{ +public: + MOCK_METHOD(void, moved, ()); + MOCK_METHOD(void, deinit, ()); + + 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 +{ +public: + explicit InterfaceWrapper(InterfaceMock* mock) + : mock_{mock} + { + } + InterfaceWrapper(InterfaceWrapper&& other) noexcept + { + move_from(std::move(other)); + } + InterfaceWrapper& operator=(InterfaceWrapper&& other) noexcept + { + move_from(std::move(other)); + return *this; + } + ~InterfaceWrapper() override + { + if (mock_ != nullptr) + { + mock_->deinit(); + mock_ = nullptr; + } + } + + // ScatteredBuffer::Interface + + CETL_NODISCARD std::size_t size() const noexcept override + { + return mock_ ? mock_->size() : 0; + } + CETL_NODISCARD std::size_t copy(const std::size_t offset_bytes, + void* const destination, + const std::size_t length_bytes) const override + { + return mock_ ? mock_->copy(offset_bytes, destination, length_bytes) : 0; + } + +private: + InterfaceMock* mock_ = nullptr; + + void move_from(InterfaceWrapper&& other) + { + mock_ = other.mock_; + other.mock_ = nullptr; + + if (mock_ != nullptr) + { + mock_->moved(); + } + } + +}; // InterfaceWrapper + +TEST(TestScatteredBuffer, move_ctor_assign_size) +{ + StrictMock 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)); + { + ScatteredBuffer src{InterfaceWrapper{&interface_mock}}; //< +1 move + EXPECT_EQ(42, src.size()); + + ScatteredBuffer dst{std::move(src)}; //< +2 moves b/c of `cetl::any` specifics (via swap with tmp) + EXPECT_EQ(0, src.size()); + EXPECT_EQ(42, dst.size()); + + src = std::move(dst); //< +2 moves + EXPECT_EQ(42, src.size()); + EXPECT_EQ(0, dst.size()); + } +} + +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)); + { + ScatteredBuffer buffer{InterfaceWrapper{&interface_mock}}; + + auto copied_bytes = buffer.copy(13, test_dst.data(), test_dst.size()); + EXPECT_EQ(7, copied_bytes); + + buffer.reset(); + copied_bytes = buffer.copy(13, test_dst.data(), test_dst.size()); + EXPECT_EQ(0, copied_bytes); + } +} + +} // namespace diff --git a/test/unittest/transport/udp/media_mock.hpp b/test/unittest/transport/udp/media_mock.hpp new file mode 100644 index 000000000..3dd5f99ca --- /dev/null +++ b/test/unittest/transport/udp/media_mock.hpp @@ -0,0 +1,32 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#ifndef LIBCYPHAL_TRANSPORT_UDP_MEDIA_MOCK_HPP_INCLUDED +#define LIBCYPHAL_TRANSPORT_UDP_MEDIA_MOCK_HPP_INCLUDED + +#include + +#include + +namespace libcyphal +{ +namespace transport +{ +namespace udp +{ + +class MediaMock : public IMedia +{ +public: + MediaMock() = default; + virtual ~MediaMock() = default; + +}; // MediaMock + +} // namespace udp +} // namespace transport +} // namespace libcyphal + +#endif // LIBCYPHAL_TRANSPORT_UDP_MEDIA_MOCK_HPP_INCLUDED diff --git a/test/unittest/transport/udp/test_udp_transport.cpp b/test/unittest/transport/udp/test_udp_transport.cpp new file mode 100644 index 000000000..1a443fbc9 --- /dev/null +++ b/test/unittest/transport/udp/test_udp_transport.cpp @@ -0,0 +1,37 @@ +/// @copyright +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include "media_mock.hpp" +#include "../multiplexer_mock.hpp" +#include + +#include + +#include + +namespace +{ +using namespace libcyphal; +using namespace libcyphal::transport; +using namespace libcyphal::transport::udp; + +using testing::StrictMock; + +TEST(test_udp_transport, makeTransport) +{ + auto mr = cetl::pmr::new_delete_resource(); + + StrictMock media_mock{}; + StrictMock multiplex_mock{}; + + { + auto maybe_transport = makeTransport(*mr, multiplex_mock, {&media_mock}, {}); + + EXPECT_FALSE(cetl::get_if>(&maybe_transport)); + EXPECT_TRUE(cetl::get_if(cetl::get_if(&maybe_transport))); + } +} + +} // namespace