Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 46 additions & 44 deletions test/test_strerror.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ TEST(test_strerror, get_error) {
#endif
}

const char expected_error_msg[] = "Failed to get error";
/*
Define the blueprint of a mock identified by `strerror_r_proto`
strerror_r possible signatures:
Expand All @@ -67,78 +68,79 @@ TEST(test_strerror, get_error) {

#if defined(_WIN32)
mmk_mock_define(strerror_s_mock, errno_t, char *, rsize_t, errno_t);
#elif defined(_GNU_SOURCE) && (!defined(ANDROID) || __ANDROID_API__ >= 23)
mmk_mock_define(strerror_r_mock, char *, int, char *, size_t);
#else
mmk_mock_define(strerror_r_mock, int, int, char *, size_t);
#endif

const char expected_error_msg[] = "Failed to get error";
#if defined(_WIN32)
// Function to be called for (Case 3)
errno_t mocked_windows_strerror(char * buf, rsize_t bufsz, errno_t errnum)
{
(void) errnum;
unsigned char index_err = 0;

while (buf && bufsz--) {
buf[index_err] = expected_error_msg[index_err];
index_err++;
}
strncpy(buf, expected_error_msg, (size_t) bufsz);
return errnum;
}
#else
// Function to be called for (Case 1)

/* Mocking test example */
TEST(test_strerror, test_mock) {
/* Mock the strerror_s function in the current module using
the `strerror_s_mock` blueprint. */
mmk_mock(RCUTILS_STRINGIFY(strerror_s) "@lib:rcutils", strerror_s_mock);
/* Tell the mock to call mocked_windows_strerror instead*/
mmk_when(
strerror_s(mmk_any(errno_t), mmk_any(char *), mmk_any(rsize_t), mmk_any(errno_t)),
.then_call = (mmk_fn) mocked_windows_strerror);

// Set the error (not used by the mock)
errno = 2;
char error_string[1024];
rcutils_strerror(error_string, sizeof(error_string));
ASSERT_STREQ(error_string, "Failed to get error");
mmk_reset(strerror_s);
}

#elif defined(_GNU_SOURCE) && (!defined(ANDROID) || __ANDROID_API__ >= 23)
mmk_mock_define(strerror_r_mock, char *, int, char *, size_t);

char * mocked_gnu_strerror(int errnum, char * buf, size_t buflen)
{
(void) errnum;
const char error_msg[] = "Failed to get error";
unsigned char index_err = 0;
while (buf && buflen--) {
buf[index_err] = error_msg[index_err];
index_err++;
}
strncpy(buf, expected_error_msg, buflen);
return buf;
}
#endif

/* Mocking test example */
TEST(test_strerror, test_mock) {
/* Mock the strerror_r function in the current module using
the `strerror_r_mock` blueprint. */
#if defined(_WIN32)
mmk_mock(RCUTILS_STRINGIFY(strerror_s) "@lib:rcutils", strerror_s_mock);
#else
mmk_mock(RCUTILS_STRINGIFY(strerror_r) "@lib:rcutils", strerror_r_mock);
#endif
/* Tell the mock to return NULL and set errno to ENOMEM
whatever the given parameter is. */
#if defined(_WIN32)
mmk_when(
strerror_s(mmk_any(errno_t), mmk_any(char *), mmk_any(rsize_t), mmk_any(errno_t)),
.then_call = (mmk_fn) mocked_windows_strerror);
#elif defined(_GNU_SOURCE) && (!defined(ANDROID) || __ANDROID_API__ >= 23)
/* Tell the mock to call mocked_gnu_strerror instead */
mmk_when(
strerror_r(mmk_any(int), mmk_any(char *), mmk_any(size_t) ),
.then_call = (mmk_fn) mocked_gnu_strerror);

// Set the error (not used by the mock)
errno = 2;
char error_string[1024];
rcutils_strerror(error_string, sizeof(error_string));
ASSERT_STREQ(error_string, "Failed to get error");
mmk_reset(strerror_r);
}

#else
mmk_mock_define(strerror_r_mock, int, int, char *, size_t);
/* Mocking test example */
TEST(test_strerror, test_mock) {
/* Mock the strerror_r function in the current module using
the `strerror_r_mock` blueprint. */
mmk_mock(RCUTILS_STRINGIFY(strerror_r) "@lib:rcutils", strerror_r_mock);
/* Tell the mock to return NULL and set errno to EINVAL
whatever the given parameter is. */
mmk_when(
strerror_r(mmk_any(int), mmk_any(char *), mmk_any(size_t) ),
Copy link
Contributor

Choose a reason for hiding this comment

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

This sets the global behavior of the mocked function? Are there reasonable scenarios where you might want the default behavior of the mocked function generally, but have the mocked version for one specific call? How might you accomplish that?

Copy link
Contributor

@hidmic hidmic Jul 13, 2020

Choose a reason for hiding this comment

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

Yeah, that's something I thought about, but unfortunately Mimick mocks do not explicitly support that feature. You could recreate behavior by building a table of mmk_when expressions, but it's impractical and brittle. I only know of gMock for a framework that can do that though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Uh, turns out it has the feature and @Blast545 found it 🎉

.then_return = mmk_val(int, EINVAL));
#endif

// Now normal usage of the function returning unexpected EINVAL
// error for the internal strerror_r
// This works only for POSIX

// Set the error "No such file or directory"
// Set the error "No such file or directory" (not used by the mock)
errno = 2;
char error_string[1024];
rcutils_strerror(error_string, sizeof(error_string));
ASSERT_STREQ(error_string, "Failed to get error");
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm sure this is a case-by-case matter, but what risks are there for maintainers adding calls to the mocked function that sits between the unit test and the targeted invocation? It seems like unit tests that call high-level functions, but mock low-level functions would be pretty brittle, and you would need to mock functions whose number of uses in the code under test is unlikely to change. rcutils_strerror is a good example where the number of uses of strerror_r are unlikely to change. Whereas mocking something like rcutils_strndup during complex code like rclcpp::init would result in behaviors that are difficult to assert.

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 a fundamental risk, besides the maintenance burden that comes from white-box testing and the coupling between test and implementation it entails. It's important to bear in mind that mocking is not a tool for fault injection in a running system, but a way to unit test the interactions between the entity under test and its direct dependencies. In an rclcpp::init test, one would mock rcutils_strndup along with all other direct dependencies to ensure correct behavior.

#if defined(_WIN32)
mmk_reset(strerror_s);
#else
mmk_reset(strerror_r);
#endif
}

#endif