diff --git a/coverage/lcovreport.py b/coverage/lcovreport.py index 7f7953ddc..1b33665c1 100644 --- a/coverage/lcovreport.py +++ b/coverage/lcovreport.py @@ -53,6 +53,43 @@ def lcov_lines( outfile.write(f"LH:{analysis.numbers.n_executed}\n") +def lcov_functions( + fr: FileReporter, + file_analysis: Analysis, + outfile: IO[str], +) -> None: + """Emit function coverage records for an analyzed file.""" + # lcov 2.2 introduces a new format for function coverage records. + # We continue to generate the old format because we don't know what + # version of the lcov tools will be used to read this report. + + # suppressions because of https://github.com/pylint-dev/pylint/issues/9923 + functions = [ + (min(region.start, min(region.lines)), #pylint: disable=nested-min-max + max(region.start, max(region.lines)), #pylint: disable=nested-min-max + region) + for region in fr.code_regions() + if region.kind == "function" + ] + if not functions: + return + + functions.sort() + functions_hit = 0 + for first_line, last_line, region in functions: + # A function counts as having been executed if any of it has been + # executed. + analysis = file_analysis.narrow(region.lines) + hit = int(analysis.numbers.n_executed > 0) + functions_hit += hit + + outfile.write(f"FN:{first_line},{last_line},{region.name}\n") + outfile.write(f"FNDA:{hit},{region.name}\n") + + outfile.write(f"FNF:{len(functions)}\n") + outfile.write(f"FNH:{functions_hit}\n") + + def lcov_arcs( analysis: Analysis, lines: list[int], @@ -177,7 +214,7 @@ def lcov_file( source_lines = [] lcov_lines(analysis, lines, source_lines, outfile) - + lcov_functions(fr, analysis, outfile) if analysis.has_arcs: lcov_arcs(analysis, lines, outfile) diff --git a/tests/test_lcov.py b/tests/test_lcov.py index 5c2061dc2..17271cef0 100644 --- a/tests/test_lcov.py +++ b/tests/test_lcov.py @@ -64,6 +64,12 @@ def IsItTrue(): DA:5,0 LF:4 LH:2 + FN:1,2,cuboid_volume + FNDA:0,cuboid_volume + FN:4,5,IsItTrue + FNDA:0,IsItTrue + FNF:2 + FNH:0 end_of_record """) self.assert_doesnt_exist(".coverage") @@ -96,6 +102,12 @@ def IsItTrue(): DA:5,0,LWILTcvARcydjFFyo9qM0A LF:4 LH:2 + FN:1,2,cuboid_volume + FNDA:0,cuboid_volume + FN:4,5,IsItTrue + FNDA:0,IsItTrue + FNF:2 + FNH:0 end_of_record """) actual_result = self.get_lcov_report_content() @@ -120,6 +132,12 @@ def test_simple_line_coverage_two_files(self) -> None: DA:5,0 LF:4 LH:2 + FN:1,2,cuboid_volume + FNDA:0,cuboid_volume + FN:4,5,IsItTrue + FNDA:0,IsItTrue + FNF:2 + FNH:0 end_of_record SF:test_file.py DA:1,1 @@ -132,6 +150,10 @@ def test_simple_line_coverage_two_files(self) -> None: DA:9,0 LF:8 LH:4 + FN:5,9,TestCuboid.test_volume + FNDA:0,TestCuboid.test_volume + FNF:1 + FNH:0 end_of_record """) actual_result = self.get_lcov_report_content(filename="data.lcov") @@ -160,6 +182,10 @@ def is_it_x(x): DA:5,0 LF:4 LH:1 + FN:1,5,is_it_x + FNDA:0,is_it_x + FNF:1 + FNH:0 BRDA:2,0,to line 3,- BRDA:2,0,to line 5,- BRF:2 @@ -203,6 +229,10 @@ def test_is_it_x(self): DA:5,0 LF:4 LH:1 + FN:1,5,is_it_x + FNDA:0,is_it_x + FNF:1 + FNH:0 BRDA:2,0,to line 3,- BRDA:2,0,to line 5,- BRF:2 @@ -217,6 +247,10 @@ def test_is_it_x(self): DA:7,0 LF:6 LH:4 + FN:5,7,TestIsItX.test_is_it_x + FNDA:0,TestIsItX.test_is_it_x + FNF:1 + FNH:0 end_of_record """) actual_result = self.get_lcov_report_content() @@ -348,6 +382,10 @@ def foo(a): DA:7,1 LF:7 LH:7 + FN:1,3,foo + FNDA:1,foo + FNF:1 + FNH:1 BRDA:2,0,to line 3,1 BRDA:2,0,to exit,1 BRF:2 @@ -381,6 +419,10 @@ def foo(a): DA:7,1 LF:7 LH:7 + FN:1,3,foo + FNDA:1,foo + FNF:1 + FNH:1 BRDA:2,0,to line 3,1 BRDA:2,0,to exit 1,1 BRDA:2,0,to exit 2,1