Skip to content

Commit dfa1ddb

Browse files
committed
feat: significantly reduce mutant syntax error and compilation error
1 parent 9506a84 commit dfa1ddb

File tree

20 files changed

+525
-1508
lines changed

20 files changed

+525
-1508
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,7 @@ logs
163163
vendor
164164
*.lcov
165165
dist*
166+
167+
*.html
168+
*.json
169+
*.log

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
Maintained by [CodeIntegrity](https://www.codeintegrity.ai). Anyone is welcome to contribute. 🌟
77

88
[![GitHub license](https://img.shields.io/badge/License-AGPL_3.0-blue.svg)](https://github.com/yourcompany/mutahunter/blob/main/LICENSE)
9+
[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.gg/S5u3RDMq)
910
[![Twitter](https://img.shields.io/twitter/follow/CodeIntegrity)](https://twitter.com/CodeIntegrity)
1011
[![Unit Tests](https://github.com/codeintegrity-ai/mutahunter/actions/workflows/test.yaml/badge.svg)](https://github.com/codeintegrity-ai/mutahunter/actions/workflows/test.yaml)
1112
<a href="https://github.com/codeintegrity-ai/mutahunter/commits/main">
@@ -84,6 +85,12 @@ Options:
8485
Default: `gpt-4o`
8586
Required: Yes
8687
Example: `--model gpt-4o`
88+
89+
--api-base <URL>
90+
Description: Base URL for the API endpoint.
91+
Default: `https://api.openai.com`
92+
Required: No
93+
Example: `--api-base https://api.openai.com`
8794
8895
--test-command <COMMAND>
8996
Description: The command used to execute the tests. Specify a single test file to run the tests on.

examples/go_webservice/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ gocov convert coverage.out | gocov-xml > coverage.xml
1717
## Running Mutahunter to analyze the tests
1818

1919
```bash
20-
mutahunter run --test-command "go test" --code-coverage-report-path "coverage.xml" --only-mutate-file-paths "app.go" --model "claude-3-5-sonnet-20240620"
20+
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"
2122
```

examples/java_maven/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +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
1415
mutahunter run --test-command "mvn test" --code-coverage-report-path "target/site/jacoco/jacoco.xml" --coverage-type jacoco --model "claude-3-5-sonnet-20240620"
1516
```

examples/java_maven/src/test/java/CalculatorTest.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,4 @@ public void testDivide() {
4242
assertEquals(-1.0, calculator.divide(-5, 5));
4343
assertEquals(2.0, calculator.divide(-10, -5));
4444
}
45-
46-
@Test
47-
public void testDivideByZero() {
48-
assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, 0));
49-
}
5045
}

examples/js_vanilla/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ npm run test:coverage
1212
## Running Mutahunter to analyze the tests
1313

1414
```bash
15-
mutahunter run --test-command "npm run test" --code-coverage-report-path "coverage/coverage.xml" --only-mutate-file-paths "ui.js"
15+
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"
1617
```

examples/python_fastapi/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +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
1516
mutahunter run --test-command "pytest" --code-coverage-report-path "coverage.xml" --only-mutate-file-paths "app.py"
1617
```

src/mutahunter/core/analyzer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from grep_ast import filename_to_lang
99
from tree_sitter_languages import get_language, get_parser
10+
1011
from mutahunter.core.logger import logger
1112

1213

src/mutahunter/core/entities/mutant.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ class Mutant:
1717
mutant_path (str): The path to the file containing the mutant.
1818
status (Union[None, str]): The status of the mutant (e.g., "SURVIVED", "KILLED").
1919
error_msg (str): The error message associated with the mutant, if any.
20-
diff (str): The diff of the mutation applied to the source code.
20+
mutant_code (str): The code of the mutant.
21+
type (str): The type of mutation applied to the code.
22+
description (str): A description of the mutation applied.
2123
"""
2224

2325
id: str
2426
source_path: str
2527
mutant_path: str
2628
status: Union[None, str] = None
2729
error_msg: str = ""
28-
diff: str = ""
30+
mutant_code: str = ""
31+
type: str = ""
32+
description: str = ""

src/mutahunter/core/hunter.py

Lines changed: 26 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -106,44 +106,21 @@ def run_mutation_testing(self) -> None:
106106
Runs mutation testing on the entire codebase.
107107
"""
108108
logger.info("Running mutation testing on the entire codebase.")
109-
for mutant_data in self.generate_mutations():
110-
self.process_mutant(mutant_data)
111-
112-
def run_mutation_testing_on_modified_files(self) -> None:
113-
"""
114-
Runs mutation testing on modified files.
115-
"""
116-
logger.info("Running mutation testing on modified files.")
117-
for mutant_data in self.generate_mutations_for_modified_files():
118-
self.process_mutant(mutant_data)
119-
120-
def generate_mutations(self) -> Generator[Dict[str, Any], None, None]:
121-
"""
122-
Generates mutations for all covered files.
123-
124-
Yields:
125-
Generator[Dict[str, Any], None, None]: Dictionary containing mutation details.
126-
"""
127109
all_covered_files = self.analyzer.file_lines_executed.keys()
128110
logger.info(f"Generating mutations for {len(all_covered_files)} covered files.")
129111

130112
for covered_file_path in tqdm(all_covered_files):
131113
if self.should_skip_file(covered_file_path):
132114
continue
133-
134-
yield from self.generate_mutations_for_file(
115+
self.generate_mutations(
135116
covered_file_path, self.analyzer.file_lines_executed[covered_file_path]
136117
)
137118

138-
def generate_mutations_for_modified_files(
139-
self,
140-
) -> Generator[Dict[str, Any], None, None]:
119+
def run_mutation_testing_on_modified_files(self) -> None:
141120
"""
142-
Generates mutations for modified files and lines based on the latest commit.
143-
144-
Yields:
145-
Generator[Dict[str, Any], None, None]: Dictionary containing mutation details.
121+
Runs mutation testing on modified files.
146122
"""
123+
logger.info("Running mutation testing on modified files.")
147124
modified_files = self.get_modified_files()
148125
logger.info(
149126
f"Generating mutations for {len(modified_files)} modified files - {','.join(modified_files)}"
@@ -159,9 +136,9 @@ def generate_mutations_for_modified_files(
159136
logger.debug(f"No modified lines found in file: {file_path}")
160137
continue
161138

162-
yield from self.generate_mutations_for_file(file_path, modified_lines)
139+
self.generate_mutations(file_path, modified_lines)
163140

164-
def generate_mutations_for_file(
141+
def generate_mutations(
165142
self, file_path: str, executed_lines: List[int]
166143
) -> Generator[Dict[str, Any], None, None]:
167144
"""
@@ -186,57 +163,56 @@ def generate_mutations_for_file(
186163
):
187164
start_byte = function_block.start_byte
188165
end_byte = function_block.end_byte
189-
166+
function_name = function_block.child_by_field_name("name").text.decode(
167+
"utf8"
168+
)
190169
mutant_generator = MutantGenerator(
191170
config=self.config,
192171
executed_lines=executed_lines,
193172
cov_files=list(self.analyzer.file_lines_executed.keys()),
194173
source_file_path=file_path,
174+
function_name=function_name,
195175
start_byte=start_byte,
196176
end_byte=end_byte,
197177
)
198-
# NOTE: May return empty content if the mutation generation fails.
199-
for _, hunk, content in mutant_generator.generate():
200-
yield {
201-
"source_path": file_path,
202-
"start_byte": start_byte,
203-
"end_byte": end_byte,
204-
"hunk": hunk,
205-
"mutant_code_snippet": content,
206-
}
178+
for mutant_data in mutant_generator.generate():
179+
self.process_mutant(mutant_data, file_path, start_byte, end_byte)
207180

208-
def process_mutant(self, mutant_data: Dict[str, Any]) -> None:
181+
def process_mutant(
182+
self, mutant_data: Dict[str, Any], source_file_path, start_byte, end_byte
183+
) -> None:
209184
"""
210185
Processes a single mutant data dictionary.
211186
"""
212187
try:
213-
logger.info(f"Processing mutant for file: {mutant_data['source_path']}")
188+
logger.info(f"Processing mutant for file: {source_file_path}")
214189
mutant_id = str(len(self.mutants) + 1)
215190
mutant_path = self.prepare_mutant_file(
216191
mutant_id=mutant_id,
217-
source_file_path=mutant_data["source_path"],
218-
start_byte=mutant_data["start_byte"],
219-
end_byte=mutant_data["end_byte"],
220-
mutant_code=mutant_data["mutant_code_snippet"],
192+
source_file_path=source_file_path,
193+
start_byte=start_byte,
194+
end_byte=end_byte,
195+
mutant_code=mutant_data["mutant_code"],
221196
)
222-
unified_diff = "".join(mutant_data["hunk"])
223197
mutant = Mutant(
224198
id=mutant_id,
225-
diff=unified_diff,
226-
source_path=mutant_data["source_path"],
199+
source_path=source_file_path,
227200
mutant_path=mutant_path,
201+
mutant_code=mutant_data["mutant_code"],
202+
type=mutant_data["type"],
203+
description=mutant_data["description"],
228204
)
229205
result = self.run_test(
230206
{
231-
"module_path": mutant_data["source_path"],
207+
"module_path": source_file_path,
232208
"replacement_module_path": mutant_path,
233209
"test_command": self.config["test_command"],
234210
}
235211
)
236212
self.process_test_result(result, mutant)
237213
except Exception as e:
238214
logger.error(
239-
f"Error processing mutant for file: {mutant_data['source_path']}",
215+
f"Error processing mutant for file: {source_file_path}",
240216
exc_info=True,
241217
)
242218

0 commit comments

Comments
 (0)