2009-10-13

Back to C: CMake and CUnit

I'm back at programming in C. I proposed something 8 months ago at work that was totally ignore. Now my boss wants me to do it because the other alternatives have totally failed. The idea is to implement an ODBC Driver. If I have the time I'll put a tutorial here on how to do that on Linux. But back to the point of this entry.

This small guide will help you create a simple project using CMake to build it and cunit to test it. Yes, I believe in test driven development, no matter the language.

Let's imagine a simple project that produces an executable that prints the result of 2 + 3 (I know, lame but sufficient for this example). To make it easier to test I split the application. The functions will go into a library and there will be a source file that starts the program. The library header file is called "lib.h" (extra points for originality). Here are its contents:
int add(int a, int b);

The implementation is on the lib.c file as follows:
int add(int a, int b) {
return a + b;
}

As you can see it is a very complicated function. The adder.c file contain the main function and starts the program as follows:
#include <stdio.h> 
#include "lib.h"

int main (int argc, char** argv) {
printf("2+3=%d\n", add(2, 3));
return 0;
}

The unit test is a little more complicated as cunit requires some boiler plate code in order to run tests. Here is the source code of the test_lib.c file:
#include "CUnit/Basic.h" 
#include "lib.h"

void simpleTest(void) {
CU_ASSERT(2 == add(1, 1));
}

int main (int argc, char** argv) {

CU_pSuite pSuite = NULL;

/* initialize the CUnit test registry */
if (CUE_SUCCESS != CU_initialize_registry())
return CU_get_error();

/* add a suite to the registry */
pSuite = CU_add_suite("Suite_1", NULL, NULL);
if (NULL == pSuite) {
CU_cleanup_registry();
return CU_get_error();
}

/* add the tests to the suite */
if (NULL == CU_add_test(pSuite, "Simple Addition Test", simpleTest)) {
CU_cleanup_registry();
return CU_get_error();
}

/* Run all tests using the CUnit Basic interface */
CU_basic_set_mode(CU_BRM_VERBOSE);
CU_basic_run_tests();
CU_cleanup_registry();
return CU_get_error();
}

To make all this build with CMake you have to create a CMakeLists.txt file with the following content:
cmake_minimum_required (VERSION 2.6)
project (Adder)

set (SOURCES lib.c)

add_executable (adder adder.c ${SOURCES})

enable_testing ()

add_executable (test_lib test_lib.c ${SOURCES})

set_target_properties (test_lib PROPERTIES LINK_FLAGS -Wl,-lcunit)

add_test (test_lib ${EXECUTABLE_OUTPUT_PATH}/test_lib)


And that is it. Now you can make a directory called "build", go into to it and type "cmake .." to generate all the necessary build files. To build the project you type "make", to run the tests you can type "make test" and to clean the output you can run "make clean". If you prefer to do it all in one pass just do "make clean all test".

Just in case you are wondering: I used c2html to generate the html formated version of the C files and VIMs convert-to-html script for the CMake file.

Happy coding!!!

5 comments:

  1. Thanks for such a nice post. I really like your blog

    ReplyDelete
  2. Thank you for the nice hints. I was googling for a way of integrating CUnit into CTest framework and found your post.

    I am using CMake to create Visual C++ projects and it did create a RUN_TESTS target for me. But it looks like CTests relies on the test executable return values to report whether they failed or not. CUnit, on the other hand, as far it doesn't have any internal problem (e.g. memory allocation), the executable will always run successfully, regardless of the test results.

    One can say that I could have the test functions return failure if the test fails, but then again, I don't want the testing to abort on the first error. What we need is a report with the results for all tests, such as the CUnit executable will output to the standard output.

    Thus, CTests doesn't look much useful for integrating with CUnit. I ended up just adding a "test" target only to build the CUnit test executable. But I still have to run it on a terminal to see the results.

    ReplyDelete
    Replies
    1. It can be done with some tweaking, here is how to do it on linux.
      1) change return part of main():
      CU_basic_run_tests();
      CU_pRunSummary runSummary = CU_get_run_summary();
      unsigned int exitValue = runSummary->nTestsFailed + runSummary->nAssertsFailed;
      CU_cleanup_registry();
      if (CU_get_error())
      return CU_get_error();
      return exitValue;
      }

      2) run test as follows:
      make CTEST_OUTPUT_ON_FAILURE=1 test

      Delete
  3. nice post for the beginners...thanks :)

    ReplyDelete