From f2631322ca5bbcf652481408e45a9fc26eff4ca0 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:08:16 +0200 Subject: [PATCH 01/39] project.yml: Exclude STM32 vendor tree from unit tests Prevent Ceedling from adding every STM32Cube directory to compiler include paths, which can exceed the system argument length limit. --- project.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/project.yml b/project.yml index d83d840..e4e0342 100644 --- a/project.yml +++ b/project.yml @@ -38,6 +38,7 @@ - src/** - inc/** - ports/** + - -:ports/extra/** :support: - test/support :libraries: [] From 9146298e285ac42cd9ab86482bd5d2ea6dbbc581 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:08:21 +0200 Subject: [PATCH 02/39] u_cx_at_client: Fix binary URCs without queue Defer direct URC callbacks until framing determines whether binary data follows. Report the preserved URC line length after parser state resets. --- src/u_cx_at_client.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index 7ae57a6..1007ff9 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -152,11 +152,7 @@ static int32_t parseLine(uCxAtClient_t *pClient, char *pLine, size_t lineLength) U_CX_LOG_LINE_I(U_CX_LOG_CH_WARN, pClient->instance, "URC queue full - dropping URC"); } #else - const struct uCxAtClientConfig *pConfig = pClient->pConfig; - if (pClient->urcCallback) { - pClient->urcCallback(pClient, pClient->pUrcCallbackTag, pConfig->pRxBuffer, - pClient->rxBufferPos, NULL, 0); - } + ret = AT_PARSER_GOT_URC; #endif } else { // Received unexpected data @@ -189,6 +185,15 @@ static int32_t parseIncomingChar(uCxAtClient_t *pClient, char ch) // URC will be handled after the command has completed ret = AT_PARSER_NOP; } +#else + if (ret == AT_PARSER_GOT_URC) { + const struct uCxAtClientConfig *pConfig = pClient->pConfig; + if (pClient->urcCallback) { + pClient->urcCallback(pClient, pClient->pUrcCallbackTag, pConfig->pRxBuffer, + strlen(pConfig->pRxBuffer), NULL, 0); + } + ret = AT_PARSER_NOP; + } #endif } else if (isprint(ch)) { pRxBuffer[pClient->rxBufferPos++] = ch; @@ -344,7 +349,7 @@ static int32_t handleBinaryRx(uCxAtClient_t *pClient) const struct uCxAtClientConfig *pConfig = pClient->pConfig; if (pClient->urcCallback) { pClient->urcCallback(pClient, pClient->pUrcCallbackTag, pConfig->pRxBuffer, - pClient->rxBufferPos, pClient->binaryRx.pBuffer, + strlen(pConfig->pRxBuffer), pClient->binaryRx.pBuffer, pClient->binaryRx.bufferPos); } #endif From 039b4f1ebcd2072ebfc59e9126cc6e1dc419e10a Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:08:27 +0200 Subject: [PATCH 03/39] test: Expand AT client coverage Cover fragmented binary data, timeouts, I/O failures, parser recovery, response APIs, and interleaved URCs. Require URC callbacks to run and validate binary payload delivery with and without the URC queue. --- test/test_u_cx_at_client.c | 222 +++++++++++++++++++++++- test/test_u_cx_at_client_no_urc_queue.c | 14 +- 2 files changed, 231 insertions(+), 5 deletions(-) diff --git a/test/test_u_cx_at_client.c b/test/test_u_cx_at_client.c index ca56d1d..2393929 100644 --- a/test/test_u_cx_at_client.c +++ b/test/test_u_cx_at_client.c @@ -56,6 +56,8 @@ static size_t gTxBufferPos; static uint8_t *gPRxDataPtr; static int32_t gRxDataLen; static int32_t gRxIoErrorCode; +static int32_t gRxBytesUntilError; +static size_t gRxMaxReadSize; static uCxAtClientConfig_t gClientConfig = { .pContext = CONTEXT_VALUE, @@ -132,18 +134,29 @@ int32_t uPortUartRead(uPortUartHandle_t handle, void *pData, size_t length, int3 (void)timeoutMs; TEST_ASSERT_EQUAL(UART_HANDLE, handle); - if (gRxIoErrorCode != 0) { + if ((gRxIoErrorCode != 0) && (gRxBytesUntilError == 0)) { if (++zeroCounter > 10) { TEST_FAIL_MESSAGE("Stuck in read loop"); } return gRxIoErrorCode; } - int32_t cpyLen = U_MIN((int32_t)length, gRxDataLen); + size_t readLimit = length; + if ((gRxMaxReadSize > 0) && (readLimit > gRxMaxReadSize)) { + readLimit = gRxMaxReadSize; + } + if ((gRxBytesUntilError > 0) && (readLimit > (size_t)gRxBytesUntilError)) { + readLimit = (size_t)gRxBytesUntilError; + } + + int32_t cpyLen = U_MIN((int32_t)readLimit, gRxDataLen); if (cpyLen > 0) { memcpy(pData, gPRxDataPtr, cpyLen); gPRxDataPtr += cpyLen; gRxDataLen -= cpyLen; + if (gRxBytesUntilError > 0) { + gRxBytesUntilError -= cpyLen; + } zeroCounter = 0; } else { if (++zeroCounter > 10) { @@ -179,6 +192,8 @@ void setUp(void) gPRxDataPtr = NULL; gRxDataLen = -1; gRxIoErrorCode = 0; + gRxBytesUntilError = 0; + gRxMaxReadSize = 0; gPTickSequence = NULL; uPortGetTickTimeMs_IgnoreAndReturn(0); @@ -266,6 +281,13 @@ void test_uCxAtClientSendCmdVaList_withNegativeIntList(void) TEST_ASSERT_EQUAL_STRING("AT+FOO=[-1,-100,50]\r", &gTxBuffer[0]); } +void test_uCxAtClientSendCmdVaList_withMultipleParams(void) +{ + uAtClientSendCmdVaList_wrapper(&gClient, "AT+FOO=", "ds", + 123, "abc", U_CX_AT_UTIL_PARAM_LAST); + TEST_ASSERT_EQUAL_STRING("AT+FOO=123,\"abc\"\r", &gTxBuffer[0]); +} + void test_uCxAtClientSendCmdVaList_withBinaryString(void) { char str[] = "test"; @@ -393,6 +415,39 @@ void test_uCxAtClientExecSimpleCmdF_withStatusOk_expectSuccess(void) TEST_ASSERT_EQUAL(0, uCxAtClientExecSimpleCmdF(&gClient, "DUMMY", "")); } +void test_uCxAtClientExecSimpleCmd_withStatusOk_expectSuccess(void) +{ + char rxData[] = { "\r\nOK\r\n" }; + gPRxDataPtr = (uint8_t *)&rxData[0]; + gRxDataLen = sizeof(rxData); + + TEST_ASSERT_EQUAL(0, uCxAtClientExecSimpleCmd(&gClient, "AT")); + TEST_ASSERT_EQUAL(3, gTxBufferPos); + TEST_ASSERT_EQUAL_MEMORY("AT\r", gTxBuffer, gTxBufferPos); +} + +void test_uCxAtClientExecSimpleCmdF_withControlByte_expectSuccess(void) +{ + uint8_t rxData[] = { '\r','\n',0x02,'O','K','\r','\n' }; + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + + TEST_ASSERT_EQUAL(0, uCxAtClientExecSimpleCmdF(&gClient, "DUMMY", "")); +} + +void test_uCxAtClientExecSimpleCmdF_afterRxOverflow_expectSuccess(void) +{ + uint8_t rxData[] = { '1','2','3','4','5','6','7','8','O','K','\r','\n' }; + size_t originalBufferLen = gClientConfig.rxBufferLen; + gClientConfig.rxBufferLen = 8; + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + + TEST_ASSERT_EQUAL(0, uCxAtClientExecSimpleCmdF(&gClient, "DUMMY", "")); + + gClientConfig.rxBufferLen = originalBufferLen; +} + void test_uCxAtClientExecSimpleCmdF_withStatusError_expectError(void) { char rxData[] = { "\r\nERROR\r\n" }; @@ -455,6 +510,37 @@ void test_uCxAtClientCmdGetRspParamLine_withCmdEchoAndRsp_expectRsp(void) TEST_ASSERT_EQUAL_STRING("123", pRsp); } +void test_uCxAtClientCmdGetRspParamLine_withoutPrefix_expectRsp(void) +{ + char rxData[] = { "NORA-W36\r\nOK\r\n" }; + gPRxDataPtr = (uint8_t *)&rxData[0]; + gRxDataLen = sizeof(rxData); + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + + char *pRsp = uCxAtClientCmdGetRspParamLine(&gClient, NULL, NULL, NULL); + + TEST_ASSERT_NOT_NULL(pRsp); + TEST_ASSERT_EQUAL_STRING("NORA-W36", pRsp); + TEST_ASSERT_EQUAL(0, uCxAtClientCmdEnd(&gClient)); +} + +void test_uCxAtClientCmdGetRspParamsF_withValidParams_expectParsedValues(void) +{ + char rxData[] = { "+FOO:123,\"abc\"\r\nOK\r\n" }; + int32_t value = 0; + char *pString = NULL; + gPRxDataPtr = (uint8_t *)&rxData[0]; + gRxDataLen = sizeof(rxData); + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + + TEST_ASSERT_EQUAL(2, uCxAtClientCmdGetRspParamsF(&gClient, "+FOO:", NULL, NULL, + "ds", &value, &pString, + U_CX_AT_UTIL_PARAM_LAST)); + TEST_ASSERT_EQUAL(123, value); + TEST_ASSERT_EQUAL_STRING("abc", pString); + TEST_ASSERT_EQUAL(0, uCxAtClientCmdEnd(&gClient)); +} + void test_uCxAtClientCmdGetRspParamLine_withReadError_expectNull(void) { // Start by putting the client in command state @@ -482,6 +568,97 @@ void test_uCxAtClientCmdGetRspParamLine_withBinary(void) TEST_ASSERT_EQUAL_STRING("\"foo\"", pRsp); } +void test_uCxAtClientCmdGetRspParamLine_withFragmentedBinary_expectCompleteResponse(void) +{ + uint8_t binaryBuf[6] = {0}; + uint16_t binaryLen = sizeof(binaryBuf); + uint8_t rxData[] = { '+','F','O','O',':',BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; + uint8_t expectedBinData[] = {0x00,0x11,0x22,0x33,0x44,0x55}; + + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + gRxMaxReadSize = 1; + + char *pRsp = uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen); + + TEST_ASSERT_NOT_NULL(pRsp); + TEST_ASSERT_EQUAL_STRING("", pRsp); + TEST_ASSERT_EQUAL(sizeof(expectedBinData), binaryLen); + TEST_ASSERT_EQUAL_MEMORY(expectedBinData, binaryBuf, sizeof(expectedBinData)); +} + +void test_uCxAtClientCmdGetRspParamLine_withBinaryHeaderReadError_expectIoError(void) +{ + uint8_t binaryBuf[6] = {0}; + uint16_t binaryLen = sizeof(binaryBuf); + uint8_t rxData[] = { '+','F','O','O',':',BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; + + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + gRxIoErrorCode = -1234; + gRxBytesUntilError = 6; + + TEST_ASSERT_NULL(uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen)); + TEST_ASSERT_EQUAL(U_CX_ERROR_IO, gClient.status); + TEST_ASSERT_EQUAL(-1234, uCxAtClientGetLastIoError(&gClient)); +} + +void test_uCxAtClientCmdGetRspParamLine_withBinaryPayloadReadError_expectIoError(void) +{ + uint8_t binaryBuf[6] = {0}; + uint16_t binaryLen = sizeof(binaryBuf); + uint8_t rxData[] = { '+','F','O','O',':',BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; + + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + gRxIoErrorCode = -1234; + gRxBytesUntilError = 10; + + TEST_ASSERT_NULL(uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen)); + TEST_ASSERT_EQUAL(U_CX_ERROR_IO, gClient.status); + TEST_ASSERT_EQUAL(-1234, uCxAtClientGetLastIoError(&gClient)); + TEST_ASSERT_EQUAL(4, gClient.binaryRx.remainingDataBytes); +} + +void test_uCxAtClientCmdGetRspParamLine_withBinaryPayloadTimeout_expectNull(void) +{ + uint8_t binaryBuf[6] = {0}; + uint16_t binaryLen = sizeof(binaryBuf); + uint8_t rxData[] = { '+','F','O','O',':',BIN_HDR(6),0x00,0x11}; + + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + uPortGetTickTimeMs_StopIgnore(); + uPortGetTickTimeMs_ExpectAndReturn(20000); + + TEST_ASSERT_NULL(uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen)); + TEST_ASSERT_TRUE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(4, gClient.binaryRx.remainingDataBytes); +} + +void test_uCxAtClientCmdGetRspParamLine_withSmallBinaryBuffer_expectTruncatedResponse(void) +{ + uint8_t binaryBuf[3] = {0}; + uint16_t binaryLen = sizeof(binaryBuf); + uint8_t rxData[] = { '+','F','O','O',':',BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; + uint8_t expectedBinData[] = {0x00,0x11,0x22}; + + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + + char *pRsp = uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen); + + TEST_ASSERT_NOT_NULL(pRsp); + TEST_ASSERT_EQUAL(sizeof(binaryBuf), binaryLen); + TEST_ASSERT_EQUAL_MEMORY(expectedBinData, binaryBuf, sizeof(expectedBinData)); + TEST_ASSERT_EQUAL(0, gRxDataLen); +} + void test_uCxAtClientCmdGetRspParamLine_withUnexpectedBinaryResponse(void) { uint8_t rxData[] = { '+','F','O','O',':','\"','f','o','o','\"',BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; @@ -498,6 +675,7 @@ void test_uCxAtClientCmdGetRspParamLine_withUnexpectedBinaryResponse(void) void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) { char rxData[] = { "\r\n" TEST_URC "\r\n" }; + int callbackCount = 0; gPRxDataPtr = (uint8_t *)&rxData[0]; gRxDataLen = strlen(rxData); @@ -510,10 +688,12 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) TEST_ASSERT_EQUAL(strlen(pLine), lineLength); TEST_ASSERT_NULL(pBinaryData); TEST_ASSERT_EQUAL(0, binaryDataLen); + callbackCount++; } uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, callbackCount); } void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) @@ -521,6 +701,7 @@ void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) char strData[] = { "\r\n" TEST_URC }; uint8_t binData[] = {BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; uint8_t rxData[strlen(strData) + sizeof(binData)]; + int callbackCount = 0; memcpy(&rxData[0], &strData[0], strlen(strData)); memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); gPRxDataPtr = &rxData[0]; @@ -537,10 +718,47 @@ void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) TEST_ASSERT_NOT_NULL(pBinaryData); TEST_ASSERT_EQUAL(sizeof(expectedBinData), binaryDataLen); TEST_ASSERT_EQUAL_MEMORY(expectedBinData, pBinaryData, sizeof(expectedBinData)); + callbackCount++; } uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, callbackCount); +} + +void test_uCxAtClientCmd_withInterleavedUrc_expectResponseAndCallback(void) +{ + char rxData[] = { TEST_URC "\r\n+MYRSP:123\r\nOK\r\n" }; + int callbackCount = 0; + gPRxDataPtr = (uint8_t *)&rxData[0]; + gRxDataLen = strlen(rxData); + + void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, + size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) + { + TEST_ASSERT_EQUAL(&gClient, pClient); + TEST_ASSERT_EQUAL(CONTEXT_VALUE, pTag); + TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); + TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); + TEST_ASSERT_NULL(pBinaryData); + TEST_ASSERT_EQUAL(0, binaryDataLen); + callbackCount++; + } + + uCxAtClientSetUrcCallback(&gClient, urcCallback, CONTEXT_VALUE); + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + TEST_ASSERT_EQUAL_STRING("123", + uCxAtClientCmdGetRspParamLine(&gClient, "+MYRSP:", NULL, NULL)); + TEST_ASSERT_EQUAL(0, uCxAtClientCmdEnd(&gClient)); + TEST_ASSERT_EQUAL(1, callbackCount); +} + +void test_uCxAtClientHandleRx_withReadError_expectIoError(void) +{ + gRxIoErrorCode = -1234; + + TEST_ASSERT_EQUAL(-1234, uCxAtClientHandleRx(&gClient)); + TEST_ASSERT_EQUAL(-1234, uCxAtClientGetLastIoError(&gClient)); } void test_uCxAtClientSetCommandTimeout_withNonPermanentTimeout(void) diff --git a/test/test_u_cx_at_client_no_urc_queue.c b/test/test_u_cx_at_client_no_urc_queue.c index b84b2ea..1c8000e 100644 --- a/test/test_u_cx_at_client_no_urc_queue.c +++ b/test/test_u_cx_at_client_no_urc_queue.c @@ -162,6 +162,7 @@ void tearDown(void) void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) { char rxData[] = { "\r\n" TEST_URC "\r\n" }; + int callbackCount = 0; gPRxDataPtr = (uint8_t *)&rxData[0]; gRxDataLen = strlen(rxData); @@ -174,10 +175,12 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) TEST_ASSERT_EQUAL(strlen(pLine), lineLength); TEST_ASSERT_NULL(pBinaryData); TEST_ASSERT_EQUAL(0, binaryDataLen); + callbackCount++; } uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, callbackCount); } void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) @@ -185,22 +188,27 @@ void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) char strData[] = { "\r\n" TEST_URC }; uint8_t binData[] = {BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; uint8_t rxData[strlen(strData) + sizeof(binData)]; + int callbackCount = 0; memcpy(&rxData[0], &strData[0], strlen(strData)); memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); gPRxDataPtr = &rxData[0]; - gRxDataLen += strlen(strData) + sizeof(binData); + gRxDataLen = strlen(strData) + sizeof(binData); void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) { + uint8_t expectedBinData[] = {0x00,0x11,0x22,0x33,0x44,0x55}; TEST_ASSERT_EQUAL(&gClient, pClient); TEST_ASSERT_NULL(pTag); TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); TEST_ASSERT_EQUAL(strlen(pLine), lineLength); - TEST_ASSERT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(0, binaryDataLen); + TEST_ASSERT_NOT_NULL(pBinaryData); + TEST_ASSERT_EQUAL(sizeof(expectedBinData), binaryDataLen); + TEST_ASSERT_EQUAL_MEMORY(expectedBinData, pBinaryData, sizeof(expectedBinData)); + callbackCount++; } uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, callbackCount); } From 13c87a666ce53015b506f11d70d045724422c179 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:32:46 +0200 Subject: [PATCH 04/39] build: Update unit tests to Ceedling 1.0.1 Migrate the project configuration to Ceedling 1 syntax and plugins. Install the same Ceedling release in the STM32 build image and pin a compatible gcovr version for coverage reports. --- docker/stm32f4/Dockerfile | 6 +- project.yml | 198 ++++++++++++-------------------------- 2 files changed, 68 insertions(+), 136 deletions(-) diff --git a/docker/stm32f4/Dockerfile b/docker/stm32f4/Dockerfile index 43f3a94..9a35e41 100644 --- a/docker/stm32f4/Dockerfile +++ b/docker/stm32f4/Dockerfile @@ -6,16 +6,18 @@ ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ wget \ xz-utils \ - make \ + build-essential \ cmake \ ccache \ git \ mono-complete \ + ruby ruby-dev \ python3 python3-pip python-is-python3 python3-dev python3-wheel \ netcat-openbsd \ sudo \ gdb-multiarch \ - && pip3 install invoke \ + && gem install ceedling --version 1.0.1 --no-document \ + && pip3 install gcovr==8.6 invoke \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/project.yml b/project.yml index e4e0342..5f13c50 100644 --- a/project.yml +++ b/project.yml @@ -1,30 +1,17 @@ --- -# Notes: -# Sample project C code is not presently written to produce a release artifact. -# As such, release build options are disabled. -# This sample, therefore, only demonstrates running a collection of unit tests. - :project: :use_exceptions: TRUE - :use_test_preprocessor: TRUE - :use_auxiliary_dependencies: TRUE + :use_test_preprocessor: :tests :build_root: build - :release_build: TRUE # This is required for :dependencies: to work + :release_build: TRUE :test_file_prefix: test_ :which_ceedling: gem - :ceedling_version: 0.31.1 + :ceedling_version: 1.0.1 :compile_threads: 16 :default_tasks: - test:all -#:test_build: -# :use_assembly: TRUE - -#:release_build: -# :output: MyApp.out -# :use_assembly: FALSE - :environment: :extension: @@ -36,6 +23,8 @@ - -:test/support :source: - src/** + :include: + - src/** - inc/** - ports/** - -:ports/extra/** @@ -44,19 +33,11 @@ :libraries: [] :defines: - # in order to add common defines: - # 1) remove the trailing [] from the :common: section - # 2) add entries to the :common: section (e.g. :test: has TEST defined) - :common: &common_defines - - TEST - - U_CX_PORT_HEADER_FILE=\"u_port_test.h\" :test: - - *common_defines - :test_preprocess: - - *common_defines - :test_u_cx_at_client_no_urc_queue: - - *common_defines - - U_CX_USE_URC_QUEUE=0 + :*: + - TEST + :test_u_cx_at_client_no_urc_queue: + - U_CX_USE_URC_QUEUE=0 :cmock: :mock_prefix: mock_ @@ -79,16 +60,14 @@ :treat_externs: :include :strippables: - '(?:uPortBgRxTaskCreate|uPortBgRxTaskDestroy)\s*\([^)]*\)' + :module_generator: - :project_root: ./ - :source_root: src/ - :inc_root: inc/ - :test_root: test/ - :naming: 'snake' + :project_root: ./ + :source_root: src/ + :inc_root: inc/ + :test_root: test/ + :naming: snake -# Add -gcov to the plugins list to make sure of the gcov plugin -# You will need to have gcov and gcovr both installed to make it work. -# For more information on these options, see docs in plugins/gcov :gcov: :reports: - HtmlDetailed @@ -96,121 +75,72 @@ :html_medium_threshold: 75 :html_high_threshold: 90 -:junit_tests_report: - :artifact_filename: report_junit.xml -#:tools: -# Ceedling defaults to using gcc for compiling, linking, etc. -# As [:tools] is blank, gcc will be used (so long as it's in your system path) -# See documentation to configure a given toolchain for use +:report_tests_log_factory: + :reports: + - cppunit + - junit + :junit: + :filename: report_junit.xml -# LIBRARIES -# These libraries are automatically injected into the build process. Those specified as -# common will be used in all types of builds. Otherwise, libraries can be injected in just -# tests or releases. These options are MERGED with the options in supplemental yaml files. :libraries: :placement: :end :flag: "-l${1}" :path_flag: "-L ${1}" - :system: [] # for example, you might list 'm' to grab the math library + :system: [] :test: [] :release: [] -:tools: - :test_file_preprocessor: - :executable: gcc - :arguments: - - '' - - '' - - "-E" - - -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR - - -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE - - "-D$": COLLECTION_DEFINES_TEST_AND_VENDOR - - "-D$": DEFINES_TEST_PREPROCESS - - "-DGNU_COMPILER" - - '"${1}"' - - -o "${2}" - :test_includes_preprocessor: - :executable: gcc - :arguments: - - '' - - '' - - "-E" - - "-MM" - - "-MG" - - -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR - - -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE - - "-D$": COLLECTION_DEFINES_TEST_AND_VENDOR - - "-D$": DEFINES_TEST_PREPROCESS - - "-DGNU_COMPILER" - - '"${1}"' - :test_dependencies_generator: - :executable: gcc - :arguments: - - '' - - '' - - "-E" - - -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR - - -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE - - "-D$": COLLECTION_DEFINES_TEST_AND_VENDOR - - "-D$": DEFINES_TEST_PREPROCESS - - "-DGNU_COMPILER" - - -MT "${3}" - - "-MM" - - "-MD" - - "-MG" - - -MF "${2}" - - -c "${1}" :plugins: - :load_paths: - - "#{Ceedling.load_path}" + :load_paths: [] :enabled: - - stdout_pretty_tests_report + - report_tests_pretty_stdout - module_generator - gcov - - xml_tests_report - - junit_tests_report + - report_tests_log_factory :flags: :test: + :preprocess: + :*: + - -DU_CX_PORT_HEADER_FILE=\"u_port_test.h\" :compile: - :*: - - -Werror - - -Wall - - -Wextra - - -fsanitize=address - - -fsanitize=undefined - - -fsanitize-address-use-after-scope - - -fno-sanitize-recover - - -fstack-protector-all - - -fno-omit-frame-pointer - - -O1 # Set O1 so that GCC correctly detects uninitialized variables: https://stackoverflow.com/questions/17705880/gcc-failing-to-warn-of-uninitialized-variable - - -Wno-clobbered # Needed when enabling -O1 for disabling warning in the CMock runners + :*: + - -DU_CX_PORT_HEADER_FILE=\"u_port_test.h\" + - -Werror + - -Wall + - -Wextra + - -fsanitize=address + - -fsanitize=undefined + - -fsanitize-address-use-after-scope + - -fno-sanitize-recover + - -fstack-protector-all + - -fno-omit-frame-pointer + - -O1 + - -Wno-clobbered :link: - :*: - - -fsanitize=address - - -fsanitize=undefined - - -fsanitize-address-use-after-scope - - -fno-sanitize-recover - - -lpthread + :*: + - -fsanitize=address + - -fsanitize=undefined + - -fsanitize-address-use-after-scope + - -fno-sanitize-recover + - -lpthread :gcov: :compile: - :*: - - -Werror - - -Wall - - -Wextra - - -fsanitize=address - - -fsanitize=undefined - - -fsanitize-address-use-after-scope - - -fno-sanitize-recover - - -fstack-protector-all - - -fno-omit-frame-pointer - - -O1 # Set O1 so that GCC correctly detects uninitialized variables: https://stackoverflow.com/questions/17705880/gcc-failing-to-warn-of-uninitialized-variable - - -Wno-clobbered # Needed when enabling -O1 for disabling warning in the CMock runners + - -DU_CX_PORT_HEADER_FILE=\"u_port_test.h\" + - -Werror + - -Wall + - -Wextra + - -fsanitize=address + - -fsanitize=undefined + - -fsanitize-address-use-after-scope + - -fno-sanitize-recover + - -fstack-protector-all + - -fno-omit-frame-pointer + - -O1 + - -Wno-clobbered :link: - :*: - - -fsanitize=address - - -fsanitize=undefined - - -fsanitize-address-use-after-scope - - -fno-sanitize-recover - - -lpthread - + - -fsanitize=address + - -fsanitize=undefined + - -fsanitize-address-use-after-scope + - -fno-sanitize-recover + - -lpthread \ No newline at end of file From 91f861f820896a199f5c0a9ab98c960e52638553 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:36:11 +0200 Subject: [PATCH 05/39] u_cx_at_client: Defer URC processing during binary RX Do not dequeue URCs while a fragmented binary URC still owns the enqueue mutex. Process the queue once reception completes. --- src/u_cx_at_client.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index 1007ff9..c183911 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -787,7 +787,9 @@ int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) U_CX_MUTEX_UNLOCK(pClient->cmdMutex); #if U_CX_USE_URC_QUEUE == 1 - processUrcs(pClient); + if (!pClient->isBinaryRx) { + processUrcs(pClient); + } #endif return ret; From 641af688ce6ee5b4123ecae3449843c2999a1c50 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:36:16 +0200 Subject: [PATCH 06/39] test: Cover fragmented binary receive stages Split binary URC headers and payloads across separate receive calls with and without the URC queue. Also fragment the response discard path when the caller's binary buffer is too small. --- test/test_u_cx_at_client.c | 17 ++++++++++++++++- test/test_u_cx_at_client_no_urc_queue.c | 25 +++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/test/test_u_cx_at_client.c b/test/test_u_cx_at_client.c index 2393929..1a70f71 100644 --- a/test/test_u_cx_at_client.c +++ b/test/test_u_cx_at_client.c @@ -650,6 +650,7 @@ void test_uCxAtClientCmdGetRspParamLine_withSmallBinaryBuffer_expectTruncatedRes uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); gPRxDataPtr = rxData; gRxDataLen = sizeof(rxData); + gRxMaxReadSize = 1; char *pRsp = uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen); @@ -696,7 +697,7 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) TEST_ASSERT_EQUAL(1, callbackCount); } -void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) +void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) { char strData[] = { "\r\n" TEST_URC }; uint8_t binData[] = {BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; @@ -706,6 +707,7 @@ void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); gPRxDataPtr = &rxData[0]; gRxDataLen = strlen(strData) + sizeof(binData); + gRxMaxReadSize = 1; void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) @@ -723,7 +725,20 @@ void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_TRUE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(1, gClient.binaryRx.rxHeaderCount); + gRxDataLen = 3; + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_TRUE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(2, gClient.binaryRx.bufferPos); + TEST_ASSERT_EQUAL(4, gClient.binaryRx.remainingDataBytes); + gRxDataLen = 4; + uCxAtClientHandleRx(&gClient); TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(0, gRxDataLen); } void test_uCxAtClientCmd_withInterleavedUrc_expectResponseAndCallback(void) diff --git a/test/test_u_cx_at_client_no_urc_queue.c b/test/test_u_cx_at_client_no_urc_queue.c index 1c8000e..efe6bcc 100644 --- a/test/test_u_cx_at_client_no_urc_queue.c +++ b/test/test_u_cx_at_client_no_urc_queue.c @@ -55,6 +55,7 @@ static size_t gTxBufferPos; static uint8_t *gPRxDataPtr; static int32_t gRxDataLen; static int32_t gRxIoErrorCode; +static size_t gRxMaxReadSize; static uCxAtClientConfig_t gClientConfig = { .pContext = CONTEXT_VALUE, @@ -120,7 +121,12 @@ int32_t uPortUartRead(uPortUartHandle_t handle, void *pData, size_t length, int3 return gRxIoErrorCode; } - int32_t cpyLen = U_MIN((int32_t)length, gRxDataLen); + size_t readLimit = length; + if ((gRxMaxReadSize > 0) && (readLimit > gRxMaxReadSize)) { + readLimit = gRxMaxReadSize; + } + + int32_t cpyLen = U_MIN((int32_t)readLimit, gRxDataLen); if (cpyLen > 0) { memcpy(pData, gPRxDataPtr, cpyLen); gPRxDataPtr += cpyLen; @@ -149,6 +155,7 @@ void setUp(void) gPRxDataPtr = NULL; gRxDataLen = -1; gRxIoErrorCode = 0; + gRxMaxReadSize = 0; uPortGetTickTimeMs_IgnoreAndReturn(0); } @@ -183,7 +190,7 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) TEST_ASSERT_EQUAL(1, callbackCount); } -void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) +void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) { char strData[] = { "\r\n" TEST_URC }; uint8_t binData[] = {BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; @@ -193,6 +200,7 @@ void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); gPRxDataPtr = &rxData[0]; gRxDataLen = strlen(strData) + sizeof(binData); + gRxMaxReadSize = 1; void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) @@ -210,5 +218,18 @@ void test_uCxAtClientHandleRx_withBinUrc_expectUrcCallback(void) uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_TRUE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(1, gClient.binaryRx.rxHeaderCount); + gRxDataLen = 3; + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_TRUE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(2, gClient.binaryRx.bufferPos); + TEST_ASSERT_EQUAL(4, gClient.binaryRx.remainingDataBytes); + gRxDataLen = 4; + uCxAtClientHandleRx(&gClient); TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(0, gRxDataLen); } From 0474ce7d5efe55a10ca4cb3b365320f649acb4e0 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:40:00 +0200 Subject: [PATCH 07/39] u_cx_at_client: Preserve binary header per client Store partial binary length bytes in the client receive state instead of a shared static buffer. This prevents interleaved clients from corrupting each other's binary frame lengths. --- inc/u_cx_at_client.h | 1 + src/u_cx_at_client.c | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/inc/u_cx_at_client.h b/inc/u_cx_at_client.h index e6e64dc..c0e4a06 100644 --- a/inc/u_cx_at_client.h +++ b/inc/u_cx_at_client.h @@ -54,6 +54,7 @@ typedef struct { uCxAtBinaryState_t state; uint8_t rxHeaderCount; + uint8_t lengthBuf[2]; uint16_t remainingDataBytes; uint8_t *pBuffer; uint16_t bufferSize; diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index c183911..83289b6 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -273,11 +273,10 @@ static int32_t handleBinaryRx(uCxAtClient_t *pClient) uCxAtBinaryRx_t *pBinRx = &pClient->binaryRx; int32_t readStatus; - static uint8_t lengthBuf[2]; if (pBinRx->rxHeaderCount < 2) { - size_t readLen = sizeof(lengthBuf) - pBinRx->rxHeaderCount; + size_t readLen = sizeof(pBinRx->lengthBuf) - pBinRx->rxHeaderCount; readStatus = uPortUartRead(pClient->uartHandle, - &lengthBuf[pBinRx->rxHeaderCount], readLen, + &pBinRx->lengthBuf[pBinRx->rxHeaderCount], readLen, pClient->pConfig->timeoutMs); CHECK_READ_ERROR(pClient, readStatus); if (readStatus > 0) { @@ -288,7 +287,8 @@ static int32_t handleBinaryRx(uCxAtClient_t *pClient) } else { // The two length bytes have now been received int32_t parse_code; - uint16_t length = (uint16_t)(lengthBuf[0] << 8) | lengthBuf[1]; + uint16_t length = (uint16_t)(pBinRx->lengthBuf[0] << 8) | + pBinRx->lengthBuf[1]; char *pRxBuffer = (char *)pClient->pConfig->pRxBuffer; parse_code = parseLine(pClient, pRxBuffer, pClient->rxBufferPos); setupBinaryTransfer(pClient, parse_code, length); From 4f4e3ef8d223087566e077e8bdaa62028dde3451 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:40:04 +0200 Subject: [PATCH 08/39] test: Cover interleaved fragmented binary headers Interleave partial headers from two clients with different nonzero length bytes. Verify each client retains its own header before receiving the remaining payload. --- test/test_u_cx_at_client_no_urc_queue.c | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/test_u_cx_at_client_no_urc_queue.c b/test/test_u_cx_at_client_no_urc_queue.c index efe6bcc..7d5c8f8 100644 --- a/test/test_u_cx_at_client_no_urc_queue.c +++ b/test/test_u_cx_at_client_no_urc_queue.c @@ -233,3 +233,65 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) TEST_ASSERT_FALSE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(0, gRxDataLen); } + +void test_uCxAtClientHandleRx_withInterleavedBinaryHeaders_expectPerClientLength(void) +{ + uint8_t secondRxBuffer[1024]; + uCxAtClientConfig_t secondConfig = gClientConfig; + uCxAtClient_t secondClient; + char strData[] = { "\r\n" TEST_URC }; + uint8_t firstHeader[strlen(strData) + 2]; + uint8_t secondHeader[strlen(strData) + 2]; + uint8_t firstTail[258]; + int callbackCount = 0; + + secondConfig.pRxBuffer = secondRxBuffer; + uCxAtClientInit(&secondConfig, &secondClient); + TEST_ASSERT_EQUAL(0, uCxAtClientOpen(&secondClient, 115200, true)); + + memcpy(firstHeader, strData, strlen(strData)); + firstHeader[strlen(strData)] = 0x01; + firstHeader[strlen(strData) + 1] = 0x01; + memcpy(secondHeader, strData, strlen(strData)); + secondHeader[strlen(strData)] = 0x01; + secondHeader[strlen(strData) + 1] = 0x02; + firstTail[0] = 0x01; + for (size_t index = 0; index < sizeof(firstTail) - 1; index++) { + firstTail[index + 1] = (uint8_t)index; + } + + void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, + size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) + { + TEST_ASSERT_EQUAL(&gClient, pClient); + TEST_ASSERT_NULL(pTag); + TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); + TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); + TEST_ASSERT_EQUAL(257, binaryDataLen); + for (size_t index = 0; index < binaryDataLen; index++) { + TEST_ASSERT_EQUAL_UINT8((uint8_t)index, pBinaryData[index]); + } + callbackCount++; + } + + uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + gPRxDataPtr = firstHeader; + gRxDataLen = sizeof(firstHeader); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, gClient.binaryRx.rxHeaderCount); + + gPRxDataPtr = secondHeader; + gRxDataLen = sizeof(secondHeader); + uCxAtClientHandleRx(&secondClient); + TEST_ASSERT_EQUAL(1, secondClient.binaryRx.rxHeaderCount); + + gPRxDataPtr = firstTail; + gRxDataLen = sizeof(firstTail); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(0, gRxDataLen); + + uCxAtClientClose(&secondClient); + uCxAtClientDeinit(&secondClient); +} From 924c6bde4ccb8cf0e3f8cb6d2224a9a442316e90 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:43:55 +0200 Subject: [PATCH 09/39] u_cx_at_client: Preserve binary URC line terminator Store direct URC payload data after the string terminator instead of overwriting it. Accept payloads that exactly fill the available direct or queued URC buffer. --- src/u_cx_at_client.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index 83289b6..4068c43 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -234,7 +234,7 @@ static void setupBinaryTransfer(uCxAtClient_t *pClient, int32_t parserRet, uint1 // Place the binary data directly after the URC string uint8_t *pPtr = pConfig->pUrcBuffer; uint16_t len = uCxAtUrcQueueEnqueueGetPayloadPtr(&pClient->urcQueue, &pPtr); - if (len > binLength) { + if (len >= binLength) { setupBinaryRxBuffer(pClient, U_CX_BIN_STATE_BINARY_URC, pPtr, len, binLength); } else { // The binary data can't be fitted into the queue so we need to drop it @@ -245,10 +245,10 @@ static void setupBinaryTransfer(uCxAtClient_t *pClient, int32_t parserRet, uint1 #else size_t bufPos = pClient->rxBufferPos; uint8_t *pPtr = pConfig->pRxBuffer; - size_t len = pConfig->rxBufferLen - bufPos; - if (len > binLength) { + size_t len = pConfig->rxBufferLen - bufPos - 1; + if (len >= binLength) { setupBinaryRxBuffer(pClient, U_CX_BIN_STATE_BINARY_URC, - &pPtr[bufPos], len, binLength); + &pPtr[bufPos + 1], len, binLength); } else { // The binary data can't be fitted into the queue so we need to drop it U_CX_LOG_LINE_I(U_CX_LOG_CH_WARN, pClient->instance, "Not enough space for URC binary data"); From 8a2a24b2e73f77899356894ae7f7b80e08d554b9 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:43:59 +0200 Subject: [PATCH 10/39] test: Expand binary receive boundary coverage Cover partial-header failures, zero-length frames, exact-fit buffers, opaque control bytes, oversized payload discard, and subsequent parser recovery with and without the URC queue. --- test/test_u_cx_at_client.c | 83 ++++++++++++++++++++++++- test/test_u_cx_at_client_no_urc_queue.c | 45 +++++++++++++- 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/test/test_u_cx_at_client.c b/test/test_u_cx_at_client.c index 1a70f71..be674c6 100644 --- a/test/test_u_cx_at_client.c +++ b/test/test_u_cx_at_client.c @@ -588,6 +588,26 @@ void test_uCxAtClientCmdGetRspParamLine_withFragmentedBinary_expectCompleteRespo TEST_ASSERT_EQUAL_MEMORY(expectedBinData, binaryBuf, sizeof(expectedBinData)); } +void test_uCxAtClientCmdGetRspParamLine_withFragmentedZeroLengthBinary_expectCompleteResponse(void) +{ + uint8_t binaryBuf[1] = {0xa5}; + uint16_t binaryLen = sizeof(binaryBuf); + uint8_t rxData[] = { '+','F','O','O',':',BIN_HDR(0)}; + + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + gRxMaxReadSize = 1; + + char *pRsp = uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen); + + TEST_ASSERT_NOT_NULL(pRsp); + TEST_ASSERT_EQUAL_STRING("", pRsp); + TEST_ASSERT_EQUAL(0, binaryLen); + TEST_ASSERT_EQUAL_HEX8(0xa5, binaryBuf[0]); + TEST_ASSERT_FALSE(gClient.isBinaryRx); +} + void test_uCxAtClientCmdGetRspParamLine_withBinaryHeaderReadError_expectIoError(void) { uint8_t binaryBuf[6] = {0}; @@ -598,11 +618,29 @@ void test_uCxAtClientCmdGetRspParamLine_withBinaryHeaderReadError_expectIoError( gPRxDataPtr = rxData; gRxDataLen = sizeof(rxData); gRxIoErrorCode = -1234; - gRxBytesUntilError = 6; + gRxBytesUntilError = 7; TEST_ASSERT_NULL(uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen)); TEST_ASSERT_EQUAL(U_CX_ERROR_IO, gClient.status); TEST_ASSERT_EQUAL(-1234, uCxAtClientGetLastIoError(&gClient)); + TEST_ASSERT_EQUAL(1, gClient.binaryRx.rxHeaderCount); +} + +void test_uCxAtClientCmdGetRspParamLine_withPartialBinaryHeaderTimeout_expectNull(void) +{ + uint8_t binaryBuf[6] = {0}; + uint16_t binaryLen = sizeof(binaryBuf); + uint8_t rxData[] = { '+','F','O','O',':',0x01,0x00}; + + uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + uPortGetTickTimeMs_StopIgnore(); + uPortGetTickTimeMs_ExpectAndReturn(20000); + + TEST_ASSERT_NULL(uCxAtClientCmdGetRspParamLine(&gClient, "+FOO:", binaryBuf, &binaryLen)); + TEST_ASSERT_TRUE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(1, gClient.binaryRx.rxHeaderCount); } void test_uCxAtClientCmdGetRspParamLine_withBinaryPayloadReadError_expectIoError(void) @@ -700,7 +738,7 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) { char strData[] = { "\r\n" TEST_URC }; - uint8_t binData[] = {BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; + uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; uint8_t rxData[strlen(strData) + sizeof(binData)]; int callbackCount = 0; memcpy(&rxData[0], &strData[0], strlen(strData)); @@ -708,11 +746,12 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) gPRxDataPtr = &rxData[0]; gRxDataLen = strlen(strData) + sizeof(binData); gRxMaxReadSize = 1; + gClient.urcQueue.bufferLen = sizeof(uUrcEntry_t) + strlen(TEST_URC) + 1 + 6; void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) { - uint8_t expectedBinData[] = {0x00,0x11,0x22,0x33,0x44,0x55}; + uint8_t expectedBinData[] = {0xa5,0x01,0x0d,0x0a,0x00,0xff}; TEST_ASSERT_EQUAL(&gClient, pClient); TEST_ASSERT_NULL(pTag); TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); @@ -741,6 +780,44 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) TEST_ASSERT_EQUAL(0, gRxDataLen); } +void test_uCxAtClientHandleRx_withOversizedBinUrc_expectDiscardAndRecovery(void) +{ + char strData[] = { "\r\n" TEST_URC }; + uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; + uint8_t rxData[strlen(strData) + sizeof(binData)]; + char nextUrc[] = { "\r\n" TEST_URC "\r\n" }; + int callbackCount = 0; + + memcpy(rxData, strData, strlen(strData)); + memcpy(&rxData[strlen(strData)], binData, sizeof(binData)); + gClient.urcQueue.bufferLen = sizeof(uUrcEntry_t) + strlen(TEST_URC) + 1 + 5; + + void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, + size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) + { + TEST_ASSERT_EQUAL(&gClient, pClient); + TEST_ASSERT_NULL(pTag); + TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); + TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); + TEST_ASSERT_NULL(pBinaryData); + TEST_ASSERT_EQUAL(0, binaryDataLen); + callbackCount++; + } + + uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(0, gRxDataLen); + + gPRxDataPtr = (uint8_t *)nextUrc; + gRxDataLen = strlen(nextUrc); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, callbackCount); +} + void test_uCxAtClientCmd_withInterleavedUrc_expectResponseAndCallback(void) { char rxData[] = { TEST_URC "\r\n+MYRSP:123\r\nOK\r\n" }; diff --git a/test/test_u_cx_at_client_no_urc_queue.c b/test/test_u_cx_at_client_no_urc_queue.c index 7d5c8f8..86854e8 100644 --- a/test/test_u_cx_at_client_no_urc_queue.c +++ b/test/test_u_cx_at_client_no_urc_queue.c @@ -146,6 +146,7 @@ int32_t uPortUartRead(uPortUartHandle_t handle, void *pData, size_t length, int3 void setUp(void) { + gClientConfig.rxBufferLen = sizeof(gRxBuffer); uCxLogPrintTime_Ignore(); uCxLogIsEnabled_IgnoreAndReturn(false); uCxAtClientInit(&gClientConfig, &gClient); @@ -193,7 +194,7 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) { char strData[] = { "\r\n" TEST_URC }; - uint8_t binData[] = {BIN_HDR(6),0x00,0x11,0x22,0x33,0x44,0x55}; + uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; uint8_t rxData[strlen(strData) + sizeof(binData)]; int callbackCount = 0; memcpy(&rxData[0], &strData[0], strlen(strData)); @@ -201,11 +202,12 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) gPRxDataPtr = &rxData[0]; gRxDataLen = strlen(strData) + sizeof(binData); gRxMaxReadSize = 1; + gClientConfig.rxBufferLen = strlen(TEST_URC) + 1 + 6; void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) { - uint8_t expectedBinData[] = {0x00,0x11,0x22,0x33,0x44,0x55}; + uint8_t expectedBinData[] = {0xa5,0x01,0x0d,0x0a,0x00,0xff}; TEST_ASSERT_EQUAL(&gClient, pClient); TEST_ASSERT_NULL(pTag); TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); @@ -234,6 +236,45 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) TEST_ASSERT_EQUAL(0, gRxDataLen); } +void test_uCxAtClientHandleRx_withOversizedBinUrc_expectDiscardAndRecovery(void) +{ + char strData[] = { "\r\n" TEST_URC }; + uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; + uint8_t rxData[strlen(strData) + sizeof(binData)]; + char nextUrc[] = { "\r\n" TEST_URC "\r\n" }; + int callbackCount = 0; + + memcpy(rxData, strData, strlen(strData)); + memcpy(&rxData[strlen(strData)], binData, sizeof(binData)); + gClientConfig.rxBufferLen = strlen(TEST_URC) + 1 + 5; + + void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, + size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) + { + TEST_ASSERT_EQUAL(&gClient, pClient); + TEST_ASSERT_NULL(pTag); + TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); + TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); + TEST_ASSERT_NULL(pBinaryData); + TEST_ASSERT_EQUAL(0, binaryDataLen); + callbackCount++; + } + + uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(0, gRxDataLen); + + gClientConfig.rxBufferLen = sizeof(gRxBuffer); + gPRxDataPtr = (uint8_t *)nextUrc; + gRxDataLen = strlen(nextUrc); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, callbackCount); +} + void test_uCxAtClientHandleRx_withInterleavedBinaryHeaders_expectPerClientLength(void) { uint8_t secondRxBuffer[1024]; From d5a299c48d7ad85489af209ca2c72c1684adc69a Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 18:59:04 +0200 Subject: [PATCH 11/39] test: Expand Zephyr port coverage Cover UART argument validation, reopen behavior, and background RX work. Fix the test fixture buffers so ring-buffer overflow checks remain bounded. --- zephyr/port_test/src/main.c | 65 ++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/zephyr/port_test/src/main.c b/zephyr/port_test/src/main.c index 28eb129..bb65949 100644 --- a/zephyr/port_test/src/main.c +++ b/zephyr/port_test/src/main.c @@ -53,7 +53,8 @@ struct u_connect_client_port_fixture { uint8_t testData[TEST_DATA_SIZE]; uCxAtClient_t client; uCxAtClientConfig_t config; - uint8_t rxBuffer[U_RINGBUFFER_SIZE]; + uint8_t rxBuffer[TEST_DATA_SIZE]; + uint8_t urcBuffer[TEST_DATA_SIZE]; }; /* ---------------------------------------------------------------- @@ -79,6 +80,10 @@ static void *u_connect_client_port_setup(void) zassert_not_null(fixture.pDev); // Initialize config with device name + fixture.config.pRxBuffer = fixture.rxBuffer; + fixture.config.rxBufferLen = sizeof(fixture.rxBuffer); + fixture.config.pUrcBuffer = fixture.urcBuffer; + fixture.config.urcBufferLen = sizeof(fixture.urcBuffer); fixture.config.pUartDevName = fixture.pDev->name; // Initialize AT client @@ -100,6 +105,7 @@ static void u_connect_client_port_before(void *f) uart_err_check(fixture->pDev); memset(&fixture->rxBuffer, 0, sizeof(fixture->rxBuffer)); + memset(&fixture->urcBuffer, 0, sizeof(fixture->urcBuffer)); gDisableRxWorker = true; @@ -221,4 +227,61 @@ ZTEST_F(u_connect_client_port, test_tx_fifo_full) zassert_mem_equal__(&fixture->rxBuffer, &fixture->testData, U_RINGBUFFER_SIZE); } +ZTEST_F(u_connect_client_port, test_invalid_arguments) +{ + zassert_is_null(uPortUartOpen(NULL, 115200, false)); + zassert_is_null(uPortUartOpen(fixture->pDev->name, 115200, false)); + + uPortUartClose(NULL); + + zassert_equal(uPortUartWrite(NULL, fixture->testData, 1), -1); + zassert_equal(uPortUartWrite(fixture->client.uartHandle, NULL, 1), -1); + zassert_equal(uPortUartWrite(fixture->client.uartHandle, fixture->testData, 0), -1); + + zassert_equal(uPortUartRead(NULL, fixture->rxBuffer, 1, 0), -1); + zassert_equal(uPortUartRead(fixture->client.uartHandle, fixture->rxBuffer, 0, 0), -1); + + uart_emul_put_rx_data(fixture->pDev, fixture->testData, 1); + k_sleep(K_MSEC(10)); + + zassert_equal(uPortUartRead(fixture->client.uartHandle, NULL, 1, 100), 0); + zassert_equal(uPortUartRead(fixture->client.uartHandle, NULL, 1, 0), 0); + zassert_equal(uPortUartRead(fixture->client.uartHandle, fixture->rxBuffer, 1, 0), 1); + zassert_mem_equal__(fixture->rxBuffer, fixture->testData, 1); +} + +ZTEST_F(u_connect_client_port, test_reopen_without_flow_control) +{ + uPortUartClose(fixture->client.uartHandle); + fixture->client.uartHandle = NULL; + + zassert_is_null(uPortUartOpen("missing-uart-device", 115200, false)); + + fixture->client.uartHandle = uPortUartOpen(fixture->pDev->name, 9600, false); + zassert_not_null(fixture->client.uartHandle); + + struct uart_config config; + zassert_equal(uart_config_get(fixture->pDev, &config), 0); + zassert_equal(config.baudrate, 9600); + zassert_equal(config.flow_ctrl, UART_CFG_FLOW_CTRL_NONE); +} + +ZTEST_F(u_connect_client_port, test_os_port_and_rx_worker) +{ + uPortInit(); + + int32_t startTimeMs = U_CX_PORT_GET_TIME_MS(); + k_sleep(K_MSEC(1)); + zassert_true(U_CX_PORT_GET_TIME_MS() >= startTimeMs); + + gDisableRxWorker = false; + uart_emul_put_rx_data(fixture->pDev, &fixture->testData[1], 1); + k_sleep(K_MSEC(10)); + gDisableRxWorker = true; + + uPortBgRxTaskDestroy(&fixture->client); + uPortBgRxTaskCreate(&fixture->client); + uPortDeinit(); +} + ZTEST_SUITE(u_connect_client_port, NULL, u_connect_client_port_setup, u_connect_client_port_before, u_connect_client_port_after, NULL); From 31f5564234db0dd4bc040716acea4fb32d6d904f Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 20:09:02 +0200 Subject: [PATCH 12/39] u_cx_at_client: Synchronize close with background RX Hold the command mutex while closing the UART and checking whether background receive work may run. This prevents queued work from reading a UART after another thread closes it. Unwind intentionally incomplete commands in unit-test teardown before closing the client. --- src/u_cx_at_client.c | 12 ++++++------ test/test_u_cx_at_client.c | 4 ++++ test/test_u_cx_at_client_no_urc_queue.c | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index 4068c43..f3f8437 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -529,16 +529,20 @@ int32_t uCxAtClientOpen(uCxAtClient_t *pClient, int32_t baudRate, bool flowContr void uCxAtClientClose(uCxAtClient_t *pClient) { + U_CX_MUTEX_LOCK(pClient->cmdMutex); + if (!pClient->opened) { + U_CX_MUTEX_UNLOCK(pClient->cmdMutex); return; } + pClient->opened = false; if (pClient->uartHandle != NULL) { uPortUartClose(pClient->uartHandle); pClient->uartHandle = NULL; } - pClient->opened = false; + U_CX_MUTEX_UNLOCK(pClient->cmdMutex); } void uCxAtClientSetUrcCallback(uCxAtClient_t *pClient, uUrcCallback_t urcCallback, void *pTag) @@ -770,14 +774,10 @@ int32_t uCxAtClientCmdEnd(uCxAtClient_t *pClient) int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) { - if (!pClient->opened) { - return 0; - } - int32_t ret = 0; U_CX_MUTEX_LOCK(pClient->cmdMutex); - if (!pClient->executingCmd) { + if (pClient->opened && !pClient->executingCmd) { int32_t parserRet = handleRxData(pClient); if (parserRet == AT_PARSER_ERROR && pClient->status == U_CX_ERROR_IO) { ret = pClient->lastIoError; diff --git a/test/test_u_cx_at_client.c b/test/test_u_cx_at_client.c index be674c6..9d20ffa 100644 --- a/test/test_u_cx_at_client.c +++ b/test/test_u_cx_at_client.c @@ -201,6 +201,10 @@ void setUp(void) void tearDown(void) { + if (gClient.executingCmd) { + gClient.executingCmd = false; + U_CX_MUTEX_UNLOCK(gClient.cmdMutex); + } uCxAtClientClose(&gClient); uCxAtClientDeinit(&gClient); } diff --git a/test/test_u_cx_at_client_no_urc_queue.c b/test/test_u_cx_at_client_no_urc_queue.c index 86854e8..0d0cd13 100644 --- a/test/test_u_cx_at_client_no_urc_queue.c +++ b/test/test_u_cx_at_client_no_urc_queue.c @@ -163,6 +163,10 @@ void setUp(void) void tearDown(void) { + if (gClient.executingCmd) { + gClient.executingCmd = false; + U_CX_MUTEX_UNLOCK(gClient.cmdMutex); + } uCxAtClientClose(&gClient); uCxAtClientDeinit(&gClient); } From d6114285caefa917e07d5856c88ff757acaba923 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sat, 5 Sep 2026 20:09:30 +0200 Subject: [PATCH 13/39] test: Add Zephyr client integration coverage Drive command responses and URCs through the UART emulator, ISR, and background RX worker. Cover fragmented text and binary responses, fragmented binary URCs, timeout recovery, and immediate teardown. --- zephyr/port_test/src/main.c | 208 ++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/zephyr/port_test/src/main.c b/zephyr/port_test/src/main.c index bb65949..cc203af 100644 --- a/zephyr/port_test/src/main.c +++ b/zephyr/port_test/src/main.c @@ -35,6 +35,8 @@ #define U_RINGBUFFER_SIZE 128 #define TEST_DATA_SIZE (U_RINGBUFFER_SIZE * 2) +#define MODEM_THREAD_STACK_SIZE 4096 +#define MODEM_THREAD_PRIORITY 5 #define TIMESTAMP_CREATE() int64_t __timestamp = k_uptime_get(); @@ -57,12 +59,91 @@ struct u_connect_client_port_fixture { uint8_t urcBuffer[TEST_DATA_SIZE]; }; +struct modem_response { + const struct device *pDev; + const uint8_t *pChunks[5]; + size_t chunkLengths[5]; + size_t chunkCount; + size_t chunksSent; + int32_t error; +}; + +struct urc_capture { + uCxAtClient_t *pClient; + char line[32]; + size_t lineLength; + uint8_t binaryData[16]; + size_t binaryDataLength; +}; + /* ---------------------------------------------------------------- * VARIABLES * -------------------------------------------------------------- */ extern bool gDisableRxWorker; +K_SEM_DEFINE(gModemRequest, 0, 1); +K_SEM_DEFINE(gModemDone, 0, 1); +K_SEM_DEFINE(gUrcReceived, 0, 1); +static struct modem_response *gpModemResponse; +static struct urc_capture gUrcCapture; + +static void urcCallback(uCxAtClient_t *pClient, void *pTag, char *pLine, + size_t lineLength, uint8_t *pBinaryData, + size_t binaryDataLength) +{ + ARG_UNUSED(pTag); + + gUrcCapture.pClient = pClient; + gUrcCapture.lineLength = MIN(lineLength, sizeof(gUrcCapture.line) - 1); + memcpy(gUrcCapture.line, pLine, gUrcCapture.lineLength); + gUrcCapture.line[gUrcCapture.lineLength] = '\0'; + gUrcCapture.binaryDataLength = MIN(binaryDataLength, + sizeof(gUrcCapture.binaryData)); + memcpy(gUrcCapture.binaryData, pBinaryData, + gUrcCapture.binaryDataLength); + k_sem_give(&gUrcReceived); +} + +static void modemResponseThread(void *p1, void *p2, void *p3) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + while (true) { + k_sem_take(&gModemRequest, K_FOREVER); + struct modem_response *pResponse = gpModemResponse; + + for (size_t i = 0; i < pResponse->chunkCount; i++) { + k_sleep(K_MSEC(5)); + int32_t sent = uart_emul_put_rx_data(pResponse->pDev, + pResponse->pChunks[i], + pResponse->chunkLengths[i]); + if (sent != pResponse->chunkLengths[i]) { + pResponse->error = sent; + break; + } + pResponse->chunksSent++; + } + k_sem_give(&gModemDone); + } +} + +K_THREAD_DEFINE(gModemThread, MODEM_THREAD_STACK_SIZE, modemResponseThread, + NULL, NULL, NULL, MODEM_THREAD_PRIORITY, 0, 0); + +static void startModemResponse(struct modem_response *pResponse) +{ + gpModemResponse = pResponse; + k_sem_give(&gModemRequest); +} + +static void waitForModemResponse(void) +{ + zassert_equal(k_sem_take(&gModemDone, K_SECONDS(1)), 0); +} + /* ---------------------------------------------------------------- * TEST SETUP * -------------------------------------------------------------- */ @@ -85,6 +166,7 @@ static void *u_connect_client_port_setup(void) fixture.config.pUrcBuffer = fixture.urcBuffer; fixture.config.urcBufferLen = sizeof(fixture.urcBuffer); fixture.config.pUartDevName = fixture.pDev->name; + fixture.config.timeoutMs = 10; // Initialize AT client uCxAtClientInit(&fixture.config, &fixture.client); @@ -106,6 +188,8 @@ static void u_connect_client_port_before(void *f) memset(&fixture->rxBuffer, 0, sizeof(fixture->rxBuffer)); memset(&fixture->urcBuffer, 0, sizeof(fixture->urcBuffer)); + memset(&gUrcCapture, 0, sizeof(gUrcCapture)); + k_sem_reset(&gUrcReceived); gDisableRxWorker = true; @@ -284,4 +368,128 @@ ZTEST_F(u_connect_client_port, test_os_port_and_rx_worker) uPortDeinit(); } +ZTEST_F(u_connect_client_port, test_fragmented_command_response) +{ + static const uint8_t responseStart[] = "\r\nO"; + static const uint8_t responseEnd[] = "K\r\n"; + struct modem_response response = { + .pDev = fixture->pDev, + .pChunks = {responseStart, responseEnd}, + .chunkLengths = {sizeof(responseStart) - 1, sizeof(responseEnd) - 1}, + .chunkCount = 2 + }; + + gDisableRxWorker = false; + startModemResponse(&response); + int32_t result = uCxAtClientExecSimpleCmd(&fixture->client, "AT"); + waitForModemResponse(); + gDisableRxWorker = true; + zassert_equal(response.error, 0); + zassert_equal(response.chunksSent, response.chunkCount); + zassert_equal(result, 0); + + uint8_t txData[3]; + zassert_equal(uart_emul_get_tx_data(fixture->pDev, txData, sizeof(txData)), + sizeof(txData)); + zassert_mem_equal__(txData, "AT\r", sizeof(txData)); +} + +ZTEST_F(u_connect_client_port, test_fragmented_binary_response) +{ + static const uint8_t responseLine[] = {'+', 'F', 'O', 'O', ':', 0x01}; + static const uint8_t lengthHigh[] = {0x00}; + static const uint8_t lengthLowAndData[] = {0x04, 0x00, 0x11}; + static const uint8_t remainingData[] = {0x22, 0xff}; + static const uint8_t responseStatus[] = "\r\nOK\r\n"; + static const uint8_t expectedData[] = {0x00, 0x11, 0x22, 0xff}; + struct modem_response response = { + .pDev = fixture->pDev, + .pChunks = {responseLine, lengthHigh, lengthLowAndData, + remainingData, responseStatus}, + .chunkLengths = {sizeof(responseLine), sizeof(lengthHigh), + sizeof(lengthLowAndData), sizeof(remainingData), + sizeof(responseStatus) - 1}, + .chunkCount = 5 + }; + uint8_t binaryData[sizeof(expectedData)] = {0}; + uint16_t binaryLength = sizeof(binaryData); + + startModemResponse(&response); + uCxAtClientCmdBeginF(&fixture->client, "AT+FOO", "", + U_CX_AT_UTIL_PARAM_LAST); + char *pParams = uCxAtClientCmdGetRspParamLine(&fixture->client, "+FOO:", + binaryData, &binaryLength); + waitForModemResponse(); + zassert_equal(response.error, 0); + zassert_equal(response.chunksSent, response.chunkCount); + zassert_not_null(pParams); + zassert_equal(pParams[0], '\0'); + zassert_equal(binaryLength, sizeof(expectedData)); + zassert_mem_equal__(binaryData, expectedData, sizeof(expectedData)); + zassert_equal(uCxAtClientCmdEnd(&fixture->client), 0); + + uint8_t txData[7]; + zassert_equal(uart_emul_get_tx_data(fixture->pDev, txData, sizeof(txData)), + sizeof(txData)); + zassert_mem_equal__(txData, "AT+FOO\r", sizeof(txData)); +} + +ZTEST_F(u_connect_client_port, test_fragmented_binary_urc) +{ + static const uint8_t urcLine[] = "\r\n+MYURC:123"; + static const uint8_t lengthHigh[] = {0x01, 0x00}; + static const uint8_t lengthLowAndData[] = {0x04, 0x00, 0x11}; + static const uint8_t remainingData[] = {0x22, 0xff}; + static const uint8_t expectedData[] = {0x00, 0x11, 0x22, 0xff}; + struct modem_response response = { + .pDev = fixture->pDev, + .pChunks = {urcLine, lengthHigh, lengthLowAndData, remainingData}, + .chunkLengths = {sizeof(urcLine) - 1, sizeof(lengthHigh), + sizeof(lengthLowAndData), sizeof(remainingData)}, + .chunkCount = 4 + }; + + uCxAtClientSetUrcCallback(&fixture->client, urcCallback, NULL); + gDisableRxWorker = false; + startModemResponse(&response); + waitForModemResponse(); + zassert_equal(k_sem_take(&gUrcReceived, K_SECONDS(1)), 0); + gDisableRxWorker = true; + + zassert_equal(response.error, 0); + zassert_equal(response.chunksSent, response.chunkCount); + zassert_equal(gUrcCapture.pClient, &fixture->client); + zassert_equal(gUrcCapture.lineLength, strlen("+MYURC:123")); + zassert_equal(strcmp(gUrcCapture.line, "+MYURC:123"), 0); + zassert_equal(gUrcCapture.binaryDataLength, sizeof(expectedData)); + zassert_mem_equal__(gUrcCapture.binaryData, expectedData, + sizeof(expectedData)); +} + +ZTEST_F(u_connect_client_port, test_command_recovers_after_timeout) +{ + static const uint8_t response[] = "\r\nOK\r\n"; + struct modem_response modemResponse = { + .pDev = fixture->pDev, + .pChunks = {response}, + .chunkLengths = {sizeof(response) - 1}, + .chunkCount = 1 + }; + + uCxAtClientSetCommandTimeout(&fixture->client, 25, false); + zassert_equal(uCxAtClientExecSimpleCmd(&fixture->client, "AT"), + U_CX_ERROR_CMD_TIMEOUT); + + startModemResponse(&modemResponse); + zassert_equal(uCxAtClientExecSimpleCmd(&fixture->client, "AT"), 0); + waitForModemResponse(); + zassert_equal(modemResponse.error, 0); + zassert_equal(modemResponse.chunksSent, modemResponse.chunkCount); + + uint8_t txData[6]; + zassert_equal(uart_emul_get_tx_data(fixture->pDev, txData, sizeof(txData)), + sizeof(txData)); + zassert_mem_equal__(txData, "AT\rAT\r", sizeof(txData)); +} + ZTEST_SUITE(u_connect_client_port, NULL, u_connect_client_port_setup, u_connect_client_port_before, u_connect_client_port_after, NULL); From 2ae44dd96a842e4417b3044ca67f8281dab98779 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 10:33:41 +0200 Subject: [PATCH 14/39] u_cx_at_urc_queue: Release lock between fragments Do not retain mutex ownership while a fragmented binary URC waits for more payload. Hide incomplete entries from dequeueing so another thread can safely abort the transaction when a receive session closes. --- src/u_cx_at_urc_queue.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/u_cx_at_urc_queue.c b/src/u_cx_at_urc_queue.c index 8418b73..851fd7e 100644 --- a/src/u_cx_at_urc_queue.c +++ b/src/u_cx_at_urc_queue.c @@ -94,10 +94,10 @@ bool uCxAtUrcQueueEnqueueBegin(uCxAtUrcQueue_t *pUrcQueue, const char *pUrcLine, ret = true; } else { // Not enough space available - U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); ret = false; } + U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); return ret; } @@ -112,6 +112,7 @@ uint16_t uCxAtUrcQueueEnqueueGetPayloadPtr(uCxAtUrcQueue_t *pUrcQueue, uint8_t * void uCxAtUrcQueueEnqueueEnd(uCxAtUrcQueue_t *pUrcQueue, uint16_t payloadSize) { + U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); U_CX_AT_PORT_ASSERT(pUrcQueue->pEnqueueEntry); U_CX_AT_PORT_ASSERT(getUnusedBuf(pUrcQueue) >= payloadSize); @@ -124,6 +125,7 @@ void uCxAtUrcQueueEnqueueEnd(uCxAtUrcQueue_t *pUrcQueue, uint16_t payloadSize) void uCxAtUrcQueueEnqueueAbort(uCxAtUrcQueue_t *pUrcQueue) { + U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); U_CX_AT_PORT_ASSERT(pUrcQueue->pEnqueueEntry); uint8_t *pEntry = (uint8_t *)pUrcQueue->pEnqueueEntry; @@ -140,7 +142,8 @@ uUrcEntry_t *uCxAtUrcQueueDequeueBegin(uCxAtUrcQueue_t *pUrcQueue) U_CX_AT_PORT_ASSERT(pUrcQueue->pDequeueEntry == NULL); U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); - if (pUrcQueue->bufferPos > 0) { + if ((pUrcQueue->bufferPos > 0) && + (pUrcQueue->pEnqueueEntry == NULL)) { pEntry = (uUrcEntry_t *)&pUrcQueue->pBuffer[0]; } U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); From 4d2eec5e129b668a202748c485f9e05bd76f21bd Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 10:33:43 +0200 Subject: [PATCH 15/39] u_cx_at_client: Reset receive state on reopen Serialize open with close and background RX. Clear partial parser and binary response state at session boundaries, and abort an incomplete queued binary URC before starting the next UART session. --- src/u_cx_at_client.c | 47 +++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index f3f8437..a601ed4 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -79,6 +79,23 @@ static int32_t gNextInstance = 0; * STATIC FUNCTIONS * -------------------------------------------------------------- */ +static void resetReceiveState(uCxAtClient_t *pClient) +{ +#if U_CX_USE_URC_QUEUE == 1 + if (pClient->urcQueue.pEnqueueEntry != NULL) { + uCxAtUrcQueueEnqueueAbort(&pClient->urcQueue); + } +#endif + pClient->rxBufferPos = 0; + pClient->urcBufferPos = 0; + pClient->pExpectedRsp = NULL; + pClient->pExpectedRspLen = 0; + pClient->pRspParams = NULL; + pClient->isBinaryRx = false; + memset(&pClient->binaryRx, 0, sizeof(pClient->binaryRx)); + memset(&pClient->rspBinaryBuf, 0, sizeof(pClient->rspBinaryBuf)); +} + // Helper function for setting up the RX binary transfer buffer static void setupBinaryRxBuffer(uCxAtClient_t *pClient, uCxAtBinaryState_t state, uint8_t *pBuffer, uint16_t bufferSize, uint16_t remainingBytes) @@ -508,23 +525,28 @@ void uCxAtClientDeinit(uCxAtClient_t *pClient) int32_t uCxAtClientOpen(uCxAtClient_t *pClient, int32_t baudRate, bool flowControl) { const struct uCxAtClientConfig *pConfig = pClient->pConfig; + int32_t ret = 0; + + U_CX_MUTEX_LOCK(pClient->cmdMutex); if (pClient->opened) { // Already opened - return U_CX_ERROR_ALREADY_EXISTS; - } - - if (pConfig->pUartDevName == NULL) { - return U_CX_ERROR_INVALID_PARAMETER; - } - - pClient->uartHandle = uPortUartOpen(pConfig->pUartDevName, baudRate, flowControl); - if (pClient->uartHandle == NULL) { - return U_CX_ERROR_IO; + ret = U_CX_ERROR_ALREADY_EXISTS; + } else if (pConfig->pUartDevName == NULL) { + ret = U_CX_ERROR_INVALID_PARAMETER; + } else { + pClient->uartHandle = uPortUartOpen(pConfig->pUartDevName, baudRate, + flowControl); + if (pClient->uartHandle == NULL) { + ret = U_CX_ERROR_IO; + } else { + resetReceiveState(pClient); + pClient->opened = true; + } } - pClient->opened = true; - return 0; + U_CX_MUTEX_UNLOCK(pClient->cmdMutex); + return ret; } void uCxAtClientClose(uCxAtClient_t *pClient) @@ -537,6 +559,7 @@ void uCxAtClientClose(uCxAtClient_t *pClient) } pClient->opened = false; + resetReceiveState(pClient); if (pClient->uartHandle != NULL) { uPortUartClose(pClient->uartHandle); pClient->uartHandle = NULL; From f58f1199898ec1151d573beb4592d9e4803dfe12 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 10:33:59 +0200 Subject: [PATCH 16/39] test: Add UART fault and lifecycle stress coverage Inject device initialization, UART configuration, and callback setup failures and verify a valid reopen after each one. Exercise repeated commands, text and binary URCs, close/reopen cycles, and interrupted binary payloads in regular and opt-in soak scenarios. --- test/test_u_cx_at_client.c | 19 +++ test/test_u_cx_at_urc_queue.c | 15 +++ zephyr/port_test/CMakeLists.txt | 5 +- zephyr/port_test/src/main.c | 155 ++++++++++++++++++++++- zephyr/port_test/src/uart_fault_driver.c | 53 ++++++++ zephyr/port_test/src/uart_fault_driver.h | 8 ++ zephyr/port_test/testcase.yaml | 7 + 7 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 zephyr/port_test/src/uart_fault_driver.c create mode 100644 zephyr/port_test/src/uart_fault_driver.h diff --git a/test/test_u_cx_at_client.c b/test/test_u_cx_at_client.c index 9d20ffa..ab88f11 100644 --- a/test/test_u_cx_at_client.c +++ b/test/test_u_cx_at_client.c @@ -739,6 +739,25 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) TEST_ASSERT_EQUAL(1, callbackCount); } +void test_uCxAtClientOpen_afterPartialBinaryHeader_expectParserReset(void) +{ + uint8_t partialBinary[] = {0x01}; + char response[] = "\r\nOK\r\n"; + + gPRxDataPtr = partialBinary; + gRxDataLen = sizeof(partialBinary); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_TRUE(gClient.isBinaryRx); + + uCxAtClientClose(&gClient); + TEST_ASSERT_EQUAL(0, uCxAtClientOpen(&gClient, 115200, true)); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + + gPRxDataPtr = (uint8_t *)response; + gRxDataLen = strlen(response); + TEST_ASSERT_EQUAL(0, uCxAtClientExecSimpleCmd(&gClient, "AT")); +} + void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) { char strData[] = { "\r\n" TEST_URC }; diff --git a/test/test_u_cx_at_urc_queue.c b/test/test_u_cx_at_urc_queue.c index ef9f235..bab9152 100644 --- a/test/test_u_cx_at_urc_queue.c +++ b/test/test_u_cx_at_urc_queue.c @@ -127,6 +127,21 @@ void test_abortedQueueing_expectEmptyQueue(void) TEST_ASSERT_EQUAL(NULL, pEntry); } +void test_incompleteQueueing_expectHiddenUntilCompletedOrAborted(void) +{ + char myString[] = "FOO123"; + + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, myString, + strlen(myString))); + TEST_ASSERT_NULL(uCxAtUrcQueueDequeueBegin(&gQueue)); + uCxAtUrcQueueEnqueueAbort(&gQueue); + + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, myString, + strlen(myString))); + uCxAtUrcQueueEnqueueEnd(&gQueue, 0); + TEST_ASSERT_NOT_NULL(uCxAtUrcQueueDequeueBegin(&gQueue)); +} + void test_uCxAtUrcQueueEnqueueBegin_withFullQueue_expectFailure(void) { char myString[sizeof(gBuffer) / 2]; diff --git a/zephyr/port_test/CMakeLists.txt b/zephyr/port_test/CMakeLists.txt index 4a7d006..9df057b 100644 --- a/zephyr/port_test/CMakeLists.txt +++ b/zephyr/port_test/CMakeLists.txt @@ -8,4 +8,7 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(app LANGUAGES C) -target_sources(app PRIVATE src/main.c) +target_sources(app PRIVATE + src/main.c + src/uart_fault_driver.c +) diff --git a/zephyr/port_test/src/main.c b/zephyr/port_test/src/main.c index cc203af..b14c202 100644 --- a/zephyr/port_test/src/main.c +++ b/zephyr/port_test/src/main.c @@ -23,6 +23,7 @@ #include "u_port.h" #include "u_cx_at_client.h" #include "u_port_uart.h" +#include "uart_fault_driver.h" /* ---------------------------------------------------------------- * COMPILE-TIME MACROS @@ -38,6 +39,10 @@ #define MODEM_THREAD_STACK_SIZE 4096 #define MODEM_THREAD_PRIORITY 5 +#ifndef STRESS_ITERATIONS +# define STRESS_ITERATIONS 100 +#endif + #define TIMESTAMP_CREATE() int64_t __timestamp = k_uptime_get(); #define TIMESTAMP_CHECK_TIME(expectMs) \ @@ -100,8 +105,10 @@ static void urcCallback(uCxAtClient_t *pClient, void *pTag, char *pLine, gUrcCapture.line[gUrcCapture.lineLength] = '\0'; gUrcCapture.binaryDataLength = MIN(binaryDataLength, sizeof(gUrcCapture.binaryData)); - memcpy(gUrcCapture.binaryData, pBinaryData, - gUrcCapture.binaryDataLength); + if (gUrcCapture.binaryDataLength > 0) { + memcpy(gUrcCapture.binaryData, pBinaryData, + gUrcCapture.binaryDataLength); + } k_sem_give(&gUrcReceived); } @@ -350,6 +357,31 @@ ZTEST_F(u_connect_client_port, test_reopen_without_flow_control) zassert_equal(config.flow_ctrl, UART_CFG_FLOW_CTRL_NONE); } +ZTEST_F(u_connect_client_port, test_open_recovers_from_device_failures) +{ + static const char *const failingDevices[] = { + UART_FAULT_NOT_READY_NAME, + UART_FAULT_CONFIGURE_NAME, + UART_FAULT_CALLBACK_NAME, + }; + + uCxAtClientClose(&fixture->client); + + for (size_t i = 0; i < ARRAY_SIZE(failingDevices); i++) { + fixture->config.pUartDevName = failingDevices[i]; + zassert_equal(uCxAtClientOpen(&fixture->client, 115200, true), + U_CX_ERROR_IO, "device %s opened", failingDevices[i]); + + fixture->config.pUartDevName = fixture->pDev->name; + zassert_equal(uCxAtClientOpen(&fixture->client, 115200, true), 0, + "reopen failed after device %s", failingDevices[i]); + uCxAtClientClose(&fixture->client); + } + + fixture->config.pUartDevName = fixture->pDev->name; + zassert_equal(uCxAtClientOpen(&fixture->client, 115200, true), 0); +} + ZTEST_F(u_connect_client_port, test_os_port_and_rx_worker) { uPortInit(); @@ -492,4 +524,123 @@ ZTEST_F(u_connect_client_port, test_command_recovers_after_timeout) zassert_mem_equal__(txData, "AT\rAT\r", sizeof(txData)); } +ZTEST_F(u_connect_client_port, test_repeated_command_urc_close_reopen) +{ + static const uint8_t commandUrc[] = "\r\n+MYURC:CMD\r\n\r\n"; + static const uint8_t responseStart[] = "O"; + static const uint8_t responseEnd[] = "K\r\n"; + static const uint8_t asyncUrcStart[] = "\r\n+MY"; + static const uint8_t asyncUrcEnd[] = "URC:ASYNC\r\n"; + static const uint8_t binaryUrcLine[] = "\r\n+MYURC:BINARY\x01"; + static const uint8_t binaryLengthHigh[] = {0x00}; + static const uint8_t binaryLengthLowAndData[] = {0x04, 0x00, 0x11}; + static const uint8_t binaryDataEnd[] = {0x22, 0xff}; + static const uint8_t expectedBinaryData[] = {0x00, 0x11, 0x22, 0xff}; + static const uint8_t partialBinaryUrc[] = "\r\n+MYURC:PARTIAL\x01"; + static const uint8_t partialBinaryHeader[] = {0x00, 0x04}; + static const uint8_t partialBinaryData[] = {0xaa}; + + uCxAtClientSetUrcCallback(&fixture->client, urcCallback, NULL); + gDisableRxWorker = false; + + for (size_t i = 0; i < STRESS_ITERATIONS; i++) { + struct modem_response commandResponse = { + .pDev = fixture->pDev, + .pChunks = {commandUrc, responseStart, responseEnd}, + .chunkLengths = {sizeof(commandUrc) - 1, + sizeof(responseStart) - 1, + sizeof(responseEnd) - 1}, + .chunkCount = 3 + }; + + k_sem_reset(&gUrcReceived); + startModemResponse(&commandResponse); + zassert_equal(uCxAtClientExecSimpleCmd(&fixture->client, "AT"), 0, + "command failed at iteration %zu", i); + waitForModemResponse(); + zassert_equal(k_sem_take(&gUrcReceived, K_SECONDS(1)), 0, + "command URC missing at iteration %zu", i); + zassert_equal(strcmp(gUrcCapture.line, "+MYURC:CMD"), 0, + "wrong command URC at iteration %zu", i); + + uint8_t txData[3]; + zassert_equal(uart_emul_get_tx_data(fixture->pDev, txData, + sizeof(txData)), sizeof(txData), + "TX missing at iteration %zu", i); + zassert_mem_equal__(txData, "AT\r", sizeof(txData), + "wrong TX at iteration %zu", i); + + uCxAtClientClose(&fixture->client); + zassert_equal(uCxAtClientOpen(&fixture->client, 115200, true), 0, + "reopen failed at iteration %zu", i); + + bool sendBinaryUrc = (i % 5) == 0; + struct modem_response asyncUrc = {.pDev = fixture->pDev}; + if (sendBinaryUrc) { + asyncUrc.pChunks[0] = binaryUrcLine; + asyncUrc.pChunks[1] = binaryLengthHigh; + asyncUrc.pChunks[2] = binaryLengthLowAndData; + asyncUrc.pChunks[3] = binaryDataEnd; + asyncUrc.chunkLengths[0] = sizeof(binaryUrcLine) - 1; + asyncUrc.chunkLengths[1] = sizeof(binaryLengthHigh); + asyncUrc.chunkLengths[2] = sizeof(binaryLengthLowAndData); + asyncUrc.chunkLengths[3] = sizeof(binaryDataEnd); + asyncUrc.chunkCount = 4; + } else { + asyncUrc.pChunks[0] = asyncUrcStart; + asyncUrc.pChunks[1] = asyncUrcEnd; + asyncUrc.chunkLengths[0] = sizeof(asyncUrcStart) - 1; + asyncUrc.chunkLengths[1] = sizeof(asyncUrcEnd) - 1; + asyncUrc.chunkCount = 2; + } + + k_sem_reset(&gUrcReceived); + startModemResponse(&asyncUrc); + waitForModemResponse(); + zassert_equal(k_sem_take(&gUrcReceived, K_SECONDS(1)), 0, + "async URC missing at iteration %zu", i); + if (sendBinaryUrc) { + zassert_equal(strcmp(gUrcCapture.line, "+MYURC:BINARY"), 0, + "wrong binary URC at iteration %zu", i); + zassert_equal(gUrcCapture.binaryDataLength, + sizeof(expectedBinaryData)); + zassert_mem_equal__(gUrcCapture.binaryData, expectedBinaryData, + sizeof(expectedBinaryData), + "wrong binary data at iteration %zu", i); + } else { + zassert_equal(strcmp(gUrcCapture.line, "+MYURC:ASYNC"), 0, + "wrong async URC at iteration %zu", i); + zassert_equal(gUrcCapture.binaryDataLength, 0); + } + + if ((i % 10) == 0) { + struct modem_response interruptedUrc = { + .pDev = fixture->pDev, + .pChunks = {partialBinaryUrc, partialBinaryHeader, + partialBinaryData}, + .chunkLengths = {sizeof(partialBinaryUrc) - 1, + sizeof(partialBinaryHeader), + sizeof(partialBinaryData)}, + .chunkCount = 3 + }; + + startModemResponse(&interruptedUrc); + waitForModemResponse(); + k_sleep(K_MSEC(20)); + zassert_true(fixture->client.isBinaryRx, + "binary RX stopped at iteration %zu", i); + zassert_not_null(fixture->client.urcQueue.pEnqueueEntry, + "URC enqueue missing at iteration %zu", i); + + uCxAtClientClose(&fixture->client); + zassert_false(fixture->client.isBinaryRx); + zassert_is_null(fixture->client.urcQueue.pEnqueueEntry); + zassert_equal(uCxAtClientOpen(&fixture->client, 115200, true), 0, + "partial URC reopen failed at iteration %zu", i); + } + } + + gDisableRxWorker = true; +} + ZTEST_SUITE(u_connect_client_port, NULL, u_connect_client_port_setup, u_connect_client_port_before, u_connect_client_port_after, NULL); diff --git a/zephyr/port_test/src/uart_fault_driver.c b/zephyr/port_test/src/uart_fault_driver.c new file mode 100644 index 0000000..e926cbe --- /dev/null +++ b/zephyr/port_test/src/uart_fault_driver.c @@ -0,0 +1,53 @@ +#include +#include +#include + +#include "uart_fault_driver.h" + +static int initFailed(const struct device *pDev) +{ + ARG_UNUSED(pDev); + return -EIO; +} + +static int initOk(const struct device *pDev) +{ + ARG_UNUSED(pDev); + return 0; +} + +static int configureFailed(const struct device *pDev, + const struct uart_config *pConfig) +{ + ARG_UNUSED(pDev); + ARG_UNUSED(pConfig); + return -EIO; +} + +static int configureOk(const struct device *pDev, + const struct uart_config *pConfig) +{ + ARG_UNUSED(pDev); + ARG_UNUSED(pConfig); + return 0; +} + +static DEVICE_API(uart, configureFailedApi) = { + .configure = configureFailed, +}; + +static DEVICE_API(uart, configureOkApi) = { + .configure = configureOk, +}; + +DEVICE_DEFINE(uartFaultNotReady, UART_FAULT_NOT_READY_NAME, initFailed, NULL, + NULL, NULL, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, + &configureOkApi); + +DEVICE_DEFINE(uartFaultConfigure, UART_FAULT_CONFIGURE_NAME, initOk, NULL, + NULL, NULL, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, + &configureFailedApi); + +DEVICE_DEFINE(uartFaultCallback, UART_FAULT_CALLBACK_NAME, initOk, NULL, + NULL, NULL, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, + &configureOkApi); \ No newline at end of file diff --git a/zephyr/port_test/src/uart_fault_driver.h b/zephyr/port_test/src/uart_fault_driver.h new file mode 100644 index 0000000..20ee340 --- /dev/null +++ b/zephyr/port_test/src/uart_fault_driver.h @@ -0,0 +1,8 @@ +#ifndef UART_FAULT_DRIVER_H +#define UART_FAULT_DRIVER_H + +#define UART_FAULT_NOT_READY_NAME "uart-fault-not-ready" +#define UART_FAULT_CONFIGURE_NAME "uart-fault-configure" +#define UART_FAULT_CALLBACK_NAME "uart-fault-callback" + +#endif \ No newline at end of file diff --git a/zephyr/port_test/testcase.yaml b/zephyr/port_test/testcase.yaml index 5d9028c..82c48af 100644 --- a/zephyr/port_test/testcase.yaml +++ b/zephyr/port_test/testcase.yaml @@ -3,3 +3,10 @@ common: tests: u_connect_client.port: platform_allow: qemu_x86 + u_connect_client.port_stress: + platform_allow: qemu_x86 + tags: + - stress + timeout: 300 + slow: true + extra_args: EXTRA_CFLAGS=-DSTRESS_ITERATIONS=1000 From dd9a8ea9c78a81efc25c5e785bcf193b3ca47470 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 10:44:35 +0200 Subject: [PATCH 17/39] u_port_uart_linux: Add high-speed baud rates Support 1, 1.5, 2, and 3 Mbaud termios mappings used during module baud-rate negotiation. --- ports/uart/u_port_uart_linux.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ports/uart/u_port_uart_linux.c b/ports/uart/u_port_uart_linux.c index bbc298a..3f5220f 100644 --- a/ports/uart/u_port_uart_linux.c +++ b/ports/uart/u_port_uart_linux.c @@ -106,6 +106,18 @@ uPortUartHandle_t uPortUartOpen(const char *pDevice, int32_t baudRate, bool useF case 921600: speed = B921600; break; + case 1000000: + speed = B1000000; + break; + case 1500000: + speed = B1500000; + break; + case 2000000: + speed = B2000000; + break; + case 3000000: + speed = B3000000; + break; default: close(pHandle->fd); free(pHandle); From 09799d6da90dc34717f2a135a3a048281ae7128a Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 10:45:00 +0200 Subject: [PATCH 18/39] u_port_uart_linux: Honor read timeouts Use poll() for nonblocking, timed, and blocking reads. Retry interrupted poll and read calls, and reject zero-progress writes to avoid an unbounded loop. --- ports/uart/u_port_uart_linux.c | 37 +++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/ports/uart/u_port_uart_linux.c b/ports/uart/u_port_uart_linux.c index 3f5220f..45f11ee 100644 --- a/ports/uart/u_port_uart_linux.c +++ b/ports/uart/u_port_uart_linux.c @@ -25,9 +25,9 @@ #include #include #include +#include #include #include -#include #include "u_port_uart.h" @@ -191,6 +191,9 @@ int32_t uPortUartWrite(uPortUartHandle_t handle, } return -1; } + if (written == 0) { + return -1; + } totalWritten += (size_t)written; } @@ -208,22 +211,32 @@ int32_t uPortUartRead(uPortUartHandle_t handle, uPortUartHandle *pHandle = (uPortUartHandle *)handle; - // For zero timeout, check if data is available without blocking - if (timeoutMs == 0) { - int available = 0; - ioctl(pHandle->fd, FIONREAD, &available); - if (available == 0) { - return 0; - } - } - // If pData is NULL, just return 0 (test case) if (pData == NULL) { return 0; } - // Read data (blocking read handled by termios VTIME setting) - ssize_t bytesRead = read(pHandle->fd, pData, length); + struct pollfd pollFd = { + .fd = pHandle->fd, + .events = POLLIN + }; + int pollResult; + do { + pollResult = poll(&pollFd, 1, timeoutMs); + } while ((pollResult < 0) && (errno == EINTR)); + + if (pollResult == 0) { + return 0; + } + if ((pollResult < 0) || ((pollFd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0)) { + return -1; + } + + ssize_t bytesRead; + do { + bytesRead = read(pHandle->fd, pData, length); + } while ((bytesRead < 0) && (errno == EINTR)); + if (bytesRead < 0) { return -1; } From 69b10cefb08df54951de7e7db648f8a70a326c5f Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 10:45:12 +0200 Subject: [PATCH 19/39] u_port_posix: Keep RX task alive across reopen Skip receive handling while the client is closed and retry transient receive failures instead of terminating the process. Track successful thread creation so teardown never joins an invalid thread, and release pthread attributes after setup. --- ports/os/u_port_posix.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ports/os/u_port_posix.c b/ports/os/u_port_posix.c index 45620b9..31f874d 100644 --- a/ports/os/u_port_posix.c +++ b/ports/os/u_port_posix.c @@ -39,6 +39,7 @@ typedef struct { uCxAtClient_t *pClient; pthread_t rxThread; volatile bool terminateRxTask; + bool rxThreadCreated; } uPortRxContext_t; /* ---------------------------------------------------------------- @@ -81,11 +82,17 @@ static void *rxTask(void *pArg) uPortRxContext_t *pCtx = (uPortRxContext_t *)pArg; while (!pCtx->terminateRxTask) { - U_CX_PORT_SLEEP_MS(10); + if (!pCtx->pClient->opened) { + U_CX_PORT_SLEEP_MS(10); + continue; + } + if (uCxAtClientHandleRx(pCtx->pClient) < 0) { - printf("Error in RX handling thread\n"); - exit(1); + U_CX_PORT_SLEEP_MS(100); + continue; } + + U_CX_PORT_SLEEP_MS(10); } U_CX_LOG_LINE_I(U_CX_LOG_CH_DBG, pCtx->pClient->instance, "RX task terminated"); @@ -148,12 +155,17 @@ void uPortBgRxTaskCreate(uCxAtClient_t *pClient) pthread_attr_getschedparam(&attr, ¶m); param.sched_priority = 9; pthread_attr_setschedparam(&attr, ¶m); - pthread_create(&gRxContext.rxThread, &attr, rxTask, &gRxContext); + gRxContext.rxThreadCreated = + pthread_create(&gRxContext.rxThread, &attr, rxTask, &gRxContext) == 0; + pthread_attr_destroy(&attr); } void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) { (void)pClient; gRxContext.terminateRxTask = true; - pthread_join(gRxContext.rxThread, NULL); + if (gRxContext.rxThreadCreated) { + pthread_join(gRxContext.rxThread, NULL); + gRxContext.rxThreadCreated = false; + } } From 5a231a0b9591863d8dadb920b5d3fb42ef3651c7 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 10:45:42 +0200 Subject: [PATCH 20/39] test: Add native POSIX port coverage Exercise Linux UART configuration, binary transfers, timeout behavior, fault handling, and repeated open/close using pseudoterminals. Run the suite with sanitizers and gcov through inv test.posix.run. --- project.yml | 1 + tasks.py | 29 +++ test/posix/CMakeLists.txt | 88 ++++++++ test/posix/test_posix_port.c | 368 ++++++++++++++++++++++++++++++++++ test/posix/test_uart_faults.c | 223 ++++++++++++++++++++ 5 files changed, 709 insertions(+) create mode 100644 test/posix/CMakeLists.txt create mode 100644 test/posix/test_posix_port.c create mode 100644 test/posix/test_uart_faults.c diff --git a/project.yml b/project.yml index 5f13c50..de73c80 100644 --- a/project.yml +++ b/project.yml @@ -21,6 +21,7 @@ :test: - +:test/** - -:test/support + - -:test/posix/** :source: - src/** :include: diff --git a/tasks.py b/tasks.py index 4ca20e0..d836d72 100644 --- a/tasks.py +++ b/tasks.py @@ -76,6 +76,22 @@ def zephyr(c, verbose=False): f"west twister -T {zephyr_tests}/ --integration {verbose_flag}", pty=True) +@task +def posix(c): + """Run native POSIX port tests and generate coverage.""" + print("Running POSIX port tests...") + build_dir = os.path.join(REPO_ROOT, "build", "posix") + test_dir = os.path.join(REPO_ROOT, "test", "posix") + + c.run(f"cmake -S {test_dir} -B {build_dir} " + "-DENABLE_COVERAGE=ON -DENABLE_SANITIZERS=ON") + c.run(f"cmake --build {build_dir} --parallel") + c.run(f"ctest --test-dir {build_dir} --output-on-failure") + c.run(f"gcovr {build_dir} --root {REPO_ROOT} --object-directory {build_dir} " + "--filter 'ports/(os/u_port_posix|uart/u_port_uart_linux)\\.c' " + f"--txt --html-details {os.path.join(build_dir, 'coverage.html')}") + + @task def clean_ceedling(c): """Clean Ceedling build artifacts.""" @@ -92,6 +108,13 @@ def clean_zephyr(c): c.run(f"rm -rf {os.path.join(REPO_ROOT, 'zephyr/build')}", pty=True, warn=True) +@task +def clean_posix(c): + """Clean native POSIX test artifacts.""" + print("Cleaning POSIX test artifacts...") + c.run(f"rm -rf {os.path.join(REPO_ROOT, 'build', 'posix')}") + + @task def clean_west(c): """Clean west workspace.""" @@ -111,10 +134,16 @@ def clean_west(c): zephyr_ns.add_task(clean_zephyr, 'clean') zephyr_ns.add_task(clean_west, 'clean-west') +# POSIX sub-collection under test +posix_ns = Collection('posix') +posix_ns.add_task(posix, 'run') +posix_ns.add_task(clean_posix, 'clean') + # Test namespace with sub-collections test_ns = Collection('test') test_ns.add_collection(ceedling_ns) test_ns.add_collection(zephyr_ns) +test_ns.add_collection(posix_ns) # Create main namespace ns = Collection() diff --git a/test/posix/CMakeLists.txt b/test/posix/CMakeLists.txt new file mode 100644 index 0000000..52add2e --- /dev/null +++ b/test/posix/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 3.16) +project(u_connect_client_posix_tests C) + +include(CTest) +find_package(Threads REQUIRED) + +option(ENABLE_COVERAGE "Build with gcov instrumentation" ON) +option(ENABLE_SANITIZERS "Build with address and undefined sanitizers" ON) + +add_executable(test_posix_port + test_posix_port.c + ../../ports/os/u_port_posix.c + ../../ports/uart/u_port_uart_linux.c + ../../src/u_cx_log.c +) + +target_compile_definitions(test_posix_port PRIVATE U_PORT_POSIX) +target_include_directories(test_posix_port PRIVATE + ../../inc + ../../ports +) +target_compile_options(test_posix_port PRIVATE + -Wall + -Wextra + -Werror + -Wconversion + -Wsign-conversion + -Wshadow + -pedantic +) +target_link_libraries(test_posix_port PRIVATE Threads::Threads util) +target_link_options(test_posix_port PRIVATE + -Wl,--wrap=pthread_create + -Wl,--wrap=pthread_join +) + +if(ENABLE_COVERAGE) + target_compile_options(test_posix_port PRIVATE --coverage -O0 -g) + target_link_options(test_posix_port PRIVATE --coverage) +endif() + +if(ENABLE_SANITIZERS) + target_compile_options(test_posix_port PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + -fno-omit-frame-pointer + ) + target_link_options(test_posix_port PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + ) +endif() + +add_test(NAME posix_port COMMAND test_posix_port) +set_tests_properties(posix_port PROPERTIES TIMEOUT 15) + +add_executable(test_uart_faults + test_uart_faults.c + ../../ports/uart/u_port_uart_linux.c +) +target_include_directories(test_uart_faults PRIVATE ../../ports) +target_compile_options(test_uart_faults PRIVATE + -Wall + -Wextra + -Werror + -Wconversion + -Wsign-conversion + -Wshadow + -pedantic +) +target_link_libraries(test_uart_faults PRIVATE util) +target_link_options(test_uart_faults PRIVATE + -Wl,--wrap=malloc + -Wl,--wrap=tcgetattr + -Wl,--wrap=tcsetattr + -Wl,--wrap=poll + -Wl,--wrap=read + -Wl,--wrap=write +) +if(ENABLE_COVERAGE) + target_compile_options(test_uart_faults PRIVATE --coverage -O0 -g) + target_link_options(test_uart_faults PRIVATE --coverage) +endif() + +add_test(NAME uart_faults COMMAND test_uart_faults) +set_tests_properties(uart_faults PROPERTIES TIMEOUT 5) diff --git a/test/posix/test_posix_port.c b/test/posix/test_posix_port.c new file mode 100644 index 0000000..9adabe6 --- /dev/null +++ b/test/posix/test_posix_port.c @@ -0,0 +1,368 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "u_cx_at_client.h" +#include "u_port.h" +#include "u_port_uart.h" + +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: check failed: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +typedef struct { + int fd; + const uint8_t *pData; + size_t length; + int32_t delayMs; +} delayedWrite_t; + +static atomic_int gRxCalls; +static atomic_int gRxFailures; +static atomic_bool gFailThreadCreate; +static atomic_int gThreadJoinCalls; + +int __real_pthread_create(pthread_t *pThread, const pthread_attr_t *pAttributes, + void *(*pEntryPoint)(void *), void *pParameter); +int __real_pthread_join(pthread_t thread, void **pReturnValue); + +int __wrap_pthread_create(pthread_t *pThread, const pthread_attr_t *pAttributes, + void *(*pEntryPoint)(void *), void *pParameter) +{ + if (atomic_exchange(&gFailThreadCreate, false)) { + return EAGAIN; + } + return __real_pthread_create(pThread, pAttributes, pEntryPoint, pParameter); +} + +int __wrap_pthread_join(pthread_t thread, void **pReturnValue) +{ + atomic_fetch_add(&gThreadJoinCalls, 1); + return __real_pthread_join(thread, pReturnValue); +} + +static int64_t monotonicTimeMs(void) +{ + struct timespec now; + CHECK(clock_gettime(CLOCK_MONOTONIC, &now) == 0); + return ((int64_t)now.tv_sec * 1000) + (now.tv_nsec / 1000000); +} + +static bool sleepMs(int32_t delayMs) +{ + struct timespec delay = { + .tv_sec = delayMs / 1000, + .tv_nsec = (delayMs % 1000) * 1000000 + }; + + while ((nanosleep(&delay, &delay) != 0) && (errno == EINTR)) { + } + return true; +} + +static void *delayedWrite(void *pParameter) +{ + delayedWrite_t *pWrite = pParameter; + size_t written = 0; + + sleepMs(pWrite->delayMs); + while (written < pWrite->length) { + ssize_t result = write(pWrite->fd, pWrite->pData + written, + pWrite->length - written); + if (result > 0) { + written += (size_t)result; + } else if ((result < 0) && (errno != EINTR)) { + break; + } + } + return NULL; +} + +static bool createPty(int *pMasterFd, char *pSlaveName, size_t nameLength) +{ + int slaveFd; + + CHECK(openpty(pMasterFd, &slaveFd, pSlaveName, NULL, NULL) == 0); + CHECK(strlen(pSlaveName) < nameLength); + CHECK(close(slaveFd) == 0); + return true; +} + +static bool testOpenAndConfiguration(void) +{ + static const int32_t baudRates[] = { + 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, + 1000000, 1500000, 2000000, 3000000 + }; + char slaveName[128]; + int masterFd; + + CHECK(uPortUartOpen(NULL, 115200, false) == NULL); + CHECK(uPortUartOpen("/path/that/does/not/exist", 115200, false) == NULL); + CHECK(createPty(&masterFd, slaveName, sizeof(slaveName))); + CHECK(uPortUartOpen(slaveName, 12345, false) == NULL); + + for (size_t index = 0; index < ARRAY_SIZE(baudRates); index++) { + uPortUartHandle_t handle = uPortUartOpen(slaveName, baudRates[index], false); + CHECK(handle != NULL); + uPortUartClose(handle); + } + + uPortUartHandle_t handle = uPortUartOpen(slaveName, 115200, false); + CHECK(handle != NULL); + int observerFd = open(slaveName, O_RDWR | O_NOCTTY); + CHECK(observerFd >= 0); + struct termios settings; + CHECK(tcgetattr(observerFd, &settings) == 0); + CHECK((settings.c_lflag & (ICANON | ECHO | ECHOE | ISIG)) == 0); + CHECK((settings.c_iflag & + (IXON | IXOFF | IXANY | ICRNL | INLCR | IGNCR | ISTRIP)) == 0); + CHECK((settings.c_oflag & OPOST) == 0); + CHECK(settings.c_cc[VMIN] == 0); + CHECK(settings.c_cc[VTIME] == 0); + CHECK(close(observerFd) == 0); + uPortUartClose(handle); + + handle = uPortUartOpen(slaveName, 115200, true); + CHECK(handle != NULL); + observerFd = open(slaveName, O_RDWR | O_NOCTTY); + CHECK(observerFd >= 0); + CHECK(tcgetattr(observerFd, &settings) == 0); + CHECK((settings.c_cflag & CRTSCTS) != 0); + CHECK(close(observerFd) == 0); + uPortUartClose(handle); + uPortUartClose(NULL); + CHECK(close(masterFd) == 0); + return true; +} + +static bool testBinaryTransferAndReopen(void) +{ + static const uint8_t payload[] = {0x00, 0x0d, 0x0a, 0x7f, 0x80, 0xff}; + uint8_t received[sizeof(payload)] = {0}; + char slaveName[128]; + int masterFd; + + CHECK(createPty(&masterFd, slaveName, sizeof(slaveName))); + uPortUartHandle_t handle = uPortUartOpen(slaveName, 115200, false); + CHECK(handle != NULL); + + CHECK(write(masterFd, payload, 2) == 2); + CHECK(uPortUartRead(handle, received, sizeof(received), 100) == 2); + CHECK(memcmp(received, payload, 2) == 0); + CHECK(write(masterFd, payload + 2, sizeof(payload) - 2) == + (ssize_t)(sizeof(payload) - 2)); + CHECK(uPortUartRead(handle, received + 2, sizeof(received) - 2, 100) == + (int32_t)(sizeof(received) - 2)); + CHECK(memcmp(received, payload, sizeof(payload)) == 0); + + CHECK(uPortUartWrite(handle, payload, sizeof(payload)) == + (int32_t)sizeof(payload)); + memset(received, 0, sizeof(received)); + CHECK(read(masterFd, received, sizeof(received)) == (ssize_t)sizeof(received)); + CHECK(memcmp(received, payload, sizeof(payload)) == 0); + + CHECK(uPortUartRead(handle, received, sizeof(received), 0) == 0); + CHECK(uPortUartRead(handle, NULL, sizeof(received), 0) == 0); + CHECK(uPortUartRead(NULL, received, sizeof(received), 0) < 0); + CHECK(uPortUartRead(handle, received, 0, 0) < 0); + CHECK(uPortUartWrite(NULL, payload, sizeof(payload)) < 0); + CHECK(uPortUartWrite(handle, NULL, sizeof(payload)) < 0); + CHECK(uPortUartWrite(handle, payload, 0) < 0); + + uPortUartClose(handle); + handle = uPortUartOpen(slaveName, 115200, false); + CHECK(handle != NULL); + CHECK(write(masterFd, payload, sizeof(payload)) == (ssize_t)sizeof(payload)); + CHECK(uPortUartRead(handle, received, sizeof(received), 100) == + (int32_t)sizeof(received)); + CHECK(memcmp(received, payload, sizeof(payload)) == 0); + + uPortUartClose(handle); + CHECK(close(masterFd) == 0); + return true; +} + +static bool testRepeatedOpenClose(void) +{ + uint8_t received; + + for (int32_t iteration = 0; iteration < 100; iteration++) { + char slaveName[128]; + int masterFd; + uint8_t expected = (uint8_t)iteration; + + CHECK(createPty(&masterFd, slaveName, sizeof(slaveName))); + uPortUartHandle_t handle = uPortUartOpen(slaveName, 115200, false); + CHECK(handle != NULL); + CHECK(write(masterFd, &expected, 1) == 1); + CHECK(uPortUartRead(handle, &received, 1, 100) == 1); + CHECK(received == expected); + uPortUartClose(handle); + CHECK(close(masterFd) == 0); + } + return true; +} + +static bool testReadTimeouts(void) +{ + static const uint8_t byte = 0xa5; + uint8_t received = 0; + char slaveName[128]; + int masterFd; + + CHECK(createPty(&masterFd, slaveName, sizeof(slaveName))); + uPortUartHandle_t handle = uPortUartOpen(slaveName, 115200, false); + CHECK(handle != NULL); + + int64_t startMs = monotonicTimeMs(); + CHECK(uPortUartRead(handle, &received, 1, 80) == 0); + int64_t elapsedMs = monotonicTimeMs() - startMs; + CHECK(elapsedMs >= 50); + CHECK(elapsedMs < 500); + + delayedWrite_t writeParameters = { + .fd = masterFd, + .pData = &byte, + .length = 1, + .delayMs = 40 + }; + pthread_t writer; + CHECK(pthread_create(&writer, NULL, delayedWrite, &writeParameters) == 0); + startMs = monotonicTimeMs(); + CHECK(uPortUartRead(handle, &received, 1, 300) == 1); + elapsedMs = monotonicTimeMs() - startMs; + CHECK(pthread_join(writer, NULL) == 0); + CHECK(received == byte); + CHECK(elapsedMs >= 20); + CHECK(elapsedMs < 250); + + uPortUartClose(handle); + CHECK(close(masterFd) == 0); + return true; +} + +int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) +{ + if (!pClient->opened) { + return -1; + } + if (atomic_load(&gRxFailures) > 0) { + atomic_fetch_sub(&gRxFailures, 1); + return -1; + } + atomic_fetch_add(&gRxCalls, 1); + return 0; +} + +static bool testPosixOsPrimitives(void) +{ + uPortInit(); + int32_t startMs = uPortGetTickTimeMs(); + CHECK(uPortSleepMs(20) == 0); + CHECK(uPortGetTickTimeMs() - startMs >= 10); + + pthread_mutex_t mutex; + CHECK(pthread_mutex_init(&mutex, NULL) == 0); + CHECK(uPortMutexTryLock(&mutex, 0) == 0); + CHECK(uPortMutexTryLock(&mutex, 20) == ETIMEDOUT); + CHECK(pthread_mutex_unlock(&mutex) == 0); + CHECK(uPortMutexTryLock(&mutex, 20) == 0); + CHECK(pthread_mutex_unlock(&mutex) == 0); + CHECK(pthread_mutex_destroy(&mutex) == 0); + uPortDeinit(); + return true; +} + +static bool testBackgroundRxCloseAndReopen(void) +{ + uCxAtClient_t client = {0}; + atomic_store(&gRxCalls, 0); + atomic_store(&gRxFailures, 0); + + client.opened = false; + uPortBgRxTaskCreate(&client); + CHECK(sleepMs(30)); + CHECK(atomic_load(&gRxCalls) == 0); + + client.opened = true; + CHECK(sleepMs(40)); + CHECK(atomic_load(&gRxCalls) > 0); + + client.opened = false; + CHECK(sleepMs(30)); + int callsWhileClosed = atomic_load(&gRxCalls); + CHECK(sleepMs(30)); + CHECK(atomic_load(&gRxCalls) == callsWhileClosed); + + client.opened = true; + CHECK(sleepMs(40)); + CHECK(atomic_load(&gRxCalls) > callsWhileClosed); + + int callsBeforeError = atomic_load(&gRxCalls); + atomic_store(&gRxFailures, 1); + CHECK(sleepMs(150)); + CHECK(atomic_load(&gRxCalls) > callsBeforeError); + uPortBgRxTaskDestroy(&client); + return true; +} + +static bool testBackgroundRxCreateFailure(void) +{ + uCxAtClient_t client = {0}; + int joinsBefore = atomic_load(&gThreadJoinCalls); + + client.opened = true; + atomic_store(&gRxCalls, 0); + atomic_store(&gFailThreadCreate, true); + uPortBgRxTaskCreate(&client); + CHECK(sleepMs(20)); + uPortBgRxTaskDestroy(&client); + CHECK(atomic_load(&gRxCalls) == 0); + CHECK(atomic_load(&gThreadJoinCalls) == joinsBefore); + return true; +} + +int main(void) +{ + static const struct { + const char *pName; + bool (*pTest)(void); + } tests[] = { + {"open and configuration", testOpenAndConfiguration}, + {"binary transfer and reopen", testBinaryTransferAndReopen}, + {"repeated open and close", testRepeatedOpenClose}, + {"read timeouts", testReadTimeouts}, + {"OS primitives", testPosixOsPrimitives}, + {"background RX close and reopen", testBackgroundRxCloseAndReopen}, + {"background RX create failure", testBackgroundRxCreateFailure} + }; + + for (size_t index = 0; index < ARRAY_SIZE(tests); index++) { + printf("[ RUN ] %s\n", tests[index].pName); + if (!tests[index].pTest()) { + printf("[ FAILED ] %s\n", tests[index].pName); + return EXIT_FAILURE; + } + printf("[ OK ] %s\n", tests[index].pName); + } + + printf("[ PASSED ] %zu tests\n", ARRAY_SIZE(tests)); + return EXIT_SUCCESS; +} diff --git a/test/posix/test_uart_faults.c b/test/posix/test_uart_faults.c new file mode 100644 index 0000000..0ba50dd --- /dev/null +++ b/test/posix/test_uart_faults.c @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "u_port_uart.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: check failed: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +typedef enum { + FAULT_NONE, + FAULT_MALLOC, + FAULT_TCGETATTR, + FAULT_TCSETATTR, + FAULT_POLL_EINTR, + FAULT_POLL_ERROR, + FAULT_READ_EINTR, + FAULT_READ_ERROR, + FAULT_WRITE_EINTR, + FAULT_WRITE_ERROR, + FAULT_WRITE_SHORT, + FAULT_WRITE_ZERO +} fault_t; + +static fault_t gFault; + +void *__real_malloc(size_t size); +int __real_tcgetattr(int fd, struct termios *pSettings); +int __real_tcsetattr(int fd, int actions, const struct termios *pSettings); +int __real_poll(struct pollfd *pFds, nfds_t count, int timeout); +ssize_t __real_read(int fd, void *pData, size_t length); +ssize_t __real_write(int fd, const void *pData, size_t length); + +void *__wrap_malloc(size_t size) +{ + if (gFault == FAULT_MALLOC) { + gFault = FAULT_NONE; + return NULL; + } + return __real_malloc(size); +} + +int __wrap_tcgetattr(int fd, struct termios *pSettings) +{ + if (gFault == FAULT_TCGETATTR) { + gFault = FAULT_NONE; + errno = EIO; + return -1; + } + return __real_tcgetattr(fd, pSettings); +} + +int __wrap_tcsetattr(int fd, int actions, const struct termios *pSettings) +{ + if (gFault == FAULT_TCSETATTR) { + gFault = FAULT_NONE; + errno = EIO; + return -1; + } + return __real_tcsetattr(fd, actions, pSettings); +} + +int __wrap_poll(struct pollfd *pFds, nfds_t count, int timeout) +{ + if (gFault == FAULT_POLL_EINTR) { + gFault = FAULT_NONE; + errno = EINTR; + return -1; + } + if (gFault == FAULT_POLL_ERROR) { + gFault = FAULT_NONE; + errno = EIO; + return -1; + } + return __real_poll(pFds, count, timeout); +} + +ssize_t __wrap_read(int fd, void *pData, size_t length) +{ + if (gFault == FAULT_READ_EINTR) { + gFault = FAULT_NONE; + errno = EINTR; + return -1; + } + if (gFault == FAULT_READ_ERROR) { + gFault = FAULT_NONE; + errno = EIO; + return -1; + } + return __real_read(fd, pData, length); +} + +ssize_t __wrap_write(int fd, const void *pData, size_t length) +{ + if (gFault == FAULT_WRITE_EINTR) { + gFault = FAULT_NONE; + errno = EINTR; + return -1; + } + if (gFault == FAULT_WRITE_ERROR) { + gFault = FAULT_NONE; + errno = EIO; + return -1; + } + if (gFault == FAULT_WRITE_SHORT) { + gFault = FAULT_NONE; + length = length > 2 ? 2 : length; + } else if (gFault == FAULT_WRITE_ZERO) { + gFault = FAULT_NONE; + return 0; + } + return __real_write(fd, pData, length); +} + +static bool createPty(int *pMasterFd, char *pSlaveName) +{ + int slaveFd; + CHECK(openpty(pMasterFd, &slaveFd, pSlaveName, NULL, NULL) == 0); + CHECK(close(slaveFd) == 0); + return true; +} + +static bool testOpenFailures(void) +{ + char slaveName[128]; + int masterFd; + CHECK(createPty(&masterFd, slaveName)); + + gFault = FAULT_MALLOC; + CHECK(uPortUartOpen(slaveName, 115200, false) == NULL); + gFault = FAULT_TCGETATTR; + CHECK(uPortUartOpen(slaveName, 115200, false) == NULL); + gFault = FAULT_TCSETATTR; + CHECK(uPortUartOpen(slaveName, 115200, false) == NULL); + + CHECK(close(masterFd) == 0); + return true; +} + +static bool testReadFailures(void) +{ + static const uint8_t expected = 0x5a; + uint8_t received = 0; + char slaveName[128]; + int masterFd; + CHECK(createPty(&masterFd, slaveName)); + uPortUartHandle_t handle = uPortUartOpen(slaveName, 115200, false); + CHECK(handle != NULL); + + CHECK(write(masterFd, &expected, 1) == 1); + gFault = FAULT_POLL_EINTR; + CHECK(uPortUartRead(handle, &received, 1, 100) == 1); + CHECK(received == expected); + + gFault = FAULT_POLL_ERROR; + CHECK(uPortUartRead(handle, &received, 1, 0) < 0); + + CHECK(write(masterFd, &expected, 1) == 1); + gFault = FAULT_READ_EINTR; + CHECK(uPortUartRead(handle, &received, 1, 100) == 1); + CHECK(received == expected); + + CHECK(write(masterFd, &expected, 1) == 1); + gFault = FAULT_READ_ERROR; + CHECK(uPortUartRead(handle, &received, 1, 100) < 0); + + CHECK(close(masterFd) == 0); + CHECK(uPortUartRead(handle, &received, 1, 0) < 0); + uPortUartClose(handle); + return true; +} + +static bool testWriteFailures(void) +{ + static const uint8_t expected[] = {1, 2, 3, 4}; + uint8_t received[sizeof(expected)]; + char slaveName[128]; + int masterFd; + CHECK(createPty(&masterFd, slaveName)); + uPortUartHandle_t handle = uPortUartOpen(slaveName, 115200, false); + CHECK(handle != NULL); + + gFault = FAULT_WRITE_EINTR; + CHECK(uPortUartWrite(handle, expected, sizeof(expected)) == + (int32_t)sizeof(expected)); + CHECK(read(masterFd, received, sizeof(received)) == (ssize_t)sizeof(received)); + + gFault = FAULT_WRITE_SHORT; + CHECK(uPortUartWrite(handle, expected, sizeof(expected)) == + (int32_t)sizeof(expected)); + CHECK(read(masterFd, received, sizeof(received)) == (ssize_t)sizeof(received)); + + gFault = FAULT_WRITE_ERROR; + CHECK(uPortUartWrite(handle, expected, sizeof(expected)) < 0); + gFault = FAULT_WRITE_ZERO; + CHECK(uPortUartWrite(handle, expected, sizeof(expected)) < 0); + + uPortUartClose(handle); + CHECK(close(masterFd) == 0); + return true; +} + +int main(void) +{ + if (!testOpenFailures() || !testReadFailures() || !testWriteFailures()) { + return EXIT_FAILURE; + } + printf("[ PASSED ] UART fault injection\n"); + return EXIT_SUCCESS; +} From 880d1676735096f9fb515cf1d0c69dd09c3fc7ca Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:05:38 +0200 Subject: [PATCH 21/39] u_port_uart_stm32f4: Disable clock after init failure Disable the UART peripheral clock when HAL initialization fails so a failed open does not leave hardware resources enabled. --- ports/uart/u_port_uart_stm32f4.c | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/uart/u_port_uart_stm32f4.c b/ports/uart/u_port_uart_stm32f4.c index bd24b47..e1ee49d 100644 --- a/ports/uart/u_port_uart_stm32f4.c +++ b/ports/uart/u_port_uart_stm32f4.c @@ -131,6 +131,7 @@ uPortUartHandle_t uPortUartOpen(const char *pDevice, int32_t baudRate, bool useF pHandle->huart.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&pHandle->huart) != HAL_OK) { + U_PORT_UART_CLK_DISABLE(); free(pHandle); return NULL; } From 5996e764e44be99dbb29ae6fb671c7849904aaab Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:06:13 +0200 Subject: [PATCH 22/39] u_port_uart_stm32f4: Check receive startup Fail UART open and release initialized resources when the first interrupt-driven receive operation cannot be armed. --- ports/uart/u_port_uart_stm32f4.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ports/uart/u_port_uart_stm32f4.c b/ports/uart/u_port_uart_stm32f4.c index e1ee49d..27ceaf5 100644 --- a/ports/uart/u_port_uart_stm32f4.c +++ b/ports/uart/u_port_uart_stm32f4.c @@ -68,7 +68,7 @@ static uPortUartHandle *gpUartHandle = NULL; * -------------------------------------------------------------- */ static uint32_t getRxBufferAvailable(uPortUartHandle *pHandle); -static void startRxInterrupt(uPortUartHandle *pHandle); +static bool startRxInterrupt(uPortUartHandle *pHandle); /* ---------------------------------------------------------------- * STATIC FUNCTIONS @@ -86,9 +86,9 @@ static uint32_t getRxBufferAvailable(uPortUartHandle *pHandle) } } -static void startRxInterrupt(uPortUartHandle *pHandle) +static bool startRxInterrupt(uPortUartHandle *pHandle) { - HAL_UART_Receive_IT(&pHandle->huart, &pHandle->rxByte, 1); + return HAL_UART_Receive_IT(&pHandle->huart, &pHandle->rxByte, 1) == HAL_OK; } /* ---------------------------------------------------------------- @@ -146,7 +146,10 @@ uPortUartHandle_t uPortUartOpen(const char *pDevice, int32_t baudRate, bool useF gpUartHandle = pHandle; // Start receiving - startRxInterrupt(pHandle); + if (!startRxInterrupt(pHandle)) { + uPortUartClose((uPortUartHandle_t)pHandle); + return NULL; + } return (uPortUartHandle_t)pHandle; } @@ -274,7 +277,7 @@ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) // If buffer full, drop the byte (could add overflow handling here) // Restart reception - startRxInterrupt(gpUartHandle); + (void)startRxInterrupt(gpUartHandle); } } From b5713ad1f03694b356c962fe2fb1f91264d736f1 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:06:29 +0200 Subject: [PATCH 23/39] u_port_uart_stm32f4: Chunk large writes Split writes at the HAL transfer-size limit instead of truncating the requested length to 16 bits. --- ports/uart/u_port_uart_stm32f4.c | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ports/uart/u_port_uart_stm32f4.c b/ports/uart/u_port_uart_stm32f4.c index 27ceaf5..92f57cb 100644 --- a/ports/uart/u_port_uart_stm32f4.c +++ b/ports/uart/u_port_uart_stm32f4.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -178,7 +179,8 @@ int32_t uPortUartWrite(uPortUartHandle_t handle, const void *pData, size_t length) { - if ((handle == NULL) || (pData == NULL) || (length == 0)) { + if ((handle == NULL) || (pData == NULL) || (length == 0) || + (length > INT32_MAX)) { return -1; } @@ -188,10 +190,19 @@ int32_t uPortUartWrite(uPortUartHandle_t handle, return -1; } - HAL_StatusTypeDef status = HAL_UART_Transmit(&pHandle->huart, (uint8_t *)pData, (uint16_t)length, HAL_MAX_DELAY); - - if (status != HAL_OK) { - return -1; + const uint8_t *pBytes = (const uint8_t *)pData; + size_t bytesWritten = 0; + while (bytesWritten < length) { + size_t bytesRemaining = length - bytesWritten; + uint16_t chunkLength = bytesRemaining > UINT16_MAX ? + UINT16_MAX : (uint16_t)bytesRemaining; + + if (HAL_UART_Transmit(&pHandle->huart, + (uint8_t *)(pBytes + bytesWritten), + chunkLength, HAL_MAX_DELAY) != HAL_OK) { + return -1; + } + bytesWritten += chunkLength; } return (int32_t)length; From 33c4c8649b4a467b734463af9aacf62eda573ae5 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:06:41 +0200 Subject: [PATCH 24/39] u_port_uart_stm32f4: Support blocking reads Treat negative read timeouts as infinite waits as documented, while retaining finite timeout and nonblocking behavior. --- ports/uart/u_port_uart_stm32f4.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ports/uart/u_port_uart_stm32f4.c b/ports/uart/u_port_uart_stm32f4.c index 92f57cb..e081456 100644 --- a/ports/uart/u_port_uart_stm32f4.c +++ b/ports/uart/u_port_uart_stm32f4.c @@ -238,12 +238,13 @@ int32_t uPortUartRead(uPortUartHandle_t handle, return 0; } - // Wait for data if blocking - if (timeoutMs > 0 && available == 0) { + // Wait for data if blocking or using a positive timeout + if ((timeoutMs != 0) && (available == 0)) { uint32_t startTime = HAL_GetTick(); while (available == 0) { available = getRxBufferAvailable(pHandle); - if ((HAL_GetTick() - startTime) >= (uint32_t)timeoutMs) { + if ((timeoutMs > 0) && + ((HAL_GetTick() - startTime) >= (uint32_t)timeoutMs)) { return 0; // Timeout } } From 9f9b7901233aa27b9cd842947e0abd7f8816fda3 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:06:51 +0200 Subject: [PATCH 25/39] u_port_freertos: Harden RX task lifecycle Reject duplicate RX task creation and let the task clear its own handle before deletion. Wait on that handle during teardown to avoid querying a failed or already deleted task. --- ports/os/u_port_freertos.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/ports/os/u_port_freertos.c b/ports/os/u_port_freertos.c index 143030b..1617b2d 100644 --- a/ports/os/u_port_freertos.c +++ b/ports/os/u_port_freertos.c @@ -50,7 +50,7 @@ typedef struct { uCxAtClient_t *pClient; - TaskHandle_t rxTaskHandle; + volatile TaskHandle_t rxTaskHandle; volatile bool terminateRxTask; } uPortRxContext_t; @@ -75,6 +75,7 @@ static void rxTask(void *pArg) } U_CX_LOG_LINE_I(U_CX_LOG_CH_DBG, pCtx->pClient->instance, "RX task terminated"); + pCtx->rxTaskHandle = NULL; vTaskDelete(NULL); } @@ -117,6 +118,12 @@ int32_t uPortMutexTryLock(SemaphoreHandle_t mutex, uint32_t timeoutMs) void uPortBgRxTaskCreate(uCxAtClient_t *pClient) { + TaskHandle_t taskHandle = NULL; + + if (gRxContext.rxTaskHandle != NULL) { + return; + } + memset(&gRxContext, 0, sizeof(gRxContext)); gRxContext.pClient = pClient; gRxContext.terminateRxTask = false; @@ -127,8 +134,9 @@ void uPortBgRxTaskCreate(uCxAtClient_t *pClient) U_PORT_FREERTOS_RX_TASK_STACK_SIZE, &gRxContext, U_PORT_FREERTOS_RX_TASK_PRIORITY, - &gRxContext.rxTaskHandle + &taskHandle ); + gRxContext.rxTaskHandle = taskHandle; } void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) @@ -136,8 +144,8 @@ void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) (void)pClient; gRxContext.terminateRxTask = true; - // Wait for task to terminate (it will delete itself) - while (eTaskGetState(gRxContext.rxTaskHandle) != eDeleted) { + // Wait for the task to release its handle before deleting itself. + while (gRxContext.rxTaskHandle != NULL) { vTaskDelay(pdMS_TO_TICKS(10)); } } From f314210cc6183e69a5ebdb66b94f49f73d6e82c6 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:07:07 +0200 Subject: [PATCH 26/39] test: Add STM32 port coverage Exercise the STM32 UART and FreeRTOS ports with host HAL and RTOS shims, including failure injection, fragmentation, overflow, timeout, large transfers, and repeated task lifecycle tests. Add Invoke tasks for sanitizer, coverage, and Renode integration runs. --- README.md | 11 ++ project.yml | 1 + tasks.py | 44 +++++ test/stm32/CMakeLists.txt | 89 ++++++++++ test/stm32/fake/FreeRTOS.h | 18 +++ test/stm32/fake/fake_freertos.c | 182 +++++++++++++++++++++ test/stm32/fake/fake_freertos.h | 23 +++ test/stm32/fake/fake_stm32_hal.c | 107 ++++++++++++ test/stm32/fake/semphr.h | 10 ++ test/stm32/fake/stm32f4xx_hal.h | 105 ++++++++++++ test/stm32/fake/task.h | 29 ++++ test/stm32/test_freertos_port.c | 160 ++++++++++++++++++ test/stm32/test_stm32_uart.c | 270 +++++++++++++++++++++++++++++++ 13 files changed, 1049 insertions(+) create mode 100644 test/stm32/CMakeLists.txt create mode 100644 test/stm32/fake/FreeRTOS.h create mode 100644 test/stm32/fake/fake_freertos.c create mode 100644 test/stm32/fake/fake_freertos.h create mode 100644 test/stm32/fake/fake_stm32_hal.c create mode 100644 test/stm32/fake/semphr.h create mode 100644 test/stm32/fake/stm32f4xx_hal.h create mode 100644 test/stm32/fake/task.h create mode 100644 test/stm32/test_freertos_port.c create mode 100644 test/stm32/test_stm32_uart.c diff --git a/README.md b/README.md index fbc0d73..bacaf9f 100644 --- a/README.md +++ b/README.md @@ -155,11 +155,22 @@ See [examples/README.md](examples/README.md) for complete build instructions and # Run Ceedling unit tests invoke test.ceedling.run +# Run native Linux/POSIX port tests with coverage +invoke test.posix.run + +# Run host-based STM32 UART and FreeRTOS port tests with coverage +invoke test.stm32.unit + +# Run STM32 HTTP and socket integration tests in Renode +invoke test.stm32.renode + # Run Zephyr Twister tests (automatically sets up west workspace) invoke test.zephyr.run # Clean test artifacts invoke test.ceedling.clean +invoke test.posix.clean +invoke test.stm32.clean invoke test.zephyr.clean invoke test.zephyr.clean-west # Remove west workspace ``` diff --git a/project.yml b/project.yml index de73c80..62eded7 100644 --- a/project.yml +++ b/project.yml @@ -22,6 +22,7 @@ - +:test/** - -:test/support - -:test/posix/** + - -:test/stm32/** :source: - src/** :include: diff --git a/tasks.py b/tasks.py index d836d72..5c9e9f8 100644 --- a/tasks.py +++ b/tasks.py @@ -92,6 +92,36 @@ def posix(c): f"--txt --html-details {os.path.join(build_dir, 'coverage.html')}") +@task +def stm32_unit(c): + """Run host-based STM32 port tests and generate coverage.""" + print("Running STM32 port unit tests...") + build_dir = os.path.join(REPO_ROOT, "build", "stm32-unit") + test_dir = os.path.join(REPO_ROOT, "test", "stm32") + + c.run(f"cmake -S {test_dir} -B {build_dir} " + "-DENABLE_COVERAGE=ON -DENABLE_SANITIZERS=ON") + c.run(f"cmake --build {build_dir} --parallel") + c.run(f"ctest --test-dir {build_dir} --output-on-failure") + c.run(f"gcovr {build_dir} --root {REPO_ROOT} --object-directory {build_dir} " + "--filter 'ports/(os/u_port_freertos|uart/u_port_uart_stm32f4)\\.c' " + f"--txt --html-details {os.path.join(build_dir, 'coverage.html')}") + + +@task(help={ + 'timeout': 'Timeout in seconds for each emulated example', +}) +def stm32_renode(c, timeout=120): + """Run STM32 integration tests in Renode.""" + print("Running STM32 Renode integration tests...") + timeout = int(timeout) + c.run("inv examples.stm32.http.emulate --build " + f"--timeout={timeout}") + c.run("SKIP_DOCKER_BUILD=1 " + "inv examples.stm32.socket.emulate --build " + f"--timeout={timeout}") + + @task def clean_ceedling(c): """Clean Ceedling build artifacts.""" @@ -115,6 +145,13 @@ def clean_posix(c): c.run(f"rm -rf {os.path.join(REPO_ROOT, 'build', 'posix')}") +@task +def clean_stm32_unit(c): + """Clean host-based STM32 test artifacts.""" + print("Cleaning STM32 port unit test artifacts...") + c.run(f"rm -rf {os.path.join(REPO_ROOT, 'build', 'stm32-unit')}") + + @task def clean_west(c): """Clean west workspace.""" @@ -139,11 +176,18 @@ def clean_west(c): posix_ns.add_task(posix, 'run') posix_ns.add_task(clean_posix, 'clean') +# STM32 sub-collection under test +stm32_ns = Collection('stm32') +stm32_ns.add_task(stm32_unit, 'unit') +stm32_ns.add_task(stm32_renode, 'renode') +stm32_ns.add_task(clean_stm32_unit, 'clean') + # Test namespace with sub-collections test_ns = Collection('test') test_ns.add_collection(ceedling_ns) test_ns.add_collection(zephyr_ns) test_ns.add_collection(posix_ns) +test_ns.add_collection(stm32_ns) # Create main namespace ns = Collection() diff --git a/test/stm32/CMakeLists.txt b/test/stm32/CMakeLists.txt new file mode 100644 index 0000000..1aad460 --- /dev/null +++ b/test/stm32/CMakeLists.txt @@ -0,0 +1,89 @@ +cmake_minimum_required(VERSION 3.16) +project(u_connect_client_stm32_tests C) + +include(CTest) +find_package(Threads REQUIRED) + +option(ENABLE_COVERAGE "Build with gcov instrumentation" ON) +option(ENABLE_SANITIZERS "Build with address and undefined sanitizers" ON) + +add_executable(test_stm32_uart + test_stm32_uart.c + fake/fake_stm32_hal.c + ../../ports/uart/u_port_uart_stm32f4.c +) + +target_include_directories(test_stm32_uart PRIVATE + fake + ../../ports + ../../ports/uart +) +target_compile_options(test_stm32_uart PRIVATE + -Wall + -Wextra + -Werror + -pedantic +) +target_link_libraries(test_stm32_uart PRIVATE Threads::Threads) + +if(ENABLE_COVERAGE) + target_compile_options(test_stm32_uart PRIVATE --coverage -O0 -g) + target_link_options(test_stm32_uart PRIVATE --coverage) +endif() + +if(ENABLE_SANITIZERS) + target_compile_options(test_stm32_uart PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + -fno-omit-frame-pointer + ) + target_link_options(test_stm32_uart PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + ) +endif() + +add_test(NAME stm32_uart COMMAND test_stm32_uart) +set_tests_properties(stm32_uart PROPERTIES TIMEOUT 10) + +add_executable(test_freertos_port + test_freertos_port.c + fake/fake_freertos.c + ../../ports/os/u_port_freertos.c + ../../src/u_cx_log.c +) +target_compile_definitions(test_freertos_port PRIVATE U_PORT_FREERTOS) +target_include_directories(test_freertos_port PRIVATE + fake + ../../inc + ../../ports +) +target_compile_options(test_freertos_port PRIVATE + -Wall + -Wextra + -Werror + -pedantic +) +target_link_libraries(test_freertos_port PRIVATE Threads::Threads) +if(ENABLE_COVERAGE) + target_compile_options(test_freertos_port PRIVATE --coverage -O0 -g) + target_link_options(test_freertos_port PRIVATE --coverage) +endif() +if(ENABLE_SANITIZERS) + target_compile_options(test_freertos_port PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + -fno-omit-frame-pointer + ) + target_link_options(test_freertos_port PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + ) +endif() + +add_test(NAME freertos_port COMMAND test_freertos_port) +set_tests_properties(freertos_port PROPERTIES TIMEOUT 10) diff --git a/test/stm32/fake/FreeRTOS.h b/test/stm32/fake/FreeRTOS.h new file mode 100644 index 0000000..8dd5f2b --- /dev/null +++ b/test/stm32/fake/FreeRTOS.h @@ -0,0 +1,18 @@ +#ifndef FREERTOS_H +#define FREERTOS_H + +#include + +typedef int BaseType_t; +typedef uint32_t TickType_t; + +#define pdTRUE 1 +#define pdFALSE 0 +#define pdPASS 1 +#define pdFAIL 0 +#define portMAX_DELAY UINT32_MAX +#define portTICK_PERIOD_MS 1U +#define configMAX_PRIORITIES 10U +#define pdMS_TO_TICKS(milliseconds) ((TickType_t)(milliseconds)) + +#endif diff --git a/test/stm32/fake/fake_freertos.c b/test/stm32/fake/fake_freertos.c new file mode 100644 index 0000000..8cbf0e9 --- /dev/null +++ b/test/stm32/fake/fake_freertos.c @@ -0,0 +1,182 @@ +#include +#include +#include +#include + +#include "fake_freertos.h" +#include "semphr.h" + +struct fakeTask { + pthread_t thread; + atomic_int state; + TaskFunction_t taskFunction; + void *pParameter; + bool threadCreated; +}; + +static struct fakeTask gTask; +static atomic_uint gTickCount; +static atomic_uint gDelayCalls; +static TickType_t gLastDelay; +static TickType_t gLastSemaphoreTimeout; +static BaseType_t gSemaphoreResult; +static BaseType_t gTaskCreateResult; +static uint16_t gTaskStackDepth; +static uint32_t gTaskPriority; +static uint32_t gTaskCreateCalls; +static const char *gpTaskName; +static bool gStateCalledWithNull; + +static void *taskEntry(void *pParameter) +{ + struct fakeTask *pTask = pParameter; + pTask->taskFunction(pTask->pParameter); + atomic_store(&pTask->state, eDeleted); + return NULL; +} + +void fakeRtosReset(void) +{ + memset(&gTask, 0, sizeof(gTask)); + atomic_store(&gTask.state, eDeleted); + atomic_store(&gTickCount, 0); + atomic_store(&gDelayCalls, 0); + gLastDelay = 0; + gLastSemaphoreTimeout = 0; + gSemaphoreResult = pdTRUE; + gTaskCreateResult = pdPASS; + gTaskStackDepth = 0; + gTaskPriority = 0; + gTaskCreateCalls = 0; + gpTaskName = NULL; + gStateCalledWithNull = false; +} + +void fakeRtosSetTickCount(TickType_t ticks) +{ + atomic_store(&gTickCount, ticks); +} + +void fakeRtosSetSemaphoreResult(BaseType_t result) +{ + gSemaphoreResult = result; +} + +void fakeRtosSetTaskCreateResult(BaseType_t result) +{ + gTaskCreateResult = result; +} + +void fakeRtosJoinTask(void) +{ + if (gTask.threadCreated) { + pthread_join(gTask.thread, NULL); + gTask.threadCreated = false; + } +} + +uint32_t fakeRtosGetDelayCalls(void) +{ + return atomic_load(&gDelayCalls); +} + +TickType_t fakeRtosGetLastDelay(void) +{ + return gLastDelay; +} + +TickType_t fakeRtosGetLastSemaphoreTimeout(void) +{ + return gLastSemaphoreTimeout; +} + +uint16_t fakeRtosGetTaskStackDepth(void) +{ + return gTaskStackDepth; +} + +uint32_t fakeRtosGetTaskPriority(void) +{ + return gTaskPriority; +} + +const char *fakeRtosGetTaskName(void) +{ + return gpTaskName; +} + +uint32_t fakeRtosGetTaskCreateCalls(void) +{ + return gTaskCreateCalls; +} + +bool fakeRtosGetStateCalledWithNull(void) +{ + return gStateCalledWithNull; +} + +BaseType_t xTaskCreate(TaskFunction_t taskFunction, + const char *pName, + uint16_t stackDepth, + void *pParameter, + uint32_t priority, + TaskHandle_t *pTaskHandle) +{ + gTaskCreateCalls++; + gTaskStackDepth = stackDepth; + gTaskPriority = priority; + gpTaskName = pName; + if (gTaskCreateResult != pdPASS) { + return gTaskCreateResult; + } + + gTask.taskFunction = taskFunction; + gTask.pParameter = pParameter; + atomic_store(&gTask.state, eRunning); + if (pthread_create(&gTask.thread, NULL, taskEntry, &gTask) != 0) { + atomic_store(&gTask.state, eDeleted); + return pdFAIL; + } + gTask.threadCreated = true; + *pTaskHandle = &gTask; + return pdPASS; +} + +TickType_t xTaskGetTickCount(void) +{ + return atomic_load(&gTickCount); +} + +void vTaskDelay(TickType_t ticks) +{ + struct timespec delay = {.tv_sec = 0, .tv_nsec = 1000000}; + gLastDelay = ticks; + atomic_fetch_add(&gTickCount, ticks); + atomic_fetch_add(&gDelayCalls, 1); + nanosleep(&delay, NULL); +} + +void vTaskDelete(TaskHandle_t taskHandle) +{ + if (taskHandle == NULL) { + atomic_store(&gTask.state, eDeleted); + } else { + atomic_store(&taskHandle->state, eDeleted); + } +} + +eTaskState eTaskGetState(TaskHandle_t taskHandle) +{ + if (taskHandle == NULL) { + gStateCalledWithNull = true; + return eDeleted; + } + return (eTaskState)atomic_load(&taskHandle->state); +} + +BaseType_t xSemaphoreTake(SemaphoreHandle_t semaphore, TickType_t ticks) +{ + (void)semaphore; + gLastSemaphoreTimeout = ticks; + return gSemaphoreResult; +} diff --git a/test/stm32/fake/fake_freertos.h b/test/stm32/fake/fake_freertos.h new file mode 100644 index 0000000..8a0ae3d --- /dev/null +++ b/test/stm32/fake/fake_freertos.h @@ -0,0 +1,23 @@ +#ifndef FAKE_FREERTOS_H +#define FAKE_FREERTOS_H + +#include +#include + +#include "task.h" + +void fakeRtosReset(void); +void fakeRtosSetTickCount(TickType_t ticks); +void fakeRtosSetSemaphoreResult(BaseType_t result); +void fakeRtosSetTaskCreateResult(BaseType_t result); +void fakeRtosJoinTask(void); +uint32_t fakeRtosGetDelayCalls(void); +TickType_t fakeRtosGetLastDelay(void); +TickType_t fakeRtosGetLastSemaphoreTimeout(void); +uint16_t fakeRtosGetTaskStackDepth(void); +uint32_t fakeRtosGetTaskPriority(void); +const char *fakeRtosGetTaskName(void); +uint32_t fakeRtosGetTaskCreateCalls(void); +bool fakeRtosGetStateCalledWithNull(void); + +#endif diff --git a/test/stm32/fake/fake_stm32_hal.c b/test/stm32/fake/fake_stm32_hal.c new file mode 100644 index 0000000..93a95d0 --- /dev/null +++ b/test/stm32/fake/fake_stm32_hal.c @@ -0,0 +1,107 @@ +#include + +#include "stm32f4xx_hal.h" + +USART_TypeDef gFakeUsart2 = {.identifier = 2}; +USART_TypeDef gFakeUsart3 = {.identifier = 3}; +fakeHalState_t gFakeHal; + +extern void HAL_UART_RxCpltCallback(UART_HandleTypeDef *pUart); + +void fakeHalReset(void) +{ + memset(&gFakeHal, 0, sizeof(gFakeHal)); + gFakeHal.initStatus = HAL_OK; + gFakeHal.deinitStatus = HAL_OK; + gFakeHal.transmitStatus = HAL_OK; + gFakeHal.receiveStatus = HAL_OK; +} + +void fakeHalInjectRxByte(uint8_t value) +{ + if ((gFakeHal.pRxUart != NULL) && (gFakeHal.pRxByte != NULL)) { + *gFakeHal.pRxByte = value; + HAL_UART_RxCpltCallback(gFakeHal.pRxUart); + } +} + +HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *pUart) +{ + gFakeHal.initCalls++; + gFakeHal.pLastUart = pUart; + return gFakeHal.initStatus; +} + +HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *pUart) +{ + gFakeHal.deinitCalls++; + gFakeHal.pLastUart = pUart; + return gFakeHal.deinitStatus; +} + +HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *pUart, + uint8_t *pData, + uint16_t length, + uint32_t timeout) +{ + gFakeHal.transmitCalls++; + gFakeHal.pLastUart = pUart; + gFakeHal.pTxData = pData; + gFakeHal.txLength = length; + gFakeHal.txTimeout = timeout; + return gFakeHal.transmitStatus; +} + +HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *pUart, + uint8_t *pData, + uint16_t length) +{ + gFakeHal.receiveCalls++; + gFakeHal.pRxUart = pUart; + gFakeHal.pRxByte = pData; + return length == 1 ? gFakeHal.receiveStatus : HAL_ERROR; +} + +void HAL_UART_IRQHandler(UART_HandleTypeDef *pUart) +{ + gFakeHal.irqHandlerCalls++; + gFakeHal.pLastUart = pUart; +} + +uint32_t HAL_GetTick(void) +{ + uint32_t tick = gFakeHal.tick; + gFakeHal.tick += gFakeHal.tickIncrement; + return tick; +} + +void HAL_NVIC_SetPriority(IRQn_Type irq, uint32_t priority, + uint32_t subPriority) +{ + gFakeHal.nvicSetPriorityCalls++; + gFakeHal.lastIrq = irq; + gFakeHal.lastPriority = priority; + gFakeHal.lastSubPriority = subPriority; +} + +void HAL_NVIC_EnableIRQ(IRQn_Type irq) +{ + gFakeHal.nvicEnableCalls++; + gFakeHal.lastIrq = irq; +} + +void HAL_NVIC_DisableIRQ(IRQn_Type irq) +{ + gFakeHal.nvicDisableCalls++; + gFakeHal.lastIrq = irq; +} + +void fakeHalUartClockEnable(void) +{ + gFakeHal.clockEnableCalls++; +} + +void fakeHalUartClockDisable(void) +{ + gFakeHal.clockDisableCalls++; +} diff --git a/test/stm32/fake/semphr.h b/test/stm32/fake/semphr.h new file mode 100644 index 0000000..c7ea4a5 --- /dev/null +++ b/test/stm32/fake/semphr.h @@ -0,0 +1,10 @@ +#ifndef SEMPHR_H +#define SEMPHR_H + +#include "FreeRTOS.h" + +typedef void *SemaphoreHandle_t; + +BaseType_t xSemaphoreTake(SemaphoreHandle_t semaphore, TickType_t ticks); + +#endif diff --git a/test/stm32/fake/stm32f4xx_hal.h b/test/stm32/fake/stm32f4xx_hal.h new file mode 100644 index 0000000..cf5adb4 --- /dev/null +++ b/test/stm32/fake/stm32f4xx_hal.h @@ -0,0 +1,105 @@ +#ifndef STM32F4XX_HAL_H +#define STM32F4XX_HAL_H + +#include +#include + +typedef struct { + uint32_t identifier; +} USART_TypeDef; + +extern USART_TypeDef gFakeUsart2; +extern USART_TypeDef gFakeUsart3; + +#define USART2 (&gFakeUsart2) +#define USART3 (&gFakeUsart3) +#define USART3_IRQn 39 + +typedef int32_t IRQn_Type; + +typedef enum { + HAL_OK = 0, + HAL_ERROR = 1, + HAL_BUSY = 2, + HAL_TIMEOUT = 3 +} HAL_StatusTypeDef; + +typedef struct { + uint32_t BaudRate; + uint32_t WordLength; + uint32_t StopBits; + uint32_t Parity; + uint32_t Mode; + uint32_t HwFlowCtl; + uint32_t OverSampling; +} UART_InitTypeDef; + +typedef struct { + USART_TypeDef *Instance; + UART_InitTypeDef Init; +} UART_HandleTypeDef; + +#define UART_WORDLENGTH_8B 8U +#define UART_STOPBITS_1 1U +#define UART_PARITY_NONE 0U +#define UART_MODE_TX_RX 3U +#define UART_HWCONTROL_NONE 0U +#define UART_HWCONTROL_RTS_CTS 3U +#define UART_OVERSAMPLING_16 16U +#define HAL_MAX_DELAY UINT32_MAX + +typedef struct { + HAL_StatusTypeDef initStatus; + HAL_StatusTypeDef deinitStatus; + HAL_StatusTypeDef transmitStatus; + HAL_StatusTypeDef receiveStatus; + UART_HandleTypeDef *pLastUart; + UART_HandleTypeDef *pRxUart; + uint8_t *pRxByte; + const uint8_t *pTxData; + uint16_t txLength; + uint32_t txTimeout; + uint32_t tick; + uint32_t tickIncrement; + uint32_t initCalls; + uint32_t deinitCalls; + uint32_t transmitCalls; + uint32_t receiveCalls; + uint32_t irqHandlerCalls; + uint32_t nvicSetPriorityCalls; + uint32_t nvicEnableCalls; + uint32_t nvicDisableCalls; + uint32_t clockEnableCalls; + uint32_t clockDisableCalls; + IRQn_Type lastIrq; + uint32_t lastPriority; + uint32_t lastSubPriority; +} fakeHalState_t; + +extern fakeHalState_t gFakeHal; + +void fakeHalReset(void); +void fakeHalInjectRxByte(uint8_t value); + +HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *pUart); +HAL_StatusTypeDef HAL_UART_DeInit(UART_HandleTypeDef *pUart); +HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *pUart, + uint8_t *pData, + uint16_t length, + uint32_t timeout); +HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *pUart, + uint8_t *pData, + uint16_t length); +void HAL_UART_IRQHandler(UART_HandleTypeDef *pUart); +uint32_t HAL_GetTick(void); +void HAL_NVIC_SetPriority(IRQn_Type irq, uint32_t priority, + uint32_t subPriority); +void HAL_NVIC_EnableIRQ(IRQn_Type irq); +void HAL_NVIC_DisableIRQ(IRQn_Type irq); +void fakeHalUartClockEnable(void); +void fakeHalUartClockDisable(void); + +#define __HAL_RCC_USART3_CLK_ENABLE() fakeHalUartClockEnable() +#define __HAL_RCC_USART3_CLK_DISABLE() fakeHalUartClockDisable() + +#endif diff --git a/test/stm32/fake/task.h b/test/stm32/fake/task.h new file mode 100644 index 0000000..b216a83 --- /dev/null +++ b/test/stm32/fake/task.h @@ -0,0 +1,29 @@ +#ifndef TASK_H +#define TASK_H + +#include "FreeRTOS.h" + +typedef struct fakeTask *TaskHandle_t; +typedef void (*TaskFunction_t)(void *); + +typedef enum { + eRunning, + eReady, + eBlocked, + eSuspended, + eDeleted, + eInvalid +} eTaskState; + +BaseType_t xTaskCreate(TaskFunction_t taskFunction, + const char *pName, + uint16_t stackDepth, + void *pParameter, + uint32_t priority, + TaskHandle_t *pTaskHandle); +TickType_t xTaskGetTickCount(void); +void vTaskDelay(TickType_t ticks); +void vTaskDelete(TaskHandle_t taskHandle); +eTaskState eTaskGetState(TaskHandle_t taskHandle); + +#endif diff --git a/test/stm32/test_freertos_port.c b/test/stm32/test_freertos_port.c new file mode 100644 index 0000000..8b264fb --- /dev/null +++ b/test/stm32/test_freertos_port.c @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "fake_freertos.h" +#include "semphr.h" +#include "u_cx_at_client.h" +#include "u_port.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: check failed: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +static atomic_int gRxCalls; + +static void hostSleepMs(int32_t milliseconds) +{ + struct timespec delay = { + .tv_sec = milliseconds / 1000, + .tv_nsec = (milliseconds % 1000) * 1000000 + }; + nanosleep(&delay, NULL); +} + +int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) +{ + (void)pClient; + atomic_fetch_add(&gRxCalls, 1); + return 0; +} + +static bool testTime(void) +{ + fakeRtosReset(); + fakeRtosSetTickCount(100); + uPortInit(); + CHECK(uPortGetTickTimeMs() == 0); + fakeRtosSetTickCount(137); + CHECK(uPortGetTickTimeMs() == 37); + uPortInit(); + CHECK(uPortGetTickTimeMs() == 37); + uPortDeinit(); + return true; +} + +static bool testMutexTimeouts(void) +{ + SemaphoreHandle_t semaphore = (SemaphoreHandle_t)(uintptr_t)1; + + fakeRtosSetSemaphoreResult(pdTRUE); + CHECK(uPortMutexTryLock(semaphore, 0) == 0); + CHECK(fakeRtosGetLastSemaphoreTimeout() == 0); + CHECK(uPortMutexTryLock(semaphore, UINT32_MAX) == 0); + CHECK(fakeRtosGetLastSemaphoreTimeout() == portMAX_DELAY); + CHECK(uPortMutexTryLock(semaphore, 25) == 0); + CHECK(fakeRtosGetLastSemaphoreTimeout() == pdMS_TO_TICKS(25)); + + fakeRtosSetSemaphoreResult(pdFALSE); + CHECK(uPortMutexTryLock(semaphore, 10) < 0); + return true; +} + +static bool testBackgroundRxTask(void) +{ + uCxAtClient_t client = {0}; + atomic_store(&gRxCalls, 0); + fakeRtosReset(); + + uPortBgRxTaskCreate(&client); + uPortBgRxTaskCreate(&client); + CHECK(fakeRtosGetTaskCreateCalls() == 1); + CHECK(strcmp(fakeRtosGetTaskName(), "ucxRx") == 0); + CHECK(fakeRtosGetTaskStackDepth() == 2048); + CHECK(fakeRtosGetTaskPriority() == configMAX_PRIORITIES - 2U); + + for (int32_t attempt = 0; + (attempt < 100) && (atomic_load(&gRxCalls) == 0); + attempt++) { + hostSleepMs(1); + } + CHECK(atomic_load(&gRxCalls) > 0); + CHECK(fakeRtosGetDelayCalls() > 0); + CHECK(fakeRtosGetLastDelay() == pdMS_TO_TICKS(10)); + + uPortBgRxTaskDestroy(&client); + fakeRtosJoinTask(); + int callsAfterDestroy = atomic_load(&gRxCalls); + hostSleepMs(3); + CHECK(atomic_load(&gRxCalls) == callsAfterDestroy); + return true; +} + +static bool testRepeatedBackgroundRxLifecycle(void) +{ + uCxAtClient_t client = {0}; + + for (int32_t iteration = 0; iteration < 100; iteration++) { + atomic_store(&gRxCalls, 0); + fakeRtosReset(); + uPortBgRxTaskCreate(&client); + for (int32_t attempt = 0; + (attempt < 100) && (atomic_load(&gRxCalls) == 0); + attempt++) { + hostSleepMs(1); + } + CHECK(atomic_load(&gRxCalls) > 0); + uPortBgRxTaskDestroy(&client); + fakeRtosJoinTask(); + } + return true; +} + +static bool testBackgroundRxCreateFailure(void) +{ + uCxAtClient_t client = {0}; + atomic_store(&gRxCalls, 0); + fakeRtosReset(); + fakeRtosSetTaskCreateResult(pdFAIL); + + uPortBgRxTaskCreate(&client); + uPortBgRxTaskDestroy(&client); + CHECK(atomic_load(&gRxCalls) == 0); + CHECK(!fakeRtosGetStateCalledWithNull()); + return true; +} + +int main(void) +{ + static const struct { + const char *pName; + bool (*pTest)(void); + } tests[] = { + {"time", testTime}, + {"mutex timeouts", testMutexTimeouts}, + {"background RX task", testBackgroundRxTask}, + {"repeated background RX lifecycle", testRepeatedBackgroundRxLifecycle}, + {"background RX create failure", testBackgroundRxCreateFailure} + }; + + for (size_t index = 0; index < sizeof(tests) / sizeof(tests[0]); index++) { + printf("[ RUN ] %s\n", tests[index].pName); + if (!tests[index].pTest()) { + printf("[ FAILED ] %s\n", tests[index].pName); + return EXIT_FAILURE; + } + printf("[ OK ] %s\n", tests[index].pName); + } + + printf("[ PASSED ] %zu tests\n", sizeof(tests) / sizeof(tests[0])); + return EXIT_SUCCESS; +} diff --git a/test/stm32/test_stm32_uart.c b/test/stm32/test_stm32_uart.c new file mode 100644 index 0000000..42ed899 --- /dev/null +++ b/test/stm32/test_stm32_uart.c @@ -0,0 +1,270 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "stm32f4xx_hal.h" +#include "u_port_uart.h" + +#define RX_BUFFER_CAPACITY 2047U +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: check failed: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +extern void HAL_UART_RxCpltCallback(UART_HandleTypeDef *pUart); +extern void uPortUart_IRQHandler(void); + +typedef struct { + uPortUartHandle_t handle; + uint8_t value; + int32_t result; +} blockingRead_t; + +static void sleepMs(int32_t milliseconds) +{ + struct timespec delay = { + .tv_sec = milliseconds / 1000, + .tv_nsec = (milliseconds % 1000) * 1000000 + }; + nanosleep(&delay, NULL); +} + +static bool testOpenConfiguration(void) +{ + fakeHalReset(); + uPortUartHandle_t handle = uPortUartOpen(NULL, 921600, false); + CHECK(handle != NULL); + CHECK(gFakeHal.initCalls == 1); + CHECK(gFakeHal.clockEnableCalls == 1); + CHECK(gFakeHal.pLastUart != NULL); + CHECK(gFakeHal.pLastUart->Instance == USART3); + CHECK(gFakeHal.pLastUart->Init.BaudRate == 921600U); + CHECK(gFakeHal.pLastUart->Init.WordLength == UART_WORDLENGTH_8B); + CHECK(gFakeHal.pLastUart->Init.StopBits == UART_STOPBITS_1); + CHECK(gFakeHal.pLastUart->Init.Parity == UART_PARITY_NONE); + CHECK(gFakeHal.pLastUart->Init.Mode == UART_MODE_TX_RX); + CHECK(gFakeHal.pLastUart->Init.HwFlowCtl == UART_HWCONTROL_NONE); + CHECK(gFakeHal.pLastUart->Init.OverSampling == UART_OVERSAMPLING_16); + CHECK(gFakeHal.nvicSetPriorityCalls == 1); + CHECK(gFakeHal.lastIrq == USART3_IRQn); + CHECK(gFakeHal.lastPriority == 6); + CHECK(gFakeHal.nvicEnableCalls == 1); + CHECK(gFakeHal.receiveCalls == 1); + CHECK(uPortUartOpen(NULL, 115200, false) == NULL); + + uPortUartClose(handle); + CHECK(gFakeHal.nvicDisableCalls == 1); + CHECK(gFakeHal.deinitCalls == 1); + CHECK(gFakeHal.clockDisableCalls == 1); + uPortUartClose(NULL); + + fakeHalReset(); + handle = uPortUartOpen(NULL, 115200, true); + CHECK(handle != NULL); + CHECK(gFakeHal.pLastUart->Init.HwFlowCtl == UART_HWCONTROL_RTS_CTS); + uPortUartClose(handle); + return true; +} + +static bool testOpenFailures(void) +{ + fakeHalReset(); + gFakeHal.initStatus = HAL_ERROR; + CHECK(uPortUartOpen(NULL, 115200, false) == NULL); + CHECK(gFakeHal.clockEnableCalls == 1); + CHECK(gFakeHal.clockDisableCalls == 1); + + fakeHalReset(); + gFakeHal.receiveStatus = HAL_ERROR; + CHECK(uPortUartOpen(NULL, 115200, false) == NULL); + CHECK(gFakeHal.deinitCalls == 1); + CHECK(gFakeHal.clockDisableCalls == 1); + + fakeHalReset(); + uPortUartHandle_t handle = uPortUartOpen(NULL, 115200, false); + CHECK(handle != NULL); + uPortUartClose(handle); + return true; +} + +static bool testWrite(void) +{ + static uint8_t payload[] = {0x00, 0x0d, 0x0a, 0x80, 0xff}; + + fakeHalReset(); + uPortUartHandle_t handle = uPortUartOpen(NULL, 115200, false); + CHECK(handle != NULL); + CHECK(uPortUartWrite(NULL, payload, sizeof(payload)) < 0); + CHECK(uPortUartWrite(handle, NULL, sizeof(payload)) < 0); + CHECK(uPortUartWrite(handle, payload, 0) < 0); + + CHECK(uPortUartWrite(handle, payload, sizeof(payload)) == + (int32_t)sizeof(payload)); + CHECK(gFakeHal.transmitCalls == 1); + CHECK(gFakeHal.pTxData == payload); + CHECK(gFakeHal.txLength == sizeof(payload)); + CHECK(gFakeHal.txTimeout == HAL_MAX_DELAY); + + gFakeHal.transmitStatus = HAL_TIMEOUT; + CHECK(uPortUartWrite(handle, payload, sizeof(payload)) < 0); + gFakeHal.transmitStatus = HAL_OK; + + uint8_t *pLargePayload = malloc((size_t)UINT16_MAX + 1U); + CHECK(pLargePayload != NULL); + uint32_t transmitCalls = gFakeHal.transmitCalls; + CHECK(uPortUartWrite(handle, pLargePayload, (size_t)UINT16_MAX + 1U) == + (int32_t)((size_t)UINT16_MAX + 1U)); + CHECK(gFakeHal.transmitCalls == transmitCalls + 2U); + CHECK(gFakeHal.txLength == 1U); + free(pLargePayload); + + uPortUartClose(handle); + return true; +} + +static bool testFragmentedBinaryRead(void) +{ + static const uint8_t payload[] = {0x00, 0x0d, 0x0a, 0x7f, 0x80, 0xff}; + uint8_t received[sizeof(payload)] = {0}; + + fakeHalReset(); + uPortUartHandle_t handle = uPortUartOpen(NULL, 115200, false); + CHECK(handle != NULL); + CHECK(uPortUartRead(NULL, received, sizeof(received), 0) < 0); + CHECK(uPortUartRead(handle, received, 0, 0) < 0); + CHECK(uPortUartRead(handle, received, sizeof(received), 0) == 0); + CHECK(uPortUartRead(handle, NULL, sizeof(received), 0) == 0); + + for (size_t index = 0; index < 2; index++) { + fakeHalInjectRxByte(payload[index]); + } + CHECK(uPortUartRead(handle, received, sizeof(received), 0) == 2); + for (size_t index = 2; index < sizeof(payload); index++) { + fakeHalInjectRxByte(payload[index]); + } + CHECK(uPortUartRead(handle, received + 2, sizeof(received) - 2, 0) == 4); + CHECK(memcmp(received, payload, sizeof(payload)) == 0); + CHECK(gFakeHal.receiveCalls == sizeof(payload) + 1U); + + uPortUartClose(handle); + return true; +} + +static bool testRingWrapAndOverflow(void) +{ + uint8_t received[RX_BUFFER_CAPACITY]; + + fakeHalReset(); + uPortUartHandle_t handle = uPortUartOpen(NULL, 115200, false); + CHECK(handle != NULL); + + for (uint32_t index = 0; index < RX_BUFFER_CAPACITY; index++) { + fakeHalInjectRxByte((uint8_t)index); + } + fakeHalInjectRxByte(0xee); + CHECK(uPortUartRead(handle, received, sizeof(received), 0) == + (int32_t)sizeof(received)); + for (uint32_t index = 0; index < RX_BUFFER_CAPACITY; index++) { + CHECK(received[index] == (uint8_t)index); + } + CHECK(uPortUartRead(handle, received, sizeof(received), 0) == 0); + + for (uint32_t index = 0; index < 100; index++) { + fakeHalInjectRxByte((uint8_t)(index + 1U)); + } + CHECK(uPortUartRead(handle, received, 40, 0) == 40); + CHECK(uPortUartRead(handle, received + 40, 60, 0) == 60); + for (uint32_t index = 0; index < 100; index++) { + CHECK(received[index] == (uint8_t)(index + 1U)); + } + + uPortUartClose(handle); + return true; +} + +static bool testTimeoutAndIrq(void) +{ + uint8_t received; + + fakeHalReset(); + uPortUartHandle_t handle = uPortUartOpen(NULL, 115200, false); + CHECK(handle != NULL); + gFakeHal.tick = UINT32_MAX - 2U; + gFakeHal.tickIncrement = 1; + CHECK(uPortUartRead(handle, &received, 1, 5) == 0); + + UART_HandleTypeDef otherUart = {.Instance = USART2}; + uint32_t receiveCalls = gFakeHal.receiveCalls; + HAL_UART_RxCpltCallback(&otherUart); + CHECK(gFakeHal.receiveCalls == receiveCalls); + + uPortUart_IRQHandler(); + CHECK(gFakeHal.irqHandlerCalls == 1); + CHECK(gFakeHal.pLastUart->Instance == USART3); + uPortUartClose(handle); + uPortUart_IRQHandler(); + CHECK(gFakeHal.irqHandlerCalls == 1); + return true; +} + +static void *blockingRead(void *pParameter) +{ + blockingRead_t *pRead = pParameter; + pRead->result = uPortUartRead(pRead->handle, &pRead->value, 1, -1); + return NULL; +} + +static bool testBlockingRead(void) +{ + fakeHalReset(); + blockingRead_t readParameters = {0}; + readParameters.handle = uPortUartOpen(NULL, 115200, false); + CHECK(readParameters.handle != NULL); + + pthread_t reader; + CHECK(pthread_create(&reader, NULL, blockingRead, &readParameters) == 0); + sleepMs(10); + fakeHalInjectRxByte(0xa5); + CHECK(pthread_join(reader, NULL) == 0); + CHECK(readParameters.result == 1); + CHECK(readParameters.value == 0xa5); + + uPortUartClose(readParameters.handle); + return true; +} + +int main(void) +{ + static const struct { + const char *pName; + bool (*pTest)(void); + } tests[] = { + {"open configuration", testOpenConfiguration}, + {"open failures", testOpenFailures}, + {"write", testWrite}, + {"fragmented binary read", testFragmentedBinaryRead}, + {"ring wrap and overflow", testRingWrapAndOverflow}, + {"timeout and IRQ", testTimeoutAndIrq}, + {"blocking read", testBlockingRead} + }; + + for (size_t index = 0; index < sizeof(tests) / sizeof(tests[0]); index++) { + printf("[ RUN ] %s\n", tests[index].pName); + if (!tests[index].pTest()) { + printf("[ FAILED ] %s\n", tests[index].pName); + return EXIT_FAILURE; + } + printf("[ OK ] %s\n", tests[index].pName); + } + + printf("[ PASSED ] %zu tests\n", sizeof(tests) / sizeof(tests[0])); + return EXIT_SUCCESS; +} From 37288fd223d5ab6c029f6e3a5f28876c5d5b77c1 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:08:16 +0200 Subject: [PATCH 27/39] u_port_no_os: Make timing wrap-safe Track initialization separately from the boot tick and calculate elapsed time with unsigned arithmetic. This avoids epoch resets at tick zero and negative elapsed times when the platform timer wraps. --- ports/os/u_port_no_os.c | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/ports/os/u_port_no_os.c b/ports/os/u_port_no_os.c index 92eb409..c593110 100644 --- a/ports/os/u_port_no_os.c +++ b/ports/os/u_port_no_os.c @@ -40,21 +40,23 @@ * STATIC VARIABLES * -------------------------------------------------------------- */ -static int32_t gBootTime = 0; +static uint32_t gBootTime = 0; +static bool gInitialized = false; /* ---------------------------------------------------------------- * STATIC FUNCTIONS * -------------------------------------------------------------- */ -static int32_t getTickTimeMs(void) +static uint32_t getTickTimeMs(void) { #ifdef _WIN32 - return (int32_t)GetTickCount(); + return (uint32_t)GetTickCount(); #else struct timespec time; clock_gettime(CLOCK_MONOTONIC_RAW, &time); - int64_t timeMs = (time.tv_sec * 1000) + (time.tv_nsec / (1000 * 1000)); - return (int32_t)(timeMs % (1000 * 60 * 60 * 24)); + uint64_t timeMs = ((uint64_t)time.tv_sec * 1000U) + + ((uint64_t)time.tv_nsec / 1000000U); + return (uint32_t)timeMs; #endif } @@ -64,8 +66,9 @@ static int32_t getTickTimeMs(void) void uPortInit(void) { - if (gBootTime == 0) { + if (!gInitialized) { gBootTime = getTickTimeMs(); + gInitialized = true; } } @@ -76,13 +79,17 @@ void uPortDeinit(void) int32_t uPortGetTickTimeMs(void) { - return getTickTimeMs() - gBootTime; + return (int32_t)(getTickTimeMs() - gBootTime); } int32_t uPortSleepMs(int32_t ms) { - int32_t startTime = getTickTimeMs(); - while (getTickTimeMs() - startTime < ms) { + if (ms <= 0) { + return 0; + } + + uint32_t startTime = getTickTimeMs(); + while ((getTickTimeMs() - startTime) < (uint32_t)ms) { // Busy wait } return 0; From a6daf3b225db8ac69e02f391d913b0ee15cad555 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:09:40 +0200 Subject: [PATCH 28/39] test: Add no-OS port coverage Exercise initialization, timer rollover, busy-wait sleep, mutex state, and background RX stubs with a deterministic host clock. Add sanitizer and coverage execution through the test.no-os Invoke namespace and keep the standalone suite out of Ceedling discovery. --- README.md | 4 ++ project.yml | 1 + tasks.py | 29 +++++++++ test/no_os/CMakeLists.txt | 60 +++++++++++++++++ test/no_os/test_no_os_port.c | 121 +++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 test/no_os/CMakeLists.txt create mode 100644 test/no_os/test_no_os_port.c diff --git a/README.md b/README.md index bacaf9f..c883232 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,9 @@ invoke test.ceedling.run # Run native Linux/POSIX port tests with coverage invoke test.posix.run +# Run bare-metal/no-OS port tests with coverage +invoke test.no-os.run + # Run host-based STM32 UART and FreeRTOS port tests with coverage invoke test.stm32.unit @@ -170,6 +173,7 @@ invoke test.zephyr.run # Clean test artifacts invoke test.ceedling.clean invoke test.posix.clean +invoke test.no-os.clean invoke test.stm32.clean invoke test.zephyr.clean invoke test.zephyr.clean-west # Remove west workspace diff --git a/project.yml b/project.yml index 62eded7..9af7803 100644 --- a/project.yml +++ b/project.yml @@ -22,6 +22,7 @@ - +:test/** - -:test/support - -:test/posix/** + - -:test/no_os/** - -:test/stm32/** :source: - src/** diff --git a/tasks.py b/tasks.py index 5c9e9f8..809e900 100644 --- a/tasks.py +++ b/tasks.py @@ -92,6 +92,22 @@ def posix(c): f"--txt --html-details {os.path.join(build_dir, 'coverage.html')}") +@task +def no_os(c): + """Run no-OS port tests and generate coverage.""" + print("Running no-OS port tests...") + build_dir = os.path.join(REPO_ROOT, "build", "no-os") + test_dir = os.path.join(REPO_ROOT, "test", "no_os") + + c.run(f"cmake -S {test_dir} -B {build_dir} " + "-DENABLE_COVERAGE=ON -DENABLE_SANITIZERS=ON") + c.run(f"cmake --build {build_dir} --parallel") + c.run(f"ctest --test-dir {build_dir} --output-on-failure") + c.run(f"gcovr {build_dir} --root {REPO_ROOT} --object-directory {build_dir} " + "--filter 'ports/os/u_port_no_os\\.c' " + f"--txt --html-details {os.path.join(build_dir, 'coverage.html')}") + + @task def stm32_unit(c): """Run host-based STM32 port tests and generate coverage.""" @@ -145,6 +161,13 @@ def clean_posix(c): c.run(f"rm -rf {os.path.join(REPO_ROOT, 'build', 'posix')}") +@task +def clean_no_os(c): + """Clean no-OS test artifacts.""" + print("Cleaning no-OS test artifacts...") + c.run(f"rm -rf {os.path.join(REPO_ROOT, 'build', 'no-os')}") + + @task def clean_stm32_unit(c): """Clean host-based STM32 test artifacts.""" @@ -176,6 +199,11 @@ def clean_west(c): posix_ns.add_task(posix, 'run') posix_ns.add_task(clean_posix, 'clean') +# No-OS sub-collection under test +no_os_ns = Collection('no-os') +no_os_ns.add_task(no_os, 'run') +no_os_ns.add_task(clean_no_os, 'clean') + # STM32 sub-collection under test stm32_ns = Collection('stm32') stm32_ns.add_task(stm32_unit, 'unit') @@ -187,6 +215,7 @@ def clean_west(c): test_ns.add_collection(ceedling_ns) test_ns.add_collection(zephyr_ns) test_ns.add_collection(posix_ns) +test_ns.add_collection(no_os_ns) test_ns.add_collection(stm32_ns) # Create main namespace diff --git a/test/no_os/CMakeLists.txt b/test/no_os/CMakeLists.txt new file mode 100644 index 0000000..b34093e --- /dev/null +++ b/test/no_os/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.16) +project(u_connect_client_no_os_tests C) + +include(CTest) + +option(ENABLE_COVERAGE "Build with gcov instrumentation" ON) +option(ENABLE_SANITIZERS "Build with address and undefined sanitizers" ON) + +add_executable(test_no_os_port + test_no_os_port.c + ../../ports/os/u_port_no_os.c +) +target_compile_definitions(test_no_os_port PRIVATE U_PORT_NO_OS) +target_include_directories(test_no_os_port PRIVATE + ../../inc + ../../ports +) +target_compile_options(test_no_os_port PRIVATE + -Wall + -Wextra + -Werror + -Wconversion + -Wsign-conversion + -Wshadow + -pedantic +) +target_link_options(test_no_os_port PRIVATE -Wl,--wrap=clock_gettime) + +if(ENABLE_COVERAGE) + target_compile_options(test_no_os_port PRIVATE --coverage -O0 -g) + target_link_options(test_no_os_port PRIVATE --coverage) +endif() + +if(ENABLE_SANITIZERS) + target_compile_options(test_no_os_port PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + -fno-omit-frame-pointer + ) + target_link_options(test_no_os_port PRIVATE + -fsanitize=address + -fsanitize=undefined + -fno-sanitize-recover + ) +endif() + +add_test(NAME no_os_boot_at_zero COMMAND test_no_os_port boot-at-zero) +add_test(NAME no_os_day_rollover COMMAND test_no_os_port day-rollover) +add_test(NAME no_os_timer_wrap COMMAND test_no_os_port timer-wrap) +add_test(NAME no_os_sleep COMMAND test_no_os_port sleep) +add_test(NAME no_os_mutex_and_rx COMMAND test_no_os_port mutex-and-rx) +set_tests_properties( + no_os_boot_at_zero + no_os_day_rollover + no_os_timer_wrap + no_os_sleep + no_os_mutex_and_rx + PROPERTIES TIMEOUT 5 +) diff --git a/test/no_os/test_no_os_port.c b/test/no_os/test_no_os_port.c new file mode 100644 index 0000000..5867ad6 --- /dev/null +++ b/test/no_os/test_no_os_port.c @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include + +#include "u_port.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: check failed: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +static uint64_t gTimeMs; +static uint32_t gAdvanceMs; +static uint32_t gClockCalls; + +int __wrap_clock_gettime(clockid_t clockId, struct timespec *pTime) +{ + if (clockId != CLOCK_MONOTONIC_RAW) { + return -1; + } + + pTime->tv_sec = (time_t)(gTimeMs / 1000U); + pTime->tv_nsec = (long)((gTimeMs % 1000U) * 1000000U); + gTimeMs += gAdvanceMs; + gClockCalls++; + return 0; +} + +static bool testBootAtZero(void) +{ + gTimeMs = 0; + uPortInit(); + gTimeMs = 10; + uPortInit(); + CHECK(uPortGetTickTimeMs() == 10); + uPortDeinit(); + return true; +} + +static bool testDayRollover(void) +{ + gTimeMs = 86399990; + uPortInit(); + gTimeMs = 86400005; + CHECK(uPortGetTickTimeMs() == 15); + uPortDeinit(); + return true; +} + +static bool testTimerWrap(void) +{ + gTimeMs = UINT32_MAX - 5U; + uPortInit(); + gTimeMs += 10U; + CHECK(uPortGetTickTimeMs() == 10); + return true; +} + +static bool testSleep(void) +{ + gClockCalls = 0; + CHECK(uPortSleepMs(0) == 0); + CHECK(uPortSleepMs(-1) == 0); + CHECK(gClockCalls == 0); + + gTimeMs = UINT32_MAX - 2U; + gAdvanceMs = 1; + CHECK(uPortSleepMs(5) == 0); + CHECK(gClockCalls == 6); + return true; +} + +static bool testMutexAndRxStubs(void) +{ + bool mutex; + U_CX_MUTEX_CREATE(mutex); + CHECK(!mutex); + CHECK(U_CX_MUTEX_TRY_LOCK(mutex, 0) == 0); + CHECK(mutex); + CHECK(U_CX_MUTEX_TRY_LOCK(mutex, UINT32_MAX) < 0); + CHECK(mutex); + U_CX_MUTEX_UNLOCK(mutex); + CHECK(!mutex); + U_CX_MUTEX_LOCK(mutex); + CHECK(mutex); + U_CX_MUTEX_UNLOCK(mutex); + U_CX_MUTEX_DELETE(mutex); + + uPortBgRxTaskCreate(NULL); + uPortBgRxTaskDestroy(NULL); + return true; +} + +int main(int argc, char **argv) +{ + if (argc != 2) { + return EXIT_FAILURE; + } + + bool passed = false; + if (strcmp(argv[1], "boot-at-zero") == 0) { + passed = testBootAtZero(); + } else if (strcmp(argv[1], "day-rollover") == 0) { + passed = testDayRollover(); + } else if (strcmp(argv[1], "timer-wrap") == 0) { + passed = testTimerWrap(); + } else if (strcmp(argv[1], "sleep") == 0) { + passed = testSleep(); + } else if (strcmp(argv[1], "mutex-and-rx") == 0) { + passed = testMutexAndRxStubs(); + } + + return passed ? EXIT_SUCCESS : EXIT_FAILURE; +} From 4d05d9e60fb1c5cd15845a0228ba63dc24ab63cb Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:28:47 +0200 Subject: [PATCH 29/39] ports: Wake RX workers from UART events Start receive workers only while a client is open and wake them from UART readiness events instead of polling every 10 milliseconds. Keep command reads timed. Event-driven workers drain only bytes that are immediately available. --- ports/README.md | 11 +++--- ports/os/u_port_freertos.c | 25 +++++++++++- ports/os/u_port_no_os.c | 5 +++ ports/os/u_port_posix.c | 18 ++++----- ports/os/u_port_windows.c | 15 +++++--- ports/os/u_port_zephyr.c | 12 ++++-- ports/u_port_uart.h | 14 +++++++ ports/uart/u_port_uart_linux.c | 65 ++++++++++++++++++++++++++++++++ ports/uart/u_port_uart_stm32f4.c | 2 + ports/uart/u_port_uart_windows.c | 45 +++++++++++++++++++++- ports/uart/u_port_uart_zephyr.c | 28 ++++++++++++-- src/u_cx_at_client.c | 51 +++++++++++++++---------- 12 files changed, 242 insertions(+), 49 deletions(-) diff --git a/ports/README.md b/ports/README.md index 39fa4b9..f254cc1 100644 --- a/ports/README.md +++ b/ports/README.md @@ -36,13 +36,14 @@ See also: [STM32F4 port](extra/stm32f4/README.md) for embedded ARM Cortex-M4 wit The port layer optionally implements `uPortBgRxTaskCreate()` and `uPortBgRxTaskDestroy()`: -* **POSIX port**: Creates a pthread that polls `uCxAtClientHandleRx()` every 10ms -* **Windows port**: Creates a Windows thread that polls `uCxAtClientHandleRx()` every 10ms -* **Zephyr port**: Uses work queue that is triggered by UART ISR -* **FreeRTOS port**: Creates a FreeRTOS task that polls `uCxAtClientHandleRx()` every 10ms +* **POSIX port**: Blocks a pthread on UART readiness and wakes it for shutdown +* **Windows port**: Blocks a thread on COM port receive events +* **Zephyr port**: Uses a work queue that is triggered by the UART ISR +* **FreeRTOS port**: Blocks a task on notifications from the UART ISR * **No-OS port**: Stub implementation - user must call `uCxAtClientHandleRx()` manually in their main loop -These functions are called automatically by `uCxAtClientInit()` and `uCxAtClientDeinit()`. +These functions are called automatically by `uCxAtClientOpen()` and +`uCxAtClientClose()`. ## Using an Example Port diff --git a/ports/os/u_port_freertos.c b/ports/os/u_port_freertos.c index 1617b2d..f2a7528 100644 --- a/ports/os/u_port_freertos.c +++ b/ports/os/u_port_freertos.c @@ -32,6 +32,8 @@ #include "u_cx_at_client.h" #include "u_cx_log.h" +extern int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient); + /* ---------------------------------------------------------------- * COMPILE-TIME MACROS * -------------------------------------------------------------- */ @@ -70,8 +72,10 @@ static void rxTask(void *pArg) uPortRxContext_t *pCtx = (uPortRxContext_t *)pArg; while (!pCtx->terminateRxTask) { - vTaskDelay(pdMS_TO_TICKS(10)); // 10ms delay - uCxAtClientHandleRx(pCtx->pClient); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + if (!pCtx->terminateRxTask) { + uCxAtClientHandleRxAvailable(pCtx->pClient); + } } U_CX_LOG_LINE_I(U_CX_LOG_CH_DBG, pCtx->pClient->instance, "RX task terminated"); @@ -137,15 +141,32 @@ void uPortBgRxTaskCreate(uCxAtClient_t *pClient) &taskHandle ); gRxContext.rxTaskHandle = taskHandle; + if (taskHandle != NULL) { + xTaskNotifyGive(taskHandle); + } } void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) { (void)pClient; gRxContext.terminateRxTask = true; + if (gRxContext.rxTaskHandle != NULL) { + xTaskNotifyGive(gRxContext.rxTaskHandle); + } // Wait for the task to release its handle before deleting itself. while (gRxContext.rxTaskHandle != NULL) { vTaskDelay(pdMS_TO_TICKS(10)); } } + +void uPortUartRxSignalFromIsr(void) +{ + BaseType_t higherPriorityTaskWoken = pdFALSE; + + if (gRxContext.rxTaskHandle != NULL) { + vTaskNotifyGiveFromISR(gRxContext.rxTaskHandle, + &higherPriorityTaskWoken); + portYIELD_FROM_ISR(higherPriorityTaskWoken); + } +} diff --git a/ports/os/u_port_no_os.c b/ports/os/u_port_no_os.c index c593110..0d90d10 100644 --- a/ports/os/u_port_no_os.c +++ b/ports/os/u_port_no_os.c @@ -114,3 +114,8 @@ void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) (void)pClient; // Nothing to do } + +void uPortUartRxSignalFromIsr(void) +{ + // No scheduler to wake. The application calls uCxAtClientHandleRx(). +} diff --git a/ports/os/u_port_posix.c b/ports/os/u_port_posix.c index 31f874d..20b3cda 100644 --- a/ports/os/u_port_posix.c +++ b/ports/os/u_port_posix.c @@ -31,6 +31,8 @@ #include "u_cx_at_client.h" #include "u_cx_log.h" +extern int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient); + /* ---------------------------------------------------------------- * TYPES * -------------------------------------------------------------- */ @@ -82,17 +84,15 @@ static void *rxTask(void *pArg) uPortRxContext_t *pCtx = (uPortRxContext_t *)pArg; while (!pCtx->terminateRxTask) { - if (!pCtx->pClient->opened) { - U_CX_PORT_SLEEP_MS(10); - continue; + int32_t waitResult = uPortUartWaitForData(pCtx->pClient->uartHandle, -1); + if (pCtx->terminateRxTask) { + break; } - - if (uCxAtClientHandleRx(pCtx->pClient) < 0) { + if ((waitResult < 0) || + ((waitResult > 0) && + (uCxAtClientHandleRxAvailable(pCtx->pClient) < 0))) { U_CX_PORT_SLEEP_MS(100); - continue; } - - U_CX_PORT_SLEEP_MS(10); } U_CX_LOG_LINE_I(U_CX_LOG_CH_DBG, pCtx->pClient->instance, "RX task terminated"); @@ -162,9 +162,9 @@ void uPortBgRxTaskCreate(uCxAtClient_t *pClient) void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) { - (void)pClient; gRxContext.terminateRxTask = true; if (gRxContext.rxThreadCreated) { + uPortUartWake(pClient->uartHandle); pthread_join(gRxContext.rxThread, NULL); gRxContext.rxThreadCreated = false; } diff --git a/ports/os/u_port_windows.c b/ports/os/u_port_windows.c index 34a8ffb..1c6bc76 100644 --- a/ports/os/u_port_windows.c +++ b/ports/os/u_port_windows.c @@ -26,6 +26,8 @@ #include "u_cx_log.h" #include "u_cx_at_client.h" + +extern int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient); #include "u_port.h" /* ---------------------------------------------------------------- @@ -75,14 +77,16 @@ static DWORD WINAPI rxThread(LPVOID lpParam) "RX thread started"); while (!pCtx->terminateRxTask) { - int32_t result = uCxAtClientHandleRx(pCtx->pClient); + int32_t result = uPortUartWaitForData(pCtx->pClient->uartHandle, -1); + if (pCtx->terminateRxTask) { + break; + } + if (result > 0) { + result = uCxAtClientHandleRxAvailable(pCtx->pClient); + } if (result < 0) { - // Don't exit on error - module may have changed baud rate or rebooted - // Just break the loop and let the thread terminate gracefully break; } - // Sleep for polling interval (10ms) - Sleep(10); } U_CX_LOG_LINE_I(U_CX_LOG_CH_DBG, pCtx->pClient->instance, "RX thread terminated"); @@ -169,6 +173,7 @@ void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) } gRxContext.terminateRxTask = true; + uPortUartWake(pClient->uartHandle); WaitForSingleObject(gRxContext.rxThread, 5000); CloseHandle(gRxContext.rxThread); gRxContext.rxThread = NULL; diff --git a/ports/os/u_port_zephyr.c b/ports/os/u_port_zephyr.c index 4c48a9a..9969867 100644 --- a/ports/os/u_port_zephyr.c +++ b/ports/os/u_port_zephyr.c @@ -28,6 +28,8 @@ #include "u_port.h" #include "u_cx_at_client.h" +extern int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient); + /* ---------------------------------------------------------------- * TYPES * -------------------------------------------------------------- */ @@ -42,6 +44,7 @@ typedef struct { * -------------------------------------------------------------- */ static uPortRxContext_t gRxContext; +static bool gRxWorkerReady; bool gDisableRxWorker = false; // Used during test /* ---------------------------------------------------------------- @@ -51,7 +54,7 @@ bool gDisableRxWorker = false; // Used during test static void rxTask(struct k_work *pItem) { uPortRxContext_t *pCtx = CONTAINER_OF(pItem, uPortRxContext_t, rxWork); - uCxAtClientHandleRx(pCtx->pClient); + uCxAtClientHandleRxAvailable(pCtx->pClient); } /* ---------------------------------------------------------------- @@ -77,17 +80,20 @@ void uPortBgRxTaskCreate(uCxAtClient_t *pClient) { gRxContext.pClient = pClient; k_work_init(&gRxContext.rxWork, rxTask); + gRxWorkerReady = true; + uPortUartRxSignalFromIsr(); } void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) { (void)pClient; + gRxWorkerReady = false; k_work_cancel(&gRxContext.rxWork); } -void uPortRxSignal(void) +void uPortUartRxSignalFromIsr(void) { - if (!k_work_is_pending(&gRxContext.rxWork) && !gDisableRxWorker) { + if (gRxWorkerReady && !gDisableRxWorker) { k_work_submit(&gRxContext.rxWork); } } diff --git a/ports/u_port_uart.h b/ports/u_port_uart.h index c2bce91..82d5d21 100644 --- a/ports/u_port_uart.h +++ b/ports/u_port_uart.h @@ -94,6 +94,20 @@ int32_t uPortUartWrite(uPortUartHandle_t handle, const void *pData, size_t lengt */ int32_t uPortUartRead(uPortUartHandle_t handle, void *pData, size_t length, int32_t timeoutMs); +/** + * @brief Wait until UART RX data is available or the wait is cancelled. + * + * @return 1 when data is available, 0 on timeout or cancellation, negative on + * error. + */ +int32_t uPortUartWaitForData(uPortUartHandle_t handle, int32_t timeoutMs); + +/** Cancel a pending uPortUartWaitForData() call. */ +void uPortUartWake(uPortUartHandle_t handle); + +/** Notify the OS RX worker that UART data arrived from interrupt context. */ +void uPortUartRxSignalFromIsr(void); + #ifdef __cplusplus } #endif diff --git a/ports/uart/u_port_uart_linux.c b/ports/uart/u_port_uart_linux.c index 45f11ee..d1e7509 100644 --- a/ports/uart/u_port_uart_linux.c +++ b/ports/uart/u_port_uart_linux.c @@ -43,6 +43,8 @@ */ typedef struct { int fd; /**< File descriptor for the UART device */ + int wakeReadFd; + int wakeWriteFd; } uPortUartHandle; /* ---------------------------------------------------------------- @@ -63,10 +65,25 @@ uPortUartHandle_t uPortUartOpen(const char *pDevice, int32_t baudRate, bool useF if (pHandle == NULL) { return NULL; } + pHandle->fd = -1; + pHandle->wakeReadFd = -1; + pHandle->wakeWriteFd = -1; + + int wakePipe[2]; + if (pipe(wakePipe) != 0) { + free(pHandle); + return NULL; + } + pHandle->wakeReadFd = wakePipe[0]; + pHandle->wakeWriteFd = wakePipe[1]; + (void)fcntl(pHandle->wakeReadFd, F_SETFL, O_NONBLOCK); + (void)fcntl(pHandle->wakeWriteFd, F_SETFL, O_NONBLOCK); // Open the UART device pHandle->fd = open(pDevice, O_RDWR | O_NOCTTY); if (pHandle->fd < 0) { + close(pHandle->wakeReadFd); + close(pHandle->wakeWriteFd); free(pHandle); return NULL; } @@ -75,6 +92,8 @@ uPortUartHandle_t uPortUartOpen(const char *pDevice, int32_t baudRate, bool useF struct termios tty; if (tcgetattr(pHandle->fd, &tty) != 0) { close(pHandle->fd); + close(pHandle->wakeReadFd); + close(pHandle->wakeWriteFd); free(pHandle); return NULL; } @@ -120,6 +139,8 @@ uPortUartHandle_t uPortUartOpen(const char *pDevice, int32_t baudRate, bool useF break; default: close(pHandle->fd); + close(pHandle->wakeReadFd); + close(pHandle->wakeWriteFd); free(pHandle); return NULL; } @@ -155,6 +176,8 @@ uPortUartHandle_t uPortUartOpen(const char *pDevice, int32_t baudRate, bool useF if (tcsetattr(pHandle->fd, TCSANOW, &tty) != 0) { close(pHandle->fd); + close(pHandle->wakeReadFd); + close(pHandle->wakeWriteFd); free(pHandle); return NULL; } @@ -167,6 +190,8 @@ void uPortUartClose(uPortUartHandle_t handle) if (handle != NULL) { uPortUartHandle *pHandle = (uPortUartHandle *)handle; close(pHandle->fd); + close(pHandle->wakeReadFd); + close(pHandle->wakeWriteFd); free(pHandle); } } @@ -243,3 +268,43 @@ int32_t uPortUartRead(uPortUartHandle_t handle, return (int32_t)bytesRead; } + +int32_t uPortUartWaitForData(uPortUartHandle_t handle, int32_t timeoutMs) +{ + if (handle == NULL) { + return -1; + } + + uPortUartHandle *pHandle = (uPortUartHandle *)handle; + struct pollfd pollFds[] = { + {.fd = pHandle->fd, .events = POLLIN}, + {.fd = pHandle->wakeReadFd, .events = POLLIN} + }; + int result; + do { + result = poll(pollFds, 2, timeoutMs); + } while ((result < 0) && (errno == EINTR)); + + if (result <= 0) { + return result; + } + if ((pollFds[1].revents & POLLIN) != 0) { + uint8_t wakeData[16]; + while (read(pHandle->wakeReadFd, wakeData, sizeof(wakeData)) > 0) { + } + return 0; + } + if ((pollFds[0].revents & POLLIN) != 0) { + return 1; + } + return -1; +} + +void uPortUartWake(uPortUartHandle_t handle) +{ + if (handle != NULL) { + uPortUartHandle *pHandle = (uPortUartHandle *)handle; + uint8_t wakeByte = 1; + (void)write(pHandle->wakeWriteFd, &wakeByte, sizeof(wakeByte)); + } +} diff --git a/ports/uart/u_port_uart_stm32f4.c b/ports/uart/u_port_uart_stm32f4.c index e081456..d641bb1 100644 --- a/ports/uart/u_port_uart_stm32f4.c +++ b/ports/uart/u_port_uart_stm32f4.c @@ -288,6 +288,8 @@ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) } // If buffer full, drop the byte (could add overflow handling here) + uPortUartRxSignalFromIsr(); + // Restart reception (void)startRxInterrupt(gpUartHandle); } diff --git a/ports/uart/u_port_uart_windows.c b/ports/uart/u_port_uart_windows.c index cbe86dd..3d44a26 100644 --- a/ports/uart/u_port_uart_windows.c +++ b/ports/uart/u_port_uart_windows.c @@ -133,6 +133,12 @@ static HANDLE openComPort(const char *pDevName, int baudRate, bool useFlowContro return INVALID_HANDLE_VALUE; } + if (!SetCommMask(hComPort, EV_RXCHAR)) { + U_CX_LOG_LINE(U_CX_LOG_CH_ERROR, "SetCommMask failed"); + CloseHandle(hComPort); + return INVALID_HANDLE_VALUE; + } + // Purge any existing data PurgeComm(hComPort, PURGE_RXCLEAR | PURGE_TXCLEAR); @@ -174,12 +180,24 @@ int32_t uPortUartRead(uPortUartHandle_t handle, void *pData, size_t length, int3 return -1; } - (void)timeoutMs; // Timeout handled by COM port configuration - if (pData == NULL) { return 0; } + if (timeoutMs == 0) { + COMSTAT status; + DWORD errors; + if (!ClearCommError(pHandle->hComPort, &errors, &status)) { + return -1; + } + if (status.cbInQue == 0) { + return 0; + } + if (length > status.cbInQue) { + length = status.cbInQue; + } + } + if (!ReadFile(pHandle->hComPort, pData, (DWORD)length, &dwBytesRead, NULL)) { return -1; } @@ -187,6 +205,29 @@ int32_t uPortUartRead(uPortUartHandle_t handle, void *pData, size_t length, int3 return (int32_t)dwBytesRead; } +int32_t uPortUartWaitForData(uPortUartHandle_t handle, int32_t timeoutMs) +{ + uPortUartHandle *pHandle = (uPortUartHandle *)handle; + DWORD eventMask = 0; + + (void)timeoutMs; + if (pHandle == NULL) { + return -1; + } + if (WaitCommEvent(pHandle->hComPort, &eventMask, NULL)) { + return (eventMask & EV_RXCHAR) != 0 ? 1 : 0; + } + return GetLastError() == ERROR_OPERATION_ABORTED ? 0 : -1; +} + +void uPortUartWake(uPortUartHandle_t handle) +{ + uPortUartHandle *pHandle = (uPortUartHandle *)handle; + if (pHandle != NULL) { + CancelIoEx(pHandle->hComPort, NULL); + } +} + uPortUartHandle_t uPortUartOpen(const char *pDevName, int32_t baudRate, bool useFlowControl) { if (pDevName == NULL) { diff --git a/ports/uart/u_port_uart_zephyr.c b/ports/uart/u_port_uart_zephyr.c index a000a86..0d9fac7 100644 --- a/ports/uart/u_port_uart_zephyr.c +++ b/ports/uart/u_port_uart_zephyr.c @@ -66,9 +66,6 @@ static void uartIsr(const struct device *dev, void *user_data); * FORWARD DECLARATIONS * -------------------------------------------------------------- */ -// Forward declare the RX signal function from OS layer -extern void uPortRxSignal(void); - /* ---------------------------------------------------------------- * STATIC FUNCTIONS * -------------------------------------------------------------- */ @@ -97,7 +94,7 @@ static void uartIsr(const struct device *dev, void *user_data) if (signalRxWorker) { k_sem_give(&pHandle->rxSem); - uPortRxSignal(); + uPortUartRxSignalFromIsr(); } } @@ -232,3 +229,26 @@ int32_t uPortUartRead(uPortUartHandle_t handle, return bytesRead; } + +int32_t uPortUartWaitForData(uPortUartHandle_t handle, int32_t timeoutMs) +{ + if (handle == NULL) { + return -1; + } + + uPortUartHandle *pHandle = (uPortUartHandle *)handle; + if (!ring_buf_is_empty(&pHandle->rxRingBuf)) { + return 1; + } + + k_timeout_t timeout = timeoutMs < 0 ? K_FOREVER : K_MSEC(timeoutMs); + return k_sem_take(&pHandle->rxSem, timeout) == 0 ? 1 : 0; +} + +void uPortUartWake(uPortUartHandle_t handle) +{ + if (handle != NULL) { + uPortUartHandle *pHandle = (uPortUartHandle *)handle; + k_sem_give(&pHandle->rxSem); + } +} diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index a601ed4..014224c 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -282,7 +282,7 @@ static void setupBinaryTransfer(uCxAtClient_t *pClient, int32_t parserRet, uint1 } } -static int32_t handleBinaryRx(uCxAtClient_t *pClient) +static int32_t handleBinaryRx(uCxAtClient_t *pClient, int32_t timeoutMs) { int32_t ret = AT_PARSER_NOP; @@ -294,7 +294,7 @@ static int32_t handleBinaryRx(uCxAtClient_t *pClient) size_t readLen = sizeof(pBinRx->lengthBuf) - pBinRx->rxHeaderCount; readStatus = uPortUartRead(pClient->uartHandle, &pBinRx->lengthBuf[pBinRx->rxHeaderCount], readLen, - pClient->pConfig->timeoutMs); + timeoutMs); CHECK_READ_ERROR(pClient, readStatus); if (readStatus > 0) { pBinRx->rxHeaderCount += (uint8_t)readStatus; @@ -320,7 +320,7 @@ static int32_t handleBinaryRx(uCxAtClient_t *pClient) size_t readLen = U_MIN(remainingBuf, pBinRx->remainingDataBytes); readStatus = uPortUartRead(pClient->uartHandle, &pBinRx->pBuffer[pBinRx->bufferPos], readLen, - pClient->pConfig->timeoutMs); + timeoutMs); CHECK_READ_ERROR(pClient, readStatus); if (readStatus > 0) { pBinRx->bufferPos += (uint16_t)readStatus; @@ -331,7 +331,7 @@ static int32_t handleBinaryRx(uCxAtClient_t *pClient) size_t readLen = U_MIN(sizeof(buf), pBinRx->remainingDataBytes); readStatus = uPortUartRead(pClient->uartHandle, &buf[0], readLen, - pClient->pConfig->timeoutMs); + timeoutMs); CHECK_READ_ERROR(pClient, readStatus); } @@ -380,7 +380,7 @@ static int32_t handleBinaryRx(uCxAtClient_t *pClient) return ret; } -static int32_t handleRxData(uCxAtClient_t *pClient) +static int32_t handleRxData(uCxAtClient_t *pClient, int32_t timeoutMs) { int32_t ret = AT_PARSER_NOP; @@ -392,7 +392,7 @@ static int32_t handleRxData(uCxAtClient_t *pClient) do { char ch; readStatus = uPortUartRead(pClient->uartHandle, &ch, 1, - pClient->pConfig->timeoutMs); + timeoutMs); CHECK_READ_ERROR(pClient, readStatus); if (readStatus != 1) { break; @@ -400,7 +400,7 @@ static int32_t handleRxData(uCxAtClient_t *pClient) ret = parseIncomingChar(pClient, ch); } while (ret == AT_PARSER_NOP); } else { - ret = handleBinaryRx(pClient); + ret = handleBinaryRx(pClient, timeoutMs); } if (ret == AT_PARSER_START_BINARY) { @@ -451,7 +451,7 @@ static void cmdBeginF(uCxAtClient_t *pClient, const char *pCmd, const char *pPar static int32_t cmdEnd(uCxAtClient_t *pClient) { while (pClient->status == NO_STATUS) { - handleRxData(pClient); + handleRxData(pClient, pClient->pConfig->timeoutMs); int32_t now = U_CX_PORT_GET_TIME_MS(); if ((now - pClient->cmdStartTime) > pClient->cmdTimeout) { @@ -506,15 +506,13 @@ void uCxAtClientInit(const uCxAtClientConfig_t *pConfig, uCxAtClient_t *pClient) uCxAtUrcQueueInit(&pClient->urcQueue, pConfig->pUrcBuffer, pConfig->urcBufferLen); #endif U_CX_MUTEX_CREATE(pClient->cmdMutex); - - // Start background RX task (if implemented by port layer) - uPortBgRxTaskCreate(pClient); } void uCxAtClientDeinit(uCxAtClient_t *pClient) { - // Stop background RX task (if implemented by port layer) - uPortBgRxTaskDestroy(pClient); + if (pClient->opened) { + uCxAtClientClose(pClient); + } #if U_CX_USE_URC_QUEUE == 1 uCxAtUrcQueueDeInit(&pClient->urcQueue); @@ -546,18 +544,23 @@ int32_t uCxAtClientOpen(uCxAtClient_t *pClient, int32_t baudRate, bool flowContr } U_CX_MUTEX_UNLOCK(pClient->cmdMutex); + if (ret == 0) { + uPortBgRxTaskCreate(pClient); + } return ret; } void uCxAtClientClose(uCxAtClient_t *pClient) { - U_CX_MUTEX_LOCK(pClient->cmdMutex); - if (!pClient->opened) { - U_CX_MUTEX_UNLOCK(pClient->cmdMutex); return; } + // Stop RX before releasing its UART handle. + uPortBgRxTaskDestroy(pClient); + + U_CX_MUTEX_LOCK(pClient->cmdMutex); + pClient->opened = false; resetReceiveState(pClient); if (pClient->uartHandle != NULL) { @@ -758,7 +761,7 @@ char *uCxAtClientCmdGetRspParamLine(uCxAtClient_t *pClient, const char *pExpecte } while (pClient->status == NO_STATUS) { - if (handleRxData(pClient) == AT_PARSER_GOT_RSP) { + if (handleRxData(pClient, pClient->pConfig->timeoutMs) == AT_PARSER_GOT_RSP) { pRet = pClient->pRspParams; break; } @@ -795,13 +798,13 @@ int32_t uCxAtClientCmdEnd(uCxAtClient_t *pClient) return cmdEnd(pClient); } -int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) +static int32_t handleRx(uCxAtClient_t *pClient, int32_t timeoutMs) { int32_t ret = 0; U_CX_MUTEX_LOCK(pClient->cmdMutex); if (pClient->opened && !pClient->executingCmd) { - int32_t parserRet = handleRxData(pClient); + int32_t parserRet = handleRxData(pClient, timeoutMs); if (parserRet == AT_PARSER_ERROR && pClient->status == U_CX_ERROR_IO) { ret = pClient->lastIoError; } @@ -818,6 +821,16 @@ int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) return ret; } +int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) +{ + return handleRx(pClient, pClient->pConfig->timeoutMs); +} + +int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient) +{ + return handleRx(pClient, 0); +} + int32_t uCxAtClientGetLastIoError(uCxAtClient_t *pClient) { return pClient->lastIoError; From b46a7e7b3cdb097f8c15bf84c5639283f6fe50cf Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:28:51 +0200 Subject: [PATCH 30/39] test: Cover event-driven RX workers Verify idle workers remain blocked, UART input wakes them, and shutdown cancels pending waits across POSIX and FreeRTOS host tests. --- test/no_os/test_no_os_port.c | 1 + test/posix/test_posix_port.c | 46 +++++++++++++++++++-------------- test/stm32/fake/FreeRTOS.h | 1 + test/stm32/fake/fake_freertos.c | 40 ++++++++++++++++++++++++++++ test/stm32/fake/task.h | 4 +++ test/stm32/test_freertos_port.c | 15 ++++++++--- test/stm32/test_stm32_uart.c | 10 +++++++ 7 files changed, 94 insertions(+), 23 deletions(-) diff --git a/test/no_os/test_no_os_port.c b/test/no_os/test_no_os_port.c index 5867ad6..10665ba 100644 --- a/test/no_os/test_no_os_port.c +++ b/test/no_os/test_no_os_port.c @@ -94,6 +94,7 @@ static bool testMutexAndRxStubs(void) U_CX_MUTEX_DELETE(mutex); uPortBgRxTaskCreate(NULL); + uPortUartRxSignalFromIsr(); uPortBgRxTaskDestroy(NULL); return true; } diff --git a/test/posix/test_posix_port.c b/test/posix/test_posix_port.c index 9adabe6..3c64734 100644 --- a/test/posix/test_posix_port.c +++ b/test/posix/test_posix_port.c @@ -258,7 +258,7 @@ static bool testReadTimeouts(void) return true; } -int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) +int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient) { if (!pClient->opened) { return -1; @@ -267,8 +267,12 @@ int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) atomic_fetch_sub(&gRxFailures, 1); return -1; } - atomic_fetch_add(&gRxCalls, 1); - return 0; + uint8_t byte; + int32_t result = uPortUartRead(pClient->uartHandle, &byte, 1, 0); + if (result > 0) { + atomic_fetch_add(&gRxCalls, 1); + } + return result < 0 ? result : 0; } static bool testPosixOsPrimitives(void) @@ -290,36 +294,38 @@ static bool testPosixOsPrimitives(void) return true; } -static bool testBackgroundRxCloseAndReopen(void) +static bool testBackgroundRxWakeAndStop(void) { uCxAtClient_t client = {0}; + int masterFd; + char slaveName[128]; + uint8_t byte = 0x55; atomic_store(&gRxCalls, 0); atomic_store(&gRxFailures, 0); - client.opened = false; + CHECK(createPty(&masterFd, slaveName, sizeof(slaveName))); + client.uartHandle = uPortUartOpen(slaveName, 115200, false); + CHECK(client.uartHandle != NULL); + client.opened = true; uPortBgRxTaskCreate(&client); CHECK(sleepMs(30)); CHECK(atomic_load(&gRxCalls) == 0); - client.opened = true; - CHECK(sleepMs(40)); - CHECK(atomic_load(&gRxCalls) > 0); - - client.opened = false; + CHECK(write(masterFd, &byte, sizeof(byte)) == (ssize_t)sizeof(byte)); CHECK(sleepMs(30)); - int callsWhileClosed = atomic_load(&gRxCalls); - CHECK(sleepMs(30)); - CHECK(atomic_load(&gRxCalls) == callsWhileClosed); + CHECK(atomic_load(&gRxCalls) == 1); - client.opened = true; - CHECK(sleepMs(40)); - CHECK(atomic_load(&gRxCalls) > callsWhileClosed); + CHECK(sleepMs(30)); + CHECK(atomic_load(&gRxCalls) == 1); - int callsBeforeError = atomic_load(&gRxCalls); atomic_store(&gRxFailures, 1); - CHECK(sleepMs(150)); - CHECK(atomic_load(&gRxCalls) > callsBeforeError); + CHECK(write(masterFd, &byte, sizeof(byte)) == (ssize_t)sizeof(byte)); + CHECK(sleepMs(30)); + CHECK(atomic_load(&gRxFailures) == 0); + uPortBgRxTaskDestroy(&client); + uPortUartClose(client.uartHandle); + CHECK(close(masterFd) == 0); return true; } @@ -350,7 +356,7 @@ int main(void) {"repeated open and close", testRepeatedOpenClose}, {"read timeouts", testReadTimeouts}, {"OS primitives", testPosixOsPrimitives}, - {"background RX close and reopen", testBackgroundRxCloseAndReopen}, + {"background RX wake and stop", testBackgroundRxWakeAndStop}, {"background RX create failure", testBackgroundRxCreateFailure} }; diff --git a/test/stm32/fake/FreeRTOS.h b/test/stm32/fake/FreeRTOS.h index 8dd5f2b..8ac9426 100644 --- a/test/stm32/fake/FreeRTOS.h +++ b/test/stm32/fake/FreeRTOS.h @@ -14,5 +14,6 @@ typedef uint32_t TickType_t; #define portTICK_PERIOD_MS 1U #define configMAX_PRIORITIES 10U #define pdMS_TO_TICKS(milliseconds) ((TickType_t)(milliseconds)) +#define portYIELD_FROM_ISR(taskWoken) ((void)(taskWoken)) #endif diff --git a/test/stm32/fake/fake_freertos.c b/test/stm32/fake/fake_freertos.c index 8cbf0e9..2b686dd 100644 --- a/test/stm32/fake/fake_freertos.c +++ b/test/stm32/fake/fake_freertos.c @@ -26,6 +26,9 @@ static uint32_t gTaskPriority; static uint32_t gTaskCreateCalls; static const char *gpTaskName; static bool gStateCalledWithNull; +static pthread_mutex_t gNotifyMutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t gNotifyCondition = PTHREAD_COND_INITIALIZER; +static uint32_t gNotificationCount; static void *taskEntry(void *pParameter) { @@ -50,6 +53,9 @@ void fakeRtosReset(void) gTaskCreateCalls = 0; gpTaskName = NULL; gStateCalledWithNull = false; + pthread_mutex_lock(&gNotifyMutex); + gNotificationCount = 0; + pthread_mutex_unlock(&gNotifyMutex); } void fakeRtosSetTickCount(TickType_t ticks) @@ -174,6 +180,40 @@ eTaskState eTaskGetState(TaskHandle_t taskHandle) return (eTaskState)atomic_load(&taskHandle->state); } +uint32_t ulTaskNotifyTake(BaseType_t clearOnExit, TickType_t ticksToWait) +{ + (void)ticksToWait; + pthread_mutex_lock(&gNotifyMutex); + while (gNotificationCount == 0) { + pthread_cond_wait(&gNotifyCondition, &gNotifyMutex); + } + uint32_t notificationCount = gNotificationCount; + if (clearOnExit == pdTRUE) { + gNotificationCount = 0; + } else { + gNotificationCount--; + } + pthread_mutex_unlock(&gNotifyMutex); + return notificationCount; +} + +BaseType_t xTaskNotifyGive(TaskHandle_t taskHandle) +{ + (void)taskHandle; + pthread_mutex_lock(&gNotifyMutex); + gNotificationCount++; + pthread_cond_signal(&gNotifyCondition); + pthread_mutex_unlock(&gNotifyMutex); + return pdPASS; +} + +void vTaskNotifyGiveFromISR(TaskHandle_t taskHandle, + BaseType_t *pHigherPriorityTaskWoken) +{ + xTaskNotifyGive(taskHandle); + *pHigherPriorityTaskWoken = pdTRUE; +} + BaseType_t xSemaphoreTake(SemaphoreHandle_t semaphore, TickType_t ticks) { (void)semaphore; diff --git a/test/stm32/fake/task.h b/test/stm32/fake/task.h index b216a83..7c43347 100644 --- a/test/stm32/fake/task.h +++ b/test/stm32/fake/task.h @@ -25,5 +25,9 @@ TickType_t xTaskGetTickCount(void); void vTaskDelay(TickType_t ticks); void vTaskDelete(TaskHandle_t taskHandle); eTaskState eTaskGetState(TaskHandle_t taskHandle); +uint32_t ulTaskNotifyTake(BaseType_t clearOnExit, TickType_t ticksToWait); +BaseType_t xTaskNotifyGive(TaskHandle_t taskHandle); +void vTaskNotifyGiveFromISR(TaskHandle_t taskHandle, + BaseType_t *pHigherPriorityTaskWoken); #endif diff --git a/test/stm32/test_freertos_port.c b/test/stm32/test_freertos_port.c index 8b264fb..7ce24ce 100644 --- a/test/stm32/test_freertos_port.c +++ b/test/stm32/test_freertos_port.c @@ -31,7 +31,7 @@ static void hostSleepMs(int32_t milliseconds) nanosleep(&delay, NULL); } -int32_t uCxAtClientHandleRx(uCxAtClient_t *pClient) +int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient) { (void)pClient; atomic_fetch_add(&gRxCalls, 1); @@ -88,8 +88,17 @@ static bool testBackgroundRxTask(void) hostSleepMs(1); } CHECK(atomic_load(&gRxCalls) > 0); - CHECK(fakeRtosGetDelayCalls() > 0); - CHECK(fakeRtosGetLastDelay() == pdMS_TO_TICKS(10)); + int callsWhileIdle = atomic_load(&gRxCalls); + hostSleepMs(3); + CHECK(atomic_load(&gRxCalls) == callsWhileIdle); + + uPortUartRxSignalFromIsr(); + for (int32_t attempt = 0; + (attempt < 100) && (atomic_load(&gRxCalls) == callsWhileIdle); + attempt++) { + hostSleepMs(1); + } + CHECK(atomic_load(&gRxCalls) == callsWhileIdle + 1); uPortBgRxTaskDestroy(&client); fakeRtosJoinTask(); diff --git a/test/stm32/test_stm32_uart.c b/test/stm32/test_stm32_uart.c index 42ed899..f28906e 100644 --- a/test/stm32/test_stm32_uart.c +++ b/test/stm32/test_stm32_uart.c @@ -28,6 +28,13 @@ typedef struct { int32_t result; } blockingRead_t; +static uint32_t gRxSignals; + +void uPortUartRxSignalFromIsr(void) +{ + gRxSignals++; +} + static void sleepMs(int32_t milliseconds) { struct timespec delay = { @@ -40,6 +47,7 @@ static void sleepMs(int32_t milliseconds) static bool testOpenConfiguration(void) { fakeHalReset(); + gRxSignals = 0; uPortUartHandle_t handle = uPortUartOpen(NULL, 921600, false); CHECK(handle != NULL); CHECK(gFakeHal.initCalls == 1); @@ -132,6 +140,7 @@ static bool testWrite(void) static bool testFragmentedBinaryRead(void) { + gRxSignals = 0; static const uint8_t payload[] = {0x00, 0x0d, 0x0a, 0x7f, 0x80, 0xff}; uint8_t received[sizeof(payload)] = {0}; @@ -146,6 +155,7 @@ static bool testFragmentedBinaryRead(void) for (size_t index = 0; index < 2; index++) { fakeHalInjectRxByte(payload[index]); } + CHECK(gRxSignals == 2); CHECK(uPortUartRead(handle, received, sizeof(received), 0) == 2); for (size_t index = 2; index < sizeof(payload); index++) { fakeHalInjectRxByte(payload[index]); From d7f013359162332e88c98f1589bd44600baaba65 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:44:02 +0200 Subject: [PATCH 31/39] u_cx_at_urc_queue: Use circular storage Keep variable-length URC entries in circular storage so dequeue can advance a cursor instead of moving every remaining entry. Preserve contiguous binary payload buffers and align entry headers for targets that require halfword-aligned access. --- inc/u_cx_at_urc_queue.h | 8 ++- src/u_cx_at_urc_queue.c | 136 ++++++++++++++++++++++++++++++++-------- 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/inc/u_cx_at_urc_queue.h b/inc/u_cx_at_urc_queue.h index 2aaa32b..0638e39 100644 --- a/inc/u_cx_at_urc_queue.h +++ b/inc/u_cx_at_urc_queue.h @@ -49,7 +49,13 @@ typedef struct { typedef struct uCxAtUrcQueue { uint8_t *pBuffer; size_t bufferLen; - size_t bufferPos; + size_t startPos; + size_t readPos; + size_t writePos; + size_t usedBytes; + size_t wrapPos; + size_t enqueueCapacity; + bool isWrapped; U_CX_MUTEX_HANDLE queueMutex; U_CX_MUTEX_HANDLE dequeueMutex; uUrcEntry_t *pEnqueueEntry; diff --git a/src/u_cx_at_urc_queue.c b/src/u_cx_at_urc_queue.c index 851fd7e..5d9e4bf 100644 --- a/src/u_cx_at_urc_queue.c +++ b/src/u_cx_at_urc_queue.c @@ -21,6 +21,7 @@ #include "stddef.h" #include "stdint.h" #include "stdbool.h" +#include "limits.h" #include "string.h" #include "u_cx_log.h" @@ -32,8 +33,8 @@ * COMPILE-TIME MACROS * -------------------------------------------------------------- */ -#define U_URC_ENTRY_SIZE(ENTRY) \ - ((size_t)(ENTRY->strLineLen + 1 + ENTRY->payloadSize)) +#define U_URC_ALIGN_SIZE(SIZE) \ + (((SIZE) + sizeof(uint16_t) - 1) & ~(sizeof(uint16_t) - 1)) /* ---------------------------------------------------------------- * TYPES @@ -51,9 +52,44 @@ * STATIC FUNCTIONS * -------------------------------------------------------------- */ -static inline size_t getUnusedBuf(uCxAtUrcQueue_t *pUrcQueue) +static size_t getEnqueueOffset(uCxAtUrcQueue_t *pUrcQueue, + size_t requiredSize) { - return pUrcQueue->bufferLen - pUrcQueue->bufferPos; + if (requiredSize > pUrcQueue->bufferLen - pUrcQueue->usedBytes) { + return SIZE_MAX; + } + + if (pUrcQueue->usedBytes == 0) { + pUrcQueue->readPos = pUrcQueue->startPos; + pUrcQueue->writePos = pUrcQueue->startPos; + pUrcQueue->wrapPos = pUrcQueue->bufferLen; + pUrcQueue->isWrapped = false; + } + + if (pUrcQueue->isWrapped) { + return requiredSize <= pUrcQueue->readPos - pUrcQueue->writePos ? + pUrcQueue->writePos : SIZE_MAX; + } + + size_t tailSpace = pUrcQueue->bufferLen - pUrcQueue->writePos; + size_t headSpace = pUrcQueue->readPos - pUrcQueue->startPos; + if ((headSpace > tailSpace) && (requiredSize <= headSpace)) { + pUrcQueue->wrapPos = pUrcQueue->writePos; + pUrcQueue->isWrapped = true; + pUrcQueue->writePos = pUrcQueue->startPos; + return pUrcQueue->startPos; + } + if (requiredSize <= tailSpace) { + return pUrcQueue->writePos; + } + if (requiredSize <= headSpace) { + pUrcQueue->wrapPos = pUrcQueue->writePos; + pUrcQueue->isWrapped = true; + pUrcQueue->writePos = pUrcQueue->startPos; + return pUrcQueue->startPos; + } + + return SIZE_MAX; } /* ---------------------------------------------------------------- @@ -67,6 +103,11 @@ void uCxAtUrcQueueInit(uCxAtUrcQueue_t *pUrcQueue, void *pBuffer, size_t bufferL U_CX_MUTEX_CREATE(pUrcQueue->dequeueMutex); pUrcQueue->pBuffer = pBuffer; pUrcQueue->bufferLen = bufferLen; + pUrcQueue->startPos = (size_t)((uintptr_t)pBuffer & + (sizeof(uint16_t) - 1)); + pUrcQueue->readPos = pUrcQueue->startPos; + pUrcQueue->writePos = pUrcQueue->startPos; + pUrcQueue->wrapPos = bufferLen; } void uCxAtUrcQueueDeInit(uCxAtUrcQueue_t *pUrcQueue) @@ -82,14 +123,24 @@ bool uCxAtUrcQueueEnqueueBegin(uCxAtUrcQueue_t *pUrcQueue, const char *pUrcLine, U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); U_CX_AT_PORT_ASSERT(pUrcQueue->pEnqueueEntry == NULL); - int32_t availableDataSpace = (int32_t)(getUnusedBuf(pUrcQueue) - sizeof(uUrcEntry_t)); - if (availableDataSpace >= (int32_t)urcLineLen + 1) { - uUrcEntry_t *pEntry = (uUrcEntry_t *)&pUrcQueue->pBuffer[pUrcQueue->bufferPos]; + size_t entryOffset = SIZE_MAX; + size_t entrySize = 0; + if ((urcLineLen <= UINT16_MAX) && + (urcLineLen <= SIZE_MAX - sizeof(uUrcEntry_t) - 1)) { + entrySize = sizeof(uUrcEntry_t) + urcLineLen + 1; + entryOffset = getEnqueueOffset(pUrcQueue, entrySize); + } + if (entryOffset != SIZE_MAX) { + uUrcEntry_t *pEntry = (uUrcEntry_t *)&pUrcQueue->pBuffer[entryOffset]; memcpy(&pEntry->data[0], pUrcLine, urcLineLen); pEntry->data[urcLineLen] = 0; // Add null term pEntry->strLineLen = (uint16_t)urcLineLen; pEntry->payloadSize = 0; - pUrcQueue->bufferPos += sizeof(uUrcEntry_t) + urcLineLen + 1; + pUrcQueue->writePos += entrySize; + pUrcQueue->usedBytes += entrySize; + size_t segmentEnd = pUrcQueue->writePos < pUrcQueue->readPos ? + pUrcQueue->readPos : pUrcQueue->bufferLen; + pUrcQueue->enqueueCapacity = segmentEnd - pUrcQueue->writePos; pUrcQueue->pEnqueueEntry = pEntry; ret = true; } else { @@ -107,18 +158,29 @@ uint16_t uCxAtUrcQueueEnqueueGetPayloadPtr(uCxAtUrcQueue_t *pUrcQueue, uint8_t * uUrcEntry_t *pEntry = pUrcQueue->pEnqueueEntry; *ppPayload = &pEntry->data[pEntry->strLineLen + 1]; - return (uint16_t)getUnusedBuf(pUrcQueue); + size_t payloadCapacity = pUrcQueue->enqueueCapacity; + return payloadCapacity > UINT16_MAX ? + UINT16_MAX : (uint16_t)payloadCapacity; } void uCxAtUrcQueueEnqueueEnd(uCxAtUrcQueue_t *pUrcQueue, uint16_t payloadSize) { U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); U_CX_AT_PORT_ASSERT(pUrcQueue->pEnqueueEntry); - U_CX_AT_PORT_ASSERT(getUnusedBuf(pUrcQueue) >= payloadSize); uUrcEntry_t *pEntry = pUrcQueue->pEnqueueEntry; + size_t entryPrefixSize = sizeof(uUrcEntry_t) + pEntry->strLineLen + 1; + size_t entrySize = U_URC_ALIGN_SIZE(entryPrefixSize + payloadSize); + size_t additionalSize = entrySize - entryPrefixSize; + if (additionalSize > pUrcQueue->enqueueCapacity) { + additionalSize = payloadSize; + } + U_CX_AT_PORT_ASSERT(pUrcQueue->enqueueCapacity >= additionalSize); + pEntry->payloadSize = payloadSize; - pUrcQueue->bufferPos += payloadSize; + pUrcQueue->writePos += additionalSize; + pUrcQueue->usedBytes += additionalSize; + pUrcQueue->enqueueCapacity = 0; pUrcQueue->pEnqueueEntry = NULL; U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); } @@ -128,8 +190,19 @@ void uCxAtUrcQueueEnqueueAbort(uCxAtUrcQueue_t *pUrcQueue) U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); U_CX_AT_PORT_ASSERT(pUrcQueue->pEnqueueEntry); - uint8_t *pEntry = (uint8_t *)pUrcQueue->pEnqueueEntry; - pUrcQueue->bufferPos = (size_t)(pEntry - pUrcQueue->pBuffer); + uUrcEntry_t *pEntry = pUrcQueue->pEnqueueEntry; + size_t entryOffset = (size_t)((uint8_t *)pEntry - pUrcQueue->pBuffer); + size_t entrySize = sizeof(uUrcEntry_t) + pEntry->strLineLen + 1; + pUrcQueue->usedBytes -= entrySize; + if ((entryOffset == pUrcQueue->startPos) && + pUrcQueue->isWrapped) { + pUrcQueue->writePos = pUrcQueue->wrapPos; + pUrcQueue->wrapPos = pUrcQueue->bufferLen; + pUrcQueue->isWrapped = false; + } else { + pUrcQueue->writePos = entryOffset; + } + pUrcQueue->enqueueCapacity = 0; pUrcQueue->pEnqueueEntry = NULL; U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); } @@ -142,9 +215,9 @@ uUrcEntry_t *uCxAtUrcQueueDequeueBegin(uCxAtUrcQueue_t *pUrcQueue) U_CX_AT_PORT_ASSERT(pUrcQueue->pDequeueEntry == NULL); U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); - if ((pUrcQueue->bufferPos > 0) && + if ((pUrcQueue->usedBytes > 0) && (pUrcQueue->pEnqueueEntry == NULL)) { - pEntry = (uUrcEntry_t *)&pUrcQueue->pBuffer[0]; + pEntry = (uUrcEntry_t *)&pUrcQueue->pBuffer[pUrcQueue->readPos]; } U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); @@ -160,21 +233,32 @@ uUrcEntry_t *uCxAtUrcQueueDequeueBegin(uCxAtUrcQueue_t *pUrcQueue) void uCxAtUrcQueueDequeueEnd(uCxAtUrcQueue_t *pUrcQueue, uUrcEntry_t *pEntry) { - int32_t remainingData; U_CX_AT_PORT_ASSERT(pUrcQueue->pDequeueEntry != NULL); U_CX_AT_PORT_ASSERT(pUrcQueue->pDequeueEntry == pEntry); U_CX_MUTEX_LOCK(pUrcQueue->queueMutex); - size_t totEntrySize = sizeof(uUrcEntry_t) + U_URC_ENTRY_SIZE(pEntry); - remainingData = (int32_t)pUrcQueue->bufferPos - (int32_t)totEntrySize; - if (remainingData > 0) { - // Move the remaining data to start of buffer - // TODO: Replace with ring buffer to improve performance - memmove(pUrcQueue->pBuffer, &pUrcQueue->pBuffer[totEntrySize], (size_t)remainingData); - pUrcQueue->bufferPos -= totEntrySize; - } else { - // This was the only entry so no need to move anything - pUrcQueue->bufferPos = 0; + size_t entryOffset = (size_t)((uint8_t *)pEntry - pUrcQueue->pBuffer); + size_t rawEntrySize = sizeof(uUrcEntry_t) + pEntry->strLineLen + 1 + + pEntry->payloadSize; + size_t totEntrySize = U_URC_ALIGN_SIZE(rawEntrySize); + size_t segmentEnd = pUrcQueue->isWrapped ? + pUrcQueue->wrapPos : pUrcQueue->bufferLen; + if (entryOffset + totEntrySize > segmentEnd) { + totEntrySize = rawEntrySize; + } + pUrcQueue->readPos += totEntrySize; + pUrcQueue->usedBytes -= totEntrySize; + if (pUrcQueue->isWrapped && + (pUrcQueue->readPos == pUrcQueue->wrapPos)) { + pUrcQueue->readPos = pUrcQueue->startPos; + pUrcQueue->wrapPos = pUrcQueue->bufferLen; + pUrcQueue->isWrapped = false; + } + if (pUrcQueue->usedBytes == 0) { + pUrcQueue->readPos = pUrcQueue->startPos; + pUrcQueue->writePos = pUrcQueue->startPos; + pUrcQueue->wrapPos = pUrcQueue->bufferLen; + pUrcQueue->isWrapped = false; } U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); From 7521ebdeca42f41b8d9756b3bb978d6a94a3d6ff Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:44:04 +0200 Subject: [PATCH 32/39] test: Cover circular URC queue reuse Cover wrapped FIFO ordering, binary payload continuity, abort recovery, exact-capacity entries, alignment, sustained reuse, and full queues. --- test/test_u_cx_at_urc_queue.c | 217 ++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/test/test_u_cx_at_urc_queue.c b/test/test_u_cx_at_urc_queue.c index bab9152..f2227a4 100644 --- a/test/test_u_cx_at_urc_queue.c +++ b/test/test_u_cx_at_urc_queue.c @@ -16,6 +16,7 @@ #include #include +#include #include "unity.h" #include "u_cx_at_urc_queue.h" @@ -165,3 +166,219 @@ void test_uCxAtUrcQueueDequeueBegin_calledTwiceWithNonEmptyQueue_expectNull(void TEST_ASSERT_NOT_NULL(uCxAtUrcQueueDequeueBegin(&gQueue)); TEST_ASSERT_NULL(uCxAtUrcQueueDequeueBegin(&gQueue)); } + +void test_queueWrap_preservesOrderPayloadAndEntryAddresses(void) +{ + char lines[4][151]; + for (size_t index = 0; index < 4; index++) { + memset(lines[index], (int)('A' + index), sizeof(lines[index]) - 1); + lines[index][sizeof(lines[index]) - 1] = 0; + } + + uUrcEntry_t *pEntries[4]; + for (size_t index = 0; index < 3; index++) { + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, lines[index], + strlen(lines[index]))); + pEntries[index] = gQueue.pEnqueueEntry; + uCxAtUrcQueueEnqueueEnd(&gQueue, 0); + } + + uUrcEntry_t *pEntry = uCxAtUrcQueueDequeueBegin(&gQueue); + TEST_ASSERT_EQUAL_PTR(pEntries[0], pEntry); + uCxAtUrcQueueDequeueEnd(&gQueue, pEntry); + + uint8_t *pPayload; + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, lines[3], 100)); + pEntries[3] = gQueue.pEnqueueEntry; + TEST_ASSERT_EQUAL_PTR(gBuffer, pEntries[3]); + TEST_ASSERT_GREATER_OR_EQUAL(32, + uCxAtUrcQueueEnqueueGetPayloadPtr(&gQueue, + &pPayload)); + memset(pPayload, 0xa5, 32); + uCxAtUrcQueueEnqueueEnd(&gQueue, 32); + + for (size_t index = 1; index < 4; index++) { + pEntry = uCxAtUrcQueueDequeueBegin(&gQueue); + TEST_ASSERT_EQUAL_PTR(pEntries[index], pEntry); + TEST_ASSERT_EQUAL_CHAR('A' + (int)index, pEntry->data[0]); + if (index == 3) { + TEST_ASSERT_EQUAL(32, pEntry->payloadSize); + TEST_ASSERT_EACH_EQUAL_HEX8(0xa5, + &pEntry->data[pEntry->strLineLen + 1], + pEntry->payloadSize); + } + uCxAtUrcQueueDequeueEnd(&gQueue, pEntry); + } + TEST_ASSERT_NULL(uCxAtUrcQueueDequeueBegin(&gQueue)); +} + +void test_abortedWrappedEnqueue_restoresTailSpace(void) +{ + char longLine[151]; + char shortLine[40]; + memset(longLine, 'A', sizeof(longLine) - 1); + longLine[sizeof(longLine) - 1] = 0; + memset(shortLine, 'B', sizeof(shortLine) - 1); + shortLine[sizeof(shortLine) - 1] = 0; + + for (size_t index = 0; index < 3; index++) { + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, longLine, + strlen(longLine))); + uCxAtUrcQueueEnqueueEnd(&gQueue, 0); + } + uUrcEntry_t *pEntry = uCxAtUrcQueueDequeueBegin(&gQueue); + uCxAtUrcQueueDequeueEnd(&gQueue, pEntry); + + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, longLine, 100)); + TEST_ASSERT_EQUAL_PTR(gBuffer, gQueue.pEnqueueEntry); + uCxAtUrcQueueEnqueueAbort(&gQueue); + TEST_ASSERT_EQUAL(468, gQueue.writePos); + TEST_ASSERT_EQUAL(sizeof(gBuffer), gQueue.wrapPos); + + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, shortLine, + strlen(shortLine))); + TEST_ASSERT_EQUAL_PTR(gBuffer, gQueue.pEnqueueEntry); + uCxAtUrcQueueEnqueueEnd(&gQueue, 0); +} + +void test_payloadUsingReportedCapacity_fitsWithAlignment(void) +{ + uint16_t alignedStorage[256]; + uint8_t *pOddBuffer = (uint8_t *)alignedStorage; + uCxAtUrcQueue_t oddQueue; + char line[] = "A"; + uint8_t *pPayload; + + uCxAtUrcQueueInit(&oddQueue, pOddBuffer, sizeof(alignedStorage) - 1); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&oddQueue, line, strlen(line))); + uint16_t capacity = uCxAtUrcQueueEnqueueGetPayloadPtr(&oddQueue, &pPayload); + TEST_ASSERT_EQUAL(505, capacity); + memset(pPayload, 0x5a, capacity); + uCxAtUrcQueueEnqueueEnd(&oddQueue, capacity); + + uUrcEntry_t *pEntry = uCxAtUrcQueueDequeueBegin(&oddQueue); + TEST_ASSERT_NOT_NULL(pEntry); + TEST_ASSERT_EQUAL(capacity, pEntry->payloadSize); + TEST_ASSERT_EACH_EQUAL_HEX8(0x5a, + &pEntry->data[pEntry->strLineLen + 1], + capacity); + uCxAtUrcQueueDequeueEnd(&oddQueue, pEntry); + TEST_ASSERT_NULL(uCxAtUrcQueueDequeueBegin(&oddQueue)); + uCxAtUrcQueueDeInit(&oddQueue); +} + +void test_unpaddedTailEntry_wrapsToHead(void) +{ + uint16_t alignedStorage[32]; + uCxAtUrcQueue_t queue; + uint8_t *pPayload; + + uCxAtUrcQueueInit(&queue, alignedStorage, sizeof(alignedStorage) - 1); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, "first", 5)); + uCxAtUrcQueueEnqueueEnd(&queue, 0); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, "tail", 4)); + TEST_ASSERT_EQUAL(44, + uCxAtUrcQueueEnqueueGetPayloadPtr(&queue, &pPayload)); + memset(pPayload, 0x5a, 44); + uCxAtUrcQueueEnqueueEnd(&queue, 44); + + uUrcEntry_t *pEntry = uCxAtUrcQueueDequeueBegin(&queue); + TEST_ASSERT_EQUAL_STRING("first", pEntry->data); + uCxAtUrcQueueDequeueEnd(&queue, pEntry); + + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, "head", 4)); + uCxAtUrcQueueEnqueueEnd(&queue, 0); + + pEntry = uCxAtUrcQueueDequeueBegin(&queue); + TEST_ASSERT_EQUAL_STRING("tail", pEntry->data); + TEST_ASSERT_EQUAL(44, pEntry->payloadSize); + uCxAtUrcQueueDequeueEnd(&queue, pEntry); + pEntry = uCxAtUrcQueueDequeueBegin(&queue); + TEST_ASSERT_EQUAL_STRING("head", pEntry->data); + uCxAtUrcQueueDequeueEnd(&queue, pEntry); + TEST_ASSERT_NULL(uCxAtUrcQueueDequeueBegin(&queue)); + uCxAtUrcQueueDeInit(&queue); +} + +void test_wrappedHeadFilledToReadPosition_rejectsOverwrite(void) +{ + uint16_t alignedStorage[32]; + uCxAtUrcQueue_t queue; + char tailLine[49]; + + memset(tailLine, 'T', sizeof(tailLine) - 1); + tailLine[sizeof(tailLine) - 1] = 0; + + uCxAtUrcQueueInit(&queue, alignedStorage, sizeof(alignedStorage)); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, "first", 5)); + uCxAtUrcQueueEnqueueEnd(&queue, 0); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, tailLine, 48)); + uCxAtUrcQueueEnqueueEnd(&queue, 0); + + uUrcEntry_t *pEntry = uCxAtUrcQueueDequeueBegin(&queue); + TEST_ASSERT_EQUAL_STRING("first", pEntry->data); + uCxAtUrcQueueDequeueEnd(&queue, pEntry); + + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, "head", 5)); + uCxAtUrcQueueEnqueueEnd(&queue, 0); + TEST_ASSERT_EQUAL(queue.readPos, queue.writePos); + TEST_ASSERT_FALSE(uCxAtUrcQueueEnqueueBegin(&queue, "x", 1)); + + pEntry = uCxAtUrcQueueDequeueBegin(&queue); + TEST_ASSERT_EQUAL_STRING(tailLine, pEntry->data); + uCxAtUrcQueueDequeueEnd(&queue, pEntry); + pEntry = uCxAtUrcQueueDequeueBegin(&queue); + TEST_ASSERT_EQUAL_STRING("head", pEntry->data); + uCxAtUrcQueueDequeueEnd(&queue, pEntry); + uCxAtUrcQueueDeInit(&queue); +} + +void test_unalignedBuffer_alignsEntryHeader(void) +{ + uint8_t storage[64]; + uint8_t *pBuffer = ((uintptr_t)storage & 1U) == 0 ? + &storage[1] : &storage[0]; + uCxAtUrcQueue_t queue; + + TEST_ASSERT_EQUAL(1, (uintptr_t)pBuffer & 1U); + uCxAtUrcQueueInit(&queue, pBuffer, sizeof(storage) - 1); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, "ABC", 3)); + uCxAtUrcQueueEnqueueEnd(&queue, 0); + + uUrcEntry_t *pEntry = uCxAtUrcQueueDequeueBegin(&queue); + TEST_ASSERT_EQUAL(0, (uintptr_t)pEntry & 1U); + TEST_ASSERT_EQUAL_STRING("ABC", pEntry->data); + uCxAtUrcQueueDequeueEnd(&queue, pEntry); + uCxAtUrcQueueDeInit(&queue); +} + +void test_repeatedWrapCycles_preserveFifoOrder(void) +{ + char line[32]; + + for (int value = 0; value < 8; value++) { + int length = snprintf(line, sizeof(line), "URC-%04d-abcdefghijkl", value); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, line, + (size_t)length)); + uCxAtUrcQueueEnqueueEnd(&gQueue, 0); + } + + for (int value = 0; value < 1000; value++) { + uUrcEntry_t *pEntry = uCxAtUrcQueueDequeueBegin(&gQueue); + TEST_ASSERT_NOT_NULL(pEntry); + int length = snprintf(line, sizeof(line), "URC-%04d-abcdefghijkl", value); + TEST_ASSERT_EQUAL_STRING(line, pEntry->data); + uCxAtUrcQueueDequeueEnd(&gQueue, pEntry); + + length = snprintf(line, sizeof(line), "URC-%04d-abcdefghijkl", value + 8); + TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&gQueue, line, + (size_t)length)); + uCxAtUrcQueueEnqueueEnd(&gQueue, 0); + } +} + +void test_unrepresentableLineLength_isRejected(void) +{ + TEST_ASSERT_FALSE(uCxAtUrcQueueEnqueueBegin(&gQueue, "A", + (size_t)UINT16_MAX + 1)); +} From e1933e4c0fcaf2f499282b29aef7eb28ca695ada Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 11:55:38 +0200 Subject: [PATCH 33/39] ci: Run port tests in pull requests Add POSIX, no-OS, and STM32 host tests to a native test matrix. Run both Renode examples and align Ceedling with the project version. Keep the regular Zephyr suite in pull requests while excluding its long-running stress configuration. --- .github/workflows/ceedling.yaml | 4 +-- .github/workflows/port-tests.yaml | 50 ++++++++++++++++++++++++++ .github/workflows/stm32-build-test.yml | 11 ++++++ .github/workflows/zephyr.yaml | 2 +- 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/port-tests.yaml diff --git a/.github/workflows/ceedling.yaml b/.github/workflows/ceedling.yaml index 76dcd02..0c4a5b8 100644 --- a/.github/workflows/ceedling.yaml +++ b/.github/workflows/ceedling.yaml @@ -23,9 +23,9 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: '3.2' - name: Install Ceedling - run: gem install ceedling -v 0.31.1 + run: gem install ceedling --version 1.0.1 --no-document - name: Install PyInvoke run: pip install invoke - name: Run Unit Tests diff --git a/.github/workflows/port-tests.yaml b/.github/workflows/port-tests.yaml new file mode 100644 index 0000000..c086b58 --- /dev/null +++ b/.github/workflows/port-tests.yaml @@ -0,0 +1,50 @@ +name: 'Native Port Unit Tests' + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + run-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: POSIX + task: test.posix.run + build-directory: posix + - name: no-OS + task: test.no-os.run + build-directory: no-os + - name: STM32 host + task: test.stm32.unit + build-directory: stm32-unit + + name: ${{ matrix.name }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install test tools + run: pip install gcovr==8.6 invoke + + - name: Run tests + run: invoke ${{ matrix.task }} + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.build-directory }}-coverage + path: build/${{ matrix.build-directory }}/coverage.html + if-no-files-found: ignore \ No newline at end of file diff --git a/.github/workflows/stm32-build-test.yml b/.github/workflows/stm32-build-test.yml index 0b152b7..a9321db 100644 --- a/.github/workflows/stm32-build-test.yml +++ b/.github/workflows/stm32-build-test.yml @@ -108,6 +108,15 @@ jobs: inv stm32.http.emulate --timeout 120 2>&1 | tee build_stm32/renode_output.txt timeout-minutes: 5 + - name: Run socket Renode emulation test + working-directory: ./examples + env: + SKIP_DOCKER_BUILD: '1' + run: | + set -o pipefail + inv stm32.socket.emulate --build --timeout 120 2>&1 | tee build_stm32/renode_socket_output.txt + timeout-minutes: 5 + - name: Upload build artifacts uses: actions/upload-artifact@v4 if: always() @@ -119,6 +128,7 @@ jobs: examples/bin/http_example_stm32.elf examples/bin/http_example_stm32.map examples/build_stm32/renode_output.txt + examples/build_stm32/renode_socket_output.txt retention-days: 30 - name: Upload test results @@ -128,4 +138,5 @@ jobs: name: test-results path: | examples/build_stm32/renode_output.txt + examples/build_stm32/renode_socket_output.txt retention-days: 7 diff --git a/.github/workflows/zephyr.yaml b/.github/workflows/zephyr.yaml index 4764e5d..51041fc 100644 --- a/.github/workflows/zephyr.yaml +++ b/.github/workflows/zephyr.yaml @@ -38,7 +38,7 @@ jobs: working-directory: u-connectClient/ shell: bash run: | - west twister -T zephyr/ --integration + west twister -T zephyr/ --integration --exclude-tag stress - name: Process Test results uses: dorny/test-reporter@v1 From e01e535fb5f579792974fe565bbe65e47723a4e9 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 12:10:31 +0200 Subject: [PATCH 34/39] u_cx_at_urc_queue: Bound wrapped payload capacity Use explicit wrap state when calculating contiguous payload space so an entry ending at the read cursor cannot overwrite unread queue data. --- src/u_cx_at_urc_queue.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/u_cx_at_urc_queue.c b/src/u_cx_at_urc_queue.c index 5d9e4bf..4b7b11d 100644 --- a/src/u_cx_at_urc_queue.c +++ b/src/u_cx_at_urc_queue.c @@ -138,7 +138,7 @@ bool uCxAtUrcQueueEnqueueBegin(uCxAtUrcQueue_t *pUrcQueue, const char *pUrcLine, pEntry->payloadSize = 0; pUrcQueue->writePos += entrySize; pUrcQueue->usedBytes += entrySize; - size_t segmentEnd = pUrcQueue->writePos < pUrcQueue->readPos ? + size_t segmentEnd = pUrcQueue->isWrapped ? pUrcQueue->readPos : pUrcQueue->bufferLen; pUrcQueue->enqueueCapacity = segmentEnd - pUrcQueue->writePos; pUrcQueue->pEnqueueEntry = pEntry; From 53408c58241fc14b17416cc377bdcc0b524df192 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 12:10:34 +0200 Subject: [PATCH 35/39] u_cx_at_client: Synchronize binary RX state check Snapshot binary receive state under the command mutex before deciding whether queued URCs can be delivered outside the lock. --- src/u_cx_at_client.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index 014224c..fc5bd62 100644 --- a/src/u_cx_at_client.c +++ b/src/u_cx_at_client.c @@ -801,6 +801,9 @@ int32_t uCxAtClientCmdEnd(uCxAtClient_t *pClient) static int32_t handleRx(uCxAtClient_t *pClient, int32_t timeoutMs) { int32_t ret = 0; +#if U_CX_USE_URC_QUEUE == 1 + bool processQueuedUrcs; +#endif U_CX_MUTEX_LOCK(pClient->cmdMutex); if (pClient->opened && !pClient->executingCmd) { @@ -810,10 +813,13 @@ static int32_t handleRx(uCxAtClient_t *pClient, int32_t timeoutMs) } } +#if U_CX_USE_URC_QUEUE == 1 + processQueuedUrcs = !pClient->isBinaryRx; +#endif U_CX_MUTEX_UNLOCK(pClient->cmdMutex); #if U_CX_USE_URC_QUEUE == 1 - if (!pClient->isBinaryRx) { + if (processQueuedUrcs) { processUrcs(pClient); } #endif From 009c9f5e12041afc3b8fba0ec67bb99c6aa0f923 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 12:10:38 +0200 Subject: [PATCH 36/39] test: Cover full wrapped payload capacity Verify that an entry filling the wrapped head reports no remaining binary payload space. --- test/test_u_cx_at_urc_queue.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_u_cx_at_urc_queue.c b/test/test_u_cx_at_urc_queue.c index f2227a4..96e92d7 100644 --- a/test/test_u_cx_at_urc_queue.c +++ b/test/test_u_cx_at_urc_queue.c @@ -305,6 +305,7 @@ void test_wrappedHeadFilledToReadPosition_rejectsOverwrite(void) uint16_t alignedStorage[32]; uCxAtUrcQueue_t queue; char tailLine[49]; + uint8_t *pPayload; memset(tailLine, 'T', sizeof(tailLine) - 1); tailLine[sizeof(tailLine) - 1] = 0; @@ -320,6 +321,8 @@ void test_wrappedHeadFilledToReadPosition_rejectsOverwrite(void) uCxAtUrcQueueDequeueEnd(&queue, pEntry); TEST_ASSERT_TRUE(uCxAtUrcQueueEnqueueBegin(&queue, "head", 5)); + TEST_ASSERT_EQUAL(0, + uCxAtUrcQueueEnqueueGetPayloadPtr(&queue, &pPayload)); uCxAtUrcQueueEnqueueEnd(&queue, 0); TEST_ASSERT_EQUAL(queue.readPos, queue.writePos); TEST_ASSERT_FALSE(uCxAtUrcQueueEnqueueBegin(&queue, "x", 1)); From a85d7f4f1bc2e3ba3b7218ce83d378b2d51a947f Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 12:32:15 +0200 Subject: [PATCH 37/39] test: Add Windows port coverage Compile the real Windows OS and UART ports against deterministic Win32 fakes. Cover COM configuration, I/O faults, event waits, and RX worker lifecycle. --- test/windows/CMakeLists.txt | 48 +++++++ test/windows/fake/devguid.h | 3 + test/windows/fake/fake_windows.c | 229 +++++++++++++++++++++++++++++++ test/windows/fake/fake_windows.h | 44 ++++++ test/windows/fake/regstr.h | 3 + test/windows/fake/setupapi.h | 3 + test/windows/fake/windows.h | 118 ++++++++++++++++ test/windows/test_windows_port.c | 105 ++++++++++++++ test/windows/test_windows_uart.c | 153 +++++++++++++++++++++ 9 files changed, 706 insertions(+) create mode 100644 test/windows/CMakeLists.txt create mode 100644 test/windows/fake/devguid.h create mode 100644 test/windows/fake/fake_windows.c create mode 100644 test/windows/fake/fake_windows.h create mode 100644 test/windows/fake/regstr.h create mode 100644 test/windows/fake/setupapi.h create mode 100644 test/windows/fake/windows.h create mode 100644 test/windows/test_windows_port.c create mode 100644 test/windows/test_windows_uart.c diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt new file mode 100644 index 0000000..317f93c --- /dev/null +++ b/test/windows/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.16) +project(u_connect_client_windows_tests C) + +include(CTest) + +function(configure_windows_test target) + target_compile_definitions(${target} PRIVATE + U_PORT_WINDOWS + U_CX_LOG_AT=0 + U_CX_LOG_DEBUG=0 + U_CX_LOG_WARNING=0 + U_CX_LOG_ERROR=0 + U_CX_LOG_PRINT_TIME=0 + ) + target_include_directories(${target} PRIVATE + fake + ../../inc + ../../ports + ) + if(MSVC) + target_compile_options(${target} PRIVATE /W4 /WX) + else() + target_compile_options(${target} PRIVATE + -Wall + -Wextra + -Werror + -pedantic + ) + endif() +endfunction() + +add_executable(test_windows_uart + test_windows_uart.c + fake/fake_windows.c + ../../ports/uart/u_port_uart_windows.c + ../../src/u_cx_log.c +) +configure_windows_test(test_windows_uart) +add_test(NAME windows_uart COMMAND test_windows_uart) + +add_executable(test_windows_port + test_windows_port.c + fake/fake_windows.c + ../../ports/os/u_port_windows.c + ../../src/u_cx_log.c +) +configure_windows_test(test_windows_port) +add_test(NAME windows_port COMMAND test_windows_port) \ No newline at end of file diff --git a/test/windows/fake/devguid.h b/test/windows/fake/devguid.h new file mode 100644 index 0000000..3793f92 --- /dev/null +++ b/test/windows/fake/devguid.h @@ -0,0 +1,3 @@ +#ifndef FAKE_DEVGUID_H +#define FAKE_DEVGUID_H +#endif \ No newline at end of file diff --git a/test/windows/fake/fake_windows.c b/test/windows/fake/fake_windows.c new file mode 100644 index 0000000..17197bf --- /dev/null +++ b/test/windows/fake/fake_windows.c @@ -0,0 +1,229 @@ +#include + +#include "fake_windows.h" + +static int gComHandle; +static int gThreadHandle; +static int gSemaphoreHandle; +static LPTHREAD_START_ROUTINE gpThreadStart; +static LPVOID gpThreadParameter; + +fakeWindowsState_t gFakeWindows; + +void fakeWindowsReset(void) +{ + memset(&gFakeWindows, 0, sizeof(gFakeWindows)); + gFakeWindows.createFileResult = TRUE; + gFakeWindows.setupCommResult = TRUE; + gFakeWindows.getCommStateResult = TRUE; + gFakeWindows.setCommStateResult = TRUE; + gFakeWindows.setCommTimeoutsResult = TRUE; + gFakeWindows.setCommMaskResult = TRUE; + gFakeWindows.readResult = TRUE; + gFakeWindows.writeResult = TRUE; + gFakeWindows.clearCommErrorResult = TRUE; + gFakeWindows.createThreadResult = TRUE; + gFakeWindows.waitResult = WAIT_OBJECT_0; + gFakeWindows.waitEventMask = EV_RXCHAR; + gFakeWindows.performanceFrequency = 1000; + gFakeWindows.dcb.DCBlength = sizeof(DCB); + gpThreadStart = NULL; + gpThreadParameter = NULL; +} + +void fakeWindowsRunThread(void) +{ + if (gpThreadStart != NULL) { + LPTHREAD_START_ROUTINE pStart = gpThreadStart; + gpThreadStart = NULL; + pStart(gpThreadParameter); + } +} + +HANDLE CreateFileA(LPCSTR pName, DWORD access, DWORD shareMode, + LPSECURITY_ATTRIBUTES pSecurity, DWORD creation, + DWORD flags, HANDLE templateFile) +{ + (void)access; + (void)shareMode; + (void)pSecurity; + (void)creation; + (void)flags; + (void)templateFile; + size_t nameLength = strlen(pName); + if (nameLength >= sizeof(gFakeWindows.openedName)) { + nameLength = sizeof(gFakeWindows.openedName) - 1; + } + memcpy(gFakeWindows.openedName, pName, nameLength); + gFakeWindows.openedName[nameLength] = 0; + return gFakeWindows.createFileResult ? &gComHandle : INVALID_HANDLE_VALUE; +} + +BOOL SetupComm(HANDLE handle, DWORD inputSize, DWORD outputSize) +{ + (void)handle; + return gFakeWindows.setupCommResult && inputSize == 16384 && + outputSize == 16384; +} + +BOOL GetCommState(HANDLE handle, DCB *pDcb) +{ + (void)handle; + if (gFakeWindows.getCommStateResult) { + *pDcb = gFakeWindows.dcb; + } + return gFakeWindows.getCommStateResult; +} + +BOOL SetCommState(HANDLE handle, DCB *pDcb) +{ + (void)handle; + gFakeWindows.dcb = *pDcb; + return gFakeWindows.setCommStateResult; +} + +BOOL SetCommTimeouts(HANDLE handle, COMMTIMEOUTS *pTimeouts) +{ + (void)handle; + gFakeWindows.timeouts = *pTimeouts; + return gFakeWindows.setCommTimeoutsResult; +} + +BOOL SetCommMask(HANDLE handle, DWORD eventMask) +{ + (void)handle; + gFakeWindows.commMask = eventMask; + return gFakeWindows.setCommMaskResult; +} + +BOOL PurgeComm(HANDLE handle, DWORD flags) +{ + (void)handle; + gFakeWindows.purgeFlags = flags; + return TRUE; +} + +BOOL WriteFile(HANDLE handle, const void *pData, DWORD length, + LPDWORD pWritten, LPOVERLAPPED pOverlapped) +{ + (void)handle; + (void)pData; + (void)pOverlapped; + gFakeWindows.writeLength = length; + *pWritten = gFakeWindows.bytesWritten; + return gFakeWindows.writeResult; +} + +BOOL ReadFile(HANDLE handle, void *pData, DWORD length, + LPDWORD pRead, LPOVERLAPPED pOverlapped) +{ + (void)handle; + (void)pData; + (void)pOverlapped; + gFakeWindows.readLength = length; + *pRead = gFakeWindows.bytesRead; + return gFakeWindows.readResult; +} + +BOOL ClearCommError(HANDLE handle, LPDWORD pErrors, COMSTAT *pStatus) +{ + (void)handle; + *pErrors = 0; + pStatus->cbInQue = gFakeWindows.queuedBytes; + return gFakeWindows.clearCommErrorResult; +} + +BOOL WaitCommEvent(HANDLE handle, LPDWORD pEventMask, + LPOVERLAPPED pOverlapped) +{ + (void)handle; + (void)pOverlapped; + gFakeWindows.waitCommCalls++; + if (gFakeWindows.waitCommCalls == 1) { + *pEventMask = gFakeWindows.waitEventMask; + return TRUE; + } + if (gFakeWindows.lastError == 0) { + gFakeWindows.lastError = ERROR_ACCESS_DENIED; + } + return FALSE; +} + +BOOL CancelIoEx(HANDLE handle, LPOVERLAPPED pOverlapped) +{ + (void)handle; + (void)pOverlapped; + gFakeWindows.cancelCalls++; + return TRUE; +} + +DWORD GetLastError(void) +{ + return gFakeWindows.lastError; +} + +BOOL CloseHandle(HANDLE handle) +{ + (void)handle; + gFakeWindows.closeCalls++; + return TRUE; +} + +BOOL QueryPerformanceFrequency(LARGE_INTEGER *pFrequency) +{ + pFrequency->QuadPart = gFakeWindows.performanceFrequency; + return TRUE; +} + +BOOL QueryPerformanceCounter(LARGE_INTEGER *pCounter) +{ + pCounter->QuadPart = gFakeWindows.performanceCounter; + return TRUE; +} + +DWORD WaitForSingleObject(HANDLE handle, DWORD timeout) +{ + (void)handle; + gFakeWindows.lastWaitTimeout = timeout; + return gFakeWindows.waitResult; +} + +HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES pAttributes, LONG initialCount, + LONG maximumCount, LPCSTR pName) +{ + (void)pAttributes; + (void)initialCount; + (void)maximumCount; + (void)pName; + return &gSemaphoreHandle; +} + +BOOL ReleaseSemaphore(HANDLE handle, LONG releaseCount, + LONG *pPreviousCount) +{ + (void)handle; + (void)releaseCount; + (void)pPreviousCount; + return TRUE; +} + +HANDLE CreateThread(LPSECURITY_ATTRIBUTES pAttributes, size_t stackSize, + LPTHREAD_START_ROUTINE pStart, LPVOID pParameter, + DWORD flags, LPDWORD pThreadId) +{ + (void)pAttributes; + (void)stackSize; + (void)flags; + (void)pThreadId; + if (!gFakeWindows.createThreadResult) { + return NULL; + } + gpThreadStart = pStart; + gpThreadParameter = pParameter; + return &gThreadHandle; +} + +void Sleep(DWORD milliseconds) +{ + (void)milliseconds; +} \ No newline at end of file diff --git a/test/windows/fake/fake_windows.h b/test/windows/fake/fake_windows.h new file mode 100644 index 0000000..b2a4bec --- /dev/null +++ b/test/windows/fake/fake_windows.h @@ -0,0 +1,44 @@ +#ifndef FAKE_WINDOWS_API_H +#define FAKE_WINDOWS_API_H + +#include "windows.h" + +typedef struct { + BOOL createFileResult; + BOOL setupCommResult; + BOOL getCommStateResult; + BOOL setCommStateResult; + BOOL setCommTimeoutsResult; + BOOL setCommMaskResult; + BOOL readResult; + BOOL writeResult; + BOOL clearCommErrorResult; + BOOL createThreadResult; + DWORD lastError; + DWORD waitResult; + DWORD waitEventMask; + DWORD queuedBytes; + DWORD bytesRead; + DWORD bytesWritten; + int64_t performanceCounter; + int64_t performanceFrequency; + char openedName[64]; + DCB dcb; + COMMTIMEOUTS timeouts; + DWORD commMask; + DWORD purgeFlags; + DWORD lastWaitTimeout; + DWORD readLength; + DWORD writeLength; + unsigned closeCalls; + unsigned cancelCalls; + unsigned waitCommCalls; + unsigned rxCalls; +} fakeWindowsState_t; + +extern fakeWindowsState_t gFakeWindows; + +void fakeWindowsReset(void); +void fakeWindowsRunThread(void); + +#endif \ No newline at end of file diff --git a/test/windows/fake/regstr.h b/test/windows/fake/regstr.h new file mode 100644 index 0000000..2c748f9 --- /dev/null +++ b/test/windows/fake/regstr.h @@ -0,0 +1,3 @@ +#ifndef FAKE_REGSTR_H +#define FAKE_REGSTR_H +#endif \ No newline at end of file diff --git a/test/windows/fake/setupapi.h b/test/windows/fake/setupapi.h new file mode 100644 index 0000000..6bc435c --- /dev/null +++ b/test/windows/fake/setupapi.h @@ -0,0 +1,3 @@ +#ifndef FAKE_SETUPAPI_H +#define FAKE_SETUPAPI_H +#endif \ No newline at end of file diff --git a/test/windows/fake/windows.h b/test/windows/fake/windows.h new file mode 100644 index 0000000..07347bb --- /dev/null +++ b/test/windows/fake/windows.h @@ -0,0 +1,118 @@ +#ifndef FAKE_WINDOWS_H +#define FAKE_WINDOWS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int BOOL; +typedef uint8_t BYTE; +typedef unsigned long DWORD; +typedef int32_t LONG; +typedef void *HANDLE; +typedef void *LPVOID; +typedef const char *LPCSTR; +typedef DWORD *LPDWORD; +typedef void *LPSECURITY_ATTRIBUTES; +typedef void *LPOVERLAPPED; +typedef DWORD (*LPTHREAD_START_ROUTINE)(LPVOID); + +typedef struct { + int64_t QuadPart; +} LARGE_INTEGER; + +typedef struct { + DWORD DCBlength; + DWORD BaudRate; + DWORD fBinary; + DWORD fOutxCtsFlow; + DWORD fOutxDsrFlow; + DWORD fDtrControl; + DWORD fDsrSensitivity; + DWORD fOutX; + DWORD fInX; + DWORD fErrorChar; + DWORD fNull; + DWORD fRtsControl; + DWORD fAbortOnError; + BYTE ByteSize; + BYTE Parity; + BYTE StopBits; +} DCB; + +typedef struct { + DWORD ReadIntervalTimeout; + DWORD ReadTotalTimeoutMultiplier; + DWORD ReadTotalTimeoutConstant; + DWORD WriteTotalTimeoutMultiplier; + DWORD WriteTotalTimeoutConstant; +} COMMTIMEOUTS; + +typedef struct { + DWORD fFlags; + DWORD cbInQue; + DWORD cbOutQue; +} COMSTAT; + +#define WINAPI +#define TRUE 1 +#define FALSE 0 +#define NULL_HANDLE ((HANDLE)0) +#define INVALID_HANDLE_VALUE ((HANDLE)(intptr_t)-1) +#define GENERIC_READ 0x80000000UL +#define GENERIC_WRITE 0x40000000UL +#define OPEN_EXISTING 3UL +#define NOPARITY 0U +#define ONESTOPBIT 0U +#define RTS_CONTROL_ENABLE 1UL +#define RTS_CONTROL_HANDSHAKE 2UL +#define DTR_CONTROL_ENABLE 1UL +#define EV_RXCHAR 1UL +#define PURGE_TXCLEAR 4UL +#define PURGE_RXCLEAR 8UL +#define ERROR_ACCESS_DENIED 5UL +#define ERROR_OPERATION_ABORTED 995UL +#define WAIT_OBJECT_0 0UL +#define WAIT_TIMEOUT 258UL +#define WAIT_FAILED 0xffffffffUL +#define INFINITE 0xffffffffUL + +HANDLE CreateFileA(LPCSTR pName, DWORD access, DWORD shareMode, + LPSECURITY_ATTRIBUTES pSecurity, DWORD creation, + DWORD flags, HANDLE templateFile); +BOOL SetupComm(HANDLE handle, DWORD inputSize, DWORD outputSize); +BOOL GetCommState(HANDLE handle, DCB *pDcb); +BOOL SetCommState(HANDLE handle, DCB *pDcb); +BOOL SetCommTimeouts(HANDLE handle, COMMTIMEOUTS *pTimeouts); +BOOL SetCommMask(HANDLE handle, DWORD eventMask); +BOOL PurgeComm(HANDLE handle, DWORD flags); +BOOL WriteFile(HANDLE handle, const void *pData, DWORD length, + LPDWORD pWritten, LPOVERLAPPED pOverlapped); +BOOL ReadFile(HANDLE handle, void *pData, DWORD length, + LPDWORD pRead, LPOVERLAPPED pOverlapped); +BOOL ClearCommError(HANDLE handle, LPDWORD pErrors, COMSTAT *pStatus); +BOOL WaitCommEvent(HANDLE handle, LPDWORD pEventMask, + LPOVERLAPPED pOverlapped); +BOOL CancelIoEx(HANDLE handle, LPOVERLAPPED pOverlapped); +DWORD GetLastError(void); +BOOL CloseHandle(HANDLE handle); +BOOL QueryPerformanceFrequency(LARGE_INTEGER *pFrequency); +BOOL QueryPerformanceCounter(LARGE_INTEGER *pCounter); +DWORD WaitForSingleObject(HANDLE handle, DWORD timeout); +HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES pAttributes, LONG initialCount, + LONG maximumCount, LPCSTR pName); +BOOL ReleaseSemaphore(HANDLE handle, LONG releaseCount, + LONG *pPreviousCount); +HANDLE CreateThread(LPSECURITY_ATTRIBUTES pAttributes, size_t stackSize, + LPTHREAD_START_ROUTINE pStart, LPVOID pParameter, + DWORD flags, LPDWORD pThreadId); +void Sleep(DWORD milliseconds); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/test/windows/test_windows_port.c b/test/windows/test_windows_port.c new file mode 100644 index 0000000..c3739ed --- /dev/null +++ b/test/windows/test_windows_port.c @@ -0,0 +1,105 @@ +#include +#include +#include +#include + +#include "fake_windows.h" +#include "u_cx_at_client.h" +#include "u_port.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: check failed: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +int32_t uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient) +{ + (void)pClient; + gFakeWindows.rxCalls++; + return 0; +} + +int32_t uPortUartWaitForData(uPortUartHandle_t handle, int32_t timeoutMs) +{ + (void)handle; + (void)timeoutMs; + gFakeWindows.waitCommCalls++; + return gFakeWindows.waitCommCalls == 1 ? 1 : -1; +} + +void uPortUartWake(uPortUartHandle_t handle) +{ + (void)handle; + gFakeWindows.cancelCalls++; +} + +static bool testTimeAndMutex(void) +{ + fakeWindowsReset(); + gFakeWindows.performanceCounter = 100; + uPortInit(); + CHECK(uPortGetTickTimeMs() == 0); + gFakeWindows.performanceCounter = 137; + CHECK(uPortGetTickTimeMs() == 37); + + HANDLE semaphore = CreateSemaphore(NULL, 1, 1, NULL); + gFakeWindows.waitResult = WAIT_OBJECT_0; + CHECK(uPortMutexTryLock(semaphore, -1) == 0); + CHECK(gFakeWindows.lastWaitTimeout == INFINITE); + gFakeWindows.waitResult = WAIT_TIMEOUT; + CHECK(uPortMutexTryLock(semaphore, 25) == -2); + CHECK(gFakeWindows.lastWaitTimeout == 25); + gFakeWindows.waitResult = WAIT_FAILED; + CHECK(uPortMutexTryLock(semaphore, 0) == -1); + uPortDeinit(); + return true; +} + +static bool testBackgroundRxLifecycle(void) +{ + uCxAtClient_t client = {0}; + client.uartHandle = (uPortUartHandle_t)(uintptr_t)1; + + fakeWindowsReset(); + uPortBgRxTaskCreate(&client); + fakeWindowsRunThread(); + CHECK(gFakeWindows.rxCalls == 1); + CHECK(gFakeWindows.waitCommCalls == 2); + uPortBgRxTaskDestroy(&client); + CHECK(gFakeWindows.cancelCalls == 1); + CHECK(gFakeWindows.closeCalls == 1); + CHECK(gFakeWindows.lastWaitTimeout == 5000); + + fakeWindowsReset(); + gFakeWindows.createThreadResult = FALSE; + uPortBgRxTaskCreate(&client); + uPortBgRxTaskDestroy(&client); + CHECK(gFakeWindows.closeCalls == 0); + return true; +} + +int main(void) +{ + static const struct { + const char *pName; + bool (*pTest)(void); + } tests[] = { + {"time and mutex", testTimeAndMutex}, + {"background RX lifecycle", testBackgroundRxLifecycle} + }; + + for (size_t index = 0; index < sizeof(tests) / sizeof(tests[0]); index++) { + printf("[ RUN ] %s\n", tests[index].pName); + if (!tests[index].pTest()) { + printf("[ FAILED ] %s\n", tests[index].pName); + return EXIT_FAILURE; + } + printf("[ OK ] %s\n", tests[index].pName); + } + printf("[ PASSED ] %zu tests\n", sizeof(tests) / sizeof(tests[0])); + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/test/windows/test_windows_uart.c b/test/windows/test_windows_uart.c new file mode 100644 index 0000000..068e1e2 --- /dev/null +++ b/test/windows/test_windows_uart.c @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include + +#include "fake_windows.h" +#include "u_port_uart.h" + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: check failed: %s\n", \ + __FILE__, __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +static bool testOpenConfiguration(void) +{ + fakeWindowsReset(); + uPortUartHandle_t handle = uPortUartOpen("COM12", 921600, true); + CHECK(handle != NULL); + CHECK(strcmp(gFakeWindows.openedName, "\\\\.\\COM12") == 0); + CHECK(gFakeWindows.dcb.DCBlength == sizeof(DCB)); + CHECK(gFakeWindows.dcb.BaudRate == 921600); + CHECK(gFakeWindows.dcb.ByteSize == 8); + CHECK(gFakeWindows.dcb.fRtsControl == RTS_CONTROL_HANDSHAKE); + CHECK(gFakeWindows.dcb.fOutxCtsFlow == TRUE); + CHECK(gFakeWindows.timeouts.ReadTotalTimeoutConstant == 100); + CHECK(gFakeWindows.timeouts.WriteTotalTimeoutConstant == 1000); + CHECK(gFakeWindows.commMask == EV_RXCHAR); + CHECK(gFakeWindows.purgeFlags == (PURGE_RXCLEAR | PURGE_TXCLEAR)); + uPortUartClose(handle); + + fakeWindowsReset(); + handle = uPortUartOpen("pipe-name", 115200, false); + CHECK(handle != NULL); + CHECK(strcmp(gFakeWindows.openedName, "pipe-name") == 0); + CHECK(gFakeWindows.dcb.fRtsControl == RTS_CONTROL_ENABLE); + CHECK(gFakeWindows.dcb.fOutxCtsFlow == FALSE); + uPortUartClose(handle); + + fakeWindowsReset(); + gFakeWindows.setupCommResult = FALSE; + handle = uPortUartOpen("COM2", 115200, false); + CHECK(handle != NULL); + uPortUartClose(handle); + return true; +} + +static bool testOpenFailures(void) +{ + CHECK(uPortUartOpen(NULL, 115200, false) == NULL); + + fakeWindowsReset(); + gFakeWindows.createFileResult = FALSE; + CHECK(uPortUartOpen("COM1", 115200, false) == NULL); + + fakeWindowsReset(); + gFakeWindows.getCommStateResult = FALSE; + CHECK(uPortUartOpen("COM1", 115200, false) == NULL); + CHECK(gFakeWindows.closeCalls == 1); + + fakeWindowsReset(); + gFakeWindows.setCommStateResult = FALSE; + CHECK(uPortUartOpen("COM1", 115200, false) == NULL); + CHECK(gFakeWindows.closeCalls == 1); + + fakeWindowsReset(); + gFakeWindows.setCommTimeoutsResult = FALSE; + CHECK(uPortUartOpen("COM1", 115200, false) == NULL); + CHECK(gFakeWindows.closeCalls == 1); + + fakeWindowsReset(); + gFakeWindows.setCommMaskResult = FALSE; + CHECK(uPortUartOpen("COM1", 115200, false) == NULL); + CHECK(gFakeWindows.closeCalls == 1); + return true; +} + +static bool testReadWriteAndEvents(void) +{ + uint8_t data[8] = {0}; + fakeWindowsReset(); + uPortUartHandle_t handle = uPortUartOpen("COM1", 115200, false); + CHECK(handle != NULL); + + gFakeWindows.queuedBytes = 3; + gFakeWindows.bytesRead = 3; + CHECK(uPortUartRead(handle, data, sizeof(data), 0) == 3); + CHECK(gFakeWindows.readLength == 3); + gFakeWindows.queuedBytes = 8; + gFakeWindows.bytesRead = 2; + CHECK(uPortUartRead(handle, data, sizeof(data), 0) == 2); + CHECK(gFakeWindows.readLength == sizeof(data)); + gFakeWindows.queuedBytes = 0; + CHECK(uPortUartRead(handle, data, sizeof(data), 0) == 0); + gFakeWindows.clearCommErrorResult = FALSE; + CHECK(uPortUartRead(handle, data, sizeof(data), 0) < 0); + + gFakeWindows.bytesWritten = 5; + CHECK(uPortUartWrite(handle, data, 5) == 5); + CHECK(gFakeWindows.writeLength == 5); + gFakeWindows.bytesWritten = 2; + CHECK(uPortUartWrite(handle, data, 5) == 2); + gFakeWindows.writeResult = FALSE; + CHECK(uPortUartWrite(handle, data, 5) < 0); + + gFakeWindows.waitCommCalls = 0; + CHECK(uPortUartWaitForData(handle, -1) == 1); + gFakeWindows.waitCommCalls = 0; + gFakeWindows.waitEventMask = 0; + CHECK(uPortUartWaitForData(handle, -1) == 0); + gFakeWindows.waitCommCalls = 1; + gFakeWindows.lastError = ERROR_OPERATION_ABORTED; + CHECK(uPortUartWaitForData(handle, -1) == 0); + uPortUartWake(handle); + CHECK(gFakeWindows.cancelCalls == 1); + + CHECK(uPortUartRead(NULL, data, sizeof(data), 0) < 0); + CHECK(uPortUartRead(handle, NULL, sizeof(data), 0) == 0); + CHECK(uPortUartRead(handle, data, 0, 0) < 0); + CHECK(uPortUartWrite(NULL, data, sizeof(data)) < 0); + CHECK(uPortUartWrite(handle, data, 0) < 0); + CHECK(uPortUartWaitForData(NULL, 0) < 0); + uPortUartClose(handle); + uPortUartClose(NULL); + return true; +} + +int main(void) +{ + static const struct { + const char *pName; + bool (*pTest)(void); + } tests[] = { + {"open configuration", testOpenConfiguration}, + {"open failures", testOpenFailures}, + {"read write and events", testReadWriteAndEvents} + }; + + for (size_t index = 0; index < sizeof(tests) / sizeof(tests[0]); index++) { + printf("[ RUN ] %s\n", tests[index].pName); + if (!tests[index].pTest()) { + printf("[ FAILED ] %s\n", tests[index].pName); + return EXIT_FAILURE; + } + printf("[ OK ] %s\n", tests[index].pName); + } + printf("[ PASSED ] %zu tests\n", sizeof(tests) / sizeof(tests[0])); + return EXIT_SUCCESS; +} \ No newline at end of file From af2eeba66f9c9a9410ebd0d41f73efbcd06182b2 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Sun, 6 Sep 2026 12:32:19 +0200 Subject: [PATCH 38/39] ci: Run Windows port tests Expose the Windows host suite through Invoke and execute it with MSVC on the native Windows pull request runner. --- .github/workflows/port-tests.yaml | 25 ++++++++++++++++++++++++- README.md | 4 ++++ tasks.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/port-tests.yaml b/.github/workflows/port-tests.yaml index c086b58..f571064 100644 --- a/.github/workflows/port-tests.yaml +++ b/.github/workflows/port-tests.yaml @@ -47,4 +47,27 @@ jobs: with: name: ${{ matrix.build-directory }}-coverage path: build/${{ matrix.build-directory }}/coverage.html - if-no-files-found: ignore \ No newline at end of file + if-no-files-found: ignore + + windows: + name: Windows + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Set up MSVC + uses: ilammy/msvc-dev-cmd@v1 + + - name: Install Invoke + run: pip install invoke + + - name: Run tests + run: invoke test.windows.run \ No newline at end of file diff --git a/README.md b/README.md index c883232..59b1494 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,9 @@ invoke test.no-os.run # Run host-based STM32 UART and FreeRTOS port tests with coverage invoke test.stm32.unit +# Windows port tests (native Windows or portable fake Win32 API) +invoke test.windows.run + # Run STM32 HTTP and socket integration tests in Renode invoke test.stm32.renode @@ -175,6 +178,7 @@ invoke test.ceedling.clean invoke test.posix.clean invoke test.no-os.clean invoke test.stm32.clean +invoke test.windows.clean invoke test.zephyr.clean invoke test.zephyr.clean-west # Remove west workspace ``` diff --git a/tasks.py b/tasks.py index 809e900..40dd4f8 100644 --- a/tasks.py +++ b/tasks.py @@ -124,6 +124,18 @@ def stm32_unit(c): f"--txt --html-details {os.path.join(build_dir, 'coverage.html')}") +@task +def windows(c): + """Run host-based Windows port tests.""" + print("Running Windows port tests...") + build_dir = os.path.join(REPO_ROOT, "build", "windows") + test_dir = os.path.join(REPO_ROOT, "test", "windows") + + c.run(f"cmake -S {test_dir} -B {build_dir}") + c.run(f"cmake --build {build_dir} --parallel") + c.run(f"ctest --test-dir {build_dir} --output-on-failure") + + @task(help={ 'timeout': 'Timeout in seconds for each emulated example', }) @@ -175,6 +187,16 @@ def clean_stm32_unit(c): c.run(f"rm -rf {os.path.join(REPO_ROOT, 'build', 'stm32-unit')}") +@task +def clean_windows(c): + """Clean host-based Windows test artifacts.""" + build_dir = os.path.join(REPO_ROOT, "build", "windows") + if os.name == "nt": + c.run(f'if exist "{build_dir}" rmdir /s /q "{build_dir}"') + else: + c.run(f"rm -rf {build_dir}") + + @task def clean_west(c): """Clean west workspace.""" @@ -210,6 +232,11 @@ def clean_west(c): stm32_ns.add_task(stm32_renode, 'renode') stm32_ns.add_task(clean_stm32_unit, 'clean') +# Windows sub-collection under test +windows_ns = Collection('windows') +windows_ns.add_task(windows, 'run') +windows_ns.add_task(clean_windows, 'clean') + # Test namespace with sub-collections test_ns = Collection('test') test_ns.add_collection(ceedling_ns) @@ -217,6 +244,7 @@ def clean_west(c): test_ns.add_collection(posix_ns) test_ns.add_collection(no_os_ns) test_ns.add_collection(stm32_ns) +test_ns.add_collection(windows_ns) # Create main namespace ns = Collection() From ab5e7a8ae2ba049527f4f6760c00a2ac65a5b199 Mon Sep 17 00:00:00 2001 From: Andreas Anderberg Date: Mon, 7 Sep 2026 15:42:40 +0200 Subject: [PATCH 39/39] ci: Fix Windows tests and STM32 cache setup Exclude native Windows tests from Ceedling and namespace mocked Win32 symbols so they do not conflict with kernel32.lib. Use one writable ccache directory for every STM32 Docker step. --- .github/workflows/stm32-build-test.yml | 2 +- project.yml | 1 + tasks.py | 5 +- test/test_u_cx_at_client.c | 117 ++++++++++------------- test/test_u_cx_at_client_no_urc_queue.c | 120 +++++++++++------------- test/windows/fake/windows.h | 22 +++++ 6 files changed, 132 insertions(+), 135 deletions(-) diff --git a/.github/workflows/stm32-build-test.yml b/.github/workflows/stm32-build-test.yml index a9321db..3881711 100644 --- a/.github/workflows/stm32-build-test.yml +++ b/.github/workflows/stm32-build-test.yml @@ -11,6 +11,7 @@ on: env: REGISTRY: ghcr.io IMAGE_NAME: u-blox/u-connectclient/stm32f4-builder + CCACHE_DIR: /tmp/ccache jobs: build-and-test-stm32: @@ -87,7 +88,6 @@ jobs: working-directory: ./examples env: SKIP_DOCKER_BUILD: '1' - CCACHE_DIR: /tmp/ccache run: | mkdir -p /tmp/ccache inv stm32.http.build --docker diff --git a/project.yml b/project.yml index 9af7803..77fa638 100644 --- a/project.yml +++ b/project.yml @@ -24,6 +24,7 @@ - -:test/posix/** - -:test/no_os/** - -:test/stm32/** + - -:test/windows/** :source: - src/** :include: diff --git a/tasks.py b/tasks.py index 40dd4f8..f8ca120 100644 --- a/tasks.py +++ b/tasks.py @@ -130,10 +130,11 @@ def windows(c): print("Running Windows port tests...") build_dir = os.path.join(REPO_ROOT, "build", "windows") test_dir = os.path.join(REPO_ROOT, "test", "windows") + build_config = "Debug" c.run(f"cmake -S {test_dir} -B {build_dir}") - c.run(f"cmake --build {build_dir} --parallel") - c.run(f"ctest --test-dir {build_dir} --output-on-failure") + c.run(f"cmake --build {build_dir} --config {build_config} --parallel") + c.run(f"ctest --test-dir {build_dir} -C {build_config} --output-on-failure") @task(help={ diff --git a/test/test_u_cx_at_client.c b/test/test_u_cx_at_client.c index ab88f11..1d411f4 100644 --- a/test/test_u_cx_at_client.c +++ b/test/test_u_cx_at_client.c @@ -72,10 +72,49 @@ static uCxAtClientConfig_t gClientConfig = { static uCxAtClient_t gClient; static int32_t *gPTickSequence; +static struct { + uCxAtClient_t *pClient; + void *pTag; + const uint8_t *pBinaryData; + size_t binaryDataLen; + int callbackCount; +} gUrcCallbackExpectations; + /* ---------------------------------------------------------------- * STATIC FUNCTIONS * -------------------------------------------------------------- */ +static void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, + size_t lineLength, uint8_t *pBinaryData, + size_t binaryDataLen) +{ + TEST_ASSERT_EQUAL(gUrcCallbackExpectations.pClient, pClient); + TEST_ASSERT_EQUAL(gUrcCallbackExpectations.pTag, pTag); + TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); + TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); + TEST_ASSERT_EQUAL(gUrcCallbackExpectations.binaryDataLen, binaryDataLen); + if (gUrcCallbackExpectations.pBinaryData == NULL) { + TEST_ASSERT_NULL(pBinaryData); + } else { + TEST_ASSERT_NOT_NULL(pBinaryData); + TEST_ASSERT_EQUAL_MEMORY(gUrcCallbackExpectations.pBinaryData, + pBinaryData, binaryDataLen); + } + gUrcCallbackExpectations.callbackCount++; +} + +static void setUrcCallbackExpectations(uCxAtClient_t *pClient, void *pTag, + const uint8_t *pBinaryData, + size_t binaryDataLen) +{ + gUrcCallbackExpectations.pClient = pClient; + gUrcCallbackExpectations.pTag = pTag; + gUrcCallbackExpectations.pBinaryData = pBinaryData; + gUrcCallbackExpectations.binaryDataLen = binaryDataLen; + gUrcCallbackExpectations.callbackCount = 0; + uCxAtClientSetUrcCallback(pClient, urcCallback, pTag); +} + int32_t uPortGetTickTimeMs_CALLBACK(int cmock_num_calls) { // Helper function for returning a sequence of ticks @@ -718,25 +757,12 @@ void test_uCxAtClientCmdGetRspParamLine_withUnexpectedBinaryResponse(void) void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) { char rxData[] = { "\r\n" TEST_URC "\r\n" }; - int callbackCount = 0; gPRxDataPtr = (uint8_t *)&rxData[0]; gRxDataLen = strlen(rxData); - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_NULL(pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(pLine), lineLength); - TEST_ASSERT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(0, binaryDataLen); - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); } void test_uCxAtClientOpen_afterPartialBinaryHeader_expectParserReset(void) @@ -762,8 +788,8 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) { char strData[] = { "\r\n" TEST_URC }; uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; + uint8_t expectedBinData[] = {0xa5,0x01,0x0d,0x0a,0x00,0xff}; uint8_t rxData[strlen(strData) + sizeof(binData)]; - int callbackCount = 0; memcpy(&rxData[0], &strData[0], strlen(strData)); memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); gPRxDataPtr = &rxData[0]; @@ -771,34 +797,21 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) gRxMaxReadSize = 1; gClient.urcQueue.bufferLen = sizeof(uUrcEntry_t) + strlen(TEST_URC) + 1 + 6; - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - uint8_t expectedBinData[] = {0xa5,0x01,0x0d,0x0a,0x00,0xff}; - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_NULL(pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(pLine), lineLength); - TEST_ASSERT_NOT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(sizeof(expectedBinData), binaryDataLen); - TEST_ASSERT_EQUAL_MEMORY(expectedBinData, pBinaryData, sizeof(expectedBinData)); - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, expectedBinData, + sizeof(expectedBinData)); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_EQUAL(0, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_TRUE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(1, gClient.binaryRx.rxHeaderCount); gRxDataLen = 3; uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_EQUAL(0, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_TRUE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(2, gClient.binaryRx.bufferPos); TEST_ASSERT_EQUAL(4, gClient.binaryRx.remainingDataBytes); gRxDataLen = 4; uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_FALSE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(0, gRxDataLen); } @@ -809,63 +822,37 @@ void test_uCxAtClientHandleRx_withOversizedBinUrc_expectDiscardAndRecovery(void) uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; uint8_t rxData[strlen(strData) + sizeof(binData)]; char nextUrc[] = { "\r\n" TEST_URC "\r\n" }; - int callbackCount = 0; memcpy(rxData, strData, strlen(strData)); memcpy(&rxData[strlen(strData)], binData, sizeof(binData)); gClient.urcQueue.bufferLen = sizeof(uUrcEntry_t) + strlen(TEST_URC) + 1 + 5; - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_NULL(pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); - TEST_ASSERT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(0, binaryDataLen); - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); gPRxDataPtr = rxData; gRxDataLen = sizeof(rxData); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_EQUAL(0, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_FALSE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(0, gRxDataLen); gPRxDataPtr = (uint8_t *)nextUrc; gRxDataLen = strlen(nextUrc); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); } void test_uCxAtClientCmd_withInterleavedUrc_expectResponseAndCallback(void) { char rxData[] = { TEST_URC "\r\n+MYRSP:123\r\nOK\r\n" }; - int callbackCount = 0; gPRxDataPtr = (uint8_t *)&rxData[0]; gRxDataLen = strlen(rxData); - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_EQUAL(CONTEXT_VALUE, pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); - TEST_ASSERT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(0, binaryDataLen); - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, CONTEXT_VALUE); + setUrcCallbackExpectations(&gClient, CONTEXT_VALUE, NULL, 0); uCxAtClientCmdBeginF(&gClient, "", "", U_CX_AT_UTIL_PARAM_LAST); TEST_ASSERT_EQUAL_STRING("123", uCxAtClientCmdGetRspParamLine(&gClient, "+MYRSP:", NULL, NULL)); TEST_ASSERT_EQUAL(0, uCxAtClientCmdEnd(&gClient)); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); } void test_uCxAtClientHandleRx_withReadError_expectIoError(void) diff --git a/test/test_u_cx_at_client_no_urc_queue.c b/test/test_u_cx_at_client_no_urc_queue.c index 0d0cd13..e422053 100644 --- a/test/test_u_cx_at_client_no_urc_queue.c +++ b/test/test_u_cx_at_client_no_urc_queue.c @@ -67,10 +67,49 @@ static uCxAtClientConfig_t gClientConfig = { static uCxAtClient_t gClient; +static struct { + uCxAtClient_t *pClient; + void *pTag; + const uint8_t *pBinaryData; + size_t binaryDataLen; + int callbackCount; +} gUrcCallbackExpectations; + /* ---------------------------------------------------------------- * STATIC FUNCTIONS * -------------------------------------------------------------- */ +static void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, + size_t lineLength, uint8_t *pBinaryData, + size_t binaryDataLen) +{ + TEST_ASSERT_EQUAL(gUrcCallbackExpectations.pClient, pClient); + TEST_ASSERT_EQUAL(gUrcCallbackExpectations.pTag, pTag); + TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); + TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); + TEST_ASSERT_EQUAL(gUrcCallbackExpectations.binaryDataLen, binaryDataLen); + if (gUrcCallbackExpectations.pBinaryData == NULL) { + TEST_ASSERT_NULL(pBinaryData); + } else { + TEST_ASSERT_NOT_NULL(pBinaryData); + TEST_ASSERT_EQUAL_MEMORY(gUrcCallbackExpectations.pBinaryData, + pBinaryData, binaryDataLen); + } + gUrcCallbackExpectations.callbackCount++; +} + +static void setUrcCallbackExpectations(uCxAtClient_t *pClient, void *pTag, + const uint8_t *pBinaryData, + size_t binaryDataLen) +{ + gUrcCallbackExpectations.pClient = pClient; + gUrcCallbackExpectations.pTag = pTag; + gUrcCallbackExpectations.pBinaryData = pBinaryData; + gUrcCallbackExpectations.binaryDataLen = binaryDataLen; + gUrcCallbackExpectations.callbackCount = 0; + uCxAtClientSetUrcCallback(pClient, urcCallback, pTag); +} + /* Mock UART open function */ uPortUartHandle_t uPortUartOpen(const char *pDeviceName, int32_t baudRate, bool flowControl) { @@ -174,33 +213,20 @@ void tearDown(void) void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) { char rxData[] = { "\r\n" TEST_URC "\r\n" }; - int callbackCount = 0; gPRxDataPtr = (uint8_t *)&rxData[0]; gRxDataLen = strlen(rxData); - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_NULL(pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(pLine), lineLength); - TEST_ASSERT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(0, binaryDataLen); - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); } void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) { char strData[] = { "\r\n" TEST_URC }; uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; + uint8_t expectedBinData[] = {0xa5,0x01,0x0d,0x0a,0x00,0xff}; uint8_t rxData[strlen(strData) + sizeof(binData)]; - int callbackCount = 0; memcpy(&rxData[0], &strData[0], strlen(strData)); memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); gPRxDataPtr = &rxData[0]; @@ -208,34 +234,21 @@ void test_uCxAtClientHandleRx_withFragmentedBinUrc_expectUrcCallback(void) gRxMaxReadSize = 1; gClientConfig.rxBufferLen = strlen(TEST_URC) + 1 + 6; - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - uint8_t expectedBinData[] = {0xa5,0x01,0x0d,0x0a,0x00,0xff}; - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_NULL(pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(pLine), lineLength); - TEST_ASSERT_NOT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(sizeof(expectedBinData), binaryDataLen); - TEST_ASSERT_EQUAL_MEMORY(expectedBinData, pBinaryData, sizeof(expectedBinData)); - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, expectedBinData, + sizeof(expectedBinData)); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_EQUAL(0, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_TRUE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(1, gClient.binaryRx.rxHeaderCount); gRxDataLen = 3; uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_EQUAL(0, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_TRUE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(2, gClient.binaryRx.bufferPos); TEST_ASSERT_EQUAL(4, gClient.binaryRx.remainingDataBytes); gRxDataLen = 4; uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_FALSE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(0, gRxDataLen); } @@ -246,29 +259,16 @@ void test_uCxAtClientHandleRx_withOversizedBinUrc_expectDiscardAndRecovery(void) uint8_t binData[] = {BIN_HDR(6),0xa5,0x01,0x0d,0x0a,0x00,0xff}; uint8_t rxData[strlen(strData) + sizeof(binData)]; char nextUrc[] = { "\r\n" TEST_URC "\r\n" }; - int callbackCount = 0; memcpy(rxData, strData, strlen(strData)); memcpy(&rxData[strlen(strData)], binData, sizeof(binData)); gClientConfig.rxBufferLen = strlen(TEST_URC) + 1 + 5; - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_NULL(pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); - TEST_ASSERT_NULL(pBinaryData); - TEST_ASSERT_EQUAL(0, binaryDataLen); - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); gPRxDataPtr = rxData; gRxDataLen = sizeof(rxData); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(0, callbackCount); + TEST_ASSERT_EQUAL(0, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_FALSE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(0, gRxDataLen); @@ -276,7 +276,7 @@ void test_uCxAtClientHandleRx_withOversizedBinUrc_expectDiscardAndRecovery(void) gPRxDataPtr = (uint8_t *)nextUrc; gRxDataLen = strlen(nextUrc); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); } void test_uCxAtClientHandleRx_withInterleavedBinaryHeaders_expectPerClientLength(void) @@ -288,7 +288,6 @@ void test_uCxAtClientHandleRx_withInterleavedBinaryHeaders_expectPerClientLength uint8_t firstHeader[strlen(strData) + 2]; uint8_t secondHeader[strlen(strData) + 2]; uint8_t firstTail[258]; - int callbackCount = 0; secondConfig.pRxBuffer = secondRxBuffer; uCxAtClientInit(&secondConfig, &secondClient); @@ -305,21 +304,8 @@ void test_uCxAtClientHandleRx_withInterleavedBinaryHeaders_expectPerClientLength firstTail[index + 1] = (uint8_t)index; } - void urcCallback(struct uCxAtClient *pClient, void *pTag, char *pLine, - size_t lineLength, uint8_t *pBinaryData, size_t binaryDataLen) - { - TEST_ASSERT_EQUAL(&gClient, pClient); - TEST_ASSERT_NULL(pTag); - TEST_ASSERT_EQUAL_STRING(TEST_URC, pLine); - TEST_ASSERT_EQUAL(strlen(TEST_URC), lineLength); - TEST_ASSERT_EQUAL(257, binaryDataLen); - for (size_t index = 0; index < binaryDataLen; index++) { - TEST_ASSERT_EQUAL_UINT8((uint8_t)index, pBinaryData[index]); - } - callbackCount++; - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, &firstTail[1], + sizeof(firstTail) - 1); gPRxDataPtr = firstHeader; gRxDataLen = sizeof(firstHeader); uCxAtClientHandleRx(&gClient); @@ -333,7 +319,7 @@ void test_uCxAtClientHandleRx_withInterleavedBinaryHeaders_expectPerClientLength gPRxDataPtr = firstTail; gRxDataLen = sizeof(firstTail); uCxAtClientHandleRx(&gClient); - TEST_ASSERT_EQUAL(1, callbackCount); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); TEST_ASSERT_FALSE(gClient.isBinaryRx); TEST_ASSERT_EQUAL(0, gRxDataLen); diff --git a/test/windows/fake/windows.h b/test/windows/fake/windows.h index 07347bb..57ec355 100644 --- a/test/windows/fake/windows.h +++ b/test/windows/fake/windows.h @@ -80,6 +80,28 @@ typedef struct { #define WAIT_FAILED 0xffffffffUL #define INFINITE 0xffffffffUL +#define CreateFileA fakeWindowsCreateFileA +#define SetupComm fakeWindowsSetupComm +#define GetCommState fakeWindowsGetCommState +#define SetCommState fakeWindowsSetCommState +#define SetCommTimeouts fakeWindowsSetCommTimeouts +#define SetCommMask fakeWindowsSetCommMask +#define PurgeComm fakeWindowsPurgeComm +#define WriteFile fakeWindowsWriteFile +#define ReadFile fakeWindowsReadFile +#define ClearCommError fakeWindowsClearCommError +#define WaitCommEvent fakeWindowsWaitCommEvent +#define CancelIoEx fakeWindowsCancelIoEx +#define GetLastError fakeWindowsGetLastError +#define CloseHandle fakeWindowsCloseHandle +#define QueryPerformanceFrequency fakeWindowsQueryPerformanceFrequency +#define QueryPerformanceCounter fakeWindowsQueryPerformanceCounter +#define WaitForSingleObject fakeWindowsWaitForSingleObject +#define CreateSemaphore fakeWindowsCreateSemaphore +#define ReleaseSemaphore fakeWindowsReleaseSemaphore +#define CreateThread fakeWindowsCreateThread +#define Sleep fakeWindowsSleep + HANDLE CreateFileA(LPCSTR pName, DWORD access, DWORD shareMode, LPSECURITY_ATTRIBUTES pSecurity, DWORD creation, DWORD flags, HANDLE templateFile);