Skip to content

Commit 5a46b0a

Browse files
committed
feat: add line coverage to the report
1 parent ab68c0b commit 5a46b0a

File tree

10 files changed

+63
-21
lines changed

10 files changed

+63
-21
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,13 @@ $ mutahunter run --test-command "pytest tests/unit" --code-coverage-report-path
7474
|\/| | | | |-| |-| | | |\| | |- |(
7575
' ` `-' ' ` ' ' ` `-' ' ` ' `-' ' '
7676
77+
2024-07-05 00:26:13,420 INFO: 📊 Line Coverage: 100% 📊
78+
2024-07-05 00:26:13,420 INFO: 🎯 Mutation Coverage: 61.54% 🎯
7779
2024-07-05 00:26:13,420 INFO: 🦠 Total Mutants: 13 🦠
7880
2024-07-05 00:26:13,420 INFO: 🛡️ Survived Mutants: 5 🛡️
7981
2024-07-05 00:26:13,420 INFO: 🗡️ Killed Mutants: 8 🗡️
8082
2024-07-05 00:26:13,421 INFO: 🕒 Timeout Mutants: 0 🕒
8183
2024-07-05 00:26:13,421 INFO: 🔥 Compile Error Mutants: 0 🔥
82-
2024-07-05 00:26:13,421 INFO: 🎯 Mutation Coverage: 61.54% 🎯
8384
2024-07-05 00:26:13,421 INFO: 💰 Total Cost: $0.00583 USD 💰
8485
2024-07-05 00:26:13,421 INFO: Report saved to logs/_latest/mutation_coverage.json
8586
2024-07-05 00:26:13,421 INFO: Report saved to logs/_latest/mutation_coverage_detail.json

examples/go_webservice/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ gocov convert coverage.out | gocov-xml > coverage.xml
1818

1919
```bash
2020
export OPENAI_API_KEY=your-key-goes-here
21-
mutahunter run --test-command "go test" --code-coverage-report-path "coverage.xml" --only-mutate-file-paths "app.go" --model "gpt-4o"
21+
mutahunter run --test-command "go test" --code-coverage-report-path "coverage.xml" --only-mutate-file-paths "app.go"
2222
```

examples/java_maven/readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ mvn test
1111
Coverage report was already generated. Now, we will run Mutahunter to analyze the tests.
1212

1313
```bash
14-
export ANTHROPIC_API_KEY=your-key-goes-here
15-
mutahunter run --test-command "mvn test" --code-coverage-report-path "target/site/jacoco/jacoco.xml" --coverage-type jacoco --model "claude-3-5-sonnet-20240620"
14+
export OPENAI_API_KEY=your-key-goes-here
15+
mutahunter run --test-command "mvn test" --code-coverage-report-path "target/site/jacoco/jacoco.xml" --coverage-type jacoco
1616
```

examples/js_vanilla/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ npm run test:coverage
1313

1414
```bash
1515
export OPENAI_API_KEY=your-key-goes-here
16-
mutahunter run --test-command "npm run test" --code-coverage-report-path "coverage/coverage.xml" --only-mutate-file-paths "ui.js" --model "gpt-3.5-turbo"
16+
mutahunter run --test-command "npm run test" --code-coverage-report-path "coverage/coverage.xml" --only-mutate-file-paths "ui.js"
1717
```

examples/python_fastapi/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ pytest --cov=. --cov-report=xml --cov-report=term
1212
## Running Mutahunter to analyze the tests
1313

1414
```bash
15-
export OPENAI_API_KEY
15+
export OPENAI_API_KEY=your-key-goes-here
1616
mutahunter run --test-command "pytest" --code-coverage-report-path "coverage.xml" --only-mutate-file-paths "app.py"
1717
```

src/mutahunter/core/analyzer.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def __init__(self, config: Dict[str, Any]) -> None:
2121
"""
2222
super().__init__()
2323
self.config = config
24+
self.line_rate = None
2425

2526
if self.config["coverage_type"] == "cobertura":
2627
self.file_lines_executed = self.parse_coverage_report_cobertura()
@@ -35,13 +36,16 @@ def __init__(self, config: Dict[str, Any]) -> None:
3536

3637
def parse_coverage_report_lcov(self) -> Dict[str, List[int]]:
3738
"""
38-
Parses an LCOV code coverage report to extract covered line numbers for each file.
39+
Parses an LCOV code coverage report to extract covered line numbers for each file and calculate overall line coverage.
3940
4041
Returns:
41-
Dict[str, List[int]]: A dictionary where keys are filenames and values are lists of covered line numbers.
42+
Dict[str, Any]: A dictionary where keys are filenames and values are lists of covered line numbers.
43+
Additionally, it includes the overall line coverage percentage.
4244
"""
4345
result = {}
4446
current_file = None
47+
total_lines_found = 0
48+
total_lines_hit = 0
4549

4650
with open(self.config["code_coverage_report_path"], "r") as file:
4751
for line in file:
@@ -54,9 +58,15 @@ def parse_coverage_report_lcov(self) -> Dict[str, List[int]]:
5458
if hits > 0:
5559
line_number = int(parts[0])
5660
result[current_file].append(line_number)
61+
elif line.startswith("LF:") and current_file:
62+
total_lines_found += int(line.strip().split(":")[1])
63+
elif line.startswith("LH:") and current_file:
64+
total_lines_hit += int(line.strip().split(":")[1])
5765
elif line.startswith("end_of_record"):
5866
current_file = None
59-
67+
self.line_rate = (
68+
(total_lines_hit / total_lines_found) if total_lines_found else 0.0
69+
)
6070
return result
6171

6272
def parse_coverage_report_cobertura(self) -> Dict[str, List[int]]:
@@ -69,6 +79,7 @@ def parse_coverage_report_cobertura(self) -> Dict[str, List[int]]:
6979
tree = ET.parse(self.config["code_coverage_report_path"])
7080
root = tree.getroot()
7181
result = {}
82+
self.line_rate = float(root.get("line-rate", 0))
7283
for cls in root.findall(".//class"):
7384
name_attr = cls.get("filename")
7485
executed_lines = []
@@ -81,17 +92,21 @@ def parse_coverage_report_cobertura(self) -> Dict[str, List[int]]:
8192
result[name_attr] = executed_lines
8293
return result
8394

84-
def parse_coverage_report_jacoco(self) -> Dict[str, List[int]]:
95+
def parse_coverage_report_jacoco(self) -> Dict[str, Any]:
8596
"""
86-
Parses a JaCoCo XML code coverage report to extract covered line numbers for each file.
97+
Parses a JaCoCo XML code coverage report to extract covered line numbers for each file and calculate overall line coverage.
8798
8899
Returns:
89-
Dict[str, List[int]]: A dictionary where keys are file paths and values are lists of covered line numbers.
100+
Dict[str, Any]: A dictionary where keys are file paths and values are lists of covered line numbers.
101+
Additionally, it includes the overall line coverage percentage.
90102
"""
91103
tree = ET.parse(self.config["code_coverage_report_path"])
92104
root = tree.getroot()
93105
result = {}
94106

107+
total_lines_missed = 0
108+
total_lines_covered = 0
109+
95110
for package in root.findall(".//package"):
96111
package_name = package.get("name").replace("/", ".")
97112
for sourcefile in package.findall(".//sourcefile"):
@@ -103,13 +118,20 @@ def parse_coverage_report_jacoco(self) -> Dict[str, List[int]]:
103118
executed_lines = []
104119
for line in sourcefile.findall(".//line"):
105120
line_number = int(line.get("nr"))
106-
int(line.get("mi"))
121+
missed = int(line.get("mi"))
107122
covered = int(line.get("ci"))
108123
if covered > 0:
109124
executed_lines.append(line_number)
125+
total_lines_missed += missed
126+
total_lines_covered += covered
110127
if executed_lines:
111128
result[full_filename] = executed_lines
112129

130+
self.line_rate = (
131+
(total_lines_covered / (total_lines_covered + total_lines_missed))
132+
if (total_lines_covered + total_lines_missed) > 0
133+
else 0.0
134+
)
113135
return result
114136

115137
def dry_run(self) -> None:

src/mutahunter/core/hunter.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ def run(self) -> None:
4444
logger.info("🦠 Running mutation testing on entire codebase... 🦠")
4545
self.run_mutation_testing()
4646
logger.info("🎯 Generating Mutation Report... 🎯")
47-
self.mutant_report.generate_report(self.mutants, self.router.total_cost)
47+
self.mutant_report.generate_report(
48+
mutants=self.mutants,
49+
total_cost=self.router.total_cost,
50+
line_rate=self.analyzer.line_rate,
51+
)
4852
logger.info(f"Mutation Testing Ended. Took {round(time.time() - start)}s")
4953
except Exception as e:
5054
logger.error(
@@ -158,6 +162,8 @@ def generate_mutations(self, file_path: str, executed_lines: List[int]) -> None:
158162
router=self.router,
159163
)
160164
for mutant_data in mutant_generator.generate():
165+
if "mutant_code" not in mutant_data:
166+
continue
161167
self.process_mutant(mutant_data, file_path, start_byte, end_byte)
162168

163169
def process_mutant(

src/mutahunter/core/report.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ class MutantReport:
2222
def __init__(self, config) -> None:
2323
self.config = config
2424

25-
def generate_report(self, mutants: List[Mutant], total_cost) -> None:
25+
def generate_report(
26+
self,
27+
mutants: List[Mutant],
28+
total_cost: float,
29+
line_rate: float,
30+
) -> None:
2631
"""
2732
Generates a comprehensive mutation testing report.
2833
@@ -32,10 +37,15 @@ def generate_report(self, mutants: List[Mutant], total_cost) -> None:
3237
mutants = [asdict(mutant) for mutant in mutants]
3338
self.save_report("logs/_latest/mutants.json", mutants)
3439
print(MUTAHUNTER_ASCII)
35-
self.generate_mutant_report(mutants, total_cost)
40+
self.generate_mutant_report(mutants, total_cost, line_rate)
3641
self.generate_mutant_report_detail(mutants)
3742

38-
def generate_mutant_report(self, mutants: List[Mutant], total_cost) -> None:
43+
def generate_mutant_report(
44+
self,
45+
mutants: List[Mutant],
46+
total_cost: float,
47+
line_rate: float,
48+
) -> None:
3949
killed_mutants = [mutant for mutant in mutants if mutant["status"] == "KILLED"]
4050
survived_mutants = [
4151
mutant for mutant in mutants if mutant["status"] == "SURVIVED"
@@ -55,13 +65,15 @@ def generate_mutant_report(self, mutants: List[Mutant], total_cost) -> None:
5565
if valid_mutants
5666
else "0.00%"
5767
)
68+
line_coverage = f"{line_rate * 100:.2f}%"
5869

70+
logger.info("📊 Line Coverage: %.2f%% 📊", line_rate * 100)
71+
logger.info("🎯 Mutation Coverage: %s 🎯", total_mutation_coverage)
5972
logger.info("🦠 Total Mutants: %d 🦠", len(mutants))
6073
logger.info("🛡️ Survived Mutants: %d 🛡️", len(survived_mutants))
6174
logger.info("🗡️ Killed Mutants: %d 🗡️", len(killed_mutants))
6275
logger.info("🕒 Timeout Mutants: %d 🕒", len(timeout_mutants))
6376
logger.info("🔥 Compile Error Mutants: %d 🔥", len(compile_error_mutants))
64-
logger.info("🎯 Mutation Coverage: %s 🎯", total_mutation_coverage)
6577
logger.info("💰 Expected Cost: $%.5f USD 💰", total_cost)
6678

6779
mutation_coverage = {
@@ -71,6 +83,7 @@ def generate_mutant_report(self, mutants: List[Mutant], total_cost) -> None:
7183
"timeout_mutants": len(timeout_mutants),
7284
"compile_error_mutants": len(compile_error_mutants),
7385
"mutation_coverage": total_mutation_coverage,
86+
"line_coverage": line_coverage,
7487
"expected_cost": total_cost,
7588
}
7689

src/mutahunter/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def parse_arguments():
2323
main_parser.add_argument(
2424
"--model",
2525
type=str,
26-
default="gpt-4o",
27-
help="The LLM model to use for mutation generation. Default is 'gpt-4o'.",
26+
default="gpt-3.5-turbo",
27+
help="The LLM model to use for mutation generation. Default is 'gpt-3.5-turbo'.",
2828
)
2929
main_parser.add_argument(
3030
"--api-base",

tests/test_report.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def test_generate_mutant_report(mutants, config):
134134
patch.object(report, "save_report") as mock_save_report,
135135
patch("mutahunter.core.logger.logger.info") as mock_logger_info,
136136
):
137-
report.generate_mutant_report(mutants, 0.0)
137+
report.generate_mutant_report(mutants, 0.0, 0.0)
138138

139139
mock_logger_info.assert_any_call("🦠 Total Mutants: %d 🦠", len(mutants))
140140
mock_logger_info.assert_any_call("🛡️ Survived Mutants: %d 🛡️", 2)

0 commit comments

Comments
 (0)