Skip to content

Commit 0c03051

Browse files
committed
feat: add example github action workflow to display mutaiton coverage
1 parent c8ad3c7 commit 0c03051

File tree

4 files changed

+92
-122
lines changed

4 files changed

+92
-122
lines changed

β€ŽREADME.md

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33

44
Open-Source Language Agnostic LLM-based Mutation Testing for Automated Software Testing
55

6-
Maintained by [CodeIntegrity](https://www.codeintegrity.ai). Anyone is welcome to contribute. 🌟
7-
86
[![GitHub license](https://img.shields.io/badge/License-AGPL_3.0-blue.svg)](https://github.com/yourcompany/mutahunter/blob/main/LICENSE)
97
[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.gg/S5u3RDMq)
10-
[![Twitter](https://img.shields.io/twitter/follow/CodeIntegrity)](https://twitter.com/CodeIntegrity)
118
[![Unit Tests](https://github.com/codeintegrity-ai/mutahunter/actions/workflows/test.yaml/badge.svg)](https://github.com/codeintegrity-ai/mutahunter/actions/workflows/test.yaml)
129
<a href="https://github.com/codeintegrity-ai/mutahunter/commits/main">
1310
<img alt="GitHub" src="https://img.shields.io/github/last-commit/codeintegrity-ai/mutahunter/main?style=for-the-badge" height="20">
@@ -19,6 +16,7 @@
1916
- [Overview](#overview)
2017
- [Features](#features)
2118
- [Getting Started](#getting-started)
19+
- [CI/CD Integration](#cicd-integration)
2220
- [Roadmap](#roadmap)
2321
- [Cash Bounty Program](#cash-bounty-program)
2422

@@ -102,12 +100,63 @@ Feel free to add more examples! ✨
102100
Check the logs directory to view the report:
103101
104102
- `mutants.json` - Contains the list of mutants generated.
105-
- `mutation_coverage.json` - Contains the mutation coverage percentage.
106-
- `mutation_coverage_detail.json` - Contains detailed information per source file.
107-
108-
## Cash Bounty Program
109-
110-
Help us improve Mutahunter and get rewarded! We have a cash bounty program to incentivize contributions to the project. Check out the [bounty board](https://docs.google.com/spreadsheets/d/1cT2_O55m5txrUgZV81g1gtqE_ZDu9LlzgbpNa_HIisc/edit?gid=0#gid=0) to see the available bounties and claim one today!
103+
- `coverage.txt` - Contains information about mutation coverage.
104+
105+
## CI/CD Integration
106+
107+
You can integrate Mutahunter into your CI/CD pipeline to automate mutation testing. Here is an example GitHub Actions workflow file:
108+
109+
![CI/CD](/images/github-bot.png)
110+
111+
```yaml
112+
name: Mutahunter CI/CD
113+
114+
on:
115+
push:
116+
branches:
117+
- main
118+
pull_request:
119+
branches:
120+
- main
121+
122+
jobs:
123+
mutahunter:
124+
runs-on: ubuntu-latest
125+
126+
steps:
127+
- name: Checkout repository
128+
uses: actions/checkout@v4
129+
with:
130+
fetch-depth: 2 # needed for git diff
131+
132+
- name: Set up Python
133+
uses: actions/setup-python@v5
134+
with:
135+
python-version: 3.11
136+
137+
- name: Install Mutahunter
138+
run: pip install git+https://github.com/codeintegrity-ai/mutahunter.git
139+
140+
- name: Set up Java for your project
141+
uses: actions/setup-java@v2
142+
with:
143+
distribution: "adopt"
144+
java-version: "17"
145+
146+
- name: Install dependencies and run tests
147+
run: mvn test
148+
149+
- name: Run Mutahunter
150+
env:
151+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
152+
run: |
153+
mutahunter run --test-command "mvn test" --code-coverage-report-path "target/site/jacoco/jacoco.xml" --coverage-type jacoco --model "gpt-4o" --modified-files-only
154+
155+
- name: PR comment the mutation coverage
156+
uses: thollander/[email protected]
157+
with:
158+
filePath: logs/_latest/coverage.txt
159+
```
111160
112161
## Roadmap
113162
@@ -116,9 +165,13 @@ Help us improve Mutahunter and get rewarded! We have a cash bounty program to in
116165
- [x] **Support for Other Coverage Report Formats:** Add compatibility for various coverage report formats.
117166
- [x] **Change-Based Testing:** Implement mutation testing on modified files based on the latest commit or pull request changes.
118167
- [x] **Extreme Mutation Testing:** Apply mutations to the codebase without using LLMs to detect pseudo-tested methods with significantly lower computational cost.
119-
- [ ] **Mutant Analysis:** Automatically analyze survived mutants to identify potential weaknesses in the test suite. Any suggestions are welcome!
120-
- [ ] **CI/CD Integration:** Develop connectors for popular CI/CD platforms like GitHub Actions.
121-
- [ ] **Automatic PR Bot:** Create a bot that automatically identifies bugs from the survived mutants list and provides fix suggestions.
168+
- [x] **CI/CD Integration:** Display mutation coverage in pull requests and automate mutation testing using GitHub Actions.
169+
- [ ] **Mutant Analysis:** Automatically analyze survived mutants to identify potential weaknesses in the test suite.
170+
171+
## Cash Bounty Program
172+
173+
Help us improve Mutahunter and get rewarded! We have a cash bounty program to incentivize contributions to the project. Check out the [bounty board](https://docs.google.com/spreadsheets/d/1cT2_O55m5txrUgZV81g1gtqE_ZDu9LlzgbpNa_HIisc/edit?gid=0#gid=0) to see the available bounties and claim one today!
174+
122175
123176
## Acknowledgements
124177

β€Žimages/github-bot.png

238 KB
Loading

β€Žsrc/mutahunter/core/report.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,19 @@ def generate_mutant_report(
8080
else:
8181
logger.info("πŸ’° Expected Cost: $%.5f USD πŸ’°", total_cost)
8282

83-
mutation_coverage = {
84-
"total_mutants": len(mutants),
85-
"killed_mutants": len(killed_mutants),
86-
"survived_mutants": len(survived_mutants),
87-
"timeout_mutants": len(timeout_mutants),
88-
"compile_error_mutants": len(compile_error_mutants),
89-
"mutation_coverage": total_mutation_coverage,
90-
"line_coverage": line_coverage,
91-
"expected_cost": total_cost,
92-
}
93-
94-
self.save_report("logs/_latest/mutation_coverage.json", mutation_coverage)
83+
with open("logs/_latest/coverage.txt", "a") as file:
84+
file.write("Mutation Coverage:\n")
85+
file.write(f"πŸ“Š Line Coverage: {line_coverage} πŸ“Š\n")
86+
file.write(f"🎯 Mutation Coverage: {total_mutation_coverage} 🎯\n")
87+
file.write(f"🦠 Total Mutants: {len(mutants)} 🦠\n")
88+
file.write(f"πŸ›‘οΈ Survived Mutants: {len(survived_mutants)} πŸ›‘οΈ\n")
89+
file.write(f"πŸ—‘οΈ Killed Mutants: {len(killed_mutants)} πŸ—‘οΈ\n")
90+
file.write(f"πŸ•’ Timeout Mutants: {len(timeout_mutants)} πŸ•’\n")
91+
file.write(f"πŸ”₯ Compile Error Mutants: {len(compile_error_mutants)} πŸ”₯\n")
92+
if self.config.extreme:
93+
file.write("πŸ’° No Cost for extreme mutation testing πŸ’°\n")
94+
else:
95+
file.write("πŸ’° Expected Cost: $%.5f USD πŸ’°\n", total_cost)
9596

9697
def generate_mutant_report_detail(self, mutants: List[Mutant]) -> None:
9798
"""
@@ -118,6 +119,7 @@ def generate_mutant_report_detail(self, mutants: List[Mutant]) -> None:
118119
report_detail[source_path]["survived_mutants"] += 1
119120
elif mutant["status"] == "TIMEOUT":
120121
report_detail[source_path]["timeout_mutants"] += 1
122+
121123
elif mutant["status"] == "COMPILE_ERROR":
122124
report_detail[source_path]["compile_error_mutants"] += 1
123125

@@ -134,7 +136,19 @@ def generate_mutant_report_detail(self, mutants: List[Mutant]) -> None:
134136
)
135137
detail["mutation_coverage"] = mutation_coverage
136138

137-
self.save_report("logs/_latest/mutation_coverage_detail.json", report_detail)
139+
with open("logs/_latest/coverage.txt", "a") as file:
140+
file.write("\nDetailed Mutation Coverage:\n")
141+
for source_path, detail in report_detail.items():
142+
file.write(f"πŸ“‚ Source File: {source_path} πŸ“‚\n")
143+
file.write(f"🎯 Mutation Coverage: {detail['mutation_coverage']}🎯\n")
144+
file.write(f"🦠 Total Mutants: {detail['total_mutants']} 🦠\n")
145+
file.write(f"πŸ›‘οΈ Survived Mutants: {detail['survived_mutants']} πŸ›‘οΈ\n")
146+
file.write(f"πŸ—‘οΈ Killed Mutants: {detail['killed_mutants']} πŸ—‘οΈ\n")
147+
file.write(f"πŸ•’ Timeout Mutants: {detail['timeout_mutants']} πŸ•’\n")
148+
file.write(
149+
f"πŸ”₯ Compile Error Mutants: {detail['compile_error_mutants']}πŸ”₯\n"
150+
)
151+
file.write("\n")
138152

139153
def save_report(self, filepath: str, data: Any) -> None:
140154
"""

β€Žtests/test_report.py

Lines changed: 0 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -70,103 +70,6 @@ def mutants():
7070
]
7171

7272

73-
def test_generate_mutant_report_detail_all_statuses(config):
74-
mutants = [
75-
Mutant(
76-
id="1",
77-
source_path="app.go",
78-
mutant_path="mutant1.py",
79-
status="KILLED",
80-
error_msg="",
81-
mutant_code="",
82-
type="",
83-
description="",
84-
),
85-
Mutant(
86-
id="2",
87-
source_path="app.go",
88-
mutant_path="mutant2.py",
89-
status="SURVIVED",
90-
error_msg="",
91-
mutant_code="",
92-
type="",
93-
description="",
94-
),
95-
Mutant(
96-
id="3",
97-
source_path="app.go",
98-
mutant_path="mutant3.py",
99-
status="TIMEOUT",
100-
error_msg="",
101-
mutant_code="",
102-
type="",
103-
description="",
104-
),
105-
Mutant(
106-
id="4",
107-
source_path="app.go",
108-
mutant_path="mutant4.py",
109-
status="COMPILE_ERROR",
110-
error_msg="",
111-
mutant_code="",
112-
type="",
113-
description="",
114-
),
115-
]
116-
report = MutantReport(config)
117-
mutants = [asdict(mutant) for mutant in mutants]
118-
119-
with patch.object(report, "save_report") as mock_save_report:
120-
report.generate_mutant_report_detail(mutants)
121-
122-
mock_save_report.assert_called_once_with(
123-
"logs/_latest/mutation_coverage_detail.json",
124-
{
125-
"app.go": {
126-
"total_mutants": 4,
127-
"killed_mutants": 1,
128-
"survived_mutants": 1,
129-
"timeout_mutants": 1,
130-
"compile_error_mutants": 1,
131-
"mutation_coverage": "50.00%",
132-
}
133-
},
134-
)
135-
136-
137-
def test_generate_mutant_report(mutants, config):
138-
report = MutantReport(config)
139-
mutants = [asdict(mutant) for mutant in mutants]
140-
141-
with (
142-
patch.object(report, "save_report") as mock_save_report,
143-
patch("mutahunter.core.logger.logger.info") as mock_logger_info,
144-
):
145-
report.generate_mutant_report(mutants, 0.0, 0.0)
146-
mock_logger_info.assert_any_call("πŸ“Š Line Coverage: %s πŸ“Š", "0.00%")
147-
mock_logger_info.assert_any_call("🎯 Mutation Coverage: %s 🎯", "50.00%")
148-
mock_logger_info.assert_any_call("🦠 Total Mutants: %d 🦠", len(mutants))
149-
mock_logger_info.assert_any_call("πŸ›‘οΈ Survived Mutants: %d πŸ›‘οΈ", 2)
150-
mock_logger_info.assert_any_call("πŸ—‘οΈ Killed Mutants: %d πŸ—‘οΈ", 2)
151-
mock_logger_info.assert_any_call("πŸ•’ Timeout Mutants: %d πŸ•’", 0)
152-
mock_logger_info.assert_any_call("πŸ”₯ Compile Error Mutants: %d πŸ”₯", 0)
153-
mock_logger_info.assert_any_call("πŸ’° Expected Cost: $%.5f USD πŸ’°", 0.0)
154-
155-
mock_save_report.assert_called_once_with(
156-
"logs/_latest/mutation_coverage.json",
157-
{
158-
"total_mutants": 4,
159-
"killed_mutants": 2,
160-
"survived_mutants": 2,
161-
"timeout_mutants": 0,
162-
"compile_error_mutants": 0,
163-
"mutation_coverage": "50.00%",
164-
"line_coverage": "0.00%",
165-
"expected_cost": 0.0,
166-
},
167-
)
168-
169-
17073
def test_save_report(config):
17174
report = MutantReport(config)
17275
data = {"key": "value"}

0 commit comments

Comments
Β (0)