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 }