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