Skip to content

Commit 21f1173

Browse files
wsobelclaude
andcommitted
Add code coverage support to the build
Wire LLVM/gcov coverage instrumentation into the Conan and CMake builds: - cmake/Coverage.cmake: compiler-aware AGENT_ENABLE_COVERAGE option (Clang -fprofile-instr-generate/-fcoverage-mapping, GCC --coverage), included before targets so flags reach the lib, agent, and tests. - conanfile.py: new "coverage" option mapped to AGENT_ENABLE_COVERAGE. - tools/coverage.sh: run ctest, merge profiles, and emit an llvm-cov text + HTML report; auto-detects shared lib vs static test binaries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 70d6996 commit 21f1173

4 files changed

Lines changed: 144 additions & 1 deletion

File tree

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ include(cmake/remove_link_directories.cmake)
7171
include(cmake/osx_no_app_or_frameworks.cmake)
7272
include(cmake/ClangFormat.cmake)
7373
include(cmake/ClangTidy.cmake)
74+
include(cmake/Coverage.cmake)
7475

7576
# Add our projects
7677
add_subdirectory(agent_lib)

‎cmake/Coverage.cmake‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Code coverage instrumentation.
2+
#
3+
# Enable with -DAGENT_ENABLE_COVERAGE=ON (or, via Conan, -o "&:coverage=True").
4+
# Must be included before the agent/test targets are defined so the flags
5+
# propagate to every subsequently-declared target.
6+
#
7+
# Clang/AppleClang -> LLVM source-based coverage (llvm-profdata + llvm-cov)
8+
# GCC -> gcov instrumentation (gcov/gcovr/lcov)
9+
10+
option(AGENT_ENABLE_COVERAGE "Instrument the agent and tests for code coverage" OFF)
11+
12+
if(AGENT_ENABLE_COVERAGE)
13+
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
14+
message(STATUS "Coverage: enabling LLVM source-based coverage instrumentation")
15+
add_compile_options(
16+
$<$<COMPILE_LANGUAGE:CXX>:-fprofile-instr-generate>
17+
$<$<COMPILE_LANGUAGE:CXX>:-fcoverage-mapping>)
18+
add_link_options(-fprofile-instr-generate)
19+
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
20+
message(STATUS "Coverage: enabling gcov instrumentation")
21+
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:--coverage>)
22+
add_link_options(--coverage)
23+
else()
24+
message(WARNING
25+
"AGENT_ENABLE_COVERAGE requested but compiler '${CMAKE_CXX_COMPILER_ID}' is not supported")
26+
endif()
27+
endif()

‎conanfile.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ class MTConnectAgentConan(ConanFile):
1414
license = "Apache License 2.0"
1515
settings = "os", "compiler", "arch", "build_type"
1616
options = { "without_ipv6": [True, False],
17-
"with_ruby": [True, False],
17+
"with_ruby": [True, False],
1818
"development" : [True, False],
19+
"coverage" : [True, False],
1920
"shared": [True, False],
2021
"winver": [None, "ANY"],
2122
"with_docs" : [True, False],
@@ -34,6 +35,7 @@ class MTConnectAgentConan(ConanFile):
3435
"without_ipv6": False,
3536
"with_ruby": True,
3637
"development": False,
38+
"coverage": False,
3739
"shared": False,
3840
"winver": "0x0A00",
3941
"with_docs": False,
@@ -177,6 +179,7 @@ def generate(self):
177179
tc.cache_variables['AGENT_WITH_DOCS'] = self.options.with_docs.__bool__()
178180
tc.cache_variables['AGENT_WITHOUT_IPV6'] = self.options.without_ipv6.__bool__()
179181
tc.cache_variables['DEVELOPMENT'] = self.options.development.__bool__()
182+
tc.cache_variables['AGENT_ENABLE_COVERAGE'] = self.options.coverage.__bool__()
180183
if self.options.agent_prefix:
181184
tc.cache_variables['AGENT_PREFIX'] = self.options.agent_prefix
182185
if is_msvc(self):

‎tools/coverage.sh‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Generate an LLVM source-based code-coverage report for the cppagent project.
4+
#
5+
# Prerequisites: build the agent + tests with coverage instrumentation, e.g.
6+
#
7+
# conan build . --build=missing -pr conan/profiles/macos \
8+
# -o "&:development=True" -o "&:coverage=True" -o "&:shared=True"
9+
#
10+
# # or, with plain CMake:
11+
# cmake -S . -B <build> -DDEVELOPMENT=ON -DSHARED_AGENT_LIB=ON -DAGENT_ENABLE_COVERAGE=ON
12+
# cmake --build <build> -j
13+
#
14+
# Usage:
15+
# tools/coverage.sh [BUILD_DIR]
16+
#
17+
# BUILD_DIR Directory containing the instrumented build (with CMakeCache.txt).
18+
# May also be set via COVERAGE_BUILD_DIR. Defaults to ./build.
19+
#
20+
# Output:
21+
# <BUILD_DIR>/coverage_report.txt per-file summary
22+
# <BUILD_DIR>/coverage_html/ browsable line-by-line HTML
23+
#
24+
# Env knobs:
25+
# REUSE_PROFILES=1 skip running ctest, reuse existing cov/*.profraw
26+
# CTEST_ARGS="..." extra args passed to ctest (default: -j<ncpu>)
27+
28+
set -euo pipefail
29+
30+
BUILD_DIR="${1:-${COVERAGE_BUILD_DIR:-build}}"
31+
32+
if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
33+
echo "error: '$BUILD_DIR' is not a CMake build directory (no CMakeCache.txt)." >&2
34+
echo " Pass the instrumented build dir as the first argument or set COVERAGE_BUILD_DIR." >&2
35+
exit 1
36+
fi
37+
38+
if ! grep -q "AGENT_ENABLE_COVERAGE:.*=ON" "$BUILD_DIR/CMakeCache.txt" 2>/dev/null; then
39+
echo "warning: AGENT_ENABLE_COVERAGE is not ON in '$BUILD_DIR'." >&2
40+
echo " The report will be empty unless the build was instrumented." >&2
41+
fi
42+
43+
# Resolve the LLVM tools (xcrun on macOS, otherwise expect them on PATH).
44+
if command -v xcrun >/dev/null 2>&1; then
45+
LLVM_PROFDATA=(xcrun llvm-profdata)
46+
LLVM_COV=(xcrun llvm-cov)
47+
else
48+
LLVM_PROFDATA=(llvm-profdata)
49+
LLVM_COV=(llvm-cov)
50+
fi
51+
52+
BUILD_DIR="$(cd "$BUILD_DIR" && pwd)"
53+
cd "$BUILD_DIR"
54+
55+
NCPU="$( (sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) )"
56+
CTEST_ARGS="${CTEST_ARGS:--j${NCPU}}"
57+
58+
# 1. Run the test suite, each process emitting its own raw profile.
59+
if [ "${REUSE_PROFILES:-0}" != "1" ]; then
60+
rm -rf cov coverage.profdata coverage_html coverage_report.txt
61+
mkdir -p cov
62+
echo "=== Running ctest (instrumented) ==="
63+
# Don't abort the whole report if a test fails; we still want coverage.
64+
LLVM_PROFILE_FILE="$BUILD_DIR/cov/%p.profraw" ctest --output-on-failure $CTEST_ARGS || true
65+
fi
66+
67+
shopt -s nullglob
68+
PROFRAW=(cov/*.profraw)
69+
if [ "${#PROFRAW[@]}" -eq 0 ]; then
70+
echo "error: no .profraw files were produced in $BUILD_DIR/cov." >&2
71+
echo " Was the build instrumented and did any tests run?" >&2
72+
exit 1
73+
fi
74+
echo "=== Merging ${#PROFRAW[@]} raw profiles ==="
75+
"${LLVM_PROFDATA[@]}" merge -sparse "${PROFRAW[@]}" -o coverage.profdata
76+
77+
# 2. Locate the instrumented object(s).
78+
# Shared build -> a single agent dylib/so. Static build -> all test binaries.
79+
OBJECTS=()
80+
while IFS= read -r lib; do OBJECTS+=("$lib"); done < <(
81+
find . \( -name 'libagent_lib.*' -o -name 'libmtconnect_agent*.dylib' \
82+
-o -name 'libagent_lib*.so' \) \
83+
! -name '*.a' -type f 2>/dev/null | head -1)
84+
85+
if [ "${#OBJECTS[@]}" -eq 0 ]; then
86+
echo "=== No shared agent lib found; using test binaries (static build) ==="
87+
COV_ARGS=()
88+
first=""
89+
while IFS= read -r bin; do
90+
if [ -z "$first" ]; then first="$bin"; else COV_ARGS+=(-object "$bin"); fi
91+
done < <(find . -maxdepth 3 -name '*_test' -type f -perm -u+x 2>/dev/null)
92+
set -- "$first" "${COV_ARGS[@]}"
93+
else
94+
echo "=== Coverage object: ${OBJECTS[0]} ==="
95+
set -- "${OBJECTS[0]}"
96+
fi
97+
98+
IGNORE='(test_package|/build/|\.conan2|_deps|/usr/|gtest|googletest|/Xcode)'
99+
100+
# 3. Per-file summary (also saved to a file).
101+
echo "=== Coverage summary ==="
102+
"${LLVM_COV[@]}" report "$@" -instr-profile=coverage.profdata \
103+
-ignore-filename-regex="$IGNORE" | tee "$BUILD_DIR/coverage_report.txt"
104+
105+
# 4. Browsable HTML.
106+
"${LLVM_COV[@]}" show "$@" -instr-profile=coverage.profdata \
107+
-format=html -output-dir="$BUILD_DIR/coverage_html" \
108+
-show-line-counts-or-regions -ignore-filename-regex="$IGNORE"
109+
110+
echo
111+
echo "Text report: $BUILD_DIR/coverage_report.txt"
112+
echo "HTML report: $BUILD_DIR/coverage_html/index.html"

0 commit comments

Comments
 (0)