Skip to content

Commit aca68bb

Browse files
authored
Merge pull request #1919 from giuseppe/add-coverage
test: add new target "coverage"
2 parents 5d9977e + 4798015 commit aca68bb

27 files changed

+788
-523
lines changed

Makefile.am

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,22 @@ libocispec/libocispec.la:
8686

8787
libcrun_la_SOURCES = $(libcrun_SOURCES)
8888
libcrun_la_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -fvisibility=hidden
89-
libcrun_la_LIBADD = libocispec/libocispec.la $(FOUND_LIBS) $(maybe_libyajl.la)
89+
if ENABLE_COVERAGE
90+
libcrun_la_CFLAGS += $(COVERAGE_CFLAGS)
91+
libcrun_la_LDFLAGS = -Wl,--version-script=$(abs_top_srcdir)/libcrun.lds $(COVERAGE_LDFLAGS)
92+
else
9093
libcrun_la_LDFLAGS = -Wl,--version-script=$(abs_top_srcdir)/libcrun.lds
94+
endif
95+
libcrun_la_LIBADD = libocispec/libocispec.la $(FOUND_LIBS) $(maybe_libyajl.la)
9196

9297
# build a version with all the symbols visible for testing
9398
if BUILD_TESTS
9499
libcrun_testing_la_SOURCES = $(libcrun_SOURCES)
95100
libcrun_testing_la_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -fvisibility=default
101+
if ENABLE_COVERAGE
102+
libcrun_testing_la_CFLAGS += $(COVERAGE_CFLAGS)
103+
libcrun_testing_la_LDFLAGS = $(COVERAGE_LDFLAGS)
104+
endif
96105
libcrun_testing_la_LIBADD = libocispec/libocispec.la $(maybe_libyajl.la)
97106
endif
98107

@@ -137,16 +146,27 @@ dist-luarock: $(LUACRUN_ROCK)
137146
endif
138147

139148
crun_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -D CRUN_LIBDIR="\"$(CRUN_LIBDIR)\""
149+
if ENABLE_COVERAGE
150+
crun_CFLAGS += $(COVERAGE_CFLAGS)
151+
endif
140152
crun_SOURCES = src/crun.c src/run.c src/delete.c src/kill.c src/pause.c src/unpause.c src/oci_features.c src/spec.c \
141153
src/exec.c src/list.c src/create.c src/start.c src/state.c src/update.c src/ps.c \
142154
src/checkpoint.c src/restore.c src/mounts.c src/run_create.c
143155

144156
if DYNLOAD_LIBCRUN
157+
if ENABLE_COVERAGE
158+
crun_LDFLAGS = -Wl,--unresolved-symbols=ignore-all $(CRUN_LDFLAGS) $(COVERAGE_LDFLAGS)
159+
else
145160
crun_LDFLAGS = -Wl,--unresolved-symbols=ignore-all $(CRUN_LDFLAGS)
161+
endif
146162
else
147163
crun_LDADD = libcrun.la $(FOUND_LIBS) $(maybe_libyajl.la)
164+
if ENABLE_COVERAGE
165+
crun_LDFLAGS = $(CRUN_LDFLAGS) $(COVERAGE_LDFLAGS)
166+
else
148167
crun_LDFLAGS = $(CRUN_LDFLAGS)
149168
endif
169+
endif
150170

151171
EXTRA_DIST = COPYING COPYING.libcrun README.md NEWS SECURITY.md rpm/crun.spec autogen.sh \
152172
src/libcrun/blake3/blake3_impl.h src/libcrun/blake3/blake3.h \
@@ -187,6 +207,7 @@ TESTS_LDADD = libcrun_testing.la $(FOUND_LIBS) $(maybe_libyajl.la)
187207

188208
tests_init_LDADD =
189209
tests_init_LDFLAGS = -static-libgcc -all-static
210+
tests_init_CFLAGS = -g -O2
190211
tests_init_SOURCES = tests/init.c
191212

192213
tests_tests_libcrun_utils_CFLAGS = -I $(abs_top_builddir)/libocispec/src -I $(abs_top_srcdir)/libocispec/src -I $(abs_top_builddir)/src -I $(abs_top_srcdir)/src
@@ -338,4 +359,105 @@ clang-format:
338359
shellcheck:
339360
shellcheck autogen.sh build-aux/release.sh tests/run_all_tests.sh tests/*/*.sh contrib/*.sh
340361

341-
.PHONY: coverity sync generate-rust-bindings generate-signals.c generate-mount_flags.c clang-format shellcheck
362+
# Code coverage targets
363+
if ENABLE_COVERAGE
364+
365+
# Clean coverage data
366+
coverage-clean:
367+
@rm -rf coverage-html coverage.info coverage.xml
368+
@find . -name "*.gcda" -delete
369+
@find . -name "*.gcno" -delete
370+
371+
# Reset coverage counters
372+
coverage-reset:
373+
@if test -n "$(LCOV)"; then \
374+
$(LCOV) --zerocounters --directory .; \
375+
fi
376+
377+
# Run tests and collect coverage data
378+
coverage-check: coverage-reset
379+
@echo "Running tests for coverage (single-threaded to avoid race conditions)..."
380+
$(MAKE) -j1 check
381+
@echo "Collecting coverage data..."
382+
383+
# Generate HTML coverage report (preferred method with lcov)
384+
coverage-html: coverage-check
385+
@if test -n "$(LCOV)"; then \
386+
echo "Generating coverage report with lcov..."; \
387+
$(LCOV) --capture --directory . --output-file coverage.info; \
388+
$(LCOV) --remove coverage.info '/usr/*' --output-file coverage.info; \
389+
$(LCOV) --remove coverage.info '*/libocispec/*' --output-file coverage.info; \
390+
$(LCOV) --remove coverage.info '*/tests/test_*.py*' --output-file coverage.info; \
391+
$(LCOV) --remove coverage.info '*/tests/init*' --output-file coverage.info; \
392+
genhtml coverage.info --output-directory coverage-html; \
393+
echo "Coverage report generated in coverage-html/index.html"; \
394+
elif test -n "$(GCOVR)"; then \
395+
echo "Generating coverage report with gcovr..."; \
396+
$(GCOVR) --html --html-details -o coverage.html \
397+
--exclude '/usr/.*' --exclude '.*/libocispec/.*' --exclude '.*/tests/test_.*\.py.*' --exclude '.*/tests/init.*'; \
398+
echo "Coverage report generated in coverage.html"; \
399+
else \
400+
echo "Generating coverage report with gcov..."; \
401+
mkdir -p coverage-html; \
402+
for src in $(libcrun_SOURCES) $(crun_SOURCES); do \
403+
if test -f "$${src}.gcno"; then \
404+
$(GCOV) -o . $$src || true; \
405+
fi; \
406+
done; \
407+
mv *.gcov coverage-html/ 2>/dev/null || true; \
408+
echo "Coverage files generated in coverage-html/"; \
409+
fi
410+
411+
# Generate XML coverage report (for CI tools)
412+
coverage-xml: coverage-check
413+
@if test -n "$(GCOVR)"; then \
414+
echo "Generating XML coverage report with gcovr..."; \
415+
$(GCOVR) --xml -o coverage.xml \
416+
--exclude '/usr/.*' --exclude '.*/libocispec/.*' --exclude '.*/tests/test_.*\.py.*' --exclude '.*/tests/init.*'; \
417+
echo "Coverage report generated in coverage.xml"; \
418+
elif test -n "$(LCOV)"; then \
419+
echo "Generating XML coverage report with lcov..."; \
420+
$(LCOV) --capture --directory . --output-file coverage.info; \
421+
$(LCOV) --remove coverage.info '/usr/*' --output-file coverage.info; \
422+
$(LCOV) --remove coverage.info '*/libocispec/*' --output-file coverage.info; \
423+
$(LCOV) --remove coverage.info '*/tests/test_*.py*' --output-file coverage.info; \
424+
$(LCOV) --remove coverage.info '*/tests/init*' --output-file coverage.info; \
425+
echo "Coverage data collected in coverage.info"; \
426+
else \
427+
echo "XML coverage requires gcovr or lcov"; \
428+
exit 1; \
429+
fi
430+
431+
# Generate coverage summary
432+
coverage-summary: coverage-check
433+
@if test -n "$(LCOV)"; then \
434+
echo "Coverage summary (lcov):"; \
435+
$(LCOV) --capture --directory . --output-file coverage.info; \
436+
$(LCOV) --remove coverage.info '/usr/*' --output-file coverage.info; \
437+
$(LCOV) --remove coverage.info '*/libocispec/*' --output-file coverage.info; \
438+
$(LCOV) --remove coverage.info '*/tests/test_*.py*' --output-file coverage.info; \
439+
$(LCOV) --remove coverage.info '*/tests/init*' --output-file coverage.info; \
440+
$(LCOV) --summary coverage.info; \
441+
elif test -n "$(GCOVR)"; then \
442+
echo "Coverage summary (gcovr):"; \
443+
$(GCOVR) --exclude '/usr/.*' --exclude '.*/libocispec/.*' --exclude '.*/tests/test_.*\.py.*' --exclude '.*/tests/init.*'; \
444+
else \
445+
echo "Coverage summary requires lcov or gcovr"; \
446+
fi
447+
448+
else
449+
450+
coverage-clean:
451+
@echo "Coverage support not enabled. Reconfigure with --enable-coverage"
452+
453+
coverage-reset coverage-check coverage-html coverage-xml coverage-summary:
454+
@echo "Coverage support not enabled. Reconfigure with --enable-coverage"
455+
456+
endif
457+
458+
clean-local: coverage-clean
459+
460+
# Coverage targets must not run in parallel due to race conditions in .gcda file writes
461+
.NOTPARALLEL: coverage-reset coverage-check coverage-html coverage-xml coverage-summary
462+
463+
.PHONY: coverity sync generate-rust-bindings generate-signals.c generate-mount_flags.c clang-format shellcheck coverage-clean coverage-reset coverage-check coverage-html coverage-xml coverage-summary

configure.ac

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,45 @@ if test -z "$GPERF"; then
366366
AC_MSG_NOTICE(gperf not found - cannot rebuild signal parser code)
367367
fi
368368

369+
dnl code coverage
370+
AC_ARG_ENABLE([coverage],
371+
AS_HELP_STRING([--enable-coverage], [Enable code coverage support]),
372+
[enable_coverage=$enableval], [enable_coverage=no])
373+
374+
AS_IF([test "x$enable_coverage" = "xyes"], [
375+
AC_CHECK_TOOL([GCOV], [gcov])
376+
if test -z "$GCOV"; then
377+
AC_MSG_ERROR([gcov is required for code coverage])
378+
fi
379+
380+
AC_CHECK_TOOL([LCOV], [lcov])
381+
AC_CHECK_TOOL([GCOVR], [gcovr])
382+
383+
# Choose the best available coverage tool
384+
if test -n "$LCOV"; then
385+
coverage_tool=lcov
386+
elif test -n "$GCOVR"; then
387+
coverage_tool=gcovr
388+
else
389+
coverage_tool=gcov
390+
fi
391+
392+
AC_MSG_NOTICE([Using $coverage_tool for code coverage reporting])
393+
394+
# Add coverage flags
395+
COVERAGE_CFLAGS="--coverage -g -O0 -fno-inline -fno-inline-small-functions -fno-default-inline"
396+
COVERAGE_LDFLAGS="--coverage"
397+
398+
AC_SUBST([COVERAGE_CFLAGS])
399+
AC_SUBST([COVERAGE_LDFLAGS])
400+
AC_SUBST([GCOV])
401+
AC_SUBST([LCOV])
402+
AC_SUBST([GCOVR])
403+
AC_SUBST([coverage_tool], [$coverage_tool])
404+
])
405+
406+
AM_CONDITIONAL([ENABLE_COVERAGE], [test "x$enable_coverage" = "xyes"])
407+
369408
AC_SEARCH_LIBS([argp_parse], [argp], [], [AC_MSG_ERROR([*** argp functions not found - install libargp or argp_standalone])])
370409

371410
AM_CONDITIONAL([PYTHON_BINDINGS], [test "x$with_python_bindings" = "xyes"])

maint.mk

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,7 +1581,7 @@ init-coverage:
15811581
lcov --directory . --zerocounters
15821582
15831583
COVERAGE_CCOPTS ?= "-g --coverage"
1584-
COVERAGE_OUT ?= doc/coverage
1584+
COVERAGE_OUT ?= docs/coverage
15851585
15861586
build-coverage:
15871587
$(MAKE) $(AM_MAKEFLAGS) CFLAGS=$(COVERAGE_CCOPTS) CXXFLAGS=$(COVERAGE_CCOPTS)
@@ -1594,7 +1594,10 @@ gen-coverage:
15941594
genhtml --output-directory $(COVERAGE_OUT) \
15951595
$(COVERAGE_OUT)/$(PACKAGE).info \
15961596
--highlight --frames --legend \
1597-
--title "$(PACKAGE_NAME)"
1597+
--title "$(PACKAGE_NAME)" \
1598+
--ignore-errors unmapped
1599+
1600+
.NOTPARALLEL: coverage init-coverage build-coverage gen-coverage
15981601
15991602
coverage:
16001603
$(MAKE) init-coverage

tests/test_bpf_devices.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,27 @@ def check_bpf_prerequisites():
4747
"""Check all prerequisites for BPF device tests. Returns 77 (skip) if not met, 0 if OK"""
4848
# Skip if not root
4949
if is_rootless():
50-
return 77
50+
return (77, "requires root privileges")
5151

5252
# Skip if not cgroup v2
5353
if not is_cgroup_v2_unified():
54-
return 77
54+
return (77, "requires cgroup v2")
5555

5656
# Skip if systemd not available
5757
if 'SYSTEMD' not in get_crun_feature_string():
58-
return 77
58+
return (77, "systemd support not compiled in")
5959

6060
# Skip if not running on systemd
6161
if not running_on_systemd():
62-
return 77
62+
return (77, "not running on systemd")
6363

6464
# Skip if no BPF support
6565
if not has_bpf_fs():
66-
return 77
66+
return (77, "BPF filesystem not available")
6767

6868
# Skip if systemd doesn't support BPFProgram
6969
if not systemd_supports_bpf_program():
70-
return 77
70+
return (77, "systemd BPFProgram not supported")
7171

7272
return 0
7373

@@ -86,7 +86,7 @@ def test_bpf_devices_systemd():
8686
bpf_path = None
8787
try:
8888
# Run container with systemd cgroup manager.
89-
_, cid = run_and_get_output(conf, command='run', detach=True, cgroup_manager="systemd")
89+
_, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True, cgroup_manager="systemd")
9090

9191
# Get systemd scope.
9292
state = run_crun_command(['state', cid])
@@ -96,20 +96,20 @@ def test_bpf_devices_systemd():
9696

9797
output = subprocess.check_output(['systemctl', 'show', '-PBPFProgram', scope], close_fds=False).decode().strip()
9898
if output == "":
99-
sys.stderr.write("# BPFProgram property not found or empty\n")
99+
logger.info("BPFProgram property not found or empty")
100100
return -1
101101

102102
# Should look like "device:/sys/fs/bpf/crun/crun-xxx_scope".
103103
if "device:/sys/fs/bpf/crun/" not in output:
104-
sys.stderr.write("# Bad BPFProgram property value: `%s`\n" % output)
104+
logger.info("Bad BPFProgram property value: `%s`", prop_value)
105105
return -1
106106

107107
# Test 2: Check that BPF program file was created.
108108

109109
# Extract the path.
110110
bpf_path = output.split("device:", 1)[1]
111111
if not os.path.exists(bpf_path):
112-
sys.stderr.write("# BPF program file `%s` not found\n" % bpf_path)
112+
logger.info("BPF program file `%s` not found", prog_file)
113113
return -1
114114

115115
# Test 3: Check that BPF program is cleaned up.
@@ -118,13 +118,13 @@ def test_bpf_devices_systemd():
118118
run_crun_command(["delete", "-f", cid])
119119
cid = None
120120
if os.path.exists(bpf_path):
121-
sys.stderr.write("# BPF program `%s` still exist after crun delete\n" % bpf_path)
121+
logger.info("BPF program `%s` still exist after crun delete", prog_file)
122122
return -1
123123

124124
return 0
125125

126126
except Exception as e:
127-
sys.stderr.write("# Test failed with exception: %s\n" % str(e))
127+
logger.info("Test failed with exception: %s", e)
128128
return -1
129129
finally:
130130
if cid is not None:

0 commit comments

Comments
 (0)