diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9306e1b..df71483 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -128,7 +128,7 @@ jobs: with: submodules: true - run: cmake -S . -B $GITHUB_WORKSPACE/build -DCMAKE_BUILD_TYPE=Debug -DNO_STATIC_ANALYSIS=ON - - run: cd $GITHUB_WORKSPACE/build && make -j"$(nproc)" udp_echo udp_time_pub + - run: cd $GITHUB_WORKSPACE/build && make -j"$(nproc)" example_echo example_time_pub - run: python3 tools/ci_example_smoke.py --build-dir $GITHUB_WORKSPACE/build test_model_montecarlo: diff --git a/.gitmodules b/.gitmodules index a6b0297..f8eeca5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,7 @@ [submodule "lib/unity"] path = lib/unity url = https://github.com/ThrowTheSwitch/Unity +[submodule "lib/libcanard"] + path = lib/libcanard + url = https://github.com/OpenCyphal/libcanard + branch = experimental diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 0cbec32..616297b 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -21,6 +21,7 @@ cyphal cyudp deinit + destructions dgrams dscp enroute diff --git a/CMakeLists.txt b/CMakeLists.txt index aaa60d7..158ccdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,13 @@ if (NOT NO_STATIC_ANALYSIS) ${CMAKE_SOURCE_DIR}/tests/*/*.[ch]pp ${CMAKE_SOURCE_DIR}/examples/*.[ch] ${CMAKE_SOURCE_DIR}/cy_udp_posix/*.[ch] + ${CMAKE_SOURCE_DIR}/cy_udp_posix/tests/*.[ch] + ${CMAKE_SOURCE_DIR}/cy_can/tests/*.[ch] + ${CMAKE_SOURCE_DIR}/cy_udp_posix/tests/*/*.[ch] + ${CMAKE_SOURCE_DIR}/cy_udp_posix/tests/*/*.[ch]pp + ${CMAKE_SOURCE_DIR}/cy_can/*.[ch] + ${CMAKE_SOURCE_DIR}/cy_can/tests/*/*.[ch] + ${CMAKE_SOURCE_DIR}/cy_can/tests/*/*.[ch]pp ) message(STATUS "Using clang-format: ${clang_format}; files to format: ${format_files}") add_custom_target(format COMMAND ${clang_format} -i -fallback-style=none -style=file --verbose ${format_files}) @@ -105,5 +112,6 @@ endif () # SUBDIRECTORIES add_subdirectory(cy_udp_posix) +add_subdirectory(cy_can) add_subdirectory(examples) add_subdirectory(tests) diff --git a/README.md b/README.md index a207d06..e514e0e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ On an embedded system, one may also prefer to use [`o1heap`](https://github.com/ management, but this is not a hard dependency -- any allocator will work. O1Heap is the recommended choice for embedded platforms due to its hard determinism and low fragmentation. +Pick one of the transport/platform glue layers suitable for your application: `cy_can`, `cy_udp_posix`, etc. +The integration instructions are identical: simply copy the C files and add them to your build system. +These components may be moved into dedicated repositories in the future. + 🌐 A live demo of the distributed consensus algorithm can be found at . For a detailed design overview, refer to `model/`. @@ -49,8 +53,9 @@ The specifics of setting up a local node depend on the platform and transport us unlike the rest of the API, which is entirely platform- and transport-agnostic. ```c++ -#include // platform- and transport-agnostic Cyphal API -#include // thin low-level glue specific to Cyphal/UDP on POSIX systems; choose one for your setup +#include // platform- and transport-agnostic Cyphal API +#include // thin low-level glue specific to Cyphal/UDP on POSIX systems; choose one for your setup +#include // thin low-level glue specific to Cyphal/CAN on SocketCAN; choose one for your setup int main(void) { @@ -58,6 +63,10 @@ int main(void) // Here we're using Cyphal/UDP on POSIX as an example. cy_platform_t* platform = cy_udp_posix_new(); if (platform == NULL) { ... } + + // If you need Cyphal/CAN on SocketCAN instead, just replace the above with: + cy_platform_t* platform = cy_can_socketcan_new(1, (const char*[]){"can0"}, 1000); // 1 iface, 1000 frames TX queue + if (platform == NULL) { ... } // Set up the local Cyphal node instance. // Every node needs a home, which should be unique across the network. @@ -150,7 +159,7 @@ Do not destroy unwanted futures right away because that cancels the associated o cy_future_callback_set(future, cy_future_destroy); // Will destroy itself when done, no need to keep the reference. ``` -The examples folder contains a simple publisher example `main_udp_time_pub.c`. +The examples folder contains a simple publisher example `example_time_pub.c`. ### 📩 Subscribe to topics and receive messages @@ -255,7 +264,7 @@ for (size_t i = 0; i < subs.count; i++) { It is also possible to monitor subscriber liveness and alert the application via its callback when messages cease to arrive; see the API docs for details. -The examples folder contains a simple subscriber example `main_udp_echo.c`. +The examples folder contains a simple subscriber example `example_echo.c`. ### 🔄 RPC & streaming diff --git a/cy/cy.c b/cy/cy.c index 010f9d0..07b7e9c 100644 --- a/cy/cy.c +++ b/cy/cy.c @@ -837,7 +837,8 @@ static cy_tree_t* reader_cavl_factory(void* const user) return NULL; } r->handle->subject_id = ctx->subject_id; - r->handle->extent = ctx->extent; // In case the platform layer didn't set it. + // Some platforms may revive a tombstoned incumbent whose extent is already larger than requested. + r->handle->extent = larger(r->handle->extent, ctx->extent); } return (cy_tree_t*)r; } diff --git a/cy/cy_platform.h b/cy/cy_platform.h index 461a9b0..c901b4a 100644 --- a/cy/cy_platform.h +++ b/cy/cy_platform.h @@ -73,7 +73,7 @@ typedef struct cy_subject_writer_t typedef struct cy_subject_reader_t { uint32_t subject_id; - size_t extent; + size_t extent; // Platform must either set this to the initial value or zero at construction time. } cy_subject_reader_t; /// Abstracts away the specifics of the transport (UDP, serial, CAN, etc) and the platform where Cy is running @@ -170,6 +170,7 @@ typedef struct cy_platform_vtable_t /// The cy_on_message() callback will be invoked from this function. /// This is the only platform function that is allowed to block. /// May return additional error codes, which will be forwarded to the application as-is. + /// This can only be invoked from within cy_spin*() functions, and is never invoked from cy_on_message(). cy_err_t (*spin)(cy_platform_t*, cy_us_t deadline); // === MISC === diff --git a/cy_can/CMakeLists.txt b/cy_can/CMakeLists.txt new file mode 100644 index 0000000..00a3db1 --- /dev/null +++ b/cy_can/CMakeLists.txt @@ -0,0 +1,38 @@ +# Copyright (c) Pavel Kirienko + +cmake_minimum_required(VERSION 3.24) +project(cy_can C) + +include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/lib") + +# Libcanard static library. +add_library(canard STATIC "${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard/canard.c") +target_compile_options(canard PRIVATE -Wno-cast-align) +target_include_directories(canard SYSTEM INTERFACE ${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard) +set_target_properties( + canard + PROPERTIES + COMPILE_WARNING_AS_ERROR OFF + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +# Platform-agnostic CAN transport glue static library. +# Consumers link cy/cy.c themselves; this target only contains the transport/platform layer. +add_library(cy_can STATIC cy_can.c) +target_link_libraries(cy_can PUBLIC canard) +target_include_directories(cy_can PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/cy) +set_target_properties(cy_can PROPERTIES COMPILE_WARNING_AS_ERROR ON C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) + +# Linux SocketCAN backend for cy_can. Includes cy_can and canard, making it self-contained except for cy.c. +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(cy_can_socketcan STATIC cy_can_socketcan.c) + target_link_libraries(cy_can_socketcan PUBLIC cy_can) + set_target_properties(cy_can_socketcan PROPERTIES COMPILE_WARNING_AS_ERROR ON C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) +endif () + +add_subdirectory(tests) diff --git a/cy_can/cy_can.c b/cy_can/cy_can.c new file mode 100644 index 0000000..ea220b1 --- /dev/null +++ b/cy_can/cy_can.c @@ -0,0 +1,855 @@ +// ____ ______ __ __ +// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / +// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / +// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / +// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ +// /_/ /____/_/ +// +// Copyright (c) Pavel Kirienko + +#include "cy_can.h" +#include + +#define RAPIDHASH_COMPACT +#include + +#include +#include + +#if __STDC_VERSION__ < 201112L +#define static_assert(x, ...) typedef char _sa_gl(_sa_, __LINE__)[(x) ? 1 : -1] +#define _sa_gl(a, b) _sa_gl2(a, b) +#define _sa_gl2(a, b) a##b +#endif + +/// The Cy session-layer header is fixed at 24 bytes. Must agree with cy.c. +#define HEADER_BYTES 24U + +/// Service-ID reserved for v1.1 unicast transfers over Cyphal/CAN. +#define UNICAST_SERVICE_ID 511U + +/// Default extent for incoming unicast messages; will grow as needed. +#define UNICAST_EXTENT_INITIAL (HEADER_BYTES + 1024U) + +typedef struct +{ + cy_platform_t base; // Must be first (upcast pattern). + uint_least8_t iface_count; + + canard_t canard; + + canard_subscription_t unicast_sub; // Service-ID 511 for unicast. + + // Per-remote unicast transfer-ID counters (one per possible CAN node-ID). + uint_least8_t unicast_tid[CANARD_NODE_ID_CAPACITY]; + + struct subject_reader_t* tombstone_head; // Deferred destruction list to avoid reentrancy issues. + struct subject_reader_t* tombstone_tail; + + uint64_t oom_count; // cy_can-level OOM (message wrapper allocation failures, etc.). + + const cy_can_vtable_t* vtable; + void* user; +} cy_can_t; + +typedef struct subject_writer_t +{ + cy_subject_writer_t base; + uint_least8_t next_tid_13b; + uint_least8_t next_tid_16b; +} subject_writer_t; + +typedef struct subject_reader_t +{ + cy_subject_reader_t base; + canard_subscription_t sub_16b; + cy_can_t* owner; + struct subject_reader_t* prev_tombstone; + struct subject_reader_t* next_tombstone; +} subject_reader_t; + +// The runtime type is determined as pinned=subject_id<=CY_SUBJECT_ID_PINNED_MAX. +typedef struct subject_reader_pinned_t +{ + subject_reader_t base; + canard_subscription_t sub_13b; + uint64_t phony_tag; + uint_least8_t phony_header[HEADER_BYTES]; // Precomputed for 13-bit reception. +} subject_reader_pinned_t; + +static subject_reader_pinned_t* as_pinned(subject_reader_t* const self) +{ + return (self->base.subject_id <= CY_SUBJECT_ID_PINNED_MAX) ? (subject_reader_pinned_t*)self : NULL; +} + +// ===================================================================================================================== +// MEMORY BRIDGE + +static void* can_mem_alloc(const canard_mem_t mem, const size_t size) +{ + cy_can_t* const self = (cy_can_t*)mem.context; + return self->vtable->realloc(self->user, NULL, size); +} + +static void can_mem_free(const canard_mem_t mem, const size_t size, void* const ptr) +{ + cy_can_t* const self = (cy_can_t*)mem.context; + (void)size; + if (ptr != NULL) { + self->vtable->realloc(self->user, ptr, 0); + } +} + +static const canard_mem_vtable_t can_mem_vtable = { .free = can_mem_free, .alloc = can_mem_alloc }; + +static canard_mem_t make_mem(cy_can_t* const self) +{ + return (canard_mem_t){ .vtable = &can_mem_vtable, .context = self }; +} + +static canard_mem_set_t make_mem_set(cy_can_t* const self) +{ + const canard_mem_t m = make_mem(self); + return (canard_mem_set_t){ .tx_transfer = m, .tx_frame = m, .rx_session = m, .rx_payload = m, .rx_filters = m }; +} + +// ===================================================================================================================== +// SERIALIZATION HELPERS (little-endian, must match cy.c) + +static void le_serialize_u32(uint_least8_t* const ptr, const uint32_t value) +{ + for (size_t i = 0; i < 4; i++) { + ptr[i] = (uint_least8_t)((value >> (i * 8U)) & 0xFFU); + } +} + +static void le_serialize_u64(uint_least8_t* const ptr, const uint64_t value) +{ + for (size_t i = 0; i < 8; i++) { + ptr[i] = (uint_least8_t)((value >> (i * 8U)) & 0xFFU); + } +} + +/// Portable decimal conversion for subject-ID -> topic name hash. Buffer must be at least 11 bytes. +static size_t uint_to_decimal(uint32_t value, char* const buf) +{ + char tmp[11]; + size_t len = 0; + do { + tmp[len++] = (char)('0' + (char)(value % 10U)); + value /= 10U; + } while (value > 0); + for (size_t i = 0; i < len; i++) { + buf[i] = tmp[len - 1U - i]; + } + return len; +} + +// ===================================================================================================================== +// MESSAGE BUFFER +// +// The logical message is seg0[] (flex array, inline data) concatenated with seg1 (optional external data), +// with `skip` bytes removed from the front. +// +// - 16-bit single-frame: seg0 = [payload copy], seg1 = NULL. origin = NULL. +// - 16-bit multi-frame: seg0 = [] (empty), seg1 = view. origin = canard buffer. +// - 13-bit single-frame: seg0 = [phony hdr + payload], seg1 = NULL. origin = NULL. +// - 13-bit multi-frame: seg0 = [phony hdr], seg1 = view. origin = canard buffer. + +typedef struct +{ + cy_message_t base; + cy_can_t* owner; + void* origin; // Multi-frame payload buffer from canard (NULL for single-frame). Freed on destroy. + const void* seg1; // Second segment: points into origin (NULL if single-segment). + size_t seg1_len; + size_t seg0_len; // Length of inline data in seg0[]. + size_t skip; + uint_least8_t seg0[]; // Flex array: inline data (phony header and/or single-frame payload). +} can_message_t; + +static void v_message_skip(cy_message_t* const base, const size_t offset) +{ + can_message_t* const self = (can_message_t*)base; + const size_t total = self->seg0_len + self->seg1_len; + const size_t avail = (total > self->skip) ? (total - self->skip) : 0; + self->skip += (offset < avail) ? offset : avail; +} + +static size_t v_message_read(const cy_message_t* const base, const size_t offset, const size_t size, void* const dest) +{ + const can_message_t* const self = (const can_message_t*)base; + const size_t total = self->seg0_len + self->seg1_len; + const size_t start = self->skip + offset; + if (start >= total) { + return 0; + } + const size_t avail = total - start; + const size_t to_read = (size < avail) ? size : avail; + size_t done = 0; + size_t pos = start; + char* out = (char*)dest; + if (pos < self->seg0_len) { + const size_t rem = self->seg0_len - pos; + const size_t n = ((to_read - done) < rem) ? (to_read - done) : rem; + (void)memcpy(out, self->seg0 + pos, n); + done += n; + out += n; + pos += n; + } + if ((done < to_read) && (self->seg1 != NULL)) { + const size_t n = to_read - done; + (void)memcpy(out, (const char*)self->seg1 + (pos - self->seg0_len), n); + done += n; + } + return done; +} + +static size_t v_message_size(const cy_message_t* const base) +{ + const can_message_t* const self = (const can_message_t*)base; + const size_t total = self->seg0_len + self->seg1_len; + return (total > self->skip) ? (total - self->skip) : 0; +} + +static void v_message_destroy(cy_message_t* const base) +{ + can_message_t* const self = (can_message_t*)base; + if (self->origin != NULL) { + self->owner->vtable->realloc(self->owner->user, self->origin, 0); + } + self->owner->vtable->realloc(self->owner->user, self, 0); +} + +static const cy_message_vtable_t message_vtable = { .skip = v_message_skip, + .read = v_message_read, + .size = v_message_size, + .destroy = v_message_destroy }; + +/// Allocate a can_message_t with `inline_extra` bytes for the flex array seg0[]. +static can_message_t* make_message(cy_can_t* const owner, const size_t inline_extra) +{ + can_message_t* const msg = + (can_message_t*)owner->vtable->realloc(owner->user, NULL, sizeof(can_message_t) + inline_extra); + if (msg != NULL) { + (void)memset(msg, 0, sizeof(*msg)); + msg->base = CY_MESSAGE_INIT(&message_vtable); + msg->owner = owner; + } + return msg; +} + +// ===================================================================================================================== +// cy_bytes_t -> canard_bytes_chain_t ALIASING + +static_assert(offsetof(canard_bytes_chain_t, bytes.size) == offsetof(cy_bytes_t, size), ""); +static_assert(offsetof(canard_bytes_chain_t, bytes.data) == offsetof(cy_bytes_t, data), ""); +static_assert(offsetof(canard_bytes_chain_t, next) == offsetof(cy_bytes_t, next), ""); + +static canard_bytes_chain_t cy_bytes_to_canard(const cy_bytes_t message) +{ + return (canard_bytes_chain_t){ .bytes = { .size = message.size, .data = message.data }, + .next = (const canard_bytes_chain_t*)message.next }; +} + +// ===================================================================================================================== +// CANARD VTABLE CALLBACKS + +static canard_us_t v_canard_now(const canard_t* const self) +{ + const cy_can_t* const owner = (const cy_can_t*)self->user_context; + return owner->vtable->now(owner->user); +} + +static bool v_canard_tx(canard_t* const self, + void* const user_context, + const canard_us_t deadline, + const uint_least8_t iface_index, + const bool fd, + const uint32_t extended_can_id, + const canard_bytes_t can_data) +{ + cy_can_t* const owner = (cy_can_t*)self->user_context; + const uint_least8_t len = (uint_least8_t)can_data.size; + (void)user_context; + (void)deadline; + assert(iface_index < owner->iface_count); + if (fd && (owner->vtable->tx_fd != NULL)) { + return owner->vtable->tx_fd(owner->user, iface_index, extended_can_id, can_data.data, len); + } + return owner->vtable->tx_classic(owner->user, iface_index, extended_can_id, can_data.data, len); +} + +static bool v_canard_filter(canard_t* const self, const size_t filter_count, const canard_filter_t* const filters) +{ + cy_can_t* const owner = (cy_can_t*)self->user_context; + assert((owner != NULL) && (owner->vtable != NULL) && (owner->vtable->filter != NULL)); + return owner->vtable->filter(owner->user, filter_count, filters); +} + +static const canard_vtable_t canard_vtbl_no_filter = { .now = v_canard_now, .tx = v_canard_tx, .filter = NULL }; +static const canard_vtable_t canard_vtbl_filter = { .now = v_canard_now, .tx = v_canard_tx, .filter = v_canard_filter }; + +// ===================================================================================================================== +// SUBSCRIPTION CALLBACKS + +static void deliver(cy_can_t* const owner, + const uint32_t* const subject_id, + const uint_least8_t source_node_id, + const canard_prio_t priority, + const canard_us_t timestamp, + can_message_t* const msg) +{ + cy_lane_t lane; + (void)memset(&lane, 0, sizeof(lane)); + lane.id = source_node_id; + lane.prio = (cy_prio_t)priority; + lane.ctx.state[0] = (unsigned char)source_node_id; + const cy_message_ts_t mts = { .timestamp = timestamp, .content = &msg->base }; + cy_on_message(&owner->base, lane, subject_id, mts); +} + +/// 16-bit subscription callback. Payload already contains the Cy session header. +static void v_on_msg_16b(canard_subscription_t* const self, + const canard_us_t timestamp, + const canard_prio_t priority, + const uint_least8_t source_node_id, + const uint_least8_t transfer_id, + const canard_payload_t payload) +{ + (void)transfer_id; + subject_reader_t* const reader = (subject_reader_t*)self->user_context; + cy_can_t* const owner = reader->owner; + const bool multiframe = (payload.origin.data != NULL); + + can_message_t* const msg = make_message(owner, multiframe ? 0 : payload.view.size); + if (msg == NULL) { + if (multiframe) { + owner->vtable->realloc(owner->user, payload.origin.data, 0); + } + owner->oom_count++; + return; + } + if (multiframe) { + msg->seg1 = payload.view.data; + msg->seg1_len = payload.view.size; + msg->origin = payload.origin.data; + } else { + (void)memcpy(msg->seg0, payload.view.data, payload.view.size); + msg->seg0_len = payload.view.size; + } + const uint32_t sid = reader->base.subject_id; + deliver(owner, &sid, source_node_id, priority, timestamp, msg); +} + +/// 13-bit subscription callback. Must prepend a precomputed phony Cy session header. +static void v_on_msg_13b(canard_subscription_t* const self, + const canard_us_t timestamp, + const canard_prio_t priority, + const uint_least8_t source_node_id, + const uint_least8_t transfer_id, + const canard_payload_t payload) +{ + (void)transfer_id; + subject_reader_t* const reader = (subject_reader_t*)self->user_context; + assert(reader != NULL); + cy_can_t* const owner = reader->owner; + subject_reader_pinned_t* const pinned = as_pinned(reader); + assert((pinned != NULL) && (owner != NULL)); + const bool multiframe = (payload.origin.data != NULL); + + const size_t inline_size = HEADER_BYTES + (multiframe ? 0 : payload.view.size); + can_message_t* const msg = make_message(owner, inline_size); + if (msg == NULL) { + if (multiframe) { + owner->vtable->realloc(owner->user, payload.origin.data, 0); + } + owner->oom_count++; + return; + } + // Stamp precomputed phony header with incrementing tag. + pinned->phony_tag++; + (void)memcpy(msg->seg0, pinned->phony_header, HEADER_BYTES); + le_serialize_u64(msg->seg0 + 16, pinned->phony_tag); + msg->seg0_len = HEADER_BYTES; + + if (multiframe) { + msg->seg1 = payload.view.data; + msg->seg1_len = payload.view.size; + msg->origin = payload.origin.data; + } else { + if (payload.view.size > 0) { + (void)memcpy(msg->seg0 + HEADER_BYTES, payload.view.data, payload.view.size); + } + msg->seg0_len += payload.view.size; + } + deliver(owner, &reader->base.subject_id, source_node_id, priority, timestamp, msg); +} + +/// Service-ID 511 subscription callback for unicast transfers. +static void v_on_msg_unicast(canard_subscription_t* const self, + const canard_us_t timestamp, + const canard_prio_t priority, + const uint_least8_t source_node_id, + const uint_least8_t transfer_id, + const canard_payload_t payload) +{ + (void)transfer_id; + cy_can_t* const owner = (cy_can_t*)self->user_context; + const bool multiframe = (payload.origin.data != NULL); + + can_message_t* const msg = make_message(owner, multiframe ? 0 : payload.view.size); + if (msg == NULL) { + if (multiframe) { + owner->vtable->realloc(owner->user, payload.origin.data, 0); + } + owner->oom_count++; + return; + } + if (multiframe) { + msg->seg1 = payload.view.data; + msg->seg1_len = payload.view.size; + msg->origin = payload.origin.data; + } else { + (void)memcpy(msg->seg0, payload.view.data, payload.view.size); + msg->seg0_len = payload.view.size; + } + deliver(owner, NULL, source_node_id, priority, timestamp, msg); +} + +static const canard_subscription_vtable_t sub_vtable_16b = { .on_message = v_on_msg_16b }; +static const canard_subscription_vtable_t sub_vtable_13b = { .on_message = v_on_msg_13b }; +static const canard_subscription_vtable_t sub_vtable_unicast = { .on_message = v_on_msg_unicast }; + +// ===================================================================================================================== +// SUBJECT WRITER + +static cy_subject_writer_t* v_subject_writer_new(cy_platform_t* const base, const uint32_t subject_id) +{ + cy_can_t* const owner = (cy_can_t*)base; + subject_writer_t* const self = (subject_writer_t*)owner->vtable->realloc(owner->user, NULL, sizeof(*self)); + if (self != NULL) { + (void)memset(self, 0, sizeof(*self)); + self->base.subject_id = subject_id; + } + CY_TRACE(owner->base.cy, "CAN writer S%08jx", (uintmax_t)subject_id); + return (cy_subject_writer_t*)self; +} + +static void v_subject_writer_destroy(cy_platform_t* const platform, cy_subject_writer_t* const base) +{ + cy_can_t* const owner = (cy_can_t*)platform; + subject_writer_t* const self = (subject_writer_t*)base; + owner->vtable->realloc(owner->user, self, 0); +} + +static cy_err_t v_subject_writer_send(cy_platform_t* const platform, + cy_subject_writer_t* const base, + const cy_us_t deadline, + const cy_prio_t priority, + const cy_bytes_t message) +{ + cy_can_t* const owner = (cy_can_t*)platform; + subject_writer_t* const self = (subject_writer_t*)base; + const uint32_t sid = base->subject_id; + const uint_least8_t ibm = (uint_least8_t)((1U << owner->iface_count) - 1U); + + assert((message.data != NULL) && (message.size >= HEADER_BYTES)); + const bool pinned = (sid <= CY_SUBJECT_ID_PINNED_MAX); + const bool best_effort = (((const uint_least8_t*)message.data)[0] == 0); // header_msg_be + const uint64_t e_oom = owner->canard.err.oom; + const uint64_t e_cap = owner->canard.err.tx_capacity; + + bool ok = false; + if (pinned && best_effort) { // V1.0 PATH: 13-bit subject-ID, strip the 24-byte Cy header. + cy_bytes_t stripped = message; + stripped.data = (const char*)stripped.data + HEADER_BYTES; + stripped.size -= HEADER_BYTES; + if ((stripped.size == 0) && (stripped.next != NULL)) { + stripped = *stripped.next; + } + ok = canard_publish_13b(&owner->canard, + deadline, + ibm, + (canard_prio_t)priority, + (uint16_t)sid, + self->next_tid_13b++, + cy_bytes_to_canard(stripped), + NULL); + } else { // V1.1 PATH: 16-bit subject-ID, full message including Cy header. + ok = canard_publish_16b(&owner->canard, + deadline, + ibm, + (canard_prio_t)priority, + (uint16_t)sid, + self->next_tid_16b++, + cy_bytes_to_canard(message), + NULL); + } + if (ok) { + return CY_OK; + } + if (owner->canard.err.oom > e_oom) { + return CY_ERR_MEMORY; + } + if (owner->canard.err.tx_capacity > e_cap) { + return CY_ERR_CAPACITY; + } + return CY_ERR_ARGUMENT; +} + +// ===================================================================================================================== +// SUBJECT READER + +static void build_phony_header(subject_reader_pinned_t* const self, const uint32_t subject_id) +{ + (void)memset(self->phony_header, 0, HEADER_BYTES); + self->phony_header[3] = 0xFFU; // lage = -1 (int8_t) + le_serialize_u32(&self->phony_header[4], (uint32_t)(UINT32_MAX - subject_id)); + char decimal[11]; + const size_t dlen = uint_to_decimal(subject_id, decimal); + le_serialize_u64(&self->phony_header[8], rapidhash(decimal, dlen)); +} + +static void reader_set_extent(subject_reader_t* const self, const size_t extent) +{ + self->base.extent = extent; + self->sub_16b.extent = extent; + subject_reader_pinned_t* const pinned = as_pinned(self); + if (pinned != NULL) { + pinned->sub_13b.extent = (extent > HEADER_BYTES) ? (extent - HEADER_BYTES) : 0; + } +} + +static void tombstone_remove(cy_can_t* const owner, subject_reader_t* const self) +{ + assert((owner != NULL) && (self != NULL)); + if (self->prev_tombstone != NULL) { + self->prev_tombstone->next_tombstone = self->next_tombstone; + } else { + owner->tombstone_head = self->next_tombstone; + } + if (self->next_tombstone != NULL) { + self->next_tombstone->prev_tombstone = self->prev_tombstone; + } else { + owner->tombstone_tail = self->prev_tombstone; + } + self->prev_tombstone = NULL; + self->next_tombstone = NULL; +} + +static void tombstone_enqueue(cy_can_t* const owner, subject_reader_t* const self) +{ + assert((owner != NULL) && (self != NULL)); + self->prev_tombstone = owner->tombstone_tail; + self->next_tombstone = NULL; + if (owner->tombstone_tail != NULL) { + owner->tombstone_tail->next_tombstone = self; + } else { + owner->tombstone_head = self; + } + owner->tombstone_tail = self; +} + +static subject_reader_t* tombstone_pop(cy_can_t* const owner) +{ + subject_reader_t* const out = owner->tombstone_head; + if (out != NULL) { + tombstone_remove(owner, out); + } + return out; +} + +static subject_reader_t* reader_try_revive(cy_can_t* const owner, const uint32_t subject_id, const size_t extent) +{ + canard_subscription_t* const incumbent = + canard_find_subscription(&owner->canard, canard_kind_message_16b, (uint16_t)subject_id); + if (incumbent == NULL) { + return NULL; + } + subject_reader_t* const self = (subject_reader_t*)incumbent->user_context; + assert((self != NULL) && (self->owner == owner) && (self->base.subject_id == subject_id)); + if (as_pinned(self) != NULL) { + canard_subscription_t* const incumbent_13b = + canard_find_subscription(&owner->canard, canard_kind_message_13b, (uint16_t)subject_id); + assert((incumbent_13b != NULL) && (((subject_reader_t*)incumbent_13b->user_context) == self)); + if ((incumbent_13b == NULL) || (((subject_reader_t*)incumbent_13b->user_context) != self)) { + return NULL; + } + } + tombstone_remove(owner, self); + if (extent > self->base.extent) { + reader_set_extent(self, extent); + } + return self; +} + +/// Finalize a reader: unsubscribe from canard and free memory. Does NOT unlink from any list. +static void reader_finalize(cy_can_t* const owner, subject_reader_t* const self) +{ + assert((owner != NULL) && (self != NULL)); + canard_unsubscribe(&owner->canard, &self->sub_16b); + subject_reader_pinned_t* const pinned = as_pinned(self); + if (pinned != NULL) { + canard_unsubscribe(&owner->canard, &pinned->sub_13b); + } + owner->vtable->realloc(owner->user, self, 0); +} + +static cy_subject_reader_t* v_subject_reader_new(cy_platform_t* const base, + const uint32_t subject_id, + const size_t extent) +{ + cy_can_t* const owner = (cy_can_t*)base; + const bool pinned = (subject_id <= CY_SUBJECT_ID_PINNED_MAX); + const size_t sz = pinned ? sizeof(subject_reader_pinned_t) : sizeof(subject_reader_t); + subject_reader_t* const revived = reader_try_revive(owner, subject_id, extent); + if (revived != NULL) { + CY_TRACE(owner->base.cy, "CAN revive S%08jx extent=%zu", (uintmax_t)subject_id, revived->base.extent); + return (cy_subject_reader_t*)revived; + } + + subject_reader_t* const self = (subject_reader_t*)owner->vtable->realloc(owner->user, NULL, sz); + if (self == NULL) { + return NULL; + } + (void)memset(self, 0, sz); + self->base.subject_id = subject_id; + self->base.extent = extent; + self->owner = owner; + + // The extent from Cy already includes the header overhead. + canard_subscription_t* const sub_16b = canard_subscribe_16b(&owner->canard, + &self->sub_16b, + (uint16_t)subject_id, + extent, + CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us, + &sub_vtable_16b); + assert(sub_16b == &self->sub_16b); + if (sub_16b != &self->sub_16b) { + owner->vtable->realloc(owner->user, self, 0); + return NULL; + } + self->sub_16b.user_context = self; + + if (pinned) { + subject_reader_pinned_t* const p = (subject_reader_pinned_t*)self; + // 13-bit payload does not include the Cy header; we prepend it ourselves. + const size_t extent_13b = (extent > HEADER_BYTES) ? (extent - HEADER_BYTES) : 0; + assert(canard_find_subscription(&owner->canard, canard_kind_message_13b, (uint16_t)subject_id) == NULL); + canard_subscription_t* const sub_13b = canard_subscribe_13b(&owner->canard, + &p->sub_13b, + (uint16_t)subject_id, + extent_13b, + CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us, + &sub_vtable_13b); + assert(sub_13b == &p->sub_13b); + if (sub_13b != &p->sub_13b) { + canard_unsubscribe(&owner->canard, &self->sub_16b); + owner->vtable->realloc(owner->user, self, 0); + return NULL; + } + p->sub_13b.user_context = self; + build_phony_header(p, subject_id); + } + + CY_TRACE(owner->base.cy, "CAN reader S%08jx extent=%zu pinned=%d", (uintmax_t)subject_id, extent, (int)pinned); + return (cy_subject_reader_t*)self; +} + +/// Tombstone a reader: unlink from main list, add to tombstone list for deferred finalization. +static void v_subject_reader_destroy(cy_platform_t* const platform, cy_subject_reader_t* const base) +{ + cy_can_t* const owner = (cy_can_t*)platform; + subject_reader_t* const self = (subject_reader_t*)base; + tombstone_enqueue(owner, self); +} + +static void v_subject_reader_extent_set(cy_platform_t* const base, + cy_subject_reader_t* const reader_base, + const size_t extent) +{ + (void)base; + subject_reader_t* const self = (subject_reader_t*)reader_base; + reader_set_extent(self, extent); +} + +// ===================================================================================================================== +// UNICAST + +static cy_err_t v_unicast_send(cy_platform_t* const base, + const cy_lane_t* const lane, + const cy_us_t deadline, + const cy_bytes_t message) +{ + cy_can_t* const owner = (cy_can_t*)base; + const uint_least8_t remote = lane->ctx.state[0]; + assert(remote <= CANARD_NODE_ID_MAX); + const uint64_t e_oom = owner->canard.err.oom; + const uint64_t e_cap = owner->canard.err.tx_capacity; + const uint_least8_t tid = owner->unicast_tid[remote]; + owner->unicast_tid[remote] = (uint_least8_t)((tid + 1U) % CANARD_TRANSFER_ID_MODULO); + const bool ok = canard_request(&owner->canard, + deadline, + (canard_prio_t)lane->prio, + UNICAST_SERVICE_ID, + remote, + tid, + cy_bytes_to_canard(message), + NULL); + if (ok) { + return CY_OK; + } + if (owner->canard.err.oom > e_oom) { + return CY_ERR_MEMORY; + } + if (owner->canard.err.tx_capacity > e_cap) { + return CY_ERR_CAPACITY; + } + return CY_ERR_ARGUMENT; +} + +static void v_unicast_extent_set(cy_platform_t* const base, const size_t extent) +{ + cy_can_t* const owner = (cy_can_t*)base; + if (extent > owner->unicast_sub.extent) { + owner->unicast_sub.extent = extent; + } +} + +// ===================================================================================================================== +// EVENT LOOP + +static cy_err_t v_spin(cy_platform_t* const base, const cy_us_t deadline) +{ + cy_can_t* const owner = (cy_can_t*)base; + const uint_least8_t ibm = (uint_least8_t)((1U << owner->iface_count) - 1U); + while (true) { + canard_poll(&owner->canard, ibm); + { + subject_reader_t* const rd = tombstone_pop(owner); + if (rd != NULL) { + reader_finalize(owner, rd); + } + } + const uint_least8_t tx_pending = canard_pending_ifaces(&owner->canard); + cy_can_rx_t frame = { 0 }; + if (owner->vtable->rx(owner->user, &frame, deadline, tx_pending)) { + const canard_bytes_t can_data = { .size = frame.len, .data = frame.data }; + (void)canard_ingest_frame(&owner->canard, frame.timestamp, frame.iface_index, frame.can_id, can_data); + if (frame.timestamp > deadline) { + break; + } + } else { + if (owner->vtable->now(owner->user) > deadline) { + break; + } + } + } + return CY_OK; +} + +// ===================================================================================================================== +// MISC + +static cy_us_t v_now(cy_platform_t* const base) +{ + const cy_can_t* const owner = (const cy_can_t*)base; + return owner->vtable->now(owner->user); +} + +static void* v_realloc(cy_platform_t* const base, void* const ptr, const size_t new_size) +{ + const cy_can_t* const owner = (const cy_can_t*)base; + return owner->vtable->realloc(owner->user, ptr, new_size); +} + +static uint64_t v_random(cy_platform_t* const base) +{ + cy_can_t* const owner = (cy_can_t*)base; + return owner->vtable->random(owner->user); +} + +static const cy_platform_vtable_t platform_vtable = { .subject_writer_new = v_subject_writer_new, + .subject_writer_destroy = v_subject_writer_destroy, + .subject_writer_send = v_subject_writer_send, + .subject_reader_new = v_subject_reader_new, + .subject_reader_destroy = v_subject_reader_destroy, + .subject_reader_extent_set = v_subject_reader_extent_set, + .unicast = v_unicast_send, + .unicast_extent_set = v_unicast_extent_set, + .spin = v_spin, + .now = v_now, + .realloc = v_realloc, + .random = v_random }; + +// ===================================================================================================================== +// PUBLIC API + +cy_platform_t* cy_can_new(const uint_least8_t iface_count, + const size_t tx_queue_capacity, + const size_t filter_count, + const cy_can_vtable_t* const vtable, + void* const user) +{ + if ((vtable == NULL) || (vtable->tx_classic == NULL) || (vtable->rx == NULL) || (vtable->now == NULL) || + (vtable->realloc == NULL) || (vtable->random == NULL) || (iface_count == 0) || + (iface_count > CANARD_IFACE_COUNT)) { + return NULL; + } + cy_can_t* const self = (cy_can_t*)vtable->realloc(user, NULL, sizeof(cy_can_t)); + if (self == NULL) { + return NULL; + } + (void)memset(self, 0, sizeof(*self)); + self->vtable = vtable; + self->user = user; + self->iface_count = iface_count; + + self->base.subject_id_modulus = CY_SUBJECT_ID_MODULUS_16bit; + self->base.vtable = &platform_vtable; + + const bool filtering_enabled = (filter_count > 0U) && (vtable->filter != NULL); + const canard_vtable_t* const canard_vtable = filtering_enabled ? &canard_vtbl_filter : &canard_vtbl_no_filter; + const bool ok = canard_new(&self->canard, + canard_vtable, + make_mem_set(self), + tx_queue_capacity, + vtable->random(user), + filtering_enabled ? filter_count : 0U); + if (!ok) { + vtable->realloc(user, self, 0); + return NULL; + } + self->canard.tx.fd = (vtable->tx_fd != NULL); + self->canard.user_context = self; + + canard_subscription_t* const sub_uni = canard_subscribe_request(&self->canard, + &self->unicast_sub, + UNICAST_SERVICE_ID, + UNICAST_EXTENT_INITIAL, + CANARD_DEFAULT_TRANSFER_ID_TIMEOUT_us, + &sub_vtable_unicast); + if (sub_uni != &self->unicast_sub) { + canard_destroy(&self->canard); + vtable->realloc(user, self, 0); + return NULL; + } + self->unicast_sub.user_context = self; + return &self->base; +} + +void* cy_can_user(const cy_platform_t* const base) { return ((const cy_can_t*)base)->user; } + +void cy_can_destroy(cy_platform_t* const base) +{ + cy_can_t* const owner = (cy_can_t*)base; + while (owner->tombstone_head != NULL) { + subject_reader_t* const rd = tombstone_pop(owner); + assert(rd != NULL); + reader_finalize(owner, rd); + } + canard_unsubscribe(&owner->canard, &owner->unicast_sub); + canard_destroy(&owner->canard); + owner->vtable->realloc(owner->user, owner, 0); +} diff --git a/cy_can/cy_can.h b/cy_can/cy_can.h new file mode 100644 index 0000000..9b62aa8 --- /dev/null +++ b/cy_can/cy_can.h @@ -0,0 +1,94 @@ +// ____ ______ __ __ +// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / +// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / +// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / +// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ +// /_/ /____/_/ +// +// A Cy platform layer for Cyphal/CAN with v1.0 interoperability. +// This module is fully platform-agnostic; platform I/O is delegated to the cy_can_vtable_t provided by the user. +// +// See cy_can_socketcan.h for a Linux SocketCAN implementation of the vtable. +// Baremetal/RTOS-based platforms are expected to provide their own vtable implementations, perhaps using the GNU/Linux +// implementation as a reference. +// +// Copyright (c) Pavel Kirienko + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/// A received CAN frame (classic or FD). +typedef struct +{ + cy_us_t timestamp; ///< Monotonic reception timestamp in microseconds, in cy_can_vtable_t::now() timebase. + uint32_t can_id; ///< 29-bit extended CAN ID. + uint_least8_t iface_index; ///< The redundant interface the frame was received from. + uint_least8_t len; ///< Data length: 0-8 for classic CAN, 0-64 for CAN FD. + bool fd; ///< True if this is a CAN FD frame. + uint_least8_t data[64]; +} cy_can_rx_t; + +/// Platform-specific CAN driver abstraction. A single vtable manages ALL redundant interfaces. +/// All functions are non-blocking except rx(), which may block up to the specified deadline. +typedef struct +{ + /// Transmit a classic CAN frame (up to 8 bytes) on the given interface. + /// Returns true if the frame should be removed from the upstream TX queue (i.e., if the frame was accepted + /// for transmission or if it encountered a fatal failure and no further attempts are needed). + /// Returns false if the underlying CAN controller is not ready to accept a new frame (e.g., no free TX mailbox); + /// the caller will retry later. + bool (*tx_classic)(void* user, uint_least8_t iface_index, uint32_t can_id, const void* data, uint_least8_t len); + + /// Transmit a CAN FD frame (up to 64 bytes) on the given interface. + /// Set to NULL if the underlying driver does not support CAN FD; all interfaces share the same FD capability. + bool (*tx_fd)(void* user, uint_least8_t iface_index, uint32_t can_id, const void* data, uint_least8_t len); + + /// Poll all redundant interfaces for a received frame. Returns true if a frame was received. + /// The implementation may block up to the given deadline; baremetal implementations may ignore the deadline + /// and return immediately if no frame is available. + /// The tx_pending_iface_bitmap indicates which interfaces have pending TX frames; the implementation should + /// also unblock when any of those interfaces become writable, to allow the caller to retry transmissions. + bool (*rx)(void* user, cy_can_rx_t* out_frame, cy_us_t deadline, uint_least8_t tx_pending_iface_bitmap); + + /// Replace the hardware acceptance filter configuration with the supplied filter set. + /// This callback is optional; if NULL, libcanard filtering is disabled even if filter_count passed to cy_can_new() + /// is nonzero. The callback is only invoked from canard_poll(). + bool (*filter)(void* user, size_t filter_count, const canard_filter_t* filters); + + /// Returns the current monotonic time in microseconds. Must be non-negative and non-decreasing. + cy_us_t (*now)(void* user); + + /// Standard realloc semantics; if size is zero, the call shall behave as free. + void* (*realloc)(void* user, void* ptr, size_t size); + + /// Returns a random 64-bit unsigned integer. See cy_platform_vtable_t for the seeding recommendations. + uint64_t (*random)(void* user); +} cy_can_vtable_t; + +/// Create a new CAN platform instance. The node-ID will be allocated automatically by libcanard. +/// The constructor will invoke vtable random() and realloc() immediately. +/// The filter_count is the number of acceptance filters available to libcanard; pass zero to disable filtering. +/// Filtering is also disabled if vtable->filter is NULL. +/// Returns NULL on failure. The iface_count must be in [1, CANARD_IFACE_COUNT]. +cy_platform_t* cy_can_new(const uint_least8_t iface_count, + const size_t tx_queue_capacity, + const size_t filter_count, + const cy_can_vtable_t* const vtable, + void* const user); + +/// Returns the user context pointer that was passed to cy_can_new(). +void* cy_can_user(const cy_platform_t* const base); + +/// Requires the Cy instance to be unlinked first and all Cy-allocated resources freed. +void cy_can_destroy(cy_platform_t* const base); + +#ifdef __cplusplus +} +#endif diff --git a/cy_can/cy_can_socketcan.c b/cy_can/cy_can_socketcan.c new file mode 100644 index 0000000..0dce28f --- /dev/null +++ b/cy_can/cy_can_socketcan.c @@ -0,0 +1,318 @@ +// ____ ______ __ __ +// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / +// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / +// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / +// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ +// /_/ /____/_/ +// +// Copyright (c) Pavel Kirienko + +// Feature test macros must come before any system headers. +// NOLINTBEGIN(bugprone-reserved-identifier) +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +// NOLINTEND(bugprone-reserved-identifier) + +#include "cy_can_socketcan.h" +#include +#include + +#define RAPIDHASH_COMPACT +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +typedef struct +{ + int sock_fd[CANARD_IFACE_COUNT]; + uint_least8_t iface_count; + bool fd_capable; + uint64_t prng_state; +} socketcan_t; + +static const size_t socketcan_filter_count = 511U; + +static cy_us_t socketcan_now(void) +{ + struct timespec ts; + (void)clock_gettime(CLOCK_MONOTONIC, &ts); + return ((cy_us_t)ts.tv_sec * 1000000LL) + (cy_us_t)(ts.tv_nsec / 1000L); +} + +// ===================================================================================================================== +// VTABLE IMPLEMENTATION + +static bool v_tx_classic(void* const user, + const uint_least8_t iface_index, + const uint32_t can_id, + const void* const data, + const uint_least8_t len) +{ + const socketcan_t* const self = (const socketcan_t*)user; + assert(iface_index < self->iface_count); + struct can_frame frame = { .can_id = can_id | CAN_EFF_FLAG, .can_dlc = len }; + if ((data != NULL) && (len > 0)) { + (void)memcpy(frame.data, data, (len <= 8) ? len : 8); + } + const ssize_t res = write(self->sock_fd[iface_index], &frame, sizeof(frame)); + return res == (ssize_t)sizeof(frame); +} + +static bool v_tx_fd(void* const user, + const uint_least8_t iface_index, + const uint32_t can_id, + const void* const data, + const uint_least8_t len) +{ + const socketcan_t* const self = (const socketcan_t*)user; + assert(iface_index < self->iface_count); + struct canfd_frame frame = { .can_id = can_id | CAN_EFF_FLAG, .len = len, .flags = CANFD_FDF }; + if ((data != NULL) && (len > 0)) { + (void)memcpy(frame.data, data, (len <= 64) ? len : 64); + } + const ssize_t res = write(self->sock_fd[iface_index], &frame, sizeof(frame)); + return res == (ssize_t)sizeof(frame); +} + +static bool v_rx(void* const user, + cy_can_rx_t* const out_frame, + const cy_us_t deadline, + const uint_least8_t tx_pending_iface_bitmap) +{ + socketcan_t* const self = (socketcan_t*)user; + struct pollfd fds[CANARD_IFACE_COUNT]; + for (uint_least8_t i = 0; i < self->iface_count; i++) { + fds[i].fd = self->sock_fd[i]; + fds[i].events = POLLIN; + fds[i].revents = 0; + if (((tx_pending_iface_bitmap >> i) & 1U) != 0) { // NOLINT(*-signed-bitwise) + fds[i].events |= POLLOUT; // NOLINT(*-signed-bitwise) + } + } + const cy_us_t now = socketcan_now(); + const int timeout_ms = (deadline > now) ? (int)((deadline - now) / 1000LL) : 0; + const int ret = poll(fds, self->iface_count, timeout_ms); + if (ret <= 0) { + return false; + } + for (uint_least8_t i = 0; i < self->iface_count; i++) { + if ((fds[i].revents & POLLIN) == 0) { // NOLINT(*-signed-bitwise) + continue; + } + out_frame->iface_index = i; + out_frame->timestamp = socketcan_now(); // embedded systems would typically sample the hardware clock + if (self->fd_capable) { + struct canfd_frame frame; + const ssize_t n = read(self->sock_fd[i], &frame, sizeof(frame)); + if (n < (ssize_t)sizeof(struct can_frame)) { + continue; + } + out_frame->can_id = frame.can_id & CAN_EFF_MASK; + out_frame->fd = ((size_t)n == sizeof(struct canfd_frame)); + out_frame->len = out_frame->fd ? frame.len : ((struct can_frame*)&frame)->can_dlc; + if (out_frame->len > 64) { + out_frame->len = 64; + } + (void)memcpy(out_frame->data, frame.data, out_frame->len); + } else { + struct can_frame frame; + const ssize_t n = read(self->sock_fd[i], &frame, sizeof(frame)); + if (n < (ssize_t)sizeof(frame)) { + continue; + } + out_frame->can_id = frame.can_id & CAN_EFF_MASK; + out_frame->fd = false; + out_frame->len = frame.can_dlc; + if (out_frame->len > 8) { + out_frame->len = 8; + } + (void)memcpy(out_frame->data, frame.data, out_frame->len); + } + return true; + } + return false; +} + +static bool v_filter(void* const user, const size_t filter_count, const canard_filter_t* const filters) +{ + const socketcan_t* const self = (const socketcan_t*)user; + const struct can_filter empty = { 0 }; + struct can_filter* raw = NULL; + if ((filter_count > 0U) && (filters == NULL)) { + return false; + } + if (filter_count > 0U) { + raw = (struct can_filter*)calloc(filter_count, sizeof(struct can_filter)); + if (raw == NULL) { + return false; + } + for (size_t i = 0; i < filter_count; i++) { + raw[i].can_id = CAN_EFF_FLAG | (filters[i].extended_can_id & CAN_EFF_MASK); + raw[i].can_mask = CAN_EFF_FLAG | CAN_RTR_FLAG | (filters[i].extended_mask & CAN_EFF_MASK); + } + } + const void* const optval = (filter_count > 0U) ? (const void*)raw : (const void*)∅ + bool ok = true; + for (uint_least8_t i = 0; i < self->iface_count; i++) { + if (setsockopt(self->sock_fd[i], + SOL_CAN_RAW, + CAN_RAW_FILTER, + optval, + (socklen_t)(filter_count * sizeof(struct can_filter))) < 0) { + ok = false; + break; + } + } + free(raw); + return ok; +} + +static cy_us_t v_now(void* const user) +{ + (void)user; + return socketcan_now(); +} + +static void* v_realloc(void* const user, void* const ptr, const size_t size) +{ + (void)user; + if (size > 0) { + return realloc(ptr, size); + } + free(ptr); + return NULL; +} + +static uint64_t v_random(void* const user) +{ + socketcan_t* const self = (socketcan_t*)user; + self->prng_state += 0xA0761D6478BD642FULL; + return rapidhash_withSeed(&self->prng_state, sizeof(uint64_t), (uintptr_t)self); +} + +static const cy_can_vtable_t socketcan_vtable_fd = { .tx_classic = v_tx_classic, + .tx_fd = v_tx_fd, + .rx = v_rx, + .filter = v_filter, + .now = v_now, + .realloc = v_realloc, + .random = v_random }; +static const cy_can_vtable_t socketcan_vtable_classic = { .tx_classic = v_tx_classic, + .tx_fd = NULL, // No FD support. + .rx = v_rx, + .filter = v_filter, + .now = v_now, + .realloc = v_realloc, + .random = v_random }; + +// ===================================================================================================================== +// PUBLIC API + +cy_platform_t* cy_can_socketcan_new(const uint_least8_t iface_count, + const char* const iface_names[], + const size_t tx_queue_capacity) +{ + if ((iface_count == 0) || (iface_count > CANARD_IFACE_COUNT) || (iface_names == NULL)) { + return NULL; + } + socketcan_t* const self = (socketcan_t*)calloc(1, sizeof(socketcan_t)); + if (self == NULL) { + return NULL; + } + self->iface_count = iface_count; + self->fd_capable = true; // Assume FD until proven otherwise. + for (uint_least8_t i = 0; i < CANARD_IFACE_COUNT; i++) { + self->sock_fd[i] = -1; + } + + // Seed the PRNG from best available source. + { + const int fd = open("/dev/urandom", O_RDONLY); + const size_t sz = sizeof(self->prng_state); + const bool ok = (fd >= 0) && (read(fd, &self->prng_state, sz) == (ssize_t)sz); + (void)close(fd); + if (!ok) { + self->prng_state = ((uint64_t)socketcan_now()) ^ (uint64_t)time(NULL) ^ ((uint64_t)(uintptr_t)self); + } + } + + for (uint_least8_t i = 0; i < iface_count; i++) { + const int sock = socket(PF_CAN, SOCK_RAW, CAN_RAW); + if (sock < 0) { + goto fail; + } + self->sock_fd[i] = sock; + // Bind to the named interface. + struct ifreq ifr; + (void)memset(&ifr, 0, sizeof(ifr)); + (void)strncpy(ifr.ifr_name, iface_names[i], sizeof(ifr.ifr_name) - 1); + if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0) { + goto fail; + } + struct sockaddr_can addr; + (void)memset(&addr, 0, sizeof(addr)); + addr.can_family = AF_CAN; + addr.can_ifindex = ifr.ifr_ifindex; + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + goto fail; + } + // Try enabling CAN FD. If it fails on any interface, disable FD for all. + { + const int enable = 1; + if (setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &enable, sizeof(enable)) < 0) { + self->fd_capable = false; + } + } + // Non-blocking mode. + { + const int flags = fcntl(sock, F_GETFL, 0); + if (flags >= 0) { + (void)fcntl(sock, F_SETFL, flags | O_NONBLOCK); // NOLINT(*-signed-bitwise) + } + } + } + + const cy_can_vtable_t* const vtable = self->fd_capable ? &socketcan_vtable_fd : &socketcan_vtable_classic; + cy_platform_t* const result = cy_can_new(iface_count, tx_queue_capacity, socketcan_filter_count, vtable, self); + if (result == NULL) { + goto fail; + } + return result; + +fail: + for (uint_least8_t i = 0; i < CANARD_IFACE_COUNT; i++) { + if (self->sock_fd[i] >= 0) { + (void)close(self->sock_fd[i]); + } + } + free(self); + return NULL; +} + +void cy_can_socketcan_destroy(cy_platform_t* const base) +{ + if (base == NULL) { + return; + } + socketcan_t* const self = (socketcan_t*)cy_can_user(base); + cy_can_destroy(base); + for (uint_least8_t i = 0; i < CANARD_IFACE_COUNT; i++) { + if (self->sock_fd[i] >= 0) { + (void)close(self->sock_fd[i]); + } + } + free(self); +} diff --git a/cy_can/cy_can_socketcan.h b/cy_can/cy_can_socketcan.h new file mode 100644 index 0000000..ef60f25 --- /dev/null +++ b/cy_can/cy_can_socketcan.h @@ -0,0 +1,33 @@ +// ____ ______ __ __ +// / __ `____ ___ ____ / ____/_ ______ / /_ ____ / / +// / / / / __ `/ _ `/ __ `/ / / / / / __ `/ __ `/ __ `/ / +// / /_/ / /_/ / __/ / / / /___/ /_/ / /_/ / / / / /_/ / / +// `____/ .___/`___/_/ /_/`____/`__, / .___/_/ /_/`__,_/_/ +// /_/ /____/_/ +// +// A cy_can_vtable_t implementation for GNU/Linux SocketCAN. +// This module is only usable on GNU/Linux (and potentially some RTOS that implement SocketCAN-like APIs). +// +// Copyright (c) Pavel Kirienko + +#pragma once + +#include "cy_can.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/// Create a SocketCAN-backed CAN platform instance bound to the given interface names (e.g., "can0", "can1"). +/// All interfaces must share the same CAN FD capability; FD is auto-detected. +/// Returns NULL on failure (e.g., interface not found, socket error). +cy_platform_t* cy_can_socketcan_new(const uint_least8_t iface_count, + const char* const iface_names[], + const size_t tx_queue_capacity); + +void cy_can_socketcan_destroy(cy_platform_t* const base); + +#ifdef __cplusplus +} +#endif diff --git a/cy_can/tests/CMakeLists.txt b/cy_can/tests/CMakeLists.txt new file mode 100644 index 0000000..557caef --- /dev/null +++ b/cy_can/tests/CMakeLists.txt @@ -0,0 +1,119 @@ +# Copyright (c) Pavel Kirienko + +cmake_minimum_required(VERSION 3.24) + +set(cy_can_tests_root ${CMAKE_CURRENT_SOURCE_DIR}) +set(unity_root "${CMAKE_SOURCE_DIR}/lib/unity") +set(canard_root "${CMAKE_SOURCE_DIR}/lib/libcanard/libcanard") + +add_library(cy_can_tests_unity STATIC "${unity_root}/src/unity.c") +target_include_directories(cy_can_tests_unity SYSTEM PUBLIC "${unity_root}/src") +target_compile_definitions(cy_can_tests_unity PUBLIC UNITY_INCLUDE_DOUBLE=1 UNITY_OUTPUT_COLOR=1 UNITY_SUPPORT_64=1) +target_compile_options( + cy_can_tests_unity + PRIVATE + -m32 + -Wno-sign-conversion + -Wno-conversion + -Wno-switch-enum + -Wno-float-equal + -Wno-double-promotion + -Wno-missing-declarations +) +set_target_properties( + cy_can_tests_unity + PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR OFF + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +add_library(cy_can_tests_canard STATIC "${canard_root}/canard.c") +target_include_directories(cy_can_tests_canard SYSTEM PUBLIC "${canard_root}") +target_compile_options(cy_can_tests_canard PRIVATE -m32 -Wno-cast-align) +set_target_properties( + cy_can_tests_canard + PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR OFF + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +function(cy_can_add_test name) + add_executable( + ${name} + ${ARGN} + ${CMAKE_SOURCE_DIR}/cy/cy.c + ${CMAKE_SOURCE_DIR}/cy_can/cy_can.c + ${cy_can_tests_root}/test_support.c + ) + target_include_directories( + ${name} + PRIVATE + ${CMAKE_SOURCE_DIR}/cy + ${CMAKE_SOURCE_DIR}/cy_can + ${canard_root} + ${cy_can_tests_root} + ) + target_link_libraries(${name} PRIVATE cy_can_tests_unity cy_can_tests_canard) + target_compile_options(${name} PRIVATE -m32) + target_link_options(${name} PRIVATE -m32) + set_target_properties( + ${name} + PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR ON + ) + add_test(NAME run_${name} COMMAND ${name}) +endfunction() + +cy_can_add_test(test_api_can_constructor test_api_can_constructor.c) +cy_can_add_test(test_api_can_pubsub test_api_can_pubsub.c) +cy_can_add_test(test_api_can_reliable_rpc test_api_can_reliable_rpc.c) +cy_can_add_test(test_api_can_redundancy_lifecycle test_api_can_redundancy_lifecycle.c) +cy_can_add_test(test_api_can_tombstone_revival test_api_can_tombstone_revival.c) +cy_can_add_test(test_api_can_failures test_api_can_failures.c) + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_executable( + test_api_can_socketcan_e2e + test_api_can_socketcan_e2e.c + ${CMAKE_SOURCE_DIR}/cy/cy.c + ${CMAKE_SOURCE_DIR}/cy_can/cy_can.c + ${CMAKE_SOURCE_DIR}/cy_can/cy_can_socketcan.c + ) + target_include_directories( + test_api_can_socketcan_e2e + PRIVATE + ${CMAKE_SOURCE_DIR}/cy + ${CMAKE_SOURCE_DIR}/cy_can + ${canard_root} + ${cy_can_tests_root} + ) + target_link_libraries(test_api_can_socketcan_e2e PRIVATE cy_can_tests_unity cy_can_tests_canard) + target_compile_definitions(test_api_can_socketcan_e2e PRIVATE _POSIX_C_SOURCE=200809L) + target_compile_options(test_api_can_socketcan_e2e PRIVATE -m32) + target_link_options(test_api_can_socketcan_e2e PRIVATE -m32) + set_target_properties( + test_api_can_socketcan_e2e + PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR ON + ) + add_test(NAME run_test_api_can_socketcan_e2e COMMAND test_api_can_socketcan_e2e) + set_tests_properties(run_test_api_can_socketcan_e2e PROPERTIES SKIP_RETURN_CODE 77) +endif () diff --git a/cy_can/tests/test_api_can_constructor.c b/cy_can/tests/test_api_can_constructor.c new file mode 100644 index 0000000..3055e65 --- /dev/null +++ b/cy_can/tests/test_api_can_constructor.c @@ -0,0 +1,161 @@ +#include "test_support.h" +#include +#include +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +static void test_api_can_constructor_rejects_invalid_arguments(void) +{ + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, true); + + TEST_ASSERT_NULL(cy_can_new(0U, 16U, 0U, &node.vtable, &node)); + TEST_ASSERT_NULL(cy_can_new(CANARD_IFACE_COUNT + 1U, 16U, 0U, &node.vtable, &node)); + TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, NULL, &node)); + + { + cy_can_vtable_t v = node.vtable; + v.tx_classic = NULL; + TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node)); + } + { + cy_can_vtable_t v = node.vtable; + v.rx = NULL; + TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node)); + } + { + cy_can_vtable_t v = node.vtable; + v.now = NULL; + TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node)); + } + { + cy_can_vtable_t v = node.vtable; + v.realloc = NULL; + TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node)); + } + { + cy_can_vtable_t v = node.vtable; + v.random = NULL; + TEST_ASSERT_NULL(cy_can_new(1U, 16U, 0U, &v, &node)); + } + + can_test_node_destroy(&node); +} + +static void test_api_can_constructor_user_roundtrip_and_destroy(void) +{ + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, true); + can_test_node_make_platform(&node, 32U, 4U); + TEST_ASSERT_TRUE(cy_can_user(node.platform) == &node); + can_test_node_make_cy(&node, "ctor_roundtrip"); + can_test_node_destroy(&node); +} + +static void test_api_can_constructor_filtering_disabled_by_zero_count(void) +{ + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, true); + can_test_node_make_platform(&node, 32U, 0U); + can_test_node_make_cy(&node, "filter_zero"); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("111#111"), 64U); + TEST_ASSERT_NOT_NULL(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_spin(&node, spin_slice_us); + TEST_ASSERT_EQUAL_size_t(0U, node.filter_calls); + + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +static void test_api_can_constructor_filtering_disabled_by_null_callback(void) +{ + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + can_test_node_make_platform(&node, 32U, 4U); + can_test_node_make_cy(&node, "filter_null"); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("112#112"), 64U); + TEST_ASSERT_NOT_NULL(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_spin(&node, spin_slice_us); + TEST_ASSERT_EQUAL_size_t(0U, node.filter_calls); + + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +static void test_api_can_constructor_filtering_enabled_when_callback_and_count_present(void) +{ + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, true); + can_test_node_make_platform(&node, 32U, 8U); + can_test_node_make_cy(&node, "filter_enabled"); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("113#113"), 64U); + TEST_ASSERT_NOT_NULL(sub); + can_test_node_spin(&node, spin_slice_us); + TEST_ASSERT_TRUE(node.filter_calls > 0U); + TEST_ASSERT_TRUE(node.last_filter_count > 0U); + + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +static void test_api_can_constructor_filter_retries_after_failure(void) +{ + can_test_bus_t bus; + can_test_node_t node; + size_t first_filter_call_count = 0U; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, true); + can_test_node_make_platform(&node, 32U, 8U); + can_test_node_make_cy(&node, "filter_retry"); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("114#114"), 64U); + TEST_ASSERT_NOT_NULL(sub); + node.filter_failures_remaining = 1U; + + can_test_node_spin(&node, spin_slice_us); + first_filter_call_count = node.filter_calls; + TEST_ASSERT_TRUE(first_filter_call_count > 0U); + TEST_ASSERT_EQUAL_size_t(0U, node.filter_failures_remaining); + TEST_ASSERT_TRUE(first_filter_call_count >= 2U); + + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_can_constructor_rejects_invalid_arguments); + RUN_TEST(test_api_can_constructor_user_roundtrip_and_destroy); + RUN_TEST(test_api_can_constructor_filtering_disabled_by_zero_count); + RUN_TEST(test_api_can_constructor_filtering_disabled_by_null_callback); + RUN_TEST(test_api_can_constructor_filtering_enabled_when_callback_and_count_present); + RUN_TEST(test_api_can_constructor_filter_retries_after_failure); + return UNITY_END(); +} diff --git a/cy_can/tests/test_api_can_failures.c b/cy_can/tests/test_api_can_failures.c new file mode 100644 index 0000000..024e906 --- /dev/null +++ b/cy_can/tests/test_api_can_failures.c @@ -0,0 +1,115 @@ +#include "test_support.h" +#include +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +static void test_api_can_failures_constructor_oom(void) +{ + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, true); + can_test_heap_fail_after(&node.heap, 0U); + TEST_ASSERT_NULL(cy_can_new(1U, 32U, 4U, &node.vtable, &node)); + can_test_heap_allow_all(&node.heap); + can_test_node_destroy(&node); +} + +static void test_api_can_failures_subscribe_oom(void) +{ + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + can_test_node_make_platform(&node, 64U, 0U); + can_test_node_make_cy(&node, "fail_subscribe"); + + can_test_heap_fail_after(&node.heap, 0U); + TEST_ASSERT_NULL(cy_subscribe(node.cy, cy_str("400#400"), 64U)); + can_test_heap_allow_all(&node.heap); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +static void test_api_can_failures_receive_wrapper_oom_drops_message_without_leak(void) +{ + const uint8_t payload[] = { 0x71U, 0x72U, 0x73U, 0x74U }; + + can_test_bus_t bus; + can_test_node_t a; + can_test_node_t b; + can_test_bus_init(&bus); + can_test_node_prepare(&a, &bus, 1U, false, false); + can_test_node_prepare(&b, &bus, 1U, false, false); + can_test_node_make_platform(&a, 128U, 0U); + can_test_node_make_platform(&b, 128U, 0U); + can_test_node_make_cy(&a, "fail_rx_a"); + can_test_node_make_cy(&b, "fail_rx_b"); + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("401#401")); + cy_future_t* const sub = cy_subscribe(b.cy, cy_str("401#401"), 64U); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + + can_test_heap_fail_after(&b.heap, 0U); + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + can_test_spin_pair(&a, &b, 12U, spin_slice_us); + TEST_ASSERT_FALSE(cy_future_done(sub)); + + can_test_heap_allow_all(&b.heap); + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + can_test_node_destroy(&a); + can_test_node_destroy(&b); +} + +static void test_api_can_failures_tx_queue_capacity_is_reported(void) +{ + uint8_t payload[48]; + bool saw_capacity = false; + + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + can_test_node_make_platform(&node, 1U, 0U); + can_test_node_make_cy(&node, "fail_capacity"); + can_test_fill_payload(payload, sizeof(payload), 0x80U); + + cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("402#402")); + TEST_ASSERT_NOT_NULL(pub); + + for (size_t i = 0U; i < 8U; i++) { + const cy_err_t err = + cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }); + if (err == CY_ERR_CAPACITY) { + saw_capacity = true; + break; + } + } + TEST_ASSERT_TRUE(saw_capacity); + + cy_unadvertise(pub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_can_failures_constructor_oom); + RUN_TEST(test_api_can_failures_subscribe_oom); + RUN_TEST(test_api_can_failures_receive_wrapper_oom_drops_message_without_leak); + RUN_TEST(test_api_can_failures_tx_queue_capacity_is_reported); + return UNITY_END(); +} diff --git a/cy_can/tests/test_api_can_pubsub.c b/cy_can/tests/test_api_can_pubsub.c new file mode 100644 index 0000000..687aee9 --- /dev/null +++ b/cy_can/tests/test_api_can_pubsub.c @@ -0,0 +1,235 @@ +#include "test_support.h" +#include +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +static void spin_until_done(can_test_node_t* const node, cy_future_t* const future) +{ + TEST_ASSERT_NOT_NULL(node); + TEST_ASSERT_NOT_NULL(future); + for (size_t i = 0U; (i < 64U) && !cy_future_done(future); i++) { + can_test_node_spin(node, spin_slice_us); + } + TEST_ASSERT_TRUE(cy_future_done(future)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future)); +} + +static void test_api_can_pubsub_pinned_best_effort_uses_shorter_legacy_path(void) +{ + const uint8_t payload[] = { 0x11U, 0x22U, 0x33U }; + size_t verbatim_frames = 0U; + size_t pinned_frames = 0U; + + { + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + node.self_loopback = true; + can_test_node_make_platform(&node, 256U, 0U); + can_test_node_make_cy(&node, "pubsub_verbatim"); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("pubsub/verbatim"), 64U); + cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("pubsub/verbatim")); + TEST_ASSERT_NOT_NULL(sub); + TEST_ASSERT_NOT_NULL(pub); + can_test_spin_pair(&node, NULL, 4U, spin_slice_us); + can_test_node_reset_history(&node); + + TEST_ASSERT_EQUAL_INT( + CY_OK, + cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + spin_until_done(&node, sub); + verbatim_frames = node.tx_history_count; + TEST_ASSERT_TRUE(verbatim_frames > 1U); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); + } + + { + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + node.self_loopback = true; + can_test_node_make_platform(&node, 256U, 0U); + can_test_node_make_cy(&node, "pubsub_pinned"); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("123#123"), 64U); + cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("123#123")); + TEST_ASSERT_NOT_NULL(sub); + TEST_ASSERT_NOT_NULL(pub); + can_test_spin_pair(&node, NULL, 4U, spin_slice_us); + can_test_node_reset_history(&node); + + TEST_ASSERT_EQUAL_INT( + CY_OK, + cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + spin_until_done(&node, sub); + pinned_frames = node.tx_history_count; + TEST_ASSERT_EQUAL_size_t(1U, pinned_frames); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); + } + + TEST_ASSERT_TRUE(verbatim_frames > pinned_frames); +} + +static void test_api_can_pubsub_multiframe_extent_truncation(void) +{ + uint8_t payload[120]; + + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + node.self_loopback = true; + can_test_node_make_platform(&node, 256U, 0U); + can_test_node_make_cy(&node, "pubsub_extent"); + can_test_fill_payload(payload, sizeof(payload), 0x40U); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("pubsub/extent"), 40U); + cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("pubsub/extent")); + TEST_ASSERT_NOT_NULL(sub); + TEST_ASSERT_NOT_NULL(pub); + can_test_spin_pair(&node, NULL, 4U, spin_slice_us); + can_test_node_reset_history(&node); + + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(node.cy) + (40 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + spin_until_done(&node, sub); + TEST_ASSERT_TRUE(node.tx_history_count > 1U); + + { + uint8_t received[64]; + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + TEST_ASSERT_EQUAL_size_t(40U, cy_message_size(arrival.message.content)); + TEST_ASSERT_EQUAL_size_t(40U, can_test_message_read_all(arrival.message.content, received, sizeof(received))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, received, 40U); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +static void test_api_can_pubsub_fd_capable_uses_fd_frames(void) +{ + uint8_t payload[20]; + + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, true, false); + node.self_loopback = true; + can_test_node_make_platform(&node, 256U, 0U); + can_test_node_make_cy(&node, "pubsub_fd"); + can_test_fill_payload(payload, sizeof(payload), 0x60U); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("pubsub/fd"), 64U); + cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("pubsub/fd")); + TEST_ASSERT_NOT_NULL(sub); + TEST_ASSERT_NOT_NULL(pub); + can_test_spin_pair(&node, NULL, 4U, spin_slice_us); + can_test_node_reset_history(&node); + + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(node.cy) + (40 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + spin_until_done(&node, sub); + TEST_ASSERT_TRUE(node.tx_fd_calls > 0U); + + { + uint8_t received[32]; + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + TEST_ASSERT_TRUE(cy_message_size(arrival.message.content) >= sizeof(payload)); + TEST_ASSERT_EQUAL_size_t(sizeof(payload), + cy_message_read(arrival.message.content, 0U, sizeof(payload), received)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, received, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +static void test_api_can_pubsub_classic_only_emits_no_fd_frames(void) +{ + uint8_t payload[48]; + + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + node.self_loopback = true; + can_test_node_make_platform(&node, 256U, 0U); + can_test_node_make_cy(&node, "pubsub_classic"); + can_test_fill_payload(payload, sizeof(payload), 0x20U); + + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("125#125"), 64U); + cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("125#125")); + TEST_ASSERT_NOT_NULL(sub); + TEST_ASSERT_NOT_NULL(pub); + can_test_spin_pair(&node, NULL, 4U, spin_slice_us); + can_test_node_reset_history(&node); + + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(node.cy) + (40 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + spin_until_done(&node, sub); + TEST_ASSERT_EQUAL_size_t(0U, node.tx_fd_calls); + TEST_ASSERT_TRUE(node.tx_classic_calls > 0U); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_can_pubsub_pinned_best_effort_uses_shorter_legacy_path); + RUN_TEST(test_api_can_pubsub_multiframe_extent_truncation); + RUN_TEST(test_api_can_pubsub_fd_capable_uses_fd_frames); + RUN_TEST(test_api_can_pubsub_classic_only_emits_no_fd_frames); + return UNITY_END(); +} diff --git a/cy_can/tests/test_api_can_redundancy_lifecycle.c b/cy_can/tests/test_api_can_redundancy_lifecycle.c new file mode 100644 index 0000000..5403d67 --- /dev/null +++ b/cy_can/tests/test_api_can_redundancy_lifecycle.c @@ -0,0 +1,219 @@ +#include "test_support.h" +#include +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +static void spin_until_done(can_test_node_t* const a, can_test_node_t* const b, cy_future_t* const future) +{ + TEST_ASSERT_NOT_NULL(future); + TEST_ASSERT_TRUE(can_test_spin_pair_until_future_done(a, b, future, 80U, spin_slice_us)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future)); +} + +static void test_api_can_redundancy_duplicate_ingress_is_delivered_once(void) +{ + const uint8_t payload[] = { 0x31U, 0x32U, 0x33U, 0x34U }; + + can_test_bus_t bus; + can_test_node_t a; + can_test_node_t b; + can_test_bus_init(&bus); + can_test_node_prepare(&a, &bus, 2U, false, false); + can_test_node_prepare(&b, &bus, 2U, false, false); + can_test_node_make_platform(&a, 256U, 0U); + can_test_node_make_platform(&b, 256U, 0U); + can_test_node_make_cy(&a, "redundancy_a"); + can_test_node_make_cy(&b, "redundancy_b"); + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("300#300")); + cy_future_t* const sub = cy_subscribe(b.cy, cy_str("300#300"), 64U); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + can_test_node_reset_history(&a); + + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + spin_until_done(&a, &b, sub); + TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 0U) > 0U); + TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 1U) > 0U); + TEST_ASSERT_EQUAL_UINT64(1U, cy_arrival_count(sub)); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + can_test_node_destroy(&a); + can_test_node_destroy(&b); +} + +static void test_api_can_redundancy_retries_backpressured_interface(void) +{ + const uint8_t payload[] = { 0x41U, 0x42U }; + + can_test_bus_t bus; + can_test_node_t a; + can_test_node_t b; + can_test_bus_init(&bus); + can_test_node_prepare(&a, &bus, 2U, false, false); + can_test_node_prepare(&b, &bus, 2U, false, false); + can_test_node_make_platform(&a, 256U, 0U); + can_test_node_make_platform(&b, 256U, 0U); + can_test_node_make_cy(&a, "backpressure_a"); + can_test_node_make_cy(&b, "backpressure_b"); + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("301#301")); + cy_future_t* const sub = cy_subscribe(b.cy, cy_str("301#301"), 64U); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + can_test_node_reset_history(&a); + a.tx_blocked[1] = 1U; + + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + can_test_spin_pair(&a, &b, 1U, spin_slice_us); + TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 0U) > 0U); + TEST_ASSERT_EQUAL_size_t(0U, a.tx_blocked[1]); + + can_test_spin_pair(&a, &b, 1U, spin_slice_us); + TEST_ASSERT_TRUE(can_test_node_count_records_on_iface(&a, 1U) > 0U); + spin_until_done(&a, &b, sub); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + can_test_node_destroy(&a); + can_test_node_destroy(&b); +} + +static void run_subscription_revival_case(const char* const topic_name, const char* const home) +{ + const uint8_t payload[] = { 0x51U, 0x52U, 0x53U, 0x54U, 0x55U }; + + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + node.self_loopback = true; + can_test_node_make_platform(&node, 256U, 0U); + can_test_node_make_cy(&node, home); + + cy_future_t* const first = cy_subscribe(node.cy, cy_str(topic_name), 64U); + cy_future_t* second = NULL; + cy_publisher_t* pub = NULL; + TEST_ASSERT_NOT_NULL(first); + cy_future_destroy(first); + + second = cy_subscribe(node.cy, cy_str(topic_name), 64U); + pub = cy_advertise(node.cy, cy_str(topic_name)); + TEST_ASSERT_NOT_NULL(second); + TEST_ASSERT_NOT_NULL(pub); + + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + for (size_t i = 0U; (i < 80U) && !cy_future_done(second); i++) { + can_test_node_spin(&node, spin_slice_us); + } + TEST_ASSERT_TRUE(cy_future_done(second)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(second)); + + { + const cy_arrival_t arrival = cy_arrival_move(second); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(second); + can_test_node_spin(&node, spin_slice_us); + can_test_node_destroy(&node); +} + +static void test_api_can_lifecycle_revives_verbatim_and_pinned_subscriptions(void) +{ + run_subscription_revival_case("lifecycle/verbatim", "revive_verbatim"); + run_subscription_revival_case("302#302", "revive_pinned"); +} + +static void test_api_can_lifecycle_recreates_cy_on_same_platform(void) +{ + const uint8_t payload[] = { 0x61U, 0x62U, 0x63U }; + + can_test_bus_t bus; + can_test_node_t node; + can_test_bus_init(&bus); + can_test_node_prepare(&node, &bus, 1U, false, false); + node.self_loopback = true; + can_test_node_make_platform(&node, 256U, 0U); + can_test_node_make_cy(&node, "first_cy"); + + { + cy_future_t* const first = cy_subscribe(node.cy, cy_str("303#303"), 64U); + TEST_ASSERT_NOT_NULL(first); + cy_future_destroy(first); + can_test_node_spin(&node, spin_slice_us); + } + cy_destroy(node.cy); + node.cy = NULL; + + can_test_node_make_cy(&node, "second_cy"); + { + cy_future_t* const sub = cy_subscribe(node.cy, cy_str("303#303"), 64U); + cy_publisher_t* const pub = cy_advertise(node.cy, cy_str("303#303")); + TEST_ASSERT_NOT_NULL(sub); + TEST_ASSERT_NOT_NULL(pub); + + TEST_ASSERT_EQUAL_INT( + CY_OK, + cy_publish(pub, cy_now(node.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL })); + for (size_t i = 0U; (i < 80U) && !cy_future_done(sub); i++) { + can_test_node_spin(&node, spin_slice_us); + } + TEST_ASSERT_TRUE(cy_future_done(sub)); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_node_spin(&node, spin_slice_us); + } + + can_test_node_destroy(&node); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_can_redundancy_duplicate_ingress_is_delivered_once); + RUN_TEST(test_api_can_redundancy_retries_backpressured_interface); + RUN_TEST(test_api_can_lifecycle_revives_verbatim_and_pinned_subscriptions); + RUN_TEST(test_api_can_lifecycle_recreates_cy_on_same_platform); + return UNITY_END(); +} diff --git a/cy_can/tests/test_api_can_reliable_rpc.c b/cy_can/tests/test_api_can_reliable_rpc.c new file mode 100644 index 0000000..fceef66 --- /dev/null +++ b/cy_can/tests/test_api_can_reliable_rpc.c @@ -0,0 +1,261 @@ +#include "test_support.h" +#include +#include +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +typedef struct +{ + size_t count; + size_t transient_error_count; + cy_err_t last_error; + uint64_t remote_id[4]; + uint64_t seqno[4]; + size_t size[4]; + uint8_t payload[4][64]; +} response_capture_t; + +typedef struct +{ + size_t calls; + cy_future_t* reliable_future; + uint8_t response_a[8]; + uint8_t response_b[8]; + size_t response_a_size; + size_t response_b_size; +} request_server_t; + +static void on_response_capture(cy_future_t* const future) +{ + response_capture_t* const cap = (response_capture_t*)cy_future_context(future).ptr[0]; + TEST_ASSERT_NOT_NULL(cap); + if (!cy_future_done(future)) { + cap->transient_error_count++; + cap->last_error = cy_future_error(future); + return; + } + if (cy_future_error(future) != CY_OK) { + cap->last_error = cy_future_error(future); + return; + } + { + const cy_response_t rsp = cy_response_move(future); + TEST_ASSERT_NOT_NULL(rsp.message.content); + TEST_ASSERT_TRUE(cap->count < 4U); + cap->remote_id[cap->count] = rsp.remote_id; + cap->seqno[cap->count] = rsp.seqno; + cap->size[cap->count] = + can_test_message_read_all(rsp.message.content, cap->payload[cap->count], sizeof(cap->payload[cap->count])); + cap->count++; + cy_message_refcount_dec(rsp.message.content); + } +} + +static void on_request_server(cy_future_t* const future) +{ + request_server_t* const server = (request_server_t*)cy_future_context(future).ptr[0]; + TEST_ASSERT_NOT_NULL(server); + if (!cy_future_done(future)) { + return; + } + { + cy_arrival_t arrival = cy_arrival_move(future); + if (arrival.message.content == NULL) { + return; + } + server->calls++; + TEST_ASSERT_EQUAL_INT(CY_OK, + cy_respond(&arrival.breadcrumb, + cy_now(arrival.breadcrumb.cy) + (20 * spin_slice_us), + (cy_bytes_t){ server->response_a_size, server->response_a, NULL })); + server->reliable_future = + cy_respond_reliable(&arrival.breadcrumb, + cy_now(arrival.breadcrumb.cy) + (20 * spin_slice_us), + (cy_bytes_t){ server->response_b_size, server->response_b, NULL }); + TEST_ASSERT_NOT_NULL(server->reliable_future); + cy_message_refcount_dec(arrival.message.content); + } +} + +static bool two_responses_received(void* const context) +{ + const response_capture_t* const cap = (const response_capture_t*)context; + return cap->count >= 2U; +} + +static void test_api_can_reliable_publish_success(void) +{ + const uint8_t payload[] = { 1U, 2U, 3U, 4U, 5U, 6U }; + + can_test_bus_t bus; + can_test_node_t a; + can_test_node_t b; + can_test_bus_init(&bus); + can_test_node_prepare(&a, &bus, 1U, false, false); + can_test_node_prepare(&b, &bus, 1U, false, false); + can_test_node_make_platform(&a, 256U, 0U); + can_test_node_make_platform(&b, 256U, 0U); + can_test_node_make_cy(&a, "reliable_pub_a"); + can_test_node_make_cy(&b, "reliable_pub_b"); + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("200#200")); + cy_future_t* const sub = cy_subscribe(b.cy, cy_str("200#200"), 64U); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + + cy_future_t* const fut = + cy_publish_reliable(pub, cy_now(a.cy) + (30 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }); + TEST_ASSERT_NOT_NULL(fut); + TEST_ASSERT_TRUE(can_test_spin_pair_until_future_done(&a, &b, fut, 80U, spin_slice_us)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(fut)); + TEST_ASSERT_TRUE(cy_future_done(sub)); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_future_destroy(fut); + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + can_test_node_destroy(&a); + can_test_node_destroy(&b); +} + +static void test_api_can_reliable_publish_times_out_when_ack_is_lost(void) +{ + const uint8_t payload[] = { 9U, 8U, 7U, 6U }; + + can_test_bus_t bus; + can_test_node_t a; + can_test_node_t b; + can_test_bus_init(&bus); + can_test_node_prepare(&a, &bus, 1U, false, false); + can_test_node_prepare(&b, &bus, 1U, false, false); + can_test_node_make_platform(&a, 256U, 0U); + can_test_node_make_platform(&b, 256U, 0U); + can_test_node_make_cy(&a, "reliable_timeout_a"); + can_test_node_make_cy(&b, "reliable_timeout_b"); + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("201#201")); + cy_future_t* const sub = cy_subscribe(b.cy, cy_str("201#201"), 64U); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + b.drop_tx[0] = true; + + cy_future_t* const fut = + cy_publish_reliable(pub, cy_now(a.cy) + (20 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }); + TEST_ASSERT_NOT_NULL(fut); + TEST_ASSERT_TRUE(can_test_spin_pair_until_future_done(&a, &b, fut, 80U, spin_slice_us)); + TEST_ASSERT_EQUAL_INT(CY_ERR_DELIVERY, cy_future_error(fut)); + TEST_ASSERT_TRUE(cy_future_done(sub)); + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + can_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_future_destroy(fut); + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_spin_pair(&a, &b, 4U, spin_slice_us); + can_test_node_destroy(&a); + can_test_node_destroy(&b); +} + +static void test_api_can_request_response_streaming_and_reliable_response(void) +{ + const uint8_t request_payload[] = { 0xAAU, 0xBBU, 0xCCU }; + + can_test_bus_t bus; + can_test_node_t client; + can_test_node_t server; + response_capture_t capture; + request_server_t server_state; + can_test_bus_init(&bus); + can_test_node_prepare(&client, &bus, 1U, false, false); + can_test_node_prepare(&server, &bus, 1U, false, false); + can_test_node_make_platform(&client, 256U, 0U); + can_test_node_make_platform(&server, 256U, 0U); + can_test_node_make_cy(&client, "rpc_client"); + can_test_node_make_cy(&server, "rpc_server"); + (void)memset(&capture, 0, sizeof(capture)); + (void)memset(&server_state, 0, sizeof(server_state)); + server_state.response_a[0] = 0x10U; + server_state.response_a[1] = 0x11U; + server_state.response_a[2] = 0x12U; + server_state.response_a_size = 3U; + server_state.response_b[0] = 0x20U; + server_state.response_b[1] = 0x21U; + server_state.response_b_size = 2U; + + cy_publisher_t* const pub = cy_advertise_client(client.cy, cy_str("202#202"), 64U); + cy_future_t* const sub = cy_subscribe(server.cy, cy_str("202#202"), 64U); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + { + cy_user_context_t ctx = CY_USER_CONTEXT_EMPTY; + ctx.ptr[0] = &server_state; + cy_future_context_set(sub, ctx); + cy_future_callback_set(sub, on_request_server); + } + can_test_spin_pair(&client, &server, 4U, spin_slice_us); + + cy_future_t* const req = cy_request(pub, + cy_now(client.cy) + (30 * spin_slice_us), + 10 * spin_slice_us, + (cy_bytes_t){ sizeof(request_payload), request_payload, NULL }); + TEST_ASSERT_NOT_NULL(req); + { + cy_user_context_t ctx = CY_USER_CONTEXT_EMPTY; + ctx.ptr[0] = &capture; + cy_future_context_set(req, ctx); + cy_future_callback_set(req, on_response_capture); + } + + TEST_ASSERT_TRUE( + can_test_spin_pair_until_condition(&client, &server, two_responses_received, &capture, 120U, spin_slice_us)); + TEST_ASSERT_EQUAL_size_t(1U, server_state.calls); + TEST_ASSERT_NOT_NULL(server_state.reliable_future); + TEST_ASSERT_TRUE( + can_test_spin_pair_until_future_done(&client, &server, server_state.reliable_future, 120U, spin_slice_us)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(server_state.reliable_future)); + TEST_ASSERT_EQUAL_size_t(2U, capture.count); + TEST_ASSERT_EQUAL_UINT64(0U, capture.seqno[0]); + TEST_ASSERT_EQUAL_UINT64(1U, capture.seqno[1]); + TEST_ASSERT_EQUAL_size_t(server_state.response_a_size, capture.size[0]); + TEST_ASSERT_EQUAL_size_t(server_state.response_b_size, capture.size[1]); + TEST_ASSERT_EQUAL_UINT8_ARRAY(server_state.response_a, capture.payload[0], server_state.response_a_size); + TEST_ASSERT_EQUAL_UINT8_ARRAY(server_state.response_b, capture.payload[1], server_state.response_b_size); + + cy_future_destroy(server_state.reliable_future); + cy_future_destroy(req); + cy_unadvertise(pub); + cy_future_destroy(sub); + can_test_spin_pair(&client, &server, 4U, spin_slice_us); + can_test_node_destroy(&client); + can_test_node_destroy(&server); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_can_reliable_publish_success); + RUN_TEST(test_api_can_reliable_publish_times_out_when_ack_is_lost); + RUN_TEST(test_api_can_request_response_streaming_and_reliable_response); + return UNITY_END(); +} diff --git a/cy_can/tests/test_api_can_socketcan_e2e.c b/cy_can/tests/test_api_can_socketcan_e2e.c new file mode 100644 index 0000000..d28d517 --- /dev/null +++ b/cy_can/tests/test_api_can_socketcan_e2e.c @@ -0,0 +1,228 @@ +#include "test_support.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct +{ + size_t calls; +} socketcan_server_t; + +static char* iface_name_buffer(void) +{ + static char value[16] = { 0 }; + return value; +} + +static void sleep_2ms(void) +{ + const struct timespec ts = { .tv_sec = 0, .tv_nsec = 2 * 1000 * 1000 }; + (void)nanosleep(&ts, NULL); +} + +static int run_cmd(const char* const command) +{ + TEST_ASSERT_NOT_NULL(command); + return system(command); // NOLINT(cert-env33-c) +} + +static int skip_with_reason(const char* const reason) +{ + (void)fprintf(stderr, "SKIP: %s\n", reason); + return CAN_TEST_SKIP_CODE; +} + +static int provision_vcan_interface(char* const out_name, const size_t out_size) +{ + char command[256]; + const unsigned long tag = (unsigned long)getpid() % 100000UL; + TEST_ASSERT_NOT_NULL(out_name); + TEST_ASSERT_TRUE(out_size >= sizeof(iface_name_buffer()[0]) * 16U); + (void)snprintf(out_name, out_size, "vcan%05lu", tag); + + if (run_cmd("command -v ip >/dev/null 2>&1") != 0) { + return skip_with_reason("ip tool is unavailable"); + } + if (run_cmd("sudo -n true >/dev/null 2>&1") != 0) { + return skip_with_reason("passwordless sudo is unavailable"); + } + if (run_cmd("sudo -n modprobe vcan >/dev/null 2>&1") != 0) { + return skip_with_reason("vcan kernel module could not be loaded"); + } + + (void)snprintf(command, sizeof(command), "sudo -n ip link add dev %s type vcan >/dev/null 2>&1", out_name); + if (run_cmd(command) != 0) { + return skip_with_reason("failed to create vcan interface"); + } + + (void)snprintf(command, sizeof(command), "sudo -n ip link set dev %s up >/dev/null 2>&1", out_name); + if (run_cmd(command) != 0) { + (void)snprintf(command, sizeof(command), "sudo -n ip link del dev %s >/dev/null 2>&1", out_name); + (void)run_cmd(command); + return skip_with_reason("failed to bring vcan interface up"); + } + return 0; +} + +static void destroy_vcan_interface(const char* const iface_name) +{ + char command[256]; + if ((iface_name == NULL) || (iface_name[0] == '\0')) { + return; + } + (void)snprintf(command, sizeof(command), "sudo -n ip link del dev %s >/dev/null 2>&1", iface_name); + (void)run_cmd(command); +} + +static void on_socketcan_request(cy_future_t* const future) +{ + socketcan_server_t* const state = (socketcan_server_t*)cy_future_context(future).ptr[0]; + TEST_ASSERT_NOT_NULL(state); + if (!cy_future_done(future)) { + return; + } + { + const uint8_t response[] = { 0x91U, 0x92U, 0x93U }; + cy_arrival_t arrival = cy_arrival_move(future); + if (arrival.message.content == NULL) { + return; + } + state->calls++; + TEST_ASSERT_EQUAL_INT(CY_OK, + cy_respond(&arrival.breadcrumb, + cy_now(arrival.breadcrumb.cy) + 300000, + (cy_bytes_t){ sizeof(response), response, NULL })); + cy_message_refcount_dec(arrival.message.content); + } +} + +static void test_api_can_socketcan_e2e_smoke(void) +{ + const char* ifaces[1] = { iface_name_buffer() }; + const uint8_t pub_payload[] = { 0x81U, 0x82U, 0x83U, 0x84U }; + const uint8_t req_payload[] = { 0xA1U, 0xA2U }; + const uint8_t rsp_payload[] = { 0x91U, 0x92U, 0x93U }; + socketcan_server_t server_state; + cy_platform_t* platform_a = NULL; + cy_platform_t* platform_b = NULL; + cy_t* cy_a = NULL; + cy_t* cy_b = NULL; + cy_future_t* sub = NULL; + cy_publisher_t* pub = NULL; + cy_publisher_t* client = NULL; + cy_future_t* server_sub = NULL; + cy_future_t* req = NULL; + (void)memset(&server_state, 0, sizeof(server_state)); + + platform_a = cy_can_socketcan_new(1U, ifaces, 256U); + platform_b = cy_can_socketcan_new(1U, ifaces, 256U); + TEST_ASSERT_NOT_NULL(platform_a); + TEST_ASSERT_NOT_NULL(platform_b); + + cy_a = cy_new(platform_a, cy_str("socketcan_a"), (cy_str_t){ 0U, NULL }); + cy_b = cy_new(platform_b, cy_str("socketcan_b"), (cy_str_t){ 0U, NULL }); + TEST_ASSERT_NOT_NULL(cy_a); + TEST_ASSERT_NOT_NULL(cy_b); + + sub = cy_subscribe(cy_b, cy_str("500#500"), 64U); + pub = cy_advertise(cy_a, cy_str("500#500")); + TEST_ASSERT_NOT_NULL(sub); + TEST_ASSERT_NOT_NULL(pub); + + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(cy_a) + 300000, (cy_bytes_t){ sizeof(pub_payload), pub_payload, NULL })); + for (size_t i = 0U; (i < 200U) && !cy_future_done(sub); i++) { + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000)); + sleep_2ms(); + } + TEST_ASSERT_TRUE(cy_future_done(sub)); + { + uint8_t received[16]; + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + TEST_ASSERT_EQUAL_size_t(sizeof(pub_payload), cy_message_size(arrival.message.content)); + TEST_ASSERT_EQUAL_size_t(sizeof(pub_payload), + cy_message_read(arrival.message.content, 0U, sizeof(received), received)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(pub_payload, received, sizeof(pub_payload)); + cy_message_refcount_dec(arrival.message.content); + } + + client = cy_advertise_client(cy_a, cy_str("501#501"), 64U); + server_sub = cy_subscribe(cy_b, cy_str("501#501"), 64U); + TEST_ASSERT_NOT_NULL(client); + TEST_ASSERT_NOT_NULL(server_sub); + { + cy_user_context_t ctx = CY_USER_CONTEXT_EMPTY; + ctx.ptr[0] = &server_state; + cy_future_context_set(server_sub, ctx); + cy_future_callback_set(server_sub, on_socketcan_request); + } + for (size_t i = 0U; i < 8U; i++) { + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000)); + sleep_2ms(); + } + + req = cy_request(client, cy_now(cy_a) + 300000, 100000, (cy_bytes_t){ sizeof(req_payload), req_payload, NULL }); + TEST_ASSERT_NOT_NULL(req); + for (size_t i = 0U; (i < 200U) && (cy_response_count(req) == 0U); i++) { + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000)); + sleep_2ms(); + } + TEST_ASSERT_EQUAL_size_t(1U, server_state.calls); + TEST_ASSERT_TRUE(cy_response_count(req) > 0U); + { + uint8_t received[16] = { 0 }; + const cy_response_t response = cy_response_move(req); + TEST_ASSERT_NOT_NULL(response.message.content); + TEST_ASSERT_TRUE(cy_message_size(response.message.content) >= sizeof(rsp_payload)); + TEST_ASSERT_EQUAL_size_t(sizeof(rsp_payload), + cy_message_read(response.message.content, 0U, sizeof(rsp_payload), received)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(rsp_payload, received, sizeof(rsp_payload)); + cy_message_refcount_dec(response.message.content); + } + + cy_future_destroy(req); + cy_unadvertise(client); + cy_future_destroy(server_sub); + cy_unadvertise(pub); + cy_future_destroy(sub); + for (size_t i = 0U; i < 8U; i++) { + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_a, cy_now(cy_a) + (cy_us_t)2000)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(cy_b, cy_now(cy_b) + (cy_us_t)2000)); + sleep_2ms(); + } + cy_destroy(cy_a); + cy_destroy(cy_b); + cy_can_socketcan_destroy(platform_a); + cy_can_socketcan_destroy(platform_b); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + int result = 0; + result = provision_vcan_interface(iface_name_buffer(), sizeof(iface_name_buffer()[0]) * 16U); + if (result != 0) { + return result; + } + + UNITY_BEGIN(); + RUN_TEST(test_api_can_socketcan_e2e_smoke); + result = UNITY_END(); + + destroy_vcan_interface(iface_name_buffer()); + return result; +} diff --git a/cy_can/tests/test_api_can_tombstone_revival.c b/cy_can/tests/test_api_can_tombstone_revival.c new file mode 100644 index 0000000..fd5ff84 --- /dev/null +++ b/cy_can/tests/test_api_can_tombstone_revival.c @@ -0,0 +1,222 @@ +#include "test_support.h" +#include +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +typedef struct +{ + can_test_bus_t bus; + can_test_node_t node; +} fixture_t; + +static void fixture_init(fixture_t* const self, const char* const home) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(home); + can_test_bus_init(&self->bus); + can_test_node_prepare(&self->node, &self->bus, 1U, false, true); + self->node.self_loopback = true; + can_test_node_make_platform(&self->node, 256U, 16U); + can_test_node_make_cy(&self->node, home); +} + +static void fixture_deinit(fixture_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + can_test_node_destroy(&self->node); +} + +static size_t settle_filters(can_test_node_t* const node) +{ + TEST_ASSERT_NOT_NULL(node); + can_test_node_spin(node, spin_slice_us); + can_test_node_spin(node, spin_slice_us); + { + const size_t out = node->filter_calls; + can_test_node_spin(node, spin_slice_us); + TEST_ASSERT_EQUAL_size_t(out, node->filter_calls); + return out; + } +} + +static void spin_until_done(can_test_node_t* const node, cy_future_t* const future) +{ + TEST_ASSERT_NOT_NULL(node); + TEST_ASSERT_NOT_NULL(future); + for (size_t i = 0U; (i < 80U) && !cy_future_done(future); i++) { + can_test_node_spin(node, spin_slice_us); + } + TEST_ASSERT_TRUE(cy_future_done(future)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(future)); +} + +static void publish_and_expect_payload(can_test_node_t* const node, + cy_future_t* const sub, + const char* const topic_name, + const void* const payload, + const size_t size) +{ + cy_publisher_t* const pub = cy_advertise(node->cy, cy_str(topic_name)); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_EQUAL_INT( + CY_OK, cy_publish(pub, cy_now(node->cy) + (20 * spin_slice_us), (cy_bytes_t){ size, payload, NULL })); + spin_until_done(node, sub); + TEST_ASSERT_EQUAL_UINT64(1U, cy_arrival_count(sub)); + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + TEST_ASSERT_EQUAL_size_t(size, cy_message_size(arrival.message.content)); + can_test_assert_message_equals(arrival.message.content, payload, size); + cy_message_refcount_dec(arrival.message.content); + } + cy_unadvertise(pub); +} + +static void run_revive_extent_case(const char* const topic_name, + const char* const home, + const size_t first_extent, + const size_t second_extent) +{ + uint8_t payload[96]; + fixture_t fix; + size_t baseline_filter_calls = 0U; + can_test_fill_payload(payload, sizeof(payload), 0x30U); + fixture_init(&fix, home); + + cy_future_t* const first = cy_subscribe(fix.node.cy, cy_str(topic_name), first_extent); + TEST_ASSERT_NOT_NULL(first); + baseline_filter_calls = settle_filters(&fix.node); + + cy_future_destroy(first); + { + cy_future_t* const second = cy_subscribe(fix.node.cy, cy_str(topic_name), second_extent); + TEST_ASSERT_NOT_NULL(second); + TEST_ASSERT_EQUAL_size_t(baseline_filter_calls, settle_filters(&fix.node)); + publish_and_expect_payload(&fix.node, second, topic_name, payload, sizeof(payload)); + cy_future_destroy(second); + } + + can_test_node_spin(&fix.node, spin_slice_us); + fixture_deinit(&fix); +} + +static void test_api_can_tombstone_verbatim_revive_preserves_old_larger_extent(void) +{ + run_revive_extent_case("tombstone/verbatim_preserve", "tombstone_verbatim_preserve", 128U, 64U); +} + +static void test_api_can_tombstone_verbatim_revive_grows_extent(void) +{ + run_revive_extent_case("tombstone/verbatim_grow", "tombstone_verbatim_grow", 64U, 128U); +} + +static void test_api_can_tombstone_pinned_revive_preserves_old_larger_extent(void) +{ + run_revive_extent_case("123#123", "tombstone_pinned_preserve", 128U, 64U); +} + +static void test_api_can_tombstone_pinned_revive_grows_extent(void) +{ + run_revive_extent_case("124#124", "tombstone_pinned_grow", 64U, 128U); +} + +static void test_api_can_tombstone_recreate_after_spin_reconfigures_filters(void) +{ + const uint8_t payload[] = { 0x41U, 0x42U, 0x43U, 0x44U }; + fixture_t fix; + size_t before_destroy = 0U; + size_t after_destroy = 0U; + fixture_init(&fix, "tombstone_recreate_after_spin"); + + cy_future_t* first = cy_subscribe(fix.node.cy, cy_str("125#125"), 64U); + TEST_ASSERT_NOT_NULL(first); + before_destroy = settle_filters(&fix.node); + + cy_future_destroy(first); + after_destroy = settle_filters(&fix.node); + TEST_ASSERT_TRUE(after_destroy > before_destroy); + + { + cy_future_t* const second = cy_subscribe(fix.node.cy, cy_str("125#125"), 64U); + TEST_ASSERT_NOT_NULL(second); + TEST_ASSERT_TRUE(settle_filters(&fix.node) > after_destroy); + publish_and_expect_payload(&fix.node, second, "125#125", payload, sizeof(payload)); + cy_future_destroy(second); + } + + can_test_node_spin(&fix.node, spin_slice_us); + fixture_deinit(&fix); +} + +static void test_api_can_tombstone_multiple_destroy_recreate_cycles_before_spin(void) +{ + const uint8_t payload[] = { 0x51U, 0x52U, 0x53U, 0x54U, 0x55U }; + fixture_t fix; + size_t baseline_filter_calls = 0U; + fixture_init(&fix, "tombstone_multi_cycle"); + + cy_future_t* first = cy_subscribe(fix.node.cy, cy_str("126#126"), 96U); + TEST_ASSERT_NOT_NULL(first); + baseline_filter_calls = settle_filters(&fix.node); + + cy_future_destroy(first); + { + cy_future_t* const second = cy_subscribe(fix.node.cy, cy_str("126#126"), 64U); + TEST_ASSERT_NOT_NULL(second); + cy_future_destroy(second); + } + { + cy_future_t* const third = cy_subscribe(fix.node.cy, cy_str("126#126"), 96U); + TEST_ASSERT_NOT_NULL(third); + TEST_ASSERT_EQUAL_size_t(baseline_filter_calls, settle_filters(&fix.node)); + publish_and_expect_payload(&fix.node, third, "126#126", payload, sizeof(payload)); + cy_future_destroy(third); + } + + can_test_node_spin(&fix.node, spin_slice_us); + fixture_deinit(&fix); +} + +static void test_api_can_tombstone_broadcast_reader_revives_across_cy_recreation(void) +{ + const uint8_t payload[] = { 0x61U, 0x62U, 0x63U }; + fixture_t fix; + size_t baseline_filter_calls = 0U; + fixture_init(&fix, "tombstone_broadcast_a"); + baseline_filter_calls = settle_filters(&fix.node); + + cy_destroy(fix.node.cy); + fix.node.cy = NULL; + can_test_node_make_cy(&fix.node, "tombstone_broadcast_b"); + TEST_ASSERT_EQUAL_size_t(baseline_filter_calls, settle_filters(&fix.node)); + + { + cy_future_t* const sub = cy_subscribe(fix.node.cy, cy_str("127#127"), 64U); + TEST_ASSERT_NOT_NULL(sub); + publish_and_expect_payload(&fix.node, sub, "127#127", payload, sizeof(payload)); + cy_future_destroy(sub); + } + + can_test_node_spin(&fix.node, spin_slice_us); + fixture_deinit(&fix); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_can_tombstone_verbatim_revive_preserves_old_larger_extent); + RUN_TEST(test_api_can_tombstone_verbatim_revive_grows_extent); + RUN_TEST(test_api_can_tombstone_pinned_revive_preserves_old_larger_extent); + RUN_TEST(test_api_can_tombstone_pinned_revive_grows_extent); + RUN_TEST(test_api_can_tombstone_recreate_after_spin_reconfigures_filters); + RUN_TEST(test_api_can_tombstone_multiple_destroy_recreate_cycles_before_spin); + RUN_TEST(test_api_can_tombstone_broadcast_reader_revives_across_cy_recreation); + return UNITY_END(); +} diff --git a/cy_can/tests/test_support.c b/cy_can/tests/test_support.c new file mode 100644 index 0000000..0f14b6a --- /dev/null +++ b/cy_can/tests/test_support.c @@ -0,0 +1,498 @@ +#include "test_support.h" +#include +#include +#include +#include +#include +#include +#include + +typedef struct allocation_header_t +{ + size_t size; + uint32_t magic; + uint32_t reserved; + can_test_heap_t* owner; +} allocation_header_t; + +static const uint32_t allocation_magic = UINT32_C(0xC0DEFACE); + +static size_t smaller(const size_t a, const size_t b) { return (a < b) ? a : b; } + +static void* heap_allocate(can_test_heap_t* self, const size_t size) +{ + if ((self->fail_after_n_allocations != SIZE_MAX) && (self->allocation_attempts >= self->fail_after_n_allocations)) { + return NULL; + } + self->allocation_attempts++; + + allocation_header_t* const header = (allocation_header_t*)malloc(sizeof(allocation_header_t) + size); + if (header == NULL) { + return NULL; + } + header->size = size; + header->magic = allocation_magic; + header->owner = self; + self->allocated_fragments++; + self->allocated_bytes += size; + return (void*)(header + 1); +} + +static allocation_header_t* header_from_payload(void* const ptr) +{ + return (allocation_header_t*)((uint8_t*)ptr - sizeof(allocation_header_t)); +} + +static const allocation_header_t* header_from_payload_const(const void* const ptr) +{ + return (const allocation_header_t*)((const uint8_t*)ptr - sizeof(allocation_header_t)); +} + +static void heap_free(can_test_heap_t* const self, void* const ptr) +{ + TEST_ASSERT_NOT_NULL(self); + if (ptr != NULL) { + allocation_header_t* const header = header_from_payload(ptr); + TEST_ASSERT_EQUAL_UINT32(allocation_magic, header->magic); + TEST_ASSERT_TRUE(header->owner == self); + TEST_ASSERT_TRUE(self->allocated_fragments > 0U); + TEST_ASSERT_TRUE(self->allocated_bytes >= header->size); + self->allocated_fragments--; + self->allocated_bytes -= header->size; + header->magic = 0U; + free(header); + } +} + +static void bus_register_node(can_test_bus_t* const bus, can_test_node_t* const node) +{ + if (bus == NULL) { + return; + } + for (size_t i = 0U; i < CAN_TEST_MAX_NODES; i++) { + if (bus->nodes[i] == NULL) { + bus->nodes[i] = node; + return; + } + } + TEST_FAIL_MESSAGE("bus registry exhausted"); +} + +static void bus_unregister_node(can_test_bus_t* const bus, can_test_node_t* const node) +{ + if (bus == NULL) { + return; + } + for (size_t i = 0U; i < CAN_TEST_MAX_NODES; i++) { + if (bus->nodes[i] == node) { + bus->nodes[i] = NULL; + return; + } + } +} + +static void enqueue_rx(can_test_node_t* const self, const cy_can_rx_t* const frame) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(frame); + TEST_ASSERT_TRUE(self->rx_count < CAN_TEST_MAX_QUEUE); + self->rx_queue[(self->rx_head + self->rx_count) % CAN_TEST_MAX_QUEUE] = *frame; + self->rx_count++; +} + +static bool dequeue_rx(can_test_node_t* const self, cy_can_rx_t* const out_frame) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(out_frame); + if (self->rx_count == 0U) { + return false; + } + *out_frame = self->rx_queue[self->rx_head]; + self->rx_head = (self->rx_head + 1U) % CAN_TEST_MAX_QUEUE; + self->rx_count--; + return true; +} + +static void record_tx(can_test_node_t* const self, + const uint_least8_t iface_index, + const uint32_t can_id, + const bool fd, + const void* const data, + const uint_least8_t len) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_TRUE(self->tx_history_count < CAN_TEST_MAX_TX_RECORD); + can_test_tx_record_t* const rec = &self->tx_history[self->tx_history_count++]; + rec->can_id = can_id; + rec->iface_index = iface_index; + rec->len = len; + rec->fd = fd; + if (len > 0U) { + (void)memcpy(rec->data, data, len); + } +} + +static void propagate_tx(can_test_node_t* const self, + const uint_least8_t iface_index, + const uint32_t can_id, + const bool fd, + const void* const data, + const uint_least8_t len) +{ + if (self->bus == NULL) { + return; + } + for (size_t i = 0U; i < CAN_TEST_MAX_NODES; i++) { + can_test_node_t* const other = self->bus->nodes[i]; + if (other == NULL) { + continue; + } + if ((other == self) && !self->self_loopback) { + continue; + } + if (iface_index >= other->iface_count) { + continue; + } + cy_can_rx_t frame; + (void)memset(&frame, 0, sizeof(frame)); + frame.timestamp = self->now; + frame.can_id = can_id; + frame.iface_index = iface_index; + frame.len = len; + frame.fd = fd; + if (len > 0U) { + (void)memcpy(frame.data, data, len); + } + enqueue_rx(other, &frame); + } +} + +static bool tx_common(void* const user, + const uint_least8_t iface_index, + const uint32_t can_id, + const bool fd, + const void* const data, + const uint_least8_t len) +{ + can_test_node_t* const self = (can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_TRUE(iface_index < self->iface_count); + if (self->tx_blocked[iface_index] > 0U) { + self->tx_blocked[iface_index]--; + return false; + } + record_tx(self, iface_index, can_id, fd, data, len); + if (!self->drop_tx[iface_index]) { + propagate_tx(self, iface_index, can_id, fd, data, len); + } + return true; +} + +static bool v_tx_classic(void* const user, + const uint_least8_t iface_index, + const uint32_t can_id, + const void* const data, + const uint_least8_t len) +{ + can_test_node_t* const self = (can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_TRUE(len <= 8U); + self->tx_classic_calls++; + return tx_common(user, iface_index, can_id, false, data, len); +} + +static bool v_tx_fd(void* const user, + const uint_least8_t iface_index, + const uint32_t can_id, + const void* const data, + const uint_least8_t len) +{ + can_test_node_t* const self = (can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_TRUE(len <= 64U); + self->tx_fd_calls++; + return tx_common(user, iface_index, can_id, true, data, len); +} + +static bool v_rx(void* const user, + cy_can_rx_t* const out_frame, + const cy_us_t deadline, + const uint_least8_t tx_pending_iface_bitmap) +{ + can_test_node_t* const self = (can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(out_frame); + self->rx_calls++; + self->last_tx_pending_iface_bitmap = tx_pending_iface_bitmap; + if (dequeue_rx(self, out_frame)) { + if (self->now < out_frame->timestamp) { + self->now = out_frame->timestamp; + } + return true; + } + if (self->now <= deadline) { + self->now = deadline + 1; + } + return false; +} + +static bool v_filter(void* const user, const size_t filter_count, const canard_filter_t* const filters) +{ + can_test_node_t* const self = (can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + if ((filter_count > 0U) && (filters == NULL)) { + return false; + } + self->filter_calls++; + self->last_filter_count = filter_count; + if (filter_count > 0U) { + TEST_ASSERT_TRUE(filter_count <= CAN_TEST_MAX_FILTERS); + (void)memcpy(self->last_filters, filters, filter_count * sizeof(canard_filter_t)); + } + if (self->filter_failures_remaining > 0U) { + self->filter_failures_remaining--; + return false; + } + return true; +} + +static cy_us_t v_now(void* const user) +{ + const can_test_node_t* const self = (const can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + return self->now; +} + +static uint64_t v_random(void* const user) +{ + can_test_node_t* const self = (can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + self->random_state = (self->random_state * UINT64_C(6364136223846793005)) + UINT64_C(1); + return self->random_state; +} + +static void* v_realloc(void* const user, void* const ptr, const size_t size) +{ + can_test_node_t* const self = (can_test_node_t*)user; + TEST_ASSERT_NOT_NULL(self); + return can_test_heap_realloc(&self->heap, ptr, size); +} + +void can_test_heap_init(can_test_heap_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + (void)memset(self, 0, sizeof(*self)); + self->fail_after_n_allocations = SIZE_MAX; +} + +void can_test_heap_fail_after(can_test_heap_t* const self, const size_t successful_allocations) +{ + TEST_ASSERT_NOT_NULL(self); + self->allocation_attempts = 0U; + self->fail_after_n_allocations = successful_allocations; +} + +void can_test_heap_allow_all(can_test_heap_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + self->allocation_attempts = 0U; + self->fail_after_n_allocations = SIZE_MAX; +} + +size_t can_test_heap_allocated_fragments(const can_test_heap_t* const self) +{ + return (self != NULL) ? self->allocated_fragments : 0U; +} + +size_t can_test_heap_allocated_bytes(const can_test_heap_t* const self) +{ + return (self != NULL) ? self->allocated_bytes : 0U; +} + +void* can_test_heap_realloc(void* const user, void* const ptr, const size_t size) +{ + can_test_heap_t* const self = (can_test_heap_t*)user; + TEST_ASSERT_NOT_NULL(self); + if (size == 0U) { + heap_free(self, ptr); + return NULL; + } + if (ptr == NULL) { + return heap_allocate(self, size); + } + + const allocation_header_t* const old_header = header_from_payload_const(ptr); + TEST_ASSERT_EQUAL_UINT32(allocation_magic, old_header->magic); + TEST_ASSERT_TRUE(old_header->owner == self); + void* const out = heap_allocate(self, size); + if (out == NULL) { + return NULL; + } + (void)memcpy(out, ptr, smaller(size, old_header->size)); + heap_free(self, ptr); + return out; +} + +void can_test_bus_init(can_test_bus_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + (void)memset(self, 0, sizeof(*self)); +} + +void can_test_node_prepare(can_test_node_t* const self, + can_test_bus_t* const bus, + const uint_least8_t iface_count, + const bool fd_capable, + const bool enable_filter_callback) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_TRUE((iface_count > 0U) && (iface_count <= CAN_TEST_MAX_IFACES)); + (void)memset(self, 0, sizeof(*self)); + self->bus = bus; + self->now = 1000; + self->random_state = UINT64_C(0x123456789ABCDEF0); + self->iface_count = iface_count; + self->fd_capable = fd_capable; + can_test_heap_init(&self->heap); + self->vtable.tx_classic = v_tx_classic; + self->vtable.tx_fd = fd_capable ? v_tx_fd : NULL; + self->vtable.rx = v_rx; + self->vtable.filter = enable_filter_callback ? v_filter : NULL; + self->vtable.now = v_now; + self->vtable.realloc = v_realloc; + self->vtable.random = v_random; + bus_register_node(bus, self); +} + +void can_test_node_make_platform(can_test_node_t* const self, const size_t tx_queue_capacity, const size_t filter_count) +{ + TEST_ASSERT_NOT_NULL(self); + self->platform = cy_can_new(self->iface_count, tx_queue_capacity, filter_count, &self->vtable, self); + TEST_ASSERT_NOT_NULL(self->platform); +} + +void can_test_node_make_cy(can_test_node_t* const self, const char* const home) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(home); + TEST_ASSERT_NOT_NULL(self->platform); + self->cy = cy_new(self->platform, cy_str(home), (cy_str_t){ 0U, NULL }); + TEST_ASSERT_NOT_NULL(self->cy); +} + +void can_test_node_destroy(can_test_node_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + if (self->cy != NULL) { + cy_destroy(self->cy); + self->cy = NULL; + } + if (self->platform != NULL) { + cy_can_destroy(self->platform); + self->platform = NULL; + } + bus_unregister_node(self->bus, self); + TEST_ASSERT_EQUAL_size_t(0U, self->rx_count); + TEST_ASSERT_EQUAL_size_t(0U, can_test_heap_allocated_fragments(&self->heap)); + TEST_ASSERT_EQUAL_size_t(0U, can_test_heap_allocated_bytes(&self->heap)); +} + +void can_test_node_spin(can_test_node_t* const self, const cy_us_t slice) +{ + TEST_ASSERT_NOT_NULL(self); + TEST_ASSERT_NOT_NULL(self->cy); + TEST_ASSERT_TRUE(slice >= 0); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(self->cy, cy_now(self->cy) + slice)); +} + +void can_test_spin_pair(can_test_node_t* const a, can_test_node_t* const b, const size_t rounds, const cy_us_t slice) +{ + for (size_t i = 0U; i < rounds; i++) { + if (a != NULL) { + can_test_node_spin(a, slice); + } + if (b != NULL) { + can_test_node_spin(b, slice); + } + } +} + +bool can_test_spin_pair_until_future_done(can_test_node_t* const a, + can_test_node_t* const b, + cy_future_t* const future, + const size_t rounds, + const cy_us_t slice) +{ + TEST_ASSERT_NOT_NULL(future); + for (size_t i = 0U; i < rounds; i++) { + if (cy_future_done(future)) { + return true; + } + can_test_spin_pair(a, b, 1U, slice); + } + return cy_future_done(future); +} + +bool can_test_spin_pair_until_condition(can_test_node_t* const a, + can_test_node_t* const b, + bool (*const condition)(void*), + void* const context, + const size_t rounds, + const cy_us_t slice) +{ + TEST_ASSERT_NOT_NULL(condition); + for (size_t i = 0U; i < rounds; i++) { + if (condition(context)) { + return true; + } + can_test_spin_pair(a, b, 1U, slice); + } + return condition(context); +} + +void can_test_node_reset_history(can_test_node_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + self->tx_history_count = 0U; + self->tx_classic_calls = 0U; + self->tx_fd_calls = 0U; + self->last_tx_pending_iface_bitmap = 0U; +} + +size_t can_test_node_count_records_on_iface(const can_test_node_t* const self, const uint_least8_t iface_index) +{ + TEST_ASSERT_NOT_NULL(self); + size_t out = 0U; + for (size_t i = 0U; i < self->tx_history_count; i++) { + if (self->tx_history[i].iface_index == iface_index) { + out++; + } + } + return out; +} + +size_t can_test_message_read_all(const cy_message_t* const message, void* const out, const size_t capacity) +{ + TEST_ASSERT_NOT_NULL(message); + TEST_ASSERT_NOT_NULL(out); + const size_t size = cy_message_size(message); + TEST_ASSERT_TRUE(size <= capacity); + TEST_ASSERT_EQUAL_size_t(size, cy_message_read(message, 0U, size, out)); + return size; +} + +void can_test_assert_message_equals(const cy_message_t* const message, const void* const expected, const size_t size) +{ + uint8_t buffer[512]; + TEST_ASSERT_TRUE(size <= sizeof(buffer)); + TEST_ASSERT_EQUAL_size_t(size, can_test_message_read_all(message, buffer, sizeof(buffer))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, buffer, size); +} + +void can_test_fill_payload(uint8_t* const out, const size_t size, const uint8_t seed) +{ + TEST_ASSERT_NOT_NULL(out); + for (size_t i = 0U; i < size; i++) { + out[i] = (uint8_t)(seed + (uint8_t)i); + } +} diff --git a/cy_can/tests/test_support.h b/cy_can/tests/test_support.h new file mode 100644 index 0000000..07d18ff --- /dev/null +++ b/cy_can/tests/test_support.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define CAN_TEST_MAX_IFACES 2U +#define CAN_TEST_MAX_NODES 4U +#define CAN_TEST_MAX_QUEUE 512U +#define CAN_TEST_MAX_TX_RECORD 1024U +#define CAN_TEST_MAX_FILTERS 64U +#define CAN_TEST_SKIP_CODE 77 + +typedef struct +{ + size_t allocated_fragments; + size_t allocated_bytes; + size_t allocation_attempts; + size_t fail_after_n_allocations; +} can_test_heap_t; + +typedef struct +{ + uint32_t can_id; + uint_least8_t iface_index; + uint_least8_t len; + bool fd; + uint8_t data[64]; +} can_test_tx_record_t; + +typedef struct can_test_node_t can_test_node_t; + +typedef struct +{ + can_test_node_t* nodes[CAN_TEST_MAX_NODES]; +} can_test_bus_t; + +struct can_test_node_t +{ + can_test_bus_t* bus; + can_test_heap_t heap; + cy_can_vtable_t vtable; + cy_platform_t* platform; + cy_t* cy; + cy_us_t now; + uint64_t random_state; + uint_least8_t iface_count; + bool fd_capable; + bool self_loopback; + bool drop_tx[CAN_TEST_MAX_IFACES]; + size_t tx_blocked[CAN_TEST_MAX_IFACES]; + size_t tx_classic_calls; + size_t tx_fd_calls; + size_t rx_calls; + uint_least8_t last_tx_pending_iface_bitmap; + size_t filter_calls; + size_t filter_failures_remaining; + size_t last_filter_count; + canard_filter_t last_filters[CAN_TEST_MAX_FILTERS]; + size_t rx_head; + size_t rx_count; + cy_can_rx_t rx_queue[CAN_TEST_MAX_QUEUE]; + size_t tx_history_count; + can_test_tx_record_t tx_history[CAN_TEST_MAX_TX_RECORD]; +}; + +void can_test_heap_init(can_test_heap_t* self); +void can_test_heap_fail_after(can_test_heap_t* self, size_t successful_allocations); +void can_test_heap_allow_all(can_test_heap_t* self); +size_t can_test_heap_allocated_fragments(const can_test_heap_t* self); +size_t can_test_heap_allocated_bytes(const can_test_heap_t* self); +void* can_test_heap_realloc(void* user, void* ptr, size_t size); + +void can_test_bus_init(can_test_bus_t* self); + +void can_test_node_prepare(can_test_node_t* self, + can_test_bus_t* bus, + uint_least8_t iface_count, + bool fd_capable, + bool enable_filter_callback); +void can_test_node_make_platform(can_test_node_t* self, size_t tx_queue_capacity, size_t filter_count); +void can_test_node_make_cy(can_test_node_t* self, const char* home); +void can_test_node_destroy(can_test_node_t* self); + +void can_test_node_spin(can_test_node_t* self, cy_us_t slice); +void can_test_spin_pair(can_test_node_t* a, can_test_node_t* b, size_t rounds, cy_us_t slice); +bool can_test_spin_pair_until_future_done(can_test_node_t* a, + can_test_node_t* b, + cy_future_t* future, + size_t rounds, + cy_us_t slice); +bool can_test_spin_pair_until_condition(can_test_node_t* a, + can_test_node_t* b, + bool (*condition)(void*), + void* context, + size_t rounds, + cy_us_t slice); + +void can_test_node_reset_history(can_test_node_t* self); +size_t can_test_node_count_records_on_iface(const can_test_node_t* self, uint_least8_t iface_index); + +size_t can_test_message_read_all(const cy_message_t* message, void* out, size_t capacity); +void can_test_assert_message_equals(const cy_message_t* message, const void* expected, size_t size); +void can_test_fill_payload(uint8_t* out, size_t size, uint8_t seed); + +#ifdef __cplusplus +} +#endif diff --git a/cy_udp_posix/CMakeLists.txt b/cy_udp_posix/CMakeLists.txt index 6a6fc00..f37c58b 100644 --- a/cy_udp_posix/CMakeLists.txt +++ b/cy_udp_posix/CMakeLists.txt @@ -21,27 +21,12 @@ set_target_properties( CXX_CPPCHECK "" ) -# Cy UDP POSIX static library. Cy is included as well. -function(configure_cy_udp_posix_target target) - target_link_libraries(${target} PUBLIC udpard) - target_include_directories(${target} SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) - target_include_directories(${target} PUBLIC ${CMAKE_SOURCE_DIR}/cy) - set_target_properties( - ${target} - PROPERTIES - COMPILE_WARNING_AS_ERROR ON - C_STANDARD_REQUIRED ON - C_EXTENSIONS OFF - ) -endfunction() +# Cy UDP POSIX transport glue static library. +# Consumers link cy/cy.c themselves; this target only contains the transport/platform layer. +add_library(cy_udp_posix STATIC cy_udp_posix.c udp_wrapper.c) +target_link_libraries(cy_udp_posix PUBLIC udpard) +target_include_directories(cy_udp_posix SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(cy_udp_posix PUBLIC ${CMAKE_SOURCE_DIR}/cy) +set_target_properties(cy_udp_posix PROPERTIES COMPILE_WARNING_AS_ERROR ON C_STANDARD_REQUIRED ON C_EXTENSIONS OFF) -set(cy_udp_posix_sources cy_udp_posix.c udp_wrapper.c ../cy/cy.c) - -# The normal production build. -add_library(cy_udp_posix STATIC ${cy_udp_posix_sources}) -configure_cy_udp_posix_target(cy_udp_posix) - -# Same but with CY_CONFIG_TRACE=1 for examples and tests. -add_library(cy_udp_posix_trace STATIC ${cy_udp_posix_sources} cy_trace_stderr.c) -target_compile_definitions(cy_udp_posix_trace PUBLIC CY_CONFIG_TRACE=1) -configure_cy_udp_posix_target(cy_udp_posix_trace) +add_subdirectory(tests) diff --git a/cy_udp_posix/cy_udp_posix.c b/cy_udp_posix/cy_udp_posix.c index 11e196c..f3a9086 100644 --- a/cy_udp_posix/cy_udp_posix.c +++ b/cy_udp_posix/cy_udp_posix.c @@ -30,6 +30,12 @@ #include #include +#if __STDC_VERSION__ < 201112L +#define static_assert(x, ...) typedef char _sa_gl(_sa_, __LINE__)[(x) ? 1 : -1] +#define _sa_gl(a, b) _sa_gl2(a, b) +#define _sa_gl2(a, b) a##b +#endif + #if __STDC_VERSION__ >= 201112L #include #else @@ -139,14 +145,15 @@ static uint64_t prng64(uint64_t* const state, const uint64_t local_uid) return rapidhash_withSeed(state, sizeof(uint64_t), local_uid); } +static_assert(offsetof(udpard_bytes_scattered_t, bytes.size) == offsetof(cy_bytes_t, size), ""); +static_assert(offsetof(udpard_bytes_scattered_t, bytes.data) == offsetof(cy_bytes_t, data), ""); +static_assert(offsetof(udpard_bytes_scattered_t, next) == offsetof(cy_bytes_t, next), ""); + static udpard_bytes_scattered_t cy_bytes_to_udpard_bytes(const cy_bytes_t message) { // Instead of converting the entire payload chain, we can just statically validate that the memory layouts // are compatible. We cannot make neither libudpard nor cy depend on each other, but perhaps in the future // we could introduce a tiny single header providing some common definitions for both, to eliminate such aliasing. - static_assert(offsetof(udpard_bytes_scattered_t, bytes.size) == offsetof(cy_bytes_t, size), ""); - static_assert(offsetof(udpard_bytes_scattered_t, bytes.data) == offsetof(cy_bytes_t, data), ""); - static_assert(offsetof(udpard_bytes_scattered_t, next) == offsetof(cy_bytes_t, next), ""); return (udpard_bytes_scattered_t){ .bytes = { .size = message.size, .data = message.data }, .next = (const udpard_bytes_scattered_t*)message.next }; } @@ -326,6 +333,9 @@ struct subject_reader_t subject_reader_t* next; }; +static_assert(sizeof(((subject_reader_t*)0)->history) / sizeof(((subject_reader_t*)0)->history[0]) == 2, ""); +static_assert(sizeof(((udpard_rx_transfer_t*)0)->remote.endpoints) <= sizeof(((cy_lane_t*)0)->ctx), ""); + // We use the same handler for both subject and unicast messages, since they both use the same ingestion callback. // The difference here is that unicast messages have no associated reader instance. static void v_on_msg(udpard_rx_t* const rx, udpard_rx_port_t* const port, const udpard_rx_transfer_t tr) @@ -335,7 +345,6 @@ static void v_on_msg(udpard_rx_t* const rx, udpard_rx_port_t* const port, const .content = make_message(owner, tr.payload_size_stored, tr.payload) }; if (msg.content != NULL) { cy_lane_t lane = { .id = tr.remote.uid, .prio = (cy_prio_t)tr.priority }; - static_assert(sizeof(tr.remote.endpoints) <= sizeof(lane.ctx), ""); memcpy(&lane.ctx, tr.remote.endpoints, sizeof(tr.remote.endpoints)); const uint32_t* const subject_id = (port->user == NULL) ? NULL // user is NULL for unicast : &((subject_reader_t*)port->user)->base.subject_id; @@ -350,7 +359,6 @@ static void v_on_msg_stateless(udpard_rx_t* const rx, udpard_rx_port_t* const po { cy_udp_posix_t* const owner = rx->user; subject_reader_t* const self = port->user; - static_assert(sizeof(self->history) / sizeof(self->history[0]) == 2, ""); // In the stateless mode, libudpard does not bother deduplicating messages. Gossips/scouts are dup-tolerant, // so we could just pass all messages as-is and it will work fine, but it would waste CPU cycles because each // message requires some log-time index lookups. @@ -490,6 +498,9 @@ static void v_subject_reader_tombstone(cy_platform_t* const platform, cy_subject assert(!self->tombstone); self->tombstone = true; // Close sockets now to stop further reads while we defer the final teardown. + // This also makes same-subject recreation safe: unlike libcanard, libudpard has no global uniqueness rule for + // subject subscriptions. The old port instance becomes inert as soon as its sockets are closed because only open + // sockets are polled, so a new reader may be constructed for the same subject-ID before the old port is freed. for (uint_fast8_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) { udp_wrapper_close(&self->sock[i]); } @@ -508,6 +519,8 @@ static void v_subject_reader_extent_set(cy_platform_t* const base, // --------------------------------------------------------------------------------------------------------------------- // UNICAST +static_assert(sizeof(udpard_udpip_ep_t[UDPARD_IFACE_COUNT_MAX]) <= sizeof(((cy_lane_t*)0)->ctx), ""); + static cy_err_t v_unicast_send(cy_platform_t* const base, const cy_lane_t* const lane, const cy_us_t deadline, @@ -516,7 +529,6 @@ static cy_err_t v_unicast_send(cy_platform_t* const base, cy_udp_posix_t* const owner = (cy_udp_posix_t*)base; // Unbox the unicast context. udpard_udpip_ep_t endpoints[UDPARD_IFACE_COUNT_MAX] = { 0 }; - static_assert(sizeof(endpoints) <= sizeof(lane->ctx), ""); memcpy(endpoints, &lane->ctx, sizeof(udpard_udpip_ep_t) * UDPARD_IFACE_COUNT_MAX); // Push the message. // We may need better error reporting in libudpard, this is a bit unwieldy. @@ -655,6 +667,8 @@ static cy_err_t spin_once_until(cy_udp_posix_t* const self, const cy_us_t deadli for (subject_reader_t* rd_iter = self->reader_head; rd_iter != NULL;) { subject_reader_t* const next = rd_iter->next; if (rd_iter->tombstone) { + // Deferred teardown exists only to keep list traversal/reentrancy simple; the reader was already rendered + // inactive when it was tombstoned by closing its sockets. subject_reader_destroy(self, rd_iter); } else { for (uint_fast8_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) { diff --git a/cy_udp_posix/cy_udp_posix.h b/cy_udp_posix/cy_udp_posix.h index dbfe160..01b6edd 100644 --- a/cy_udp_posix/cy_udp_posix.h +++ b/cy_udp_posix/cy_udp_posix.h @@ -52,7 +52,7 @@ typedef struct cy_udp_posix_stats_t } cy_udp_posix_stats_t; /// The default factory that automatically assigns the node parameters that fit most applications: -/// - A semi-random EUI-64: 20 most significant bits are host-specific, the lower 44 bits are random. +/// - A semi-random EUI-64. /// - The local interfaces are chosen per the defaults configured on the local system. /// - The TX queue capacity is set to a reasonable large value. cy_platform_t* cy_udp_posix_new(void); diff --git a/cy_udp_posix/eui64.h b/cy_udp_posix/eui64.h index 63d5620..55bc105 100644 --- a/cy_udp_posix/eui64.h +++ b/cy_udp_posix/eui64.h @@ -2,11 +2,10 @@ #include "rapidhash.h" #include -#include #include -#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) -#include -#endif +#include +#include +#include /// Generates a new random locally administered unicast EUI-64 identifier suitable for use as a 64-bit Cyphal node-ID. /// Returns zero on failure, which is not a valid EUI-64. @@ -19,41 +18,16 @@ static inline uint64_t eui64_semirandom(void) { uint32_t host_20 = 0; // 2 of these bits are used for EUI-64 flags, 18 bits remain. These are first 5 hex digits. uint64_t rand_44 = 0; // The remaining 44 random bits, which are the last 11 hex digits. -#ifdef __linux__ { - const int fd = open("/etc/machine-id", O_RDONLY); - if (fd < 0) { - return 0; - } - char buf[32]; - const ssize_t n = read(fd, buf, sizeof(buf)); - close(fd); - if (n < 32) { + struct utsname info = { 0 }; + if (uname(&info) != 0) { return 0; } - host_20 = (uint32_t)(rapidhash(buf, (size_t)n) & 0xFFFFFU); - } - { - const int fd = open("/dev/urandom", O_RDONLY); - if (fd < 0) { - return 0; - } - if (read(fd, &rand_44, sizeof(rand_44)) != sizeof(rand_44)) { - close(fd); - return 0; - } - close(fd); - } -#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) - { - // Use sysctl kern.hostid for host identifier - int mib[2] = { CTL_KERN, KERN_HOSTID }; - uint32_t hostid = 0; - size_t len = sizeof(hostid); - if (sysctl(mib, 2, &hostid, &len, NULL, 0) != 0) { + const size_t length = strlen(info.nodename); + if (length == 0U) { return 0; } - host_20 = hostid & 0xFFFFFU; + host_20 = (uint32_t)(rapidhash(info.nodename, length) & 0xFFFFFU); } { const int fd = open("/dev/urandom", O_RDONLY); @@ -66,9 +40,6 @@ static inline uint64_t eui64_semirandom(void) } close(fd); } -#else -#error "eui64_semirandom() is not implemented for this platform yet." -#endif uint64_t out = (((uint64_t)host_20) << 44U) | (rand_44 & ((1ULL << 44U) - 1U)); out &= ~(1ULL << 56U); // clear bit I/G (unicast) out |= (1ULL << 57U); // set bit U/L (locally administered) diff --git a/cy_udp_posix/tests/CMakeLists.txt b/cy_udp_posix/tests/CMakeLists.txt new file mode 100644 index 0000000..d0f73b6 --- /dev/null +++ b/cy_udp_posix/tests/CMakeLists.txt @@ -0,0 +1,94 @@ +# Copyright (c) Pavel Kirienko + +cmake_minimum_required(VERSION 3.24) + +set(cy_udp_posix_tests_root ${CMAKE_CURRENT_SOURCE_DIR}) +set(unity_root "${CMAKE_SOURCE_DIR}/lib/unity") +set(udpard_root "${CMAKE_SOURCE_DIR}/lib/libudpard/libudpard") + +add_library(cy_udp_posix_tests_unity STATIC "${unity_root}/src/unity.c") +target_include_directories(cy_udp_posix_tests_unity SYSTEM PUBLIC "${unity_root}/src") +target_compile_definitions( + cy_udp_posix_tests_unity + PUBLIC + UNITY_INCLUDE_DOUBLE=1 + UNITY_OUTPUT_COLOR=1 + UNITY_SUPPORT_64=1 +) +target_compile_options( + cy_udp_posix_tests_unity + PRIVATE + -m32 + -Wno-sign-conversion + -Wno-conversion + -Wno-switch-enum + -Wno-float-equal + -Wno-double-promotion + -Wno-missing-declarations +) +set_target_properties( + cy_udp_posix_tests_unity + PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR OFF + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +add_library(cy_udp_posix_tests_udpard STATIC "${udpard_root}/udpard.c") +target_include_directories(cy_udp_posix_tests_udpard SYSTEM PUBLIC "${udpard_root}") +target_compile_options(cy_udp_posix_tests_udpard PRIVATE -m32 -Wno-cast-align) +set_target_properties( + cy_udp_posix_tests_udpard + PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR OFF + C_CLANG_TIDY "" + CXX_CLANG_TIDY "" + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +function(cy_udp_posix_add_test name) + add_executable( + ${name} + ${ARGN} + ${CMAKE_SOURCE_DIR}/cy/cy.c + ${CMAKE_SOURCE_DIR}/cy_udp_posix/cy_udp_posix.c + ${CMAKE_SOURCE_DIR}/cy_udp_posix/udp_wrapper.c + ${cy_udp_posix_tests_root}/test_support.c + ) + target_include_directories( + ${name} + PRIVATE + ${CMAKE_SOURCE_DIR}/cy + ${CMAKE_SOURCE_DIR}/cy_udp_posix + ${udpard_root} + ${cy_udp_posix_tests_root} + ) + target_link_libraries(${name} PRIVATE cy_udp_posix_tests_unity cy_udp_posix_tests_udpard) + target_compile_definitions(${name} PRIVATE _POSIX_C_SOURCE=200809L) + target_compile_options(${name} PRIVATE -m32) + target_link_options(${name} PRIVATE -m32) + set_target_properties( + ${name} + PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF + COMPILE_WARNING_AS_ERROR ON + ) + add_test(NAME run_${name} COMMAND ${name}) +endfunction() + +cy_udp_posix_add_test(test_api_udp_posix_basic test_api_udp_posix_basic.c) +cy_udp_posix_add_test(test_api_udp_posix_pubsub test_api_udp_posix_pubsub.c) +cy_udp_posix_add_test(test_api_udp_posix_rpc_lifecycle test_api_udp_posix_rpc_lifecycle.c) +cy_udp_posix_add_test(test_api_udp_posix_default_ctor test_api_udp_posix_default_ctor.c) +set_tests_properties(run_test_api_udp_posix_default_ctor PROPERTIES SKIP_RETURN_CODE 77) diff --git a/cy_udp_posix/tests/test_api_udp_posix_basic.c b/cy_udp_posix/tests/test_api_udp_posix_basic.c new file mode 100644 index 0000000..4d4fed3 --- /dev/null +++ b/cy_udp_posix/tests/test_api_udp_posix_basic.c @@ -0,0 +1,93 @@ +#include "test_support.h" + +#include + +#include +#include + +static void test_api_udp_posix_basic_parse_iface_address(void) +{ + TEST_ASSERT_EQUAL_HEX32(0x7F000001U, cy_udp_parse_iface_address("127.0.0.1")); + TEST_ASSERT_EQUAL_HEX32(0U, cy_udp_parse_iface_address("127.0.0")); + TEST_ASSERT_EQUAL_HEX32(0U, cy_udp_parse_iface_address("not_an_ip")); + TEST_ASSERT_EQUAL_HEX32(0U, cy_udp_parse_iface_address(NULL)); +} + +static void test_api_udp_posix_basic_manual_constructor_and_home(void) +{ + static const uint32_t no_ifaces[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0U, 0U, 0U }; + static const uint32_t loopback_iface[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0x7F000001U, 0U, 0U }; + cy_platform_t* platform = NULL; + cy_str_t home; + cy_udp_posix_stats_t stats; + + TEST_ASSERT_NULL(cy_udp_posix_new_manual(UINT64_C(0x0123456789ABCDEF), no_ifaces, 16U)); + + platform = cy_udp_posix_new_manual(UINT64_C(0x0123456789ABCDEF), loopback_iface, 64U); + TEST_ASSERT_NOT_NULL(platform); + + home = cy_udp_posix_home(platform, NULL); + TEST_ASSERT_EQUAL_size_t(16U, home.len); + TEST_ASSERT_EQUAL_STRING_LEN("0123456789abcdef", home.str, 16); + + home = cy_udp_posix_home(platform, "udp"); + TEST_ASSERT_EQUAL_size_t(strlen("udp/0123456789abcdef"), home.len); + TEST_ASSERT_EQUAL_STRING_LEN("udp/0123456789abcdef", home.str, home.len); + + stats = cy_udp_posix_stats(platform); + TEST_ASSERT_EQUAL_size_t(0U, stats.subject_writer_count); + TEST_ASSERT_EQUAL_size_t(0U, stats.subject_reader_count); + TEST_ASSERT_EQUAL_size_t(0U, stats.mem.allocated_fragments); + TEST_ASSERT_EQUAL_size_t(0U, stats.mem.allocated_bytes); + TEST_ASSERT_EQUAL_UINT64(0U, stats.mem.oom_count); + TEST_ASSERT_EQUAL_UINT64(0U, stats.message_loss_count); + for (size_t i = 0U; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) { + TEST_ASSERT_EQUAL_UINT64(0U, stats.sock_tx.error_count[i]); + TEST_ASSERT_EQUAL_UINT64(0U, stats.sock_rx.error_count[i]); + } + TEST_ASSERT_EQUAL_INT64(0, stats.sock_tx.last_error_at); + TEST_ASSERT_EQUAL_INT64(0, stats.sock_rx.last_error_at); + + cy_udp_posix_destroy(platform); +} + +static void test_api_udp_posix_basic_namespace_and_time(void) +{ + const char* const old_env = getenv("CYPHAL_NAMESPACE"); + char* const backup = (old_env != NULL) ? strdup(old_env) : NULL; + const cy_us_t t0 = cy_udp_posix_now(); + const cy_us_t t1 = cy_udp_posix_now(); + + TEST_ASSERT_TRUE(t1 >= t0); + + TEST_ASSERT_EQUAL_INT(0, setenv("CYPHAL_NAMESPACE", "ns/test", 1)); + { + const cy_str_t ns = cy_udp_posix_namespace(); + TEST_ASSERT_EQUAL_size_t(strlen("ns/test"), ns.len); + TEST_ASSERT_EQUAL_STRING_LEN("ns/test", ns.str, ns.len); + } + + TEST_ASSERT_EQUAL_INT(0, unsetenv("CYPHAL_NAMESPACE")); + { + const cy_str_t ns = cy_udp_posix_namespace(); + TEST_ASSERT_EQUAL_size_t(0U, ns.len); + } + + if (backup != NULL) { + TEST_ASSERT_EQUAL_INT(0, setenv("CYPHAL_NAMESPACE", backup, 1)); + free(backup); + } +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_udp_posix_basic_parse_iface_address); + RUN_TEST(test_api_udp_posix_basic_manual_constructor_and_home); + RUN_TEST(test_api_udp_posix_basic_namespace_and_time); + return UNITY_END(); +} diff --git a/cy_udp_posix/tests/test_api_udp_posix_default_ctor.c b/cy_udp_posix/tests/test_api_udp_posix_default_ctor.c new file mode 100644 index 0000000..ca06eab --- /dev/null +++ b/cy_udp_posix/tests/test_api_udp_posix_default_ctor.c @@ -0,0 +1,52 @@ +#include "test_support.h" + +#include + +#include + +static void test_api_udp_posix_default_ctor_hostname_hash_and_flags(void) +{ + cy_platform_t* const platform_a = cy_udp_posix_new(); + cy_platform_t* const platform_b = cy_udp_posix_new(); + const uint32_t expected = udp_test_expected_eui64_prefix(); + + TEST_ASSERT_NOT_NULL(platform_a); + TEST_ASSERT_NOT_NULL(platform_b); + + { + const uint64_t uid_a = udp_test_parse_uid_from_home(cy_udp_posix_home(platform_a, NULL)); + const uint64_t uid_b = udp_test_parse_uid_from_home(cy_udp_posix_home(platform_b, NULL)); + + TEST_ASSERT_EQUAL_HEX32(expected, (uint32_t)(uid_a >> 44U)); + TEST_ASSERT_EQUAL_HEX32(expected, (uint32_t)(uid_b >> 44U)); + TEST_ASSERT_TRUE((uid_a & (1ULL << 57U)) != 0U); + TEST_ASSERT_TRUE((uid_b & (1ULL << 57U)) != 0U); + TEST_ASSERT_TRUE((uid_a & (1ULL << 56U)) == 0U); + TEST_ASSERT_TRUE((uid_b & (1ULL << 56U)) == 0U); + } + + cy_udp_posix_destroy(platform_a); + cy_udp_posix_destroy(platform_b); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + cy_platform_t* const probe_a = cy_udp_posix_new(); + cy_platform_t* const probe_b = cy_udp_posix_new(); + if ((probe_a == NULL) || (probe_b == NULL)) { + cy_udp_posix_destroy(probe_a); + cy_udp_posix_destroy(probe_b); + (void)fprintf(stderr, "cy_udp_posix_new() unavailable on this host, skipping default-constructor smoke test\n"); + return UDP_TEST_SKIP_CODE; + } + cy_udp_posix_destroy(probe_a); + cy_udp_posix_destroy(probe_b); + + UNITY_BEGIN(); + RUN_TEST(test_api_udp_posix_default_ctor_hostname_hash_and_flags); + return UNITY_END(); +} diff --git a/cy_udp_posix/tests/test_api_udp_posix_pubsub.c b/cy_udp_posix/tests/test_api_udp_posix_pubsub.c new file mode 100644 index 0000000..93cc226 --- /dev/null +++ b/cy_udp_posix/tests/test_api_udp_posix_pubsub.c @@ -0,0 +1,136 @@ +#include "test_support.h" + +#include + +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +typedef struct +{ + cy_future_t* future; +} future_done_context_t; + +typedef struct +{ + cy_future_t* future; + uint64_t minimum_count; +} arrival_count_context_t; + +static bool is_future_done(void* const context) +{ + const future_done_context_t* const ctx = (const future_done_context_t*)context; + return (ctx != NULL) && cy_future_done(ctx->future); +} + +static bool has_arrival_count(void* const context) +{ + const arrival_count_context_t* const ctx = (const arrival_count_context_t*)context; + return (ctx != NULL) && (cy_arrival_count(ctx->future) >= ctx->minimum_count); +} + +static void test_api_udp_posix_pubsub_best_effort_and_stats(void) +{ + static const uint8_t payload[] = { 0x11U, 0x22U, 0x33U, 0x44U }; + udp_test_node_t a; + udp_test_node_t b; + size_t writer_baseline = 0U; + size_t reader_baseline = 0U; + + udp_test_node_init_manual(&a, UINT64_C(0xA000000000000001), "udp_pub_a", 256U); + udp_test_node_init_manual(&b, UINT64_C(0xA000000000000002), "udp_pub_b", 256U); + udp_test_spin_pair(&a, &b, 4U, spin_slice_us); + writer_baseline = cy_udp_posix_stats(a.platform).subject_writer_count; + reader_baseline = cy_udp_posix_stats(b.platform).subject_reader_count; + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("udp/basic")); + cy_future_t* const sub = cy_subscribe(b.cy, cy_str("udp/basic"), 64U); + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + + udp_test_spin_pair(&a, &b, 8U, spin_slice_us); + TEST_ASSERT_EQUAL_size_t(writer_baseline + 1U, cy_udp_posix_stats(a.platform).subject_writer_count); + TEST_ASSERT_TRUE(cy_udp_posix_stats(b.platform).subject_reader_count >= (reader_baseline + 1U)); + + TEST_ASSERT_EQUAL_INT(CY_OK, + cy_publish(pub, cy_now(a.cy) + (50 * spin_slice_us), (cy_bytes_t){ 4U, payload, NULL })); + + { + arrival_count_context_t ctx = { .future = sub, .minimum_count = 1U }; + TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition(&a, &b, has_arrival_count, &ctx, 100U, spin_slice_us)); + } + + { + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + udp_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(sub); + udp_test_spin_pair(&a, &b, 6U, spin_slice_us); + + udp_test_node_deinit(&a); + udp_test_node_deinit(&b); +} + +static void test_api_udp_posix_pubsub_large_reliable_delivery(void) +{ + uint8_t payload[1024]; + udp_test_node_t a; + udp_test_node_t b; + future_done_context_t delivery_ctx; + + udp_test_fill_payload(payload, sizeof(payload), 0x40U); + udp_test_node_init_manual(&a, UINT64_C(0xA000000000000011), "udp_rel_a", 512U); + udp_test_node_init_manual(&b, UINT64_C(0xA000000000000012), "udp_rel_b", 512U); + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("udp/large")); + cy_future_t* const sub = cy_subscribe(b.cy, cy_str("udp/large"), sizeof(payload)); + cy_future_t* delivery = NULL; + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(sub); + + udp_test_spin_pair(&a, &b, 8U, spin_slice_us); + delivery = + cy_publish_reliable(pub, cy_now(a.cy) + (200 * spin_slice_us), (cy_bytes_t){ sizeof(payload), payload, NULL }); + TEST_ASSERT_NOT_NULL(delivery); + delivery_ctx.future = delivery; + TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition(&a, &b, is_future_done, &delivery_ctx, 200U, spin_slice_us)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(delivery)); + TEST_ASSERT_TRUE(cy_arrival_count(sub) > 0U); + + { + uint8_t received[sizeof(payload)]; + const cy_arrival_t arrival = cy_arrival_move(sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + TEST_ASSERT_EQUAL_size_t(sizeof(payload), cy_message_size(arrival.message.content)); + TEST_ASSERT_EQUAL_size_t(sizeof(payload), + udp_test_message_read_all(arrival.message.content, received, sizeof(received))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(payload, received, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_future_destroy(delivery); + cy_unadvertise(pub); + cy_future_destroy(sub); + udp_test_spin_pair(&a, &b, 6U, spin_slice_us); + + udp_test_node_deinit(&a); + udp_test_node_deinit(&b); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_udp_posix_pubsub_best_effort_and_stats); + RUN_TEST(test_api_udp_posix_pubsub_large_reliable_delivery); + return UNITY_END(); +} diff --git a/cy_udp_posix/tests/test_api_udp_posix_rpc_lifecycle.c b/cy_udp_posix/tests/test_api_udp_posix_rpc_lifecycle.c new file mode 100644 index 0000000..63e88fa --- /dev/null +++ b/cy_udp_posix/tests/test_api_udp_posix_rpc_lifecycle.c @@ -0,0 +1,161 @@ +#include "test_support.h" + +#include + +#include +#include +#include + +static const cy_us_t spin_slice_us = (cy_us_t)10000; + +typedef struct +{ + cy_future_t* future; +} future_done_context_t; + +typedef struct +{ + cy_future_t* future; + uint64_t minimum_count; +} response_count_context_t; + +static bool is_future_done(void* const context) +{ + const future_done_context_t* const ctx = (const future_done_context_t*)context; + return (ctx != NULL) && cy_future_done(ctx->future); +} + +static bool has_response_count(void* const context) +{ + const response_count_context_t* const ctx = (const response_count_context_t*)context; + return (ctx != NULL) && (cy_response_count(ctx->future) >= ctx->minimum_count); +} + +static void test_api_udp_posix_rpc_request_response_reliable(void) +{ + static const uint8_t request_payload[] = { 0x10U, 0x20U, 0x30U }; + static const uint8_t response_payload[] = { 0xAAU, 0xBBU, 0xCCU, 0xDDU }; + udp_test_node_t client_node; + udp_test_node_t server_node; + cy_future_t* response_delivery = NULL; + + udp_test_node_init_manual(&client_node, UINT64_C(0xB000000000000001), "udp_cli", 256U); + udp_test_node_init_manual(&server_node, UINT64_C(0xB000000000000002), "udp_srv", 256U); + + cy_publisher_t* const client_pub = cy_advertise_client(client_node.cy, cy_str("udp/rpc"), 64U); + cy_future_t* const server_sub = cy_subscribe(server_node.cy, cy_str("udp/rpc"), 64U); + cy_future_t* request = NULL; + TEST_ASSERT_NOT_NULL(client_pub); + TEST_ASSERT_NOT_NULL(server_sub); + + udp_test_spin_pair(&client_node, &server_node, 8U, spin_slice_us); + request = cy_request(client_pub, + cy_now(client_node.cy) + (200 * spin_slice_us), + 50 * spin_slice_us, + (cy_bytes_t){ sizeof(request_payload), request_payload, NULL }); + TEST_ASSERT_NOT_NULL(request); + { + future_done_context_t ctx = { .future = server_sub }; + TEST_ASSERT_TRUE( + udp_test_spin_pair_until_condition(&client_node, &server_node, is_future_done, &ctx, 120U, spin_slice_us)); + } + + { + const cy_arrival_t arrival = cy_arrival_move(server_sub); + cy_breadcrumb_t breadcrumb = arrival.breadcrumb; + TEST_ASSERT_NOT_NULL(arrival.message.content); + udp_test_assert_message_equals(arrival.message.content, request_payload, sizeof(request_payload)); + response_delivery = cy_respond_reliable(&breadcrumb, + cy_now(server_node.cy) + (200 * spin_slice_us), + (cy_bytes_t){ sizeof(response_payload), response_payload, NULL }); + TEST_ASSERT_NOT_NULL(response_delivery); + cy_message_refcount_dec(arrival.message.content); + } + + { + response_count_context_t ctx = { .future = request, .minimum_count = 1U }; + TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition( + &client_node, &server_node, has_response_count, &ctx, 200U, spin_slice_us)); + } + TEST_ASSERT_TRUE(cy_future_done(request)); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(request)); + { + future_done_context_t ctx = { .future = response_delivery }; + TEST_ASSERT_TRUE( + udp_test_spin_pair_until_condition(&client_node, &server_node, is_future_done, &ctx, 120U, spin_slice_us)); + } + TEST_ASSERT_EQUAL_INT(CY_OK, cy_future_error(response_delivery)); + + { + const cy_response_t response = cy_response_move(request); + TEST_ASSERT_NOT_NULL(response.message.content); + TEST_ASSERT_EQUAL_UINT64(server_node.uid, response.remote_id); + udp_test_assert_message_equals(response.message.content, response_payload, sizeof(response_payload)); + cy_message_refcount_dec(response.message.content); + } + + cy_future_destroy(response_delivery); + cy_future_destroy(request); + cy_unadvertise(client_pub); + cy_future_destroy(server_sub); + udp_test_spin_pair(&client_node, &server_node, 6U, spin_slice_us); + + udp_test_node_deinit(&client_node); + udp_test_node_deinit(&server_node); +} + +static void test_api_udp_posix_lifecycle_subscriber_recreation_before_spin(void) +{ + static const uint8_t payload[] = { 0x51U, 0x52U, 0x53U, 0x54U }; + udp_test_node_t a; + udp_test_node_t b; + + udp_test_node_init_manual(&a, UINT64_C(0xB000000000000011), "udp_life_a", 256U); + udp_test_node_init_manual(&b, UINT64_C(0xB000000000000012), "udp_life_b", 256U); + + cy_publisher_t* const pub = cy_advertise(a.cy, cy_str("udp/revive")); + cy_future_t* old_sub = cy_subscribe(b.cy, cy_str("udp/revive"), 64U); + cy_future_t* new_sub = NULL; + TEST_ASSERT_NOT_NULL(pub); + TEST_ASSERT_NOT_NULL(old_sub); + + cy_future_destroy(old_sub); + old_sub = NULL; + new_sub = cy_subscribe(b.cy, cy_str("udp/revive"), 64U); + TEST_ASSERT_NOT_NULL(new_sub); + + udp_test_spin_pair(&a, &b, 8U, spin_slice_us); + TEST_ASSERT_EQUAL_INT(CY_OK, + cy_publish(pub, cy_now(a.cy) + (50 * spin_slice_us), (cy_bytes_t){ 4U, payload, NULL })); + + { + future_done_context_t ctx = { .future = new_sub }; + TEST_ASSERT_TRUE(udp_test_spin_pair_until_condition(&a, &b, is_future_done, &ctx, 120U, spin_slice_us)); + } + + { + const cy_arrival_t arrival = cy_arrival_move(new_sub); + TEST_ASSERT_NOT_NULL(arrival.message.content); + udp_test_assert_message_equals(arrival.message.content, payload, sizeof(payload)); + cy_message_refcount_dec(arrival.message.content); + } + + cy_unadvertise(pub); + cy_future_destroy(new_sub); + udp_test_spin_pair(&a, &b, 6U, spin_slice_us); + + udp_test_node_deinit(&a); + udp_test_node_deinit(&b); +} + +void setUp(void) {} + +void tearDown(void) {} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_api_udp_posix_rpc_request_response_reliable); + RUN_TEST(test_api_udp_posix_lifecycle_subscriber_recreation_before_spin); + return UNITY_END(); +} diff --git a/cy_udp_posix/tests/test_support.c b/cy_udp_posix/tests/test_support.c new file mode 100644 index 0000000..5719d1f --- /dev/null +++ b/cy_udp_posix/tests/test_support.c @@ -0,0 +1,134 @@ +#include "test_support.h" + +#include + +#define RAPIDHASH_COMPACT +#include + +#include +#include +#include + +void udp_test_node_init_manual(udp_test_node_t* const self, + const uint64_t uid, + const char* const home_prefix, + const size_t tx_queue_capacity) +{ + static const uint32_t iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0x7F000001U, 0U, 0U }; + TEST_ASSERT_NOT_NULL(self); + (void)memset(self, 0, sizeof(*self)); + self->uid = uid; + self->platform = cy_udp_posix_new_manual(uid, iface_address, tx_queue_capacity); + TEST_ASSERT_NOT_NULL(self->platform); + self->cy = cy_new(self->platform, cy_udp_posix_home(self->platform, home_prefix), cy_str("")); + TEST_ASSERT_NOT_NULL(self->cy); +} + +void udp_test_node_deinit(udp_test_node_t* const self) +{ + TEST_ASSERT_NOT_NULL(self); + if (self->cy != NULL) { + cy_destroy(self->cy); + self->cy = NULL; + } + if (self->platform != NULL) { + cy_udp_posix_destroy(self->platform); + self->platform = NULL; + } +} + +void udp_test_spin(udp_test_node_t* const node, const cy_us_t slice) +{ + TEST_ASSERT_NOT_NULL(node); + TEST_ASSERT_NOT_NULL(node->cy); + TEST_ASSERT_TRUE(slice >= 0); + TEST_ASSERT_EQUAL_INT(CY_OK, cy_spin_until(node->cy, cy_now(node->cy) + slice)); +} + +void udp_test_spin_pair(udp_test_node_t* const a, udp_test_node_t* const b, const size_t rounds, const cy_us_t slice) +{ + for (size_t i = 0U; i < rounds; i++) { + if (a != NULL) { + udp_test_spin(a, slice); + } + if (b != NULL) { + udp_test_spin(b, slice); + } + } +} + +bool udp_test_spin_pair_until_condition(udp_test_node_t* const a, + udp_test_node_t* const b, + bool (*const condition)(void*), + void* const context, + const size_t rounds, + const cy_us_t slice) +{ + TEST_ASSERT_NOT_NULL(condition); + for (size_t i = 0U; i < rounds; i++) { + if (condition(context)) { + return true; + } + udp_test_spin_pair(a, b, 1U, slice); + } + return condition(context); +} + +size_t udp_test_message_read_all(const cy_message_t* const message, void* const out, const size_t capacity) +{ + TEST_ASSERT_NOT_NULL(message); + TEST_ASSERT_NOT_NULL(out); + const size_t size = cy_message_size(message); + TEST_ASSERT_TRUE(size <= capacity); + TEST_ASSERT_EQUAL_size_t(size, cy_message_read(message, 0U, size, out)); + return size; +} + +void udp_test_assert_message_equals(const cy_message_t* const message, const void* const expected, const size_t size) +{ + uint8_t buffer[4096]; + TEST_ASSERT_TRUE(size <= sizeof(buffer)); + TEST_ASSERT_EQUAL_size_t(size, udp_test_message_read_all(message, buffer, sizeof(buffer))); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, buffer, size); +} + +void udp_test_fill_payload(uint8_t* const out, const size_t size, const uint8_t seed) +{ + TEST_ASSERT_NOT_NULL(out); + for (size_t i = 0U; i < size; i++) { + out[i] = (uint8_t)(seed + (uint8_t)i); + } +} + +uint64_t udp_test_parse_uid_from_home(const cy_str_t home) +{ + char buffer[17] = { 0 }; + size_t start = 0U; + TEST_ASSERT_NOT_NULL(home.str); + for (size_t i = 0U; i < home.len; i++) { + if (home.str[i] == '/') { + start = i + 1U; + } + } + TEST_ASSERT_TRUE(home.len >= start); + TEST_ASSERT_EQUAL_size_t(16U, home.len - start); + (void)memcpy(buffer, &home.str[start], 16U); + return (uint64_t)strtoull(buffer, NULL, 16); +} + +uint32_t udp_test_hostname_hash20(void) +{ + char hostname[256] = { 0 }; + TEST_ASSERT_EQUAL_INT(0, gethostname(hostname, sizeof(hostname))); + hostname[sizeof(hostname) - 1U] = 0; + TEST_ASSERT_TRUE(hostname[0] != 0); + return (uint32_t)(rapidhash(hostname, strlen(hostname)) & 0xFFFFFU); +} + +uint32_t udp_test_expected_eui64_prefix(void) +{ + uint32_t out = udp_test_hostname_hash20(); + out &= ~(1U << 12U); + out |= 1U << 13U; + return out; +} diff --git a/cy_udp_posix/tests/test_support.h b/cy_udp_posix/tests/test_support.h new file mode 100644 index 0000000..6c953b7 --- /dev/null +++ b/cy_udp_posix/tests/test_support.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define UDP_TEST_SKIP_CODE 77 + +typedef struct +{ + cy_platform_t* platform; + cy_t* cy; + uint64_t uid; +} udp_test_node_t; + +void udp_test_node_init_manual(udp_test_node_t* self, uint64_t uid, const char* home_prefix, size_t tx_queue_capacity); +void udp_test_node_deinit(udp_test_node_t* self); + +void udp_test_spin(udp_test_node_t* node, cy_us_t slice); +void udp_test_spin_pair(udp_test_node_t* a, udp_test_node_t* b, size_t rounds, cy_us_t slice); +bool udp_test_spin_pair_until_condition(udp_test_node_t* a, + udp_test_node_t* b, + bool (*condition)(void*), + void* context, + size_t rounds, + cy_us_t slice); + +size_t udp_test_message_read_all(const cy_message_t* message, void* out, size_t capacity); +void udp_test_assert_message_equals(const cy_message_t* message, const void* expected, size_t size); +void udp_test_fill_payload(uint8_t* out, size_t size, uint8_t seed); +uint64_t udp_test_parse_uid_from_home(const cy_str_t home); +uint32_t udp_test_hostname_hash20(void); +uint32_t udp_test_expected_eui64_prefix(void); + +#ifdef __cplusplus +} +#endif diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 27efa83..158a825 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -6,24 +6,40 @@ enable_testing() include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/lib") -# UDP node examples. -add_executable(udp_pub main_udp_pub.c) -add_executable(udp_sub main_udp_sub.c) -add_executable(udp_time_pub main_udp_time_pub.c) -add_executable(udp_echo main_udp_echo.c) -target_link_libraries(udp_pub cy_udp_posix_trace) -target_link_libraries(udp_sub cy_udp_posix_trace) -target_link_libraries(udp_time_pub cy_udp_posix_trace) -target_link_libraries(udp_echo cy_udp_posix_trace) - -# UDP file transfer examples. -add_executable(udp_file_server main_udp_file_server.c) -add_executable(udp_file_client main_udp_file_client.c) -target_link_libraries(udp_file_server cy_udp_posix_trace) -target_link_libraries(udp_file_client cy_udp_posix_trace) - -# UDP streaming examples. -add_executable(udp_streaming_server main_udp_streaming_server.c) -add_executable(udp_streaming_client main_udp_streaming_client.c) -target_link_libraries(udp_streaming_server cy_udp_posix_trace) -target_link_libraries(udp_streaming_client cy_udp_posix_trace) +option(CY_EXAMPLES_TRACE "Enable cy_trace() logging in the example executables" ON) + +add_library(cy_examples_support STATIC ../cy/cy.c) +target_include_directories(cy_examples_support PUBLIC ${CMAKE_SOURCE_DIR}/cy) +set_target_properties( + cy_examples_support + PROPERTIES + COMPILE_WARNING_AS_ERROR ON + C_STANDARD_REQUIRED ON + C_EXTENSIONS OFF +) +if (CY_EXAMPLES_TRACE) + target_sources(cy_examples_support PRIVATE cy_trace_stderr.c) + target_compile_definitions(cy_examples_support PUBLIC CY_CONFIG_TRACE=1) +endif () + +function(cy_add_example target platform_target) + add_executable(${target} ${ARGN}) + target_link_libraries(${target} PRIVATE cy_examples_support ${platform_target}) + if (TARGET cy_can_socketcan) + target_link_libraries(${target} PRIVATE cy_can_socketcan) + endif () +endfunction() + +# Node examples. +cy_add_example(example_pub cy_udp_posix example_pub.c) +cy_add_example(example_sub cy_udp_posix example_sub.c) +cy_add_example(example_time_pub cy_udp_posix example_time_pub.c) +cy_add_example(example_echo cy_udp_posix example_echo.c) + +# File transfer examples. +cy_add_example(example_file_server cy_udp_posix example_file_server.c) +cy_add_example(example_file_client cy_udp_posix example_file_client.c) + +# Streaming examples. +cy_add_example(example_streaming_server cy_udp_posix example_streaming_server.c) +cy_add_example(example_streaming_client cy_udp_posix example_streaming_client.c) diff --git a/cy_udp_posix/cy_trace_stderr.c b/examples/cy_trace_stderr.c similarity index 99% rename from cy_udp_posix/cy_trace_stderr.c rename to examples/cy_trace_stderr.c index 30dde00..f33d674 100644 --- a/cy_udp_posix/cy_trace_stderr.c +++ b/examples/cy_trace_stderr.c @@ -1,10 +1,12 @@ // Add this file to your build to define cy_trace() that prints trace messages into stderr. #include "cy_platform.h" -#include -#include + +#include #include +#include #include +#include void cy_trace(cy_t* const cy, const char* const file, diff --git a/examples/main_udp_echo.c b/examples/example_echo.c similarity index 85% rename from examples/main_udp_echo.c rename to examples/example_echo.c index 8bb8d6c..45c9cf0 100644 --- a/examples/main_udp_echo.c +++ b/examples/example_echo.c @@ -2,8 +2,8 @@ // Hint: use pattern subscriptions to receive data from multiple topics concurrently; e.g., `>` to receive everything. #include -#include +#include "example_platform.h" #include "hexdump.h" #include @@ -43,19 +43,18 @@ static void on_message(cy_future_t* const subscriber) cy_message_refcount_dec(arrival.message.content); } -int main(const int argc, const char* const argv[]) +int main(int argc, char* argv[]) { - if (argc != 2) { // TODO: subscribe to multiple patterns - (void)fprintf(stderr, "Usage: %s \n", argv[0]); + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { return 1; } - - cy_platform_t* const platform = cy_udp_posix_new(); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); + if (argc != 2) { // TODO: subscribe to multiple patterns + (void)fprintf(stderr, "Usage: %s [iface=...] \n", argv[0]); return 1; } - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_echo"), cy_udp_posix_namespace()); + + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); return 1; diff --git a/examples/main_udp_file_client.c b/examples/example_file_client.c similarity index 92% rename from examples/main_udp_file_client.c rename to examples/example_file_client.c index 39646b2..934a887 100644 --- a/examples/main_udp_file_client.c +++ b/examples/example_file_client.c @@ -1,9 +1,10 @@ #include -#include + +#include "example_platform.h" + #include #include #include -#include #define MEGA 1000000LL @@ -27,10 +28,14 @@ typedef struct file_read_response_t // Command line arguments: file name. // The read file will be written into stdout as-is. -int main(const int argc, const char* const argv[]) +int main(int argc, char* argv[]) { + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { + return 1; + } if (argc < 2) { - (void)fprintf(stderr, "Usage: %s \n", argv[0]); + (void)fprintf(stderr, "Usage: %s [iface=...] \n", argv[0]); return 1; } @@ -45,12 +50,7 @@ int main(const int argc, const char* const argv[]) memcpy(req.path, argv[1], req.path_len); // SET UP THE NODE. - cy_platform_t* const platform = cy_udp_posix_new(); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); - return 1; - } - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_file_client"), cy_udp_posix_namespace()); + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); return 1; diff --git a/examples/main_udp_file_server.c b/examples/example_file_server.c similarity index 91% rename from examples/main_udp_file_server.c rename to examples/example_file_server.c index a28ed60..f6aea11 100644 --- a/examples/main_udp_file_server.c +++ b/examples/example_file_server.c @@ -1,7 +1,8 @@ #include -#include + +#include "example_platform.h" + #include -#include #include #include @@ -81,14 +82,13 @@ static void on_file_read_msg(cy_future_t* const subscriber) cy_message_refcount_dec(arv.message.content); } -int main(void) +int main(int argc, char* argv[]) { - cy_platform_t* const platform = cy_udp_posix_new(); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { return 1; } - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_file_server"), cy_udp_posix_namespace()); + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); return 1; diff --git a/examples/example_platform.h b/examples/example_platform.h new file mode 100644 index 0000000..b63b1ce --- /dev/null +++ b/examples/example_platform.h @@ -0,0 +1,151 @@ +/// A simple helper that constructs a cy_platform_t instance based on the command-line arguments. + +#pragma once + +#include +#if defined(__linux__) +#include +#endif +#include +#include + +#include +#include +#include +#include +#include + +/// Stores the platform instance constructed through parsing the argv and the corresponding destructor. +typedef struct example_platform_t +{ + cy_platform_t* platform; + void (*destroy)(cy_platform_t*); ///< Invoke this to dispose the platform instance. +} example_platform_t; + +/// The returned string reference is valid until the next invocation. +static inline cy_str_t example_platform_home(void) +{ +#if defined(__cplusplus) || (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202311L)) + thread_local +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) + _Thread_local +#endif + static char home_storage[CY_TOPIC_NAME_MAX + 1U]; + const uint64_t uid = eui64_semirandom(); + const int written = snprintf(home_storage, sizeof(home_storage), "%016llx", (unsigned long long)uid); + if ((uid == 0U) || (written <= 0) || ((size_t)written >= sizeof(home_storage))) { + return (cy_str_t){ .len = 0, .str = NULL }; + } + return (cy_str_t){ .len = (size_t)written, .str = home_storage }; +} + +/// The returned string reference is valid until the environment is modified, otherwise static. +static inline cy_str_t example_platform_namespace(void) { return cy_str(getenv("CYPHAL_NAMESPACE")); } + +/// Consumes the argv elements starting with `iface=` and constructs the appropriate platform instance. +/// The argc/argv will be modified in-place to remove the consumed arguments. +/// Returns platform=NULL if the arguments are invalid. +static inline example_platform_t example_platform_make(int* const argc, char** argv) +{ + example_platform_t out = { 0 }; + if ((argc == NULL) || (*argc <= 0) || (argv == NULL)) { + (void)fprintf(stderr, "Invalid argv\n"); + return out; + } + + uint32_t udp_iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX] = { 0 }; +#if defined(__linux__) + const char* can_iface_name[CANARD_IFACE_COUNT] = { 0 }; +#endif + size_t iface_count = 0U; + bool use_udp = false; + bool use_can = false; + int dst = 1; + for (int src = 1; src < *argc; src++) { + char* const arg = argv[src]; + if ((arg != NULL) && (strncmp(arg, "iface=", 6) == 0)) { + const char* const spec = arg + 6; + if (spec[0] == '\0') { + (void)fprintf(stderr, "iface= requires a value\n"); + return out; + } + if (strncmp(spec, "socketcan:", 10) == 0) { +#if defined(__linux__) + const char* const name = spec + 10; + if (name[0] == '\0') { + (void)fprintf(stderr, "SocketCAN iface name is empty: %s\n", arg); + return out; + } + if (use_udp) { + (void)fprintf(stderr, "Mixed UDP and SocketCAN iface specs are not supported\n"); + return out; + } + if (iface_count >= CANARD_IFACE_COUNT) { + (void)fprintf(stderr, "Too many SocketCAN ifaces, limit is %u\n", CANARD_IFACE_COUNT); + return out; + } + for (size_t i = 0; i < iface_count; i++) { + if ((can_iface_name[i] != NULL) && (strcmp(can_iface_name[i], name) == 0)) { + (void)fprintf(stderr, "Duplicate SocketCAN iface: %s\n", name); + return out; + } + } + can_iface_name[iface_count++] = name; + use_can = true; +#else + (void)fprintf(stderr, "SocketCAN is only supported on Linux\n"); + return out; +#endif + } else { + const uint32_t addr = cy_udp_parse_iface_address(spec); + if (addr == 0U) { + (void)fprintf(stderr, "Invalid iface spec: %s\n", arg); + return out; + } + if (use_can) { + (void)fprintf(stderr, "Mixed UDP and SocketCAN iface specs are not supported\n"); + return out; + } + if (iface_count >= CY_UDP_POSIX_IFACE_COUNT_MAX) { + (void)fprintf(stderr, "Too many UDP ifaces, limit is %u\n", (unsigned)CY_UDP_POSIX_IFACE_COUNT_MAX); + return out; + } + for (size_t i = 0; i < iface_count; i++) { + if (udp_iface_address[i] == addr) { + (void)fprintf(stderr, "Duplicate UDP iface: %s\n", spec); + return out; + } + } + udp_iface_address[iface_count++] = addr; + use_udp = true; + } + } else { + argv[dst++] = argv[src]; + } + } + argv[dst] = NULL; + *argc = dst; + + if (use_can) { +#if defined(__linux__) + out.platform = cy_can_socketcan_new((uint_least8_t)iface_count, can_iface_name, 1000U); + out.destroy = cy_can_socketcan_destroy; +#endif + } else if (use_udp) { + const uint64_t uid = eui64_semirandom(); + if (uid == 0U) { + (void)fprintf(stderr, "eui64_semirandom\n"); + return out; + } + out.platform = cy_udp_posix_new_manual(uid, udp_iface_address, 50000U); + out.destroy = cy_udp_posix_destroy; + } else { + out.platform = cy_udp_posix_new(); + out.destroy = cy_udp_posix_destroy; + } + if (out.platform == NULL) { + (void)fprintf(stderr, "platform factory failure\n"); + return out; + } + return out; +} diff --git a/examples/main_udp_pub.c b/examples/example_pub.c similarity index 71% rename from examples/main_udp_pub.c rename to examples/example_pub.c index 7254939..3716909 100644 --- a/examples/main_udp_pub.c +++ b/examples/example_pub.c @@ -1,13 +1,12 @@ #include -#include -#include + +#include "example_platform.h" #include "arg_kv.h" #include "hexdump.h" #include #include #include #include -#include #include #define KILO 1000L @@ -20,31 +19,18 @@ struct config_publication_t struct config_t { - uint64_t local_uid; - uint32_t iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX]; - size_t tx_queue_capacity; - size_t pub_count; struct config_publication_t* pubs; }; static struct config_t load_config(const int argc, char* const argv[]) { - struct config_t cfg = { - .local_uid = eui64_semirandom(), - .tx_queue_capacity = 1000, - .pub_count = 0, - .pubs = calloc((size_t)(argc - 1), sizeof(struct config_publication_t)), - }; - size_t iface_count = 0; - arg_kv_t arg; + const size_t config_capacity = (size_t)((argc > 1) ? (argc - 1) : 1); + struct config_t cfg = { .pub_count = 0, .pubs = calloc(config_capacity, sizeof(struct config_publication_t)) }; + arg_kv_t arg; while ((arg = arg_kv_next(argc, argv)).key_hash != 0) { assert(arg.value != NULL); - if ((arg_kv_hash("iface") == arg.key_hash) && (iface_count < CY_UDP_POSIX_IFACE_COUNT_MAX)) { - cfg.iface_address[iface_count++] = cy_udp_parse_iface_address(arg.value); - } else if (arg_kv_hash("txq") == arg.key_hash) { - cfg.tx_queue_capacity = strtoul(arg.value, NULL, 0); - } else if (arg_kv_hash("pub") == arg.key_hash) { + if (arg_kv_hash("pub") == arg.key_hash) { struct config_publication_t* x = NULL; for (size_t i = 0; i < cfg.pub_count; i++) { if (strcmp(cfg.pubs[i].name, arg.value) == 0) { @@ -59,12 +45,6 @@ static struct config_t load_config(const int argc, char* const argv[]) } } // Print the actual configs we're using. - (void)fprintf(stderr, "ifaces:"); - for (size_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) { - (void)fprintf(stderr, " 0x%08x", (unsigned)cfg.iface_address[i]); - } - (void)fprintf(stderr, "\nuid: 0x%016llx\n", (unsigned long long)cfg.local_uid); - (void)fprintf(stderr, "tx_queue_frames: %zu\n", cfg.tx_queue_capacity); (void)fprintf(stderr, "publications:\n"); for (size_t i = 0; i < cfg.pub_count; i++) { (void)fprintf(stderr, "\t%s\n", cfg.pubs[i].name); @@ -110,23 +90,20 @@ static void on_result(cy_future_t* const future) cy_future_destroy(future); } -int main(const int argc, char* const argv[]) +int main(int argc, char* argv[]) { srand((unsigned)time(NULL)); - const struct config_t cfg = load_config(argc, argv); - - // Set up the platform layer that connects Cy to the underlying transport and OS. - // This is the only part of the code that is platform-specific; the rest is all portable Cy API usage. - cy_platform_t* const platform = cy_udp_posix_new_manual(cfg.local_uid, cfg.iface_address, cfg.tx_queue_capacity); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { return 1; } + const struct config_t cfg = load_config(argc, argv); // Set up the node instance. - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_pub"), cy_udp_posix_namespace()); + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); + free(cfg.pubs); return 1; } @@ -136,6 +113,7 @@ int main(const int argc, char* const argv[]) publishers[i] = cy_advertise_client(cy, cy_str(cfg.pubs[i].name), MEGA); if (publishers[i] == NULL) { (void)fprintf(stderr, "cy_advertise_client: NULL\n"); + free(cfg.pubs); return 1; } } @@ -153,10 +131,7 @@ int main(const int argc, char* const argv[]) if (now >= next_publish_at) { for (size_t i = 0; i < cfg.pub_count; i++) { char msg[256]; - (void)sprintf(msg, - "Hello from %016llx! The current time is %.6f s.", - (unsigned long long)cfg.local_uid, - 1e-6 * (double)now); + (void)sprintf(msg, "Hello from %s! The current time is %.6f s.", cy_home(cy).str, 1e-6 * (double)now); cy_future_t* const future = cy_request(publishers[i], // now + (MEGA * 2), MEGA * 10, @@ -171,5 +146,6 @@ int main(const int argc, char* const argv[]) next_publish_at += 5 * MEGA; } } + free(cfg.pubs); return 0; } diff --git a/examples/main_udp_streaming_client.c b/examples/example_streaming_client.c similarity index 92% rename from examples/main_udp_streaming_client.c rename to examples/example_streaming_client.c index cc2abef..2b6dcfa 100644 --- a/examples/main_udp_streaming_client.c +++ b/examples/example_streaming_client.c @@ -1,7 +1,8 @@ // A tiny streaming client demo: send one request and keep receiving responses. #include -#include + +#include "example_platform.h" #include #include @@ -58,8 +59,13 @@ static void on_stream_response(cy_future_t* const future) } } -int main(const int argc, const char* const argv[]) +int main(int argc, char* argv[]) { + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { + return 1; + } + // Parse optional args: count [period_ms]. uint32_t count = DEFAULT_COUNT; uint32_t period_ms = DEFAULT_PERIOD_ms; @@ -77,12 +83,7 @@ int main(const int argc, const char* const argv[]) } // Initialize the node. - cy_platform_t* const platform = cy_udp_posix_new(); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); - return 1; - } - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, NULL), cy_udp_posix_namespace()); + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); return 1; diff --git a/examples/main_udp_streaming_server.c b/examples/example_streaming_server.c similarity index 96% rename from examples/main_udp_streaming_server.c rename to examples/example_streaming_server.c index 22d2c38..02a2684 100644 --- a/examples/main_udp_streaming_server.c +++ b/examples/example_streaming_server.c @@ -1,7 +1,9 @@ // A tiny streaming server demo: receive a request and stream reliable responses. #include -#include + +#include "example_platform.h" + #include #include @@ -178,14 +180,13 @@ static void on_stream_request(cy_future_t* const subscriber) cy_message_refcount_dec(arv.message.content); } -int main(void) +int main(int argc, char* argv[]) { - cy_platform_t* const platform = cy_udp_posix_new(); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { return 1; } - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, NULL), cy_udp_posix_namespace()); + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); return 1; diff --git a/examples/main_udp_sub.c b/examples/example_sub.c similarity index 73% rename from examples/main_udp_sub.c rename to examples/example_sub.c index 6003767..0b9c5b8 100644 --- a/examples/main_udp_sub.c +++ b/examples/example_sub.c @@ -1,13 +1,12 @@ #include -#include -#include + +#include "example_platform.h" #include "arg_kv.h" #include "hexdump.h" #include #include #include #include -#include #include #define KILO 1000L @@ -21,33 +20,18 @@ struct config_subscription_t struct config_t { - uint32_t iface_address[CY_UDP_POSIX_IFACE_COUNT_MAX]; - uint64_t local_uid; - size_t tx_queue_capacity; - size_t sub_count; struct config_subscription_t* subs; }; static struct config_t load_config(const int argc, char* const argv[]) { - struct config_t cfg = { - .local_uid = eui64_semirandom(), - .tx_queue_capacity = 1000, - .sub_count = 0, - .subs = calloc((size_t)(argc - 1), sizeof(struct config_subscription_t)), - }; - size_t iface_count = 0; - arg_kv_t arg; + const size_t config_capacity = (size_t)((argc > 1) ? (argc - 1) : 1); + struct config_t cfg = { .sub_count = 0, .subs = calloc(config_capacity, sizeof(struct config_subscription_t)) }; + arg_kv_t arg; while ((arg = arg_kv_next(argc, argv)).key_hash != 0) { assert(arg.value != NULL); - if ((arg_kv_hash("iface") == arg.key_hash) && (iface_count < CY_UDP_POSIX_IFACE_COUNT_MAX)) { - cfg.iface_address[iface_count++] = cy_udp_parse_iface_address(arg.value); - } else if (arg_kv_hash("uid") == arg.key_hash) { - cfg.local_uid = (uint64_t)strtoull(arg.value, NULL, 0); - } else if (arg_kv_hash("txq") == arg.key_hash) { - cfg.tx_queue_capacity = strtoul(arg.value, NULL, 0); - } else if ((arg_kv_hash("sub") == arg.key_hash) || (arg_kv_hash("subord") == arg.key_hash)) { + if ((arg_kv_hash("sub") == arg.key_hash) || (arg_kv_hash("subord") == arg.key_hash)) { struct config_subscription_t* x = NULL; for (size_t i = 0; i < cfg.sub_count; i++) { if (strcmp(cfg.subs[i].name, arg.value) == 0) { @@ -63,12 +47,6 @@ static struct config_t load_config(const int argc, char* const argv[]) } } // Print the actual configs we're using. - (void)fprintf(stderr, "ifaces:"); - for (size_t i = 0; i < CY_UDP_POSIX_IFACE_COUNT_MAX; i++) { - (void)fprintf(stderr, " 0x%08x", (unsigned)cfg.iface_address[i]); - } - (void)fprintf(stderr, "\nuid: 0x%016llx\n", (unsigned long long)cfg.local_uid); - (void)fprintf(stderr, "tx_queue_frames: %zu\n", cfg.tx_queue_capacity); (void)fprintf(stderr, "subscriptions:\n"); for (size_t i = 0; i < cfg.sub_count; i++) { (void)fprintf(stderr, "\t%s\n", cfg.subs[i].name); @@ -135,23 +113,20 @@ static void on_message(cy_future_t* const subscriber) cy_message_refcount_dec(arrival.message.content); } -int main(const int argc, char* const argv[]) +int main(int argc, char* argv[]) { srand((unsigned)time(NULL)); - const struct config_t cfg = load_config(argc, argv); - - // Set up the platform layer that connects Cy to the underlying transport and OS. - // This is the only part of the code that is platform-specific; the rest is all portable Cy API usage. - cy_platform_t* const platform = cy_udp_posix_new_manual(cfg.local_uid, cfg.iface_address, cfg.tx_queue_capacity); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { return 1; } + const struct config_t cfg = load_config(argc, argv); // Set up the node instance. - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_sub"), cy_udp_posix_namespace()); + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); + free(cfg.subs); return 1; } @@ -162,6 +137,7 @@ int main(const int argc, char* const argv[]) : cy_subscribe(cy, cy_str(cfg.subs[i].name), MEGA); if (subscribers[i] == NULL) { (void)fprintf(stderr, "cy_subscribe(): NULL\n"); + free(cfg.subs); return 1; } cy_future_callback_set(subscribers[i], on_message); @@ -175,5 +151,6 @@ int main(const int argc, char* const argv[]) break; } } + free(cfg.subs); return 0; } diff --git a/examples/main_udp_time_pub.c b/examples/example_time_pub.c similarity index 87% rename from examples/main_udp_time_pub.c rename to examples/example_time_pub.c index ab310be..b7e8c00 100644 --- a/examples/main_udp_time_pub.c +++ b/examples/example_time_pub.c @@ -1,7 +1,8 @@ // Publishes the current time every second. #include -#include + +#include "example_platform.h" #include #include @@ -26,14 +27,13 @@ static void on_publish(cy_future_t* const future) } } -int main(void) +int main(int argc, char* argv[]) { - cy_platform_t* const platform = cy_udp_posix_new(); - if (platform == NULL) { - (void)fprintf(stderr, "cy_udp_posix_new\n"); + const example_platform_t platform = example_platform_make(&argc, argv); + if (platform.platform == NULL) { return 1; } - cy_t* const cy = cy_new(platform, cy_udp_posix_home(platform, "udp_time_pub"), cy_udp_posix_namespace()); + cy_t* const cy = cy_new(platform.platform, example_platform_home(), example_platform_namespace()); if (cy == NULL) { (void)fprintf(stderr, "cy_new\n"); return 1; diff --git a/lib/libcanard b/lib/libcanard new file mode 160000 index 0000000..f5a00fc --- /dev/null +++ b/lib/libcanard @@ -0,0 +1 @@ +Subproject commit f5a00fc64ba9898948a4a7504bb1d16d87a946d6 diff --git a/tests/src/intrusive_fixture_utils.h b/tests/src/intrusive_fixture_utils.h index de65924..615b2ab 100644 --- a/tests/src/intrusive_fixture_utils.h +++ b/tests/src/intrusive_fixture_utils.h @@ -40,6 +40,7 @@ static inline cy_subject_reader_t* intrusive_subject_reader_new(guarded_heap_t* (intrusive_test_subject_reader_t*)guarded_heap_alloc(heap, sizeof(*out)); if (out != NULL) { out->base.subject_id = subject_id; + out->base.extent = extent; out->extent = extent; } return (out != NULL) ? &out->base : NULL; @@ -48,6 +49,7 @@ static inline cy_subject_reader_t* intrusive_subject_reader_new(guarded_heap_t* static inline void intrusive_subject_reader_extent_set(cy_subject_reader_t* const reader, const size_t extent) { intrusive_test_subject_reader_t* const r = (intrusive_test_subject_reader_t*)reader; + reader->extent = extent; r->extent = extent; } diff --git a/tools/ci_example_smoke.py b/tools/ci_example_smoke.py index 62541c6..278ff7f 100644 --- a/tools/ci_example_smoke.py +++ b/tools/ci_example_smoke.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """ -Smoke-test a pair of UDP examples in unattended CI: -1) udp_echo subscribes to a topic. -2) udp_time_pub publishes wall-clock timestamps onto that topic. +Smoke-test a pair of examples in unattended CI: +1) example_echo subscribes to a topic. +2) example_time_pub publishes wall-clock timestamps onto that topic. The test passes once echo output contains both topic name and timestamp text. """ @@ -12,6 +12,7 @@ import os import pathlib import re +import shutil import signal import subprocess import sys @@ -30,6 +31,22 @@ class ProcessFiles: stderr: pathlib.Path +@dataclass +class SmokeResult: + passed: bool + failure_reason: str + echo_files: ProcessFiles + pub_files: ProcessFiles + + +@dataclass +class VcanSetup: + iface_name: str + ip_cmd: str + privilege_prefix: list[str] + created: bool + + def terminate_process(proc: subprocess.Popen[str] | None) -> None: if proc is None: return @@ -55,25 +72,71 @@ def short(text: str, limit: int = 1200) -> str: return text[-limit:] -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--build-dir", default="build", help="CMake build directory path") - parser.add_argument("--timeout-sec", type=float, default=12.0, help="Overall smoke-test timeout") - parser.add_argument("--publisher-delay-sec", type=float, default=1.0, help="Delay before starting publisher") - args = parser.parse_args() - - build_dir = pathlib.Path(args.build_dir).resolve() - echo_bin = build_dir / "examples" / "udp_echo" - pub_bin = build_dir / "examples" / "udp_time_pub" - for bin_path in (echo_bin, pub_bin): - if not (bin_path.is_file() and os.access(bin_path, os.X_OK)): - print(f"Missing executable: {bin_path}", file=sys.stderr) - return 2 - - tmp = build_dir - tmp.mkdir(parents=True, exist_ok=True) - echo_files = ProcessFiles(stdout=tmp / "example_smoke_echo.out", stderr=tmp / "example_smoke_echo.err") - pub_files = ProcessFiles(stdout=tmp / "example_smoke_pub.out", stderr=tmp / "example_smoke_pub.err") +def run_quiet(cmd: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(cmd, text=True, capture_output=True, check=False) + + +def privileged_command_prefix() -> list[str] | None: + if not sys.platform.startswith("linux"): + return None + if getattr(os, "geteuid", lambda: 1)() == 0: + return [] + sudo = shutil.which("sudo") + if sudo is None: + return None + return [sudo, "-n"] if run_quiet([sudo, "-n", "true"]).returncode == 0 else None + + +def setup_vcan() -> tuple[VcanSetup | None, str]: + if not sys.platform.startswith("linux"): + return None, "non-Linux platform" + ip_cmd = shutil.which("ip") + if ip_cmd is None: + return None, "`ip` command is unavailable" + privilege_prefix = privileged_command_prefix() + if privilege_prefix is None: + return None, "passwordless sudo is unavailable" + + iface_name = "vcan0" + created = False + if run_quiet([ip_cmd, "link", "show", "dev", iface_name]).returncode != 0: + modprobe = shutil.which("modprobe") + if modprobe is not None: + _ = run_quiet(privilege_prefix + [modprobe, "vcan"]) + add_result = run_quiet(privilege_prefix + [ip_cmd, "link", "add", "dev", iface_name, "type", "vcan"]) + if add_result.returncode != 0: + return None, f"unable to create {iface_name}: {short(add_result.stderr.strip())}" + created = True + + up_result = run_quiet(privilege_prefix + [ip_cmd, "link", "set", "dev", iface_name, "up"]) + if up_result.returncode != 0: + if created: + _ = run_quiet(privilege_prefix + [ip_cmd, "link", "delete", "dev", iface_name]) + return None, f"unable to activate {iface_name}: {short(up_result.stderr.strip())}" + return VcanSetup(iface_name=iface_name, ip_cmd=ip_cmd, privilege_prefix=privilege_prefix, created=created), "" + + +def cleanup_vcan(setup: VcanSetup | None) -> None: + if (setup is None) or (not setup.created): + return + _ = run_quiet(setup.privilege_prefix + [setup.ip_cmd, "link", "delete", "dev", setup.iface_name]) + + +def run_smoke_case( + build_dir: pathlib.Path, + echo_bin: pathlib.Path, + pub_bin: pathlib.Path, + timeout_sec: float, + publisher_delay_sec: float, + label: str, + echo_args: list[str], + pub_args: list[str], +) -> SmokeResult: + suffix = label.replace("/", "_") + echo_files = ProcessFiles(stdout=build_dir / f"example_smoke_{suffix}_echo.out", + stderr=build_dir / f"example_smoke_{suffix}_echo.err") + pub_files = ProcessFiles(stdout=build_dir / f"example_smoke_{suffix}_pub.out", + stderr=build_dir / f"example_smoke_{suffix}_pub.err") for f in (echo_files.stdout, echo_files.stderr, pub_files.stdout, pub_files.stderr): f.write_text("", encoding="utf8") @@ -86,20 +149,20 @@ def main() -> int: with echo_files.stdout.open("w", encoding="utf8") as echo_out, echo_files.stderr.open( "w", encoding="utf8" ) as echo_err: - echo_proc = subprocess.Popen([str(echo_bin), TOPIC], stdout=echo_out, stderr=echo_err) + echo_proc = subprocess.Popen([str(echo_bin), *echo_args], stdout=echo_out, stderr=echo_err) - time.sleep(args.publisher_delay_sec) + time.sleep(publisher_delay_sec) with pub_files.stdout.open("w", encoding="utf8") as pub_out, pub_files.stderr.open("w", encoding="utf8") as pub_err: - pub_proc = subprocess.Popen([str(pub_bin)], stdout=pub_out, stderr=pub_err) + pub_proc = subprocess.Popen([str(pub_bin), *pub_args], stdout=pub_out, stderr=pub_err) - deadline = time.monotonic() + args.timeout_sec + deadline = time.monotonic() + timeout_sec while time.monotonic() < deadline: if (echo_proc.poll() is not None) and (echo_proc.returncode != 0): - failure_reason = f"udp_echo exited early with code {echo_proc.returncode}" + failure_reason = f"example_echo exited early with code {echo_proc.returncode}" break if (pub_proc.poll() is not None) and (pub_proc.returncode != 0): - failure_reason = f"udp_time_pub exited early with code {pub_proc.returncode}" + failure_reason = f"example_time_pub exited early with code {pub_proc.returncode}" break echo_stdout = read_text(echo_files.stdout) @@ -113,17 +176,73 @@ def main() -> int: terminate_process(pub_proc) terminate_process(echo_proc) - if passed: - print("Example smoke test passed: observed echoed demo/time timestamp.") + return SmokeResult(passed=passed, failure_reason=failure_reason, echo_files=echo_files, pub_files=pub_files) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--build-dir", default="build", help="CMake build directory path") + parser.add_argument("--timeout-sec", type=float, default=12.0, help="Overall smoke-test timeout") + parser.add_argument("--publisher-delay-sec", type=float, default=1.0, help="Delay before starting publisher") + args = parser.parse_args() + + build_dir = pathlib.Path(args.build_dir).resolve() + echo_bin = build_dir / "examples" / "example_echo" + pub_bin = build_dir / "examples" / "example_time_pub" + for bin_path in (echo_bin, pub_bin): + if not (bin_path.is_file() and os.access(bin_path, os.X_OK)): + print(f"Missing executable: {bin_path}", file=sys.stderr) + return 2 + + build_dir.mkdir(parents=True, exist_ok=True) + + udp_result = run_smoke_case(build_dir, + echo_bin, + pub_bin, + args.timeout_sec, + args.publisher_delay_sec, + "udp", + [TOPIC], + []) + if not udp_result.passed: + print(f"Example smoke test failed: {udp_result.failure_reason}", file=sys.stderr) + print("--- UDP example_echo stdout (tail) ---", file=sys.stderr) + print(short(read_text(udp_result.echo_files.stdout)), file=sys.stderr) + print("--- UDP example_echo stderr (tail) ---", file=sys.stderr) + print(short(read_text(udp_result.echo_files.stderr)), file=sys.stderr) + print("--- UDP example_time_pub stderr (tail) ---", file=sys.stderr) + print(short(read_text(udp_result.pub_files.stderr)), file=sys.stderr) + return 1 + + vcan_setup, vcan_skip_reason = setup_vcan() + if vcan_setup is None: + print(f"Example smoke test passed: observed echoed demo/time timestamp; vcan skipped ({vcan_skip_reason}).") + return 0 + + try: + iface_arg = f"iface=socketcan:{vcan_setup.iface_name}" + vcan_result = run_smoke_case(build_dir, + echo_bin, + pub_bin, + args.timeout_sec, + args.publisher_delay_sec, + "vcan", + [iface_arg, TOPIC], + [iface_arg]) + finally: + cleanup_vcan(vcan_setup) + + if vcan_result.passed: + print("Example smoke test passed: observed echoed demo/time timestamp over UDP and vcan.") return 0 - print(f"Example smoke test failed: {failure_reason}", file=sys.stderr) - print("--- udp_echo stdout (tail) ---", file=sys.stderr) - print(short(read_text(echo_files.stdout)), file=sys.stderr) - print("--- udp_echo stderr (tail) ---", file=sys.stderr) - print(short(read_text(echo_files.stderr)), file=sys.stderr) - print("--- udp_time_pub stderr (tail) ---", file=sys.stderr) - print(short(read_text(pub_files.stderr)), file=sys.stderr) + print(f"Example smoke test failed: {vcan_result.failure_reason}", file=sys.stderr) + print("--- vcan example_echo stdout (tail) ---", file=sys.stderr) + print(short(read_text(vcan_result.echo_files.stdout)), file=sys.stderr) + print("--- vcan example_echo stderr (tail) ---", file=sys.stderr) + print(short(read_text(vcan_result.echo_files.stderr)), file=sys.stderr) + print("--- vcan example_time_pub stderr (tail) ---", file=sys.stderr) + print(short(read_text(vcan_result.pub_files.stderr)), file=sys.stderr) return 1