diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f24421a5..d3b20286 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,56 +1,10 @@ name: Main Workflow - -on: - push: - branches: [ master ] - pull_request: - branches: [ master, actions ] - +on: [push, pull_request] env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - + LLVM_VERSION: 13 jobs: debug: - env: - BUILD_TYPE: Debug - - runs-on: ubuntu-latest - - strategy: - matrix: - toolchain: ['clang', 'gcc'] - include: - - toolchain: gcc - c-compiler: gcc - cxx-compiler: g++ - - toolchain: clang - c-compiler: clang-11 - cxx-compiler: clang++-11 - - steps: - - uses: actions/checkout@v2 - - - name: Install Dependencies - run: sudo apt install gcc-multilib g++-multilib clang-tidy-11 clang-format-11 - - - name: Configure CMake - run: cmake -B ${{ github.workspace }}/build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DCMAKE_C_COMPILER=${{ matrix.c-compiler }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx-compiler }} tests - - - name: Build - working-directory: ${{github.workspace}}/build - run: make VERBOSE=1 - - - name: Test - working-directory: ${{github.workspace}}/build - run: make test - - release: - env: - BUILD_TYPE: Release - runs-on: ubuntu-latest - strategy: matrix: toolchain: ['clang', 'gcc'] @@ -59,95 +13,104 @@ jobs: c-compiler: gcc cxx-compiler: g++ - toolchain: clang - c-compiler: clang-11 - cxx-compiler: clang++-11 - + c-compiler: clang + cxx-compiler: clang++ steps: - uses: actions/checkout@v2 - - - name: Install Dependencies - run: sudo apt install gcc-multilib g++-multilib - - - name: Configure CMake - run: cmake -B ${{ github.workspace }}/build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DCMAKE_C_COMPILER=${{ matrix.c-compiler }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx-compiler }} -DNO_STATIC_ANALYSIS=1 tests - - - name: Build - working-directory: ${{github.workspace}}/build - run: make VERBOSE=1 - - - name: Test - working-directory: ${{github.workspace}}/build - run: make test - - minsizerel: - env: - BUILD_TYPE: MinSizeRel - + - run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh $LLVM_VERSION + sudo apt-get -y install gcc-multilib g++-multilib clang-tidy-$LLVM_VERSION + sudo update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-$LLVM_VERSION 50 + clang-tidy --version + - run: > + cmake + -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_C_COMPILER=${{ matrix.c-compiler }} + -DCMAKE_CXX_COMPILER=${{ matrix.cxx-compiler }} + tests + - working-directory: ${{github.workspace}}/build + run: | + make VERBOSE=1 + make test + - uses: actions/upload-artifact@v2 + if: always() + with: + name: ${{github.job}} + path: ${{github.workspace}}/**/* + retention-days: 2 + + optimizations: runs-on: ubuntu-latest - strategy: matrix: toolchain: ['clang', 'gcc'] + build_type: [Release, MinSizeRel] include: - toolchain: gcc c-compiler: gcc cxx-compiler: g++ - toolchain: clang - c-compiler: clang-11 - cxx-compiler: clang++-11 - + c-compiler: clang + cxx-compiler: clang++ steps: - uses: actions/checkout@v2 - - - name: Install Dependencies - run: sudo apt install gcc-multilib g++-multilib - - - name: Configure CMake - run: cmake -B ${{ github.workspace }}/build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DCMAKE_C_COMPILER=${{ matrix.c-compiler }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx-compiler }} -DNO_STATIC_ANALYSIS=1 tests - - - name: Build - working-directory: ${{github.workspace}}/build - run: make VERBOSE=1 - - - name: Test - working-directory: ${{github.workspace}}/build - run: make test + - run: sudo apt install gcc-multilib g++-multilib + - run: > + cmake + -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -DCMAKE_C_COMPILER=${{ matrix.c-compiler }} + -DCMAKE_CXX_COMPILER=${{ matrix.cxx-compiler }} + -DNO_STATIC_ANALYSIS=1 + tests + - working-directory: ${{github.workspace}}/build + run: | + make VERBOSE=1 + make test + - uses: actions/upload-artifact@v2 + if: always() + with: + name: ${{github.job}} + path: ${{github.workspace}}/**/* + retention-days: 2 avr: + runs-on: ubuntu-latest env: mcu: at90can64 flags: -Wall -Wextra -Werror -pedantic -Wconversion -Wtype-limits - + steps: + - uses: actions/checkout@v2 + - run: | + sudo apt install gcc-avr avr-libc + avr-gcc --version + - run: avr-gcc libcanard/*.c -c -std=c99 -mmcu=${{ env.mcu }} ${{ env.flags }} + - run: avr-gcc libcanard/*.c -c -std=c11 -mmcu=${{ env.mcu }} ${{ env.flags }} + - run: avr-gcc libcanard/*.c -c -std=gnu99 -mmcu=${{ env.mcu }} ${{ env.flags }} + - run: avr-gcc libcanard/*.c -c -std=gnu11 -mmcu=${{ env.mcu }} ${{ env.flags }} + + style_check: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v2 - - - name: Install Dependencies - run: sudo apt install gcc-avr avr-libc - - - name: Build C99 - run: avr-gcc libcanard/*.c -c -std=c99 -mmcu=${{ env.mcu }} ${{ env.flags }} - - - name: Build C11 - run: avr-gcc libcanard/*.c -c -std=c11 -mmcu=${{ env.mcu }} ${{ env.flags }} - - - name: Build GNU99 - run: avr-gcc libcanard/*.c -c -std=gnu99 -mmcu=${{ env.mcu }} ${{ env.flags }} - - - name: Build GNU11 - run: avr-gcc libcanard/*.c -c -std=gnu11 -mmcu=${{ env.mcu }} ${{ env.flags }} + - uses: DoozyX/clang-format-lint-action@v0.13 + with: + source: './libcanard ./tests' + exclude: './tests/catch' + extensions: 'c,h,cpp,hpp' + clangFormatVersion: ${{ env.LLVM_VERSION }} sonarcloud: + runs-on: ubuntu-latest env: SONAR_SCANNER_VERSION: 4.6.1.2450 SONAR_SERVER_URL: "https://sonarcloud.io" BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - runs-on: ubuntu-latest - steps: - uses: actions/checkout@v2 with: @@ -156,7 +119,7 @@ jobs: - name: Install Dependencies run: sudo apt install gcc-multilib g++-multilib - - name: Set up JDK 11 + - name: Set up JDK uses: actions/setup-java@v1 with: java-version: 11 @@ -173,7 +136,7 @@ jobs: SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip run: | mkdir -p $HOME/.sonar - curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }} + curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }} unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/ echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH @@ -190,10 +153,27 @@ jobs: cmake tests -DCMAKE_BUILD_TYPE=Debug -DNO_STATIC_ANALYSIS=1 -DCMAKE_C_FLAGS='-DNDEBUG=1' build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all make test - gcov-10 --preserve-paths --long-file-names $(find CMakeFiles/test_private_cov.dir -name '*.gcno') - gcov-10 --preserve-paths --long-file-names $(find CMakeFiles/test_private_le_cov.dir -name '*.gcno') - gcov-10 --preserve-paths --long-file-names $(find CMakeFiles/test_public_cov.dir -name '*.gcno') + gcov --preserve-paths --long-file-names $(find CMakeFiles/test_private_cov.dir -name '*.gcno') + gcov --preserve-paths --long-file-names $(find CMakeFiles/test_public_cov.dir -name '*.gcno') - name: Run sonar-scanner - run: | - sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" --define sonar.login=${{ secrets.SONAR_TOKEN }} + # Don't run sonar-scanner on builds originating from forks due to secrets not being available + run: > + [ -z "$SONAR_TOKEN" ] || sonar-scanner + --define sonar.organization=uavcan + --define sonar.projectName=libcanard + --define sonar.projectKey=libcanard + --define sonar.sources=libcanard + --define sonar.exclusions=libcanard/cavl.h + --define sonar.cfamily.gcov.reportsPath=. + --define sonar.cfamily.cache.enabled=false + --define sonar.cfamily.threads=2 + --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" + --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" + + - uses: actions/upload-artifact@v2 + if: always() + with: + name: ${{github.job}} + path: ${{github.workspace}}/**/* + retention-days: 2 diff --git a/.gitignore b/.gitignore index e6cd3319..f0e9f65e 100644 --- a/.gitignore +++ b/.gitignore @@ -48,8 +48,5 @@ build-avr/ !**/.idea/dictionaries !**/.idea/dictionaries/* -# Generated files -dsdlc_generated/ - # Pycache __pycache__/ diff --git a/.idea/dictionaries/pavel.xml b/.idea/dictionaries/pavel.xml index 40c9b88c..2a74d699 100644 --- a/.idea/dictionaries/pavel.xml +++ b/.idea/dictionaries/pavel.xml @@ -12,6 +12,7 @@ coverity crtp dataflow + dcanard ddtid deallocated deallocating @@ -46,6 +47,7 @@ msec multiframe nbytes + noninfringement nosonar nutt pdst @@ -66,9 +68,11 @@ snid socketcan storages + stringmakers submoduling subtreeing supremum + transcompilers uavcan uint unicast @@ -79,6 +83,8 @@ wget xenial zubax + zzzzz + zzzzzz \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea5f1ba2..3ae23201 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,9 +8,6 @@ The code shall follow applicable high-reliability coding guidelines as explained The implementation shall be fully compliant with the UAVCAN/CAN specification. The implementation and the API should be kept simple. -The core library `canard.c` (that is, excluding the optional DSDL presentation layer extension) shall never become -larger than 1000 logical lines of code. -This restriction ensures that the library is kept simple and easy to validate and verify. There will be no high-level abstractions -- if that is desired, other implementations of UAVCAN should be used. The library is intended for deeply embedded systems where the resources may be scarce. @@ -53,12 +50,13 @@ to prevent non-compliant code from being accepted into upstream. ## Tools -The following tools are required to conduct library development locally: +The following tools are required to conduct library development locally +(check the CI workflow files for the required versions): -- GCC v10 or newer. -- Clang and Clang-Tools v11 or newer. -- CMake v3.12 or newer. -- An AMD64 machine. +- GCC +- Clang and Clang-Tools +- CMake +- An AMD64 machine ### Clang-Tidy @@ -102,6 +100,10 @@ We would welcome contributions implementing CI/CD testing against popular embedd the ARM Cortex M series and AVR in an emulator. As a high-integrity library, the Libcanard test suite should provide full test coverage for all commonly used platforms. +**WARNING:** +[Catch2 is NOT thread-safe!](https://github.com/catchorg/Catch2/blob/1e379de9d7522b294e201700dcbb36d4f8037301/docs/limitations.md#thread-safe-assertions) +Never use `REQUIRE` etc. anywhere but the main thread. + ## Releasing Simply create a new release on GitHub: diff --git a/README.md b/README.md index 57a9b8c3..534e91e8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Compact UAVCAN/CAN v1 in C -[![Build Status](https://travis-ci.org/UAVCAN/libcanard.svg?branch=master)](https://travis-ci.org/UAVCAN/libcanard) +[![Main Workflow](https://github.com/UAVCAN/libcanard/actions/workflows/main.yml/badge.svg)](https://github.com/UAVCAN/libcanard/actions/workflows/main.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=libcanard&metric=alert_status)](https://sonarcloud.io/dashboard?id=libcanard) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=libcanard&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=libcanard) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=libcanard&metric=coverage)](https://sonarcloud.io/dashboard?id=libcanard) @@ -23,19 +23,19 @@ If you want to contribute, please read [`CONTRIBUTING.md`](/CONTRIBUTING.md). ## Features -- Full test coverage and static analysis. -- Partial compliance with automatically enforceable MISRA C rules (compliance report not available). +- Full test coverage and extensive static analysis. +- Compliance with automatically enforceable MISRA C rules (reach out to for details). - Detailed time complexity and memory requirement models for the benefit of real-time high-integrity applications. - Purely reactive API without the need for background servicing. - Support for the Classic CAN and CAN FD. - Support for redundant transports. - Compatibility with 8/16/32/64-bit platforms. -- Compatibility with extremely resource-constrained baremetal environments starting from ca. 32K ROM, 4..8K RAM. -- Implemented in less than 1500 logical lines of code. +- Compatibility with extremely resource-constrained baremetal environments starting from 32K ROM and 8K RAM. +- Implemented in ≈1000 lines of code. ## Platforms -The library is designed to be usable without modification with any conventional 8/16/32/64-bit platform, +The library is designed to be usable out of the box with any conventional 8/16/32/64-bit platform, including deeply embedded baremetal platforms, as long as there is a standard-compliant compiler available. The platform-specific media IO layer (driver) is supposed to be provided by the application: @@ -66,15 +66,15 @@ so let's suppose that we're using [O1Heap](https://github.com/pavel-kirienko/o1h We are going to need basic wrappers: ```c -static void* memAllocate(CanardInstance* const ins, const size_t amount) +static void* memAllocate(CanardInstance* const canard, const size_t amount) { - (void) ins; + (void) canard; return o1heapAllocate(my_allocator, amount); } -static void memFree(CanardInstance* const ins, void* const pointer) +static void memFree(CanardInstance* const canard, void* const pointer) { - (void) ins; + (void) canard; o1heapFree(my_allocator, pointer); } ``` @@ -82,73 +82,83 @@ static void memFree(CanardInstance* const ins, void* const pointer) Init a library instance: ```c -CanardInstance ins = canardInit(&memAllocate, &memFree); -ins.mtu_bytes = CANARD_MTU_CAN_CLASSIC; // Defaults to 64 (CAN FD); here we select Classic CAN. -ins.node_id = 42; // Defaults to anonymous; can be set up later at any point. +CanardInstance canard = canardInit(&memAllocate, &memFree); +canard.node_id = 42; // Defaults to anonymous; can be set up later at any point. ``` -Publish a message: +In order to be able to send transfers over the network, we will need one transmission queue per redundant CAN interface: + +```c +CanardTxQueue queue = canardTxInit(100, // Limit the size of the queue at 100 frames. + CANARD_MTU_CAN_FD); // Set MTU = 64 bytes. There is also CANARD_MTU_CAN_CLASSIC. +``` + +Publish a message (message serialization not shown): ```c static uint8_t my_message_transfer_id; // Must be static or heap-allocated to retain state between calls. -const CanardTransfer transfer = { - .timestamp_usec = transmission_deadline, // Zero if transmission deadline is not limited. +const CanardTransferMetadata transfer_metadata = { .priority = CanardPriorityNominal, .transfer_kind = CanardTransferKindMessage, .port_id = 1234, // This is the subject-ID. .remote_node_id = CANARD_NODE_ID_UNSET, // Messages cannot be unicast, so use UNSET. .transfer_id = my_message_transfer_id, - .payload_size = 47, - .payload = "\x2D\x00" "Sancho, it strikes me thou art in great fear.", }; ++my_message_transfer_id; // The transfer-ID shall be incremented after every transmission on this subject. -int32_t result = canardTxPush(&ins, &transfer); +int32_t result = canardTxPush(&queue, // Call this once per redundant CAN interface (queue). + &canard, + tx_deadline_usec, // Zero if transmission deadline is not limited. + &transfer_metadata, + 47, // Size of the message payload (see Nunavut transpiler). + "\x2D\x00" "Sancho, it strikes me thou art in great fear."); if (result < 0) { - // An error has occurred: either an argument is invalid or we've ran out of memory. + // An error has occurred: either an argument is invalid, the TX queue is full, or we've run out of memory. // It is possible to statically prove that an out-of-memory will never occur for a given application if the // heap is sized correctly; for background, refer to the Robson's Proof and the documentation for O1Heap. - abort(); } ``` -The CAN frames generated from the message transfer are now stored in the transmission queue. +Use [Nunavut](https://github.com/UAVCAN/nunavut) to automatically generate (de)serialization code from DSDL definitions. + +The CAN frames generated from the message transfer are now stored in the `queue`. We need to pick them out one by one and have them transmitted. Normally, the following fragment should be invoked periodically to unload the CAN frames from the prioritized transmission queue into the CAN driver (or several, if redundant interfaces are used): ```c -for (const CanardFrame* txf = NULL; (txf = canardTxPeek(&ins)) != NULL;) // Look at the top of the TX queue. +for (const CanardTxQueueItem* ti = NULL; (ti = canardTxPeek(&queue)) != NULL;) // Peek at the top of the queue. { - if ((0U == txf->timestamp_usec) || (txf->timestamp_usec > getCurrentMicroseconds())) // Check the deadline. + if ((0U == ti->tx_deadline_usec) || (ti->tx_deadline_usec > getCurrentMicroseconds())) // Check the deadline. { - if (!pleaseTransmit(txf)) // Send the frame. Redundant interfaces may be used here. + if (!pleaseTransmit(ti)) // Send the frame over this redundant CAN iface. { break; // If the driver is busy, break and retry later. } } - canardTxPop(&ins); // Remove the frame from the queue after it's transmitted. - ins.memory_free(&ins, (CanardFrame*)txf); // Deallocate the dynamic memory afterwards. + // After the frame is transmitted or if it has timed out while waiting, pop it from the queue and deallocate: + canard.memory_free(&canard, canardTxPop(&queue, ti)); } ``` -Transfer reception is done by feeding frames into the transfer reassembly state machine. +Transfer reception is done by feeding frames into the transfer reassembly state machine +from any of the redundant interfaces. But first, we need to subscribe: ```c CanardRxSubscription heartbeat_subscription; -(void) canardRxSubscribe(&ins, // Subscribe to messages uavcan.node.Heartbeat. +(void) canardRxSubscribe(&canard, // Subscribe to messages uavcan.node.Heartbeat. CanardTransferKindMessage, - 7509, // The fixed Subject-ID of the Heartbeat message type (see DSDL definition). - 16, // The extent (the maximum possible payload size); pick a huge value if not sure. + 7509, // The fixed Subject-ID of the Heartbeat message type (see DSDL definition). + 16, // The extent (the maximum possible payload size) provided by Nunavut. CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &heartbeat_subscription); CanardRxSubscription my_service_subscription; -(void) canardRxSubscribe(&ins, // Subscribe to an arbitrary service response. +(void) canardRxSubscribe(&canard, // Subscribe to an arbitrary service response. CanardTransferKindResponse, // Specify that we want service responses, not requests. - 123, // The Service-ID whose responses we will receive. - 1024, // The extent (the maximum payload size); pick a huge value if not sure. + 123, // The Service-ID whose responses we will receive. + 1024, // The extent (see above). CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, &my_service_subscription); ``` @@ -160,8 +170,7 @@ It is typically larger than the maximum object size in order to allow the data t fields in the future versions of the type; for example, `MyMessage.1.0` may have the maximum size of 100 bytes and the extent 200 bytes; a revised version `MyMessage.1.1` may have the maximum size anywhere between 0 and 200 bytes. -It is always safe to pick a larger value if not sure. -You will find a more rigorous description in the UAVCAN Specification. +Extent values are provided per data type by DSDL transcompilers such as Nunavut. In Libcanard we use the term "subscription" not only for subjects (messages), but also for services, for simplicity. @@ -170,23 +179,24 @@ Normally, however, an embedded application would subscribe once and roll with it Okay, this is how we receive transfers: ```c -CanardTransfer transfer; -const int8_t result = canardRxAccept(&ins, +CanardRxTransfer transfer; +const int8_t result = canardRxAccept(&canard, + rx_timestamp_usec, // When the frame was received, in microseconds. &received_frame, // The CAN frame received from the bus. redundant_interface_index, // If the transport is not redundant, use 0. - &transfer); + &transfer, + NULL); if (result < 0) { // An error has occurred: either an argument is invalid or we've ran out of memory. // It is possible to statically prove that an out-of-memory will never occur for a given application if // the heap is sized correctly; for background, refer to the Robson's Proof and the documentation for O1Heap. // Reception of an invalid frame is NOT an error. - abort(); } else if (result == 1) { processReceivedTransfer(redundant_interface_index, &transfer); // A transfer has been received, process it. - ins.memory_free(&ins, (void*)transfer.payload); // Deallocate the dynamic memory afterwards. + canard.memory_free(&canard, transfer.payload); // Deallocate the dynamic memory afterwards. } else { @@ -196,32 +206,23 @@ else } ``` -To automatically generate (de-)serialization code from DSDL definitions, -use [Nunavut](https://github.com/UAVCAN/nunavut). -If for some reason this is found undesirable, you may write (de-)serialization logic manually using -the optional tiny add-on for libcanard: `canard_dsdl.c`/`canard_dsdl.h`. -Here's a simple deserialization example for a `uavcan.node.Heartbeat.1.0` message: - -```c -uint8_t mode = canardDSDLGetU8(heartbeat_transfer->payload, heartbeat_transfer->payload_size, 40, 8); -uint32_t uptime = canardDSDLGetU32(heartbeat_transfer->payload, heartbeat_transfer->payload_size, 0, 32); -uint8_t vssc = canardDSDLGetU32(heartbeat_transfer->payload, heartbeat_transfer->payload_size, 48, 8); -uint8_t health = canardDSDLGetU8(heartbeat_transfer->payload, heartbeat_transfer->payload_size, 32, 8); -``` - -And the opposite: +A simple API for generating CAN hardware acceptance filter configurations is also provided. +Acceptance filters are generated in an extended 29-bit ID + mask scheme and can be used to minimize +the number of irrelevant transfers processed in software. ```c -uint8_t buffer[7]; -// destination offset value bit-length -canardDSDLSetUxx(&buffer[0], 40, 2, 8); // mode -canardDSDLSetUxx(&buffer[0], 0, 0xDEADBEEF, 32); // uptime -canardDSDLSetUxx(&buffer[0], 48, 0x7F, 8); // vssc -canardDSDLSetUxx(&buffer[0], 32, 2, 8); // health -// Now it can be transmitted: -my_transfer->payload = &buffer[0]; -my_transfer->payload_size = sizeof(buffer); -result = canardTxPush(&ins, &my_transfer); +// Generate an acceptance filter to receive only uavcan.node.Heartbeat.1.0 messages (fixed port-ID 7509): +CanardAcceptanceFilterConfig heartbeat_config = canardMakeAcceptanceFilterConfigForSubject(7509); +// And to receive only uavcan.register.Access.1.0 service transfers (fixed port-ID 384): +CanardAcceptanceFilterConfig register_access_config = canardMakeAcceptanceFilterConfigForService(384, ins.node_id); + +// You can also combine the two filter configurations into one (may also accept irrelevant messages). +// This allows consolidating a large set of configurations to fit the number of hardware filters. +// For more information on the optimal subset of configurations to consolidate to minimize wasted CPU, +// see the UAVCAN specification. +CanardAcceptanceFilterConfig combined_config = + canardConsolidateAcceptanceFilterConfigs(&heartbeat_config, ®ister_access_config); +configureHardwareFilters(combined_config.extended_can_id, combined_config.extended_mask); ``` Full API specification is available in the documentation. @@ -229,6 +230,30 @@ If you find the examples to be unclear or incorrect, please, open a ticket. ## Revisions +### v2.0 + +- Dedicated transmission queues per redundant CAN interface with depth limits. + The application is now expected to instantiate `CanardTxQueue` (or several in case of redundant transport) manually. + +- Replace O(n) linked lists with fast O(log n) AVL trees + ([Cavl](https://github.com/pavel-kirienko/cavl) library is distributed with libcanard). + Traversing the list of RX subscriptions now requires recursive traversal of the tree. + +- Manual DSDL serialization helpers removed; use [Nunavut](https://github.com/UAVCAN/nunavut) instead. + +- Replace bitwise CRC computation with much faster static table by default + ([#185](https://github.com/UAVCAN/libcanard/issues/185)). + This can be disabled by setting `CANARD_CRC_TABLE=0`, which is expected to save ca. 500 bytes of ROM. + +- Fixed issues with const-correctness in the API ([#175](https://github.com/UAVCAN/libcanard/issues/175)). + +- `canardRxAccept2()` renamed to `canardRxAccept()`. + +- Support build configuration headers via `CANARD_CONFIG_HEADER`. + +- Add API for generating CAN hardware acceptance filter configurations + ([#169](https://github.com/UAVCAN/libcanard/issues/169)). + ### v1.1 - Add new API function `canardRxAccept2()`, deprecate `canardRxAccept()`. diff --git a/libcanard/.clang-tidy b/libcanard/.clang-tidy index 0652d10b..d7f291d9 100644 --- a/libcanard/.clang-tidy +++ b/libcanard/.clang-tidy @@ -15,7 +15,12 @@ Checks: >- readability-*, -google-readability-todo, -readability-avoid-const-params-in-decls, + -readability-identifier-length, + -bugprone-easily-swappable-parameters, -llvm-header-guard, + -cert-dcl03-c, + -hicpp-static-assert, + -misc-static-assert, CheckOptions: - key: readability-function-cognitive-complexity.Threshold value: '99' diff --git a/libcanard/canard.c b/libcanard/canard.c index 79a88e07..685e8fc2 100644 --- a/libcanard/canard.c +++ b/libcanard/canard.c @@ -1,30 +1,44 @@ /// This software is distributed under the terms of the MIT License. -/// Copyright (c) 2016-2020 UAVCAN Development Team. -/// Author: Pavel Kirienko +/// Copyright (c) 2016 UAVCAN Consortium. +/// Author: Pavel Kirienko #include "canard.h" -#include +#include "cavl.h" #include // --------------------------------------------- BUILD CONFIGURATION --------------------------------------------- +/// Define this macro to include build configuration header. +/// Usage example with CMake: "-DCANARD_CONFIG_HEADER=\"${CMAKE_CURRENT_SOURCE_DIR}/my_canard_config.h\"" +#ifdef CANARD_CONFIG_HEADER +# include CANARD_CONFIG_HEADER +#endif + /// By default, this macro resolves to the standard assert(). The user can redefine this if necessary. /// To disable assertion checks completely, make it expand into `(void)(0)`. #ifndef CANARD_ASSERT +// Intentional violation of MISRA: inclusion not at the top of the file to eliminate unnecessary dependency on assert.h. +# include // NOSONAR // Intentional violation of MISRA: assertion macro cannot be replaced with a function definition. # define CANARD_ASSERT(x) assert(x) // NOSONAR #endif -/// This macro is needed only for testing and for library development. Do not redefine this in production. +/// Define CANARD_CRC_TABLE=0 to use slow but ROM-efficient transfer-CRC computation algorithm. +/// Doing so is expected to save ca. 500 bytes of ROM and increase the cost of RX/TX transfer processing by ~half. +#ifndef CANARD_CRC_TABLE +# define CANARD_CRC_TABLE 1 +#endif + +/// This macro is needed for testing and for library development. #ifndef CANARD_PRIVATE -# define CANARD_PRIVATE static +# define CANARD_PRIVATE static inline #endif #if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) # error "Unsupported language: ISO C99 or a newer version is required." #endif -// --------------------------------------------- COMMON CONSTANTS --------------------------------------------- +// --------------------------------------------- COMMON DEFINITIONS --------------------------------------------- #define BITS_PER_BYTE 8U #define BYTE_MAX 0xFFU @@ -52,6 +66,12 @@ #define INITIAL_TOGGLE_STATE true +/// Used for inserting new items into AVL trees. +CANARD_PRIVATE CanardTreeNode* avlTrivialFactory(void* const user_reference) +{ + return (CanardTreeNode*) user_reference; +} + // --------------------------------------------- TRANSFER CRC --------------------------------------------- typedef uint16_t TransferCRC; @@ -60,22 +80,52 @@ typedef uint16_t TransferCRC; #define CRC_RESIDUE 0x0000U #define CRC_SIZE_BYTES 2U +#if (CANARD_CRC_TABLE != 0) +static const uint16_t CRCTable[256] = { + 0x0000U, 0x1021U, 0x2042U, 0x3063U, 0x4084U, 0x50A5U, 0x60C6U, 0x70E7U, 0x8108U, 0x9129U, 0xA14AU, 0xB16BU, 0xC18CU, + 0xD1ADU, 0xE1CEU, 0xF1EFU, 0x1231U, 0x0210U, 0x3273U, 0x2252U, 0x52B5U, 0x4294U, 0x72F7U, 0x62D6U, 0x9339U, 0x8318U, + 0xB37BU, 0xA35AU, 0xD3BDU, 0xC39CU, 0xF3FFU, 0xE3DEU, 0x2462U, 0x3443U, 0x0420U, 0x1401U, 0x64E6U, 0x74C7U, 0x44A4U, + 0x5485U, 0xA56AU, 0xB54BU, 0x8528U, 0x9509U, 0xE5EEU, 0xF5CFU, 0xC5ACU, 0xD58DU, 0x3653U, 0x2672U, 0x1611U, 0x0630U, + 0x76D7U, 0x66F6U, 0x5695U, 0x46B4U, 0xB75BU, 0xA77AU, 0x9719U, 0x8738U, 0xF7DFU, 0xE7FEU, 0xD79DU, 0xC7BCU, 0x48C4U, + 0x58E5U, 0x6886U, 0x78A7U, 0x0840U, 0x1861U, 0x2802U, 0x3823U, 0xC9CCU, 0xD9EDU, 0xE98EU, 0xF9AFU, 0x8948U, 0x9969U, + 0xA90AU, 0xB92BU, 0x5AF5U, 0x4AD4U, 0x7AB7U, 0x6A96U, 0x1A71U, 0x0A50U, 0x3A33U, 0x2A12U, 0xDBFDU, 0xCBDCU, 0xFBBFU, + 0xEB9EU, 0x9B79U, 0x8B58U, 0xBB3BU, 0xAB1AU, 0x6CA6U, 0x7C87U, 0x4CE4U, 0x5CC5U, 0x2C22U, 0x3C03U, 0x0C60U, 0x1C41U, + 0xEDAEU, 0xFD8FU, 0xCDECU, 0xDDCDU, 0xAD2AU, 0xBD0BU, 0x8D68U, 0x9D49U, 0x7E97U, 0x6EB6U, 0x5ED5U, 0x4EF4U, 0x3E13U, + 0x2E32U, 0x1E51U, 0x0E70U, 0xFF9FU, 0xEFBEU, 0xDFDDU, 0xCFFCU, 0xBF1BU, 0xAF3AU, 0x9F59U, 0x8F78U, 0x9188U, 0x81A9U, + 0xB1CAU, 0xA1EBU, 0xD10CU, 0xC12DU, 0xF14EU, 0xE16FU, 0x1080U, 0x00A1U, 0x30C2U, 0x20E3U, 0x5004U, 0x4025U, 0x7046U, + 0x6067U, 0x83B9U, 0x9398U, 0xA3FBU, 0xB3DAU, 0xC33DU, 0xD31CU, 0xE37FU, 0xF35EU, 0x02B1U, 0x1290U, 0x22F3U, 0x32D2U, + 0x4235U, 0x5214U, 0x6277U, 0x7256U, 0xB5EAU, 0xA5CBU, 0x95A8U, 0x8589U, 0xF56EU, 0xE54FU, 0xD52CU, 0xC50DU, 0x34E2U, + 0x24C3U, 0x14A0U, 0x0481U, 0x7466U, 0x6447U, 0x5424U, 0x4405U, 0xA7DBU, 0xB7FAU, 0x8799U, 0x97B8U, 0xE75FU, 0xF77EU, + 0xC71DU, 0xD73CU, 0x26D3U, 0x36F2U, 0x0691U, 0x16B0U, 0x6657U, 0x7676U, 0x4615U, 0x5634U, 0xD94CU, 0xC96DU, 0xF90EU, + 0xE92FU, 0x99C8U, 0x89E9U, 0xB98AU, 0xA9ABU, 0x5844U, 0x4865U, 0x7806U, 0x6827U, 0x18C0U, 0x08E1U, 0x3882U, 0x28A3U, + 0xCB7DU, 0xDB5CU, 0xEB3FU, 0xFB1EU, 0x8BF9U, 0x9BD8U, 0xABBBU, 0xBB9AU, 0x4A75U, 0x5A54U, 0x6A37U, 0x7A16U, 0x0AF1U, + 0x1AD0U, 0x2AB3U, 0x3A92U, 0xFD2EU, 0xED0FU, 0xDD6CU, 0xCD4DU, 0xBDAAU, 0xAD8BU, 0x9DE8U, 0x8DC9U, 0x7C26U, 0x6C07U, + 0x5C64U, 0x4C45U, 0x3CA2U, 0x2C83U, 0x1CE0U, 0x0CC1U, 0xEF1FU, 0xFF3EU, 0xCF5DU, 0xDF7CU, 0xAF9BU, 0xBFBAU, 0x8FD9U, + 0x9FF8U, 0x6E17U, 0x7E36U, 0x4E55U, 0x5E74U, 0x2E93U, 0x3EB2U, 0x0ED1U, 0x1EF0U, +}; +#endif + CANARD_PRIVATE TransferCRC crcAddByte(const TransferCRC crc, const uint8_t byte) { +#if (CANARD_CRC_TABLE != 0) + return (uint16_t) ((uint16_t) (crc << BITS_PER_BYTE) ^ + CRCTable[(uint16_t) ((uint16_t) (crc >> BITS_PER_BYTE) ^ byte) & BYTE_MAX]); +#else static const TransferCRC Top = 0x8000U; static const TransferCRC Poly = 0x1021U; - TransferCRC out = crc ^ (uint16_t)((uint16_t)(byte) << BITS_PER_BYTE); + TransferCRC out = crc ^ (uint16_t) ((uint16_t) (byte) << BITS_PER_BYTE); // Consider adding a compilation option that replaces this with a CRC table. Adds 512 bytes of ROM. // Do not fold this into a loop because a size-optimizing compiler won't unroll it degrading the performance. - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); - out = (uint16_t)((uint16_t)(out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); + out = (uint16_t) ((uint16_t) (out << 1U) ^ (((out & Top) != 0U) ? Poly : 0U)); return out; +#endif } CANARD_PRIVATE TransferCRC crcAdd(const TransferCRC crc, const size_t size, const void* const data) @@ -93,22 +143,29 @@ CANARD_PRIVATE TransferCRC crcAdd(const TransferCRC crc, const size_t size, cons // --------------------------------------------- TRANSMISSION --------------------------------------------- -/// This is a subclass of CanardFrame. A pointer to this type can be cast to CanardFrame safely. +/// This is a subclass of CanardTxQueueItem. A pointer to this type can be cast to CanardTxQueueItem safely. /// This is standard-compliant. The paragraph 6.7.2.1.15 says: /// A pointer to a structure object, suitably converted, points to its initial member (or if that member is a /// bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a /// structure object, but not at its beginning. -typedef struct CanardInternalTxQueueItem +typedef struct TxItem { - CanardFrame frame; - struct CanardInternalTxQueueItem* next; + CanardTxQueueItem base; // Intentional violation of MISRA: this flex array is the lesser of three evils. The other two are: // - Make the payload pointer point to the remainder of the allocated memory following this structure. // The pointer is bad because it requires us to use pointer arithmetics. // - Use a separate memory allocation for data. This is terribly wasteful (both time & memory). uint8_t payload_buffer[]; // NOSONAR -} CanardInternalTxQueueItem; +} TxItem; + +/// Chain of TX frames prepared for insertion into a TX queue. +typedef struct +{ + TxItem* head; + TxItem* tail; + size_t size; +} TxChain; CANARD_PRIVATE uint32_t txMakeMessageSessionSpecifier(const CanardPortID subject_id, const CanardNodeID src_node_id) { @@ -132,17 +189,17 @@ CANARD_PRIVATE uint32_t txMakeServiceSessionSpecifier(const CanardPortID service } /// This is the transport MTU rounded up to next full DLC minus the tail byte. -CANARD_PRIVATE size_t txGetPresentationLayerMTU(const CanardInstance* const ins) +CANARD_PRIVATE size_t adjustPresentationLayerMTU(const size_t mtu_bytes) { const size_t max_index = (sizeof(CanardCANLengthToDLC) / sizeof(CanardCANLengthToDLC[0])) - 1U; size_t mtu = 0U; - if (ins->mtu_bytes < CANARD_MTU_CAN_CLASSIC) + if (mtu_bytes < CANARD_MTU_CAN_CLASSIC) { mtu = CANARD_MTU_CAN_CLASSIC; } - else if (ins->mtu_bytes <= max_index) + else if (mtu_bytes <= max_index) { - mtu = CanardCANDLCToLength[CanardCANLengthToDLC[ins->mtu_bytes]]; // Round up to nearest valid length. + mtu = CanardCANDLCToLength[CanardCANLengthToDLC[mtu_bytes]]; // Round up to nearest valid length. } else { @@ -151,9 +208,11 @@ CANARD_PRIVATE size_t txGetPresentationLayerMTU(const CanardInstance* const ins) return mtu - 1U; } -CANARD_PRIVATE int32_t txMakeCANID(const CanardTransfer* const tr, - const CanardNodeID local_node_id, - const size_t presentation_layer_mtu) +CANARD_PRIVATE int32_t txMakeCANID(const CanardTransferMetadata* const tr, + const size_t payload_size, + const void* const payload, + const CanardNodeID local_node_id, + const size_t presentation_layer_mtu) { CANARD_ASSERT(tr != NULL); CANARD_ASSERT(presentation_layer_mtu > 0); @@ -166,12 +225,11 @@ CANARD_PRIVATE int32_t txMakeCANID(const CanardTransfer* const tr, out = (int32_t) txMakeMessageSessionSpecifier(tr->port_id, local_node_id); CANARD_ASSERT(out >= 0); } - else if (tr->payload_size <= presentation_layer_mtu) + else if (payload_size <= presentation_layer_mtu) { - CANARD_ASSERT((tr->payload != NULL) || (tr->payload_size == 0U)); - const CanardNodeID c = - (CanardNodeID)(crcAdd(CRC_INITIAL, tr->payload_size, tr->payload) & CANARD_NODE_ID_MAX); - const uint32_t spec = txMakeMessageSessionSpecifier(tr->port_id, c) | FLAG_ANONYMOUS_MESSAGE; + CANARD_ASSERT((payload != NULL) || (payload_size == 0U)); + const CanardNodeID c = (CanardNodeID) (crcAdd(CRC_INITIAL, payload_size, payload) & CANARD_NODE_ID_MAX); + const uint32_t spec = txMakeMessageSessionSpecifier(tr->port_id, c) | FLAG_ANONYMOUS_MESSAGE; CANARD_ASSERT(spec <= CAN_EXT_ID_MASK); out = (int32_t) spec; } @@ -223,8 +281,9 @@ CANARD_PRIVATE uint8_t txMakeTailByte(const bool start_of_transfer, const CanardTransferID transfer_id) { CANARD_ASSERT(start_of_transfer ? (toggle == INITIAL_TOGGLE_STATE) : true); - return (uint8_t)((start_of_transfer ? TAIL_START_OF_TRANSFER : 0U) | (end_of_transfer ? TAIL_END_OF_TRANSFER : 0U) | - (toggle ? TAIL_TOGGLE : 0U) | (transfer_id & CANARD_TRANSFER_ID_MAX)); + return (uint8_t) ((start_of_transfer ? TAIL_START_OF_TRANSFER : 0U) | + (end_of_transfer ? TAIL_END_OF_TRANSFER : 0U) | (toggle ? TAIL_TOGGLE : 0U) | + (transfer_id & CANARD_TRANSFER_ID_MAX)); } /// Takes a frame payload size, returns a new size that is >=x and is rounded up to the nearest valid DLC. @@ -237,51 +296,47 @@ CANARD_PRIVATE size_t txRoundFramePayloadSizeUp(const size_t x) return CanardCANDLCToLength[y]; } -CANARD_PRIVATE CanardInternalTxQueueItem* txAllocateQueueItem(CanardInstance* const ins, - const uint32_t id, - const CanardMicrosecond deadline_usec, - const size_t payload_size) +/// The item is only allocated and initialized, but NOT included into the queue! The caller needs to do that. +CANARD_PRIVATE TxItem* txAllocateQueueItem(CanardInstance* const ins, + const uint32_t id, + const CanardMicrosecond deadline_usec, + const size_t payload_size) { CANARD_ASSERT(ins != NULL); CANARD_ASSERT(payload_size > 0U); - CanardInternalTxQueueItem* const out = - (CanardInternalTxQueueItem*) ins->memory_allocate(ins, sizeof(CanardInternalTxQueueItem) + payload_size); + TxItem* const out = (TxItem*) ins->memory_allocate(ins, sizeof(TxItem) + payload_size); if (out != NULL) { - out->next = NULL; - out->frame.timestamp_usec = deadline_usec; - out->frame.payload_size = payload_size; - out->frame.payload = out->payload_buffer; - out->frame.extended_can_id = id; + out->base.base.up = NULL; + out->base.base.lr[0] = NULL; + out->base.base.lr[1] = NULL; + out->base.base.bf = 0; + + out->base.next_in_transfer = NULL; // Last by default. + out->base.tx_deadline_usec = deadline_usec; + + out->base.frame.payload_size = payload_size; + out->base.frame.payload = out->payload_buffer; + out->base.frame.extended_can_id = id; } return out; } -/// Returns the element after which new elements with the specified CAN ID should be inserted. -/// Returns NULL if the element shall be inserted in the beginning of the list (i.e., no prior elements). -CANARD_PRIVATE CanardInternalTxQueueItem* txFindQueueSupremum(const CanardInstance* const ins, const uint32_t can_id) +/// Frames with identical CAN ID that are added later always compare greater than their counterparts with same CAN ID. +/// This ensures that CAN frames with the same CAN ID are transmitted in the FIFO order. +/// Frames that should be transmitted earlier compare smaller (i.e., put on the left side of the tree). +CANARD_PRIVATE int8_t txAVLPredicate(void* const user_reference, // NOSONAR Cavl API requires pointer to non-const. + const CanardTreeNode* const node) { - CANARD_ASSERT(ins != NULL); - CANARD_ASSERT(can_id <= CAN_EXT_ID_MASK); - CanardInternalTxQueueItem* out = ins->_tx_queue; - if ((NULL == out) || (out->frame.extended_can_id > can_id)) - { - out = NULL; - } - else - { - // TODO The linear search should be replaced with O(log n) at least. Please help us here. - while ((out != NULL) && (out->next != NULL) && (out->next->frame.extended_can_id <= can_id)) - { - out = out->next; - } - } - CANARD_ASSERT((out == NULL) || (out->frame.extended_can_id <= can_id)); - return out; + const CanardTxQueueItem* const target = (const CanardTxQueueItem*) user_reference; + const CanardTxQueueItem* const other = (const CanardTxQueueItem*) node; + CANARD_ASSERT((target != NULL) && (other != NULL)); + return (target->frame.extended_can_id >= other->frame.extended_can_id) ? +1 : -1; } /// Returns the number of frames enqueued or error (i.e., =1 or <0). -CANARD_PRIVATE int32_t txPushSingleFrame(CanardInstance* const ins, +CANARD_PRIVATE int32_t txPushSingleFrame(CanardTxQueue* const que, + CanardInstance* const ins, const CanardMicrosecond deadline_usec, const uint32_t can_id, const CanardTransferID transfer_id, @@ -290,14 +345,13 @@ CANARD_PRIVATE int32_t txPushSingleFrame(CanardInstance* const ins, { CANARD_ASSERT(ins != NULL); CANARD_ASSERT((payload != NULL) || (payload_size == 0)); - const size_t frame_payload_size = txRoundFramePayloadSizeUp(payload_size + 1U); CANARD_ASSERT(frame_payload_size > payload_size); const size_t padding_size = frame_payload_size - payload_size - 1U; CANARD_ASSERT((padding_size + payload_size + 1U) == frame_payload_size); - int32_t out = 0; - - CanardInternalTxQueueItem* const tqi = txAllocateQueueItem(ins, can_id, deadline_usec, frame_payload_size); + int32_t out = 0; + TxItem* const tqi = + (que->size < que->capacity) ? txAllocateQueueItem(ins, can_id, deadline_usec, frame_payload_size) : NULL; if (tqi != NULL) { if (payload_size > 0U) // The check is needed to avoid calling memcpy() with a NULL pointer, it's an UB. @@ -307,23 +361,16 @@ CANARD_PRIVATE int32_t txPushSingleFrame(CanardInstance* const ins, // We ignore it because the safe functions are poorly supported; reliance on them may limit the portability. (void) memcpy(&tqi->payload_buffer[0], payload, payload_size); // NOLINT } - // Clang-Tidy raises an error recommending the use of memset_s() instead. // We ignore it because the safe functions are poorly supported; reliance on them may limit the portability. (void) memset(&tqi->payload_buffer[payload_size], PADDING_BYTE_VALUE, padding_size); // NOLINT - tqi->payload_buffer[frame_payload_size - 1U] = txMakeTailByte(true, true, true, transfer_id); - CanardInternalTxQueueItem* const sup = txFindQueueSupremum(ins, can_id); - if (sup != NULL) - { - tqi->next = sup->next; - sup->next = tqi; - } - else - { - tqi->next = ins->_tx_queue; - ins->_tx_queue = tqi; - } + // Insert the newly created TX item into the queue. + const CanardTreeNode* const res = cavlSearch(&que->root, &tqi->base.base, &txAVLPredicate, &avlTrivialFactory); + (void) res; + CANARD_ASSERT(res == &tqi->base.base); + que->size++; + CANARD_ASSERT(que->size <= que->capacity); out = 1; // One frame enqueued. } else @@ -334,51 +381,44 @@ CANARD_PRIVATE int32_t txPushSingleFrame(CanardInstance* const ins, return out; } -/// Returns the number of frames enqueued or error. -CANARD_PRIVATE int32_t txPushMultiFrame(CanardInstance* const ins, - const size_t presentation_layer_mtu, - const CanardMicrosecond deadline_usec, - const uint32_t can_id, - const CanardTransferID transfer_id, - const size_t payload_size, - const void* const payload) +/// Produces a chain of Tx queue items for later insertion into the Tx queue. The tail is NULL if OOM. +CANARD_PRIVATE TxChain txGenerateMultiFrameChain(CanardInstance* const ins, + const size_t presentation_layer_mtu, + const CanardMicrosecond deadline_usec, + const uint32_t can_id, + const CanardTransferID transfer_id, + const size_t payload_size, + const void* const payload) { CANARD_ASSERT(ins != NULL); CANARD_ASSERT(presentation_layer_mtu > 0U); CANARD_ASSERT(payload_size > presentation_layer_mtu); // Otherwise, a single-frame transfer should be used. CANARD_ASSERT(payload != NULL); - int32_t out = 0; // The number of frames enqueued or negated error. - - CanardInternalTxQueueItem* head = NULL; // Head and tail of the linked list of frames of this transfer. - CanardInternalTxQueueItem* tail = NULL; - + TxChain out = {NULL, NULL, 0}; const size_t payload_size_with_crc = payload_size + CRC_SIZE_BYTES; size_t offset = 0U; TransferCRC crc = crcAdd(CRC_INITIAL, payload_size, payload); - bool start_of_transfer = true; bool toggle = INITIAL_TOGGLE_STATE; const uint8_t* payload_ptr = (const uint8_t*) payload; - while (offset < payload_size_with_crc) { - ++out; + out.size++; const size_t frame_payload_size_with_tail = ((payload_size_with_crc - offset) < presentation_layer_mtu) ? txRoundFramePayloadSizeUp((payload_size_with_crc - offset) + 1U) // Padding in the last frame only. : (presentation_layer_mtu + 1U); - CanardInternalTxQueueItem* const tqi = - txAllocateQueueItem(ins, can_id, deadline_usec, frame_payload_size_with_tail); - if (NULL == head) + TxItem* const tqi = txAllocateQueueItem(ins, can_id, deadline_usec, frame_payload_size_with_tail); + if (NULL == out.head) { - head = tqi; + out.head = tqi; } else { - tail->next = tqi; + out.tail->base.next_in_transfer = &tqi->base; } - tail = tqi; - if (NULL == tail) + out.tail = tqi; + if (NULL == out.tail) { break; } @@ -395,7 +435,8 @@ CANARD_PRIVATE int32_t txPushMultiFrame(CanardInstance* const ins, } // Clang-Tidy raises an error recommending the use of memcpy_s() instead. // We ignore it because the safe functions are poorly supported; reliance on them may limit the portability. - (void) memcpy(&tail->payload_buffer[0], payload_ptr, move_size); // NOLINT + // SonarQube incorrectly detects a buffer overflow here. + (void) memcpy(&out.tail->payload_buffer[0], payload_ptr, move_size); // NOLINT NOSONAR frame_offset = frame_offset + move_size; offset += move_size; payload_ptr += move_size; @@ -407,7 +448,7 @@ CANARD_PRIVATE int32_t txPushMultiFrame(CanardInstance* const ins, // Insert padding -- only in the last frame. Don't forget to include padding into the CRC. while ((frame_offset + CRC_SIZE_BYTES) < frame_payload_size) { - tail->payload_buffer[frame_offset] = PADDING_BYTE_VALUE; + out.tail->payload_buffer[frame_offset] = PADDING_BYTE_VALUE; ++frame_offset; crc = crcAddByte(crc, PADDING_BYTE_VALUE); } @@ -415,53 +456,90 @@ CANARD_PRIVATE int32_t txPushMultiFrame(CanardInstance* const ins, // Insert the CRC. if ((frame_offset < frame_payload_size) && (offset == payload_size)) { - tail->payload_buffer[frame_offset] = (uint8_t)(crc >> BITS_PER_BYTE); + // SonarQube incorrectly detects a buffer overflow here. + out.tail->payload_buffer[frame_offset] = (uint8_t) (crc >> BITS_PER_BYTE); // NOSONAR ++frame_offset; ++offset; } if ((frame_offset < frame_payload_size) && (offset > payload_size)) { - tail->payload_buffer[frame_offset] = (uint8_t)(crc & BYTE_MAX); + out.tail->payload_buffer[frame_offset] = (uint8_t) (crc & BYTE_MAX); ++frame_offset; ++offset; } } // Finalize the frame. - CANARD_ASSERT((frame_offset + 1U) == tail->frame.payload_size); - tail->payload_buffer[frame_offset] = - txMakeTailByte(start_of_transfer, offset >= payload_size_with_crc, toggle, transfer_id); - start_of_transfer = false; - toggle = !toggle; + CANARD_ASSERT((frame_offset + 1U) == out.tail->base.frame.payload_size); + // SonarQube incorrectly detects a buffer overflow here. + out.tail->payload_buffer[frame_offset] = // NOSONAR + txMakeTailByte(out.head == out.tail, offset >= payload_size_with_crc, toggle, transfer_id); + toggle = !toggle; } + return out; +} - if (tail != NULL) +/// Returns the number of frames enqueued or error. +CANARD_PRIVATE int32_t txPushMultiFrame(CanardTxQueue* const que, + CanardInstance* const ins, + const size_t presentation_layer_mtu, + const CanardMicrosecond deadline_usec, + const uint32_t can_id, + const CanardTransferID transfer_id, + const size_t payload_size, + const void* const payload) +{ + CANARD_ASSERT((ins != NULL) && (que != NULL)); + CANARD_ASSERT(presentation_layer_mtu > 0U); + CANARD_ASSERT(payload_size > presentation_layer_mtu); // Otherwise, a single-frame transfer should be used. + + int32_t out = 0; // The number of frames enqueued or negated error. + const size_t payload_size_with_crc = payload_size + CRC_SIZE_BYTES; + const size_t num_frames = ((payload_size_with_crc + presentation_layer_mtu) - 1U) / presentation_layer_mtu; + CANARD_ASSERT(num_frames >= 2); + if ((que->size + num_frames) <= que->capacity) // Bail early if we can see that we won't fit anyway. { - CANARD_ASSERT(head->next != NULL); // This is not a single-frame transfer so at least two frames shall exist. - CANARD_ASSERT(tail->next == NULL); // The list shall be properly terminated. - CanardInternalTxQueueItem* const sup = txFindQueueSupremum(ins, can_id); - if (NULL == sup) // Once the insertion point is located, we insert the entire frame sequence in constant time. + const TxChain sq = txGenerateMultiFrameChain(ins, + presentation_layer_mtu, + deadline_usec, + can_id, + transfer_id, + payload_size, + payload); + if (sq.tail != NULL) { - tail->next = ins->_tx_queue; - ins->_tx_queue = head; + CanardTxQueueItem* next = &sq.head->base; + do + { + const CanardTreeNode* const res = + cavlSearch(&que->root, &next->base, &txAVLPredicate, &avlTrivialFactory); + (void) res; + CANARD_ASSERT(res == &next->base); + CANARD_ASSERT(que->root != NULL); + next = next->next_in_transfer; + } while (next != NULL); + CANARD_ASSERT(num_frames == sq.size); + que->size += sq.size; + CANARD_ASSERT(que->size <= que->capacity); + CANARD_ASSERT((sq.size + 0ULL) <= INT32_MAX); // +0 is to suppress warning. + out = (int32_t) sq.size; } else { - tail->next = sup->next; - sup->next = head; + out = -CANARD_ERROR_OUT_OF_MEMORY; + CanardTxQueueItem* head = &sq.head->base; + while (head != NULL) + { + CanardTxQueueItem* const next = head->next_in_transfer; + ins->memory_free(ins, head); + head = next; + } } } - else // Failed to allocate at least one frame in the queue! Remove all frames and abort. + else // We predict that we're going to run out of queue, don't bother serializing the transfer. { out = -CANARD_ERROR_OUT_OF_MEMORY; - while (head != NULL) - { - CanardInternalTxQueueItem* const next = head->next; - ins->memory_free(ins, head); - head = next; - } } - CANARD_ASSERT((out < 0) || (out >= 2)); return out; } @@ -505,7 +583,9 @@ typedef struct } RxFrameModel; /// Returns truth if the frame is valid and parsed successfully. False if the frame is not a valid UAVCAN/CAN frame. -CANARD_PRIVATE bool rxTryParseFrame(const CanardFrame* const frame, RxFrameModel* const out) +CANARD_PRIVATE bool rxTryParseFrame(const CanardMicrosecond timestamp_usec, + const CanardFrame* const frame, + RxFrameModel* const out) { CANARD_ASSERT(frame != NULL); CANARD_ASSERT(frame->extended_can_id <= CAN_EXT_ID_MASK); @@ -514,16 +594,16 @@ CANARD_PRIVATE bool rxTryParseFrame(const CanardFrame* const frame, RxFrameModel if (frame->payload_size > 0) { CANARD_ASSERT(frame->payload != NULL); - out->timestamp_usec = frame->timestamp_usec; + out->timestamp_usec = timestamp_usec; // CAN ID parsing. const uint32_t can_id = frame->extended_can_id; - out->priority = (CanardPriority)((can_id >> OFFSET_PRIORITY) & CANARD_PRIORITY_MAX); - out->source_node_id = (CanardNodeID)(can_id & CANARD_NODE_ID_MAX); + out->priority = (CanardPriority) ((can_id >> OFFSET_PRIORITY) & CANARD_PRIORITY_MAX); + out->source_node_id = (CanardNodeID) (can_id & CANARD_NODE_ID_MAX); if (0 == (can_id & FLAG_SERVICE_NOT_MESSAGE)) { out->transfer_kind = CanardTransferKindMessage; - out->port_id = (CanardPortID)((can_id >> OFFSET_SUBJECT_ID) & CANARD_SUBJECT_ID_MAX); + out->port_id = (CanardPortID) ((can_id >> OFFSET_SUBJECT_ID) & CANARD_SUBJECT_ID_MAX); if ((can_id & FLAG_ANONYMOUS_MESSAGE) != 0) { out->source_node_id = CANARD_NODE_ID_UNSET; @@ -536,8 +616,8 @@ CANARD_PRIVATE bool rxTryParseFrame(const CanardFrame* const frame, RxFrameModel { out->transfer_kind = ((can_id & FLAG_REQUEST_NOT_RESPONSE) != 0) ? CanardTransferKindRequest : CanardTransferKindResponse; - out->port_id = (CanardPortID)((can_id >> OFFSET_SERVICE_ID) & CANARD_SERVICE_ID_MAX); - out->destination_node_id = (CanardNodeID)((can_id >> OFFSET_DST_NODE_ID) & CANARD_NODE_ID_MAX); + out->port_id = (CanardPortID) ((can_id >> OFFSET_SERVICE_ID) & CANARD_SERVICE_ID_MAX); + out->destination_node_id = (CanardNodeID) ((can_id >> OFFSET_DST_NODE_ID) & CANARD_NODE_ID_MAX); // The reserved bit may be unreserved in the future. It may be used to extend the service-ID to 10 bits. // Per Specification, source cannot be the same as the destination. valid = (0 == (can_id & FLAG_RESERVED_23)) && (out->source_node_id != out->destination_node_id); @@ -569,18 +649,17 @@ CANARD_PRIVATE bool rxTryParseFrame(const CanardFrame* const frame, RxFrameModel return valid; } -CANARD_PRIVATE void rxInitTransferFromFrame(const RxFrameModel* const frame, CanardTransfer* const out_transfer) +CANARD_PRIVATE void rxInitTransferMetadataFromFrame(const RxFrameModel* const frame, + CanardTransferMetadata* const out_transfer) { CANARD_ASSERT(frame != NULL); CANARD_ASSERT(frame->payload != NULL); CANARD_ASSERT(out_transfer != NULL); - out_transfer->timestamp_usec = frame->timestamp_usec; out_transfer->priority = frame->priority; out_transfer->transfer_kind = frame->transfer_kind; out_transfer->port_id = frame->port_id; out_transfer->remote_node_id = frame->source_node_id; out_transfer->transfer_id = frame->transfer_id; - // Payload not populated. } /// The implementation is borrowed from the Specification. @@ -588,11 +667,11 @@ CANARD_PRIVATE uint8_t rxComputeTransferIDDifference(const uint8_t a, const uint { CANARD_ASSERT(a <= CANARD_TRANSFER_ID_MAX); CANARD_ASSERT(b <= CANARD_TRANSFER_ID_MAX); - int16_t diff = (int16_t)(((int16_t) a) - ((int16_t) b)); + int16_t diff = (int16_t) (((int16_t) a) - ((int16_t) b)); if (diff < 0) { const uint8_t modulo = 1U << CANARD_TRANSFER_ID_BIT_LENGTH; - diff = (int16_t)(diff + (int16_t) modulo); + diff = (int16_t) (diff + (int16_t) modulo); } return (uint8_t) diff; } @@ -658,7 +737,7 @@ CANARD_PRIVATE void rxSessionRestart(CanardInstance* const ins, CanardInternalRx rxs->payload_size = 0U; rxs->payload = NULL; rxs->calculated_crc = CRC_INITIAL; - rxs->transfer_id = (CanardTransferID)((rxs->transfer_id + 1U) & CANARD_TRANSFER_ID_MAX); + rxs->transfer_id = (CanardTransferID) ((rxs->transfer_id + 1U) & CANARD_TRANSFER_ID_MAX); // The transport index is retained. rxs->toggle = INITIAL_TOGGLE_STATE; } @@ -667,7 +746,7 @@ CANARD_PRIVATE int8_t rxSessionAcceptFrame(CanardInstance* const ins, CanardInternalRxSession* const rxs, const RxFrameModel* const frame, const size_t extent, - CanardTransfer* const out_transfer) + CanardRxTransfer* const out_transfer) { CANARD_ASSERT(ins != NULL); CANARD_ASSERT(rxs != NULL); @@ -701,7 +780,7 @@ CANARD_PRIVATE int8_t rxSessionAcceptFrame(CanardInstance* const ins, if (single_frame || (CRC_RESIDUE == rxs->calculated_crc)) { out = 1; // One transfer received, notify the application. - rxInitTransferFromFrame(frame, out_transfer); + rxInitTransferMetadataFromFrame(frame, &out_transfer->metadata); out_transfer->timestamp_usec = rxs->transfer_timestamp_usec; out_transfer->payload_size = rxs->payload_size; out_transfer->payload = rxs->payload; @@ -739,7 +818,7 @@ CANARD_PRIVATE int8_t rxSessionUpdate(CanardInstance* const ins, const uint8_t redundant_transport_index, const CanardMicrosecond transfer_id_timeout_usec, const size_t extent, - CanardTransfer* const out_transfer) + CanardRxTransfer* const out_transfer) { CANARD_ASSERT(ins != NULL); CANARD_ASSERT(rxs != NULL); @@ -788,7 +867,7 @@ CANARD_PRIVATE int8_t rxAcceptFrame(CanardInstance* const ins, CanardRxSubscription* const subscription, const RxFrameModel* const frame, const uint8_t redundant_transport_index, - CanardTransfer* const out_transfer) + CanardRxTransfer* const out_transfer) { CANARD_ASSERT(ins != NULL); CANARD_ASSERT(subscription != NULL); @@ -804,11 +883,11 @@ CANARD_PRIVATE int8_t rxAcceptFrame(CanardInstance* const ins, { // If such session does not exist, create it. This only makes sense if this is the first frame of a // transfer, otherwise, we won't be able to receive the transfer anyway so we don't bother. - if ((NULL == subscription->_sessions[frame->source_node_id]) && frame->start_of_transfer) + if ((NULL == subscription->sessions[frame->source_node_id]) && frame->start_of_transfer) { CanardInternalRxSession* const rxs = (CanardInternalRxSession*) ins->memory_allocate(ins, sizeof(CanardInternalRxSession)); - subscription->_sessions[frame->source_node_id] = rxs; + subscription->sessions[frame->source_node_id] = rxs; if (rxs != NULL) { rxs->transfer_timestamp_usec = frame->timestamp_usec; @@ -826,11 +905,11 @@ CANARD_PRIVATE int8_t rxAcceptFrame(CanardInstance* const ins, } } // There are two possible reasons why the session may not exist: 1. OOM; 2. SOT-miss. - if (subscription->_sessions[frame->source_node_id] != NULL) + if (subscription->sessions[frame->source_node_id] != NULL) { CANARD_ASSERT(out == 0); out = rxSessionUpdate(ins, - subscription->_sessions[frame->source_node_id], + subscription->sessions[frame->source_node_id], frame, redundant_transport_index, subscription->transfer_id_timeout_usec, @@ -849,9 +928,10 @@ CANARD_PRIVATE int8_t rxAcceptFrame(CanardInstance* const ins, void* const payload = ins->memory_allocate(ins, payload_size); if (payload != NULL) { - rxInitTransferFromFrame(frame, out_transfer); - out_transfer->payload_size = payload_size; - out_transfer->payload = payload; + rxInitTransferMetadataFromFrame(frame, &out_transfer->metadata); + out_transfer->timestamp_usec = frame->timestamp_usec; + out_transfer->payload_size = payload_size; + out_transfer->payload = payload; // Clang-Tidy raises an error recommending the use of memcpy_s() instead. // We ignore it because the safe functions are poorly supported; reliance on them may limit the portability. (void) memcpy(payload, frame->payload, payload_size); // NOLINT @@ -865,6 +945,24 @@ CANARD_PRIVATE int8_t rxAcceptFrame(CanardInstance* const ins, return out; } +CANARD_PRIVATE int8_t +rxSubscriptionPredicateOnPortID(void* const user_reference, // NOSONAR Cavl API requires pointer to non-const. + const CanardTreeNode* const node) +{ + const CanardPortID sought = *((const CanardPortID*) user_reference); + const CanardPortID other = ((const CanardRxSubscription*) node)->port_id; + static const int8_t NegPos[2] = {-1, +1}; + // Clang-Tidy mistakenly identifies a narrowing cast to int8_t here, which is incorrect. + return (sought == other) ? 0 : NegPos[sought > other]; // NOLINT no narrowing conversion is taking place here +} + +CANARD_PRIVATE int8_t +rxSubscriptionPredicateOnStruct(void* const user_reference, // NOSONAR Cavl API requires pointer to non-const. + const CanardTreeNode* const node) +{ + return rxSubscriptionPredicateOnPortID(&((CanardRxSubscription*) user_reference)->port_id, node); +} + // --------------------------------------------- PUBLIC API --------------------------------------------- const uint8_t CanardCANDLCToLength[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64}; @@ -885,43 +983,62 @@ CanardInstance canardInit(const CanardMemoryAllocate memory_allocate, const Cana CANARD_ASSERT(memory_free != NULL); const CanardInstance out = { .user_reference = NULL, - .mtu_bytes = CANARD_MTU_CAN_FD, .node_id = CANARD_NODE_ID_UNSET, .memory_allocate = memory_allocate, .memory_free = memory_free, .rx_subscriptions = {NULL, NULL, NULL}, - ._tx_queue = NULL, }; return out; } -int32_t canardTxPush(CanardInstance* const ins, const CanardTransfer* const transfer) +CanardTxQueue canardTxInit(const size_t capacity, const size_t mtu_bytes) +{ + CanardTxQueue out = { + .capacity = capacity, + .mtu_bytes = mtu_bytes, + .size = 0, + .root = NULL, + .user_reference = NULL, + }; + return out; +} + +int32_t canardTxPush(CanardTxQueue* const que, + CanardInstance* const ins, + const CanardMicrosecond tx_deadline_usec, + const CanardTransferMetadata* const metadata, + const size_t payload_size, + const void* const payload) { int32_t out = -CANARD_ERROR_INVALID_ARGUMENT; - if ((ins != NULL) && (transfer != NULL) && ((transfer->payload != NULL) || (0U == transfer->payload_size))) + if ((ins != NULL) && (que != NULL) && (metadata != NULL) && ((payload != NULL) || (0U == payload_size))) { - const size_t pl_mtu = txGetPresentationLayerMTU(ins); - const int32_t maybe_can_id = txMakeCANID(transfer, ins->node_id, pl_mtu); + const size_t pl_mtu = adjustPresentationLayerMTU(que->mtu_bytes); + const int32_t maybe_can_id = txMakeCANID(metadata, payload_size, payload, ins->node_id, pl_mtu); if (maybe_can_id >= 0) { - if (transfer->payload_size <= pl_mtu) + if (payload_size <= pl_mtu) { - out = txPushSingleFrame(ins, - transfer->timestamp_usec, + out = txPushSingleFrame(que, + ins, + tx_deadline_usec, (uint32_t) maybe_can_id, - transfer->transfer_id, - transfer->payload_size, - transfer->payload); + metadata->transfer_id, + payload_size, + payload); + CANARD_ASSERT((out < 0) || (out == 1)); } else { - out = txPushMultiFrame(ins, + out = txPushMultiFrame(que, + ins, pl_mtu, - transfer->timestamp_usec, + tx_deadline_usec, (uint32_t) maybe_can_id, - transfer->transfer_id, - transfer->payload_size, - transfer->payload); + metadata->transfer_id, + payload_size, + payload); + CANARD_ASSERT((out < 0) || (out >= 2)); } } else @@ -929,65 +1046,69 @@ int32_t canardTxPush(CanardInstance* const ins, const CanardTransfer* const tran out = maybe_can_id; } } + CANARD_ASSERT(out != 0); return out; } -const CanardFrame* canardTxPeek(const CanardInstance* const ins) +const CanardTxQueueItem* canardTxPeek(const CanardTxQueue* const que) { - const CanardFrame* out = NULL; - if ((ins != NULL) && (ins->_tx_queue != NULL)) + const CanardTxQueueItem* out = NULL; + if (que != NULL) { - // Return pointer to the TX queue item typed as CanardFrame. Later, the application will be able to free - // the memory allocated for the TX queue item using this pointer typed as CanardFrame. Although it may look - // sketchy, this is actually safe and standard-compliant. The paragraph 6.7.2.1.15 of the C standard says: - // A pointer to a structure object, suitably converted, points to its initial member (or if that member is a - // bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a - // structure object, but not at its beginning. - out = &ins->_tx_queue->frame; - CANARD_ASSERT(((void*) out) == ((void*) ins->_tx_queue)); + // Paragraph 6.7.2.1.15 of the C standard says: + // A pointer to a structure object, suitably converted, points to its initial member, and vice versa. + out = (const CanardTxQueueItem*) cavlFindExtremum(que->root, false); } return out; } -void canardTxPop(CanardInstance* const ins) +CanardTxQueueItem* canardTxPop(CanardTxQueue* const que, const CanardTxQueueItem* const item) { - if ((ins != NULL) && (ins->_tx_queue != NULL)) + CanardTxQueueItem* out = NULL; + if ((que != NULL) && (item != NULL)) { - // The memory is NOT deallocated. The application is responsible for that. - ins->_tx_queue = ins->_tx_queue->next; + // Intentional violation of MISRA: casting away const qualifier. This is considered safe because the API + // contract dictates that the pointer shall point to a mutable entity in RAM previously allocated by the + // memory manager. It is difficult to avoid this cast in this context. + out = (CanardTxQueueItem*) item; // NOSONAR casting away const qualifier. + // Paragraph 6.7.2.1.15 of the C standard says: + // A pointer to a structure object, suitably converted, points to its initial member, and vice versa. + // Note that the highest-priority frame is always a leaf node in the AVL tree, which means that it is very + // cheap to remove. + cavlRemove(&que->root, &item->base); + que->size--; } + return out; } -int8_t canardRxAccept2(CanardInstance* const ins, - const CanardFrame* const frame, - const uint8_t redundant_transport_index, - CanardTransfer* const out_transfer, - CanardRxSubscription** const out_subscription) +int8_t canardRxAccept(CanardInstance* const ins, + const CanardMicrosecond timestamp_usec, + const CanardFrame* const frame, + const uint8_t redundant_transport_index, + CanardRxTransfer* const out_transfer, + CanardRxSubscription** const out_subscription) { int8_t out = -CANARD_ERROR_INVALID_ARGUMENT; if ((ins != NULL) && (out_transfer != NULL) && (frame != NULL) && (frame->extended_can_id <= CAN_EXT_ID_MASK) && ((frame->payload != NULL) || (0 == frame->payload_size))) { RxFrameModel model = {0}; - if (rxTryParseFrame(frame, &model)) + if (rxTryParseFrame(timestamp_usec, frame, &model)) { if ((CANARD_NODE_ID_UNSET == model.destination_node_id) || (ins->node_id == model.destination_node_id)) { - // Find subscription. This is the reason the function has a linear time complexity from the number of - // subscriptions. Note also that this one of the two variable-complexity operations in the RX pipeline; - // the other one is memcpy(). Excepting these two cases, the entire RX pipeline logic contains neither - // loops nor recursion. - CanardRxSubscription* sub = ins->rx_subscriptions[(size_t) model.transfer_kind]; - while ((sub != NULL) && (sub->port_id != model.port_id)) - { - sub = sub->next; - } - + // This is the reason the function has a logarithmic time complexity of the number of subscriptions. + // Note also that this one of the two variable-complexity operations in the RX pipeline; the other one + // is memcpy(). Excepting these two cases, the entire RX pipeline contains neither loops nor recursion. + CanardRxSubscription* const sub = + (CanardRxSubscription*) cavlSearch(&ins->rx_subscriptions[(size_t) model.transfer_kind], + &model.port_id, + &rxSubscriptionPredicateOnPortID, + NULL); if (out_subscription != NULL) { *out_subscription = sub; // Expose selected instance to the caller. } - if (sub != NULL) { CANARD_ASSERT(sub->port_id == model.port_id); @@ -1012,14 +1133,6 @@ int8_t canardRxAccept2(CanardInstance* const ins, return out; } -int8_t canardRxAccept(CanardInstance* const ins, - const CanardFrame* const frame, - const uint8_t redundant_transport_index, - CanardTransfer* const out_transfer) -{ - return canardRxAccept2(ins, frame, redundant_transport_index, out_transfer, NULL); -} - int8_t canardRxSubscribe(CanardInstance* const ins, const CanardTransferKind transfer_kind, const CanardPortID port_id, @@ -1037,19 +1150,23 @@ int8_t canardRxSubscribe(CanardInstance* const ins, out = canardRxUnsubscribe(ins, transfer_kind, port_id); if (out >= 0) { + out_subscription->transfer_id_timeout_usec = transfer_id_timeout_usec; + out_subscription->extent = extent; + out_subscription->port_id = port_id; for (size_t i = 0; i < RX_SESSIONS_PER_SUBSCRIPTION; i++) { // The sessions will be created ad-hoc. Normally, for a low-jitter deterministic system, // we could have pre-allocated sessions here, but that requires too much memory to be feasible. // We could accept an extra argument that would instruct us to pre-allocate sessions here? - out_subscription->_sessions[i] = NULL; + out_subscription->sessions[i] = NULL; } - out_subscription->transfer_id_timeout_usec = transfer_id_timeout_usec; - out_subscription->extent = extent; - out_subscription->port_id = port_id; - out_subscription->next = ins->rx_subscriptions[tk]; - ins->rx_subscriptions[tk] = out_subscription; - out = (out > 0) ? 0 : 1; + const CanardTreeNode* const res = cavlSearch(&ins->rx_subscriptions[tk], + out_subscription, + &rxSubscriptionPredicateOnStruct, + &avlTrivialFactory); + (void) res; + CANARD_ASSERT(res == &out_subscription->base); + out = (out > 0) ? 0 : 1; } } return out; @@ -1063,33 +1180,19 @@ int8_t canardRxUnsubscribe(CanardInstance* const ins, const size_t tk = (size_t) transfer_kind; if ((ins != NULL) && (tk < CANARD_NUM_TRANSFER_KINDS)) { - CanardRxSubscription* prv = NULL; - CanardRxSubscription* sub = ins->rx_subscriptions[tk]; - while ((sub != NULL) && (sub->port_id != port_id)) - { - prv = sub; - sub = sub->next; - } - + CanardPortID port_id_mutable = port_id; + CanardRxSubscription* const sub = (CanardRxSubscription*) + cavlSearch(&ins->rx_subscriptions[tk], &port_id_mutable, &rxSubscriptionPredicateOnPortID, NULL); if (sub != NULL) { + cavlRemove(&ins->rx_subscriptions[tk], &sub->base); CANARD_ASSERT(sub->port_id == port_id); out = 1; - - if (prv != NULL) - { - prv->next = sub->next; - } - else - { - ins->rx_subscriptions[tk] = sub->next; - } - for (size_t i = 0; i < RX_SESSIONS_PER_SUBSCRIPTION; i++) { - ins->memory_free(ins, (sub->_sessions[i] != NULL) ? sub->_sessions[i]->payload : NULL); - ins->memory_free(ins, sub->_sessions[i]); - sub->_sessions[i] = NULL; + ins->memory_free(ins, (sub->sessions[i] != NULL) ? sub->sessions[i]->payload : NULL); + ins->memory_free(ins, sub->sessions[i]); + sub->sessions[i] = NULL; } } else @@ -1099,3 +1202,45 @@ int8_t canardRxUnsubscribe(CanardInstance* const ins, } return out; } + +CanardFilter canardMakeFilterForSubject(const CanardPortID subject_id) +{ + CanardFilter out = {0}; + + out.extended_can_id = ((uint32_t) subject_id) << OFFSET_SUBJECT_ID; + out.extended_mask = FLAG_SERVICE_NOT_MESSAGE | FLAG_RESERVED_07 | (CANARD_SUBJECT_ID_MAX << OFFSET_SUBJECT_ID); + + return out; +} + +CanardFilter canardMakeFilterForService(const CanardPortID service_id, const CanardNodeID local_node_id) +{ + CanardFilter out = {0}; + + out.extended_can_id = FLAG_SERVICE_NOT_MESSAGE | (((uint32_t) service_id) << OFFSET_SERVICE_ID) | + (((uint32_t) local_node_id) << OFFSET_DST_NODE_ID); + out.extended_mask = FLAG_SERVICE_NOT_MESSAGE | FLAG_RESERVED_23 | (CANARD_SERVICE_ID_MAX << OFFSET_SERVICE_ID) | + (CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID); + + return out; +} + +CanardFilter canardMakeFilterForServices(const CanardNodeID local_node_id) +{ + CanardFilter out = {0}; + + out.extended_can_id = FLAG_SERVICE_NOT_MESSAGE | (((uint32_t) local_node_id) << OFFSET_DST_NODE_ID); + out.extended_mask = FLAG_SERVICE_NOT_MESSAGE | FLAG_RESERVED_23 | (CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID); + + return out; +} + +CanardFilter canardConsolidateFilters(const CanardFilter* a, const CanardFilter* b) +{ + CanardFilter out = {0}; + + out.extended_mask = a->extended_mask & b->extended_mask & ~(a->extended_can_id ^ b->extended_can_id); + out.extended_can_id = a->extended_can_id & out.extended_mask; + + return out; +} diff --git a/libcanard/canard.h b/libcanard/canard.h index 8d1916fe..c2737a8c 100644 --- a/libcanard/canard.h +++ b/libcanard/canard.h @@ -7,29 +7,23 @@ /// ----o------o------------o---------o------o---------o------- /// /// Libcanard is a compact implementation of the UAVCAN/CAN protocol for high-integrity real-time embedded systems. -/// It is designed for use in robust deterministic embedded systems equipped with at least 32K ROM and 4..8K RAM. +/// It is designed for use in robust deterministic embedded systems equipped with at least 32K ROM and 8K RAM. /// The codebase follows the MISRA C rules, has 100% test coverage, and is validated by at least two static analyzers. /// The library is designed to be compatible with any target platform and instruction set architecture, from 8 to 64 /// bit, little- and big-endian, RTOS-based or baremetal, etc., as long as there is a standards-compliant compiler. /// /// INTEGRATION /// -/// The library is intended to be integrated into the end application by simply copying the file canard.c into the +/// The library is intended to be integrated into the end application by simply copying its source files into the /// source tree of the project; it does not require any special compilation options and should work out of the box. -/// There are optional build configuration macros defined near the top of canard.c; they may be used to fine-tune -/// the library for the target platform (but it is not necessary). This header file should be located in the same -/// directory with canard.c, or its location should be in the include look-up paths of the compiler. +/// There are build-time configuration parameters defined near the top of canard.c, but they are safe to ignore. /// /// As explained in this documentation, the library requires a deterministic constant-time bounded-fragmentation dynamic /// memory allocator. If your target platform does not provide a deterministic memory manager (most platforms don't), /// it is recommended to use O1Heap (MIT licensed): https://github.com/pavel-kirienko/o1heap. /// -/// There is an optional two-file extension library canard_dsdl.c + canard_dsdl.h which can be used alongside -/// this core library to simplify DSDL object serialization and deserialization. It is intended to be integrated in -/// the same manner. Please read its usage manual for further information. -/// /// There are no specific requirements to the underlying I/O layer. Some low-level drivers maintained by the -/// UAVCAN Development Team may be found at https://github.com/UAVCAN/platform_specific_components. +/// UAVCAN Consortium may be found at https://github.com/UAVCAN/platform_specific_components. /// /// If your application requires a MISRA C compliance report, please get in touch with the maintainers via the forum /// at https://forum.uavcan.org. @@ -39,46 +33,38 @@ /// UAVCAN, as a protocol stack, is composed of two layers: TRANSPORT and PRESENTATION. The transport layer is portable /// across different transport protocols, one of which is CAN (FD), formally referred to as UAVCAN/CAN. This library /// is focused on UAVCAN/CAN only and it will not support other transports. The presentation layer is implemented -/// through the DSDL language and the associated data type regulation policies. Much like the UAVCAN stack itself, -/// this library consists of two major components: -/// -/// 1. TRANSPORT -- the UAVCAN/CAN transport layer implementation. This is implemented in canard.c/.h, -/// the documentation for which you are currently reading. This is the core component of the library. -/// -/// 2. PRESENTATION -- the optional DSDL support extension library. This is implemented in canard_dsdl.c/.h, -/// an optional component which may be used by some applications where automatic DSDL code generation is -/// not used. Normally, applications may prefer to rely on auto-generated code using DSDL-to-C translators -/// such as Nunavut (https://github.com/UAVCAN/nunavut). -/// -/// The DSDL extension is trivial and there is not much to document -- please refer to its header file for details. -/// -/// This transport layer implementation consists of two components: the transmission (TX) pipeline and the -/// reception (RX) pipeline. +/// through the DSDL language and the associated data type regulation policies; these parts are out of the scope of +/// this library as it is focused purely on the transport. /// -/// The TX and RX pipelines are completely independent from each other except that they both rely on the same -/// dynamic memory manager. The TX pipeline uses the dynamic memory to store outgoing CAN frames in the prioritized -/// transmission queue. The RX pipeline uses the dynamic memory to store contiguous payload buffers for received -/// transfers and for keeping the transfer reassembly state machine data. The exact memory consumption model is defined -/// for both pipelines, so it is possible to statically determine the minimum size of the dynamic memory pool required -/// to guarantee that a given application will never encounter an out-of-memory error at runtime. +/// This library consists of two components: the transmission (TX) pipeline and the reception (RX) pipeline. +/// The pipelines are completely independent from each other except that they both rely on the same dynamic memory +/// manager. The TX pipeline uses the dynamic memory to store outgoing CAN frames in the prioritized transmission +/// queue. The RX pipeline uses the dynamic memory to store contiguous payload buffers for received transfers and +/// for keeping the transfer reassembly state machine data. The exact memory consumption model is defined for both +/// pipelines, so it is possible to statically determine the minimum size of the dynamic memory pool required to +/// guarantee that a given application will never encounter an out-of-memory error at runtime. /// /// Much like with dynamic memory, the time complexity of every API function is well-characterized, allowing the /// application to guarantee predictable real-time performance. /// -/// The TX pipeline is managed with the help of three API functions. When the application needs to emit a transfer, -/// it invokes canardTxPush(). The function splits the transfer into CAN frames and stores them into the prioritized -/// transmission queue. The application then picks the CAN frames from the queue one-by-one by calling canardTxPeek() -/// followed by canardTxPop() -- the former allows the application to look at the frame and the latter tells the library -/// that the frame shall be removed from the queue. The returned frames need to be deallocated by the application. -/// -/// The RX pipeline is managed with the help of three API functions. The main function canardRxAccept() takes a -/// received CAN frame and updates the appropriate transfer reassembly state machine. The functions canardRxSubscribe() -/// and its counterpart canardRxUnsubscribe() instruct the library which transfers should be received (by default, all -/// transfers are ignored); also the subscription function specifies vital transfer reassembly parameters such as the -/// maximum payload size (i.e., the maximum size of a serialized representation of a DSDL object) and the transfer-ID -/// timeout. Transfers that carry more payload than the configured maximum per subscription are truncated following the -/// Implicit Truncation Rule (ITR) defined by the UAVCAN Specification -- the rule is implemented to facilitate -/// backward-compatible DSDL data type extensibility. +/// The TX pipeline is managed with the help of four API functions. The first one -- canardTxInit() -- is used for +/// constructing a new TX queue, of which there should be as many as there are redundant CAN interfaces; +/// each queue is managed independently. When the application needs to emit a transfer, it invokes canardTxPush() +/// on each queue separately. The function splits the transfer into CAN frames and stores them into the queue. +/// The application then picks the produced CAN frames from the queue one-by-one by calling canardTxPeek() followed +/// by canardTxPop() -- the former allows the application to look at the next frame scheduled for transmission, +/// and the latter tells the library that the frame shall be removed from the queue. +/// Popped frames need to be manually deallocated by the application upon transmission. +/// +/// The RX pipeline is managed with the help of three API functions; unlike the TX pipeline, there is one shared +/// state for all redundant interfaces that manages deduplication transparently. The main function canardRxAccept() +/// takes a received CAN frame and updates the appropriate transfer reassembly state machine. The functions +/// canardRxSubscribe() and its counterpart canardRxUnsubscribe() instruct the library which transfers should be +/// received (by default, all transfers are ignored); also, the subscription function specifies vital transfer +/// reassembly parameters such as the maximum payload size (i.e., the maximum size of a serialized representation +/// of a DSDL object) and the transfer-ID timeout. Transfers that carry more payload than the configured maximum per +/// subscription are truncated following the Implicit Truncation Rule (ITR) defined by the UAVCAN Specification -- +/// the rule is implemented to facilitate backward-compatible DSDL data type extensibility. /// /// The library supports a practically unlimited number of redundant transports. /// @@ -86,13 +72,13 @@ /// to provide adequate synchronization. /// /// The library is purely reactive: it does not perform any background processing and does not require periodic -/// servicing. Its internal state is only updated as a response to well-specified explicit API calls. +/// servicing. Its internal state is only updated as a response to well-specified external events. /// /// -------------------------------------------------------------------------------------------------------------------- /// /// This software is distributed under the terms of the MIT License. -/// Copyright (c) 2016-2020 UAVCAN Development Team. -/// Author: Pavel Kirienko +/// Copyright (c) 2016 UAVCAN Consortium. +/// Author: Pavel Kirienko /// Contributors: https://github.com/UAVCAN/libcanard/contributors. #ifndef CANARD_H_INCLUDED @@ -108,8 +94,8 @@ extern "C" { /// Semantic version of this library (not the UAVCAN specification). /// API will be backward compatible within the same major version. -#define CANARD_VERSION_MAJOR 1 -#define CANARD_VERSION_MINOR 1 +#define CANARD_VERSION_MAJOR 2 +#define CANARD_VERSION_MINOR 0 /// The version number of the UAVCAN specification implemented by this library. #define CANARD_UAVCAN_SPECIFICATION_VERSION_MAJOR 1 @@ -146,11 +132,13 @@ extern "C" { #define CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC 2000000UL // Forward declarations. -typedef struct CanardInstance CanardInstance; -typedef uint64_t CanardMicrosecond; -typedef uint16_t CanardPortID; -typedef uint8_t CanardNodeID; -typedef uint8_t CanardTransferID; +typedef struct CanardInstance CanardInstance; +typedef struct CanardTreeNode CanardTreeNode; +typedef struct CanardTxQueueItem CanardTxQueueItem; +typedef uint64_t CanardMicrosecond; +typedef uint16_t CanardPortID; +typedef uint8_t CanardNodeID; +typedef uint8_t CanardTransferID; /// Transfer priority level mnemonics per the recommendations given in the UAVCAN Specification. typedef enum @@ -174,15 +162,19 @@ typedef enum } CanardTransferKind; #define CANARD_NUM_TRANSFER_KINDS 3 +/// The AVL tree node structure is exposed here to avoid pointer casting/arithmetics inside the library. +/// The user code is not expected to interact with this type except if advanced introspection is required. +struct CanardTreeNode +{ + CanardTreeNode* up; ///< Do not access this field. + CanardTreeNode* lr[2]; ///< Left and right children of this node may be accessed for tree traversal. + int8_t bf; ///< Do not access this field. +}; + /// CAN data frame with an extended 29-bit ID. RTR/Error frames are not used and therefore not modeled here. /// CAN frames with 11-bit ID are not used by UAVCAN/CAN and so they are not supported by the library. typedef struct { - /// For RX frames: reception timestamp. - /// For TX frames: transmission deadline. - /// The time system may be arbitrary as long as the clock is monotonic (steady). - CanardMicrosecond timestamp_usec; - /// 29-bit extended ID. The bits above 29-th shall be zero. uint32_t extended_can_id; @@ -203,17 +195,12 @@ extern const uint8_t CanardCANDLCToLength[16]; /// Conversion look-up table from data length to CAN DLC; the length is rounded up. extern const uint8_t CanardCANLengthToDLC[65]; -/// A UAVCAN transfer model (either incoming or outgoing). +/// A UAVCAN transfer metadata (everything except the payload). /// Per Specification, a transfer is represented on the wire as a non-empty set of transport frames (i.e., CAN frames). /// The library is responsible for serializing transfers into transport frames when transmitting, and reassembling -/// transfers from an incoming stream of frames during reception. +/// transfers from an incoming stream of frames (possibly duplicated if redundant interfaces are used) during reception. typedef struct { - /// For RX transfers: reception timestamp. - /// For TX transfers: transmission deadline. - /// The time system may be arbitrary as long as the clock is monotonic (steady). - CanardMicrosecond timestamp_usec; - /// Per the Specification, all frames belonging to a given transfer shall share the same priority level. /// If this is not the case, then this field contains the priority level of the last frame to arrive. CanardPriority priority; @@ -245,54 +232,111 @@ typedef struct /// A simple and robust way of managing transfer-ID counters is to keep a separate static variable per subject-ID /// and per (service-ID, server-node-ID) pair. CanardTransferID transfer_id; +} CanardTransferMetadata; + +/// Prioritized transmission queue that keeps CAN frames destined for transmission via one CAN interface. +/// Applications with redundant interfaces are expected to have one instance of this type per interface. +/// Applications that are not interested in transmission may have zero queues. +/// All operations (push, peek, pop) are O(log n); there is exactly one heap allocation per element. +/// API functions that work with this type are named "canardTx*()", find them below. +typedef struct CanardTxQueue +{ + /// The maximum number of frames this queue is allowed to contain. An attempt to push more will fail with an + /// out-of-memory error even if the memory is not exhausted. This value can be changed by the user at any moment. + /// The purpose of this limitation is to ensure that a blocked queue does not exhaust the heap memory. + size_t capacity; - /// This is the actual transfer payload. - /// If the payload is empty (payload_size = 0), the payload pointer may be NULL. - /// The const pointer makes it incompatible with memory deallocation function, this is due to the limitations of C; - /// therefore, when freeing the memory allocated for the payload, cast away the pointer's const qualifier. - /// For RX transfers: the application is required to free the payload buffer after the transfer is processed. - /// For TX transfers: the library does not expect the lifetime of the payload buffer to extend beyond the point - /// of return from the API function because the payload is copied into the TX frame objects. - /// A more detailed overview of the dataflow and related resource management issues is provided in the API docs. - size_t payload_size; - const void* payload; -} CanardTransfer; + /// The transport-layer maximum transmission unit (MTU). The value can be changed arbitrarily at any time between + /// pushes. It defines the maximum number of data bytes per CAN data frame in outgoing transfers via this queue. + /// + /// Only the standard values should be used as recommended by the specification; + /// otherwise, networking interoperability issues may arise. See recommended values CANARD_MTU_*. + /// + /// Valid values are any valid CAN frame data length value not smaller than 8. + /// Invalid values are treated as the nearest valid value. The default is the maximum valid value. + size_t mtu_bytes; + + /// The number of frames that are currently contained in the queue, initially zero. + /// Do not modify this field! + size_t size; + + /// The root of the priority queue is NULL if the queue is empty. Do not modify this field! + CanardTreeNode* root; + + /// This field can be arbitrarily mutated by the user. It is never accessed by the library. + /// Its purpose is to simplify integration with OOP interfaces. + void* user_reference; +} CanardTxQueue; + +/// One frame stored in the transmission queue along with its metadata. +struct CanardTxQueueItem +{ + /// Internal use only; do not access this field. + CanardTreeNode base; + + /// Points to the next frame in this transfer or NULL. This field is mostly intended for own needs of the library. + /// Normally, the application would not use it because transfer frame ordering is orthogonal to global TX ordering. + /// It can be useful though for pulling pending frames from the TX queue if at least one frame of their transfer + /// failed to transmit; the idea is that if at least one frame is missing, the transfer will not be received by + /// remote nodes anyway, so all its remaining frames can be dropped from the queue at once using canardTxPop(). + CanardTxQueueItem* next_in_transfer; + + /// This is the same value that is passed to canardTxPush(). + /// Frames whose transmission deadline is in the past shall be dropped. + CanardMicrosecond tx_deadline_usec; + + /// The actual CAN frame data. + CanardFrame frame; +}; /// Transfer subscription state. The application can register its interest in a particular kind of data exchanged /// over the bus by creating such subscription objects. Frames that carry data for which there is no active -/// subscription will be silently dropped by the library. +/// subscription will be silently dropped by the library. The entire RX pipeline is invariant to the number of +/// redundant CAN interfaces used. /// -/// WARNING: SUBSCRIPTION INSTANCES SHALL NOT BE COPIED OR MUTATED BY THE APPLICATION (except user_reference). +/// SUBSCRIPTION INSTANCES SHALL NOT BE MOVED WHILE IN USE. /// /// The memory footprint of a subscription is large. On a 32-bit platform it slightly exceeds half a KiB. /// This is an intentional time-memory trade-off: use a large look-up table to ensure predictable temporal properties. typedef struct CanardRxSubscription { - struct CanardRxSubscription* next; ///< Read-only DO NOT MODIFY THIS + CanardTreeNode base; ///< Read-only DO NOT MODIFY THIS + + CanardMicrosecond transfer_id_timeout_usec; + size_t extent; ///< Read-only DO NOT MODIFY THIS + CanardPortID port_id; ///< Read-only DO NOT MODIFY THIS + + /// This field can be arbitrarily mutated by the user. It is never accessed by the library. + /// Its purpose is to simplify integration with OOP interfaces. + void* user_reference; /// The current architecture is an acceptable middle ground between worst-case execution time and memory /// consumption. Instead of statically pre-allocating a dedicated RX session for each remote node-ID here in /// this table, we only keep pointers, which are NULL by default, populating a new RX session dynamically - /// on an ad-hoc basis when we first receive a transfer from that node. This is deterministic because our memory - /// allocation routines are assumed to be deterministic and we make at most one allocation per remote node, - /// but the disadvantage is that these additional operations lift the upper bound on the execution time. - /// Further, the pointers here add an extra indirection, which is bad for systems that leverage cached memory, - /// plus a pointer itself takes about 2-8 bytes of memory, too. + /// on an ad-hoc basis when we first receive a transfer from that node. This is O(1) because our memory + /// allocation routines are assumed to be O(1) and we make at most one allocation per remote node. /// - /// A far more predictable and a much simpler approach is to pre-allocate states here statically instead of keeping + /// A more predictable and simpler approach is to pre-allocate states here statically instead of keeping /// just pointers, but it would push the size of this instance from about 0.5 KiB to ~3 KiB for a typical 32-bit /// system. Since this is a general-purpose library, we have to pick a middle ground so we use the more complex /// but more memory-efficient approach. - struct CanardInternalRxSession* _sessions[CANARD_NODE_ID_MAX + 1U]; + struct CanardInternalRxSession* sessions[CANARD_NODE_ID_MAX + 1U]; ///< Read-only DO NOT MODIFY THIS +} CanardRxSubscription; - CanardMicrosecond transfer_id_timeout_usec; ///< Read-only DO NOT MODIFY THIS - size_t extent; ///< Read-only DO NOT MODIFY THIS - CanardPortID port_id; ///< Read-only DO NOT MODIFY THIS +/// Reassembled incoming transfer returned by canardRxAccept(). +typedef struct CanardRxTransfer +{ + CanardTransferMetadata metadata; - /// This field can be arbitrarily mutated by the user. It is never accessed by the library. - /// Its purpose is to simplify integration with OOP interfaces. - void* user_reference; -} CanardRxSubscription; + /// The timestamp of the first received CAN frame of this transfer. + /// The time system may be arbitrary as long as the clock is monotonic (steady). + CanardMicrosecond timestamp_usec; + + /// If the payload is empty (payload_size = 0), the payload pointer may be NULL. + /// The application is required to deallocate the payload buffer after the transfer is processed. + size_t payload_size; + void* payload; +} CanardRxTransfer; /// A pointer to the memory allocation function. The semantics are similar to malloc(): /// - The returned pointer shall point to an uninitialized block of memory that is at least "amount" bytes large. @@ -312,7 +356,6 @@ typedef void* (*CanardMemoryAllocate)(CanardInstance* ins, size_t amount); typedef void (*CanardMemoryFree)(CanardInstance* ins, void* pointer); /// This is the core structure that keeps all of the states and allocated resources of the library instance. -/// The application may directly alter the fields whose names do not begin with an underscore. struct CanardInstance { /// User pointer that can link this instance with other objects. @@ -320,17 +363,6 @@ struct CanardInstance /// The default value is NULL. void* user_reference; - /// The transport-layer maximum transmission unit (MTU). The value can be changed arbitrarily at any time. - /// This setting defines the maximum number of bytes per CAN data frame in all outgoing transfers. - /// Regardless of this setting, CAN frames with any MTU can always be accepted. - /// - /// Only the standard values should be used as recommended by the specification; - /// otherwise, networking interoperability issues may arise. See recommended values CANARD_MTU_*. - /// - /// Valid values are any valid CAN frame data length value not smaller than 8. - /// Invalid values are treated as the nearest valid value. The default is the maximum valid value. - size_t mtu_bytes; - /// The node-ID of the local node. /// Per the UAVCAN Specification, the node-ID should not be assigned more than once. /// Invalid values are treated as CANARD_NODE_ID_UNSET. The default value is CANARD_NODE_ID_UNSET. @@ -348,37 +380,55 @@ struct CanardInstance CanardMemoryFree memory_free; /// Read-only DO NOT MODIFY THIS - CanardRxSubscription* rx_subscriptions[CANARD_NUM_TRANSFER_KINDS]; - - /// This field is for internal use only. Do not access from the application. - struct CanardInternalTxQueueItem* _tx_queue; + CanardTreeNode* rx_subscriptions[CANARD_NUM_TRANSFER_KINDS]; }; +/// CAN acceptance filter configuration with an extended 29-bit ID utilizing an ID + mask filter scheme. +/// Filter configuration can be programmed into a CAN controller to filter out irrelevant messages in hardware. +/// This allows the software application to reduce CPU load spent on processing irrelevant messages. +typedef struct CanardFilter +{ + /// 29-bit extended ID. Defines the extended CAN ID to filter incoming frames against. + /// The bits above 29-th shall be zero. + uint32_t extended_can_id; + /// 29-bit extended mask. Defines the bitmask used to enable/disable bits used to filter messages. + /// Only bits that are enabled are compared to the extended_can_id for filtering. + /// The bits above 29-th shall be zero. + uint32_t extended_mask; +} CanardFilter; + /// Construct a new library instance. /// The default values will be assigned as specified in the structure field documentation. /// If any of the pointers are NULL, the behavior is undefined. /// /// The instance does not hold any resources itself except for the allocated memory. -/// If the instance should be de-initialized, the application shall clear the TX queue by calling the pop function -/// repeatedly, and remove all RX subscriptions. Once that is done, the instance will be holding no memory resources, -/// so it can be discarded freely. +/// To safely discard it, simply remove all existing subscriptions, and don't forget about the TX queues. /// /// The time complexity is constant. This function does not invoke the dynamic memory manager. CanardInstance canardInit(const CanardMemoryAllocate memory_allocate, const CanardMemoryFree memory_free); +/// Construct a new transmission queue instance with the specified values for capacity and mtu_bytes. +/// No memory allocation is going to take place until the queue is actually pushed to. +/// Applications are expected to have one instance of this type per redundant interface. +/// +/// The instance does not hold any resources itself except for the allocated memory. +/// To safely discard it, simply pop all items from the queue. +/// +/// The time complexity is constant. This function does not invoke the dynamic memory manager. +CanardTxQueue canardTxInit(const size_t capacity, const size_t mtu_bytes); + /// This function serializes a transfer into a sequence of transport frames and inserts them into the prioritized /// transmission queue at the appropriate position. Afterwards, the application is supposed to take the enqueued frames /// from the transmission queue using the function canardTxPeek() and transmit them. Each transmitted (or otherwise /// discarded, e.g., due to timeout) frame should be removed from the queue using canardTxPop(). The queue is /// prioritized following the normal CAN frame arbitration rules to avoid the inner priority inversion. The transfer /// payload will be copied into the transmission queue so that the lifetime of the frames is not related to the -/// lifetime of the input transfer instance or its payload buffer. +/// lifetime of the input payload buffer. /// /// The MTU of the generated frames is dependent on the value of the MTU setting at the time when this function -/// is invoked. The MTU setting can be changed arbitrarily between invocations. No other functions rely on that -/// parameter. +/// is invoked. The MTU setting can be changed arbitrarily between invocations. /// -/// The timestamp value of the transfer will be used to populate the timestamp values of the resulting transport +/// The tx_deadline_usec will be used to populate the timestamp values of the resulting transport /// frames (so all frames will have the same timestamp value). This feature is intended to facilitate transmission /// deadline tracking, i.e., aborting frames that could not be transmitted before the specified deadline. /// Therefore, normally, the timestamp value should be in the future. @@ -401,29 +451,32 @@ CanardInstance canardInit(const CanardMemoryAllocate memory_allocate, const Cana /// - If the transfer-ID is above the maximum, the excessive bits are silently masked away /// (i.e., the modulo is computed automatically, so the caller doesn't have to bother). /// -/// An out-of-memory error is returned if a TX frame could not be allocated due to the memory being exhausted. -/// In that case, all previously allocated frames will be deallocated automatically. In other words, either all frames -/// of the transfer are enqueued successfully, or none are. +/// An out-of-memory error is returned if a TX frame could not be allocated due to the memory being exhausted, +/// or if the capacity of the queue would be exhausted by this operation. In such cases, all frames allocated for +/// this transfer (if any) will be deallocated automatically. In other words, either all frames of the transfer are +/// enqueued successfully, or none are. /// -/// The time complexity is O(p+e), where p is the amount of payload in the transfer, and e is the number of frames -/// already enqueued in the transmission queue. +/// The time complexity is O(p + log e), where p is the amount of payload in the transfer, and e is the number of +/// frames already enqueued in the transmission queue. /// /// The memory allocation requirement is one allocation per transport frame. A single-frame transfer takes one -/// allocation; a multi-frame transfer of N frames takes N allocations. The maximum size of each allocation is -/// (sizeof(CanardFrame) + sizeof(void*) + MTU). -int32_t canardTxPush(CanardInstance* const ins, const CanardTransfer* const transfer); +/// allocation; a multi-frame transfer of N frames takes N allocations. The size of each allocation is +/// (sizeof(CanardTxQueueItem) + MTU). +int32_t canardTxPush(CanardTxQueue* const que, + CanardInstance* const ins, + const CanardMicrosecond tx_deadline_usec, + const CanardTransferMetadata* const metadata, + const size_t payload_size, + const void* const payload); /// This function accesses the top element of the prioritized transmission queue. The queue itself is not modified /// (i.e., the accessed element is not removed). The application should invoke this function to collect the transport /// frames of serialized transfers pushed into the prioritized transmission queue by canardTxPush(). /// -/// Nodes with redundant transports should replicate every frame into each of the transport interfaces. -/// Such replication may require additional buffering in the media I/O layer, depending on the implementation. -/// -/// The timestamp values of returned frames are initialized with the timestamp value of the transfer instance they -/// originate from. Timestamps are used to specify the transmission deadline. It is up to the application and/or -/// the media layer to implement the discardment of timed-out transport frames. The library does not check it, -/// so a frame that is already timed out may be returned here. +/// The timestamp values of returned frames are initialized with tx_deadline_usec from canardTxPush(). +/// Timestamps are used to specify the transmission deadline. It is up to the application and/or the media layer +/// to implement the discardment of timed-out transport frames. The library does not check it, so a frame that is +/// already timed out may be returned here. /// /// If the queue is empty or if the argument is NULL, the returned value is NULL. /// @@ -437,31 +490,26 @@ int32_t canardTxPush(CanardInstance* const ins, const CanardTransfer* const tran /// The payload buffer is located shortly after the object itself, in the same memory fragment. The application shall /// not attempt to free it. /// -/// The time complexity is constant. This function does not invoke the dynamic memory manager. -const CanardFrame* canardTxPeek(const CanardInstance* const ins); +/// The time complexity is logarithmic of the queue size. This function does not invoke the dynamic memory manager. +const CanardTxQueueItem* canardTxPeek(const CanardTxQueue* const que); -/// This function transfers the ownership of the top element of the prioritized transmission queue to the application. -/// The application should invoke this function to remove the top element from the prioritized transmission queue. -/// The element is removed but it is not invalidated; it is the responsibility of the application to deallocate -/// the memory used by the object later. The object SHALL NOT be deallocated UNTIL this function is invoked. +/// This function transfers the ownership of the specified element of the prioritized transmission queue from the queue +/// to the application. The element does not necessarily need to be the top one -- it is safe to dequeue any element. +/// The element is dequeued but not invalidated; it is the responsibility of the application to deallocate the +/// memory used by the object later. The memory SHALL NOT be deallocated UNTIL this function is invoked. +/// The function returns the same pointer that it is given except that it becomes mutable. /// -/// WARNING: -/// Invocation of canardTxPush() may add new elements at the top of the prioritized transmission queue. -/// The calling code shall take that into account to eliminate the possibility of data loss and memory leak due to -/// the frame at the top of the queue being unexpectedly replaced between calls of canardTxPeek() and this function. +/// If any of the arguments are NULL, the function has no effect and returns NULL. /// -/// If the input argument is NULL or if the transmission queue is empty, the function has no effect. -/// -/// The time complexity is constant. This function does not invoke the dynamic memory manager. -void canardTxPop(CanardInstance* const ins); +/// The time complexity is logarithmic of the queue size. This function does not invoke the dynamic memory manager. +CanardTxQueueItem* canardTxPop(CanardTxQueue* const que, const CanardTxQueueItem* const item); -/// This function implements the transfer reassembly logic. It accepts a transport frame, locates the appropriate -/// subscription state, and, if found, updates it. If the frame completed a transfer, the return value is 1 (one) -/// and the out_transfer pointer is populated with the parameters of the newly reassembled transfer. The transfer -/// reassembly logic is defined in the UAVCAN specification. +/// This function implements the transfer reassembly logic. It accepts a transport frame from any of the redundant +/// interfaces, locates the appropriate subscription state, and, if found, updates it. If the frame completed a +/// transfer, the return value is 1 (one) and the out_transfer pointer is populated with the parameters of the +/// newly reassembled transfer. The transfer reassembly logic is defined in the UAVCAN specification. /// -/// The MTU of the accepted frame is not limited and is not dependent on the MTU setting of the local node; -/// that is, any MTU is accepted. The DLC compliance is also not checked. +/// The MTU of the accepted frame can be arbitrary; that is, any MTU is accepted. The DLC validity is irrelevant. /// /// Any value of redundant_transport_index is accepted; that is, up to 256 redundant transports are supported. /// The index of the transport from which the transfer is accepted is always the same as redundant_transport_index @@ -511,13 +559,14 @@ void canardTxPop(CanardInstance* const ins); /// for a detailed treatment of the problem and the related theory please refer to the documentation of O1Heap -- /// a deterministic memory allocator for hard real-time embedded systems. /// -/// The time complexity is O(n+p) where n is the number of subject-IDs or service-IDs subscribed to by the application, -/// depending on the transfer kind of the supplied frame, and p is the amount of payload in the received frame -/// (because it will be copied into an internal contiguous buffer). Observe that the time complexity is invariant to -/// the network configuration (such as the number of online nodes) -- this is a very important design guarantee for -/// real-time applications because the execution time is dependent only on the number of active subscriptions for -/// a given transfer kind, and the MTU, both of which are easy to predict and account for. Excepting the -/// subscription search and the payload data copying, the entire RX pipeline contains neither loops nor recursion. +/// The time complexity is O(p + log n) where n is the number of subject-IDs or service-IDs subscribed to by the +/// application, depending on the transfer kind of the supplied frame, and p is the amount of payload in the received +/// frame (because it will be copied into an internal contiguous buffer). Observe that the time complexity is +/// invariant to the network configuration (such as the number of online nodes) -- this is a very important +/// design guarantee for real-time applications because the execution time is dependent only on the number of +/// active subscriptions for a given transfer kind, and the MTU, both of which are easy to predict and account for. +/// Excepting the subscription search and the payload data copying, the entire RX pipeline contains neither loops +/// nor recursion. /// Misaddressed and malformed frames are discarded in constant time. /// /// The function returns 1 (one) if the new frame completed a transfer. In this case, the details of the transfer @@ -541,30 +590,12 @@ void canardTxPop(CanardInstance* const ins); /// - The received frame is a valid UAVCAN/CAN transport frame, but there is no matching subscription, /// the frame did not complete a transfer, the frame forms an invalid frame sequence, the frame is a duplicate, /// the frame is unicast to a different node (address mismatch). -/// -/// The function is designed to facilitate almost zero-copy data exchange across the protocol stack: once a buffer is -/// allocated, its data is never copied around but only passed by reference. This design allows us to reduce the -/// worst-case execution time and reduce the jitter caused by the linear time complexity of memcpy(). -/// One data copy still has to take place, though: from the frame payload into the contiguous transfer payload buffer. -/// In CAN, the MTU is small (at most 64 bytes for CAN FD), so the extra copy does not cost us much here, -/// but it allows us to completely decouple the lifetime of the input frame buffer from the lifetime of the final -/// transfer object, regardless of whether it's a single-frame or a multi-frame transfer. -/// If we were building, say, an UAVCAN/UDP library, then we would likely resort to a different design, where the -/// frame buffer is allocated once from the heap (which may be done from the interrupt handler if the heap is -/// sufficiently deterministic), and in the case of single-frame transfer it is then carried over to the application -/// without copying. This design somewhat complicates the media layer though. -int8_t canardRxAccept2(CanardInstance* const ins, - const CanardFrame* const frame, - const uint8_t redundant_transport_index, - CanardTransfer* const out_transfer, - CanardRxSubscription** const out_subscription); - -/// This is a deprecated wrapper over canardRxAccept2() without the out_subscription. -/// It is kept for backward compatibility and may be eventually removed in a future release. -int8_t canardRxAccept(CanardInstance* const ins, - const CanardFrame* const frame, - const uint8_t redundant_transport_index, - CanardTransfer* const out_transfer); +int8_t canardRxAccept(CanardInstance* const ins, + const CanardMicrosecond timestamp_usec, + const CanardFrame* const frame, + const uint8_t redundant_transport_index, + CanardRxTransfer* const out_transfer, + CanardRxSubscription** const out_subscription); /// This function creates a new subscription, allowing the application to register its interest in a particular /// category of transfers. The library will reject all transport frames for which there is no active subscription. @@ -590,17 +621,13 @@ int8_t canardRxAccept(CanardInstance* const ins, /// the existing subscription is terminated and then a new one is created in its place. Pending transfers may be lost. /// The return value is a negated invalid argument error if any of the input arguments are invalid. /// -/// The time complexity is linear from the number of current subscriptions under the specified transfer kind. +/// The time complexity is logarithmic from the number of current subscriptions under the specified transfer kind. /// This function does not allocate new memory. The function may deallocate memory if such subscription already /// existed; the deallocation behavior is specified in the documentation for canardRxUnsubscribe(). /// /// Subscription instances have large look-up tables to ensure that the temporal properties of the algorithms are /// invariant to the network configuration (i.e., a node that is validated on a network containing one other node -/// will provably perform identically on a network that contains X nodes). -/// This is a conscious time-memory trade-off. It may have adverse effects on RAM-constrained applications, -/// but this is considered tolerable because it is expected that the types of applications leveraging Libcanard -/// will be either real-time function nodes where time determinism is critical, or bootloaders where time determinism -/// is usually not required but the amount of available memory is not an issue (the main constraint is ROM, not RAM). +/// will provably perform identically on a network that contains X nodes). This is a conscious time-memory trade-off. int8_t canardRxSubscribe(CanardInstance* const ins, const CanardTransferKind transfer_kind, const CanardPortID port_id, @@ -616,12 +643,56 @@ int8_t canardRxSubscribe(CanardInstance* const ins, /// The return value is 0 if such subscription does not exist. In this case, the function has no effect. /// The return value is a negated invalid argument error if any of the input arguments are invalid. /// -/// The time complexity is linear from the number of current subscriptions under the specified transfer kind. +/// The time complexity is logarithmic from the number of current subscriptions under the specified transfer kind. /// This function does not allocate new memory. int8_t canardRxUnsubscribe(CanardInstance* const ins, const CanardTransferKind transfer_kind, const CanardPortID port_id); +/// Utilities for generating CAN controller hardware acceptance filter configurations +/// to accept specific subjects, services, or nodes. +/// +/// Complex applications will likely subscribe to more subject IDs than there are +/// acceptance filters available in the CAN hardware. In this case, the application +/// should implement filter consolidation. See canardConsolidateFilters() +/// as well as the UAVCAN specification for details. + +/// Generate an acceptance filter configuration to accept a specific subject ID. +CanardFilter canardMakeFilterForSubject(const CanardPortID subject_id); + +/// Generate an acceptance filter configuration to accept both requests and responses for a specific service. +/// +/// Users may prefer to instead use a catch-all acceptance filter configuration for accepting +/// all service requests and responses targeted at the specified local node ID. +/// See canardMakeFilterForServices() for this. +CanardFilter canardMakeFilterForService(const CanardPortID service_id, const CanardNodeID local_node_id); + +/// Generate an acceptance filter configuration to accept all service +/// requests and responses targeted to the specified local node ID. +/// +/// Due to the relatively low frequency of service transfers expected on a network, +/// and the fact that a service directed at a specific node is not likely to be rejected by that node, +/// a user may prefer to use this over canardMakeFilterForService() +/// in order to simplify the API usage and reduce the number of required hardware CAN acceptance filters. +CanardFilter canardMakeFilterForServices(const CanardNodeID local_node_id); + +/// Consolidate two acceptance filter configurations into a single configuration. +/// +/// Complex applications will likely subscribe to more subject IDs than there are +/// acceptance filters available in the CAN hardware. In this case, the application +/// should implement filter consolidation. While this may make it impossible to create +/// a 'perfect' filter that only accepts desired subject IDs, the application should apply +/// consolidation in a manner that minimizes the number of undesired messages that pass +/// through the hardware acceptance filters and require software filtering (implemented by canardRxSubscribe). +/// +/// While optimal choice of filter consolidation is a function of the number of available hardware filters, +/// the set of transfers needed by the application, and the expected frequency of occurrence +/// of all possible distinct transfers on the bus, it is possible to generate a quasi-optimal configuration +/// if information about the frequency of occurrence of different transfers is not known. +/// For details, see the "Automatic hardware acceptance filter configuration" note under the UAVCAN/CAN section +/// in the Transport Layer chapter of the UAVCAN specification. +CanardFilter canardConsolidateFilters(const CanardFilter* const a, const CanardFilter* const b); + #ifdef __cplusplus } #endif diff --git a/libcanard/canard_dsdl.c b/libcanard/canard_dsdl.c deleted file mode 100644 index 46ab448c..00000000 --- a/libcanard/canard_dsdl.c +++ /dev/null @@ -1,448 +0,0 @@ -/// This software is distributed under the terms of the MIT License. -/// Copyright (c) 2016-2020 UAVCAN Development Team. -/// Author: Pavel Kirienko - -#include "canard_dsdl.h" -#include -#include -#include - -// --------------------------------------------- BUILD CONFIGURATION --------------------------------------------- - -/// There are two implementations of the primitive (de-)serialization algorithms: a generic one, which is invariant -/// to the native byte order (and therefore compatible with any platform), and the optimized one which is compatible -/// with little-endian platforms only. By default, the slow generic algorithm is used. -/// If the target platform is little-endian, the user can enable this option to use the optimized algorithm. -#ifndef CANARD_DSDL_CONFIG_LITTLE_ENDIAN -# define CANARD_DSDL_CONFIG_LITTLE_ENDIAN false -#endif - -/// By default, this macro resolves to the standard assert(). The user can redefine this if necessary. -/// To disable assertion checks completely, make it expand into `(void)(0)`. -#ifndef CANARD_ASSERT -// Intentional violation of MISRA: assertion macro cannot be replaced with a function definition. -# define CANARD_ASSERT(x) assert(x) // NOSONAR -#endif - -/// This macro is needed only for testing and for library development. Do not redefine this in production. -#ifndef CANARD_PRIVATE -# define CANARD_PRIVATE static -#endif - -#if !defined(__STDC_VERSION__) || (__STDC_VERSION__ < 199901L) -# error "Unsupported language: ISO C99 or a newer version is required." -#endif - -/// In general, _Static_assert is not present on C99 compilers, except for gnu99 -#if !defined(static_assert) -// Intentional violation of MISRA: static assertion macro cannot be replaced with a function definition. -# define static_assert(x, ...) typedef char _static_assert_gl(_static_assertion_, __LINE__)[(x) ? 1 : -1] // NOSONAR -# define _static_assert_gl(a, b) _static_assert_gl_impl(a, b) // NOSONAR -// Intentional violation of MISRA: the paste operator ## cannot be avoided in this context. -# define _static_assert_gl_impl(a, b) a##b // NOSONAR -#endif - -/// Detect whether the target platform is compatible with IEEE 754. -#define CANARD_DSDL_PLATFORM_IEEE754_FLOAT \ - ((FLT_RADIX == 2) && (FLT_MANT_DIG == 24) && (FLT_MIN_EXP == -125) && (FLT_MAX_EXP == 128)) -#define CANARD_DSDL_PLATFORM_IEEE754_DOUBLE \ - ((FLT_RADIX == 2) && (DBL_MANT_DIG == 53) && (DBL_MIN_EXP == -1021) && (DBL_MAX_EXP == 1024)) - -// --------------------------------------------- COMMON ITEMS --------------------------------------------- - -/// Per the DSDL specification, 1 byte = 8 bits. -#define BYTE_WIDTH 8U -#define BYTE_MAX 0xFFU - -#define WIDTH16 16U -#define WIDTH32 32U -#define WIDTH64 64U - -// --------------------------------------------- PRIMITIVE SERIALIZATION --------------------------------------------- - -CANARD_PRIVATE size_t chooseMin(size_t a, size_t b) -{ - return (a < b) ? a : b; -} - -CANARD_PRIVATE size_t getBitCopySize(const size_t buf_size_bytes, - const size_t offset_bit, - const size_t requested_length_bit, - const uint8_t value_length_bit) -{ - const size_t buf_size_bit = buf_size_bytes * BYTE_WIDTH; - const size_t remaining_bit = buf_size_bit - chooseMin(buf_size_bit, offset_bit); - return chooseMin(remaining_bit, chooseMin(requested_length_bit, value_length_bit)); -} - -// --------------------------------------------- PUBLIC API - BIT ARRAY --------------------------------------------- - -void canardDSDLCopyBits(const size_t length_bit, - const size_t src_offset_bit, - const size_t dst_offset_bit, - const void* const src, - void* const dst) -{ - CANARD_ASSERT((src != NULL) && (dst != NULL) && (src != dst)); - if ((0U == (src_offset_bit % BYTE_WIDTH)) && (0U == (dst_offset_bit % BYTE_WIDTH))) - { - const size_t length_bytes = (size_t)(length_bit / BYTE_WIDTH); - // Intentional violation of MISRA: Pointer arithmetics. This is done to remove the API constraint that - // offsets be under 8 bits. Fewer constraints reduce the chance of API misuse. - const uint8_t* const psrc = (src_offset_bit / BYTE_WIDTH) + (const uint8_t*) src; // NOSONAR NOLINT - uint8_t* const pdst = (dst_offset_bit / BYTE_WIDTH) + (uint8_t*) dst; // NOSONAR NOLINT - // Clang-Tidy raises an error recommending the use of memcpy_s() instead. - // We ignore it because the safe functions are poorly supported; reliance on them may limit the portability. - (void) memcpy(pdst, psrc, length_bytes); // NOLINT - const uint8_t length_mod = (uint8_t)(length_bit % BYTE_WIDTH); - if (0U != length_mod) // If the length is unaligned, the last byte requires special treatment. - { - // Intentional violation of MISRA: Pointer arithmetics. It is unavoidable in this context. - const uint8_t* const last_src = psrc + length_bytes; // NOLINT NOSONAR - uint8_t* const last_dst = pdst + length_bytes; // NOLINT NOSONAR - CANARD_ASSERT(length_mod < BYTE_WIDTH); - const uint8_t mask = (uint8_t)((1U << length_mod) - 1U); - *last_dst = (uint8_t)(*last_dst & (uint8_t) ~mask) | (uint8_t)(*last_src & mask); - } - } - else - { - // The algorithm was originally designed by Ben Dyer for Libuavcan v0: - // https://github.com/UAVCAN/libuavcan/blob/legacy-v0/libuavcan/src/marshal/uc_bit_array_copy.cpp#L12-L58 - // This version is modified for v1 where the bit order is the opposite. - const uint8_t* const psrc = (const uint8_t*) src; - uint8_t* const pdst = (uint8_t*) dst; - size_t src_off = src_offset_bit; - size_t dst_off = dst_offset_bit; - const size_t last_bit = src_off + length_bit; - while (last_bit > src_off) - { - const uint8_t src_mod = (uint8_t)(src_off % BYTE_WIDTH); - const uint8_t dst_mod = (uint8_t)(dst_off % BYTE_WIDTH); - const uint8_t max_mod = (src_mod > dst_mod) ? src_mod : dst_mod; - - const uint8_t size = (uint8_t) chooseMin(BYTE_WIDTH - max_mod, last_bit - src_off); - CANARD_ASSERT((size > 0U) && (size <= BYTE_WIDTH)); - - // Suppress a false warning from Clang-Tidy & Sonar that size is being over-shifted. It's not. - const uint8_t mask = (uint8_t)((((1U << size) - 1U) << dst_mod) & BYTE_MAX); // NOLINT NOSONAR - CANARD_ASSERT(mask > 0U); - - // Intentional violation of MISRA: indexing on a pointer. - // This simplifies the implementation greatly and avoids pointer arithmetics. - const uint8_t in = - (uint8_t)((uint8_t)(psrc[src_off / BYTE_WIDTH] >> src_mod) << dst_mod) & BYTE_MAX; // NOSONAR - - // Intentional violation of MISRA: indexing on a pointer. - // This simplifies the implementation greatly and avoids pointer arithmetics. - const uint8_t a = pdst[dst_off / BYTE_WIDTH] & ((uint8_t) ~mask); // NOSONAR - const uint8_t b = in & mask; - - // Intentional violation of MISRA: indexing on a pointer. - // This simplifies the implementation greatly and avoids pointer arithmetics. - pdst[dst_off / BYTE_WIDTH] = a | b; // NOSONAR - - src_off += size; - dst_off += size; - } - CANARD_ASSERT(last_bit == src_off); - } -} - -// --------------------------------------------- PUBLIC API - INTEGER --------------------------------------------- - -void canardDSDLSetBit(uint8_t* const buf, const size_t off_bit, const bool value) -{ - CANARD_ASSERT(buf != NULL); - const uint8_t val = value ? 1U : 0U; - canardDSDLCopyBits(1U, 0U, off_bit, &val, buf); -} - -static_assert(WIDTH64 == (sizeof(uint64_t) * BYTE_WIDTH), "Unexpected size of uint64_t"); - -void canardDSDLSetUxx(uint8_t* const buf, const size_t off_bit, const uint64_t value, const uint8_t len_bit) -{ - CANARD_ASSERT(buf != NULL); - const size_t saturated_len_bit = chooseMin(len_bit, WIDTH64); -#if CANARD_DSDL_CONFIG_LITTLE_ENDIAN - canardDSDLCopyBits(saturated_len_bit, 0U, off_bit, (const uint8_t*) &value, buf); -#else - const uint8_t tmp[sizeof(uint64_t)] = { - (uint8_t)((value >> 0U) & BYTE_MAX), // Suppress warnings about the magic numbers. Their purpose is clear. - (uint8_t)((value >> 8U) & BYTE_MAX), // NOLINT NOSONAR - (uint8_t)((value >> 16U) & BYTE_MAX), // NOLINT NOSONAR - (uint8_t)((value >> 24U) & BYTE_MAX), // NOLINT NOSONAR - (uint8_t)((value >> 32U) & BYTE_MAX), // NOLINT NOSONAR - (uint8_t)((value >> 40U) & BYTE_MAX), // NOLINT NOSONAR - (uint8_t)((value >> 48U) & BYTE_MAX), // NOLINT NOSONAR - (uint8_t)((value >> 56U) & BYTE_MAX), // NOLINT NOSONAR - }; - canardDSDLCopyBits(saturated_len_bit, 0U, off_bit, &tmp[0], buf); -#endif -} - -void canardDSDLSetIxx(uint8_t* const buf, const size_t off_bit, const int64_t value, const uint8_t len_bit) -{ - // The naive sign conversion is safe and portable according to the C standard: - // 6.3.1.3.3: if the new type is unsigned, the value is converted by repeatedly adding or subtracting one more - // than the maximum value that can be represented in the new type until the value is in the range of the new type. - canardDSDLSetUxx(buf, off_bit, (uint64_t) value, len_bit); -} - -bool canardDSDLGetBit(const uint8_t* const buf, const size_t buf_size, const size_t off_bit) -{ - return 1U == canardDSDLGetU8(buf, buf_size, off_bit, 1U); -} - -uint8_t canardDSDLGetU8(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - CANARD_ASSERT(buf != NULL); - const size_t copy_size = getBitCopySize(buf_size, off_bit, len_bit, BYTE_WIDTH); - CANARD_ASSERT(copy_size <= (sizeof(uint8_t) * BYTE_WIDTH)); - uint8_t val = 0; - canardDSDLCopyBits(copy_size, off_bit, 0U, buf, &val); - return val; -} - -uint16_t canardDSDLGetU16(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - CANARD_ASSERT(buf != NULL); - const size_t copy_size = getBitCopySize(buf_size, off_bit, len_bit, WIDTH16); - CANARD_ASSERT(copy_size <= (sizeof(uint16_t) * BYTE_WIDTH)); -#if CANARD_DSDL_CONFIG_LITTLE_ENDIAN - uint16_t val = 0U; - canardDSDLCopyBits(copy_size, off_bit, 0U, buf, (uint8_t*) &val); - return val; -#else - uint8_t tmp[sizeof(uint16_t)] = {0}; - canardDSDLCopyBits(copy_size, off_bit, 0U, buf, &tmp[0]); - return (uint16_t)(tmp[0] | (uint16_t)(((uint16_t) tmp[1]) << BYTE_WIDTH)); -#endif -} - -uint32_t canardDSDLGetU32(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - CANARD_ASSERT(buf != NULL); - const size_t copy_size = getBitCopySize(buf_size, off_bit, len_bit, WIDTH32); - CANARD_ASSERT(copy_size <= (sizeof(uint32_t) * BYTE_WIDTH)); -#if CANARD_DSDL_CONFIG_LITTLE_ENDIAN - uint32_t val = 0U; - canardDSDLCopyBits(copy_size, off_bit, 0U, buf, (uint8_t*) &val); - return val; -#else - uint8_t tmp[sizeof(uint32_t)] = {0}; - canardDSDLCopyBits(copy_size, off_bit, 0U, buf, &tmp[0]); - return (uint32_t)(tmp[0] | // Suppress warnings about the magic numbers. - ((uint32_t) tmp[1] << 8U) | // NOLINT NOSONAR - ((uint32_t) tmp[2] << 16U) | // NOLINT NOSONAR - ((uint32_t) tmp[3] << 24U)); // NOLINT NOSONAR -#endif -} - -uint64_t canardDSDLGetU64(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - CANARD_ASSERT(buf != NULL); - const size_t copy_size = getBitCopySize(buf_size, off_bit, len_bit, WIDTH64); - CANARD_ASSERT(copy_size <= (sizeof(uint64_t) * BYTE_WIDTH)); -#if CANARD_DSDL_CONFIG_LITTLE_ENDIAN - uint64_t val = 0U; - canardDSDLCopyBits(copy_size, off_bit, 0U, buf, (uint8_t*) &val); - return val; -#else - uint8_t tmp[sizeof(uint64_t)] = {0}; - canardDSDLCopyBits(copy_size, off_bit, 0U, buf, &tmp[0]); - return (uint64_t)(tmp[0] | // Suppress warnings about the magic numbers. - ((uint64_t) tmp[1] << 8U) | // NOLINT NOSONAR - ((uint64_t) tmp[2] << 16U) | // NOLINT NOSONAR - ((uint64_t) tmp[3] << 24U) | // NOLINT NOSONAR - ((uint64_t) tmp[4] << 32U) | // NOLINT NOSONAR - ((uint64_t) tmp[5] << 40U) | // NOLINT NOSONAR - ((uint64_t) tmp[6] << 48U) | // NOLINT NOSONAR - ((uint64_t) tmp[7] << 56U)); // NOLINT NOSONAR -#endif -} - -int8_t canardDSDLGetI8(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - const uint8_t sat = (uint8_t) chooseMin(len_bit, BYTE_WIDTH); - uint8_t val = canardDSDLGetU8(buf, buf_size, off_bit, sat); - const bool neg = (sat > 0U) && ((val & (1ULL << (sat - 1U))) != 0U); - val = ((sat < BYTE_WIDTH) && neg) ? (uint8_t)(val | ~((1U << sat) - 1U)) : val; // Sign extension - return neg ? (int8_t)((-(int8_t)(uint8_t) ~val) - 1) : (int8_t) val; -} - -int16_t canardDSDLGetI16(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - const uint8_t sat = (uint8_t) chooseMin(len_bit, WIDTH16); - uint16_t val = canardDSDLGetU16(buf, buf_size, off_bit, sat); - const bool neg = (sat > 0U) && ((val & (1ULL << (sat - 1U))) != 0U); - val = ((sat < WIDTH16) && neg) ? (uint16_t)(val | ~((1U << sat) - 1U)) : val; // Sign extension - return neg ? (int16_t)((-(int16_t)(uint16_t) ~val) - 1) : (int16_t) val; -} - -int32_t canardDSDLGetI32(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - const uint8_t sat = (uint8_t) chooseMin(len_bit, WIDTH32); - uint32_t val = canardDSDLGetU32(buf, buf_size, off_bit, sat); - const bool neg = (sat > 0U) && ((val & (1ULL << (sat - 1U))) != 0U); - val = ((sat < WIDTH32) && neg) ? (uint32_t)(val | ~((1UL << sat) - 1U)) : val; // Sign extension - return neg ? (int32_t)((-(int32_t) ~val) - 1) : (int32_t) val; -} - -int64_t canardDSDLGetI64(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit) -{ - const uint8_t sat = (uint8_t) chooseMin(len_bit, WIDTH64); - uint64_t val = canardDSDLGetU64(buf, buf_size, off_bit, sat); - const bool neg = (sat > 0U) && ((val & (1ULL << (sat - 1U))) != 0U); - val = ((sat < WIDTH64) && neg) ? (uint64_t)(val | ~((1ULL << sat) - 1U)) : val; // Sign extension - return neg ? (int64_t)((-(int64_t) ~val) - 1) : (int64_t) val; -} - -// --------------------------------------------- PUBLIC API - FLOAT16 --------------------------------------------- - -#if CANARD_DSDL_PLATFORM_IEEE754_FLOAT - -static_assert(WIDTH32 == (sizeof(CanardDSDLFloat32) * BYTE_WIDTH), "Unsupported floating point model"); - -// Intentional violation of MISRA: we need this union because the alternative is far more error prone. -// We have to rely on low-level data representation details to do the conversion; unions are helpful. -typedef union // NOSONAR -{ - uint32_t bits; - CanardDSDLFloat32 real; -} Float32Bits; - -CANARD_PRIVATE uint16_t float16Pack(const CanardDSDLFloat32 value) -{ - // The no-lint statements suppress the warnings about magic numbers. - // The no-lint statements suppress the warning about the use of union. This is required for low-level bit access. - const uint32_t round_mask = ~(uint32_t) 0x0FFFU; // NOLINT NOSONAR - const Float32Bits f32inf = {.bits = ((uint32_t) 255U) << 23U}; // NOLINT NOSONAR - const Float32Bits f16inf = {.bits = ((uint32_t) 31U) << 23U}; // NOLINT NOSONAR - const Float32Bits magic = {.bits = ((uint32_t) 15U) << 23U}; // NOLINT NOSONAR - Float32Bits in = {.real = value}; // NOSONAR - const uint32_t sign = in.bits & (((uint32_t) 1U) << 31U); // NOLINT NOSONAR - in.bits ^= sign; - uint16_t out = 0; - if (in.bits >= f32inf.bits) - { - // The no-lint statements suppress the warnings about magic numbers. - if ((in.bits & 0x7FFFFFUL) != 0) // NOLINT NOSONAR - { - out = 0x7E00U; // NOLINT NOSONAR - } - else - { - out = (in.bits > f32inf.bits) ? (uint16_t) 0x7FFFU : (uint16_t) 0x7C00U; // NOLINT NOSONAR - } - } - else - { - in.bits &= round_mask; - in.real *= magic.real; - in.bits -= round_mask; - if (in.bits > f16inf.bits) - { - in.bits = f16inf.bits; - } - out = (uint16_t)(in.bits >> 13U); // NOLINT NOSONAR - } - out |= (uint16_t)(sign >> 16U); // NOLINT NOSONAR - return out; -} - -CANARD_PRIVATE CanardDSDLFloat32 float16Unpack(const uint16_t value) -{ - // The no-lint statements suppress the warnings about magic numbers. - // The no-lint statements suppress the warning about the use of union. This is required for low-level bit access. - const Float32Bits magic = {.bits = ((uint32_t) 0xEFU) << 23U}; // NOLINT NOSONAR - const Float32Bits inf_nan = {.bits = ((uint32_t) 0x8FU) << 23U}; // NOLINT NOSONAR - Float32Bits out = {.bits = ((uint32_t)(value & 0x7FFFU)) << 13U}; // NOLINT NOSONAR - out.real *= magic.real; - if (out.real >= inf_nan.real) - { - out.bits |= ((uint32_t) 0xFFU) << 23U; // NOLINT NOSONAR - } - out.bits |= ((uint32_t)(value & 0x8000U)) << 16U; // NOLINT NOSONAR - return out.real; -} - -void canardDSDLSetF16(uint8_t* const buf, const size_t off_bit, const CanardDSDLFloat32 value) -{ - canardDSDLSetUxx(buf, off_bit, float16Pack(value), WIDTH16); -} - -CanardDSDLFloat32 canardDSDLGetF16(const uint8_t* const buf, const size_t buf_size, const size_t off_bit) -{ - return float16Unpack(canardDSDLGetU16(buf, buf_size, off_bit, WIDTH16)); -} - -#endif // CANARD_DSDL_PLATFORM_IEEE754_FLOAT - -// --------------------------------------------- PUBLIC API - FLOAT32 --------------------------------------------- - -#if CANARD_DSDL_PLATFORM_IEEE754_FLOAT - -static_assert(WIDTH32 == (sizeof(CanardDSDLFloat32) * BYTE_WIDTH), "Unsupported floating point model"); - -void canardDSDLSetF32(uint8_t* const buf, const size_t off_bit, const CanardDSDLFloat32 value) -{ - // Intentional violation of MISRA: use union to perform fast conversion from an IEEE 754-compatible native - // representation into a serializable integer. The assumptions about the target platform properties are made - // clear. In the future we may add a more generic conversion that is platform-invariant. - union // NOSONAR - { - CanardDSDLFloat32 fl; - uint32_t in; - } const tmp = {value}; // NOSONAR - canardDSDLSetUxx(buf, off_bit, tmp.in, sizeof(tmp) * BYTE_WIDTH); -} - -CanardDSDLFloat32 canardDSDLGetF32(const uint8_t* const buf, const size_t buf_size, const size_t off_bit) -{ - // Intentional violation of MISRA: use union to perform fast conversion to an IEEE 754-compatible native - // representation into a serializable integer. The assumptions about the target platform properties are made - // clear. In the future we may add a more generic conversion that is platform-invariant. - union // NOSONAR - { - uint32_t in; - CanardDSDLFloat32 fl; - } const tmp = {canardDSDLGetU32(buf, buf_size, off_bit, WIDTH32)}; // NOSONAR - return tmp.fl; -} - -#endif // CANARD_DSDL_PLATFORM_IEEE754_FLOAT - -// --------------------------------------------- PUBLIC API - FLOAT64 --------------------------------------------- - -#if CANARD_DSDL_PLATFORM_IEEE754_DOUBLE - -static_assert(WIDTH64 == (sizeof(CanardDSDLFloat64) * BYTE_WIDTH), "Unsupported floating point model"); - -CanardDSDLFloat64 canardDSDLGetF64(const uint8_t* const buf, const size_t buf_size, const size_t off_bit) -{ - // Intentional violation of MISRA: use union to perform fast conversion to an IEEE 754-compatible native - // representation into a serializable integer. The assumptions about the target platform properties are made - // clear. In the future we may add a more generic conversion that is platform-invariant. - union // NOSONAR - { - uint64_t in; - CanardDSDLFloat64 fl; - } const tmp = {canardDSDLGetU64(buf, buf_size, off_bit, WIDTH64)}; // NOSONAR - return tmp.fl; -} - -void canardDSDLSetF64(uint8_t* const buf, const size_t off_bit, const CanardDSDLFloat64 value) -{ - // Intentional violation of MISRA: use union to perform fast conversion from an IEEE 754-compatible native - // representation into a serializable integer. The assumptions about the target platform properties are made - // clear. In the future we may add a more generic conversion that is platform-invariant. - union // NOSONAR - { - CanardDSDLFloat64 fl; - uint64_t in; - } const tmp = {value}; // NOSONAR - canardDSDLSetUxx(buf, off_bit, tmp.in, sizeof(tmp) * BYTE_WIDTH); -} - -#endif // CANARD_DSDL_PLATFORM_IEEE754_DOUBLE diff --git a/libcanard/canard_dsdl.h b/libcanard/canard_dsdl.h deleted file mode 100644 index cbc0471f..00000000 --- a/libcanard/canard_dsdl.h +++ /dev/null @@ -1,111 +0,0 @@ -/// __ __ _______ __ __ _______ _______ __ __ -/// | | | | / _ ` | | | | / ____| / _ ` | ` | | -/// | | | | | |_| | | | | | | | | |_| | | `| | -/// | |_| | | _ | ` `_/ / | |____ | _ | | |` | -/// `_______/ |__| |__| `_____/ `_______| |__| |__| |__| `__| -/// | | | | | | -/// ----o------o------------o---------o------o---------o------- -/// -/// This is a DSDL serialization helper for libcanard -- a trivial optional extension library that contains basic -/// DSDL field serialization routines. It is intended for use in unconventional applications where auto-generated -/// DSDL serialization routines are not available. Most applications are not expected to need this; instead, they are -/// recommended to automatically transpile DSDL into C using Nunavut: https://github.com/UAVCAN/nunavut. -/// -/// This library is designed to be compatible with any instruction set architecture (including big endian platforms) -/// but the floating point functionality will be automatically disabled at compile time if the target platform does not -/// use an IEEE 754-compatible floating point model. Support for other floating point models may be implemented later. -/// If your application requires non-IEEE-754 floats, please reach out to the maintainers via https://forum.uavcan.org. -/// -/// To use the library, copy the files canard_dsdl.c and canard_dsdl.h into the source tree of the application. -/// No special compilation options are required. There are optional build configuration options defined near the top -/// of canard_dsdl.c; they may be used to fine-tune the library for the target platform (but it is not necessary). -/// -/// Some high-integrity systems may prefer to avoid this extension because it relies on unsafe memory operations. -/// -/// This software is distributed under the terms of the MIT License. -/// Copyright (c) 2016-2020 UAVCAN Development Team. -/// Author: Pavel Kirienko - -#ifndef CANARD_DSDL_H_INCLUDED -#define CANARD_DSDL_H_INCLUDED - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef float CanardDSDLFloat32; -typedef double CanardDSDLFloat64; - -/// Copy the specified number of bits from the source buffer into the destination buffer in accordance with the -/// DSDL bit-level serialization specification. The offsets may be arbitrary (may exceed 8 bits). -/// If both offsets are byte-aligned, the algorithm degenerates to memcpy() (the last byte may be copied separately). -/// If the source and the destination overlap, the behavior is undefined. -/// If either source or destination pointers are NULL, the behavior is undefined. -/// Arguments: -/// length_bit The number of bits to copy. Both source and destination shall be large enough. -/// src_offset_bit Offset in bits from the source pointer. May exceed 8. -/// dst_offset_bit Offset in bits from the destination pointer. May exceed 8. -/// src Source buffer. Shall be at least ceil(length_bit/8) bytes large. -/// dst Destination buffer. Shall be at least ceil(length_bit/8) bytes large. -void canardDSDLCopyBits(const size_t length_bit, - const size_t src_offset_bit, - const size_t dst_offset_bit, - const void* const src, - void* const dst); - -/// Serialize a DSDL field value at the specified bit offset from the beginning of the destination buffer. -/// The behavior is undefined if the input pointer is NULL. The time complexity is linear of the bit length. -/// One-bit-wide signed integers are processed without raising an error but the result is unspecified. -/// The floating point functions are only available if the target platform has an IEEE 754-compatible float model. -/// -/// Arguments: -/// buf Destination buffer where the result will be stored. -/// off_bit Offset, in bits, from the beginning of the buffer. May exceed one byte. -/// value The value itself (in case of integers it is promoted to 64-bit for unification). -/// len_bit Length of the serialized representation, in bits. Zero has no effect. Values above 64 bit are saturated. -void canardDSDLSetBit(uint8_t* const buf, const size_t off_bit, const bool value); -void canardDSDLSetUxx(uint8_t* const buf, const size_t off_bit, const uint64_t value, const uint8_t len_bit); -void canardDSDLSetIxx(uint8_t* const buf, const size_t off_bit, const int64_t value, const uint8_t len_bit); -void canardDSDLSetF16(uint8_t* const buf, const size_t off_bit, const CanardDSDLFloat32 value); -void canardDSDLSetF32(uint8_t* const buf, const size_t off_bit, const CanardDSDLFloat32 value); -void canardDSDLSetF64(uint8_t* const buf, const size_t off_bit, const CanardDSDLFloat64 value); - -/// Deserialize a DSDL field value located at the specified bit offset from the beginning of the source buffer. -/// If the deserialized value extends beyond the end of the buffer, the missing bits are taken as zero, as required -/// by the DSDL specification (see Implicit Zero Extension Rule, IZER). -/// The floating point functions are only available if the target platform has an IEEE 754-compatible float model. -/// -/// If len_bit is greater than the return type, extra bits will be truncated per standard narrowing conversion rules. -/// If len_bit is shorter than the return type, missing bits will be zero per standard integer promotion rules. -/// Essentially, for integers, it would be enough to have 64-bit versions only; narrower variants exist only to avoid -/// narrowing type conversions of the result and for some performance gains. -/// -/// The behavior is undefined if the input pointer is NULL. The time complexity is linear of the bit length. -/// One-bit-wide signed integers are processed without raising an error but the result is unspecified. -/// -/// Arguments: -/// buf Source buffer where the serialized representation will be read from. -/// buf_size The size of the source buffer, in bytes. Reads past this limit will be assumed to return zero bits. -/// off_bit Offset, in bits, from the beginning of the buffer. May exceed one byte. -/// len_bit Length of the serialized representation, in bits. Zero returns zero. Out-of-range values are saturated. -bool canardDSDLGetBit(const uint8_t* const buf, const size_t buf_size, const size_t off_bit); -uint8_t canardDSDLGetU8(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -uint16_t canardDSDLGetU16(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -uint32_t canardDSDLGetU32(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -uint64_t canardDSDLGetU64(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -int8_t canardDSDLGetI8(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -int16_t canardDSDLGetI16(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -int32_t canardDSDLGetI32(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -int64_t canardDSDLGetI64(const uint8_t* const buf, const size_t buf_size, const size_t off_bit, const uint8_t len_bit); -CanardDSDLFloat32 canardDSDLGetF16(const uint8_t* const buf, const size_t buf_size, const size_t off_bit); -CanardDSDLFloat32 canardDSDLGetF32(const uint8_t* const buf, const size_t buf_size, const size_t off_bit); -CanardDSDLFloat64 canardDSDLGetF64(const uint8_t* const buf, const size_t buf_size, const size_t off_bit); - -#ifdef __cplusplus -} -#endif -#endif // CANARD_DSDL_H_INCLUDED diff --git a/libcanard/cavl.h b/libcanard/cavl.h new file mode 100644 index 00000000..640ceeb4 --- /dev/null +++ b/libcanard/cavl.h @@ -0,0 +1,337 @@ +/// Source: https://github.com/pavel-kirienko/cavl +/// +/// Cavl is a single-header C library providing an implementation of AVL tree suitable for deeply embedded systems. +/// To integrate it into your project, simply copy this file into your source tree. Read the API docs below. +/// +/// See also O1Heap -- a deterministic memory manager for hard-real-time +/// high-integrity embedded systems. +/// +/// Copyright (c) 2021 Pavel Kirienko +/// +/// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +/// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +/// and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in all copies or substantial portions of +/// the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +/// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +/// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +/// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include "canard.h" + +/// Modified for use with Libcanard: use the same assertion check macro if provided. +#ifdef CANARD_ASSERT +# define CAVL_ASSERT CANARD_ASSERT +#else +// Intentional violation of MISRA: inclusion not at the top of the file to eliminate unnecessary dependency on assert.h. +# include // NOSONAR +# define CAVL_ASSERT assert +#endif + +#ifdef __cplusplus +// This is, strictly speaking, useless because we do not define any functions with external linkage here, +// but it tells static analyzers that what follows should be interpreted as C code rather than C++. +extern "C" { +#endif + +// ---------------------------------------- PUBLIC API SECTION ---------------------------------------- + +/// Modified for use with Libcanard: expose the Cavl structure via public API as CanardTreeNode. +typedef CanardTreeNode Cavl; + +/// Returns POSITIVE if the search target is GREATER than the provided node, negative if smaller, zero on match (found). +/// Values other than {-1, 0, +1} are not recommended to avoid overflow during the narrowing conversion of the result. +typedef int8_t (*CavlPredicate)(void* user_reference, const Cavl* node); + +/// If provided, the factory will be invoked when the sought node does not exist in the tree. +/// It is expected to return a new node that will be inserted immediately (without the need to traverse the tree again). +/// If the factory returns NULL or is not provided, the tree is not modified. +typedef Cavl* (*CavlFactory)(void* user_reference); + +/// Look for a node in the tree using the specified search predicate. The worst-case complexity is O(log n). +/// - If the node is found, return it. +/// - If the node is not found and the factory is NULL, return NULL. +/// - Otherwise, construct a new node using the factory; if the result is not NULL, insert it; return the result. +/// The user_reference is passed into the predicate & factory unmodified. +/// The root node may be replaced in the process. +/// If predicate is NULL, returns NULL. +static inline Cavl* cavlSearch(Cavl** const root, + void* const user_reference, + const CavlPredicate predicate, + const CavlFactory factory); + +/// Remove the specified node from its tree. The root node may be replaced in the process. +/// The worst-case complexity is O(log n). +/// The function has no effect if either of the pointers are NULL. +/// If the node is not in the tree, the behavior is undefined; it may create cycles in the tree which is deadly. +/// It is safe to pass the result of cavlSearch() directly as the second argument: +/// cavlRemove(&root, cavlSearch(&root, user_reference, search_predicate, NULL)); +/// It is recommended to invalidate the pointers stored in the node after its removal. +static inline void cavlRemove(Cavl** const root, const Cavl* const node); + +/// Return the min-/max-valued node stored in the tree, depending on the flag. This is an extremely fast query. +/// Returns NULL iff the argument is NULL (i.e., the tree is empty). The worst-case complexity is O(log n). +static inline Cavl* cavlFindExtremum(Cavl* const root, const bool maximum) +{ + Cavl* result = NULL; + Cavl* c = root; + while (c != NULL) + { + result = c; + c = c->lr[maximum]; + } + return result; +} + +// ---------------------------------------- END OF PUBLIC API SECTION ---------------------------------------- +// ---------------------------------------- POLICE LINE DO NOT CROSS ---------------------------------------- + +/// INTERNAL USE ONLY. Makes the '!r' child of node 'x' its parent; i.e., rotates 'x' toward 'r'. +static inline void cavlPrivateRotate(Cavl* const x, const bool r) +{ + CAVL_ASSERT((x != NULL) && (x->lr[!r] != NULL) && ((x->bf >= -1) && (x->bf <= +1))); + Cavl* const z = x->lr[!r]; + if (x->up != NULL) + { + x->up->lr[x->up->lr[1] == x] = z; + } + z->up = x->up; + x->up = z; + x->lr[!r] = z->lr[r]; + if (x->lr[!r] != NULL) + { + x->lr[!r]->up = x; + } + z->lr[r] = x; +} + +/// INTERNAL USE ONLY. +/// Accepts a node and how its balance factor needs to be changed -- either +1 or -1. +/// Returns the new node to replace the old one if tree rotation took place, same node otherwise. +static inline Cavl* cavlPrivateAdjustBalance(Cavl* const x, const bool increment) +{ + CAVL_ASSERT((x != NULL) && ((x->bf >= -1) && (x->bf <= +1))); + Cavl* out = x; + const int8_t new_bf = (int8_t) (x->bf + (increment ? +1 : -1)); + if ((new_bf < -1) || (new_bf > 1)) + { + const bool r = new_bf < 0; // bf<0 if left-heavy --> right rotation is needed. + const int8_t sign = r ? +1 : -1; // Positive if we are rotating right. + Cavl* const z = x->lr[!r]; + CAVL_ASSERT(z != NULL); // Heavy side cannot be empty. + if ((z->bf * sign) <= 0) // Parent and child are heavy on the same side or the child is balanced. + { + out = z; + cavlPrivateRotate(x, r); + if (0 == z->bf) + { + x->bf = (int8_t) (-sign); + z->bf = (int8_t) (+sign); + } + else + { + x->bf = 0; + z->bf = 0; + } + } + else // Otherwise, the child needs to be rotated in the opposite direction first. + { + Cavl* const y = z->lr[r]; + CAVL_ASSERT(y != NULL); // Heavy side cannot be empty. + out = y; + cavlPrivateRotate(z, !r); + cavlPrivateRotate(x, r); + if ((y->bf * sign) < 0) + { + x->bf = (int8_t) (+sign); + y->bf = 0; + z->bf = 0; + } + else if ((y->bf * sign) > 0) + { + x->bf = 0; + y->bf = 0; + z->bf = (int8_t) (-sign); + } + else + { + x->bf = 0; + z->bf = 0; + } + } + } + else + { + x->bf = new_bf; // Balancing not needed, just update the balance factor and call it a day. + } + return out; +} + +/// INTERNAL USE ONLY. +/// Takes the culprit node (the one that is added); returns NULL or the root of the tree (possibly new one). +/// When adding a new node, set its balance factor to zero and call this function to propagate the changes upward. +static inline Cavl* cavlPrivateRetraceOnGrowth(Cavl* const added) +{ + CAVL_ASSERT((added != NULL) && (0 == added->bf)); + Cavl* c = added; // Child + Cavl* p = added->up; // Parent + while (p != NULL) + { + const bool r = p->lr[1] == c; // c is the right child of parent + CAVL_ASSERT(p->lr[r] == c); + c = cavlPrivateAdjustBalance(p, r); + p = c->up; + if (0 == c->bf) + { // The height change of the subtree made this parent perfectly balanced (as all things should be), + break; // hence, the height of the outer subtree is unchanged, so upper balance factors are unchanged. + } + } + CAVL_ASSERT(c != NULL); + return (NULL == p) ? c : NULL; // New root or nothing. +} + +static inline Cavl* cavlSearch(Cavl** const root, + void* const user_reference, + const CavlPredicate predicate, + const CavlFactory factory) +{ + Cavl* out = NULL; + if ((root != NULL) && (predicate != NULL)) + { + Cavl* up = *root; + Cavl** n = root; + while (*n != NULL) + { + const int8_t cmp = predicate(user_reference, *n); + if (0 == cmp) + { + out = *n; + break; + } + up = *n; + n = &(*n)->lr[cmp > 0]; + CAVL_ASSERT((NULL == *n) || ((*n)->up == up)); + } + if (NULL == out) + { + out = (NULL == factory) ? NULL : factory(user_reference); + if (out != NULL) + { + *n = out; // Overwrite the pointer to the new node in the parent node. + out->lr[0] = NULL; + out->lr[1] = NULL; + out->up = up; + out->bf = 0; + Cavl* const rt = cavlPrivateRetraceOnGrowth(out); + if (rt != NULL) + { + *root = rt; + } + } + } + } + return out; +} + +static inline void cavlRemove(Cavl** const root, const Cavl* const node) +{ + if ((root != NULL) && (node != NULL)) + { + CAVL_ASSERT(*root != NULL); // Otherwise, the node would have to be NULL. + CAVL_ASSERT((node->up != NULL) || (node == *root)); + Cavl* p = NULL; // The lowest parent node that suffered a shortening of its subtree. + bool r = false; // Which side of the above was shortened. + // The first step is to update the topology and remember the node where to start the retracing from later. + // Balancing is not performed yet so we may end up with an unbalanced tree. + if ((node->lr[0] != NULL) && (node->lr[1] != NULL)) + { + Cavl* const re = cavlFindExtremum(node->lr[1], false); + CAVL_ASSERT((re != NULL) && (NULL == re->lr[0]) && (re->up != NULL)); + re->bf = node->bf; + re->lr[0] = node->lr[0]; + re->lr[0]->up = re; + if (re->up != node) + { + p = re->up; // Retracing starts with the ex-parent of our replacement node. + CAVL_ASSERT(p->lr[0] == re); + p->lr[0] = re->lr[1]; // Reducing the height of the left subtree here. + if (p->lr[0] != NULL) + { + p->lr[0]->up = p; + } + re->lr[1] = node->lr[1]; + re->lr[1]->up = re; + r = false; + } + else // In this case, we are reducing the height of the right subtree, so r=1. + { + p = re; // Retracing starts with the replacement node itself as we are deleting its parent. + r = true; // The right child of the replacement node remains the same so we don't bother relinking it. + } + re->up = node->up; + if (re->up != NULL) + { + re->up->lr[re->up->lr[1] == node] = re; // Replace link in the parent of node. + } + else + { + *root = re; + } + } + else // Either or both of the children are NULL. + { + p = node->up; + const bool rr = node->lr[1] != NULL; + if (node->lr[rr] != NULL) + { + node->lr[rr]->up = p; + } + if (p != NULL) + { + r = p->lr[1] == node; + p->lr[r] = node->lr[rr]; + if (p->lr[r] != NULL) + { + p->lr[r]->up = p; + } + } + else + { + *root = node->lr[rr]; + } + } + // Now that the topology is updated, perform the retracing to restore balance. We climb up adjusting the + // balance factors until we reach the root or a parent whose balance factor becomes plus/minus one, which + // means that that parent was able to absorb the balance delta; in other words, the height of the outer + // subtree is unchanged, so upper balance factors shall be kept unchanged. + if (p != NULL) + { + Cavl* c = NULL; + for (;;) + { + c = cavlPrivateAdjustBalance(p, !r); + p = c->up; + if ((c->bf != 0) || (NULL == p)) // Reached the root or the height difference is absorbed by c. + { + break; + } + r = p->lr[1] == c; + } + if (NULL == p) + { + CAVL_ASSERT(c != NULL); + *root = c; + } + } + } +} + +#ifdef __cplusplus +} +#endif diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 3c562d5f..00000000 --- a/sonar-project.properties +++ /dev/null @@ -1,8 +0,0 @@ -sonar.organization=uavcan -sonar.projectName=libcanard -sonar.projectKey=libcanard - -sonar.sources=libcanard -sonar.cfamily.gcov.reportsPath=. -sonar.cfamily.cache.enabled=false -sonar.cfamily.threads=1 \ No newline at end of file diff --git a/tests/.clang-tidy b/tests/.clang-tidy index 0c248721..78a09efa 100644 --- a/tests/.clang-tidy +++ b/tests/.clang-tidy @@ -22,12 +22,20 @@ Checks: >- -readability-avoid-const-params-in-decls, -readability-magic-numbers, -readability-function-size, + -readability-function-cognitive-complexity, + -readability-identifier-length, + -bugprone-easily-swappable-parameters, -llvm-header-guard, + -llvm-include-order, -misc-non-private-member-variables-in-classes, -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-reinterpret-cast, + -*-no-malloc, + -cert-msc30-c, + -cert-msc50-cpp, WarningsAsErrors: '*' HeaderFilterRegex: '.*\.hpp' AnalyzeTemporaryDtors: false diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e54fd165..aa1eb6d3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,66 +1,62 @@ # This software is distributed under the terms of the MIT License. -# Copyright (c) 2016-2020 UAVCAN Development Team. +# Copyright (c) 2016 UAVCAN Consortium. +# Author: Pavel Kirienko +# Contributors: https://github.com/UAVCAN/libcanard/contributors. cmake_minimum_required(VERSION 3.12) project(canard_tests C CXX) enable_testing() -if (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") - # assert() shall be disabled in release builds to enable testing of bad free() calls. - add_definitions(-DNDEBUG=1) -endif () - +set(CTEST_OUTPUT_ON_FAILURE ON) set(library_dir "${CMAKE_SOURCE_DIR}/../libcanard") # Use -DNO_STATIC_ANALYSIS=1 to suppress static analysis. # If not suppressed, the tools used here shall be available, otherwise the build will fail. if (NOT NO_STATIC_ANALYSIS) # clang-tidy (separate config files per directory) - find_program(clang_tidy NAMES clang-tidy-12 clang-tidy-11 clang-tidy) + find_program(clang_tidy NAMES clang-tidy) if (NOT clang_tidy) message(FATAL_ERROR "Could not locate clang-tidy") endif () message(STATUS "Using clang-tidy: ${clang_tidy}") - set(CMAKE_C_CLANG_TIDY ${clang_tidy}) + set(CMAKE_C_CLANG_TIDY ${clang_tidy}) set(CMAKE_CXX_CLANG_TIDY ${clang_tidy}) +endif () - # clang-format - find_program(clang_format NAMES clang-format-12 clang-format-11 clang-format) - if (NOT clang_format) - message(FATAL_ERROR "Could not locate clang-format") - endif () - file(GLOB format_files - ${library_dir}/*.[ch] - ${CMAKE_SOURCE_DIR}/*.[ch]pp) +# clang-format +find_program(clang_format NAMES clang-format) +if (NOT clang_format) + message(STATUS "Could not locate clang-format") +else () + file(GLOB format_files ${library_dir}/*.[ch] ${CMAKE_SOURCE_DIR}/*.[ch]pp) message(STATUS "Using clang-format: ${clang_format}; files: ${format_files}") add_custom_target(format COMMAND ${clang_format} -i -fallback-style=none -style=file --verbose ${format_files}) endif () -# C options -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror -pedantic -fstrict-aliasing") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wdouble-promotion -Wswitch-enum -Wfloat-equal -Wundef") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wconversion -Wtype-limits") -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wsign-conversion -Wcast-align -Wmissing-declarations") - -# C++ options set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror -pedantic -fstrict-aliasing") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wdouble-promotion -Wswitch-enum -Wfloat-equal -Wundef") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wconversion -Wsign-promo") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-conversion -Wcast-align -Wmissing-declarations") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wtype-limits -Wzero-as-null-pointer-constant -Wnon-virtual-dtor") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Woverloaded-virtual -Wsign-promo -Wold-style-cast") +set(CXX_EXTENSIONS OFF) +add_compile_options( + -Wall -Wextra -Werror -pedantic -fstrict-aliasing -Wdouble-promotion -Wswitch-enum -Wfloat-equal -Wundef + -Wconversion -Wtype-limits -Wsign-conversion -Wcast-align +) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Woverloaded-virtual -Wnon-virtual-dtor -Wsign-promo") include_directories(catch ${library_dir}) -add_definitions(-DCATCH_CONFIG_FAST_COMPILE=1) +add_definitions(-DCATCH_CONFIG_FAST_COMPILE=1 -DCATCH_CONFIG_ENABLE_ALL_STRINGMAKERS=1) -set(common_sources catch/main.cpp ${library_dir}/canard.c ${library_dir}/canard_dsdl.c) +set(common_sources catch/main.cpp ${library_dir}/canard.c) function(gen_test name files compile_definitions compile_flags link_flags c_standard) add_executable(${name} ${common_sources} ${files}) target_compile_definitions(${name} PUBLIC ${compile_definitions}) target_link_libraries(${name} pthread) - set_target_properties(${name} PROPERTIES COMPILE_FLAGS "${compile_flags}" LINK_FLAGS "${link_flags}" C_STANDARD "${c_standard}") + set_target_properties( + ${name} + PROPERTIES + COMPILE_FLAGS "${compile_flags}" + LINK_FLAGS "${link_flags}" + C_STANDARD "${c_standard}" + ) add_test("run_${name}" "${name}" --rng-seed time) endfunction() @@ -72,28 +68,27 @@ function(gen_test_matrix name files compile_definitions compile_flags) # Coverage is only available for GCC builds. if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) gen_test("${name}_cov" - "${files}" - "${compile_definitions}" - "${compile_flags} -g -O0 --coverage" - "--coverage" - "11") + "${files}" + "${compile_definitions}" + "${compile_flags} -g -O0 --coverage" + "--coverage" + "11") endif () endfunction() # Disable missing declaration warning to allow exposure of private definitions. gen_test_matrix(test_private - "test_private_crc.cpp;test_private_rx.cpp;test_private_tx.cpp;test_dsdl.cpp" - "CANARD_PRIVATE=" - "-Wno-missing-declarations") - -# We assume here that the target platform is little-endian. If it's not, the test will fail. -# Perhaps this needs fixing: these tests need not be run if the target is not little-endian. -gen_test_matrix(test_private_le - "test_private_crc.cpp;test_private_rx.cpp;test_private_tx.cpp;test_dsdl.cpp" - "CANARD_PRIVATE=;CANARD_DSDL_CONFIG_LITTLE_ENDIAN=1" - "-Wno-missing-declarations") + "test_private_crc.cpp;test_private_rx.cpp;test_private_tx.cpp;test_private_cavl.cpp;" + "-DCANARD_CONFIG_HEADER=\"${CMAKE_CURRENT_SOURCE_DIR}/canard_config_private.h\"" + "-Wno-missing-declarations") +# test CRC with static table disabled +gen_test_matrix(test_private_crc_table + "test_private_crc.cpp;" + "-DCANARD_CONFIG_HEADER=\"${CMAKE_CURRENT_SOURCE_DIR}/canard_config_private.h\"" + "-DCANARD_CRC_TABLE=0" + "-Wno-missing-declarations") gen_test_matrix(test_public - "test_public_tx.cpp;test_public_rx.cpp;test_public_roundtrip.cpp;test_self.cpp" - "" - "") + "test_public_tx.cpp;test_public_rx.cpp;test_public_roundtrip.cpp;test_self.cpp;test_public_filters.cpp" + "" + "-Wmissing-declarations") diff --git a/tests/canard_config_private.h b/tests/canard_config_private.h new file mode 100644 index 00000000..c7db8dbe --- /dev/null +++ b/tests/canard_config_private.h @@ -0,0 +1,4 @@ +// This libcanard config header is included from canard.c via CANARD_CONFIG_HEADER + +// Expose the internal definitions for testing. +#define CANARD_PRIVATE diff --git a/tests/exposed.hpp b/tests/exposed.hpp index a29ff45f..104f5705 100644 --- a/tests/exposed.hpp +++ b/tests/exposed.hpp @@ -1,13 +1,13 @@ // This software is distributed under the terms of the MIT License. -// Copyright (c) 2016-2020 UAVCAN Development Team. +// Copyright (c) 2016 UAVCAN Development Team. #pragma once #include "canard.h" -#include "catch.hpp" #include #include #include +#include /// Definitions that are not exposed by the library but that are needed for testing. /// Please keep them in sync with the library by manually updating as necessary. @@ -15,11 +15,8 @@ namespace exposed { using TransferCRC = std::uint16_t; -struct TxQueueItem final +struct TxItem final : CanardTxQueueItem { - CanardFrame frame{}; - TxQueueItem* next = nullptr; - [[nodiscard]] auto getPayloadByte(const std::size_t offset) const -> std::uint8_t { return reinterpret_cast(frame.payload)[offset]; @@ -27,7 +24,11 @@ struct TxQueueItem final [[nodiscard]] auto getTailByte() const { - REQUIRE(frame.payload_size >= 1U); + if (frame.payload_size < 1U) + { + // Can't use REQUIRE because it is not thread-safe. + throw std::logic_error("Can't get the tail byte because the frame payload is empty."); + } return getPayloadByte(frame.payload_size - 1U); } @@ -35,11 +36,11 @@ struct TxQueueItem final [[nodiscard]] auto isEndOfTransfer() const { return (getTailByte() & 64U) != 0; } [[nodiscard]] auto isToggleBitSet() const { return (getTailByte() & 32U) != 0; } - ~TxQueueItem() = default; - TxQueueItem(const TxQueueItem&) = delete; - TxQueueItem(const TxQueueItem&&) = delete; - auto operator=(const TxQueueItem&) -> TxQueueItem& = delete; - auto operator=(const TxQueueItem&&) -> TxQueueItem& = delete; + ~TxItem() = default; + TxItem(const TxItem&) = delete; + TxItem(const TxItem&&) = delete; + auto operator=(const TxItem&) -> TxItem& = delete; + auto operator=(const TxItem&&) -> TxItem& = delete; }; struct RxSession @@ -81,11 +82,13 @@ auto txMakeServiceSessionSpecifier(const std::uint16_t service_id, const std::uint8_t src_node_id, const std::uint8_t dst_node_id) -> std::uint32_t; -auto txGetPresentationLayerMTU(const CanardInstance* const ins) -> std::size_t; +auto adjustPresentationLayerMTU(const std::size_t mtu_bytes) -> std::size_t; -auto txMakeCANID(const CanardTransfer* const transfer, - const std::uint8_t local_node_id, - const std::size_t presentation_layer_mtu) -> std::int32_t; +auto txMakeCANID(const CanardTransferMetadata* const tr, + const size_t payload_size, + const void* const payload, + const CanardNodeID local_node_id, + const size_t presentation_layer_mtu) -> std::int32_t; auto txMakeTailByte(const bool start_of_transfer, const bool end_of_transfer, @@ -94,9 +97,9 @@ auto txMakeTailByte(const bool start_of_transfer, auto txRoundFramePayloadSizeUp(const std::size_t x) -> std::size_t; -auto txFindQueueSupremum(const CanardInstance* const ins, const std::uint32_t can_id) -> TxQueueItem*; - -auto rxTryParseFrame(const CanardFrame* const frame, RxFrameModel* const out_result) -> bool; +auto rxTryParseFrame(const CanardMicrosecond timestamp_usec, + const CanardFrame* const frame, + RxFrameModel* const out_result) -> bool; auto rxSessionWritePayload(CanardInstance* const ins, RxSession* const rxs, @@ -112,9 +115,6 @@ auto rxSessionUpdate(CanardInstance* const ins, const std::uint8_t redundant_transport_index, const CanardMicrosecond transfer_id_timeout_usec, const std::size_t extent, - CanardTransfer* const out_transfer) -> std::int8_t; - -auto float16Pack(const float value) -> std::uint16_t; -auto float16Unpack(const std::uint16_t value) -> float; + CanardRxTransfer* const out_transfer) -> std::int8_t; } } // namespace exposed diff --git a/tests/helpers.hpp b/tests/helpers.hpp index 48d52fe3..ab8991a6 100644 --- a/tests/helpers.hpp +++ b/tests/helpers.hpp @@ -1,5 +1,5 @@ // This software is distributed under the terms of the MIT License. -// Copyright (c) 2016-2020 UAVCAN Development Team. +// Copyright (c) 2016 UAVCAN Development Team. #pragma once @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #if !(defined(CANARD_VERSION_MAJOR) && defined(CANARD_VERSION_MINOR)) # error "Library version not defined" @@ -46,14 +48,21 @@ inline auto getRandomNatural(const T upper_open) -> T return static_cast(static_cast(std::rand()) % upper_open); // NOLINT } +template +static inline void traverse(const CanardTreeNode* const root, const F& fun) // NOLINT recursion +{ + if (root != nullptr) + { + traverse(root->lr[0], fun); + fun(root); + traverse(root->lr[1], fun); + } +} + /// An allocator that sits on top of the standard malloc() providing additional testing capabilities. /// It allows the user to specify the maximum amount of memory that can be allocated; further requests will emulate OOM. class TestAllocator { - mutable std::recursive_mutex lock_; - std::unordered_map allocated_; - std::atomic ceiling_ = std::numeric_limits::max(); - public: TestAllocator() = default; TestAllocator(const TestAllocator&) = delete; @@ -67,54 +76,54 @@ class TestAllocator for (auto [ptr, _] : allocated_) { // Clang-tidy complains about manual memory management. Suppressed because we need it for testing purposes. - std::free(ptr); // NOLINT + std::free(ptr - canary_.size()); // NOLINT } } - [[nodiscard]] auto allocate(const std::size_t amount) + [[nodiscard]] auto allocate(const std::size_t amount) -> void* { std::unique_lock locker(lock_); - void* p = nullptr; + std::uint8_t* p = nullptr; if ((amount > 0U) && ((getTotalAllocatedAmount() + amount) <= ceiling_)) { + const auto amount_with_canaries = amount + canary_.size() * 2U; // Clang-tidy complains about manual memory management. Suppressed because we need it for testing purposes. - p = std::malloc(amount); // NOLINT + p = static_cast(std::malloc(amount_with_canaries)); // NOLINT if (p == nullptr) { throw std::bad_alloc(); // This is a test suite failure, not a failed test. Mind the difference. } - // Random-fill the memory to make sure no assumptions are made about its contents. - std::uniform_int_distribution dist(0, 255U); - std::generate_n(reinterpret_cast(p), amount, [&]() { - return static_cast(getRandomNatural(256U)); - }); + p += canary_.size(); + std::generate_n(p, amount, []() { return static_cast(getRandomNatural(256U)); }); + std::memcpy(p - canary_.size(), canary_.begin(), canary_.size()); + std::memcpy(p + amount, canary_.begin(), canary_.size()); allocated_.emplace(p, amount); } return p; } - /// This overload is needed to avoid unnecessary const_cast<> in tests. - /// The casts are needed because allocated memory is pointed to by const-qualified pointers. - /// This is due to certain fundamental limitations of C; see the API docs for info. - void deallocate(const void* const pointer) + void deallocate(void* const user_pointer) { - deallocate(const_cast(pointer)); // NOLINT - } - - void deallocate(void* const pointer) - { - if (pointer != nullptr) + if (user_pointer != nullptr) { std::unique_lock locker(lock_); - const auto it = allocated_.find(pointer); - REQUIRE(it != std::end(allocated_)); // Catch an attempt to deallocate memory that is not allocated. - // Damage the memory to make sure it's not used after deallocation. - std::uniform_int_distribution dist(0, 255U); - std::generate_n(reinterpret_cast(pointer), it->second, [&]() { - return static_cast(getRandomNatural(256U)); - }); - // Clang-tidy complains about manual memory management. Suppressed because we need it for testing purposes. - std::free(it->first); // NOLINT + const auto it = allocated_.find(static_cast(user_pointer)); + if (it == std::end(allocated_)) // Catch an attempt to deallocate memory that is not allocated. + { + throw std::logic_error("Attempted to deallocate memory that was never allocated; ptr=" + + std::to_string(reinterpret_cast(user_pointer))); + } + const auto [p, amount] = *it; + if ((0 != std::memcmp(p - canary_.size(), canary_.begin(), canary_.size())) || + (0 != std::memcmp(p + amount, canary_.begin(), canary_.size()))) + { + throw std::logic_error("Dead canary detected at ptr=" + + std::to_string(reinterpret_cast(user_pointer))); + } + std::generate_n(p - canary_.size(), // Damage the memory to make sure it's not used after deallocation. + amount + canary_.size() * 2U, + []() { return static_cast(getRandomNatural(256U)); }); + std::free(p - canary_.size()); // NOLINT we require manual memory management here. allocated_.erase(it); } } @@ -138,26 +147,25 @@ class TestAllocator [[nodiscard]] auto getAllocationCeiling() const { return static_cast(ceiling_); } void setAllocationCeiling(const std::size_t amount) { ceiling_ = amount; } -}; -/// An enhancing wrapper over the library to remove boilerplate from the tests. -class Instance -{ - CanardInstance canard_ = canardInit(&Instance::trampolineAllocate, &Instance::trampolineDeallocate); - TestAllocator allocator_; - - static auto trampolineAllocate(CanardInstance* const ins, const std::size_t amount) -> void* +private: + static auto makeCanary() -> std::array { - auto* p = reinterpret_cast(ins->user_reference); - return p->allocator_.allocate(amount); + std::array out{}; + std::generate_n(out.begin(), out.size(), []() { return static_cast(getRandomNatural(256U)); }); + return out; } - static void trampolineDeallocate(CanardInstance* const ins, void* const pointer) - { - auto* p = reinterpret_cast(ins->user_reference); - p->allocator_.deallocate(pointer); - } + const std::array canary_ = makeCanary(); + + mutable std::recursive_mutex lock_; + std::unordered_map allocated_; + std::atomic ceiling_ = std::numeric_limits::max(); +}; +/// An enhancing wrapper over the library to remove boilerplate from the tests. +class Instance +{ public: Instance() { canard_.user_reference = this; } @@ -168,18 +176,18 @@ class Instance auto operator=(const Instance&) -> Instance& = delete; auto operator=(const Instance&&) -> Instance& = delete; - [[nodiscard]] auto txPush(const CanardTransfer& transfer) { return canardTxPush(&canard_, &transfer); } - - [[nodiscard]] auto txPeek() const { return canardTxPeek(&canard_); } - - void txPop() { canardTxPop(&canard_); } - - [[nodiscard]] auto rxAccept(const CanardFrame& frame, + [[nodiscard]] auto rxAccept(const CanardMicrosecond timestamp_usec, + const CanardFrame& frame, const uint8_t redundant_transport_index, - CanardTransfer& out_transfer, + CanardRxTransfer& out_transfer, CanardRxSubscription** const out_subscription) { - return canardRxAccept2(&canard_, &frame, redundant_transport_index, &out_transfer, out_subscription); + return canardRxAccept(&canard_, + timestamp_usec, + &frame, + redundant_transport_index, + &out_transfer, + out_subscription); } [[nodiscard]] auto rxSubscribe(const CanardTransferKind transfer_kind, @@ -196,33 +204,148 @@ class Instance return canardRxUnsubscribe(&canard_, transfer_kind, port_id); } + /// The items are sorted by port-ID. + [[nodiscard]] auto getSubs(const CanardTransferKind tk) const -> std::vector + { + std::vector out; + traverse(canard_.rx_subscriptions[tk], [&](const CanardTreeNode* const item) { + out.push_back(reinterpret_cast(item)); + }); + return out; + } + [[nodiscard]] auto getMessageSubs() const { return getSubs(CanardTransferKindMessage); } + [[nodiscard]] auto getResponseSubs() const { return getSubs(CanardTransferKindResponse); } + [[nodiscard]] auto getRequestSubs() const { return getSubs(CanardTransferKindRequest); } + [[nodiscard]] auto getNodeID() const { return canard_.node_id; } void setNodeID(const std::uint8_t x) { canard_.node_id = x; } - [[nodiscard]] auto getMTU() const { return canard_.mtu_bytes; } - void setMTU(const std::size_t x) { canard_.mtu_bytes = x; } + [[nodiscard]] auto getAllocator() -> TestAllocator& { return allocator_; } + + [[nodiscard]] auto getInstance() -> CanardInstance& { return canard_; } + [[nodiscard]] auto getInstance() const -> const CanardInstance& { return canard_; } + +private: + static auto trampolineAllocate(CanardInstance* const ins, const std::size_t amount) -> void* + { + auto* p = reinterpret_cast(ins->user_reference); + return p->allocator_.allocate(amount); + } + + static void trampolineDeallocate(CanardInstance* const ins, void* const pointer) + { + auto* p = reinterpret_cast(ins->user_reference); + p->allocator_.deallocate(pointer); + } + + CanardInstance canard_ = canardInit(&Instance::trampolineAllocate, &Instance::trampolineDeallocate); + TestAllocator allocator_; +}; + +class TxQueue +{ +public: + explicit TxQueue(const std::size_t capacity, const std::size_t mtu_bytes) : que_(canardTxInit(capacity, mtu_bytes)) + { + enforce(que_.user_reference == nullptr, "Incorrect initialization of the user reference in TxQueue"); + enforce(que_.mtu_bytes == mtu_bytes, "Incorrect MTU"); + que_.user_reference = this; // This is simply to ensure it is not overwritten unexpectedly. + checkInvariants(); + } + virtual ~TxQueue() = default; + + TxQueue(const TxQueue&) = delete; + TxQueue(TxQueue&&) = delete; + auto operator=(const TxQueue&) -> TxQueue& = delete; + auto operator=(TxQueue&&) -> TxQueue& = delete; + + [[nodiscard]] auto getMTU() const { return que_.mtu_bytes; } + void setMTU(const std::size_t x) { que_.mtu_bytes = x; } - [[nodiscard]] auto getTxQueueRoot() const + [[nodiscard]] auto push(CanardInstance* const ins, + const CanardMicrosecond transmission_deadline_usec, + const CanardTransferMetadata& metadata, + const size_t payload_size, + const void* const payload) { - return reinterpret_cast(canard_._tx_queue); + checkInvariants(); + const auto size_before = que_.size; + const auto ret = canardTxPush(&que_, ins, transmission_deadline_usec, &metadata, payload_size, payload); + enforce((ret < 0) || ((size_before + static_cast(ret)) == que_.size), + "Unexpected size change after push"); + checkInvariants(); + return ret; } - [[nodiscard]] auto getTxQueueLength() const + [[nodiscard]] auto peek() const -> const exposed::TxItem* { - std::size_t out = 0U; - const auto* p = getTxQueueRoot(); - while (p != nullptr) + checkInvariants(); + const auto before = que_.size; + const auto* const ret = canardTxPeek(&que_); + enforce(((ret == nullptr) ? (before == 0) : (before > 0)) && (que_.size == before), "Bad peek"); + checkInvariants(); + return static_cast(ret); // NOLINT static downcast + } + + [[nodiscard]] auto pop(const CanardTxQueueItem* const which) -> exposed::TxItem* + { + checkInvariants(); + const auto size_before = que_.size; + const auto* volatile pk = peek(); + auto* out = canardTxPop(&que_, which); + enforce(pk == out, "Peek/pop pointer mismatch"); + if (out == nullptr) { - ++out; - p = p->next; + enforce((size_before == 0) && (que_.size == 0), "Bad empty pop"); } + else + { + enforce((size_before > 0) && (que_.size == (size_before - 1U)), "Bad non-empty pop"); + } + checkInvariants(); + return static_cast(out); // NOLINT static downcast + } + + [[nodiscard]] auto getSize() const + { + std::size_t out = 0; + traverse(que_.root, [&](auto* _) { + (void) _; + out++; + }); + enforce(que_.size == out, "Size miscalculation"); return out; } - [[nodiscard]] auto getAllocator() -> TestAllocator& { return allocator_; } + [[nodiscard]] auto linearize() const -> std::vector + { + std::vector out; + traverse(que_.root, [&](const CanardTreeNode* const item) { + out.push_back(reinterpret_cast(item)); + }); + enforce(out.size() == getSize(), "Internal error"); + return out; + } - [[nodiscard]] auto getInstance() -> CanardInstance& { return canard_; } - [[nodiscard]] auto getInstance() const -> const CanardInstance& { return canard_; } + [[nodiscard]] auto getInstance() -> CanardTxQueue& { return que_; } + [[nodiscard]] auto getInstance() const -> const CanardTxQueue& { return que_; } + +private: + static void enforce(const bool expect_true, const std::string& message) + { + if (!expect_true) + { + throw std::logic_error("TxQueue invariant violation: " + message); + } + } + + void checkInvariants() const + { + enforce(que_.user_reference == this, "User reference damaged"); + enforce(que_.size == getSize(), "Size miscalculation"); + } + + CanardTxQueue que_; }; } // namespace helpers diff --git a/tests/test_dsdl.cpp b/tests/test_dsdl.cpp deleted file mode 100644 index 43b9c2f2..00000000 --- a/tests/test_dsdl.cpp +++ /dev/null @@ -1,577 +0,0 @@ -// This software is distributed under the terms of the MIT License. -// Copyright (c) 2016-2020 UAVCAN Development Team. - -#include "canard_dsdl.h" -#include "exposed.hpp" -#include -#include -#include -#include - -TEST_CASE("float16Pack") -{ - using exposed::float16Pack; - REQUIRE(0b0000000000000000 == float16Pack(0.0F)); - REQUIRE(0b0011110000000000 == float16Pack(1.0F)); - REQUIRE(0b1100000000000000 == float16Pack(-2.0F)); - REQUIRE(0b0111110000000000 == float16Pack(999999.0F)); // +inf - REQUIRE(0b1111101111111111 == float16Pack(-65519.0F)); // -max - - // These are intrusive tests, they make assumptions about the specific implementation of the conversion logic. - // Normally, one wouldn't be able to compare a NaN against a particular number because there are many ways to - // represent it. We do not differentiate between sNaN and qNaN because there is no platform-agnostic way to do - // that; see https://github.com/UAVCAN/nunavut/pull/115#issuecomment-704185463 - REQUIRE(0b0111111000000000 == float16Pack(+std::numeric_limits::quiet_NaN())); - REQUIRE(0b1111111000000000 == float16Pack(-std::numeric_limits::quiet_NaN())); - REQUIRE(0b0111111000000000 == float16Pack(+std::numeric_limits::signaling_NaN())); - REQUIRE(0b1111111000000000 == float16Pack(-std::numeric_limits::signaling_NaN())); -} - -TEST_CASE("float16Unpack") -{ - using exposed::float16Unpack; - REQUIRE(Approx(0.0F) == float16Unpack(0b0000000000000000)); - REQUIRE(Approx(1.0F) == float16Unpack(0b0011110000000000)); - REQUIRE(Approx(-2.0F) == float16Unpack(0b1100000000000000)); - REQUIRE(Approx(-65504.0F) == float16Unpack(0b1111101111111111)); - REQUIRE(std::isinf(float16Unpack(0b0111110000000000))); - - const auto explode_sign_exp_mantissa = [](const float f) -> std::tuple { - std::uint32_t n = 0; - std::memcpy(&n, &f, 4); - return std::make_tuple((n & (1UL << 31U)) != 0, - static_cast((n >> 23U) & 0xFFU), - n & ((1UL << 23U) - 1U)); - }; - - { - const auto [sign, exp, man] = explode_sign_exp_mantissa(float16Unpack(0b0111111111111111)); // +qNaN - REQUIRE(!sign); - REQUIRE(exp == 0xFFU); - REQUIRE(man != 0); - } - { - const auto [sign, exp, man] = explode_sign_exp_mantissa(float16Unpack(0b0111111000000000)); // +qNaN - REQUIRE(!sign); - REQUIRE(exp == 0xFFU); - REQUIRE(man != 0); - } - { - const auto [sign, exp, man] = explode_sign_exp_mantissa(float16Unpack(0b1111111111111111)); // -qNaN - REQUIRE(sign); - REQUIRE(exp == 0xFFU); - REQUIRE(man != 0); - } - { - const auto [sign, exp, man] = explode_sign_exp_mantissa(float16Unpack(0b0111110111111111)); // +sNaN - REQUIRE(!sign); - REQUIRE(exp == 0xFFU); - REQUIRE(man != 0); - } - { - const auto [sign, exp, man] = explode_sign_exp_mantissa(float16Unpack(0b1111110000000001)); // -sNaN - REQUIRE(sign); - REQUIRE(exp == 0xFFU); - REQUIRE(man != 0); - } - - REQUIRE(bool(std::isnan(float16Unpack(0b0111111111111111)))); // +quiet - REQUIRE(bool(std::isnan(float16Unpack(0b0111111000000000)))); // +quiet - REQUIRE(bool(std::isnan(float16Unpack(0b1111111111111111)))); // -quiet - REQUIRE(bool(std::isnan(float16Unpack(0b1111111000000000)))); // -quiet - REQUIRE(bool(std::isnan(float16Unpack(0b0111110111111111)))); // +signaling - REQUIRE(bool(std::isnan(float16Unpack(0b0111110000000001)))); // +signaling - REQUIRE(bool(std::isnan(float16Unpack(0b1111110111111111)))); // -signaling - REQUIRE(bool(std::isnan(float16Unpack(0b1111110000000001)))); // -signaling -} - -TEST_CASE("canardDSDLFloat16") -{ - using exposed::float16Pack; - using exposed::float16Unpack; - float x = -1000.0F; - while (x <= 1000.0F) - { - REQUIRE(Approx(x) == float16Unpack(float16Pack(x))); - x += 0.5F; - } - - REQUIRE(0b0111110000000000 == float16Pack(float16Unpack(0b0111110000000000))); // +inf - REQUIRE(0b1111110000000000 == float16Pack(float16Unpack(0b1111110000000000))); // -inf - - // These are intrusive tests, they make assumptions about the specific implementation of the conversion logic. - // Normally, one wouldn't be able to compare a NaN against a particular number because there are many ways to - // represent it. We do not differentiate between sNaN and qNaN because there is no platform-agnostic way to do - // that; see https://github.com/UAVCAN/nunavut/pull/115#issuecomment-704185463 - REQUIRE(0b0111111000000000 == float16Pack(float16Unpack(0b0111111111111111))); // +qNaN, extra bits stripped - REQUIRE(0b0111111000000000 == float16Pack(float16Unpack(0b0111110111111111))); // +sNaN, extra bits stripped - REQUIRE(0b1111111000000000 == float16Pack(float16Unpack(0b1111111111111111))); // -qNaN, extra bits stripped - REQUIRE(0b1111111000000000 == float16Pack(float16Unpack(0b1111110111111111))); // -sNaN, extra bits stripped -} - -TEST_CASE("canardDSDLCopyBits") -{ - { - uint8_t a = 0; - uint8_t b = 0; - canardDSDLCopyBits(0, 0, 0, &a, &b); - } - - const auto test = [&](const size_t length_bit, - const size_t src_offset_bit, - const size_t dst_offset_bit, - const std::vector& src, - const std::vector& dst, - const std::vector& ref) { - REQUIRE(length_bit <= (dst.size() * 8)); - REQUIRE(length_bit <= (src.size() * 8)); - std::vector result = dst; - canardDSDLCopyBits(length_bit, src_offset_bit, dst_offset_bit, src.data(), result.data()); - return std::equal(std::begin(ref), std::end(ref), std::begin(result)); - }; - - REQUIRE(test(8, 0, 0, {0xFF}, {0x00}, {0xFF})); - REQUIRE(test(16, 0, 0, {0xFF, 0xFF}, {0x00, 0x00}, {0xFF, 0xFF})); - REQUIRE(test(12, 0, 0, {0xFF, 0x0A}, {0x55, 0x00}, {0xFF, 0x0A})); - REQUIRE(test(12, 0, 0, {0xFF, 0x0A}, {0x00, 0xF0}, {0xFF, 0xFA})); - REQUIRE(test(12, 0, 4, {0xFF, 0x0A}, {0x53, 0x55}, {0xF3, 0xAF})); - REQUIRE(test(8, 4, 4, {0x55, 0x55}, {0xAA, 0xAA}, {0x5A, 0xA5})); -} - -TEST_CASE("canardDSDLSerialize_aligned") -{ - // The reference values for the following test have been taken from the PyUAVCAN test suite. - const std::vector Reference({0xA7, 0xEF, 0xCD, 0xAB, 0x90, 0x78, 0x56, 0x34, 0x12, 0x88, 0xA9, 0xCB, - 0xED, 0xFE, 0xFF, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, - 0x3F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x7C, 0xDA, 0x0E, 0xDA, 0xBE, 0xFE, - 0x01, 0xAD, 0xDE, 0xEF, 0xBE, 0xC5, 0x67, 0xC5, 0x0B}); - - std::vector dest(std::size(Reference)); - - const auto set_b = [&](const std::size_t offset_bit, const bool value) { - canardDSDLSetBit(dest.data(), offset_bit, value); - }; - const auto set_u = [&](const std::size_t offset_bit, const std::uint64_t value, const std::uint8_t length_bit) { - canardDSDLSetUxx(dest.data(), offset_bit, value, length_bit); - }; - const auto set_i = [&](const std::size_t offset_bit, const std::int64_t value, const std::uint8_t length_bit) { - canardDSDLSetIxx(dest.data(), offset_bit, value, length_bit); - }; - const auto set_f16 = [&](const std::size_t offset_bit, const float value) { - canardDSDLSetF16(dest.data(), offset_bit, value); - }; - const auto set_f32 = [&](const std::size_t offset_bit, const float value) { - canardDSDLSetF32(dest.data(), offset_bit, value); - }; - const auto set_f64 = [&](const std::size_t offset_bit, const double value) { - canardDSDLSetF64(dest.data(), offset_bit, value); - }; - - set_u(0, 0b1010'0111, 8); - set_i(8, 0x1234'5678'90ab'cdef, 64); - set_i(72, -0x1234'5678, 32); - set_i(104, -2, 16); - set_u(120, 0, 8); - set_i(128, 127, 8); - set_f64(136, 1.0); - set_f32(200, 1.0F); - set_f16(232, 99999.9F); - set_u(248, 0xBEDA, 12); // Truncation - set_u(260, 0, 4); - set_u(264, 0xBEDA, 16); - set_i(280, -2, 9); - set_i(289, 0, 7); - set_u(296, 0xDEAD, 16); - set_u(312, 0xBEEF, 16); - - std::size_t offset = 328; - const auto push_bit = [&](const bool value) { - set_b(offset, value); - ++offset; - }; - - push_bit(true); - push_bit(false); - push_bit(true); - push_bit(false); - push_bit(false); - push_bit(false); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(false); - push_bit(false); - push_bit(true); - push_bit(true); - push_bit(false); - - push_bit(true); - push_bit(false); - push_bit(true); - push_bit(false); - push_bit(false); - push_bit(false); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(false); - push_bit(true); - push_bit(false); - - set_u(357, 0, 3); - - REQUIRE(std::size(dest) == std::size(Reference)); - REQUIRE_THAT(dest, Catch::Matchers::Equals(Reference)); -} - -TEST_CASE("canardDSDLSerialize_unaligned") -{ - // The reference values for the following test have been taken from the PyUAVCAN test suite. - const std::vector Reference({ - 0xC5, 0x2F, 0x57, 0x82, 0xC6, 0xCA, 0x12, 0x34, 0x56, 0xD9, 0xBF, 0xEC, 0x06, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xFC, 0x01, 0xE0, 0x6F, 0xF5, 0x7E, 0xF7, 0x05 // - }); - - std::vector dest(std::size(Reference)); - - const auto set_b = [&](const std::size_t offset_bit, const bool value) { - canardDSDLSetBit(dest.data(), offset_bit, value); - }; - const auto set_u = [&](const std::size_t offset_bit, const std::uint64_t value, const std::uint8_t length_bit) { - canardDSDLSetUxx(dest.data(), offset_bit, value, length_bit); - }; - const auto set_i = [&](const std::size_t offset_bit, const std::int64_t value, const std::uint8_t length_bit) { - canardDSDLSetIxx(dest.data(), offset_bit, value, length_bit); - }; - const auto set_f16 = [&](const std::size_t offset_bit, const float value) { - canardDSDLSetF16(dest.data(), offset_bit, value); - }; - const auto set_f32 = [&](const std::size_t offset_bit, const float value) { - canardDSDLSetF32(dest.data(), offset_bit, value); - }; - const auto set_f64 = [&](const std::size_t offset_bit, const double value) { - canardDSDLSetF64(dest.data(), offset_bit, value); - }; - - std::size_t offset = 0; - const auto push_bit = [&](const bool value) { - set_b(offset, value); - ++offset; - }; - - push_bit(true); - push_bit(false); - push_bit(true); - push_bit(false); - push_bit(false); - push_bit(false); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(true); - - push_bit(true); - push_bit(false); - push_bit(true); - push_bit(false); - push_bit(false); - push_bit(true); - push_bit(true); - push_bit(true); - push_bit(false); - push_bit(true); - - REQUIRE_THAT(std::vector(std::begin(dest), std::begin(dest) + 2), - Catch::Matchers::Equals(std::vector(std::begin(Reference), std::begin(Reference) + 2))); - - set_u(21, 0x12, 8); - set_u(29, 0x34, 8); - set_u(37, 0x56, 8); - - REQUIRE_THAT(std::vector(std::begin(dest), std::begin(dest) + 5), - Catch::Matchers::Equals(std::vector(std::begin(Reference), std::begin(Reference) + 5))); - - offset = 45; - push_bit(false); - push_bit(true); - push_bit(true); - - set_u(48, 0x12, 8); - set_u(56, 0x34, 8); - set_u(64, 0x56, 8); - - offset = 72; - push_bit(true); - push_bit(false); - push_bit(false); - push_bit(true); - push_bit(true); - - REQUIRE_THAT(std::vector(std::begin(dest), std::begin(dest) + 9), - Catch::Matchers::Equals(std::vector(std::begin(Reference), std::begin(Reference) + 9))); - - set_i(77, -2, 8); - set_u(85, 0b11101100101, 11); - set_u(96, 0b1110, 3); // Truncation - - REQUIRE_THAT(std::vector(std::begin(dest), std::begin(dest) + 12), - Catch::Matchers::Equals(std::vector(std::begin(Reference), std::begin(Reference) + 12))); - - set_f64(99, 1.0); - set_f32(163, 1.0F); - set_f16(195, -99999.0F); - - set_u(211, 0xDEAD, 16); - set_u(227, 0xBEEF, 16); - set_u(243, 0, 5); - - REQUIRE(std::size(dest) == std::size(Reference)); - REQUIRE_THAT(dest, Catch::Matchers::Equals(Reference)); -} - -TEST_CASE("canardDSDLSerialize_heartbeat") -{ - const std::vector Reference({239, 190, 173, 222, 3, 2, 127, 0}); - - std::vector dest(std::size(Reference)); - - const auto set_u = [&](const std::size_t offset_bit, const std::uint64_t value, const std::uint8_t length_bit) { - canardDSDLSetUxx(dest.data(), offset_bit, value, length_bit); - }; - - set_u(40, 2, 8); // mode - set_u(0, 0xDEADBEEF, 32); // uptime - set_u(48, 0x7F, 8); // vssc - set_u(32, 3, 8); // health - - REQUIRE(std::size(dest) == std::size(Reference)); - REQUIRE_THAT(dest, Catch::Matchers::Equals(Reference)); -} - -TEST_CASE("canardDSDLDeserialize_manual") -{ - const std::array data{ - {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77}}; - REQUIRE(0xFF == canardDSDLGetU8(data.data(), 8, 0, 8)); - REQUIRE(0xFF == canardDSDLGetU8(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI8(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI8(data.data(), 8, 0, 8)); - REQUIRE(-1 == canardDSDLGetI8(data.data(), 8, 0, 7)); - REQUIRE(-1 == canardDSDLGetI8(data.data(), 8, 0, 2)); - - REQUIRE(0xFFFF == canardDSDLGetU16(data.data(), 8, 0, 16)); - REQUIRE(0xFFFF == canardDSDLGetU16(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI16(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI16(data.data(), 8, 0, 16)); - REQUIRE(-1 == canardDSDLGetI16(data.data(), 8, 0, 15)); - REQUIRE(-1 == canardDSDLGetI16(data.data(), 8, 0, 2)); - - REQUIRE(0xFFFFFFFF == canardDSDLGetU32(data.data(), 8, 0, 32)); - REQUIRE(0xFFFFFFFF == canardDSDLGetU32(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI32(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI32(data.data(), 8, 0, 32)); - REQUIRE(-1 == canardDSDLGetI32(data.data(), 8, 0, 31)); - REQUIRE(-1 == canardDSDLGetI32(data.data(), 8, 0, 2)); - - REQUIRE(0xFFFFFFFFFFFFFFFF == canardDSDLGetU64(data.data(), 8, 0, 64)); - REQUIRE(0xFFFFFFFFFFFFFFFF == canardDSDLGetU64(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI64(data.data(), 8, 0, 255)); - REQUIRE(-1 == canardDSDLGetI64(data.data(), 8, 0, 64)); - REQUIRE(-1 == canardDSDLGetI64(data.data(), 8, 0, 63)); - REQUIRE(-1 == canardDSDLGetI64(data.data(), 8, 0, 2)); - - REQUIRE(0 == canardDSDLGetI8(data.data(), 8, 0, 0)); - REQUIRE(0 == canardDSDLGetI16(data.data(), 8, 0, 0)); - REQUIRE(0 == canardDSDLGetI32(data.data(), 8, 0, 0)); - REQUIRE(0 == canardDSDLGetI64(data.data(), 8, 0, 0)); - - REQUIRE(0 == canardDSDLGetI8(data.data(), 0, 0, 255)); - REQUIRE(0 == canardDSDLGetI16(data.data(), 0, 0, 255)); - REQUIRE(0 == canardDSDLGetI32(data.data(), 0, 0, 255)); - REQUIRE(0 == canardDSDLGetI64(data.data(), 0, 0, 255)); - - REQUIRE(0x77 == canardDSDLGetU8(data.data(), 16, 64, 8)); - REQUIRE(0x77 == canardDSDLGetU8(data.data(), 16, 64, 255)); - REQUIRE(0x77 == canardDSDLGetI8(data.data(), 16, 64, 255)); - REQUIRE(0x77 == canardDSDLGetI8(data.data(), 16, 64, 8)); - REQUIRE(0 > canardDSDLGetI8(data.data(), 16, 64, 7)); - - REQUIRE(0x7777 == canardDSDLGetU16(data.data(), 16, 64, 16)); - REQUIRE(0x7777 == canardDSDLGetU16(data.data(), 16, 64, 255)); - REQUIRE(0x7777 == canardDSDLGetI16(data.data(), 16, 64, 255)); - REQUIRE(0x7777 == canardDSDLGetI16(data.data(), 16, 64, 16)); - REQUIRE(0 > canardDSDLGetI16(data.data(), 16, 64, 15)); - - REQUIRE(0x77777777 == canardDSDLGetU32(data.data(), 16, 64, 32)); - REQUIRE(0x77777777 == canardDSDLGetU32(data.data(), 16, 64, 255)); - REQUIRE(0x77777777 == canardDSDLGetI32(data.data(), 16, 64, 255)); - REQUIRE(0x77777777 == canardDSDLGetI32(data.data(), 16, 64, 32)); - REQUIRE(0 > canardDSDLGetI32(data.data(), 16, 64, 31)); - - REQUIRE(0x7777777777777777 == canardDSDLGetU64(data.data(), 16, 64, 64)); - REQUIRE(0x7777777777777777 == canardDSDLGetU64(data.data(), 16, 64, 255)); - REQUIRE(0x7777777777777777 == canardDSDLGetI64(data.data(), 16, 64, 255)); - REQUIRE(0x7777777777777777 == canardDSDLGetI64(data.data(), 16, 64, 64)); - REQUIRE(0 > canardDSDLGetI64(data.data(), 16, 64, 63)); -} - -TEST_CASE("canardDSDLDeserialize_aligned") -{ - // The reference values for the following test have been taken from the PyUAVCAN test suite. - const std::vector Reference({0xA7, 0xEF, 0xCD, 0xAB, 0x90, 0x78, 0x56, 0x34, 0x12, 0x88, 0xA9, 0xCB, - 0xED, 0xFE, 0xFF, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, - 0x3F, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x7C, 0xDA, 0x0E, 0xDA, 0xBE, 0xFE, - 0x01, 0xAD, 0xDE, 0xEF, 0xBE, 0xC5, 0x67, 0xC5, 0x0B}); - const std::uint8_t* const buf = Reference.data(); - - REQUIRE(canardDSDLGetBit(buf, 1, 0)); - REQUIRE(!canardDSDLGetBit(buf, 1, 3)); - REQUIRE(!canardDSDLGetBit(buf, 0, 0)); // IZER - - REQUIRE(0b1010'0111 == canardDSDLGetU8(buf, 45, 0, 8)); - - REQUIRE(0x1234'5678'90ab'cdef == canardDSDLGetI64(buf, 45, 8, 64)); - REQUIRE(0x1234'5678'90ab'cdef == canardDSDLGetU64(buf, 45, 8, 64)); - REQUIRE(0xef == canardDSDLGetU8(buf, 45, 8, 64)); - - REQUIRE(-0x1234'5678 == canardDSDLGetI32(buf, 45, 72, 32)); - REQUIRE(-2 == canardDSDLGetI16(buf, 45, 104, 16)); - REQUIRE(0 == canardDSDLGetU8(buf, 45, 120, 8)); - REQUIRE(127 == canardDSDLGetI8(buf, 45, 128, 8)); - REQUIRE(Approx(1.0) == canardDSDLGetF64(buf, 45, 136)); - REQUIRE(Approx(1.0F) == canardDSDLGetF32(buf, 45, 200)); - REQUIRE(std::isinf(canardDSDLGetF16(buf, 45, 232))); - - REQUIRE(0x0EDA == canardDSDLGetU16(buf, 45, 248, 12)); - REQUIRE(0 == canardDSDLGetU8(buf, 45, 260, 4)); - REQUIRE(0xBEDA == canardDSDLGetU16(buf, 45, 264, 16)); - REQUIRE(-2 == canardDSDLGetI16(buf, 45, 280, 9)); - REQUIRE(0 == canardDSDLGetI16(buf, 45, 289, 7)); - REQUIRE(0 == canardDSDLGetU16(buf, 45, 289, 7)); - REQUIRE(0 == canardDSDLGetI8(buf, 45, 289, 7)); - REQUIRE(0 == canardDSDLGetU8(buf, 45, 289, 7)); - - REQUIRE(0xDEAD == canardDSDLGetU16(buf, 45, 296, 16)); - REQUIRE(0xBEEF == canardDSDLGetU16(buf, 45, 312, 16)); - REQUIRE(0xDEAD == canardDSDLGetU32(buf, 45, 296, 16)); - REQUIRE(0xBEEF == canardDSDLGetU32(buf, 45, 312, 16)); - REQUIRE(0xDEAD == canardDSDLGetU64(buf, 45, 296, 16)); - REQUIRE(0xBEEF == canardDSDLGetU64(buf, 45, 312, 16)); - REQUIRE(0xAD == canardDSDLGetU8(buf, 45, 296, 16)); - REQUIRE(0xEF == canardDSDLGetU8(buf, 45, 312, 16)); - REQUIRE(0x00AD == canardDSDLGetU16(buf, 38, 296, 16)); - REQUIRE(0x0000 == canardDSDLGetU32(buf, 37, 296, 16)); - - REQUIRE(canardDSDLGetBit(buf, 45, 328)); - REQUIRE(!canardDSDLGetBit(buf, 45, 329)); - REQUIRE(canardDSDLGetBit(buf, 45, 330)); - REQUIRE(!canardDSDLGetBit(buf, 45, 331)); - REQUIRE(!canardDSDLGetBit(buf, 45, 332)); - REQUIRE(!canardDSDLGetBit(buf, 45, 333)); - REQUIRE(canardDSDLGetBit(buf, 45, 334)); - REQUIRE(canardDSDLGetBit(buf, 45, 335)); - REQUIRE(canardDSDLGetBit(buf, 45, 336)); - REQUIRE(canardDSDLGetBit(buf, 45, 337)); - REQUIRE(canardDSDLGetBit(buf, 45, 338)); - REQUIRE(!canardDSDLGetBit(buf, 45, 339)); - REQUIRE(!canardDSDLGetBit(buf, 45, 340)); - REQUIRE(canardDSDLGetBit(buf, 45, 341)); - REQUIRE(canardDSDLGetBit(buf, 45, 342)); - REQUIRE(!canardDSDLGetBit(buf, 45, 343)); - - REQUIRE(canardDSDLGetBit(buf, 45, 344)); - REQUIRE(!canardDSDLGetBit(buf, 45, 345)); - REQUIRE(canardDSDLGetBit(buf, 45, 346)); - REQUIRE(!canardDSDLGetBit(buf, 45, 347)); - REQUIRE(!canardDSDLGetBit(buf, 45, 348)); - REQUIRE(!canardDSDLGetBit(buf, 45, 349)); - REQUIRE(canardDSDLGetBit(buf, 45, 350)); - REQUIRE(canardDSDLGetBit(buf, 45, 351)); - REQUIRE(canardDSDLGetBit(buf, 45, 352)); - REQUIRE(canardDSDLGetBit(buf, 45, 353)); - REQUIRE(!canardDSDLGetBit(buf, 45, 354)); - REQUIRE(canardDSDLGetBit(buf, 45, 355)); - REQUIRE(!canardDSDLGetBit(buf, 45, 356)); - - REQUIRE(0 == canardDSDLGetU8(buf, 45, 357, 3)); - - REQUIRE(!canardDSDLGetBit(buf, 44, 355)); -} - -TEST_CASE("canardDSDLDeserialize_unaligned") -{ - // The reference values for the following test have been taken from the PyUAVCAN test suite. - const std::vector Reference({ - 0xC5, 0x2F, 0x57, 0x82, 0xC6, 0xCA, 0x12, 0x34, 0x56, 0xD9, 0xBF, 0xEC, 0x06, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0x00, 0xFC, 0x01, 0xE0, 0x6F, 0xF5, 0x7E, 0xF7, 0x05 // - }); - const std::uint8_t* const buf = Reference.data(); - - REQUIRE(canardDSDLGetBit(buf, 31, 0)); - REQUIRE(!canardDSDLGetBit(buf, 31, 1)); - REQUIRE(canardDSDLGetBit(buf, 31, 2)); - REQUIRE(!canardDSDLGetBit(buf, 31, 3)); - REQUIRE(!canardDSDLGetBit(buf, 31, 4)); - REQUIRE(!canardDSDLGetBit(buf, 31, 5)); - REQUIRE(canardDSDLGetBit(buf, 31, 6)); - REQUIRE(canardDSDLGetBit(buf, 31, 7)); - REQUIRE(canardDSDLGetBit(buf, 31, 8)); - REQUIRE(canardDSDLGetBit(buf, 31, 9)); - REQUIRE(canardDSDLGetBit(buf, 31, 10)); - - REQUIRE(canardDSDLGetBit(buf, 31, 11)); - REQUIRE(!canardDSDLGetBit(buf, 31, 12)); - REQUIRE(canardDSDLGetBit(buf, 31, 13)); - REQUIRE(!canardDSDLGetBit(buf, 31, 14)); - REQUIRE(!canardDSDLGetBit(buf, 31, 15)); - REQUIRE(canardDSDLGetBit(buf, 31, 16)); - REQUIRE(canardDSDLGetBit(buf, 31, 17)); - REQUIRE(canardDSDLGetBit(buf, 31, 18)); - REQUIRE(!canardDSDLGetBit(buf, 31, 19)); - REQUIRE(canardDSDLGetBit(buf, 31, 20)); - - REQUIRE(0x12 == canardDSDLGetU8(buf, 31, 21, 8)); - REQUIRE(0x34 == canardDSDLGetU8(buf, 31, 29, 8)); - REQUIRE(0x56 == canardDSDLGetU8(buf, 31, 37, 8)); - - REQUIRE(!canardDSDLGetBit(buf, 31, 45)); - REQUIRE(canardDSDLGetBit(buf, 31, 46)); - REQUIRE(canardDSDLGetBit(buf, 31, 47)); - - REQUIRE(0x12 == canardDSDLGetU8(buf, 31, 48, 8)); - REQUIRE(0x34 == canardDSDLGetU8(buf, 31, 56, 8)); - REQUIRE(0x56 == canardDSDLGetU8(buf, 31, 64, 8)); - - REQUIRE(canardDSDLGetBit(buf, 31, 72)); - REQUIRE(!canardDSDLGetBit(buf, 31, 73)); - REQUIRE(!canardDSDLGetBit(buf, 31, 74)); - REQUIRE(canardDSDLGetBit(buf, 31, 75)); - REQUIRE(canardDSDLGetBit(buf, 31, 76)); - - REQUIRE(-2 == canardDSDLGetI8(buf, 31, 77, 8)); - REQUIRE(0b11101100101 == canardDSDLGetU16(buf, 31, 85, 11)); - REQUIRE(0b110 == canardDSDLGetU8(buf, 31, 96, 3)); - - REQUIRE(Approx(1.0) == canardDSDLGetF64(buf, 31, 99)); - REQUIRE(Approx(1.0F) == canardDSDLGetF32(buf, 31, 163)); - REQUIRE(std::isinf(canardDSDLGetF16(buf, 31, 195))); - REQUIRE(0.0F > canardDSDLGetF16(buf, 31, 195)); - - REQUIRE(0xDEAD == canardDSDLGetU16(buf, 31, 211, 16)); - REQUIRE(0xBEEF == canardDSDLGetU16(buf, 31, 227, 16)); - REQUIRE(0 == canardDSDLGetU8(buf, 31, 243, 5)); -} - -TEST_CASE("canardDSDLDeserialize_heartbeat") -{ - const std::vector Reference({239, 190, 173, 222, 3, 2, 127, 0}); - const std::uint8_t* const buf = Reference.data(); - REQUIRE(2 == canardDSDLGetU8(buf, 7, 40, 8)); // mode - REQUIRE(0xDEADBEEF == canardDSDLGetU32(buf, 7, 0, 32)); // uptime - REQUIRE(0x7F == canardDSDLGetU32(buf, 7, 48, 8)); // vssc - REQUIRE(3 == canardDSDLGetU8(buf, 7, 32, 8)); // health -} diff --git a/tests/test_private_cavl.cpp b/tests/test_private_cavl.cpp new file mode 100644 index 00000000..fb3446af --- /dev/null +++ b/tests/test_private_cavl.cpp @@ -0,0 +1,1372 @@ +// This software is distributed under the terms of the MIT License. +// Copyright (c) 2016-2020 UAVCAN Development Team. +// These tests have been adapted from the Cavl test suite that you can find at https://github.com/pavel-kirienko/cavl + +#include +#include "catch.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +/// These aliases are introduced to keep things nicely aligned in test cases. +constexpr auto Zz = nullptr; +constexpr auto Zzzzz = nullptr; +constexpr auto Zzzzzz = nullptr; + +template +struct Node final : Cavl +{ + explicit Node(const T val) : Cavl{Cavl{}}, value(val) {} + Node(const Cavl& cv, const T val) : Cavl{cv}, value(val) {} + Node() : Cavl{Cavl{}} {} + + T value{}; + + auto checkLinkageUpLeftRightBF(const Cavl* const check_up, + const Cavl* const check_le, + const Cavl* const check_ri, + const std::int8_t check_bf) const -> bool + { + return (up == check_up) && // + (lr[0] == check_le) && (lr[1] == check_ri) && // + (bf == check_bf) && // + ((check_up == nullptr) || (check_up->lr[0] == this) || (check_up->lr[1] == this)) && // + ((check_le == nullptr) || (check_le->up == this)) && // + ((check_ri == nullptr) || (check_ri->up == this)); + } + + auto min() -> Node* { return reinterpret_cast(cavlFindExtremum(this, false)); } + auto max() -> Node* { return reinterpret_cast(cavlFindExtremum(this, true)); } + + auto operator=(const Cavl& cv) -> Node& + { + static_cast(*this) = cv; + return *this; + } +}; + +/// Wrapper over cavlSearch() that supports closures. +template +auto search(Node** const root, const Predicate& predicate, const Factory& factory) -> Node* +{ + struct Refs + { + Predicate predicate; + Factory factory; + + static auto callPredicate(void* const user_reference, const Cavl* const node) -> std::int8_t + { + const auto ret = static_cast(user_reference)->predicate(reinterpret_cast&>(*node)); + if (ret > 0) + { + return 1; + } + if (ret < 0) + { + return -1; + } + return 0; + } + + static auto callFactory(void* const user_reference) -> Cavl* + { + return static_cast(user_reference)->factory(); + } + } refs{predicate, factory}; + Cavl* const out = cavlSearch(reinterpret_cast(root), &refs, &Refs::callPredicate, &Refs::callFactory); + return reinterpret_cast*>(out); +} +template +auto search(Node** const root, const Predicate& predicate) -> Node* +{ + return search(root, predicate, []() { return nullptr; }); +} + +/// Wrapper over cavlRemove(). +template +void remove(Node** const root, const Node* const n) +{ + cavlRemove(reinterpret_cast(root), n); +} + +template +auto getHeight(const Node* const n) -> std::uint8_t // NOLINT recursion +{ + return (n != nullptr) ? std::uint8_t(1U + std::max(getHeight(reinterpret_cast*>(n->lr[0])), + getHeight(reinterpret_cast*>(n->lr[1])))) + : 0; +} + +template +void print(const Node* const nd, const std::uint8_t depth = 0, const char marker = 'T') // NOLINT recursion +{ + REQUIRE(10 > getHeight(nd)); // Fail early for malformed cyclic trees, do not overwhelm stdout. + if (nd != nullptr) + { + print(reinterpret_cast*>(nd->lr[0]), static_cast(depth + 1U), 'L'); + for (std::uint16_t i = 1U; i < depth; i++) + { + std::cout << " "; + } + if (marker == 'L') + { + std::cout << " ............."; + } + else if (marker == 'R') + { + std::cout << " `````````````"; + } + else + { + (void) 0; + } + std::cout << marker << "=" << static_cast(nd->value) // + << " [" << static_cast(nd->bf) << "]" << std::endl; + print(reinterpret_cast*>(nd->lr[1]), static_cast(depth + 1U), 'R'); + } +} + +template +void traverse(Node* const root, const Visitor& visitor) // NOLINT recursion needed for testing +{ + if (root != nullptr) + { + traverse(reinterpret_cast(root->lr[!Ascending]), visitor); + visitor(root); + traverse(reinterpret_cast(root->lr[Ascending]), visitor); + } +} + +template +auto checkAscension(const Node* const root) -> std::optional +{ + const Node* prev = nullptr; + bool valid = true; + std::size_t size = 0; + traverse>(root, [&](const Node* const nd) { + if (prev != nullptr) + { + valid = valid && (prev->value < nd->value); + } + prev = nd; + size++; + }); + return valid ? std::optional(size) : std::optional{}; +} + +template +auto findBrokenAncestry(const Node* const n, const Cavl* const parent = nullptr) // NOLINT recursion + -> const Node* +{ + if ((n != nullptr) && (n->up == parent)) + { + for (auto* ch : n->lr) // NOLINT array decay due to C API + { + if (const Node* p = findBrokenAncestry(reinterpret_cast*>(ch), n)) + { + return p; + } + } + return nullptr; + } + return n; +} + +template +auto findBrokenBalanceFactor(const Node* const n) -> const Cavl* // NOLINT recursion +{ + if (n != nullptr) + { + if (std::abs(n->bf) > 1) + { + return n; + } + const std::int16_t hl = getHeight(reinterpret_cast*>(n->lr[0])); + const std::int16_t hr = getHeight(reinterpret_cast*>(n->lr[1])); + if (n->bf != (hr - hl)) + { + return n; + } + for (auto* ch : n->lr) // NOLINT array decay due to C API + { + if (const Cavl* p = findBrokenBalanceFactor(reinterpret_cast*>(ch))) + { + return p; + } + } + } + return nullptr; +} +} // namespace + +TEST_CASE("CheckAscension") +{ + using N = Node; + N t{2}; + N l{1}; + N r{3}; + N rr{4}; + // Correctly arranged tree -- smaller items on the left. + t.lr[0] = &l; + t.lr[1] = &r; + r.lr[1] = &rr; + REQUIRE(4 == checkAscension(&t)); + REQUIRE(3 == getHeight(&t)); + // Break the arrangement and make sure the breakage is detected. + t.lr[1] = &l; + t.lr[0] = &r; + REQUIRE(4 != checkAscension(&t)); + REQUIRE(3 == getHeight(&t)); + REQUIRE(&t == findBrokenBalanceFactor(&t)); // All zeros, incorrect. + r.lr[1] = nullptr; + REQUIRE(2 == getHeight(&t)); + REQUIRE(nullptr == findBrokenBalanceFactor(&t)); // Balanced now as we removed one node. +} + +TEST_CASE("Rotation") +{ + using N = Node; + // Original state: + // x.left = a + // x.right = z + // z.left = b + // z.right = c + // After left rotation of X: + // x.left = a + // x.right = b + // z.left = x + // z.right = c + N c{{Zz, {Zz, Zz}, 0}, 3}; + N b{{Zz, {Zz, Zz}, 0}, 2}; + N a{{Zz, {Zz, Zz}, 0}, 1}; + N z{{Zz, {&b, &c}, 0}, 8}; + N x{{Zz, {&a, &z}, 1}, 9}; + z.up = &x; + c.up = &z; + b.up = &z; + a.up = &x; + + std::cout << "Before rotation:\n"; + REQUIRE(nullptr == findBrokenAncestry(&x)); + print(&x); + + std::cout << "After left rotation:\n"; + cavlPrivateRotate(&x, false); // z is now the root + REQUIRE(nullptr == findBrokenAncestry(&z)); + print(&z); + REQUIRE(&a == x.lr[0]); + REQUIRE(&b == x.lr[1]); + REQUIRE(&x == z.lr[0]); + REQUIRE(&c == z.lr[1]); + + std::cout << "After right rotation, back into the original configuration:\n"; + cavlPrivateRotate(&z, true); // x is now the root + REQUIRE(nullptr == findBrokenAncestry(&x)); + print(&x); + REQUIRE(&a == x.lr[0]); + REQUIRE(&z == x.lr[1]); + REQUIRE(&b == z.lr[0]); + REQUIRE(&c == z.lr[1]); +} + +TEST_CASE("BalancingA") +{ + using N = Node; + // Double left-right rotation. + // X X Y + // / ` / ` / ` + // Z C => Y C => Z X + // / ` / ` / ` / ` + // D Y Z G D F G C + // / ` / ` + // F G D F + N x{{Zz, {Zz, Zz}, 0}, 1}; // bf = -2 + N z{{&x, {Zz, Zz}, 0}, 2}; // bf = +1 + N c{{&x, {Zz, Zz}, 0}, 3}; + N d{{&z, {Zz, Zz}, 0}, 4}; + N y{{&z, {Zz, Zz}, 0}, 5}; + N f{{&y, {Zz, Zz}, 0}, 6}; + N g{{&y, {Zz, Zz}, 0}, 7}; + x.lr[0] = &z; + x.lr[1] = &c; + z.lr[0] = &d; + z.lr[1] = &y; + y.lr[0] = &f; + y.lr[1] = &g; + print(&x); + REQUIRE(nullptr == findBrokenAncestry(&x)); + REQUIRE(&x == cavlPrivateAdjustBalance(&x, false)); // bf = -1, same topology + REQUIRE(-1 == x.bf); + REQUIRE(&z == cavlPrivateAdjustBalance(&z, true)); // bf = +1, same topology + REQUIRE(+1 == z.bf); + REQUIRE(&y == cavlPrivateAdjustBalance(&x, false)); // bf = -2, rotation needed + print(&y); + REQUIRE(nullptr == findBrokenBalanceFactor(&y)); // Should be balanced now. + REQUIRE(nullptr == findBrokenAncestry(&y)); + REQUIRE(&z == y.lr[0]); + REQUIRE(&x == y.lr[1]); + REQUIRE(&d == z.lr[0]); + REQUIRE(&f == z.lr[1]); + REQUIRE(&g == x.lr[0]); + REQUIRE(&c == x.lr[1]); + REQUIRE(Zz == d.lr[0]); + REQUIRE(Zz == d.lr[1]); + REQUIRE(Zz == f.lr[0]); + REQUIRE(Zz == f.lr[1]); + REQUIRE(Zz == g.lr[0]); + REQUIRE(Zz == g.lr[1]); + REQUIRE(Zz == c.lr[0]); + REQUIRE(Zz == c.lr[1]); +} + +TEST_CASE("BalancingB") +{ + using N = Node; + // Without F the handling of Z and Y is more complex; Z flips the sign of its balance factor: + // X X Y + // / ` / ` / ` + // Z C => Y C => Z X + // / ` / ` / / ` + // D Y Z G D G C + // ` / + // G D + N x{}; + N z{}; + N c{}; + N d{}; + N y{}; + N g{}; + x = {{Zz, {&z, &c}, 0}, 1}; // bf = -2 + z = {{&x, {&d, &y}, 0}, 2}; // bf = +1 + c = {{&x, {Zz, Zz}, 0}, 3}; + d = {{&z, {Zz, Zz}, 0}, 4}; + y = {{&z, {Zz, &g}, 0}, 5}; // bf = +1 + g = {{&y, {Zz, Zz}, 0}, 7}; + print(&x); + REQUIRE(nullptr == findBrokenAncestry(&x)); + REQUIRE(&x == cavlPrivateAdjustBalance(&x, false)); // bf = -1, same topology + REQUIRE(-1 == x.bf); + REQUIRE(&z == cavlPrivateAdjustBalance(&z, true)); // bf = +1, same topology + REQUIRE(+1 == z.bf); + REQUIRE(&y == cavlPrivateAdjustBalance(&y, true)); // bf = +1, same topology + REQUIRE(+1 == y.bf); + REQUIRE(&y == cavlPrivateAdjustBalance(&x, false)); // bf = -2, rotation needed + print(&y); + REQUIRE(nullptr == findBrokenBalanceFactor(&y)); // Should be balanced now. + REQUIRE(nullptr == findBrokenAncestry(&y)); + REQUIRE(&z == y.lr[0]); + REQUIRE(&x == y.lr[1]); + REQUIRE(&d == z.lr[0]); + REQUIRE(Zz == z.lr[1]); + REQUIRE(&g == x.lr[0]); + REQUIRE(&c == x.lr[1]); + REQUIRE(Zz == d.lr[0]); + REQUIRE(Zz == d.lr[1]); + REQUIRE(Zz == g.lr[0]); + REQUIRE(Zz == g.lr[1]); + REQUIRE(Zz == c.lr[0]); + REQUIRE(Zz == c.lr[1]); +} + +TEST_CASE("BalancingC") +{ + using N = Node; + // Both X and Z are heavy on the same side. + // X Z + // / ` / ` + // Z C => D X + // / ` / ` / ` + // D Y F G Y C + // / ` + // F G + N x{}; + N z{}; + N c{}; + N d{}; + N y{}; + N f{}; + N g{}; + x = {{Zz, {&z, &c}, 0}, 1}; // bf = -2 + z = {{&x, {&d, &y}, 0}, 2}; // bf = -1 + c = {{&x, {Zz, Zz}, 0}, 3}; + d = {{&z, {&f, &g}, 0}, 4}; + y = {{&z, {Zz, Zz}, 0}, 5}; + f = {{&d, {Zz, Zz}, 0}, 6}; + g = {{&d, {Zz, Zz}, 0}, 7}; + print(&x); + REQUIRE(nullptr == findBrokenAncestry(&x)); + REQUIRE(&x == cavlPrivateAdjustBalance(&x, false)); // bf = -1, same topology + REQUIRE(-1 == x.bf); + REQUIRE(&z == cavlPrivateAdjustBalance(&z, false)); // bf = -1, same topology + REQUIRE(-1 == z.bf); + REQUIRE(&z == cavlPrivateAdjustBalance(&x, false)); + print(&z); + REQUIRE(nullptr == findBrokenBalanceFactor(&z)); + REQUIRE(nullptr == findBrokenAncestry(&z)); + REQUIRE(&d == z.lr[0]); + REQUIRE(&x == z.lr[1]); + REQUIRE(&f == d.lr[0]); + REQUIRE(&g == d.lr[1]); + REQUIRE(&y == x.lr[0]); + REQUIRE(&c == x.lr[1]); + REQUIRE(Zz == f.lr[0]); + REQUIRE(Zz == f.lr[1]); + REQUIRE(Zz == g.lr[0]); + REQUIRE(Zz == g.lr[1]); + REQUIRE(Zz == y.lr[0]); + REQUIRE(Zz == y.lr[1]); + REQUIRE(Zz == c.lr[0]); + REQUIRE(Zz == c.lr[1]); +} + +TEST_CASE("RetracingOnGrowth") +{ + using N = Node; + std::array t{}; + for (std::uint8_t i = 0; i < 100; i++) + { + t[i].value = i; + } + // 50 30 + // / ` / ` + // 30 60? => 20 50 + // / ` / / ` + // 20 40? 10 40? 60? + // / + // 10 + t[50] = {Zzzzzz, {&t[30], &t[60]}, -1}; + t[30] = {&t[50], {&t[20], &t[40]}, 00}; + t[60] = {&t[50], {Zzzzzz, Zzzzzz}, 00}; + t[20] = {&t[30], {&t[10], Zzzzzz}, 00}; + t[40] = {&t[30], {Zzzzzz, Zzzzzz}, 00}; + t[10] = {&t[20], {Zzzzzz, Zzzzzz}, 00}; + print(&t[50]); // The tree is imbalanced because we just added 1 and are about to retrace it. + REQUIRE(nullptr == findBrokenAncestry(&t[50])); + REQUIRE(6 == checkAscension(&t[50])); + REQUIRE(&t[30] == cavlPrivateRetraceOnGrowth(&t[10])); + std::puts("ADD 10:"); + print(&t[30]); // This is the new root. + REQUIRE(&t[20] == t[30].lr[0]); + REQUIRE(&t[50] == t[30].lr[1]); + REQUIRE(&t[10] == t[20].lr[0]); + REQUIRE(Zzzzzz == t[20].lr[1]); + REQUIRE(&t[40] == t[50].lr[0]); + REQUIRE(&t[60] == t[50].lr[1]); + REQUIRE(Zzzzzz == t[10].lr[0]); + REQUIRE(Zzzzzz == t[10].lr[1]); + REQUIRE(Zzzzzz == t[40].lr[0]); + REQUIRE(Zzzzzz == t[40].lr[1]); + REQUIRE(Zzzzzz == t[60].lr[0]); + REQUIRE(Zzzzzz == t[60].lr[1]); + REQUIRE(-1 == t[20].bf); + REQUIRE(+0 == t[30].bf); + REQUIRE(nullptr == findBrokenAncestry(&t[30])); + REQUIRE(nullptr == findBrokenBalanceFactor(&t[30])); + REQUIRE(6 == checkAscension(&t[30])); + // Add a new child under 20 and ensure that retracing stops at 20 because it becomes perfectly balanced: + // 30 + // / ` + // 20 50 + // / ` / ` + // 10 21 40 60 + REQUIRE(nullptr == findBrokenAncestry(&t[30])); + REQUIRE(nullptr == findBrokenBalanceFactor(&t[30])); + t[21] = {&t[20], {Zzzzzz, Zzzzzz}, 0}; + t[20].lr[1] = &t[21]; + REQUIRE(nullptr == cavlPrivateRetraceOnGrowth(&t[21])); // Root not reached, NULL returned. + std::puts("ADD 21:"); + print(&t[30]); + REQUIRE(0 == t[20].bf); + REQUIRE(0 == t[30].bf); + REQUIRE(nullptr == findBrokenAncestry(&t[30])); + REQUIRE(nullptr == findBrokenBalanceFactor(&t[30])); + REQUIRE(7 == checkAscension(&t[30])); + // 30 + // / ` + // 20 50 + // / ` / ` + // 10 21 40 60 + // ` + // 15 <== first we add this, no balancing needed + // ` + // 17 <== then we add this, forcing left rotation at 10 + // + // After the left rotation of 10, we get: + // + // 30 + // / ` + // 20 50 + // / ` / ` + // 15 21 40 60 + // / ` + // 10 17 + // + // When we add one extra item after 17, we force a double rotation (15 left, 20 right). Before the rotation: + // + // 30 + // / ` + // 20 50 + // / ` / ` + // 15 21 40 60 + // / ` + // 10 17 + // ` + // 18 <== new item causes imbalance + // + // After left rotation of 15: + // + // 30 + // / ` + // 20 50 + // / ` / ` + // 17 21 40 60 + // / ` + // 15 18 + // / + // 10 + // + // After right rotation of 20, this is the final state: + // + // 30 + // / ` + // 17 50 + // / ` / ` + // 15 20 40 60 + // / / ` + // 10 18 21 + std::puts("ADD 15:"); + REQUIRE(nullptr == findBrokenAncestry(&t[30])); + REQUIRE(nullptr == findBrokenBalanceFactor(&t[30])); + REQUIRE(7 == checkAscension(&t[30])); + t[15] = {&t[10], {Zzzzzz, Zzzzzz}, 0}; + t[10].lr[1] = &t[15]; + REQUIRE(&t[30] == cavlPrivateRetraceOnGrowth(&t[15])); // Same root, its balance becomes -1. + print(&t[30]); + REQUIRE(+1 == t[10].bf); + REQUIRE(-1 == t[20].bf); + REQUIRE(-1 == t[30].bf); + REQUIRE(nullptr == findBrokenAncestry(&t[30])); + REQUIRE(nullptr == findBrokenBalanceFactor(&t[30])); + REQUIRE(8 == checkAscension(&t[30])); + + std::puts("ADD 17:"); + t[17] = {&t[15], {Zzzzzz, Zzzzzz}, 0}; + t[15].lr[1] = &t[17]; + REQUIRE(nullptr == cavlPrivateRetraceOnGrowth(&t[17])); // Same root, same balance, 10 rotated left. + print(&t[30]); + // Check 10 + REQUIRE(&t[15] == t[10].up); + REQUIRE(0 == t[10].bf); + REQUIRE(nullptr == t[10].lr[0]); + REQUIRE(nullptr == t[10].lr[1]); + // Check 17 + REQUIRE(&t[15] == t[17].up); + REQUIRE(0 == t[17].bf); + REQUIRE(nullptr == t[17].lr[0]); + REQUIRE(nullptr == t[17].lr[1]); + // Check 15 + REQUIRE(&t[20] == t[15].up); + REQUIRE(0 == t[15].bf); + REQUIRE(&t[10] == t[15].lr[0]); + REQUIRE(&t[17] == t[15].lr[1]); + // Check 20 -- leaning left + REQUIRE(&t[30] == t[20].up); + REQUIRE(-1 == t[20].bf); + REQUIRE(&t[15] == t[20].lr[0]); + REQUIRE(&t[21] == t[20].lr[1]); + // Check the root -- still leaning left by one. + REQUIRE(nullptr == t[30].up); + REQUIRE(-1 == t[30].bf); + REQUIRE(&t[20] == t[30].lr[0]); + REQUIRE(&t[50] == t[30].lr[1]); + // Check hard invariants. + REQUIRE(nullptr == findBrokenAncestry(&t[30])); + REQUIRE(nullptr == findBrokenBalanceFactor(&t[30])); + REQUIRE(9 == checkAscension(&t[30])); + + std::puts("ADD 18:"); + t[18] = {&t[17], {Zzzzzz, Zzzzzz}, 0}; + t[17].lr[1] = &t[18]; + REQUIRE(nullptr == cavlPrivateRetraceOnGrowth(&t[18])); // Same root, 15 went left, 20 went right. + print(&t[30]); + // Check 17 + REQUIRE(&t[30] == t[17].up); + REQUIRE(0 == t[17].bf); + REQUIRE(&t[15] == t[17].lr[0]); + REQUIRE(&t[20] == t[17].lr[1]); + // Check 15 + REQUIRE(&t[17] == t[15].up); + REQUIRE(-1 == t[15].bf); + REQUIRE(&t[10] == t[15].lr[0]); + REQUIRE(nullptr == t[15].lr[1]); + // Check 20 + REQUIRE(&t[17] == t[20].up); + REQUIRE(0 == t[20].bf); + REQUIRE(&t[18] == t[20].lr[0]); + REQUIRE(&t[21] == t[20].lr[1]); + // Check 10 + REQUIRE(&t[15] == t[10].up); + REQUIRE(0 == t[10].bf); + REQUIRE(nullptr == t[10].lr[0]); + REQUIRE(nullptr == t[10].lr[1]); + // Check 18 + REQUIRE(&t[20] == t[18].up); + REQUIRE(0 == t[18].bf); + REQUIRE(nullptr == t[18].lr[0]); + REQUIRE(nullptr == t[18].lr[1]); + // Check 21 + REQUIRE(&t[20] == t[21].up); + REQUIRE(0 == t[21].bf); + REQUIRE(nullptr == t[21].lr[0]); + REQUIRE(nullptr == t[21].lr[1]); + // Check hard invariants. + REQUIRE(nullptr == findBrokenAncestry(&t[30])); + REQUIRE(nullptr == findBrokenBalanceFactor(&t[30])); + REQUIRE(10 == checkAscension(&t[30])); +} + +TEST_CASE("SearchTrivial") +{ + using N = Node; + // A + // B C + // D E F G + N a{4}; + N b{2}; + N c{6}; + N d{1}; + N e{3}; + N f{5}; + N g{7}; + N q{9}; + a = {Zz, {&b, &c}, 0}; + b = {&a, {&d, &e}, 0}; + c = {&a, {&f, &g}, 0}; + d = {&b, {Zz, Zz}, 0}; + e = {&b, {Zz, Zz}, 0}; + f = {&c, {Zz, Zz}, 0}; + g = {&c, {Zz, Zz}, 0}; + q = {Zz, {Zz, Zz}, 0}; + REQUIRE(nullptr == findBrokenBalanceFactor(&a)); + REQUIRE(nullptr == findBrokenAncestry(&a)); + REQUIRE(7 == checkAscension(&a)); + N* root = &a; + REQUIRE(nullptr == cavlSearch(reinterpret_cast(&root), nullptr, nullptr, nullptr)); // Bad arguments. + REQUIRE(&a == root); + REQUIRE(nullptr == search(&root, [&](const N& v) { return q.value - v.value; })); + REQUIRE(&a == root); + REQUIRE(&e == search(&root, [&](const N& v) { return e.value - v.value; })); + REQUIRE(&b == search(&root, [&](const N& v) { return b.value - v.value; })); + REQUIRE(&a == root); + print(&a); + REQUIRE(nullptr == cavlFindExtremum(nullptr, true)); + REQUIRE(nullptr == cavlFindExtremum(nullptr, false)); + REQUIRE(&g == a.max()); + REQUIRE(&d == a.min()); + REQUIRE(&g == g.max()); + REQUIRE(&g == g.min()); + REQUIRE(&d == d.max()); + REQUIRE(&d == d.min()); +} + +TEST_CASE("RemovalA") +{ + using N = Node; + // 4 + // / ` + // 2 6 + // / ` / ` + // 1 3 5 8 + // / ` + // 7 9 + std::array t{}; + for (std::uint8_t i = 0; i < 10; i++) + { + t[i].value = i; + } + t[1] = {&t[2], {Zzzzz, Zzzzz}, 00}; + t[2] = {&t[4], {&t[1], &t[3]}, 00}; + t[3] = {&t[2], {Zzzzz, Zzzzz}, 00}; + t[4] = {Zzzzz, {&t[2], &t[6]}, +1}; + t[5] = {&t[6], {Zzzzz, Zzzzz}, 00}; + t[6] = {&t[4], {&t[5], &t[8]}, +1}; + t[7] = {&t[8], {Zzzzz, Zzzzz}, 00}; + t[8] = {&t[6], {&t[7], &t[9]}, 00}; + t[9] = {&t[8], {Zzzzz, Zzzzz}, 00}; + N* root = &t[4]; + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(9 == checkAscension(root)); + + // Remove 9, the easiest case. The rest of the tree remains unchanged. + // 4 + // / ` + // 2 6 + // / ` / ` + // 1 3 5 8 + // / + // 7 + std::puts("REMOVE 9:"); + remove(&root, &t[9]); + REQUIRE(&t[4] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(8 == checkAscension(root)); + // 1 + REQUIRE(&t[2] == t[1].up); + REQUIRE(Zzzzz == t[1].lr[0]); + REQUIRE(Zzzzz == t[1].lr[1]); + REQUIRE(00 == t[1].bf); + // 2 + REQUIRE(&t[4] == t[2].up); + REQUIRE(&t[1] == t[2].lr[0]); + REQUIRE(&t[3] == t[2].lr[1]); + REQUIRE(00 == t[2].bf); + // 3 + REQUIRE(&t[2] == t[3].up); + REQUIRE(Zzzzz == t[3].lr[0]); + REQUIRE(Zzzzz == t[3].lr[1]); + REQUIRE(00 == t[3].bf); + // 4 + REQUIRE(Zzzzz == t[4].up); // Nihil Supernum + REQUIRE(&t[2] == t[4].lr[0]); + REQUIRE(&t[6] == t[4].lr[1]); + REQUIRE(+1 == t[4].bf); + // 5 + REQUIRE(&t[6] == t[5].up); + REQUIRE(Zzzzz == t[5].lr[0]); + REQUIRE(Zzzzz == t[5].lr[1]); + REQUIRE(00 == t[5].bf); + // 6 + REQUIRE(&t[4] == t[6].up); + REQUIRE(&t[5] == t[6].lr[0]); + REQUIRE(&t[8] == t[6].lr[1]); + REQUIRE(+1 == t[6].bf); + // 7 + REQUIRE(&t[8] == t[7].up); + REQUIRE(Zzzzz == t[7].lr[0]); + REQUIRE(Zzzzz == t[7].lr[1]); + REQUIRE(00 == t[7].bf); + // 8 + REQUIRE(&t[6] == t[8].up); + REQUIRE(&t[7] == t[8].lr[0]); + REQUIRE(Zzzzz == t[8].lr[1]); + REQUIRE(-1 == t[8].bf); + + // Remove 8, 7 takes its place (the one-child case). The rest of the tree remains unchanged. + // 4 + // / ` + // 2 6 + // / ` / ` + // 1 3 5 7 + std::puts("REMOVE 8:"); + remove(&root, &t[8]); + REQUIRE(&t[4] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(7 == checkAscension(root)); + // 1 + REQUIRE(&t[2] == t[1].up); + REQUIRE(Zzzzz == t[1].lr[0]); + REQUIRE(Zzzzz == t[1].lr[1]); + REQUIRE(00 == t[1].bf); + // 2 + REQUIRE(&t[4] == t[2].up); + REQUIRE(&t[1] == t[2].lr[0]); + REQUIRE(&t[3] == t[2].lr[1]); + REQUIRE(00 == t[2].bf); + // 3 + REQUIRE(&t[2] == t[3].up); + REQUIRE(Zzzzz == t[3].lr[0]); + REQUIRE(Zzzzz == t[3].lr[1]); + REQUIRE(00 == t[3].bf); + // 4 + REQUIRE(Zzzzz == t[4].up); // Nihil Supernum + REQUIRE(&t[2] == t[4].lr[0]); + REQUIRE(&t[6] == t[4].lr[1]); + REQUIRE(00 == t[4].bf); + // 5 + REQUIRE(&t[6] == t[5].up); + REQUIRE(Zzzzz == t[5].lr[0]); + REQUIRE(Zzzzz == t[5].lr[1]); + REQUIRE(00 == t[5].bf); + // 6 + REQUIRE(&t[4] == t[6].up); + REQUIRE(&t[5] == t[6].lr[0]); + REQUIRE(&t[7] == t[6].lr[1]); + REQUIRE(00 == t[6].bf); + // 7 + REQUIRE(&t[6] == t[7].up); + REQUIRE(Zzzzz == t[7].lr[0]); + REQUIRE(Zzzzz == t[7].lr[1]); + REQUIRE(00 == t[7].bf); + + // Remove the root node 4, 5 takes its place. The overall structure remains unchanged except that 5 is now the root. + // 5 + // / ` + // 2 6 + // / ` ` + // 1 3 7 + std::puts("REMOVE 4:"); + remove(&root, &t[4]); + REQUIRE(&t[5] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(6 == checkAscension(root)); + // 1 + REQUIRE(&t[2] == t[1].up); + REQUIRE(Zzzzz == t[1].lr[0]); + REQUIRE(Zzzzz == t[1].lr[1]); + REQUIRE(00 == t[1].bf); + // 2 + REQUIRE(&t[5] == t[2].up); + REQUIRE(&t[1] == t[2].lr[0]); + REQUIRE(&t[3] == t[2].lr[1]); + REQUIRE(00 == t[2].bf); + // 3 + REQUIRE(&t[2] == t[3].up); + REQUIRE(Zzzzz == t[3].lr[0]); + REQUIRE(Zzzzz == t[3].lr[1]); + REQUIRE(00 == t[3].bf); + // 5 + REQUIRE(Zzzzz == t[5].up); // Nihil Supernum + REQUIRE(&t[2] == t[5].lr[0]); + REQUIRE(&t[6] == t[5].lr[1]); + REQUIRE(00 == t[5].bf); + // 6 + REQUIRE(&t[5] == t[6].up); + REQUIRE(Zzzzz == t[6].lr[0]); + REQUIRE(&t[7] == t[6].lr[1]); + REQUIRE(+1 == t[6].bf); + // 7 + REQUIRE(&t[6] == t[7].up); + REQUIRE(Zzzzz == t[7].lr[0]); + REQUIRE(Zzzzz == t[7].lr[1]); + REQUIRE(00 == t[7].bf); + + // Remove the root node 5, 6 takes its place. + // 6 + // / ` + // 2 7 + // / ` + // 1 3 + std::puts("REMOVE 5:"); + remove(&root, &t[5]); + REQUIRE(&t[6] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(5 == checkAscension(root)); + // 1 + REQUIRE(&t[2] == t[1].up); + REQUIRE(Zzzzz == t[1].lr[0]); + REQUIRE(Zzzzz == t[1].lr[1]); + REQUIRE(00 == t[1].bf); + // 2 + REQUIRE(&t[6] == t[2].up); + REQUIRE(&t[1] == t[2].lr[0]); + REQUIRE(&t[3] == t[2].lr[1]); + REQUIRE(00 == t[2].bf); + // 3 + REQUIRE(&t[2] == t[3].up); + REQUIRE(Zzzzz == t[3].lr[0]); + REQUIRE(Zzzzz == t[3].lr[1]); + REQUIRE(00 == t[3].bf); + // 6 + REQUIRE(Zzzzz == t[6].up); // Nihil Supernum + REQUIRE(&t[2] == t[6].lr[0]); + REQUIRE(&t[7] == t[6].lr[1]); + REQUIRE(-1 == t[6].bf); + // 7 + REQUIRE(&t[6] == t[7].up); + REQUIRE(Zzzzz == t[7].lr[0]); + REQUIRE(Zzzzz == t[7].lr[1]); + REQUIRE(00 == t[7].bf); + + // Remove the root node 6, 7 takes its place, then right rotation is done to restore balance, 2 is the new root. + // 2 + // / ` + // 1 7 + // / + // 3 + std::puts("REMOVE 6:"); + remove(&root, &t[6]); + REQUIRE(&t[2] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(4 == checkAscension(root)); + // 1 + REQUIRE(&t[2] == t[1].up); + REQUIRE(Zzzzz == t[1].lr[0]); + REQUIRE(Zzzzz == t[1].lr[1]); + REQUIRE(00 == t[1].bf); + // 2 + REQUIRE(Zzzzz == t[2].up); // Nihil Supernum + REQUIRE(&t[1] == t[2].lr[0]); + REQUIRE(&t[7] == t[2].lr[1]); + REQUIRE(+1 == t[2].bf); + // 3 + REQUIRE(&t[7] == t[3].up); + REQUIRE(Zzzzz == t[3].lr[0]); + REQUIRE(Zzzzz == t[3].lr[1]); + REQUIRE(00 == t[3].bf); + // 7 + REQUIRE(&t[2] == t[7].up); + REQUIRE(&t[3] == t[7].lr[0]); + REQUIRE(Zzzzz == t[7].lr[1]); + REQUIRE(-1 == t[7].bf); + + // Remove 1, then balancing makes 3 the new root node. + // 3 + // / ` + // 2 7 + std::puts("REMOVE 1:"); + remove(&root, &t[1]); + REQUIRE(&t[3] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(3 == checkAscension(root)); + // 2 + REQUIRE(&t[3] == t[2].up); + REQUIRE(Zzzzz == t[2].lr[0]); + REQUIRE(Zzzzz == t[2].lr[1]); + REQUIRE(0 == t[2].bf); + // 3 + REQUIRE(Zzzzz == t[3].up); // Nihil Supernum + REQUIRE(&t[2] == t[3].lr[0]); + REQUIRE(&t[7] == t[3].lr[1]); + REQUIRE(00 == t[3].bf); + // 7 + REQUIRE(&t[3] == t[7].up); + REQUIRE(Zzzzz == t[7].lr[0]); + REQUIRE(Zzzzz == t[7].lr[1]); + REQUIRE(00 == t[7].bf); + + // Remove 7. + // 3 + // / + // 2 + std::puts("REMOVE 7:"); + remove(&root, &t[7]); + REQUIRE(&t[3] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(2 == checkAscension(root)); + // 2 + REQUIRE(&t[3] == t[2].up); + REQUIRE(Zzzzz == t[2].lr[0]); + REQUIRE(Zzzzz == t[2].lr[1]); + REQUIRE(0 == t[2].bf); + // 3 + REQUIRE(Zzzzz == t[3].up); // Nihil Supernum + REQUIRE(&t[2] == t[3].lr[0]); + REQUIRE(Zzzzz == t[3].lr[1]); + REQUIRE(-1 == t[3].bf); + + // Remove 3. Only 2 is left, which is now obviously the root. + std::puts("REMOVE 3:"); + remove(&root, &t[3]); + REQUIRE(&t[2] == root); + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(1 == checkAscension(root)); + // 2 + REQUIRE(Zzzzz == t[2].up); + REQUIRE(Zzzzz == t[2].lr[0]); + REQUIRE(Zzzzz == t[2].lr[1]); + REQUIRE(0 == t[2].bf); + + // Remove 2. The tree is now empty, make sure the root pointer is updated accordingly. + std::puts("REMOVE 2:"); + remove(&root, &t[2]); + REQUIRE(nullptr == root); +} + +TEST_CASE("MutationManual") +{ + using N = Node; + // Build a tree with 31 elements from 1 to 31 inclusive by adding new elements successively: + // 16 + // / ` + // 8 24 + // / ` / ` + // 4 12 20 28 + // / ` / ` / ` / ` + // 2 6 10 14 18 22 26 30 + // / ` / ` / ` / ` / ` / ` / ` / ` + // 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 + std::array t{}; + for (std::uint8_t i = 0; i < 32; i++) + { + t[i].value = i; + } + // Build the actual tree. + N* root = nullptr; + for (std::uint8_t i = 1; i < 32; i++) + { + const auto pred = [&](const N& v) { return t.at(i).value - v.value; }; + REQUIRE(nullptr == search(&root, pred)); + REQUIRE(&t[i] == search(&root, pred, [&]() { return &t.at(i); })); + REQUIRE(&t[i] == search(&root, pred)); + // Validate the tree after every mutation. + REQUIRE(nullptr != root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(i == checkAscension(root)); + } + print(root); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(31 == checkAscension(root)); + // Check composition -- ensure that every element is in the tree and it is there exactly once. + { + std::array seen{}; + traverse(root, [&](const N* const n) { + REQUIRE(!seen.at(n->value)); + seen[n->value] = true; + }); + REQUIRE(std::all_of(&seen[1], &seen[31], [](bool x) { return x; })); + } + + // REMOVE 24 + // 16 + // / ` + // 8 25 + // / ` / ` + // 4 12 20 28 + // / ` / ` / ` / ` + // 2 6 10 14 18 22 26 30 + // / ` / ` / ` / ` / ` / ` ` / ` + // 1 3 5 7 9 11 13 15 17 19 21 23 27 29 31 + std::puts("REMOVE 24:"); + REQUIRE(t[24].checkLinkageUpLeftRightBF(&t[16], &t[20], &t[28], 00)); + remove(&root, &t[24]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[25].checkLinkageUpLeftRightBF(&t[16], &t[20], &t[28], 00)); + REQUIRE(t[26].checkLinkageUpLeftRightBF(&t[28], Zzzzzz, &t[27], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(30 == checkAscension(root)); + + // REMOVE 25 + // 16 + // / ` + // 8 26 + // / ` / ` + // 4 12 20 28 + // / ` / ` / ` / ` + // 2 6 10 14 18 22 27 30 + // / ` / ` / ` / ` / ` / ` / ` + // 1 3 5 7 9 11 13 15 17 19 21 23 29 31 + std::puts("REMOVE 25:"); + REQUIRE(t[25].checkLinkageUpLeftRightBF(&t[16], &t[20], &t[28], 00)); + remove(&root, &t[25]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[26].checkLinkageUpLeftRightBF(&t[16], &t[20], &t[28], 00)); + REQUIRE(t[28].checkLinkageUpLeftRightBF(&t[26], &t[27], &t[30], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(29 == checkAscension(root)); + + // REMOVE 26 + // 16 + // / ` + // 8 27 + // / ` / ` + // 4 12 20 30 + // / ` / ` / ` / ` + // 2 6 10 14 18 22 28 31 + // / ` / ` / ` / ` / ` / ` ` + // 1 3 5 7 9 11 13 15 17 19 21 23 29 + std::puts("REMOVE 26:"); + REQUIRE(t[26].checkLinkageUpLeftRightBF(&t[16], &t[20], &t[28], 00)); + remove(&root, &t[26]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[27].checkLinkageUpLeftRightBF(&t[16], &t[20], &t[30], 00)); + REQUIRE(t[30].checkLinkageUpLeftRightBF(&t[27], &t[28], &t[31], -1)); + REQUIRE(t[28].checkLinkageUpLeftRightBF(&t[30], Zzzzzz, &t[29], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(28 == checkAscension(root)); + + // REMOVE 20 + // 16 + // / ` + // 8 27 + // / ` / ` + // 4 12 21 30 + // / ` / ` / ` / ` + // 2 6 10 14 18 22 28 31 + // / ` / ` / ` / ` / ` ` ` + // 1 3 5 7 9 11 13 15 17 19 23 29 + std::puts("REMOVE 20:"); + REQUIRE(t[20].checkLinkageUpLeftRightBF(&t[27], &t[18], &t[22], 00)); + remove(&root, &t[20]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[21].checkLinkageUpLeftRightBF(&t[27], &t[18], &t[22], 00)); + REQUIRE(t[22].checkLinkageUpLeftRightBF(&t[21], Zzzzzz, &t[23], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(27 == checkAscension(root)); + + // REMOVE 27 + // 16 + // / ` + // 8 28 + // / ` / ` + // 4 12 21 30 + // / ` / ` / ` / ` + // 2 6 10 14 18 22 29 31 + // / ` / ` / ` / ` / ` ` + // 1 3 5 7 9 11 13 15 17 19 23 + std::puts("REMOVE 27:"); + REQUIRE(t[27].checkLinkageUpLeftRightBF(&t[16], &t[21], &t[30], 00)); + remove(&root, &t[27]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[28].checkLinkageUpLeftRightBF(&t[16], &t[21], &t[30], -1)); + REQUIRE(t[30].checkLinkageUpLeftRightBF(&t[28], &t[29], &t[31], 00)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(26 == checkAscension(root)); + + // REMOVE 28 + // 16 + // / ` + // 8 29 + // / ` / ` + // 4 12 21 30 + // / ` / ` / ` ` + // 2 6 10 14 18 22 31 + // / ` / ` / ` / ` / ` ` + // 1 3 5 7 9 11 13 15 17 19 23 + std::puts("REMOVE 28:"); + REQUIRE(t[28].checkLinkageUpLeftRightBF(&t[16], &t[21], &t[30], -1)); + remove(&root, &t[28]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[29].checkLinkageUpLeftRightBF(&t[16], &t[21], &t[30], -1)); + REQUIRE(t[30].checkLinkageUpLeftRightBF(&t[29], Zzzzzz, &t[31], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(25 == checkAscension(root)); + + // REMOVE 29; UNBALANCED TREE BEFORE ROTATION: + // 16 + // / ` + // 8 30 + // / ` / ` + // 4 12 21 31 + // / ` / ` / ` + // 2 6 10 14 18 22 + // / ` / ` / ` / ` / ` ` + // 1 3 5 7 9 11 13 15 17 19 23 + // + // FINAL STATE AFTER ROTATION: + // 16 + // / ` + // 8 21 + // / ` / ` + // 4 12 18 30 + // / ` / ` / ` / ` + // 2 6 10 14 17 19 22 31 + // / ` / ` / ` / ` ` + // 1 3 5 7 9 11 13 15 23 + std::puts("REMOVE 29:"); + REQUIRE(t[29].checkLinkageUpLeftRightBF(&t[16], &t[21], &t[30], -1)); + remove(&root, &t[29]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[21].checkLinkageUpLeftRightBF(&t[16], &t[18], &t[30], +1)); + REQUIRE(t[18].checkLinkageUpLeftRightBF(&t[21], &t[17], &t[19], 00)); + REQUIRE(t[30].checkLinkageUpLeftRightBF(&t[21], &t[22], &t[31], -1)); + REQUIRE(t[22].checkLinkageUpLeftRightBF(&t[30], Zzzzzz, &t[23], +1)); + REQUIRE(t[16].checkLinkageUpLeftRightBF(Zzzzzz, &t[8], &t[21], 00)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(24 == checkAscension(root)); + + // REMOVE 8 + // 16 + // / ` + // 9 21 + // / ` / ` + // 4 12 18 30 + // / ` / ` / ` / ` + // 2 6 10 14 17 19 22 31 + // / ` / ` ` / ` ` + // 1 3 5 7 11 13 15 23 + std::puts("REMOVE 8:"); + REQUIRE(t[8].checkLinkageUpLeftRightBF(&t[16], &t[4], &t[12], 00)); + remove(&root, &t[8]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[9].checkLinkageUpLeftRightBF(&t[16], &t[4], &t[12], 00)); + REQUIRE(t[10].checkLinkageUpLeftRightBF(&t[12], Zzzzz, &t[11], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(23 == checkAscension(root)); + + // REMOVE 9 + // 16 + // / ` + // 10 21 + // / ` / ` + // 4 12 18 30 + // / ` / ` / ` / ` + // 2 6 11 14 17 19 22 31 + // / ` / ` / ` ` + // 1 3 5 7 13 15 23 + std::puts("REMOVE 9:"); + REQUIRE(t[9].checkLinkageUpLeftRightBF(&t[16], &t[4], &t[12], 00)); + remove(&root, &t[9]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[10].checkLinkageUpLeftRightBF(&t[16], &t[4], &t[12], 00)); + REQUIRE(t[12].checkLinkageUpLeftRightBF(&t[10], &t[11], &t[14], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(22 == checkAscension(root)); + + // REMOVE 1 + // 16 + // / ` + // 10 21 + // / ` / ` + // 4 12 18 30 + // / ` / ` / ` / ` + // 2 6 11 14 17 19 22 31 + // ` / ` / ` ` + // 3 5 7 13 15 23 + std::puts("REMOVE 1:"); + REQUIRE(t[1].checkLinkageUpLeftRightBF(&t[2], Zzzzz, Zzzzz, 00)); + remove(&root, &t[1]); + REQUIRE(&t[16] == root); + print(root); + REQUIRE(t[2].checkLinkageUpLeftRightBF(&t[4], Zzzzz, &t[3], +1)); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(21 == checkAscension(root)); +} + +auto getRandomByte() +{ + return static_cast((0xFFLL * std::rand()) / RAND_MAX); +} + +TEST_CASE("MutationRandomized") +{ + using N = Node; + std::array t{}; + for (auto i = 0U; i < 256U; i++) + { + t.at(i).value = static_cast(i); + } + std::array mask{}; + std::size_t size = 0; + N* root = nullptr; + + std::uint64_t cnt_addition = 0; + std::uint64_t cnt_removal = 0; + + const auto validate = [&]() { + REQUIRE(size == std::accumulate(mask.begin(), mask.end(), 0U, [](const std::size_t a, const std::size_t b) { + return a + b; + })); + REQUIRE(nullptr == findBrokenBalanceFactor(root)); + REQUIRE(nullptr == findBrokenAncestry(root)); + REQUIRE(size == checkAscension(root)); + std::array new_mask{}; + traverse(root, [&](const N* node) { new_mask.at(node->value) = true; }); + REQUIRE(mask == new_mask); // Otherwise, the contents of the tree does not match our expectations. + }; + validate(); + + const auto add = [&](const std::uint8_t x) { + const auto predicate = [&](const N& v) { return x - v.value; }; + if (N* const existing = search(&root, predicate)) + { + REQUIRE(mask.at(x)); + REQUIRE(x == existing->value); + REQUIRE(x == search(&root, predicate, []() -> N* { + FAIL("Attempted to create a new node when there is one already"); + return nullptr; + })->value); + } + else + { + REQUIRE(!mask.at(x)); + bool factory_called = false; + REQUIRE(x == search(&root, predicate, [&]() -> N* { + factory_called = true; + return &t.at(x); + })->value); + REQUIRE(factory_called); + size++; + cnt_addition++; + mask.at(x) = true; + } + }; + + const auto drop = [&](const std::uint8_t x) { + const auto predicate = [&](const N& v) { return x - v.value; }; + if (N* const existing = search(&root, predicate)) + { + REQUIRE(mask.at(x)); + REQUIRE(x == existing->value); + remove(&root, existing); + size--; + cnt_removal++; + mask.at(x) = false; + REQUIRE(nullptr == search(&root, predicate)); + } + else + { + REQUIRE(!mask.at(x)); + } + }; + + std::puts("Running the randomized test..."); + for (std::uint32_t iteration = 0U; iteration < 100'000U; iteration++) + { + if ((getRandomByte() % 2U) != 0) + { + add(getRandomByte()); + } + else + { + drop(getRandomByte()); + } + validate(); + } + + std::cout << "Randomized test finished. Final state:\n" // + << "\tsize: " << size // + << "\tcnt_addition: " << cnt_addition // + << "\tcnt_removal: " << cnt_removal // + << std::endl; + if (root != nullptr) + { + std::cout << "\tmin/max: " << unsigned(root->min()->value) << "/" << unsigned(root->max()->value) // + << std::endl; + } + validate(); +} diff --git a/tests/test_private_crc.cpp b/tests/test_private_crc.cpp index bebb4a6a..954ccfc5 100644 --- a/tests/test_private_crc.cpp +++ b/tests/test_private_crc.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2016-2020 UAVCAN Development Team. #include "exposed.hpp" +#include "catch.hpp" TEST_CASE("TransferCRC") { diff --git a/tests/test_private_rx.cpp b/tests/test_private_rx.cpp index 9fe6c4db..7d96c023 100644 --- a/tests/test_private_rx.cpp +++ b/tests/test_private_rx.cpp @@ -3,6 +3,7 @@ #include "exposed.hpp" #include "helpers.hpp" +#include "catch.hpp" #include TEST_CASE("rxTryParseFrame") @@ -18,12 +19,11 @@ TEST_CASE("rxTryParseFrame") static std::vector payload_storage; payload_storage = payload; CanardFrame frame{}; - frame.timestamp_usec = timestamp_usec; frame.extended_can_id = extended_can_id; frame.payload_size = std::size(payload); frame.payload = payload_storage.data(); model = RxFrameModel{}; - return rxTryParseFrame(&frame, &model); + return rxTryParseFrame(timestamp_usec, &frame, &model); }; // MESSAGE @@ -309,7 +309,7 @@ TEST_CASE("rxSessionUpdate") rxs.transfer_id = 31; rxs.redundant_transport_index = 1; - CanardTransfer transfer{}; + CanardRxTransfer transfer{}; const auto update = [&](const std::uint8_t redundant_transport_index, const std::uint64_t tid_timeout_usec, @@ -335,11 +335,11 @@ TEST_CASE("rxSessionUpdate") REQUIRE(rxs.toggle); REQUIRE(rxs.redundant_transport_index == 1); REQUIRE(transfer.timestamp_usec == 10'000'000); - REQUIRE(transfer.priority == CanardPrioritySlow); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 2'222); - REQUIRE(transfer.remote_node_id == 55); - REQUIRE(transfer.transfer_id == 11); + REQUIRE(transfer.metadata.priority == CanardPrioritySlow); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 2'222); + REQUIRE(transfer.metadata.remote_node_id == 55); + REQUIRE(transfer.metadata.transfer_id == 11); REQUIRE(transfer.payload_size == 3); REQUIRE(0 == std::memcmp(transfer.payload, "\x01\x01\x01", 3)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); @@ -371,11 +371,11 @@ TEST_CASE("rxSessionUpdate") REQUIRE(rxs.toggle); REQUIRE(rxs.redundant_transport_index == 1); REQUIRE(transfer.timestamp_usec == 10'000'050); - REQUIRE(transfer.priority == CanardPrioritySlow); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 2'222); - REQUIRE(transfer.remote_node_id == 55); - REQUIRE(transfer.transfer_id == 12); + REQUIRE(transfer.metadata.priority == CanardPrioritySlow); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 2'222); + REQUIRE(transfer.metadata.remote_node_id == 55); + REQUIRE(transfer.metadata.transfer_id == 12); REQUIRE(transfer.payload_size == 3); REQUIRE(0 == std::memcmp(transfer.payload, "\x03\x03\x03", 3)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); @@ -408,11 +408,11 @@ TEST_CASE("rxSessionUpdate") REQUIRE(rxs.toggle); REQUIRE(rxs.redundant_transport_index == 0); REQUIRE(transfer.timestamp_usec == 20'000'000); - REQUIRE(transfer.priority == CanardPrioritySlow); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 2'222); - REQUIRE(transfer.remote_node_id == 55); - REQUIRE(transfer.transfer_id == 12); + REQUIRE(transfer.metadata.priority == CanardPrioritySlow); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 2'222); + REQUIRE(transfer.metadata.remote_node_id == 55); + REQUIRE(transfer.metadata.transfer_id == 12); REQUIRE(transfer.payload_size == 3); REQUIRE(0 == std::memcmp(transfer.payload, "\x05\x05\x05", 3)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); @@ -486,11 +486,11 @@ TEST_CASE("rxSessionUpdate") REQUIRE(rxs.toggle); REQUIRE(rxs.redundant_transport_index == 0); REQUIRE(transfer.timestamp_usec == 20'000'100); - REQUIRE(transfer.priority == CanardPrioritySlow); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 2'222); - REQUIRE(transfer.remote_node_id == 55); - REQUIRE(transfer.transfer_id == 13); + REQUIRE(transfer.metadata.priority == CanardPrioritySlow); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 2'222); + REQUIRE(transfer.metadata.remote_node_id == 55); + REQUIRE(transfer.metadata.transfer_id == 13); REQUIRE(transfer.payload_size == 16); REQUIRE(0 == std::memcmp(transfer.payload, "\x06\x06\x06\x06\x06\x06\x06\x07\x07\x07\x07\x07\x07\x07\x09\x09", 16)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); @@ -571,11 +571,11 @@ TEST_CASE("rxSessionUpdate") REQUIRE(rxs.toggle); REQUIRE(rxs.redundant_transport_index == 2); REQUIRE(transfer.timestamp_usec == 20'000'200); - REQUIRE(transfer.priority == CanardPrioritySlow); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 2'222); - REQUIRE(transfer.remote_node_id == 55); - REQUIRE(transfer.transfer_id == 11); + REQUIRE(transfer.metadata.priority == CanardPrioritySlow); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 2'222); + REQUIRE(transfer.metadata.remote_node_id == 55); + REQUIRE(transfer.metadata.transfer_id == 11); REQUIRE(transfer.payload_size == 10); REQUIRE(0 == std::memcmp(transfer.payload, "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0D\x0D\x0D", 10)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); @@ -618,11 +618,11 @@ TEST_CASE("rxSessionUpdate") REQUIRE(rxs.toggle); REQUIRE(rxs.redundant_transport_index == 0); REQUIRE(transfer.timestamp_usec == 30'000'000); - REQUIRE(transfer.priority == CanardPrioritySlow); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 2'222); - REQUIRE(transfer.remote_node_id == 55); - REQUIRE(transfer.transfer_id == 0); + REQUIRE(transfer.metadata.priority == CanardPrioritySlow); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 2'222); + REQUIRE(transfer.metadata.remote_node_id == 55); + REQUIRE(transfer.metadata.transfer_id == 0); REQUIRE(transfer.payload_size == 7); // ONE CRC BYTE BACKTRACKED! REQUIRE(0 == std::memcmp(transfer.payload, "\x0E\x0E\x0E\x0E\x0E\x0E\x0E", 7)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); diff --git a/tests/test_private_tx.cpp b/tests/test_private_tx.cpp index a904b866..312a5767 100644 --- a/tests/test_private_tx.cpp +++ b/tests/test_private_tx.cpp @@ -3,6 +3,7 @@ #include "exposed.hpp" #include "helpers.hpp" +#include "catch.hpp" TEST_CASE("SessionSpecifier") { @@ -14,44 +15,32 @@ TEST_CASE("SessionSpecifier") exposed::txMakeServiceSessionSpecifier(0b0100110011, false, 0b0101010, 0b1010101)); } -TEST_CASE("txGetPresentationLayerMTU") +TEST_CASE("adjustPresentationLayerMTU") { - auto ins = canardInit(&helpers::dummy_allocator::allocate, &helpers::dummy_allocator::free); - REQUIRE(63 == exposed::txGetPresentationLayerMTU(&ins)); // This is the default. - ins.mtu_bytes = 0; - REQUIRE(7 == exposed::txGetPresentationLayerMTU(&ins)); - ins.mtu_bytes = 255; - REQUIRE(63 == exposed::txGetPresentationLayerMTU(&ins)); - ins.mtu_bytes = 32; - REQUIRE(31 == exposed::txGetPresentationLayerMTU(&ins)); - ins.mtu_bytes = 30; // Round up. - REQUIRE(31 == exposed::txGetPresentationLayerMTU(&ins)); + REQUIRE(63 == exposed::adjustPresentationLayerMTU(64)); + REQUIRE(7 == exposed::adjustPresentationLayerMTU(0)); + REQUIRE(63 == exposed::adjustPresentationLayerMTU(255)); + REQUIRE(31 == exposed::adjustPresentationLayerMTU(32)); + REQUIRE(31 == exposed::adjustPresentationLayerMTU(30)); } TEST_CASE("txMakeCANID") { using exposed::txMakeCANID; - CanardTransfer transfer{}; - std::vector transfer_payload; - - const auto mk_transfer = [&](const CanardPriority priority, - const CanardTransferKind kind, - const std::uint16_t port_id, - const std::uint8_t remote_node_id, - const std::vector& payload = {}) { - transfer_payload = payload; - transfer.priority = priority; - transfer.transfer_kind = kind; - transfer.port_id = port_id; - transfer.remote_node_id = remote_node_id; - transfer.payload = transfer_payload.data(); - transfer.payload_size = transfer_payload.size(); - return &transfer; + CanardTransferMetadata meta{}; + + const auto mk_meta = [&](const CanardPriority priority, + const CanardTransferKind kind, + const std::uint16_t port_id, + const std::uint8_t remote_node_id) { + meta.priority = priority; + meta.transfer_kind = kind; + meta.port_id = port_id; + meta.remote_node_id = remote_node_id; + return &meta; }; - const auto crc123 = exposed::crcAdd(0xFFFFU, 3, "\x01\x02\x03"); - union PriorityAlias { std::uint8_t bits; @@ -60,70 +49,86 @@ TEST_CASE("txMakeCANID") // MESSAGE TRANSFERS REQUIRE(0b000'00'0'11'1001100110011'0'1010101 == // Regular message. - txMakeCANID(mk_transfer(CanardPriorityExceptional, - CanardTransferKindMessage, - 0b1001100110011, - CANARD_NODE_ID_UNSET), - 0b1010101, - 7U)); - REQUIRE(0b111'00'0'11'1001100110011'0'1010101 == // Regular message. - txMakeCANID(mk_transfer(CanardPriorityOptional, - CanardTransferKindMessage, - 0b1001100110011, - CANARD_NODE_ID_UNSET), + txMakeCANID(mk_meta(CanardPriorityExceptional, + CanardTransferKindMessage, + 0b1001100110011, + CANARD_NODE_ID_UNSET), + 0, + "", 0b1010101, 7U)); - REQUIRE((0b010'01'0'11'1001100110011'0'0000000U | (crc123 & CANARD_NODE_ID_MAX)) == // Anonymous message. - txMakeCANID(mk_transfer(CanardPriorityFast, - CanardTransferKindMessage, - 0b1001100110011, - CANARD_NODE_ID_UNSET, - {1, 2, 3}), - 128U, // Invalid local node-ID. - 7U)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Multi-frame anonymous messages are not allowed. - txMakeCANID(mk_transfer(CanardPriorityImmediate, - CanardTransferKindMessage, - 0b1001100110011, - CANARD_NODE_ID_UNSET, - {1, 2, 3, 4, 5, 6, 7, 8}), - 128U, // Invalid local node-ID is treated as anonymous/unset. - 7U)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Bad remote node-ID -- unicast messages not supported. - txMakeCANID(mk_transfer(CanardPriorityHigh, CanardTransferKindMessage, 0b1001100110011, 123U), 0U, 7U)); REQUIRE( - -CANARD_ERROR_INVALID_ARGUMENT == // Bad subject-ID. - txMakeCANID(mk_transfer(CanardPriorityLow, CanardTransferKindMessage, 0xFFFFU, CANARD_NODE_ID_UNSET), 0U, 7U)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Bad priority. - txMakeCANID(mk_transfer(PriorityAlias{123}.prio, - CanardTransferKindMessage, - 0b1001100110011, - CANARD_NODE_ID_UNSET), - 0b1010101, + 0b111'00'0'11'1001100110011'0'1010101 == // Regular message. + txMakeCANID(mk_meta(CanardPriorityOptional, CanardTransferKindMessage, 0b1001100110011, CANARD_NODE_ID_UNSET), + 0, + "", + 0b1010101, + 7U)); + REQUIRE( + (0b010'01'0'11'1001100110011'0'0000000U | (exposed::crcAdd(0xFFFFU, 3, "\x01\x02\x03") & CANARD_NODE_ID_MAX)) == + txMakeCANID(mk_meta(CanardPriorityFast, CanardTransferKindMessage, 0b1001100110011, CANARD_NODE_ID_UNSET), + 3, + "\x01\x02\x03", + 128U, // Invalid local node-ID --> anonymous message. + 7U)); + REQUIRE( + -CANARD_ERROR_INVALID_ARGUMENT == // Multi-frame anonymous messages are not allowed. + txMakeCANID(mk_meta(CanardPriorityImmediate, CanardTransferKindMessage, 0b1001100110011, CANARD_NODE_ID_UNSET), + 8, + "\x01\x02\x03\x04\x05\x06\x07\x08", + 128U, // Invalid local node-ID is treated as anonymous/unset. + 7U)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Bad remote node-ID -- unicast messages not supported. + txMakeCANID(mk_meta(CanardPriorityHigh, CanardTransferKindMessage, 0b1001100110011, 123U), 0, "", 0U, 7U)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Bad subject-ID. + txMakeCANID(mk_meta(CanardPriorityLow, CanardTransferKindMessage, 0xFFFFU, CANARD_NODE_ID_UNSET), + 0, + "", + 0U, 7U)); + REQUIRE( + -CANARD_ERROR_INVALID_ARGUMENT == // Bad priority. + txMakeCANID(mk_meta(PriorityAlias{123}.prio, CanardTransferKindMessage, 0b1001100110011, CANARD_NODE_ID_UNSET), + 0, + "", + 0b1010101, + 7U)); // SERVICE TRANSFERS REQUIRE(0b000'11'0100110011'0101010'1010101 == // Request. - txMakeCANID(mk_transfer(CanardPriorityExceptional, CanardTransferKindRequest, 0b0100110011, 0b0101010), + txMakeCANID(mk_meta(CanardPriorityExceptional, CanardTransferKindRequest, 0b0100110011, 0b0101010), + 0, + "", 0b1010101, 7U)); REQUIRE(0b111'10'0100110011'0101010'1010101 == // Response. - txMakeCANID(mk_transfer(CanardPriorityOptional, CanardTransferKindResponse, 0b0100110011, 0b0101010), + txMakeCANID(mk_meta(CanardPriorityOptional, CanardTransferKindResponse, 0b0100110011, 0b0101010), + 0, + "", 0b1010101, 7U)); REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Anonymous service transfers not permitted. - txMakeCANID(mk_transfer(CanardPriorityExceptional, CanardTransferKindRequest, 0b0100110011, 0b0101010), + txMakeCANID(mk_meta(CanardPriorityExceptional, CanardTransferKindRequest, 0b0100110011, 0b0101010), + 0, + "", CANARD_NODE_ID_UNSET, 7U)); REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Broadcast service transfers not permitted. - txMakeCANID(mk_transfer(CanardPrioritySlow, CanardTransferKindResponse, 0b0100110011, CANARD_NODE_ID_UNSET), + txMakeCANID(mk_meta(CanardPrioritySlow, CanardTransferKindResponse, 0b0100110011, CANARD_NODE_ID_UNSET), + 0, + "", + 0b1010101, + 7U)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Bad service-ID. + txMakeCANID(mk_meta(CanardPriorityNominal, CanardTransferKindResponse, 0xFFFFU, 0b0101010), + 0, + "", 0b1010101, 7U)); - REQUIRE( - -CANARD_ERROR_INVALID_ARGUMENT == // Bad service-ID. - txMakeCANID(mk_transfer(CanardPriorityNominal, CanardTransferKindResponse, 0xFFFFU, 0b0101010), 0b1010101, 7U)); REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == // Bad priority. - txMakeCANID(mk_transfer(PriorityAlias{123}.prio, CanardTransferKindResponse, 0b0100110011, 0b0101010), + txMakeCANID(mk_meta(PriorityAlias{123}.prio, CanardTransferKindResponse, 0b0100110011, 0b0101010), + 0, + "", 0b1010101, 7U)); } @@ -169,53 +174,3 @@ TEST_CASE("txRoundFramePayloadSizeUp") REQUIRE(64 == txRoundFramePayloadSizeUp(50)); REQUIRE(64 == txRoundFramePayloadSizeUp(64)); } - -TEST_CASE("txFindQueueSupremum") -{ - using exposed::txFindQueueSupremum; - using TxQueueItem = exposed::TxQueueItem; - - auto ins = canardInit(&helpers::dummy_allocator::allocate, &helpers::dummy_allocator::free); - - const auto find = [&](std::uint32_t x) -> TxQueueItem* { return txFindQueueSupremum(&ins, x); }; - - REQUIRE(nullptr == find(0)); - REQUIRE(nullptr == find((1UL << 29U) - 1U)); - - TxQueueItem a{}; - a.frame.extended_can_id = 1000; - ins._tx_queue = reinterpret_cast(&a); - - REQUIRE(nullptr == find(999)); - REQUIRE(&a == find(1000)); - REQUIRE(&a == find(1001)); - - TxQueueItem b{}; - b.frame.extended_can_id = 1010; - a.next = &b; - - REQUIRE(nullptr == find(999)); - REQUIRE(&a == find(1000)); - REQUIRE(&a == find(1001)); - REQUIRE(&a == find(1009)); - REQUIRE(&b == find(1010)); - REQUIRE(&b == find(1011)); - - TxQueueItem c{}; - c.frame.extended_can_id = 990; - c.next = &a; - ins._tx_queue = reinterpret_cast(&c); - REQUIRE(reinterpret_cast(ins._tx_queue)->frame.extended_can_id == 990); - REQUIRE(reinterpret_cast(ins._tx_queue)->next->frame.extended_can_id == 1000); - REQUIRE(reinterpret_cast(ins._tx_queue)->next->next->frame.extended_can_id == 1010); - REQUIRE(reinterpret_cast(ins._tx_queue)->next->next->next == nullptr); - - REQUIRE(nullptr == find(989)); - REQUIRE(&c == find(990)); - REQUIRE(&c == find(999)); - REQUIRE(&a == find(1000)); - REQUIRE(&a == find(1001)); - REQUIRE(&a == find(1009)); - REQUIRE(&b == find(1010)); - REQUIRE(&b == find(1011)); -} diff --git a/tests/test_public_filters.cpp b/tests/test_public_filters.cpp new file mode 100644 index 00000000..6208031f --- /dev/null +++ b/tests/test_public_filters.cpp @@ -0,0 +1,67 @@ +// This software is distributed under the terms of the MIT License. +// Copyright (c) 2016-2021 UAVCAN Development Team. + +#include "exposed.hpp" +#include "catch.hpp" + +namespace +{ +constexpr std::uint32_t OFFSET_SUBJECT_ID = 8U; +constexpr std::uint32_t OFFSET_SERVICE_ID = 14U; +constexpr std::uint32_t OFFSET_DST_NODE_ID = 7U; + +constexpr std::uint32_t FLAG_SERVICE_NOT_MESSAGE = std::uint32_t(1) << 25U; +constexpr std::uint32_t FLAG_RESERVED_23 = std::uint32_t(1) << 23U; +constexpr std::uint32_t FLAG_RESERVED_07 = std::uint32_t(1) << 7U; + +TEST_CASE("FilterSubject") +{ + const std::uint16_t heartbeat_subject_id = 7509; + CanardFilter heartbeat_config = canardMakeFilterForSubject(heartbeat_subject_id); + REQUIRE((heartbeat_config.extended_can_id & + static_cast(heartbeat_subject_id << OFFSET_SUBJECT_ID)) != 0); + REQUIRE((heartbeat_config.extended_mask & FLAG_SERVICE_NOT_MESSAGE) != 0); + REQUIRE((heartbeat_config.extended_mask & FLAG_RESERVED_07) != 0); + REQUIRE((heartbeat_config.extended_mask & (CANARD_SUBJECT_ID_MAX << OFFSET_SUBJECT_ID)) != 0); +} + +TEST_CASE("FilterService") +{ + const std::uint16_t access_service_id = 7509; + const std::uint16_t node_id = 42; + CanardFilter access_config = canardMakeFilterForService(access_service_id, node_id); + REQUIRE((access_config.extended_can_id & static_cast(access_service_id << OFFSET_SERVICE_ID)) != 0); + REQUIRE((access_config.extended_can_id & static_cast(node_id << OFFSET_DST_NODE_ID)) != 0); + REQUIRE((access_config.extended_can_id & FLAG_SERVICE_NOT_MESSAGE) != 0); + REQUIRE((access_config.extended_mask & FLAG_SERVICE_NOT_MESSAGE) != 0); + REQUIRE((access_config.extended_mask & FLAG_RESERVED_23) != 0); + REQUIRE((access_config.extended_mask & static_cast(CANARD_SERVICE_ID_MAX << OFFSET_SERVICE_ID)) != + 0); + REQUIRE((access_config.extended_mask & static_cast(CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID)) != 0); +} + +TEST_CASE("FilterServices") +{ + const std::uint8_t node_id = 42; + CanardFilter access_config = canardMakeFilterForServices(node_id); + REQUIRE((access_config.extended_can_id & static_cast(node_id << OFFSET_DST_NODE_ID)) != 0); + REQUIRE((access_config.extended_can_id & FLAG_SERVICE_NOT_MESSAGE) != 0); + REQUIRE((access_config.extended_mask & FLAG_SERVICE_NOT_MESSAGE) != 0); + REQUIRE((access_config.extended_mask & FLAG_RESERVED_23) != 0); + REQUIRE((access_config.extended_mask & static_cast(CANARD_NODE_ID_MAX << OFFSET_DST_NODE_ID)) != 0); +} + +TEST_CASE("Consolidate") +{ + const std::uint16_t heartbeat_subject_id = 7509; + CanardFilter heartbeat_config = canardMakeFilterForSubject(heartbeat_subject_id); + + const std::uint16_t access_service_id = 384; + const std::uint8_t node_id = 42; + CanardFilter access_config = canardMakeFilterForService(access_service_id, node_id); + + CanardFilter combined = canardConsolidateFilters(&heartbeat_config, &access_config); + REQUIRE((combined.extended_mask | heartbeat_config.extended_mask) == heartbeat_config.extended_mask); + REQUIRE((combined.extended_mask | access_config.extended_mask) == access_config.extended_mask); +} +} // namespace diff --git a/tests/test_public_roundtrip.cpp b/tests/test_public_roundtrip.cpp index 2aca5078..4b08e4c7 100644 --- a/tests/test_public_roundtrip.cpp +++ b/tests/test_public_roundtrip.cpp @@ -3,6 +3,7 @@ #include "helpers.hpp" #include "exposed.hpp" +#include "catch.hpp" #include #include #include @@ -56,10 +57,12 @@ TEST_CASE("RoundtripSimple") ins_rx.getAllocator().setAllocationCeiling(rx_worst_case_memory_consumption); // This is guaranteed to be enough. helpers::Instance ins_tx; + helpers::TxQueue que_tx(1024UL * 1024U * 1024U, CANARD_MTU_CAN_FD); ins_tx.setNodeID(99); - ins_tx.getAllocator().setAllocationCeiling(1024 * 1024 * 1024); + ins_tx.getAllocator().setAllocationCeiling(1024UL * 1024U * 1024U); - std::unordered_map pending_transfers; + using Pending = std::tuple; + std::unordered_map pending_transfers; std::atomic transfer_counter = 0; std::atomic frames_in_flight = 0; @@ -79,34 +82,36 @@ TEST_CASE("RoundtripSimple") std::generate_n(payload, payload_size, [&]() { return static_cast(getRandomNatural(256U)); }); // Generate the transfer. - CanardTransfer tran{}; - tran.timestamp_usec = transfer_counter++; - tran.priority = st.priority; - tran.transfer_kind = st.transfer_kind; - tran.port_id = st.port_id; + const CanardMicrosecond timestamp_usec = transfer_counter++; + CanardTransferMetadata tran{}; + tran.priority = st.priority; + tran.transfer_kind = st.transfer_kind; + tran.port_id = st.port_id; tran.remote_node_id = (tran.transfer_kind == CanardTransferKindMessage) ? CANARD_NODE_ID_UNSET : ins_rx.getNodeID(); - tran.transfer_id = (st.transfer_id++) & CANARD_TRANSFER_ID_MAX; - tran.payload_size = payload_size; - tran.payload = payload; + tran.transfer_id = (st.transfer_id++) & CANARD_TRANSFER_ID_MAX; // Use a random MTU. - ins_tx.setMTU(static_cast(getRandomNatural(256U))); + que_tx.setMTU(static_cast(getRandomNatural(256U))); // Push the transfer. bool sleep = false; { std::lock_guard locker(lock); - const auto result = ins_tx.txPush(tran); + const auto result = que_tx.push(&ins_tx.getInstance(), timestamp_usec, tran, payload_size, payload); if (result > 0) { - pending_transfers.emplace(tran.timestamp_usec, tran); + pending_transfers.emplace(timestamp_usec, Pending{tran, payload_size, payload}); frames_in_flight += static_cast(result); peak_frames_in_flight = std::max(peak_frames_in_flight, frames_in_flight); } else { - REQUIRE(result == -CANARD_ERROR_OUT_OF_MEMORY); + if (result != -CANARD_ERROR_OUT_OF_MEMORY) + { + // Can't use REQUIRE because it is not thread-safe. + throw std::logic_error("Unexpected result: " + std::to_string(result)); + } sleep = true; } } @@ -125,43 +130,43 @@ TEST_CASE("RoundtripSimple") try { - const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(20); while (true) { - const CanardFrame* frame = nullptr; + CanardTxQueueItem* ti = nullptr; { std::lock_guard locker(lock); - frame = ins_tx.txPeek(); // Peek-pop form an atomic transaction. - ins_tx.txPop(); // No effect if the queue is empty. - if (frame != nullptr) + ti = que_tx.pop(que_tx.peek()); + if (ti != nullptr) { REQUIRE(frames_in_flight > 0); --frames_in_flight; } } - if (frame != nullptr) + if (ti != nullptr) { - const auto tail = reinterpret_cast(frame->payload)[frame->payload_size - 1U]; - log_file << frame->timestamp_usec << " " // - << std::hex << std::setfill('0') << std::setw(8) << frame->extended_can_id // - << " [" << std::dec << std::setfill(' ') << std::setw(2) << frame->payload_size << "] " // - << (bool(tail & 128U) ? 'S' : ' ') // - << (bool(tail & 64U) ? 'E' : ' ') // - << (bool(tail & 32U) ? 'T' : ' ') // - << " " << std::uint16_t(tail & 31U) // + const auto tail = static_cast(ti->frame.payload)[ti->frame.payload_size - 1U]; + log_file << ti->tx_deadline_usec << " " // + << std::hex << std::setfill('0') << std::setw(8) << ti->frame.extended_can_id // + << " [" << std::dec << std::setfill(' ') << std::setw(2) << ti->frame.payload_size << "] " // + << (bool(tail & 128U) ? 'S' : ' ') // + << (bool(tail & 64U) ? 'E' : ' ') // + << (bool(tail & 32U) ? 'T' : ' ') // + << " " << std::uint16_t(tail & 31U) // << '\n'; - CanardTransfer transfer{}; + CanardRxTransfer transfer{}; CanardRxSubscription* subscription = nullptr; - std::int8_t result = ins_rx.rxAccept(*frame, 0, transfer, &subscription); - REQUIRE(0 == ins_rx.rxAccept(*frame, + std::int8_t result = ins_rx.rxAccept(ti->tx_deadline_usec, ti->frame, 0, transfer, &subscription); + REQUIRE(0 == ins_rx.rxAccept(ti->tx_deadline_usec, + ti->frame, 1, transfer, &subscription)); // Redundant interface will never be used here. if (result == 1) { - CanardTransfer reference{}; // Fetch the reference transfer from the list of pending. + Pending reference{}; // Fetch the reference transfer from the list of pending. { std::lock_guard locker(lock); const auto pt_it = pending_transfers.find(transfer.timestamp_usec); @@ -169,19 +174,19 @@ TEST_CASE("RoundtripSimple") reference = pt_it->second; pending_transfers.erase(pt_it); } + const auto [ref_meta, ref_payload_size, ref_payload] = reference; - REQUIRE(transfer.timestamp_usec == reference.timestamp_usec); - REQUIRE(transfer.priority == reference.priority); - REQUIRE(transfer.transfer_kind == reference.transfer_kind); - REQUIRE(transfer.port_id == reference.port_id); - REQUIRE(transfer.remote_node_id == ins_tx.getNodeID()); - REQUIRE(transfer.transfer_id == reference.transfer_id); + REQUIRE(transfer.metadata.priority == ref_meta.priority); + REQUIRE(transfer.metadata.transfer_kind == ref_meta.transfer_kind); + REQUIRE(transfer.metadata.port_id == ref_meta.port_id); + REQUIRE(transfer.metadata.remote_node_id == ins_tx.getNodeID()); + REQUIRE(transfer.metadata.transfer_id == ref_meta.transfer_id); // The payload size is not checked because the variance is huge due to padding and truncation. if (transfer.payload != nullptr) { REQUIRE(0 == std::memcmp(transfer.payload, - reference.payload, - std::min(transfer.payload_size, reference.payload_size))); + ref_payload, + std::min(transfer.payload_size, ref_payload_size))); } else { @@ -189,7 +194,7 @@ TEST_CASE("RoundtripSimple") } ins_rx.getAllocator().deallocate(transfer.payload); - std::free(const_cast(reference.payload)); // NOLINT + std::free(ref_payload); // NOLINT } else { @@ -204,7 +209,11 @@ TEST_CASE("RoundtripSimple") } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - ins_tx.getAllocator().deallocate(frame); + + { + std::lock_guard locker(lock); + ins_tx.getAllocator().deallocate(ti); + } if (std::chrono::steady_clock::now() > deadline) { @@ -228,16 +237,16 @@ TEST_CASE("RoundtripSimple") std::cout << "PEAK FRAMES IN FLIGHT: " << peak_frames_in_flight << std::endl; std::size_t i = 0; - for (const auto [k, v] : pending_transfers) + for (const auto& [k, v] : pending_transfers) { - REQUIRE(k == v.timestamp_usec); - std::cout << "#" << i << "/" << std::size(pending_transfers) << ":" // - << " ts=" << v.timestamp_usec // - << " prio=" << static_cast(v.priority) // - << " kind=" << static_cast(v.transfer_kind) // - << " port=" << v.port_id // - << " nid=" << static_cast(v.remote_node_id) // - << " tid=" << static_cast(v.transfer_id) // + const auto [ref_meta, ref_payload_size, ref_payload] = v; + std::cout << "#" << i++ << "/" << std::size(pending_transfers) << ":" // + << " ts=" << k // + << " prio=" << static_cast(ref_meta.priority) // + << " kind=" << static_cast(ref_meta.transfer_kind) // + << " port=" << ref_meta.port_id // + << " nid=" << static_cast(ref_meta.remote_node_id) // + << " tid=" << static_cast(ref_meta.transfer_id) // << std::endl; } diff --git a/tests/test_public_rx.cpp b/tests/test_public_rx.cpp index fc2b016b..96ee791e 100644 --- a/tests/test_public_rx.cpp +++ b/tests/test_public_rx.cpp @@ -3,11 +3,12 @@ #include "exposed.hpp" #include "helpers.hpp" +#include "catch.hpp" #include // clang-tidy mistakenly suggests to avoid C arrays here, which is clearly an error template -auto ensureAllNullptr(P* (&arr)[N]) -> bool // NOLINT +auto ensureAllNullptr(P* const (&arr)[N]) -> bool // NOLINT { return std::all_of(std::begin(arr), std::end(arr), [](const auto* const x) { return x == nullptr; }); } @@ -18,7 +19,7 @@ TEST_CASE("RxBasic0") using exposed::RxSession; Instance ins; - CanardTransfer transfer{}; + CanardRxTransfer transfer{}; CanardRxSubscription* subscription = nullptr; const auto accept = [&](const std::uint8_t redundant_transport_index, @@ -28,19 +29,18 @@ TEST_CASE("RxBasic0") static std::vector payload_storage; payload_storage = payload; CanardFrame frame{}; - frame.timestamp_usec = timestamp_usec; frame.extended_can_id = extended_can_id; frame.payload_size = std::size(payload); frame.payload = payload_storage.data(); - return ins.rxAccept(frame, redundant_transport_index, transfer, &subscription); + return ins.rxAccept(timestamp_usec, frame, redundant_transport_index, transfer, &subscription); }; ins.getAllocator().setAllocationCeiling(sizeof(RxSession) + 16); // A session and a 16-byte payload buffer. // No subscriptions by default. - REQUIRE(ins.getInstance().rx_subscriptions[0] == nullptr); - REQUIRE(ins.getInstance().rx_subscriptions[1] == nullptr); - REQUIRE(ins.getInstance().rx_subscriptions[2] == nullptr); + REQUIRE(ins.getMessageSubs().empty()); + REQUIRE(ins.getResponseSubs().empty()); + REQUIRE(ins.getRequestSubs().empty()); // A valid single-frame transfer for which there is no subscription. subscription = nullptr; @@ -51,50 +51,46 @@ TEST_CASE("RxBasic0") CanardRxSubscription sub_msg{}; REQUIRE(1 == ins.rxSubscribe(CanardTransferKindMessage, 0b0110011001100, 32, 2'000'000, sub_msg)); // New. REQUIRE(0 == ins.rxSubscribe(CanardTransferKindMessage, 0b0110011001100, 16, 1'000'000, sub_msg)); // Replaced. - REQUIRE(ins.getInstance().rx_subscriptions[0] == &sub_msg); - REQUIRE(ins.getInstance().rx_subscriptions[0]->next == nullptr); - REQUIRE(ins.getInstance().rx_subscriptions[0]->port_id == 0b0110011001100); - REQUIRE(ins.getInstance().rx_subscriptions[0]->extent == 16); - REQUIRE(ins.getInstance().rx_subscriptions[0]->transfer_id_timeout_usec == 1'000'000); - REQUIRE(ensureAllNullptr(ins.getInstance().rx_subscriptions[0]->_sessions)); - REQUIRE(ins.getInstance().rx_subscriptions[1] == nullptr); - REQUIRE(ins.getInstance().rx_subscriptions[2] == nullptr); + REQUIRE(ins.getMessageSubs().at(0) == &sub_msg); + REQUIRE(ins.getMessageSubs().at(0)->port_id == 0b0110011001100); + REQUIRE(ins.getMessageSubs().at(0)->extent == 16); + REQUIRE(ins.getMessageSubs().at(0)->transfer_id_timeout_usec == 1'000'000); + REQUIRE(ensureAllNullptr(ins.getMessageSubs().at(0)->sessions)); + REQUIRE(ins.getResponseSubs().empty()); + REQUIRE(ins.getRequestSubs().empty()); // Create a request subscription. CanardRxSubscription sub_req{}; REQUIRE(1 == ins.rxSubscribe(CanardTransferKindRequest, 0b0000110011, 20, 3'000'000, sub_req)); - REQUIRE(ins.getInstance().rx_subscriptions[0] == &sub_msg); - REQUIRE(ins.getInstance().rx_subscriptions[1] == nullptr); - REQUIRE(ins.getInstance().rx_subscriptions[2] == &sub_req); - REQUIRE(ins.getInstance().rx_subscriptions[2]->next == nullptr); - REQUIRE(ins.getInstance().rx_subscriptions[2]->port_id == 0b0000110011); - REQUIRE(ins.getInstance().rx_subscriptions[2]->extent == 20); - REQUIRE(ins.getInstance().rx_subscriptions[2]->transfer_id_timeout_usec == 3'000'000); - REQUIRE(ensureAllNullptr(ins.getInstance().rx_subscriptions[2]->_sessions)); + REQUIRE(ins.getMessageSubs().at(0) == &sub_msg); + REQUIRE(ins.getResponseSubs().empty()); + REQUIRE(ins.getRequestSubs().at(0) == &sub_req); + REQUIRE(ins.getRequestSubs().at(0)->port_id == 0b0000110011); + REQUIRE(ins.getRequestSubs().at(0)->extent == 20); + REQUIRE(ins.getRequestSubs().at(0)->transfer_id_timeout_usec == 3'000'000); + REQUIRE(ensureAllNullptr(ins.getRequestSubs().at(0)->sessions)); // Create a response subscription. CanardRxSubscription sub_res{}; REQUIRE(1 == ins.rxSubscribe(CanardTransferKindResponse, 0b0000111100, 10, 100'000, sub_res)); - REQUIRE(ins.getInstance().rx_subscriptions[0] == &sub_msg); - REQUIRE(ins.getInstance().rx_subscriptions[1] == &sub_res); - REQUIRE(ins.getInstance().rx_subscriptions[1]->next == nullptr); - REQUIRE(ins.getInstance().rx_subscriptions[1]->port_id == 0b0000111100); - REQUIRE(ins.getInstance().rx_subscriptions[1]->extent == 10); - REQUIRE(ins.getInstance().rx_subscriptions[1]->transfer_id_timeout_usec == 100'000); - REQUIRE(ensureAllNullptr(ins.getInstance().rx_subscriptions[1]->_sessions)); - REQUIRE(ins.getInstance().rx_subscriptions[2] == &sub_req); - - // Create a second response subscription. + REQUIRE(ins.getMessageSubs().at(0) == &sub_msg); + REQUIRE(ins.getResponseSubs().at(0) == &sub_res); + REQUIRE(ins.getResponseSubs().at(0)->port_id == 0b0000111100); + REQUIRE(ins.getResponseSubs().at(0)->extent == 10); + REQUIRE(ins.getResponseSubs().at(0)->transfer_id_timeout_usec == 100'000); + REQUIRE(ensureAllNullptr(ins.getResponseSubs().at(0)->sessions)); + REQUIRE(ins.getRequestSubs().at(0) == &sub_req); + + // Create a second response subscription. It will come before the one we added above due to lower port-ID. CanardRxSubscription sub_res2{}; REQUIRE(1 == ins.rxSubscribe(CanardTransferKindResponse, 0b0000000000, 10, 1'000, sub_res2)); - REQUIRE(ins.getInstance().rx_subscriptions[0] == &sub_msg); - REQUIRE(ins.getInstance().rx_subscriptions[1] == &sub_res2); - REQUIRE(ins.getInstance().rx_subscriptions[1]->next == &sub_res); - REQUIRE(ins.getInstance().rx_subscriptions[1]->port_id == 0b0000000000); - REQUIRE(ins.getInstance().rx_subscriptions[1]->extent == 10); - REQUIRE(ins.getInstance().rx_subscriptions[1]->transfer_id_timeout_usec == 1'000); - REQUIRE(ensureAllNullptr(ins.getInstance().rx_subscriptions[1]->_sessions)); - REQUIRE(ins.getInstance().rx_subscriptions[2] == &sub_req); + REQUIRE(ins.getMessageSubs().at(0) == &sub_msg); + REQUIRE(ins.getResponseSubs().at(0) == &sub_res2); + REQUIRE(ins.getResponseSubs().at(0)->port_id == 0b0000000000); + REQUIRE(ins.getResponseSubs().at(0)->extent == 10); + REQUIRE(ins.getResponseSubs().at(0)->transfer_id_timeout_usec == 1'000); + REQUIRE(ins.getResponseSubs().at(1) == &sub_res); // The earlier one. + REQUIRE(ins.getRequestSubs().at(0) == &sub_req); // Accepted message. subscription = nullptr; @@ -102,17 +98,17 @@ TEST_CASE("RxBasic0") REQUIRE(subscription != nullptr); REQUIRE(subscription->port_id == 0b0110011001100); REQUIRE(transfer.timestamp_usec == 100'000'001); - REQUIRE(transfer.priority == CanardPriorityImmediate); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 0b0110011001100); - REQUIRE(transfer.remote_node_id == 0b0100111); - REQUIRE(transfer.transfer_id == 0); + REQUIRE(transfer.metadata.priority == CanardPriorityImmediate); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 0b0110011001100); + REQUIRE(transfer.metadata.remote_node_id == 0b0100111); + REQUIRE(transfer.metadata.transfer_id == 0); REQUIRE(transfer.payload_size == 0); REQUIRE(0 == std::memcmp(transfer.payload, "", 0)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 2); // The SESSION and the PAYLOAD BUFFER. REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == (sizeof(RxSession) + 16)); - REQUIRE(ins.getInstance().rx_subscriptions[0]->_sessions[0b0100111] != nullptr); - const auto* msg_payload = transfer.payload; // Will need it later. + REQUIRE(ins.getMessageSubs().at(0)->sessions[0b0100111] != nullptr); + auto* msg_payload = transfer.payload; // Will need it later. // Provide the space for an extra session and its payload. ins.getAllocator().setAllocationCeiling(sizeof(RxSession) * 2 + 16 + 20); @@ -134,16 +130,16 @@ TEST_CASE("RxBasic0") REQUIRE(subscription != nullptr); REQUIRE(subscription->port_id == 0b0000110011); REQUIRE(transfer.timestamp_usec == 100'000'002); - REQUIRE(transfer.priority == CanardPriorityHigh); - REQUIRE(transfer.transfer_kind == CanardTransferKindRequest); - REQUIRE(transfer.port_id == 0b0000110011); - REQUIRE(transfer.remote_node_id == 0b0100101); - REQUIRE(transfer.transfer_id == 4); + REQUIRE(transfer.metadata.priority == CanardPriorityHigh); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindRequest); + REQUIRE(transfer.metadata.port_id == 0b0000110011); + REQUIRE(transfer.metadata.remote_node_id == 0b0100101); + REQUIRE(transfer.metadata.transfer_id == 4); REQUIRE(transfer.payload_size == 3); REQUIRE(0 == std::memcmp(transfer.payload, "\x01\x02\x03", 3)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 4); // Two SESSIONS and two PAYLOAD BUFFERS. REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == (2 * sizeof(RxSession) + 16 + 20)); - REQUIRE(ins.getInstance().rx_subscriptions[2]->_sessions[0b0100101] != nullptr); + REQUIRE(ins.getRequestSubs().at(0)->sessions[0b0100101] != nullptr); // Response transfer not accepted because the local node has a different node-ID. // There is no dynamic memory available, but it doesn't matter because a rejection does not require allocation. @@ -183,16 +179,15 @@ TEST_CASE("RxBasic0") REQUIRE(subscription != nullptr); REQUIRE(subscription->port_id == 0b0000111100); REQUIRE(transfer.timestamp_usec == 100'000'003); - REQUIRE(transfer.priority == CanardPriorityNominal); - REQUIRE(transfer.transfer_kind == CanardTransferKindResponse); - REQUIRE(transfer.port_id == 0b0000111100); - REQUIRE(transfer.remote_node_id == 0b0011011); - REQUIRE(transfer.transfer_id == 3); + REQUIRE(transfer.metadata.priority == CanardPriorityNominal); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindResponse); + REQUIRE(transfer.metadata.port_id == 0b0000111100); + REQUIRE(transfer.metadata.remote_node_id == 0b0011011); + REQUIRE(transfer.metadata.transfer_id == 3); REQUIRE(transfer.payload_size == 1); REQUIRE(0 == std::memcmp(transfer.payload, "\x05", 1)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 4); REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == (2 * sizeof(RxSession) + 10 + 20)); - REQUIRE(ins.getInstance().rx_subscriptions[1]->next->_sessions[0b0011011] != nullptr); // Bad frames shall be rejected silently. subscription = nullptr; @@ -217,7 +212,7 @@ TEST_CASE("RxAnonymous") using exposed::RxSession; Instance ins; - CanardTransfer transfer{}; + CanardRxTransfer transfer{}; CanardRxSubscription* subscription = nullptr; const auto accept = [&](const std::uint8_t redundant_transport_index, @@ -227,11 +222,10 @@ TEST_CASE("RxAnonymous") static std::vector payload_storage; payload_storage = payload; CanardFrame frame{}; - frame.timestamp_usec = timestamp_usec; frame.extended_can_id = extended_can_id; frame.payload_size = std::size(payload); frame.payload = payload_storage.data(); - return ins.rxAccept(frame, redundant_transport_index, transfer, &subscription); + return ins.rxAccept(timestamp_usec, frame, redundant_transport_index, transfer, &subscription); }; ins.getAllocator().setAllocationCeiling(16); @@ -242,7 +236,9 @@ TEST_CASE("RxAnonymous") REQUIRE(subscription == nullptr); // Create a message subscription. + void* const my_user_reference = &ins; CanardRxSubscription sub_msg{}; + sub_msg.user_reference = my_user_reference; REQUIRE(1 == ins.rxSubscribe(CanardTransferKindMessage, 0b0110011001100, 16, 2'000'000, sub_msg)); // New. // Accepted anonymous message. @@ -253,17 +249,18 @@ TEST_CASE("RxAnonymous") {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0b111'00000})); REQUIRE(subscription != nullptr); REQUIRE(subscription->port_id == 0b0110011001100); + REQUIRE(subscription->user_reference == my_user_reference); REQUIRE(transfer.timestamp_usec == 100'000'001); - REQUIRE(transfer.priority == CanardPriorityImmediate); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 0b0110011001100); - REQUIRE(transfer.remote_node_id == CANARD_NODE_ID_UNSET); - REQUIRE(transfer.transfer_id == 0); + REQUIRE(transfer.metadata.priority == CanardPriorityImmediate); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 0b0110011001100); + REQUIRE(transfer.metadata.remote_node_id == CANARD_NODE_ID_UNSET); + REQUIRE(transfer.metadata.transfer_id == 0); REQUIRE(transfer.payload_size == 16); // Truncated. REQUIRE(0 == std::memcmp(transfer.payload, "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10", 0)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); // The PAYLOAD BUFFER only! No session for anons. REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == 16); - REQUIRE(ensureAllNullptr(ins.getInstance().rx_subscriptions[0]->_sessions)); // No RX states! + REQUIRE(ensureAllNullptr(ins.getMessageSubs().at(0)->sessions)); // No RX states! // Anonymous message not accepted because OOM. The transfer shall remain unmodified by the call, so we re-check it. REQUIRE(-CANARD_ERROR_OUT_OF_MEMORY == @@ -271,16 +268,16 @@ TEST_CASE("RxAnonymous") REQUIRE(subscription != nullptr); REQUIRE(subscription->port_id == 0b0110011001100); REQUIRE(transfer.timestamp_usec == 100'000'001); - REQUIRE(transfer.priority == CanardPriorityImmediate); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 0b0110011001100); - REQUIRE(transfer.remote_node_id == CANARD_NODE_ID_UNSET); - REQUIRE(transfer.transfer_id == 0); + REQUIRE(transfer.metadata.priority == CanardPriorityImmediate); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 0b0110011001100); + REQUIRE(transfer.metadata.remote_node_id == CANARD_NODE_ID_UNSET); + REQUIRE(transfer.metadata.transfer_id == 0); REQUIRE(transfer.payload_size == 16); // Truncated. REQUIRE(0 == std::memcmp(transfer.payload, "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10", 0)); REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); // The PAYLOAD BUFFER only! No session for anons. REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == 16); - REQUIRE(ensureAllNullptr(ins.getInstance().rx_subscriptions[0]->_sessions)); // No RX states! + REQUIRE(ensureAllNullptr(ins.getMessageSubs().at(0)->sessions)); // No RX states! // Release the memory. ins.getAllocator().deallocate(transfer.payload); @@ -293,16 +290,16 @@ TEST_CASE("RxAnonymous") REQUIRE(subscription != nullptr); REQUIRE(subscription->port_id == 0b0110011001100); REQUIRE(transfer.timestamp_usec == 100'000'001); - REQUIRE(transfer.priority == CanardPriorityImmediate); - REQUIRE(transfer.transfer_kind == CanardTransferKindMessage); - REQUIRE(transfer.port_id == 0b0110011001100); - REQUIRE(transfer.remote_node_id == CANARD_NODE_ID_UNSET); - REQUIRE(transfer.transfer_id == 0); + REQUIRE(transfer.metadata.priority == CanardPriorityImmediate); + REQUIRE(transfer.metadata.transfer_kind == CanardTransferKindMessage); + REQUIRE(transfer.metadata.port_id == 0b0110011001100); + REQUIRE(transfer.metadata.remote_node_id == CANARD_NODE_ID_UNSET); + REQUIRE(transfer.metadata.transfer_id == 0); REQUIRE(transfer.payload_size == 6); // NOT truncated. REQUIRE(0 == std::memcmp(transfer.payload, "\x01\x02\x03\x04\x05\x06", 0)); - REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); // The PAYLOAD BUFFER only! No session for anons. - REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == 6); // Smaller allocation. - REQUIRE(ensureAllNullptr(ins.getInstance().rx_subscriptions[0]->_sessions)); // No RX states! + REQUIRE(ins.getAllocator().getNumAllocatedFragments() == 1); // The PAYLOAD BUFFER only! No session for anons. + REQUIRE(ins.getAllocator().getTotalAllocatedAmount() == 6); // Smaller allocation. + REQUIRE(ensureAllNullptr(ins.getMessageSubs().at(0)->sessions)); // No RX states! } TEST_CASE("RxSubscriptionErrors") @@ -327,10 +324,10 @@ TEST_CASE("RxSubscriptionErrors") CanardFrame frame{}; frame.payload_size = 1U; - CanardTransfer transfer{}; - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(&ins.getInstance(), &frame, 0, &transfer)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(nullptr, &frame, 0, &transfer)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(&ins.getInstance(), nullptr, 0, &transfer)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(&ins.getInstance(), &frame, 0, nullptr)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(nullptr, nullptr, 0, nullptr)); + CanardRxTransfer transfer{}; + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(&ins.getInstance(), 0, &frame, 0, &transfer, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(nullptr, 0, &frame, 0, &transfer, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(&ins.getInstance(), 0, nullptr, 0, &transfer, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(&ins.getInstance(), 0, &frame, 0, nullptr, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardRxAccept(nullptr, 0, nullptr, 0, nullptr, nullptr)); } diff --git a/tests/test_public_tx.cpp b/tests/test_public_tx.cpp index 0c162d40..a4da2f63 100644 --- a/tests/test_public_tx.cpp +++ b/tests/test_public_tx.cpp @@ -1,15 +1,17 @@ // This software is distributed under the terms of the MIT License. -// Copyright (c) 2016-2020 UAVCAN Development Team. +// Copyright (c) 2016 UAVCAN Development Team. #include "exposed.hpp" #include "helpers.hpp" +#include "catch.hpp" #include TEST_CASE("TxBasic0") { - using exposed::TxQueueItem; + using exposed::TxItem; helpers::Instance ins; + helpers::TxQueue que(200, CANARD_MTU_CAN_FD); auto& alloc = ins.getAllocator(); @@ -20,343 +22,620 @@ TEST_CASE("TxBasic0") } REQUIRE(CANARD_NODE_ID_UNSET == ins.getNodeID()); - REQUIRE(CANARD_MTU_CAN_FD == ins.getMTU()); - REQUIRE(nullptr == ins.getTxQueueRoot()); - REQUIRE(0 == ins.getTxQueueLength()); + REQUIRE(CANARD_MTU_CAN_FD == que.getMTU()); + REQUIRE(0 == que.getSize()); REQUIRE(0 == alloc.getNumAllocatedFragments()); - alloc.setAllocationCeiling(200); + alloc.setAllocationCeiling(400); - CanardTransfer transfer{}; - transfer.payload = payload.data(); + CanardTransferMetadata meta{}; // Single-frame with padding. - transfer.timestamp_usec = 1'000'000'000'000ULL; - transfer.priority = CanardPriorityNominal; - transfer.transfer_kind = CanardTransferKindMessage; - transfer.port_id = 321; - transfer.remote_node_id = CANARD_NODE_ID_UNSET; - transfer.transfer_id = 21; - transfer.payload_size = 8; - REQUIRE(1 == ins.txPush(transfer)); - REQUIRE(1 == ins.getTxQueueLength()); + meta.priority = CanardPriorityNominal; + meta.transfer_kind = CanardTransferKindMessage; + meta.port_id = 321; + meta.remote_node_id = CANARD_NODE_ID_UNSET; + meta.transfer_id = 21; + REQUIRE(1 == que.push(&ins.getInstance(), 1'000'000'000'000ULL, meta, 8, payload.data())); + REQUIRE(1 == que.getSize()); REQUIRE(1 == alloc.getNumAllocatedFragments()); REQUIRE(10 < alloc.getTotalAllocatedAmount()); - REQUIRE(80 > alloc.getTotalAllocatedAmount()); - REQUIRE(ins.getTxQueueRoot()->frame.timestamp_usec == 1'000'000'000'000ULL); - REQUIRE(ins.getTxQueueRoot()->frame.payload_size == 12); // Three bytes of padding. - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(0) == 0); // Payload start. - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(1) == 1); - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(2) == 2); - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(3) == 3); - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(4) == 4); - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(5) == 5); - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(6) == 6); - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(7) == 7); // Payload end. - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(8) == 0); // Padding. - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(9) == 0); // Padding. - REQUIRE(ins.getTxQueueRoot()->getPayloadByte(10) == 0); // Padding. - REQUIRE(ins.getTxQueueRoot()->isStartOfTransfer()); // Tail byte at the end. - REQUIRE(ins.getTxQueueRoot()->isEndOfTransfer()); - REQUIRE(ins.getTxQueueRoot()->isToggleBitSet()); + REQUIRE(160 > alloc.getTotalAllocatedAmount()); + REQUIRE(que.peek()->tx_deadline_usec == 1'000'000'000'000ULL); + REQUIRE(que.peek()->frame.payload_size == 12); // Three bytes of padding. + REQUIRE(que.peek()->getPayloadByte(0) == 0); // Payload start. + REQUIRE(que.peek()->getPayloadByte(1) == 1); + REQUIRE(que.peek()->getPayloadByte(2) == 2); + REQUIRE(que.peek()->getPayloadByte(3) == 3); + REQUIRE(que.peek()->getPayloadByte(4) == 4); + REQUIRE(que.peek()->getPayloadByte(5) == 5); + REQUIRE(que.peek()->getPayloadByte(6) == 6); + REQUIRE(que.peek()->getPayloadByte(7) == 7); // Payload end. + REQUIRE(que.peek()->getPayloadByte(8) == 0); // Padding. + REQUIRE(que.peek()->getPayloadByte(9) == 0); // Padding. + REQUIRE(que.peek()->getPayloadByte(10) == 0); // Padding. + REQUIRE(que.peek()->isStartOfTransfer()); // Tail byte at the end. + REQUIRE(que.peek()->isEndOfTransfer()); + REQUIRE(que.peek()->isToggleBitSet()); // Multi-frame. Priority low, inserted at the end of the TX queue. - transfer.timestamp_usec = 1'000'000'000'100ULL; - transfer.priority = CanardPriorityLow; - transfer.transfer_id = 22; - transfer.payload_size = 8; - ins.setMTU(CANARD_MTU_CAN_CLASSIC); + meta.priority = CanardPriorityLow; + meta.transfer_id = 22; + que.setMTU(CANARD_MTU_CAN_CLASSIC); ins.setNodeID(42); - REQUIRE(2 == ins.txPush(transfer)); // 8 bytes --> 2 frames - REQUIRE(3 == ins.getTxQueueLength()); + REQUIRE(2 == que.push(&ins.getInstance(), 1'000'000'000'100ULL, meta, 8, payload.data())); // 8 bytes --> 2 frames + REQUIRE(3 == que.getSize()); REQUIRE(3 == alloc.getNumAllocatedFragments()); REQUIRE(20 < alloc.getTotalAllocatedAmount()); - REQUIRE(200 > alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); // Check the TX queue. { - const auto* q = ins.getTxQueueRoot(); - REQUIRE(q != nullptr); - REQUIRE(q->frame.timestamp_usec == 1'000'000'000'000ULL); - REQUIRE(q->frame.payload_size == 12); - REQUIRE(q->isStartOfTransfer()); - REQUIRE(q->isEndOfTransfer()); - REQUIRE(q->isToggleBitSet()); - q = q->next; - REQUIRE(q != nullptr); - REQUIRE(q->frame.timestamp_usec == 1'000'000'000'100ULL); - REQUIRE(q->frame.payload_size == 8); - REQUIRE(q->isStartOfTransfer()); - REQUIRE(!q->isEndOfTransfer()); - REQUIRE(q->isToggleBitSet()); - q = q->next; - REQUIRE(q != nullptr); - REQUIRE(q->frame.timestamp_usec == 1'000'000'000'100ULL); - REQUIRE(q->frame.payload_size == 4); // One leftover, two CRC, one tail. - REQUIRE(!q->isStartOfTransfer()); - REQUIRE(q->isEndOfTransfer()); - REQUIRE(!q->isToggleBitSet()); - q = q->next; - REQUIRE(q == nullptr); + const auto q = que.linearize(); + REQUIRE(3 == q.size()); + REQUIRE(q.at(0)->tx_deadline_usec == 1'000'000'000'000ULL); + REQUIRE(q.at(0)->frame.payload_size == 12); + REQUIRE(q.at(0)->isStartOfTransfer()); + REQUIRE(q.at(0)->isEndOfTransfer()); + REQUIRE(q.at(0)->isToggleBitSet()); + // + REQUIRE(q.at(1)->tx_deadline_usec == 1'000'000'000'100ULL); + REQUIRE(q.at(1)->frame.payload_size == 8); + REQUIRE(q.at(1)->isStartOfTransfer()); + REQUIRE(!q.at(1)->isEndOfTransfer()); + REQUIRE(q.at(1)->isToggleBitSet()); + // + REQUIRE(q.at(2)->tx_deadline_usec == 1'000'000'000'100ULL); + REQUIRE(q.at(2)->frame.payload_size == 4); // One leftover, two CRC, one tail. + REQUIRE(!q.at(2)->isStartOfTransfer()); + REQUIRE(q.at(2)->isEndOfTransfer()); + REQUIRE(!q.at(2)->isToggleBitSet()); } // Single-frame, OOM. alloc.setAllocationCeiling(alloc.getTotalAllocatedAmount()); // Seal up the heap at this level. - transfer.timestamp_usec = 1'000'000'000'200ULL; - transfer.priority = CanardPriorityLow; - transfer.transfer_id = 23; - transfer.payload_size = 1; - REQUIRE(-CANARD_ERROR_OUT_OF_MEMORY == ins.txPush(transfer)); - REQUIRE(3 == ins.getTxQueueLength()); + meta.priority = CanardPriorityLow; + meta.transfer_id = 23; + REQUIRE(-CANARD_ERROR_OUT_OF_MEMORY == que.push(&ins.getInstance(), 1'000'000'000'200ULL, meta, 1, payload.data())); + REQUIRE(3 == que.getSize()); REQUIRE(3 == alloc.getNumAllocatedFragments()); // Multi-frame, first frame added successfully, then OOM. The entire transaction rejected. - alloc.setAllocationCeiling(alloc.getTotalAllocatedAmount() + sizeof(TxQueueItem) + 10U); - transfer.timestamp_usec = 1'000'000'000'300ULL; - transfer.priority = CanardPriorityHigh; - transfer.transfer_id = 24; - transfer.payload_size = 100; - REQUIRE(-CANARD_ERROR_OUT_OF_MEMORY == ins.txPush(transfer)); - REQUIRE(3 == ins.getTxQueueLength()); + alloc.setAllocationCeiling(alloc.getTotalAllocatedAmount() + sizeof(TxItem) + 10U); + meta.priority = CanardPriorityHigh; + meta.transfer_id = 24; + REQUIRE(-CANARD_ERROR_OUT_OF_MEMORY == + que.push(&ins.getInstance(), 1'000'000'000'300ULL, meta, 100, payload.data())); + REQUIRE(3 == que.getSize()); REQUIRE(3 == alloc.getNumAllocatedFragments()); REQUIRE(20 < alloc.getTotalAllocatedAmount()); - REQUIRE(200 > alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); // Pop the queue. // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(8))).value) - constexpr std::uint16_t CRC8 = 0x178DU; - const CanardFrame* frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 12); - REQUIRE(0 == std::memcmp(frame->payload, payload.data(), 8)); - REQUIRE(0 == reinterpret_cast(frame->payload)[8]); // Padding. - REQUIRE(0 == reinterpret_cast(frame->payload)[9]); // Padding. - REQUIRE(0 == reinterpret_cast(frame->payload)[10]); // Padding. - REQUIRE((0b11100000U | 21U) == reinterpret_cast(frame->payload)[11]); - REQUIRE(frame->timestamp_usec == 1'000'000'000'000ULL); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); // Make sure we get the same frame again. - REQUIRE(frame->payload_size == 12); - REQUIRE(0 == std::memcmp(frame->payload, payload.data(), 8)); - REQUIRE((0b11100000U | 21U) == reinterpret_cast(frame->payload)[11]); - REQUIRE(frame->timestamp_usec == 1'000'000'000'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(2 == ins.getTxQueueLength()); + constexpr std::uint16_t CRC8 = 0x178DU; + const CanardTxQueueItem* ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 12); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 8)); + REQUIRE(0 == reinterpret_cast(ti->frame.payload)[8]); // Padding. + REQUIRE(0 == reinterpret_cast(ti->frame.payload)[9]); // Padding. + REQUIRE(0 == reinterpret_cast(ti->frame.payload)[10]); // Padding. + REQUIRE((0b11100000U | 21U) == reinterpret_cast(ti->frame.payload)[11]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'000ULL); + ti = que.peek(); + REQUIRE(nullptr != ti); // Make sure we get the same frame again. + REQUIRE(ti->frame.payload_size == 12); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 8)); + REQUIRE((0b11100000U | 21U) == reinterpret_cast(ti->frame.payload)[11]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(2 == que.getSize()); REQUIRE(2 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 8); - REQUIRE(0 == std::memcmp(frame->payload, payload.data(), 7)); - REQUIRE((0b10100000U | 22U) == reinterpret_cast(frame->payload)[7]); - REQUIRE(frame->timestamp_usec == 1'000'000'000'100ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(1 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 8); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 7)); + REQUIRE((0b10100000U | 22U) == reinterpret_cast(ti->frame.payload)[7]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'100ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); REQUIRE(1 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 4); - REQUIRE(0 == std::memcmp(frame->payload, payload.data() + 7U, 1)); - REQUIRE((CRC8 >> 8U) == reinterpret_cast(frame->payload)[1]); - REQUIRE((CRC8 & 0xFFU) == reinterpret_cast(frame->payload)[2]); - REQUIRE((0b01000000U | 22U) == reinterpret_cast(frame->payload)[3]); - REQUIRE(frame->timestamp_usec == 1'000'000'000'100ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(0 == ins.getTxQueueLength()); - REQUIRE(nullptr == ins.getTxQueueRoot()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 4); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 7U, 1)); + REQUIRE((CRC8 >> 8U) == reinterpret_cast(ti->frame.payload)[1]); + REQUIRE((CRC8 & 0xFFU) == reinterpret_cast(ti->frame.payload)[2]); + REQUIRE((0b01000000U | 22U) == reinterpret_cast(ti->frame.payload)[3]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'100ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); REQUIRE(0 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr == frame); - ins.txPop(); // Invocation when empty has no effect. - ins.txPop(); - REQUIRE(0 == ins.getTxQueueLength()); - REQUIRE(nullptr == ins.getTxQueueRoot()); + ti = que.peek(); + REQUIRE(nullptr == ti); + REQUIRE(nullptr == que.pop(nullptr)); + REQUIRE(0 == que.getSize()); REQUIRE(0 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr == frame); + ti = que.peek(); + REQUIRE(nullptr == ti); alloc.setAllocationCeiling(1000); // Multi-frame, success. CRC split over the frame boundary. // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(61))).value) constexpr std::uint16_t CRC61 = 0x554EU; - ins.setMTU(32); - transfer.timestamp_usec = 1'000'000'001'000ULL; - transfer.priority = CanardPriorityFast; - transfer.transfer_id = 25; - transfer.payload_size = 31 + 30; // CRC takes 2 bytes at the end; 3 frames: (31+1) + (30+1+1) + (1+1) - REQUIRE(3 == ins.txPush(transfer)); - REQUIRE(3 == ins.getTxQueueLength()); + que.setMTU(32); + meta.priority = CanardPriorityFast; + meta.transfer_id = 25; + // CRC takes 2 bytes at the end; 3 frames: (31+1) + (30+1+1) + (1+1) + REQUIRE(3 == que.push(&ins.getInstance(), 1'000'000'001'000ULL, meta, 31 + 30, payload.data())); + REQUIRE(3 == que.getSize()); REQUIRE(3 == alloc.getNumAllocatedFragments()); REQUIRE(40 < alloc.getTotalAllocatedAmount()); - REQUIRE(220 > alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); // Read the generated frames. - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 32); - REQUIRE(0 == std::memcmp(frame->payload, payload.data(), 31)); - REQUIRE((0b10100000U | 25U) == reinterpret_cast(frame->payload)[31]); - REQUIRE(frame->timestamp_usec == 1'000'000'001'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(2 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 31)); + REQUIRE((0b10100000U | 25U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'001'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(2 == que.getSize()); REQUIRE(2 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 32); - REQUIRE(0 == std::memcmp(frame->payload, payload.data() + 31U, 30)); - REQUIRE((CRC61 >> 8U) == reinterpret_cast(frame->payload)[30]); - REQUIRE((0b00000000U | 25U) == reinterpret_cast(frame->payload)[31]); - REQUIRE(frame->timestamp_usec == 1'000'000'001'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(1 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 31U, 30)); + REQUIRE((CRC61 >> 8U) == reinterpret_cast(ti->frame.payload)[30]); + REQUIRE((0b00000000U | 25U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'001'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); REQUIRE(1 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 2); // The last byte of CRC plus the tail byte. - REQUIRE((CRC61 & 0xFFU) == reinterpret_cast(frame->payload)[0]); - REQUIRE((0b01100000U | 25U) == reinterpret_cast(frame->payload)[1]); - REQUIRE(frame->timestamp_usec == 1'000'000'001'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(0 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 2); // The last byte of CRC plus the tail byte. + REQUIRE((CRC61 & 0xFFU) == reinterpret_cast(ti->frame.payload)[0]); + REQUIRE((0b01100000U | 25U) == reinterpret_cast(ti->frame.payload)[1]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'001'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); REQUIRE(0 == alloc.getNumAllocatedFragments()); // Multi-frame, success. CRC is in the last frame-> // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(62))).value) constexpr std::uint16_t CRC62 = 0xA3AEU; - ins.setMTU(32); - transfer.timestamp_usec = 1'000'000'002'000ULL; - transfer.priority = CanardPrioritySlow; - transfer.transfer_id = 26; - transfer.payload_size = 31 + 31; // CRC takes 2 bytes at the end; 3 frames: (31+1) + (31+1) + (2+1) - REQUIRE(3 == ins.txPush(transfer)); - REQUIRE(3 == ins.getTxQueueLength()); + que.setMTU(32); + meta.priority = CanardPrioritySlow; + meta.transfer_id = 26; + // CRC takes 2 bytes at the end; 3 frames: (31+1) + (31+1) + (2+1) + REQUIRE(3 == que.push(&ins.getInstance(), 1'000'000'002'000ULL, meta, 31 + 31, payload.data())); + REQUIRE(3 == que.getSize()); REQUIRE(3 == alloc.getNumAllocatedFragments()); REQUIRE(40 < alloc.getTotalAllocatedAmount()); - REQUIRE(220 > alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); // Read the generated frames. - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 32); - REQUIRE(0 == std::memcmp(frame->payload, payload.data(), 31)); - REQUIRE((0b10100000U | 26U) == reinterpret_cast(frame->payload)[31]); - REQUIRE(frame->timestamp_usec == 1'000'000'002'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(2 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 31)); + REQUIRE((0b10100000U | 26U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'002'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(2 == que.getSize()); REQUIRE(2 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 32); - REQUIRE(0 == std::memcmp(frame->payload, payload.data() + 31U, 31)); - REQUIRE((0b00000000U | 26U) == reinterpret_cast(frame->payload)[31]); - REQUIRE(frame->timestamp_usec == 1'000'000'002'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(1 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 31U, 31)); + REQUIRE((0b00000000U | 26U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'002'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); REQUIRE(1 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 3); // The CRC plus the tail byte. - REQUIRE((CRC62 >> 8U) == reinterpret_cast(frame->payload)[0]); - REQUIRE((CRC62 & 0xFFU) == reinterpret_cast(frame->payload)[1]); - REQUIRE((0b01100000U | 26U) == reinterpret_cast(frame->payload)[2]); - REQUIRE(frame->timestamp_usec == 1'000'000'002'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(0 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 3); // The CRC plus the tail byte. + REQUIRE((CRC62 >> 8U) == reinterpret_cast(ti->frame.payload)[0]); + REQUIRE((CRC62 & 0xFFU) == reinterpret_cast(ti->frame.payload)[1]); + REQUIRE((0b01100000U | 26U) == reinterpret_cast(ti->frame.payload)[2]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'002'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); REQUIRE(0 == alloc.getNumAllocatedFragments()); // Multi-frame with padding. // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(112)) + [0] * 12).value) constexpr std::uint16_t CRC112Padding12 = 0xE7A5U; - ins.setMTU(64); - transfer.timestamp_usec = 1'000'000'003'000ULL; - transfer.priority = CanardPriorityImmediate; - transfer.transfer_id = 27; - transfer.payload_size = 112; // 63 + 63 - 2 = 124 bytes; 124 - 112 = 12 bytes of padding. - REQUIRE(2 == ins.txPush(transfer)); - REQUIRE(2 == ins.getTxQueueLength()); + que.setMTU(64); + meta.priority = CanardPriorityImmediate; + meta.transfer_id = 27; + // 63 + 63 - 2 = 124 bytes; 124 - 112 = 12 bytes of padding. + REQUIRE(2 == que.push(&ins.getInstance(), 1'000'000'003'000ULL, meta, 112, payload.data())); + REQUIRE(2 == que.getSize()); REQUIRE(2 == alloc.getNumAllocatedFragments()); // Read the generated frames. - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 64); - REQUIRE(0 == std::memcmp(frame->payload, payload.data(), 63)); - REQUIRE((0b10100000U | 27U) == reinterpret_cast(frame->payload)[63]); - REQUIRE(frame->timestamp_usec == 1'000'000'003'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(1 == ins.getTxQueueLength()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 64); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 63)); + REQUIRE((0b10100000U | 27U) == reinterpret_cast(ti->frame.payload)[63]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'003'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); REQUIRE(1 == alloc.getNumAllocatedFragments()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 64); - REQUIRE(0 == std::memcmp(frame->payload, payload.data() + 63U, 49)); - REQUIRE(std::all_of(reinterpret_cast(frame->payload) + 49, // Check padding. - reinterpret_cast(frame->payload) + 61, + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 64); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 63U, 49)); + REQUIRE(std::all_of(reinterpret_cast(ti->frame.payload) + 49, // Check padding. + reinterpret_cast(ti->frame.payload) + 61, [](auto x) { return x == 0U; })); - REQUIRE((CRC112Padding12 >> 8U) == reinterpret_cast(frame->payload)[61]); // CRC - REQUIRE((CRC112Padding12 & 0xFFU) == reinterpret_cast(frame->payload)[62]); // CRC - REQUIRE((0b01000000U | 27U) == reinterpret_cast(frame->payload)[63]); // Tail - REQUIRE(frame->timestamp_usec == 1'000'000'003'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(0 == ins.getTxQueueLength()); + REQUIRE((CRC112Padding12 >> 8U) == reinterpret_cast(ti->frame.payload)[61]); // CRC + REQUIRE((CRC112Padding12 & 0xFFU) == reinterpret_cast(ti->frame.payload)[62]); // CRC + REQUIRE((0b01000000U | 27U) == reinterpret_cast(ti->frame.payload)[63]); // Tail + REQUIRE(ti->tx_deadline_usec == 1'000'000'003'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); REQUIRE(0 == alloc.getNumAllocatedFragments()); // Single-frame empty. - transfer.timestamp_usec = 1'000'000'004'000ULL; - transfer.transfer_id = 28; - transfer.payload_size = 0; - transfer.payload = nullptr; // Null is OK if size is 0. - REQUIRE(1 == ins.txPush(transfer)); - REQUIRE(1 == ins.getTxQueueLength()); + meta.transfer_id = 28; + REQUIRE(1 == que.push(&ins.getInstance(), 1'000'000'004'000ULL, meta, 0, nullptr)); + REQUIRE(1 == que.getSize()); REQUIRE(1 == alloc.getNumAllocatedFragments()); - REQUIRE(60 > alloc.getTotalAllocatedAmount()); - REQUIRE(ins.getTxQueueRoot()->frame.timestamp_usec == 1'000'000'004'000ULL); - REQUIRE(ins.getTxQueueRoot()->frame.payload_size == 1); - REQUIRE(ins.getTxQueueRoot()->isStartOfTransfer()); - REQUIRE(ins.getTxQueueRoot()->isEndOfTransfer()); - REQUIRE(ins.getTxQueueRoot()->isToggleBitSet()); - frame = ins.txPeek(); - REQUIRE(nullptr != frame); - REQUIRE(frame->payload_size == 1); - REQUIRE((0b11100000U | 28U) == reinterpret_cast(frame->payload)[0]); - REQUIRE(frame->timestamp_usec == 1'000'000'004'000ULL); - ins.txPop(); - ins.getAllocator().deallocate(frame); - REQUIRE(0 == ins.getTxQueueLength()); + REQUIRE(120 > alloc.getTotalAllocatedAmount()); + REQUIRE(que.peek()->tx_deadline_usec == 1'000'000'004'000ULL); + REQUIRE(que.peek()->frame.payload_size == 1); + REQUIRE(que.peek()->isStartOfTransfer()); + REQUIRE(que.peek()->isEndOfTransfer()); + REQUIRE(que.peek()->isToggleBitSet()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 1); + REQUIRE((0b11100000U | 28U) == reinterpret_cast(ti->frame.payload)[0]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'004'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); REQUIRE(0 == alloc.getNumAllocatedFragments()); // Nothing left to peek at. - frame = ins.txPeek(); - REQUIRE(nullptr == frame); + ti = que.peek(); + REQUIRE(nullptr == ti); // Invalid transfer. - transfer.payload = payload.data(); - transfer.timestamp_usec = 1'000'000'005'000ULL; - transfer.transfer_kind = CanardTransferKindMessage; - transfer.remote_node_id = 42; - transfer.transfer_id = 123; - transfer.payload_size = 8; - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == ins.txPush(transfer)); - frame = ins.txPeek(); - REQUIRE(nullptr == frame); + meta.transfer_kind = CanardTransferKindMessage; + meta.remote_node_id = 42; + meta.transfer_id = 123; + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == + que.push(&ins.getInstance(), 1'000'000'005'000ULL, meta, 8, payload.data())); + ti = que.peek(); + REQUIRE(nullptr == ti); // Error handling. - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, nullptr)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, &transfer)); - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(&ins.getInstance(), nullptr)); - transfer.payload_size = 1; - transfer.payload = nullptr; - REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == ins.txPush(transfer)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, nullptr, 0, nullptr, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, nullptr, 0, &meta, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, &ins.getInstance(), 0, &meta, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == + canardTxPush(&que.getInstance(), &ins.getInstance(), 0, nullptr, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == que.push(&ins.getInstance(), 1'000'000'006'000ULL, meta, 1, nullptr)); REQUIRE(nullptr == canardTxPeek(nullptr)); + REQUIRE(nullptr == canardTxPop(nullptr, nullptr)); // No effect. + REQUIRE(nullptr == canardTxPop(&que.getInstance(), nullptr)); // No effect. +} + +TEST_CASE("TxBasic1") +{ + helpers::Instance ins; + helpers::TxQueue que(3, CANARD_MTU_CAN_FD); // Limit capacity at 3 frames. + + auto& alloc = ins.getAllocator(); + + std::array payload{}; + for (std::size_t i = 0; i < std::size(payload); i++) + { + payload.at(i) = static_cast(i & 0xFFU); + } + + REQUIRE(CANARD_NODE_ID_UNSET == ins.getNodeID()); + REQUIRE(CANARD_MTU_CAN_FD == que.getMTU()); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == alloc.getNumAllocatedFragments()); + + CanardTransferMetadata meta{}; + + // Single-frame with padding. + meta.priority = CanardPriorityNominal; + meta.transfer_kind = CanardTransferKindMessage; + meta.port_id = 321; + meta.remote_node_id = CANARD_NODE_ID_UNSET; + meta.transfer_id = 21; + REQUIRE(1 == que.push(&ins.getInstance(), 1'000'000'000'000ULL, meta, 8, payload.data())); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == alloc.getNumAllocatedFragments()); + REQUIRE(10 < alloc.getTotalAllocatedAmount()); + REQUIRE(160 > alloc.getTotalAllocatedAmount()); + REQUIRE(que.peek()->tx_deadline_usec == 1'000'000'000'000ULL); + REQUIRE(que.peek()->frame.payload_size == 12); // Three bytes of padding. + REQUIRE(que.peek()->getPayloadByte(0) == 0); // Payload start. + REQUIRE(que.peek()->getPayloadByte(1) == 1); + REQUIRE(que.peek()->getPayloadByte(2) == 2); + REQUIRE(que.peek()->getPayloadByte(3) == 3); + REQUIRE(que.peek()->getPayloadByte(4) == 4); + REQUIRE(que.peek()->getPayloadByte(5) == 5); + REQUIRE(que.peek()->getPayloadByte(6) == 6); + REQUIRE(que.peek()->getPayloadByte(7) == 7); // Payload end. + REQUIRE(que.peek()->getPayloadByte(8) == 0); // Padding. + REQUIRE(que.peek()->getPayloadByte(9) == 0); // Padding. + REQUIRE(que.peek()->getPayloadByte(10) == 0); // Padding. + REQUIRE(que.peek()->isStartOfTransfer()); // Tail byte at the end. + REQUIRE(que.peek()->isEndOfTransfer()); + REQUIRE(que.peek()->isToggleBitSet()); + + // Multi-frame. Priority low, inserted at the end of the TX queue. Two frames exhaust the capacity of the queue. + meta.priority = CanardPriorityLow; + meta.transfer_id = 22; + que.setMTU(CANARD_MTU_CAN_CLASSIC); + ins.setNodeID(42); + REQUIRE(2 == que.push(&ins.getInstance(), 1'000'000'000'100ULL, meta, 8, payload.data())); // 8 bytes --> 2 frames + REQUIRE(3 == que.getSize()); + REQUIRE(3 == alloc.getNumAllocatedFragments()); + REQUIRE(20 < alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); + + // Check the TX queue. + { + const auto q = que.linearize(); + REQUIRE(3 == q.size()); + REQUIRE(q.at(0)->tx_deadline_usec == 1'000'000'000'000ULL); + REQUIRE(q.at(0)->frame.payload_size == 12); + REQUIRE(q.at(0)->isStartOfTransfer()); + REQUIRE(q.at(0)->isEndOfTransfer()); + REQUIRE(q.at(0)->isToggleBitSet()); + // + REQUIRE(q.at(1)->tx_deadline_usec == 1'000'000'000'100ULL); + REQUIRE(q.at(1)->frame.payload_size == 8); + REQUIRE(q.at(1)->isStartOfTransfer()); + REQUIRE(!q.at(1)->isEndOfTransfer()); + REQUIRE(q.at(1)->isToggleBitSet()); + // + REQUIRE(q.at(2)->tx_deadline_usec == 1'000'000'000'100ULL); + REQUIRE(q.at(2)->frame.payload_size == 4); // One leftover, two CRC, one tail. + REQUIRE(!q.at(2)->isStartOfTransfer()); + REQUIRE(q.at(2)->isEndOfTransfer()); + REQUIRE(!q.at(2)->isToggleBitSet()); + } + + // Single-frame, OOM reported but the heap is not exhausted (because queue is filled up). + meta.priority = CanardPriorityLow; + meta.transfer_id = 23; + REQUIRE(-CANARD_ERROR_OUT_OF_MEMORY == que.push(&ins.getInstance(), 1'000'000'000'200ULL, meta, 1, payload.data())); + REQUIRE(3 == que.getSize()); + REQUIRE(3 == alloc.getNumAllocatedFragments()); + + // Multi-frame, no frames are added -- bail early always. + meta.priority = CanardPriorityHigh; + meta.transfer_id = 24; + REQUIRE(-CANARD_ERROR_OUT_OF_MEMORY == + que.push(&ins.getInstance(), 1'000'000'000'300ULL, meta, 100, payload.data())); + REQUIRE(3 == que.getSize()); + REQUIRE(3 == alloc.getNumAllocatedFragments()); + REQUIRE(20 < alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); + + // Pop the queue. + // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(8))).value) + constexpr std::uint16_t CRC8 = 0x178DU; + const CanardTxQueueItem* ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 12); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 8)); + REQUIRE(0 == reinterpret_cast(ti->frame.payload)[8]); // Padding. + REQUIRE(0 == reinterpret_cast(ti->frame.payload)[9]); // Padding. + REQUIRE(0 == reinterpret_cast(ti->frame.payload)[10]); // Padding. + REQUIRE((0b11100000U | 21U) == reinterpret_cast(ti->frame.payload)[11]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'000ULL); + ti = que.peek(); + REQUIRE(nullptr != ti); // Make sure we get the same frame again. + REQUIRE(ti->frame.payload_size == 12); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 8)); + REQUIRE((0b11100000U | 21U) == reinterpret_cast(ti->frame.payload)[11]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 8); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 7)); + REQUIRE((0b10100000U | 22U) == reinterpret_cast(ti->frame.payload)[7]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'100ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 4); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 7U, 1)); + REQUIRE((CRC8 >> 8U) == reinterpret_cast(ti->frame.payload)[1]); + REQUIRE((CRC8 & 0xFFU) == reinterpret_cast(ti->frame.payload)[2]); + REQUIRE((0b01000000U | 22U) == reinterpret_cast(ti->frame.payload)[3]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'000'100ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr == ti); + REQUIRE(nullptr == que.pop(ti)); // Invocation when empty has no effect. + REQUIRE(0 == que.getSize()); + REQUIRE(0 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr == ti); - canardTxPop(&ins.getInstance()); // No effect. + // Multi-frame, success. CRC split over the frame boundary. + // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(61))).value) + constexpr std::uint16_t CRC61 = 0x554EU; + que.setMTU(32); + meta.priority = CanardPriorityFast; + meta.transfer_id = 25; + // CRC takes 2 bytes at the end; 3 frames: (31+1) + (30+1+1) + (1+1) + REQUIRE(3 == que.push(&ins.getInstance(), 1'000'000'001'000ULL, meta, 31 + 30, payload.data())); + REQUIRE(3 == que.getSize()); + REQUIRE(3 == alloc.getNumAllocatedFragments()); + REQUIRE(40 < alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); + // Read the generated frames. + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 31)); + REQUIRE((0b10100000U | 25U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'001'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 31U, 30)); + REQUIRE((CRC61 >> 8U) == reinterpret_cast(ti->frame.payload)[30]); + REQUIRE((0b00000000U | 25U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'001'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 2); // The last byte of CRC plus the tail byte. + REQUIRE((CRC61 & 0xFFU) == reinterpret_cast(ti->frame.payload)[0]); + REQUIRE((0b01100000U | 25U) == reinterpret_cast(ti->frame.payload)[1]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'001'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == alloc.getNumAllocatedFragments()); + + // Multi-frame, success. CRC is in the last frame-> + // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(62))).value) + constexpr std::uint16_t CRC62 = 0xA3AEU; + que.setMTU(32); + meta.priority = CanardPrioritySlow; + meta.transfer_id = 26; + // CRC takes 2 bytes at the end; 3 frames: (31+1) + (31+1) + (2+1) + REQUIRE(3 == que.push(&ins.getInstance(), 1'000'000'002'000ULL, meta, 31 + 31, payload.data())); + REQUIRE(3 == que.getSize()); + REQUIRE(3 == alloc.getNumAllocatedFragments()); + REQUIRE(40 < alloc.getTotalAllocatedAmount()); + REQUIRE(400 > alloc.getTotalAllocatedAmount()); + // Read the generated frames. + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 31)); + REQUIRE((0b10100000U | 26U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'002'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 32); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 31U, 31)); + REQUIRE((0b00000000U | 26U) == reinterpret_cast(ti->frame.payload)[31]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'002'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 3); // The CRC plus the tail byte. + REQUIRE((CRC62 >> 8U) == reinterpret_cast(ti->frame.payload)[0]); + REQUIRE((CRC62 & 0xFFU) == reinterpret_cast(ti->frame.payload)[1]); + REQUIRE((0b01100000U | 26U) == reinterpret_cast(ti->frame.payload)[2]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'002'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == alloc.getNumAllocatedFragments()); + + // Multi-frame with padding. + // hex(pyuavcan.transport.commons.crc.CRC16CCITT.new(list(range(112)) + [0] * 12).value) + constexpr std::uint16_t CRC112Padding12 = 0xE7A5U; + que.setMTU(64); + meta.priority = CanardPriorityImmediate; + meta.transfer_id = 27; + // 63 + 63 - 2 = 124 bytes; 124 - 112 = 12 bytes of padding. + REQUIRE(2 == que.push(&ins.getInstance(), 1'000'000'003'000ULL, meta, 112, payload.data())); + REQUIRE(2 == que.getSize()); + REQUIRE(2 == alloc.getNumAllocatedFragments()); + // Read the generated frames. + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 64); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data(), 63)); + REQUIRE((0b10100000U | 27U) == reinterpret_cast(ti->frame.payload)[63]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'003'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == alloc.getNumAllocatedFragments()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 64); + REQUIRE(0 == std::memcmp(ti->frame.payload, payload.data() + 63U, 49)); + REQUIRE(std::all_of(reinterpret_cast(ti->frame.payload) + 49, // Check padding. + reinterpret_cast(ti->frame.payload) + 61, + [](auto x) { return x == 0U; })); + REQUIRE((CRC112Padding12 >> 8U) == reinterpret_cast(ti->frame.payload)[61]); // CRC + REQUIRE((CRC112Padding12 & 0xFFU) == reinterpret_cast(ti->frame.payload)[62]); // CRC + REQUIRE((0b01000000U | 27U) == reinterpret_cast(ti->frame.payload)[63]); // Tail + REQUIRE(ti->tx_deadline_usec == 1'000'000'003'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == alloc.getNumAllocatedFragments()); + + // Single-frame empty. + meta.transfer_id = 28; + REQUIRE(1 == que.push(&ins.getInstance(), 1'000'000'004'000ULL, meta, 0, nullptr)); + REQUIRE(1 == que.getSize()); + REQUIRE(1 == alloc.getNumAllocatedFragments()); + REQUIRE(120 > alloc.getTotalAllocatedAmount()); + REQUIRE(que.peek()->tx_deadline_usec == 1'000'000'004'000ULL); + REQUIRE(que.peek()->frame.payload_size == 1); + REQUIRE(que.peek()->isStartOfTransfer()); + REQUIRE(que.peek()->isEndOfTransfer()); + REQUIRE(que.peek()->isToggleBitSet()); + ti = que.peek(); + REQUIRE(nullptr != ti); + REQUIRE(ti->frame.payload_size == 1); + REQUIRE((0b11100000U | 28U) == reinterpret_cast(ti->frame.payload)[0]); + REQUIRE(ti->tx_deadline_usec == 1'000'000'004'000ULL); + ins.getAllocator().deallocate(que.pop(ti)); + REQUIRE(0 == que.getSize()); + REQUIRE(0 == alloc.getNumAllocatedFragments()); + + // Nothing left to peek at. + ti = que.peek(); + REQUIRE(nullptr == ti); + + // Invalid transfer. + meta.transfer_kind = CanardTransferKindMessage; + meta.remote_node_id = 42; + meta.transfer_id = 123; + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == + que.push(&ins.getInstance(), 1'000'000'005'000ULL, meta, 8, payload.data())); + ti = que.peek(); + REQUIRE(nullptr == ti); + + // Error handling. + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, nullptr, 0, nullptr, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, nullptr, 0, &meta, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == canardTxPush(nullptr, &ins.getInstance(), 0, &meta, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == + canardTxPush(&que.getInstance(), &ins.getInstance(), 0, nullptr, 0, nullptr)); + REQUIRE(-CANARD_ERROR_INVALID_ARGUMENT == que.push(&ins.getInstance(), 1'000'000'006'000ULL, meta, 1, nullptr)); + + REQUIRE(nullptr == canardTxPeek(nullptr)); + REQUIRE(nullptr == canardTxPop(nullptr, nullptr)); // No effect. + REQUIRE(nullptr == canardTxPop(&que.getInstance(), nullptr)); // No effect. } diff --git a/tests/test_self.cpp b/tests/test_self.cpp index a79f9bde..6da649fd 100644 --- a/tests/test_self.cpp +++ b/tests/test_self.cpp @@ -3,6 +3,7 @@ #include "exposed.hpp" #include "helpers.hpp" +#include "catch.hpp" TEST_CASE("TestAllocator") { diff --git a/tools/Dockerfile b/tools/Dockerfile new file mode 100644 index 00000000..3f710630 --- /dev/null +++ b/tools/Dockerfile @@ -0,0 +1,28 @@ +# Development environment for libcanard, based on Ubuntu 20.04 Focal. +# +# This software is distributed under the terms of the MIT License. +# Copyright (c) 2021 UAVCAN Consortium. +# Author: Kalyan Sriram + +FROM ubuntu:focal + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update && apt-get -y upgrade +RUN apt-get -y --no-install-recommends install \ + build-essential cmake gcc-multilib g++-multilib \ + clang-tidy-13 clang-format-13 \ + gcc-avr avr-libc \ + sudo curl git ca-certificates + +# borrowed from MAVSDK https://github.com/mavlink/MAVSDK/blob/main/docker/Dockerfile-Ubuntu-20.04 +RUN curl -L https://github.com/ncopa/su-exec/archive/dddd1567b7c76365e1e0aac561287975020a8fad.tar.gz | tar xvz && \ + cd su-exec-* && make && mv su-exec /usr/local/bin && cd .. && rm -rf su-exec-* + +RUN useradd --shell /bin/bash -u 1001 -c "" -m user + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +WORKDIR "/home/user/libcanard" diff --git a/tools/entrypoint.sh b/tools/entrypoint.sh new file mode 100755 index 00000000..ab254952 --- /dev/null +++ b/tools/entrypoint.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Utility to use local user, taken from: +# https://github.com/mavlink/MAVSDK/blob/main/docker/entrypoint.sh + +# Use LOCAL_USER_ID if passed in at runtime. + +if [ -n "${LOCAL_USER_ID}" ]; then + echo "Starting with UID: $LOCAL_USER_ID" + usermod -u $LOCAL_USER_ID user + export HOME=/home/user + chown -R user:user $HOME + + exec su-exec user "$@" +else + exec "$@" +fi diff --git a/tools/run-docker.sh b/tools/run-docker.sh new file mode 100755 index 00000000..68c33f64 --- /dev/null +++ b/tools/run-docker.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +# First-time usage: docker build . && docker tag libcanard && ./run-docker.sh + +dockerimage=libcanard + +docker run -it --rm -v $(pwd):/home/user/libcanard:z -e LOCAL_USER_ID=`id -u` $dockerimage "$@"