1 /** 2 Copyright: Copyright (c) 2018-2021, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 This module contains the a basic database interface that have minimal 11 dependencies on internal modules. It is intended to be reusable from the test 12 suite. 13 14 The only acceptable dependency are: 15 * ../type.d 16 * ..backend/type.d 17 * ../database/type.d 18 * ../database/schema.d 19 */ 20 module dextool.plugin.mutate.backend.database.standalone; 21 22 import core.time : Duration, dur; 23 import logger = std.experimental.logger; 24 import std.algorithm : copy, map, joiner, filter; 25 import std.array : Appender, appender, array, empty; 26 import std.conv : to; 27 import std.datetime : SysTime, Clock; 28 import std.exception : collectException; 29 import std.format : format; 30 import std.path : relativePath; 31 import std.regex : Regex, matchFirst; 32 import std.typecons : Nullable, Flag, No; 33 34 import d2sqlite3 : SqlDatabase = Database; 35 import miniorm : Miniorm, select, insert, insertOrReplace, delete_, 36 insertOrIgnore, toSqliteDateTime, fromSqLiteDateTime, Bind; 37 import my.named_type; 38 import my.optional; 39 import my.term_color; 40 41 import dextool.type : AbsolutePath, Path, ExitStatusType; 42 43 import dextool.plugin.mutate.backend.database.schema; 44 import dextool.plugin.mutate.backend.database.type; 45 import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum, Language, Offset; 46 import dextool.plugin.mutate.type : MutationOrder; 47 48 /** Database wrapper with minimal dependencies. 49 */ 50 struct Database { 51 Miniorm db; 52 alias db this; 53 54 /** Create a database by either opening an existing or initializing a new. 55 * 56 * Params: 57 * db = path to the database 58 */ 59 static auto make(string db) @safe { 60 return Database(initializeDB(db)); 61 } 62 63 bool isToolVersionDifferent(ToolVersion compareTo) @trusted { 64 foreach (a; db.run(select!DextoolVersionTable)) { 65 return a.checksum != compareTo.get; 66 } 67 // if there is no tool version recorded then assume it is different. 68 return true; 69 } 70 71 /// Update the version of the tool. 72 void updateToolVersion(const ToolVersion tv) @trusted { 73 db.run(delete_!DextoolVersionTable); 74 db.run(insert!DextoolVersionTable, DextoolVersionTable(tv.get)); 75 } 76 77 /** Add all mutants with the specific status to the worklist. 78 */ 79 void updateWorklist(const Mutation.Kind[] kinds, const Mutation.Status status, 80 const long basePrio = 100, const MutationOrder userOrder = MutationOrder.random) @trusted { 81 const order = fromOrder(userOrder); 82 83 const sql = format!"INSERT OR IGNORE INTO %s (id,prio) 84 SELECT t1.id,%s FROM %s t0, %s t1 WHERE t0.kind IN (%(%s,%)) AND 85 t0.st_id = t1.id AND 86 t1.status = :status 87 "(mutantWorklistTable, order, 88 mutationTable, mutationStatusTable, kinds.map!(a => cast(int) a)); 89 auto stmt = db.prepare(sql); 90 stmt.get.bind(":status", cast(long) status); 91 stmt.get.bind(":base_prio", basePrio); 92 stmt.get.execute; 93 } 94 95 /// Add a mutant to the worklist. 96 void addToWorklist(const MutationStatusId id, const long basePrio = 0, 97 const MutationOrder userOrder = MutationOrder.consecutive) @trusted { 98 const order = fromOrder(userOrder); 99 const sql = format!"INSERT OR REPLACE INTO %s (id,prio) 100 SELECT t1.id,%s FROM %s t1 WHERE t1.id = :id 101 "(mutantWorklistTable, order, mutationStatusTable); 102 auto stmt = db.prepare(sql); 103 stmt.get.bind(":id", id.get); 104 stmt.get.bind(":base_prio", basePrio); 105 stmt.get.execute; 106 } 107 108 /// Remove a mutant from the worklist. 109 void removeFromWorklist(const MutationStatusId id) @trusted { 110 static immutable sql = format!"DELETE FROM %1$s WHERE id = :id"(mutantWorklistTable); 111 auto stmt = db.prepare(sql); 112 stmt.get.bind(":id", id.get); 113 stmt.get.execute; 114 } 115 116 void clearWorklist() @trusted { 117 static immutable sql = format!"DELETE FROM %1$s"(mutantWorklistTable); 118 auto stmt = db.prepare(sql); 119 stmt.get.execute; 120 } 121 122 long getWorklistCount() @trusted { 123 static immutable sql = format!"SELECT count(*) FROM %1$s"(mutantWorklistTable); 124 auto stmt = db.prepare(sql); 125 auto res = stmt.get.execute; 126 return res.oneValue!long; 127 } 128 129 /// If the file has already been analyzed. 130 bool isAnalyzed(const Path p) @trusted { 131 auto stmt = db.prepare("SELECT count(*) FROM files WHERE path=:path LIMIT 1"); 132 stmt.get.bind(":path", cast(string) p); 133 auto res = stmt.get.execute; 134 return res.oneValue!long != 0; 135 } 136 137 /// If the file has already been analyzed. 138 bool isAnalyzed(const Path p, const Checksum cs) @trusted { 139 auto stmt = db.prepare( 140 "SELECT count(*) FROM files WHERE path=:path AND checksum0=:cs0 AND checksum1=:cs1 LIMIT 1"); 141 stmt.get.bind(":path", cast(string) p); 142 stmt.get.bind(":cs0", cast(long) cs.c0); 143 stmt.get.bind(":cs1", cast(long) cs.c1); 144 auto res = stmt.get.execute; 145 return res.oneValue!long != 0; 146 } 147 148 bool exists(MutationStatusId id) { 149 static immutable s = format!"SELECT COUNT(*) FROM %s WHERE id=:id LIMIT 1"( 150 mutationStatusTable); 151 auto stmt = db.prepare(s); 152 stmt.get.bind(":id", cast(long) id); 153 auto res = stmt.get.execute; 154 return res.oneValue!long == 0; 155 } 156 157 bool isMarked(MutationId id) @trusted { 158 static immutable s = format!"SELECT COUNT(*) FROM %s WHERE st_id IN 159 (SELECT st_id FROM %s WHERE id=:id)"( 160 markedMutantTable, mutationTable); 161 auto stmt = db.prepare(s); 162 stmt.get.bind(":id", cast(long) id); 163 auto res = stmt.get.execute; 164 return res.oneValue!long != 0; 165 } 166 167 /// All marked mutants whom have a mutation status checksum that has been removed from the database. 168 MarkedMutant[] getLostMarkings() @trusted { 169 static immutable sql = format!"SELECT checksum0 FROM %s 170 WHERE 171 checksum0 NOT IN (SELECT checksum0 FROM %s)"( 172 markedMutantTable, mutationStatusTable); 173 174 auto stmt = db.prepare(sql); 175 auto app = appender!(MarkedMutant[])(); 176 foreach (res; stmt.get.execute) { 177 foreach (m; db.run(select!MarkedMutantTbl.where("checksum0 = :cs0", 178 Bind("cs0")), res.peek!long(0))) { 179 app.put(.make(m)); 180 } 181 } 182 183 return app.data; 184 } 185 186 Nullable!FileId getFileId(const Path p) @trusted { 187 static immutable sql = format("SELECT id FROM %s WHERE path=:path", filesTable); 188 auto stmt = db.prepare(sql); 189 stmt.get.bind(":path", p.toString); 190 auto res = stmt.get.execute; 191 192 typeof(return) rval; 193 if (!res.empty) 194 rval = FileId(res.oneValue!long); 195 return rval; 196 } 197 198 /// Returns: the path ID for the mutant. 199 Nullable!FileId getFileId(const MutationId id) @trusted { 200 static immutable sql = format("SELECT t1.file_id 201 FROM %s t0, %s t1 202 WHERE t0.id = :id AND t0.mp_id = t1.id", 203 mutationTable, mutationPointTable); 204 auto stmt = db.prepare(sql); 205 stmt.get.bind(":id", cast(long) id); 206 207 typeof(return) rval; 208 foreach (ref r; stmt.get.execute) 209 rval = FileId(r.peek!long(0)); 210 return rval; 211 } 212 213 /// Returns: the file path that the id correspond to. 214 Nullable!Path getFile(const FileId id) @trusted { 215 static immutable sql = format("SELECT path FROM %s WHERE id = :id", filesTable); 216 auto stmt = db.prepare(sql); 217 stmt.get.bind(":id", id.get); 218 219 typeof(return) rval; 220 foreach (ref r; stmt.get.execute) 221 rval = Path(r.peek!string(0)); 222 return rval; 223 } 224 225 Optional!Language getFileIdLanguage(const FileId id) @trusted { 226 static immutable sql = format!"SELECT lang FROM %s WHERE id = :id"(filesTable); 227 auto stmt = db.prepare(sql); 228 stmt.get.bind(":id", id.get); 229 230 foreach (ref r; stmt.get.execute) 231 return some(r.peek!ubyte(0).to!Language); 232 return none!Language; 233 } 234 235 /// Returns: all files tagged as a root. 236 FileId[] getRootFiles() @trusted { 237 static immutable sql = format!"SELECT id FROM %s WHERE root=1"(filesTable); 238 239 auto app = appender!(FileId[])(); 240 auto stmt = db.prepare(sql); 241 foreach (ref r; stmt.get.execute) { 242 app.put(r.peek!long(0).FileId); 243 } 244 return app.data; 245 } 246 247 /// Remove the file with all mutations that are coupled to it. 248 void removeFile(const Path p) @trusted { 249 auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(filesTable)); 250 stmt.get.bind(":path", p.toString); 251 stmt.get.execute; 252 } 253 254 /// Returns: All files in the database as relative paths. 255 Path[] getFiles() @trusted { 256 auto stmt = db.prepare(format!"SELECT path FROM %s"(filesTable)); 257 auto res = stmt.get.execute; 258 259 auto app = appender!(Path[]); 260 foreach (ref r; res) { 261 app.put(Path(r.peek!string(0))); 262 } 263 264 return app.data; 265 } 266 267 Nullable!Checksum getFileChecksum(const Path p) @trusted { 268 static immutable sql = format!"SELECT checksum0,checksum1 FROM %s WHERE path=:path"( 269 filesTable); 270 auto stmt = db.prepare(sql); 271 stmt.get.bind(":path", p.toString); 272 auto res = stmt.get.execute; 273 274 typeof(return) rval; 275 if (!res.empty) { 276 rval = Checksum(res.front.peek!long(0), res.front.peek!long(1)); 277 } 278 279 return rval; 280 } 281 282 /// Returns: the timestamp of the newest file that was added. 283 Optional!SysTime getNewestFile() @trusted { 284 auto stmt = db.prepare(format!"SELECT timestamp 285 FROM %s ORDER BY datetime(timestamp) DESC LIMIT 1"( 286 filesTable)); 287 auto res = stmt.get.execute; 288 289 foreach (ref r; res) { 290 return some(r.peek!string(0).fromSqLiteDateTime); 291 } 292 293 return none!SysTime; 294 } 295 296 void put(const Path p, Checksum cs, const Language lang, bool isRoot) @trusted { 297 static immutable sql = format!"INSERT OR IGNORE INTO %s (path, checksum0, checksum1, lang, timestamp, root) 298 VALUES (:path, :checksum0, :checksum1, :lang, :time, :root)"( 299 filesTable); 300 auto stmt = db.prepare(sql); 301 stmt.get.bind(":path", p.toString); 302 stmt.get.bind(":checksum0", cast(long) cs.c0); 303 stmt.get.bind(":checksum1", cast(long) cs.c1); 304 stmt.get.bind(":lang", cast(long) lang); 305 stmt.get.bind(":time", Clock.currTime.toSqliteDateTime); 306 stmt.get.bind(":root", isRoot); 307 stmt.get.execute; 308 } 309 310 void put(const TestFile tfile) @trusted { 311 static immutable sql = format!"INSERT OR IGNORE INTO %s (path, checksum0, checksum1, timestamp) 312 VALUES (:path, :checksum0, :checksum1, :timestamp)"( 313 testFilesTable); 314 auto stmt = db.prepare(sql); 315 stmt.get.bind(":path", tfile.file.get.toString); 316 stmt.get.bind(":checksum0", cast(long) tfile.checksum.get.c0); 317 stmt.get.bind(":checksum1", cast(long) tfile.checksum.get.c1); 318 stmt.get.bind(":timestamp", tfile.timeStamp.toSqliteDateTime); 319 stmt.get.execute; 320 } 321 322 TestFile[] getTestFiles() @trusted { 323 static immutable sql = format!"SELECT path,checksum0,checksum1,timestamp FROM %s"( 324 testFilesTable); 325 auto stmt = db.prepare(sql); 326 auto res = stmt.get.execute; 327 328 auto app = appender!(TestFile[]); 329 foreach (ref r; res) { 330 app.put(TestFile(TestFilePath(Path(r.peek!string(0))), 331 TestFileChecksum(Checksum(r.peek!long(1), r.peek!long(2))), 332 r.peek!string(3).fromSqLiteDateTime)); 333 } 334 335 return app.data; 336 } 337 338 /// Returns: the oldest test file, if it exists. 339 Optional!TestFile getNewestTestFile() @trusted { 340 auto stmt = db.prepare(format!"SELECT path,checksum0,checksum1,timestamp 341 FROM %s ORDER BY datetime(timestamp) DESC LIMIT 1"( 342 testFilesTable)); 343 auto res = stmt.get.execute; 344 345 foreach (ref r; res) { 346 return some(TestFile(TestFilePath(Path(r.peek!string(0))), 347 TestFileChecksum(Checksum(r.peek!long(1), r.peek!long(2))), 348 r.peek!string(3).fromSqLiteDateTime)); 349 } 350 351 return none!TestFile; 352 } 353 354 /// Remove the file with all mutations that are coupled to it. 355 void removeFile(const TestFilePath p) @trusted { 356 auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(testFilesTable)); 357 stmt.get.bind(":path", p.get.toString); 358 stmt.get.execute; 359 } 360 361 enum CntAction { 362 /// Increment the counter 363 incr, 364 /// Reset the counter to zero 365 reset, 366 } 367 368 /** Update the status of a mutant. 369 * 370 * Params: 371 * id = ID of the mutant 372 * st = status to broadcast 373 * d = time spent on veryfing the mutant 374 * tcs = test cases that killed the mutant 375 * counter = how to act with the counter 376 */ 377 void updateMutation(const MutationId id, const Mutation.Status st, 378 const ExitStatus ecode, const MutantTimeProfile p, const(TestCase)[] tcs) @trusted { 379 static immutable sql = "UPDATE %s SET 380 status=:st,compile_time_ms=:compile,test_time_ms=:test,update_ts=:update_ts 381 WHERE id IN (SELECT st_id FROM %s WHERE id = :id)"; 382 383 auto stmt = db.prepare(format!sql(mutationStatusTable, mutationTable)); 384 stmt.get.bind(":st", cast(long) st); 385 stmt.get.bind(":id", id.get); 386 stmt.get.bind(":compile", p.compile.total!"msecs"); 387 stmt.get.bind(":test", p.test.total!"msecs"); 388 stmt.get.bind(":update_ts", Clock.currTime.toSqliteDateTime); 389 stmt.get.execute; 390 391 updateMutationTestCases(id, tcs); 392 } 393 394 /** Update the status of a mutant. 395 * 396 * Params: 397 * id = ID of the mutant 398 * st = status to broadcast 399 * d = time spent on veryfing the mutant 400 * tcs = test cases that killed the mutant 401 * counter = how to act with the counter 402 */ 403 void updateMutation(const MutationStatusId id, const Mutation.Status st, 404 const ExitStatus ecode, const MutantTimeProfile p) @trusted { 405 static immutable sql = "UPDATE %s SET 406 status=:st,compile_time_ms=:compile,test_time_ms=:test,update_ts=:update_ts 407 WHERE id = :id"; 408 409 auto stmt = db.prepare(format!sql(mutationStatusTable)); 410 stmt.get.bind(":id", id.get); 411 stmt.get.bind(":st", cast(long) st); 412 stmt.get.bind(":compile", p.compile.total!"msecs"); 413 stmt.get.bind(":test", p.test.total!"msecs"); 414 stmt.get.bind(":update_ts", Clock.currTime.toSqliteDateTime); 415 stmt.get.execute; 416 } 417 418 /// Update the time used to test the mutant. 419 void updateMutation(const MutationStatusId id, const MutantTimeProfile p) @trusted { 420 static immutable sql = format!"UPDATE %s SET compile_time_ms=:compile,test_time_ms=:test WHERE id = :id"( 421 mutationStatusTable); 422 auto stmt = db.prepare(sql); 423 stmt.get.bind(":id", id.get); 424 stmt.get.bind(":compile", p.compile.total!"msecs"); 425 stmt.get.bind(":test", p.test.total!"msecs"); 426 stmt.get.execute; 427 } 428 429 /** Update the status of a mutant. 430 * 431 * Params: 432 * id = mutation status ID 433 * st = new status 434 * update_ts = if the update timestamp should be updated. 435 */ 436 void updateMutationStatus(const MutationStatusId id, const Mutation.Status st, 437 const ExitStatus ecode, Flag!"updateTs" update_ts = No.updateTs) @trusted { 438 439 auto stmt = () { 440 if (update_ts) { 441 const ts = Clock.currTime.toSqliteDateTime; 442 auto s = db.prepare(format!"UPDATE %s SET status=:st,update_ts=:update_ts WHERE id=:id"( 443 mutationStatusTable)); 444 s.get.bind(":update_ts", ts); 445 return s; 446 } else 447 return db.prepare(format!"UPDATE %s SET status=:st WHERE id=:id"( 448 mutationStatusTable)); 449 }(); 450 stmt.get.bind(":st", st.to!long); 451 stmt.get.bind(":id", id.to!long); 452 stmt.get.execute; 453 } 454 455 /// Returns: all mutation status IDs. 456 MutationStatusId[] getAllMutationStatus() @trusted { 457 static immutable sql = format!"SELECT id FROM %s"(mutationStatusTable); 458 459 auto app = appender!(MutationStatusId[])(); 460 auto stmt = db.prepare(sql); 461 foreach (r; stmt.get.execute) 462 app.put(MutationStatusId(r.peek!long(0))); 463 return app.data; 464 } 465 466 // TODO: change to my.optional 467 Nullable!(Mutation.Status) getMutationStatus(const MutationStatusId id) @trusted { 468 static immutable sql = format!"SELECT status FROM %s WHERE id=:id"(mutationStatusTable); 469 auto stmt = db.prepare(sql); 470 stmt.get.bind(":id", id.get); 471 472 typeof(return) rval; 473 foreach (a; stmt.get.execute) { 474 rval = cast(Mutation.Status) a.peek!long(0); 475 } 476 return rval; 477 } 478 479 Nullable!MutationEntry getMutation(const MutationId id) @trusted { 480 import dextool.plugin.mutate.backend.type; 481 482 typeof(return) rval; 483 484 static immutable get_mut_sql = format("SELECT 485 t0.id, 486 t0.kind, 487 t3.compile_time_ms, 488 t3.test_time_ms, 489 t1.offset_begin, 490 t1.offset_end, 491 t1.line, 492 t1.column, 493 t2.path, 494 t2.lang 495 FROM %s t0,%s t1,%s t2,%s t3 496 WHERE 497 t0.id = :id AND 498 t0.mp_id = t1.id AND 499 t1.file_id = t2.id AND 500 t3.id = t0.st_id 501 ", mutationTable, 502 mutationPointTable, filesTable, mutationStatusTable); 503 504 auto stmt = db.prepare(get_mut_sql); 505 stmt.get.bind(":id", cast(long) id); 506 auto res = stmt.get.execute; 507 508 if (res.empty) 509 return rval; 510 511 auto v = res.front; 512 513 auto mp = MutationPoint(Offset(v.peek!uint(4), v.peek!uint(5))); 514 mp.mutations = [Mutation(v.peek!long(1).to!(Mutation.Kind))]; 515 auto pkey = MutationId(v.peek!long(0)); 516 auto file = Path(v.peek!string(8)); 517 auto sloc = SourceLoc(v.peek!uint(6), v.peek!uint(7)); 518 auto lang = v.peek!long(9).to!Language; 519 520 rval = MutationEntry(pkey, file, sloc, mp, 521 MutantTimeProfile(v.peek!long(2).dur!"msecs", v.peek!long(3).dur!"msecs"), lang); 522 523 return rval; 524 } 525 526 MutantMetaData getMutantationMetaData(const MutationId id) @trusted { 527 auto rval = MutantMetaData(id); 528 foreach (res; db.run(select!NomutDataTbl.where("mut_id = :mutid", Bind("mutid")), id.get)) { 529 rval.set(NoMut(res.tag, res.comment)); 530 } 531 return rval; 532 } 533 534 // TODO: fix spelling error 535 // TODO: this is a bit inefficient. it should use a callback iterator 536 MutantMetaData[] getMutantationMetaData(const Mutation.Kind[] kinds, const Mutation 537 .Status status) @trusted { 538 const sql = format!"SELECT DISTINCT t.mut_id, t.tag, t.comment 539 FROM %s t, %s t1, %s t2 540 WHERE 541 t.mut_id = t1.id AND 542 t1.st_id = t2.id AND 543 t2.status = :status AND 544 t1.kind IN (%(%s,%)) 545 ORDER BY 546 t.mut_id"(nomutDataTable, mutationTable, 547 mutationStatusTable, kinds.map!(a => cast(long) a)); 548 auto stmt = db.prepare(sql); 549 stmt.get.bind(":status", cast(long) status); 550 551 auto app = appender!(MutantMetaData[])(); 552 foreach (res; stmt.get.execute) { 553 app.put(MutantMetaData(MutationId(res.peek!long(0)), 554 MutantAttr(NoMut(res.peek!string(1), res.peek!string(2))))); 555 } 556 return app.data; 557 } 558 559 Nullable!Path getPath(const MutationId id) @trusted { 560 static immutable get_path_sql = format("SELECT t2.path 561 FROM 562 %s t0, %s t1, %s t2 563 WHERE 564 t0.id = :id AND 565 t0.mp_id = t1.id AND 566 t1.file_id = t2.id 567 ", mutationTable, 568 mutationPointTable, filesTable); 569 570 auto stmt = db.prepare(get_path_sql); 571 stmt.get.bind(":id", cast(long) id); 572 auto res = stmt.get.execute; 573 574 typeof(return) rval; 575 if (!res.empty) 576 rval = Path(res.front.peek!string(0)); 577 return rval; 578 } 579 580 /// Returns: the mutants that are connected to the mutation statuses. 581 MutantInfo[] getMutantsInfo(const Mutation.Kind[] kinds, const(MutationStatusId)[] id) @trusted { 582 const get_mutid_sql = format("SELECT t0.id,t2.status,t2.exit_code,t0.kind,t1.line,t1.column 583 FROM %s t0,%s t1, %s t2 584 WHERE 585 t0.st_id IN (%(%s,%)) AND 586 t0.st_id = t2.id AND 587 t0.kind IN (%(%s,%)) AND 588 t0.mp_id = t1.id", mutationTable, 589 mutationPointTable, mutationStatusTable, id.map!(a => a.get), 590 kinds.map!(a => cast(int) a)); 591 auto stmt = db.prepare(get_mutid_sql); 592 593 auto app = appender!(MutantInfo[])(); 594 foreach (res; stmt.get.execute) { 595 app.put(MutantInfo(MutationId(res.peek!long(0)), res.peek!long(1) 596 .to!(Mutation.Status), res.peek!int(2).ExitStatus, 597 res.peek!long(3).to!(Mutation.Kind), 598 SourceLoc(res.peek!uint(4), res.peek!uint(5)))); 599 } 600 601 return app.data; 602 } 603 604 /// Returns: the mutants that are connected to the mutation statuses. 605 MutationId[] getMutationIds(const(Mutation.Kind)[] kinds, const(MutationStatusId)[] id) @trusted { 606 if (id.length == 0) 607 return null; 608 609 const get_mutid_sql = format!"SELECT id FROM %s t0 610 WHERE 611 t0.st_id IN (%(%s,%)) AND 612 t0.kind IN (%(%s,%))"(mutationTable, 613 id.map!(a => cast(long) a), kinds.map!(a => cast(int) a)); 614 auto stmt = db.prepare(get_mutid_sql); 615 616 auto app = appender!(MutationId[])(); 617 foreach (res; stmt.get.execute) 618 app.put(MutationId(res.peek!long(0))); 619 return app.data; 620 } 621 622 Nullable!MutationId getMutationId(const MutationStatusId id) @trusted { 623 static immutable sql = format!"SELECT id FROM %s WHERE st_id=:st_id"(mutationTable); 624 auto stmt = db.prepare(sql); 625 stmt.get.bind(":st_id", id.get); 626 627 typeof(return) rval; 628 foreach (res; stmt.get.execute) { 629 rval = res.peek!long(0).MutationId; 630 break; 631 } 632 return rval; 633 } 634 635 Nullable!MutationStatusId getMutationStatusId(const MutationId id) @trusted { 636 static immutable sql = format!"SELECT st_id FROM %s WHERE id=:id"(mutationTable); 637 auto stmt = db.prepare(sql); 638 stmt.get.bind(":id", cast(long) id); 639 640 typeof(return) rval; 641 foreach (res; stmt.get.execute) { 642 rval = MutationStatusId(res.peek!long(0)); 643 } 644 return rval; 645 } 646 647 Nullable!MutationStatusId getMutationStatusId(const Checksum cs) @trusted { 648 static immutable sql = format!"SELECT id FROM %s WHERE checksum0=:cs0 AND checksum1=:cs1"( 649 mutationStatusTable); 650 auto stmt = db.prepare(sql); 651 stmt.get.bind(":cs0", cast(long) cs.c0); 652 stmt.get.bind(":cs1", cast(long) cs.c1); 653 654 typeof(return) rval; 655 foreach (res; stmt.get.execute) { 656 rval = MutationStatusId(res.peek!long(0)); 657 } 658 return rval; 659 } 660 661 // TODO: maybe this need to return the exit code too? 662 // Returns: the status of the mutant 663 Nullable!(Mutation.Status) getMutationStatus(const MutationId id) @trusted { 664 auto s = format!"SELECT status FROM %s WHERE id IN (SELECT st_id FROM %s WHERE id=:mut_id)"( 665 mutationStatusTable, mutationTable); 666 auto stmt = db.prepare(s); 667 stmt.get.bind(":mut_id", cast(long) id); 668 typeof(return) rval; 669 foreach (res; stmt.get.execute) 670 rval = res.peek!long(0).to!(Mutation.Status); 671 return rval; 672 } 673 674 /// Returns: the mutants in the file at the line. 675 MutationStatusId[] getMutationsOnLine(const(Mutation.Kind)[] kinds, FileId fid, SourceLoc sloc) @trusted { 676 // TODO: should it also be line_end? 677 const sql = format("SELECT DISTINCT t0.id FROM %s t0, %s t1, %s t2 678 WHERE 679 t1.st_id = t0.id AND 680 t1.kind IN (%(%s,%)) AND 681 t1.mp_id = t2.id AND 682 t2.file_id = :fid AND 683 (:line BETWEEN t2.line AND t2.line_end)", 684 mutationStatusTable, mutationTable, mutationPointTable, 685 kinds.map!(a => cast(int) a)); 686 auto stmt = db.prepare(sql); 687 stmt.get.bind(":fid", cast(long) fid); 688 stmt.get.bind(":line", sloc.line); 689 690 auto app = appender!(typeof(return))(); 691 foreach (res; stmt.get.execute) 692 app.put(MutationStatusId(res.peek!long(0))); 693 return app.data; 694 } 695 696 LineMetadata getLineMetadata(const FileId fid, const SourceLoc sloc) @trusted { 697 // TODO: change this select to using microrm 698 static immutable sql = format("SELECT nomut,tag,comment FROM %s 699 WHERE 700 file_id = :fid AND 701 line = :line", rawSrcMetadataTable); 702 auto stmt = db.prepare(sql); 703 stmt.get.bind(":fid", cast(long) fid); 704 stmt.get.bind(":line", sloc.line); 705 706 auto rval = typeof(return)(fid, sloc.line); 707 foreach (res; stmt.get.execute) { 708 if (res.peek!long(0) != 0) 709 rval.set(NoMut(res.peek!string(1), res.peek!string(2))); 710 } 711 712 return rval; 713 } 714 715 /// Returns: the `nr` mutants that where the longst since they where tested. 716 MutationStatusTime[] getOldestMutants(const(Mutation.Kind)[] kinds, const long nr) @trusted { 717 const sql = format("SELECT t0.id,t0.update_ts FROM %s t0, %s t1 718 WHERE 719 t0.update_ts IS NOT NULL AND 720 t1.st_id = t0.id AND 721 t1.kind IN (%(%s,%)) 722 ORDER BY datetime(t0.update_ts) ASC LIMIT :limit", 723 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a)); 724 auto stmt = db.prepare(sql); 725 stmt.get.bind(":limit", nr); 726 727 auto app = appender!(MutationStatusTime[])(); 728 foreach (res; stmt.get.execute) 729 app.put(MutationStatusTime(MutationStatusId(res.peek!long(0)), 730 res.peek!string(1).fromSqLiteDateTime)); 731 return app.data; 732 } 733 734 /// Returns: the `nr` mutants that where last tested. 735 MutationStatusTime[] getLatestMutants(const(Mutation.Kind)[] kinds, const long nr) @trusted { 736 const sql = format("SELECT t0.id,t0.update_ts FROM %s t0, %s t1 737 WHERE 738 t0.update_ts IS NOT NULL AND 739 t1.st_id = t0.id AND 740 t1.kind IN (%(%s,%)) 741 ORDER BY datetime(t0.update_ts) DESC LIMIT :limit", 742 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a)); 743 auto stmt = db.prepare(sql); 744 stmt.get.bind(":limit", nr); 745 746 auto app = appender!(MutationStatusTime[])(); 747 foreach (res; stmt.get.execute) 748 app.put(MutationStatusTime(MutationStatusId(res.peek!long(0)), 749 res.peek!string(1).fromSqLiteDateTime)); 750 return app.data; 751 } 752 753 /// Returns: the `nr` mutant with the highest count that has not been killed and existed in the system the longest. 754 MutationStatus[] getHighestPrioMutant(const(Mutation.Kind)[] kinds, 755 const Mutation.Status status, const long nr) @trusted { 756 const sql = format("SELECT t0.id,t0.status,t0.prio,t0.update_ts,t0.added_ts 757 FROM %s t0, %s t1 758 WHERE 759 t0.update_ts IS NOT NULL AND 760 t0.status = :status AND 761 t1.st_id = t0.id AND 762 t1.kind IN (%(%s,%)) AND 763 t1.st_id NOT IN (SELECT st_id FROM %s WHERE nomut != 0) 764 ORDER BY t0.prio DESC LIMIT :limit", 765 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a), srcMetadataTable); 766 auto stmt = db.prepare(sql); 767 stmt.get.bind(":status", cast(long) status); 768 stmt.get.bind(":limit", nr); 769 770 auto app = appender!(MutationStatus[])(); 771 foreach (res; stmt.get.execute) { 772 auto added = () { 773 auto raw = res.peek!string(4); 774 if (raw.length == 0) 775 return Nullable!SysTime(); 776 return Nullable!SysTime(raw.fromSqLiteDateTime); 777 }(); 778 779 // dfmt off 780 app.put(MutationStatus( 781 MutationStatusId(res.peek!long(0)), 782 res.peek!long(1).to!(Mutation.Status), 783 res.peek!long(2).MutantPrio, 784 res.peek!string(3).fromSqLiteDateTime, 785 added, 786 )); 787 // dfmt on 788 } 789 790 return app.data; 791 } 792 793 /** Get SourceLoc for a specific mutation id. 794 */ 795 Nullable!SourceLoc getSourceLocation(MutationId id) @trusted { 796 auto s = format!"SELECT line, column FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE id=:mut_id)"( 797 mutationPointTable, mutationTable); 798 auto stmt = db.prepare(s); 799 stmt.get.bind(":mut_id", cast(long) id); 800 typeof(return) rval; 801 foreach (res; stmt.get.execute) 802 rval = SourceLoc(res.peek!uint(0), res.peek!uint(1)); 803 return rval; 804 } 805 806 /** Remove all mutations of kinds. 807 */ 808 void removeMutant(const Mutation.Kind[] kinds) @trusted { 809 const s = format!"DELETE FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE kind IN (%(%s,%)))"( 810 mutationPointTable, mutationTable, kinds.map!(a => cast(int) a)); 811 auto stmt = db.prepare(s); 812 stmt.get.execute; 813 } 814 815 /** Reset all mutations of kinds with the status `st` to unknown. 816 */ 817 void resetMutant(const Mutation.Kind[] kinds, Mutation.Status st, Mutation.Status to_st) @trusted { 818 const s = format!"UPDATE %s SET status=%s WHERE status = %s AND id IN(SELECT st_id FROM %s WHERE kind IN (%(%s,%)))"( 819 mutationStatusTable, to_st.to!long, st.to!long, 820 mutationTable, kinds.map!(a => cast(int) a)); 821 auto stmt = db.prepare(s); 822 stmt.get.execute; 823 } 824 825 /** Mark a mutant with status and rationale (also adds metadata). 826 */ 827 void markMutant(const MutationId id, const Path file, const SourceLoc sloc, 828 const MutationStatusId statusId, const Checksum cs, 829 const Mutation.Status s, const Rationale r, string mutationTxt) @trusted { 830 db.run(insertOrReplace!MarkedMutantTbl, MarkedMutantTbl(cs.c0, cs.c1, 831 statusId.get, id.get, sloc.line, sloc.column, file, s, 832 Clock.currTime.toUTC, r.get, mutationTxt)); 833 } 834 835 void removeMarkedMutant(const Checksum cs) @trusted { 836 db.run(delete_!MarkedMutantTbl.where("checksum0 = :cs0", Bind("cs0")), cast(long) cs.c0); 837 } 838 839 void removeMarkedMutant(const MutationStatusId id) @trusted { 840 db.run(delete_!MarkedMutantTbl.where("st_id = :st_id", Bind("st_id")), id.get); 841 } 842 843 /// Returns: All mutants with that are marked orderd by their path 844 MarkedMutant[] getMarkedMutants() @trusted { 845 import miniorm : OrderingTermSort; 846 847 auto app = appender!(MarkedMutant[])(); 848 foreach (m; db.run(select!MarkedMutantTbl.orderBy(OrderingTermSort.ASC, [ 849 "path" 850 ]))) { 851 app.put(.make(m)); 852 } 853 854 return app.data; 855 } 856 857 Mutation.Kind getKind(MutationId id) @trusted { 858 static immutable sql = format!"SELECT kind FROM %s WHERE id=:id"(mutationTable); 859 auto stmt = db.prepare(sql); 860 stmt.get.bind(":id", cast(long) id); 861 862 typeof(return) rval; 863 foreach (res; stmt.get.execute) { 864 rval = res.peek!long(0).to!(Mutation.Kind); 865 } 866 return rval; 867 } 868 869 import dextool.plugin.mutate.backend.type; 870 871 alias aliveSrcMutants = countMutants!([ 872 Mutation.Status.alive, Mutation.Status.noCoverage 873 ]); 874 alias killedSrcMutants = countMutants!([Mutation.Status.killed]); 875 alias timeoutSrcMutants = countMutants!([Mutation.Status.timeout]); 876 alias noCovSrcMutants = countMutants!([Mutation.Status.noCoverage]); 877 878 /// Returns: Total that should be counted when calculating the mutation score. 879 alias totalSrcMutants = countMutants!([ 880 Mutation.Status.alive, Mutation.Status.killed, 881 Mutation.Status.timeout, Mutation.Status.noCoverage 882 ]); 883 884 alias unknownSrcMutants = countMutants!([Mutation.Status.unknown]); 885 alias killedByCompilerSrcMutants = countMutants!([ 886 Mutation.Status.killedByCompiler 887 ]); 888 889 /** Count the mutants with the nomut metadata. 890 * 891 * Params: 892 * status = status the mutants must be in to be counted. 893 * distinc = count based on unique source code changes. 894 * kinds = the kind of mutants to count. 895 * file = file to count mutants in. 896 */ 897 private MutationReportEntry countMutants(int[] status)(const Mutation.Kind[] kinds, 898 string file = null) @trusted { 899 const qq = " 900 SELECT count(*),sum(compile_time_ms),sum(test_time_ms) 901 FROM ( 902 SELECT sum(t1.compile_time_ms) compile_time_ms,sum(t1.test_time_ms) test_time_ms 903 FROM %s t0, %s t1%s 904 WHERE 905 %s 906 t0.st_id = t1.id AND 907 t1.status IN (%(%s,%)) AND 908 t0.kind IN (%(%s,%)) 909 GROUP BY t1.id)"; 910 const query = () { 911 auto fq = file.length == 0 912 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND"; 913 auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3", 914 mutationPointTable, filesTable); 915 return format(qq, mutationTable, mutationStatusTable, fq_from, fq, 916 status, kinds.map!(a => cast(int) a)); 917 }(); 918 919 typeof(return) rval; 920 auto stmt = db.prepare(query); 921 if (file.length != 0) 922 stmt.get.bind(":path", file); 923 auto res = stmt.get.execute; 924 if (!res.empty) { 925 rval = MutationReportEntry(res.front.peek!long(0), 926 MutantTimeProfile(res.front.peek!long(1).dur!"msecs", 927 res.front.peek!long(2).dur!"msecs")); 928 } 929 return rval; 930 } 931 932 /** Count the mutants with the nomut metadata. 933 * 934 * Params: 935 * status = status the mutants must be in to be counted. 936 * distinc = count based on unique source code changes. 937 * kinds = the kind of mutants to count. 938 * file = file to count mutants in. 939 */ 940 private MetadataNoMutEntry countNoMutMutants(int[] status, bool distinct)( 941 const Mutation.Kind[] kinds, string file = null) @trusted { 942 static if (distinct) { 943 auto sql_base = " 944 SELECT count(*) 945 FROM ( 946 SELECT count(*) 947 FROM %s t0, %s t1,%s t4%s 948 WHERE 949 %s 950 t0.st_id = t1.id AND 951 t0.st_id = t4.st_id AND 952 t4.nomut != 0 AND 953 t1.status IN (%(%s,%)) AND 954 t0.kind IN (%(%s,%)) 955 GROUP BY t1.id)"; 956 } else { 957 auto sql_base = " 958 SELECT count(*) 959 FROM %s t0, %s t1,%s t4%s 960 WHERE 961 %s 962 t0.st_id = t1.id AND 963 t0.st_id = t4.st_id AND 964 t4.nomut != 0 AND 965 t1.status IN (%(%s,%)) AND 966 t0.kind IN (%(%s,%))"; 967 } 968 const query = () { 969 auto fq = file.length == 0 970 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND"; 971 auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3", 972 mutationPointTable, filesTable); 973 return format(sql_base, mutationTable, mutationStatusTable, 974 srcMetadataTable, fq_from, fq, status, kinds.map!(a => cast(int) a)); 975 }(); 976 977 typeof(return) rval; 978 auto stmt = db.prepare(query); 979 if (file.length != 0) 980 stmt.get.bind(":path", file); 981 auto res = stmt.get.execute; 982 if (!res.empty) 983 rval = MetadataNoMutEntry(res.front.peek!long(0)); 984 return rval; 985 } 986 987 /// ditto. 988 alias aliveNoMutSrcMutants = countNoMutMutants!([Mutation.Status.alive], true); 989 990 /// Returns: mutants killed by the test case. 991 MutationStatusId[] testCaseKilledSrcMutants(const Mutation.Kind[] kinds, TestCase tc) @trusted { 992 const query = format(" 993 SELECT t1.id 994 FROM %s t0, %s t1, %s t2, %s t3 995 WHERE 996 t0.st_id = t1.id AND 997 t1.status = :st AND 998 t0.kind IN (%(%s,%)) AND 999 t2.name = :name AND 1000 t2.id = t3.tc_id AND 1001 t3.st_id = t1.id 1002 GROUP BY t1.id", mutationTable, mutationStatusTable, 1003 allTestCaseTable, killedTestCaseTable, kinds.map!(a => cast(int) a)); 1004 1005 auto stmt = db.prepare(query); 1006 stmt.get.bind(":st", cast(long) Mutation.Status.killed); 1007 stmt.get.bind(":name", tc.name); 1008 1009 auto app = appender!(MutationStatusId[])(); 1010 foreach (res; stmt.get.execute) 1011 app.put(MutationStatusId(res.peek!long(0))); 1012 1013 return app.data; 1014 } 1015 1016 /// Returns: mutants at mutations points that the test case has killed mutants at. 1017 alias testCaseMutationPointAliveSrcMutants = testCaseCountSrcMutants!([ 1018 Mutation.Status.alive 1019 ]); 1020 /// ditto 1021 alias testCaseMutationPointTimeoutSrcMutants = testCaseCountSrcMutants!( 1022 [Mutation.Status.timeout]); 1023 /// ditto 1024 alias testCaseMutationPointKilledSrcMutants = testCaseCountSrcMutants!([ 1025 Mutation.Status.killed 1026 ]); 1027 /// ditto 1028 alias testCaseMutationPointUnknownSrcMutants = testCaseCountSrcMutants!( 1029 [Mutation.Status.unknown]); 1030 /// ditto 1031 alias testCaseMutationPointKilledByCompilerSrcMutants = testCaseCountSrcMutants!( 1032 [Mutation.Status.killedByCompiler]); 1033 /// ditto 1034 alias testCaseMutationPointTotalSrcMutants = testCaseCountSrcMutants!( 1035 [ 1036 Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout 1037 ]); 1038 1039 private MutationStatusId[] testCaseCountSrcMutants(int[] status)( 1040 const Mutation.Kind[] kinds, TestCase tc) @trusted { 1041 const query = format(" 1042 SELECT t1.id 1043 FROM %s t0, %s t1 1044 WHERE 1045 t0.mp_id IN (SELECT t1.id 1046 FROM %s t0,%s t1, %s t2, %s t3 1047 WHERE 1048 t0.mp_id = t1.id AND 1049 t2.name = :name AND 1050 t2.id = t3.tc_id AND 1051 t3.st_id = t0.st_id 1052 ) 1053 AND 1054 t0.st_id = t1.id AND 1055 t1.status IN (%(%s,%)) AND 1056 t0.kind IN (%(%s,%)) 1057 GROUP BY t1.id", mutationTable, mutationStatusTable, mutationTable, mutationPointTable, 1058 allTestCaseTable, killedTestCaseTable, status, kinds.map!(a => cast(int) a)); 1059 1060 auto stmt = db.prepare(query); 1061 stmt.get.bind(":name", tc.name); 1062 1063 auto app = appender!(MutationStatusId[])(); 1064 foreach (res; stmt.get.execute) 1065 app.put(MutationStatusId(res.peek!long(0))); 1066 1067 return app.data; 1068 } 1069 1070 Nullable!Checksum getChecksum(MutationStatusId id) @trusted { 1071 static immutable sql = format!"SELECT checksum0, checksum1 FROM %s WHERE id=:id"( 1072 mutationStatusTable); 1073 auto stmt = db.prepare(sql); 1074 stmt.get.bind(":id", id.get); 1075 1076 typeof(return) rval; 1077 foreach (res; stmt.get.execute) { 1078 rval = Checksum(res.peek!long(0), res.peek!long(1)); 1079 break; 1080 } 1081 return rval; 1082 } 1083 1084 /// Remove all metadata. 1085 void clearMetadata() { 1086 static immutable sql = format!"DELETE FROM %s"(rawSrcMetadataTable); 1087 db.run(sql); 1088 } 1089 1090 /** Save line metadata to the database which is used to associate line 1091 * metadata with mutants. 1092 */ 1093 void put(const LineMetadata[] mdata) { 1094 import sumtype; 1095 1096 if (mdata.empty) 1097 return; 1098 1099 // TODO: convert to microrm 1100 static immutable sql = format!"INSERT OR IGNORE INTO %s 1101 (file_id, line, nomut, tag, comment) 1102 VALUES(:fid, :line, :nomut, :tag, :comment)"( 1103 rawSrcMetadataTable); 1104 1105 auto stmt = db.prepare(sql); 1106 foreach (meta; mdata) { 1107 auto nomut = meta.attr.match!((NoMetadata a) => NoMut.init, (NoMut a) => a); 1108 stmt.get.bindAll(cast(long) meta.id, meta.line, meta.isNoMut, 1109 nomut.tag, nomut.comment); 1110 stmt.get.execute; 1111 stmt.get.reset; 1112 } 1113 } 1114 1115 /// Store all found mutants. 1116 void put(MutationPointEntry2[] mps, AbsolutePath root) @trusted { 1117 if (mps.empty) 1118 return; 1119 1120 static immutable insert_mp_sql = format("INSERT OR IGNORE INTO %s 1121 (file_id, offset_begin, offset_end, line, column, line_end, column_end) 1122 SELECT id,:begin,:end,:line,:column,:line_end,:column_end 1123 FROM %s 1124 WHERE 1125 path = :path", 1126 mutationPointTable, filesTable); 1127 auto mp_stmt = db.prepare(insert_mp_sql); 1128 1129 foreach (mp; mps) { 1130 auto rel_file = relativePath(mp.file, root).Path; 1131 mp_stmt.get.bind(":begin", mp.offset.begin); 1132 mp_stmt.get.bind(":end", mp.offset.end); 1133 mp_stmt.get.bind(":line", mp.sloc.line); 1134 mp_stmt.get.bind(":column", mp.sloc.column); 1135 mp_stmt.get.bind(":line_end", mp.slocEnd.line); 1136 mp_stmt.get.bind(":column_end", mp.slocEnd.column); 1137 mp_stmt.get.bind(":path", cast(string) rel_file); 1138 mp_stmt.get.execute; 1139 mp_stmt.get.reset; 1140 } 1141 1142 static immutable insert_cmut_sql = format("INSERT OR IGNORE INTO %s 1143 (status,exit_code,compile_time_ms,test_time_ms,update_ts,added_ts,checksum0,checksum1,prio) 1144 VALUES(:st,0,0,0,:update_ts,:added_ts,:c0,:c1,:prio)", 1145 mutationStatusTable); 1146 auto cmut_stmt = db.prepare(insert_cmut_sql); 1147 const ts = Clock.currTime.toSqliteDateTime; 1148 cmut_stmt.get.bind(":st", Mutation.Status.unknown); 1149 cmut_stmt.get.bind(":update_ts", ts); 1150 cmut_stmt.get.bind(":added_ts", ts); 1151 foreach (mp; mps) { 1152 const prio = (mp.offset.begin < mp.offset.end) ? mp.offset.end - mp.offset.begin : 0; 1153 foreach (cm; mp.cms) { 1154 cmut_stmt.get.bind(":c0", cast(long) cm.id.c0); 1155 cmut_stmt.get.bind(":c1", cast(long) cm.id.c1); 1156 cmut_stmt.get.bind(":prio", prio); 1157 cmut_stmt.get.execute; 1158 cmut_stmt.get.reset; 1159 } 1160 } 1161 1162 static immutable insert_m_sql = format("INSERT OR IGNORE INTO %s 1163 (mp_id, st_id, kind) 1164 SELECT t0.id,t1.id,:kind FROM %s t0, %s t1, %s t2 WHERE 1165 t2.path = :path AND 1166 t0.file_id = t2.id AND 1167 t0.offset_begin = :off_begin AND 1168 t0.offset_end = :off_end AND 1169 t1.checksum0 = :c0 AND 1170 t1.checksum1 = :c1", 1171 mutationTable, mutationPointTable, mutationStatusTable, filesTable); 1172 auto insert_m = db.prepare(insert_m_sql); 1173 1174 foreach (mp; mps) { 1175 foreach (m; mp.cms) { 1176 auto rel_file = relativePath(mp.file, root).Path; 1177 insert_m.get.bind(":path", cast(string) rel_file); 1178 insert_m.get.bind(":off_begin", mp.offset.begin); 1179 insert_m.get.bind(":off_end", mp.offset.end); 1180 insert_m.get.bind(":c0", cast(long) m.id.c0); 1181 insert_m.get.bind(":c1", cast(long) m.id.c1); 1182 insert_m.get.bind(":kind", m.mut.kind); 1183 insert_m.get.execute; 1184 insert_m.get.reset; 1185 } 1186 } 1187 } 1188 1189 /** Remove all mutants points from the database. 1190 * 1191 * This removes all the mutants because of the cascade delete of the 1192 * tables. But it will keep the mutation statuses and thus the checksums 1193 * and the status of the code changes. 1194 * 1195 * This then mean that when mutations+mutation points are added back they 1196 * may reconnect with a mutation status. 1197 */ 1198 void removeAllMutationPoints() @trusted { 1199 static immutable sql = format!"DELETE FROM %s"(mutationPointTable); 1200 db.run(sql); 1201 } 1202 1203 /// ditto 1204 void removeAllFiles() @trusted { 1205 static immutable sql = format!"DELETE FROM %s"(filesTable); 1206 db.run(sql); 1207 } 1208 1209 /// Remove mutants that have no connection to a mutation point, orphaned mutants. 1210 void removeOrphanedMutants() @trusted { 1211 import std.datetime.stopwatch : StopWatch, AutoStart; 1212 1213 const removeIds = () { 1214 static immutable sql = format!"SELECT id FROM %1$s WHERE id NOT IN (SELECT st_id FROM %2$s)"( 1215 mutationStatusTable, mutationTable); 1216 auto stmt = db.prepare(sql); 1217 auto removeIds = appender!(long[])(); 1218 foreach (res; stmt.get.execute) 1219 removeIds.put(res.peek!long(0)); 1220 return removeIds.data; 1221 }(); 1222 1223 immutable batchNr = 1000; 1224 static immutable sql = format!"DELETE FROM %1$s WHERE id=:id"(mutationStatusTable); 1225 auto stmt = db.prepare(sql); 1226 auto sw = StopWatch(AutoStart.yes); 1227 foreach (const i, const id; removeIds) { 1228 stmt.get.bind(":id", id); 1229 stmt.get.execute; 1230 stmt.get.reset; 1231 1232 // continuously print to inform the user of the progress and avoid 1233 // e.g. timeout on jenkins. 1234 if (i > 0 && i % batchNr == 0) { 1235 const avg = cast(long)(cast(double) sw.peek.total!"msecs" / cast(double) batchNr); 1236 const t = dur!"msecs"(avg * (removeIds.length - i)); 1237 logger.infof("%s/%s removed (average %sms) (%s) (%s)", i, 1238 removeIds.length, avg, t, (Clock.currTime + t).toSimpleString); 1239 sw.reset; 1240 } 1241 } 1242 1243 logger.infof(!removeIds.empty, "%1$s/%1$s removed", removeIds.length); 1244 } 1245 1246 /** Add a link between the mutation and what test case killed it. 1247 * 1248 * Params: 1249 * id = ? 1250 * tcs = test cases to add 1251 */ 1252 void updateMutationTestCases(const MutationId id, const(TestCase)[] tcs) @trusted { 1253 if (tcs.length == 0) 1254 return; 1255 1256 immutable statusId = () { 1257 static immutable st_id_for_mutation_q = format!"SELECT st_id FROM %s WHERE id=:id"( 1258 mutationTable); 1259 auto stmt = db.prepare(st_id_for_mutation_q); 1260 stmt.get.bind(":id", cast(long) id); 1261 return stmt.get.execute.oneValue!long; 1262 }(); 1263 updateMutationTestCases(MutationStatusId(statusId), tcs); 1264 } 1265 1266 /** Add a link between the mutation and what test case killed it. 1267 * 1268 * Params: 1269 * id = ? 1270 * tcs = test cases to add 1271 */ 1272 void updateMutationTestCases(const MutationStatusId statusId, const(TestCase)[] tcs) @trusted { 1273 if (tcs.length == 0) 1274 return; 1275 1276 try { 1277 static immutable remove_old_sql = format!"DELETE FROM %s WHERE st_id=:id"( 1278 killedTestCaseTable); 1279 auto stmt = db.prepare(remove_old_sql); 1280 stmt.get.bind(":id", statusId.get); 1281 stmt.get.execute; 1282 } catch (Exception e) { 1283 } 1284 1285 static immutable add_if_non_exist_tc_sql = format!"INSERT INTO %s (name) SELECT :name1 WHERE NOT EXISTS (SELECT * FROM %s WHERE name = :name2)"( 1286 allTestCaseTable, allTestCaseTable); 1287 auto stmt_insert_tc = db.prepare(add_if_non_exist_tc_sql); 1288 1289 static immutable add_new_sql = format!"INSERT OR IGNORE INTO %s (st_id, tc_id, location) SELECT :st_id,t1.id,:loc FROM %s t1 WHERE t1.name = :tc"( 1290 killedTestCaseTable, allTestCaseTable); 1291 auto stmt_insert = db.prepare(add_new_sql); 1292 foreach (const tc; tcs) { 1293 try { 1294 stmt_insert_tc.get.reset; 1295 stmt_insert_tc.get.bind(":name1", tc.name); 1296 stmt_insert_tc.get.bind(":name2", tc.name); 1297 stmt_insert_tc.get.execute; 1298 1299 stmt_insert.get.reset; 1300 stmt_insert.get.bind(":st_id", statusId.get); 1301 stmt_insert.get.bind(":loc", tc.location); 1302 stmt_insert.get.bind(":tc", tc.name); 1303 stmt_insert.get.execute; 1304 } catch (Exception e) { 1305 logger.warning(e.msg); 1306 } 1307 } 1308 } 1309 1310 /** Set detected test cases. 1311 * 1312 * This will replace those that where previously stored. 1313 * 1314 * Returns: ID of affected mutation statuses. 1315 */ 1316 MutationStatusId[] setDetectedTestCases(const(TestCase)[] tcs) @trusted { 1317 if (tcs.length == 0) 1318 return null; 1319 1320 auto ids = appender!(MutationStatusId[])(); 1321 1322 static immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string; 1323 internalAddDetectedTestCases(tcs, tmp_name); 1324 1325 static immutable mut_st_id = format!"SELECT DISTINCT t1.st_id 1326 FROM %s t0, %s t1 1327 WHERE 1328 t0.name NOT IN (SELECT name FROM %s) AND 1329 t0.id = t1.tc_id"(allTestCaseTable, 1330 killedTestCaseTable, tmp_name); 1331 auto stmt = db.prepare(mut_st_id); 1332 foreach (res; stmt.get.execute) { 1333 ids.put(res.peek!long(0).MutationStatusId); 1334 } 1335 1336 static immutable remove_old_sql = format!"DELETE FROM %s WHERE name NOT IN (SELECT name FROM %s)"( 1337 allTestCaseTable, tmp_name); 1338 db.run(remove_old_sql); 1339 1340 db.run(format!"DROP TABLE %s"(tmp_name)); 1341 1342 return ids.data; 1343 } 1344 1345 /** Add test cases to those that have been detected. 1346 * 1347 * They will be added if they are unique. 1348 */ 1349 void addDetectedTestCases(const(TestCase)[] tcs) @trusted { 1350 if (tcs.length == 0) 1351 return; 1352 1353 static immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string; 1354 internalAddDetectedTestCases(tcs, tmp_name); 1355 db.run(format!"DROP TABLE %s"(tmp_name)); 1356 } 1357 1358 /// ditto. 1359 private void internalAddDetectedTestCases(const(TestCase)[] tcs, string tmp_tbl) @trusted { 1360 db.run(format!"CREATE TEMP TABLE %s (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"( 1361 tmp_tbl)); 1362 1363 const add_tc_sql = format!"INSERT OR IGNORE INTO %s (name) VALUES(:name)"(tmp_tbl); 1364 auto insert_s = db.prepare(add_tc_sql); 1365 foreach (tc; tcs.filter!(a => !a.name.empty)) { 1366 insert_s.get.bind(":name", tc.name); 1367 insert_s.get.execute; 1368 insert_s.get.reset; 1369 } 1370 1371 // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table 1372 //Q: What is happening here? 1373 // 1374 //A: Conceptually, we select all rows from table1 and for each row we 1375 //attempt to find a row in table2 with the same value for the name 1376 //column. If there is no such row, we just leave the table2 portion of 1377 //our result empty for that row. Then we constrain our selection by 1378 //picking only those rows in the result where the matching row does not 1379 //exist. Finally, We ignore all fields from our result except for the 1380 //name column (the one we are sure that exists, from table1). 1381 // 1382 //While it may not be the most performant method possible in all cases, 1383 //it should work in basically every database engine ever that attempts 1384 //to implement ANSI 92 SQL 1385 const add_missing_sql = format!"INSERT INTO %s (name) SELECT t1.name FROM %s t1 LEFT JOIN %s t2 ON t2.name = t1.name WHERE t2.name IS NULL"( 1386 allTestCaseTable, tmp_tbl, allTestCaseTable); 1387 db.run(add_missing_sql); 1388 } 1389 1390 /// Returns: detected test cases. 1391 TestCase[] getDetectedTestCases() @trusted { 1392 auto rval = appender!(TestCase[])(); 1393 db.run(select!AllTestCaseTbl).map!(a => TestCase(a.name)).copy(rval); 1394 return rval.data; 1395 } 1396 1397 /// Returns: detected test cases. 1398 TestCaseId[] getDetectedTestCaseIds() @trusted { 1399 auto rval = appender!(TestCaseId[])(); 1400 db.run(select!AllTestCaseTbl).map!(a => TestCaseId(a.id)).copy(rval); 1401 return rval.data; 1402 } 1403 1404 /// Returns: test cases that has killed zero mutants. 1405 TestCase[] getTestCasesWithZeroKills() @trusted { 1406 static immutable sql = format("SELECT t1.name FROM %s t1 WHERE t1.id NOT IN (SELECT tc_id FROM %s)", 1407 allTestCaseTable, killedTestCaseTable); 1408 1409 auto rval = appender!(TestCase[])(); 1410 auto stmt = db.prepare(sql); 1411 foreach (a; stmt.get.execute) 1412 rval.put(TestCase(a.peek!string(0))); 1413 1414 return rval.data; 1415 } 1416 1417 /** Guarantees that the there are no duplications of `TestCaseId`. 1418 * 1419 * Returns: test cases that has killed at least one mutant. 1420 */ 1421 TestCaseId[] getTestCasesWithAtLeastOneKill(const Mutation.Kind[] kinds) @trusted { 1422 const sql = format!"SELECT DISTINCT t1.id 1423 FROM %s t1, %s t2, %s t3 1424 WHERE 1425 t1.id = t2.tc_id AND 1426 t2.st_id == t3.st_id AND 1427 t3.kind IN (%(%s,%))"(allTestCaseTable, 1428 killedTestCaseTable, mutationTable, kinds.map!(a => cast(int) a)); 1429 1430 auto rval = appender!(TestCaseId[])(); 1431 auto stmt = db.prepare(sql); 1432 foreach (a; stmt.get.execute) 1433 rval.put(TestCaseId(a.peek!long(0))); 1434 1435 return rval.data; 1436 } 1437 1438 /// Returns: the name of the test case. 1439 string getTestCaseName(const TestCaseId id) @trusted { 1440 static immutable sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable); 1441 auto stmt = db.prepare(sql); 1442 stmt.get.bind(":id", cast(long) id); 1443 auto res = stmt.get.execute; 1444 return res.oneValue!string; 1445 } 1446 1447 /// Returns: stats about the test case. 1448 Nullable!TestCaseInfo getTestCaseInfo(const TestCase tc, const Mutation.Kind[] kinds) @trusted { 1449 const sql = format("SELECT sum(ctime),sum(ttime),count(*) 1450 FROM ( 1451 SELECT sum(t2.compile_time_ms) ctime,sum(t2.test_time_ms) ttime 1452 FROM %s t0, %s t1, %s t2, %s t3 1453 WHERE 1454 t0.name = :name AND 1455 t0.id = t1.tc_id AND 1456 t1.st_id = t2.id AND 1457 t1.st_id = t3.st_id AND 1458 t3.kind IN (%(%s,%)) 1459 GROUP BY t1.st_id)", allTestCaseTable, 1460 killedTestCaseTable, mutationStatusTable, mutationTable, 1461 kinds.map!(a => cast(int) a)); 1462 auto stmt = db.prepare(sql); 1463 stmt.get.bind(":name", tc.name); 1464 1465 typeof(return) rval; 1466 foreach (a; stmt.get.execute) { 1467 rval = TestCaseInfo(MutantTimeProfile(a.peek!long(0).dur!"msecs", 1468 a.peek!long(1).dur!"msecs"), a.peek!long(2)); 1469 } 1470 return rval; 1471 } 1472 1473 /// Returns: all test cases for the file and the mutants they killed. 1474 TestCaseInfo2[] getAllTestCaseInfo2(const FileId file, const Mutation.Kind[] kinds) @trusted { 1475 // row of test case name and mutation id. 1476 const sql = format("SELECT t0.name,t3.id 1477 FROM %s t0, %s t1, %s t2, %s t3, %s t4 1478 WHERE 1479 t0.id = t1.tc_id AND 1480 t1.st_id = t2.id AND 1481 t2.id = t3.st_id AND 1482 t4.id = :file_id AND 1483 t3.kind IN (%(%s,%))", allTestCaseTable, killedTestCaseTable, 1484 mutationStatusTable, mutationTable, filesTable, kinds.map!(a => cast(int) a)); 1485 auto stmt = db.prepare(sql); 1486 stmt.get.bind(":file_id", cast(long) file); 1487 1488 MutationId[][string] data; 1489 foreach (row; stmt.get.execute) { 1490 const name = row.peek!string(0); 1491 if (auto v = name in data) { 1492 *v ~= MutationId(row.peek!long(1)); 1493 } else { 1494 data[name] = [MutationId(row.peek!long(1))]; 1495 } 1496 } 1497 1498 auto app = appender!(TestCaseInfo2[])(); 1499 data.byKeyValue.map!(a => TestCaseInfo2(TestCase(a.key), a.value)).copy(app); 1500 return app.data; 1501 } 1502 1503 /// Returns: the test case. 1504 Nullable!TestCase getTestCase(const TestCaseId id) @trusted { 1505 static immutable sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable); 1506 auto stmt = db.prepare(sql); 1507 stmt.get.bind(":id", cast(long) id); 1508 1509 typeof(return) rval; 1510 foreach (res; stmt.get.execute) { 1511 rval = TestCase(res.peek!string(0)); 1512 } 1513 return rval; 1514 } 1515 1516 /// Returns: the test case id. 1517 Nullable!TestCaseId getTestCaseId(const TestCase tc) @trusted { 1518 static immutable sql = format!"SELECT id FROM %s WHERE name = :name"(allTestCaseTable); 1519 auto stmt = db.prepare(sql); 1520 stmt.get.bind(":name", tc.name); 1521 1522 typeof(return) rval; 1523 foreach (res; stmt.get.execute) { 1524 rval = TestCaseId(res.peek!long(0)); 1525 } 1526 return rval; 1527 } 1528 1529 /// The mutation ids are guaranteed to be sorted. 1530 /// Returns: the mutants the test case killed. 1531 MutationId[] getTestCaseMutantKills(const TestCaseId id, const Mutation.Kind[] kinds) @trusted { 1532 immutable sql = format!"SELECT t2.id 1533 FROM %s t1, %s t2 1534 WHERE 1535 t1.tc_id = :tid AND 1536 t1.st_id = t2.st_id AND 1537 t2.kind IN (%(%s,%)) 1538 GROUP BY t2.st_id 1539 ORDER BY t2.id"(killedTestCaseTable, 1540 mutationTable, kinds.map!(a => cast(int) a)); 1541 1542 auto rval = appender!(MutationId[])(); 1543 auto stmt = db.prepare(sql); 1544 stmt.get.bind(":tid", cast(long) id); 1545 foreach (a; stmt.get.execute) 1546 rval.put(MutationId(a.peek!long(0))); 1547 1548 return rval.data; 1549 } 1550 1551 /// Returns: test cases that killed the mutant. 1552 TestCase[] getTestCases(const MutationId id) @trusted { 1553 Appender!(TestCase[]) rval; 1554 1555 static immutable get_test_cases_sql = format!"SELECT t1.name,t2.location 1556 FROM %s t1, %s t2, %s t3 1557 WHERE 1558 t3.id = :id AND 1559 t3.st_id = t2.st_id AND 1560 t2.tc_id = t1.id"( 1561 allTestCaseTable, killedTestCaseTable, mutationTable); 1562 auto stmt = db.prepare(get_test_cases_sql); 1563 stmt.get.bind(":id", cast(long) id); 1564 foreach (a; stmt.get.execute) 1565 rval.put(TestCase(a.peek!string(0), a.peek!string(1))); 1566 1567 return rval.data; 1568 } 1569 1570 /// Returns: if the mutant have any test cases recorded that killed it 1571 bool hasTestCases(const MutationStatusId id) @trusted { 1572 static immutable sql = format!"SELECT count(*) FROM %s t0 WHERE t0.st_id = :id"( 1573 killedTestCaseTable); 1574 auto stmt = db.prepare(sql); 1575 stmt.get.bind(":id", id.get); 1576 foreach (a; stmt.get.execute) { 1577 return a.peek!long(0) != 0; 1578 } 1579 return false; 1580 } 1581 1582 /** Returns: number of test cases 1583 */ 1584 long getNumOfTestCases() @trusted { 1585 static immutable num_test_cases_sql = format!"SELECT count(*) FROM %s"(allTestCaseTable); 1586 return db.execute(num_test_cases_sql).oneValue!long; 1587 } 1588 1589 /** Returns: test cases that killed other mutants at the same mutation point as `id`. 1590 */ 1591 TestCase[] getSurroundingTestCases(const MutationId id) @trusted { 1592 Appender!(TestCase[]) rval; 1593 1594 // get the mutation point ID that id reside at 1595 long mp_id; 1596 { 1597 auto stmt = db.prepare(format!"SELECT mp_id FROM %s WHERE id=:id"(mutationTable)); 1598 stmt.get.bind(":id", cast(long) id); 1599 auto res = stmt.get.execute; 1600 if (res.empty) 1601 return null; 1602 mp_id = res.oneValue!long; 1603 } 1604 1605 // get all the test cases that are killed at the mutation point 1606 static immutable get_test_cases_sql = format!"SELECT t2.name,t1.location FROM %s t1,%s t2 WHERE t1.tc_id == t2.id AND t0.st_id IN (SELECT st_id FROM %s WHERE mp_id=:id)"( 1607 killedTestCaseTable, allTestCaseTable, mutationTable); 1608 auto stmt = db.prepare(get_test_cases_sql); 1609 stmt.get.bind(":id", mp_id); 1610 foreach (a; stmt.get.execute) { 1611 rval.put(TestCase(a.peek!string(0), a.peek!string(1))); 1612 } 1613 1614 return rval.data; 1615 } 1616 1617 void removeTestCase(const TestCaseId id) @trusted { 1618 auto stmt = db.prepare(format!"DELETE FROM %s WHERE id=:id"(allTestCaseTable)); 1619 stmt.get.bind(":id", cast(long) id); 1620 stmt.get.execute; 1621 } 1622 1623 /// Change the status of all mutants that the test case has killed to unknown. 1624 void resetTestCaseId(const TestCaseId id) @trusted { 1625 { 1626 static immutable sql = format!"UPDATE %1$s SET status=0 WHERE id IN (SELECT t1.id FROM %2$s t0, %1$s t1 WHERE t0.tc_id = :id AND t0.st_id = t1.id)"( 1627 mutationStatusTable, killedTestCaseTable); 1628 auto stmt = db.prepare(sql); 1629 stmt.get.bind(":id", cast(long) id); 1630 stmt.get.execute; 1631 } 1632 { 1633 static immutable sql2 = format!"DELETE FROM %1$s WHERE tc_id = :id"(killedTestCaseTable); 1634 auto stmt = db.prepare(sql2); 1635 stmt.get.bind(":id", cast(long) id); 1636 stmt.get.execute; 1637 } 1638 } 1639 1640 /// Returns: the context for the timeout algorithm. 1641 MutantTimeoutCtx getMutantTimeoutCtx() @trusted { 1642 foreach (res; db.run(select!MutantTimeoutCtx)) 1643 return res; 1644 return MutantTimeoutCtx.init; 1645 } 1646 1647 void putMutantTimeoutCtx(const MutantTimeoutCtx ctx) @trusted { 1648 db.run(delete_!MutantTimeoutCtx); 1649 db.run(insert!MutantTimeoutCtx.insert, ctx); 1650 } 1651 1652 void putMutantInTimeoutWorklist(const MutationStatusId id) @trusted { 1653 const sql = format!"INSERT OR IGNORE INTO %s (id) VALUES (:id)"(mutantTimeoutWorklistTable); 1654 auto stmt = db.prepare(sql); 1655 stmt.get.bind(":id", id.get); 1656 stmt.get.execute; 1657 } 1658 1659 /** Remove all mutants that are in the worklist that do NOT have the 1660 * mutation status timeout. 1661 */ 1662 void reduceMutantTimeoutWorklist() @trusted { 1663 static immutable sql = format!"DELETE FROM %1$s 1664 WHERE 1665 id IN (SELECT id FROM %2$s WHERE status != :status)"( 1666 mutantTimeoutWorklistTable, mutationStatusTable); 1667 auto stmt = db.prepare(sql); 1668 stmt.get.bind(":status", cast(ubyte) Mutation.Status.timeout); 1669 stmt.get.execute; 1670 } 1671 1672 /// Remove all mutants from the worklist. 1673 void clearMutantTimeoutWorklist() @trusted { 1674 static immutable sql = format!"DELETE FROM %1$s"(mutantTimeoutWorklistTable); 1675 db.run(sql); 1676 } 1677 1678 /// Returns: the number of mutants in the worklist. 1679 long countMutantTimeoutWorklist() @trusted { 1680 static immutable sql = format!"SELECT count(*) FROM %1$s"(mutantTimeoutWorklistTable); 1681 auto stmt = db.prepare(sql); 1682 auto res = stmt.get.execute(); 1683 return res.oneValue!long; 1684 } 1685 1686 /// Changes the status of mutants in the timeout worklist to unknown. 1687 void resetMutantTimeoutWorklist(Mutation.Status toStatus) @trusted { 1688 static immutable sql = format!"UPDATE %1$s SET status=:st WHERE id IN (SELECT id FROM %2$s)"( 1689 mutationStatusTable, mutantTimeoutWorklistTable); 1690 auto stmt = db.prepare(sql); 1691 stmt.get.bind(":st", cast(ubyte) toStatus); 1692 stmt.get.execute; 1693 } 1694 1695 /// Copy the timeout mutants to the worklist of mutants to test. 1696 void copyMutantTimeoutWorklist(const long prio = 100) @trusted { 1697 immutable sql = format!"INSERT OR IGNORE INTO %1$s (id,prio) 1698 SELECT id,%3$s FROM %2$s"(mutantWorklistTable, 1699 mutantTimeoutWorklistTable, prio); 1700 auto stmt = db.prepare(sql); 1701 stmt.get.execute; 1702 } 1703 1704 /** Update the content of metadata tables with what has been added to the 1705 * raw table data. 1706 */ 1707 void updateMetadata() @trusted { 1708 db.run(format!"DELETE FROM %s"(srcMetadataTable)); 1709 db.run(format!"DELETE FROM %s"(nomutTable)); 1710 db.run(format!"DELETE FROM %s"(nomutDataTable)); 1711 1712 static immutable nomut_tbl = "INSERT INTO %s 1713 SELECT 1714 t0.id mp_id, 1715 t1.line line, 1716 count(*) status 1717 FROM %s t0, %s t1 1718 WHERE 1719 t0.file_id = t1.file_id AND 1720 (t1.line BETWEEN t0.line AND t0.line_end) 1721 GROUP BY 1722 t0.id"; 1723 db.run(format!nomut_tbl(nomutTable, mutationPointTable, rawSrcMetadataTable)); 1724 1725 static immutable src_metadata_sql = "INSERT INTO %s 1726 SELECT DISTINCT 1727 t0.id AS mut_id, 1728 t1.id AS st_id, 1729 t2.id AS mp_id, 1730 t3.id AS file_id, 1731 (SELECT count(*) FROM %s WHERE nomut.mp_id = t2.id) as nomut 1732 FROM %s t0, %s t1, %s t2, %s t3 1733 WHERE 1734 t0.mp_id = t2.id AND 1735 t0.st_id = t1.id AND 1736 t2.file_id = t3.id"; 1737 db.run(format!src_metadata_sql(srcMetadataTable, nomutTable, 1738 mutationTable, mutationStatusTable, mutationPointTable, filesTable)); 1739 1740 static immutable nomut_data_tbl = "INSERT INTO %s 1741 SELECT 1742 t0.id as mut_id, 1743 t0.mp_id as mp_id, 1744 t1.line as line, 1745 t1.tag as tag, 1746 t1.comment as comment 1747 FROM %s t0, %s t1, %s t2 1748 WHERE 1749 t0.mp_id = t2.mp_id AND 1750 t1.line = t2.line"; 1751 db.run(format!nomut_data_tbl(nomutDataTable, mutationTable, 1752 rawSrcMetadataTable, nomutTable)); 1753 } 1754 1755 /// Returns: all schematas excluding those that are known to not be 1756 /// possible to compile. 1757 SchemataId[] getSchematas() @trusted { 1758 static immutable sql = format!"SELECT t0.id 1759 FROM %1$s t0 1760 WHERE 1761 t0.id NOT IN (SELECT id FROM %2$s)"( 1762 schemataTable, schemataUsedTable); 1763 auto stmt = db.prepare(sql); 1764 auto app = appender!(SchemataId[])(); 1765 foreach (a; stmt.get.execute) { 1766 app.put(SchemataId(a.peek!long(0))); 1767 } 1768 return app.data; 1769 } 1770 1771 Nullable!Schemata getSchemata(SchemataId id) @trusted { 1772 import std.zlib : uncompress; 1773 1774 static immutable sql = format!"SELECT 1775 t1.path, t0.text, t0.offset_begin, t0.offset_end 1776 FROM %1$s t0, %2$s t1 1777 WHERE 1778 t0.schem_id = :id AND 1779 t0.file_id = t1.id 1780 ORDER BY t0.order_ ASC 1781 "(schemataFragmentTable, filesTable); 1782 1783 typeof(return) rval; 1784 auto stmt = db.prepare(sql); 1785 stmt.get.bind(":id", cast(long) id); 1786 1787 auto app = appender!(SchemataFragment[])(); 1788 foreach (a; stmt.get.execute) { 1789 auto raw = a.peek!(ubyte[])(1); 1790 auto offset = Offset(a.peek!uint(2), a.peek!uint(3)); 1791 app.put(SchemataFragment(a.peek!string(0).Path, offset, 1792 cast(const(ubyte)[]) uncompress(raw, offset.end - offset.begin))); 1793 } 1794 1795 if (!app.data.empty) { 1796 rval = Schemata(SchemataId(id), app.data); 1797 } 1798 1799 return rval; 1800 } 1801 1802 /// Returns: number of mutants in a schemata that are marked for testing. 1803 long schemataMutantsCount(const SchemataId id, const Mutation.Kind[] kinds) @trusted { 1804 const sql = format!"SELECT count(*) 1805 FROM %s t1, %s t2, %s t3, %s t4 1806 WHERE 1807 t1.schem_id = :id AND 1808 t1.st_id = t2.id AND 1809 t3.st_id = t1.st_id AND 1810 t2.id = t4.id AND 1811 t3.kind IN (%(%s,%)) 1812 "(schemataMutantTable, mutationStatusTable, 1813 mutationTable, mutantWorklistTable, kinds.map!(a => cast(int) a)); 1814 1815 auto stmt = db.prepare(sql); 1816 stmt.get.bind(":id", id.get); 1817 return stmt.get.execute.oneValue!long; 1818 } 1819 1820 MutationStatusId[] getSchemataMutants(const SchemataId id, const Mutation.Kind[] kinds) @trusted { 1821 // TODO: DISTINCT should not be needed. Instead use e.g. a constraint on the table or something 1822 immutable sql = format!"SELECT DISTINCT t1.st_id 1823 FROM %s t1, %s t2, %s t3, %s t4 1824 WHERE 1825 t1.schem_id = :id AND 1826 t1.st_id = t2.id AND 1827 t3.st_id = t1.st_id AND 1828 t2.id = t4.id AND 1829 t3.kind IN (%(%s,%)) 1830 "(schemataMutantTable, mutationStatusTable, 1831 mutationTable, mutantWorklistTable, kinds.map!(a => cast(int) a)); 1832 auto stmt = db.prepare(sql); 1833 stmt.get.bind(":id", id.get); 1834 1835 auto app = appender!(MutationStatusId[])(); 1836 foreach (a; stmt.get.execute) { 1837 app.put(a.peek!long(0).MutationStatusId); 1838 } 1839 1840 return app.data; 1841 } 1842 1843 /// Returns: the kind of mutants a schemata contains. 1844 Mutation.Kind[] getSchemataKinds(const SchemataId id) @trusted { 1845 static immutable sql = format!"SELECT DISTINCT t1.kind 1846 FROM %1$s t0, %2$s t1 1847 WHERE 1848 t0.schem_id = :id AND 1849 t0.st_id = t1.st_id 1850 "(schemataMutantTable, mutationTable); 1851 auto stmt = db.prepare(sql); 1852 stmt.get.bind(":id", cast(long) id); 1853 1854 auto app = appender!(Mutation.Kind[])(); 1855 foreach (a; stmt.get.execute) { 1856 app.put(a.peek!long(0).to!(Mutation.Kind)); 1857 } 1858 1859 return app.data; 1860 } 1861 1862 /// Mark a schemata as used. 1863 void markUsed(const SchemataId id) @trusted { 1864 static immutable sql = format!"INSERT OR IGNORE INTO %1$s VALUES(:id)"(schemataUsedTable); 1865 auto stmt = db.prepare(sql); 1866 stmt.get.bind(":id", cast(long) id); 1867 stmt.get.execute; 1868 } 1869 1870 /// Create a schemata from a bundle of fragments. 1871 Nullable!SchemataId putSchemata(SchemataChecksum cs, 1872 const SchemataFragment[] fragments, MutationStatusId[] mutants) @trusted { 1873 import std.range : enumerate; 1874 import std.zlib : compress; 1875 1876 const schemId = cast(long) cs.value.c0; 1877 1878 const exists = () { 1879 static immutable sql = format!"SELECT count(*) FROM %1$s WHERE id=:id"(schemataTable); 1880 auto stmt = db.prepare(sql); 1881 stmt.get.bind(":id", schemId); 1882 return stmt.get.execute.oneValue!long != 0; 1883 1884 }(); 1885 1886 if (exists) 1887 return typeof(return)(); 1888 1889 { 1890 static immutable sql = format!"INSERT INTO %1$s VALUES(:id, :nr)"(schemataTable); 1891 auto stmt = db.prepare(sql); 1892 stmt.get.bind(":id", cast(long) cs.value.c0); 1893 stmt.get.bind(":nr", cast(long) fragments.length); 1894 stmt.get.execute; 1895 } 1896 1897 foreach (f; fragments.enumerate) { 1898 const fileId = getFileId(f.value.file); 1899 if (fileId.isNull) { 1900 logger.warningf("Unable to add schemata fragment for file %s because it doesn't exist", 1901 f.value.file); 1902 continue; 1903 } 1904 1905 db.run(insert!SchemataFragmentTable, SchemataFragmentTable(0, 1906 schemId, cast(long) fileId.get, f.index, 1907 compress(f.value.text), f.value.offset.begin, f.value.offset.end)); 1908 } 1909 1910 // relate mutants to this schemata. 1911 db.run(insertOrIgnore!SchemataMutantTable, 1912 mutants.map!(a => SchemataMutantTable(cast(long) a, schemId))); 1913 1914 return typeof(return)(schemId.SchemataId); 1915 } 1916 1917 /// Prunes the database of schemas that where created by an older version. 1918 void deleteAllSchemas() @trusted { 1919 db.run(delete_!SchemataTable); 1920 } 1921 1922 /// Prunes the database of schemas that are unusable. 1923 void pruneSchemas() @trusted { 1924 auto remove = () { 1925 auto remove = appender!(long[])(); 1926 1927 // remove those that have lost some fragments 1928 static immutable sqlFragment = format!"SELECT t0.id 1929 FROM 1930 %1$s t0, 1931 (SELECT schem_id id,count(*) fragments FROM %2$s GROUP BY schem_id) t1 1932 WHERE 1933 t0.id = t1.id AND 1934 t0.fragments != t1.fragments 1935 "(schemataTable, 1936 schemataFragmentTable); 1937 auto stmt = db.prepare(sqlFragment); 1938 foreach (a; stmt.get.execute) { 1939 remove.put(a.peek!long(0)); 1940 } 1941 1942 // remove those that have lost all fragments 1943 static immutable sqlNoFragment = format!"SELECT t0.id FROM %1$s t0 WHERE t0.id NOT IN (SELECT schem_id FROM %2$s)"( 1944 schemataTable, schemataFragmentTable); 1945 stmt = db.prepare(sqlNoFragment); 1946 foreach (a; stmt.get.execute) { 1947 remove.put(a.peek!long(0)); 1948 } 1949 1950 return remove.data; 1951 }(); 1952 1953 static immutable sql = format!"DELETE FROM %1$s WHERE id=:id"(schemataTable); 1954 auto stmt = db.prepare(sql); 1955 foreach (a; remove) { 1956 stmt.get.bind(":id", a); 1957 stmt.get.execute; 1958 stmt.get.reset; 1959 } 1960 } 1961 1962 /** Removes all schemas that either do not compile or have zero mutants. 1963 * 1964 * Returns: number of schemas removed. 1965 */ 1966 long pruneUsedSchemas() @trusted { 1967 auto remove = () { 1968 auto remove = appender!(long[])(); 1969 1970 static immutable sqlUsed = format!"SELECT id FROM %1$s"(schemataUsedTable); 1971 auto stmt = db.prepare(sqlUsed); 1972 foreach (a; stmt.get.execute) { 1973 remove.put(a.peek!long(0)); 1974 } 1975 return remove.data; 1976 }(); 1977 1978 static immutable sql = format!"DELETE FROM %1$s WHERE id=:id"(schemataTable); 1979 auto stmt = db.prepare(sql); 1980 foreach (a; remove) { 1981 stmt.get.bind(":id", a); 1982 stmt.get.execute; 1983 stmt.get.reset; 1984 } 1985 1986 return remove.length; 1987 } 1988 1989 /// Compact the database by running a VACUUM operation 1990 void vacuum() @trusted { 1991 db.run("VACUUM"); 1992 } 1993 1994 /// Returns: the stored runtimes in ascending order by their `timeStamp`. 1995 TestCmdRuntime[] getTestCmdRuntimes() @trusted { 1996 import std.algorithm : sort; 1997 1998 auto app = appender!(TestCmdRuntime[])(); 1999 foreach (r; db.run(select!RuntimeHistoryTable)) { 2000 app.put(TestCmdRuntime(r.timeStamp, r.timeMs.dur!"msecs")); 2001 } 2002 2003 return app.data.sort!((a, b) => a.timeStamp < b.timeStamp).array; 2004 } 2005 2006 /// Drop all currently stored runtimes and replaces with `runtime`. 2007 void setTestCmdRuntimes(const TestCmdRuntime[] runtimes) @trusted { 2008 import std.range : enumerate; 2009 2010 db.run(format!"DELETE FROM %s"(runtimeHistoryTable)); 2011 db.run(insertOrReplace!RuntimeHistoryTable, 2012 runtimes.enumerate.map!(a => RuntimeHistoryTable(a.index, 2013 a.value.timeStamp, a.value.runtime.total!"msecs"))); 2014 } 2015 2016 /// Returns: the stored scores in ascending order by their `time`. 2017 MutationScore[] getMutationScoreHistory() @trusted { 2018 import std.algorithm : sort; 2019 2020 auto app = appender!(MutationScore[])(); 2021 foreach (r; db.run(select!MutationScoreHistoryTable)) { 2022 app.put(MutationScore(r.timeStamp, typeof(MutationScore.score)(r.score))); 2023 } 2024 2025 return app.data.sort!((a, b) => a.timeStamp < b.timeStamp).array; 2026 } 2027 2028 /// Add a mutation score to the history table. 2029 void putMutationScore(const MutationScore score) @trusted { 2030 db.run(insert!MutationScoreHistoryTable, MutationScoreHistoryTable(0, 2031 score.timeStamp, score.score.get)); 2032 } 2033 2034 /// Trim the mutation score history table to only contain the last `keep` scores. 2035 void trimMutationScore(const long keep) @trusted { 2036 auto stmt = db.prepare(format!"SELECT count(*) FROM %s"(mutationScoreHistoryTable)); 2037 const sz = stmt.get.execute.oneValue!long; 2038 2039 if (sz < keep) { 2040 return; 2041 } 2042 2043 auto ids = appender!(long[])(); 2044 stmt = db.prepare(format!"SELECT t0.id FROM t0 %s ORDER BY t0.time ASC LIMIT :limit"( 2045 mutationScoreHistoryTable)); 2046 stmt.get.bind(":limit", sz - keep); 2047 foreach (a; stmt.get.execute) { 2048 ids.put(a.peek!long(0)); 2049 } 2050 2051 stmt = db.prepare(format!"DELETE FROM %s WHERE id = :id"(mutationScoreHistoryTable)); 2052 foreach (a; ids.data) { 2053 stmt.get.bind(":id", a); 2054 stmt.get.execute; 2055 stmt.get.reset; 2056 } 2057 } 2058 2059 /// Add coverage regions. 2060 void putCoverageMap(const FileId id, const Offset[] region) @trusted { 2061 static immutable sql = format!"INSERT OR IGNORE INTO %1$s (file_id, begin, end) 2062 VALUES(:fid, :begin, :end)"(srcCovTable); 2063 auto stmt = db.prepare(sql); 2064 2065 foreach (a; region) { 2066 stmt.get.bind(":fid", id.get); 2067 stmt.get.bind(":begin", a.begin); 2068 stmt.get.bind(":end", a.end); 2069 stmt.get.execute; 2070 stmt.get.reset; 2071 } 2072 } 2073 2074 CovRegion[][FileId] getCoverageMap() @trusted { 2075 static immutable sql = format!"SELECT file_id,begin,end,id FROM %s"(srcCovTable); 2076 auto stmt = db.prepare(sql); 2077 2078 typeof(return) rval; 2079 foreach (ref r; stmt.get.execute) { 2080 auto region = CovRegion(r.peek!long(3).CoverageRegionId, 2081 Offset(r.peek!uint(1), r.peek!uint(2))); 2082 if (auto v = FileId(r.peek!long(0)) in rval) { 2083 *v ~= region; 2084 } else { 2085 rval[FileId(r.peek!long(0))] = [region]; 2086 } 2087 } 2088 2089 return rval; 2090 } 2091 2092 long getCoverageMapCount() @trusted { 2093 static immutable sql = format!"SELECT count(*) FROM %s"(srcCovTable); 2094 auto stmt = db.prepare(sql); 2095 foreach (ref r; stmt.get.execute) 2096 return r.peek!long(0); 2097 return 0; 2098 } 2099 2100 void clearCoverageMap(const FileId id) @trusted { 2101 static immutable sql = format!"DELETE FROM %1$s WHERE file_id = :id"(srcCovTable); 2102 auto stmt = db.prepare(sql); 2103 stmt.get.bind(":id", id.get); 2104 stmt.get.execute; 2105 } 2106 2107 void putCoverageInfo(const CoverageRegionId regionId, bool status) { 2108 static immutable sql = format!"INSERT OR REPLACE INTO %1$s (id, status) VALUES(:id, :status)"( 2109 srcCovInfoTable); 2110 auto stmt = db.prepare(sql); 2111 stmt.get.bind(":id", regionId.get); 2112 stmt.get.bind(":status", status); 2113 stmt.get.execute; 2114 } 2115 2116 Optional!SysTime getCoverageTimeStamp() @trusted { 2117 static immutable sql = format!"SELECT timeStamp FROM %s"(srcCovTimeStampTable); 2118 auto stmt = db.prepare(sql); 2119 2120 foreach (ref r; stmt.get.execute) { 2121 return some(r.peek!string(0).fromSqLiteDateTime); 2122 } 2123 return none!SysTime; 2124 } 2125 2126 /// Set the timestamp to the current UTC time. 2127 void updateCoverageTimeStamp() @trusted { 2128 static immutable sql = format!"INSERT OR REPLACE INTO %s (id, timestamp) VALUES(0, :time)"( 2129 srcCovTimeStampTable); 2130 auto stmt = db.prepare(sql); 2131 stmt.get.bind(":time", Clock.currTime.toSqliteDateTime); 2132 stmt.get.execute; 2133 } 2134 2135 /// Returns: the latest/newest timestamp of the tracked SUT or test files. 2136 Optional!SysTime getLatestTimeStampOfTestOrSut() @trusted { 2137 import std.algorithm : max; 2138 2139 auto test = getNewestTestFile; 2140 auto sut = getNewestFile; 2141 2142 if (!(test.hasValue || sut.hasValue)) 2143 return none!SysTime; 2144 2145 return some(max(test.orElse(TestFile.init).timeStamp, sut.orElse(SysTime.init))); 2146 } 2147 2148 MutationStatusId[] getNotCoveredMutants() @trusted { 2149 static immutable sql = format!"SELECT DISTINCT t3.st_id FROM %1$s t0, %2$s t1, %3$s t2, %4$s t3 2150 WHERE t0.status = 0 AND 2151 t0.id = t1.id AND 2152 t1.file_id = t2.file_id AND 2153 (t2.offset_begin BETWEEN t1.begin AND t1.end) AND 2154 (t2.offset_end BETWEEN t1.begin AND t1.end) AND 2155 t2.id = t3.mp_id"(srcCovInfoTable, 2156 srcCovTable, mutationPointTable, mutationTable); 2157 2158 auto app = appender!(MutationStatusId[])(); 2159 auto stmt = db.prepare(sql); 2160 foreach (ref r; stmt.get.execute) { 2161 app.put(MutationStatusId(r.peek!long(0))); 2162 } 2163 2164 return app.data; 2165 } 2166 2167 DbDependency dependencyApi() return @trusted { 2168 return DbDependency(&this); 2169 } 2170 } 2171 2172 /** Dependencies between root and those files that should trigger a re-analyze 2173 * of the root if they are changed. 2174 */ 2175 struct DbDependency { 2176 private Database* db; 2177 2178 /// The root must already exist or the whole operation will fail with an sql error. 2179 void set(const Path path, const DepFile[] deps) @trusted { 2180 static immutable insertDepSql = format!"INSERT OR IGNORE INTO %1$s (file,checksum0,checksum1) 2181 VALUES(:file,:cs0,:cs1) 2182 ON CONFLICT (file) DO UPDATE SET checksum0=:cs0,checksum1=:cs1 WHERE file=:file"( 2183 depFileTable); 2184 2185 auto stmt = db.prepare(insertDepSql); 2186 auto ids = appender!(long[])(); 2187 foreach (a; deps) { 2188 stmt.get.bind(":file", a.file.toString); 2189 stmt.get.bind(":cs0", cast(long) a.checksum.c0); 2190 stmt.get.bind(":cs1", cast(long) a.checksum.c1); 2191 stmt.get.execute; 2192 stmt.get.reset; 2193 2194 // can't use lastInsertRowid because a conflict would not update 2195 // the ID. 2196 auto id = getId(a.file); 2197 if (id.hasValue) 2198 ids.put(id.orElse(0L)); 2199 } 2200 2201 static immutable addRelSql = format!"INSERT OR IGNORE INTO %1$s (dep_id,file_id) VALUES(:did, :fid)"( 2202 depRootTable); 2203 stmt = db.prepare(addRelSql); 2204 const fid = () { 2205 auto a = db.getFileId(path); 2206 if (a.isNull) { 2207 throw new Exception( 2208 "File is not tracked (is missing from the files table in the database) " 2209 ~ path); 2210 } 2211 return a.get; 2212 }(); 2213 2214 foreach (id; ids.data) { 2215 stmt.get.bind(":did", id); 2216 stmt.get.bind(":fid", fid.get); 2217 stmt.get.execute; 2218 stmt.get.reset; 2219 } 2220 } 2221 2222 private Optional!long getId(const Path file) { 2223 foreach (a; db.run(select!DependencyFileTable.where("file = :file", 2224 Bind("file")), file.toString)) { 2225 return some(a.id); 2226 } 2227 return none!long; 2228 } 2229 2230 /// Returns: all dependencies. 2231 DepFile[] getAll() @trusted { 2232 return db.run(select!DependencyFileTable) 2233 .map!(a => DepFile(Path(a.file), Checksum(a.checksum0, a.checksum1))).array; 2234 } 2235 2236 /// Returns: all files that a root is dependent on. 2237 Path[] get(const Path root) @trusted { 2238 static immutable sql = format!"SELECT t0.file 2239 FROM %1$s t0, %2$s t1, %3$s t2 2240 WHERE 2241 t0.id = t1.dep_id AND 2242 t1.file_id = t2.id AND 2243 t2.path = :file"(depFileTable, 2244 depRootTable, filesTable); 2245 2246 auto stmt = db.prepare(sql); 2247 stmt.get.bind(":file", root.toString); 2248 auto app = appender!(Path[])(); 2249 foreach (ref a; stmt.get.execute) { 2250 app.put(Path(a.peek!string(0))); 2251 } 2252 2253 return app.data; 2254 } 2255 2256 /// Remove all dependencies that have no relation to a root. 2257 void cleanup() @trusted { 2258 db.run(format!"DELETE FROM %1$s 2259 WHERE id NOT IN (SELECT dep_id FROM %2$s)"(depFileTable, 2260 depRootTable)); 2261 } 2262 } 2263 2264 private: 2265 2266 MarkedMutant make(MarkedMutantTbl m) { 2267 import dextool.plugin.mutate.backend.type; 2268 2269 return MarkedMutant(m.mutationStatusId.MutationStatusId, Checksum(m.checksum0, 2270 m.checksum1), m.mutationId.MutationId, SourceLoc(m.line, m.column), 2271 m.path.Path, m.toStatus.to!(Mutation.Status), m.time, 2272 m.rationale.Rationale, m.mutText); 2273 } 2274 2275 string fromOrder(const MutationOrder userOrder) { 2276 final switch (userOrder) { 2277 case MutationOrder.random: 2278 return ":base_prio + t1.prio + abs(random() % 100)"; 2279 case MutationOrder.consecutive: 2280 return ":base_prio"; 2281 case MutationOrder.bySize: 2282 return ":base_prio + t1.prio"; 2283 } 2284 }