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 sumtype; 23 import proc : DrainElement; 24 25 import dextool.plugin.mutate.backend.database : MutationId; 26 import dextool.plugin.mutate.backend.interface_; 27 import dextool.plugin.mutate.backend.test_mutant.common; 28 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport; 29 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner; 30 import dextool.plugin.mutate.backend.type : Mutation, TestCase; 31 import dextool.plugin.mutate.config; 32 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand; 33 import dextool.set; 34 import dextool.type : AbsolutePath, Path; 35 36 version (unittest) { 37 import unit_threaded.assertions; 38 } 39 40 @safe: 41 42 /** Analyze stdout/stderr output from a test suite for test cases that failed 43 * (killed) a mutant, which test cases that exists and if any of them are 44 * unstable. 45 */ 46 struct TestCaseAnalyzer { 47 private { 48 ShellCommand[] externalAnalysers; 49 TestCaseAnalyzeBuiltin[] builtins; 50 AutoCleanup cleanup; 51 } 52 53 static struct Success { 54 TestCase[] failed; 55 TestCase[] found; 56 } 57 58 static struct Unstable { 59 TestCase[] unstable; 60 TestCase[] found; 61 } 62 63 static struct Failed { 64 } 65 66 alias Result = SumType!(Success, Unstable, Failed); 67 68 this(TestCaseAnalyzeBuiltin[] builtins, ShellCommand[] externalAnalyzers, AutoCleanup cleanup) { 69 this.externalAnalysers = externalAnalyzers; 70 this.builtins = builtins; 71 this.cleanup = cleanup; 72 } 73 74 Result analyze(DrainElement[] data, Flag!"allFound" allFound = No.allFound) { 75 import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase; 76 77 auto gather = new GatherTestCase; 78 // TODO: maybe destroy it too, to cleanup memory earlier? But it isn't 79 // @safe 80 //scope(exit) .destroy(gather); 81 82 // the post processer must succeeed for the data to be stored. It is 83 // considered a major error that may corrupt existing data if it fails. 84 bool success = true; 85 86 if (!externalAnalysers.empty) { 87 foreach (cmd; externalAnalysers) { 88 success = success && externalProgram(cmd, data, gather, cleanup); 89 } 90 } 91 if (!builtins.empty) { 92 builtin(data, builtins, gather); 93 } 94 95 if (!gather.unstable.empty) { 96 return Result(Unstable(gather.unstableAsArray, allFound ? gather.foundAsArray : null)); 97 } 98 99 if (success) { 100 return Result(Success(gather.failedAsArray, allFound ? gather.foundAsArray : null)); 101 } 102 103 return Result(Failed.init); 104 } 105 106 /// Returns: true if there are no analyzers setup. 107 bool empty() @safe pure nothrow const @nogc { 108 return externalAnalysers.empty && builtins.empty; 109 } 110 } 111 112 /** Analyze the output from the test suite with one of the builtin analyzers. 113 */ 114 void builtin(DrainElement[] output, 115 const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, TestCaseReport app) @safe nothrow { 116 import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze; 117 import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze; 118 import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze; 119 120 GtestParser gtest; 121 CtestParser ctest; 122 MakefileParser makefile; 123 124 void analyzeLine(const(char)[] line) { 125 // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this? 126 immutable magic_nr = 2048; 127 if (line.length > magic_nr) { 128 // The byLine split may fail and thus result in one huge line. 129 // The result of this is that regex's that use backtracking become really slow. 130 // By skipping these lines dextool at list doesn't hang. 131 logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...", 132 line.length, magic_nr); 133 return; 134 } 135 136 foreach (const p; tc_analyze_builtin) { 137 final switch (p) { 138 case TestCaseAnalyzeBuiltin.gtest: 139 gtest.process(line, app); 140 break; 141 case TestCaseAnalyzeBuiltin.ctest: 142 ctest.process(line, app); 143 break; 144 case TestCaseAnalyzeBuiltin.makefile: 145 makefile.process(line, app); 146 break; 147 } 148 } 149 } 150 151 foreach (l; LineRange(output)) { 152 try { 153 analyzeLine(l); 154 } catch (Exception e) { 155 logger.warning("A error encountered when trying to analyze the output from the test suite. Ignoring the offending line.") 156 .collectException; 157 logger.warning(e.msg).collectException; 158 } 159 } 160 161 foreach (const p; tc_analyze_builtin) { 162 final switch (p) { 163 case TestCaseAnalyzeBuiltin.gtest: 164 gtest.finalize(app); 165 break; 166 case TestCaseAnalyzeBuiltin.ctest: 167 break; 168 case TestCaseAnalyzeBuiltin.makefile: 169 break; 170 } 171 } 172 } 173 174 struct LineRange { 175 DrainElement[] elems; 176 const(char)[] buf; 177 const(char)[] line; 178 179 const(char)[] front() @safe pure nothrow { 180 assert(!empty, "Can't get front of an empty range"); 181 return line; 182 } 183 184 void popFront() @safe nothrow { 185 assert(!empty, "Can't pop front of an empty range"); 186 import std.algorithm : countUntil; 187 188 static auto nextLine(ref const(char)[] buf) @safe nothrow { 189 const(char)[] line; 190 191 try { 192 const idx = buf.countUntil('\n'); 193 if (idx != -1) { 194 line = buf[0 .. idx]; 195 if (idx < buf.length) { 196 buf = buf[idx + 1 .. $]; 197 } else { 198 buf = null; 199 } 200 } 201 } catch (Exception e) { 202 logger.warning(e.msg).collectException; 203 logger.warning("Unable to parse the buffered data for a newline. Ignoring the rest.") 204 .collectException; 205 buf = null; 206 } 207 208 return line; 209 } 210 211 line = null; 212 while (!elems.empty && line.empty) { 213 try { 214 auto tmp = elems[0].byUTF8.array; 215 buf ~= tmp; 216 } catch (Exception e) { 217 logger.warning(e.msg).collectException; 218 logger.warning( 219 "A error encountered when trying to parse the output as UTF-8. Ignoring the offending data.") 220 .collectException; 221 } 222 elems = elems[1 .. $]; 223 line = nextLine(buf); 224 } 225 226 const s = buf.length; 227 // there are data in the buffer that may contain lines 228 if (elems.empty && !buf.empty && line.empty) { 229 line = nextLine(buf); 230 } 231 232 // the last data in the buffer. This is a special case if an 233 // application write data but do not end the last block of data with a 234 // newline. 235 // `s == buf.length` handles the case wherein there is an empty line. 236 if (elems.empty && !buf.empty && line.empty && (s == buf.length)) { 237 line = buf; 238 buf = null; 239 } 240 } 241 242 bool empty() @safe pure nothrow const @nogc { 243 return elems.empty && buf.empty && line.empty; 244 } 245 } 246 247 @("shall end the parsing of DrainElements even if the last is missing a newline") 248 unittest { 249 import std.algorithm : copy; 250 import std.array : appender; 251 252 auto app = appender!(DrainElement[])(); 253 ["foo", "bar\n", "smurf"].map!(a => DrainElement(DrainElement.Type.stdout, 254 cast(const(ubyte)[]) a)).copy(app); 255 256 auto r = LineRange(app.data); 257 258 r.empty.shouldBeFalse; 259 r.popFront; 260 r.front.shouldEqual("foobar"); 261 262 r.empty.shouldBeFalse; 263 r.popFront; 264 r.front.shouldEqual("smurf"); 265 266 r.empty.shouldBeFalse; 267 r.popFront; 268 r.empty.shouldBeTrue; 269 } 270 271 /** Run an external program that analyze the output from the test suite for 272 * test cases that failed. 273 * 274 * Params: 275 * cmd = user analyze command to execute on the output 276 * output = output from the test command to be passed on to the analyze command 277 * report = the result is stored in the report 278 * 279 * Returns: True if it successfully analyzed the output 280 */ 281 bool externalProgram(ShellCommand cmd, DrainElement[] output, 282 TestCaseReport report, AutoCleanup cleanup) @safe nothrow { 283 import std.datetime : dur; 284 import std.algorithm : copy; 285 import std.ascii : newline; 286 import std.string : strip, startsWith; 287 import proc; 288 289 immutable passed = "passed:"; 290 immutable failed = "failed:"; 291 immutable unstable = "unstable:"; 292 293 auto tmpdir = createTmpDir(); 294 if (tmpdir.empty) { 295 return false; 296 } 297 298 ShellCommand writeOutput(ShellCommand cmd) @safe { 299 import std.stdio : File; 300 301 const stdoutPath = buildPath(tmpdir, "stdout.log"); 302 const stderrPath = buildPath(tmpdir, "stderr.log"); 303 auto stdout = File(stdoutPath, "w"); 304 auto stderr = File(stderrPath, "w"); 305 306 foreach (a; output) { 307 final switch (a.type) { 308 case DrainElement.Type.stdout: 309 stdout.rawWrite(a.data); 310 break; 311 case DrainElement.Type.stderr: 312 stderr.rawWrite(a.data); 313 break; 314 } 315 } 316 317 cmd.value ~= [stdoutPath, stderrPath]; 318 return cmd; 319 } 320 321 try { 322 cleanup.add(tmpdir.Path.AbsolutePath); 323 cmd = writeOutput(cmd); 324 auto p = pipeProcess(cmd.value).sandbox.scopeKill; 325 foreach (l; p.process.drainByLineCopy(200.dur!"msecs").map!(a => a.strip) 326 .filter!(a => !a.empty)) { 327 if (l.startsWith(passed)) 328 report.reportFound(TestCase(l[passed.length .. $].strip.idup)); 329 else if (l.startsWith(failed)) 330 report.reportFailed(TestCase(l[failed.length .. $].strip.idup)); 331 else if (l.startsWith(unstable)) 332 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup)); 333 } 334 335 if (p.wait == 0) { 336 return true; 337 } 338 339 logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd); 340 } catch (Exception e) { 341 logger.warning(e.msg).collectException; 342 } 343 344 return false; 345 } 346 347 /// Returns: path to a tmp directory or null on failure. 348 string createTmpDir() @safe nothrow { 349 import std.random : uniform; 350 import std.format : format; 351 import std.file : mkdir; 352 353 string test_tmp_output; 354 355 // try 5 times or bailout 356 foreach (const _; 0 .. 5) { 357 try { 358 auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong); 359 mkdir(tmp); 360 test_tmp_output = AbsolutePath(Path(tmp)); 361 break; 362 } catch (Exception e) { 363 logger.warning(e.msg).collectException; 364 } 365 } 366 367 if (test_tmp_output.length == 0) { 368 logger.warning("Unable to create a temporary directory to store stdout/stderr in") 369 .collectException; 370 } 371 372 return test_tmp_output; 373 } 374 375 /** Paths stored will be removed automatically either when manually called or 376 * goes out of scope. 377 */ 378 class AutoCleanup { 379 private string[] remove_dirs; 380 381 void add(AbsolutePath p) @safe nothrow { 382 remove_dirs ~= cast(string) p; 383 } 384 385 // trusted: the paths are forced to be valid paths. 386 void cleanup() @trusted nothrow { 387 import std.file : rmdirRecurse, exists; 388 389 foreach (ref p; remove_dirs.filter!(a => !a.empty)) { 390 try { 391 if (exists(p)) 392 rmdirRecurse(p); 393 if (!exists(p)) 394 p = null; 395 } catch (Exception e) { 396 logger.info(e.msg).collectException; 397 } 398 } 399 400 remove_dirs = remove_dirs.filter!(a => !a.empty).array; 401 } 402 } 403 404 alias CompileResult = SumType!(Mutation.Status, bool); 405 406 CompileResult compile(ShellCommand cmd, bool printToStdout = false) nothrow { 407 import proc; 408 import std.stdio : write; 409 410 try { 411 auto p = pipeProcess(cmd.value).sandbox.scopeKill; 412 foreach (a; p.process.drain(200.dur!"msecs")) { 413 if (!a.empty && printToStdout) { 414 write(a.byUTF8); 415 } 416 } 417 if (p.wait != 0) { 418 return CompileResult(Mutation.Status.killedByCompiler); 419 } 420 } catch (Exception e) { 421 logger.warning("Unknown error when executing the build command").collectException; 422 logger.warning(e.msg).collectException; 423 return CompileResult(Mutation.Status.unknown); 424 } 425 426 return CompileResult(true); 427 } 428 429 /** Run the test suite to verify a mutation. 430 * 431 * Params: 432 * compile_p = compile command 433 * tester_p = test command 434 * timeout = kill the test command and mark mutant as timeout if the runtime exceed this value. 435 * fio = i/o 436 * 437 * Returns: the result of testing the mutant. 438 */ 439 auto runTester(ref TestRunner runner) nothrow { 440 import proc; 441 442 struct Rval { 443 Mutation.Status status; 444 DrainElement[] output; 445 } 446 447 Rval rval; 448 try { 449 auto res = runner.run; 450 rval.output = res.output; 451 452 final switch (res.status) with (TestResult.Status) { 453 case passed: 454 rval.status = Mutation.Status.alive; 455 break; 456 case failed: 457 rval.status = Mutation.Status.killed; 458 break; 459 case timeout: 460 rval.status = Mutation.Status.timeout; 461 break; 462 case error: 463 rval.status = Mutation.Status.unknown; 464 break; 465 } 466 } catch (Exception e) { 467 // unable to for example execute the test suite 468 logger.warning(e.msg).collectException; 469 rval.status = Mutation.Status.unknown; 470 } 471 472 return rval; 473 }