feat(ble): added safe unity component

This commit is contained in:
Zhou Xiao
2025-09-26 10:25:25 +08:00
parent d73cf17616
commit 64ef451586
4 changed files with 332 additions and 0 deletions

View File

@@ -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)

View File

@@ -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();
}
```

View File

@@ -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);

View File

@@ -0,0 +1,156 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/* Include headers */
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/wait.h>
#include <unistd.h>
#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);
}
}