Added incremental_coverage.py to calculate incremental_coverage.
Added a file incremental_coverage.py that calculates
incremental coverage. It uses information from files
changed_lines.py and lcov_parser.py.
We are hoping to run this file in KUnit's presubmit script
"kunit.sh" and redirect this output to a file so that
users get information about their incremental coverage.
Signed-off-by: Darya Verzhbinsky <daryaver@google.com>
Signed-off-by: Daniel Latypov <dlatypov@google.com>
Change-Id: Ie8e4069d24ebf5b9af896f874d115a908a4e603b
diff --git a/BUILD.bazel b/BUILD.bazel
index 5de783c..abc1179 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -76,3 +76,15 @@
python_version = "PY3",
deps = ["lcov_parser/get_changed_lines.py"],
)
+
+py_library(
+ name = "incrememtal_coverage",
+ srcs = ["lcov_parser/incrememtal_coverage.py"],
+)
+
+py_test(
+ name = "incrememtal_coverage_test",
+ srcs = ["lcov_parser/incrememtal_coverage_test.py"],
+ python_version = "PY3",
+ deps = ["lcov_parser/incrememtal_coverage.py"],
+)
diff --git a/lcov_parser/incremental_coverage.py b/lcov_parser/incremental_coverage.py
new file mode 100644
index 0000000..592a1c7
--- /dev/null
+++ b/lcov_parser/incremental_coverage.py
@@ -0,0 +1,91 @@
+"""This module calculates the incremental coverage of a git commit using
+information from `git diff` and LCOV's ".info" file.
+"""
+from typing import Tuple, Text, List, Dict
+import changed_lines
+import lcov_parser
+import os
+import re
+import sys
+
+Lines = Dict[int, bool]
+
+
+class CurrDirectoryError(Exception):
+ """CurrDirectoryError is raised when the current working directory
+ cannot be found at the beginning of the absolute paths of the files
+ listed in the ".info" file.
+ """
+ pass
+
+
+def coverage(updated_lines: Dict[Text, List[int]],
+ covered_lines: Dict[Text, Lines],
+ curr_dir: Text) -> float:
+ """Computes the the proportion of lines covered in updated_lines, ignoring
+ those that are not instrumented (e.g. like comments or macros).
+
+ Args:
+ updated_lines: a dict mapping files to a list of its updated lines
+ covered_lines: a dict mapping files to another dict mapping lines to
+ whether they've been covered or not
+ curr_dir: name of the current directory, to be removed from beginning of
+ absolute file paths in covered_lines
+
+ Raises:
+ CurrDirectoryError: if there is an issue removing the current directory from the
+ beginning of the absolute paths of the files listed in the ".info" file.
+ """
+ num_covered_lines = 0
+ total_lines = 0
+
+ for (file_name, covered) in covered_lines.items():
+
+ # remove the prefix to the absolute path in the files
+ if not file_name.startswith(curr_dir):
+ raise CurrDirectoryError('Error removing "' + curr_dir + \
+ '" from beginning of "' + file_name + '".')
+
+ trimmed_filename = file_name[len(curr_dir) + 1:]
+
+ if trimmed_filename in updated_lines:
+
+ for updated_line in updated_lines[trimmed_filename]:
+ if updated_line in covered:
+ total_lines += 1
+
+ if covered[updated_line]:
+ num_covered_lines += 1
+
+ # some files may not have any instrumented code, e.g. header files with
+ # only macros
+ if total_lines == 0:
+ return None
+
+ return float(num_covered_lines) / total_lines
+
+
+def main(lcov_file_name: Text):
+ """Takes in the name of an LCOV ".info" file and calculates the incremental
+ coverage results of the current git commit.
+
+ Raises:
+ GitDiffSyntaxError if there are formatting issues parsing file name in `file_diff`.
+ """
+ _, covered_lines = lcov_parser.parse(open(lcov_file_name, 'r'))
+
+ git_diff = os.popen('git diff HEAD~').read()
+ updated_lines = changed_lines.from_diff(git_diff)
+
+ perc = coverage(updated_lines, covered_lines, os.getcwd())
+
+ if perc == None:
+ print("None")
+ return
+
+ # prints the coverage as a percentage
+ print(round(perc*100, 2))
+
+
+if __name__ == '__main__':
+ main(sys.argv[1])
diff --git a/lcov_parser/incremental_coverage_test.py b/lcov_parser/incremental_coverage_test.py
new file mode 100755
index 0000000..2979d83
--- /dev/null
+++ b/lcov_parser/incremental_coverage_test.py
@@ -0,0 +1,127 @@
+#!/usr/bin/python3
+
+import unittest
+import changed_lines
+import lcov_parser
+import incremental_coverage
+from io import StringIO
+import os
+
+
+class IncrementalCoverageTest(unittest.TestCase):
+
+
+ def test_normal_output(self):
+ updated_lines = {
+ 'file1.txt': [2,3],
+ 'file2.txt': [4]
+ }
+
+ covered_lines = {
+ '/dir/file1.txt': {2: True, 3: False},
+ '/dir/file2.txt': {4: True}
+ }
+
+ coverage = \
+ incremental_coverage.coverage(updated_lines, covered_lines, '/dir')
+
+ self.assertAlmostEqual(coverage, 2/3)
+
+ def test_no_coverage(self):
+ updated_lines = {
+ 'file1.txt': [9],
+ 'file2.txt': [9]
+ }
+
+ covered_lines = {
+ '/dir/file1.txt': {9: False},
+ '/dir/file2.txt': {9: False}
+ }
+
+ coverage = \
+ incremental_coverage.coverage(updated_lines, covered_lines, '/dir')
+
+ self.assertAlmostEqual(coverage, 0.0)
+
+ def test_full_coverage(self):
+ updated_lines = {
+ 'file1.txt': [13, 23],
+ 'file2.txt': [12]
+ }
+
+ covered_lines = {
+ '/dir/file1.txt': {13: True, 23: True},
+ '/dir/file2.txt': {12: True}
+ }
+
+ coverage = \
+ incremental_coverage.coverage(updated_lines, covered_lines, '/dir')
+
+ self.assertAlmostEqual(coverage, 1.0)
+
+ def test_only_comments(self):
+ updated_lines = {
+ 'file1.txt': [4],
+ 'file2.txt': [3]
+ }
+
+ covered_lines = {
+ '/dir/file1.txt': {2: True, 3: False},
+ '/dir/file2.txt': {4: True}
+ }
+
+ coverage = \
+ incremental_coverage.coverage(updated_lines, covered_lines, '/dir')
+
+ self.assertIsNone(coverage)
+
+ def test_method_integration(self):
+ updated_lines = changed_lines.from_diff(_GIT_DIFF.strip())
+
+ with StringIO(_TEST_FILE_DATA.strip()) as test_file:
+ _, covered_lines = lcov_parser.parse(test_file)
+
+ coverage = \
+ incremental_coverage.coverage(updated_lines, covered_lines, '/dir')
+
+ self.assertAlmostEqual(2/3, coverage)
+
+
+_TEST_FILE_DATA = """
+TN:kunit_presubmit_tests
+SF: /dir/file1.txt
+DA:2,1
+DA:3,0
+DA:5,1
+end_of_record
+SF: /dir/file2.txt
+DA:1,1
+DA:2,0
+DA:4,1
+end_of_record
+"""
+
+_GIT_DIFF = """
+diff --git a/file1.txt b/file1.txt
+index 170f11f..041325e 100755
+--- a/file1.txt
++++ b/file1.txt
+@@ -1,10 +1,10 @@
+ line 1
++line 2
++line 3
+
+diff --git a/file2.txt b/file2.txt
+index 6675c3b..e69b66c 100644
+--- a/file2.txt
++++ b/file2.txt
+@@ -1,25 +1,28 @@
+ line 1
+ line 2
++# line 3
++line 4
+"""
+
+
+if __name__ == '__main__':
+ unittest.main()