Almost all of my recent coding efforts have gone into work on BMessage
. Early on, I decided that wanted to try a more "modern" back-end to
BMessage
, something that would leverage the power of the STL. The end result is out there enough that I'm not sure I want to use it. At any rate, I'm not going to talk about that right now; maybe in another article, if you're all good. ;)
The OpenBeOS project has a pretty serious policy concerning unit testing, and BMessage
is no different. BMessage
has a great many convenience functions for directly adding/finding/replacing data for common types. While it could be argued that all of these functions are similar enough that writing tests for one set would be good enough,
real programmers won't settle for anything less than complete coverage. =) For those of you who aren't actually writing code for OpenBeOS and haven't ever written unit tests, let me assure you that they are often mind-numbingly tedious to code. Now imagine the fun that lay in store for me as I write the same group of tests for each and every set of convenience functions: there are, so far, 8 tests for the various convenience functions, each of which needs to be implemented for
bool, int8, int16, int32, int64, float, double, char*, BString, BPoint, BRect, entry_ref, BMessage, BMessenger
, raw pointer and
BFlattenable
. Do the math and I've got 128 functions to implement. "What about copy-'n'-paste and search-'n'-replace?" you ask. For 120 functions?? I'd rather be gummed to death by a mob of toothless grandmothers!
My first idea was to procrastinate (during which time Nathan Whitehorn's Resourcer app got a lot of attention from me). My second idea was more productive: I thought I would write a light-weight scripting engine to generate the source for me. That actually made OK progress up to a point, but it felt clunky and it became clear that the engine would be difficult to extend. As it turns out, the third time was a charm, but before I get into that, a little background. On and off through 2002, I read a book called "Modern C++ Design: Generic Programming and Design Patterns Applied" by Andrei Alexandrescu. If, like me, you happen to think that C++ templates are the greatest thing since iced water or just plain want to melt large portions of your cortex, this book is the
bomb. The things that Mr. Alexandrescu does with templates is absolutely sick -- and eminently useful, if you are writing libraries. As it turns out, one of the techniques he discusses actually managed to lodge itself in my brain: policy-based class design. And now the fun begins.
The basic concept of policy-based class design is that certain bits of functionality in a class are grouped together into their own interface--the policy--which then becomes a template parameter for the class itself. Here's an example: let's suppose that you're writing a library of classes and you want to make the method of memory allocation for the library customizable. You could specify some base class which the user would then derive from to create his custom allocator. The problem with this approach becomes apparent in that you want your allocator class to handle different types (after all, this library contains
classes in the plural); something which is difficult to express through inheritance alone. The alternate approach is to make your allocator class templatized on the type it's going to allocate. Lifted straight from "Modern C++ Design" are two examples:
template struct OpNewCreator {
static T* Create() {
return new T;
}
};
template struct MallocCreator {
static T* Create() {
void* buf = std::malloc(sizeof(T));
if (!buf) return 0;
return new(buf) T;
}
};
The second allocator, MallocCreator, ensures that T can be a type with a constructor by using placement new on the malloc'd buffer. Now the issue becomes one of getting your library classes to use these allocators -- after all, they're not inherited from some base class, so you can't just go passing base pointers around. Again, templates ride to the rescue (also from "Modern C++ Design"):
template
class WidgetManager : public CreationPolicy {
...
};
Now if you want to use OpNewCreator for your allocation you simply do this:
typedef WidgetManager< OpNewCreator > MyWidgetMgr;
If you want to use malloc, just do the same, substituting MallocCreator instead. This concept works because the host class (WidgetManager
in this case) relies not on the policy class's type, but on its interface--static T* Create()
.
As I was getting my scripting engine to the point where it was almost generating tests for BMessage
's BRect
convenience functions correctly ("Just a few more hours of ruthless hacking!"), it occurred to me that I was looking at a series of policies:
- the set of convenience functions getting tested (add/find/replace, etc.)
- the initialization of variables (and arrays of variables)
- the assertion of results in the tests
Thrown on top of those policies were two other vital pieces of information: the type we were testing for (e.g., int32
) and it's type_code (e.g.,
B_INT32_TYPE
). Taken all together, we end up with a class declaration like this:
template
<
class Type,
type_code TypeCode,
class FuncPolicy,
class InitPolicy,
class AssertPolicy
>
class TMessageItemTest {
public:
void MessageItemTest1();
...
void MessageItemTest8();
};
The tests are a bit big to list one here in its entirety, but they look something like the following:
void MessageItemTest1() {
BMessage msg;
Type out = InitPolicy::Zero();
assert(FuncPolicy::Find(msg, "item", 0, &out) == B_NAME_NOT_FOUND);
assert(out == AssertPolicy::Invalid());
}
As I said, this isn't a full test, but it should give you an idea of how the policies are getting used. Just for clarity's sake, here's the same code, but written for a specific type (
int32
):
void MessageItemTest1() {
BMessage msg;
int32 out = 0;
assert(msg.Find("item", 0, &out) == B_NAME_NOT_FOUND);
assert(out == 0);
}
An interesting thing to note here is that for int32
, InitPolicy::Zero()
and AssertPolicy::Zero()
boil down to the same value: zero. Why not use one or the other in both places? As it turns out, for other types, "zero" and "invalid" are not the same thing. For instance, a "zero"
BRect
has left, top, right and bottom all set to 0. However, an invalid BRect
has left and top set to 0 and right and bottom set to -1. You might begin to see why scripting could have a hard time cutting the mustard.
As it turns out, for most types FuncPolicy
is conceptually identical: call the type's convenience function, passing in a name and value or index. In fact, they're so identical that we can create a policy-based class for that as well! Here's an abbreviated version:
template
<
typename Type,
status_t (BMessage::*AddFunc)(const char*, Type),
...
>
struct TMessageItemFuncPolicy {
static status_t Add(BMessage& msg, const char* name, Type& val) {
return (msg.*AddFunc)(name, val);
}
...
}
It you've been considering what the fully fleshed out code for all this looks like, you're probably thinking "This is getting pretty out of hand" by now, and if I had to do this more than once, I would absolutely agree. The beautiful thing, though, is that I only have to write the test code
once. After that, I can specify a full set of tests for a given type with just this much code:
typedef TMessageItemFuncPolicy
<
int32,
&BMessage::AddInt32,
&BMessage::FindInt32,
&BMessage::FindInt32, // The version that returns int32 directly
&BMessage::HasInt32,
&BMessage::ReplaceInt32
>
TInt32FuncPolicy;
struct TInt32InitPolicy {
inline static int32 Zero() { return 0; }
inline static int32 Test1() { return 1234; }
inline static int32 Test2() { return 5678; }
};
struct TInt32AssertPolicy {
inline static int32 Zero() { return 0; }
inline static int32 Invalid() { return 0; }
};
typedef TMessageItemTest
<
int32,
B_INT32_TYPE,
TInt32FuncPolicy,
TInt32InitPolicy,
TInt32AssertPolicy
>
TMessageInt32ItemTest;
There! In a little over 30 lines of code, I've just created nearly 400 lines of tests! Once that was done, it took less than half an hour to create the tests for about half of the types with
BMessage
convenience functions. The key to all this is that you're letting the compiler generate all the test code for you.
As I said before, I left a bit out, so if you have a burning curiosity (or twisted perversion, depending on your perspective) to know more, I encourage you to check out the various MessageXXXItemTest.h files in current/src/tests/kits/app/bmessage. If that doesn't cook your noodle, you just might be the very kind of sick monkey that would love "Modern C++ Design". If so, you definitely owe it to yourself to read it.
You know, I just noticed I haven't created the tests for floats or doubles yet--I think I'll spend the next 10 minutes banging out 800 lines worth of code!