1 /** 2 Copyright: Copyright (c) 2018, 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; 25 import std.array : Appender, appender, array; 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 miniorm : toSqliteDateTime, fromSqLiteDateTime; 35 36 import dextool.type : AbsolutePath, Path, ExitStatusType; 37 38 import dextool.plugin.mutate.backend.database.schema; 39 import dextool.plugin.mutate.backend.database.type; 40 import dextool.plugin.mutate.backend.type : Language, Checksum; 41 42 /** Database wrapper with minimal dependencies. 43 */ 44 struct Database { 45 import miniorm : Miniorm, select, insert, insertOrReplace, delete_; 46 import d2sqlite3 : SqlDatabase = Database; 47 import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum; 48 49 Miniorm db; 50 alias db this; 51 52 /** Create a database by either opening an existing or initializing a new. 53 * 54 * Params: 55 * db = path to the database 56 */ 57 static auto make(string db) @safe { 58 return Database(initializeDB(db)); 59 } 60 61 /// If the file has already been analyzed. 62 bool isAnalyzed(const Path p) @trusted { 63 auto stmt = db.prepare("SELECT count(*) FROM files WHERE path=:path LIMIT 1"); 64 stmt.get.bind(":path", cast(string) p); 65 auto res = stmt.get.execute; 66 return res.oneValue!long != 0; 67 } 68 69 /// If the file has already been analyzed. 70 bool isAnalyzed(const Path p, const Checksum cs) @trusted { 71 auto stmt = db.prepare( 72 "SELECT count(*) FROM files WHERE path=:path AND checksum0=:cs0 AND checksum1=:cs1 LIMIT 1"); 73 stmt.get.bind(":path", cast(string) p); 74 stmt.get.bind(":cs0", cast(long) cs.c0); 75 stmt.get.bind(":cs1", cast(long) cs.c1); 76 auto res = stmt.get.execute; 77 return res.oneValue!long != 0; 78 } 79 80 bool exists(MutationStatusId status_id) { 81 immutable s = format!"SELECT COUNT(*) FROM %s WHERE id=:id LIMIT 1"(mutationStatusTable); 82 auto stmt = db.prepare(s); 83 stmt.get.bind(":id", cast(long) status_id); 84 auto res = stmt.get.execute; 85 return res.oneValue!long == 0; 86 } 87 88 bool isMarked(MutationId id) @trusted { 89 immutable s = format!"SELECT COUNT(*) FROM %s WHERE st_id IN 90 (SELECT st_id FROM %s WHERE id=:id)"( 91 markedMutantTable, mutationTable); 92 auto stmt = db.prepare(s); 93 stmt.get.bind(":id", cast(long) id); 94 auto res = stmt.get.execute; 95 return res.oneValue!long != 0; 96 } 97 98 /// All marked mutants whom have a mutation status checksum that has been removed from the database. 99 MarkedMutant[] getLostMarkings() @trusted { 100 immutable sql = format!"SELECT checksum0 FROM %s 101 WHERE 102 checksum0 NOT IN (SELECT checksum0 FROM %s)"( 103 markedMutantTable, mutationStatusTable); 104 105 auto stmt = db.prepare(sql); 106 auto app = appender!(MarkedMutant[])(); 107 foreach (res; stmt.get.execute) { 108 foreach (m; db.run(select!MarkedMutantTbl.where("checksum0 = ", res.peek!long(0)))) { 109 app.put(.make(m)); 110 } 111 } 112 113 return app.data; 114 } 115 116 Nullable!FileId getFileId(const Path p) @trusted { 117 enum sql = format("SELECT id FROM %s WHERE path=:path", filesTable); 118 auto stmt = db.prepare(sql); 119 stmt.get.bind(":path", cast(string) p); 120 auto res = stmt.get.execute; 121 122 typeof(return) rval; 123 if (!res.empty) 124 rval = FileId(res.oneValue!long); 125 return rval; 126 } 127 128 /// Returns: the path ID for the mutant. 129 Nullable!FileId getFileId(const MutationId id) @trusted { 130 enum sql = format("SELECT t1.file_id 131 FROM %s t0, %s t1 132 WHERE t0.id = :id AND t0.mp_id = t1.id", 133 mutationTable, mutationPointTable); 134 auto stmt = db.prepare(sql); 135 stmt.get.bind(":id", cast(long) id); 136 137 typeof(return) rval; 138 foreach (r; stmt.get.execute) 139 rval = FileId(r.peek!long(0)); 140 return rval; 141 } 142 143 /// Returns: the file path that the id correspond to. 144 Nullable!Path getFile(const FileId id) @trusted { 145 enum sql = format("SELECT path FROM %s WHERE id = :id", filesTable); 146 auto stmt = db.prepare(sql); 147 stmt.get.bind(":id", cast(long) id); 148 149 typeof(return) rval; 150 foreach (r; stmt.get.execute) 151 rval = Path(r.peek!string(0)); 152 return rval; 153 } 154 155 /// Remove the file with all mutations that are coupled to it. 156 void removeFile(const Path p) @trusted { 157 auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(filesTable)); 158 stmt.get.bind(":path", cast(string) p); 159 stmt.get.execute; 160 } 161 162 /// Returns: All files in the database as relative paths. 163 Path[] getFiles() @trusted { 164 auto stmt = db.prepare(format!"SELECT path from %s"(filesTable)); 165 auto res = stmt.get.execute; 166 167 auto app = appender!(Path[]); 168 foreach (ref r; res) { 169 app.put(Path(r.peek!string(0))); 170 } 171 172 return app.data; 173 } 174 175 enum CntAction { 176 /// Increment the counter 177 incr, 178 /// Reset the counter to zero 179 reset, 180 } 181 182 /** Update the status of a mutant. 183 * 184 * Params: 185 * id = ID of the mutant 186 * st = status to broadcast 187 * d = time spent on veryfing the mutant 188 * tcs = test cases that killed the mutant 189 * counter = how to act with the counter 190 */ 191 void updateMutation(const MutationId id, const Mutation.Status st, 192 const Duration d, const(TestCase)[] tcs, CntAction counter = CntAction.incr) @trusted { 193 enum sql = "UPDATE %s SET 194 status=:st,time=:time,update_ts=:update_ts,%s 195 WHERE 196 id IN (SELECT st_id FROM %s WHERE id = :id)"; 197 198 auto stmt = () { 199 final switch (counter) { 200 case CntAction.incr: 201 return db.prepare(format!sql(mutationStatusTable, 202 "test_cnt=test_cnt+1", mutationTable)); 203 case CntAction.reset: 204 return db.prepare(format!sql(mutationStatusTable, 205 "test_cnt=0", mutationTable)); 206 } 207 }(); 208 209 stmt.get.bind(":st", st.to!long); 210 stmt.get.bind(":id", id.to!long); 211 stmt.get.bind(":time", d.total!"msecs"); 212 stmt.get.bind(":update_ts", Clock.currTime.toUTC.toSqliteDateTime); 213 stmt.get.execute; 214 215 updateMutationTestCases(id, tcs); 216 } 217 218 /** Update the counter of how many times the mutants has been alive. 219 * 220 * Params: 221 * id = ID of the mutant 222 * counter = how to act with the counter 223 */ 224 void updateMutation(const MutationStatusId id, const CntAction counter) @trusted { 225 enum sql = "UPDATE %s SET %s WHERE id = :id"; 226 227 auto stmt = () { 228 final switch (counter) { 229 case CntAction.incr: 230 return db.prepare(format!sql(mutationStatusTable, 231 "test_cnt=test_cnt+1")); 232 case CntAction.reset: 233 return db.prepare(format!sql(mutationStatusTable, "test_cnt=0")); 234 } 235 }(); 236 237 stmt.get.bind(":id", id.to!long); 238 stmt.get.execute; 239 } 240 241 /// Update the time used to test the mutant. 242 void updateMutation(const MutationStatusId id, const Duration testTime) @trusted { 243 enum sql = format!"UPDATE %s SET time=:time WHERE id = :id"(mutationStatusTable); 244 auto stmt = db.prepare(sql); 245 stmt.get.bind(":id", id.to!long); 246 stmt.get.bind(":time", testTime.total!"msecs"); 247 stmt.get.execute; 248 } 249 250 /** Update the status of a mutant. 251 * 252 * Params: 253 * id = mutation status ID 254 * st = new status 255 * update_ts = if the update timestamp should be updated. 256 */ 257 void updateMutationStatus(const MutationStatusId id, const Mutation.Status st, 258 Flag!"updateTs" update_ts = No.updateTs) @trusted { 259 260 auto stmt = () { 261 if (update_ts) { 262 const ts = Clock.currTime.toUTC.toSqliteDateTime; 263 auto s = db.prepare(format!"UPDATE %s SET status=:st,update_ts=:update_ts WHERE id=:id"( 264 mutationStatusTable)); 265 s.get.bind(":update_ts", ts); 266 return s; 267 } else 268 return db.prepare(format!"UPDATE %s SET status=:st WHERE id=:id"( 269 mutationStatusTable)); 270 }(); 271 stmt.get.bind(":st", st.to!long); 272 stmt.get.bind(":id", id.to!long); 273 stmt.get.execute; 274 } 275 276 /// Returns: all mutation status IDs. 277 MutationStatusId[] getAllMutationStatus() @trusted { 278 enum sql = format!"SELECT id FROM %s"(mutationStatusTable); 279 280 auto app = appender!(MutationStatusId[])(); 281 auto stmt = db.prepare(sql); 282 foreach (r; stmt.get.execute) 283 app.put(MutationStatusId(r.peek!long(0))); 284 return app.data; 285 } 286 287 Nullable!(Mutation.Status) getMutationStatus(const MutationStatusId id) @trusted { 288 enum sql = format!"SELECT status FROM %s WHERE id=:id"(mutationStatusTable); 289 auto stmt = db.prepare(sql); 290 stmt.get.bind(":id", cast(long) id); 291 292 typeof(return) rval; 293 foreach (a; stmt.get.execute) { 294 rval = cast(Mutation.Status) a.peek!long(0); 295 } 296 return rval; 297 } 298 299 Nullable!MutationEntry getMutation(const MutationId id) @trusted { 300 import dextool.plugin.mutate.backend.type; 301 import dextool.type : FileName; 302 303 typeof(return) rval; 304 305 enum get_mut_sql = format("SELECT 306 t0.id, 307 t0.kind, 308 t3.time, 309 t1.offset_begin, 310 t1.offset_end, 311 t1.line, 312 t1.column, 313 t2.path, 314 t2.lang 315 FROM %s t0,%s t1,%s t2,%s t3 316 WHERE 317 t0.id == :id AND 318 t0.mp_id == t1.id AND 319 t1.file_id == t2.id AND 320 t3.id = t0.st_id 321 ", mutationTable, mutationPointTable, 322 filesTable, mutationStatusTable); 323 324 auto stmt = db.prepare(get_mut_sql); 325 stmt.get.bind(":id", cast(long) id); 326 auto res = stmt.get.execute; 327 328 if (res.empty) 329 return rval; 330 331 auto v = res.front; 332 333 auto mp = MutationPoint(Offset(v.peek!uint(3), v.peek!uint(4))); 334 mp.mutations = [Mutation(v.peek!long(1).to!(Mutation.Kind))]; 335 auto pkey = MutationId(v.peek!long(0)); 336 auto file = Path(FileName(v.peek!string(7))); 337 auto sloc = SourceLoc(v.peek!uint(5), v.peek!uint(6)); 338 auto lang = v.peek!long(8).to!Language; 339 340 rval = MutationEntry(pkey, file, sloc, mp, v.peek!long(2).dur!"msecs", lang); 341 342 return rval; 343 } 344 345 MutantMetaData getMutantationMetaData(const MutationId id) @trusted { 346 auto rval = MutantMetaData(id); 347 foreach (res; db.run(select!NomutDataTbl.where("mut_id =", cast(long) id))) { 348 rval.set(NoMut(res.tag, res.comment)); 349 } 350 return rval; 351 } 352 353 //TODO: this is a bit inefficient. it should use a callback iterator 354 MutantMetaData[] getMutantationMetaData(const Mutation.Kind[] kinds, const Mutation 355 .Status status) @trusted { 356 const sql = format!"SELECT DISTINCT t.mut_id, t.tag, t.comment 357 FROM %s t, %s t1, %s t2 358 WHERE 359 t.mut_id = t1.id AND 360 t1.st_id = t2.id AND 361 t2.status = :status AND 362 t1.kind IN (%(%s,%)) 363 ORDER BY 364 t.mut_id"(nomutDataTable, mutationTable, 365 mutationStatusTable, kinds.map!(a => cast(long) a)); 366 auto stmt = db.prepare(sql); 367 stmt.get.bind(":status", cast(long) status); 368 369 auto app = appender!(MutantMetaData[])(); 370 foreach (res; stmt.get.execute) { 371 app.put(MutantMetaData(MutationId(res.peek!long(0)), 372 MutantAttr(NoMut(res.peek!string(1), res.peek!string(2))))); 373 } 374 return app.data; 375 } 376 377 Nullable!Path getPath(const MutationId id) @trusted { 378 enum get_path_sql = format("SELECT t2.path 379 FROM 380 %s t0, %s t1, %s t2 381 WHERE 382 t0.id = :id AND 383 t0.mp_id = t1.id AND 384 t1.file_id = t2.id 385 ", mutationTable, mutationPointTable, filesTable); 386 387 auto stmt = db.prepare(get_path_sql); 388 stmt.get.bind(":id", cast(long) id); 389 auto res = stmt.get.execute; 390 391 typeof(return) rval; 392 if (!res.empty) 393 rval = Path(res.front.peek!string(0)); 394 return rval; 395 } 396 397 /// Returns: the mutants that are connected to the mutation statuses. 398 MutantInfo[] getMutantsInfo(const Mutation.Kind[] kinds, const(MutationStatusId)[] id) @trusted { 399 const get_mutid_sql = format("SELECT t0.id,t2.status,t0.kind,t1.line,t1.column 400 FROM %s t0,%s t1, %s t2 401 WHERE 402 t0.st_id IN (%(%s,%)) AND 403 t0.st_id = t2.id AND 404 t0.kind IN (%(%s,%)) AND 405 t0.mp_id = t1.id", 406 mutationTable, mutationPointTable, 407 mutationStatusTable, id.map!(a => cast(long) a), kinds.map!(a => cast(int) a)); 408 auto stmt = db.prepare(get_mutid_sql); 409 410 auto app = appender!(MutantInfo[])(); 411 foreach (res; stmt.get.execute) 412 app.put(MutantInfo(MutationId(res.peek!long(0)), res.peek!long(1) 413 .to!(Mutation.Status), res.peek!long(2).to!(Mutation.Kind), 414 SourceLoc(res.peek!uint(3), res.peek!uint(4)))); 415 416 return app.data; 417 } 418 419 /// Returns: the mutants that are connected to the mutation statuses. 420 MutationId[] getMutationIds(const(Mutation.Kind)[] kinds, const(MutationStatusId)[] id) @trusted { 421 if (id.length == 0) 422 return null; 423 424 const get_mutid_sql = format!"SELECT id FROM %s t0 425 WHERE 426 t0.st_id IN (%(%s,%)) AND 427 t0.kind IN (%(%s,%))"(mutationTable, 428 id.map!(a => cast(long) a), kinds.map!(a => cast(int) a)); 429 auto stmt = db.prepare(get_mutid_sql); 430 431 auto app = appender!(MutationId[])(); 432 foreach (res; stmt.get.execute) 433 app.put(MutationId(res.peek!long(0))); 434 return app.data; 435 } 436 437 Nullable!MutationId getMutationId(const MutationStatusId id) @trusted { 438 immutable sql = format!"SELECT id FROM %s WHERE st_id=:st_id LIMIT=1"(mutationTable); 439 auto stmt = db.prepare(sql); 440 stmt.get.bind(":st_id", cast(long) id); 441 442 typeof(return) rval; 443 foreach (res; stmt.get.execute) { 444 rval = res.peek!long(0).MutationId; 445 } 446 return rval; 447 } 448 449 Nullable!MutationStatusId getMutationStatusId(const MutationId id) @trusted { 450 immutable sql = format!"SELECT st_id FROM %s WHERE id=:id"(mutationTable); 451 auto stmt = db.prepare(sql); 452 stmt.get.bind(":id", cast(long) id); 453 454 typeof(return) rval; 455 foreach (res; stmt.get.execute) { 456 rval = MutationStatusId(res.peek!long(0)); 457 } 458 return rval; 459 } 460 461 Nullable!MutationStatusId getMutationStatusId(const Checksum cs) @trusted { 462 immutable sql = format!"SELECT st_id FROM %s WHERE checksum0=:cs0 AND checksum1=:cs1"( 463 mutationStatusTable); 464 auto stmt = db.prepare(sql); 465 stmt.get.bind(":cs0", cast(long) cs.c0); 466 stmt.get.bind(":cs1", cast(long) cs.c1); 467 468 typeof(return) rval; 469 foreach (res; stmt.get.execute) { 470 rval = MutationStatusId(res.peek!long(0)); 471 } 472 return rval; 473 } 474 475 // Returns: the status of the mutant 476 Nullable!(Mutation.Status) getMutationStatus(const MutationId id) @trusted { 477 auto s = format!"SELECT status FROM %s WHERE id IN (SELECT st_id FROM %s WHERE id=:mut_id)"( 478 mutationStatusTable, mutationTable); 479 auto stmt = db.prepare(s); 480 stmt.get.bind(":mut_id", cast(long) id); 481 typeof(return) rval; 482 foreach (res; stmt.get.execute) 483 rval = res.peek!long(0).to!(Mutation.Status); 484 return rval; 485 } 486 487 /// Returns: the mutants in the file at the line. 488 MutationStatusId[] getMutationsOnLine(const(Mutation.Kind)[] kinds, FileId fid, SourceLoc sloc) @trusted { 489 // TODO: should it also be line_end? 490 const sql = format("SELECT DISTINCT t0.id FROM %s t0, %s t1, %s t2 491 WHERE 492 t1.st_id = t0.id AND 493 t1.kind IN (%(%s,%)) AND 494 t1.mp_id = t2.id AND 495 t2.file_id = :fid AND 496 (:line BETWEEN t2.line AND t2.line_end) 497 ", 498 mutationStatusTable, mutationTable, mutationPointTable, 499 kinds.map!(a => cast(int) a)); 500 auto stmt = db.prepare(sql); 501 stmt.get.bind(":fid", cast(long) fid); 502 stmt.get.bind(":line", sloc.line); 503 504 auto app = appender!(typeof(return))(); 505 foreach (res; stmt.get.execute) 506 app.put(MutationStatusId(res.peek!long(0))); 507 return app.data; 508 } 509 510 LineMetadata getLineMetadata(const FileId fid, const SourceLoc sloc) @trusted { 511 // TODO: change this select to using microrm 512 enum sql = format("SELECT nomut,tag,comment FROM %s 513 WHERE 514 file_id = :fid AND 515 line = :line", rawSrcMetadataTable); 516 auto stmt = db.prepare(sql); 517 stmt.get.bind(":fid", cast(long) fid); 518 stmt.get.bind(":line", sloc.line); 519 520 auto rval = typeof(return)(fid, sloc.line); 521 foreach (res; stmt.get.execute) { 522 if (res.peek!long(0) != 0) 523 rval.set(NoMut(res.peek!string(1), res.peek!string(2))); 524 } 525 526 return rval; 527 } 528 529 /// Returns: the `nr` mutants that where the longst since they where tested. 530 MutationStatusTime[] getOldestMutants(const(Mutation.Kind)[] kinds, const long nr) @trusted { 531 const sql = format("SELECT t0.id,t0.update_ts FROM %s t0, %s t1 532 WHERE 533 t0.update_ts IS NOT NULL AND 534 t1.st_id = t0.id AND 535 t1.kind IN (%(%s,%)) 536 ORDER BY t0.update_ts ASC LIMIT :limit", 537 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a)); 538 auto stmt = db.prepare(sql); 539 stmt.get.bind(":limit", nr); 540 541 auto app = appender!(MutationStatusTime[])(); 542 foreach (res; stmt.get.execute) 543 app.put(MutationStatusTime(MutationStatusId(res.peek!long(0)), 544 res.peek!string(1).fromSqLiteDateTime)); 545 return app.data; 546 } 547 548 /// Returns: the `nr` mutant with the highest count that has not been killed and existed in the system the longest. 549 MutationStatus[] getHardestToKillMutant(const(Mutation.Kind)[] kinds, 550 const Mutation.Status status, const long nr) @trusted { 551 const sql = format("SELECT t0.id,t0.status,t0.test_cnt,t0.update_ts,t0.added_ts 552 FROM %s t0, %s t1 553 WHERE 554 t0.update_ts IS NOT NULL AND 555 t0.status = :status AND 556 t1.st_id = t0.id AND 557 t1.kind IN (%(%s,%)) AND 558 t1.st_id NOT IN (SELECT st_id FROM %s WHERE nomut != 0) 559 ORDER BY 560 t0.test_cnt DESC, 561 t0.added_ts ASC, 562 t0.update_ts ASC 563 LIMIT :limit", 564 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a), srcMetadataTable); 565 auto stmt = db.prepare(sql); 566 stmt.get.bind(":status", cast(long) status); 567 stmt.get.bind(":limit", nr); 568 569 auto app = appender!(MutationStatus[])(); 570 foreach (res; stmt.get.execute) { 571 auto added = () { 572 auto raw = res.peek!string(4); 573 if (raw.length == 0) 574 return Nullable!SysTime(); 575 return Nullable!SysTime(raw.fromSqLiteDateTime); 576 }(); 577 578 // dfmt off 579 app.put(MutationStatus( 580 MutationStatusId(res.peek!long(0)), 581 res.peek!long(1).to!(Mutation.Status), 582 res.peek!long(2).MutantTestCount, 583 res.peek!string(3).fromSqLiteDateTime, 584 added, 585 )); 586 // dfmt on 587 } 588 589 return app.data; 590 } 591 592 /** Get SourceLoc for a specific mutation id. 593 */ 594 Nullable!SourceLoc getSourceLocation(MutationId id) @trusted { 595 auto s = format!"SELECT line, column FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE id=:mut_id)"( 596 mutationPointTable, mutationTable); 597 auto stmt = db.prepare(s); 598 stmt.get.bind(":mut_id", cast(long) id); 599 typeof(return) rval; 600 foreach (res; stmt.get.execute) 601 rval = SourceLoc(res.peek!uint(0), res.peek!uint(1)); 602 return rval; 603 } 604 605 /** Remove all mutations of kinds. 606 */ 607 void removeMutant(const Mutation.Kind[] kinds) @trusted { 608 const s = format!"DELETE FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE kind IN (%(%s,%)))"( 609 mutationPointTable, mutationTable, kinds.map!(a => cast(int) a)); 610 auto stmt = db.prepare(s); 611 stmt.get.execute; 612 } 613 614 /** Reset all mutations of kinds with the status `st` to unknown. 615 */ 616 void resetMutant(const Mutation.Kind[] kinds, Mutation.Status st, Mutation.Status to_st) @trusted { 617 const s = format!"UPDATE %s SET status=%s WHERE status = %s AND id IN(SELECT st_id FROM %s WHERE kind IN (%(%s,%)))"( 618 mutationStatusTable, to_st.to!long, st.to!long, 619 mutationTable, kinds.map!(a => cast(int) a)); 620 auto stmt = db.prepare(s); 621 stmt.get.execute; 622 } 623 624 /** Mark a mutant with status and rationale (also adds metadata). 625 */ 626 void markMutant(const MutationId id, const Path file, const SourceLoc sloc, 627 const MutationStatusId st_id, const Checksum cs, 628 const Mutation.Status s, const Rationale r, string mutationTxt) @trusted { 629 db.run(insertOrReplace!MarkedMutantTbl, MarkedMutantTbl(cs.c0, cs.c1, 630 st_id, id, sloc.line, sloc.column, file, s, Clock.currTime.toUTC, r, mutationTxt)); 631 } 632 633 void removeMarkedMutant(const Checksum cs) @trusted { 634 db.run(delete_!MarkedMutantTbl.where("checksum0 = ", cast(long) cs.c0)); 635 } 636 637 void removeMarkedMutant(const MutationStatusId id) @trusted { 638 db.run(delete_!MarkedMutantTbl.where("st_id = ", id.to!string)); 639 } 640 641 /// Returns: All mutants with that are marked orderd by their path 642 MarkedMutant[] getMarkedMutants() @trusted { 643 import miniorm : OrderingTermSort; 644 645 auto app = appender!(MarkedMutant[])(); 646 foreach (m; db.run(select!MarkedMutantTbl.orderBy(OrderingTermSort.ASC, [ 647 "path" 648 ]))) { 649 app.put(.make(m)); 650 } 651 652 return app.data; 653 } 654 655 Mutation.Kind getKind(MutationId id) @trusted { 656 immutable s = format!"SELECT kind FROM %s WHERE id=:id"(mutationTable); 657 auto stmt = db.prepare(s); 658 stmt.get.bind(":id", cast(long) id); 659 660 typeof(return) rval; 661 foreach (res; stmt.get.execute) { 662 rval = res.peek!long(0).to!(Mutation.Kind); 663 } 664 return rval; 665 } 666 667 import dextool.plugin.mutate.backend.type; 668 669 alias aliveMutants = countMutants!([Mutation.Status.alive], false); 670 alias killedMutants = countMutants!([Mutation.Status.killed], false); 671 alias timeoutMutants = countMutants!([Mutation.Status.timeout], false); 672 673 /// Returns: Total that should be counted when calculating the mutation score. 674 alias totalMutants = countMutants!([ 675 Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout 676 ], false); 677 678 alias unknownMutants = countMutants!([Mutation.Status.unknown], false); 679 alias killedByCompilerMutants = countMutants!([ 680 Mutation.Status.killedByCompiler 681 ], false); 682 683 alias aliveSrcMutants = countMutants!([Mutation.Status.alive], true); 684 alias killedSrcMutants = countMutants!([Mutation.Status.killed], true); 685 alias timeoutSrcMutants = countMutants!([Mutation.Status.timeout], true); 686 687 /// Returns: Total that should be counted when calculating the mutation score. 688 alias totalSrcMutants = countMutants!([ 689 Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout 690 ], true); 691 692 alias unknownSrcMutants = countMutants!([Mutation.Status.unknown], true); 693 alias killedByCompilerSrcMutants = countMutants!([ 694 Mutation.Status.killedByCompiler 695 ], true); 696 697 /** Count the mutants with the nomut metadata. 698 * 699 * Params: 700 * status = status the mutants must be in to be counted. 701 * distinc = count based on unique source code changes. 702 * kinds = the kind of mutants to count. 703 * file = file to count mutants in. 704 */ 705 private MutationReportEntry countMutants(int[] status, bool distinct)( 706 const Mutation.Kind[] kinds, string file = null) @trusted { 707 static if (distinct) { 708 auto qq = " 709 SELECT count(*),sum(time) 710 FROM ( 711 SELECT count(*),sum(t1.time) time 712 FROM %s t0, %s t1%s 713 WHERE 714 %s 715 t0.st_id = t1.id AND 716 t1.status IN (%(%s,%)) AND 717 t0.kind IN (%(%s,%)) 718 GROUP BY t1.id)"; 719 } else { 720 auto qq = " 721 SELECT count(*),sum(t1.time) time 722 FROM %s t0, %s t1%s 723 WHERE 724 %s 725 t0.st_id = t1.id AND 726 t1.status IN (%(%s,%)) AND 727 t0.kind IN (%(%s,%))"; 728 } 729 const query = () { 730 auto fq = file.length == 0 731 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND"; 732 auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3", 733 mutationPointTable, filesTable); 734 return format(qq, mutationTable, mutationStatusTable, fq_from, fq, 735 status, kinds.map!(a => cast(int) a)); 736 }(); 737 738 typeof(return) rval; 739 auto stmt = db.prepare(query); 740 if (file.length != 0) 741 stmt.get.bind(":path", file); 742 auto res = stmt.get.execute; 743 if (!res.empty) 744 rval = MutationReportEntry(res.front.peek!long(0), 745 res.front.peek!long(1).dur!"msecs"); 746 return rval; 747 } 748 749 /** Count the mutants with the nomut metadata. 750 * 751 * Params: 752 * status = status the mutants must be in to be counted. 753 * distinc = count based on unique source code changes. 754 * kinds = the kind of mutants to count. 755 * file = file to count mutants in. 756 */ 757 private MetadataNoMutEntry countNoMutMutants(int[] status, bool distinct)( 758 const Mutation.Kind[] kinds, string file = null) @trusted { 759 static if (distinct) { 760 auto sql_base = " 761 SELECT count(*) 762 FROM ( 763 SELECT count(*) 764 FROM %s t0, %s t1,%s t4%s 765 WHERE 766 %s 767 t0.st_id = t1.id AND 768 t0.st_id = t4.st_id AND 769 t4.nomut != 0 AND 770 t1.status IN (%(%s,%)) AND 771 t0.kind IN (%(%s,%)) 772 GROUP BY t1.id)"; 773 } else { 774 auto sql_base = " 775 SELECT count(*) 776 FROM %s t0, %s t1,%s t4%s 777 WHERE 778 %s 779 t0.st_id = t1.id AND 780 t0.st_id = t4.st_id AND 781 t4.nomut != 0 AND 782 t1.status IN (%(%s,%)) AND 783 t0.kind IN (%(%s,%))"; 784 } 785 const query = () { 786 auto fq = file.length == 0 787 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND"; 788 auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3", 789 mutationPointTable, filesTable); 790 return format(sql_base, mutationTable, mutationStatusTable, 791 srcMetadataTable, fq_from, fq, status, kinds.map!(a => cast(int) a)); 792 }(); 793 794 typeof(return) rval; 795 auto stmt = db.prepare(query); 796 if (file.length != 0) 797 stmt.get.bind(":path", file); 798 auto res = stmt.get.execute; 799 if (!res.empty) 800 rval = MetadataNoMutEntry(res.front.peek!long(0)); 801 return rval; 802 } 803 804 /// ditto. 805 alias aliveNoMutSrcMutants = countNoMutMutants!([Mutation.Status.alive], true); 806 807 /// Returns: mutants killed by the test case. 808 MutationStatusId[] testCaseKilledSrcMutants(const Mutation.Kind[] kinds, TestCase tc) @trusted { 809 const query = format(" 810 SELECT t1.id 811 FROM %s t0, %s t1, %s t2, %s t3 812 WHERE 813 t0.st_id = t1.id AND 814 t1.status = :st AND 815 t0.kind IN (%(%s,%)) AND 816 t2.name = :name AND 817 t2.id = t3.tc_id AND 818 t3.st_id = t1.id 819 GROUP BY t1.id", mutationTable, mutationStatusTable, 820 allTestCaseTable, killedTestCaseTable, kinds.map!(a => cast(int) a)); 821 822 auto stmt = db.prepare(query); 823 stmt.get.bind(":st", cast(long) Mutation.Status.killed); 824 stmt.get.bind(":name", tc.name); 825 826 auto app = appender!(MutationStatusId[])(); 827 foreach (res; stmt.get.execute) 828 app.put(MutationStatusId(res.peek!long(0))); 829 830 return app.data; 831 } 832 833 /// Returns: mutants at mutations points that the test case has killed mutants at. 834 alias testCaseMutationPointAliveSrcMutants = testCaseCountSrcMutants!([ 835 Mutation.Status.alive 836 ]); 837 /// ditto 838 alias testCaseMutationPointTimeoutSrcMutants = testCaseCountSrcMutants!( 839 [Mutation.Status.timeout]); 840 /// ditto 841 alias testCaseMutationPointKilledSrcMutants = testCaseCountSrcMutants!([ 842 Mutation.Status.killed 843 ]); 844 /// ditto 845 alias testCaseMutationPointUnknownSrcMutants = testCaseCountSrcMutants!( 846 [Mutation.Status.unknown]); 847 /// ditto 848 alias testCaseMutationPointKilledByCompilerSrcMutants = testCaseCountSrcMutants!( 849 [Mutation.Status.killedByCompiler]); 850 /// ditto 851 alias testCaseMutationPointTotalSrcMutants = testCaseCountSrcMutants!( 852 [ 853 Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout 854 ]); 855 856 private MutationStatusId[] testCaseCountSrcMutants(int[] status)( 857 const Mutation.Kind[] kinds, TestCase tc) @trusted { 858 const query = format(" 859 SELECT t1.id 860 FROM %s t0, %s t1 861 WHERE 862 t0.mp_id IN (SELECT t1.id 863 FROM %s t0,%s t1, %s t2, %s t3 864 WHERE 865 t0.mp_id = t1.id AND 866 t2.name = :name AND 867 t2.id = t3.tc_id AND 868 t3.st_id = t0.st_id 869 ) 870 AND 871 t0.st_id = t1.id AND 872 t1.status IN (%(%s,%)) AND 873 t0.kind IN (%(%s,%)) 874 GROUP BY t1.id", mutationTable, mutationStatusTable, mutationTable, mutationPointTable, 875 allTestCaseTable, killedTestCaseTable, status, kinds.map!(a => cast(int) a)); 876 877 auto stmt = db.prepare(query); 878 stmt.get.bind(":name", tc.name); 879 880 auto app = appender!(MutationStatusId[])(); 881 foreach (res; stmt.get.execute) 882 app.put(MutationStatusId(res.peek!long(0))); 883 884 return app.data; 885 } 886 887 Nullable!Checksum getChecksum(MutationStatusId id) @trusted { 888 immutable sql = format!"SELECT checksum0, checksum1 FROM %s WHERE id=:id"( 889 mutationStatusTable); 890 auto stmt = db.prepare(sql); 891 stmt.get.bind(":id", cast(long) id); 892 893 typeof(return) rval; 894 foreach (res; stmt.get.execute) { 895 rval = Checksum(res.peek!long(0), res.peek!long(1)); 896 break; 897 } 898 return rval; 899 } 900 901 Nullable!Checksum getFileChecksum(const Path p) @trusted { 902 immutable sql = format!"SELECT checksum0,checksum1 FROM %s WHERE path=:path"(filesTable); 903 auto stmt = db.prepare(sql); 904 stmt.get.bind(":path", cast(string) p); 905 auto res = stmt.get.execute; 906 907 typeof(return) rval; 908 if (!res.empty) { 909 rval = Checksum(res.front.peek!long(0), res.front.peek!long(1)); 910 } 911 912 return rval; 913 } 914 915 void put(const Path p, Checksum cs, const Language lang) @trusted { 916 immutable sql = format!"INSERT OR IGNORE INTO %s (path, checksum0, checksum1, lang) 917 VALUES (:path, :checksum0, :checksum1, :lang)"( 918 filesTable); 919 auto stmt = db.prepare(sql); 920 stmt.get.bind(":path", cast(string) p); 921 stmt.get.bind(":checksum0", cast(long) cs.c0); 922 stmt.get.bind(":checksum1", cast(long) cs.c1); 923 stmt.get.bind(":lang", cast(long) lang); 924 stmt.get.execute; 925 } 926 927 /// Remove all metadata. 928 void clearMetadata() { 929 immutable sql = format!"DELETE FROM %s"(rawSrcMetadataTable); 930 db.run(sql); 931 } 932 933 /** Save line metadata to the database which is used to associate line 934 * metadata with mutants. 935 */ 936 void put(const LineMetadata[] mdata) { 937 import sumtype; 938 939 // TODO: convert to microrm 940 enum sql = format!"INSERT OR IGNORE INTO %s 941 (file_id, line, nomut, tag, comment) 942 VALUES(:fid, :line, :nomut, :tag, :comment)"( 943 rawSrcMetadataTable); 944 945 auto stmt = db.prepare(sql); 946 foreach (meta; mdata) { 947 auto nomut = meta.attr.match!((NoMetadata a) => NoMut.init, (NoMut a) => a); 948 stmt.get.bindAll(cast(long) meta.id, meta.line, meta.isNoMut, 949 nomut.tag, nomut.comment); 950 stmt.get.execute; 951 stmt.get.reset; 952 } 953 } 954 955 /// Store all found mutants. 956 void put(MutationPointEntry2[] mps, AbsolutePath rel_dir) @trusted { 957 enum insert_mp_sql = format("INSERT OR IGNORE INTO %s 958 (file_id, offset_begin, offset_end, line, column, line_end, column_end) 959 SELECT id,:begin,:end,:line,:column,:line_end,:column_end 960 FROM %s 961 WHERE 962 path = :path", mutationPointTable, filesTable); 963 auto mp_stmt = db.prepare(insert_mp_sql); 964 965 foreach (mp; mps) { 966 auto rel_file = relativePath(mp.file, rel_dir).Path; 967 mp_stmt.get.bind(":begin", mp.offset.begin); 968 mp_stmt.get.bind(":end", mp.offset.end); 969 mp_stmt.get.bind(":line", mp.sloc.line); 970 mp_stmt.get.bind(":column", mp.sloc.column); 971 mp_stmt.get.bind(":line_end", mp.slocEnd.line); 972 mp_stmt.get.bind(":column_end", mp.slocEnd.column); 973 mp_stmt.get.bind(":path", cast(string) rel_file); 974 mp_stmt.get.execute; 975 mp_stmt.get.reset; 976 } 977 978 enum insert_cmut_sql = format("INSERT OR IGNORE INTO %s 979 (status,test_cnt,update_ts,added_ts,checksum0,checksum1) 980 VALUES(:st,0,:update_ts,:added_ts,:c0,:c1)", 981 mutationStatusTable); 982 auto cmut_stmt = db.prepare(insert_cmut_sql); 983 const ts = Clock.currTime.toUTC.toSqliteDateTime; 984 cmut_stmt.get.bind(":st", Mutation.Status.unknown); 985 cmut_stmt.get.bind(":update_ts", ts); 986 cmut_stmt.get.bind(":added_ts", ts); 987 foreach (cm; mps.map!(a => a.cms).joiner) { 988 cmut_stmt.get.bind(":c0", cast(long) cm.id.c0); 989 cmut_stmt.get.bind(":c1", cast(long) cm.id.c1); 990 cmut_stmt.get.execute; 991 cmut_stmt.get.reset; 992 } 993 994 enum insert_m_sql = format("INSERT OR IGNORE INTO %s 995 (mp_id, st_id, kind) 996 SELECT t0.id,t1.id,:kind FROM %s t0, %s t1, %s t2 WHERE 997 t2.path = :path AND 998 t0.file_id = t2.id AND 999 t0.offset_begin = :off_begin AND 1000 t0.offset_end = :off_end AND 1001 t1.checksum0 = :c0 AND 1002 t1.checksum1 = :c1", mutationTable, 1003 mutationPointTable, mutationStatusTable, filesTable); 1004 auto insert_m = db.prepare(insert_m_sql); 1005 1006 foreach (mp; mps) { 1007 foreach (m; mp.cms) { 1008 auto rel_file = relativePath(mp.file, rel_dir).Path; 1009 insert_m.get.bind(":path", cast(string) rel_file); 1010 insert_m.get.bind(":off_begin", mp.offset.begin); 1011 insert_m.get.bind(":off_end", mp.offset.end); 1012 insert_m.get.bind(":c0", cast(long) m.id.c0); 1013 insert_m.get.bind(":c1", cast(long) m.id.c1); 1014 insert_m.get.bind(":kind", m.mut.kind); 1015 insert_m.get.execute; 1016 insert_m.get.reset; 1017 } 1018 } 1019 } 1020 1021 /** Remove all mutants points from the database. 1022 * 1023 * This removes all the mutants because of the cascade delete of the 1024 * tables. But it will keep the mutation statuses and thus the checksums 1025 * and the status of the code changes. 1026 * 1027 * This then mean that when mutations+mutation points are added back they 1028 * may reconnect with a mutation status. 1029 */ 1030 void removeAllMutationPoints() @trusted { 1031 enum sql = format!"DELETE FROM %s"(mutationPointTable); 1032 db.run(sql); 1033 } 1034 1035 /// ditto 1036 void removeAllFiles() @trusted { 1037 enum sql = format!"DELETE FROM %s"(filesTable); 1038 db.run(sql); 1039 } 1040 1041 /// Remove mutants that have no connection to a mutation point, orphened mutants. 1042 void removeOrphanedMutants() @trusted { 1043 enum sql = format!"DELETE FROM %s WHERE id NOT IN (SELECT st_id FROM %s)"( 1044 mutationStatusTable, mutationTable); 1045 db.run(sql); 1046 } 1047 1048 /** Add a link between the mutation and what test case killed it. 1049 * 1050 * Params: 1051 * id = ? 1052 * tcs = test cases to add 1053 */ 1054 void updateMutationTestCases(const MutationId id, const(TestCase)[] tcs) @trusted { 1055 if (tcs.length == 0) 1056 return; 1057 1058 immutable st_id = () { 1059 enum st_id_for_mutation_q = format("SELECT st_id FROM %s WHERE id=:id", mutationTable); 1060 auto stmt = db.prepare(st_id_for_mutation_q); 1061 stmt.get.bind(":id", cast(long) id); 1062 return stmt.get.execute.oneValue!long; 1063 }(); 1064 updateMutationTestCases(MutationStatusId(st_id), tcs); 1065 } 1066 1067 /** Add a link between the mutation and what test case killed it. 1068 * 1069 * Params: 1070 * id = ? 1071 * tcs = test cases to add 1072 */ 1073 void updateMutationTestCases(const MutationStatusId st_id, const(TestCase)[] tcs) @trusted { 1074 if (tcs.length == 0) 1075 return; 1076 1077 try { 1078 enum remove_old_sql = format("DELETE FROM %s WHERE st_id=:id", killedTestCaseTable); 1079 auto stmt = db.prepare(remove_old_sql); 1080 stmt.get.bind(":id", cast(long) st_id); 1081 stmt.get.execute; 1082 } catch (Exception e) { 1083 } 1084 1085 enum add_if_non_exist_tc_sql = format( 1086 "INSERT INTO %s (name) SELECT :name1 WHERE NOT EXISTS (SELECT * FROM %s WHERE name = :name2)", 1087 allTestCaseTable, allTestCaseTable); 1088 auto stmt_insert_tc = db.prepare(add_if_non_exist_tc_sql); 1089 1090 enum add_new_sql = format( 1091 "INSERT INTO %s (st_id, tc_id, location) SELECT :st_id,t1.id,:loc FROM %s t1 WHERE t1.name = :tc", 1092 killedTestCaseTable, allTestCaseTable); 1093 auto stmt_insert = db.prepare(add_new_sql); 1094 foreach (const tc; tcs) { 1095 try { 1096 stmt_insert_tc.get.reset; 1097 stmt_insert_tc.get.bind(":name1", tc.name); 1098 stmt_insert_tc.get.bind(":name2", tc.name); 1099 stmt_insert_tc.get.execute; 1100 1101 stmt_insert.get.reset; 1102 stmt_insert.get.bind(":st_id", cast(long) st_id); 1103 stmt_insert.get.bind(":loc", tc.location); 1104 stmt_insert.get.bind(":tc", tc.name); 1105 stmt_insert.get.execute; 1106 } catch (Exception e) { 1107 logger.warning(e.msg); 1108 } 1109 } 1110 } 1111 1112 /** Set detected test cases. 1113 * 1114 * This will replace those that where previously stored. 1115 * 1116 * Returns: ID of affected mutation statuses. 1117 */ 1118 MutationStatusId[] setDetectedTestCases(const(TestCase)[] tcs) @trusted { 1119 if (tcs.length == 0) 1120 return null; 1121 1122 auto mut_status_ids = appender!(MutationStatusId[])(); 1123 1124 immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string; 1125 internalAddDetectedTestCases(tcs, tmp_name); 1126 1127 immutable mut_st_id = format!"SELECT DISTINCT t1.st_id 1128 FROM %s t0, %s t1 1129 WHERE 1130 t0.name NOT IN (SELECT name FROM %s) AND 1131 t0.id = t1.tc_id"(allTestCaseTable, 1132 killedTestCaseTable, tmp_name); 1133 auto stmt = db.prepare(mut_st_id); 1134 foreach (res; stmt.get.execute) 1135 mut_status_ids.put(res.peek!long(0).MutationStatusId); 1136 1137 immutable remove_old_sql = format!"DELETE FROM %s WHERE name NOT IN (SELECT name FROM %s)"( 1138 allTestCaseTable, tmp_name); 1139 db.run(remove_old_sql); 1140 1141 db.run(format!"DROP TABLE %s"(tmp_name)); 1142 1143 return mut_status_ids.data; 1144 } 1145 1146 /** Add test cases to those that have been detected. 1147 * 1148 * They will be added if they are unique. 1149 */ 1150 void addDetectedTestCases(const(TestCase)[] tcs) @trusted { 1151 if (tcs.length == 0) 1152 return; 1153 1154 immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string; 1155 internalAddDetectedTestCases(tcs, tmp_name); 1156 db.run(format!"DROP TABLE %s"(tmp_name)); 1157 } 1158 1159 /// ditto. 1160 private void internalAddDetectedTestCases(const(TestCase)[] tcs, string tmp_tbl) @trusted { 1161 db.run(format!"CREATE TEMP TABLE %s (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"( 1162 tmp_tbl)); 1163 1164 immutable add_tc_sql = format!"INSERT INTO %s (name) VALUES(:name)"(tmp_tbl); 1165 auto insert_s = db.prepare(add_tc_sql); 1166 foreach (tc; tcs) { 1167 insert_s.get.bind(":name", tc.name); 1168 insert_s.get.execute; 1169 insert_s.get.reset; 1170 } 1171 1172 // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table 1173 //Q: What is happening here? 1174 // 1175 //A: Conceptually, we select all rows from table1 and for each row we 1176 //attempt to find a row in table2 with the same value for the name 1177 //column. If there is no such row, we just leave the table2 portion of 1178 //our result empty for that row. Then we constrain our selection by 1179 //picking only those rows in the result where the matching row does not 1180 //exist. Finally, We ignore all fields from our result except for the 1181 //name column (the one we are sure that exists, from table1). 1182 // 1183 //While it may not be the most performant method possible in all cases, 1184 //it should work in basically every database engine ever that attempts 1185 //to implement ANSI 92 SQL 1186 immutable 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"( 1187 allTestCaseTable, tmp_tbl, allTestCaseTable); 1188 db.run(add_missing_sql); 1189 } 1190 1191 /// Returns: detected test cases. 1192 TestCase[] getDetectedTestCases() @trusted { 1193 auto rval = appender!(TestCase[])(); 1194 db.run(select!AllTestCaseTbl).map!(a => TestCase(a.name)).copy(rval); 1195 return rval.data; 1196 } 1197 1198 /// Returns: detected test cases. 1199 TestCaseId[] getDetectedTestCaseIds() @trusted { 1200 auto rval = appender!(TestCaseId[])(); 1201 db.run(select!AllTestCaseTbl).map!(a => TestCaseId(a.id)).copy(rval); 1202 return rval.data; 1203 } 1204 1205 /// Returns: test cases that has killed zero mutants. 1206 TestCase[] getTestCasesWithZeroKills() @trusted { 1207 enum sql = format("SELECT t1.name FROM %s t1 WHERE t1.id NOT IN (SELECT tc_id FROM %s)", 1208 allTestCaseTable, killedTestCaseTable); 1209 1210 auto rval = appender!(TestCase[])(); 1211 auto stmt = db.prepare(sql); 1212 foreach (a; stmt.get.execute) 1213 rval.put(TestCase(a.peek!string(0))); 1214 1215 return rval.data; 1216 } 1217 1218 /** Guarantees that the there are no duplications of `TestCaseId`. 1219 * 1220 * Returns: test cases that has killed at least one mutant. 1221 */ 1222 TestCaseId[] getTestCasesWithAtLeastOneKill(const Mutation.Kind[] kinds) @trusted { 1223 immutable sql = format!"SELECT DISTINCT t1.id 1224 FROM %s t1, %s t2, %s t3 1225 WHERE 1226 t1.id = t2.tc_id AND 1227 t2.st_id == t3.st_id AND 1228 t3.kind IN (%(%s,%))"(allTestCaseTable, 1229 killedTestCaseTable, mutationTable, kinds.map!(a => cast(int) a)); 1230 1231 auto rval = appender!(TestCaseId[])(); 1232 auto stmt = db.prepare(sql); 1233 foreach (a; stmt.get.execute) 1234 rval.put(TestCaseId(a.peek!long(0))); 1235 1236 return rval.data; 1237 } 1238 1239 /// Returns: the name of the test case. 1240 string getTestCaseName(const TestCaseId id) @trusted { 1241 enum sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable); 1242 auto stmt = db.prepare(sql); 1243 stmt.get.bind(":id", cast(long) id); 1244 auto res = stmt.get.execute; 1245 return res.oneValue!string; 1246 } 1247 1248 /// Returns: stats about the test case. 1249 Nullable!TestCaseInfo getTestCaseInfo(const TestCase tc, const Mutation.Kind[] kinds) @trusted { 1250 const sql = format("SELECT sum(t2.time),count(t1.st_id) 1251 FROM %s t0, %s t1, %s t2, %s t3 1252 WHERE 1253 t0.name = :name AND 1254 t0.id = t1.tc_id AND 1255 t1.st_id = t2.id AND 1256 t2.id = t3.st_id AND 1257 t3.kind IN (%(%s,%))", allTestCaseTable, 1258 killedTestCaseTable, mutationStatusTable, mutationTable, 1259 kinds.map!(a => cast(int) a)); 1260 auto stmt = db.prepare(sql); 1261 stmt.get.bind(":name", tc.name); 1262 1263 typeof(return) rval; 1264 foreach (a; stmt.get.execute) 1265 rval = TestCaseInfo(a.peek!long(0).dur!"msecs", a.peek!long(1)); 1266 return rval; 1267 } 1268 1269 /// Returns: all test cases for the file and the mutants they killed. 1270 TestCaseInfo2[] getAllTestCaseInfo2(const FileId file, const Mutation.Kind[] kinds) @trusted { 1271 // row of test case name and mutation id. 1272 const sql = format("SELECT t0.name,t3.id 1273 FROM %s t0, %s t1, %s t2, %s t3, %s t4 1274 WHERE 1275 t0.id = t1.tc_id AND 1276 t1.st_id = t2.id AND 1277 t2.id = t3.st_id AND 1278 t4.id = :file_id AND 1279 t3.kind IN (%(%s,%))", allTestCaseTable, killedTestCaseTable, 1280 mutationStatusTable, mutationTable, filesTable, kinds.map!(a => cast(int) a)); 1281 auto stmt = db.prepare(sql); 1282 stmt.get.bind(":file_id", cast(long) file); 1283 1284 MutationId[][string] data; 1285 foreach (row; stmt.get.execute) { 1286 const name = row.peek!string(0); 1287 if (auto v = name in data) { 1288 *v ~= MutationId(row.peek!long(1)); 1289 } else { 1290 data[name] = [MutationId(row.peek!long(1))]; 1291 } 1292 } 1293 1294 auto app = appender!(TestCaseInfo2[])(); 1295 data.byKeyValue.map!(a => TestCaseInfo2(TestCase(a.key), a.value)).copy(app); 1296 return app.data; 1297 } 1298 1299 /// Returns: the test case. 1300 Nullable!TestCase getTestCase(const TestCaseId id) @trusted { 1301 enum sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable); 1302 auto stmt = db.prepare(sql); 1303 stmt.get.bind(":id", cast(long) id); 1304 1305 typeof(return) rval; 1306 foreach (res; stmt.get.execute) { 1307 rval = TestCase(res.peek!string(0)); 1308 } 1309 return rval; 1310 } 1311 1312 /// Returns: the test case id. 1313 Nullable!TestCaseId getTestCaseId(const TestCase tc) @trusted { 1314 enum sql = format!"SELECT id FROM %s WHERE name = :name"(allTestCaseTable); 1315 auto stmt = db.prepare(sql); 1316 stmt.get.bind(":name", tc.name); 1317 1318 typeof(return) rval; 1319 foreach (res; stmt.get.execute) { 1320 rval = TestCaseId(res.peek!long(0)); 1321 } 1322 return rval; 1323 } 1324 1325 /// The mutation ids are guaranteed to be sorted. 1326 /// Returns: the mutants the test case killed. 1327 MutationId[] getTestCaseMutantKills(const TestCaseId id, const Mutation.Kind[] kinds) @trusted { 1328 immutable sql = format!"SELECT t2.id 1329 FROM %s t1, %s t2 1330 WHERE 1331 t1.tc_id = :tid AND 1332 t1.st_id = t2.st_id AND 1333 t2.kind IN (%(%s,%)) 1334 ORDER BY 1335 t2.id"(killedTestCaseTable, 1336 mutationTable, kinds.map!(a => cast(int) a)); 1337 1338 auto rval = appender!(MutationId[])(); 1339 auto stmt = db.prepare(sql); 1340 stmt.get.bind(":tid", cast(long) id); 1341 foreach (a; stmt.get.execute) 1342 rval.put(MutationId(a.peek!long(0))); 1343 1344 return rval.data; 1345 } 1346 1347 /// Returns: test cases that killed the mutant. 1348 TestCase[] getTestCases(const MutationId id) @trusted { 1349 Appender!(TestCase[]) rval; 1350 1351 enum get_test_cases_sql = format!"SELECT t1.name,t2.location 1352 FROM %s t1, %s t2, %s t3 1353 WHERE 1354 t3.id = :id AND 1355 t3.st_id = t2.st_id AND 1356 t2.tc_id = t1.id"(allTestCaseTable, 1357 killedTestCaseTable, mutationTable); 1358 auto stmt = db.prepare(get_test_cases_sql); 1359 stmt.get.bind(":id", cast(long) id); 1360 foreach (a; stmt.get.execute) 1361 rval.put(TestCase(a.peek!string(0), a.peek!string(1))); 1362 1363 return rval.data; 1364 } 1365 1366 /** Returns: number of test cases 1367 */ 1368 long getNumOfTestCases() @trusted { 1369 enum num_test_cases_sql = format!"SELECT count(*) FROM %s"(allTestCaseTable); 1370 return db.execute(num_test_cases_sql).oneValue!long; 1371 } 1372 1373 /** Returns: test cases that killed other mutants at the same mutation point as `id`. 1374 */ 1375 TestCase[] getSurroundingTestCases(const MutationId id) @trusted { 1376 Appender!(TestCase[]) rval; 1377 1378 // TODO: optimize this. should be able to merge the two first queries to one. 1379 1380 // get the mutation point ID that id reside at 1381 long mp_id; 1382 { 1383 auto stmt = db.prepare(format!"SELECT mp_id FROM %s WHERE id=:id"(mutationTable)); 1384 stmt.get.bind(":id", cast(long) id); 1385 auto res = stmt.get.execute; 1386 if (res.empty) 1387 return null; 1388 mp_id = res.oneValue!long; 1389 } 1390 1391 // get all the mutation status ids at the mutation point 1392 long[] mut_st_ids; 1393 { 1394 auto stmt = db.prepare(format!"SELECT st_id FROM %s WHERE mp_id=:id"(mutationTable)); 1395 stmt.get.bind(":id", mp_id); 1396 auto res = stmt.get.execute; 1397 if (res.empty) 1398 return null; 1399 mut_st_ids = res.map!(a => a.peek!long(0)).array; 1400 } 1401 1402 // get all the test cases that are killed at the mutation point 1403 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 (%(%s,%))"( 1404 killedTestCaseTable, allTestCaseTable, mut_st_ids); 1405 auto stmt = db.prepare(get_test_cases_sql); 1406 foreach (a; stmt.get.execute) { 1407 rval.put(TestCase(a.peek!string(0), a.peek!string(1))); 1408 } 1409 1410 return rval.data; 1411 } 1412 1413 void removeTestCase(const Regex!char rex, const(Mutation.Kind)[] kinds) @trusted { 1414 immutable sql = format!"SELECT t1.id,t1.name FROM %s t1,%s t2, %s t3 WHERE t1.id = t2.tc_id AND t2.st_id = t3.st_id AND t3.kind IN (%(%s,%))"( 1415 allTestCaseTable, killedTestCaseTable, mutationTable, 1416 kinds.map!(a => cast(long) a)); 1417 auto stmt = db.prepare(sql); 1418 1419 auto del_stmt = db.prepare(format!"DELETE FROM %s WHERE id=:id"(allTestCaseTable)); 1420 foreach (row; stmt.get.execute) { 1421 string tc = row.peek!string(1); 1422 if (tc.matchFirst(rex).empty) 1423 continue; 1424 1425 long id = row.peek!long(0); 1426 del_stmt.get.bind(":id", id); 1427 del_stmt.get.execute; 1428 del_stmt.get.reset; 1429 } 1430 } 1431 1432 /// Returns: the context for the timeout algorithm. 1433 MutantTimeoutCtx getMutantTimeoutCtx() @trusted { 1434 foreach (res; db.run(select!MutantTimeoutCtx)) 1435 return res; 1436 return MutantTimeoutCtx.init; 1437 } 1438 1439 void putMutantTimeoutCtx(const MutantTimeoutCtx ctx) @trusted { 1440 db.run(delete_!MutantTimeoutCtx); 1441 db.run(insert!MutantTimeoutCtx.insert, ctx); 1442 } 1443 1444 void putMutantInTimeoutWorklist(const MutationStatusId id) @trusted { 1445 const sql = format!"INSERT OR IGNORE INTO %s (id) VALUES (:id)"(mutantTimeoutWorklistTable); 1446 auto stmt = db.prepare(sql); 1447 stmt.get.bind(":id", cast(long) id); 1448 stmt.get.execute; 1449 } 1450 1451 /** Remove all mutants that are in the worklist that do NOT have the 1452 * mutation status timeout. 1453 */ 1454 void reduceMutantTimeoutWorklist() @trusted { 1455 immutable sql = format!"DELETE FROM %1$s 1456 WHERE 1457 id IN (SELECT id FROM %2$s WHERE status != :status)"( 1458 mutantTimeoutWorklistTable, mutationStatusTable); 1459 auto stmt = db.prepare(sql); 1460 stmt.get.bind(":status", cast(ubyte) Mutation.Status.timeout); 1461 stmt.get.execute; 1462 } 1463 1464 /// Remove all mutants from the worklist. 1465 void clearMutantTimeoutWorklist() @trusted { 1466 immutable sql = format!"DELETE FROM %1$s"(mutantTimeoutWorklistTable); 1467 db.run(sql); 1468 } 1469 1470 /// Returns: the number of mutants in the worklist. 1471 long countMutantTimeoutWorklist() @trusted { 1472 immutable sql = format!"SELECT count(*) FROM %1$s"(mutantTimeoutWorklistTable); 1473 auto stmt = db.prepare(sql); 1474 auto res = stmt.get.execute(); 1475 return res.oneValue!long; 1476 } 1477 1478 /// Changes the status of mutants in the timeout worklist to unknown. 1479 void resetMutantTimeoutWorklist() @trusted { 1480 immutable sql = format!"UPDATE %1$s SET status=:st WHERE id IN (SELECT id FROM %2$s)"( 1481 mutationStatusTable, mutantTimeoutWorklistTable); 1482 auto stmt = db.prepare(sql); 1483 stmt.get.bind(":st", cast(ubyte) Mutation.Status.unknown); 1484 stmt.get.execute; 1485 } 1486 1487 /** Update the content of metadata tables with what has been added to the 1488 * raw table data. 1489 */ 1490 void updateMetadata() @trusted { 1491 db.run(format!"DELETE FROM %s"(srcMetadataTable)); 1492 db.run(format!"DELETE FROM %s"(nomutTable)); 1493 db.run(format!"DELETE FROM %s"(nomutDataTable)); 1494 1495 immutable nomut_tbl = "INSERT INTO %s 1496 SELECT 1497 t0.id mp_id, 1498 t1.line line, 1499 count(*) status 1500 FROM %s t0, %s t1 1501 WHERE 1502 t0.file_id = t1.file_id AND 1503 (t1.line BETWEEN t0.line AND t0.line_end) 1504 GROUP BY 1505 t0.id"; 1506 db.run(format!nomut_tbl(nomutTable, mutationPointTable, rawSrcMetadataTable)); 1507 1508 immutable src_metadata_sql = "INSERT INTO %s 1509 SELECT DISTINCT 1510 t0.id AS mut_id, 1511 t1.id AS st_id, 1512 t2.id AS mp_id, 1513 t3.id AS file_id, 1514 (SELECT count(*) FROM %s WHERE nomut.mp_id = t2.id) as nomut 1515 FROM %s t0, %s t1, %s t2, %s t3 1516 WHERE 1517 t0.mp_id = t2.id AND 1518 t0.st_id = t1.id AND 1519 t2.file_id = t3.id"; 1520 db.run(format!src_metadata_sql(srcMetadataTable, nomutTable, 1521 mutationTable, mutationStatusTable, mutationPointTable, filesTable)); 1522 1523 immutable nomut_data_tbl = "INSERT INTO %s 1524 SELECT 1525 t0.id as mut_id, 1526 t0.mp_id as mp_id, 1527 t1.line as line, 1528 t1.tag as tag, 1529 t1.comment as comment 1530 FROM %s t0, %s t1, %s t2 1531 WHERE 1532 t0.mp_id = t2.mp_id AND 1533 t1.line = t2.line"; 1534 db.run(format!nomut_data_tbl(nomutDataTable, mutationTable, 1535 rawSrcMetadataTable, nomutTable)); 1536 } 1537 } 1538 1539 private: 1540 1541 MarkedMutant make(MarkedMutantTbl m) { 1542 import dextool.plugin.mutate.backend.type; 1543 1544 return MarkedMutant(m.mutationStatusId.MutationStatusId, Checksum(m.checksum0, 1545 m.checksum1), m.mutationId.MutationId, SourceLoc(m.line, m.column), 1546 m.path.Path, m.toStatus.to!(Mutation.Status), m.time, 1547 m.rationale.Rationale, m.mutText); 1548 }