Skip to content

Commit da67a12

Browse files
committed
Support for running tests with coverage
1 parent 0ae303e commit da67a12

6 files changed

Lines changed: 101 additions & 3 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ include(python)
1212
include(externals)
1313
include(diagnostics)
1414
include(testing)
15+
include(coverage)
1516

1617
enable_testing()
1718

@@ -21,3 +22,4 @@ add_subdirectory(bin)
2122
add_subdirectory(test)
2223

2324
add_tests_build_target()
25+
enable_coverage_if_used()

cmake/coverage.cmake

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
function(enable_coverage_if_used)
2+
if(NOT VANADIUM_USE_COVERAGE)
3+
return()
4+
endif()
5+
6+
# TODO: extract to a separate aux .cmake file if one will be needed
7+
function(collect_targets dir output)
8+
get_property(local_targets DIRECTORY ${dir} PROPERTY BUILDSYSTEM_TARGETS)
9+
get_property(subdirs DIRECTORY ${dir} PROPERTY SUBDIRECTORIES)
10+
foreach(subdir ${subdirs})
11+
collect_targets(${subdir} sub_targets)
12+
list(APPEND local_targets ${sub_targets})
13+
endforeach()
14+
set(${output} ${local_targets} PARENT_SCOPE)
15+
endfunction()
16+
collect_targets(${CMAKE_SOURCE_DIR} all_targets)
17+
18+
foreach(t ${all_targets})
19+
get_target_property(type ${t} TYPE)
20+
if(NOT type MATCHES "EXECUTABLE|STATIC_LIBRARY|SHARED_LIBRARY|MODULE_LIBRARY|OBJECT_LIBRARY")
21+
continue()
22+
endif()
23+
24+
get_target_property(dir ${t} SOURCE_DIR)
25+
if(dir MATCHES "^${PROJECT_SOURCE_DIR}")
26+
target_compile_options(${t} PRIVATE --coverage)
27+
target_link_options(${t} PRIVATE --coverage)
28+
endif()
29+
endforeach()
30+
endfunction()

inv/params/build.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
@dataclass(slots=True)
1111
class BuildOptions:
1212
toolchain: str = DEFAULT_TOOLCHAIN
13+
1314
sanitizers: bool = False
15+
coverage: bool = False
16+
1417
release: bool = False
1518
static: bool = False
1619

inv/params/test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99
@dataclass(slots=True)
1010
class TestOptions:
1111
jobs: int | None = None
12+
1213
filter: str | None = None
1314
exclude: str | None = None
15+
1416
skip_build: bool = False
1517
no_run: bool = False
18+
19+
report_coverage: bool = False
20+
1621
ctest_args: str | None = None
1722

1823

inv/tasks/build.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,20 @@ def configure(
5050

5151
use_compile_commands = True
5252

53+
# convenience thing: interesting flags that impact build are to be put in defs, flags that does not to be put in env
54+
defs = {
55+
"CMAKE_GENERATOR": "Ninja",
56+
}
57+
if build_opts(c).coverage:
58+
# way too specific to add a separate preset for it
59+
defs["VANADIUM_USE_COVERAGE"] = _cmake_bool(True)
60+
5361
c.run(
54-
f"cmake -DCMAKE_GENERATOR=Ninja --preset '{preset}' -B '{build_dir}'",
62+
f"cmake"
63+
f" {' '.join(f'-D{k}={v}' for k, v in defs.items())}"
64+
f" --preset '{preset}'"
65+
f" -B '{build_dir}'",
66+
#
5567
env={
5668
"CMAKE_EXPORT_COMPILE_COMMANDS": _cmake_bool(use_compile_commands),
5769
"CMAKE_COLOR_DIAGNOSTICS": _cmake_bool(sys.stdout.isatty()),

inv/tasks/test.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import os
2+
import shutil
3+
14
from invoke import Context, task
25

6+
from inv.config import OUTPUT_DIR
37
from inv.params import override_params_defaults
4-
from inv.params.build import with_build_params
8+
from inv.params.build import build_opts, with_build_params
59
from inv.params.test import test_opts, with_test_params
610

711
from . import build
812

13+
COVERAGE_DIR = OUTPUT_DIR / "coverage"
14+
915

1016
@task(default=True)
1117
@override_params_defaults(sanitizers=True)
@@ -31,9 +37,39 @@ def test(c: Context, label: str):
3137
args.append(test_opts(c).ctest_args)
3238

3339
c.run(
34-
f"ctest -L '{label}' --output-on-failure --test-dir '{str(build_dir)}' {' '.join(args)}"
40+
f"ctest"
41+
f" -L '{label}'"
42+
f" --output-on-failure"
43+
f" --test-dir '{str(build_dir)}'"
44+
f" {' '.join(args)}"
3545
)
3646

47+
if test_opts(c).report_coverage:
48+
if COVERAGE_DIR.exists():
49+
old_coverage_dir = COVERAGE_DIR.with_suffix(".old")
50+
if old_coverage_dir.exists():
51+
shutil.rmtree(old_coverage_dir)
52+
os.rename(COVERAGE_DIR, old_coverage_dir)
53+
COVERAGE_DIR.mkdir()
54+
55+
excluded_src_dirs = {
56+
build_dir / "_deps",
57+
}
58+
excluded_src_dirs_args = " ".join(f"--exclude '{dir}'" for dir in excluded_src_dirs)
59+
60+
gcov_exec = "llvm-cov gcov" if build_opts(c).toolchain == "clang" else "gcov"
61+
c.run(
62+
f"gcovr"
63+
f" --root .."
64+
f" --object-directory '{build_dir}'"
65+
f" --gcov-executable '{gcov_exec}'"
66+
f" {excluded_src_dirs_args}"
67+
f" --print-summary"
68+
f" --html --html-details -o '{COVERAGE_DIR}/index.html'"
69+
f" -j {test_opts(c).jobs}"
70+
)
71+
print(f"Coverage report is available at '{COVERAGE_DIR}'")
72+
3773

3874
@task
3975
@override_params_defaults(sanitizers=True)
@@ -52,3 +88,13 @@ def e2e(
5288
overwrite_snapshots: bool = False,
5389
):
5490
test(c, label="e2e")
91+
92+
93+
@task
94+
def serve_coverage(c: Context):
95+
if not COVERAGE_DIR.exists():
96+
print(f"Coverage report not found at '{COVERAGE_DIR}'")
97+
exit(1) # TODO(inv): add something like failure(err_msg) that exits
98+
return
99+
100+
c.run(f"python3 -m http.server -d '{COVERAGE_DIR}'")

0 commit comments

Comments
 (0)