Skip to content

Commit 95ca950

Browse files
brawnerahcorde
authored andcommitted
Fault injection macros and functionality (plus example) (#264)
* Initial commit of fault injection macros and functionality Signed-off-by: Stephen Brawner <brawner@gmail.com> * Add can_return_with_failure to rcutils_vsnprintf Signed-off-by: Stephen Brawner <brawner@gmail.com> * Add getter for fault injection count Signed-off-by: Stephen Brawner <brawner@gmail.com> * Adjust headers Signed-off-by: Stephen Brawner <brawner@gmail.com> * Adding more fault injection locations Signed-off-by: Stephen Brawner <brawner@gmail.com> * Revert header strdup Signed-off-by: Stephen Brawner <brawner@gmail.com> * Adjust names Signed-off-by: Stephen Brawner <brawner@gmail.com> * Remove init/fini macros Signed-off-by: Stephen Brawner <brawner@gmail.com> * uncrustify fix Signed-off-by: Stephen Brawner <brawner@gmail.com> * Update definitions Signed-off-by: Stephen Brawner <brawner@gmail.com> * Change maybe_fail int int_least64 Signed-off-by: Stephen Brawner <brawner@gmail.com> * ATOMIC_VAR_INIT Signed-off-by: Stephen Brawner <brawner@gmail.com> * Move get/set to public functions Signed-off-by: Stephen Brawner <brawner@gmail.com> * Add RCUTILS_CAN_FAIL_WITH Signed-off-by: Stephen Brawner <brawner@gmail.com> * Updating comments Signed-off-by: Stephen Brawner <brawner@gmail.com>
1 parent 654c1e6 commit 95ca950

10 files changed

+327
-0
lines changed

CMakeLists.txt

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ set(rcutils_sources
6565
src/strerror.c
6666
src/string_array.c
6767
src/string_map.c
68+
src/testing/fault_injection.c
6869
src/time.c
6970
${time_impl_c}
7071
src/uint8_array.c
@@ -119,6 +120,10 @@ target_include_directories(${PROJECT_NAME} PUBLIC
119120
# which is appropriate when building the dll but not consuming it.
120121
target_compile_definitions(${PROJECT_NAME} PRIVATE "RCUTILS_BUILDING_DLL")
121122

123+
if(BUILD_TESTING AND NOT RCUTILS_DISABLE_FAULT_INJECTION)
124+
target_compile_definitions(${PROJECT_NAME} PUBLIC RCUTILS_ENABLE_FAULT_INJECTION)
125+
endif()
126+
122127
target_link_libraries(${PROJECT_NAME} ${CMAKE_DL_LIBS})
123128

124129
# Needed if pthread is used for thread local storage.

Doxyfile

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ EXPAND_ONLY_PREDEF = YES
2424
PREDEFINED += RCUTILS_PUBLIC=
2525
PREDEFINED += RCUTILS_PUBLIC_TYPE=
2626
PREDEFINED += RCUTILS_WARN_UNUSED=
27+
PREDEFINED += RCUTILS_ENABLE_FAULT_INJECTION=
2728

2829
# Tag files that do not exist will produce a warning and cross-project linking will not work.
2930
TAGFILES += "../../../doxygen_tag_files/cppreference-doxygen-web.tag.xml=http://en.cppreference.com/w/"

include/rcutils/error_handling.h

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ extern "C"
3636
#include "rcutils/allocator.h"
3737
#include "rcutils/macros.h"
3838
#include "rcutils/snprintf.h"
39+
#include "rcutils/testing/fault_injection.h"
3940
#include "rcutils/types/rcutils_ret.h"
4041
#include "rcutils/visibility_control.h"
4142

include/rcutils/macros.h

+69
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,75 @@ extern "C"
134134
# define RCUTILS_UNLIKELY(x) (x)
135135
#endif // _WIN32
136136

137+
#if defined RCUTILS_ENABLE_FAULT_INJECTION
138+
#include "rcutils/testing/fault_injection.h"
139+
140+
/**
141+
* \def RCUTILS_CAN_RETURN_WITH_ERROR_OF
142+
* Indicating macro that the function intends to return possible error value.
143+
*
144+
* Put this macro as the first line in the function. For example:
145+
*
146+
* int rcutils_function_that_can_fail() {
147+
* RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);
148+
* ... // rest of function
149+
* }
150+
*
151+
* For now, this macro just simply calls `RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR` if fault
152+
* injection is enabled. However, for source code, the macro annotation
153+
* `RCUTILS_CAN_RETURN_WITH_ERROR_OF` helps clarify that a function may return a value signifying
154+
* an error and what those are.
155+
*
156+
* In general, you should only include a return value that originates in the function you're
157+
* annotating instead of one that is merely passed on from a called function already annotated with
158+
*`RCUTILS_CAN_RETURN_WITH_ERROR_OF`. If you are passing on return values from a called function,
159+
* but that function is not annotated with `RCUTILS_CAN_RETURN_WITH_ERROR_OF`, then you might
160+
* consider annotating that function first. If for some reason that is not desired or possible,
161+
* then annotate your function as if the return values you are passing on originated from your
162+
* function.
163+
*
164+
* If the function can return multiple return values indicating separate failure types, each one
165+
* should go on a separate line.
166+
*
167+
* If in your function, there are expected effects on output parameters that occur during
168+
* the failure case, then it will introduce a discrepancy between fault injection testing and
169+
* production operation. This is because the fault injection will cause the function to return
170+
* where this macro is used, not at the location the error values are typically returned. To help
171+
* protect against this scenario you may consider adding unit tests that check your function does
172+
* not modify output parameters when it actually returns a failing error code if it's possible for
173+
* your code.
174+
*
175+
* If your function is void, this macro can be used without parameters. However, for the above
176+
* reasoning, there should be no side effects on output parameters for all possible early returns.
177+
*
178+
* \param error_return_value the value returned as a result of an error. It does not need to be
179+
* a rcutils_ret_t type. It could also be NULL, -1, a string error message, etc
180+
*/
181+
# define RCUTILS_CAN_RETURN_WITH_ERROR_OF(error_return_value) \
182+
RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR(error_return_value);
183+
184+
/**
185+
* \def RCUTILS_CAN_FAIL_WITH
186+
* Indicating macro similar to RCUTILS_CAN_RETURN_WITH_ERROR_OF but for use with more complicated
187+
* statements.
188+
*
189+
* The `failure_code` will be executed inside a scoped if block, so any variables declared within
190+
* will not be available outside of the macro.
191+
*
192+
* One example where you might need this version, is if a side-effect may occur within a function.
193+
* For example, in snprintf, rcutils_snprintf needs to set both errno and return -1 on failure.
194+
* This macro is used to capture both effects.
195+
*
196+
* \param failure_code Code that is representative of the failure case in this function.
197+
*/
198+
# define RCUTILS_CAN_FAIL_WITH(failure_code) \
199+
RCUTILS_FAULT_INJECTION_MAYBE_FAIL(failure_code);
200+
201+
#else
202+
# define RCUTILS_CAN_RETURN_WITH_ERROR_OF(error_return_value)
203+
# define RCUTILS_CAN_FAIL_WITH(failure_code)
204+
#endif // defined RCUTILS_ENABLE_FAULT_INJECTION
205+
137206
#ifdef __cplusplus
138207
}
139208
#endif
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright 2020 Open Source Robotics Foundation, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef RCUTILS__TESTING__FAULT_INJECTION_H_
16+
#define RCUTILS__TESTING__FAULT_INJECTION_H_
17+
#include <stdbool.h>
18+
#include <stdio.h>
19+
#include <stdint.h>
20+
21+
#include "rcutils/macros.h"
22+
#include "rcutils/visibility_control.h"
23+
24+
#ifdef __cplusplus
25+
extern "C"
26+
{
27+
#endif
28+
29+
#define RCUTILS_FAULT_INJECTION_NEVER_FAIL -1
30+
31+
#define RCUTILS_FAULT_INJECTION_FAIL_NOW 0
32+
33+
RCUTILS_PUBLIC
34+
RCUTILS_WARN_UNUSED
35+
bool
36+
rcutils_fault_injection_is_test_complete(void);
37+
38+
/**
39+
* \brief Atomically set the fault injection counter.
40+
*
41+
* This is typically not the preferred method of interacting directly with the fault injection
42+
* logic, instead use `RCUTILS_FAULT_INJECTION_TEST` instead.
43+
*
44+
* This function may also be used for pausing code inside of a `RCUTILS_FAULT_INJECTION_TEST` with
45+
* something like the following:
46+
*
47+
* RCUTILS_FAULT_INJECTION_TEST({
48+
* ... // code to run with fault injection
49+
* int64_t count = rcutils_fault_injection_get_count();
50+
* rcutils_fault_injection_set_count(RCUTILS_FAULT_INJECTION_NEVER_FAIL);
51+
* ... // code to run without fault injection
52+
* rcutils_fault_injection_set_count(count);
53+
* ... // code to run with fault injection
54+
* });
55+
*
56+
* \param count The count to set the fault injection counter to. If count is negative, then fault
57+
* injection errors will be disabled. The counter is globally initialized to
58+
* RCUTILS_FAULT_INJECTION_NEVER_FAIL.
59+
*/
60+
RCUTILS_PUBLIC
61+
void
62+
rcutils_fault_injection_set_count(int count);
63+
64+
/**
65+
* \brief Atomically get the fault injection counter value
66+
*
67+
* This function is typically not used directly but instead indirectly inside an
68+
* `RCUTILS_FAULT_INJECTION_TEST`
69+
*/
70+
RCUTILS_PUBLIC
71+
RCUTILS_WARN_UNUSED
72+
int_least64_t
73+
rcutils_fault_injection_get_count(void);
74+
75+
/**
76+
* \brief Implementation of fault injection decrementer
77+
*
78+
* This is included inside of macros, so it needs to be exported as a public function, but it
79+
* should not be used directly.
80+
*/
81+
RCUTILS_PUBLIC
82+
RCUTILS_WARN_UNUSED
83+
int_least64_t
84+
_rcutils_fault_injection_maybe_fail(void);
85+
86+
/**
87+
* \def RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR
88+
* \brief This macro checks and decrements a static global variable atomic counter and returns
89+
* `return_value_on_error` if 0.
90+
*
91+
* This macro is not a function itself, so when this macro returns it will cause the calling
92+
* function to return with the return value.
93+
*
94+
* Set the counter with `RCUTILS_FAULT_INJECTION_SET_COUNT`. If the count is less than 0, then
95+
* `RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR` will not cause an early return.
96+
*
97+
* This macro is thread-safe, and ensures that at most one invocation results in a failure for each
98+
* time the fault injection counter is set with `RCUTILS_FAULT_INJECTION_SET_COUNT`
99+
*
100+
* \param return_value_on_error the value to return in the case of fault injected failure.
101+
*/
102+
#define RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR(return_value_on_error) \
103+
if (RCUTILS_FAULT_INJECTION_FAIL_NOW == _rcutils_fault_injection_maybe_fail()) { \
104+
printf( \
105+
"%s:%d Injecting fault and returning " #return_value_on_error "\n", __FILE__, __LINE__); \
106+
return return_value_on_error; \
107+
}
108+
109+
/**
110+
* \def RCUTILS_FAULT_INJECTION_MAYBE_FAIL
111+
* \brief This macro checks and decrements a static global variable atomic counter and executes
112+
* `failure_code` if the counter is 0 inside a scoped block (any variables declared in
113+
* failure_code) will not be avaliable outside of this scoped block.
114+
*
115+
* This macro is not a function itself, so it will cause the calling function to execute the code
116+
* from within an if loop.
117+
*
118+
* Set the counter with `RCUTILS_FAULT_INJECTION_SET_COUNT`. If the count is less than 0, then
119+
* `RCUTILS_FAULT_INJECTION_MAYBE_FAIL` will not execute the failure code.
120+
*
121+
* This macro is thread-safe, and ensures that at most one invocation results in a failure for each
122+
* time the fault injection counter is set with `RCUTILS_FAULT_INJECTION_SET_COUNT`
123+
*
124+
* \param failure_code the code to execute in the case of fault injected failure.
125+
*/
126+
#define RCUTILS_FAULT_INJECTION_MAYBE_FAIL(failure_code) \
127+
if (RCUTILS_FAULT_INJECTION_FAIL_NOW == _rcutils_fault_injection_maybe_fail()) { \
128+
printf( \
129+
"%s:%d Injecting fault and executing " #failure_code "\n", __FILE__, __LINE__); \
130+
failure_code; \
131+
}
132+
133+
/**
134+
* \def RCUTILS_FAULT_INJECTION_TEST
135+
*
136+
* The fault injection macro for use with unit tests to check that `code` can tolerate injected
137+
* failures at all points along the execution path where the indicating macros
138+
* `RCUTILS_CAN_RETURN_WITH_ERROR_OF` and `RCUTILS_CAN_FAIL_WITH` are located.
139+
*
140+
* This macro is intended to be used within a gtest function macro like 'TEST', 'TEST_F', etc.
141+
*
142+
* `code` is executed within a do-while loop and therefore any variables declared within are in
143+
* their own scope block.
144+
*
145+
* Here's a simple example:
146+
* RCUTILS_FAULT_INJECTION_TEST(
147+
* rcl_ret_t ret = rcl_init(argc, argv, options, context);
148+
* if (RCL_RET_OK == ret)
149+
* {
150+
* ret = rcl_shutdown(context);
151+
* }
152+
* });
153+
*
154+
* In this example, you will need have conditional execution based on the return value of
155+
* `rcl_init`. If it failed, then it wouldn't make sense to call rcl_shutdown. In your own test,
156+
* there might be similar logic that requires conditional checks. The goal of writing this test
157+
* is less about checking the behavior is consistent, but instead that failures do not cause
158+
* program crashes, memory errors, or unnecessary memory leaks.
159+
*/
160+
#define RCUTILS_FAULT_INJECTION_TEST(code) \
161+
do { \
162+
int fault_injection_count = 0; \
163+
do { \
164+
rcutils_fault_injection_set_count(fault_injection_count++); \
165+
code; \
166+
} while (!rcutils_fault_injection_is_test_complete()); \
167+
rcutils_fault_injection_set_count(RCUTILS_FAULT_INJECTION_NEVER_FAIL); \
168+
} while (0)
169+
170+
#ifdef __cplusplus
171+
}
172+
#endif
173+
174+
#endif // RCUTILS__TESTING__FAULT_INJECTION_H_

src/shared_library.c

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ C_ASSERT(sizeof(void *) == sizeof(HINSTANCE));
2727
#endif // _WIN32
2828

2929
#include "rcutils/error_handling.h"
30+
#include "rcutils/macros.h"
3031
#include "rcutils/shared_library.h"
3132
#include "rcutils/strdup.h"
3233

@@ -46,6 +47,10 @@ rcutils_load_shared_library(
4647
const char * library_path,
4748
rcutils_allocator_t allocator)
4849
{
50+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);
51+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_BAD_ALLOC);
52+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_ERROR);
53+
4954
RCUTILS_CHECK_ARGUMENT_FOR_NULL(lib, RCUTILS_RET_INVALID_ARGUMENT);
5055
RCUTILS_CHECK_ARGUMENT_FOR_NULL(library_path, RCUTILS_RET_INVALID_ARGUMENT);
5156
RCUTILS_CHECK_ALLOCATOR(&allocator, return RCUTILS_RET_INVALID_ARGUMENT);

src/snprintf.c

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ rcutils_snprintf(char * buffer, size_t buffer_size, const char * format, ...)
3939
int
4040
rcutils_vsnprintf(char * buffer, size_t buffer_size, const char * format, va_list args)
4141
{
42+
RCUTILS_CAN_FAIL_WITH({errno = EINVAL; return -1;});
43+
4244
if (NULL == format) {
4345
errno = EINVAL;
4446
return -1;

src/strdup.c

+6
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ extern "C"
2323
#include <string.h>
2424

2525
#include "./common.h"
26+
#include "rcutils/macros.h"
27+
2628

2729
char *
2830
rcutils_strdup(const char * str, rcutils_allocator_t allocator)
2931
{
32+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(NULL);
33+
3034
if (NULL == str) {
3135
return NULL;
3236
}
@@ -36,6 +40,8 @@ rcutils_strdup(const char * str, rcutils_allocator_t allocator)
3640
char *
3741
rcutils_strndup(const char * str, size_t string_length, rcutils_allocator_t allocator)
3842
{
43+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(NULL);
44+
3945
if (NULL == str) {
4046
return NULL;
4147
}

src/string_array.c

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ rcutils_string_array_init(
4242
size_t size,
4343
const rcutils_allocator_t * allocator)
4444
{
45+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);
46+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_BAD_ALLOC);
47+
4548
if (NULL == allocator) {
4649
RCUTILS_SET_ERROR_MSG("allocator is null");
4750
return RCUTILS_RET_INVALID_ARGUMENT;
@@ -63,6 +66,8 @@ rcutils_string_array_init(
6366
rcutils_ret_t
6467
rcutils_string_array_fini(rcutils_string_array_t * string_array)
6568
{
69+
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);
70+
6671
if (NULL == string_array) {
6772
RCUTILS_SET_ERROR_MSG("string_array is null");
6873
return RCUTILS_RET_INVALID_ARGUMENT;

0 commit comments

Comments
 (0)