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