Skip to content

Commit e6bd3f0

Browse files
committed
feat: add udiff in mutant output
1 parent 3923edb commit e6bd3f0

File tree

5 files changed

+34
-49
lines changed

5 files changed

+34
-49
lines changed

src/mutahunter/core/entities/mutant.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +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", "COMPILE_ERROR").
1919
error_msg (str): The error message associated with the mutant, if any.
20-
mutant_code (str): The code of the mutant.
2120
type (str): The type of mutation applied to the code.
2221
description (str): A description of the mutation applied.
22+
udiff (str): The unified diff between the original source code and the mutant code.
2323
"""
2424

2525
id: str
2626
source_path: str
2727
mutant_path: Union[None, str] = None
2828
status: Union[None, str] = None
2929
error_msg: str = ""
30-
mutant_code: str = ""
3130
type: str = ""
3231
description: str = ""
32+
udiff: str = ""

src/mutahunter/core/hunter.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from mutahunter.core.report import MutantReport
1414
from mutahunter.core.router import LLMRouter
1515
from mutahunter.core.runner import TestRunner
16+
import difflib
1617

1718
TEST_FILE_PATTERNS = [
1819
"test_",
@@ -162,11 +163,12 @@ def process_mutant(
162163
mutant = Mutant(
163164
id=str(len(self.mutants) + 1),
164165
source_path=source_file_path,
165-
mutant_code=mutant_data["mutant_code"],
166166
type=mutant_data["type"],
167167
description=mutant_data["description"],
168168
)
169-
mutant_path = self.prepare_mutant_file(mutant, start_byte, end_byte)
169+
mutant_path = self.prepare_mutant_file(
170+
mutant, start_byte, end_byte, mutant_code=mutant_data["mutant_code"]
171+
)
170172

171173
if mutant_path: # Only run tests if the mutant file is prepared successfully
172174
mutant.mutant_path = mutant_path
@@ -178,13 +180,24 @@ def process_mutant(
178180
}
179181
)
180182
self.process_test_result(result, mutant)
183+
udiff_list = self.get_unified_diff(source_file_path, mutant_path, mutant)
184+
mutant.udiff = "\n".join(udiff_list)
181185
else:
182186
mutant.status = "COMPILE_ERROR"
183187

184188
self.mutants.append(mutant)
185189

190+
def get_unified_diff(self, source_file_path, mutant_path, mutant):
191+
with open(source_file_path, "r") as file:
192+
original = file.readlines()
193+
with open(mutant_path, "r") as file:
194+
mutant = file.readlines()
195+
diff = difflib.unified_diff(original, mutant, lineterm="")
196+
diff_lines = list(diff)
197+
return diff_lines
198+
186199
def prepare_mutant_file(
187-
self, mutant: Mutant, start_byte: int, end_byte: int
200+
self, mutant: Mutant, start_byte: int, end_byte: int, mutant_code: str
188201
) -> str:
189202
"""
190203
Prepares the mutant file for testing.
@@ -200,7 +213,7 @@ def prepare_mutant_file(
200213

201214
modified_byte_code = (
202215
source_code[:start_byte]
203-
+ bytes(mutant.mutant_code, "utf-8")
216+
+ bytes(mutant_code, "utf-8")
204217
+ source_code[end_byte:]
205218
)
206219

src/mutahunter/core/mutator.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def generate_mutant(self, repo_map_result):
132132
covered_lines=self.executed_lines,
133133
example_output=self.prompt.example_output,
134134
function_block=function_block_with_line_num,
135-
maximum_num_of_mutants_per_function_block=3,
135+
maximum_num_of_mutants_per_function_block=2,
136136
)
137137
prompt = {
138138
"system": system_template,
@@ -180,25 +180,13 @@ def generate(self):
180180
# dont modify the original lines
181181
temp_lines = original_lines.copy()
182182
# check if the temp_lines[i] ends with a newline character
183-
if temp_lines[i].endswith("\n"):
184-
# get indentation of the original line
185-
indentation_space = len(temp_lines[i]) - len(
186-
temp_lines[i].lstrip()
187-
)
188-
# add the indentation to the mutated line after lstripping
189-
mutated_line = " " * indentation_space + mutated_line.lstrip()
190-
temp_lines[i] = mutated_line + "\n"
191-
# updated change dict
192-
change["mutant_code"] = "".join(temp_lines)
193-
else:
194-
# get indentation of the original line
195-
indentation_space = len(temp_lines[i]) - len(
196-
temp_lines[i].lstrip()
197-
)
198-
# add the indentation to the mutated line after lstripping
199-
mutated_line = " " * indentation_space + mutated_line.lstrip()
200-
temp_lines[i] = mutated_line
201-
change["mutant_code"] = "".join(temp_lines)
183+
# get indentation of the original line
184+
indentation_space = len(temp_lines[i]) - len(temp_lines[i].lstrip())
185+
# add the indentation to the mutated line after lstripping
186+
mutated_line = " " * indentation_space + mutated_line.lstrip()
187+
temp_lines[i] = mutated_line + "\n"
188+
# updated change dict
189+
change["mutant_code"] = "".join(temp_lines)
202190
break
203191
else:
204192
logger.error(f"Could not apply mutation. Skipping mutation.")

tests/test_analyzer.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -357,19 +357,3 @@ def test_find_method_blocks_nodes(
357357
mock_find_blocks_nodes.assert_called_once_with(
358358
source_file_path, source_code, ["if_statement", "loop", "return"]
359359
)
360-
361-
362-
@patch("xml.etree.ElementTree.parse")
363-
@patch("mutahunter.core.analyzer.Analyzer._find_blocks_nodes")
364-
def test_find_function_blocks_nodes(
365-
mock_find_blocks_nodes, mock_parse, config, cobertura_xml_content
366-
):
367-
source_code = b"def foo():\n pass"
368-
source_file_path = "test_file.py"
369-
mock_parse.return_value = ET.ElementTree(ET.fromstring(cobertura_xml_content))
370-
371-
analyzer = Analyzer(config)
372-
analyzer.find_function_blocks_nodes(source_file_path, source_code)
373-
mock_find_blocks_nodes.assert_called_once_with(
374-
source_file_path, source_code, ["definition.function", "definition.method"]
375-
)

tests/test_report.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def mutants():
3333
mutant_path="mutant1.py",
3434
status="KILLED",
3535
error_msg="",
36-
mutant_code="",
36+
udiff="",
3737
type="",
3838
description="",
3939
),
@@ -43,7 +43,7 @@ def mutants():
4343
mutant_path="mutant2.py",
4444
status="SURVIVED",
4545
error_msg="",
46-
mutant_code="",
46+
udiff="",
4747
type="",
4848
description="",
4949
),
@@ -53,7 +53,7 @@ def mutants():
5353
mutant_path="mutant3.py",
5454
status="KILLED",
5555
error_msg="",
56-
mutant_code="",
56+
udiff="",
5757
type="",
5858
description="",
5959
),
@@ -63,7 +63,7 @@ def mutants():
6363
mutant_path="mutant4.py",
6464
status="SURVIVED",
6565
error_msg="",
66-
mutant_code="",
66+
udiff="",
6767
type="",
6868
description="",
6969
),
@@ -73,7 +73,7 @@ def mutants():
7373
mutant_path="mutant1.py",
7474
status="TIMEOUT",
7575
error_msg="",
76-
mutant_code="",
76+
udiff="",
7777
type="",
7878
description="",
7979
),
@@ -83,7 +83,7 @@ def mutants():
8383
mutant_path="mutant2.py",
8484
status="TIMEOUT",
8585
error_msg="",
86-
mutant_code="",
86+
udiff="",
8787
type="",
8888
description="",
8989
),
@@ -93,7 +93,7 @@ def mutants():
9393
mutant_path="mutant3.py",
9494
status="COMPILE_ERROR",
9595
error_msg="",
96-
mutant_code="",
96+
udiff="",
9797
type="",
9898
description="",
9999
),

0 commit comments

Comments
 (0)