1 /**
2 Date: 2016, Joakim Brännström
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 autobuild;
7 
8 import std.algorithm : map, splitter, filter, among, each, canFind, count;
9 import std.array : array;
10 import std.file : thisExePath, exists, mkdir, remove, dirEntries, SpanMode, chdir;
11 import std.format : format;
12 import std.path : buildPath, dirName, extension;
13 import std.process : execute, spawnShell, Config, wait;
14 import std.range : only, chain, take;
15 import std.stdio : stdin, writeln, File, write, writefln;
16 import std.string : splitLines, toStringz, fromStringz;
17 import std.typecons : Flag, Yes, No, Tuple;
18 
19 Flag!"SignalInterrupt" signalInterrupt;
20 Flag!"TestsPassed" signalExitStatus;
21 
22 bool skipStaticAnalysis = true;
23 
24 /// Tag a string as a path and make it absolute+normalized.
25 struct Path {
26     import std.path : absolutePath, buildNormalizedPath;
27 
28     private string value_;
29     alias value this;
30 
31     this(Path p) {
32         value_ = p.value_;
33     }
34 
35     this(string p) {
36         value_ = p.absolutePath.buildNormalizedPath;
37     }
38 
39     string value() @safe pure nothrow const @nogc {
40         return value_;
41     }
42 
43     void opAssign(string rhs) @safe pure {
44         value_ = rhs.absolutePath.buildNormalizedPath;
45     }
46 
47     void opAssign(typeof(this) rhs) @safe pure nothrow {
48         value_ = rhs.value_;
49     }
50 
51     string toString() @safe pure nothrow const @nogc {
52         return value_;
53     }
54 }
55 
56 enum Color {
57     red,
58     green,
59     yellow,
60     cancel
61 }
62 
63 enum Status {
64     Fail,
65     Warn,
66     Ok,
67     Run
68 }
69 
70 auto sourcePath() {
71     // those that are supported by the developer of Dextool
72 
73     // dfmt off
74     return only(
75                 "libs",
76                 "plugin",
77                 "source",
78                )
79         .map!(a => buildPath(thisExePath.dirName, a))
80         .array();
81     // dfmt on
82 }
83 
84 auto gitHEAD() {
85     // Initial commit: diff against an empty tree object
86     string against = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
87 
88     auto res = execute("git rev-parse --verify HEAD");
89     if (res.status == 0) {
90         against = res.output;
91     }
92 
93     return against;
94 }
95 
96 auto gitChangdedFiles(string[] file_extensions) {
97     string[] a;
98     a ~= "git";
99     a ~= "diff-index";
100     a ~= "--name-status";
101     a ~= ["--cached", gitHEAD];
102 
103     auto res = execute(a);
104     if (res.status != 0) {
105         writeln("error: ", res.output);
106     }
107 
108     // dfmt off
109     return res.output
110         .splitLines
111         .map!(a => a.splitter.array())
112         .filter!(a => a.length == 2)
113         .filter!(a => a[0].among("M", "A"))
114         .filter!(a => canFind(file_extensions, extension(a[1])))
115         .map!(a => a[1]);
116     // dfmt on
117 }
118 
119 void print(T...)(Color c, T args) {
120     static immutable string[] escCodes = [
121         "\033[31;1m", "\033[32;1m", "\033[33;1m", "\033[0;;m"
122     ];
123     write(escCodes[c], args, escCodes[Color.cancel]);
124 }
125 
126 void println(T...)(Color c, T args) {
127     static immutable string[] escCodes = [
128         "\033[31;1m", "\033[32;1m", "\033[33;1m", "\033[0;;m"
129     ];
130     writeln(escCodes[c], args, escCodes[Color.cancel]);
131 }
132 
133 void printStatus(T...)(Status s, T args) {
134     Color c;
135     string txt;
136 
137     final switch (s) {
138     case Status.Ok:
139         c = Color.green;
140         txt = "[  OK ] ";
141         break;
142     case Status.Run:
143         c = Color.yellow;
144         txt = "[ RUN ] ";
145         break;
146     case Status.Fail:
147         c = Color.red;
148         txt = "[ FAIL] ";
149         break;
150     case Status.Warn:
151         c = Color.red;
152         txt = "[ WARN] ";
153         break;
154     }
155 
156     print(c, txt);
157     writeln(args);
158 }
159 
160 void playSound(Flag!"Positive" positive) nothrow {
161     static import std.stdio;
162     import std.process;
163 
164     static Pid last_pid;
165 
166     try {
167         auto devnull = std.stdio.File("/dev/null", "w");
168 
169         if (last_pid !is null && last_pid.processID != 0) {
170             // cleanup possible zombie process
171             last_pid.wait;
172         }
173 
174         auto a = ["mplayer", "-nostop-xscreensaver"];
175         if (positive)
176             a ~= "/usr/share/sounds/KDE-Sys-App-Positive.ogg";
177         else
178             a ~= "/usr/share/sounds/KDE-Sys-App-Negative.ogg";
179 
180         last_pid = spawnProcess(a, std.stdio.stdin, devnull, devnull);
181     } catch (ProcessException ex) {
182     } catch (Exception ex) {
183     }
184 }
185 
186 bool sanityCheck() {
187     if (!exists("dub.sdl")) {
188         writeln("Missing dub.sdl");
189         return false;
190     }
191 
192     return true;
193 }
194 
195 void consoleToFile(Path fname, string console) {
196     writeln("console log written to -> ", fname);
197 
198     auto f = File(fname.toString, "w");
199     f.write(console);
200 }
201 
202 Path cmakeDir() {
203     return buildPath(thisExePath.dirName, "build").Path;
204 }
205 
206 void setup() {
207     //echoOn;
208 
209     if (!exists("build")) {
210         mkdir("build");
211     }
212 
213     auto r = execute([
214             "cmake", "-DCMAKE_BUILD_TYPE=Debug", "-DBUILD_TEST=ON", ".."
215             ], null, Config.none, size_t.max, cmakeDir);
216     writeln(r.output);
217 
218     import core.stdc.signal;
219 
220     signal(SIGINT, &handleSIGINT);
221 }
222 
223 extern (C) void handleSIGINT(int sig) nothrow @nogc @system {
224     .signalInterrupt = Yes.SignalInterrupt;
225 }
226 
227 void cleanup(Flag!"keepCoverage" keep_cov) {
228     import std.algorithm : predSwitch;
229 
230     printStatus(Status.Run, "Cleanup");
231     scope (failure)
232         printStatus(Status.Fail, "Cleanup");
233 
234     // dfmt off
235     chain(
236           dirEntries(".", "trace.*", SpanMode.shallow).map!(a => Path(a)).array(),
237           keep_cov.predSwitch(Yes.keepCoverage, string[].init.map!(a => Path(a)).array(),
238                               No.keepCoverage, dirEntries(".", "*.lst", SpanMode.shallow).map!(a => Path(a)).array())
239          )
240         .each!(a => remove(a));
241     // dfmt on
242 
243     printStatus(Status.Ok, "Cleanup");
244 }
245 
246 /** Call appropriate function for for the state.
247  *
248  * Generate calls to functions of fsm based on st.
249  *
250  * Params:
251  *  fsm = object with methods with prefix st_
252  *  st = current state
253  */
254 auto GenerateFsmAction(T, TEnum)(ref T fsm, TEnum st) {
255     import std.traits;
256 
257     final switch (st) {
258         foreach (e; EnumMembers!TEnum) {
259             mixin(format(q{
260                          case %s.%s.%s:
261                            fsm.state%s();
262                            break;
263 
264                          }, typeof(fsm).stringof, TEnum.stringof, e, e));
265         }
266     }
267 }
268 
269 /// Moore FSM
270 /// Exceptions are clearly documented with // FSM exception: REASON
271 struct Fsm {
272     enum State {
273         Init,
274         Reset,
275         Wait,
276         Start,
277         Ut_run,
278         Ut_skip,
279         Debug_build,
280         Integration_test,
281         Test_passed,
282         Test_failed,
283         Doc_check_counter,
284         Doc_build,
285         Sloc_check_counter,
286         Slocs,
287         AudioStatus,
288         ExitOrRestart,
289         Exit
290     }
291 
292     State st;
293     Path[] inotify_paths;
294 
295     Flag!"utDebug" flagUtDebug;
296 
297     // Signals used to determine next state
298     Flag!"UtTestPassed" flagUtTestPassed;
299     Flag!"CompileError" flagCompileError;
300     Flag!"TotalTestPassed" flagTotalTestPassed;
301     int docCount;
302     int analyzeCount;
303 
304     alias ErrorMsg = Tuple!(Path, "fname", string, "msg", string, "output");
305     ErrorMsg[] testErrorLog;
306 
307     void run(Path[] inotify_paths, Flag!"Travis" travis,
308             Flag!"utDebug" ut_debug, Flag!"utSkip" ut_skip) {
309         this.inotify_paths = inotify_paths;
310         this.flagUtDebug = ut_debug;
311 
312         while (!signalInterrupt) {
313             debug {
314                 writeln("State ", st.to!string);
315             }
316 
317             GenerateFsmAction(this, st);
318 
319             updateTotalTestStatus();
320 
321             st = Fsm.next(st, docCount, analyzeCount, flagUtTestPassed,
322                     flagCompileError, flagTotalTestPassed, travis, ut_skip);
323         }
324     }
325 
326     void updateTotalTestStatus() {
327         if (testErrorLog.length != 0) {
328             flagTotalTestPassed = No.TotalTestPassed;
329         } else if (flagUtTestPassed == No.UtTestPassed) {
330             flagTotalTestPassed = No.TotalTestPassed;
331         } else if (flagCompileError == Yes.CompileError) {
332             flagTotalTestPassed = No.TotalTestPassed;
333         } else {
334             flagTotalTestPassed = Yes.TotalTestPassed;
335         }
336     }
337 
338     static State next(State st, int docCount, int analyzeCount,
339             Flag!"UtTestPassed" flagUtTestPassed, Flag!"CompileError" flagCompileError,
340             Flag!"TotalTestPassed" flagTotalTestPassed, Flag!"Travis" travis,
341             Flag!"utSkip" ut_skip) {
342         auto next_ = st;
343 
344         final switch (st) {
345         case State.Init:
346             next_ = State.Start;
347             break;
348         case State.AudioStatus:
349             next_ = State.Reset;
350             break;
351         case State.Reset:
352             next_ = State.Wait;
353             break;
354         case State.Wait:
355             next_ = State.Start;
356             break;
357         case State.Start:
358             next_ = State.Ut_run;
359             if (ut_skip) {
360                 next_ = State.Ut_skip;
361             }
362             break;
363         case State.Ut_run:
364             next_ = State.ExitOrRestart;
365             if (flagUtTestPassed)
366                 next_ = State.Debug_build;
367             break;
368         case State.Ut_skip:
369             next_ = State.Debug_build;
370             break;
371         case State.Debug_build:
372             next_ = State.Integration_test;
373             if (flagCompileError)
374                 next_ = State.ExitOrRestart;
375             break;
376         case State.Integration_test:
377             next_ = State.ExitOrRestart;
378             if (flagTotalTestPassed)
379                 next_ = State.Test_passed;
380             else
381                 next_ = State.Test_failed;
382             break;
383         case State.Test_passed:
384             next_ = State.Doc_check_counter;
385             break;
386         case State.Test_failed:
387             next_ = State.ExitOrRestart;
388             break;
389         case State.Doc_check_counter:
390             next_ = State.ExitOrRestart;
391             if (docCount >= 10 && !travis)
392                 next_ = State.Doc_build;
393             break;
394         case State.Doc_build:
395             next_ = State.Sloc_check_counter;
396             break;
397         case State.Sloc_check_counter:
398             next_ = State.ExitOrRestart;
399             if (analyzeCount >= 10) {
400                 next_ = State.Slocs;
401             }
402             break;
403         case State.Slocs:
404             next_ = State.ExitOrRestart;
405             break;
406         case State.ExitOrRestart:
407             next_ = State.AudioStatus;
408             if (travis) {
409                 next_ = State.Exit;
410             }
411             break;
412         case State.Exit:
413             break;
414         }
415 
416         return next_;
417     }
418 
419     static void printExitStatus(T...)(int status, T args) {
420         if (status == 0)
421             printStatus(Status.Ok, args);
422         else
423             printStatus(Status.Fail, args);
424     }
425 
426     void stateInit() {
427         // force rebuild of doc and show code stat
428         docCount = 10;
429         analyzeCount = 10;
430 
431         writeln("Watching the following paths for changes:");
432         inotify_paths.each!writeln;
433     }
434 
435     void stateAudioStatus() {
436         if (!flagCompileError && flagUtTestPassed && testErrorLog.length == 0)
437             playSound(Yes.Positive);
438         else
439             playSound(No.Positive);
440     }
441 
442     void stateReset() {
443         flagCompileError = No.CompileError;
444         flagUtTestPassed = No.UtTestPassed;
445         testErrorLog.length = 0;
446     }
447 
448     void stateStart() {
449     }
450 
451     void stateWait() {
452         println(Color.yellow, "================================");
453 
454         string[] a;
455         a ~= "inotifywait";
456         a ~= "-q";
457         a ~= "-r";
458         a ~= ["-e", "modify"];
459         a ~= ["-e", "attrib"];
460         a ~= ["-e", "create"];
461         a ~= ["-e", "move_self"];
462         a ~= ["--format", "%w"];
463         a ~= inotify_paths;
464 
465         auto r = execute(a, null, Config.none, size_t.max, thisExePath.dirName);
466 
467         import core.thread;
468 
469         if (signalInterrupt) {
470             // do nothing, a SIGINT has been received while sleeping
471         } else if (r.status == 0) {
472             writeln("Change detected in ", r.output);
473             // wait for editor to finish saving the file
474             Thread.sleep(dur!("msecs")(500));
475         } else {
476             enum SLEEP = 10;
477             writefln("%-(%s %)", a);
478             printStatus(Status.Warn, "Error: ", r.output);
479             writeln("sleeping ", SLEEP, "s");
480             Thread.sleep(dur!("seconds")(SLEEP));
481         }
482     }
483 
484     void stateUt_run() {
485         immutable test_header = "Compile and run unittest";
486         printStatus(Status.Run, test_header);
487 
488         auto status = spawnShell("make check -j $(nproc)", null, Config.none, cmakeDir).wait;
489         flagUtTestPassed = cast(Flag!"UtTestPassed")(status == 0);
490 
491         printExitStatus(status, test_header);
492     }
493 
494     void stateUt_skip() {
495         flagUtTestPassed = Yes.UtTestPassed;
496     }
497 
498     void stateDebug_build() {
499         printStatus(Status.Run, "Debug build");
500 
501         auto r = spawnShell("make all -j $(nproc)", null, Config.none, cmakeDir).wait;
502         flagCompileError = cast(Flag!"CompileError")(r != 0);
503         printExitStatus(r, "Debug build with debug symbols");
504     }
505 
506     void stateIntegration_test() {
507         immutable test_header = "Compile and run integration tests";
508         printStatus(Status.Run, test_header);
509 
510         auto status = spawnShell(`make check_integration -j $(nproc)`, null,
511                 Config.none, cmakeDir).wait;
512 
513         if (status != 0) {
514             testErrorLog ~= ErrorMsg(cmakeDir, "integration_test", "failed");
515         }
516 
517         printExitStatus(status, test_header);
518     }
519 
520     void stateTest_passed() {
521         docCount++;
522         analyzeCount++;
523         printStatus(Status.Ok, "Test of code generation");
524     }
525 
526     void stateTest_failed() {
527         // separate the log dump to the console from the list of files the logs can be found in.
528         // Most common scenario is one failure.
529         testErrorLog.each!((a) { writeln(a.output); });
530         testErrorLog.each!((a) {
531             printStatus(Status.Fail, a.msg, ", log at ", a.fname);
532         });
533 
534         printStatus(Status.Fail, "Test of code generation");
535     }
536 
537     void stateDoc_check_counter() {
538     }
539 
540     void stateDoc_build() {
541     }
542 
543     void stateSloc_check_counter() {
544     }
545 
546     void stateSlocs() {
547         printStatus(Status.Run, "Code statistics");
548         scope (exit)
549             printStatus(Status.Ok, "Code statistics");
550 
551         string[] a;
552         a ~= "dscanner";
553         a ~= "--sloc";
554         a ~= sourcePath.array();
555 
556         auto r = execute(a, null, Config.none, size_t.max, thisExePath.dirName);
557         if (r.status == 0) {
558             writeln(r.output);
559         }
560 
561         analyzeCount = 0;
562     }
563 
564     void stateExitOrRestart() {
565     }
566 
567     void stateExit() {
568         if (flagTotalTestPassed) {
569             .signalExitStatus = Yes.TestsPassed;
570         } else {
571             .signalExitStatus = No.TestsPassed;
572         }
573         .signalInterrupt = Yes.SignalInterrupt;
574     }
575 }
576 
577 int main(string[] args) {
578     Flag!"keepCoverage" keep_cov;
579 
580     chdir(thisExePath.dirName);
581     scope (exit)
582         cleanup(keep_cov);
583 
584     if (!sanityCheck) {
585         writeln("error: Sanity check failed");
586         return 1;
587     }
588 
589     import std.getopt;
590 
591     bool run_and_exit;
592     bool ut_debug;
593     bool ut_skip;
594 
595     // dfmt off
596     auto help_info = getopt(args,
597         "run_and_exit", "run the tests in one pass and exit", &run_and_exit,
598         "ut_debug", "run tests in single threaded debug mode", &ut_debug,
599         "ut_skip", "skip unittests to go straight to the integration tests", &ut_skip);
600     // dfmt on
601 
602     if (help_info.helpWanted) {
603         defaultGetoptPrinter("Usage: autobuild.sh [options]", help_info.options);
604         return 0;
605     }
606 
607     setup();
608 
609     // dfmt off
610     auto inotify_paths = only(
611                               "dub.sdl",
612                               "libs",
613                               "plugin",
614                               "source",
615                               "test/source",
616                               "test/testdata",
617                               "vendor",
618         )
619         .map!(a => buildPath(thisExePath.dirName, a).Path)
620         .array;
621     // dfmt on
622 
623     import std.stdio;
624 
625     (Fsm()).run(inotify_paths, cast(Flag!"Travis") run_and_exit,
626             cast(Flag!"utDebug") ut_debug, cast(Flag!"utSkip") ut_skip);
627 
628     return signalExitStatus ? 0 : -1;
629 }