blob: d44b83bf14efa9121e34daf35a640cbf7b6d8bee [file]
"""Check that the coverage is collected with the bundled coverage tool.
The tests run the instrumented program with the same environment variables
that `bazel coverage` sets and check the lcov file that it produces.
"""
import glob
import os
import pathlib
import subprocess
import unittest
from python.runfiles import runfiles
_LIB = "tests/coverage/lib.py"
_MAIN = "tests/coverage/instrumented.py"
def _parse_lcov(path):
"""Parse the line and branch coverage from an lcov file.
Returns:
A dict of the source file to a struct-like dict with the `lines`
(line number to hit count) and `branches` (line number to the list of
the branch hit counts).
"""
result = {}
current = None
for line in pathlib.Path(path).read_text().splitlines():
if line.startswith("SF:"):
current = result.setdefault(
line[len("SF:") :],
{"lines": {}, "branches": {}},
)
elif current is None:
continue
elif line.startswith("DA:"):
lineno, hits = line[len("DA:") :].split(",")[:2]
current["lines"][int(lineno)] = int(hits)
elif line.startswith("BRDA:"):
lineno, _, _, taken = line[len("BRDA:") :].split(",")
current["branches"].setdefault(int(lineno), []).append(
0 if taken == "-" else int(taken)
)
return result
class CoverageCollectionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
rf = runfiles.CreateOrRaise()
root = rf.root()
program = root / os.environ["INSTRUMENTED"]
assert program.exists(), program
cls.lib_lines = (root / "_main" / _LIB).read_text().splitlines()
tmp = pathlib.Path(os.environ["TEST_TMPDIR"])
coverage_dir = tmp / "coverage"
coverage_dir.mkdir()
manifest = tmp / "coverage_manifest.txt"
# Like `bazel coverage`, list all of the instrumented files.
manifest.write_text("\n".join([_LIB, _MAIN]) + "\n")
# Do not leak the coverage settings of this test (e.g. when running
# `bazel coverage`) into the instrumented program.
env = {
k: v
for k, v in os.environ.items()
if not k.startswith("COVERAGE") and k != "PYTHON_COVERAGE"
}
env.update(rf.EnvVars())
env["COVERAGE"] = "1"
env["COVERAGE_DIR"] = str(coverage_dir)
env["COVERAGE_MANIFEST"] = str(manifest)
cls.result = subprocess.run(
[program],
env=env,
capture_output=True,
text=True,
)
cls.lcov_files = glob.glob(str(coverage_dir / "pylcov_*.dat"))
cls.lcov = _parse_lcov(cls.lcov_files[0]) if len(cls.lcov_files) == 1 else {}
def _lineno(self, content):
"""Return the 1-based line number of the unique line in lib.py."""
matches = [
i + 1 for i, line in enumerate(self.lib_lines) if line.strip() == content
]
self.assertEqual(len(matches), 1, f"'{content}' in {_LIB}: {matches}")
return matches[0]
def _line_hits(self, content):
self.assertIn(_LIB, self.lcov, f"lcov: {self.lcov}")
lineno = self._lineno(content)
lines = self.lcov[_LIB]["lines"]
self.assertIn(lineno, lines, f"line {lineno} '{content}' not in lcov: {lines}")
return lines[lineno]
def test_program_succeeds(self):
self.assertEqual(
self.result.returncode,
0,
f"stdout:\n{self.result.stdout}\nstderr:\n{self.result.stderr}",
)
self.assertEqual(self.result.stdout.splitlines(), ["truthy", "hello coverage"])
def test_single_lcov_file_is_produced(self):
self.assertEqual(
len(self.lcov_files),
1,
f"lcov files: {self.lcov_files}\nstderr:\n{self.result.stderr}",
)
def test_instrumented_files_use_relative_paths(self):
# NOTE: coverage.py also reports the files from the source directories
# that were not imported, so only check the instrumented files.
for path in [_LIB, _MAIN]:
with self.subTest(path=path):
self.assertIn(path, self.lcov, f"lcov files: {sorted(self.lcov)}")
self.assertEqual(
[f for f in self.lcov if f != path and f.endswith("/" + path)],
[],
)
def test_executed_lines_are_covered(self):
for content in [
"if value:",
'return "truthy"',
'return "hello " + name',
]:
with self.subTest(line=content):
self.assertGreater(self._line_hits(content), 0)
def test_definitions_are_covered(self):
# Module level statements are executed on import.
for content in [
"def covered(value):",
"def not_covered():",
"class Greeter:",
"def unused(self):",
]:
with self.subTest(line=content):
self.assertGreater(self._line_hits(content), 0)
def test_skipped_branch_is_not_covered(self):
self.assertEqual(self._line_hits('return "falsy"'), 0)
def test_uncalled_function_is_not_covered(self):
self.assertEqual(self._line_hits('return "never called"'), 0)
def test_uncalled_method_is_not_covered(self):
self.assertEqual(self._line_hits('return "unused method"'), 0)
def test_partial_branch_coverage(self):
lineno = self._lineno("if value:")
branches = self.lcov[_LIB]["branches"]
self.assertIn(lineno, branches, f"branches: {branches}")
# Only the `True` branch of the `if` was taken.
self.assertEqual(sorted(b > 0 for b in branches[lineno]), [False, True])