Sunday, February 17, 2008

CTFE and testability

A lot of my code makes heavy use of CTFE. Really heavy. As in, more lines of code get executed at compile time than at runtime. How do I start to write unittests for it?

Answer: I don't.

That's rather harsh. Let's look at one case, or actually an abbreviated form of it. This is taken from DMocks:


string Methods (T, string name) () {
string methodBodies = "";
foreach (method; __traits(getVirtualFunctions, T, name)) {
alias typeof(method) func;
static if (is(ReturnType!(func) == void)) {
methodBodies ~= VoidMethod!(T.stringof, name, ParameterTypeTuple!(func));
} else {
methodBodies ~= ReturningMethod!(
T.stringof,
name,
ReturnType!(func),
ParameterTypeTuple!(func));
}
}
return methodBodies;
}

string ReturningMethod (string type, string name, T, U...)() {
string qualified = type ~ `.` ~ name;
string args = String!(U)();
string argArgs = Arguments!(U);
string nameArgs = `"` ~ qualified ~ (U.length == 0 ? `"` : `", ` ~ Arguments!(U)());
string retArgs = T.stringof ~ (U.length == 0 ? `` : `, ` ~ args);
return `some very long string that uses those strings I just assigned above`;
}
// VoidMethod looks pretty much like ReturningMethod.


That is terribly ugly, of course. If I could do it at runtime, I could introduce about two classes for the first template, then use tango.text.convert.Layout and public static format strings to deal with ReturningMethod. I'd do it with a format string looking like this:


public const string methodOutline = `{0} {1} ({2}) {{{
{{0}}
}}}`;
public const string methodBodyPart1 = `{0} {1} ...`;
public const string methodBodyPart2 = `{0} {1} ...`; // and so on


Of course, Layout won't work at compile time. Even if it did, I'd be left with a glob of functions to go through for my "unit" tests. But at that rate, I might as well complete the integration test by running the code that mixes in this giant string.

So I've been thinking of how to do it. The answer lies in two parts:

  1. Write a compile-time string formatting function.

  2. Implement any method I want to test as a string that I mix in.



The former makes it much easier to make sure the result string has the right arguments in the right place. The latter means I can test each in isolation by providing default implementations of templates that I'm not testing.

But for now, I've got some ugly, unmaintainable blobs of code to rewrite. The joy.

No comments: