33import sqlite3
44from contextlib import contextmanager
55from typing import Any , Dict , List , Optional , Tuple
6+ from datetime import datetime
67
78
89class DatabaseError (Exception ):
@@ -44,9 +45,20 @@ def create_tables(self):
4445 UNIQUE (source_file_id, version_hash)
4546 );
4647
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+
4758 CREATE TABLE IF NOT EXISTS Mutants (
4859 id INTEGER PRIMARY KEY,
4960 file_version_id INTEGER,
61+ run_id INTEGER,
5062 status TEXT,
5163 type TEXT,
5264 line_number INTEGER,
@@ -57,11 +69,39 @@ def create_tables(self):
5769 error_msg TEXT,
5870 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
5971 FOREIGN KEY (file_version_id) REFERENCES FileVersions(id)
72+ FOREIGN KEY (run_id) REFERENCES Runs(id)
6073 );
6174 """
6275 )
6376 conn .commit ()
6477
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+
65105 def get_file_version (self , file_path : str ) -> Tuple [int , int , bool ]:
66106 """
67107 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]:
114154 conn .rollback ()
115155 raise DatabaseError (f"Error processing file version: { str (e )} " )
116156
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 ):
118158 with self .get_connection () as conn :
119159 cursor = conn .cursor ()
120160 try :
121161 cursor .execute (
122162 """
123163 INSERT INTO Mutants (
124- file_version_id, status, type, line_number,
164+ run_id, file_version_id, status, type, line_number,
125165 original_code, mutated_code, description, mutant_path, error_msg
126- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
166+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
127167 """ ,
128168 (
169+ run_id ,
129170 file_version_id ,
130171 mutant_data ["status" ],
131172 mutant_data ["type" ],
@@ -223,6 +264,44 @@ def update_mutant_status(self, mutant_id: int, status: str):
223264 conn .rollback ()
224265 raise DatabaseError (f"Error updating mutant status: { str (e )} " )
225266
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+
226305 def get_survived_mutants (self , source_file_path ):
227306 with self .get_connection () as conn :
228307 cursor = conn .cursor ()
@@ -324,7 +403,50 @@ def get_file_mutations(self, file_name: str) -> List[Dict[str, Any]]:
324403 except sqlite3 .Error as e :
325404 raise DatabaseError (f"Error fetching file mutations: { str (e )} " )
326405
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 ]]:
328450 with self .get_connection () as conn :
329451 cursor = conn .cursor ()
330452 try :
@@ -335,12 +457,18 @@ def get_file_data(self) -> List[Dict[str, Any]]:
335457 sf.file_path,
336458 COUNT(m.id) as total_mutants,
337459 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
339465 FROM SourceFiles sf
340466 JOIN FileVersions fv ON sf.id = fv.source_file_id
341467 JOIN Mutants m ON fv.id = m.file_version_id
468+ WHERE m.run_id = ?
342469 GROUP BY sf.file_path
343- """
470+ """ ,
471+ (run_id ,),
344472 )
345473 return [
346474 {
@@ -350,41 +478,18 @@ def get_file_data(self) -> List[Dict[str, Any]]:
350478 "mutationCoverage" : (
351479 f"{ (row [3 ] / row [2 ] * 100 ):.2f} " if row [2 ] > 0 else "0.00"
352480 ),
481+ "killedMutants" : row [3 ],
353482 "survivedMutants" : row [4 ],
483+ "timeoutMutants" : row [5 ],
484+ "compileErrorMutants" : row [6 ],
485+ "syntaxErrorMutants" : row [7 ],
486+ "unexpectedTestErrorMutants" : row [8 ],
354487 }
355488 for row in cursor .fetchall ()
356489 ]
357490 except sqlite3 .Error as e :
358491 raise DatabaseError (f"Error fetching file data: { str (e )} " )
359492
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-
388493 def remove_mutants_by_file_version_id (self , file_version_id : int ):
389494 with self .get_connection () as conn :
390495 cursor = conn .cursor ()
0 commit comments