Cpp Platform Spike

The C++ version of the Framework for Integration Testing (FIT) was spiked by PaulChisholm. Thse are his notes.

We come neither to praise C++ not to bury it, but simply to implement the framework for it. One can argue C++ is better in some ways, one can argue it's worse; we argue that it's different, and needs to accepted as it is.

C++ has no built-in runtime reflection (a.k.a. introspection) facilities. There's no way to construct an object of a class whose name is known at runtime; even if there was, there's no universal base class that can be used to represent it. (You could put an arbitrary object's address in a void*; but having done that, there's no good way to get it back to a object.) There's no way to call a function whose name is known at runtime. One can build a table of strings and function pointers, but C++ won't do it for you.

C++ reflection is done at compile time! It can be done with preprocessor macros, or with templates; but it's often done with code generation. (How does a C++ programmer un-serialize an object that's been reduced to a bitstring? With a common base class, and a table of class names and derived classes. That table is usually generated from some description of all the serializable types.)

The C++ FIT implementation consists of two parts: a code generator that reads the FIT data (HTML tables) and generates the code to run the tests; and a C++ library that helps run the tests. The test output depends on the test input and the test program; the test program depends on the production and generated code; and the generated code depends on the test input. This is easily expressed in a makefile.

As a result, quite a lot of C++ FIT is implemented in Perl. (Maybe all???)

The input data (represented by the Parse class in the Java version) is read at code generation time; all but the test results are also written during code generation.

The TypeAdapter hierarchy supports comparing a value read from an input file with a value computed at runtime. The C++ version doesn't need "adapted" types; it can generate variables and values of the appropriate static types. (Maybe.)

Some additional notes:

o The goal is, not to write a framework that reads the input table, runs the tests, and writes the output table; but to write a framework that reads the input table, and then generates a program that runs the tests and writes the output table. The C++ implementation can be implemented in any language: Java, to reuse Ward's existing work; C++, to guarantee the implementation language is one the users know; or Perl, for productivity. I'm using Perl at the moment.

o The Parse class can be pretty much the same for the C++ implementation as for the Java implementation.

o The Fixture classes will generate code. Users will need to write Fixture subclasses for their particular tests; these subclasses should be written in the same implementation language as the rest of the C++ implementation.

o I haven't quite figured out yet what the equivalent of the TypeAdapter classes is.

At the moment (December 3), I don't have any code I think it would be useful to share. I can parse a table, and I have some of the trivial Fixture classes; but I don't yet generate any code.

Early February 2003: CppPlatformSpike

I want to emphasize that I have no exclusive right to the C++ implementation. If anyone wants to spend some time on a parallel effort, you're welcome to do so.

The FIT work (for all languages) is very promising, but brand new; as is the case with all technology, early adopters will have their work cut out for them, but the benefits may turn out to greatly outweigh the investment. --PaulChisholm

This is how one possible CppPlatform could work: to paraphrase Ward, by compiling the acceptance test files rather than interpreting them.

Consider the following acceptance test specification and its associated report file (which shows the HTML source of two of the interesting cells, because I don't know how to do fancier stuff inside Wiki tables). The first column is a value put in the global variable "x"; the second column is a value put in a global variable "y"; and the third column is the result of the function "compute()" that multiples the two global variables. (You can't tell that from the input or output files; those details would be in a fixture.)


22<td bgcolor="#cfffef">4</td>
34<td bgcolor="#ffcfcf">12 expected<hr>7 actual</td>

Shown below is a C++ program that could be generated from an acceptance test input file and some appropriate fixture(s). When compiled and run, this generated program would generate the acceptance test output file (on standard output). I cheated and generated the program by hand, to show what the fixtures would need to do.

Each of the output cells (<td.../td>) is in its own function, and would be in its own "compilation unit" (.c/.cpp source file, and its own object file) for reasons described below. The main function regurgitates the non-cell output, and calls the cell functions. Parse generates main(); fixtures generate the cell functions.

The fixtures don't need to get information about fields and function. The fixtures only have to know the names of the various elements, and how to produce code; the compiler does the heavy lifting.

Not shown here: what if you tried to run an acceptance test against code that hasn't been written, so the cell functions don't even compile? First, every cell function is in its own source file. Second, if that file can't compile, the makefile would compile an alternate source file (table0row0cell2_error.cpp) to the same object file (table0row0cell2.o), which would display a special color, the expected value, and some message that the cell couldn't be generated. The main generated file and all the cell function generated files (some of which may be standing in for other files that couldn't be compiled) would all get linked together into the program that generates the report HTML.

There are countless things I omitted, over simplified, or plain got wrong. I hope this is enough to demonstrate the approach I'm suggesting. --PaulChisholm

P.S: Style note: If a C or C++ compiler sees two or more string literals together, they're treated as one string literal. That is:

    puts("a\n" "b\n" c\n");
is the same as:
but (I hope) slightly more readable once you know what you're looking at.

    #include <stdio.h>

int x, y;

int compute() { return x + y; }

void table0row0column0() { fputs("<td colspan=3>Trivial</td>\n", stdout); }

void table0row1column0() { x = 2; fputs("<td>2</td>\n", stdout); }

void table0row1column1() { y = 2; fputs("<td>2</td>\n", stdout); }

void table0row1column2() { int value = compute(); if (value == 4) { fputs("<td bgcolor=\"#cfffef\">4</td>\n", stdout); } else { printf("<td bgcolor=\"#ffcfcf\">4 expected<hr>%d actual</td>\n", value); } }

void table0row2column0() { x = 3; fputs("<td>3</td>\n", stdout); }

void table0row2column1() { y = 4; fputs("<td>4</td>\n", stdout); }

void table0row2column2() { int value = compute(); if (value == 12) { fputs("<td bgcolor=\"#cfffef\">12</td>\n", stdout); } else { printf("<td bgcolor=\"#ffcfcf\">12 expected<hr>%d actual</td>\n", value); } }

int main() { fputs( "<h1>Example</h1>\n" "<table border cols=3 cellspacing=0 cellpadding=3>\n" " <tr>\n", stdout); table0row0column0(); fputs( " </tr>\n" " <tr>\n", stdout); table0row1column0(); table0row1column1(); table0row1column2(); fputs( " </tr>\n" " <tr>\n", stdout); table0row2column0(); table0row2column1(); table0row2column2(); fputs( " </tr>\n" "<table>\n", stdout); return 0; }


Last edited April 23, 2003
Return to WelcomeVisitors