Skip to content

Commit 29786a5

Browse files
Jonathan Woollett-Lightpb8o
Jonathan Woollett-Light
andcommitted
test: Update code coverage to grcov
Moved code coverage from kcov to grcov. Updated `verify_dependencies.rs` as no longer proxying through `cargo kcov` and the environment variable `CARGO_MANIFEST_DIR` is now always set. Signed-off-by: Jonathan Woollett-Light <[email protected]> Co-authored-by: Pablo Barbáchano <[email protected]>
1 parent d184522 commit 29786a5

File tree

8 files changed

+196
-215
lines changed

8 files changed

+196
-215
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ __pycache__
99
.vscode
1010
test_results/*
1111
*.core
12+
*.profraw

src/firecracker/tests/verify_dependencies.rs

+1-6
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@ fn test_no_comparison_requirements() {
1212
// HashMap mapping crate -> [(violating dependency, specified version)]
1313
let mut violating_dependencies = HashMap::new();
1414

15-
let src_firecracker_path = match std::env::var("CARGO_MANIFEST_DIR") {
16-
Ok(path) => path,
17-
Err(_) => return, /* when running under kcov, this variable is not set. As we do not
18-
* actually run any firecracker code, we can just skip this test
19-
* without affecting coverage */
20-
};
15+
let src_firecracker_path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
2116
let src_path = format!("{}/..", src_firecracker_path);
2217

2318
for fc_crate in std::fs::read_dir(src_path).unwrap() {

tests/integration_tests/build/test_coverage.py

+77-100
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@
22
# SPDX-License-Identifier: Apache-2.0
33
"""Tests enforcing code coverage for production code."""
44

5-
6-
import os
7-
import platform
8-
import re
9-
import shutil
105
import pytest
116

127
from framework import utils
13-
import host_tools.cargo_build as host # pylint: disable=import-error
148
from host_tools import proc
159

1610
# We have different coverages based on the host kernel version. This is
@@ -22,116 +16,99 @@
2216
# this contains the frequency while on AMD it does not.
2317
# Checkout the cpuid crate. In the future other
2418
# differences may appear.
25-
if utils.is_io_uring_supported():
26-
COVERAGE_DICT = {"Intel": 82.90, "AMD": 82.23, "ARM": 82.50}
19+
if utils.is_io_uring_supported():a
20+
COVERAGE_DICT = {"Intel": 83.81, "AMD": 82.35, "ARM": 83.29}
2721
else:
28-
COVERAGE_DICT = {"Intel": 79.98, "AMD": 79.35, "ARM": 79.63}
22+
COVERAGE_DICT = {"Intel": 80.09, "AMD": 78.62, "ARM": 79.32}
2923

3024
PROC_MODEL = proc.proc_type()
3125

32-
COVERAGE_MAX_DELTA = 0.05
33-
34-
CARGO_KCOV_REL_PATH = os.path.join(host.CARGO_BUILD_REL_PATH, "kcov")
35-
36-
KCOV_COVERAGE_FILE = "index.js"
37-
"""kcov will aggregate coverage data in this file."""
38-
39-
KCOV_COVERED_LINES_REGEX = r'"covered_lines":"(\d+)"'
40-
"""Regex for extracting number of total covered lines found by kcov."""
26+
# Toolchain target architecture.
27+
if ("Intel" in PROC_MODEL) or ("AMD" in PROC_MODEL):
28+
ARCH = "x86_64"
29+
elif "ARM" in PROC_MODEL:
30+
ARCH = "aarch64"
31+
else:
32+
raise Exception(f"Unsupported processor model ({PROC_MODEL})")
4133

42-
KCOV_TOTAL_LINES_REGEX = r'"total_lines" : "(\d+)"'
43-
"""Regex for extracting number of total executable lines found by kcov."""
34+
# Toolchain target.
35+
# Currently profiling with `aarch64-unknown-linux-musl` is unsupported (see
36+
# https://github.com/rust-lang/rustup/issues/3095#issuecomment-1280705619) therefore we profile and
37+
# run coverage with the `gnu` toolchains and run unit tests with the `musl` toolchains.
38+
TARGET = f"{ARCH}-unknown-linux-gnu"
4439

45-
SECCOMPILER_BUILD_DIR = "../build/seccompiler"
40+
# We allow coverage to have a max difference of `COVERAGE_MAX_DELTA` as percentage before failing
41+
# the test (currently 0.05%).
42+
COVERAGE_MAX_DELTA = 0.0005
4643

4744

4845
@pytest.mark.timeout(400)
49-
def test_coverage(test_fc_session_root_path, test_session_tmp_path, record_property):
50-
"""Test line coverage for rust tests is within bounds.
51-
52-
The result is extracted from the $KCOV_COVERAGE_FILE file created by kcov
53-
after a coverage run.
46+
def test_coverage(monkeypatch, record_property):
47+
"""Test code coverage
5448
5549
@type: build
5650
"""
57-
proc_model = [item for item in COVERAGE_DICT if item in PROC_MODEL]
58-
assert len(proc_model) == 1, "Could not get processor model!"
59-
coverage_target_pct = COVERAGE_DICT[proc_model[0]]
60-
exclude_pattern = (
61-
"${CARGO_HOME:-$HOME/.cargo/},"
62-
"build/,"
63-
"tests/,"
64-
"usr/lib/gcc,"
65-
"lib/x86_64-linux-gnu/,"
66-
"test_utils.rs,"
67-
# The following files/directories are auto-generated
68-
"bootparam.rs,"
69-
"elf.rs,"
70-
"mpspec.rs,"
71-
"msr_index.rs,"
72-
"bindings.rs,"
73-
"_gen"
51+
# Get coverage target.
52+
processor_model = [item for item in COVERAGE_DICT if item in PROC_MODEL]
53+
assert len(processor_model) == 1, "Could not get processor model!"
54+
coverage_target = COVERAGE_DICT[processor_model[0]]
55+
56+
# Re-direct to repository root.
57+
monkeypatch.chdir("..")
58+
59+
# Generate test profiles.
60+
utils.run_cmd(
61+
f'\
62+
RUSTFLAGS="-Cinstrument-coverage" \
63+
LLVM_PROFILE_FILE="coverage-%p-%m.profraw" \
64+
cargo test --all --target={TARGET} -- --test-threads=1 \
65+
'
7466
)
75-
exclude_region = "'mod tests {'"
76-
target = "{}-unknown-linux-musl".format(platform.machine())
77-
78-
cmd = (
79-
'CARGO_WRAPPER="kcov" RUSTFLAGS="{}" CARGO_TARGET_DIR={} '
80-
"cargo kcov --all "
81-
"--target {} --output {} -- "
82-
"--exclude-pattern={} "
83-
"--exclude-region={} --verify"
84-
).format(
85-
host.get_rustflags(),
86-
os.path.join(test_fc_session_root_path, CARGO_KCOV_REL_PATH),
87-
target,
88-
test_session_tmp_path,
89-
exclude_pattern,
90-
exclude_region,
91-
)
92-
# We remove the seccompiler custom build directory, created by the
93-
# vmm-level `build.rs`.
94-
# If we don't delete it before and after running the kcov command, we will
95-
# run into linker errors.
96-
shutil.rmtree(SECCOMPILER_BUILD_DIR, ignore_errors=True)
97-
# By default, `cargo kcov` passes `--exclude-pattern=$CARGO_HOME --verify`
98-
# to kcov. To pass others arguments, we need to include the defaults.
99-
utils.run_cmd(cmd)
100-
101-
shutil.rmtree(SECCOMPILER_BUILD_DIR)
102-
103-
coverage_file = os.path.join(test_session_tmp_path, KCOV_COVERAGE_FILE)
104-
with open(coverage_file, encoding="utf-8") as cov_output:
105-
contents = cov_output.read()
106-
covered_lines = int(re.findall(KCOV_COVERED_LINES_REGEX, contents)[0])
107-
total_lines = int(re.findall(KCOV_TOTAL_LINES_REGEX, contents)[0])
108-
coverage = covered_lines / total_lines * 100
109-
print("Number of executable lines: {}".format(total_lines))
110-
print("Number of covered lines: {}".format(covered_lines))
111-
print("Thus, coverage is: {:.2f}%".format(coverage))
112-
113-
coverage_low_msg = (
114-
"Current code coverage ({:.2f}%) is >{:.2f}% below the target ({}%).".format(
115-
coverage, COVERAGE_MAX_DELTA, coverage_target_pct
116-
)
117-
)
118-
119-
assert coverage >= coverage_target_pct - COVERAGE_MAX_DELTA, coverage_low_msg
12067

121-
# Get the name of the variable that needs updating.
122-
namespace = globals()
123-
cov_target_name = [name for name in namespace if namespace[name] is COVERAGE_DICT][
124-
0
125-
]
68+
# Generate coverage report.
69+
utils.run_cmd(
70+
f"\
71+
grcov . \
72+
-s . \
73+
--binary-path ./build/cargo_target/{TARGET}/debug/ \
74+
--excl-start \"mod tests\" \
75+
--ignore \"['build/*', 'tests/*']\" \
76+
-t html \
77+
--ignore-not-existing \
78+
-o ./build/cargo_target/{TARGET}/debug/coverage \
79+
"
80+
)
12681

127-
coverage_high_msg = (
128-
"Current code coverage ({:.2f}%) is >{:.2f}% above the target ({}%).\n"
129-
"Please update the value of {}.".format(
130-
coverage, COVERAGE_MAX_DELTA, coverage_target_pct, cov_target_name
131-
)
82+
# Extract coverage from html report.
83+
#
84+
# The line looks like `<abbr title="44724 / 49237">90.83 %</abbr></p>` and is the first
85+
# occurrence of the `<abbr>` element in the file.
86+
#
87+
# When we update grcov to 0.8.* we can update this to pull the coverage from a generated .json
88+
# file.
89+
index = open(
90+
f"./build/cargo_target/{TARGET}/debug/coverage/index.html", encoding="utf-8"
13291
)
92+
index_contents = index.read()
93+
end = index_contents.find(" %</abbr></p>")
94+
start = index_contents[:end].rfind(">")
95+
coverage_str = index_contents[start + 1 : end]
96+
coverage = float(coverage_str)
97+
98+
# Compare coverage.
99+
high = coverage_target * (1.0 + COVERAGE_MAX_DELTA)
100+
low = coverage_target * (1.0 - COVERAGE_MAX_DELTA)
133101

102+
# Record coverage.
134103
record_property(
135-
"coverage", f"{coverage}% {coverage_target_pct}% ±{COVERAGE_MAX_DELTA:.2}%"
104+
"coverage", f"{coverage}% {coverage_target}% ±{COVERAGE_MAX_DELTA:.2}%"
136105
)
137-
assert coverage <= coverage_target_pct + COVERAGE_MAX_DELTA, coverage_high_msg
106+
107+
# Assert coverage within delta.
108+
coverage_max_delta_percent = 100 * COVERAGE_MAX_DELTA
109+
assert (
110+
coverage >= low
111+
), f"Current code coverage ({coverage:.2f}%) is more than {coverage_max_delta_percent:.2f}% below the target ({coverage_target:.2f}%)"
112+
assert (
113+
coverage <= high
114+
), f"Current code coverage ({coverage:.2f}%) is more than {coverage_max_delta_percent:.2f}% above the target ({coverage_target:.2f}%)"

tests/integration_tests/build/test_unittests.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
import host_tools.cargo_build as host # pylint:disable=import-error
88

99
MACHINE = platform.machine()
10-
# No need to run unittests for musl since
11-
# we run coverage with musl for all platforms.
12-
TARGET = "{}-unknown-linux-gnu".format(MACHINE)
10+
# Currently profiling with `aarch64-unknown-linux-musl` is unsupported (see
11+
# https://github.com/rust-lang/rustup/issues/3095#issuecomment-1280705619) therefore we profile and
12+
# run coverage with the `gnu` toolchains and run unit tests with the `musl` toolchains.
13+
TARGET = "{}-unknown-linux-musl".format(MACHINE)
1314

1415

1516
def test_unittests(test_fc_session_root_path):

tools/devctr/Dockerfile.aarch64

+7-14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ ARG CARGO_GIT_REGISTRY_DIR="$FIRECRACKER_BUILD_DIR/cargo_git_registry"
1515
ARG DEBIAN_FRONTEND=noninteractive
1616
# By default we don't provide a poetry.lock file
1717
ARG POETRY_LOCK_PATH="/dev/null/*"
18+
# grcov 0.8.* requires GLIBC >2.27, this is not present in ubuntu 18.04, when we update the docker
19+
# container with a newer version of ubuntu we can also update this.
20+
ARG GRCOV_VERSION="0.7.1"
1821

1922
ENV CARGO_HOME=/usr/local/rust
2023
ENV RUSTUP_HOME=/usr/local/rust
@@ -101,20 +104,10 @@ RUN cd "$TMP_POETRY_DIR" \
101104
&& poetry install --no-dev --no-interaction
102105

103106
# Install the Rust toolchain
104-
#
105-
RUN mkdir "$TMP_BUILD_DIR" \
106-
&& curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain "$RUST_TOOLCHAIN" \
107-
&& rustup target add aarch64-unknown-linux-musl \
108-
&& rustup component add clippy \
109-
&& cd "$TMP_BUILD_DIR" \
110-
&& cargo install cargo-kcov \
111-
&& cargo kcov --print-install-kcov-sh | sh \
112-
&& rm -rf "$CARGO_HOME/registry" \
113-
&& ln -s "$CARGO_REGISTRY_DIR" "$CARGO_HOME/registry" \
114-
&& rm -rf "$CARGO_HOME/git" \
115-
&& ln -s "$CARGO_GIT_REGISTRY_DIR" "$CARGO_HOME/git" \
116-
&& cd / \
117-
&& rm -rf "$TMP_BUILD_DIR"
107+
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" \
108+
&& rustup target add aarch64-unknown-linux-musl \
109+
&& rustup component add llvm-tools-preview \
110+
&& cargo install --version $GRCOV_VERSION grcov
118111

119112
RUN ln -s /usr/bin/musl-gcc /usr/bin/aarch64-linux-musl-gcc
120113

tools/devctr/Dockerfile.x86_64

+9-18
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ ARG CARGO_GIT_REGISTRY_DIR="$FIRECRACKER_BUILD_DIR/cargo_git_registry"
1515
ARG DEBIAN_FRONTEND=noninteractive
1616
# By default we don't provide a poetry.lock file
1717
ARG POETRY_LOCK_PATH="/dev/null/*"
18+
# grcov 0.8.* requires GLIBC >2.27, this is not present in ubuntu 18.04, when we update the docker
19+
# container with a newer version of ubuntu we can also update this.
20+
ARG GRCOV_VERSION="0.7.1"
1821

1922
ENV CARGO_HOME=/usr/local/rust
2023
ENV RUSTUP_HOME=/usr/local/rust
@@ -106,24 +109,12 @@ RUN (curl -sL https://deb.nodesource.com/setup_14.x | bash) \
106109
&& rm -rf /var/lib/apt/lists/*
107110

108111
# Install the Rust toolchain
109-
#
110-
RUN mkdir "$TMP_BUILD_DIR" \
111-
&& curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain "$RUST_TOOLCHAIN" \
112-
&& rustup target add x86_64-unknown-linux-musl \
113-
&& rustup component add rustfmt clippy clippy-preview \
114-
&& rustup install --profile minimal "stable" \
115-
&& cd "$TMP_BUILD_DIR" \
116-
&& cargo install cargo-kcov \
117-
&& cargo +"stable" install cargo-audit \
118-
# Fix a version that does not require cargo edition 2021.
119-
&& cargo install --locked cargo-deny --version '^0.9.1' \
120-
&& cargo kcov --print-install-kcov-sh | sh \
121-
&& rm -rf "$CARGO_HOME/registry" \
122-
&& ln -s "$CARGO_REGISTRY_DIR" "$CARGO_HOME/registry" \
123-
&& rm -rf "$CARGO_HOME/git" \
124-
&& ln -s "$CARGO_GIT_REGISTRY_DIR" "$CARGO_HOME/git" \
125-
&& cd / \
126-
&& rm -rf "$TMP_BUILD_DIR"
112+
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain "$RUST_TOOLCHAIN" \
113+
&& rustup target add x86_64-unknown-linux-musl \
114+
&& rustup component add llvm-tools-preview \
115+
&& cargo install cargo-audit \
116+
&& cargo install --locked cargo-deny \
117+
&& cargo install --version $GRCOV_VERSION grcov
127118

128119
# help musl-gcc find linux headers
129120
RUN cd /usr/include/x86_64-linux-musl \

0 commit comments

Comments
 (0)