1 /** 2 Copyright: Copyright (c) 2015-2017, Joakim Brännström. All rights reserved. 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 dextool_test.utils; 7 8 import scriptlike; 9 10 import std.range : isInputRange; 11 import std.typecons : Flag; 12 public import std.typecons : Yes, No; 13 14 import logger = std.experimental.logger; 15 16 enum dextoolExePath = "path_to_dextool/dextool_debug"; 17 18 import dextool_test.builders : BuildCommandRun; 19 20 auto buildArtifacts() { 21 return Path("build"); 22 } 23 24 auto gmockLib() { 25 return buildArtifacts ~ "libgmock_gtest.a"; 26 } 27 28 private void delegate(string) oldYap = null; 29 private string[] yapLog; 30 31 static this() { 32 scriptlikeCustomEcho = (string s) => dextoolYap(s); 33 echoOn; 34 } 35 36 void dextoolYap(string msg) nothrow { 37 yapLog ~= msg; 38 } 39 40 void dextoolYap(T...)(T args) { 41 import std.format : format; 42 43 yapLog ~= format(args); 44 } 45 46 string[] getYapLog() { 47 return yapLog.dup; 48 } 49 50 void resetYapLog() { 51 yapLog.length = 0; 52 } 53 54 void echoOn() { 55 .scriptlikeEcho = true; 56 } 57 58 void echoOff() { 59 .scriptlikeEcho = false; 60 } 61 62 string escapePath(in Path p) { 63 import scriptlike : escapeShellArg; 64 65 return p.raw.dup.escapeShellArg; 66 } 67 68 deprecated("to be removed") auto runAndLog(T)(T args_) { 69 import std.traits : Unqual; 70 71 static if (is(Unqual!T == Path)) { 72 string args = args_.escapePath; 73 } else static if (is(Unqual!T == Args)) { 74 string args = args_.data; 75 } else { 76 string args = args_; 77 } 78 79 auto status = tryRunCollect(args); 80 81 yap("Exit status: ", status.status); 82 yap(status.output); 83 return status; 84 } 85 86 void syncMkdirRecurse(string p) nothrow { 87 synchronized { 88 try { 89 mkdirRecurse(p); 90 } 91 catch (Exception e) { 92 } 93 } 94 } 95 96 struct TestEnv { 97 import std.ascii : newline; 98 99 private Path outdir_; 100 private string outdir_suffix; 101 private Path dextool_; 102 103 this(Path dextool) { 104 this.dextool_ = dextool.absolutePath; 105 } 106 107 Path outdir() const nothrow { 108 try { 109 return ((buildArtifacts ~ outdir_).stripExtension ~ outdir_suffix).absolutePath; 110 } 111 catch (Exception e) { 112 return ((buildArtifacts ~ outdir_).stripExtension ~ outdir_suffix); 113 } 114 } 115 116 Path dextool() const { 117 return dextool_; 118 } 119 120 string toString() { 121 // dfmt off 122 return only( 123 ["dextool:", dextool.toString], 124 ["outdir:", outdir.toString], 125 ) 126 .map!(a => leftJustifier(a[0], 10).text ~ a[1]) 127 .joiner(newline) 128 .text; 129 // dfmt on 130 } 131 132 void setOutput(Path outdir__) { 133 this.outdir_ = outdir__; 134 } 135 136 /** Setup the test environment 137 * 138 * Example of using the outputSuffix. 139 * --- 140 * mixin(envSetup(globalTestdir, No.setupEnv)); 141 * testEnv.outputSuffix("foo"); 142 * testEnv.setupEnv; 143 * --- 144 */ 145 void outputSuffix(string suffix) { 146 this.outdir_suffix = suffix; 147 } 148 149 void setupEnv() { 150 yap("Test environment:", newline, toString); 151 syncMkdirRecurse(outdir.toString); 152 cleanOutdir; 153 } 154 155 void cleanOutdir() nothrow { 156 // ensure logs are empty 157 const auto d = outdir(); 158 159 string[] files; 160 161 try { 162 files = dirEntries(d, SpanMode.depth).filter!(a => a.isFile).map!(a => a.name).array(); 163 } 164 catch (Exception e) { 165 } 166 167 foreach (a; files) { 168 // tryRemove can fail, usually duo to I/O when tests are ran in 169 // parallel. 170 try { 171 tryRemove(Path(a)); 172 } 173 catch (Exception e) { 174 } 175 } 176 } 177 178 void setup(Path outdir__) { 179 setOutput(outdir__); 180 setupEnv; 181 } 182 183 void teardown() { 184 auto stdout_path = outdir ~ "console.log"; 185 File logfile; 186 try { 187 logfile = File(stdout_path.toString, "w"); 188 } 189 catch (Exception e) { 190 logger.trace(e.msg); 191 return; 192 } 193 194 // Use when saving error data for later analyze 195 foreach (l; getYapLog) { 196 logfile.writeln(l); 197 } 198 resetYapLog(); 199 } 200 } 201 202 //TODO deprecated, use envSetup instead. 203 string EnvSetup(string logdir) { 204 return envSetup(logdir, Yes.setupEnv); 205 } 206 207 string envSetup(string logdir, Flag!"setupEnv" setupEnv = Yes.setupEnv) { 208 import std.format : format; 209 210 auto txt = ` 211 import scriptlike; 212 213 auto testEnv = TestEnv(Path("%s")); 214 215 // Setup and cleanup 216 scope (exit) { 217 testEnv.teardown(); 218 } 219 chdir(thisExePath.dirName); 220 221 { 222 import std.traits : fullyQualifiedName; 223 int _ = 0; 224 testEnv.setOutput(Path("%s/" ~ fullyQualifiedName!_)); 225 } 226 `; 227 228 txt = format(txt, dextoolExePath, logdir); 229 230 if (setupEnv) { 231 txt ~= "\ntestEnv.setupEnv();\n"; 232 } 233 234 return txt; 235 } 236 237 struct GR { 238 Path gold; 239 Path result; 240 } 241 242 auto removeJunk(R)(R r, Flag!"skipComments" skipComments) { 243 import std.algorithm : filter; 244 import std.range : tee; 245 246 // dfmt off 247 return r 248 // remove comments 249 .filter!(a => !skipComments || !(a.value.strip.length > 2 && a.value.strip[0 .. 2] == "//")) 250 // remove the line with the version 251 .filter!(a => !(a.value.length > 39 && a.value[0 .. 39] == "/// @brief Generated by dextool version")) 252 .filter!(a => !(a.value.length > 32 && a.value[0 .. 32] == "/// Generated by dextool version")) 253 // remove empty lines 254 .filter!(a => a.value.strip.length != 0); 255 // dfmt on 256 } 257 258 /** Sorted compare of gold and result. 259 * 260 * TODO remove this function when all tests are converted to using BuildCompare. 261 * 262 * max_diff is arbitrarily chosen to 5. 263 * The purpose is to limit the amount of text that is dumped. 264 * The reasoning is that it is better to give more than one line as feedback. 265 */ 266 deprecated("to be removed") void compare(in Path gold, in Path result, 267 Flag!"sortLines" sortLines, Flag!"skipComments" skipComments = Yes.skipComments) { 268 import std.stdio : File; 269 270 yap("Comparing gold:", gold.raw); 271 yap(" result:", result.raw); 272 273 File goldf; 274 File resultf; 275 276 try { 277 goldf = File(gold.escapePath); 278 resultf = File(result.escapePath); 279 } 280 catch (ErrnoException ex) { 281 throw new ErrorLevelException(-1, ex.msg); 282 } 283 284 auto maybeSort(T)(T lines) { 285 import std.array : array; 286 import std.algorithm : sort; 287 288 if (sortLines) { 289 return sort!((a, b) => a[1] < b[1])(lines.array()).array(); 290 } 291 292 return lines.array(); 293 } 294 295 bool diff_detected = false; 296 immutable max_diff = 5; 297 int accumulated_diff; 298 // dfmt off 299 foreach (g, r; 300 lockstep(maybeSort(goldf 301 .byLineCopy() 302 .enumerate 303 .removeJunk(skipComments)), 304 maybeSort(resultf 305 .byLineCopy() 306 .enumerate 307 .removeJunk(skipComments)) 308 )) { 309 if (g[1] != r[1] && accumulated_diff < max_diff) { 310 // +1 of index because editors start counting lines from 1 311 yap("Line ", g[0] + 1, " gold:", g[1]); 312 yap("Line ", r[0] + 1, " out:", r[1], "\n"); 313 diff_detected = true; 314 ++accumulated_diff; 315 } 316 } 317 // dfmt on 318 319 //TODO replace with enforce 320 if (diff_detected) { 321 yap("Output is different from reference file (gold): " ~ gold.escapePath); 322 throw new ErrorLevelException(-1, 323 "Output is different from reference file (gold): " ~ gold.escapePath); 324 } 325 } 326 327 deprecated("to be removed") bool stdoutContains(const string txt) { 328 import std..string : indexOf; 329 330 return getYapLog().joiner().array().indexOf(txt) != -1; 331 } 332 333 /// Check if a log contains the fragment txt. 334 bool sliceContains(const string[] log, const string txt) { 335 import std..string : indexOf; 336 337 return log.dup.joiner().array().indexOf(txt) != -1; 338 } 339 340 /// Check if the logged stdout data contains the input range. 341 bool stdoutContains(T)(const T gold_lines) if (isInputRange!T) { 342 auto result_lines = getYapLog().map!(a => a.splitLines).joiner().array(); 343 return sliceContains(result_lines, gold_lines); 344 } 345 346 /// Check if the log contains the input range. 347 bool sliceContains(T)(const string[] log, const T gold_lines) if (isInputRange!T) { 348 import std.array : array; 349 import std.range : enumerate; 350 import std..string : indexOf; 351 import std.traits : isArray; 352 353 enum ContainState { 354 NotFoundFirstLine, 355 Comparing, 356 BlockFound, 357 BlockNotFound 358 } 359 360 ContainState state; 361 362 auto result_lines = log; 363 size_t gold_idx, result_idx; 364 365 while (!state.among(ContainState.BlockFound, ContainState.BlockNotFound)) { 366 string result_line; 367 // ensure it doesn't do an out-of-range indexing 368 if (result_idx < result_lines.length) { 369 result_line = result_lines[result_idx]; 370 } 371 372 switch (state) with (ContainState) { 373 case NotFoundFirstLine: 374 if (result_line.indexOf(gold_lines[0].strip) != -1) { 375 state = Comparing; 376 ++gold_idx; 377 } else if (result_lines.length == result_idx) { 378 state = BlockNotFound; 379 } 380 break; 381 case Comparing: 382 if (gold_lines.length == gold_idx) { 383 state = BlockFound; 384 } else if (result_lines.length == result_idx) { 385 state = BlockNotFound; 386 } else if (result_line.indexOf(gold_lines[gold_idx].strip) == -1) { 387 state = BlockNotFound; 388 } else { 389 ++gold_idx; 390 } 391 break; 392 default: 393 } 394 395 if (state == ContainState.BlockNotFound && result_lines.length == result_idx) { 396 yap("Error: log do not contain the reference lines"); 397 yap(" Expected: " ~ gold_lines[0]); 398 } else if (state == ContainState.BlockNotFound) { 399 yap("Error: Difference from reference. Line ", gold_idx); 400 yap(" Expected: " ~ gold_lines[gold_idx]); 401 yap(" Actual: " ~ result_line); 402 } 403 404 if (state.among(ContainState.BlockFound, ContainState.BlockNotFound)) { 405 break; 406 } 407 408 ++result_idx; 409 } 410 411 return state == ContainState.BlockFound; 412 } 413 414 /// Check if the logged stdout contains the golden block. 415 ///TODO refactor function. It is unnecessarily complex. 416 bool stdoutContains(in Path gold) { 417 import std.array : array; 418 import std.range : enumerate; 419 import std.stdio : File; 420 421 yap("Contains gold:", gold.raw); 422 423 File goldf; 424 425 try { 426 goldf = File(gold.escapePath); 427 } 428 catch (ErrnoException ex) { 429 yap(ex.msg); 430 return false; 431 } 432 433 bool status = stdoutContains(goldf.byLine.array()); 434 435 if (!status) { 436 yap("Output do not contain the reference file (gold): " ~ gold.escapePath); 437 return false; 438 } 439 440 return true; 441 } 442 443 /** Run dextool. 444 * 445 * Return: The runtime in ms. 446 */ 447 deprecated("to be removed") auto runDextool(T)(in T input, 448 const ref TestEnv testEnv, in string[] pre_args, in string[] flags) { 449 import std.traits : isArray; 450 import std.algorithm : min; 451 452 Args args; 453 args ~= testEnv.dextool; 454 args ~= pre_args.dup; 455 args ~= "--out=" ~ testEnv.outdir.escapePath; 456 457 static if (isArray!T) { 458 foreach (f; input) { 459 args ~= "--in=" ~ f.escapePath; 460 } 461 } else { 462 if (input.escapePath.length > 0) { 463 args ~= "--in=" ~ input.escapePath; 464 } 465 } 466 467 if (flags.length > 0) { 468 args ~= "--"; 469 args ~= flags.dup; 470 } 471 472 import std.datetime; 473 474 StopWatch sw; 475 sw.start; 476 auto output = runAndLog(args.data); 477 sw.stop; 478 yap("Dextool execution time was ms: " ~ sw.peek().msecs.text); 479 480 if (output.status != 0) { 481 auto l = min(100, output.output.length); 482 483 throw new ErrorLevelException(output.status, output.output[0 .. l].dup); 484 } 485 486 return sw.peek.msecs; 487 } 488 489 deprecated("to be removed") auto filesToDextoolInFlags(T)(const T in_files) { 490 Args args; 491 492 static if (isArray!T) { 493 foreach (f; input) { 494 args ~= "--in=" ~ f.escapePath; 495 } 496 } else { 497 if (input.escapePath.length > 0) { 498 args ~= "--in=" ~ input.escapePath; 499 } 500 } 501 502 return args; 503 } 504 505 /** Construct an execution of dextool with needed arguments. 506 */ 507 auto makeDextool(const ref TestEnv testEnv) { 508 import dextool_test.builders : BuildDextoolRun; 509 510 return BuildDextoolRun(testEnv.dextool.escapePath, testEnv.outdir.escapePath); 511 } 512 513 /** Construct an execution of a command. 514 */ 515 auto makeCommand(string command) { 516 return BuildCommandRun(command); 517 } 518 519 /** Construct an execution of a command. 520 */ 521 auto makeCommand(const ref TestEnv testEnv, string command) { 522 return BuildCommandRun(command, testEnv.outdir.escapePath); 523 } 524 525 auto makeCompare(const ref TestEnv env) { 526 import dextool_test.golden : BuildCompare; 527 528 return BuildCompare(env.outdir.escapePath); 529 } 530 531 deprecated("to be removed") void compareResult(T...)(Flag!"sortLines" sortLines, 532 Flag!"skipComments" skipComments, in T args) { 533 static assert(args.length >= 1); 534 535 foreach (a; args) { 536 if (existsAsFile(a.gold)) { 537 compare(a.gold, a.result, sortLines, skipComments); 538 } 539 } 540 } 541 542 string testId(uint line = __LINE__) { 543 import std.conv : to; 544 545 // assuming it is always the UDA for a test and thus +1 to get the correct line 546 return "id:" ~ (line + 1).to!string() ~ " "; 547 } 548 549 /** 550 * Params: 551 * dir = directory to perform the recursive search in 552 * ext = extension of the files to match (including dot) 553 * 554 * Returns: a list of all files with the extension 555 */ 556 auto recursiveFilesWithExtension(Path dir, string ext) { 557 // dfmt off 558 return std.file.dirEntries(dir.toString, SpanMode.depth) 559 .filter!(a => a.isFile) 560 .filter!(a => extension(a.name) == ext) 561 .map!(a => Path(a)); 562 // dfmt on 563 }