Skip to content
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

Fault injection macros and functionality (plus example) #264

Merged
merged 15 commits into from
Aug 19, 2020
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ set(rcutils_sources
src/strerror.c
src/string_array.c
src/string_map.c
src/testing/fault_injection.c
src/time.c
${time_impl_c}
src/uint8_array.c
Expand Down
1 change: 1 addition & 0 deletions Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ EXPAND_ONLY_PREDEF = YES
PREDEFINED += RCUTILS_PUBLIC=
PREDEFINED += RCUTILS_PUBLIC_TYPE=
PREDEFINED += RCUTILS_WARN_UNUSED=
PREDEFINED += RCUTILS_ENABLE_FAULT_INJECTION=

# Tag files that do not exist will produce a warning and cross-project linking will not work.
TAGFILES += "../../../doxygen_tag_files/cppreference-doxygen-web.tag.xml=http://en.cppreference.com/w/"
Expand Down
1 change: 1 addition & 0 deletions include/rcutils/error_handling.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ extern "C"
#include "rcutils/allocator.h"
#include "rcutils/macros.h"
#include "rcutils/snprintf.h"
#include "rcutils/testing/fault_injection.h"
#include "rcutils/types/rcutils_ret.h"
#include "rcutils/visibility_control.h"

Expand Down
49 changes: 49 additions & 0 deletions include/rcutils/macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,55 @@ extern "C"
# define RCUTILS_UNLIKELY(x) (x)
#endif // _WIN32

#if defined RCUTILS_ENABLE_FAULT_INJECTION
#include "rcutils/testing/fault_injection.h"

/**
* \def RCUTILS_CAN_RETURN_WITH_ERROR_OF
* Indicating macro that the function intends to return possible error value.
*
* Put this macro as the first line in the function. For example:
*
* int rcutils_function_that_can_fail() {
* RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);
* ... // rest of function
* }
*
* For now, this macro just simply calls `RCUTILS_MAYBE_RETURN_ERROR` if fault injection is
* enabled. However, for source code, the macro annotation `RCUTILS_CAN_RETURN_WITH_ERROR_OF` helps
* clarify that a function may return a value signifying an error and what those are.
*
* In general, you should only include a return value that originates in the function you're
* annotating instead of one that is merely passed on from a called function already annotated with
*`RCUTILS_CAN_RETURN_WITH_ERROR_OF`. If you are passing on return values from a called function,
* but that function is not annotated with `RCUTILS_CAN_RETURN_WITH_ERROR_OF`, then you might
* consider annotating that function first. If for some reason that is not desired or possible,
* then annotate your function as if the return values you are passing on originated from your
* function.
*
* If the function can return multiple return values indicating separate failure types, each one
* should go on a separate line.
*
* If in your function, there are expected effects on output parameters that occur during
* the failure case, then it will introduce a discrepency between fault injection testing and
* production operation. This is because the fault injection will cause the function to return
* where this macro is used, not at the location the error values are typically returned. To help
* protect against this scenario you may consider adding unit tests that checks your function does
* not modify output parameters when it actually returns a failing error code.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brawner I think this the a strong (the strongest?) constrain. This will cover 90% of our use cases, but, in the most general case, not all errors occur during early checks and not all errors can (should?) have zero observable side-effects. Could the macro take some statements to mimic such side effects if any?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that may be a reasonable extension to add. The difficulty though is that there may be multiple instances of returning the same error code in a function (for example RCUTILS_RET_ERROR or RCUTILS_BAD_ALLOC), then the side effects may be different depending on where it returns.

While many cases might have side effects after returning through an error case, my limited experience with this code base suggests those typically result in an undefined state more than an expected partially completed state.

I think for this case, we'll just have to build up a list of examples and what the requirements for those functions would be.

Copy link
Contributor

@hidmic hidmic Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I'd like to point out though that while:

While many cases might have side effects after returning through an error case, my limited experience with this code base suggests those typically result in an undefined state more than an expected partially completed state.

often holds true, IMHO it's bad practice. Really bad practice. If your program is left in an undefined state after returning an error, you may as well terminate it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(does) not modify output parameters when it actually returns a failing error code.

Meta: that's not always true or possible, finalization functions being a good counterexample. I don't think the limitation should be addressed here though (or ever if mocks help us cover those cases).

*
* If your function is void, this macro can be used without parameters. However, for the above
* reasoning, there should be no side effects on output parameters for all possible early returns.
*
* \param error_return_value the value returned as a result of an error. It does not need to be
* a rcutils_ret_t type. It could also be NULL, -1, a string error message, etc
*/
# define RCUTILS_CAN_RETURN_WITH_ERROR_OF(error_return_value) \
RCUTILS_MAYBE_RETURN_ERROR(error_return_value);

#else
# define RCUTILS_CAN_RETURN_WITH_ERROR_OF(error_return_value)
#endif // defined RCUTILS_ENABLE_FAULT_INJECTION

#ifdef __cplusplus
}
#endif
Expand Down
122 changes: 122 additions & 0 deletions include/rcutils/testing/fault_injection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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>
#ifdef __cplusplus
extern "C"
{
#endif

#define RCUTILS_FAULT_INJECTION_NEVER_FAIL -1

#define RCUTILS_FAULT_INJECTION_FAIL_NOW 0

bool rcutils_fault_injection_is_test_complete();

void _rcutils_set_fault_injection_count(int count);

int_least64_t _rcutils_get_fault_injection_count();

int _rcutils_maybe_fail();

#if defined RCUTILS_ENABLE_FAULT_INJECTION

/**
* \def RCUTILS_MAYBE_RETURN_ERROR
* \brief This macro checks and decrements a static global variable atomic counter and returns
* `return_value_on_error` if 0.
Copy link
Contributor

Choose a reason for hiding this comment

The 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 RCUTILS_CHECK_FAULT_SAFETY macro below taking an optional FLUKE or MASSIVE argument for each case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:

  1. Setting potential alternate behaviors when the counter is negative, which is what you are suggesting. I think the idea works great, it would just require writing multiple unit tests for each behavior.

  2. Instead of leaving it as -1, the RCUTILS_SET_FAULT_COUNT macro could have a variadic version which takes as parameters counts that it's reset to after it reaches 0.

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.

  1. For instances where testing two particular faults is important, then we could introduce a named fault injection. Instead of RCUTILS_CAN_RETURN_WITH_ERROR, there would be a RCUTILS_CAN_RETURN_WITH_ERROR_NAMED. Then in the unit test, we could target those locations to fail explicitly in a unit test.
RCUTILS_SET_FAULT_INJECTION_NAMED(...);
RCUTILS_SET_FAULT_INJECTION_NAMED(...);
... // Run test code

If 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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_SET_FAULT_INJECTION_COUNT`. If the count is less than 0, then
* `RCUTILS_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_SET_FAULT_INJECTION_COUNT`
*
* \param return_value_on_error the value to return in the case of fault injected failure.
*/
#define RCUTILS_MAYBE_RETURN_ERROR(return_value_on_error) \
if (RCUTILS_FAULT_INJECTION_FAIL_NOW == _rcutils_maybe_fail()) { \
printf( \
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why not having an RCUTILS_LOG_DEBUG call here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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__); \
return return_value_on_error; \
}

/**
* \def RCUTILS_SET_FAULT_INJECTION_COUNT
* \brief Atomically set the fault injection counter.
*
* There will be at most one fault injected failure per call to RCUTILS_SET_FAULT_INJECTION_COUNT.
* To test all reachable fault injection locations, call this macro inside a loop and set the count
* to an incrementing count variable.
*
* for (int i = 0; i < SUFFICIENTLY_LARGE_ITERATION_COUNT; ++i) {
* RCUTILS_SET_FAULT_INJECTION_COUNT(i);
* ... // Call function under test
* }
* ASSERT_LT(RCUTILS_FAULT_INJECTION_NEVER_FAIL, RCUTILS_GET_FAULT_INJECTION_COUNT());
*
* Where SUFFICIENTLY_LARGE_ITERATION_COUNT is a value larger than the maximum expected calls to
* `RCUTILS_MAYBE_RETURN_ERROR`. This last assertion just insures that your choice for
* SUFFICIENTLY_LARGE_ITERATION_COUNT was large enough. To avoid having to choose this count
* yourself, you can use a do-while loop.
*
* int i = 0;
* do {
* RCUTILS_SET_FAULT_INJECTION_COUNT(i++);
* ... // Call function under test
* } while (RCUTILS_GET_FAULT_INJECTION_COUNT() <= RCUTILS_FAULT_INJECTION_NEVER_FAIL);
*
* \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.
*/
#define RCUTILS_SET_FAULT_INJECTION_COUNT(count) \
_rcutils_set_fault_injection_count(count);

/**
* \def RCUTILS_GET_FAULT_INJECTION_COUNT
* \brief Atomically get the fault injection counter value
*
* Use this macro after running the code under test to check whether the counter reached a negative
* value. This is helpful so you can verify that you ran the fault injection test in a loop a
* sufficient number of times. Likewise, if the code under test returned with an error, but the
* count value was greater or equal to 0, then the failure was not caused by the fault injection
* counter.
*/
#define RCUTILS_GET_FAULT_INJECTION_COUNT() \
_rcutils_get_fault_injection_count();

#else // RCUTILS_ENABLE_FAULT_INJECTION

#define RCUTILS_SET_FAULT_INJECTION_COUNT(count)

// This needs to be set to an int for compatibility
#define RCUTILS_GET_FAULT_INJECTION_COUNT() RCUTILS_FAULT_INJECTION_NEVER_FAIL

#define RCUTILS_MAYBE_RETURN_ERROR(msg, error_statement)

#endif // defined RCUTILS_ENABLE_FAULT_INJECTION

#ifdef __cplusplus
}
#endif

#endif // RCUTILS__TESTING__FAULT_INJECTION_H_
5 changes: 5 additions & 0 deletions src/shared_library.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ C_ASSERT(sizeof(void *) == sizeof(HINSTANCE));
#endif // _WIN32

#include "rcutils/error_handling.h"
#include "rcutils/macros.h"
#include "rcutils/shared_library.h"
#include "rcutils/strdup.h"

Expand All @@ -46,6 +47,10 @@ rcutils_load_shared_library(
const char * library_path,
rcutils_allocator_t allocator)
{
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_BAD_ALLOC);
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_ERROR);

RCUTILS_CHECK_ARGUMENT_FOR_NULL(lib, RCUTILS_RET_INVALID_ARGUMENT);
RCUTILS_CHECK_ARGUMENT_FOR_NULL(library_path, RCUTILS_RET_INVALID_ARGUMENT);
RCUTILS_CHECK_ALLOCATOR(&allocator, return RCUTILS_RET_INVALID_ARGUMENT);
Expand Down
2 changes: 2 additions & 0 deletions src/snprintf.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ rcutils_snprintf(char * buffer, size_t buffer_size, const char * format, ...)
int
rcutils_vsnprintf(char * buffer, size_t buffer_size, const char * format, va_list args)
{
RCUTILS_CAN_RETURN_WITH_ERROR_OF(-1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main concern here is that we are going to litter our code with these macros. It's not the end of the world, but it is noisy and can make the code harder to read.

I have 2 separate suggestions:

  1. Before we start down this road, let's see what @hidmic's work on Mimick looks like. If we can make that work, that will obviate the need for this, I think (I'm currently ignoring the fact that Mimick doesn't work for C++, while this one would).
  2. If we can't get Mimick to work for some reason, another alternative is to use the LD_PRELOAD mechanism to override the symbols as needed. This is less desirable than Mimick since it is limited to Linux-only, but I think we can make the case (at least for now) that doing it on one platform is better than doing it on zero platforms.

The benefit to both solutions above is that there is no changes to the code, just changes to the tests. But I'd also like to hear what others think.

Copy link
Contributor Author

@brawner brawner Jul 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed this PR last night, and intended to give a more full writeup. Please take a look at the above PR description for some more information as well as possible extensions.

The approach taken in this PR allows for immediate cross-platform, cross-language utilization with simple unit tests with no external dependencies. You can write a unit test that easily verifies your code is hardened against failures in upstream libraries. See ros2/rcl_logging#48 for an example.

Another approach was to add an RCUTILS_CHECK_ macro that replaced simple if statements. Based on feedback from you and @wjwwood that suggested we want to be able to test the same code that we ship, I took this approach. The use of a macro like RCUTILS_CAN_RETURN_WITH_ERROR_OF allows for testing shipped production libraries against modified upstream libraries, which is not something our CI or buildfarm are setup for today but if desired could be feasibly updated in the future.

I took care in the design of RCUTILS_CAN_RETURN_WITH_ERROR_OF to ensure that it wasn't just litter. It actually serves an additional purpose of describing intended failure cases of the function. While docblocks typically try to capture the possible failures cases, this would be another approach that can also be integrated with unit testing. It may be difficult for a maintainer to see all the possible return values of a high level function that it passes on from lower level functions. However, with unit tests utilizing RCUTILS_MAYBE_RETURN_ERROR, you can verify that all possible return values are sufficiently understood.

This PR is intended as a starting point, and I want to make it as useful as possible. There are several possible feature additions that I think could fill it out, but I would like to get some more buy in as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for using potentially harsh language above. That wasn't my intention.

As this stands, I think you've done a good job of minimizing the impact of these macros on the "live" code (that is, the real code we are trying to test and that we ship). That is, I don't see how this method of mocking could be any simpler than adding a single macro line to any function/method that you want to mock out. As we've discussed elsewhere, this also sets the stage for having the macro enabled for our nightly and CI builds, but disabled for our packaging and debian builds.

My biggest concern is deploying this kind of change across a large number of functions and methods throughout the ROS 2 codebase. Adding things like this to every function places additional mental burden on contributors, as all readers have to understand why they are here and what they do. It also makes it somewhat difficult to move away from this solution in the future if we find a better way.

I want to be clear that I am not totally against this change. However, I think some of the other options we are pursuing don't have the downsides of this solution, so I'd prefer to investigate those other options first. If those solutions turn out not to work for one reason or another, this is an acceptable path forward.

Does that clear up my thinking on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, it does help clarify your thinking. My hope is that this sort of feature is used in conjunction with mocking like what @hidmic is investigating. That sort of testing would allow for more targeted failures, while this approach especially with the for loop example might be more like throwing a grenade at your code and seeing if it survives.

You're absolutely right that there are a large number of places where the RCUTILS_CAN_RETURN_WITH_ERROR_OF macro can be placed, and it would be overwhelming to put them in every single location at the outset, or even for every contribution. But I also think it's possible to add this macro in piecemeal, and we can start by only adding it in when it's needed for unit tests. For example, to push rcl_logging_spdlog to 100% coverage, you only need rcutils_snprintf to fail, but given the correct inputs that function probably never will on our supported platforms.

My suggestion with this sort of change is to start small and expand it as its role in this codebase is better understood. That's partially the reason why I only introduce one flavor of RCUTILS_CAN_RETURN_WITH_ERROR_OF, even though I also want a variadic version.

That macro is empty if fault injection is not enabled, which I think helps make it easier to remove in the future. The example unit test in rcl_logging_spdlog would pass if RCUTILS_CAN_RETURN_WITH_ERROR_OF did not exist at all. We could also properly deprecate RCUTILS_SET_FAULT_INJECTION_COUNT to help point external developers to the appropriate replacement.

I also agree that seeing this macro at the top of a function would be strange and unusual to many developers, particularly newer ROS 2 users who are unfamiliar with the code. While I tried to be careful about the macro name, I'm also very open to rewording RCUTILS_CAN_RETURN_WITH_ERROR_OF. I chose the wording so that it's hopefully clear to developers unfamiliar with it that doesn't change the nominal behavior of the function, while conveying more information about the intent of the function's behavior if you are familiar with it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before we start down this road, let's see what @hidmic's work on Mimick looks like. If we can make that work, that will obviate the need for this, I think (I'm currently ignoring the fact that Mimick doesn't work for C++, while this one would).

@clalancette I think this serves a different use case. Mocking is good for white-box unit testing, while this is performing black-box integration testing to see how the system, with all its side effects, behaves in an exceptional event (akin to exception safety verification). Could we use mocks to implement it? Yes, but we'd end up coupling a package to its dependencies' implementation and recreating side effects -- possible, but much harder.

IMHO, RCUTILS_CAN_RETURN_WITH_ERROR_OF, though atypical, isn't strictly worse than the prologue of RCUTILS_CHECK_ARGUMENT_FOR_NULL our C functions usually have.


if (NULL == format) {
errno = EINVAL;
return -1;
Expand Down
9 changes: 7 additions & 2 deletions src/strdup.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ extern "C"
{
#endif

#include "rcutils/strdup.h"

#include <stddef.h>
#include <string.h>

#include "./common.h"
#include "rcutils/macros.h"
#include "rcutils/strdup.h"


char *
rcutils_strdup(const char * str, rcutils_allocator_t allocator)
{
RCUTILS_CAN_RETURN_WITH_ERROR_OF(NULL);

if (NULL == str) {
return NULL;
}
Expand All @@ -36,6 +39,8 @@ rcutils_strdup(const char * str, rcutils_allocator_t allocator)
char *
rcutils_strndup(const char * str, size_t string_length, rcutils_allocator_t allocator)
{
RCUTILS_CAN_RETURN_WITH_ERROR_OF(NULL);

if (NULL == str) {
return NULL;
}
Expand Down
5 changes: 5 additions & 0 deletions src/string_array.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ rcutils_string_array_init(
size_t size,
const rcutils_allocator_t * allocator)
{
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_BAD_ALLOC);

if (NULL == allocator) {
RCUTILS_SET_ERROR_MSG("allocator is null");
return RCUTILS_RET_INVALID_ARGUMENT;
Expand All @@ -63,6 +66,8 @@ rcutils_string_array_init(
rcutils_ret_t
rcutils_string_array_fini(rcutils_string_array_t * string_array)
{
RCUTILS_CAN_RETURN_WITH_ERROR_OF(RCUTILS_RET_INVALID_ARGUMENT);

if (NULL == string_array) {
RCUTILS_SET_ERROR_MSG("string_array is null");
return RCUTILS_RET_INVALID_ARGUMENT;
Expand Down
60 changes: 60 additions & 0 deletions src/testing/fault_injection.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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.

#include "rcutils/testing/fault_injection.h"

#include "rcutils/stdatomic_helper.h"

static atomic_int_least64_t g_rcutils_fault_injection_count = -1;

bool rcutils_fault_injection_is_test_complete()
{
#ifndef RCUTILS_ENABLE_FAULT_INJECTION
return true;
#endif // RCUTILS_ENABLE_FAULT_INJECTION

return _rcutils_get_fault_injection_count() > RCUTILS_FAULT_INJECTION_NEVER_FAIL;
}

int _rcutils_maybe_fail()
{
bool set_atomic_success = false;
int_least64_t current_count = RCUTILS_FAULT_INJECTION_NEVER_FAIL;
rcutils_atomic_load(&g_rcutils_fault_injection_count, current_count);
do {
// A fault_injection_count less than 0 means that maybe_fail doesn't fail, so just return.
if (current_count <= RCUTILS_FAULT_INJECTION_NEVER_FAIL) {
return current_count;
}

// Otherwise decrement by one, but do so in a thread-safe manner so that exactly one calling
// thread gets the 0 case.
int_least64_t desired_count = current_count - 1;
rcutils_atomic_compare_exchange_strong(
&g_rcutils_fault_injection_count, set_atomic_success, &current_count, desired_count);
} while (!set_atomic_success);
return current_count;
}

void _rcutils_set_fault_injection_count(int count)
{
rcutils_atomic_store(&g_rcutils_fault_injection_count, count);
}

int_least64_t _rcutils_get_fault_injection_count()
{
int_least64_t count = 0;
rcutils_atomic_load(&g_rcutils_fault_injection_count, count);
return count;
}