Skip to content

Commit

Permalink
Update code coverage
Browse files Browse the repository at this point in the history
Updated code coverage to use grcov.

Signed-off-by: Jonathan Woollett-Light <[email protected]>
  • Loading branch information
Jonathan Woollett-Light committed Oct 19, 2022
1 parent 3e6df4e commit f2c31df
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 106 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ __pycache__
.vscode
test_results/*
*.core
*.profraw
179 changes: 80 additions & 99 deletions tests/integration_tests/build/test_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,10 @@
target should be put in `s3://spec.firecracker` and automatically updated.
"""


import os
import platform
import re
import shutil
import pytest

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

# We have different coverages based on the host kernel version. This is
Expand All @@ -29,114 +24,100 @@
# Checkout the cpuid crate. In the future other
# differences may appear.
if utils.is_io_uring_supported():
COVERAGE_DICT = {"Intel": 82.99, "AMD": 82.31, "ARM": 82.41}
COVERAGE_DICT = {"Intel": 90.84, "AMD": 90.11, "ARM": 90.51}
else:
COVERAGE_DICT = {"Intel": 80.15, "AMD": 79.48, "ARM": 79.59}
COVERAGE_DICT = {"Intel": 88.39, "AMD": 87.67, "ARM": 87.95}

PROC_MODEL = proc.proc_type()

COVERAGE_MAX_DELTA = 0.05

CARGO_KCOV_REL_PATH = os.path.join(host.CARGO_BUILD_REL_PATH, "kcov")

KCOV_COVERAGE_FILE = "index.js"
"""kcov will aggregate coverage data in this file."""
# Toolchain target architecture.
if ("Intel" in PROC_MODEL) or ("AMD" in PROC_MODEL):
ARCH = "x86_64"
elif "ARM" in PROC_MODEL:
ARCH = "aarch64"
else:
raise Exception(f"Unsupported processor model ({PROC_MODEL})")

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

KCOV_TOTAL_LINES_REGEX = r'"total_lines" : "(\d+)"'
"""Regex for extracting number of total executable lines found by kcov."""
# We allow coverage to have a max difference of `COVERAGE_MAX_DELTA` as percentage before failing
# the test.
COVERAGE_MAX_DELTA = 0.05

SECCOMPILER_BUILD_DIR = "../build/seccompiler"
# grcov 0.8.* requires GLIBC >2.27, this is not present in ubuntu 18.04, when we update the docker
# container with a newer version of ubuntu we can also update this.
GRCOV_VERSION = "0.7.1"


@pytest.mark.timeout(400)
def test_coverage(test_fc_session_root_path, test_session_tmp_path):
"""Test line coverage for rust tests is within bounds.
The result is extracted from the $KCOV_COVERAGE_FILE file created by kcov
after a coverage run.
def test_coverage():
"""Test code coverage
@type: build
"""
proc_model = [item for item in COVERAGE_DICT if item in PROC_MODEL]
assert len(proc_model) == 1, "Could not get processor model!"
coverage_target_pct = COVERAGE_DICT[proc_model[0]]
exclude_pattern = (
"${CARGO_HOME:-$HOME/.cargo/},"
"build/,"
"tests/,"
"usr/lib/gcc,"
"lib/x86_64-linux-gnu/,"
"test_utils.rs,"
# The following files/directories are auto-generated
"bootparam.rs,"
"elf.rs,"
"mpspec.rs,"
"msr_index.rs,"
"bindings.rs,"
"_gen"
# Get coverage target.
processor_model = [item for item in COVERAGE_DICT if item in PROC_MODEL]
assert len(processor_model) == 1, "Could not get processor model!"
coverage_target = COVERAGE_DICT[processor_model[0]]

# Re-direct to repository root.
os.chdir("..")

# Add `llvm-tools-preview` component
utils.run_cmd("rustup component add llvm-tools-preview")

# Generate test profiles.
utils.run_cmd(
f'\
env RUSTFLAGS="-Cinstrument-coverage" \
LLVM_PROFILE_FILE="coverage-%p-%m.profraw" \
cargo test --all --target={TARGET} -- --test-threads=1 \
'
)
exclude_region = "'mod tests {'"
target = "{}-unknown-linux-musl".format(platform.machine())

cmd = (
'CARGO_WRAPPER="kcov" RUSTFLAGS="{}" CARGO_TARGET_DIR={} '
"cargo kcov --all "
"--target {} --output {} -- "
"--exclude-pattern={} "
"--exclude-region={} --verify"
).format(
host.get_rustflags(),
os.path.join(test_fc_session_root_path, CARGO_KCOV_REL_PATH),
target,
test_session_tmp_path,
exclude_pattern,
exclude_region,
)
# We remove the seccompiler custom build directory, created by the
# vmm-level `build.rs`.
# If we don't delete it before and after running the kcov command, we will
# run into linker errors.
shutil.rmtree(SECCOMPILER_BUILD_DIR, ignore_errors=True)
# By default, `cargo kcov` passes `--exclude-pattern=$CARGO_HOME --verify`
# to kcov. To pass others arguments, we need to include the defaults.
utils.run_cmd(cmd)

shutil.rmtree(SECCOMPILER_BUILD_DIR)

coverage_file = os.path.join(test_session_tmp_path, KCOV_COVERAGE_FILE)
with open(coverage_file, encoding="utf-8") as cov_output:
contents = cov_output.read()
covered_lines = int(re.findall(KCOV_COVERED_LINES_REGEX, contents)[0])
total_lines = int(re.findall(KCOV_TOTAL_LINES_REGEX, contents)[0])
coverage = covered_lines / total_lines * 100
print("Number of executable lines: {}".format(total_lines))
print("Number of covered lines: {}".format(covered_lines))
print("Thus, coverage is: {:.2f}%".format(coverage))

coverage_low_msg = (
"Current code coverage ({:.2f}%) is >{:.2f}% below the target ({}%).".format(
coverage, COVERAGE_MAX_DELTA, coverage_target_pct
)
)

assert coverage >= coverage_target_pct - COVERAGE_MAX_DELTA, coverage_low_msg

# Get the name of the variable that needs updating.
namespace = globals()
cov_target_name = [name for name in namespace if namespace[name] is COVERAGE_DICT][
0
]

coverage_high_msg = (
"Current code coverage ({:.2f}%) is >{:.2f}% above the target ({}%).\n"
"Please update the value of {}.".format(
coverage, COVERAGE_MAX_DELTA, coverage_target_pct, cov_target_name
)
# Generate coverage report.
utils.run_cmd(
f'\
cargo install --version {GRCOV_VERSION} grcov \
&& grcov . \
-s . \
--binary-path ./build/cargo_target/{TARGET}/debug/ \
--ignore "build/*" \
-t html \
--branch \
--ignore-not-existing \
-o ./build/cargo_target/{TARGET}/debug/coverage \
'
)

assert coverage <= coverage_target_pct + COVERAGE_MAX_DELTA, coverage_high_msg

return (f"{coverage}%", f"{coverage_target_pct}% +/- {COVERAGE_MAX_DELTA * 100}%")
# Extract coverage from html report.
#
# The line looks like `<abbr title="44724 / 49237">90.83 %</abbr></p>` and is the first
# occurrence of the `<abbr>` element in the file.
#
# When we update grcov to 0.8.* we can update this to pull the coverage from a generated .json
# file.
index = open(
f"./build/cargo_target/{TARGET}/debug/coverage/index.html", encoding="utf-8"
)
index_contents = index.read()
end = index_contents.find(" %</abbr></p>")
start = index_contents[:end].rfind(">")
coverage_str = index_contents[start + 1 : end]
coverage = float(coverage_str)

# Compare coverage.
high = coverage_target * (1.0 + COVERAGE_MAX_DELTA)
low = coverage_target * (1.0 - COVERAGE_MAX_DELTA)
assert (
coverage >= low
), f"Current code coverage ({coverage:.2f}%) is more than {COVERAGE_MAX_DELTA:.2f}% below \
the target ({coverage_target:.2f}%)"
assert (
coverage <= high
), f"Current code coverage ({coverage:.2f}%) is more than {COVERAGE_MAX_DELTA:.2f}% above \
the target ({coverage_target:.2f}%)"
12 changes: 5 additions & 7 deletions tests/integration_tests/build/test_unittests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
import host_tools.cargo_build as host # pylint:disable=import-error

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


def test_unittests(test_fc_session_root_path):
Expand All @@ -20,7 +21,4 @@ def test_unittests(test_fc_session_root_path):
"""
extra_args = "--release --target {} ".format(TARGET)

host.cargo_test(
test_fc_session_root_path,
extra_args=extra_args
)
host.cargo_test(test_fc_session_root_path, extra_args=extra_args)

0 comments on commit f2c31df

Please sign in to comment.