1 #!/usr/bin/env rdmd 2 module d_do_test; 3 4 import std.algorithm; 5 import std.array; 6 import std.conv; 7 import std.datetime.stopwatch; 8 import std.datetime.systime; 9 import std.exception; 10 import std.file; 11 import std.format; 12 import std.process; 13 import std.random; 14 import std.range : chain; 15 import std.regex; 16 import std.path; 17 import std.stdio; 18 import std..string; 19 import core.sys.posix.sys.wait; 20 21 const dmdTestDir = __FILE_FULL_PATH__.dirName.dirName; 22 23 version(Win32) 24 { 25 extern(C) int putenv(const char*); 26 } 27 28 void usage() 29 { 30 write("d_do_test <test_file>\n" 31 ~ "\n" 32 ~ " Note: this program is normally called through the Makefile, it" 33 ~ " is not meant to be called directly by the user.\n" 34 ~ "\n" 35 ~ " example: d_do_test runnable/pi.d\n" 36 ~ "\n" 37 ~ " relevant environment variables:\n" 38 ~ " ARGS: set to execute all combinations of\n" 39 ~ " REQUIRED_ARGS: arguments always passed to the compiler\n" 40 ~ " DMD: compiler to use, ex: ../src/dmd (required)\n" 41 ~ " CC: C++ compiler to use, ex: dmc, g++\n" 42 ~ " OS: windows, linux, freebsd, osx, netbsd, dragonflybsd\n" 43 ~ " RESULTS_DIR: base directory for test results\n" 44 ~ " MODEL: 32 or 64 (required)\n" 45 ~ " AUTO_UPDATE: set to 1 to auto-update mismatching test output\n" 46 ~ " PRINT_RUNTIME: set to 1 to print test runtime\n" 47 ~ "\n" 48 ~ " windows vs non-windows portability env vars:\n" 49 ~ " DSEP: \\\\ or /\n" 50 ~ " SEP: \\ or / (required)\n" 51 ~ " OBJ: .obj or .o (required)\n" 52 ~ " EXE: .exe or <null> (required)\n"); 53 } 54 55 enum TestMode 56 { 57 COMPILE, 58 FAIL_COMPILE, 59 RUN, 60 DSHELL, 61 } 62 63 struct TestArgs 64 { 65 TestMode mode; 66 67 bool compileSeparately; 68 bool link; 69 bool clearDflags; /// whether DFLAGS should be cleared before invoking dmd 70 string executeArgs; 71 string cxxflags; 72 string[] sources; 73 string[] compiledImports; 74 string[] cppSources; 75 string[] objcSources; 76 string permuteArgs; 77 string[] argSets; 78 string compileOutput; 79 string compileOutputFile; /// file containing the expected output 80 string runOutput; /// Expected output of the compiled executable 81 string gdbScript; 82 string gdbMatch; 83 string postScript; 84 string[] outputFiles; /// generated files appended to the compilation output 85 string transformOutput; /// Transformations for the compiler output 86 string requiredArgs; 87 string requiredArgsForLink; 88 string disabledReason; // if empty, the test is not disabled 89 90 bool isDisabled() const { return disabledReason.length != 0; } 91 } 92 93 struct EnvData 94 { 95 string all_args; 96 string dmd; 97 string results_dir; 98 string sep; 99 string dsep; 100 string obj; 101 string exe; 102 string os; 103 string compiler; 104 string ccompiler; 105 string model; 106 string required_args; 107 string cxxCompatFlags; /// Additional flags passed to $(compiler) when `EXTRA_CPP_SOURCES` is present 108 string[] picFlag; /// Compiler flag for PIC (if requested from environment) 109 bool dobjc; 110 bool coverage_build; 111 bool autoUpdate; 112 bool printRuntime; /// Print time spent on a single test 113 bool usingMicrosoftCompiler; 114 bool tryDisabled; /// Silently try disabled tests (ignore failure but report success) 115 } 116 117 /++ 118 Creates a new EnvData instance based on the current environment. 119 Other code should not read from the environment. 120 121 Returns: an initialized EnvData instance 122 ++/ 123 immutable(EnvData) processEnvironment() 124 { 125 static string envGetRequired(in char[] name) 126 { 127 if (auto value = environment.get(name)) 128 return value; 129 130 writefln("Error: Missing environment variable '%s', was this called through the Makefile?", 131 name); 132 throw new SilentQuit(); 133 } 134 135 EnvData envData; 136 envData.all_args = environment.get("ARGS"); 137 envData.results_dir = envGetRequired("RESULTS_DIR"); 138 envData.sep = envGetRequired ("SEP"); 139 envData.dsep = environment.get("DSEP"); 140 envData.obj = envGetRequired ("OBJ"); 141 envData.exe = envGetRequired ("EXE"); 142 envData.os = environment.get("OS"); 143 envData.dmd = replace(envGetRequired("DMD"), "/", envData.sep); 144 envData.compiler = "dmd"; //should be replaced for other compilers 145 envData.ccompiler = environment.get("CC"); 146 envData.model = envGetRequired("MODEL"); 147 envData.required_args = environment.get("REQUIRED_ARGS"); 148 envData.dobjc = environment.get("D_OBJC") == "1"; 149 envData.coverage_build = environment.get("DMD_TEST_COVERAGE") == "1"; 150 envData.autoUpdate = environment.get("AUTO_UPDATE", "") == "1"; 151 envData.printRuntime = environment.get("PRINT_RUNTIME", "") == "1"; 152 envData.tryDisabled = environment.get("TRY_DISABLED") == "1"; 153 154 if (envData.ccompiler.empty) 155 { 156 if (envData.os != "windows") 157 envData.ccompiler = "c++"; 158 else if (envData.model == "32") 159 envData.ccompiler = "dmc"; 160 else if (envData.model == "64") 161 envData.ccompiler = `C:\"Program Files (x86)"\"Microsoft Visual Studio 10.0"\VC\bin\amd64\cl.exe`; 162 else 163 { 164 writeln("Unknown $OS$MODEL combination: ", envData.os, envData.model); 165 throw new SilentQuit(); 166 } 167 } 168 169 envData.usingMicrosoftCompiler = envData.ccompiler.toLower.endsWith("cl.exe"); 170 171 version (Windows) {} else 172 { 173 version(X86_64) 174 envData.picFlag = ["-fPIC"]; 175 if (environment.get("PIC", null) == "1") 176 envData.picFlag = ["-fPIC"]; 177 } 178 179 switch (envData.compiler) 180 { 181 case "dmd": 182 case "ldc": 183 if(envData.os != "windows") 184 envData.cxxCompatFlags = " -L-lstdc++ -L--no-demangle"; 185 break; 186 187 case "gdc": 188 envData.cxxCompatFlags = "-Xlinker -lstdc++ -Xlinker --no-demangle"; 189 break; 190 191 default: 192 writeln("Unknown compiler: ", envData.compiler); 193 throw new SilentQuit(); 194 } 195 196 return cast(immutable) envData; 197 } 198 199 bool findTestParameter(const ref EnvData envData, string file, string token, ref string result, string multiLineDelimiter = " ") 200 { 201 auto tokenStart = std..string.indexOf(file, token); 202 if (tokenStart == -1) return false; 203 204 file = file[tokenStart + token.length .. $]; 205 206 auto lineEndR = std..string.indexOf(file, "\r"); 207 auto lineEndN = std..string.indexOf(file, "\n"); 208 auto lineEnd = lineEndR == -1 ? 209 (lineEndN == -1 ? file.length : lineEndN) : 210 (lineEndN == -1 ? lineEndR : min(lineEndR, lineEndN)); 211 212 //writeln("found ", token, " in line: ", file.length, ", ", tokenStart, ", ", tokenStart+lineEnd); 213 //writeln("found ", token, " in line: '", file[tokenStart .. tokenStart+lineEnd], "'"); 214 215 result = file[0 .. lineEnd]; 216 const commentStart = std..string.indexOf(result, "//"); 217 if (commentStart != -1) 218 result = result[0 .. commentStart]; 219 result = strip(result); 220 221 // filter by OS specific setting (os1 os2 ...) 222 if (result.startsWith("(")) 223 { 224 auto close = std..string.indexOf(result, ")"); 225 if (close >= 0) 226 { 227 string[] oss = split(result[1 .. close], " "); 228 if (oss.canFind(envData.os)) 229 result = result[close + 1 .. $]; 230 else 231 result = null; 232 } 233 } 234 // skips the :, if present 235 if (result.startsWith(":")) 236 result = strip(result[1 .. $]); 237 238 //writeln("arg: '", result, "'"); 239 240 string result2; 241 if (findTestParameter(envData, file[lineEnd .. $], token, result2, multiLineDelimiter)) 242 { 243 if (result2.length > 0) 244 { 245 if (result.length == 0) 246 result = result2; 247 else 248 result ~= multiLineDelimiter ~ result2; 249 } 250 } 251 252 // fix-up separators 253 result = result.unifyDirSep(envData.sep); 254 255 return true; 256 } 257 258 bool findOutputParameter(string file, string token, out string result, string sep) 259 { 260 bool found = false; 261 262 while (true) 263 { 264 const istart = std..string.indexOf(file, token); 265 if (istart == -1) 266 break; 267 found = true; 268 269 file = file[istart + token.length .. $]; 270 271 enum embed_sep = "---"; 272 auto n = std..string.indexOf(file, embed_sep); 273 274 enforce(n != -1, "invalid "~token~" format"); 275 n += embed_sep.length; 276 while (file[n] == '-') ++n; 277 if (file[n] == '\r') ++n; 278 if (file[n] == '\n') ++n; 279 280 file = file[n .. $]; 281 auto iend = std..string.indexOf(file, embed_sep); 282 enforce(iend != -1, "invalid TEST_OUTPUT format"); 283 284 result ~= file[0 .. iend]; 285 286 while (file[iend] == '-') ++iend; 287 file = file[iend .. $]; 288 } 289 290 if (found) 291 { 292 result = std..string.strip(result); 293 result = result.unifyNewLine().unifyDirSep(sep); 294 result = result ? result : ""; // keep non-null 295 } 296 return found; 297 } 298 299 void replaceResultsDir(ref string arguments, const ref EnvData envData) 300 { 301 // Bash would expand this automatically on Posix, but we need to manually 302 // perform the replacement for Windows compatibility. 303 arguments = replace(arguments, "${RESULTS_DIR}", envData.results_dir); 304 } 305 306 string getDisabledReason(string[] disabledPlatforms, const ref EnvData envData) 307 { 308 if (disabledPlatforms.length == 0) 309 return null; 310 311 const target = ((envData.os == "windows") ? "win" : envData.os) ~ envData.model; 312 313 // allow partial matching, e.g. `win` to disable both win32 and win64 314 const i = disabledPlatforms.countUntil!(p => target.canFind(p)); 315 if (i != -1) 316 return "on " ~ disabledPlatforms[i]; 317 318 return null; 319 } 320 321 bool gatherTestParameters(ref TestArgs testArgs, string input_dir, string input_file, const ref EnvData envData) 322 { 323 string file = cast(string)std.file.read(input_file); 324 325 string dflagsStr; 326 testArgs.clearDflags = findTestParameter(envData, file, "DFLAGS", dflagsStr); 327 enforce(dflagsStr.empty, "The DFLAGS test argument must be empty: It is '" ~ dflagsStr ~ "'"); 328 329 findTestParameter(envData, file, "REQUIRED_ARGS", testArgs.requiredArgs); 330 if (envData.required_args.length) 331 { 332 if (testArgs.requiredArgs.length) 333 testArgs.requiredArgs ~= " " ~ envData.required_args; 334 else 335 testArgs.requiredArgs = envData.required_args; 336 } 337 replaceResultsDir(testArgs.requiredArgs, envData); 338 339 if (! findTestParameter(envData, file, "PERMUTE_ARGS", testArgs.permuteArgs)) 340 { 341 if (testArgs.mode == TestMode.RUN) 342 testArgs.permuteArgs = envData.all_args; 343 } 344 replaceResultsDir(testArgs.permuteArgs, envData); 345 346 // remove permute args enforced as required anyway 347 if (testArgs.requiredArgs.length && testArgs.permuteArgs.length) 348 { 349 const required = split(testArgs.requiredArgs); 350 const newPermuteArgs = split(testArgs.permuteArgs) 351 .filter!(a => !required.canFind(a)) 352 .join(" "); 353 testArgs.permuteArgs = newPermuteArgs; 354 } 355 356 // tests can override -verrors by using REQUIRED_ARGS 357 if (testArgs.mode == TestMode.FAIL_COMPILE) 358 testArgs.requiredArgs = "-verrors=0 " ~ testArgs.requiredArgs; 359 360 // https://issues.dlang.org/show_bug.cgi?id=10664: exceptions don't work reliably with COMDAT folding 361 // it also slows down some tests drastically, e.g. runnable/test17338.d 362 if (envData.usingMicrosoftCompiler) 363 testArgs.requiredArgs ~= " -L/OPT:NOICF"; 364 365 { 366 string argSetsStr; 367 findTestParameter(envData, file, "ARG_SETS", argSetsStr, ";"); 368 foreach(s; split(argSetsStr, ";")) 369 { 370 replaceResultsDir(s, envData); 371 testArgs.argSets ~= s; 372 } 373 } 374 375 // win(32|64) doesn't support pic 376 if (envData.os == "windows") 377 { 378 auto index = std..string.indexOf(testArgs.permuteArgs, "-fPIC"); 379 if (index != -1) 380 testArgs.permuteArgs = testArgs.permuteArgs[0 .. index] ~ testArgs.permuteArgs[index+5 .. $]; 381 } 382 383 // clean up extra spaces 384 testArgs.permuteArgs = strip(replace(testArgs.permuteArgs, " ", " ")); 385 386 if (findTestParameter(envData, file, "EXECUTE_ARGS", testArgs.executeArgs)) 387 { 388 replaceResultsDir(testArgs.executeArgs, envData); 389 // Always run main even if compiled with '-unittest' but let 390 // tests switch to another behaviour if necessary 391 if (!testArgs.executeArgs.canFind("--DRT-testmode")) 392 testArgs.executeArgs ~= " --DRT-testmode=run-main"; 393 } 394 395 string extraSourcesStr; 396 findTestParameter(envData, file, "EXTRA_SOURCES", extraSourcesStr); 397 testArgs.sources = [input_file]; 398 // prepend input_dir to each extra source file 399 foreach(s; split(extraSourcesStr)) 400 testArgs.sources ~= input_dir ~ "/" ~ s; 401 402 { 403 string compiledImports; 404 findTestParameter(envData, file, "COMPILED_IMPORTS", compiledImports); 405 foreach(s; split(compiledImports)) 406 testArgs.compiledImports ~= input_dir ~ "/" ~ s; 407 } 408 409 findTestParameter(envData, file, "CXXFLAGS", testArgs.cxxflags); 410 string extraCppSourcesStr; 411 findTestParameter(envData, file, "EXTRA_CPP_SOURCES", extraCppSourcesStr); 412 testArgs.cppSources = split(extraCppSourcesStr); 413 414 if (testArgs.cppSources.length) 415 testArgs.requiredArgs ~= envData.cxxCompatFlags; 416 417 string extraObjcSourcesStr; 418 auto objc = findTestParameter(envData, file, "EXTRA_OBJC_SOURCES", extraObjcSourcesStr); 419 420 if (objc && !envData.dobjc) 421 return false; 422 423 testArgs.objcSources = split(extraObjcSourcesStr); 424 425 // swap / with $SEP 426 if (envData.sep && envData.sep != "/") 427 foreach (ref s; testArgs.sources) 428 s = replace(s, "/", to!string(envData.sep)); 429 //writeln ("sources: ", testArgs.sources); 430 431 { 432 string throwAway; 433 testArgs.link = findTestParameter(envData, file, "LINK", throwAway); 434 } 435 436 // COMPILE_SEPARATELY can take optional compiler switches when link .o files 437 testArgs.compileSeparately = findTestParameter(envData, file, "COMPILE_SEPARATELY", testArgs.requiredArgsForLink); 438 439 string disabledPlatformsStr; 440 findTestParameter(envData, file, "DISABLED", disabledPlatformsStr); 441 442 version (DragonFlyBSD) 443 { 444 // DragonFlyBSD is x86_64 only, instead of adding DISABLED to a lot of tests, just exclude them from running 445 if (testArgs.requiredArgs.canFind("-m32")) 446 testArgs.disabledReason = "on DragonFlyBSD (no -m32)"; 447 } 448 449 version (ARM) enum supportsM64 = false; 450 else version (MIPS32) enum supportsM64 = false; 451 else version (PPC) enum supportsM64 = false; 452 else enum supportsM64 = true; 453 454 static if (!supportsM64) 455 { 456 if (testArgs.requiredArgs.canFind("-m64")) 457 testArgs.disabledReason = "because target doesn't support -m64"; 458 } 459 460 if (!testArgs.isDisabled) 461 testArgs.disabledReason = getDisabledReason(split(disabledPlatformsStr), envData); 462 463 findTestParameter(envData, file, "TEST_OUTPUT_FILE", testArgs.compileOutputFile); 464 465 // Only check for TEST_OUTPUT is no file was given because it would 466 // partially match TEST_OUTPUT_FILE 467 if (testArgs.compileOutputFile) 468 { 469 // Don't require tests to specify the test directory 470 testArgs.compileOutputFile = input_dir.buildPath(testArgs.compileOutputFile); 471 testArgs.compileOutput = readText(testArgs.compileOutputFile) 472 .unifyNewLine() // Avoid CRLF issues 473 .strip(); 474 } 475 else 476 findOutputParameter(file, "TEST_OUTPUT", testArgs.compileOutput, envData.sep); 477 478 string outFilesStr; 479 findTestParameter(envData, file, "OUTPUT_FILES", outFilesStr); 480 testArgs.outputFiles = outFilesStr.split(';'); 481 482 findTestParameter(envData, file, "TRANSFORM_OUTPUT", testArgs.transformOutput); 483 484 findOutputParameter(file, "RUN_OUTPUT", testArgs.runOutput, envData.sep); 485 486 findOutputParameter(file, "GDB_SCRIPT", testArgs.gdbScript, envData.sep); 487 findTestParameter(envData, file, "GDB_MATCH", testArgs.gdbMatch); 488 489 if (findTestParameter(envData, file, "POST_SCRIPT", testArgs.postScript)) 490 testArgs.postScript = replace(testArgs.postScript, "/", to!string(envData.sep)); 491 492 return true; 493 } 494 495 string[] combinations(string argstr) 496 { 497 string[] results; 498 string[] args = split(argstr); 499 long combinations = 1 << args.length; 500 for (size_t i = 0; i < combinations; i++) 501 { 502 string r; 503 bool printed = false; 504 505 for (size_t j = 0; j < args.length; j++) 506 { 507 if (i & 1 << j) 508 { 509 if (printed) 510 r ~= " "; 511 r ~= args[j]; 512 printed = true; 513 } 514 } 515 516 results ~= r; 517 } 518 519 return results; 520 } 521 522 string genTempFilename(string result_path) 523 { 524 auto a = appender!string(); 525 a.put(result_path); 526 foreach (ref e; 0 .. 8) 527 { 528 formattedWrite(a, "%x", rndGen.front); 529 rndGen.popFront(); 530 } 531 532 return a.data; 533 } 534 535 int system(string command) 536 { 537 static import core.stdc.stdlib; 538 if (!command) return core.stdc.stdlib.system(null); 539 const commandz = toStringz(command); 540 auto status = core.stdc.stdlib.system(commandz); 541 if (status == -1) return status; 542 version (Windows) status <<= 8; 543 return status; 544 } 545 546 version(Windows) 547 { 548 extern (D) bool WIFEXITED( int status ) { return ( status & 0x7F ) == 0; } 549 extern (D) int WEXITSTATUS( int status ) { return ( status & 0xFF00 ) >> 8; } 550 extern (D) int WTERMSIG( int status ) { return status & 0x7F; } 551 extern (D) bool WIFSIGNALED( int status ) 552 { 553 return ( cast(byte) ( ( status & 0x7F ) + 1 ) >> 1 ) > 0; 554 } 555 } 556 557 void removeIfExists(in char[] filename) 558 { 559 if (std.file.exists(filename)) 560 std.file.remove(filename); 561 } 562 563 string execute(ref File f, string command, bool expectpass, string result_path) 564 { 565 auto filename = genTempFilename(result_path); 566 scope(exit) removeIfExists(filename); 567 568 auto rc = system(command ~ " > " ~ filename ~ " 2>&1"); 569 570 string output = readText(filename); 571 f.writeln(command); 572 f.write(output); 573 574 if (WIFSIGNALED(rc)) 575 { 576 auto value = WTERMSIG(rc); 577 enforce(0 == value, "caught signal: " ~ to!string(value)); 578 } 579 else if (WIFEXITED(rc)) 580 { 581 auto value = WEXITSTATUS(rc); 582 if (expectpass) 583 enforce(0 == value, "expected rc == 0, exited with rc == " ~ to!string(value)); 584 else 585 enforce(1 == value, "expected rc == 1, but exited with rc == " ~ to!string(value)); 586 } 587 588 return output; 589 } 590 591 /// add quotes around the whole string if it contains spaces that are not in quotes 592 string quoteSpaces(string str) 593 { 594 if (str.indexOf(' ') < 0) 595 return str; 596 bool inquote = false; 597 foreach(dchar c; str) 598 if (c == '"') 599 inquote = !inquote; 600 else if (c == ' ' && !inquote) 601 return "\"" ~ str ~ "\""; 602 return str; 603 } 604 605 string unifyNewLine(string str) 606 { 607 // On Windows, Outbuffer.writenl() puts `\r\n` into the buffer, 608 // then fprintf() adds another `\r` when formatting the message. 609 // This is why there's a match for `\r\r\n` in this regex. 610 static re = regex(`\r\r\n|\r\n|\r|\n`, "g"); 611 return std.regex.replace(str, re, "\n"); 612 } 613 614 string unifyDirSep(string str, string sep) 615 { 616 static re = regex(`(?<=[-\w{}][-\w{}]*)/(?=[-\w][-\w/]*\.(di?|mixin)\b)`, "g"); 617 return std.regex.replace(str, re, sep); 618 } 619 unittest 620 { 621 assert(`fail_compilation/test.d(1) Error: dummy error message for 'test'`.unifyDirSep(`\`) 622 == `fail_compilation\test.d(1) Error: dummy error message for 'test'`); 623 assert(`fail_compilation/test.d(1) Error: at fail_compilation/test.d(2)`.unifyDirSep(`\`) 624 == `fail_compilation\test.d(1) Error: at fail_compilation\test.d(2)`); 625 626 assert(`fail_compilation/test.d(1) Error: at fail_compilation/imports/test.d(2)`.unifyDirSep(`\`) 627 == `fail_compilation\test.d(1) Error: at fail_compilation\imports\test.d(2)`); 628 assert(`fail_compilation/diag.d(2): Error: fail_compilation/imports/fail.d must be imported`.unifyDirSep(`\`) 629 == `fail_compilation\diag.d(2): Error: fail_compilation\imports\fail.d must be imported`); 630 631 assert(`{{RESULTS_DIR}}/fail_compilation/mixin_test.mixin(7): Error:`.unifyDirSep(`\`) 632 == `{{RESULTS_DIR}}\fail_compilation\mixin_test.mixin(7): Error:`); 633 } 634 635 bool collectExtraSources (in string input_dir, in string output_dir, in string[] extraSources, 636 ref string[] sources, in EnvData envData, in string compiler, 637 const(char)[] cxxflags, ref File logfile) 638 { 639 foreach (cur; extraSources) 640 { 641 auto curSrc = input_dir ~ envData.sep ~"extra-files" ~ envData.sep ~ cur; 642 auto curObj = output_dir ~ envData.sep ~ cur ~ envData.obj; 643 string command = quoteSpaces(compiler); 644 if (envData.compiler == "dmd") 645 { 646 if (envData.usingMicrosoftCompiler) 647 { 648 command ~= ` /c /nologo `~curSrc~` /Fo`~curObj; 649 } 650 else if (envData.os == "windows" && envData.model == "32") 651 { 652 command ~= " -c "~curSrc~" -o"~curObj; 653 } 654 else 655 { 656 command ~= " -m"~envData.model~" -c "~curSrc~" -o "~curObj; 657 } 658 } 659 else 660 { 661 command ~= " -m"~envData.model~" -c "~curSrc~" -o "~curObj; 662 } 663 if (cxxflags) 664 command ~= " " ~ cxxflags; 665 666 logfile.writeln(command); 667 logfile.flush(); // Avoid reordering due to buffering 668 669 auto pid = spawnShell(command, stdin, logfile, logfile, null, Config.retainStdout | Config.retainStderr); 670 if(wait(pid)) 671 { 672 return false; 673 } 674 sources ~= curObj; 675 } 676 677 return true; 678 } 679 680 /++ 681 Applies custom transformations defined in transformOutput to testOutput. 682 683 Currently the following actions are supported: 684 * "sanitize_json" = replace compiler/plattform specific data from generated JSON 685 * "remove_lines(<re>)" = remove all lines matching a regex <re> 686 687 Params: 688 testOutput = the existing output to be modified 689 transformOutput = list of transformation identifiers 690 ++/ 691 void applyOutputTransformations(ref string testOutput, string transformOutput) 692 { 693 while (transformOutput.length) 694 { 695 string step, arg; 696 697 const idx = transformOutput.countUntil(' ', '('); 698 if (idx == -1) 699 { 700 step = transformOutput; 701 transformOutput = null; 702 } 703 else 704 { 705 step = transformOutput[0 .. idx]; 706 const hasArgs = transformOutput[idx] == '('; 707 transformOutput = transformOutput[idx + 1 .. $]; 708 if (hasArgs) 709 { 710 // "..." quotes are optional but necessary if the arg contains ')' 711 const isQuoted = transformOutput[0] == '"'; 712 const end = isQuoted ? `"` : `)`; 713 auto parts = transformOutput[isQuoted .. $].findSplit(end); 714 enforce(parts, "Missing closing `" ~ end ~ "`!"); 715 arg = parts[0]; 716 transformOutput = parts[2][isQuoted .. $]; 717 } 718 719 // Skip space between steps 720 import std.ascii : isWhite; 721 transformOutput.skipOver!isWhite(); 722 } 723 724 switch (step) 725 { 726 case "sanitize_json": 727 { 728 import sanitize_json : sanitize; 729 sanitize(testOutput); 730 break; 731 } 732 733 case "remove_lines": 734 { 735 auto re = regex(arg); 736 testOutput = testOutput 737 .splitter('\n') 738 .filter!(line => !line.matchFirst(re)) 739 .join('\n'); 740 break; 741 } 742 743 default: 744 throw new Exception(format(`Unknown transformation: "%s"!`, step)); 745 } 746 } 747 } 748 749 unittest 750 { 751 static void test(string input, const string transformations, const string expected) 752 { 753 applyOutputTransformations(input, transformations); 754 assert(input == expected); 755 } 756 757 static void testJson(const string transformations, const string expectedJson) 758 { 759 test(`{ 760 "modules": [ 761 { 762 "file": "/path/to/the/file", 763 "kind": "module", 764 "members": [] 765 } 766 ] 767 }`, transformations, expectedJson); 768 } 769 770 771 testJson("sanitize_json", `{ 772 "modules": [ 773 { 774 "file": "VALUE_REMOVED_FOR_TEST", 775 "kind": "module", 776 "members": [] 777 } 778 ] 779 }`); 780 781 testJson(`sanitize_json remove_lines("kind")`, `{ 782 "modules": [ 783 { 784 "file": "VALUE_REMOVED_FOR_TEST", 785 "members": [] 786 } 787 ] 788 }`); 789 790 testJson(`sanitize_json remove_lines("kind") remove_lines("file")`, `{ 791 "modules": [ 792 { 793 "members": [] 794 } 795 ] 796 }`); 797 798 test(`This is a text containing 799 some words which is a text sample 800 nevertheless`, 801 `remove_lines(text sample)`, 802 `This is a text containing 803 nevertheless`); 804 805 test(`This is a text with 806 a random ) which should 807 still work`, 808 `remove_lines("random \)")`, 809 `This is a text with 810 still work`); 811 812 test(`Tom bought 813 12 apples 814 and 6 berries 815 from the store`, 816 `remove_lines("(\d+)")`, 817 `Tom bought 818 from the store`); 819 820 assertThrown(test("", "unknown", "")); 821 } 822 823 /++ 824 Compares the output string to the reference string by character 825 except parts marked with one of the following special sequences: 826 827 $n$ = numbers (e.g. compiler generated unique identifiers) 828 $p:<path>$ = real paths ending with <path> 829 $?:<choices>$ = environment dependent content supplied as a list 830 choices (either <condition>=<content> or <default>), 831 separated by a '|'. Currently supported conditions are 832 OS and model as supplied from the environment 833 834 Params: 835 output = the real output 836 refoutput = the expected output 837 envData = test environment 838 839 Returns: whether output matches the expected refoutput 840 ++/ 841 bool compareOutput(string output, string refoutput, const ref EnvData envData) 842 { 843 // If no output is expected, only check that nothing was captured. 844 if (refoutput.length == 0) 845 return (output.length == 0) ? true : false; 846 847 for ( ; ; ) 848 { 849 auto special = refoutput.find("$n$", "$p:", "$?:").rename!("remainder", "id"); 850 851 // Simple equality check if no special tokens remain 852 if (special.id == 0) 853 return refoutput == output; 854 855 const expected = refoutput[0 .. $ - special.remainder.length]; 856 857 // Check until the special token 858 if (!output.skipOver(expected)) 859 return false; 860 861 // Discard the special token and progress output appropriately 862 refoutput = special.remainder[3 .. $]; 863 864 if (special.id == 1) // $n$ 865 { 866 import std.ascii : isDigit; 867 output.skipOver!isDigit(); 868 continue; 869 } 870 871 // $<identifier>:<special content>$ 872 /// ( special content, "$", remaining expected output ) 873 auto refparts = refoutput.findSplit("$"); 874 enforce(refparts, "Malformed special sequence!"); 875 refoutput = refparts[2]; 876 877 if (special.id == 2) // $p:<some path>$ 878 { 879 // special content is the expected path tail 880 // Substitute / with the appropriate directory separator 881 auto pathEnd = refparts[0].replace("/", envData.sep); 882 883 /// ( whole path, remaining output ) 884 auto parts = output.findSplitAfter(pathEnd); 885 886 if (!parts || !exists(parts[0])) 887 return false; 888 889 output = parts[1]; 890 continue; 891 } 892 893 // $?:<predicate>=<content>(;<predicate>=<content>)*(;<default>)?$ 894 string toSkip = null; 895 896 foreach (const chunk; refparts[0].splitter('|')) 897 { 898 // ( <predicate> , "=", <content> ) 899 const conditional = chunk.findSplit("="); 900 901 if (!conditional) // <default> 902 { 903 toSkip = chunk; 904 break; 905 } 906 // Match against OS or model (accepts "32mscoff" as "32") 907 else if (conditional[0].splitter('+').all!(c => c.among(envData.os, envData.model, envData.model[0 .. min(2, $)]))) 908 { 909 toSkip = conditional[2]; 910 break; 911 } 912 } 913 914 if (toSkip !is null && !output.skipOver(toSkip)) 915 return false; 916 } 917 } 918 919 unittest 920 { 921 EnvData ed; 922 version (Windows) 923 ed.sep = `\`; 924 else 925 ed.sep = `/`; 926 927 assert( compareOutput(`Grass is green`, `Grass is green`, ed)); 928 assert(!compareOutput(`Grass is green`, `Grass was green`, ed)); 929 930 assert( compareOutput(`Bob took 12 apples`, `Bob took $n$ apples`, ed)); 931 assert(!compareOutput(`Bob took abc apples`, `Bob took $n$ apples`, ed)); 932 assert(!compareOutput(`Bob took 12 berries`, `Bob took $n$ apples`, ed)); 933 934 assert( compareOutput(`HINT: ` ~ __FILE_FULL_PATH__ ~ ` is important`, `HINT: $p:d_do_test.d$ is important`, ed)); 935 assert( compareOutput(`HINT: ` ~ __FILE_FULL_PATH__ ~ ` is important`, `HINT: $p:test/tools/d_do_test.d$ is important`, ed)); 936 937 ed.sep = "/"; 938 assert(!compareOutput(`See /path/to/druntime/import/object.d`, `See $p:druntime/import/object.d$`, ed)); 939 940 assertThrown(compareOutput(`Path /a/b/c.d!`, `Path $p:c.d!`, ed)); // Missing closing $ 941 942 const fmt = "This $?:windows=A|posix=B|C$ uses $?:64=1|32=2|3$ bytes"; 943 944 assert( compareOutput("This C uses 3 bytes", fmt, ed)); 945 946 ed.os = "posix"; 947 ed.model = "64"; 948 assert( compareOutput("This B uses 1 bytes", fmt, ed)); 949 assert(!compareOutput("This C uses 3 bytes", fmt, ed)); 950 951 const emptyFmt = "On <$?:windows=abc|$> use <$?:posix=$>!"; 952 assert(compareOutput("On <> use <>!", emptyFmt, ed)); 953 954 ed.model = "32mscoff"; 955 assert(compareOutput("size_t is uint!", "size_t is $?:32=uint|64=ulong$!", ed)); 956 957 assert(compareOutput("no", "$?:posix+64=yes|no$", ed)); 958 ed.model = "64"; 959 assert(compareOutput("yes", "$?:posix+64=yes|no$", ed)); 960 } 961 962 /++ 963 Creates a diff of the expected and actual test output. 964 965 Params: 966 expected = the expected output 967 expectedFile = file containing expected (if present, null otherwise) 968 actual = the actual output 969 name = the test files name 970 971 Returns: the comparison created by the `diff` utility 972 ++/ 973 string generateDiff(const string expected, string expectedFile, 974 const string actual, const string name) 975 { 976 string actualFile = tempDir.buildPath("actual_" ~ name); 977 File(actualFile, "w").writeln(actual); // Append \n 978 scope (exit) remove(actualFile); 979 980 const needTmp = !expectedFile; 981 if (needTmp) // Create a temporary file 982 { 983 expectedFile = tempDir.buildPath("expected_" ~ name); 984 File(expectedFile, "w").writeln(expected); // Append \n 985 } 986 // Remove temporary file 987 scope (exit) if (needTmp) 988 remove(expectedFile); 989 990 const cmd = ["diff", "-pu", "--strip-trailing-cr", expectedFile, actualFile]; 991 try 992 { 993 string diff = std.process.execute(cmd).output; 994 // Skip diff's status lines listing the diffed files and line count 995 foreach (_; 0..3) 996 diff = diff.findSplitAfter("\n")[1]; 997 return diff; 998 } 999 catch (Exception e) 1000 return format(`%-(%s, %) failed: %s`, cmd, e.msg); 1001 } 1002 1003 class SilentQuit : Exception { this() { super(null); } } 1004 1005 class CompareException : Exception 1006 { 1007 string expected; 1008 string actual; 1009 bool fromRun; /// Compared execution instead of compilation output 1010 1011 this(string expected, string actual, string diff, bool fromRun = false) { 1012 string msg = "\nexpected:\n----\n" ~ expected ~ 1013 "\n----\nactual:\n----\n" ~ actual ~ 1014 "\n----\ndiff:\n----\n" ~ diff ~ "----\n"; 1015 super(msg); 1016 this.expected = expected; 1017 this.actual = actual; 1018 this.fromRun = fromRun; 1019 } 1020 } 1021 1022 version(unittest) void main(){} else 1023 int main(string[] args) 1024 { 1025 try { return tryMain(args); } 1026 catch(SilentQuit) { return 1; } 1027 } 1028 1029 int tryMain(string[] args) 1030 { 1031 if (args.length != 2) 1032 { 1033 usage(); 1034 return 1; 1035 } 1036 1037 immutable envData = processEnvironment(); 1038 1039 const input_file = args[1]; 1040 const input_dir = input_file.dirName(); 1041 const test_base_name = input_file.baseName(); 1042 const test_name = test_base_name.stripExtension(); 1043 1044 const result_path = envData.results_dir ~ envData.sep; 1045 const output_dir = result_path ~ input_dir; 1046 const output_file = result_path ~ input_file ~ ".out"; 1047 1048 TestArgs testArgs; 1049 switch (input_dir) 1050 { 1051 case "compilable": testArgs.mode = TestMode.COMPILE; break; 1052 case "fail_compilation": testArgs.mode = TestMode.FAIL_COMPILE; break; 1053 case "runnable", "runnable_cxx": 1054 // running & linking costs time - for coverage builds we can save this 1055 testArgs.mode = envData.coverage_build ? TestMode.COMPILE : TestMode.RUN; 1056 break; 1057 1058 case "dshell": 1059 testArgs.mode = TestMode.DSHELL; 1060 return runDShellTest(input_dir, test_name, envData, output_dir, output_file); 1061 1062 default: 1063 writefln("Error: invalid test directory '%s', expected 'compilable', 'fail_compilation', 'runnable', 'runnable_cxx' or 'dshell'", input_dir); 1064 return 1; 1065 } 1066 1067 if (test_base_name.extension() == ".sh") 1068 { 1069 string file = cast(string) std.file.read(input_file); 1070 string disabledPlatforms; 1071 if (findTestParameter(envData, file, "DISABLED", disabledPlatforms)) 1072 { 1073 const reason = getDisabledReason(split(disabledPlatforms), envData); 1074 if (reason.length != 0) 1075 { 1076 writefln(" ... %-30s [DISABLED %s]", input_file, reason); 1077 return 0; 1078 } 1079 } 1080 1081 return runBashTest(input_dir, test_name, envData); 1082 } 1083 1084 // envData.sep is required as the results_dir path can be `generated` 1085 const absoluteResultDirPath = envData.results_dir.absolutePath ~ envData.sep; 1086 const resultsDirReplacement = "{{RESULTS_DIR}}" ~ envData.sep; 1087 const test_app_dmd_base = output_dir ~ envData.sep ~ test_name ~ "_"; 1088 1089 auto stopWatch = StopWatch(AutoStart.no); 1090 if (envData.printRuntime) 1091 stopWatch.start(); 1092 1093 if (!gatherTestParameters(testArgs, input_dir, input_file, envData)) 1094 return 0; 1095 1096 // Clear the DFLAGS environment variable if it was specified in the test file 1097 if (testArgs.clearDflags) 1098 { 1099 // `environment["DFLAGS"] = "";` doesn't seem to work on Win32 (might be a bug 1100 // in std.process). So, resorting to `putenv` in snn.lib 1101 version(Win32) 1102 { 1103 putenv("DFLAGS="); 1104 } 1105 else 1106 { 1107 environment["DFLAGS"] = ""; 1108 } 1109 } 1110 1111 writef(" ... %-30s %s%s(%s)", 1112 input_file, 1113 testArgs.requiredArgs, 1114 (!testArgs.requiredArgs.empty ? " " : ""), 1115 testArgs.permuteArgs); 1116 1117 if (testArgs.isDisabled) 1118 { 1119 writef("!!! [DISABLED %s]", testArgs.disabledReason); 1120 if (!envData.tryDisabled) 1121 { 1122 writeln(); 1123 return 0; 1124 } 1125 } 1126 1127 removeIfExists(output_file); 1128 1129 auto f = File(output_file, "a"); 1130 1131 if ( 1132 //prepare cpp extra sources 1133 !collectExtraSources(input_dir, output_dir, testArgs.cppSources, testArgs.sources, envData, envData.ccompiler, testArgs.cxxflags, f) || 1134 1135 //prepare objc extra sources 1136 !collectExtraSources(input_dir, output_dir, testArgs.objcSources, testArgs.sources, envData, "clang", null, f) 1137 ) 1138 { 1139 writeln(); 1140 1141 // Ignore failed test 1142 if (testArgs.isDisabled) 1143 return 0; 1144 1145 f.close(); 1146 printTestFailure(input_file, output_file); 1147 return 1; 1148 } 1149 1150 enum Result { continue_, return0, return1 } 1151 1152 // Runs the test with a specific combination of arguments 1153 Result testCombination(bool autoCompileImports, string argSet, size_t permuteIndex, string permutedArgs) 1154 { 1155 string test_app_dmd = test_app_dmd_base ~ to!string(permuteIndex) ~ envData.exe; 1156 string command; // copy of the last executed command so that it can be re-invoked on failures 1157 try 1158 { 1159 string[] toCleanup; 1160 1161 auto thisRunName = genTempFilename(result_path); 1162 auto fThisRun = File(thisRunName, "w"); 1163 scope(exit) 1164 { 1165 fThisRun.close(); 1166 f.write(readText(thisRunName)); 1167 f.writeln(); 1168 removeIfExists(thisRunName); 1169 } 1170 1171 string compile_output; 1172 if (!testArgs.compileSeparately) 1173 { 1174 string objfile = output_dir ~ envData.sep ~ test_name ~ "_" ~ to!string(permuteIndex) ~ envData.obj; 1175 toCleanup ~= objfile; 1176 1177 command = format("%s -conf= -m%s -I%s %s %s -od%s -of%s %s %s%s %s", envData.dmd, envData.model, input_dir, 1178 testArgs.requiredArgs, permutedArgs, output_dir, 1179 (testArgs.mode == TestMode.RUN || testArgs.link ? test_app_dmd : objfile), 1180 argSet, 1181 (testArgs.mode == TestMode.RUN || testArgs.link ? "" : "-c "), 1182 join(testArgs.sources, " "), 1183 (autoCompileImports ? "-i" : join(testArgs.compiledImports, " "))); 1184 1185 try 1186 compile_output = execute(fThisRun, command, testArgs.mode != TestMode.FAIL_COMPILE, result_path); 1187 catch (Exception e) 1188 { 1189 writeln(""); // We're at "... runnable/xxxx.d (args)" 1190 printCppSources(testArgs.sources); 1191 throw e; 1192 } 1193 } 1194 else 1195 { 1196 foreach (filename; testArgs.sources ~ (autoCompileImports ? null : testArgs.compiledImports)) 1197 { 1198 string newo= result_path ~ replace(replace(filename, ".d", envData.obj), envData.sep~"imports"~envData.sep, envData.sep); 1199 toCleanup ~= newo; 1200 1201 command = format("%s -conf= -m%s -I%s %s %s -od%s -c %s %s", envData.dmd, envData.model, input_dir, 1202 testArgs.requiredArgs, permutedArgs, output_dir, argSet, filename); 1203 compile_output ~= execute(fThisRun, command, testArgs.mode != TestMode.FAIL_COMPILE, result_path); 1204 } 1205 1206 if (testArgs.mode == TestMode.RUN || testArgs.link) 1207 { 1208 // link .o's into an executable 1209 command = format("%s -conf= -m%s%s%s %s %s -od%s -of%s %s", envData.dmd, envData.model, 1210 autoCompileImports ? " -i" : "", 1211 autoCompileImports ? "extraSourceIncludePaths" : "", 1212 envData.required_args, testArgs.requiredArgsForLink, output_dir, test_app_dmd, join(toCleanup, " ")); 1213 1214 execute(fThisRun, command, true, result_path); 1215 } 1216 } 1217 1218 void prepare(ref string compile_output) 1219 { 1220 if (compile_output.empty) 1221 return; 1222 1223 compile_output = compile_output.unifyNewLine(); 1224 compile_output = std.regex.replaceAll(compile_output, regex(`^DMD v2\.[0-9]+.*\n? DEBUG$`, "m"), ""); 1225 compile_output = std..string.strip(compile_output); 1226 // replace test_result path with fixed ones 1227 compile_output = compile_output.replace(result_path, resultsDirReplacement); 1228 compile_output = compile_output.replace(absoluteResultDirPath, resultsDirReplacement); 1229 1230 compile_output.applyOutputTransformations(testArgs.transformOutput); 1231 } 1232 1233 prepare(compile_output); 1234 1235 auto m = std.regex.match(compile_output, `Internal error: .*$`); 1236 enforce(!m, m.hit); 1237 m = std.regex.match(compile_output, `core.exception.AssertError@dmd.*`); 1238 enforce(!m, m.hit); 1239 1240 // Prepare and append the content of each OUTPUT_FILE conforming to 1241 // the HAR (https://code.dlang.org/packages/har) format, e.g. 1242 // === <FILENAME_1> 1243 // <CONTENT_1> 1244 // === <FILENAME_2> 1245 // <CONTENT_2> 1246 // ... 1247 foreach (const outfile; testArgs.outputFiles) 1248 { 1249 string path = outfile; 1250 replaceResultsDir(path, envData); 1251 1252 // Don't abort if a file is missing, at least verify the remaining output. 1253 string content = readText(path).ifThrown("<< File missing >>"); 1254 prepare(content); 1255 1256 // Make sure file starts on a new line 1257 if (!compile_output.empty && !compile_output.endsWith("\n")) 1258 compile_output ~= '\n'; 1259 1260 // Prepend a header listing the explicit file 1261 compile_output.reserve(outfile.length + content.length + 5); 1262 1263 compile_output ~= "=== "; 1264 compile_output ~= outfile; 1265 compile_output ~= '\n'; 1266 compile_output ~= content; 1267 } 1268 1269 if (!compareOutput(compile_output, testArgs.compileOutput, envData)) 1270 { 1271 const diff = generateDiff(testArgs.compileOutput, testArgs.compileOutputFile, 1272 compile_output, test_base_name); 1273 throw new CompareException(testArgs.compileOutput, compile_output, diff); 1274 } 1275 1276 if (testArgs.mode == TestMode.RUN) 1277 { 1278 toCleanup ~= test_app_dmd; 1279 version(Windows) 1280 if (envData.usingMicrosoftCompiler) 1281 { 1282 toCleanup ~= test_app_dmd_base ~ to!string(permuteIndex) ~ ".ilk"; 1283 toCleanup ~= test_app_dmd_base ~ to!string(permuteIndex) ~ ".pdb"; 1284 } 1285 1286 if (testArgs.gdbScript is null) 1287 { 1288 command = test_app_dmd; 1289 if (testArgs.executeArgs) command ~= " " ~ testArgs.executeArgs; 1290 1291 const output = execute(fThisRun, command, true, result_path) 1292 .strip() 1293 .unifyNewLine(); 1294 1295 if (testArgs.runOutput && !compareOutput(output, testArgs.runOutput, envData)) 1296 { 1297 const diff = generateDiff(testArgs.runOutput, null, output, test_base_name); 1298 throw new CompareException(testArgs.runOutput, output, diff, true); 1299 } 1300 } 1301 else version (linux) 1302 { 1303 // Tests failed on SemaphoreCI when multiple GDB tests were run at once 1304 scope lockfile = File(envData.results_dir.buildPath("gdb.lock"), "w"); 1305 lockfile.lock(); 1306 scope (exit) lockfile.unlock(); 1307 1308 auto script = test_app_dmd_base ~ to!string(permuteIndex) ~ ".gdb"; 1309 toCleanup ~= script; 1310 with (File(script, "w")) 1311 { 1312 writeln("set disable-randomization off"); 1313 write(testArgs.gdbScript); 1314 } 1315 string gdbCommand = "gdb "~test_app_dmd~" --batch -x "~script; 1316 auto gdb_output = execute(fThisRun, gdbCommand, true, result_path); 1317 if (testArgs.gdbMatch !is null) 1318 { 1319 enforce(match(gdb_output, regex(testArgs.gdbMatch)), 1320 "\nGDB regex: '"~testArgs.gdbMatch~"' didn't match output:\n----\n"~gdb_output~"\n----\n"); 1321 } 1322 } 1323 } 1324 1325 fThisRun.close(); 1326 1327 if (testArgs.postScript && !envData.coverage_build) 1328 { 1329 f.write("Executing post-test script: "); 1330 string prefix = ""; 1331 version (Windows) prefix = "bash "; 1332 execute(f, prefix ~ "tools/postscript.sh " ~ testArgs.postScript ~ " " ~ input_dir ~ " " ~ test_name ~ " " ~ thisRunName, true, result_path); 1333 } 1334 1335 foreach (file; toCleanup) collectException(std.file.remove(file)); 1336 return Result.continue_; 1337 } 1338 catch(Exception e) 1339 { 1340 // it failed but it was disabled, exit as if it was successful 1341 if (testArgs.isDisabled) 1342 { 1343 writeln(); 1344 return Result.return0; 1345 } 1346 1347 if (envData.autoUpdate) 1348 if (auto ce = cast(CompareException) e) 1349 { 1350 // remove the output file in test_results as its outdated 1351 // (might fail for runnable tests on Windows) 1352 if (output_file.remove().collectException()) 1353 writef("\nWARNING: Failed to remove `%s`!", output_file); 1354 1355 if (testArgs.compileOutputFile && !ce.fromRun) 1356 { 1357 std.file.write(testArgs.compileOutputFile, ce.actual); 1358 writefln("\n==> `TEST_OUTPUT_FILE` `%s` has been updated", testArgs.compileOutputFile); 1359 return Result.return0; 1360 } 1361 1362 auto existingText = input_file.readText; 1363 auto updatedText = existingText.replace(ce.expected, ce.actual); 1364 if (existingText != updatedText) 1365 { 1366 std.file.write(input_file, updatedText); 1367 writefln("\n==> `TEST_OUTPUT` of %s has been updated", input_file); 1368 } 1369 else 1370 { 1371 writefln("\nWARNING: %s has multiple `TEST_OUTPUT` blocks and can't be auto-updated", input_file); 1372 } 1373 return Result.return0; 1374 } 1375 f.writeln(); 1376 f.writeln("=============================="); 1377 f.writef("Test %s failed: ", input_file); 1378 f.writeln(e.msg); 1379 f.close(); 1380 1381 writefln("\nTest %s failed. The logged output:", input_file); 1382 auto outputText = output_file.readText; 1383 writeln(outputText); 1384 output_file.remove(); 1385 1386 // auto-update if a diff is found and can be updated 1387 if (envData.autoUpdate && 1388 outputText.canFind("diff ") && outputText.canFind("--- ") && outputText.canFind("+++ ")) 1389 { 1390 import std.range : dropOne; 1391 auto newFile = outputText.findSplitAfter("+++ ")[1].until("\t"); 1392 auto baseFile = outputText.findSplitAfter("--- ")[1].until("\t"); 1393 writefln("===> Updating %s with %s", baseFile, newFile); 1394 newFile.copy(baseFile); 1395 return Result.return0; 1396 } 1397 1398 // automatically rerun a segfaulting test and print its stack trace 1399 version(linux) 1400 if (e.msg.canFind("exited with rc == 139")) 1401 { 1402 auto gdbCommand = "gdb -q -n -ex 'set backtrace limit 100' -ex run -ex bt -batch -args " ~ command; 1403 spawnShell(gdbCommand).wait; 1404 } 1405 1406 return Result.return1; 1407 } 1408 } 1409 1410 size_t index = 0; // index over all tests to avoid identical output names in consecutive tests 1411 auto argSets = (testArgs.argSets.length == 0) ? [""] : testArgs.argSets; 1412 for(auto autoCompileImports = false;; autoCompileImports = true) 1413 { 1414 foreach(argSet; argSets) 1415 { 1416 foreach (c; combinations(testArgs.permuteArgs)) 1417 { 1418 final switch(testCombination(autoCompileImports, argSet, index, c)) 1419 { 1420 case Result.continue_: break; 1421 case Result.return0: return 0; 1422 case Result.return1: return 1; 1423 } 1424 index++; 1425 } 1426 } 1427 if(autoCompileImports || testArgs.compiledImports.length == 0) 1428 break; 1429 } 1430 1431 if (envData.printRuntime) 1432 { 1433 const long ms = stopWatch.peek.total!"msecs"; 1434 writefln(" [%.3f secs]", ms / 1000.0); 1435 } 1436 else 1437 writeln(); 1438 1439 // it was disabled but it passed! print an informational message 1440 if (testArgs.isDisabled) 1441 writefln(" !!! %-30s DISABLED but PASSES!", input_file); 1442 1443 return 0; 1444 } 1445 1446 int runBashTest(string input_dir, string test_name, const ref EnvData envData) 1447 { 1448 enum script = "tools/sh_do_test.sh"; 1449 1450 version(Windows) 1451 { 1452 const cmd = "bash " ~ script ~ ' ' ~ input_dir ~ ' ' ~ test_name; 1453 const env = [ 1454 // Make sure the path is bash-friendly 1455 "DMD": envData.dmd.relativePath(dmdTestDir).replace('\\', '/') 1456 ]; 1457 1458 auto process = spawnShell(cmd, env, Config.none, dmdTestDir); 1459 } 1460 else 1461 { 1462 const scriptPath = dmdTestDir.buildPath(script); 1463 auto process = spawnProcess([scriptPath, input_dir, test_name]); 1464 } 1465 return process.wait(); 1466 } 1467 1468 /// Run a dshell test 1469 int runDShellTest(string input_dir, string test_name, const ref EnvData envData, 1470 string output_dir, string output_file) 1471 { 1472 const testScriptDir = buildPath(dmdTestDir, input_dir); 1473 const testScriptPath = buildPath(testScriptDir, test_name ~ ".d"); 1474 const testOutDir = buildPath(output_dir, test_name); 1475 const testLogName = format("%s/%s.d", input_dir, test_name); 1476 1477 writefln(" ... %s", testLogName); 1478 1479 removeIfExists(output_file); 1480 if (exists(testOutDir)) 1481 rmdirRecurse(testOutDir); 1482 mkdirRecurse(testOutDir); 1483 1484 // create the "dshell" module for the tests 1485 { 1486 auto dshellFile = File(buildPath(testOutDir, "dshell.d"), "w"); 1487 dshellFile.writeln(`module dshell; 1488 public import dshell_prebuilt; 1489 static this() 1490 { 1491 dshellPrebuiltInit("` ~ input_dir ~ `", "`, test_name , `"); 1492 } 1493 `); 1494 } 1495 1496 const testScriptExe = buildPath(testOutDir, "run" ~ envData.exe); 1497 const output_file_temp = output_file ~ ".tmp"; 1498 1499 // 1500 // compile the test 1501 // 1502 { 1503 auto outfile = File(output_file_temp, "w"); 1504 const compile = [envData.dmd, "-conf=", "-m"~envData.model] ~ 1505 envData.picFlag ~ [ 1506 "-od" ~ testOutDir, 1507 "-of" ~ testScriptExe, 1508 "-I=" ~ testScriptDir, 1509 "-I=" ~ testOutDir, 1510 "-I=" ~ buildPath(dmdTestDir, "tools", "dshell_prebuilt"), 1511 "-i", 1512 // Causing linker errors for some reason? 1513 "-i=-dshell_prebuilt", buildPath(envData.results_dir, "dshell_prebuilt" ~ envData.obj), 1514 testScriptPath, 1515 ]; 1516 outfile.writeln("[COMPILE_TEST] ", escapeShellCommand(compile)); 1517 // Note that spawnprocess closes the file, so it will need to be re-opened 1518 // below when we run the test 1519 auto compileProc = std.process.spawnProcess(compile, stdin, outfile, outfile); 1520 const exitCode = wait(compileProc); 1521 if (exitCode != 0) 1522 { 1523 printTestFailure(testLogName, output_file_temp); 1524 return exitCode; 1525 } 1526 } 1527 1528 // 1529 // run the test 1530 // 1531 { 1532 auto outfile = File(output_file_temp, "a"); 1533 const runTest = [testScriptExe]; 1534 outfile.writeln("[RUN_TEST] ", escapeShellCommand(runTest)); 1535 auto runTestProc = std.process.spawnProcess(runTest, stdin, outfile, outfile); 1536 const exitCode = wait(runTestProc); 1537 1538 if (exitCode == 125) // = DISABLED from tools/dshell_prebuilt.d 1539 { 1540 writefln(" !!! %s is disabled!", testLogName); 1541 return 0; 1542 } 1543 else if (exitCode != 0) 1544 { 1545 printTestFailure(testLogName, output_file_temp); 1546 return exitCode; 1547 } 1548 } 1549 1550 rename(output_file_temp, output_file); 1551 // TODO: should we remove all the test artifacts if the test passes? rmdirRecurse(testOutDir)? 1552 return 0; 1553 } 1554 1555 void printTestFailure(string testLogName, string output_file_temp) 1556 { 1557 writeln("=============================="); 1558 writefln("Test '%s' failed. The logged output:", testLogName); 1559 const output = readText(output_file_temp); 1560 write(output); 1561 if (!output.endsWith("\n")) 1562 writeln(); 1563 writeln("=============================="); 1564 remove(output_file_temp); 1565 } 1566 1567 /** 1568 * Print symbols in C++ objects 1569 * 1570 * If linking failed, we print the symbols present in the C++ object file being 1571 * linked it. This is so that C++ `runnable` tests are easier to debug, 1572 * as the CI machines can have different environment than the users, 1573 * and it is generally painful to work with them when trying to support 1574 * newer (C++11, C++14, C++17, etc...) features. 1575 */ 1576 void printCppSources (in const(char)[][] compiled) 1577 { 1578 version (Posix) 1579 { 1580 foreach (file; compiled) 1581 { 1582 if (!file.endsWith(".cpp.o")) 1583 continue; 1584 writeln("========== Symbols for C++ object file: ", file, " =========="); 1585 std.process.spawnProcess(["nm", file]).wait(); 1586 } 1587 } 1588 }