-
Notifications
You must be signed in to change notification settings - Fork 112
Fault injection macros and functionality (plus example) #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
153f42e
e9cc019
97dc369
a4f74ae
adad3bb
e86483a
e083a0d
203747d
9a72ee0
f5bb491
36050ca
2e99f4c
75009d9
b03d89e
6a5daad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| // Copyright 2020 Open Source Robotics Foundation, Inc. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| #ifndef RCUTILS__TESTING__FAULT_INJECTION_H_ | ||
| #define RCUTILS__TESTING__FAULT_INJECTION_H_ | ||
| #include <stdbool.h> | ||
| #include <stdio.h> | ||
| #include <stdint.h> | ||
|
|
||
| #include "rcutils/macros.h" | ||
| #include "rcutils/visibility_control.h" | ||
|
|
||
| #ifdef __cplusplus | ||
| extern "C" | ||
| { | ||
| #endif | ||
|
|
||
| #define RCUTILS_FAULT_INJECTION_NEVER_FAIL -1 | ||
|
|
||
| #define RCUTILS_FAULT_INJECTION_FAIL_NOW 0 | ||
|
|
||
| RCUTILS_PUBLIC | ||
| RCUTILS_WARN_UNUSED | ||
| bool | ||
| rcutils_fault_injection_is_test_complete(void); | ||
|
|
||
| /** | ||
| * \brief Atomically set the fault injection counter. | ||
| * | ||
| * This is typically not the preferred method of interacting directly with the fault injection | ||
| * logic, instead use `RCUTILS_FAULT_INJECTION_TEST` instead. | ||
| * | ||
| * This function may also be used for pausing code inside of a `RCUTILS_FAULT_INJECTION_TEST` with | ||
| * something like the following: | ||
| * | ||
| * RCUTILS_FAULT_INJECTION_TEST({ | ||
| * ... // code to run with fault injection | ||
| * int64_t count = rcutils_fault_injection_get_count(); | ||
| * rcutils_fault_injection_set_count(RCUTILS_FAULT_INJECTION_NEVER_FAIL); | ||
| * ... // code to run without fault injection | ||
| * rcutils_fault_injection_set_count(count); | ||
| * ... // code to run with fault injection | ||
| * }); | ||
| * | ||
| * \param count The count to set the fault injection counter to. If count is negative, then fault | ||
| * injection errors will be disabled. The counter is globally initialized to | ||
| * RCUTILS_FAULT_INJECTION_NEVER_FAIL. | ||
| */ | ||
| RCUTILS_PUBLIC | ||
| void | ||
| rcutils_fault_injection_set_count(int count); | ||
|
|
||
| /** | ||
| * \brief Atomically get the fault injection counter value | ||
| * | ||
| * This function is typically not used directly but instead indirectly inside an | ||
| * `RCUTILS_FAULT_INJECTION_TEST` | ||
| */ | ||
| RCUTILS_PUBLIC | ||
| RCUTILS_WARN_UNUSED | ||
| int_least64_t | ||
| rcutils_fault_injection_get_count(void); | ||
|
|
||
| /** | ||
| * \brief Implementation of fault injection decrementer | ||
| * | ||
| * This is included inside of macros, so it needs to be exported as a public function, but it | ||
| * should not be used directly. | ||
| */ | ||
| RCUTILS_PUBLIC | ||
| RCUTILS_WARN_UNUSED | ||
| int_least64_t | ||
| _rcutils_fault_injection_maybe_fail(void); | ||
|
|
||
| /** | ||
| * \def RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR | ||
| * \brief This macro checks and decrements a static global variable atomic counter and returns | ||
| * `return_value_on_error` if 0. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @brawner one thing this scheme cannot do is force an error during cleanup after one or more errors have been forced. What about if, in addition to forcing a single function to fail, we make all following functions fail as well? Like a flag to prevent the count to ever be decremented below 0. I envision the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are some examples where a failure in one location is likely to result in a failure in another location. For example, allocations may continue to fail, or file system functions may continue to fail. For the sake of coverage, there are locations where it would take two failures to reach some lines of code. Your example of cleanup is a good one. I think there are a few of potential solutions:
RCUTILS_SET_FAULT_COUNT(0, 0)For example, would trip a second failure immediately after its first failure. This would let you test more types of failures, and then we could use this type of macro to check that code is not only one-fault tolerant, but two-fault tolerant, three-fault tolerant etc.
RCUTILS_SET_FAULT_INJECTION_NAMED(...);
RCUTILS_SET_FAULT_INJECTION_NAMED(...);
... // Run test codeIf we do introduce this one at a later date, I could see it replacing the unnamed version macro entirely. This idea requires multiple unit tests for each targeted failed injection, but is also the only option that allows for targeted injections in the first place. Do you have any thoughts about the other options suitability for the situation you are describing?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think solution (2.) would be best. We don't really know what we're hitting, so N-fault tolerant sounds as good as it gets.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be added in a followup PR when the functionality becomes necessary for better coverage. |
||
| * | ||
| * This macro is not a function itself, so when this macro returns it will cause the calling | ||
| * function to return with the return value. | ||
| * | ||
| * Set the counter with `RCUTILS_FAULT_INJECTION_SET_COUNT`. If the count is less than 0, then | ||
| * `RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR` will not cause an early return. | ||
| * | ||
| * This macro is thread-safe, and ensures that at most one invocation results in a failure for each | ||
| * time the fault injection counter is set with `RCUTILS_FAULT_INJECTION_SET_COUNT` | ||
| * | ||
| * \param return_value_on_error the value to return in the case of fault injected failure. | ||
| */ | ||
| #define RCUTILS_FAULT_INJECTION_MAYBE_RETURN_ERROR(return_value_on_error) \ | ||
| if (RCUTILS_FAULT_INJECTION_FAIL_NOW == _rcutils_fault_injection_maybe_fail()) { \ | ||
| printf( \ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has been a very helpful error message to print out while debugging tests. Is there not a way to use a logging macro here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see why not having an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RCUTILS_LOG_DEBUG is not defined inside the rcutils library unfortunately. |
||
| "%s:%d Injecting fault and returning " #return_value_on_error "\n", __FILE__, __LINE__); \ | ||
brawner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return return_value_on_error; \ | ||
| } | ||
|
|
||
| /** | ||
| * \def RCUTILS_FAULT_INJECTION_MAYBE_FAIL | ||
| * \brief This macro checks and decrements a static global variable atomic counter and executes | ||
| * `failure_code` if the counter is 0 inside a scoped block (any variables declared in | ||
| * failure_code) will not be avaliable outside of this scoped block. | ||
| * | ||
| * This macro is not a function itself, so it will cause the calling function to execute the code | ||
| * from within an if loop. | ||
| * | ||
| * Set the counter with `RCUTILS_FAULT_INJECTION_SET_COUNT`. If the count is less than 0, then | ||
| * `RCUTILS_FAULT_INJECTION_MAYBE_FAIL` will not execute the failure code. | ||
| * | ||
| * This macro is thread-safe, and ensures that at most one invocation results in a failure for each | ||
| * time the fault injection counter is set with `RCUTILS_FAULT_INJECTION_SET_COUNT` | ||
| * | ||
| * \param failure_code the code to execute in the case of fault injected failure. | ||
| */ | ||
| #define RCUTILS_FAULT_INJECTION_MAYBE_FAIL(failure_code) \ | ||
| if (RCUTILS_FAULT_INJECTION_FAIL_NOW == _rcutils_fault_injection_maybe_fail()) { \ | ||
| printf( \ | ||
| "%s:%d Injecting fault and executing " #failure_code "\n", __FILE__, __LINE__); \ | ||
| failure_code; \ | ||
| } | ||
|
|
||
| /** | ||
| * \def RCUTILS_FAULT_INJECTION_TEST | ||
| * | ||
| * The fault injection macro for use with unit tests to check that `code` can tolerate injected | ||
| * failures at all points along the execution path where the indicating macros | ||
| * `RCUTILS_CAN_RETURN_WITH_ERROR_OF` and `RCUTILS_CAN_FAIL_WITH` are located. | ||
| * | ||
| * This macro is intended to be used within a gtest function macro like 'TEST', 'TEST_F', etc. | ||
| * | ||
| * `code` is executed within a do-while loop and therefore any variables declared within are in | ||
| * their own scope block. | ||
| * | ||
| * Here's a simple example: | ||
| * RCUTILS_FAULT_INJECTION_TEST( | ||
| * rcl_ret_t ret = rcl_init(argc, argv, options, context); | ||
| * if (RCL_RET_OK == ret) | ||
| * { | ||
| * ret = rcl_shutdown(context); | ||
| * } | ||
| * }); | ||
| * | ||
| * In this example, you will need have conditional execution based on the return value of | ||
| * `rcl_init`. If it failed, then it wouldn't make sense to call rcl_shutdown. In your own test, | ||
| * there might be similar logic that requires conditional checks. The goal of writing this test | ||
| * is less about checking the behavior is consistent, but instead that failures do not cause | ||
| * program crashes, memory errors, or unnecessary memory leaks. | ||
| */ | ||
| #define RCUTILS_FAULT_INJECTION_TEST(code) \ | ||
| do { \ | ||
| int fault_injection_count = 0; \ | ||
| do { \ | ||
| rcutils_fault_injection_set_count(fault_injection_count++); \ | ||
| code; \ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @brawner nice! I still think we need a way to disable fault injection temporarily within the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That can be done by way of: Do you think we need something more like:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, some sugar would be nice. I'd personally like to have something along the lines of: RCUTILS_FAULT_INJECTION_NO_INJECT({
// ...
});
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think your solution will introduce scoping issues, but I still agree that a potential use case along these lines may be useful. As I currently don't need it for the existing packages, I'll save this for a followup PR. |
||
| } while (!rcutils_fault_injection_is_test_complete()); \ | ||
| rcutils_fault_injection_set_count(RCUTILS_FAULT_INJECTION_NEVER_FAIL); \ | ||
| } while (0) | ||
|
|
||
| #ifdef __cplusplus | ||
| } | ||
| #endif | ||
|
|
||
| #endif // RCUTILS__TESTING__FAULT_INJECTION_H_ | ||
Uh oh!
There was an error while loading. Please reload this page.