Gluttton
Gluttton

Reputation: 5968

Add *.gcno and *.gcda files to the make clean rule using CMake

I have the project with structure similar like this:

├── CMakeLists.txt
├── src
│   ├── logic.cpp
│   └── logic.h
└── test
    ├── CMakeLists.txt
    └── logic_test.cpp

The main CMakeLists.txt file is:

cmake_minimum_required (VERSION 2.8)
project (Logic)
set (Logic_SOURCES ${PROJECT_SOURCE_DIR}/src/logic.cpp)
include_directories (${PROJECT_SOURCE_DIR}/src)
add_library (logic SHARED ${Logic_SOURCES})
add_subdirectory (test)

And CMakeLists.txt for tests is:

find_package (GTest)
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -march=native -mtune=native -fprofile-arcs -ftest-coverage")
set (CMAKE_EXE_LINKER_FLAGS "-fprofile-arcs -ftest-coverage")
set (Test_SOURCES ${Logic_SOURCES} ${PROJECT_SOURCE_DIR}/test/logic_test.cpp)
add_executable (logic_test ${Test_SOURCES})
target_link_libraries (${TestName} gtest gtest_main gcov pthread)

For dealing with test coverage reports I've added custom target into test/CMakeLists.txt:

set (Coverage_REPORT ${PROJECT_BINARY_DIR}/coverage.info)
set (Coverage_DIR    ${PROJECT_BINARY_DIR}/coverage)
add_custom_command (
    OUTPUT  ${Coverage_REPORT}
    COMMAND lcov -q -c -f -b . -d ${PROJECT_BINARY_DIR}/test -o ${Coverage_REPORT}
    COMMAND lcov -e ${Coverage_REPORT} '${PROJECT_SOURCE_DIR}/src/*' -o ${Coverage_REPORT}
    COMMAND genhtml ${Coverage_REPORT} --legend --demangle-cpp -f -q -o ${Coverage_DIR}
    DEPENDS logic_test
)
add_custom_target (coverage DEPENDS ${Coverage_REPORT})

All this code works correct and as expected. The workflow looks like this:

mkdir build
cd build
cmake ..
make
./test/logictest
make coverage

But now I would like add test coverage artifacts to the make clean rule. I have tried add this code to the test/CMakeLists.txt:

file (GLOB_RECURSE Test_GCNOS ${PROJECT_BINARY_DIR}/*.gcno)
file (GLOB_RECURSE Test_GCDAS ${PROJECT_BINARY_DIR}/*.gcda)
list (APPEND Test_COVERAGE_DATA "${Coverage_REPORT}")
list (APPEND Test_COVERAGE_DATA "${Coverage_DIR}")
list (APPEND Test_COVERAGE_DATA "${Coverage_GCNO}")
list (APPEND Test_COVERAGE_DATA "${Coverage_GCDA}")
set_directory_properties (PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${Test_COVERAGE_DATA}")

But this approach doesn't work as expected (for me). When calling cmake .. artifatcs don't exist yet, so variable Coverage_DATA is empty and this require recall cmake .. after running tests. This looks ugly (for me).

So my question is: how can I add test coverage artifacts to the make clean rule?

Upvotes: 5

Views: 4944

Answers (3)

SuperNeko
SuperNeko

Reputation: 11

cmake_minimum_required(VERSION 3.20)

project(A)


set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
Set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -fno-inline") 

add_executable(main main.cpp Hello.cpp)

add_executable(main2 main.cpp Hello.cpp)

list(APPEND test_targets main main2)

foreach(test_target IN LISTS test_targets)
  get_property(target_srcs TARGET ${test_target} PROPERTY SOURCES)
  foreach(target_src IN LISTS target_srcs)
    add_custom_command(OUTPUT ./CMakeFiles/${test_target}.dir/clear_${test_target}_${target_src}_coverage.h
    DEPENDS ${target_src}
    COMMAND ${CMAKE_COMMAND} -E echo "clearing ${target_src}.gcno"
    COMMAND ${CMAKE_COMMAND} -E rm -f ./${target_src}.gcno
    COMMAND ${CMAKE_COMMAND} -E touch clear_${test_target}_${target_src}_coverage.h
    WORKING_DIRECTORY ./CMakeFiles/${test_target}.dir
    )
    target_sources(${test_target} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${test_target}.dir/clear_${test_target}_${target_src}_coverage.h")

    list(APPEND ${test_target}_gcda ./${target_src}.gcda)
  endforeach()

  add_custom_target(clear_${test_target}_gcda 
  COMMAND ${CMAKE_COMMAND} -E echo "clearing ${${test_target}_gcda}"
  COMMAND ${CMAKE_COMMAND} -E rm -f "${${test_target}_gcda}" 
  WORKING_DIRECTORY ./CMakeFiles/${test_target}.dir
  COMMAND_EXPAND_LISTS)
  
  add_dependencies(${test_target} clear_${test_target}_gcda)
endforeach()

I write a simple code for this issue.I use add_custom_command() with a empty file acting as a flag to delete .gcno files. This method has some advantages. When a source file has changed, it ONLY delete the correspond .gcno file before recompiling the source file. After the source file is compiled, the new .gcno file will create successfully.

besides, I use add_custom_target() to delete .gcda file.

So, I can easily run cmake --build . to regenerate changed .gcno files and delete all .gcda files.

Upvotes: 1

Joakim
Joakim

Reputation: 11978

I have written a code coverage cmake script that is easy to use together with http://coveralls.io/ if you're doing an open source project:

https://github.com/JoakimSoderberg/coveralls-cmake https://github.com/JoakimSoderberg/coveralls-cmake-example

However if you want a script that simply generates the coverage data locally there is this project:

https://github.com/bilke/cmake-modules/blob/master/CodeCoverage.cmake

I solved the problem with cleaning the coverage data by always first removing it in my coverage target before generating the new ones. It's not a make clean solution but works fine:

file(REMOVE_RECURSE ${PROJECT_BINARY_DIR}/*.gcda)

(I hardly ever use make clean but rather multiple build dirs and the occasional rm -rf *. The entire concept of make clean is broken in my opinion, having the build and source separate makes things much cleaner)

Upvotes: 3

Gluttton
Gluttton

Reputation: 5968

It's couldn't be called graceful solution, but in my opinion it's better then cmake recall. I've tried create macro which determine names of coverage artefacts (*.gcno and *.gcda files) by list of source files:

macro (determine_coverage_data Sources TestName Artifacts Suffix)
set (CoverageDirectory "${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/${TestName}.dir")
foreach (File ${Sources})
    string (REGEX MATCH "^${CMAKE_CURRENT_SOURCE_DIR}*" Directory "${File}")
    if    (Directory STREQUAL CMAKE_CURRENT_SOURCE_DIR)
        string (REGEX REPLACE "^${CMAKE_CURRENT_SOURCE_DIR}*" "${CoverageDirectory}" File "${File}")
    else  (Directory STREQUAL CMAKE_CURRENT_SOURCE_DIR)
        string (REGEX REPLACE "/" ";" A "${CMAKE_CURRENT_SOURCE_DIR}")
        string (REGEX REPLACE "/" ";" B "${File}")
        list (LENGTH A DeepDirectory)
        list (LENGTH B DeepFile)
        set (File "${CoverageDirectory}")
        set (I 1)
        while    (I LESS DeepDirectory)
            list (GET A ${I} AI)
            list (GET B ${I} BI)
            if    (AI STREQUAL BI)
                math (EXPR I "${I} + 1")
            else  (AI STREQUAL BI)
                math (EXPR DeepDiff "${DeepFile} - ${I} - 1")
                while    (DeepDiff GREATER 0)
                    set (File "${File}/__")
                    math (EXPR DeepDiff "${DeepDiff} - 1")
                endwhile (DeepDiff GREATER 0)
                while    (I LESS DeepFile)
                    list (GET B ${I} BI)
                    set (File "${File}/${BI}")
                    math (EXPR I "${I} + 1")
                endwhile (I LESS DeepFile)
            endif (AI STREQUAL BI)
        endwhile (I LESS DeepDirectory)
    endif (Directory STREQUAL CMAKE_CURRENT_SOURCE_DIR)
    set (${Artifacts} ${${Artifacts}} "${File}${Suffix}")
endforeach (File)
endmacro (determine_coverage_data)

So full solution is looks like:

├── cmake
│   └── UseGCov.cmake
├── CMakeLists.txt
├── src
│   ├── logic.cpp
│   └── logic.h
└── test
    ├── CMakeLists.txt
    └── logic_test.cpp

The main CMakeLists.txt file is:

cmake_minimum_required (VERSION 2.8)
project (Logic)
set (Logic_SOURCES ${PROJECT_SOURCE_DIR}/src/logic.cpp)
# Include macro.
include ("${PROJECT_SOURCE_DIR}/cmake/UseGCov.cmake")
include_directories (${PROJECT_SOURCE_DIR}/src)
add_library (logic SHARED ${Logic_SOURCES})
add_subdirectory (test)

And CMakeLists.txt for tests is:

find_package (GTest)

set (Test_SOURCES ${Logic_SOURCES} ${PROJECT_SOURCE_DIR}/test/logic_test.cpp)
set (TestName logic_test)

set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -march=native -mtune=native -fprofile-arcs -ftest-coverage")
set (CMAKE_EXE_LINKER_FLAGS "-fprofile-arcs -ftest-coverage")

# Call macro.
determine_coverage_data ("${Test_SOURCES}" "${TestName}" Test_GCNOS ".gcno")
determine_coverage_data ("${Test_SOURCES}" "${TestName}" Test_GCDAS ".gcda")

set (Coverage_REPORT ${PROJECT_BINARY_DIR}/coverage.info)
set (Coverage_DIR    ${PROJECT_BINARY_DIR}/coverage)
add_custom_command (
    OUTPUT  ${Coverage_REPORT}
    COMMAND lcov -q -c -f -b . -d ${PROJECT_BINARY_DIR}/test -o ${Coverage_REPORT}
    COMMAND lcov -e ${Coverage_REPORT} '${PROJECT_SOURCE_DIR}/src/*' -o ${Coverage_REPORT}
    COMMAND genhtml ${Coverage_REPORT} --legend --demangle-cpp -f -q -o ${Coverage_DIR}
    DEPENDS logic_test
)
add_custom_target (coverage DEPENDS ${Coverage_REPORT})

set (Test_COVERAGE_DATA ${Coverage_REPORT} ${Coverage_DIR} ${Test_GCNOS} ${Test_GCDAS})
set_directory_properties (PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${Test_COVERAGE_DATA}")
add_executable (${TestName} ${Test_SOURCES})
target_link_libraries (${TestName} gtest gtest_main gcov pthread)

Upvotes: 1

Related Questions