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 dependencies on internal modules. 11 It is intended to be reusable from the test suite. 12 13 The only acceptable dependency are: 14 * ../type.d 15 * ..backend/type.d 16 * ../database/type.d 17 * ../database/schema.d 18 */ 19 module dextool.plugin.mutate.backend.database.standalone; 20 21 import core.time : Duration; 22 import logger = std.experimental.logger; 23 24 import d2sqlite3 : sqlDatabase = Database; 25 26 import dextool.type : AbsolutePath, Path; 27 28 import dextool.plugin.mutate.backend.database.schema; 29 import dextool.plugin.mutate.backend.database.type; 30 31 /** Database wrapper with minimal dependencies. 32 */ 33 struct Database { 34 import std.conv : to; 35 import std.exception : collectException; 36 import std.typecons : Nullable; 37 import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, 38 Checksum; 39 40 sqlDatabase* db; 41 alias db this; 42 43 /** Create a database by either opening an existing or initializing a new. 44 * 45 * Params: 46 * db = path to the database 47 */ 48 static auto make(string db) @safe { 49 return Database(initializeDB(db)); 50 } 51 52 // Not movable. The database should only be passed around as a reference, 53 // if at all. 54 @disable this(this); 55 56 ~this() @trusted { 57 destroy(db); 58 } 59 60 /// If the file has already been analyzed. 61 bool isAnalyzed(const Path p) @trusted { 62 auto stmt = db.prepare("SELECT count(*) FROM files WHERE path=:path LIMIT 1"); 63 stmt.bind(":path", cast(string) p); 64 auto res = stmt.execute; 65 return res.oneValue!long != 0; 66 } 67 68 /// If the file has already been analyzed. 69 bool isAnalyzed(const Path p, const Checksum cs) @trusted { 70 auto stmt = db.prepare( 71 "SELECT count(*) FROM files WHERE path=:path AND checksum0=:cs0 AND checksum1=:cs1 LIMIT 1"); 72 stmt.bind(":path", cast(string) p); 73 stmt.bind(":cs0", cs.c0); 74 stmt.bind(":cs1", cs.c1); 75 auto res = stmt.execute; 76 return res.oneValue!long != 0; 77 } 78 79 Nullable!FileId getFileId(const Path p) @trusted { 80 auto stmt = db.prepare("SELECT id FROM files WHERE path=:path"); 81 stmt.bind(":path", cast(string) p); 82 auto res = stmt.execute; 83 84 typeof(return) rval; 85 if (!res.empty) { 86 rval = FileId(res.oneValue!long); 87 } 88 89 return rval; 90 } 91 92 /// Remove the file with all mutations that are coupled to it. 93 void removeFile(const Path p) @trusted { 94 auto stmt = db.prepare("DELETE FROM files WHERE path=:path"); 95 stmt.bind(":path", cast(string) p); 96 stmt.execute; 97 } 98 99 /// Returns: All files in the database as relative paths. 100 Path[] getFiles() @trusted { 101 import std.array : appender; 102 103 auto stmt = db.prepare("SELECT path from files"); 104 auto res = stmt.execute; 105 106 auto app = appender!(Path[]); 107 foreach (ref r; res) { 108 app.put(Path(r.peek!string(0))); 109 } 110 111 return app.data; 112 } 113 114 /** Update the status of a mutant. 115 * Params: 116 * id = ? 117 * st = ? 118 * d = time spent on veryfing the mutant 119 */ 120 void updateMutation(const MutationId id, const Mutation.Status st, 121 const Duration d, const(TestCase)[] tcs) @trusted { 122 auto stmt = db.prepare( 123 "UPDATE mutation SET status=:st,time=:time WHERE mutation.id == :id"); 124 stmt.bind(":st", st.to!long); 125 stmt.bind(":id", id.to!long); 126 stmt.bind(":time", d.total!"msecs"); 127 stmt.execute; 128 129 updateMutationTestCases(id, tcs); 130 } 131 132 /** Update the status of a mutant and broadcast the status to other mutants at that point. 133 * 134 * Params: 135 * bcast = mutants to broadcast the status to in addition to the id 136 */ 137 void updateMutationBroadcast(const MutationId id, const Mutation.Status st, 138 const Duration d, const(TestCase)[] tcs, const(Mutation.Kind)[] bcast) @trusted { 139 import std.algorithm : map; 140 import std.array : array; 141 import std.format : format; 142 143 if (bcast.length == 1) { 144 updateMutation(id, st, d, tcs); 145 return; 146 } 147 148 auto stmt = db.prepare("SELECT mp_id FROM mutation WHERE id=:id"); 149 stmt.bind(":id", id.to!long); 150 auto res = stmt.execute; 151 152 if (res.empty) 153 return; 154 long mp_id = res.front.peek!long(0); 155 156 stmt = db.prepare(format("SELECT id FROM mutation WHERE mp_id=:id AND kind IN (%(%s,%))", 157 bcast.map!(a => cast(int) a))); 158 stmt.bind(":id", mp_id); 159 res = stmt.execute; 160 161 if (res.empty) 162 return; 163 164 auto mut_ids = res.map!(a => a.peek!long(0).MutationId).array; 165 166 stmt = db.prepare(format("UPDATE mutation SET status=:st,time=:time WHERE id IN (%(%s,%))", 167 mut_ids.map!(a => cast(long) a))); 168 stmt.bind(":st", st.to!long); 169 stmt.bind(":time", d.total!"msecs"); 170 stmt.execute; 171 172 foreach (const mut_id; mut_ids) { 173 updateMutationTestCases(mut_id, tcs); 174 } 175 } 176 177 Nullable!MutationEntry getMutation(const MutationId id) nothrow @trusted { 178 import dextool.plugin.mutate.backend.type; 179 import dextool.type : FileName; 180 181 typeof(return) rval; 182 183 try { 184 auto stmt = db.prepare("SELECT 185 mutation.id, 186 mutation.kind, 187 mutation.time, 188 mutation_point.offset_begin, 189 mutation_point.offset_end, 190 mutation_point.line, 191 mutation_point.column, 192 files.path 193 FROM mutation,mutation_point,files 194 WHERE 195 mutation.id == :id AND 196 mutation.mp_id == mutation_point.id AND 197 mutation_point.file_id == files.id"); 198 stmt.bind(":id", cast(long) id); 199 auto res = stmt.execute; 200 if (res.empty) 201 return rval; 202 203 auto v = res.front; 204 205 auto mp = MutationPoint(Offset(v.peek!uint(3), v.peek!uint(4))); 206 mp.mutations = [Mutation(v.peek!long(1).to!(Mutation.Kind))]; 207 auto pkey = MutationId(v.peek!long(0)); 208 auto file = Path(FileName(v.peek!string(7))); 209 auto sloc = SourceLoc(v.peek!uint(5), v.peek!uint(6)); 210 211 import core.time : dur; 212 213 rval = MutationEntry(pkey, file, sloc, mp, v.peek!long(2).dur!"msecs"); 214 } 215 catch (Exception e) { 216 logger.warning(e.msg).collectException; 217 } 218 219 return rval; 220 } 221 222 /** Remove all mutations of kinds. 223 */ 224 void removeMutant(const Mutation.Kind[] kinds) @trusted { 225 import std.algorithm : map; 226 import std.format : format; 227 228 auto s = format("DELETE FROM mutation_point WHERE id IN (SELECT mp_id FROM mutation WHERE kind IN (%(%s,%)))", 229 kinds.map!(a => cast(int) a)); 230 auto stmt = db.prepare(s); 231 stmt.execute; 232 } 233 234 /** Reset all mutations of kinds with the status `st` to unknown. 235 */ 236 void resetMutant(const Mutation.Kind[] kinds, Mutation.Status st, Mutation.Status to_st) @trusted { 237 import std.algorithm : map; 238 import std.format : format; 239 240 auto s = format("UPDATE mutation SET status=%s WHERE status == %s AND kind IN (%(%s,%))", 241 to_st.to!long, st.to!long, kinds.map!(a => cast(int) a)); 242 auto stmt = db.prepare(s); 243 stmt.execute; 244 } 245 246 import dextool.plugin.mutate.backend.type; 247 248 alias aliveMutants = countMutants!(Mutation.Status.alive); 249 alias killedMutants = countMutants!(Mutation.Status.killed); 250 alias timeoutMutants = countMutants!(Mutation.Status.timeout); 251 alias unknownMutants = countMutants!(Mutation.Status.unknown); 252 alias killedByCompilerMutants = countMutants!(Mutation.Status.killedByCompiler); 253 254 private Nullable!MutationReportEntry countMutants(int status)(const Mutation.Kind[] kinds) nothrow @trusted { 255 import core.time : dur; 256 import std.algorithm : map; 257 import std.format : format; 258 259 enum query = format("SELECT count(*),sum(mutation.time) FROM mutation WHERE status==%s AND kind IN (%s)", 260 status, "%(%s,%)"); 261 262 typeof(return) rval; 263 try { 264 auto stmt = db.prepare(format(query, kinds.map!(a => cast(int) a))); 265 auto res = stmt.execute; 266 if (res.empty) 267 return rval; 268 rval = MutationReportEntry(res.front.peek!long(0), 269 res.front.peek!long(1).dur!"msecs"); 270 } 271 catch (Exception e) { 272 logger.warning(e.msg).collectException; 273 } 274 275 return rval; 276 } 277 278 void put(const Path p, Checksum cs) @trusted { 279 if (isAnalyzed(p)) 280 return; 281 282 auto stmt = db.prepare( 283 "INSERT INTO files (path, checksum0, checksum1) VALUES (:path, :checksum0, :checksum1)"); 284 stmt.bind(":path", cast(string) p); 285 stmt.bind(":checksum0", cast(long) cs.c0); 286 stmt.bind(":checksum1", cast(long) cs.c1); 287 stmt.execute; 288 } 289 290 /** Save mutation points found in a specific file. 291 * 292 * Note: this assumes a file is never added more than once. 293 * If it where ever to be the mutation points would be duplicated. 294 * 295 * trusted: the d2sqlite3 interface is assumed to work correctly when the 296 * data via bind is *ok*. 297 */ 298 void put(const(MutationPointEntry)[] mps, AbsolutePath rel_dir) @trusted { 299 import std.path : relativePath; 300 301 auto mp_stmt = db.prepare("INSERT INTO mutation_point (file_id, offset_begin, offset_end, line, column) VALUES (:fid, :begin, :end, :line, :column)"); 302 auto m_stmt = db.prepare( 303 "INSERT INTO mutation (mp_id, kind, status) VALUES (:mp_id, :kind, :status)"); 304 305 db.begin; 306 scope (success) 307 db.commit; 308 scope (failure) 309 db.rollback; 310 311 FileId[Path] file_ids; 312 foreach (a; mps) { 313 // remove mutation points that would never result in a mutation 314 if (a.mp.mutations.length == 0) 315 continue; 316 317 if (a.file is null) { 318 debug logger.trace("this should not happen. The file is null file"); 319 continue; 320 } 321 auto rel_file = relativePath(a.file, rel_dir).Path; 322 323 FileId id; 324 // assuming it is slow to lookup in the database so cache the lookups. 325 if (auto e = rel_file in file_ids) { 326 id = *e; 327 } else { 328 auto e = getFileId(rel_file); 329 if (e.isNull) { 330 // this only happens when the database is out of sync with 331 // the filesystem or absolute paths are used. 332 logger.errorf("File '%s' do not exist in the database", 333 rel_file).collectException; 334 continue; 335 } 336 id = e; 337 file_ids[rel_file] = id; 338 } 339 340 // fails if the constraint for mutation_point is violated 341 // TODO still a bit slow because this generates many exceptions. 342 try { 343 const long mp_id = () { 344 scope (exit) 345 mp_stmt.reset; 346 mp_stmt.bind(":fid", cast(long) id); 347 mp_stmt.bind(":begin", a.mp.offset.begin); 348 mp_stmt.bind(":end", a.mp.offset.end); 349 mp_stmt.bind(":line", a.sloc.line); 350 mp_stmt.bind(":column", a.sloc.column); 351 mp_stmt.execute; 352 return db.lastInsertRowid; 353 }(); 354 355 m_stmt.bind(":mp_id", mp_id); 356 foreach (k; a.mp.mutations) { 357 m_stmt.bind(":kind", k.kind); 358 m_stmt.bind(":status", k.status); 359 m_stmt.execute; 360 m_stmt.reset; 361 } 362 } 363 catch (Exception e) { 364 } 365 } 366 } 367 368 /** Add a link between the mutation and what test case killed it. 369 Params: 370 id = ? 371 tcs = test cases to add 372 */ 373 void updateMutationTestCases(const MutationId id, const(TestCase)[] tcs) @trusted { 374 import std.format : format; 375 376 if (tcs.length == 0) 377 return; 378 379 immutable mut_id = id.to!string; 380 381 try { 382 immutable remove_old_sql = format("DELETE FROM %s WHERE mut_id=:id", testCaseTable); 383 auto stmt = db.prepare(remove_old_sql); 384 stmt.bind(":id", mut_id); 385 stmt.execute; 386 } 387 catch (Exception e) { 388 } 389 390 immutable add_new_sql = format("INSERT INTO %s (mut_id, test_case) VALUES(:mut_id, :tc)", 391 testCaseTable); 392 foreach (const tc; tcs) { 393 try { 394 auto stmt = db.prepare(add_new_sql); 395 stmt.bind(":mut_id", mut_id); 396 stmt.bind(":tc", cast(string) tc); 397 stmt.execute; 398 } 399 catch (Exception e) { 400 logger.warning(e.msg); 401 } 402 } 403 } 404 405 /** Returns: test cases that killed the mutant. 406 */ 407 TestCase[] getTestCases(const MutationId id) @trusted { 408 import std.array : Appender; 409 import std.format : format; 410 411 Appender!(TestCase[]) rval; 412 413 immutable get_test_cases_sql = format("SELECT test_case FROM %s WHERE mut_id=:id", 414 testCaseTable); 415 auto stmt = db.prepare(get_test_cases_sql); 416 stmt.bind(":id", cast(long) id); 417 foreach (a; stmt.execute) { 418 rval.put(TestCase(a.peek!string(0))); 419 } 420 421 return rval.data; 422 } 423 424 /** Returns: test cases that killed other mutants at the same mutation point as `id`. 425 */ 426 TestCase[] getSurroundingTestCases(const MutationId id) @trusted { 427 import std.algorithm : map; 428 import std.array : Appender, array; 429 import std.format : format; 430 431 Appender!(TestCase[]) rval; 432 433 // TODO: optimize this. should be able to merge the two first queries to one. 434 435 // get the mutation point ID that id reside at 436 long mp_id; 437 { 438 auto stmt = db.prepare(format("SELECT mp_id FROM %s WHERE id=:id", mutationTable)); 439 stmt.bind(":id", cast(long) id); 440 auto res = stmt.execute; 441 if (res.empty) 442 return null; 443 mp_id = res.oneValue!long; 444 } 445 446 // get all the mutation ids at the mutation point 447 long[] mut_ids; 448 { 449 auto stmt = db.prepare(format("SELECT id FROM %s WHERE mp_id=:id", mutationTable)); 450 stmt.bind(":id", mp_id); 451 auto res = stmt.execute; 452 if (res.empty) 453 return null; 454 mut_ids = res.map!(a => a.peek!long(0)).array; 455 } 456 457 // get all the test cases that are killed at the mutation point 458 immutable get_test_cases_sql = format("SELECT test_case FROM %s WHERE mut_id IN (%(%s,%))", 459 testCaseTable, mut_ids); 460 auto stmt = db.prepare(get_test_cases_sql); 461 foreach (a; stmt.execute) { 462 rval.put(TestCase(a.peek!string(0))); 463 } 464 465 return rval.data; 466 } 467 468 import std.regex : Regex; 469 470 void removeTestCase(const Regex!char rex, const(Mutation.Kind)[] kinds) @trusted { 471 import std.algorithm : map; 472 import std.format : format; 473 import std.regex : matchFirst; 474 475 immutable sql = format( 476 "SELECT test_case.id,test_case.test_case FROM %s,%s WHERE %s.mut_id=%s.id AND %s.kind IN (%(%s,%))", 477 testCaseTable, mutationTable, 478 testCaseTable, mutationTable, mutationTable, kinds.map!(a => cast(long) a)); 479 auto stmt = db.prepare(sql); 480 481 foreach (row; stmt.execute) { 482 string tc = row.peek!string(1); 483 if (tc.matchFirst(rex).empty) 484 continue; 485 486 long id = row.peek!long(0); 487 auto del_stmt = db.prepare(format("DELETE FROM %s WHERE id=:id", testCaseTable)); 488 del_stmt.bind(":id", id); 489 del_stmt.execute; 490 } 491 } 492 }