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