diff --git a/CMakeLists.txt b/CMakeLists.txt index 18fb46fe..0d1fda46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,23 +29,44 @@ if (NOT DEFINED PROJECT_CXX_FLAGS_DEBUG) set(PROJECT_CXX_FLAGS_DEBUG ${PROJECT_CXX_FLAGS} -ggdb -Og CACHE STRING "debug c++ compiler flags") endif() -message("-- PROJECT_CXX_FLAGS_DEBUG: debug c++ flags are [${PROJECT_CXX_FLAGS_DEBUG}]") +if (${CMAKE_BUILD_TYPE} STREQUAL debug) + message("-- PROJECT_CXX_FLAGS_DEBUG: debug c++ flags are [${PROJECT_CXX_FLAGS_DEBUG}]") +endif() add_compile_options("$<$:${PROJECT_CXX_FLAGS_DEBUG}>") # ---------------------------------------------------------------- -# unit test setup +# cmake -DCMAKE_BUILD_TYPE=coverage -# activate code coverage for all executables + libraries (when configured with -DCODE_COVERAGE=ON) -add_code_coverage() -# 1. assuming that /nix/store/ prefixes .hpp files belonging to gcc, catch2 etc. -# we're not interested in code coverage for these sources. -# 2. exclude the utest/ subdir, we don't need coverage on the unit tests themselves; -# rather, want coverage on the code that the unit tests exercise. +if (NOT DEFINED PROJECT_CXX_FLAGS_COVERAGE) + # note: for clang would use -fprofile-instr-generate -fcoverage-mapping here instead and also at link time + set(PROJECT_CXX_FLAGS_COVERAGE ${PROJECT_CXX_FLAGS} -ggdb -Og -fprofile-arcs -ftest-coverage + CACHE STRING "coverage c++ compiler flags") +endif() +if (${CMAKE_BUILD_TYPE} STREQUAL "coverage") + message(STATUS "-- PROJECT_CXX_FLAGS_COVERAGE: coverage c++ flags are [${PROJECT_CXX_FLAGS_COVERAGE}]") +endif() + +add_compile_options("$<$:${PROJECT_CXX_FLAGS_COVERAGE}>") +# when -DCMAKE_BUILD_TYPE=coverage, link executables with gcov +link_libraries("$<$:gcov>") + +find_program(LCOV_EXECUTABLE NAMES lcov) +find_program(GENHTML_EXECUTABLE NAMES genhtml) + +# with coverage build: +# 1. invoke instrumented executables for which you want coverage: +# (cd path/to/build && ctest) +# 2. post-process low-level coverage data +# (path/to/build/gen-ccov) +# 3. point browser to generated html data +# file:///path/to/build/ccov/html/index.html # -# NOTE: this seems to work only with the 'ccov-all' target. In particular, doesn't seem to do anything with the 'ccov' target -# -add_code_coverage_all_targets(EXCLUDE /nix/store/* ${PROJECT_SOURCE_DIR}/utest/* ${PROJECT_BINARY_DIR}/local/* ${PROJECT_SOURCE_DIR}/repo/*) +configure_file( + ${PROJECT_SOURCE_DIR}/cmake/gen-ccov.in + ${PROJECT_BINARY_DIR}/gen-ccov) + +file(CHMOD ${PROJECT_BINARY_DIR}/gen-ccov PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) # ---------------------------------------------------------------- # c++ settings diff --git a/README.md b/README.md index 5ef9d025..16c1635b 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,17 @@ When this completes, point local browser to `xo-unit/.build/docs/sphinx/index.h ``` $ cd xo-unit $ mkdir .build-ccov -$ cd .build-ccov -$ cmake -DCMAKE_MODULE_PATH=${PREFIX}/share/cmake -DCMAKE_PREFIX_PATH=${PREFIX} -DCODE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug .. +$ cmake -DCMAKE_BUILD_TYPE=coverage -DCMAKE_PREFIX_PATH=${PREFIX} -B .build-ccov +``` + +run coverage-enabled unit tests +``` +$ cmake --build .build-ccov -- test +``` + +generate html+text coverage report +``` +$cmake --build .build-ccov -- ccov ``` ### LSP support diff --git a/cmake/gen-ccov.in b/cmake/gen-ccov.in new file mode 100644 index 00000000..e335aed4 --- /dev/null +++ b/cmake/gen-ccov.in @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +srcdir=@PROJECT_SOURCE_DIR@ +builddir=@PROJECT_BINARY_DIR@ +lcov=@LCOV_EXECUTABLE@ +genhtml=@GENHTML_EXECUTABLE@ + +if [[ $lcov == "LCOV_EXECUTABLE-NOTFOUND" ]]; then + echo "gen-ccov: lcov executable not found" + exit 1 +fi + +if [[ $genhtml == "GENHTML_EXECUTABLE-NOTFOUND" ]]; then + echo "gen-ccov: genhtml executable not found" + exit 1 +fi + +mkdir $builddir/ccov + +$srcdir/cmake/lcov-harness $srcdir $builddir $builddir/ccov/out $lcov $genhtml diff --git a/cmake/lcov-harness b/cmake/lcov-harness new file mode 100755 index 00000000..27ac8be9 --- /dev/null +++ b/cmake/lcov-harness @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +srcdir=$1 +builddir=$2 +outputstem=$3 +lcov=$4 +genhtml=$5 + +if [[ -z "${srcdir}" ]]; then + echo "lcov-harness: expected non-empty srcdir" + exit 1 +fi + +if [[ -z ${builddir} ]]; then + echo "lcov-harness: expected non-empty builddir" + exit 1 +fi + +if [[ -z ${outputstem} ]]; then + echo "lcov-harness: expected non-empty outputstem" + exit 1 +fi + +if [[ -z ${lcov} ]]; then + echo "lcov-harness: exepcted non-empty lcov" + exit 1 +fi + +if [[ -z ${genhtml} ]]; then + echo "lcov-harness: expected non-empty genhtml" + exit 1 +fi + +# directory stems for location of {.gcda, gcno} coverage information, +# +# if we have source tree: +# +# ${srcdir} +# +- foo +# | \- foo.cpp +# \- bar +# \- quux +# +- quux.cpp +# \- quux_main.cpp +# +# then we expect build tree: +# +# ${builddir} +# +- foo +# | \- CMakeFiles +# | \- foo_target.dir +# | +- foo.cpp.gcda +# | \- foo.cpp.gcno +# +- bar +# \- quux +# \- CMakeFiles +# \- target4quux.dir +# +- quux.cpp.gcda +# +- quux.cpp.gcno +# +- quux_main.cpp.gcda +# \- quux_main.cpp.gcno +# +# in which case will have cmd_body: +# +# ${primarydirs} +# ./foo/CMakeFiles/foo_target.dir +# ./bar/quux/CMakeFiles/target4quux.dir +# +# here foo_target, quux_target are whatever build is using for corresponding cmake target names. +# +# We want to invoke lcov like: +# +# lcov --capture \ +# --output ${builddir}/ccov \ +# --exclude /utest/ \ +# --base-directory ${srcdir}/foo --directory ${builddir}/foo/CMakeFiles/foo_target.dir \ +# --base-directory ${srcdir}/bar/quux --directory ${builddir}/bar/quux/CMakeFiles/target4quux.dir +# +primarydirs=$(cd ${builddir} && find -name '*.gcno' \ + | xargs --replace=xx dirname xx \ + | uniq \ + | sed -e 's:^\./::') + +#echo "primarydirs=${primarydirs}" + +cmd="${lcov} --output ${outputstem}.info --capture --ignore-errors source" + +for bdir in ${primarydirs}; do + sdir=$(dirname $(dirname ${bdir})) + + cmd="${cmd} --base-directory ${srcdir}/${sdir} --directory ${builddir}/${bdir}" +done + +#echo cmd=${cmd} + +set -x + +# capture +${cmd} + +# keep only files with paths under source tree +# (don't want coverage for external libraries such as libstdc++ etc) +${lcov} --extract ${outputstem}.info "${srcdir}/*" --output ${outputstem}2.info + +# remove unit test dirs +# (we're interested in coverage of our installed code, not of the unit tests that exercise it) +${lcov} --remove ${outputstem}2.info '*/utest/*' --output ${outputstem}3.info + +# generate .html tree +mkdir -p ${builddir}/ccov/html +${genhtml} --ignore-errors source --show-details --prefix ${srcdir} --output-directory ${builddir}/ccov/html ${outputstem}3.info + +# also send report to stdout +${lcov} --list ${outputstem}3.info diff --git a/utest/CMakeLists.txt b/utest/CMakeLists.txt index 002be7b6..67beb4e4 100644 --- a/utest/CMakeLists.txt +++ b/utest/CMakeLists.txt @@ -8,19 +8,51 @@ set(SELF_SOURCE_FILES add_executable(${SELF_EXECUTABLE_NAME} ${SELF_SOURCE_FILES}) xo_include_options2(${SELF_EXECUTABLE_NAME}) - add_test(NAME ${SELF_EXECUTABLE_NAME} COMMAND ${SELF_EXECUTABLE_NAME}) -target_code_coverage(${SELF_EXECUTABLE_NAME} AUTO ALL) # ---------------------------------------------------------------- -# internal dependencies: logutil, ... +# in coverage build, target to build+install coverage report + +if (XO_SUBMODULE_BUILD) + # in submodule build, generate aggregate coverage report + # for all xo libraries +else() + set(CCOV_OUTPUT_DIR ${PROJECT_BINARY_DIR}/ccov/html) + set(CCOV_INDEX_FILE ${CCOV_OUTPUT_DIR}/index.html) + set(CCOV_REPORT_EXE ${PROJECT_BINARY_DIR}/gen-ccov) + # CMAKE_INSTALL_DOCDIR + # =default=> DATAROOTDIR/doc/PROJECT_NAME + # =default=> CMAKE_INSTALL_PREFIX/share/doc/xo_flatstring + set(CCOV_INSTALL_DOCDIR ${CMAKE_INSTALL_DOCDIR}/ccov) + + # 'test' target should always be out-of-date + # + # DEPENDS: reminder - can't put 'test' here, requires 'all' target + # + add_custom_command( + OUTPUT ${CCOV_INDEX_FILE} + DEPENDS ${SELF_EXE} + COMMAND ${CCOV_REPORT_EXE} + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + COMMENT "Generating coverage report -> [${CCOV_OUTPUT_DIR}]") + + add_custom_target( + ccov + DEPENDS ${CCOV_INDEX_FILE} ${SELF_EXE}) + + # OPTIONAL: quietly skip this step if ccov report not generated + install( + DIRECTORY ${CCOV_OUTPUT_DIR} + FILE_PERMISSIONS OWNER_READ GROUP_READ WORLD_READ + DESTINATION ${CCOV_INSTALL_DOCDIR} + COMPONENT Documentation + OPTIONAL) +endif() + +# ---------------------------------------------------------------- +# dependencies.. xo_self_dependency(${SELF_EXECUTABLE_NAME} xo_unit) -#xo_dependency(${SELF_EXECUTABLE_NAME} reflect) - -# ---------------------------------------------------------------- -# 3rd party dependency: catch2: - xo_external_target_dependency(${SELF_EXECUTABLE_NAME} Catch2 Catch2::Catch2) # end CMakeLists.txt