yet another interesting TDD episode

In the previous episode I described how a custom assert function declared a local array which I did not initialize.
static void assert_fizz_buzz(const char * expected, int n)
{
    char actual[16];
    ...
}

The effect of not initializing the array was that state from one test leaked into another test and I got an unexpectedly passing test. I fixed the problem by initializing the array.
static void assert_fizz_buzz(const char * expected, int n)
{
    char actual[16] = "";
    ...
}

The most recent episode (in cyber-dojo naturally) revolved around this same issue. This time I was redoing the roman numerals exercise (1 → "I", 2 → "II", etc) in C. My custom assert function started like this.
...
#define PRINT(s) print_string(#s, s)

static void print_string(const char * name, const char * s)
{
    printf("%10s: \"%s\"\n", name, s);
}

static void assert_to_roman(const char * expected, int n)
{
    char actual[32];
    to_roman(actual, sizeof actual, n);
    if (strcmp(expected, actual) != 0)
    {
        printf("to_roman(%d) FAILED\n", n);
        PRINT(expected);
        PRINT(actual);
        assert(false);
    }
}

Once again I failed to initialize the array. I'm a slow learner. This time the test failed unexpectedly! And the diagnostic was even more unexpected:
to_roman(1) FAILED
  expected: "I"
    actual: "I"

This is a less than ideal diagnostic! It seemed that strcmp and printf had differing opinions on what a string is! I fixed it by adding initialization.
...
static void assert_to_roman(const char * expected, int n)
{
    char actual[32] = "";
    to_roman(actual, sizeof actual, n);
    if (strcmp(expected, actual) != 0)
    {
        printf("to_roman(%d) FAILED\n", n);
        PRINT(expected);
        PRINT(actual);
        assert(false);
    }
}

After this the tests passed. This addressed the immediate problem but it did not address to the root cause. So I removed the initialization and (with a hat tip to Mr Jonathon Wakely) I reworked print_string to display the length of the string as well as an indication of the (un)printability of each character:
static void print_string(const char * name, const char * s)
{
    printf("%10s: \"%s\" %d ", name, s, (int)strlen(s));
    for (size_t i = 0; i != strlen(s); i++)
    {
         putchar(isprint(s[i]) ? 'P' : 'U');   
    }
    putchar('\n');
}

With this change the diagnostic became:
to_roman(1) FAILED
  expected: "I" 1 P
    actual: "I" 4 UUUP

Much better. Then I thought about initializing the array a bit more. I realized that initializing the array to the empty string was a poor choice since it masked a fault in the implementation of to_roman which did not start like this:
void to_roman(char buffer[], size_t size, int n)
{
    buffer[0] = '\0';
    ...
}

So I added that and reworked print_string as follows:
static void assert_to_roman(const char * expected, int n)
{
    char actual[32];
    memset(actual, '!', sizeof actual);
    to_roman(actual, sizeof actual, n);
    ...
}

And I was back to green :-)

No comments:

Post a Comment