diff --git a/.gitignore b/.gitignore index 4d5a5f7934b..76713f74350 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__ .vscode test_results/* *.core +*.profraw diff --git a/tests/integration_tests/build/test_coverage.py b/tests/integration_tests/build/test_coverage.py index 2922130a772..91e3f282d85 100644 --- a/tests/integration_tests/build/test_coverage.py +++ b/tests/integration_tests/build/test_coverage.py @@ -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 @@ -35,108 +30,92 @@ 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("..") + + # 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/ \ + --excl-start "mod tests" \ + --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 `90.83 %
` and is the first + # occurrence of the `` 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(" %") + 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}%)" diff --git a/tests/integration_tests/build/test_unittests.py b/tests/integration_tests/build/test_unittests.py index 5ba0be585c3..5477e728689 100644 --- a/tests/integration_tests/build/test_unittests.py +++ b/tests/integration_tests/build/test_unittests.py @@ -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): @@ -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) diff --git a/tools/devctr/Dockerfile.aarch64 b/tools/devctr/Dockerfile.aarch64 index cb7a9b76b3d..bd248667bf8 100644 --- a/tools/devctr/Dockerfile.aarch64 +++ b/tools/devctr/Dockerfile.aarch64 @@ -100,10 +100,8 @@ RUN cd "$TMP_POETRY_DIR" \ RUN mkdir "$TMP_BUILD_DIR" \ && curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain "$RUST_TOOLCHAIN" \ && rustup target add aarch64-unknown-linux-musl \ - && rustup component add clippy \ + && rustup component add clippy llvm-tools-preview \ && cd "$TMP_BUILD_DIR" \ - && cargo install cargo-kcov \ - && cargo kcov --print-install-kcov-sh | sh \ && rm -rf "$CARGO_HOME/registry" \ && ln -s "$CARGO_REGISTRY_DIR" "$CARGO_HOME/registry" \ && rm -rf "$CARGO_HOME/git" \ diff --git a/tools/devctr/Dockerfile.x86_64 b/tools/devctr/Dockerfile.x86_64 index 25aaa8b9eb5..c4192ffbfc7 100644 --- a/tools/devctr/Dockerfile.x86_64 +++ b/tools/devctr/Dockerfile.x86_64 @@ -109,14 +109,12 @@ RUN (curl -sL https://deb.nodesource.com/setup_14.x | bash) \ RUN mkdir "$TMP_BUILD_DIR" \ && curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain "$RUST_TOOLCHAIN" \ && rustup target add x86_64-unknown-linux-musl \ - && rustup component add rustfmt clippy clippy-preview \ + && rustup component add rustfmt clippy clippy-preview llvm-tools-preview \ && rustup install --profile minimal "stable" \ && cd "$TMP_BUILD_DIR" \ - && cargo install cargo-kcov \ && cargo +"stable" install cargo-audit \ # Fix a version that does not require cargo edition 2021. && cargo install --locked cargo-deny --version '^0.9.1' \ - && cargo kcov --print-install-kcov-sh | sh \ && rm -rf "$CARGO_HOME/registry" \ && ln -s "$CARGO_REGISTRY_DIR" "$CARGO_HOME/registry" \ && rm -rf "$CARGO_HOME/git" \ diff --git a/tools/devtool b/tools/devtool index 3d2c8ba790b..f141710c99b 100755 --- a/tools/devtool +++ b/tools/devtool @@ -72,7 +72,7 @@ DEVCTR_IMAGE_NO_TAG="public.ecr.aws/firecracker/fcuvm" # Development container tag -DEVCTR_IMAGE_TAG="v44" +DEVCTR_IMAGE_TAG="cpuid-grcov" # Development container image (name:tag) # This should be updated whenever we upgrade the development container.