Skip to content

Commit 79bd1d5

Browse files
Samuel Ortizrbradford
authored andcommitted
tests: Add kcov based coverage generation scripts
And update/add the corresponding README sections. Signed-off-by: Samuel Ortiz <sameo@linux.intel.com>
1 parent eef5535 commit 79bd1d5

File tree

4 files changed

+210
-12
lines changed

4 files changed

+210
-12
lines changed

README.md

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,77 @@ cd linux-loader
1313
cargo build
1414
```
1515

16-
## How to run test for Elf loader
16+
## Tests
1717

18-
```shell
19-
# Assuming your linux-loader is under $HOME
20-
$ cd linux-loader
21-
$ cargo test
22-
$ cargo test -- --nocapture
18+
Our Continuous Integration (CI) pipeline is implemented on top of
19+
[Buildkite](https://buildkite.com/).
20+
For the complete list of tests, check our
21+
[CI pipeline](https://buildkite.com/rust-vmm/vm-virtio-ci).
22+
23+
Each individual test runs in a container. To reproduce a test locally, you can
24+
use the dev-container on both x86 and arm64.
25+
26+
```bash
27+
docker run -it \
28+
--security-opt seccomp=unconfined \
29+
--volume $(pwd):/linux-loader \
30+
fandree/rust-vmm-dev
31+
cd linux-loader/
32+
cargo test
2333
```
2434

25-
## How to run test for bzImage loader
35+
### Test Profiles
36+
37+
The integration tests support two test profiles:
38+
- **devel**: this is the recommended profile for running the integration tests
39+
on a local development machine.
40+
- **ci** (default option): this is the profile used when running the
41+
integration tests as part of the the Continuous Integration (CI).
42+
43+
The test profiles are applicable to tests that run using pytest. Currently only
44+
the [coverage test](tests/test_coverage.py) follows this model as all the other
45+
integration tests are run using the
46+
[Buildkite pipeline](https://buildkite.com/rust-vmm/vm-virtio-ci).
47+
48+
The difference between is declaring tests as passed or failed:
49+
- with the **devel** profile the coverage test passes if the current coverage
50+
is equal or higher than the upstream coverage value. In case the current
51+
coverage is higher, the coverage file is updated to the new coverage value.
52+
- with the **ci** profile the coverage test passes only if the current coverage
53+
is equal to the upstream coverage value.
54+
55+
Further details about the coverage test can be found in the
56+
[Adaptive Coverage](#adaptive-coverage) section.
57+
58+
### Adaptive Coverage
59+
60+
The line coverage is saved in [tests/coverage](tests/coverage). To update the
61+
coverage before submitting a PR, run the coverage test:
62+
63+
```bash
64+
docker run -it \
65+
--security-opt seccomp=unconfined \
66+
--volume $(pwd):/linux-loader \
67+
fandree/rust-vmm-dev
68+
cd linux-loader/
69+
pytest --profile=devel tests/test_coverage.py
70+
```
71+
72+
If the PR coverage is higher than the upstream coverage, the coverage file
73+
needs to be manually added to the commit before submitting the PR:
74+
75+
```bash
76+
git add tests/coverage
77+
```
78+
79+
Failing to do so will generate a fail on the CI pipeline when publishing the
80+
PR.
81+
82+
**NOTE:** The coverage file is only updated in the `devel` test profile. In
83+
the `ci` profile the coverage test will fail if the current coverage is higher
84+
than the coverage reported in [tests/coverage](tests/coverage).
85+
86+
### bzImage test
2687

2788
As we don't want to distribute an entire kernel bzImage, the `load_bzImage` test is ignored by
2889
default. In order to test the bzImage support, one needs to locally build a bzImage, copy it
@@ -35,10 +96,10 @@ $ cd linux-stable
3596
$ make bzImage
3697
$ cp linux-stable/arch/x86/boot/bzImage $LINUX_LOADER/linux-loader/src/loader/
3798
$ cd $LINUX_LOADER/linux-loader
99+
$ docker run -it \
100+
--security-opt seccomp=unconfined \
101+
--volume $(pwd):/linux-loader \
102+
fandree/rust-vmm-dev
103+
$ cd linux-loader/
38104
$ cargo test -- --ignored
39105
```
40-
41-
## Platform Support
42-
- Arch: x86
43-
- OS: Linux/Unix
44-

tests/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
3+
4+
PROFILE_CI="ci"
5+
PROFILE_DEVEL="devel"
6+
7+
8+
def pytest_addoption(parser):
9+
parser.addoption(
10+
"--profile",
11+
default=PROFILE_CI,
12+
choices=[PROFILE_CI, PROFILE_DEVEL],
13+
help="Profile for running the test: {} or {}".format(
14+
PROFILE_CI,
15+
PROFILE_DEVEL
16+
)
17+
)
18+
19+
20+
@pytest.fixture
21+
def profile(request):
22+
return request.config.getoption("--profile")
23+
24+
25+
# This is used for defining global variables in pytest.
26+
def pytest_configure():
27+
pytest.profile_ci = PROFILE_CI
28+
pytest.profile_devel = PROFILE_DEVEL

tests/coverage

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
63.0

tests/test_coverage.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
3+
"""Test the coverage and update the threshold when coverage is increased."""
4+
5+
import os, re, shutil, subprocess
6+
import pytest
7+
8+
def _get_current_coverage():
9+
"""Helper function that returns the coverage computed with kcov."""
10+
kcov_ouput_dir = os.path.join(
11+
os.path.dirname(os.path.realpath(__file__)),
12+
"kcov_output"
13+
)
14+
15+
# By default the build output for kcov and unit tests are both in the debug
16+
# directory. This causes some linker errors that I haven't investigated.
17+
# Error: error: linking with `cc` failed: exit code: 1
18+
# An easy fix is to have separate build directories for kcov & unit tests.
19+
kcov_build_dir = os.path.join(
20+
os.path.dirname(os.path.realpath(__file__)),
21+
"kcov_build"
22+
)
23+
24+
# Remove kcov output and build directory to be sure we are always working
25+
# on a clean environment.
26+
shutil.rmtree(kcov_ouput_dir, ignore_errors=True)
27+
shutil.rmtree(kcov_build_dir, ignore_errors=True)
28+
29+
exclude_pattern = (
30+
'${CARGO_HOME:-$HOME/.cargo/},'
31+
'usr/lib/,'
32+
'lib/'
33+
)
34+
exclude_region = "'mod tests {'"
35+
36+
kcov_cmd = "CARGO_TARGET_DIR={} cargo kcov --all " \
37+
"--output {} -- " \
38+
"--exclude-region={} " \
39+
"--exclude-pattern={} " \
40+
"--verify".format(
41+
kcov_build_dir,
42+
kcov_ouput_dir,
43+
exclude_region,
44+
exclude_pattern
45+
)
46+
47+
subprocess.run(kcov_cmd, shell=True, check=True)
48+
49+
# Read the coverage reported by kcov.
50+
coverage_file = os.path.join(kcov_ouput_dir, 'index.js')
51+
with open(coverage_file) as cov_output:
52+
coverage = float(re.findall(
53+
r'"covered":"(\d+\.\d)"',
54+
cov_output.read()
55+
)[0])
56+
57+
# Remove coverage related directories.
58+
shutil.rmtree(kcov_ouput_dir, ignore_errors=True)
59+
shutil.rmtree(kcov_build_dir, ignore_errors=True)
60+
61+
return coverage
62+
63+
64+
def _get_previous_coverage():
65+
"""Helper function that returns the last reported coverage."""
66+
coverage_path = os.path.join(
67+
os.path.dirname(os.path.realpath(__file__)),
68+
'coverage'
69+
)
70+
71+
# The first and only line of the file contains the coverage.
72+
with open(coverage_path) as f:
73+
coverage = f.readline()
74+
return float(coverage.strip())
75+
76+
def _update_coverage(cov_value):
77+
"""Updates the coverage in the coverage file."""
78+
coverage_path = os.path.join(
79+
os.path.dirname(os.path.realpath(__file__)),
80+
'coverage'
81+
)
82+
83+
with open(coverage_path, "w") as f:
84+
f.write(str(cov_value))
85+
86+
def test_coverage(profile):
87+
current_coverage = _get_current_coverage()
88+
previous_coverage = _get_previous_coverage()
89+
if previous_coverage < current_coverage:
90+
if profile == pytest.profile_ci:
91+
# In the CI Profile we expect the coverage to be manually updated.
92+
assert False, "Coverage is increased from {} to {}. " \
93+
"Please update the coverage in " \
94+
"tests/coverage.".format(
95+
previous_coverage,
96+
current_coverage
97+
)
98+
elif profile == pytest.profile_devel:
99+
_update_coverage(current_coverage)
100+
else:
101+
# This should never happen because pytest should only accept
102+
# the valid test profiles specified with `choices` in
103+
# `pytest_addoption`.
104+
assert False, "Invalid test profile."
105+
elif previous_coverage > current_coverage:
106+
diff = float(previous_coverage - current_coverage)
107+
assert False, "Coverage drops by {:.2f}%. Please add unit tests for" \
108+
"the uncovered lines.".format(diff)

0 commit comments

Comments
 (0)