Skip to content

Commit 5435b85

Browse files
129tycJacob Tankingbuzzman
authored
Enhance Lcov format parse functionaility (#493)
* Enhance LCov report parsing to include branch coverage and function hit filtering * Add unit tests for the changes made to the Lcov parsing function. * Update diff_cover/violationsreporters/violations_reporter.py Co-authored-by: Javier Buzzi <[email protected]> * Fixed comments * Reformatted the codes * Enhance LCOV coverage reporting by adding support for function coverage and improving branch coverage logic. Introduce new tests for real-world C++, Python, and TypeScript LCOV data, ensuring accurate output and handling of various coverage formats. * Refactor LCOV coverage logic in violations_reporter.py to improve clarity and maintainability. Update test cases in test_violations_reporter.py to cover new scenarios for branch and function coverage overrides. * Refactor test cases in TestDiffCoverIntegration to compare console output against expected results for LCOV data. Add new fixture files for C++ and TypeScript console reports to ensure accurate validation of coverage output. --------- Co-authored-by: Jacob Tan <[email protected]> Co-authored-by: Javier Buzzi <[email protected]>
1 parent 6397ba8 commit 5435b85

19 files changed

+698
-28
lines changed

diff_cover/violationsreporters/violations_reporter.py

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,15 @@ def __init__(self, lcov_roots, src_roots=None):
308308
def parse(lcov_file):
309309
"""
310310
Parse a single LCov coverage report
311-
File format: https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
311+
File format: https://linux.die.net/man/1/geninfo
312+
More info: https://github.com/linux-test-project/lcov/issues/113#issuecomment-762335134
312313
"""
314+
branch_coverage = defaultdict(
315+
lambda: defaultdict(lambda: {"total": 0, "hit": 0, "executions": 0})
316+
)
317+
function_lines = defaultdict(
318+
dict
319+
) # { source_file: { func_name: (line_no, hit_count) } }
313320
lcov_report = defaultdict(dict)
314321
lcov = open(lcov_file)
315322
while True:
@@ -336,22 +343,92 @@ def parse(lcov_file):
336343
if line_no not in lcov_report[source_file]:
337344
lcov_report[source_file][line_no] = 0
338345
lcov_report[source_file][line_no] += num_executions
346+
elif directive == "BRDA":
347+
args = content.split(",")
348+
if len(args) != 4:
349+
raise ValueError(f"Unknown syntax in lcov report: {line}")
350+
if source_file is None:
351+
raise ValueError(
352+
f"No source file specified for line coverage: {line}"
353+
)
354+
line_no = int(args[0])
355+
taken = (
356+
int(args[3]) if args[3] != "-" else 0
357+
) # Handle '-' for untaken branches
358+
branch_coverage[source_file][line_no]["total"] += 1
359+
branch_coverage[source_file][line_no]["executions"] += taken
360+
if taken > 0:
361+
branch_coverage[source_file][line_no]["hit"] += 1
362+
elif directive == "FN":
363+
args = content.split(",")
364+
if len(args) != 2:
365+
raise ValueError(f"Unknown syntax in lcov report: {line}")
366+
if source_file is None:
367+
raise ValueError(
368+
f"No source file specified for line coverage: {line}"
369+
)
370+
line_no = int(args[0])
371+
func_name = args[1]
372+
function_lines[source_file][func_name] = (line_no, 0)
373+
elif directive == "FNDA":
374+
args = content.split(",")
375+
if len(args) != 2:
376+
raise ValueError(f"Unknown syntax in lcov report: {line}")
377+
if source_file is None:
378+
raise ValueError(
379+
f"No source file specified for line coverage: {line}"
380+
)
381+
hit_count = int(args[0])
382+
func_name = args[1]
383+
if func_name in function_lines[source_file]:
384+
line_no, _ = function_lines[source_file][func_name]
385+
function_lines[source_file][func_name] = (line_no, hit_count)
339386
elif directive in [
340-
"TN",
341-
"FNF",
342-
"FNH",
343-
"FN",
344-
"FNDA",
345-
"LH",
346-
"LF",
347-
"BRF",
348-
"BRH",
349-
"BRDA",
350-
"VER",
387+
"TN", # Test name
388+
"FNF", # Functions found
389+
"FNH", # Functions hit
390+
"LH", # Lines hit
391+
"LF", # Lines found
392+
"BRF", # Branches found
393+
"BRH", # Branches hit
394+
"VER", # Version
395+
"FNL", # Function line coverage (alternative format)
396+
"FNA", # Function name (alternative format)
351397
]:
352-
# these are valid lines, but not we don't need them
398+
# Valid directives that we don't need to process
353399
continue
354400
elif directive == "end_of_record":
401+
# Process collected coverage data for current source file
402+
403+
# 1. Apply branch coverage logic
404+
for line_no, info in branch_coverage[source_file].items():
405+
has_da_directive = line_no in lcov_report[source_file]
406+
407+
if not has_da_directive:
408+
# No line execution data, use branch coverage
409+
if info["total"] > 0 and info["hit"] < info["total"]:
410+
lcov_report[source_file][
411+
line_no
412+
] = 0 # Partial branch coverage
413+
else:
414+
lcov_report[source_file][line_no] = info["executions"]
415+
continue
416+
if not lcov_report[source_file][line_no]:
417+
# Line shows 0 executions, but check if branches were hit
418+
if info["executions"] > 0:
419+
lcov_report[source_file][line_no] = info["executions"]
420+
# Note: Don't override existing positive execution counts
421+
422+
# 2. Apply function coverage logic
423+
for func_name, (line_no, hit) in function_lines[source_file].items():
424+
if line_no not in lcov_report[source_file]:
425+
# No existing line data, use function hit count
426+
lcov_report[source_file][line_no] = hit
427+
# Note: Don't override existing line execution data
428+
429+
# 3. Clean up temporary data for current file
430+
branch_coverage[source_file].clear()
431+
function_lines[source_file].clear()
355432
source_file = None
356433
else:
357434
raise ValueError(f"Unknown syntax in lcov report: {line}")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-------------
2+
Diff Coverage
3+
Diff: origin/main...HEAD, staged and unstaged changes
4+
-------------
5+
calculator.cpp (92.3%): Missing lines 12
6+
-------------
7+
Total: 13 lines
8+
Missing: 1 line
9+
Coverage: 92%
10+
-------------
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
TN:
2+
SF:calculator.cpp
3+
FNL:0,3,4
4+
FNA:0,1,_ZN10Calculator3addEdd
5+
FNL:1,7,12
6+
FNA:1,2,_ZN10Calculator6divideEdd
7+
FNL:2,14,28
8+
FNA:2,2,_ZN10Calculator14processNumbersE
9+
FNF:3
10+
FNH:3
11+
DA:3,1
12+
DA:4,1
13+
DA:7,2
14+
DA:8,2
15+
DA:9,1
16+
DA:11,1
17+
DA:12,0
18+
DA:14,2
19+
DA:16,2
20+
DA:17,4
21+
DA:18,1
22+
DA:20,2
23+
DA:21,2
24+
DA:23,8
25+
DA:24,6
26+
DA:27,2
27+
DA:28,2
28+
LF:17
29+
LH:16
30+
end_of_record

tests/fixtures/git_diff_cpp.txt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
diff --git a/calculator.cpp b/calculator.cpp
2+
new file mode 100644
3+
index 0000000..1234567
4+
--- /dev/null
5+
+++ b/calculator.cpp
6+
@@ -0,0 +1,44 @@
7+
+#include "calculator.h"
8+
+
9+
+double Calculator::add(double a, double b) {
10+
+ return a + b;
11+
+}
12+
+
13+
+double Calculator::divide(double a, double b) {
14+
+ if (b == 0) {
15+
+ throw std::invalid_argument("Division by zero");
16+
+ }
17+
+ return a / b;
18+
+}
19+
+
20+
+std::vector<double> Calculator::processNumbers(const std::vector<double>& numbers,
21+
+ std::function<double(double)> processor) {
22+
+ if (!processor) {
23+
+ processor = [](double x) { return x * 2; };
24+
+ }
25+
+
26+
+ std::vector<double> result;
27+
+ result.reserve(numbers.size());
28+
+
29+
+ for (const auto& num : numbers) {
30+
+ result.push_back(processor(num));
31+
+ }
32+
+
33+
+ return result;
34+
+}
35+
+
36+
+std::string Calculator::getGrade(double score) {
37+
+ if (score < 0 || score > 100) {
38+
+ throw std::invalid_argument("Invalid score");
39+
+ }
40+
+
41+
+ if (score >= 90) {
42+
+ return "A";
43+
+ } else if (score >= 80) {
44+
+ return "B";
45+
+ } else if (score >= 70) {
46+
+ return "C";
47+
+ } else {
48+
+ return "F";
49+
+ }
50+
+}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
diff --git a/calculator.cpp b/calculator.cpp
2+
new file mode 100644
3+
index 0000000..abc1234
4+
--- /dev/null
5+
+++ b/calculator.cpp
6+
@@ -0,0 +1,28 @@
7+
+#include "calculator.h"
8+
+
9+
+double Calculator::add(double a, double b) {
10+
+ return a + b;
11+
+}
12+
+
13+
+double Calculator::divide(double a, double b) {
14+
+ if (b == 0) {
15+
+ throw std::invalid_argument("Division by zero");
16+
+ }
17+
+ return a / b;
18+
+}
19+
+
20+
+std::vector<double> Calculator::processNumbers(const std::vector<double>& numbers, std::function<double(double)> processor) {
21+
+ std::vector<double> result;
22+
+
23+
+ for (const auto& num : numbers) {
24+
+ result.push_back(processor(num));
25+
+ }
26+
+
27+
+ return result;
28+
+}

tests/fixtures/git_diff_python.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
diff --git a/calculator.py b/calculator.py
2+
new file mode 100644
3+
index 0000000..abcdef1
4+
--- /dev/null
5+
+++ b/calculator.py
6+
@@ -0,0 +1,32 @@
7+
+from typing import List, Callable
8+
+
9+
+class Calculator:
10+
+ def add(self, a: float, b: float) -> float:
11+
+ """Function coverage example"""
12+
+ return a + b
13+
+
14+
+ def divide(self, a: float, b: float) -> float:
15+
+ """Branch coverage example"""
16+
+ if b == 0:
17+
+ raise ValueError("Division by zero")
18+
+ return a / b
19+
+
20+
+ def process_numbers(self, numbers: List[float], processor: Callable[[float], float] = None) -> List[float]:
21+
+ """Lambda function coverage example"""
22+
+ if processor is None:
23+
+ processor = lambda x: x * 2
24+
+ return list(map(processor, numbers))
25+
+
26+
+ def get_grade(self, score: float) -> str:
27+
+ """Line and branch coverage example"""
28+
+ if score < 0 or score > 100:
29+
+ raise ValueError("Invalid score")
30+
+
31+
+ if score >= 90:
32+
+ return "A"
33+
+ elif score >= 80:
34+
+ return "B"
35+
+ elif score >= 70:
36+
+ return "C"
37+
+ else:
38+
+ return "F"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
diff --git a/src/calculator.ts b/src/calculator.ts
2+
new file mode 100644
3+
index 0000000..fedcba0
4+
--- /dev/null
5+
+++ b/src/calculator.ts
6+
@@ -0,0 +1,40 @@
7+
+export class Calculator {
8+
+ // Function coverage
9+
+ add(a: number, b: number): number {
10+
+ return a + b;
11+
+ }
12+
+
13+
+ // Branch coverage
14+
+ divide(a: number, b: number): number {
15+
+ if (b === 0) {
16+
+ throw new Error("Division by zero");
17+
+ }
18+
+ return a / b;
19+
+ }
20+
+
21+
+ // Anonymous function coverage
22+
+ processNumbers(
23+
+ numbers: number[],
24+
+ processor: (n: number) => number = (n) => n * 2
25+
+ ): number[] {
26+
+ return numbers.map(processor);
27+
+ }
28+
+
29+
+ // Line and branch coverage
30+
+ getGrade(score: number): string {
31+
+ if (score < 0 || score > 100) {
32+
+ throw new Error("Invalid score");
33+
+ }
34+
+
35+
+ if (score >= 90) {
36+
+ return "A";
37+
+ } else if (score >= 80) {
38+
+ return "B";
39+
+ } else if (score >= 70) {
40+
+ return "C";
41+
+ } else {
42+
+ return "F";
43+
+ }
44+
+ }
45+
+}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
diff --git a/src/calculator.ts b/src/calculator.ts
2+
new file mode 100644
3+
index 0000000..def5678
4+
--- /dev/null
5+
+++ b/src/calculator.ts
6+
@@ -0,0 +1,36 @@
7+
+export class Calculator {
8+
+
9+
+ static divide(a: number, b: number): number {
10+
+ if (b === 0) {
11+
+ throw new Error('Division by zero');
12+
+ }
13+
+ return a / b;
14+
+ }
15+
+
16+
+ static getGrade(score: number): string {
17+
+ if (score >= 90) {
18+
+ return 'A';
19+
+ } else if (score >= 80) {
20+
+ return 'B';
21+
+ } else if (score >= 70) {
22+
+ return 'C';
23+
+ } else if (score >= 60) {
24+
+ return 'D';
25+
+ } else {
26+
+ return 'F';
27+
+ }
28+
+ }
29+
+}

tests/fixtures/lcov.info

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ LH:6
2121
BRF:0
2222
BRH:0
2323
BRDA:3,0,0,0
24-
end_of_record
24+
end_of_record
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-------------
2+
Diff Coverage
3+
Diff: origin/main...HEAD, staged and unstaged changes
4+
-------------
5+
calculator.cpp (96.4%): Missing lines 12
6+
-------------
7+
Total: 28 lines
8+
Missing: 1 line
9+
Coverage: 96%
10+
-------------

0 commit comments

Comments
 (0)