1 /** 2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 */ 6 module dextool.plugin.mutate.backend.test_mutant.coverage; 7 8 import core.time : Duration; 9 import logger = std.experimental.logger; 10 import std.algorithm : map, filter, sort; 11 import std.array : array, appender, empty; 12 import std.exception : collectException; 13 import std.stdio : File; 14 import std.typecons : tuple, Tuple; 15 16 import blob_model; 17 import miniorm; 18 import my.fsm : next, act; 19 import my.optional; 20 import my.path; 21 import my.set; 22 import sumtype; 23 24 static import my.fsm; 25 26 import dextool.plugin.mutate.backend.database : CovRegion, CoverageRegionId, FileId; 27 import dextool.plugin.mutate.backend.database : Database; 28 import dextool.plugin.mutate.backend.interface_ : FilesysIO, Blob; 29 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, TestResult; 30 import dextool.plugin.mutate.backend.type : Mutation, Language; 31 import dextool.plugin.mutate.type : ShellCommand, UserRuntime; 32 33 @safe: 34 35 struct CoverageDriver { 36 static struct None { 37 } 38 39 static struct Initialize { 40 } 41 42 static struct InitializeRoots { 43 bool hasRoot; 44 } 45 46 static struct SaveOriginal { 47 } 48 49 static struct Instrument { 50 } 51 52 static struct Compile { 53 bool error; 54 } 55 56 static struct Run { 57 // something happend, throw away the result. 58 bool error; 59 60 CovEntry[] covMap; 61 } 62 63 static struct SaveToDb { 64 CovEntry[] covMap; 65 } 66 67 static struct Restore { 68 } 69 70 static struct Done { 71 } 72 73 alias Fsm = my.fsm.Fsm!(None, Initialize, InitializeRoots, SaveOriginal, 74 Instrument, Compile, Run, SaveToDb, Restore, Done); 75 76 private { 77 Fsm fsm; 78 bool isRunning_ = true; 79 80 // If an error has occurd that should be signaled to the user of the 81 // coverage driver. 82 bool error_; 83 84 // Write the instrumented source code to .cov.<ext> for separate 85 // inspection. 86 bool log; 87 88 FilesysIO fio; 89 Database* db; 90 91 ShellCommand buildCmd; 92 Duration buildCmdTimeout; 93 94 AbsolutePath[] restore; 95 Language[AbsolutePath] lang; 96 97 /// Runs the test commands. 98 TestRunner* runner; 99 100 CovRegion[][AbsolutePath] regions; 101 102 // a map of incrementing numbers from 0 which map to the global, unique 103 // ID of the region. 104 CoverageRegionId[long] localId; 105 106 // the files to inject the code that setup the coverage map. 107 Set!AbsolutePath roots; 108 } 109 110 this(FilesysIO fio, Database* db, TestRunner* runner, UserRuntime[] userRuntimeCtrl, 111 ShellCommand buildCmd, Duration buildCmdTimeout, bool log) { 112 this.fio = fio; 113 this.db = db; 114 this.runner = runner; 115 this.buildCmd = buildCmd; 116 this.buildCmdTimeout = buildCmdTimeout; 117 this.log = log; 118 119 foreach (a; userRuntimeCtrl) { 120 auto p = fio.toAbsoluteRoot(a.file); 121 roots.add(p); 122 lang[p] = a.lang; 123 } 124 125 if (logger.globalLogLevel == logger.LogLevel.trace) 126 fsm.logger = (string s) { logger.trace(s); }; 127 } 128 129 static void execute_(ref CoverageDriver self) @trusted { 130 self.fsm.next!((None a) => Initialize.init, 131 (Initialize a) => InitializeRoots.init, (InitializeRoots a) { 132 if (a.hasRoot) 133 return fsm(SaveOriginal.init); 134 return fsm(Done.init); 135 }, (SaveOriginal a) => Instrument.init, (Instrument a) => Compile.init, (Compile a) { 136 if (a.error) 137 return fsm(Restore.init); 138 return fsm(Run.init); 139 }, (Run a) { 140 if (a.error) 141 return fsm(Restore.init); 142 return fsm(SaveToDb(a.covMap)); 143 }, (SaveToDb a) => Restore.init, (Restore a) => Done.init, (Done a) => a); 144 145 self.fsm.act!self; 146 } 147 148 nothrow: 149 void execute() { 150 try { 151 execute_(this); 152 } catch (Exception e) { 153 isRunning_ = false; 154 error_ = true; 155 logger.warning(e.msg).collectException; 156 } 157 } 158 159 bool isRunning() { 160 return isRunning_; 161 } 162 163 bool hasFatalError() { 164 return error_; 165 } 166 167 void opCall(None data) { 168 } 169 170 void opCall(Initialize data) { 171 foreach (a; spinSql!(() => db.getCoverageMap).byKeyValue 172 .map!(a => tuple(spinSql!(() => db.getFile(a.key)), a.value, 173 spinSql!(() => db.getFileIdLanguage(a.key)))) 174 .filter!(a => !a[0].isNull) 175 .map!(a => tuple(a[0].get, a[1], a[2].orElse(Language.cpp)))) { 176 try { 177 auto p = fio.toAbsoluteRoot(a[0]); 178 regions[p] = a[1]; 179 lang[p] = a[2]; 180 } catch (Exception e) { 181 logger.warning(e.msg).collectException; 182 } 183 } 184 185 logger.tracef("%s files to instrument", regions.length).collectException; 186 } 187 188 void opCall(ref InitializeRoots data) { 189 if (roots.empty) { 190 auto rootIds = () { 191 auto tmp = spinSql!(() => db.getRootFiles); 192 if (tmp.empty) { 193 // no root found, inject instead in all instrumented files and 194 // "hope for the best". 195 tmp = spinSql!(() => db.getCoverageMap).byKey.array; 196 } 197 return tmp; 198 }(); 199 200 foreach (id; rootIds) { 201 try { 202 auto p = fio.toAbsoluteRoot(spinSql!(() => db.getFile(id)).get); 203 lang[p] = spinSql!(() => db.getFileIdLanguage(id)).orElse(Language.init); 204 roots.add(p); 205 } catch (Exception e) { 206 logger.warning(e.msg).collectException; 207 } 208 } 209 } 210 211 foreach (p; roots.toRange) { 212 try { 213 if (p !in regions) { 214 // add a dummy such that the instrumentation state do not 215 // need a special case for if no root is being 216 // instrumented. 217 regions[p] = (CovRegion[]).init; 218 } 219 } catch (Exception e) { 220 logger.warning(e.msg).collectException; 221 } 222 } 223 224 data.hasRoot = !roots.empty; 225 226 if (regions.empty) { 227 logger.info("No files to gather coverage data from").collectException; 228 } else if (roots.empty) { 229 logger.warning("No root file found to inject the coverage instrumentation runtime in") 230 .collectException; 231 } 232 } 233 234 void opCall(SaveOriginal data) { 235 try { 236 restore = regions.byKey.array; 237 } catch (Exception e) { 238 isRunning_ = false; 239 logger.warning(e.msg).collectException; 240 } 241 } 242 243 void opCall(Instrument data) { 244 import std.path : extension, stripExtension; 245 246 Blob makeInstrumentation(Blob original, CovRegion[] regions, Language lang, Edit[] extra) { 247 auto edits = appender!(Edit[])(); 248 edits.put(extra); 249 foreach (a; regions) { 250 long id = cast(long) localId.length; 251 localId[id] = a.id; 252 edits.put(new Edit(Interval(a.region.begin, a.region.begin), 253 makeInstrCode(id, lang))); 254 } 255 auto m = merge(original, edits.data); 256 return change(new Blob(original.uri, original.content), m.edits); 257 } 258 259 try { 260 // sort by filename to enforce that the IDs are stable. 261 foreach (a; regions.byKeyValue.array.sort!((a, b) => a.key < b.key)) { 262 auto f = fio.makeInput(a.key); 263 auto extra = () { 264 if (a.key in roots) { 265 logger.info("Injecting coverage runtime in ", a.key); 266 return makeRootImpl(f.content.length); 267 } 268 return makeHdr; 269 }(); 270 271 logger.infof("Coverage instrumenting %s regions in %s", a.value.length, a.key); 272 auto instr = makeInstrumentation(f, a.value, lang[a.key], extra); 273 fio.makeOutput(a.key).write(instr); 274 275 if (log) { 276 const ext = a.key.toString.extension; 277 const l = AbsolutePath(a.key.toString.stripExtension ~ ".cov" ~ ext); 278 fio.makeOutput(l).write(instr); 279 } 280 } 281 } catch (Exception e) { 282 logger.warning(e.msg).collectException; 283 error_ = true; 284 } 285 286 // release back to GC 287 regions = null; 288 } 289 290 void opCall(ref Compile data) { 291 import dextool.plugin.mutate.backend.test_mutant.common : compile; 292 293 try { 294 logger.info("Compiling instrumented source code"); 295 296 compile(buildCmd, buildCmdTimeout, true).match!((Mutation.Status a) { 297 data.error = true; 298 }, (bool success) { data.error = !success; }); 299 } catch (Exception e) { 300 data.error = true; 301 logger.warning(e.msg).collectException; 302 } 303 } 304 305 void opCall(ref Run data) @trusted { 306 import std.file : remove; 307 import std.range : repeat; 308 import my.random; 309 import my.xdg : makeXdgRuntimeDir; 310 311 try { 312 logger.info("Gathering runtime coverage data"); 313 314 // TODO: make this a configurable parameter? 315 const dir = makeXdgRuntimeDir(AbsolutePath("/dev/shm")); 316 const covMapFname = AbsolutePath(dir ~ randomId(20)); 317 318 createCovMap(covMapFname, cast(long) localId.length); 319 scope (exit) 320 () { remove(covMapFname.toString); }(); 321 322 runner.env[dextoolCovMapKey] = covMapFname.toString; 323 scope (exit) 324 runner.env.remove(dextoolCovMapKey); 325 326 auto res = runner.run; 327 if (res.status != TestResult.Status.passed) { 328 logger.info( 329 "An error occurred when executing instrumented binaries to gather coverage information"); 330 logger.info("This is not a fatal error. Continuing without coverage information"); 331 data.error = true; 332 return; 333 } 334 335 data.covMap = readCovMap(covMapFname, cast(long) localId.length); 336 } catch (Exception e) { 337 data.error = true; 338 logger.warning(e.msg).collectException; 339 } 340 } 341 342 void opCall(SaveToDb data) { 343 logger.info("Saving coverage data to database").collectException; 344 void save() @trusted { 345 auto trans = db.transaction; 346 foreach (a; data.covMap) { 347 db.putCoverageInfo(localId[a.id], a.status); 348 } 349 db.updateCoverageTimeStamp; 350 trans.commit; 351 } 352 353 spinSql!(save); 354 } 355 356 void opCall(Restore data) { 357 import dextool.plugin.mutate.backend.test_mutant.common : restoreFiles; 358 359 try { 360 restoreFiles(restore, fio); 361 } catch (Exception e) { 362 error_ = true; 363 logger.error(e.msg).collectException; 364 } 365 } 366 367 void opCall(Done data) { 368 isRunning_ = false; 369 } 370 } 371 372 private: 373 374 import dextool.plugin.mutate.backend.resource : coverageMapHdr, coverageMapImpl; 375 376 immutable dextoolCovMapKey = "DEXTOOL_COVMAP"; 377 378 struct CovEntry { 379 long id; 380 bool status; 381 } 382 383 const(ubyte)[] makeInstrCode(long id, Language l) { 384 import std.format : format; 385 386 final switch (l) { 387 case Language.assumeCpp: 388 goto case; 389 case Language.cpp: 390 return cast(const(ubyte)[]) format!"::dextool_cov(%s);"(id + 1); 391 case Language.c: 392 return cast(const(ubyte)[]) format!"dextool_cov(%s);"(id + 1); 393 } 394 } 395 396 Edit[] makeRootImpl(ulong end) { 397 return [ 398 makeHdr[0], 399 new Edit(Interval(end, end), cast(const(ubyte)[]) coverageMapImpl) 400 ]; 401 } 402 403 Edit[] makeHdr() { 404 return [new Edit(Interval(0, 0), cast(const(ubyte)[]) coverageMapHdr)]; 405 } 406 407 void createCovMap(const AbsolutePath fname, const long localIdSz) { 408 const size_t K = 1024; 409 // create a margin of 1K in case something goes awry. 410 const allocSz = 1 + localIdSz + K; 411 412 auto covMap = File(fname.toString, "w"); 413 414 ubyte[K] zeroes; 415 for (size_t i; i < allocSz; i += zeroes.length) { 416 covMap.rawWrite(zeroes); 417 } 418 } 419 420 // TODO: should check if anything is written to the extra bytes at the end. if 421 // there are data there then something is wrong and the coverage map should be 422 // discarded. 423 CovEntry[] readCovMap(const AbsolutePath fname, const long localIdSz) { 424 auto rval = appender!(CovEntry[])(); 425 426 auto covMap = File(fname.toString); 427 428 // TODO: read multiple IDs at a time to speed up. 429 ubyte[1] buf; 430 431 // check that at least one test has executed and thus set the first byte. 432 auto r = covMap.rawRead(buf); 433 if (r[0] == 0) { 434 logger.info("No coverage instrumented binaries executed"); 435 return typeof(return).init; 436 } 437 438 foreach (i; 0 .. localIdSz) { 439 r = covMap.rawRead(buf); 440 // something is wrong. 441 if (r.empty) 442 return typeof(return).init; 443 444 rval.put(CovEntry(cast(long) i, r[0] == 1)); 445 } 446 447 return rval.data; 448 }