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