Why I Started Using This Tool
My little C math library has three modules: arithmetic, power_abs and
statistics. In my last post, only
add had unit tests. So I sat down to give every function at least one test, and I put each
module's tests in its own Unity test file.
When I built it, I noticed something silly in the build log. power_abs.c was compiled for
main_demo, compiled again for test_src, and a third time for my new
test_power_abs. Every program listed the same source files and compiled them on its own. With
three small files that costs nothing. But it's the kind of thing that turns into slow builds and
copy-pasted file lists in a bigger project.
On top of that, my coverage report only looked at test_src. The new tests ran, but gcov
ignored them. Both problems pointed to the same fix: build the shared code once, as a library.
What It Does
A static library is an archive of compiled object files. With GCC it's a .a
file, with MSVC a .lib. Programs link against it, and the linker copies the parts they need
into the executable.
In CMake it's one target:
add_library(src_lib STATIC
src/arithmetic.c
src/power_abs.c
src/statistics.c
)
target_include_directories(src_lib PUBLIC src)
add_executable(main_demo src/main.c)
target_link_libraries(main_demo PRIVATE src_lib)
The PUBLIC include directory is the part I like most. Anything that links
src_lib automatically gets src/ on its include path. So my three test programs
shrink to a test file plus target_link_libraries(... PRIVATE src_lib unity):
add_executable(test_power_abs tests/test_power_abs.c)
target_link_libraries(test_power_abs PRIVATE src_lib unity)
add_test(NAME test_power_abs COMMAND test_power_abs)
Each Unity test file has its own main(), so each one is its own executable and its own
add_test. Sixteen tests in total, at least one per function. One small trap: Unity has
double support switched off by default, so TEST_ASSERT_EQUAL_DOUBLE fails with
"Unity Double Precision Disabled". I compared as float instead, which is exact for values like 3.5:
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(3.5f, (float)divide(7.0, 2.0),
"Verifies that dividing 7 by 2 correctly results in 3.5.");
For coverage, I moved --coverage from the test program to the library: PRIVATE
for compiling, so only src/ is instrumented, and INTERFACE for linking, so every
executable that uses the library gets the gcov runtime.
target_compile_options(src_lib PRIVATE --coverage)
target_link_options(src_lib INTERFACE --coverage)
All three test programs now write into the same coverage data, and gcov reports 100% of 42 lines across all three files:
File 'C:/git/09_cmake/src/arithmetic.c'
Lines executed:100.00% of 10
File 'C:/git/09_cmake/src/power_abs.c'
Lines executed:100.00% of 12
File 'C:/git/09_cmake/src/statistics.c'
Lines executed:100.00% of 20
Lines executed:100.00% of 42
Final Verdict
This is one of those changes that makes a project feel "real". Each source file is compiled once, and adding a new test program is now two lines of CMake. Coverage finally counts every test. And because the library sits outside the cross-compile guard, the ARM build picked it up without any changes.
Some honest caveats:
- main_demo is instrumented too. Because the link option is
INTERFACE, my demo program also gets coverage on a host build. Running it adds its own counts to the coverage data. That's fine for learning, but a real project would usually have a separate coverage build. - MSVC ignores
--coverage. It prints a warning and moves on. Coverage only works with the GCC build. - 100% line coverage doesn't mean everything is tested.
divideandmodulohave a divide-by-zero check, and it's written on the same line as the normal case. Every line ran, but no test actually checks what happens when you divide by zero. The tests also only check one "happy path" value per function. Coverage tells you what code ran, not whether you checked the right answers.
If your C project has more than one program using the same code, and it will once you add tests, make it a
library. It's a few lines of CMake, and the PUBLIC/PRIVATE/INTERFACE
idea will make the rest of CMake much easier to understand. Next for me: tests for the edge cases that the
coverage number hides.