• No results found

Dual-Target Incompatibilities

In document Test.driven.development.for.Embedded.C (Page 111-116)

Embedded TDD Strategy

5.5 Dual-Target Incompatibilities

The world is real, not ideal. Consequently, there will be differences in and out of the target. To test production code in both environments, you need code that works the same in both environments. Let’s look at some of the portability problems you might encounter.

Runtime Libraries Have Bugs

Shocking but true, runtime libraries have bugs. A few years back, I was working with a client adopting TDD. We were porting the test harness to a new target processor. We ran into a little snag. The target version ofstrstr( ) did not behave like all other versions ofstrstr( ) before it.

CppUTest compiled with no problem. The unit test harness had its own suite of unit tests, so the first order of business was to run those tests in the target. We waited through the upload, we ran the tests, and then we got some odd failures. Hmmm, the tests ran fine on the development system but failed on the target. What could that be?

We fired up the in-target debugger and started stepping through the code. After some digging, we discovered thatstrstr( ), from the standard

Report erratum this copy is (P1.0 printing, April, 2011)

Download from Wow! eBook <www.wowebook.com>

DUAL-TARGETINCOMPATIBILITIES 112

Continuous Integration

Continuous integration is a companion practice of Test-Driven Development. In continuous integration, team members inte-grate and check in changes to their version control system main branch regularly, usually many times a day. As a precon-dition to check in, all tests must pass.

An automated build is needed for successful CI. It has to be easy to build the system. If the build is a tedious manual process with numerous mouse clicks and file copies, you won’t build as often as you should. The goal is a single command build.

In the dual-target approach suggested in this chapter, the test build must be automated. But that’s not the end of it. The pro-duction build should also be automated. In the mouse-heavy IDEs of today, this may take some doing.

With a single command build, you can automate the running of the build. The current state of the art is the continuous inte-gration server. The CI server watches for check-ins to the code repository and initiates a complete build and test sequence once the check-in is complete. If a build breaks or any test fail-ures occur, the team is notified usually by email. Fixing the build becomes the number-one responsibility of the team.

An embedded build would be done in two stages, first for the development system tests. If successful, the target build would run next. If your product deploys to more execution platforms, you would want a build for each.

CI is a risk reduction strategy and a time-saver. When develop-ers go for long periods of time without integrating, the difficulty and risk of the integration grows. Like TDD, if testing is hard, do it all the time—it gets easier. With CI the mind-set is similar. If inte-gration is hard, do it all the time. You avoid those long and error-prone code merges. Merges are smaller, and they are assisted by automated tests created via TDD.

There are good open source tools, such as CruiseControl and Husdon, to help automate CI builds and error notifications.

DUAL-TARGETINCOMPATIBILITIES 113

library, was not handling empty strings properly. Instead of working like strstr( ) does in the rest of the free world, where an empty string is contained in every string, this implementation reported that the empty string was not part of any string. We found a bug in the target compiler’s implementation of the standard library functionstrstr( )!

Once we understood the incompatibility, we modified the code so that it would pass on both systems, covering up thestrstr( ) bug. We introduced a new platform-specific function called PlatformSpecificStrStr( ). The GCC implementation looks like this:

int PlatformSpecificStrStr(const char * s, const char * other) const {

return strstr(s, other) != NULL;

}

The target compiler’s implementation covers up the bug so that all plat-forms work the same. It looks like this:

int PlatformSpecificStrStr(const char * s, const char * other) const {

//strstr on the XXXX processor library does not handle "" correctly //"" should be found in any string.

//The conditional logic works around that problem if (strlen(other) == 0)

return TRUE;

else if (strlen(s) == 0) return FALSE;

else

return strstr(s, other) != NULL;

}

We added the comment because the reason for the check was not obvi-ous. To someone who knows the standard C library, that code looks unneeded. The comment explains why the seemingly unnecessary con-ditional logic is there.

Interestingly enough, the existing production code was peppered with the same kludge to overcome the bug in the target library implementa-tion. Someone should have fixed that long ago.

Incompatible Header Files

Header file compatibility can be a significant portability problem. There can be different signatures, function names, defines, and include paths for essentially the same functionality. An example of this kind of incom-patibility is the safer versions of sprintf( ). In the Unix world, there is

Report erratum this copy is (P1.0 printing, April, 2011)

Download from Wow! eBook <www.wowebook.com>

DUAL-TARGETINCOMPATIBILITIES 114

snprintf( ), and in the Windows world there is _snprintf( ), two functions that do almost the same thing.

Many C programmers use conditional compilation to handle platform-specific code. I suggest you avoid conditional compilation because it makes a mess of the code. It also makes it hard to see what code is really compiled under various situations.

Rather than conditional compilation, you could use a platform-specific header file that can map names like this:

#define snprintf _snprintf

This kludge works, but it is ugly. There is a better way.

In the development of CppUTest, we decided to isolate platform-specific code in one place. Each platform has a directory to hold the platform-specific code. We created a header file that defines function prototypes for operations that have to be implemented in a platform-specific way.

Then we created an implementation for each platform, isolating it in a directory for that target. We used the compiler and linker, instead of the preprocessor.

We also created platform-independent test cases that describe how these functions must behave. For example, this is one of many test cases that define howsnprintf( ) should behave:

TEST(PlatformSpecificSprintf, OutputFitsInBuffer) {

char buf[10];

int count = PlatformSpecificSprintf(buf, sizeof buf, "%s", "12345");

STRCMP_EQUAL("12345", buf);

LONGS_EQUAL(5, count);

}

Here’s the prototype that was added to the header file, right next to the other platform-specific prototypes:

int PlatformSpecificSprintf(char *str, size_t size, const char *format, ...);

Starting with the Visual C++ implementation and its variable-length argument list support, the implementation looks like this:

int PlatformSpecificSprintf(char *str, size_t size, const char *format, ...) {

int result;

va_list args;

va_start(args, format);

memset(str, 0, size);

DUAL-TARGETINCOMPATIBILITIES 115

result = _vsnprintf( str, size-1, format, args);

va_end(args);

return result;

}

The gcc code is the same, with the exception of a missing underscore.

Both builds passed their tests. Before cutting over to the new code, we added another test to make sure both implementations behaved the same when the buffer is not big enough to hold the whole string. We did not want CppUTest to have a buffer overrun. Reading the Unix-defined behavior for vsnprintf( ), it says that if the output is truncated, the return value is the number of characters (excluding the trailing \0), which would have been written if space had been available. That led to this test, which passed just fine...with gcc.

TEST(SimpleString, PlatformSpecificSprintf_doesNotFit) {

char buf[10];

int count = PlatformSpecificSprintf(buf, sizeof buf, "%s", "12345678901");

STRCMP_EQUAL("123456789", buf);

LONGS_EQUAL(11, count);

}

However, the test failed for Visual C++. Visual C++ and GNU do not agree. Insufficient buffer space in Visual C++ results in a -1 return value.

Initially, we had no interest in the precision of the Unix-style return value, and neither did any of the rest of the CppUTest code. So, we dumbed down the gcc version to mimic the Visual C++ version like this:

int PlatformSpecificSprintf(char *str, size_t size, const char *format, ...) {

va_list args;

va_start(args, format);

size_t count = vsnprintf( str, size, format, args);

if (size < count) return -1;

else

return count;

}

Later, CppUTest needed the Unix-style return value for a new feature, andPlatformSpecificSprintf( ) had to evolve to meet the new need.

Report erratum this copy is (P1.0 printing, April, 2011)

Download from Wow! eBook <www.wowebook.com>

TESTING WITHHARDWARE 116

This incompatibility problem was solved by making a common inter-face that was independent of either platform’s native function and then implementing the function for each platform.

In the prior two examples, the solution to an incompatibility was to implement a form of an adapter in C. An adapter converts the interface needed by the client to the interface provided by the server. This is a common pattern for solving platform-independence problems. It is described in the book Design Patterns [GHJV95]. The adapter pattern is very helpful for managing dependencies between code you control and code you don’t control.

In document Test.driven.development.for.Embedded.C (Page 111-116)