diff --git a/codeflash/verification/coverage_utils.py b/codeflash/verification/coverage_utils.py index e14f01a84..f5a41a737 100644 --- a/codeflash/verification/coverage_utils.py +++ b/codeflash/verification/coverage_utils.py @@ -169,6 +169,68 @@ def load_from_jest_json( class JacocoCoverageUtils: """Coverage utils class for parsing JaCoCo XML reports (Java).""" + @staticmethod + def _extract_lines_for_method( + method_start_line: int | None, all_method_start_lines: list[int], line_data: dict[int, dict[str, int]] + ) -> tuple[list[int], list[int], list[list[int]], list[list[int]]]: + """Extract executed/unexecuted lines and branches for a method given its start line.""" + executed_lines: list[int] = [] + unexecuted_lines: list[int] = [] + executed_branches: list[list[int]] = [] + unexecuted_branches: list[list[int]] = [] + + if method_start_line: + method_end_line = None + for start_line in all_method_start_lines: + if start_line > method_start_line: + method_end_line = start_line - 1 + break + if method_end_line is None: + all_lines = sorted(line_data.keys()) + method_end_line = max(all_lines) if all_lines else method_start_line + + for line_nr, data in sorted(line_data.items()): + if method_start_line <= line_nr <= method_end_line: + if data["ci"] > 0: + executed_lines.append(line_nr) + elif data["mi"] > 0: + unexecuted_lines.append(line_nr) + if data["cb"] > 0: + for i in range(data["cb"]): + executed_branches.append([line_nr, i]) + if data["mb"] > 0: + for i in range(data["mb"]): + unexecuted_branches.append([line_nr, data["cb"] + i]) + else: + for line_nr, data in sorted(line_data.items()): + if data["ci"] > 0: + executed_lines.append(line_nr) + elif data["mi"] > 0: + unexecuted_lines.append(line_nr) + if data["cb"] > 0: + for i in range(data["cb"]): + executed_branches.append([line_nr, i]) + if data["mb"] > 0: + for i in range(data["mb"]): + unexecuted_branches.append([line_nr, data["cb"] + i]) + + return executed_lines, unexecuted_lines, executed_branches, unexecuted_branches + + @staticmethod + def _compute_coverage_pct(executed_lines: list[int], unexecuted_lines: list[int], method_elem: Any | None) -> float: + """Compute coverage %, preferring method-level LINE counter over line-by-line calculation.""" + total_lines = set(executed_lines) | set(unexecuted_lines) + coverage_pct = (len(executed_lines) / len(total_lines) * 100) if total_lines else 0.0 + if method_elem is not None: + for counter in method_elem.findall("counter"): + if counter.get("type") == "LINE": + missed = int(counter.get("missed", 0)) + covered = int(counter.get("covered", 0)) + if missed + covered > 0: + coverage_pct = covered / (missed + covered) * 100 + break + return coverage_pct + @staticmethod def load_from_jacoco_xml( jacoco_xml_path: Path, @@ -241,32 +303,31 @@ def load_from_jacoco_xml( # Determine expected source file name from path source_filename = source_code_path.name - # Find the matching sourcefile element and collect all method start lines + # Find the matching sourcefile element and collect all methods sourcefile_elem = None method_elem = None method_start_line = None all_method_start_lines: list[int] = [] + # bare method name -> (element, start_line) for dependent function lookup + all_methods: dict[str, tuple[Any, int]] = {} for package in root.findall(".//package"): - # Look for the sourcefile matching our source file for sf in package.findall("sourcefile"): if sf.get("name") == source_filename: sourcefile_elem = sf break - # Look for the class and method, collect all method start lines for cls in package.findall("class"): cls_source = cls.get("sourcefilename") if cls_source == source_filename: - # Collect all method start lines for boundary detection for method in cls.findall("method"): method_line = int(method.get("line", 0)) if method_line > 0: all_method_start_lines.append(method_line) - - # Check if this is our target method - method_name = method.get("name") - if method_name == function_name: + bare_name = method.get("name") + if bare_name: + all_methods[bare_name] = (method, method_line) + if bare_name == function_name: method_elem = method method_start_line = method_line @@ -277,16 +338,9 @@ def load_from_jacoco_xml( logger.debug(f"No coverage data found for {source_filename} in JaCoCo report") return CoverageData.create_empty(source_code_path, function_name, code_context) - # Sort method start lines to determine boundaries all_method_start_lines = sorted(set(all_method_start_lines)) - # Parse line-level coverage from sourcefile - executed_lines: list[int] = [] - unexecuted_lines: list[int] = [] - executed_branches: list[list[int]] = [] - unexecuted_branches: list[list[int]] = [] - - # Get all line data + # Get all line data from the sourcefile element line_data: dict[int, dict[str, int]] = {} for line in sourcefile_elem.findall("line"): line_nr = int(line.get("nr", 0)) @@ -297,67 +351,11 @@ def load_from_jacoco_xml( "cb": int(line.get("cb", 0)), # covered branches } - # Determine method boundaries - if method_start_line: - # Find the next method's start line to determine this method's end - method_end_line = None - for start_line in all_method_start_lines: - if start_line > method_start_line: - # Next method starts here, so our method ends before this - method_end_line = start_line - 1 - break - - # If no next method found, use the max line in the file - if method_end_line is None: - all_lines = sorted(line_data.keys()) - method_end_line = max(all_lines) if all_lines else method_start_line - - # Filter to lines within the method boundaries - for line_nr, data in sorted(line_data.items()): - if method_start_line <= line_nr <= method_end_line: - # Line is covered if it has covered instructions - if data["ci"] > 0: - executed_lines.append(line_nr) - elif data["mi"] > 0: - unexecuted_lines.append(line_nr) - - # Branch coverage - if data["cb"] > 0: - # Covered branches - each branch is [line, branch_id] - for i in range(data["cb"]): - executed_branches.append([line_nr, i]) - if data["mb"] > 0: - # Missed branches - for i in range(data["mb"]): - unexecuted_branches.append([line_nr, data["cb"] + i]) - else: - # No method found - use all lines in the file - for line_nr, data in sorted(line_data.items()): - if data["ci"] > 0: - executed_lines.append(line_nr) - elif data["mi"] > 0: - unexecuted_lines.append(line_nr) - - if data["cb"] > 0: - for i in range(data["cb"]): - executed_branches.append([line_nr, i]) - if data["mb"] > 0: - for i in range(data["mb"]): - unexecuted_branches.append([line_nr, data["cb"] + i]) - - # Calculate coverage percentage - total_lines = set(executed_lines) | set(unexecuted_lines) - coverage_pct = (len(executed_lines) / len(total_lines) * 100) if total_lines else 0.0 - - # If we found method-level counters, use them as the authoritative source - if method_elem is not None: - for counter in method_elem.findall("counter"): - if counter.get("type") == "LINE": - missed = int(counter.get("missed", 0)) - covered = int(counter.get("covered", 0)) - if missed + covered > 0: - coverage_pct = covered / (missed + covered) * 100 - break + # Extract main function coverage + executed_lines, unexecuted_lines, executed_branches, unexecuted_branches = ( + JacocoCoverageUtils._extract_lines_for_method(method_start_line, all_method_start_lines, line_data) + ) + coverage_pct = JacocoCoverageUtils._compute_coverage_pct(executed_lines, unexecuted_lines, method_elem) main_func_coverage = FunctionCoverage( name=function_name, @@ -368,6 +366,42 @@ def load_from_jacoco_xml( unexecuted_branches=unexecuted_branches, ) + # Find dependent (helper) function — mirrors Python behavior: only when exactly 1 helper exists + dependent_func_coverage = None + dep_helpers = code_context.helper_functions + if len(dep_helpers) == 1: + dep_helper = dep_helpers[0] + dep_bare_name = dep_helper.only_function_name + if dep_bare_name in all_methods: + dep_method_elem, dep_start_line = all_methods[dep_bare_name] + dep_executed, dep_unexecuted, dep_exec_branches, dep_unexec_branches = ( + JacocoCoverageUtils._extract_lines_for_method(dep_start_line, all_method_start_lines, line_data) + ) + dep_coverage_pct = JacocoCoverageUtils._compute_coverage_pct( + dep_executed, dep_unexecuted, dep_method_elem + ) + dependent_func_coverage = FunctionCoverage( + name=dep_helper.qualified_name, + coverage=dep_coverage_pct, + executed_lines=sorted(dep_executed), + unexecuted_lines=sorted(dep_unexecuted), + executed_branches=dep_exec_branches, + unexecuted_branches=dep_unexec_branches, + ) + + # Total coverage = main function + helper (if any), matching Python behavior + total_executed = set(executed_lines) + total_unexecuted = set(unexecuted_lines) + if dependent_func_coverage: + total_executed.update(dependent_func_coverage.executed_lines) + total_unexecuted.update(dependent_func_coverage.unexecuted_lines) + total_lines_set = total_executed | total_unexecuted + total_coverage_pct = (len(total_executed) / len(total_lines_set) * 100) if total_lines_set else coverage_pct + + functions_being_tested = [function_name] + if dependent_func_coverage: + functions_being_tested.append(dependent_func_coverage.name) + graph = { function_name: { "executed_lines": set(executed_lines), @@ -376,16 +410,23 @@ def load_from_jacoco_xml( "unexecuted_branches": unexecuted_branches, } } + if dependent_func_coverage: + graph[dependent_func_coverage.name] = { + "executed_lines": set(dependent_func_coverage.executed_lines), + "unexecuted_lines": set(dependent_func_coverage.unexecuted_lines), + "executed_branches": dependent_func_coverage.executed_branches, + "unexecuted_branches": dependent_func_coverage.unexecuted_branches, + } return CoverageData( file_path=source_code_path, - coverage=coverage_pct, + coverage=total_coverage_pct, function_name=function_name, - functions_being_tested=[function_name], + functions_being_tested=functions_being_tested, graph=graph, code_context=code_context, main_func_coverage=main_func_coverage, - dependent_func_coverage=None, + dependent_func_coverage=dependent_func_coverage, status=CoverageStatus.PARSED_SUCCESSFULLY, ) diff --git a/tests/test_languages/test_java/test_coverage.py b/tests/test_languages/test_java/test_coverage.py index 27d69ff6b..d747a2b4c 100644 --- a/tests/test_languages/test_java/test_coverage.py +++ b/tests/test_languages/test_java/test_coverage.py @@ -1,5 +1,7 @@ """Tests for Java coverage utilities (JaCoCo integration).""" +from __future__ import annotations + from pathlib import Path from codeflash.languages.java.build_tools import ( @@ -8,11 +10,11 @@ get_jacoco_xml_path, is_jacoco_configured, ) -from codeflash.models.models import CodeOptimizationContext, CodeStringsMarkdown, CoverageStatus +from codeflash.models.models import CodeOptimizationContext, CodeStringsMarkdown, CoverageStatus, FunctionSource from codeflash.verification.coverage_utils import JacocoCoverageUtils -def create_mock_code_context() -> CodeOptimizationContext: +def create_mock_code_context(helper_functions: list[FunctionSource] | None = None) -> CodeOptimizationContext: """Create a minimal mock CodeOptimizationContext for testing.""" empty_markdown = CodeStringsMarkdown(code_strings=[], language="java") return CodeOptimizationContext( @@ -21,11 +23,21 @@ def create_mock_code_context() -> CodeOptimizationContext: read_only_context_code="", hashing_code_context="", hashing_code_context_hash="", - helper_functions=[], + helper_functions=helper_functions or [], preexisting_objects=set(), ) +def make_function_source(only_function_name: str, qualified_name: str, file_path: Path) -> FunctionSource: + return FunctionSource( + file_path=file_path, + qualified_name=qualified_name, + fully_qualified_name=qualified_name, + only_function_name=only_function_name, + source_code="", + ) + + # Sample JaCoCo XML report for testing SAMPLE_JACOCO_XML = """ @@ -172,7 +184,7 @@ def create_mock_code_context() -> CodeOptimizationContext: class TestJacocoCoverageUtils: """Tests for JaCoCo XML parsing.""" - def test_load_from_jacoco_xml_basic(self, tmp_path: Path): + def test_load_from_jacoco_xml_basic(self, tmp_path: Path) -> None: """Test loading coverage data from a JaCoCo XML report.""" # Create JaCoCo XML file jacoco_xml = tmp_path / "jacoco.xml" @@ -195,7 +207,7 @@ def test_load_from_jacoco_xml_basic(self, tmp_path: Path): assert coverage_data.status == CoverageStatus.PARSED_SUCCESSFULLY assert coverage_data.function_name == "add" - def test_load_from_jacoco_xml_covered_method(self, tmp_path: Path): + def test_load_from_jacoco_xml_covered_method(self, tmp_path: Path) -> None: """Test parsing a fully covered method.""" jacoco_xml = tmp_path / "jacoco.xml" jacoco_xml.write_text(SAMPLE_JACOCO_XML) @@ -215,7 +227,7 @@ def test_load_from_jacoco_xml_covered_method(self, tmp_path: Path): assert len(coverage_data.main_func_coverage.executed_lines) == 2 assert len(coverage_data.main_func_coverage.unexecuted_lines) == 0 - def test_load_from_jacoco_xml_uncovered_method(self, tmp_path: Path): + def test_load_from_jacoco_xml_uncovered_method(self, tmp_path: Path) -> None: """Test parsing a fully uncovered method.""" jacoco_xml = tmp_path / "jacoco.xml" jacoco_xml.write_text(SAMPLE_JACOCO_XML) @@ -235,7 +247,7 @@ def test_load_from_jacoco_xml_uncovered_method(self, tmp_path: Path): assert len(coverage_data.main_func_coverage.executed_lines) == 0 assert len(coverage_data.main_func_coverage.unexecuted_lines) == 2 - def test_load_from_jacoco_xml_branch_coverage(self, tmp_path: Path): + def test_load_from_jacoco_xml_branch_coverage(self, tmp_path: Path) -> None: """Test parsing branch coverage data.""" jacoco_xml = tmp_path / "jacoco.xml" jacoco_xml.write_text(SAMPLE_JACOCO_XML) @@ -256,7 +268,7 @@ def test_load_from_jacoco_xml_branch_coverage(self, tmp_path: Path): assert len(coverage_data.main_func_coverage.executed_branches) > 0 assert len(coverage_data.main_func_coverage.unexecuted_branches) > 0 - def test_load_from_jacoco_xml_missing_file(self, tmp_path: Path): + def test_load_from_jacoco_xml_missing_file(self, tmp_path: Path) -> None: """Test handling of missing JaCoCo XML file.""" # Non-existent file jacoco_xml = tmp_path / "nonexistent.xml" @@ -275,7 +287,7 @@ def test_load_from_jacoco_xml_missing_file(self, tmp_path: Path): assert coverage_data.status == CoverageStatus.NOT_FOUND assert coverage_data.coverage == 0.0 - def test_load_from_jacoco_xml_invalid_xml(self, tmp_path: Path): + def test_load_from_jacoco_xml_invalid_xml(self, tmp_path: Path) -> None: """Test handling of invalid XML.""" jacoco_xml = tmp_path / "jacoco.xml" jacoco_xml.write_text("this is not valid xml") @@ -294,7 +306,7 @@ def test_load_from_jacoco_xml_invalid_xml(self, tmp_path: Path): assert coverage_data.status == CoverageStatus.NOT_FOUND assert coverage_data.coverage == 0.0 - def test_load_from_jacoco_xml_no_matching_source(self, tmp_path: Path): + def test_load_from_jacoco_xml_no_matching_source(self, tmp_path: Path) -> None: """Test handling when source file is not found in report.""" jacoco_xml = tmp_path / "jacoco.xml" jacoco_xml.write_text(SAMPLE_JACOCO_XML) @@ -314,32 +326,135 @@ def test_load_from_jacoco_xml_no_matching_source(self, tmp_path: Path): assert coverage_data.status == CoverageStatus.NOT_FOUND assert coverage_data.coverage == 0.0 + def test_no_helper_functions_no_dependent_coverage(self, tmp_path: Path) -> None: + """With zero helper functions, dependent_func_coverage stays None and total == main.""" + jacoco_xml = tmp_path / "jacoco.xml" + jacoco_xml.write_text(SAMPLE_JACOCO_XML) + source_path = tmp_path / "Calculator.java" + source_path.write_text("// placeholder") + + coverage_data = JacocoCoverageUtils.load_from_jacoco_xml( + jacoco_xml_path=jacoco_xml, + function_name="add", + code_context=create_mock_code_context(helper_functions=[]), + source_code_path=source_path, + ) + + assert coverage_data.dependent_func_coverage is None + assert coverage_data.functions_being_tested == ["add"] + assert coverage_data.coverage == 100.0 # add is fully covered + + def test_multiple_helpers_no_dependent_coverage(self, tmp_path: Path) -> None: + """With more than one helper, dependent_func_coverage stays None (mirrors Python behavior).""" + jacoco_xml = tmp_path / "jacoco.xml" + jacoco_xml.write_text(SAMPLE_JACOCO_XML) + source_path = tmp_path / "Calculator.java" + source_path.write_text("// placeholder") + + helpers = [ + make_function_source("subtract", "Calculator.subtract", source_path), + make_function_source("multiply", "Calculator.multiply", source_path), + ] + coverage_data = JacocoCoverageUtils.load_from_jacoco_xml( + jacoco_xml_path=jacoco_xml, + function_name="add", + code_context=create_mock_code_context(helper_functions=helpers), + source_code_path=source_path, + ) + + assert coverage_data.dependent_func_coverage is None + assert coverage_data.functions_being_tested == ["add"] + + def test_single_helper_found_in_jacoco_xml(self, tmp_path: Path) -> None: + """With exactly one helper present in the JaCoCo XML, dependent_func_coverage is populated.""" + jacoco_xml = tmp_path / "jacoco.xml" + jacoco_xml.write_text(SAMPLE_JACOCO_XML) + source_path = tmp_path / "Calculator.java" + source_path.write_text("// placeholder") + + # "add" is the main function; "multiply" is the helper + helpers = [make_function_source("multiply", "Calculator.multiply", source_path)] + coverage_data = JacocoCoverageUtils.load_from_jacoco_xml( + jacoco_xml_path=jacoco_xml, + function_name="add", + code_context=create_mock_code_context(helper_functions=helpers), + source_code_path=source_path, + ) + + assert coverage_data.dependent_func_coverage is not None + assert coverage_data.dependent_func_coverage.name == "Calculator.multiply" + # multiply has LINE counter: missed=0, covered=3 → 100% + assert coverage_data.dependent_func_coverage.coverage == 100.0 + assert coverage_data.functions_being_tested == ["add", "Calculator.multiply"] + assert "Calculator.multiply" in coverage_data.graph + + def test_single_helper_absent_from_jacoco_xml(self, tmp_path: Path) -> None: + """Helper listed in code_context but not in the JaCoCo XML → dependent_func_coverage stays None.""" + jacoco_xml = tmp_path / "jacoco.xml" + jacoco_xml.write_text(SAMPLE_JACOCO_XML) + source_path = tmp_path / "Calculator.java" + source_path.write_text("// placeholder") + + helpers = [make_function_source("nonExistentMethod", "Calculator.nonExistentMethod", source_path)] + coverage_data = JacocoCoverageUtils.load_from_jacoco_xml( + jacoco_xml_path=jacoco_xml, + function_name="add", + code_context=create_mock_code_context(helper_functions=helpers), + source_code_path=source_path, + ) + + assert coverage_data.dependent_func_coverage is None + assert coverage_data.functions_being_tested == ["add"] + + def test_total_coverage_aggregates_main_and_helper(self, tmp_path: Path) -> None: + """Total coverage is computed over main + helper lines combined, not just main.""" + jacoco_xml = tmp_path / "jacoco.xml" + jacoco_xml.write_text(SAMPLE_JACOCO_XML) + source_path = tmp_path / "Calculator.java" + source_path.write_text("// placeholder") + + # add (100% covered, lines 40-41) + subtract (0% covered, lines 50-51) + # Combined: 2 executed + 2 unexecuted = 50% total + helpers = [make_function_source("subtract", "Calculator.subtract", source_path)] + coverage_data = JacocoCoverageUtils.load_from_jacoco_xml( + jacoco_xml_path=jacoco_xml, + function_name="add", + code_context=create_mock_code_context(helper_functions=helpers), + source_code_path=source_path, + ) + + assert coverage_data.dependent_func_coverage is not None + assert coverage_data.main_func_coverage.coverage == 100.0 + assert coverage_data.dependent_func_coverage.coverage == 0.0 + # 2 covered (add) + 0 covered (subtract) out of 4 total lines = 50% + assert coverage_data.coverage == 50.0 + class TestJacocoPluginDetection: """Tests for JaCoCo plugin detection in pom.xml.""" - def test_is_jacoco_configured_with_plugin(self, tmp_path: Path): + def test_is_jacoco_configured_with_plugin(self, tmp_path: Path) -> None: """Test detecting JaCoCo when it's configured.""" pom_path = tmp_path / "pom.xml" pom_path.write_text(POM_WITH_JACOCO) assert is_jacoco_configured(pom_path) is True - def test_is_jacoco_configured_without_plugin(self, tmp_path: Path): + def test_is_jacoco_configured_without_plugin(self, tmp_path: Path) -> None: """Test detecting JaCoCo when it's not configured.""" pom_path = tmp_path / "pom.xml" pom_path.write_text(POM_WITHOUT_JACOCO) assert is_jacoco_configured(pom_path) is False - def test_is_jacoco_configured_minimal_pom(self, tmp_path: Path): + def test_is_jacoco_configured_minimal_pom(self, tmp_path: Path) -> None: """Test detecting JaCoCo in minimal pom without build section.""" pom_path = tmp_path / "pom.xml" pom_path.write_text(POM_MINIMAL) assert is_jacoco_configured(pom_path) is False - def test_is_jacoco_configured_missing_file(self, tmp_path: Path): + def test_is_jacoco_configured_missing_file(self, tmp_path: Path) -> None: """Test detection when pom.xml doesn't exist.""" pom_path = tmp_path / "pom.xml" @@ -349,7 +464,7 @@ def test_is_jacoco_configured_missing_file(self, tmp_path: Path): class TestJacocoPluginAddition: """Tests for adding JaCoCo plugin to pom.xml.""" - def test_add_jacoco_plugin_to_minimal_pom(self, tmp_path: Path): + def test_add_jacoco_plugin_to_minimal_pom(self, tmp_path: Path) -> None: """Test adding JaCoCo to a minimal pom.xml.""" pom_path = tmp_path / "pom.xml" pom_path.write_text(POM_MINIMAL) @@ -368,7 +483,7 @@ def test_add_jacoco_plugin_to_minimal_pom(self, tmp_path: Path): assert "prepare-agent" in content assert "report" in content - def test_add_jacoco_plugin_to_pom_with_build(self, tmp_path: Path): + def test_add_jacoco_plugin_to_pom_with_build(self, tmp_path: Path) -> None: """Test adding JaCoCo to pom.xml that has a build section.""" pom_path = tmp_path / "pom.xml" pom_path.write_text(POM_WITHOUT_JACOCO) @@ -380,7 +495,7 @@ def test_add_jacoco_plugin_to_pom_with_build(self, tmp_path: Path): # Verify it's now configured assert is_jacoco_configured(pom_path) is True - def test_add_jacoco_plugin_already_present(self, tmp_path: Path): + def test_add_jacoco_plugin_already_present(self, tmp_path: Path) -> None: """Test adding JaCoCo when it's already configured.""" pom_path = tmp_path / "pom.xml" pom_path.write_text(POM_WITH_JACOCO) @@ -392,7 +507,7 @@ def test_add_jacoco_plugin_already_present(self, tmp_path: Path): # Verify it's still configured assert is_jacoco_configured(pom_path) is True - def test_add_jacoco_plugin_no_namespace(self, tmp_path: Path): + def test_add_jacoco_plugin_no_namespace(self, tmp_path: Path) -> None: """Test adding JaCoCo to pom.xml without XML namespace.""" pom_path = tmp_path / "pom.xml" pom_path.write_text(POM_NO_NAMESPACE) @@ -404,14 +519,14 @@ def test_add_jacoco_plugin_no_namespace(self, tmp_path: Path): # Verify it's now configured assert is_jacoco_configured(pom_path) is True - def test_add_jacoco_plugin_missing_file(self, tmp_path: Path): + def test_add_jacoco_plugin_missing_file(self, tmp_path: Path) -> None: """Test adding JaCoCo when pom.xml doesn't exist.""" pom_path = tmp_path / "pom.xml" result = add_jacoco_plugin_to_pom(pom_path) assert result is False - def test_add_jacoco_plugin_invalid_xml(self, tmp_path: Path): + def test_add_jacoco_plugin_invalid_xml(self, tmp_path: Path) -> None: """Test adding JaCoCo to invalid pom.xml.""" pom_path = tmp_path / "pom.xml" pom_path.write_text("this is not valid xml") @@ -423,12 +538,12 @@ def test_add_jacoco_plugin_invalid_xml(self, tmp_path: Path): class TestJacocoXmlPath: """Tests for JaCoCo XML path resolution.""" - def test_get_jacoco_xml_path(self, tmp_path: Path): + def test_get_jacoco_xml_path(self, tmp_path: Path) -> None: """Test getting the expected JaCoCo XML path.""" path = get_jacoco_xml_path(tmp_path) assert path == tmp_path / "target" / "site" / "jacoco" / "jacoco.xml" - def test_jacoco_plugin_version(self): + def test_jacoco_plugin_version(self) -> None: """Test that JaCoCo version constant is defined.""" assert JACOCO_PLUGIN_VERSION == "0.8.13"