Search code examples
cunit-testinggccgcov

How do I unit-test a dynamic library using gcov?


I am building a dynamically-linked library, and am setting up a simple test suite. I want to use gcov to generate a static code analysis coverage report.

My library is a C file containing function implementations and a header file containing function prototypes. My test suite is simply an application that calls the functions in various ways and confirms the validity of the output.

I am compiling both the library and the test suite with the -fprofile-arcs and -ftest-coverage flags, as described on GNU's guide to GCOV. I am also including the -O0 flag to disable compiler optimization and the -g flag to enable debug symbols. The executable generated from the test suite is linked dynamically to the library.

All files compile cleanly and without warning, but it fails to link the test suite to the library -- citing a "hidden symbol __gcov_merge_add". If I compile without the -fprofile-arcs and -ftest-coverage flags, the linking succeeds and I am able to run the test suite executable.

So I have a few questions which still aren't resolved after reading the GNU guide to GCOV.

  1. Why is the linking failing? How can I resolve this?
  2. Do I need to include the profile and coverage flags when compiling both the library and the test suite?

Here is my inc/mylib.h file:

#ifndef __MYLIB_H__
#define __MYLIB_H__

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

int
foo (int a);

int
bar (int a);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __MYLIB_H__ */

Here is my src/mylib.c file:

#include <stdio.h>
#include "mylib.h"

int foo(int a) {
    if (a > 5) {
        return 5;
    }
    return a;
}

int bar(int a) {
    if (a < 0) {
        return 0;
    }
    return a;
}

Here is my test/unittests.c file:

#include <stdio.h>
#include "mylib.h"

void run_foo_tests() {
    int inputs[] = {3, 6};
    int expected_results[] = {3, 5};
    int i, actual_result;

    for ( i = 0; i < sizeof(inputs) / sizeof(int); i++ ) {
        actual_result = foo(inputs[i]);
        if (actual_result == expected_results[i]) {
            printf("Test %d passed!\n", i + 1);
        } else {
            printf("Test %d failed!\n", i + 1);
            printf("  Expected result: %d\n", expected_results[i]);
            printf("  Actual result: %d\n", actual_result);
        }
    }
}

void run_bar_tests() {
    int inputs[] = {3, -1};
    int expected_results[] = {3, 0};
    int i, actual_result;

    for ( i = 0; i < sizeof(inputs) / sizeof(int); i++ ) {
        actual_result = bar(inputs[i]);
        if (actual_result == expected_results[i]) {
            printf("Test %d passed!\n", i + 1);
        } else {
            printf("Test %d failed!\n", i + 1);
            printf("  Expected result: %d\n", expected_results[i]);
            printf("  Actual result: %d\n", actual_result);
        }
    }
}

int main(int argc, char *argv[]) {
    run_foo_tests();
    run_bar_tests();
    return 0;
}

Here is my Makefile:

CC=gcc
CFLAGS=-Wall -std=c89 -g -O0 -Iinc -fprofile-arcs -ftest-coverage

all: clean build run_tests

build:
    $(CC) $(CFLAGS) -fPIC -c src/*.c -o lib/mylib.o
    $(CC) -shared lib/mylib.o -o lib/libmylib.so
    $(CC) $(CFLAGS) test/*.c -o bin/unittests -Llib -lmylib

run_tests:
    LD_LIBRARY_PATH=lib bin/unittests
    gcov src/*.c

clean:
    rm -f *.gcda *.gcno *.gcov
    rm -rf bin lib ; mkdir bin lib

When I run make, I am presented with this output:

rm -f *.gcda *.gcno *.gcov
rm -rf bin lib ; mkdir bin lib
gcc -Wall -std=c89 -g -O0 -Iinc -fprofile-arcs -ftest-coverage -fPIC -c src/*.c -o lib/mylib.o
gcc -shared lib/mylib.o -o lib/libmylib.so
gcc -Wall -std=c89 -g -O0 -Iinc -fprofile-arcs -ftest-coverage test/*.c -o bin/unittests -Llib -lmylib
/usr/bin/ld: bin/unittests: hidden symbol `__gcov_merge_add' in /usr/lib/gcc/x86_64-redhat-linux/4.8.5/libgcov.a(_gcov_merge_add.o) is referenced by DSO
/usr/bin/ld: final link failed: Bad value
collect2: error: ld returned 1 exit status
make: *** [build] Error 1

Solution

  • You need to give the command line argument -lgcov for the linker (normally -ftest-coverage implies -lgcov, but you have a separate linking step where -ftest-coverage is not given as a command line argument). Alternatively, you can just use the --coverage command line argument, which is a shortcut also for -fprofile-arcs and -ftest-coverage, as explained here: http://www.univ-orleans.fr/sciences/info/ressources/webada/doc/gnat/gcc_3.html:

    --coverage

    This option is used to compile and link code instrumented for coverage analysis. The option is a synonym for `-fprofile-arcs' `-ftest-coverage' (when compiling) and `-lgcov' (when linking). See the documentation for those options for more details.

    At the same place, btw., it is also explained that you don't have to compile all files with these options, which hopefully answers your question 2.