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 }