diff --git a/tools/bt/safe_unity/CMakeLists.txt b/tools/bt/safe_unity/CMakeLists.txt new file mode 100644 index 0000000000..8236d15549 --- /dev/null +++ b/tools/bt/safe_unity/CMakeLists.txt @@ -0,0 +1,13 @@ +# This component is not supported by ESP targets +if(${target} not STREQUAL "linux") + return() +endif() + +idf_component_register( + SRCS src/safe_unity.c + INCLUDE_DIRS include + REQUIRES unity +) + +target_compile_options(${COMPONENT_LIB} PUBLIC --coverage) +target_link_libraries(${COMPONENT_LIB} PUBLIC --coverage) diff --git a/tools/bt/safe_unity/README.md b/tools/bt/safe_unity/README.md new file mode 100644 index 0000000000..20f913bec9 --- /dev/null +++ b/tools/bt/safe_unity/README.md @@ -0,0 +1,144 @@ +# Safe Unity Test Runner + +A safe test execution wrapper for the Unity test framework in ESP-IDF projects, designed to prevent test crashes from terminating the entire test suite. + +## Overview + +The Safe Unity component provides isolated test execution for Unity framework tests by running each test in a separate child process. This isolation prevents crashes, segmentation faults, or other fatal errors in individual tests from affecting the test runner or other tests. + +## Features + +- **Process Isolation**: Each test runs in a separate child process +- **Crash Protection**: Handles common crash signals (SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS) +- **Code Coverage Support**: Integrates with gcov for code coverage collection +- **Detailed Reporting**: Provides clear test result reporting with crash detection +- **Linux Host Testing**: Specifically designed for ESP-IDF host-based unit testing + +## Supported Platforms + +- **Linux only**: This component is designed specifically for Linux host-based testing +- **ESP targets**: Not supported (component automatically returns for ESP targets) + +## Basic Usage + +### Simple Test Execution + +```c +#include "safe_unity.h" + +void test_example_function(void) +{ + TEST_ASSERT_EQUAL(42, my_function_that_returns_42()); +} + +void app_main(void) +{ + UNITY_BEGIN(); + + // Run test safely - crashes won't terminate the test runner + RUN_TEST_SAFE(test_example_function); + + UNITY_END(); +} +``` + +## API Reference + +### Macros + +#### `RUN_TEST_SAFE(func)` + +Safely executes a Unity test function in an isolated process. + +**Parameters:** +- `func`: Unity test function to execute + +**Example:** +```c +RUN_TEST_SAFE(my_test_function); +``` + +## How It Works + +### Process Isolation + +1. **Fork Process**: For each test, a child process is created using `fork()` +2. **Signal Handling**: The child process registers signal handlers for crash detection +3. **Test Execution**: The test runs with proper Unity setup/teardown in the child process +4. **Result Collection**: The parent process waits for the child and analyzes the exit status +5. **Coverage Flush**: Code coverage data is flushed before process termination + +### Signal Handling + +The component handles the following signals in child processes: + +- `SIGSEGV`: Segmentation fault +- `SIGABRT`: Abort signal +- `SIGFPE`: Floating point exception +- `SIGILL`: Illegal instruction +- `SIGBUS`: Bus error + +When any of these signals are received, the test is marked as crashed and the process exits gracefully after flushing coverage data. + +## Code Coverage Integration + +The component automatically integrates with gcov for code coverage collection: + +- Coverage data is flushed before each test process exits +- Both passing and crashing tests contribute to coverage statistics +- No additional configuration required when using `--coverage` flags + +## Build Configuration + +The component requires the following CMake configuration: + +```cmake +# In your project's CMakeLists.txt +if(${target} STREQUAL "linux") + idf_component_register( + SRCS "your_test.c" + INCLUDE_DIRS "." + REQUIRES safe_unity + ) +endif() +``` + +## Examples + +### Complete Test Suite + +```c +#include "safe_unity.h" + +void setUp(void) { + // Test setup code +} + +void tearDown(void) { + // Test cleanup code +} + +void test_normal_operation(void) { + TEST_ASSERT_EQUAL(42, my_function()); +} + +void test_edge_case(void) { + TEST_ASSERT_NULL(my_function_with_null_return()); +} + +void test_potential_crash(void) { + // This might crash in some conditions + my_risky_function(); + TEST_ASSERT_TRUE(true); +} + +void app_main(void) { + UNITY_BEGIN(); + + RUN_TEST_SAFE(test_normal_operation); + RUN_TEST_SAFE(test_edge_case); + RUN_TEST_SAFE(test_potential_crash); + + UNITY_END(); +} +``` diff --git a/tools/bt/safe_unity/include/safe_unity.h b/tools/bt/safe_unity/include/safe_unity.h new file mode 100644 index 0000000000..680c47c796 --- /dev/null +++ b/tools/bt/safe_unity/include/safe_unity.h @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "unity.h" + +/* Macros */ +#define RUN_TEST_SAFE(func) safe_unity_run_test(func, #func, __LINE__) + +/* Enums */ +enum { + SAFE_UNITY_TEST_PASSED, + SAFE_UNITY_TEST_FAILED, + SAFE_UNITY_TEST_CRASHED, +}; + +/* Public interfaces */ +void safe_unity_run_test(UnityTestFunction func, const char* func_name, const int func_line_num); diff --git a/tools/bt/safe_unity/src/safe_unity.c b/tools/bt/safe_unity/src/safe_unity.c new file mode 100644 index 0000000000..beb6f11f9d --- /dev/null +++ b/tools/bt/safe_unity/src/safe_unity.c @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/* Include headers */ +#include +#include +#include +#include +#include + +#include "safe_unity.h" + +/* Extern gcov exit hook */ +extern void __gcov_exit(void); + +/* Private function declarations */ +static void register_signal_handler(void); +static void signal_handler(int sig); +static void isolated_test_runner_child(UnityTestFunction func); +static void isolated_test_runner(UnityTestFunction func); + +/* Signal handling */ +static volatile sig_atomic_t signal_received = 0; + +/* Private functions */ +static void register_signal_handler(void) +{ + signal(SIGSEGV, signal_handler); + signal(SIGABRT, signal_handler); + signal(SIGFPE, signal_handler); + signal(SIGILL, signal_handler); + signal(SIGBUS, signal_handler); +} + +static void signal_handler(int sig) +{ + /* Prevent recursive signal handling */ + if (signal_received) { + _exit(SAFE_UNITY_TEST_CRASHED); + } + signal_received = 1; + + /* Flush gcov data */ + __gcov_exit(); + + /* Exit with code indicating crashed */ + _exit(SAFE_UNITY_TEST_CRASHED); +} + +static void isolated_test_runner_child(UnityTestFunction func) +{ + /* Register signal handlers */ + register_signal_handler(); + + /* Run the test */ + if (TEST_PROTECT()) { + setUp(); + func(); + } + if (TEST_PROTECT()) { + tearDown(); + } + + /* Flush gcov data */ + __gcov_exit(); + + /* Exit with code indicating test result */ + Unity.CurrentTestFailed ? _exit(SAFE_UNITY_TEST_FAILED) : _exit(SAFE_UNITY_TEST_PASSED); +} + +static void isolated_test_runner(UnityTestFunction func) +{ + /* Fork the process */ + pid_t pid = fork(); + + /* Fork failed */ + if (pid < 0) { + printf("[FAIL] Fork failed unexpectedly\n"); + Unity.CurrentTestFailed = 1; + return; + } + + /* Child process */ + if (pid == 0) { + isolated_test_runner_child(func); + + /* Should never reach here */ + _exit(0); + } + + /* Parent process */ + /* Wait for child process to finish */ + int status; + waitpid(pid, &status, 0); + + /* Child process exited */ + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + switch (exit_code) { + case SAFE_UNITY_TEST_PASSED: + /* Test passed */ + Unity.CurrentTestFailed = 0; + break; + case SAFE_UNITY_TEST_FAILED: + printf("\n[FAIL] Test failed"); + Unity.CurrentTestFailed = 1; + break; + case SAFE_UNITY_TEST_CRASHED: + /* Test crashed */ + printf("[FAIL] Test crashed"); + Unity.CurrentTestFailed = 1; + break; + default: + /* Unexpected exit code */ + Unity.CurrentTestFailed = 1; + printf("[FAIL] Test exited with unexpected code %d\n", exit_code); + break; + } + return; + } + + /* Child process terminated by signals that can not be captured */ + Unity.CurrentTestFailed = 1; + if (WIFSIGNALED(status)) { + printf("[CRASH] Test terminated by signal %d\n", WTERMSIG(status)); + } else { + printf("[CRASH] Test terminated with unknown reason\n"); + } +} + +/* Test runner */ +void safe_unity_run_test(UnityTestFunction func, const char* func_name, const int func_line_num) +{ + /* Announce test start */ + printf("========== Running Test: %s ==========\n", func_name); + + /* Update Unity singleton */ + Unity.CurrentTestName = func_name; + Unity.CurrentTestLineNumber = (UNITY_LINE_TYPE)func_line_num; + Unity.NumberOfTests++; + + /* Run test in isolated test runner */ + isolated_test_runner(func); + + /* Conclude test */ + UnityConcludeTest(); + + /* Announce test completion */ + if (Unity.CurrentTestFailed) { + printf("========== Test %s FAILED ==========\n\n", func_name); + } else { + printf("========== Test %s PASSED ==========\n\n", func_name); + } +}