Skip to content

Commit f514bd5

Browse files
committed
Merge branch 'feat/add_safe_unity_component' into 'master'
feat(ble): added safe unity component Closes BLERP-2272 See merge request espressif/esp-idf!42190
2 parents 105a64a + 64ef451 commit f514bd5

File tree

4 files changed

+332
-0
lines changed

4 files changed

+332
-0
lines changed

tools/bt/safe_unity/CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# This component is not supported by ESP targets
2+
if(${target} not STREQUAL "linux")
3+
return()
4+
endif()
5+
6+
idf_component_register(
7+
SRCS src/safe_unity.c
8+
INCLUDE_DIRS include
9+
REQUIRES unity
10+
)
11+
12+
target_compile_options(${COMPONENT_LIB} PUBLIC --coverage)
13+
target_link_libraries(${COMPONENT_LIB} PUBLIC --coverage)

tools/bt/safe_unity/README.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Safe Unity Test Runner
2+
3+
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.
4+
5+
## Overview
6+
7+
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.
8+
9+
## Features
10+
11+
- **Process Isolation**: Each test runs in a separate child process
12+
- **Crash Protection**: Handles common crash signals (SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS)
13+
- **Code Coverage Support**: Integrates with gcov for code coverage collection
14+
- **Detailed Reporting**: Provides clear test result reporting with crash detection
15+
- **Linux Host Testing**: Specifically designed for ESP-IDF host-based unit testing
16+
17+
## Supported Platforms
18+
19+
- **Linux only**: This component is designed specifically for Linux host-based testing
20+
- **ESP targets**: Not supported (component automatically returns for ESP targets)
21+
22+
## Basic Usage
23+
24+
### Simple Test Execution
25+
26+
```c
27+
#include "safe_unity.h"
28+
29+
void test_example_function(void)
30+
{
31+
TEST_ASSERT_EQUAL(42, my_function_that_returns_42());
32+
}
33+
34+
void app_main(void)
35+
{
36+
UNITY_BEGIN();
37+
38+
// Run test safely - crashes won't terminate the test runner
39+
RUN_TEST_SAFE(test_example_function);
40+
41+
UNITY_END();
42+
}
43+
```
44+
45+
## API Reference
46+
47+
### Macros
48+
49+
#### `RUN_TEST_SAFE(func)`
50+
51+
Safely executes a Unity test function in an isolated process.
52+
53+
**Parameters:**
54+
- `func`: Unity test function to execute
55+
56+
**Example:**
57+
```c
58+
RUN_TEST_SAFE(my_test_function);
59+
```
60+
61+
## How It Works
62+
63+
### Process Isolation
64+
65+
1. **Fork Process**: For each test, a child process is created using `fork()`
66+
2. **Signal Handling**: The child process registers signal handlers for crash detection
67+
3. **Test Execution**: The test runs with proper Unity setup/teardown in the child process
68+
4. **Result Collection**: The parent process waits for the child and analyzes the exit status
69+
5. **Coverage Flush**: Code coverage data is flushed before process termination
70+
71+
### Signal Handling
72+
73+
The component handles the following signals in child processes:
74+
75+
- `SIGSEGV`: Segmentation fault
76+
- `SIGABRT`: Abort signal
77+
- `SIGFPE`: Floating point exception
78+
- `SIGILL`: Illegal instruction
79+
- `SIGBUS`: Bus error
80+
81+
When any of these signals are received, the test is marked as crashed and the process exits gracefully after flushing coverage data.
82+
83+
## Code Coverage Integration
84+
85+
The component automatically integrates with gcov for code coverage collection:
86+
87+
- Coverage data is flushed before each test process exits
88+
- Both passing and crashing tests contribute to coverage statistics
89+
- No additional configuration required when using `--coverage` flags
90+
91+
## Build Configuration
92+
93+
The component requires the following CMake configuration:
94+
95+
```cmake
96+
# In your project's CMakeLists.txt
97+
if(${target} STREQUAL "linux")
98+
idf_component_register(
99+
SRCS "your_test.c"
100+
INCLUDE_DIRS "."
101+
REQUIRES safe_unity
102+
)
103+
endif()
104+
```
105+
106+
## Examples
107+
108+
### Complete Test Suite
109+
110+
```c
111+
#include "safe_unity.h"
112+
113+
void setUp(void) {
114+
// Test setup code
115+
}
116+
117+
void tearDown(void) {
118+
// Test cleanup code
119+
}
120+
121+
void test_normal_operation(void) {
122+
TEST_ASSERT_EQUAL(42, my_function());
123+
}
124+
125+
void test_edge_case(void) {
126+
TEST_ASSERT_NULL(my_function_with_null_return());
127+
}
128+
129+
void test_potential_crash(void) {
130+
// This might crash in some conditions
131+
my_risky_function();
132+
TEST_ASSERT_TRUE(true);
133+
}
134+
135+
void app_main(void) {
136+
UNITY_BEGIN();
137+
138+
RUN_TEST_SAFE(test_normal_operation);
139+
RUN_TEST_SAFE(test_edge_case);
140+
RUN_TEST_SAFE(test_potential_crash);
141+
142+
UNITY_END();
143+
}
144+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
#include "unity.h"
7+
8+
/* Macros */
9+
#define RUN_TEST_SAFE(func) safe_unity_run_test(func, #func, __LINE__)
10+
11+
/* Enums */
12+
enum {
13+
SAFE_UNITY_TEST_PASSED,
14+
SAFE_UNITY_TEST_FAILED,
15+
SAFE_UNITY_TEST_CRASHED,
16+
};
17+
18+
/* Public interfaces */
19+
void safe_unity_run_test(UnityTestFunction func, const char* func_name, const int func_line_num);
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
/* Include headers */
7+
#include <stdio.h>
8+
#include <signal.h>
9+
#include <setjmp.h>
10+
#include <sys/wait.h>
11+
#include <unistd.h>
12+
13+
#include "safe_unity.h"
14+
15+
/* Extern gcov exit hook */
16+
extern void __gcov_exit(void);
17+
18+
/* Private function declarations */
19+
static void register_signal_handler(void);
20+
static void signal_handler(int sig);
21+
static void isolated_test_runner_child(UnityTestFunction func);
22+
static void isolated_test_runner(UnityTestFunction func);
23+
24+
/* Signal handling */
25+
static volatile sig_atomic_t signal_received = 0;
26+
27+
/* Private functions */
28+
static void register_signal_handler(void)
29+
{
30+
signal(SIGSEGV, signal_handler);
31+
signal(SIGABRT, signal_handler);
32+
signal(SIGFPE, signal_handler);
33+
signal(SIGILL, signal_handler);
34+
signal(SIGBUS, signal_handler);
35+
}
36+
37+
static void signal_handler(int sig)
38+
{
39+
/* Prevent recursive signal handling */
40+
if (signal_received) {
41+
_exit(SAFE_UNITY_TEST_CRASHED);
42+
}
43+
signal_received = 1;
44+
45+
/* Flush gcov data */
46+
__gcov_exit();
47+
48+
/* Exit with code indicating crashed */
49+
_exit(SAFE_UNITY_TEST_CRASHED);
50+
}
51+
52+
static void isolated_test_runner_child(UnityTestFunction func)
53+
{
54+
/* Register signal handlers */
55+
register_signal_handler();
56+
57+
/* Run the test */
58+
if (TEST_PROTECT()) {
59+
setUp();
60+
func();
61+
}
62+
if (TEST_PROTECT()) {
63+
tearDown();
64+
}
65+
66+
/* Flush gcov data */
67+
__gcov_exit();
68+
69+
/* Exit with code indicating test result */
70+
Unity.CurrentTestFailed ? _exit(SAFE_UNITY_TEST_FAILED) : _exit(SAFE_UNITY_TEST_PASSED);
71+
}
72+
73+
static void isolated_test_runner(UnityTestFunction func)
74+
{
75+
/* Fork the process */
76+
pid_t pid = fork();
77+
78+
/* Fork failed */
79+
if (pid < 0) {
80+
printf("[FAIL] Fork failed unexpectedly\n");
81+
Unity.CurrentTestFailed = 1;
82+
return;
83+
}
84+
85+
/* Child process */
86+
if (pid == 0) {
87+
isolated_test_runner_child(func);
88+
89+
/* Should never reach here */
90+
_exit(0);
91+
}
92+
93+
/* Parent process */
94+
/* Wait for child process to finish */
95+
int status;
96+
waitpid(pid, &status, 0);
97+
98+
/* Child process exited */
99+
if (WIFEXITED(status)) {
100+
int exit_code = WEXITSTATUS(status);
101+
switch (exit_code) {
102+
case SAFE_UNITY_TEST_PASSED:
103+
/* Test passed */
104+
Unity.CurrentTestFailed = 0;
105+
break;
106+
case SAFE_UNITY_TEST_FAILED:
107+
printf("\n[FAIL] Test failed");
108+
Unity.CurrentTestFailed = 1;
109+
break;
110+
case SAFE_UNITY_TEST_CRASHED:
111+
/* Test crashed */
112+
printf("[FAIL] Test crashed");
113+
Unity.CurrentTestFailed = 1;
114+
break;
115+
default:
116+
/* Unexpected exit code */
117+
Unity.CurrentTestFailed = 1;
118+
printf("[FAIL] Test exited with unexpected code %d\n", exit_code);
119+
break;
120+
}
121+
return;
122+
}
123+
124+
/* Child process terminated by signals that can not be captured */
125+
Unity.CurrentTestFailed = 1;
126+
if (WIFSIGNALED(status)) {
127+
printf("[CRASH] Test terminated by signal %d\n", WTERMSIG(status));
128+
} else {
129+
printf("[CRASH] Test terminated with unknown reason\n");
130+
}
131+
}
132+
133+
/* Test runner */
134+
void safe_unity_run_test(UnityTestFunction func, const char* func_name, const int func_line_num)
135+
{
136+
/* Announce test start */
137+
printf("========== Running Test: %s ==========\n", func_name);
138+
139+
/* Update Unity singleton */
140+
Unity.CurrentTestName = func_name;
141+
Unity.CurrentTestLineNumber = (UNITY_LINE_TYPE)func_line_num;
142+
Unity.NumberOfTests++;
143+
144+
/* Run test in isolated test runner */
145+
isolated_test_runner(func);
146+
147+
/* Conclude test */
148+
UnityConcludeTest();
149+
150+
/* Announce test completion */
151+
if (Unity.CurrentTestFailed) {
152+
printf("========== Test %s FAILED ==========\n\n", func_name);
153+
} else {
154+
printf("========== Test %s PASSED ==========\n\n", func_name);
155+
}
156+
}

0 commit comments

Comments
 (0)