At work I’m part of a team that’s responsible for maintaining a C codebase we didn’t write. This is not an uncommon occurrence in real-life software development, and it means that we don’t really know “our” codebase all that well. To make matters worse, there were no unit tests before we took over the project, so knowing what any function is supposed to do is… challenging.
So what’s a coder to do when confronted with a bug? I’ve come to use a technique I call valgrind-driven development. For those not in the know (i.e. not C or C++ programmers), valgrind is a tool that, amongst other things, lets you precisely find out where in the code memory is leaking, conditional jumps are done based on uninitialised values, etc., etc. The usefulness of valgrind and the address sanitizers in clang and gcc cannot be overstated – but what does that have to do with the problem at hand? Say you have this C function:
int do_stuff(struct Foo* foo, int i, struct Bar* bar);
I have no idea how this is supposed to work. The first thing I do? This (using Google Test and writing tests in C++14):
EXPECT_EQ(0, do_stuff(nullptr, 0, nullptr);
99.9% of the time this won’t work. But the reasons why aren’t documented anywhere. Usually passing null pointers is assumed to not happen. So the program blows up, and I add this to the function under test:
int do_stuff(struct Foo* foo, int i, struct Bar* bar) { assert(foo != NULL); assert(bar != NULL); //...
At least now the assumption is documented as assertions, and the next developer who comes along will know that it’s a violation of the contract to pass in NULLs. And, of course, that this function is only used internally. Functions at library boundaries have to check their arguments and fail nicely.
After the assertions have been added, the unit test will look like this:
Foo foo{}; Bar bar{}; EXPECT_EQ(0, do_stuff(&foo, 0, &bar);
This will usually blow up spectacularly as well, but now I have valgrind to tell me why. I carry on like this until there are no more valgrind errors, at which point I can actually start testing the functionality at hand. Along the way I’ve built up all the expected dependencies of the function under test, making explicit what was once implicit in the code. More often than not, I usually find other bugs lurking in the code just by trying to document, via unit tests, what the current behaviour is.
If you have to maintain C/C++ code you didn’t write, give valgrind-driven development a try.
Super cool!
Not to be noisy and demanding, but would it be possible for you to show an actual example from start to finish? I know it might be a lot of work..
Yeah, sorry, it’d be a lot of work. I think the post explains it well enough, let me know if you try it out and it doesn’t work for you.
[…] I started out with my valgrind-driven development and ended up filling up both structs with suitable values, and all the test did was assert on the […]