mirror of
https://github.com/espressif/esp-idf.git
synced 2025-10-03 18:40:59 +02:00
feat(ble): added safe unity component
This commit is contained in:
13
tools/bt/safe_unity/CMakeLists.txt
Normal file
13
tools/bt/safe_unity/CMakeLists.txt
Normal 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)
|
144
tools/bt/safe_unity/README.md
Normal file
144
tools/bt/safe_unity/README.md
Normal 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();
|
||||
}
|
||||
```
|
19
tools/bt/safe_unity/include/safe_unity.h
Normal file
19
tools/bt/safe_unity/include/safe_unity.h
Normal 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);
|
156
tools/bt/safe_unity/src/safe_unity.c
Normal file
156
tools/bt/safe_unity/src/safe_unity.c
Normal 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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user