Compile several test source files as a single merged file with CMake

232 Views Asked by At

I have a library that has many different cpp test files. They look like this, A.cpp, B.cpp, C.pp, etc.

A.cpp:

#define BOOST_TEST_MODULE "C++ Unit Tests A"
#include<boost/test/unit_test.hpp>

#include <multi/array.hpp>

BOOST_AUTO_TEST_CASE(test_case_A1) {
...
}

B.cpp:

#define BOOST_TEST_MODULE "C++ Unit Tests B"
#include<boost/test/unit_test.hpp>

#include <multi/array.hpp>

BOOST_AUTO_TEST_CASE(test_case_B1) {
...
}

I have the following Cmake code to compile and test

include(CTest)

file(
    GLOB TEST_SRCS
    RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
    *.cpp
)

foreach(TEST_FILE ${TEST_SRCS})
    set(TEST_EXE "${TEST_FILE}.x")
    add_executable(${TEST_EXE} ${TEST_FILE})

    target_link_libraries(${TEST_EXE} PRIVATE multi)
    target_include_directories(${TEST_EXE}        PRIVATE ${PROJECT_SOURCE_DIR}/include)
    target_include_directories(${TEST_EXE} SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}        )
    target_link_libraries     (${TEST_EXE}        PRIVATE Boost::unit_test_framework   )
endforeach()

The problem with this is that each test takes sometime to compile. I know that if I merge all these cpp files it will compile much faster overall.

(For example, I have 40 cpp test files, each takes 10 second, total compilation is 400 second. If I merge all it takes perhaps 20 seconds. Even if I could parallelize the build of individual files a 20x factor is hard to achieve. Obviously the compiler is doing a lot of repeated work. Supposedly gch pre-compiled headers would help but I never figured out how to use them with cmake).

Is there a way to force the compilation of these test to work on a single merged file?

I tried replacing the loop with this:

add_executable(multi_test ${TEST_SRCS})
add_test(NAME multi_test COMMAND ./multi_test)
target_include_directories(multi_test        PRIVATE ${PROJECT_SOURCE_DIR}/include)
target_include_directories(multi_test SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}        )
target_link_libraries     (multi_test        PRIVATE Boost::unit_test_framework   )

However, it still has two problems: First, it is still slow to compile because each cpp is used to generate individual .o files (later link into the same executable). The second problem is that each .o will contain a main function (defined by Boost.Test) and therefore there will be a linker error.

Is there a change I can make to make to compile several cpp files as if it was a single cpp file? (I would like to avoid generating a temporary merged file manually) Is there a Boost.Test flag that can help me with this?

I would like a solution where each test source file can still be compiled into its own executable.


As an illustration, I was able to do this process to obtain a single file.

echo '#define BOOST_TEST_MODULE "C++ Unit Tests for Multi, All in one"' > ../test/all.cpp  # Boost Test need a module name
cat ../test/*.cpp | grep -v BOOST_TEST_MODULE >> ../test/all.cpp  # filter macros to avoid warnings
1

There are 1 best solutions below

0
On

You can do the #define BOOST_TEST_MODULE "C++ Unit Tests <...>"s via the target_compile_definitions command, which is useful here since you want to give the user the option of whether to build tests to individual executables, or one single executable together.

The problem with this is that each test takes some time to compile. I know that if I merge all these cpp files it will compile much faster overall. [...] Supposedly gch pre-compiled headers would help but I never figured out how to use them with CMake).

For using precompiled headers in CMake, see the target_precompile_headers command. But what you're looking for with concatenating source files to compile as if they were a single source file is a thing and has a name: "Unity Build". CMake even comes with a feature to support it: its UNITY_BUILD target property. Do read the docs and learn about how it is suggested to be used properly, and what general caveats there are with respect to possible ODR violations when using the unity build technique with any build tool.

The solution might look something like this (in block quotes because it's mostly taken from your question post and what you wrote in chat):

include(CTest)

file(
    GLOB TEST_SRCS
    RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
    *.cpp
)

option(
    TEST_UNITY_BUILD
    "whether to create a single executable for all tests (instead of one per test source file)"
    <default> # replace <default> with desired default value
)

if("${TEST_UNITY_BUILD}")
    foreach(TEST_FILE ${TEST_SRCS})
        set(TEST_EXE "${TEST_FILE}.x")
        add_executable(${TEST_EXE} ${TEST_FILE})

        target_link_libraries(${TEST_EXE} PRIVATE multi)
        target_include_directories(${TEST_EXE}        PRIVATE ${PROJECT_SOURCE_DIR}/include)
        target_include_directories(${TEST_EXE} SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}        )
        target_link_libraries     (${TEST_EXE}        PRIVATE Boost::unit_test_framework   )
        target_compile_definitions(${TEST_EXE}        PRIVATE BOOST_TEST_MODULE="C++ Unit Tests for ${TEST_FILE}")
    endforeach()
else()
    add_executable(multi_test ${TEST_SRCS})
    set_property(TARGET multi_test PROPERTY UNITY_BUILD ON)

    target_include_directories(multi_test PRIVATE ${PROJECT_SOURCE_DIR}/include)
    target_include_directories(multi_test SYSTEM PRIVATE ${Boost_INCLUDE_DIRS} )
    target_link_libraries     (multi_test PRIVATE Boost::unit_test_framework )
    target_compile_definitions(multi_test PRIVATE BOOST_TEST_MODULE="C++ Unit Tests for Multi GLOBAL TEST")

    add_test(NAME multi_test COMMAND ./multi_test)
endif()

Note that file(GLOB) is discouraged for use by the maintainers of CMake, and by various long-time users of CMake on Stack Overflow, as seen here and here. You can instead define the list of files manually like set(TEST_SRCS A.cpp B.cpp C.cpp ...).

Also note that if you use file(GLOB), the order files in the result variable in unspecified for versions older than CMake v3.6, and specified as lexicographical for versions 3.6 and above. (see the file(GLOB) docs), which has relevance to CMake's unity build feature. Quoting from the UNITY_BUILD docs:

The order of source files added to the target via commands like add_library(), add_executable() or target_sources() will be preserved in the generated unity source files. This can be used to manually enforce a specific grouping based on the UNITY_BUILD_BATCH_SIZE target property.


You may also be interested in create_test_sourcelist, which links many tests into a single executable.