diff --git a/M2/CMakeLists.txt b/M2/CMakeLists.txt index 0566491be3c..23724423e43 100644 --- a/M2/CMakeLists.txt +++ b/M2/CMakeLists.txt @@ -68,6 +68,7 @@ include(prechecks) ## CMake macros for preprocessor checks and linting include(CTest) ## CMake module for generating a 'test' target for all unit-tests include(profiling) ## CMake macros for profiling Macaulay2 code include(configure) ## CMake script for configuring various variables +include(gcov) ## CMake macros for gcc/gcov code coverage (needs GCOV from configure) include(check-libraries) ## CMake script for checking which libraries exist and which need to be built include(build-libraries) ## CMake script for downloading, building, and installing external libraries include(startup) ## CMake script for messy substitutions in Macaulay2/bin/startup.c diff --git a/M2/Makefile.in b/M2/Makefile.in index bfe19b05982..2a6242e7164 100644 --- a/M2/Makefile.in +++ b/M2/Makefile.in @@ -120,6 +120,38 @@ unmark-packages:; $(MAKE) -C Macaulay2/packages $@ reconfigure: reconfigure-top-only unconfigure-libs unconfigure-libs:; $(MAKE) -C libraries unconfigure remove-deps:; find . -name \*.dep -delete + +## gcc/gcov code coverage (configure with --enable-gcov). +## Coverage data (.gcda) is generated automatically whenever a binary built in +## coverage mode exits -- run the unit tests, "make check", or the M2 binary on +## your own code. These targets only clear and report on the accumulated data. +## By default the report covers the whole source tree; narrow it by overriding +## COVERAGE_ROOT and COVERAGE_OBJDIR (e.g. =@srcdir@/Macaulay2/e and +## =Macaulay2/e for just the engine). +GCOVR = gcovr +COVERAGE_DIR = coverage +COVERAGE_ROOT = @srcdir@ +# COVERAGE_OBJDIR is passed to gcovr as a positional search path, NOT via +# --object-directory: the latter forces gcov to run in a single directory, which +# can't resolve sources for objects built in deeper VPATH subdirectories (e.g. +# Macaulay2/e/unit-tests). As a search path, gcovr runs gcov in each data file's +# own directory, so every relative source path resolves. +COVERAGE_OBJDIR = @builddir@ +COVERAGE_INDEX = $(CURDIR)/$(COVERAGE_DIR)/index.html +# gcov can report a function on more than one line (e.g. inlines built at -O0); +# merge those instead of erroring, attributing the function to its first line. +COVERAGE_MERGE_MODE = merge-use-line-min +# Extra options for gcovr, e.g. GCOVR_OPTIONS='--filter Macaulay2/e/' to scope +# the report to the engine, or '--exclude .*/unit-tests/.*' to drop test sources. +GCOVR_OPTIONS = +.PHONY: coverage-reset coverage-report +coverage-reset:; find . -name \*.gcda -delete +coverage-report: + @ $(MKDIR_P) $(COVERAGE_DIR) + $(GCOVR) --root $(COVERAGE_ROOT) $(COVERAGE_OBJDIR) \ + --merge-mode-functions=$(COVERAGE_MERGE_MODE) $(GCOVR_OPTIONS) \ + --html-details $(COVERAGE_DIR)/index.html --print-summary + @ printf '%s\033]8;;file://%s%s\033\\%s\033]8;;\033\\\n' '-- coverage report written to ' "$$(hostname)" "$(COVERAGE_INDEX)" "$(COVERAGE_INDEX)" find-conflicts:; grep -r -nH --exclude-dir BUILD -e '^<<<<<<< ' @srcdir@ || true log-archive:; find . -name config.log |xargs tar xzf config-logs.tgz shell:; PKG_CONFIG_PATH=$(M2_PKG_CONFIG_PATH) bash @@ -208,6 +240,8 @@ help: @ echo " relink-nostrip remove M2@EXE@ and rebuild it, unstripped" @ echo " install make and install files" @ echo " check run the tests" + @ echo " coverage-reset clear accumulated gcov coverage data (requires --enable-gcov)" + @ echo " coverage-report generate an HTML coverage report with gcovr" @ echo " clean remove all generated files except configured files" @ echo " distclean remove all generated files" @ echo " dist generate a source tarball for distribution" diff --git a/M2/cmake/configure.cmake b/M2/cmake/configure.cmake index dfb93c21556..a72d8cefa0c 100644 --- a/M2/cmake/configure.cmake +++ b/M2/cmake/configure.cmake @@ -20,6 +20,7 @@ option(LINTING "Enable linting source files" OFF) option(MEMDEBUG "Enable memory allocation debugging" OFF) option(PROFILING "Enable profiling build flags" OFF) option(COVERAGE "Enable Clang code coverage test" OFF) +option(GCOV "Enable gcc/gcov code coverage" OFF) option(GIT_SUBMODULE "Update submodules during build" ON) option(BUILD_NATIVE "Use native SIMD instructions" ON) option(BUILD_SHARED_LIBS "Build shared libraries" OFF) @@ -106,6 +107,7 @@ message("## Configure Macaulay2 BUILD_TESTING = ${BUILD_TESTING} BUILD_DOCS = ${BUILD_DOCS}\n COVERAGE = ${COVERAGE} + GCOV = ${GCOV} MEMDEBUG = ${MEMDEBUG} PROFILING = ${PROFILING}\n DEVELOPMENT = ${DEVELOPMENT} @@ -200,6 +202,11 @@ if(PROFILING) add_compile_options(-pg) add_link_options(-pg) endif() +if(GCOV) + # gcc/gcov coverage: --coverage instruments; -O0 keeps line attribution meaningful. + add_compile_options(--coverage -O0) + add_link_options(--coverage) +endif() # Flags based on build type # Note: certain flags are initialized by CMake based on the compiler and build type. diff --git a/M2/cmake/coverage.cmake b/M2/cmake/coverage.cmake index b93d0581aa7..71565fd2f24 100644 --- a/M2/cmake/coverage.cmake +++ b/M2/cmake/coverage.cmake @@ -1,7 +1,7 @@ ############################################################################### # See https://clang.llvm.org/docs/SourceBasedCodeCoverage.html find_program(LLVM_PROFDATA NAMES llvm-profdata) -find_program(LLVM_COV NAMES clang-format) +find_program(LLVM_COV NAMES llvm-cov) set(_coverage_dir ${CMAKE_BINARY_DIR}/coverage) diff --git a/M2/cmake/gcov.cmake b/M2/cmake/gcov.cmake new file mode 100644 index 00000000000..29891eada69 --- /dev/null +++ b/M2/cmake/gcov.cmake @@ -0,0 +1,56 @@ +# gcc/gcov code coverage (configure with -DGCOV=ON). +# Instrumentation flags are added in configure.cmake; this module only provides +# convenience targets to clear and report on the accumulated .gcda data. The data +# itself is generated automatically whenever a binary built in coverage mode exits +# (the unit tests, ctest, or the M2 binary run on your own code). +# +# By default the report covers the whole source tree; pass extra gcovr options +# via GCOVR_OPTIONS, e.g. -DGCOVR_OPTIONS="--filter Macaulay2/e/" to scope it to +# the engine, or "--exclude .*/unit-tests/.*" to drop the test sources. +# +# Usage: +# cmake -S . -B BUILD/cov -GNinja -DGCOV=ON -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON +# cmake --build BUILD/cov --target M2-engine M2-unit-tests +# cmake --build BUILD/cov --target coverage-reset # start from a clean slate +# ctest --test-dir BUILD/cov -R unit-tests # (or run M2 however you like) +# cmake --build BUILD/cov --target coverage-report # writes coverage/index.html + +if(GCOV) + find_program(GCOVR NAMES gcovr) + set(_coverage_dir ${CMAKE_BINARY_DIR}/coverage) + set(_coverage_index ${_coverage_dir}/index.html) + set(GCOVR_OPTIONS "" CACHE STRING + "Extra options passed to gcovr for the coverage-report target") + separate_arguments(_gcovr_options UNIX_COMMAND "${GCOVR_OPTIONS}") + + # coverage-reset clears all accumulated data; the M2 binary may have written + # .gcda anywhere in the tree, so this is deliberately not engine-scoped. + add_custom_target(coverage-reset + COMMENT "Deleting accumulated .gcda coverage counters" + COMMAND find ${CMAKE_BINARY_DIR} -name "*.gcda" -delete) + + if(NOT GCOVR) + message(WARNING "GCOV is ON but gcovr was not found; the 'coverage-report' target will not be created. Install gcovr (e.g. 'pip install gcovr').") + else() + add_custom_target(coverage-report + COMMENT "Generating gcov/gcovr coverage report" + COMMAND ${CMAKE_COMMAND} -E make_directory ${_coverage_dir} + # Pass the build tree as a positional search path (not --object-directory) + # so gcovr runs gcov in each data file's own directory, matching the + # autotools target. + COMMAND ${GCOVR} + --root ${CMAKE_SOURCE_DIR} + ${CMAKE_BINARY_DIR} + # gcov can report a function on multiple lines (inlines at -O0); merge + # those instead of erroring, attributing the function to its first line. + --merge-mode-functions=merge-use-line-min + ${_gcovr_options} + --html-details ${_coverage_index} + --print-summary + # Print the report path as an OSC 8 terminal hyperlink so it is clickable. + # The printf format is a bracket argument (no CMake escaping); the report + # path is passed as $1 to sh. + COMMAND sh -c [==[printf '%s\033]8;;file://%s%s\033\\%s\033]8;;\033\\\n' 'Coverage report: ' "$(hostname)" "$1" "$1"]==] sh "${_coverage_index}" + USES_TERMINAL) + endif() +endif() diff --git a/M2/configure.ac b/M2/configure.ac index 043c4938e8d..106a2ae8d26 100644 --- a/M2/configure.ac +++ b/M2/configure.ac @@ -817,6 +817,24 @@ AS_IF([test $BUILD_lapack = yes], LAPACK_LIBS=-llapack]) AC_SUBST([LINALGLIBS], ["$LAPACK_LIBS $BLAS_LIBS"]) +dnl Apply gcov coverage flags here, *after* the Fortran/BLAS/LAPACK library +dnl detection: gfortran expands --coverage to -lgcov, which would otherwise be +dnl captured into FCLIBS/BLAS_LIBS and clash with the compiler's own coverage +dnl runtime (e.g. clang's libclang_rt.profile) at link time. +AC_SUBST([GCOV],[no]) +AC_ARG_ENABLE([gcov], + [AS_HELP_STRING([--enable-gcov], + [enable gcc/gcov code coverage (implies -O0 and disables stripping)])], + [GCOV=$enableval]) +AS_IF([test "$GCOV" = yes], + [# --coverage implies -fprofile-arcs -ftest-coverage (compile) and + # -lgcov (link); -O0 keeps line/branch attribution meaningful. + CFLAGS="$CFLAGS --coverage -O0" + CXXFLAGS="$CXXFLAGS --coverage -O0" + LDFLAGS="$LDFLAGS --coverage" + ENABLE_STRIP=no + OPTIMIZE=no]) + AS_IF([test $BUILD_givaro = no], [AC_LANG([C++]) AC_CHECK_HEADER([givaro/givinteger.h], @@ -1892,6 +1910,7 @@ AC_MSG_NOTICE([with OS REL = $OS $REL]) AC_MSG_NOTICE([with ARCH = $ARCH]) AC_MSG_NOTICE([with OPTIMIZE = $OPTIMIZE]) AC_MSG_NOTICE([with DEBUG = $DEBUG]) +AC_MSG_NOTICE([with GCOV = $GCOV]) AC_MSG_NOTICE([with GIT_DESCRIPTION = $GIT_DESCRIPTION]) AC_MSG_NOTICE([with GIT_BRANCH = $GIT_BRANCH]) AC_MSG_NOTICE([with DOCUMENTATION = $DOCUMENTATION])