1 /** 2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Common functionality used both by source and schemata testing of a mutant. 11 */ 12 module dextool.plugin.mutate.backend.test_mutant.common; 13 14 import logger = std.experimental.logger; 15 import std.algorithm : map, filter; 16 import std.array : empty, array; 17 import std.datetime : Duration, dur; 18 import std.exception : collectException; 19 import std.path : buildPath; 20 import std.typecons : Flag, No; 21 22 import blob_model : Blob; 23 import my.named_type; 24 import proc : DrainElement; 25 import sumtype; 26 27 import dextool.plugin.mutate.backend.database : MutationId; 28 import dextool.plugin.mutate.backend.interface_; 29 import dextool.plugin.mutate.backend.test_mutant.test_case_analyze : GatherTestCase; 30 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner; 31 import dextool.plugin.mutate.config; 32 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand; 33 import dextool.type : AbsolutePath, Path; 34 35 public import dextool.plugin.mutate.backend.type : Mutation, TestCase, 36 ExitStatus, MutantTimeProfile; 37 38 version (unittest) { 39 import unit_threaded.assertions; 40 } 41 42 @safe: 43 44 /// The result of running the test suite on one mutant. 45 struct MutationTestResult { 46 import std.datetime : Duration; 47 import dextool.plugin.mutate.backend.database : MutationStatusId, MutationId; 48 import dextool.plugin.mutate.backend.type : Mutation, TestCase, ExitStatus; 49 50 MutationId mutId; 51 MutationStatusId id; 52 Mutation.Status status; 53 MutantTimeProfile profile; 54 TestCase[] testCases; 55 ExitStatus exitStatus; 56 } 57 58 /** Analyze stdout/stderr output from a test suite for test cases that failed 59 * (killed) a mutant, which test cases that exists and if any of them are 60 * unstable. 61 */ 62 struct TestCaseAnalyzer { 63 private { 64 ShellCommand[] externalAnalysers; 65 TestCaseAnalyzeBuiltin[] builtins; 66 AutoCleanup cleanup; 67 } 68 69 static struct Success { 70 TestCase[] failed; 71 TestCase[] found; 72 } 73 74 static struct Unstable { 75 TestCase[] unstable; 76 TestCase[] found; 77 } 78 79 static struct Failed { 80 } 81 82 alias Result = SumType!(Success, Unstable, Failed); 83 84 this(TestCaseAnalyzeBuiltin[] builtins, ShellCommand[] externalAnalyzers, AutoCleanup cleanup) { 85 this.externalAnalysers = externalAnalyzers; 86 this.builtins = builtins; 87 this.cleanup = cleanup; 88 } 89 90 Result analyze(DrainElement[] data, Flag!"allFound" allFound = No.allFound) { 91 import dextool.plugin.mutate.backend.test_mutant.test_case_analyze : GatherTestCase; 92 93 GatherTestCase gather; 94 95 // the post processer must succeeed for the data to be stored. It is 96 // considered a major error that may corrupt existing data if it fails. 97 bool success = true; 98 99 if (!externalAnalysers.empty) { 100 foreach (cmd; externalAnalysers) { 101 success = success && externalProgram(cmd, data, gather, cleanup); 102 } 103 } 104 if (!builtins.empty) { 105 builtin(data, builtins, gather); 106 } 107 108 if (!gather.unstable.empty) { 109 return Result(Unstable(gather.unstableAsArray, allFound ? gather.foundAsArray : null)); 110 } 111 112 if (success) { 113 return Result(Success(gather.failedAsArray, allFound ? gather.foundAsArray : null)); 114 } 115 116 return Result(Failed.init); 117 } 118 119 /// Returns: true if there are no analyzers setup. 120 bool empty() @safe pure nothrow const @nogc { 121 return externalAnalysers.empty && builtins.empty; 122 } 123 } 124 125 /** Analyze the output from the test suite with one of the builtin analyzers. 126 */ 127 void builtin(DrainElement[] output, 128 const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, ref GatherTestCase app) @safe nothrow { 129 import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze; 130 import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze; 131 import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze; 132 133 GtestParser gtest; 134 CtestParser ctest; 135 MakefileParser makefile; 136 137 void analyzeLine(const(char)[] line) { 138 // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this? 139 immutable magic_nr = 2048; 140 if (line.length > magic_nr) { 141 // The byLine split may fail and thus result in one huge line. 142 // The result of this is that regex's that use backtracking become really slow. 143 // By skipping these lines dextool at list doesn't hang. 144 logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...", 145 line.length, magic_nr); 146 return; 147 } 148 149 foreach (const p; tc_analyze_builtin) { 150 final switch (p) { 151 case TestCaseAnalyzeBuiltin.gtest: 152 gtest.process(line, app); 153 break; 154 case TestCaseAnalyzeBuiltin.ctest: 155 ctest.process(line, app); 156 break; 157 case TestCaseAnalyzeBuiltin.makefile: 158 makefile.process(line, app); 159 break; 160 } 161 } 162 } 163 164 foreach (l; LineRange(output)) { 165 try { 166 analyzeLine(l); 167 } catch (Exception e) { 168 logger.warning("A error encountered when trying to analyze the output from the test suite. Ignoring the offending line.") 169 .collectException; 170 logger.warning(e.msg).collectException; 171 } 172 } 173 174 foreach (const p; tc_analyze_builtin) { 175 final switch (p) { 176 case TestCaseAnalyzeBuiltin.gtest: 177 gtest.finalize(app); 178 break; 179 case TestCaseAnalyzeBuiltin.ctest: 180 break; 181 case TestCaseAnalyzeBuiltin.makefile: 182 break; 183 } 184 } 185 } 186 187 struct LineRange { 188 DrainElement[] elems; 189 const(char)[] buf; 190 const(char)[] line; 191 192 const(char)[] front() @safe pure nothrow { 193 assert(!empty, "Can't get front of an empty range"); 194 return line; 195 } 196 197 void popFront() @safe nothrow { 198 assert(!empty, "Can't pop front of an empty range"); 199 import std.algorithm : countUntil; 200 201 static auto nextLine(ref const(char)[] buf) @safe nothrow { 202 const(char)[] line; 203 204 try { 205 const idx = buf.countUntil('\n'); 206 if (idx != -1) { 207 line = buf[0 .. idx]; 208 if (idx < buf.length) { 209 buf = buf[idx + 1 .. $]; 210 } else { 211 buf = null; 212 } 213 } 214 } catch (Exception e) { 215 logger.warning(e.msg).collectException; 216 logger.warning("Unable to parse the buffered data for a newline. Ignoring the rest.") 217 .collectException; 218 buf = null; 219 } 220 221 return line; 222 } 223 224 line = null; 225 while (!elems.empty && line.empty) { 226 try { 227 auto tmp = elems[0].byUTF8.array; 228 buf ~= tmp; 229 } catch (Exception e) { 230 logger.warning(e.msg).collectException; 231 logger.warning( 232 "A error encountered when trying to parse the output as UTF-8. Ignoring the offending data.") 233 .collectException; 234 } 235 elems = elems[1 .. $]; 236 line = nextLine(buf); 237 } 238 239 const s = buf.length; 240 // there are data in the buffer that may contain lines 241 if (elems.empty && !buf.empty && line.empty) { 242 line = nextLine(buf); 243 } 244 245 // the last data in the buffer. This is a special case if an 246 // application write data but do not end the last block of data with a 247 // newline. 248 // `s == buf.length` handles the case wherein there is an empty line. 249 if (elems.empty && !buf.empty && line.empty && (s == buf.length)) { 250 line = buf; 251 buf = null; 252 } 253 } 254 255 bool empty() @safe pure nothrow const @nogc { 256 return elems.empty && buf.empty && line.empty; 257 } 258 } 259 260 @("shall end the parsing of DrainElements even if the last is missing a newline") 261 unittest { 262 import std.algorithm : copy; 263 import std.array : appender; 264 265 auto app = appender!(DrainElement[])(); 266 ["foo", "bar\n", "smurf"].map!(a => DrainElement(DrainElement.Type.stdout, 267 cast(const(ubyte)[]) a)).copy(app); 268 269 auto r = LineRange(app.data); 270 271 r.empty.shouldBeFalse; 272 r.popFront; 273 r.front.shouldEqual("foobar"); 274 275 r.empty.shouldBeFalse; 276 r.popFront; 277 r.front.shouldEqual("smurf"); 278 279 r.empty.shouldBeFalse; 280 r.popFront; 281 r.empty.shouldBeTrue; 282 } 283 284 /** Run an external program that analyze the output from the test suite for 285 * test cases that failed. 286 * 287 * Params: 288 * cmd = user analyze command to execute on the output 289 * output = output from the test command to be passed on to the analyze command 290 * report = the result is stored in the report 291 * 292 * Returns: True if it successfully analyzed the output 293 */ 294 bool externalProgram(ShellCommand cmd, DrainElement[] output, 295 ref GatherTestCase report, AutoCleanup cleanup) @safe nothrow { 296 import std.datetime : dur; 297 import std.algorithm : copy; 298 import std.ascii : newline; 299 import std.string : strip, startsWith; 300 import proc; 301 302 immutable passed = "passed:"; 303 immutable failed = "failed:"; 304 immutable unstable = "unstable:"; 305 306 auto tmpdir = createTmpDir(); 307 if (tmpdir.empty) { 308 return false; 309 } 310 311 ShellCommand writeOutput(ShellCommand cmd) @safe { 312 import std.stdio : File; 313 314 const stdoutPath = buildPath(tmpdir, "stdout.log"); 315 const stderrPath = buildPath(tmpdir, "stderr.log"); 316 auto stdout = File(stdoutPath, "w"); 317 auto stderr = File(stderrPath, "w"); 318 319 foreach (a; output) { 320 final switch (a.type) { 321 case DrainElement.Type.stdout: 322 stdout.rawWrite(a.data); 323 break; 324 case DrainElement.Type.stderr: 325 stderr.rawWrite(a.data); 326 break; 327 } 328 } 329 330 cmd.value ~= [stdoutPath, stderrPath]; 331 return cmd; 332 } 333 334 try { 335 cleanup.add(tmpdir.Path.AbsolutePath); 336 cmd = writeOutput(cmd); 337 auto p = pipeProcess(cmd.value).sandbox.rcKill; 338 foreach (l; p.process.drainByLineCopy().map!(a => a.strip) 339 .filter!(a => !a.empty)) { 340 if (l.startsWith(passed)) 341 report.reportFound(TestCase(l[passed.length .. $].strip.idup)); 342 else if (l.startsWith(failed)) 343 report.reportFailed(TestCase(l[failed.length .. $].strip.idup)); 344 else if (l.startsWith(unstable)) 345 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup)); 346 } 347 348 if (p.wait == 0) { 349 return true; 350 } 351 352 logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd); 353 } catch (Exception e) { 354 logger.warning(e.msg).collectException; 355 } 356 357 return false; 358 } 359 360 /// Returns: path to a tmp directory or null on failure. 361 string createTmpDir() @safe nothrow { 362 import std.random : uniform; 363 import std.format : format; 364 import std.file : mkdir; 365 366 string test_tmp_output; 367 368 // try 5 times or bailout 369 foreach (const _; 0 .. 5) { 370 try { 371 auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong); 372 mkdir(tmp); 373 test_tmp_output = AbsolutePath(Path(tmp)); 374 break; 375 } catch (Exception e) { 376 logger.warning(e.msg).collectException; 377 } 378 } 379 380 if (test_tmp_output.length == 0) { 381 logger.warning("Unable to create a temporary directory to store stdout/stderr in") 382 .collectException; 383 } 384 385 return test_tmp_output; 386 } 387 388 /** Paths stored will be removed automatically either when manually called or 389 * goes out of scope. 390 */ 391 class AutoCleanup { 392 private string[] remove_dirs; 393 394 void add(AbsolutePath p) @safe nothrow { 395 remove_dirs ~= cast(string) p; 396 } 397 398 // trusted: the paths are forced to be valid paths. 399 void cleanup() @trusted nothrow { 400 import std.file : rmdirRecurse, exists; 401 402 foreach (ref p; remove_dirs.filter!(a => !a.empty)) { 403 try { 404 if (exists(p)) 405 rmdirRecurse(p); 406 if (!exists(p)) 407 p = null; 408 } catch (Exception e) { 409 logger.info(e.msg).collectException; 410 } 411 } 412 413 remove_dirs = remove_dirs.filter!(a => !a.empty).array; 414 } 415 } 416 417 alias CompileResult = SumType!(Mutation.Status, bool); 418 419 CompileResult compile(ShellCommand cmd, Duration timeout, bool printToStdout = false) @trusted nothrow { 420 import proc; 421 import std.stdio : write; 422 423 try { 424 auto p = () { 425 if (cmd.value.length == 1) { 426 return pipeShell(cmd.value[0]).sandbox.timeout(timeout).rcKill; 427 } 428 return pipeProcess(cmd.value).sandbox.timeout(timeout).rcKill; 429 }(); 430 foreach (a; p.process.drain) { 431 if (!a.empty && printToStdout) { 432 write(a.byUTF8); 433 } 434 } 435 if (p.wait != 0) { 436 return CompileResult(Mutation.Status.killedByCompiler); 437 } 438 } catch (Exception e) { 439 logger.warning("Unknown error when executing the build command").collectException; 440 logger.warning(cmd.value).collectException; 441 logger.warning(e.msg).collectException; 442 return CompileResult(Mutation.Status.unknown); 443 } 444 445 return CompileResult(true); 446 } 447 448 struct TestResult { 449 Mutation.Status status; 450 DrainElement[] output; 451 ExitStatus exitStatus; 452 ShellCommand[] testCmds; 453 } 454 455 /** Run the test suite to verify a mutation. 456 * 457 * Params: 458 * compile_p = compile command 459 * tester_p = test command 460 * timeout = kill the test command and mark mutant as timeout if the runtime exceed this value. 461 * fio = i/o 462 * 463 * Returns: the result of testing the mutant. 464 */ 465 TestResult runTester(ref TestRunner runner) nothrow { 466 import proc; 467 468 TestResult rval; 469 try { 470 auto res = runner.run; 471 rval.output = res.output; 472 rval.exitStatus = res.exitStatus; 473 rval.testCmds = res.testCmds; 474 475 final switch (res.status) with ( 476 dextool.plugin.mutate.backend.test_mutant.test_cmd_runner.TestResult.Status) { 477 case passed: 478 rval.status = Mutation.Status.alive; 479 break; 480 case failed: 481 rval.status = Mutation.Status.killed; 482 break; 483 case timeout: 484 rval.status = Mutation.Status.timeout; 485 break; 486 case error: 487 rval.status = Mutation.Status.unknown; 488 break; 489 } 490 } catch (Exception e) { 491 // unable to for example execute the test suite 492 logger.warning(e.msg).collectException; 493 rval.status = Mutation.Status.unknown; 494 } 495 496 return rval; 497 } 498 499 void restoreFiles(AbsolutePath[] files, FilesysIO fio) { 500 foreach (a; files) { 501 fio.makeOutput(a).write(fio.makeInput(a)); 502 } 503 } 504 505 /// The conditions for when to stop mutation testing. 506 /// Intended to be re-used by both the main FSM and the sub-FSMs. 507 struct TestStopCheck { 508 import std.format : format; 509 import std.datetime.systime : Clock, SysTime; 510 import my.optional; 511 import dextool.plugin.mutate.config : ConfigMutationTest; 512 513 enum HaltReason { 514 none, 515 maxRuntime, 516 aliveTested, 517 overloaded, 518 } 519 520 private { 521 typeof(ConfigMutationTest.loadBehavior) loadBehavior; 522 typeof(ConfigMutationTest.loadThreshold) baseLoadThreshold; 523 typeof(ConfigMutationTest.loadThreshold) loadThreshold; 524 525 Optional!int maxAlive; 526 527 /// Max time to run the mutation testing for. 528 SysTime stopAt; 529 Duration maxRuntime; 530 531 long aliveMutants_; 532 } 533 534 this(ConfigMutationTest conf) { 535 loadBehavior = conf.loadBehavior; 536 loadThreshold = conf.loadThreshold; 537 baseLoadThreshold = conf.loadThreshold; 538 if (!conf.maxAlive.isNull) 539 maxAlive = some(conf.maxAlive.get); 540 stopAt = Clock.currTime + conf.maxRuntime; 541 maxRuntime = conf.maxRuntime; 542 } 543 544 void incrAliveMutants() @safe pure nothrow @nogc { 545 ++aliveMutants_; 546 } 547 548 long aliveMutants() @safe pure nothrow const @nogc { 549 return aliveMutants_; 550 } 551 552 /// A halt conditions has occured. Mutation testing should stop. 553 HaltReason isHalt() @safe nothrow { 554 if (isMaxRuntime) 555 return HaltReason.maxRuntime; 556 557 if (isAliveTested) 558 return HaltReason.aliveTested; 559 560 if (loadBehavior == ConfigMutationTest.LoadBehavior.halt && load15 > baseLoadThreshold.get) 561 return HaltReason.overloaded; 562 563 return HaltReason.none; 564 } 565 566 /// The system is overloaded and the user has configured the tool to slowdown. 567 bool isOverloaded() @safe nothrow const @nogc { 568 if (loadBehavior == ConfigMutationTest.LoadBehavior.slowdown && load15 > loadThreshold.get) 569 return true; 570 571 return false; 572 } 573 574 bool isAliveTested() @safe pure nothrow @nogc { 575 return maxAlive.hasValue && aliveMutants_ >= maxAlive.orElse(0); 576 } 577 578 bool isMaxRuntime() @safe nothrow const { 579 return Clock.currTime > stopAt; 580 } 581 582 double load15() nothrow const @nogc @trusted { 583 import my.libc : getloadavg; 584 585 double[3] load; 586 const nr = getloadavg(&load[0], 3); 587 if (nr <= 0 || nr > load.length) { 588 return 0.0; 589 } 590 return load[nr - 1]; 591 }; 592 593 /// Pause the current thread by sleeping. 594 void pause() @trusted nothrow { 595 import core.thread : Thread; 596 import std.algorithm : max; 597 598 const sleepFor = 30.dur!"seconds"; 599 logger.infof("Sleeping %s", sleepFor).collectException; 600 Thread.sleep(sleepFor); 601 602 // make it more sensitive if the system is still overloaded. 603 if (load15 > loadThreshold.get) 604 loadThreshold.get = max(1, baseLoadThreshold.get - 1); 605 else 606 loadThreshold = baseLoadThreshold; 607 } 608 609 string overloadToString() @safe const { 610 return format!"Detected overload (%s > %s)"(load15, loadThreshold.get); 611 } 612 613 string maxRuntimeToString() @safe const { 614 return format!"Max runtime of %s reached at %s"(maxRuntime, Clock.currTime); 615 } 616 }