From 90d7ba5ee745fa439015c8e19b0390f6b7fa94c8 Mon Sep 17 00:00:00 2001 From: Laudin Molina Troconis Date: Thu, 19 Mar 2026 18:16:58 +0100 Subject: [PATCH 1/5] feat: log Transport Service bridge node IDs With Z-Wave LR, the node ID change the representation from 8-bit to 16-bit. Different bugs have appear because of the incorrect or representation. Therefore, we are adding debug logs on the transport service bridge to debug and follow the correct representation of node IDs. Relates-to: ZGW-3457 Origin: https://github.com/SiliconLabs/zipgateway/pull/47 Signed-off-by: Laudin Molina Troconis --- src/transport/ZW_SendDataAppl.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/transport/ZW_SendDataAppl.c b/src/transport/ZW_SendDataAppl.c index 5106c54..16a173e 100644 --- a/src/transport/ZW_SendDataAppl.c +++ b/src/transport/ZW_SendDataAppl.c @@ -810,7 +810,12 @@ PROCESS_THREAD(ZW_SendDataAppl_process, ev, data) if (SupportsCmdClass(current_session_ll->fb->param.dnode, COMMAND_CLASS_TRANSPORT_SERVICE)) { - DBG_PRINTF("SEND_EVENT_SEND_NEXT_LL | SupportsCmdClass | ZW_TransportService_SendData \n"); + DBG_PRINTF("SEND_EVENT_SEND_NEXT_LL | SupportsCmdClass | ZW_TransportService_SendData\n" + "snode=%u dnode=%u frame_len=%u dnode_low8=%u\n", + (unsigned)current_session_ll->fb->param.snode, + (unsigned)current_session_ll->fb->param.dnode, + (unsigned)current_session_ll->fb->frame_len, + (unsigned)(current_session_ll->fb->param.dnode & 0xFFu)); //ASSERT(current_session_ll->param.snode == MyNodeID); //TODO make transport service bridge aware rc = ZW_TransportService_SendData(¤t_session_ll->fb->param, (u8_t*) current_session_ll->fb->frame_data, From f6c0624c2dd2c78d9916dc010f892a7eb3eeace2 Mon Sep 17 00:00:00 2001 From: Laudin Molina Troconis Date: Sat, 21 Mar 2026 01:47:17 +0100 Subject: [PATCH 2/5] chore: bootstrap CMock in CMake Ensure test builds have access CMock generator by downloading it into the build folder. Relates-to: ZGW-3457 Origin: https://github.com/SiliconLabs/zipgateway/pull/47 Signed-off-by: Laudin Molina Troconis --- helper.mk | 2 +- libs2/test/CMakeLists.txt | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/helper.mk b/helper.mk index dead32b..f252068 100755 --- a/helper.mk +++ b/helper.mk @@ -22,7 +22,7 @@ docker_arch_i386=i386 docker_arch?=$(docker_arch_${target_debian_arch}) packages?=make cmake time file git sudo -packages+=build-essential pkg-config bison flex python +packages+=build-essential pkg-config bison flex python ruby packages+=libusb-1.0-0-dev libssl-dev libxml2-dev libjson-c-dev packages+=doxygen xsltproc plantuml roffit packages+=radvd parprouted bridge-utils net-tools zip unzip diff --git a/libs2/test/CMakeLists.txt b/libs2/test/CMakeLists.txt index 0f132fc..39daf61 100644 --- a/libs2/test/CMakeLists.txt +++ b/libs2/test/CMakeLists.txt @@ -32,6 +32,52 @@ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "C51") target_link_libraries(new_test_t2 zipgateway-lib) endif() add_test(test_transport_service2 new_test_t2) + + set(CMOCK_RB "" CACHE FILEPATH "Path to CMock lib/cmock.rb used for local mock regeneration") + set(CMOCK_VERSION "2.4.6" CACHE STRING "CMock version used for regeneration") + set(CMOCK_ARCHIVE "${CMAKE_BINARY_DIR}/tools/cmock-v${CMOCK_VERSION}.tar.gz") + set(CMOCK_ARCHIVE_SHA256 "7b4b0a1ca9ecd4d461e3a442b4b2c98c37e451d8966e636483142fdf49667ed7" + CACHE STRING "Expected SHA256 for CMock archive") + set(CMOCK_ROOT "${CMAKE_BINARY_DIR}/tools/CMock-${CMOCK_VERSION}") + set(CMOCK_RB_DEFAULT "${CMOCK_ROOT}/lib/cmock.rb") + if(CMOCK_RB STREQUAL "") + if(NOT EXISTS "${CMOCK_RB_DEFAULT}") + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/tools") + if(NOT EXISTS "${CMOCK_ARCHIVE}") + message(STATUS "Downloading CMock v${CMOCK_VERSION} for mock regeneration") + file(DOWNLOAD + "https://github.com/ThrowTheSwitch/CMock/archive/refs/tags/v${CMOCK_VERSION}.tar.gz" + "${CMOCK_ARCHIVE}" + SHOW_PROGRESS + STATUS CMOCK_DOWNLOAD_STATUS + TLS_VERIFY ON) + list(GET CMOCK_DOWNLOAD_STATUS 0 CMOCK_DOWNLOAD_CODE) + list(GET CMOCK_DOWNLOAD_STATUS 1 CMOCK_DOWNLOAD_MESSAGE) + if(NOT CMOCK_DOWNLOAD_CODE EQUAL 0) + message(FATAL_ERROR "Failed to download CMock archive: ${CMOCK_DOWNLOAD_MESSAGE}") + endif() + endif() + + file(SHA256 "${CMOCK_ARCHIVE}" CMOCK_ARCHIVE_SHA256_ACTUAL) + if(NOT CMOCK_ARCHIVE_SHA256_ACTUAL STREQUAL CMOCK_ARCHIVE_SHA256) + message(FATAL_ERROR + "CMock archive SHA256 mismatch. Expected ${CMOCK_ARCHIVE_SHA256}, got ${CMOCK_ARCHIVE_SHA256_ACTUAL}") + endif() + + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar xzf "${CMOCK_ARCHIVE}" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/tools" + RESULT_VARIABLE CMOCK_EXTRACT_RESULT) + if(NOT CMOCK_EXTRACT_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to extract ${CMOCK_ARCHIVE}") + endif() + + file(MAKE_DIRECTORY "${CMOCK_ROOT}/vendor/unity/auto") + file(COPY "${CMAKE_SOURCE_DIR}/libs2/TestFramework/unity/auto/type_sanitizer.rb" + DESTINATION "${CMOCK_ROOT}/vendor/unity/auto") + endif() + set(CMOCK_RB "${CMOCK_RB_DEFAULT}" CACHE FILEPATH "Path to CMock lib/cmock.rb used by regen_mock_zw_transport_api" FORCE) + endif() endif() add_definitions( -DRANDLEN=64 ) From f1ebfa8e45b07e4af5bf739191b05c030389d4a4 Mon Sep 17 00:00:00 2001 From: Laudin Molina Troconis Date: Sat, 21 Mar 2026 02:19:36 +0100 Subject: [PATCH 3/5] fix: use 16-bit IDs to avoid LR node ID truncation Transport Service ZIPGW bridge path was narrowing the node IDs from 16-bit to 8-bits. Add a standalong test that uses the flags used to build the Z/IP Gateway and avoid the specific NEW_TEST_T2, as the test would result in different `ZW_SendData_Bridge` object. Use CMock to simplify mocking and to easy eventual new tests. Relates-to: ZGW-3457 Origin: https://github.com/SiliconLabs/zipgateway/pull/47 Signed-off-by: Laudin Molina Troconis --- libs2/test/CMakeLists.txt | 68 +++++++++++++++++ libs2/test/cmock/cmock_config.yml | 7 ++ libs2/test/cmock/cmock_legacy_unity_compat.h | 16 ++++ libs2/test/test_t2_zipgw_runtime_nodeid.c | 77 ++++++++++++++++++++ libs2/transport_service/transport_service2.c | 2 +- 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 libs2/test/cmock/cmock_config.yml create mode 100644 libs2/test/cmock/cmock_legacy_unity_compat.h create mode 100644 libs2/test/test_t2_zipgw_runtime_nodeid.c diff --git a/libs2/test/CMakeLists.txt b/libs2/test/CMakeLists.txt index 39daf61..8399d17 100644 --- a/libs2/test/CMakeLists.txt +++ b/libs2/test/CMakeLists.txt @@ -78,6 +78,74 @@ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "C51") endif() set(CMOCK_RB "${CMOCK_RB_DEFAULT}" CACHE FILEPATH "Path to CMock lib/cmock.rb used by regen_mock_zw_transport_api" FORCE) endif() + + set(CMOCK_RUNTIME_C "${CMOCK_ROOT}/src/cmock.c" + CACHE FILEPATH "Path to CMock runtime cmock.c used by unit tests") + if(NOT EXISTS "${CMOCK_RUNTIME_C}") + message(FATAL_ERROR "CMock runtime source not found: ${CMOCK_RUNTIME_C}") + endif() + + set(CMOCK_GEN_DIR "${CMAKE_BINARY_DIR}/cmock_gen") + set(CMOCK_INPUT_DIR "${CMAKE_BINARY_DIR}/cmock-input") + set(CMOCK_INPUT_HEADER "${CMOCK_INPUT_DIR}/ZW_transport_api.h") + set(MOCK_ZW_TRANSPORT_API_C "${CMOCK_GEN_DIR}/MockZW_transport_api.c") + set(MOCK_ZW_TRANSPORT_API_H "${CMOCK_GEN_DIR}/MockZW_transport_api.h") + + add_custom_command( + OUTPUT "${CMOCK_INPUT_HEADER}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMOCK_INPUT_DIR}" + COMMAND "${CMAKE_C_COMPILER}" -E -P + -DZW_CONTROLLER_BRIDGE + -I"${CMAKE_SOURCE_DIR}/Z-Wave/include" + "${CMAKE_SOURCE_DIR}/Z-Wave/include/ZW_transport_api.h" + -o "${CMOCK_INPUT_HEADER}" + DEPENDS "${CMAKE_SOURCE_DIR}/Z-Wave/include/ZW_transport_api.h" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Preprocessing ZW_transport_api.h for CMock generation") + + add_custom_command( + OUTPUT "${MOCK_ZW_TRANSPORT_API_C}" "${MOCK_ZW_TRANSPORT_API_H}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMOCK_GEN_DIR}" + COMMAND ${CMAKE_COMMAND} -E env ruby "${CMOCK_RB}" + -o"${CMAKE_CURRENT_SOURCE_DIR}/cmock/cmock_config.yml" + "${CMOCK_INPUT_HEADER}" + DEPENDS "${CMOCK_INPUT_HEADER}" "${CMAKE_CURRENT_SOURCE_DIR}/cmock/cmock_config.yml" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Generating MockZW_transport_api in build directory") + set_source_files_properties("${MOCK_ZW_TRANSPORT_API_C}" "${MOCK_ZW_TRANSPORT_API_H}" PROPERTIES GENERATED TRUE) + + add_custom_target(regen_mock_zw_transport_api + DEPENDS "${MOCK_ZW_TRANSPORT_API_C}" "${MOCK_ZW_TRANSPORT_API_H}") + + add_unity_test(NAME test_t2_zipgw_runtime_nodeid FILES + test_t2_zipgw_runtime_nodeid.c + "${CMOCK_RUNTIME_C}" + "${MOCK_ZW_TRANSPORT_API_C}" + ${CMAKE_CURRENT_SOURCE_DIR}/../transport_service/transport_service2.c + ${CMAKE_CURRENT_SOURCE_DIR}/../transport_service/transport2_fsm.c) + add_dependencies(test_t2_zipgw_runtime_nodeid regen_mock_zw_transport_api) + target_compile_definitions(test_t2_zipgw_runtime_nodeid PRIVATE ZIPGW) + + # Explicitly undefine NEW_TEST_T2 here so this regression test follows the + # production TS code path. + target_compile_options(test_t2_zipgw_runtime_nodeid PRIVATE -U NEW_TEST_T2) + # Generated CMock code may reference legacy Unity detail macros that are + # not available in the Unity variant used by this tree. + target_compile_options(test_t2_zipgw_runtime_nodeid PRIVATE + -include "${CMAKE_CURRENT_SOURCE_DIR}/cmock/cmock_legacy_unity_compat.h") + target_include_directories(test_t2_zipgw_runtime_nodeid PRIVATE + ${CMAKE_BINARY_DIR} + "${CMOCK_GEN_DIR}" + "${CMOCK_ROOT}/src" + ${CMAKE_CURRENT_SOURCE_DIR}/../transport_service/ + ${CMAKE_CURRENT_SOURCE_DIR}/../include/ + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/transport + ${CMAKE_SOURCE_DIR}/src/utls + ${CMAKE_SOURCE_DIR}/contiki/core + ${CMAKE_SOURCE_DIR}/contiki/core/sys + ${CMAKE_SOURCE_DIR}/contiki/platform/linux + ${CMAKE_SOURCE_DIR}/Z-Wave/include) endif() add_definitions( -DRANDLEN=64 ) diff --git a/libs2/test/cmock/cmock_config.yml b/libs2/test/cmock/cmock_config.yml new file mode 100644 index 0000000..e727431 --- /dev/null +++ b/libs2/test/cmock/cmock_config.yml @@ -0,0 +1,7 @@ +:cmock: + :mock_path: cmock_gen + :plugins: + - :ignore + - :ignore_arg + - :expect + :treat_externs: :include diff --git a/libs2/test/cmock/cmock_legacy_unity_compat.h b/libs2/test/cmock/cmock_legacy_unity_compat.h new file mode 100644 index 0000000..90dbd1a --- /dev/null +++ b/libs2/test/cmock/cmock_legacy_unity_compat.h @@ -0,0 +1,16 @@ +#ifndef CMOCK_LEGACY_UNITY_COMPAT_H +#define CMOCK_LEGACY_UNITY_COMPAT_H + +#ifndef UNITY_SET_DETAIL +#define UNITY_SET_DETAIL(msg) ((void)(msg)) +#endif + +#ifndef UNITY_CLR_DETAILS +#define UNITY_CLR_DETAILS() ((void)0) +#endif + +#ifndef UNITY_SET_DETAILS +#define UNITY_SET_DETAILS(msg1, msg2) ((void)(msg1), (void)(msg2)) +#endif + +#endif diff --git a/libs2/test/test_t2_zipgw_runtime_nodeid.c b/libs2/test/test_t2_zipgw_runtime_nodeid.c new file mode 100644 index 0000000..ddca1f3 --- /dev/null +++ b/libs2/test/test_t2_zipgw_runtime_nodeid.c @@ -0,0 +1,77 @@ +/* Copyright Silicon Laboratories Inc. + * + * Build Transport Service with ZIPGW and without NEW_TEST_T2, then verify + * that LR node IDs are preserved on the TS -> ZW_SendData_Bridge call path. + */ + +#include +#include + +// clang-format off +#include "unity.h" +#include "MockZW_transport_api.h" +// clang-format on + +#include "S2.h" +#include "transport_service2.h" + +BYTE MyNodeID = 1; + +void setUp(void) { MockZW_transport_api_Init(); } + +void tearDown(void) { + MockZW_transport_api_Verify(); + MockZW_transport_api_Destroy(); +} + +struct ctimer; +typedef unsigned long clock_time_t; +void ctimer_set(struct ctimer *c, clock_time_t t, void (*f)(void *), void *ptr) { + (void)c; + (void)t; + (void)f; + (void)ptr; +} + +void ctimer_stop(struct ctimer *c) { (void)c; } + +clock_time_t clock_time(void) { return 0; } + +uint16_t zgw_crc16(uint16_t crc16, uint8_t *data, unsigned long data_len) { + (void)data; + (void)data_len; + return crc16; +} + +static void app_cmd_handler(ts_param_t *p, ZW_APPLICATION_TX_BUFFER *pCmd, uint16_t cmdLength) { + (void)p; + (void)pCmd; + (void)cmdLength; +} + +static void status_cb(uint8_t txStatus, TX_STATUS_TYPE *txStatusReport) { + (void)txStatus; + (void)txStatusReport; +} + +void test_zipgw_ts_preserves_lr_destination_nodeid(void) { + ts_param_t p; + uint8_t payload[64]; + + memset(&p, 0, sizeof(p)); + memset(payload, 0xAA, sizeof(payload)); + + p.snode = 0x00ff; + p.dnode = 257; + p.tx_flags = TRANSMIT_OPTION_ACK | TRANSMIT_OPTION_AUTO_ROUTE | TRANSMIT_OPTION_EXPLORE; + + ZW_SendData_Bridge_ExpectAndReturn(p.snode, p.dnode, NULL, 0, 0, NULL, 1); + ZW_SendData_Bridge_IgnoreArg_pData(); + ZW_SendData_Bridge_IgnoreArg_dataLength(); + ZW_SendData_Bridge_IgnoreArg_txOptions(); + ZW_SendData_Bridge_IgnoreArg_completedFunc(); + + ZW_TransportService_Init(app_cmd_handler); + TEST_ASSERT_TRUE_MESSAGE(ZW_TransportService_SendData(&p, payload, sizeof(payload), status_cb), + "ZW_TransportService_SendData returned false"); +} diff --git a/libs2/transport_service/transport_service2.c b/libs2/transport_service/transport_service2.c index e972959..d816997 100644 --- a/libs2/transport_service/transport_service2.c +++ b/libs2/transport_service/transport_service2.c @@ -107,7 +107,7 @@ typedef void (*ZW_CommandHandler_Callback_t)(ts_param_t* p, ZW_APPLICATION_TX_BU #endif #ifndef __C51__ -extern uint8_t ZW_SendData_Bridge(uint8_t, uint8_t, uint8_t *, uint8_t, uint8_t, VOID_CALLBACKFUNC(completedFunc)(uint8_t, TX_STATUS_TYPE*)); +extern uint8_t ZW_SendData_Bridge(uint16_t, uint16_t, uint8_t *, uint8_t, uint8_t, VOID_CALLBACKFUNC(completedFunc)(uint8_t, TX_STATUS_TYPE*)); #endif /* ifndef __C51__ */ #endif From 6093cb01cf47cfc1e3cb83fedfc286950de30569 Mon Sep 17 00:00:00 2001 From: Laudin Molina Troconis Date: Wed, 25 Mar 2026 00:48:00 +0100 Subject: [PATCH 4/5] fix: pop session after TRANSMIT_COMPLETE_FAIL Transport service (TS) fails to deliver a fragmented frame it called send_data_callback_func(TRANSMIT_COMPLETE_FAIL), in this case the TS does not pop the falied session. Therefore, subsequent sends appended a fresh frame to the tail while the element in the top stales and stays in the head, causing all following commands to be re-driven through TS. Relates-to: ZGW-3457 Origin: https://github.com/SiliconLabs/zipgateway/pull/47 Signed-off-by: Laudin Molina Troconis --- src/transport/S2_wrap.c | 2 +- src/transport/ZW_SendDataAppl.c | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/transport/S2_wrap.c b/src/transport/S2_wrap.c index 385ba03..f602a4c 100644 --- a/src/transport/S2_wrap.c +++ b/src/transport/S2_wrap.c @@ -603,7 +603,7 @@ uint8_t S2_send_frame(struct S2* ctxt,const s2_connection_t* conn, uint8_t* buf, p.snode = conn->l_node; p.dnode = conn->r_node; p.tx_flags = conn->zw_tx_options; - LOG_PRINTF(" Sending S2_send_frame %i %d -> %d\n", len, p.snode, p.dnode); + LOG_PRINTF("Sending S2_send_frame %i %d -> %d\n", len, p.snode, p.dnode); transmit_start_time = clock_time(); return send_data(&p, buf, len,S2_send_frame_callback,ctxt); } diff --git a/src/transport/ZW_SendDataAppl.c b/src/transport/ZW_SendDataAppl.c index 16a173e..277d168 100644 --- a/src/transport/ZW_SendDataAppl.c +++ b/src/transport/ZW_SendDataAppl.c @@ -288,25 +288,36 @@ send_data_callback_func(u8_t status, TX_STATUS_TYPE* ts) return; } - - // if (status == TRANSMIT_COMPLETE_NO_ACK) + // TS and other senders report TRANSMIT_COMPLETE_FAIL. Pop and + // free the session so the queue advances; the original hold-for-retry + // behaviour (previously on NO_ACK) is preserved only when the resend watchdog + // is active. if (status == TRANSMIT_COMPLETE_FAIL) { etimer_stop(&emergency_timer); if(resend_counter == 0) { - send_data_appl_session_t *s = list_head(send_data_list); + send_data_appl_session_t *s = list_pop(send_data_list); if (s) { if (s->callback) { s->callback(status, s->user, ts); } + zw_frame_buffer_free(s->fb); + + char refcount = memb_free(&session_memb, s); + if(refcount == -1) { + ERR_PRINTF("attempt to deallocate illegal memory block\n"); + ASSERT(0); + } } else { + DBG_PRINTF("event in the send data list is NULL\n"); ASSERT(0); } + process_post(&ZW_SendDataAppl_process, SEND_EVENT_SEND_NEXT_LL, NULL); } lock_ll = FALSE; From 3df7e921e1c4ed7309a36b6fb11ba217d701e4ed Mon Sep 17 00:00:00 2001 From: Laudin Molina Troconis Date: Fri, 3 Apr 2026 11:24:31 +0200 Subject: [PATCH 5/5] test: add unit tests for send_data_callback_func FAIL path Before the fix, TRANSMIT_COMPLETE_FAIL with resend_counter==0 left the session in send_data_list, causing every subsequent SEND_EVENT_SEND_NEXT_LL to re-drive the same stuck session through Transport Service. The implementation adds UNIT_TEST-guarded seam functions to ZW_SendDataAppl.c that expose private/static states. The test uses wraps to replace process_post and zw_frame_buffer_free. Choose this solution over CMock for sake of simplicity. Relates-to: ZGW-3457 Origin: https://github.com/SiliconLabs/zipgateway/pull/47 Signed-off-by: Laudin Molina Troconis --- libs2/test/CMakeLists.txt | 28 ++++ libs2/test/test_send_data_callback.c | 196 +++++++++++++++++++++++++++ src/transport/ZW_SendDataAppl.c | 21 +++ 3 files changed, 245 insertions(+) create mode 100644 libs2/test/test_send_data_callback.c diff --git a/libs2/test/CMakeLists.txt b/libs2/test/CMakeLists.txt index 8399d17..a712798 100644 --- a/libs2/test/CMakeLists.txt +++ b/libs2/test/CMakeLists.txt @@ -148,6 +148,34 @@ if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "C51") ${CMAKE_SOURCE_DIR}/Z-Wave/include) endif() +# ZW_SendDataAppl.c is compiled directly into the test (not taken from the +# pre-built zipgateway-lib) so that the -DUNIT_TEST flag (set globally by +# add_definitions at the top of this file) is in effect. This is required +# to compile the UNIT_TEST seam functions that expose private state. +# +# The remaining zipgateway-lib symbols that ZW_SendDataAppl.c needs are +# provided either by linking zipgateway-lib or by --wrap stubs in the test. +# --------------------------------------------------------------------------- +if (${CMAKE_PROJECT_NAME} MATCHES "zipgateway") + add_unity_test(NAME test_send_data_callback + FILES test_send_data_callback.c + ${CMAKE_SOURCE_DIR}/src/transport/ZW_SendDataAppl.c + ${CMAKE_SOURCE_DIR}/test/zipgateway_main_stubs.c + LIBRARIES zipgateway-lib) + + set_target_properties(test_send_data_callback PROPERTIES LINK_FLAGS + "-Wl,-wrap=process_post \ + -Wl,-wrap=process_exit \ + -Wl,-wrap=process_start \ + -Wl,-wrap=get_queue_state \ + -Wl,-wrap=zw_frame_buffer_free \ + -Wl,-wrap=etimer_stop \ + -Wl,-wrap=etimer_set \ + -Wl,-wrap=etimer_expired \ + -Wl,-wrap=sec0_abort_all_tx_sessions \ + -Wl,-wrap=ima_send_data_done") +endif() + add_definitions( -DRANDLEN=64 ) add_unity_test(NAME test_ctr_dbrg FILES test_ctr_dbrg.c ../crypto/ctr_drbg/ctr_drbg.c ../crypto/aes/aes.c) diff --git a/libs2/test/test_send_data_callback.c b/libs2/test/test_send_data_callback.c new file mode 100644 index 0000000..602edd8 --- /dev/null +++ b/libs2/test/test_send_data_callback.c @@ -0,0 +1,196 @@ +/* SPDX-License-Identifier: LicenseRef-MSLA + * SPDX-FileCopyrightText: Silicon Laboratories Inc. https://www.silabs.com + */ + +#include +#include +#include + +#include "unity.h" + +#include "ZW_SendDataAppl.h" /* public API + ts_param_t */ +#include "ZW_transport_api.h" /* TRANSMIT_COMPLETE_*, TX_STATUS_TYPE */ +#include "node_queue.h" /* en_queue_state, QS_IDLE */ +#include "zw_frame_buffer.h" /* zw_frame_buffer_element_t */ + +/* + * Test-seam declarations + * + * These are compiled into ZW_SendDataAppl.c only when -DUNIT_TEST is set + * (which libs2/test/CMakeLists.txt sets globally with add_definitions). + * -------------------------------------------------------------------------*/ +void ZW_SendDataAppl_set_lock_ll(uint8_t v); +void ZW_SendDataAppl_set_resend_counter(uint8_t v); +uint8_t ZW_SendDataAppl_get_lock_ll(void); +void *ZW_SendDataAppl_alloc_session(void); +void ZW_SendDataAppl_push_session(void *s); +void *ZW_SendDataAppl_list_head(void); +void ZW_SendDataAppl_trigger_fail_for_test(void); + +typedef struct { + void *next; + zw_frame_buffer_element_t *fb; + void *user; + ZW_SendDataAppl_Callback_t callback; +} test_session_t; + +static int app_cb_count; +static uint8_t app_cb_status; +static void *app_cb_user; + +static void spy_app_callback(uint8_t status, void *user, TX_STATUS_TYPE *tx) { + (void)tx; + app_cb_count++; + app_cb_status = status; + app_cb_user = user; +} + +static int fb_free_count; +static zw_frame_buffer_element_t *fb_free_ptr; + +void __wrap_zw_frame_buffer_free(zw_frame_buffer_element_t *e) { + fb_free_count++; + fb_free_ptr = e; +} + +static int pp_count; +static process_event_t pp_last_event; + +#define EXPECTED_SEND_NEXT_LL ((process_event_t)1) + +int __wrap_process_post(struct process *p, process_event_t ev, void *data) { + (void)p; + (void)data; + pp_count++; + pp_last_event = ev; + return 0; +} + +enum en_queue_state __wrap_get_queue_state(void) { return QS_IDLE; } + +void __wrap_process_exit(struct process *p) { (void)p; } +void __wrap_process_start(struct process *p, const char *arg) { + (void)p; + (void)arg; +} + +void __wrap_etimer_stop(struct etimer *t) { (void)t; } +void __wrap_etimer_set(struct etimer *t, unsigned long i) { + (void)t; + (void)i; +} +int __wrap_etimer_expired(struct etimer *t) { + (void)t; + return 1; +} + +void __wrap_sec0_abort_all_tx_sessions(void) {} +void __wrap_ima_send_data_done(uint16_t n, uint8_t s, TX_STATUS_TYPE *t) { + (void)n; + (void)s; + (void)t; +} + +static void reset_spies(void) { + app_cb_count = 0; + app_cb_status = 0xFF; + app_cb_user = NULL; + + fb_free_count = 0; + fb_free_ptr = NULL; + + pp_count = 0; + pp_last_event = 0xFF; +} + +/* ------------------------------------------------------------------------- + * ZW_SendDataAppl_init() sets lock=FALSE, lock_ll=FALSE, clears the list + * and reinitialises the memb pool. It also (re)starts the Contiki process, + * which is stubbed to a no-op here. + * -------------------------------------------------------------------------*/ +void setUp(void) { + reset_spies(); + ZW_SendDataAppl_init(); +} + +void tearDown(void) {} + +void test_ts_should_pop_session_and_advance_when_transmit_complete_fail_and_resend_zero(void) { + static zw_frame_buffer_element_t fb_a, fb_b; + int sentinel = 0xBEEF; + + test_session_t *session_a = (test_session_t *)ZW_SendDataAppl_alloc_session(); + test_session_t *session_b = (test_session_t *)ZW_SendDataAppl_alloc_session(); + TEST_ASSERT_NOT_NULL_MESSAGE(session_a, "memb_alloc failed — session pool exhausted"); + TEST_ASSERT_NOT_NULL_MESSAGE(session_b, "memb_alloc failed — session pool exhausted"); + + session_a->fb = &fb_a; + session_a->user = &sentinel; + session_a->callback = spy_app_callback; + + session_b->fb = &fb_b; + session_b->user = NULL; + session_b->callback = NULL; + + /* head = A, A->next = B */ + ZW_SendDataAppl_push_session(session_b); + ZW_SendDataAppl_push_session(session_a); + + TEST_ASSERT_EQUAL_PTR_MESSAGE(session_a, ZW_SendDataAppl_list_head(), + "Pre-condition: A must be at the head before the failure"); + + ZW_SendDataAppl_set_lock_ll(1); + ZW_SendDataAppl_set_resend_counter(0); + + ZW_SendDataAppl_trigger_fail_for_test(); + + TEST_ASSERT_EQUAL_PTR_MESSAGE(session_b, ZW_SendDataAppl_list_head(), + "Session B must be the head after session A is popped on FAIL"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(1, fb_free_count, "Exactly one buffer must be freed (session A's)"); + TEST_ASSERT_EQUAL_PTR_MESSAGE(&fb_a, fb_free_ptr, + "The freed buffer must be session A's, not B's"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(1, app_cb_count, + "Application callback must be called exactly once"); + TEST_ASSERT_EQUAL_UINT8_MESSAGE(TRANSMIT_COMPLETE_FAIL, app_cb_status, + "Application callback must receive TRANSMIT_COMPLETE_FAIL"); + TEST_ASSERT_EQUAL_PTR_MESSAGE(&sentinel, app_cb_user, + "Application callback must receive the original user pointer"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(1, pp_count, + "SEND_EVENT_SEND_NEXT_LL must be posted after A is removed"); + TEST_ASSERT_EQUAL_MESSAGE(EXPECTED_SEND_NEXT_LL, pp_last_event, + "The posted event must be SEND_EVENT_SEND_NEXT_LL"); +} + +void test_ts_should_not_pop_session_when_resend_is_needed() { + static zw_frame_buffer_element_t fb; + + test_session_t *s = (test_session_t *)ZW_SendDataAppl_alloc_session(); + TEST_ASSERT_NOT_NULL(s); + s->fb = &fb; + s->user = NULL; + s->callback = spy_app_callback; + ZW_SendDataAppl_push_session(s); + + ZW_SendDataAppl_set_lock_ll(1); + ZW_SendDataAppl_set_resend_counter(1); + + ZW_SendDataAppl_trigger_fail_for_test(); + + TEST_ASSERT_EQUAL_PTR_MESSAGE(s, ZW_SendDataAppl_list_head(), + "Session must remain in send_data_list when resend_counter > 0"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(0, fb_free_count, + "zw_frame_buffer_free must NOT be called when resend_counter > 0"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(0, app_cb_count, + "Application callback must NOT be called when resend_counter > 0"); + + TEST_ASSERT_EQUAL_INT_MESSAGE(0, pp_count, + "process_post must NOT be called when resend_counter > 0"); + + TEST_ASSERT_EQUAL_UINT8_MESSAGE(0, ZW_SendDataAppl_get_lock_ll(), + "lock_ll must be FALSE after the callback (retry path)"); +} diff --git a/src/transport/ZW_SendDataAppl.c b/src/transport/ZW_SendDataAppl.c index 277d168..5ad09ce 100644 --- a/src/transport/ZW_SendDataAppl.c +++ b/src/transport/ZW_SendDataAppl.c @@ -912,3 +912,24 @@ PROCESS_THREAD(ZW_SendDataAppl_process, ev, data) } PROCESS_END(); } + +#ifdef UNIT_TEST +void ZW_SendDataAppl_set_lock_ll(uint8_t value) { lock_ll = value; } +void ZW_SendDataAppl_set_resend_counter(uint8_t value) { resend_counter = value; } +uint8_t ZW_SendDataAppl_get_lock_ll(void) { return lock_ll; } + +void *ZW_SendDataAppl_alloc_session(void) { return memb_alloc(&session_memb); } +void ZW_SendDataAppl_push_session(void *s) { list_push(send_data_list, s); } +void *ZW_SendDataAppl_list_head(void) { return list_head(send_data_list); } + +/* + * Drive send_data_callback_func(TRANSMIT_COMPLETE_FAIL, NULL) directly. + * This is the same call the emergency timer makes when the NCP misses a + * callback. Exposing it here lets tests trigger the FAIL path without + * needing a running Contiki scheduler. + */ +void ZW_SendDataAppl_trigger_fail_for_test(void) +{ + send_data_callback_func(TRANSMIT_COMPLETE_FAIL, NULL); +} +#endif /* UNIT_TEST */