3
3
import sqlite3
4
4
from contextlib import contextmanager
5
5
from typing import Any , Dict , List , Optional , Tuple
6
+ from datetime import datetime
6
7
7
8
8
9
class DatabaseError (Exception ):
@@ -44,9 +45,20 @@ def create_tables(self):
44
45
UNIQUE (source_file_id, version_hash)
45
46
);
46
47
48
+ CREATE TABLE IF NOT EXISTS Runs (
49
+ id INTEGER PRIMARY KEY,
50
+ command_line TEXT NOT NULL,
51
+ start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
52
+ end_time TIMESTAMP,
53
+ execution_time REAL,
54
+ mutation_score REAL,
55
+ line_coverage REAL
56
+ );
57
+
47
58
CREATE TABLE IF NOT EXISTS Mutants (
48
59
id INTEGER PRIMARY KEY,
49
60
file_version_id INTEGER,
61
+ run_id INTEGER,
50
62
status TEXT,
51
63
type TEXT,
52
64
line_number INTEGER,
@@ -57,11 +69,39 @@ def create_tables(self):
57
69
error_msg TEXT,
58
70
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
59
71
FOREIGN KEY (file_version_id) REFERENCES FileVersions(id)
72
+ FOREIGN KEY (run_id) REFERENCES Runs(id)
60
73
);
61
74
"""
62
75
)
63
76
conn .commit ()
64
77
78
+ def start_new_run (self , command_line : str ) -> int :
79
+ """
80
+ Start a new run by inserting a record into the Runs table.
81
+
82
+ Args:
83
+ command_line (str): The command line used to start the mutation testing run.
84
+
85
+ Returns:
86
+ int: The ID of the newly created run.
87
+ """
88
+ with self .get_connection () as conn :
89
+ try :
90
+ cursor = conn .cursor ()
91
+ current_time = datetime .now ().isoformat ()
92
+ cursor .execute (
93
+ """
94
+ INSERT INTO Runs (command_line, start_time)
95
+ VALUES (?, ?)
96
+ """ ,
97
+ (command_line , current_time ),
98
+ )
99
+ conn .commit ()
100
+ return cursor .lastrowid
101
+ except sqlite3 .Error as e :
102
+ conn .rollback ()
103
+ raise DatabaseError (f"Failed to start new run: { str (e )} " )
104
+
65
105
def get_file_version (self , file_path : str ) -> Tuple [int , int , bool ]:
66
106
"""
67
107
Get or create a file version for the given file path.
@@ -114,18 +154,19 @@ def get_file_version(self, file_path: str) -> Tuple[int, int, bool]:
114
154
conn .rollback ()
115
155
raise DatabaseError (f"Error processing file version: { str (e )} " )
116
156
117
- def add_mutant (self , file_version_id : int , mutant_data : dict ):
157
+ def add_mutant (self , run_id : int , file_version_id : int , mutant_data : dict ):
118
158
with self .get_connection () as conn :
119
159
cursor = conn .cursor ()
120
160
try :
121
161
cursor .execute (
122
162
"""
123
163
INSERT INTO Mutants (
124
- file_version_id, status, type, line_number,
164
+ run_id, file_version_id, status, type, line_number,
125
165
original_code, mutated_code, description, mutant_path, error_msg
126
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
166
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
127
167
""" ,
128
168
(
169
+ run_id ,
129
170
file_version_id ,
130
171
mutant_data ["status" ],
131
172
mutant_data ["type" ],
@@ -223,6 +264,44 @@ def update_mutant_status(self, mutant_id: int, status: str):
223
264
conn .rollback ()
224
265
raise DatabaseError (f"Error updating mutant status: { str (e )} " )
225
266
267
+ def get_survived_mutants_by_run_id (self , run_id : int ) -> List [Dict [str , Any ]]:
268
+ with self .get_connection () as conn :
269
+ cursor = conn .cursor ()
270
+ try :
271
+ cursor .execute (
272
+ """
273
+ SELECT
274
+ m.id,
275
+ sf.file_path,
276
+ m.line_number,
277
+ m.mutant_path,
278
+ m.original_code,
279
+ m.mutated_code,
280
+ m.description,
281
+ m.type
282
+ FROM Mutants m
283
+ JOIN FileVersions fv ON m.file_version_id = fv.id
284
+ JOIN SourceFiles sf ON fv.source_file_id = sf.id
285
+ WHERE m.run_id = ? AND m.status = 'SURVIVED'
286
+ """ ,
287
+ (run_id ,),
288
+ )
289
+ return [
290
+ {
291
+ "id" : row [0 ],
292
+ "file_path" : row [1 ],
293
+ "line_number" : row [2 ],
294
+ "mutant_path" : row [3 ],
295
+ "original_code" : row [4 ],
296
+ "mutated_code" : row [5 ],
297
+ "description" : row [6 ],
298
+ "type" : row [7 ],
299
+ }
300
+ for row in cursor .fetchall ()
301
+ ]
302
+ except sqlite3 .Error as e :
303
+ raise DatabaseError (f"Error fetching survived mutants: { str (e )} " )
304
+
226
305
def get_survived_mutants (self , source_file_path ):
227
306
with self .get_connection () as conn :
228
307
cursor = conn .cursor ()
@@ -324,7 +403,50 @@ def get_file_mutations(self, file_name: str) -> List[Dict[str, Any]]:
324
403
except sqlite3 .Error as e :
325
404
raise DatabaseError (f"Error fetching file mutations: { str (e )} " )
326
405
327
- def get_file_data (self ) -> List [Dict [str , Any ]]:
406
+ def get_mutant_summary (self , run_id ) -> Dict [str , int ]:
407
+ with self .get_connection () as conn :
408
+ cursor = conn .cursor ()
409
+ try :
410
+ cursor .execute (
411
+ """
412
+ SELECT
413
+ COUNT(*) as total_mutants,
414
+ SUM(CASE WHEN status = 'KILLED' THEN 1 ELSE 0 END) as killed_mutants,
415
+ SUM(CASE WHEN status = 'SURVIVED' THEN 1 ELSE 0 END) as survived_mutants,
416
+ SUM(CASE WHEN status = 'TIMEOUT' THEN 1 ELSE 0 END) as timeout_mutants,
417
+ SUM(CASE WHEN status = 'COMPILE_ERROR' THEN 1 ELSE 0 END) as compile_error_mutants,
418
+ SUM(CASE WHEN status = 'SYNTAX_ERROR' THEN 1 ELSE 0 END) as syntax_error_mutants,
419
+ SUM(CASE WHEN status = 'UNEXPECTED_TEST_ERROR' THEN 1 ELSE 0 END) as unexpected_test_error_mutants
420
+ FROM Mutants
421
+ WHERE run_id = ?
422
+ """ ,
423
+ (run_id ,),
424
+ )
425
+ result = cursor .fetchone ()
426
+ if result [0 ] == 0 :
427
+ return None
428
+
429
+ else :
430
+ valid_mutants = (
431
+ result [0 ] - result [3 ] - result [4 ] - result [5 ] - result [6 ]
432
+ )
433
+ mutation_coverage = (
434
+ result [1 ] / valid_mutants if valid_mutants > 0 else 0.0
435
+ )
436
+ return {
437
+ "total_mutants" : result [0 ],
438
+ "killed_mutants" : result [1 ],
439
+ "survived_mutants" : result [2 ],
440
+ "timeout_mutants" : result [3 ],
441
+ "compile_error_mutants" : result [4 ],
442
+ "syntax_error_mutants" : result [5 ],
443
+ "unexpected_test_error_mutants" : result [6 ],
444
+ "mutation_coverage" : mutation_coverage ,
445
+ }
446
+ except sqlite3 .Error as e :
447
+ raise DatabaseError (f"Error fetching mutant summary: { str (e )} " )
448
+
449
+ def get_file_data (self , run_id : int ) -> List [Dict [str , Any ]]:
328
450
with self .get_connection () as conn :
329
451
cursor = conn .cursor ()
330
452
try :
@@ -335,12 +457,18 @@ def get_file_data(self) -> List[Dict[str, Any]]:
335
457
sf.file_path,
336
458
COUNT(m.id) as total_mutants,
337
459
SUM(CASE WHEN m.status = 'KILLED' THEN 1 ELSE 0 END) as killed_mutants,
338
- SUM(CASE WHEN m.status = 'SURVIVED' THEN 1 ELSE 0 END) as survived_mutants
460
+ SUM(CASE WHEN m.status = 'SURVIVED' THEN 1 ELSE 0 END) as survived_mutants,
461
+ SUM(CASE WHEN m.status = 'TIMEOUT' THEN 1 ELSE 0 END) as timeout_mutants,
462
+ SUM(CASE WHEN m.status = 'COMPILE_ERROR' THEN 1 ELSE 0 END) as compile_error_mutants,
463
+ SUM(CASE WHEN m.status = 'SYNTAX_ERROR' THEN 1 ELSE 0 END) as syntax_error_mutants,
464
+ SUM(CASE WHEN m.status = 'UNEXPECTED_TEST_ERROR' THEN 1 ELSE 0 END) as unexpected_test_error_mutants
339
465
FROM SourceFiles sf
340
466
JOIN FileVersions fv ON sf.id = fv.source_file_id
341
467
JOIN Mutants m ON fv.id = m.file_version_id
468
+ WHERE m.run_id = ?
342
469
GROUP BY sf.file_path
343
- """
470
+ """ ,
471
+ (run_id ,),
344
472
)
345
473
return [
346
474
{
@@ -350,41 +478,18 @@ def get_file_data(self) -> List[Dict[str, Any]]:
350
478
"mutationCoverage" : (
351
479
f"{ (row [3 ] / row [2 ] * 100 ):.2f} " if row [2 ] > 0 else "0.00"
352
480
),
481
+ "killedMutants" : row [3 ],
353
482
"survivedMutants" : row [4 ],
483
+ "timeoutMutants" : row [5 ],
484
+ "compileErrorMutants" : row [6 ],
485
+ "syntaxErrorMutants" : row [7 ],
486
+ "unexpectedTestErrorMutants" : row [8 ],
354
487
}
355
488
for row in cursor .fetchall ()
356
489
]
357
490
except sqlite3 .Error as e :
358
491
raise DatabaseError (f"Error fetching file data: { str (e )} " )
359
492
360
- def get_mutant_summary (self ) -> Dict [str , int ]:
361
- with self .get_connection () as conn :
362
- cursor = conn .cursor ()
363
- try :
364
- cursor .execute (
365
- """
366
- SELECT
367
- COUNT(*) as total_mutants,
368
- SUM(CASE WHEN status = 'KILLED' THEN 1 ELSE 0 END) as killed_mutants,
369
- SUM(CASE WHEN status = 'SURVIVED' THEN 1 ELSE 0 END) as survived_mutants,
370
- SUM(CASE WHEN status = 'TIMEOUT' THEN 1 ELSE 0 END) as timeout_mutants,
371
- SUM(CASE WHEN status = 'COMPILE_ERROR' THEN 1 ELSE 0 END) as compile_error_mutants
372
- FROM Mutants
373
- """
374
- )
375
- result = cursor .fetchone ()
376
- if result [0 ] == 0 :
377
- return None
378
- return {
379
- "total_mutants" : result [0 ],
380
- "killed_mutants" : result [1 ],
381
- "survived_mutants" : result [2 ],
382
- "timeout_mutants" : result [3 ],
383
- "compile_error_mutants" : result [4 ],
384
- }
385
- except sqlite3 .Error as e :
386
- raise DatabaseError (f"Error fetching mutant summary: { str (e )} " )
387
-
388
493
def remove_mutants_by_file_version_id (self , file_version_id : int ):
389
494
with self .get_connection () as conn :
390
495
cursor = conn .cursor ()
0 commit comments