1
1
import os
2
2
import subprocess
3
3
import time
4
- from typing import Any , Dict , Generator , List
4
+ from typing import Any , Dict , List
5
5
6
6
from tqdm import tqdm
7
7
@@ -17,15 +17,6 @@ class MutantHunter:
17
17
def __init__ (self , config : Dict [str , Any ]) -> None :
18
18
"""
19
19
Initializes the MutantHunter class with the given configuration.
20
-
21
- Args:
22
- config (Dict[str, Any]): Configuration dictionary containing various settings.
23
- - model (str): LLM model to use for mutation testing.
24
- - api_base (str): Base URL for self-hosted LLM models.
25
- - test_command (str): Command to run the tests.
26
- - code_coverage_report_path (Optional[str]): Path to the code coverage report file.
27
- - exclude_files (List[str]): List of files to exclude from analysis.
28
- - only_mutate_file_paths (List[str]): List of specific files to mutate.
29
20
"""
30
21
self .config : Dict [str , Any ] = config
31
22
self .mutants : List [Mutant ] = []
@@ -52,40 +43,28 @@ def run(self) -> None:
52
43
self .mutant_report .generate_report (self .mutants )
53
44
logger .info (f"Mutation Testing Ended. Took { round (time .time () - start )} s" )
54
45
except Exception as e :
55
- import traceback
56
-
57
46
logger .error (
58
47
"Error during mutation testing. Please report this issue." ,
59
48
exc_info = True ,
60
49
)
61
- print (traceback .format_exc ())
62
50
63
51
def should_skip_file (self , filename : str ) -> bool :
64
52
"""
65
53
Determines if a file should be skipped based on various conditions.
66
-
67
- Args:
68
- filename (str): The filename to check.
69
-
70
- Returns:
71
- bool: True if the file should be skipped, False otherwise.
72
54
"""
73
55
logger .debug (f"Checking if file should be skipped: { filename } " )
74
56
if self .config ["only_mutate_file_paths" ]:
75
- # NOTE: Check if the file exists before proceeding.
76
57
for file_path in self .config ["only_mutate_file_paths" ]:
77
58
if not os .path .exists (file_path ):
78
59
logger .error (f"File { file_path } does not exist." )
79
60
raise FileNotFoundError (f"File { file_path } does not exist." )
80
- # NOTE: Only mutate the files specified in the config.
81
61
return all (
82
62
file_path != filename
83
63
for file_path in self .config ["only_mutate_file_paths" ]
84
64
)
85
65
if filename in self .config ["exclude_files" ]:
86
66
return True
87
67
88
- # NOTE: Line coverage may contains test files. Exclue them from mutation testing.
89
68
TEST_FILE_PATTERNS = [
90
69
"test_" ,
91
70
"_test" ,
@@ -138,9 +117,7 @@ def run_mutation_testing_on_modified_files(self) -> None:
138
117
139
118
self .generate_mutations (file_path , modified_lines )
140
119
141
- def generate_mutations (
142
- self , file_path : str , executed_lines : List [int ]
143
- ) -> Generator [Dict [str , Any ], None , None ]:
120
+ def generate_mutations (self , file_path : str , executed_lines : List [int ]) -> None :
144
121
"""
145
122
Generates mutations for a single file based on the executed lines.
146
123
"""
@@ -179,29 +156,26 @@ def generate_mutations(
179
156
self .process_mutant (mutant_data , file_path , start_byte , end_byte )
180
157
181
158
def process_mutant (
182
- self , mutant_data : Dict [str , Any ], source_file_path , start_byte , end_byte
159
+ self ,
160
+ mutant_data : Dict [str , Any ],
161
+ source_file_path : str ,
162
+ start_byte : int ,
163
+ end_byte : int ,
183
164
) -> None :
184
165
"""
185
166
Processes a single mutant data dictionary.
186
167
"""
187
- try :
188
- logger .info (f"Processing mutant for file: { source_file_path } " )
189
- mutant_id = str (len (self .mutants ) + 1 )
190
- mutant_path = self .prepare_mutant_file (
191
- mutant_id = mutant_id ,
192
- source_file_path = source_file_path ,
193
- start_byte = start_byte ,
194
- end_byte = end_byte ,
195
- mutant_code = mutant_data ["mutant_code" ],
196
- )
197
- mutant = Mutant (
198
- id = mutant_id ,
199
- source_path = source_file_path ,
200
- mutant_path = mutant_path ,
201
- mutant_code = mutant_data ["mutant_code" ],
202
- type = mutant_data ["type" ],
203
- description = mutant_data ["description" ],
204
- )
168
+ mutant = Mutant (
169
+ id = str (len (self .mutants ) + 1 ),
170
+ source_path = source_file_path ,
171
+ mutant_code = mutant_data ["mutant_code" ],
172
+ type = mutant_data ["type" ],
173
+ description = mutant_data ["description" ],
174
+ )
175
+ mutant_path = self .prepare_mutant_file (mutant , start_byte , end_byte )
176
+
177
+ if mutant_path : # Only run tests if the mutant file is prepared successfully
178
+ mutant .mutant_path = mutant_path
205
179
result = self .run_test (
206
180
{
207
181
"module_path" : source_file_path ,
@@ -210,72 +184,47 @@ def process_mutant(
210
184
}
211
185
)
212
186
self .process_test_result (result , mutant )
213
- except Exception as e :
214
- logger .error (
215
- f"Error processing mutant for file: { source_file_path } " ,
216
- exc_info = True ,
217
- )
187
+ else :
188
+ mutant .status = "COMPILE_ERROR"
189
+
190
+ self .mutants .append (mutant )
218
191
219
192
def prepare_mutant_file (
220
- self ,
221
- mutant_id : str ,
222
- source_file_path : str ,
223
- start_byte : int ,
224
- end_byte : int ,
225
- mutant_code : str ,
193
+ self , mutant : Mutant , start_byte : int , end_byte : int
226
194
) -> str :
227
195
"""
228
196
Prepares the mutant file for testing.
229
-
230
- Args:
231
- mutant_id (str): The ID of the mutant.
232
- source_path (str): The path to the original source file.
233
- start_byte (int): The start byte position of the mutation.
234
- end_byte (int): The end byte position of the mutation.
235
- mutant_code (str): The mutated code snippet.
236
-
237
- Returns:
238
- str: The path to the mutant file.
239
-
240
- Raises:
241
- Exception: If the mutant code has syntax errors.
242
197
"""
243
- mutant_file_name = f"{ mutant_id } _{ os .path .basename (source_file_path )} "
198
+ mutant_file_name = f"{ mutant . id } _{ os .path .basename (mutant . source_path )} "
244
199
mutant_path = os .path .join (
245
200
os .getcwd (), f"logs/_latest/mutants/{ mutant_file_name } "
246
201
)
247
202
logger .debug (f"Preparing mutant file: { mutant_path } " )
248
203
249
- with open (source_file_path , "rb" ) as f :
204
+ with open (mutant . source_path , "rb" ) as f :
250
205
source_code = f .read ()
251
206
252
207
modified_byte_code = (
253
208
source_code [:start_byte ]
254
- + bytes (mutant_code , "utf-8" )
209
+ + bytes (mutant . mutant_code , "utf-8" )
255
210
+ source_code [end_byte :]
256
211
)
257
212
258
213
if self .analyzer .check_syntax (
259
- source_file_path = source_file_path ,
214
+ source_file_path = mutant . source_path ,
260
215
source_code = modified_byte_code .decode ("utf-8" ),
261
216
):
262
217
with open (mutant_path , "wb" ) as f :
263
218
f .write (modified_byte_code )
264
219
logger .info (f"Mutant file prepared: { mutant_path } " )
265
220
return mutant_path
266
221
else :
267
- logger .error (f"Syntax error in mutant code for file: { source_file_path } " )
268
- raise SyntaxError ( "Mutant code has syntax errors." )
222
+ logger .error (f"Syntax error in mutant code for file: { mutant . source_path } " )
223
+ return ""
269
224
270
225
def run_test (self , params : Dict [str , str ]) -> Any :
271
226
"""
272
227
Runs the test command on the given parameters.
273
-
274
- Args:
275
- params (Dict[str, str]): Dictionary containing test command parameters.
276
-
277
- Returns:
278
- Any: The result of the test command execution.
279
228
"""
280
229
logger .info (
281
230
f"Running test command: { params ['test_command' ]} for mutant file: { params ['replacement_module_path' ]} "
@@ -295,12 +244,18 @@ def process_test_result(self, result: Any, mutant: Mutant) -> None:
295
244
mutant .status = "SURVIVED"
296
245
elif result .returncode == 1 :
297
246
logger .info (f"🗡️ Mutant { mutant .id } killed 🗡️\n " )
298
- mutant .error_msg = result .stderr + result .stdout
247
+ error_output = result .stderr + result .stdout .lower ()
248
+ mutant .error_msg = error_output
299
249
mutant .status = "KILLED"
250
+ elif result .returncode == 2 :
251
+ logger .info (f"⏱️ Mutant { mutant .id } timed out ⏱️\n " )
252
+ mutant .error_msg = result .stderr
253
+ mutant .status = "TIMEOUT"
300
254
else :
301
- logger .error (f"Mutant { mutant .id } failed to run tests." )
302
- return
303
- self .mutants .append (mutant )
255
+ error_output = result .stderr + result .stdout
256
+ logger .info (f"🔧 Mutant { mutant .id } caused a compile error 🔧\n " )
257
+ mutant .error_msg = error_output
258
+ mutant .status = "COMPILE_ERROR"
304
259
305
260
def get_modified_files (self ) -> List [str ]:
306
261
"""
0 commit comments