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 }