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 process : 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 162 struct LineRange { 163 DrainElement[] elems; 164 const(char)[] buf; 165 const(char)[] line; 166 167 const(char)[] front() @safe pure nothrow { 168 assert(!empty, "Can't get front of an empty range"); 169 return line; 170 } 171 172 void popFront() @safe nothrow { 173 assert(!empty, "Can't pop front of an empty range"); 174 import std.algorithm : countUntil; 175 176 static auto nextLine(ref const(char)[] buf) @safe nothrow { 177 const(char)[] line; 178 179 try { 180 const idx = buf.countUntil('\n'); 181 if (idx != -1) { 182 line = buf[0 .. idx]; 183 if (idx < buf.length) { 184 buf = buf[idx + 1 .. $]; 185 } else { 186 buf = null; 187 } 188 } 189 } catch (Exception e) { 190 logger.warning(e.msg).collectException; 191 logger.warning("Unable to parse the buffered data for a newline. Ignoring the rest.") 192 .collectException; 193 buf = null; 194 } 195 196 return line; 197 } 198 199 line = null; 200 while (!elems.empty && line.empty) { 201 try { 202 auto tmp = elems[0].byUTF8.array; 203 buf ~= tmp; 204 } catch (Exception e) { 205 logger.warning(e.msg).collectException; 206 logger.warning( 207 "A error encountered when trying to parse the output as UTF-8. Ignoring the offending data.") 208 .collectException; 209 } 210 elems = elems[1 .. $]; 211 line = nextLine(buf); 212 } 213 214 const s = buf.length; 215 // there are data in the buffer that may contain lines 216 if (elems.empty && !buf.empty && line.empty) { 217 line = nextLine(buf); 218 } 219 220 // the last data in the buffer. This is a special case if an 221 // application write data but do not end the last block of data with a 222 // newline. 223 // `s == buf.length` handles the case wherein there is an empty line. 224 if (elems.empty && !buf.empty && line.empty && (s == buf.length)) { 225 line = buf; 226 buf = null; 227 } 228 } 229 230 bool empty() @safe pure nothrow const @nogc { 231 return elems.empty && buf.empty && line.empty; 232 } 233 } 234 235 @("shall end the parsing of DrainElements even if the last is missing a newline") 236 unittest { 237 import std.algorithm : copy; 238 import std.array : appender; 239 240 auto app = appender!(DrainElement[])(); 241 ["foo", "bar\n", "smurf"].map!(a => DrainElement(DrainElement.Type.stdout, 242 cast(const(ubyte)[]) a)).copy(app); 243 244 auto r = LineRange(app.data); 245 246 r.empty.shouldBeFalse; 247 r.popFront; 248 r.front.shouldEqual("foobar"); 249 250 r.empty.shouldBeFalse; 251 r.popFront; 252 r.front.shouldEqual("smurf"); 253 254 r.empty.shouldBeFalse; 255 r.popFront; 256 r.empty.shouldBeTrue; 257 } 258 259 /** Run an external program that analyze the output from the test suite for 260 * test cases that failed. 261 * 262 * Params: 263 * cmd = user analyze command to execute on the output 264 * output = output from the test command to be passed on to the analyze command 265 * report = the result is stored in the report 266 * 267 * Returns: True if it successfully analyzed the output 268 */ 269 bool externalProgram(ShellCommand cmd, DrainElement[] output, 270 TestCaseReport report, AutoCleanup cleanup) @safe nothrow { 271 import std.datetime : dur; 272 import std.algorithm : copy; 273 import std.ascii : newline; 274 import std.string : strip, startsWith; 275 import process; 276 277 immutable passed = "passed:"; 278 immutable failed = "failed:"; 279 immutable unstable = "unstable:"; 280 281 auto tmpdir = createTmpDir(); 282 if (tmpdir.empty) { 283 return false; 284 } 285 286 ShellCommand writeOutput(ShellCommand cmd) @safe { 287 import std.stdio : File; 288 289 const stdoutPath = buildPath(tmpdir, "stdout.log"); 290 const stderrPath = buildPath(tmpdir, "stderr.log"); 291 auto stdout = File(stdoutPath, "w"); 292 auto stderr = File(stderrPath, "w"); 293 294 foreach (a; output) { 295 final switch (a.type) { 296 case DrainElement.Type.stdout: 297 stdout.write(a.data); 298 break; 299 case DrainElement.Type.stderr: 300 stderr.write(a.data); 301 break; 302 } 303 } 304 305 cmd.value ~= [stdoutPath, stderrPath]; 306 return cmd; 307 } 308 309 try { 310 cleanup.add(tmpdir.Path.AbsolutePath); 311 cmd = writeOutput(cmd); 312 auto p = pipeProcess(cmd.value).sandbox.scopeKill; 313 foreach (l; p.process.drainByLineCopy(200.dur!"msecs").map!(a => a.strip) 314 .filter!(a => !a.empty)) { 315 if (l.startsWith(passed)) 316 report.reportFound(TestCase(l[passed.length .. $].strip.idup)); 317 else if (l.startsWith(failed)) 318 report.reportFailed(TestCase(l[failed.length .. $].strip.idup)); 319 else if (l.startsWith(unstable)) 320 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup)); 321 } 322 323 if (p.wait == 0) { 324 return true; 325 } 326 327 logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd); 328 } catch (Exception e) { 329 logger.warning(e.msg).collectException; 330 } 331 332 return false; 333 } 334 335 /// Returns: path to a tmp directory or null on failure. 336 string createTmpDir() @safe nothrow { 337 import std.random : uniform; 338 import std.format : format; 339 import std.file : mkdir; 340 341 string test_tmp_output; 342 343 // try 5 times or bailout 344 foreach (const _; 0 .. 5) { 345 try { 346 auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong); 347 mkdir(tmp); 348 test_tmp_output = AbsolutePath(Path(tmp)); 349 break; 350 } catch (Exception e) { 351 logger.warning(e.msg).collectException; 352 } 353 } 354 355 if (test_tmp_output.length == 0) { 356 logger.warning("Unable to create a temporary directory to store stdout/stderr in") 357 .collectException; 358 } 359 360 return test_tmp_output; 361 } 362 363 /** Paths stored will be removed automatically either when manually called or 364 * goes out of scope. 365 */ 366 class AutoCleanup { 367 private string[] remove_dirs; 368 369 void add(AbsolutePath p) @safe nothrow { 370 remove_dirs ~= cast(string) p; 371 } 372 373 // trusted: the paths are forced to be valid paths. 374 void cleanup() @trusted nothrow { 375 import std.file : rmdirRecurse, exists; 376 377 foreach (ref p; remove_dirs.filter!(a => !a.empty)) { 378 try { 379 if (exists(p)) 380 rmdirRecurse(p); 381 if (!exists(p)) 382 p = null; 383 } catch (Exception e) { 384 logger.info(e.msg).collectException; 385 } 386 } 387 388 remove_dirs = remove_dirs.filter!(a => !a.empty).array; 389 } 390 } 391 392 alias CompileResult = SumType!(Mutation.Status, bool); 393 394 CompileResult compile(ShellCommand cmd) nothrow { 395 import process; 396 397 try { 398 auto p = pipeProcess(cmd.value).sandbox.drainToNull(200.dur!"msecs").scopeKill; 399 if (p.wait != 0) { 400 return CompileResult(Mutation.Status.killedByCompiler); 401 } 402 } catch (Exception e) { 403 logger.warning("Unknown error when executing the build command").collectException; 404 logger.warning(e.msg).collectException; 405 return CompileResult(Mutation.Status.unknown); 406 } 407 408 return CompileResult(true); 409 } 410 411 /** Run the test suite to verify a mutation. 412 * 413 * Params: 414 * compile_p = compile command 415 * tester_p = test command 416 * timeout = kill the test command and mark mutant as timeout if the runtime exceed this value. 417 * fio = i/o 418 * 419 * Returns: the result of testing the mutant. 420 */ 421 auto runTester(ref TestRunner runner) nothrow { 422 import process; 423 424 struct Rval { 425 Mutation.Status status; 426 DrainElement[] output; 427 } 428 429 Rval rval; 430 try { 431 auto res = runner.run; 432 rval.output = res.output; 433 434 final switch (res.status) with (TestResult.Status) { 435 case passed: 436 rval.status = Mutation.Status.alive; 437 break; 438 case failed: 439 rval.status = Mutation.Status.killed; 440 break; 441 case timeout: 442 rval.status = Mutation.Status.timeout; 443 break; 444 case error: 445 rval.status = Mutation.Status.unknown; 446 break; 447 } 448 } catch (Exception e) { 449 // unable to for example execute the test suite 450 logger.warning(e.msg).collectException; 451 rval.status = Mutation.Status.unknown; 452 } 453 454 return rval; 455 }