-
Notifications
You must be signed in to change notification settings - Fork 112
Add mock test for rcutils/strerror #265
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
Conversation
test/test_strerror.cpp
Outdated
| #include <string> | ||
| #else | ||
| #include <string> | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brawner @wjwwood @clalancette this is a non-portable workaround for an issue @Blast545 and I stumbled upon. Turns out librcutls.so and this test's executable where linking against different strerror_r versions. Different libc versions, and different symbol names (strerror_r is actually a macro for the libc version librcutils.so links to on Ubuntu 20.04).
This seems like a significant pain point whenever mocking system libraries if it turns out such practices are widespread (and I'd think so). We need a way to ensure both test and library use and link against the same third party OR use a wrapper with a stable ABI instead of system libraries directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brawner @wjwwood @clalancette this is a non-portable workaround for an issue @Blast545 and I stumbled upon. Turns out
librcutls.soand this test's executable where linking against differentstrerror_rversions.
Huh, weird. Can you explain a little more of the details? I'm just trying to get a handle on how common this will be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
holiday mode OFF
Can you explain a little more of the details? I'm just trying to get a handle on how common this will be.
Maybe ELF dumps help.
Output fragment of readelf --relocs path/to/librcutils.so:
Relocation section '.rela.plt' at offset 0x3480 contains 107 entries:
Offset Info Type Sym. Value Sym. Name + Addend
...
0000000180c8 000e00000007 R_X86_64_JUMP_SLO 0000000000000000 __xpg_strerror_r@GLIBC_2.3.4 + 0
000000018358 007000000007 R_X86_64_JUMP_SLO 000000000000e250 rcutils_strerror + 0
...
Output fragment of readelf --relocs path/to/test_strerror:
Relocation section '.rela.plt' at offset 0x6780 contains 206 entries:
Offset Info Type Sym. Value Sym. Name + Addend
...
0000000799e8 001700000007 R_X86_64_JUMP_SLO 0000000000000000 strerror@GLIBC_2.2.5 + 0
000000079bd8 005700000007 R_X86_64_JUMP_SLO 0000000000000000 rcutils_strerror + 0
...
Relevant glibc string.h header section.
I don't know exactly why this happens, though the C++ runtime may have something do with it (librcutils.so is C only).
holiday mode on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Blast545 we need a solution for this. Playing with system library defines is brittle and non-portable.
Digging a bit, it turns out that it's libstdc++ that requires _GNU_SOURCE to be set (see here). It's admittedly a bad workaround. We could set _GNU_SOURCE when building rcutils on Linux. @clalancette @dirk-thomas @wjwwood for feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After talking to @hidmic about this offline, I offered this opinion:
I think so long as a) we do not require these GNU specific functions (graceful degradation to POSIX or Windows is possible) and b) they (the GNU functions) are available, then it is fine to use them or force it to use them.
I don't have a sense for any intrinsic drawback or advantage to using the GNU ones over the POSIX ones.
Like performance or security benefits, but they could convince me otherwise, if they exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ultimately, the problem's in g++, but the bug's been standing for 19 years and is tagged as wontfix. It also creates some fun booby traps in C++ code I wasn't aware off e.g. you can't use the stl and name a free function minor, it'll clash with some GNU macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the most part, I agree with @wjwwood 's assessment. My only concern is about booby-traps like @hidmic laid out in the last comment.
There are specific areas I'm worried about:
- The cross-platform area. If you use a non-portable GNU API by accident, will it obviously show up (fail to compile or link) on the other platforms?
- The name clash areas. As @hidmic found out, it looks like the GNU API is "wider" than the POSIX one. Is it obvious when you run into one of these clashes? Have we tried running a full CI run to see what happens?
Assuming we are happy with the answers to those questions, then yeah, I'm fine with going ahead with the GNU API on Linux.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cross-platform area. If you use a non-portable GNU API by accident, will it obviously show up (fail to compile or link) on the other platforms?
In general, yes. But there are other cases, more subtle, that we have to bear in mind.
The name clash areas. As @hidmic found out, it looks like the GNU API is "wider" than the POSIX one. Is it obvious when you run into one of these clashes? Have we tried running a full CI run to see what happens?
So far, rcutils builds and tests succeed locally. That does not guarantee we won't ever come across a name clash (with a macro, other redefinitions will be caught by the compiler).
Nevermind the choice we make here, I'd want to stress the fact that, because of what g++ does when it finds glibc in the system, in general we cannot ignore GNU API peculiarities. A C++ program using getopt is potentially broken already if these are not taken into account.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind the choice we make here, I'd want to stress the fact that, because of what
g++does when it findsglibcin the system, in general we cannot ignore GNU API peculiarities. A C++ program usinggetoptis potentially broken already if these are not taken into account.
Actually, that brings up another point to consider here. I don't remember the exact way to do it in cmake/ament, but I think we need to "export" -D_GNU_SOURCE to downstream packages so that they also compile properly. Does that sound right to you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's probably the safest bet, but I fear we're spreading the disease. Should a downstream package that uses rcl be exposed to GNU extensions? Looking at glibc documentation, _GNU_SOURCE introduces LFS extensions that are no-op in 64 bits but have side effects in 32 bits. If I'm in 32 bits, I understand POSIX limitations, and having a dependency hijacking my carefully crafted code is not good.
What if we just make sure we don't do anything stupid like using off_t in a function signature?
test/test_strerror.cpp
Outdated
| /* Tell the mock to return NULL and set errno to ENOMEM | ||
| whatever the given parameter is. */ | ||
| mmk_when( | ||
| strerror_r(mmk_any(int), mmk_any(char *), mmk_any(size_t) ), |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🎉
| errno = 2; | ||
| char error_string[1024]; | ||
| rcutils_strerror(error_string, sizeof(error_string)); | ||
| ASSERT_STREQ(error_string, "Failed to get error"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
test/test_strerror.cpp
Outdated
| // mmk_mock("strerror_r@rcutils", strerror_r_mock); | ||
| // mmk_mock("__xpg_strerror_r@self", strerror_r_mock); | ||
| // mmk_mock("strerror_r@self", strerror_r_mock); | ||
| mmk_mock("__xpg_strerror_r@lib:rcutils", strerror_r_mock); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Blast545 hmm the symbol is non-portable. Maybe something like:
| mmk_mock("__xpg_strerror_r@lib:rcutils", strerror_r_mock); | |
| mmk_mock(RCUTILS_STRINGIFY(strerror_r) "@lib:rcutils", strerror_r_mock); |
can do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean that we could define another macro similar to RCUTILS_STRINGIFY? I don't understand this suggestion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean to let the compiler find out what strerror_r actual symbol is. If strerror_r is a macro that expands to something else, that'll get resolved before RCUTILS_STRINGIFY turns it into a string literal. Then we rely on C to concatenate string literals (in C, "foo" " bar" is the same as writing "foobar") and voilà! We get to write it in a portable way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm, any ideas on how to do that? that would be great, not sure if the compiler is able to find that actual symbol name.
Without anything else, it resolves to "strerror_r@lib:rcutils". We could write a macro that returns the hardcoded generated name in each platform if we find how that name is generated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind what it expands to, I think it's more portable to let the compiler figure out what the symbol is instead of hardcoding it.
test/test_strerror.cpp
Outdated
| #include <string> | ||
| #else | ||
| #include <string> | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Blast545 we need a solution for this. Playing with system library defines is brittle and non-portable.
Digging a bit, it turns out that it's libstdc++ that requires _GNU_SOURCE to be set (see here). It's admittedly a bad workaround. We could set _GNU_SOURCE when building rcutils on Linux. @clalancette @dirk-thomas @wjwwood for feedback.
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
924e386 to
8338f9e
Compare
|
@hidmic I rewrote some of the tests to use the "then_return" functionality, I was worried about the cases that don't return any useful, but modify one of the passed arguments. Seems to be working really well! |
test/test_strerror.cpp
Outdated
| /* Tell the mock to return NULL and set errno to ENOMEM | ||
| whatever the given parameter is. */ | ||
| mmk_when( | ||
| strerror_r(mmk_any(int), mmk_any(char *), mmk_any(size_t) ), |
There was a problem hiding this comment.
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 🎉
|
BTW this PR needs a new name. |
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
|
@hidmic I addressed your suggestions, let me know what you think |
hidmic
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM pending green CI
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
Signed-off-by: Jorge Perez <[email protected]>
hidmic
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM !
|
@ros-pull-request-builder retest this please |
|
Tier 1 platforms green, PR job green + 1 reviewer, merging 🎉 |
* Add test using mimick vendored library * Add path to mimick_vendor * Link test to mimick library * Add multi-platform support * Reformat to separate different platform tests * Reformat to use strncpy to copy buffer * Change reset macro to take strerror_mock * Use mallock return type to reset function * Remove test_depend to mimick * Replace mock type * Change function strncpy to strncpy_s Signed-off-by: Jorge Perez <[email protected]>
* Add test using mimick vendored library * Add path to mimick_vendor * Link test to mimick library * Add multi-platform support * Reformat to separate different platform tests * Reformat to use strncpy to copy buffer * Change reset macro to take strerror_mock * Use mallock return type to reset function * Remove test_depend to mimick * Replace mock type * Change function strncpy to strncpy_s Signed-off-by: Jorge Perez <[email protected]>
This PR includes a test for the
rcutils_strerrorfunction that required making a mock of thestrerror_r / strerror_s.Signed-off-by: Jorge Perez [email protected]