Skip to content

Commit 391a250

Browse files
authored
feat(test): support coverage (#1205)
1 parent 5d9473c commit 391a250

File tree

5 files changed

+222
-14
lines changed

5 files changed

+222
-14
lines changed

.github/workflows/cpp.yml

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ permissions:
1313

1414
jobs:
1515
build-and-test:
16-
name: "build & test (${{ matrix.cxx.name }} - ${{ matrix.build }})"
16+
name: "build & test (${{ matrix.cxx.name }} - ${{ matrix.build }}${{ matrix.coverage && ' - coverage' || '' }})"
1717
runs-on: ${{ matrix.cxx.os }}
1818
strategy:
1919
fail-fast: false
2020
matrix:
2121
build: [dev, release]
22+
coverage: [false]
2223
cxx:
2324
- name: "Linux - Clang 16"
2425
cmd: clang++-16
@@ -53,6 +54,14 @@ jobs:
5354
- name: "macOS 15 - Apple Clang"
5455
cmd: c++
5556
os: macos-15
57+
include:
58+
# Coverage testing with GCC 14
59+
- build: dev
60+
coverage: true
61+
cxx:
62+
name: "Linux - GCC 14"
63+
cmd: g++-14
64+
os: ubuntu-24.04
5665
env:
5766
CXX: ${{ matrix.cxx.cmd }}
5867
steps:
@@ -69,6 +78,10 @@ jobs:
6978
if: runner.os == 'Linux'
7079
uses: ./.github/actions/setup-ubuntu-deps
7180

81+
- name: Install lcov
82+
if: matrix.coverage
83+
run: sudo apt-get install -y lcov
84+
7285
- name: Setup macOS dependencies
7386
if: runner.os == 'macOS'
7487
uses: ./.github/actions/setup-macos-deps
@@ -100,7 +113,10 @@ jobs:
100113

101114
- name: Stage 2 - Build & Test
102115
run: |
103-
./build/cabin --verbose run ${{ matrix.build == 'release' && '--release' || '' }} test --verbose
116+
[[ '${{ matrix.build }}' == 'release' ]] && RELEASE='--release'
117+
[[ '${{ matrix.coverage }}' == 'true' ]] && COVERAGE='--coverage'
118+
# shellcheck disable=SC2086
119+
./build/cabin --verbose run $RELEASE test --verbose $COVERAGE
104120
105121
- name: Stage 2 - Print version
106122
run: ./cabin-out/${{ matrix.build }}/cabin version --verbose
@@ -111,14 +127,13 @@ jobs:
111127
CABIN: ${{ github.workspace }}/cabin-out/${{ matrix.build }}/cabin
112128
CABIN_TERM_COLOR: auto
113129

114-
# - name: Print coverage
115-
# if: success() && matrix.coverage == 'on'
116-
# run: |
117-
# lcov --directory . --capture --output-file coverage.info --gcov-tool "${CC_PATH/gcc/gcov}"
118-
# lcov --remove coverage.info '/usr/*' "${HOME}"'/.cache/*' --output-file coverage.info
119-
# lcov --list coverage.info
120-
# env:
121-
# CC_PATH: /usr/bin/${{ env.CC }}
130+
- name: Print coverage
131+
if: success() && matrix.coverage
132+
run: |
133+
lcov --capture --directory . --no-external \
134+
--gcov-tool "gcov-${CXX##*-}" \
135+
--output-file coverage.info
136+
lcov --list coverage.info
122137
123138
format:
124139
needs: build-and-test

src/BuildConfig.cc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,16 +746,27 @@ Result<void> BuildConfig::configureBuild() {
746746
return Ok();
747747
}
748748

749+
void BuildConfig::enableCoverage() {
750+
project.compilerOpts.cFlags.others.emplace_back("--coverage");
751+
project.compilerOpts.ldFlags.others.emplace_back("--coverage");
752+
}
753+
749754
Result<BuildConfig> emitMakefile(const Manifest& manifest,
750755
const BuildProfile& buildProfile,
751-
const bool includeDevDeps) {
756+
const bool includeDevDeps,
757+
const bool enableCoverage) {
752758
const Profile& profile = manifest.profiles.at(buildProfile);
753759
auto config = Try(BuildConfig::init(manifest, buildProfile));
754760

755761
// When emitting Makefile, we also build the project. So, we need to
756762
// make sure the dependencies are installed.
757763
Try(config.installDeps(includeDevDeps));
758764

765+
// Add coverage flags if requested
766+
if (enableCoverage) {
767+
config.enableCoverage();
768+
}
769+
759770
bool buildProj = false;
760771
bool buildCompDb = false;
761772
if (config.makefileIsUpToDate()) {

src/BuildConfig.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,13 @@ class BuildConfig {
182182
tbb::spin_mutex* mtx = nullptr);
183183

184184
Result<void> configureBuild();
185+
void enableCoverage();
185186
};
186187

187188
Result<BuildConfig> emitMakefile(const Manifest& manifest,
188189
const BuildProfile& buildProfile,
189-
bool includeDevDeps);
190+
bool includeDevDeps,
191+
bool enableCoverage = false);
190192
Result<std::string> emitCompdb(const Manifest& manifest,
191193
const BuildProfile& buildProfile,
192194
bool includeDevDeps);

src/Cmd/Test.cc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Test {
2828
Manifest manifest;
2929
std::string unittestTargetPrefix;
3030
std::vector<std::string> unittestTargets;
31+
bool enableCoverage = false;
3132

3233
explicit Test(Manifest manifest) : manifest(std::move(manifest)) {}
3334

@@ -43,14 +44,15 @@ const Subcmd TEST_CMD = //
4344
.setShort("t")
4445
.setDesc("Run the tests of a local package")
4546
.addOpt(OPT_JOBS)
47+
.addOpt(Opt{ "--coverage" }.setDesc("Enable code coverage analysis"))
4648
.setMainFn(Test::exec);
4749

4850
Result<void> Test::compileTestTargets() {
4951
const auto start = std::chrono::steady_clock::now();
5052

5153
const BuildProfile buildProfile = BuildProfile::Test;
52-
const BuildConfig config =
53-
Try(emitMakefile(manifest, buildProfile, /*includeDevDeps=*/true));
54+
const BuildConfig config = Try(emitMakefile(
55+
manifest, buildProfile, /*includeDevDeps=*/true, enableCoverage));
5456

5557
// Collect test targets from the generated Makefile.
5658
unittestTargetPrefix = (config.outBasePath / "unittests").string() + '/';
@@ -156,6 +158,8 @@ Result<void> Test::runTestTargets() {
156158
}
157159

158160
Result<void> Test::exec(const CliArgsView cliArgs) {
161+
bool enableCoverage = false;
162+
159163
for (auto itr = cliArgs.begin(); itr != cliArgs.end(); ++itr) {
160164
const std::string_view arg = *itr;
161165

@@ -175,13 +179,16 @@ Result<void> Test::exec(const CliArgsView cliArgs) {
175179
nextArg.data(), nextArg.data() + nextArg.size(), numThreads);
176180
Ensure(ec == std::errc(), "invalid number of threads: {}", nextArg);
177181
setParallelism(numThreads);
182+
} else if (arg == "--coverage") {
183+
enableCoverage = true;
178184
} else {
179185
return TEST_CMD.noSuchArg(arg);
180186
}
181187
}
182188

183189
Manifest manifest = Try(Manifest::tryParse());
184190
Test cmd(std::move(manifest));
191+
cmd.enableCoverage = enableCoverage;
185192

186193
Try(cmd.compileTestTargets());
187194
if (cmd.unittestTargets.empty()) {

tests/08-test.sh

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/bin/sh
2+
3+
test_description='Test the test command'
4+
5+
WHEREAMI=$(dirname "$(realpath "$0")")
6+
. $WHEREAMI/setup.sh
7+
8+
test_expect_success 'cabin test basic functionality' '
9+
OUT=$(mktemp -d) &&
10+
test_when_finished "rm -rf $OUT" &&
11+
cd $OUT &&
12+
"$CABIN" new test_project &&
13+
cd test_project &&
14+
15+
# Add a simple test to the main.cc file
16+
cat >src/main.cc <<-EOF &&
17+
#include <iostream>
18+
19+
#ifdef CABIN_TEST
20+
void test_addition() {
21+
int result = 2 + 2;
22+
if (result != 4) {
23+
std::cerr << "Test failed: 2 + 2 = " << result << ", expected 4" << std::endl;
24+
std::exit(1);
25+
}
26+
std::cout << "test test addition ... ok" << std::endl;
27+
}
28+
29+
int main() {
30+
test_addition();
31+
return 0;
32+
}
33+
#else
34+
int main() {
35+
std::cout << "Hello, world!" << std::endl;
36+
return 0;
37+
}
38+
#endif
39+
EOF
40+
41+
"$CABIN" test 1>stdout 2>stderr &&
42+
(
43+
test -d cabin-out &&
44+
test -d cabin-out/test &&
45+
test -d cabin-out/test/unittests
46+
) &&
47+
grep -q "test addition.*ok" stdout &&
48+
grep -q "1 passed; 0 failed" stderr
49+
'
50+
51+
test_expect_success 'cabin test --help shows coverage option' '
52+
OUT=$(mktemp -d) &&
53+
test_when_finished "rm -rf $OUT" &&
54+
cd $OUT &&
55+
"$CABIN" new test_project &&
56+
cd test_project &&
57+
"$CABIN" test --help >help_output 2>&1 &&
58+
grep -q -- "--coverage" help_output &&
59+
grep -q "Enable code coverage analysis" help_output
60+
'
61+
62+
test_expect_success 'cabin test --coverage generates coverage files' '
63+
OUT=$(mktemp -d) &&
64+
test_when_finished "rm -rf $OUT" &&
65+
cd $OUT &&
66+
"$CABIN" new coverage_project &&
67+
cd coverage_project &&
68+
69+
# Add a simple test
70+
cat >src/main.cc <<-EOF &&
71+
#include <iostream>
72+
73+
#ifdef CABIN_TEST
74+
void test_function() {
75+
std::cout << "test coverage function ... ok" << std::endl;
76+
}
77+
78+
int main() {
79+
test_function();
80+
return 0;
81+
}
82+
#else
83+
int main() {
84+
std::cout << "Hello, world!" << std::endl;
85+
return 0;
86+
}
87+
#endif
88+
EOF
89+
90+
"$CABIN" test --coverage 1>stdout 2>stderr &&
91+
92+
# Check that coverage files were generated
93+
find cabin-out/test -name "*.gcda" | head -1 | grep -q "\.gcda$" &&
94+
find cabin-out/test -name "*.gcno" | head -1 | grep -q "\.gcno$" &&
95+
96+
# Check test output
97+
grep -q "coverage function.*ok" stdout &&
98+
grep -q "1 passed; 0 failed" stderr
99+
'
100+
101+
test_expect_success 'cabin test --coverage uses coverage flags in compilation' '
102+
OUT=$(mktemp -d) &&
103+
test_when_finished "rm -rf $OUT" &&
104+
cd $OUT &&
105+
"$CABIN" new verbose_project &&
106+
cd verbose_project &&
107+
108+
# Add a simple test
109+
cat >src/main.cc <<-EOF &&
110+
#include <iostream>
111+
112+
#ifdef CABIN_TEST
113+
int main() {
114+
std::cout << "test verbose compilation ... ok" << std::endl;
115+
return 0;
116+
}
117+
#else
118+
int main() {
119+
std::cout << "Hello, world!" << std::endl;
120+
return 0;
121+
}
122+
#endif
123+
EOF
124+
125+
# Clear any existing build artifacts to force recompilation
126+
rm -rf cabin-out &&
127+
128+
"$CABIN" test --coverage -vv 1>stdout 2>stderr &&
129+
130+
# Check that --coverage flag appears in compilation commands
131+
grep -q -- "--coverage" stdout &&
132+
133+
# Check test passes
134+
grep -q "verbose compilation.*ok" stdout &&
135+
grep -q "1 passed; 0 failed" stderr
136+
'
137+
138+
test_expect_success 'cabin test without --coverage does not generate coverage files' '
139+
OUT=$(mktemp -d) &&
140+
test_when_finished "rm -rf $OUT" &&
141+
cd $OUT &&
142+
"$CABIN" new no_coverage_project &&
143+
cd no_coverage_project &&
144+
145+
# Add a simple test
146+
cat >src/main.cc <<-EOF &&
147+
#include <iostream>
148+
149+
#ifdef CABIN_TEST
150+
int main() {
151+
std::cout << "test no coverage ... ok" << std::endl;
152+
return 0;
153+
}
154+
#else
155+
int main() {
156+
std::cout << "Hello, world!" << std::endl;
157+
return 0;
158+
}
159+
#endif
160+
EOF
161+
162+
"$CABIN" test 1>stdout 2>stderr &&
163+
164+
# Check that no coverage files were generated in a clean test
165+
# (Note: there might be some from previous tests, so we check that coverage files are not created)
166+
test $(find cabin-out/test -name "*.gcda" | wc -l) -eq 0 &&
167+
168+
# Check test passes
169+
grep -q "no coverage.*ok" stdout &&
170+
grep -q "1 passed; 0 failed" stderr
171+
'
172+
173+
test_done

0 commit comments

Comments
 (0)