diff --git a/libudpard/udpard.c b/libudpard/udpard.c index 123208a..56a107b 100644 --- a/libudpard/udpard.c +++ b/libudpard/udpard.c @@ -787,10 +787,18 @@ static inline bool rxParseFrame(const struct UdpardMutablePayload datagram_paylo const bool service = (out->meta.data_specifier & DATA_SPECIFIER_SERVICE_NOT_MESSAGE_MASK) != 0; const bool single_frame = (out->base.index == 0) && out->base.end_of_transfer; ok = service ? ((!broadcast) && (!anonymous)) : (broadcast && ((!anonymous) || single_frame)); + ok = ok && (out->meta.transfer_id != TRANSFER_ID_UNSET); } return ok; } +static inline bool rxValidateMemoryResources(const struct UdpardRxMemoryResources memory) +{ + return (memory.session.allocate != NULL) && (memory.session.deallocate != NULL) && + (memory.fragment.allocate != NULL) && (memory.fragment.deallocate != NULL) && + (memory.payload.deallocate != NULL); +} + /// This helper is needed to minimize the risk of argument swapping when passing these two resources around, /// as they almost always go side by side. typedef struct @@ -886,7 +894,7 @@ struct UdpardInternalRxSession /// Frees all fragments in the tree and their payload buffers. Destroys the passed fragment. /// This is meant to be invoked on the root of the tree. -/// The maximum recursion depth is ceil(1.44*log2(FRAME_INDEX_MAX+1)-0.328) = 22 levels. +/// The maximum recursion depth is ceil(1.44*log2(FRAME_INDEX_MAX+1)-0.328) = 45 levels. // NOLINTNEXTLINE(misc-no-recursion) MISRA C:2012 rule 17.2 static inline void rxFragmentDestroyTree(RxFragment* const self, const RxMemory memory) { @@ -996,7 +1004,7 @@ typedef struct } RxSlotEjectContext; /// See rxSlotEject() for details. -/// The maximum recursion depth is ceil(1.44*log2(FRAME_INDEX_MAX+1)-0.328) = 22 levels. +/// The maximum recursion depth is ceil(1.44*log2(FRAME_INDEX_MAX+1)-0.328) = 45 levels. /// NOLINTNEXTLINE(misc-no-recursion) MISRA C:2012 rule 17.2 static inline void rxSlotEjectFragment(RxFragment* const frag, RxSlotEjectContext* const ctx) { @@ -1464,20 +1472,23 @@ static inline void rxSessionInit(struct UdpardInternalRxSession* const self, con static inline void rxSessionDestroyTree(struct UdpardInternalRxSession* const self, const struct UdpardRxMemoryResources memory) { - for (uint_fast8_t i = 0; i < UDPARD_NETWORK_INTERFACE_COUNT_MAX; i++) - { - rxIfaceFree(&self->ifaces[i], (RxMemory){.fragment = memory.fragment, .payload = memory.payload}); - } - for (uint_fast8_t i = 0; i < 2; i++) + if (self != NULL) { - struct UdpardInternalRxSession* const child = (struct UdpardInternalRxSession*) (void*) self->base.lr[i]; - if (child != NULL) + for (uint_fast8_t i = 0; i < UDPARD_NETWORK_INTERFACE_COUNT_MAX; i++) + { + rxIfaceFree(&self->ifaces[i], (RxMemory){.fragment = memory.fragment, .payload = memory.payload}); + } + for (uint_fast8_t i = 0; i < 2; i++) { - UDPARD_ASSERT(child->base.up == &self->base); - rxSessionDestroyTree(child, memory); // NOSONAR recursion + struct UdpardInternalRxSession* const child = (struct UdpardInternalRxSession*) (void*) self->base.lr[i]; + if (child != NULL) + { + UDPARD_ASSERT(child->base.up == &self->base); + rxSessionDestroyTree(child, memory); // NOSONAR recursion + } } + memFree(memory.session, sizeof(struct UdpardInternalRxSession), self); } - memFree(memory.session, sizeof(struct UdpardInternalRxSession), self); } // -------------------------------------------------- RX PORT -------------------------------------------------- @@ -1624,6 +1635,7 @@ static inline void rxPortInit(struct UdpardRxPort* const self) static inline void rxPortFree(struct UdpardRxPort* const self, const struct UdpardRxMemoryResources memory) { rxSessionDestroyTree(self->sessions, memory); + self->sessions = NULL; } // -------------------------------------------------- RX API -------------------------------------------------- @@ -1642,12 +1654,67 @@ int_fast8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, const size_t extent, const struct UdpardRxMemoryResources memory) { - (void) self; - (void) subject_id; - (void) extent; - (void) memory; - (void) &rxPortAcceptFrame; - (void) &rxPortInit; - (void) &rxPortFree; - return 0; + int_fast8_t result = -UDPARD_ERROR_ARGUMENT; + if ((self != NULL) && (subject_id <= UDPARD_SUBJECT_ID_MAX) && rxValidateMemoryResources(memory)) + { + memZero(sizeof(*self), self); + rxPortInit(&self->port); + self->port.extent = extent; + self->udp_ip_endpoint = makeSubjectUDPIPEndpoint(subject_id); + self->memory = memory; + result = 0; + } + return result; +} + +void udpardRxSubscriptionFree(struct UdpardRxSubscription* const self) +{ + if (self != NULL) + { + rxPortFree(&self->port, self->memory); + } +} + +int_fast8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self, + const UdpardMicrosecond timestamp_usec, + const struct UdpardMutablePayload datagram_payload, + const uint_fast8_t redundant_iface_index, + struct UdpardRxTransfer* const out_transfer) +{ + int_fast8_t result = -UDPARD_ERROR_ARGUMENT; + if ((self != NULL) && (timestamp_usec != TIMESTAMP_UNSET) && (datagram_payload.data != NULL) && + (redundant_iface_index < UDPARD_NETWORK_INTERFACE_COUNT_MAX) && (out_transfer != NULL)) + { + result = rxPortAcceptFrame(&self->port, + redundant_iface_index, + timestamp_usec, + datagram_payload, + self->memory, + out_transfer); + } + return result; +} + +// ===================================================================================================================== +// ==================================================== MISC ===================================================== +// ===================================================================================================================== + +size_t udpardGather(const struct UdpardFragment head, const size_t destination_size_bytes, void* const destination) +{ + size_t offset = 0; + if (NULL != destination) + { + const struct UdpardFragment* frag = &head; + while ((frag != NULL) && (offset < destination_size_bytes)) + { + UDPARD_ASSERT(frag->view.data != NULL); + const size_t frag_size = smaller(frag->view.size, destination_size_bytes - offset); + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + (void) memmove(((byte_t*) destination) + offset, frag->view.data, frag_size); + offset += frag_size; + UDPARD_ASSERT(offset <= destination_size_bytes); + frag = frag->next; + } + } + return offset; } diff --git a/libudpard/udpard.h b/libudpard/udpard.h index 0f18753..b5077f5 100644 --- a/libudpard/udpard.h +++ b/libudpard/udpard.h @@ -825,6 +825,7 @@ int_fast8_t udpardRxSubscriptionInit(struct UdpardRxSubscription* const self, /// Frees all memory held by the subscription instance. /// After invoking this function, the instance is no longer usable. +/// The function has no effect if the instance is NULL. /// Do not forget to close the sockets that were opened for this subscription. void udpardRxSubscriptionFree(struct UdpardRxSubscription* const self); @@ -872,8 +873,6 @@ void udpardRxSubscriptionFree(struct UdpardRxSubscription* const self); /// No data copy takes place. Malformed frames are discarded in constant time. /// Linear time is spent on the CRC verification of the transfer payload when the transfer is complete. /// -/// This function performs log(n) of recursive calls internally, where n is the number of frames in a transfer. -/// /// UDPARD_ERROR_MEMORY is returned if the function fails to allocate memory. /// UDPARD_ERROR_ARGUMENT is returned if any of the input arguments are invalid. int_fast8_t udpardRxSubscriptionReceive(struct UdpardRxSubscription* const self, @@ -1021,6 +1020,22 @@ int_fast8_t udpardRxRPCDispatcherReceive(struct UdpardRxRPCDispatcher* const sel const uint_fast8_t redundant_iface_index, struct UdpardRxRPCTransfer* const out_transfer); +// ===================================================================================================================== +// ==================================================== MISC ===================================================== +// ===================================================================================================================== + +/// This helper function takes the head of a fragmented buffer list and copies the data into the contiguous buffer +/// provided by the user. If the total size of all fragments combined exceeds the size of the user-provided buffer, +/// copying will stop early after the buffer is filled, thus truncating the fragmented data short. +/// +/// The source list is not modified. Do not forget to free its memory afterward if it was dynamically allocated. +/// +/// The function has no effect and returns zero if the destination buffer is NULL. +/// The data pointers in the fragment list shall be valid, otherwise the behavior is undefined. +/// +/// Returns the number of bytes copied into the contiguous destination buffer. +size_t udpardGather(const struct UdpardFragment head, const size_t destination_size_bytes, void* const destination); + #ifdef __cplusplus } #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2f3f93e..51583ba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -86,6 +86,9 @@ endfunction() gen_test_matrix(test_helpers "src/test_helpers.c") gen_test_matrix(test_cavl "src/test_cavl.cpp") gen_test_matrix(test_tx "${library_dir}/udpard.c;src/test_tx.cpp") +gen_test_matrix(test_rx "${library_dir}/udpard.c;src/test_rx.cpp") +gen_test_matrix(test_e2e "${library_dir}/udpard.c;src/test_e2e.cpp") +gen_test_matrix(test_misc "${library_dir}/udpard.c;src/test_misc.cpp") gen_test_matrix(test_intrusive_crc "src/test_intrusive_crc.c") gen_test_matrix(test_intrusive_tx "src/test_intrusive_tx.c") gen_test_matrix(test_intrusive_rx "src/test_intrusive_rx.c") diff --git a/tests/src/helpers.h b/tests/src/helpers.h index 87d881e..f4e035f 100644 --- a/tests/src/helpers.h +++ b/tests/src/helpers.h @@ -8,6 +8,7 @@ #include #include #include +#include #if !(defined(UDPARD_VERSION_MAJOR) && defined(UDPARD_VERSION_MINOR)) # error "Library version not defined" @@ -158,6 +159,18 @@ static inline struct UdpardMemoryDeleter instrumentedAllocatorMakeMemoryDeleter( return out; } +static inline void seedRandomNumberGenerator(void) +{ + unsigned seed = (unsigned) time(NULL); + const char* const env_var = getenv("RANDOM_SEED"); + if (env_var != NULL) + { + seed = (unsigned) atoll(env_var); // Conversion errors are possible but ignored. + } + srand(seed); + (void) fprintf(stderr, "RANDOM_SEED=%u\n", seed); +} + #ifdef __cplusplus } #endif diff --git a/tests/src/test_e2e.cpp b/tests/src/test_e2e.cpp new file mode 100644 index 0000000..d919958 --- /dev/null +++ b/tests/src/test_e2e.cpp @@ -0,0 +1,24 @@ +/// This software is distributed under the terms of the MIT License. +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include + +namespace +{ + +// Here be dragons. + +} // namespace + +void setUp() {} + +void tearDown() {} + +int main() +{ + UNITY_BEGIN(); + // TODO + return UNITY_END(); +} diff --git a/tests/src/test_intrusive_rx.c b/tests/src/test_intrusive_rx.c index 8330114..1389400 100644 --- a/tests/src/test_intrusive_rx.c +++ b/tests/src/test_intrusive_rx.c @@ -332,6 +332,15 @@ static void testParseFrameEmpty(void) TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = "", .size = 0}, &rxf)); } +static void testParseFrameInvalidTransferID(void) +{ + byte_t data[] = {1, 2, 41, 9, 56, 21, 230, 29, 255, 255, 255, 255, + 255, 255, 255, 255, 57, 48, 0, 0, 0, 0, 42, 107, // + 'a', 'b', 'c'}; + RxFrame rxf = {0}; + TEST_ASSERT_FALSE(rxParseFrame((struct UdpardMutablePayload){.data = data, .size = sizeof(data)}, &rxf)); +} + // -------------------------------------------------- SLOT -------------------------------------------------- static void testSlotRestartEmpty(void) @@ -2395,6 +2404,7 @@ int main(void) RUN_TEST(testParseFrameUnknownHeaderVersion); RUN_TEST(testParseFrameHeaderWithoutPayload); RUN_TEST(testParseFrameEmpty); + RUN_TEST(testParseFrameInvalidTransferID); // slot RUN_TEST(testSlotRestartEmpty); RUN_TEST(testSlotRestartNonEmpty); diff --git a/tests/src/test_misc.cpp b/tests/src/test_misc.cpp new file mode 100644 index 0000000..1830a16 --- /dev/null +++ b/tests/src/test_misc.cpp @@ -0,0 +1,83 @@ +/// This software is distributed under the terms of the MIT License. +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include +#include "helpers.h" +#include "hexdump.hpp" +#include +#include +#include +#include + +namespace +{ +void testGather() +{ + const std::string_view payload = + "It's very simple. The attacker must first transform themselves into life forms that can survive in a " + "low-dimensional universe. For instance, a four-dimensional species can transform itself into " + "three-dimensional creatures, or a three-dimensional species can transform itself into two-dimensional life. " + "After the entire civilization has entered a lower dimension, they can initiate a dimensional strike against " + "the enemy without concern for the consequences."; + + std::array frags{{}}; + frags.at(0).next = &frags.at(1); + frags.at(1).next = &frags.at(2); + frags.at(2).next = &frags.at(3); + frags.at(3).next = nullptr; + + frags.at(0).view.data = payload.data(); + frags.at(0).view.size = 100; + + frags.at(1).view.data = payload.data() + frags.at(0).view.size; + frags.at(1).view.size = 100; + + frags.at(2).view.data = payload.data() + frags.at(1).view.size + frags.at(0).view.size; + frags.at(2).view.size = 0; // Edge case. + + frags.at(3).view.data = payload.data() + frags.at(2).view.size + frags.at(1).view.size + frags.at(0).view.size; + frags.at(3).view.size = payload.size() - frags.at(2).view.size - frags.at(1).view.size - frags.at(0).view.size; + + std::array mono{}; + + // Copy full size payload. + std::generate(mono.begin(), mono.end(), [] { return std::rand() % 256; }); + TEST_ASSERT_EQUAL(payload.size(), udpardGather(frags.at(0), mono.size(), mono.data())); + TEST_ASSERT_EQUAL_MEMORY(payload.data(), mono.data(), payload.size()); + + // Truncation mid-fragment. + std::generate(mono.begin(), mono.end(), [] { return std::rand() % 256; }); + TEST_ASSERT_EQUAL(150, udpardGather(frags.at(0), 150, mono.data())); + TEST_ASSERT_EQUAL_MEMORY(payload.data(), mono.data(), 150); + + // Truncation at the fragment boundary. + std::generate(mono.begin(), mono.end(), [] { return std::rand() % 256; }); + TEST_ASSERT_EQUAL(200, udpardGather(frags.at(0), 200, mono.data())); + TEST_ASSERT_EQUAL_MEMORY(payload.data(), mono.data(), 200); + + // Empty destination. + mono.fill(0xA5); + TEST_ASSERT_EQUAL(0, udpardGather(frags.at(0), 0, mono.data())); + TEST_ASSERT_EQUAL(0, std::count_if(mono.begin(), mono.end(), [](const auto x) { return x != 0xA5; })); + + // Edge cases. + TEST_ASSERT_EQUAL(0, udpardGather(frags.at(0), 0, nullptr)); + TEST_ASSERT_EQUAL(0, udpardGather(frags.at(0), 100, nullptr)); +} +} // namespace + +void setUp() +{ + seedRandomNumberGenerator(); // Re-seed the RNG for each test to avoid coupling. +} + +void tearDown() {} + +int main() +{ + UNITY_BEGIN(); + RUN_TEST(testGather); + return UNITY_END(); +} diff --git a/tests/src/test_rx.cpp b/tests/src/test_rx.cpp new file mode 100644 index 0000000..b26455b --- /dev/null +++ b/tests/src/test_rx.cpp @@ -0,0 +1,162 @@ +/// This software is distributed under the terms of the MIT License. +/// Copyright (C) OpenCyphal Development Team +/// Copyright Amazon.com Inc. or its affiliates. +/// SPDX-License-Identifier: MIT + +#include +#include "helpers.h" +#include "hexdump.hpp" +#include +#include +#include +#include +#include +#include + +namespace +{ +void testRxSubscriptionInit() +{ + InstrumentedAllocator mem_session{}; + InstrumentedAllocator mem_fragment{}; + InstrumentedAllocator mem_payload{}; + instrumentedAllocatorNew(&mem_session); + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + UdpardRxSubscription sub{}; + TEST_ASSERT_EQUAL(0, + udpardRxSubscriptionInit(&sub, + 0x1234, + 1000, + { + .session = instrumentedAllocatorMakeMemoryResource(&mem_session), + .fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment), + .payload = instrumentedAllocatorMakeMemoryDeleter(&mem_payload), + })); + TEST_ASSERT_EQUAL(&instrumentedAllocatorAllocate, sub.memory.session.allocate); + TEST_ASSERT_EQUAL(&instrumentedAllocatorDeallocate, sub.memory.session.deallocate); + TEST_ASSERT_EQUAL(1000, sub.port.extent); + TEST_ASSERT_EQUAL(UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, sub.port.transfer_id_timeout_usec); + TEST_ASSERT_EQUAL(nullptr, sub.port.sessions); + TEST_ASSERT_EQUAL(0xEF001234UL, sub.udp_ip_endpoint.ip_address); + TEST_ASSERT_EQUAL(9382, sub.udp_ip_endpoint.udp_port); + TEST_ASSERT_EQUAL(0, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + udpardRxSubscriptionFree(&sub); + TEST_ASSERT_EQUAL(0, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + udpardRxSubscriptionFree(nullptr); // No-op. + // Invalid arguments. + TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, + udpardRxSubscriptionInit(nullptr, + 0xFFFF, + 1000, + { + .session = instrumentedAllocatorMakeMemoryResource(&mem_session), + .fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment), + .payload = instrumentedAllocatorMakeMemoryDeleter(&mem_payload), + })); + TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, + udpardRxSubscriptionInit(&sub, + 0xFFFF, + 1000, + { + .session = instrumentedAllocatorMakeMemoryResource(&mem_session), + .fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment), + .payload = instrumentedAllocatorMakeMemoryDeleter(&mem_payload), + })); + TEST_ASSERT_EQUAL(-UDPARD_ERROR_ARGUMENT, udpardRxSubscriptionInit(&sub, 1234, 1000, {})); +} + +void testRxSubscriptionReceive() +{ + InstrumentedAllocator mem_session{}; + InstrumentedAllocator mem_fragment{}; + InstrumentedAllocator mem_payload{}; + instrumentedAllocatorNew(&mem_session); + instrumentedAllocatorNew(&mem_fragment); + instrumentedAllocatorNew(&mem_payload); + UdpardRxSubscription sub{}; + TEST_ASSERT_EQUAL(0, + udpardRxSubscriptionInit(&sub, + 0x1234, + 1000, + { + .session = instrumentedAllocatorMakeMemoryResource(&mem_session), + .fragment = instrumentedAllocatorMakeMemoryResource(&mem_fragment), + .payload = instrumentedAllocatorMakeMemoryDeleter(&mem_payload), + })); + TEST_ASSERT_EQUAL(1000, sub.port.extent); + TEST_ASSERT_EQUAL(UDPARD_DEFAULT_TRANSFER_ID_TIMEOUT_USEC, sub.port.transfer_id_timeout_usec); + TEST_ASSERT_EQUAL(nullptr, sub.port.sessions); + TEST_ASSERT_EQUAL(0xEF001234UL, sub.udp_ip_endpoint.ip_address); + TEST_ASSERT_EQUAL(9382, sub.udp_ip_endpoint.udp_port); + TEST_ASSERT_EQUAL(0, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); + UdpardRxTransfer transfer{}; + // Feed a single-frame transfer. Remember that in Cyphal/UDP, the payload CRC is part of the payload itself. + // + //>>> from pycyphal.transport.commons.crc import CRC32C + //>>> CRC32C.new(b"Hello!").value_as_bytes + // + // >>> from pycyphal.transport.udp import UDPFrame + // >>> from pycyphal.transport import Priority, MessageDataSpecifier, ServiceDataSpecifier + // >>> frame = UDPFrame(priority=Priority.FAST, transfer_id=0xbadc0ffee0ddf00d, index=0, end_of_transfer=True, + // payload=memoryview(b'Hello!\xd6\xeb\xfd\t'), source_node_id=2345, destination_node_id=0xFFFF, + // data_specifier=MessageDataSpecifier(0x1234), user_data=0) + // >>> list(frame.compile_header_and_payload()[0]) + // >>> list(frame.compile_header_and_payload()[1]) + { + const std::array data{{1, 2, 41, 9, 255, 255, 52, 18, 13, 240, 221, 224, + 254, 15, 220, 186, 0, 0, 0, 128, 0, 0, 246, 129, // + 72, 101, 108, 108, 111, 33, 214, 235, 253, 9}}; + const UdpardMutablePayload datagram{ + .size = sizeof(data), + .data = instrumentedAllocatorAllocate(&mem_payload, sizeof(data)), + }; + TEST_ASSERT_NOT_NULL(datagram.data); + std::memcpy(datagram.data, data.data(), data.size()); + TEST_ASSERT_EQUAL(1, udpardRxSubscriptionReceive(&sub, 10'000'000, datagram, 0, &transfer)); + } + TEST_ASSERT_EQUAL(1, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); // Head optimization in effect. + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); + TEST_ASSERT_EQUAL(10'000'000, transfer.timestamp_usec); + TEST_ASSERT_EQUAL(UdpardPriorityFast, transfer.priority); + TEST_ASSERT_EQUAL(2345, transfer.source_node_id); + TEST_ASSERT_EQUAL(0xBADC0FFEE0DDF00DUL, transfer.transfer_id); + TEST_ASSERT_EQUAL(6, transfer.payload_size); + TEST_ASSERT_EQUAL(6, transfer.payload.view.size); + TEST_ASSERT_EQUAL_MEMORY("Hello!", transfer.payload.view.data, 6); + TEST_ASSERT_NULL(transfer.payload.next); + // Free the subscription, ensure the payload is not affected because its ownership has been transferred to us. + udpardRxSubscriptionFree(&sub); + udpardRxSubscriptionFree(&sub); // The API does not guarantee anything but this is for extra safety. + TEST_ASSERT_EQUAL(0, mem_session.allocated_fragments); // Session gone. Bye bye. + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(1, mem_payload.allocated_fragments); // Stayin' alive. + // Free the payload as well. + udpardRxFragmentFree(transfer.payload, + instrumentedAllocatorMakeMemoryResource(&mem_fragment), + instrumentedAllocatorMakeMemoryDeleter(&mem_payload)); + TEST_ASSERT_EQUAL(0, mem_session.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_fragment.allocated_fragments); + TEST_ASSERT_EQUAL(0, mem_payload.allocated_fragments); // Yeah. +} + +} // namespace + +void setUp() {} + +void tearDown() {} + +int main() +{ + UNITY_BEGIN(); + RUN_TEST(testRxSubscriptionInit); + RUN_TEST(testRxSubscriptionReceive); + return UNITY_END(); +}