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..f571064 --- /dev/null +++ b/.github/workflows/port-tests.yaml @@ -0,0 +1,73 @@ +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 + + 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/.github/workflows/stm32-build-test.yml b/.github/workflows/stm32-build-test.yml index 0b152b7..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 @@ -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 diff --git a/README.md b/README.md index fbc0d73..59b1494 100644 --- a/README.md +++ b/README.md @@ -155,11 +155,30 @@ 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 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 + +# 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 + # 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.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/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/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/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/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 143030b..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 * -------------------------------------------------------------- */ @@ -50,7 +52,7 @@ typedef struct { uCxAtClient_t *pClient; - TaskHandle_t rxTaskHandle; + volatile TaskHandle_t rxTaskHandle; volatile bool terminateRxTask; } uPortRxContext_t; @@ -70,11 +72,14 @@ 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"); + pCtx->rxTaskHandle = NULL; vTaskDelete(NULL); } @@ -117,6 +122,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,17 +138,35 @@ 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; + if (taskHandle != NULL) { + xTaskNotifyGive(taskHandle); + } } void uPortBgRxTaskDestroy(uCxAtClient_t *pClient) { (void)pClient; gRxContext.terminateRxTask = true; + if (gRxContext.rxTaskHandle != NULL) { + xTaskNotifyGive(gRxContext.rxTaskHandle); + } - // 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)); } } + +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 92eb409..0d90d10 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; @@ -107,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 45620b9..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 * -------------------------------------------------------------- */ @@ -39,6 +41,7 @@ typedef struct { uCxAtClient_t *pClient; pthread_t rxThread; volatile bool terminateRxTask; + bool rxThreadCreated; } uPortRxContext_t; /* ---------------------------------------------------------------- @@ -81,10 +84,14 @@ static void *rxTask(void *pArg) uPortRxContext_t *pCtx = (uPortRxContext_t *)pArg; while (!pCtx->terminateRxTask) { - U_CX_PORT_SLEEP_MS(10); - if (uCxAtClientHandleRx(pCtx->pClient) < 0) { - printf("Error in RX handling thread\n"); - exit(1); + int32_t waitResult = uPortUartWaitForData(pCtx->pClient->uartHandle, -1); + if (pCtx->terminateRxTask) { + break; + } + if ((waitResult < 0) || + ((waitResult > 0) && + (uCxAtClientHandleRxAvailable(pCtx->pClient) < 0))) { + U_CX_PORT_SLEEP_MS(100); } } @@ -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) { + 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 bbc298a..d1e7509 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" @@ -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; } @@ -106,8 +125,22 @@ 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); + close(pHandle->wakeReadFd); + close(pHandle->wakeWriteFd); free(pHandle); return NULL; } @@ -143,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; } @@ -155,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); } } @@ -179,6 +216,9 @@ int32_t uPortUartWrite(uPortUartHandle_t handle, } return -1; } + if (written == 0) { + return -1; + } totalWritten += (size_t)written; } @@ -196,25 +236,75 @@ 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; } 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 bd24b47..d641bb1 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 @@ -68,7 +69,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 +87,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; } /* ---------------------------------------------------------------- @@ -131,6 +132,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; } @@ -145,7 +147,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; } @@ -174,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; } @@ -184,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; @@ -223,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 } } @@ -272,8 +288,10 @@ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) } // If buffer full, drop the byte (could add overflow handling here) + uPortUartRxSignalFromIsr(); + // Restart reception - startRxInterrupt(gpUartHandle); + (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/project.yml b/project.yml index d83d840..77fa638 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: @@ -34,28 +21,27 @@ :test: - +:test/** - -:test/support + - -:test/posix/** + - -:test/no_os/** + - -:test/stm32/** + - -:test/windows/** :source: - src/** + :include: + - src/** - inc/** - ports/** + - -:ports/extra/** :support: - test/support :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_ @@ -78,16 +64,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 @@ -95,121 +79,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 diff --git a/src/u_cx_at_client.c b/src/u_cx_at_client.c index 7ae57a6..fc5bd62 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) @@ -152,11 +169,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 +202,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; @@ -229,7 +251,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 @@ -240,10 +262,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"); @@ -260,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; @@ -268,12 +290,11 @@ 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, - pClient->pConfig->timeoutMs); + &pBinRx->lengthBuf[pBinRx->rxHeaderCount], readLen, + timeoutMs); CHECK_READ_ERROR(pClient, readStatus); if (readStatus > 0) { pBinRx->rxHeaderCount += (uint8_t)readStatus; @@ -283,7 +304,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); @@ -298,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; @@ -309,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); } @@ -344,7 +366,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 @@ -358,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; @@ -370,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; @@ -378,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) { @@ -429,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) { @@ -484,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); @@ -503,23 +523,31 @@ 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; + 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->uartHandle = uPortUartOpen(pConfig->pUartDevName, baudRate, flowControl); - if (pClient->uartHandle == NULL) { - return U_CX_ERROR_IO; + U_CX_MUTEX_UNLOCK(pClient->cmdMutex); + if (ret == 0) { + uPortBgRxTaskCreate(pClient); } - - pClient->opened = true; - return 0; + return ret; } void uCxAtClientClose(uCxAtClient_t *pClient) @@ -528,12 +556,19 @@ void uCxAtClientClose(uCxAtClient_t *pClient) 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) { 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) @@ -726,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; } @@ -763,31 +798,45 @@ 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) { - if (!pClient->opened) { - return 0; - } - int32_t ret = 0; +#if U_CX_USE_URC_QUEUE == 1 + bool processQueuedUrcs; +#endif U_CX_MUTEX_LOCK(pClient->cmdMutex); - if (!pClient->executingCmd) { - int32_t parserRet = handleRxData(pClient); + if (pClient->opened && !pClient->executingCmd) { + int32_t parserRet = handleRxData(pClient, timeoutMs); if (parserRet == AT_PARSER_ERROR && pClient->status == U_CX_ERROR_IO) { ret = pClient->lastIoError; } } +#if U_CX_USE_URC_QUEUE == 1 + processQueuedUrcs = !pClient->isBinaryRx; +#endif U_CX_MUTEX_UNLOCK(pClient->cmdMutex); #if U_CX_USE_URC_QUEUE == 1 - processUrcs(pClient); + if (processQueuedUrcs) { + processUrcs(pClient); + } #endif 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; diff --git a/src/u_cx_at_urc_queue.c b/src/u_cx_at_urc_queue.c index 8418b73..4b7b11d 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,22 +123,32 @@ 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->isWrapped ? + pUrcQueue->readPos : pUrcQueue->bufferLen; + pUrcQueue->enqueueCapacity = segmentEnd - pUrcQueue->writePos; pUrcQueue->pEnqueueEntry = pEntry; ret = true; } else { // Not enough space available - U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); ret = false; } + U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); return ret; } @@ -107,27 +158,51 @@ 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); } 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); } @@ -140,8 +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) { - pEntry = (uUrcEntry_t *)&pUrcQueue->pBuffer[0]; + if ((pUrcQueue->usedBytes > 0) && + (pUrcQueue->pEnqueueEntry == NULL)) { + pEntry = (uUrcEntry_t *)&pUrcQueue->pBuffer[pUrcQueue->readPos]; } U_CX_MUTEX_UNLOCK(pUrcQueue->queueMutex); @@ -157,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); diff --git a/tasks.py b/tasks.py index 4ca20e0..f8ca120 100644 --- a/tasks.py +++ b/tasks.py @@ -76,6 +76,81 @@ 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 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.""" + 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 +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") + build_config = "Debug" + + c.run(f"cmake -S {test_dir} -B {build_dir}") + 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={ + '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.""" @@ -92,6 +167,37 @@ 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_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.""" + print("Cleaning STM32 port unit test artifacts...") + 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.""" @@ -111,10 +217,35 @@ 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') + +# 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') +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) 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) +test_ns.add_collection(windows_ns) # Create main namespace ns = Collection() 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..10665ba --- /dev/null +++ b/test/no_os/test_no_os_port.c @@ -0,0 +1,122 @@ +#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); + uPortUartRxSignalFromIsr(); + 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; +} 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..3c64734 --- /dev/null +++ b/test/posix/test_posix_port.c @@ -0,0 +1,374 @@ +#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 uCxAtClientHandleRxAvailable(uCxAtClient_t *pClient) +{ + if (!pClient->opened) { + return -1; + } + if (atomic_load(&gRxFailures) > 0) { + atomic_fetch_sub(&gRxFailures, 1); + return -1; + } + 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) +{ + 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 testBackgroundRxWakeAndStop(void) +{ + uCxAtClient_t client = {0}; + int masterFd; + char slaveName[128]; + uint8_t byte = 0x55; + atomic_store(&gRxCalls, 0); + atomic_store(&gRxFailures, 0); + + 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); + + CHECK(write(masterFd, &byte, sizeof(byte)) == (ssize_t)sizeof(byte)); + CHECK(sleepMs(30)); + CHECK(atomic_load(&gRxCalls) == 1); + + CHECK(sleepMs(30)); + CHECK(atomic_load(&gRxCalls) == 1); + + atomic_store(&gRxFailures, 1); + 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; +} + +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 wake and stop", testBackgroundRxWakeAndStop}, + {"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; +} 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..8ac9426 --- /dev/null +++ b/test/stm32/fake/FreeRTOS.h @@ -0,0 +1,19 @@ +#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)) +#define portYIELD_FROM_ISR(taskWoken) ((void)(taskWoken)) + +#endif diff --git a/test/stm32/fake/fake_freertos.c b/test/stm32/fake/fake_freertos.c new file mode 100644 index 0000000..2b686dd --- /dev/null +++ b/test/stm32/fake/fake_freertos.c @@ -0,0 +1,222 @@ +#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 pthread_mutex_t gNotifyMutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t gNotifyCondition = PTHREAD_COND_INITIALIZER; +static uint32_t gNotificationCount; + +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; + pthread_mutex_lock(&gNotifyMutex); + gNotificationCount = 0; + pthread_mutex_unlock(&gNotifyMutex); +} + +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); +} + +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; + 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..7c43347 --- /dev/null +++ b/test/stm32/fake/task.h @@ -0,0 +1,33 @@ +#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); +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 new file mode 100644 index 0000000..7ce24ce --- /dev/null +++ b/test/stm32/test_freertos_port.c @@ -0,0 +1,169 @@ +#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 uCxAtClientHandleRxAvailable(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); + 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(); + 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..f28906e --- /dev/null +++ b/test/stm32/test_stm32_uart.c @@ -0,0 +1,280 @@ +#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 uint32_t gRxSignals; + +void uPortUartRxSignalFromIsr(void) +{ + gRxSignals++; +} + +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(); + gRxSignals = 0; + 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) +{ + gRxSignals = 0; + 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(gRxSignals == 2); + 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; +} diff --git a/test/test_u_cx_at_client.c b/test/test_u_cx_at_client.c index ca56d1d..1d411f4 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, @@ -70,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 @@ -132,18 +173,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 +231,8 @@ void setUp(void) gPRxDataPtr = NULL; gRxDataLen = -1; gRxIoErrorCode = 0; + gRxBytesUntilError = 0; + gRxMaxReadSize = 0; gPTickSequence = NULL; uPortGetTickTimeMs_IgnoreAndReturn(0); @@ -186,6 +240,10 @@ void setUp(void) void tearDown(void) { + if (gClient.executingCmd) { + gClient.executingCmd = false; + U_CX_MUTEX_UNLOCK(gClient.cmdMutex); + } uCxAtClientClose(&gClient); uCxAtClientDeinit(&gClient); } @@ -266,6 +324,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 +458,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 +553,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 +611,136 @@ 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_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}; + 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 = 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) +{ + 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); + gRxMaxReadSize = 1; + + 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}; @@ -501,46 +760,107 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) 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); - } + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.callbackCount); +} - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); +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_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}; + 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)]; memcpy(&rxData[0], &strData[0], strlen(strData)); memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); 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}; - 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)); - } + setUrcCallbackExpectations(&gClient, NULL, expectedBinData, + sizeof(expectedBinData)); + uCxAtClientHandleRx(&gClient); + 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, 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, gUrcCallbackExpectations.callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(0, gRxDataLen); +} - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); +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" }; + + memcpy(rxData, strData, strlen(strData)); + memcpy(&rxData[strlen(strData)], binData, sizeof(binData)); + gClient.urcQueue.bufferLen = sizeof(uUrcEntry_t) + strlen(TEST_URC) + 1 + 5; + + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); uCxAtClientHandleRx(&gClient); + 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, gUrcCallbackExpectations.callbackCount); +} + +void test_uCxAtClientCmd_withInterleavedUrc_expectResponseAndCallback(void) +{ + char rxData[] = { TEST_URC "\r\n+MYRSP:123\r\nOK\r\n" }; + gPRxDataPtr = (uint8_t *)&rxData[0]; + gRxDataLen = strlen(rxData); + + 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, gUrcCallbackExpectations.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..e422053 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, @@ -66,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) { @@ -120,7 +160,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; @@ -140,6 +185,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); @@ -149,12 +195,17 @@ void setUp(void) gPRxDataPtr = NULL; gRxDataLen = -1; gRxIoErrorCode = 0; + gRxMaxReadSize = 0; uPortGetTickTimeMs_IgnoreAndReturn(0); } void tearDown(void) { + if (gClient.executingCmd) { + gClient.executingCmd = false; + U_CX_MUTEX_UNLOCK(gClient.cmdMutex); + } uCxAtClientClose(&gClient); uCxAtClientDeinit(&gClient); } @@ -165,42 +216,113 @@ void test_uCxAtClientHandleRx_withStringUrc_expectUrcCallback(void) 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); - } - - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(1, gUrcCallbackExpectations.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}; + 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)]; memcpy(&rxData[0], &strData[0], strlen(strData)); memcpy(&rxData[strlen(strData)], &binData[0], sizeof(binData)); gPRxDataPtr = &rxData[0]; - gRxDataLen += strlen(strData) + sizeof(binData); - - 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); + gRxDataLen = strlen(strData) + sizeof(binData); + gRxMaxReadSize = 1; + gClientConfig.rxBufferLen = strlen(TEST_URC) + 1 + 6; + + setUrcCallbackExpectations(&gClient, NULL, expectedBinData, + sizeof(expectedBinData)); + uCxAtClientHandleRx(&gClient); + 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, 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, gUrcCallbackExpectations.callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + 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" }; + + memcpy(rxData, strData, strlen(strData)); + memcpy(&rxData[strlen(strData)], binData, sizeof(binData)); + gClientConfig.rxBufferLen = strlen(TEST_URC) + 1 + 5; + + setUrcCallbackExpectations(&gClient, NULL, NULL, 0); + gPRxDataPtr = rxData; + gRxDataLen = sizeof(rxData); + uCxAtClientHandleRx(&gClient); + TEST_ASSERT_EQUAL(0, gUrcCallbackExpectations.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, gUrcCallbackExpectations.callbackCount); +} + +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]; + + 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; } - uCxAtClientSetUrcCallback(&gClient, urcCallback, NULL); + setUrcCallbackExpectations(&gClient, NULL, &firstTail[1], + sizeof(firstTail) - 1); + 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, gUrcCallbackExpectations.callbackCount); + TEST_ASSERT_FALSE(gClient.isBinaryRx); + TEST_ASSERT_EQUAL(0, gRxDataLen); + + uCxAtClientClose(&secondClient); + uCxAtClientDeinit(&secondClient); } diff --git a/test/test_u_cx_at_urc_queue.c b/test/test_u_cx_at_urc_queue.c index ef9f235..96e92d7 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" @@ -127,6 +128,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]; @@ -150,3 +166,222 @@ 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]; + uint8_t *pPayload; + + 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)); + TEST_ASSERT_EQUAL(0, + uCxAtUrcQueueEnqueueGetPayloadPtr(&queue, &pPayload)); + 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)); +} 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..57ec355 --- /dev/null +++ b/test/windows/fake/windows.h @@ -0,0 +1,140 @@ +#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 + +#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); +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 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 28eb129..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 @@ -35,6 +36,12 @@ #define U_RINGBUFFER_SIZE 128 #define TEST_DATA_SIZE (U_RINGBUFFER_SIZE * 2) +#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(); @@ -53,7 +60,25 @@ 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]; +}; + +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; }; /* ---------------------------------------------------------------- @@ -62,6 +87,70 @@ struct u_connect_client_port_fixture { 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)); + if (gUrcCapture.binaryDataLength > 0) { + 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 * -------------------------------------------------------------- */ @@ -79,7 +168,12 @@ 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; + fixture.config.timeoutMs = 10; // Initialize AT client uCxAtClientInit(&fixture.config, &fixture.client); @@ -100,6 +194,9 @@ 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)); + memset(&gUrcCapture, 0, sizeof(gUrcCapture)); + k_sem_reset(&gUrcReceived); gDisableRxWorker = true; @@ -221,4 +318,329 @@ 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_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(); + + 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_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_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