yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
05cae938c
master
1// slang-test-main.cpp 2 3#include "../../source/compiler-core/slang-artifact-desc-util.h" 4#include "../../source/compiler-core/slang-artifact-helper.h" 5#include "../../source/core/slang-byte-encode-util.h" 6#include "../../source/core/slang-castable.h" 7#include "../../source/core/slang-char-util.h" 8#include "../../source/core/slang-hex-dump-util.h" 9#include "../../source/core/slang-io.h" 10#include "../../source/core/slang-memory-arena.h" 11#include "../../source/core/slang-process-util.h" 12#include "../../source/core/slang-render-api-util.h" 13#include "../../source/core/slang-shared-library.h" 14#include "../../source/core/slang-std-writers.h" 15#include "../../source/core/slang-string-escape-util.h" 16#include "../../source/core/slang-string-util.h" 17#include "../../source/core/slang-token-reader.h" 18#include "../../source/core/slang-type-text-util.h" 19#include "slang-com-helper.h" 20#include "unit-test/slang-unit-test.h" 21#undef SLANG_UNIT_TEST 22 23#include "../../source/compiler-core/slang-artifact-associated-impl.h" 24#include "../../source/compiler-core/slang-downstream-compiler.h" 25#include "../../source/compiler-core/slang-language-server-protocol.h" 26#include "../../source/compiler-core/slang-nvrtc-compiler.h" 27#include "../render-test/slang-support.h" 28#include "directory-util.h" 29#include "options.h" 30#include "parse-diagnostic-util.h" 31#include "slangc-tool.h" 32#include "slangi-tool.h" 33#include "test-context.h" 34#include "test-reporter.h" 35 36#define STB_IMAGE_IMPLEMENTATION 37#include "stb_image.h" 38 39#include <math.h> 40#include <random> 41#include <stdarg.h> 42#include <stdio.h> 43#include <stdlib.h> 44 45#define SLANG_PRELUDE_NAMESPACE CPPPrelude 46#include "../../prelude/slang-cpp-types.h" 47 48#include <atomic> 49#include <thread> 50 51#if defined(_WIN32 ) 52#include <slang-rhi/agility-sdk.h> 53SLANG_RHI_EXPORT_AGILITY_SDK 54#endif 55 56using namespace Slang ; 57 58// Constants for slang-test specific options 59static const char * kPreserveEmbeddedSourceOption = "-preserve-embedded-source" ; 60 61// Options for a particular test 62struct TestOptions 63{ 64enum Type 65 { 66Normal ,///< A regular test 67Diagnostic ,///< Diagnostic tests will always run (as form of failure is being tested) 68 }; 69 70void addCategory (TestCategory * category ) 71 { 72if (categories .indexOf (category )< 0 ) 73 { 74categories .add (category ); 75 } 76 } 77void addCategories (TestCategory * const * inCategories ,Index count ) 78 { 79for (Index i = 0 ;i < count ;++ i ) 80 { 81addCategory (inCategories [i ]); 82 } 83 } 84 85// Small helper to help consistently interrogating for filecheck usage 86bool getFileCheckPrefix (String & prefix )const 87 { 88return commandOptions .tryGetValue ("filecheck" ,prefix ); 89 } 90bool getFileCheckBufferPrefix (String & prefix )const 91 { 92return commandOptions .tryGetValue ("filecheck-buffer" ,prefix ); 93 } 94 95Type type = Type ::Normal ; 96 97String command ; 98List < String > args ; 99 100Dictionary < String ,String > commandOptions ; 101 102// The categories that this test was assigned to 103List < TestCategory *> categories ; 104 105bool isEnabled = true; 106bool isSynthesized = false; 107}; 108 109struct FileTestInfoImpl :public FileTestInfo 110{ 111String testName ; 112String filePath ; 113String outputStem ; 114TestOptions options ; 115}; 116 117struct TestDetails 118{ 119TestDetails () {} 120explicit TestDetails (const TestOptions & inOptions ) 121 :options (inOptions ) 122 { 123 } 124 125TestOptions options ;///< The options for the test 126TestRequirements requirements ;///< The requirements for the test to work 127}; 128 129// Information on tests to run for a particular file 130struct FileTestList 131{ 132List < TestDetails > tests ; 133}; 134 135 136struct TestInput 137{ 138// Path to the input file for the test 139String filePath ; 140 141// Prefix for the path that test output should write to 142// (usually the same as `filePath`, but will differ when 143// we run multiple tests out of the same file) 144String outputStem ; 145 146// Arguments for the test (usually to be interpreted 147// as command line args) 148TestOptions const * testOptions ; 149 150// Determines how the test will be spawned 151SpawnType spawnType ; 152}; 153 154typedef TestResult (* TestCallback )(TestContext * context ,TestInput & input ); 155 156// Globals 157 158// Pre declare 159static void _addRenderTestOptions (const Options & options ,CommandLine & ioCmdLine ); 160 161/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ 162 163// Tries to read in order 164// - The file specific to this test: input.outputStem + suffix 165// - The general file for a set of tests: input.fileName + suffix; 166static SlangResult _readTestFile (const TestInput & input ,const String & suffix ,String & out ) 167{ 168StringBuilder buf ; 169buf <<input .outputStem <<suffix ; 170if (auto r = Slang ::File ::readAllText (buf ,out );SLANG_SUCCEEDED (r )) 171 { 172return r ; 173 } 174 175buf .clear (); 176buf <<input .filePath <<suffix ; 177return Slang ::File ::readAllText (buf ,out ); 178} 179 180 181bool match (char const ** ioCursor ,char const * expected ) 182{ 183char const * cursor = * ioCursor ; 184while (* expected && * cursor == * expected ) 185 { 186cursor ++ ; 187expected ++ ; 188 } 189if (* expected != 0 ) 190return false; 191 192* ioCursor = cursor ; 193return true; 194} 195 196void skipHorizontalSpace (char const ** ioCursor ) 197{ 198char const * cursor = * ioCursor ; 199for (;;) 200 { 201switch (* cursor ) 202 { 203case ' ' : 204case '\t' : 205cursor ++ ; 206continue ; 207 208default : 209break ; 210 } 211 212break ; 213 } 214* ioCursor = cursor ; 215} 216 217void skipToEndOfLine (char const ** ioCursor ) 218{ 219char const * cursor = * ioCursor ; 220for (;;) 221 { 222int c = * cursor ; 223switch (c ) 224 { 225default : 226cursor ++ ; 227continue ; 228 229case '\r' : 230case '\n' : 231 { 232cursor ++ ; 233int d = * cursor ; 234if ((c ^d )== ('\r' ^'\n' )) 235 { 236cursor ++ ; 237 } 238 } 239 [[fallthrough ]]; 240case 0 : 241* ioCursor = cursor ; 242return ; 243 } 244 } 245} 246 247String getString (char const * textBegin ,char const * textEnd ) 248{ 249StringBuilder sb ; 250sb .append (textBegin ,textEnd - textBegin ); 251return sb .produceString (); 252} 253 254String collectRestOfLine (char const ** ioCursor ) 255{ 256char const * cursor = * ioCursor ; 257 258char const * textBegin = cursor ; 259skipToEndOfLine (& cursor ); 260char const * textEnd = cursor ; 261 262* ioCursor = cursor ; 263return getString (textBegin ,textEnd ); 264} 265 266static bool _isEndOfLineOrParens (char c ) 267{ 268switch (c ) 269 { 270case '\n' : 271case '\r' : 272case 0 : 273case ')' : 274 { 275return true; 276 } 277default : 278return false; 279 } 280} 281 282static SlangResult _parseCategories ( 283TestCategorySet * categorySet , 284char const ** ioCursor , 285TestOptions & out ) 286{ 287char const * cursor = * ioCursor ; 288 289// If don't have ( we don't have category list 290if (* cursor == '(' ) 291 { 292cursor ++ ; 293const char * const start = cursor ; 294 295// Find the end 296for (; !_isEndOfLineOrParens (* cursor );++ cursor ) 297 ; 298if (* cursor != ')' ) 299 { 300* ioCursor = cursor ; 301return SLANG_FAIL ; 302 } 303cursor ++ ; 304 305List < UnownedStringSlice > slices ; 306StringUtil ::split (UnownedStringSlice (start ,cursor - 1 ),',' ,slices ); 307 308for (auto & slice :slices ) 309 { 310// Trim any whitespace 311auto categoryName = slice .trim (); 312 313TestCategory * category = categorySet -> find (categoryName ); 314 315if (!category ) 316 { 317// Mark this test as disabled, as we don't have all of the categories 318out .isEnabled = false; 319break ; 320 } 321 322out .addCategory (category ); 323 } 324 } 325 326* ioCursor = cursor ; 327return SLANG_OK ; 328} 329 330static SlangResult _parseCommandArguments (char const ** ioCursor ,TestOptions & out ) 331{ 332char const * cursor = * ioCursor ; 333 334// If don't have ( we don't have any additional options 335if (* cursor == '(' ) 336 { 337cursor ++ ; 338const char * const start = cursor ; 339 340// Find the end 341for (; !_isEndOfLineOrParens (* cursor );++ cursor ) 342 ; 343if (* cursor != ')' ) 344 { 345* ioCursor = cursor ; 346return SLANG_FAIL ; 347 } 348cursor ++ ; 349 350List < UnownedStringSlice > options ; 351StringUtil ::split (UnownedStringSlice (start ,cursor - 1 ),',' ,options ); 352 353for (auto & option :options ) 354 { 355auto i = option .indexOf ('=' ); 356if (i == -1 ) 357 { 358out .commandOptions .add (option .trim (),"" ); 359 } 360else 361 { 362out .commandOptions .add (option .head (i ).trim (),option .tail (i + 1 ).trim ()); 363 } 364 } 365 } 366 367* ioCursor = cursor ; 368return SLANG_OK ; 369} 370 371static SlangResult _parseArg (const char ** ioCursor ,UnownedStringSlice & outArg ) 372{ 373const char * cursor = * ioCursor ; 374const char * const argBegin = cursor ; 375 376// Let's try to read one option 377for (;;) 378 { 379switch (* cursor ) 380 { 381default : 382 { 383++ cursor ; 384break ; 385 } 386case '"' : 387 { 388// If we have quotes let's just parse them as is and make output 389auto escapeHandler = StringEscapeUtil ::getHandler (StringEscapeUtil ::Style ::Space ); 390SLANG_RETURN_ON_FAIL (escapeHandler -> lexQuoted (cursor ,& cursor )); 391break ; 392 } 393case 0 : 394case '\r' : 395case '\n' : 396case ' ' : 397case '\t' : 398 { 399char const * argEnd = cursor ; 400assert (argBegin != argEnd ); 401 402outArg = UnownedStringSlice (argBegin ,argEnd ); 403* ioCursor = cursor ; 404return SLANG_OK ; 405 } 406 } 407 } 408} 409 410static SlangResult _gatherTestOptions ( 411TestCategorySet * categorySet , 412char const ** ioCursor , 413TestOptions & outOptions ) 414{ 415SLANG_RETURN_ON_FAIL (_parseCategories (categorySet ,ioCursor ,outOptions )); 416 417char const * cursor = * ioCursor ; 418 419if (* cursor != ':' ) 420 { 421return SLANG_FAIL ; 422 } 423cursor ++ ; 424 425// Next scan for a sub-command name 426char const * commandStart = cursor ; 427for (;;) 428 { 429switch (* cursor ) 430 { 431default : 432cursor ++ ; 433continue ; 434 435case '(' : 436case ':' : 437break ; 438 439case 0 : 440case '\r' : 441case '\n' : 442return SLANG_FAIL ; 443 } 444 445break ; 446 } 447char const * commandEnd = cursor ; 448 449outOptions .command = getString (commandStart ,commandEnd ); 450 451// Allow parameterizing the test command separately from the arguments, this 452// is because the arguments are often passed to the compiler verbatim, and 453// it's messy to have the test runner rifling through and picking things 454// out 455// Format is: (foo=bar, baz = 2) 456SLANG_RETURN_ON_FAIL (_parseCommandArguments (& cursor ,outOptions )); 457 458if (* cursor == ':' ) 459cursor ++ ; 460else 461 { 462return SLANG_FAIL ; 463 } 464 465// Now scan for arguments. For now we just assume that 466// any whitespace separation indicates a new argument 467for (;;) 468 { 469skipHorizontalSpace (& cursor ); 470 471// End of line? then no more options. 472switch (* cursor ) 473 { 474case 0 : 475case '\r' : 476case '\n' : 477skipToEndOfLine (& cursor ); 478 479* ioCursor = cursor ; 480return SLANG_OK ; 481 482default : 483break ; 484 } 485 486// Let's try to read one option 487UnownedStringSlice arg ; 488SLANG_RETURN_ON_FAIL (_parseArg (& cursor ,arg )); 489 490outOptions .args .add (arg ); 491 } 492} 493 494 495static RenderApiFlags _getRequiredRenderApisByCommand (const UnownedStringSlice & name ); 496 497static void _combineOptions ( 498TestCategorySet * categorySet , 499const TestOptions & fileOptions , 500TestOptions & ioOptions ) 501{ 502// And the file categories 503ioOptions .addCategories (fileOptions .categories .getBuffer (),fileOptions .categories .getCount ()); 504 505// If no categories were specified, then add the default category 506if (ioOptions .categories .getCount ()== 0 ) 507 { 508ioOptions .categories .add (categorySet -> defaultCategory ); 509 } 510} 511 512static SlangResult _extractCommand (const char ** ioCursor ,UnownedStringSlice & outCommand ) 513{ 514const char * cursor = * ioCursor ; 515const char * const start = cursor ; 516 517while (true) 518 { 519const char c = * cursor ; 520 521if (CharUtil ::isAlpha (c )|| c == '_' ) 522 { 523cursor ++ ; 524continue ; 525 } 526 527if (c == ':' || c == '(' || c == 0 || c == '\n' || c == '\r' ) 528 { 529* ioCursor = cursor ; 530outCommand = UnownedStringSlice (start ,cursor ); 531return SLANG_OK ; 532 } 533 534return SLANG_FAIL ; 535 } 536} 537 538static void applyMacroSubstitution (String filePath ,TestDetails & details ) 539{ 540for (auto & arg :details .options .args ) 541 { 542arg = StringUtil ::replaceAll ( 543arg .getUnownedSlice (), 544toSlice ("$dirname" ), 545Path ::getParentDirectory (filePath ).getUnownedSlice ()); 546 } 547} 548 549// Try to read command-line options from the test file itself 550static SlangResult _gatherTestsForFile ( 551TestCategorySet * categorySet , 552String filePath , 553FileTestList * outTestList , 554TestContext * context = nullptr ) 555{ 556outTestList -> tests .clear (); 557 558String fileContents ; 559 560TestReporter * testReporter = nullptr ; 561if (context ) 562testReporter = context -> getTestReporter (); 563 564// Try reading the file with retries on failure to handle intermittent I/O errors 565// (commonly seen on macOS in CI environments) 566SlangResult readResult = SLANG_FAIL ; 567for (int retryCount = 0 ;retryCount < 3 && SLANG_FAILED (readResult );++ retryCount ) 568 { 569if (retryCount ) 570 { 571if (testReporter ) 572 { 573testReporter -> messageFormat ( 574TestMessageType ::Info , 575"Retrying to read test file '%s' (attempt %d)" , 576filePath .getBuffer (), 577retryCount + 1 ); 578 } 579else 580 { 581fprintf ( 582stderr , 583"Retrying to read test file '%s' (attempt %d)\n" , 584filePath .getBuffer (), 585retryCount + 1 ); 586 } 587 std::this_thread::sleep_for (std::chrono::milliseconds (retryCount * 100 )); 588 } 589readResult = Slang ::File ::readAllText (filePath ,fileContents ); 590 } 591if (SLANG_FAILED (readResult )) 592 { 593// Log file reading failure with details (thread-safe) 594if (testReporter ) 595 { 596testReporter -> messageFormat ( 597TestMessageType ::RunError , 598"Failed to read test file '%s' (error: 0x%08X)" , 599filePath .getBuffer (), 600 (unsigned int )readResult ); 601 } 602else 603 { 604// Fallback to stderr if no context available 605fprintf ( 606stderr , 607"Failed to read test file '%s' (error: 0x%08X)\n" , 608filePath .getBuffer (), 609 (unsigned int )readResult ); 610 } 611return readResult ; 612 } 613 614// Walk through the lines of the file, looking for test commands 615char const * cursor = fileContents .begin (); 616 617// Options that are specified across all tests in the file. 618TestOptions fileOptions ; 619 620while (* cursor ) 621 { 622// We are at the start of a line of input. 623 624skipHorizontalSpace (& cursor ); 625 626if (!match (& cursor ,"//" )) 627 { 628skipToEndOfLine (& cursor ); 629continue ; 630 } 631 632// Skip any extra slashes and spaces to handle malformed directives like ///TEST or // TEST 633while (* cursor == '/' ) 634 { 635cursor ++ ; 636 } 637skipHorizontalSpace (& cursor ); 638 639UnownedStringSlice command ; 640 641if (SLANG_FAILED (_extractCommand (& cursor ,command ))) 642 { 643// Couldn't find a command so skip 644skipToEndOfLine (& cursor ); 645continue ; 646 } 647 648// Look for a pattern that matches what we want 649if (command == "TEST_IGNORE_FILE" ) 650 { 651outTestList -> tests .clear (); 652return SLANG_OK ; 653 } 654 655const UnownedStringSlice disablePrefix = UnownedStringSlice ::fromLiteral ("DISABLE_" ); 656 657TestDetails testDetails ; 658 659 { 660if (command .startsWith (disablePrefix )) 661 { 662testDetails .options .isEnabled = false; 663command = command .tail (disablePrefix .getLength ()); 664 } 665 } 666 667if (command == "TEST_CATEGORY" ) 668 { 669SlangResult res = _parseCategories (categorySet ,& cursor ,fileOptions ); 670 671// If it failed we are done, unless it was just 'not available' 672if (SLANG_FAILED (res )&& res != SLANG_E_NOT_AVAILABLE ) 673 { 674if (context && context -> getTestReporter ()) 675 { 676context -> getTestReporter ()-> messageFormat ( 677TestMessageType ::RunError , 678"Failed to parse TEST_CATEGORY in file '%s' (error: 0x%08X)" , 679filePath .getBuffer (), 680 (unsigned int )res ); 681 } 682else 683 { 684fprintf ( 685stderr , 686"Failed to parse TEST_CATEGORY in file '%s' (error: 0x%08X)\n" , 687filePath .getBuffer (), 688 (unsigned int )res ); 689 } 690return res ; 691 } 692 693skipToEndOfLine (& cursor ); 694continue ; 695 } 696 697if (command == "TEST" ) 698 { 699SlangResult testRes = _gatherTestOptions (categorySet ,& cursor ,testDetails .options ); 700if (SLANG_FAILED (testRes )) 701 { 702if (context && context -> getTestReporter ()) 703 { 704context -> getTestReporter ()-> messageFormat ( 705TestMessageType ::RunError , 706"Failed to parse TEST directive in file '%s' (error: 0x%08X)" , 707filePath .getBuffer (), 708 (unsigned int )testRes ); 709 } 710else 711 { 712fprintf ( 713stderr , 714"Failed to parse TEST directive in file '%s' (error: 0x%08X)\n" , 715filePath .getBuffer (), 716 (unsigned int )testRes ); 717 } 718return testRes ; 719 } 720applyMacroSubstitution (filePath ,testDetails ); 721 722// See if the type of test needs certain APIs available 723const RenderApiFlags testRequiredApis = 724_getRequiredRenderApisByCommand (testDetails .options .command .getUnownedSlice ()); 725testDetails .requirements .addUsedRenderApis (testRequiredApis ); 726 727// Apply the file wide options 728_combineOptions (categorySet ,fileOptions ,testDetails .options ); 729 730outTestList -> tests .add (testDetails ); 731 } 732else if (command == "DIAGNOSTIC_TEST" ) 733 { 734SlangResult diagRes = _gatherTestOptions (categorySet ,& cursor ,testDetails .options ); 735if (SLANG_FAILED (diagRes )) 736 { 737if (context && context -> getTestReporter ()) 738 { 739context -> getTestReporter ()-> messageFormat ( 740TestMessageType ::RunError , 741"Failed to parse DIAGNOSTIC_TEST directive in file '%s' (error: 0x%08X)" , 742filePath .getBuffer (), 743 (unsigned int )diagRes ); 744 } 745else 746 { 747fprintf ( 748stderr , 749"Failed to parse DIAGNOSTIC_TEST directive in file '%s' (error: 0x%08X)\n" , 750filePath .getBuffer (), 751 (unsigned int )diagRes ); 752 } 753return diagRes ; 754 } 755applyMacroSubstitution (filePath ,testDetails ); 756 757// Apply the file wide options 758_combineOptions (categorySet ,fileOptions ,testDetails .options ); 759 760// Mark that it is a diagnostic test 761testDetails .options .type = TestOptions ::Type ::Diagnostic ; 762outTestList -> tests .add (testDetails ); 763 } 764else 765 { 766// Hmm we don't know what kind of test this actually is. 767// Assume that's ok and this *isn't* a test and ignore. 768skipToEndOfLine (& cursor ); 769 } 770 } 771 772return SLANG_OK ; 773} 774 775static void SLANG_STDCALL _fileCheckDiagnosticCallback ( 776void * data , 777const TestMessageType messageType , 778const char * message )noexcept 779{ 780auto & testReporter = * reinterpret_cast < TestReporter *> (data ); 781testReporter .message (messageType ,message ); 782} 783struct bool2 784{ 785bool x ,y ; 786}; 787// 788// Check some generated output with FileCheck 789// 790static TestResult _fileCheckTest ( 791TestContext & context , 792const String & fileCheckRules , 793const String & fileCheckPrefix , 794const String & outputToCheck ) 795{ 796auto & testReporter = * context .getTestReporter (); 797 798IFileCheck * fc = context .getFileCheck (); 799if (!fc ) 800 { 801// Ignore if FileCheck is not available. 802// We could report an error, but our ARM64 CI doesn't have FileCheck yet. 803testReporter .message (TestMessageType ::Info ,"FileCheck is not available" ); 804return TestResult ::Ignored ; 805 } 806 807const bool coloredOutput = true; 808testReporter .message (TestMessageType ::Info ,outputToCheck .getBuffer ()); 809return fc -> performTest ( 810"slang-test" , 811fileCheckRules .begin (), 812fileCheckPrefix .begin (), 813outputToCheck .begin (), 814"actual-output" , 815_fileCheckDiagnosticCallback , 816& testReporter , 817coloredOutput ); 818} 819 820template < typename Compare > 821static TestResult _fileComparisonTest ( 822TestContext & context , 823const TestInput & input , 824const char * defaultExpectedContent , 825const char * expectedFileSuffix , 826const String & actualOutput , 827Compare compare ) 828{ 829String expectedOutput ; 830 831if (SLANG_FAILED (_readTestFile (input ,expectedFileSuffix ,expectedOutput ))) 832 { 833if (defaultExpectedContent ) 834 { 835expectedOutput = defaultExpectedContent ; 836 } 837else 838 { 839context .getTestReporter ()-> messageFormat ( 840TestMessageType ::RunError , 841"Unable to read %s output for '%s'\n" , 842expectedFileSuffix , 843input .outputStem .getBuffer ()); 844return TestResult ::Fail ; 845 } 846 } 847 848// Otherwise we compare to the expected output 849if (!compare (actualOutput ,expectedOutput )) 850 { 851context .getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 852return TestResult ::Fail ; 853 } 854return TestResult ::Pass ; 855} 856 857static bool _areLinesEqual (const String & a ,const String & e ) 858{ 859return StringUtil ::areLinesEqual (a .getUnownedSlice (),e .getUnownedSlice ()); 860} 861 862// Either run FileCheck over the result, or read and compare with a .expected file 863// On a comparison failure, dump the difference 864// On any failure, write a .actual file. 865template < typename Compare = decltype(_areLinesEqual )> 866static TestResult _validateOutput ( 867TestContext * const context , 868const TestInput & input , 869const String & actualOutput , 870const bool forceFailure = false, 871const char * defaultExpectedContent = nullptr , 872const Compare compare = _areLinesEqual ) 873{ 874String fileCheckPrefix ; 875const TestResult result = 876input .testOptions -> getFileCheckPrefix (fileCheckPrefix ) 877 ?_fileCheckTest (* context ,input .filePath ,fileCheckPrefix ,actualOutput ) 878 :_fileComparisonTest ( 879* context , 880input , 881defaultExpectedContent , 882".expected" , 883actualOutput , 884compare ); 885 886// If the test failed, then we write the actual output to a file 887// so that we can easily diff it from the command line and 888// diagnose the problem. 889if (result == TestResult ::Fail || forceFailure ) 890 { 891String actualOutputPath = input .outputStem + ".actual" ; 892Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 893return TestResult ::Fail ; 894 } 895else 896 { 897return result ; 898 } 899} 900 901Result spawnAndWaitExe ( 902TestContext * context , 903const String & testPath , 904const CommandLine & cmdLine , 905ExecuteResult & outRes ) 906{ 907 std::lock_guard < std::mutex > lock (context -> mutex ); 908 909const auto & options = context -> options ; 910 911if (options .verbosity == VerbosityLevel ::Verbose ) 912 { 913String commandLine = cmdLine .toString (); 914context -> getTestReporter ()-> messageFormat ( 915TestMessageType ::Info , 916"%s\n" , 917commandLine .begin ()); 918 } 919 920Result res = ProcessUtil ::execute (cmdLine ,outRes ); 921if (SLANG_FAILED (res )) 922 { 923// fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); 924context -> getTestReporter ()-> messageFormat ( 925TestMessageType ::RunError , 926"failed to run test '%S'" , 927testPath .toWString ().begin ()); 928 } 929return res ; 930} 931 932 933Result spawnAndWaitSharedLibrary ( 934TestContext * context , 935const String & testPath , 936const CommandLine & cmdLine , 937ExecuteResult & outRes ) 938{ 939 std::lock_guard < std::mutex > lock (context -> mutex ); 940 941const auto & options = context -> options ; 942String exeName = Path ::getFileNameWithoutExt (cmdLine .m_executableLocation .m_pathOrName ); 943 944if (options .verbosity == VerbosityLevel ::Verbose ) 945 { 946CommandLine testCmdLine ; 947 948testCmdLine .setExecutableLocation (ExecutableLocation ("slang-test" )); 949 950if (options .binDir .getLength ()) 951 { 952testCmdLine .addArg ("-bindir" ); 953testCmdLine .addArg (options .binDir ); 954 } 955 956testCmdLine .addArg (exeName ); 957testCmdLine .m_args .addRange (cmdLine .m_args ); 958 959context -> getTestReporter ()-> messageFormat ( 960TestMessageType ::Info , 961"%s\n" , 962testCmdLine .toString ().getBuffer ()); 963 } 964 965auto func = context -> getInnerMainFunc (context -> options .binDir ,exeName ); 966if (func ) 967 { 968StringBuilder stdErrorString ; 969StringBuilder stdOutString ; 970 renderer_test::CoreDebugCallback coreDebugCallback ; 971 renderer_test::CoreToRHIDebugBridge rhiDebugBridge ; 972rhiDebugBridge .setCoreCallback (& coreDebugCallback ); 973 974// Say static so not released 975StringWriter stdError (& stdErrorString ,WriterFlag ::IsConsole |WriterFlag ::IsStatic ); 976StringWriter stdOut (& stdOutString ,WriterFlag ::IsConsole |WriterFlag ::IsStatic ); 977 978StdWriters * prevStdWriters = StdWriters ::getSingleton (); 979 980StdWriters stdWriters ; 981stdWriters .setWriter (SLANG_WRITER_CHANNEL_STD_ERROR ,& stdError ); 982stdWriters .setWriter (SLANG_WRITER_CHANNEL_STD_OUTPUT ,& stdOut ); 983stdWriters .setDebugCallback (& coreDebugCallback ); 984 985if (exeName == "slangc" || exeName == "slangi" ) 986 { 987stdWriters .setWriter (SLANG_WRITER_CHANNEL_DIAGNOSTIC ,& stdError ); 988 } 989 990String exePath = Path ::combine (context -> exeDirectoryPath ,exeName ); 991 992List < const char *> args ; 993args .add (exePath .getBuffer ()); 994for (const auto & cmdArg :cmdLine .m_args ) 995 { 996args .add (cmdArg .getBuffer ()); 997 } 998 999SlangResult res = 1000func (& stdWriters ,context -> getSession (),int (args .getCount ()),args .begin ()); 1001 1002StdWriters ::setSingleton (prevStdWriters ); 1003 1004outRes .standardError = stdErrorString ; 1005outRes .standardOutput = stdOutString ; 1006outRes .debugLayer = coreDebugCallback .getString (); 1007 1008outRes .resultCode = (int )TestToolUtil ::getReturnCode (res ); 1009 1010return SLANG_OK ; 1011 } 1012 1013return SLANG_FAIL ; 1014} 1015 1016 1017Result spawnAndWaitProxy ( 1018TestContext * context , 1019const String & testPath , 1020const CommandLine & inCmdLine , 1021ExecuteResult & outRes ) 1022{ 1023 std::lock_guard < std::mutex > lock (context -> mutex ); 1024 1025// Get the name of the thing to execute 1026String exeName = Path ::getFileNameWithoutExt (inCmdLine .m_executableLocation .m_pathOrName ); 1027 1028if (exeName == "slangc" || exeName == "slangi" ) 1029 { 1030// If the test is slangc there is a command line version we can just directly use 1031// return spawnAndWaitExe(context, testPath, inCmdLine, outRes); 1032return spawnAndWaitSharedLibrary (context ,testPath ,inCmdLine ,outRes ); 1033 } 1034 1035CommandLine cmdLine (inCmdLine ); 1036 1037// Make the first arg the name of the tool to invoke 1038cmdLine .m_args .insert (0 ,exeName ); 1039cmdLine .setExecutableLocation (ExecutableLocation (context -> exeDirectoryPath ,"test-proxy" )); 1040 1041const auto & options = context -> options ; 1042if (options .verbosity == VerbosityLevel ::Verbose ) 1043 { 1044String commandLine = cmdLine .toString (); 1045context -> getTestReporter ()-> messageFormat ( 1046TestMessageType ::Info , 1047"%s\n" , 1048commandLine .begin ()); 1049 } 1050 1051// Execute 1052Result res = ProcessUtil ::execute (cmdLine ,outRes ); 1053if (SLANG_FAILED (res )) 1054 { 1055// fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); 1056context -> getTestReporter ()-> messageFormat ( 1057TestMessageType ::RunError , 1058"failed to run test '%S'" , 1059testPath .toWString ().begin ()); 1060 } 1061 1062return res ; 1063} 1064 1065static Result _executeRPC ( 1066TestContext * context , 1067SpawnType spawnType , 1068const UnownedStringSlice & method , 1069const RttiInfo * rttiInfo , 1070const void * args , 1071ExecuteResult & outRes ) 1072{ 1073// If we are 'fully isolated', we cannot share a test server. 1074// So tear down the RPC connection if there is one currently. 1075if (spawnType == SpawnType ::UseFullyIsolatedTestServer ) 1076 { 1077context -> destroyRPCConnection (); 1078 } 1079 1080JSONRPCConnection * rpcConnection = context -> getOrCreateJSONRPCConnection (); 1081if (!rpcConnection ) 1082 { 1083context -> getTestReporter ()-> messageFormat ( 1084TestMessageType ::RunError , 1085"JSON RPC failure: getOrCreateJSONRPCConnection()" ); 1086return SLANG_FAIL ; 1087 } 1088 1089// Execute 1090if (SLANG_FAILED (rpcConnection -> sendCall (method ,rttiInfo ,args ))) 1091 { 1092context -> getTestReporter ()-> messageFormat ( 1093TestMessageType ::RunError , 1094"JSON RPC failure: sendCall()" ); 1095 1096context -> destroyRPCConnection (); 1097return SLANG_FAIL ; 1098 } 1099 1100// Wait for the result 1101if (SLANG_FAILED (rpcConnection -> waitForResult (context -> connectionTimeOutInMs ))) 1102 { 1103context -> getTestReporter ()-> messageFormat ( 1104TestMessageType ::RunError , 1105"JSON RPC failure: waitForResult()" ); 1106 } 1107 1108if (!rpcConnection -> hasMessage ()) 1109 { 1110context -> getTestReporter ()-> messageFormat ( 1111TestMessageType ::RunError , 1112"JSON RPC failure: hasMessage()" ); 1113 1114// We can assume somethings gone wrong. So lets kill the connection and fail. 1115context -> destroyRPCConnection (); 1116return SLANG_FAIL ; 1117 } 1118 1119if (rpcConnection -> getMessageType ()!= JSONRPCMessageType ::Result ) 1120 { 1121context -> getTestReporter ()-> messageFormat ( 1122TestMessageType ::RunError , 1123"JSON RPC failure: getMessageType() != JSONRPCMessageType::Result" ); 1124 1125context -> destroyRPCConnection (); 1126return SLANG_FAIL ; 1127 } 1128 1129// Get the result 1130TestServerProtocol ::ExecutionResult exeRes ; 1131if (SLANG_FAILED (rpcConnection -> getMessage (& exeRes ))) 1132 { 1133context -> getTestReporter ()-> messageFormat ( 1134TestMessageType ::RunError , 1135"JSON RPC failure: getMessage()" ); 1136 1137context -> destroyRPCConnection (); 1138return SLANG_FAIL ; 1139 } 1140 1141outRes .resultCode = exeRes .returnCode ; 1142outRes .standardError = exeRes .stdError ; 1143outRes .standardOutput = exeRes .stdOut ; 1144outRes .debugLayer = exeRes .debugLayer ; 1145 1146return SLANG_OK ; 1147} 1148 1149template < typename T > 1150static Result _executeRPC ( 1151TestContext * context , 1152SpawnType spawnType , 1153const UnownedStringSlice & method , 1154const T * msg , 1155ExecuteResult & outRes ) 1156{ 1157return _executeRPC (context ,spawnType ,method ,GetRttiInfo < T > ::get (), (const void * )msg ,outRes ); 1158} 1159 1160Result spawnAndWaitTestServer ( 1161TestContext * context , 1162SpawnType spawnType , 1163const String & testPath , 1164const CommandLine & inCmdLine , 1165ExecuteResult & outRes ) 1166{ 1167String exeName = Path ::getFileNameWithoutExt (inCmdLine .m_executableLocation .m_pathOrName ); 1168 1169// This is a test tool execution 1170TestServerProtocol ::ExecuteToolTestArgs args ; 1171 1172args .toolName = exeName ; 1173args .args = inCmdLine .m_args ; 1174 1175return _executeRPC ( 1176context , 1177spawnType , 1178TestServerProtocol ::ExecuteToolTestArgs ::g_methodName , 1179& args , 1180outRes ); 1181} 1182 1183static SlangResult _extractArg (const CommandLine & cmdLine ,const String & argName ,String & outValue ) 1184{ 1185SLANG_ASSERT (argName .getLength ()> 0 && argName [0 ]== '-' ); 1186Index index = cmdLine .findArgIndex (argName .getUnownedSlice ()); 1187 1188if (index >=0 && index < cmdLine .getArgCount ()- 1 ) 1189 { 1190outValue = cmdLine .m_args [index + 1 ]; 1191return SLANG_OK ; 1192 } 1193return SLANG_FAIL ; 1194} 1195 1196static bool _hasOption (const List < String >& args ,const String & argName ) 1197{ 1198return args .indexOf (argName )!= Index (-1 ); 1199} 1200 1201static PassThroughFlags _getPassThroughFlagsForTarget (SlangCompileTarget target ) 1202{ 1203switch (target ) 1204 { 1205case SLANG_TARGET_UNKNOWN : 1206 1207case SLANG_HLSL : 1208case SLANG_GLSL : 1209case SLANG_C_SOURCE : 1210case SLANG_CPP_SOURCE : 1211case SLANG_CPP_PYTORCH_BINDING : 1212case SLANG_HOST_CPP_SOURCE : 1213case SLANG_CUDA_SOURCE : 1214case SLANG_METAL : 1215case SLANG_WGSL : 1216case SLANG_HOST_VM : 1217 { 1218return 0 ; 1219 } 1220case SLANG_WGSL_SPIRV : 1221case SLANG_WGSL_SPIRV_ASM : 1222 { 1223return PassThroughFlag ::Tint ; 1224 } 1225case SLANG_DXBC : 1226case SLANG_DXBC_ASM : 1227 { 1228return PassThroughFlag ::Fxc ; 1229 } 1230case SLANG_SPIRV : 1231case SLANG_SPIRV_ASM : 1232 { 1233return PassThroughFlag ::Glslang ; 1234 } 1235case SLANG_DXIL : 1236case SLANG_DXIL_ASM : 1237 { 1238return PassThroughFlag ::Dxc ; 1239 } 1240 1241case SLANG_METAL_LIB : 1242case SLANG_METAL_LIB_ASM : 1243 { 1244return PassThroughFlag ::Metal ; 1245 } 1246 1247case SLANG_SHADER_HOST_CALLABLE : 1248case SLANG_HOST_HOST_CALLABLE : 1249 1250case SLANG_HOST_EXECUTABLE : 1251case SLANG_SHADER_SHARED_LIBRARY : 1252case SLANG_HOST_SHARED_LIBRARY : 1253 { 1254return PassThroughFlag ::Generic_C_CPP ; 1255 } 1256case SLANG_PTX : 1257 { 1258return PassThroughFlag ::NVRTC ; 1259 } 1260 1261default : 1262 { 1263SLANG_ASSERT (!"Unknown type" ); 1264return 0 ; 1265 } 1266 } 1267} 1268 1269static SlangResult _extractRenderTestRequirements ( 1270const CommandLine & cmdLine , 1271TestRequirements * ioRequirements ) 1272{ 1273const auto & args = cmdLine .m_args ; 1274 1275// TODO(JS): 1276// This is rather convoluted in that it has to work out from the command line parameters passed 1277// to render-test what renderer will be used. 1278// That a similar logic has to be kept inside the implementation of render-test and both this 1279// and render-test will have to be kept in sync. 1280 1281bool useDxbc = cmdLine .findArgIndex (UnownedStringSlice ::fromLiteral ("-use-dxbc" )) >=0 ; 1282 1283bool usePassthru = false; 1284 1285// Work out what kind of render will be used 1286RenderApiType renderApiType ; 1287 { 1288RenderApiType foundRenderApiType = RenderApiType ::Unknown ; 1289RenderApiType foundLanguageRenderType = RenderApiType ::Unknown ; 1290 1291for (const auto & arg :args ) 1292 { 1293Slang ::UnownedStringSlice argSlice = arg .getUnownedSlice (); 1294if (argSlice .getLength ()&& argSlice [0 ]== '-' ) 1295 { 1296// Look up the rendering API if set 1297UnownedStringSlice argName = 1298UnownedStringSlice (argSlice .begin ()+ 1 ,argSlice .end ()); 1299RenderApiType renderApiType = RenderApiUtil ::findApiTypeByName (argName ); 1300 1301if (renderApiType != RenderApiType ::Unknown ) 1302 { 1303foundRenderApiType = renderApiType ; 1304 1305// There should be only one explicit api 1306SLANG_ASSERT ( 1307ioRequirements -> explicitRenderApi == RenderApiType ::Unknown || 1308ioRequirements -> explicitRenderApi == renderApiType ); 1309 1310// Set the explicitly set render api 1311ioRequirements -> explicitRenderApi = renderApiType ; 1312continue ; 1313 } 1314 1315// Lookup the target language type 1316RenderApiType languageRenderType = 1317RenderApiUtil ::findImplicitLanguageRenderApiType (argName ); 1318if (languageRenderType != RenderApiType ::Unknown ) 1319 { 1320foundLanguageRenderType = languageRenderType ; 1321 1322// Use the pass thru compiler if these are the sources 1323usePassthru |= (argName == "hlsl" || argName == "glsl" ); 1324 1325continue ; 1326 } 1327 } 1328 } 1329 1330// If a render option isn't set use defaultRenderType 1331renderApiType = (foundRenderApiType == RenderApiType ::Unknown ) ?foundLanguageRenderType 1332 :foundRenderApiType ; 1333 } 1334 1335// The native language for the API 1336SlangSourceLanguage nativeLanguage = SLANG_SOURCE_LANGUAGE_UNKNOWN ; 1337SlangCompileTarget target = SLANG_TARGET_NONE ; 1338SlangPassThrough passThru = SLANG_PASS_THROUGH_NONE ; 1339 1340switch (renderApiType ) 1341 { 1342case RenderApiType ::D3D11 : 1343target = SLANG_DXBC ; 1344nativeLanguage = SLANG_SOURCE_LANGUAGE_HLSL ; 1345passThru = SLANG_PASS_THROUGH_FXC ; 1346break ; 1347case RenderApiType ::D3D12 : 1348target = SLANG_DXIL ; 1349nativeLanguage = SLANG_SOURCE_LANGUAGE_HLSL ; 1350passThru = SLANG_PASS_THROUGH_DXC ; 1351if (useDxbc ) 1352 { 1353target = SLANG_DXBC ; 1354passThru = SLANG_PASS_THROUGH_FXC ; 1355 } 1356break ; 1357case RenderApiType ::Vulkan : 1358target = SLANG_SPIRV ; 1359nativeLanguage = SLANG_SOURCE_LANGUAGE_GLSL ; 1360passThru = SLANG_PASS_THROUGH_GLSLANG ; 1361break ; 1362case RenderApiType ::Metal : 1363target = SLANG_METAL_LIB ; 1364nativeLanguage = SLANG_SOURCE_LANGUAGE_METAL ; 1365passThru = SLANG_PASS_THROUGH_METAL ; 1366break ; 1367case RenderApiType ::CPU : 1368target = SLANG_SHADER_HOST_CALLABLE ; 1369nativeLanguage = SLANG_SOURCE_LANGUAGE_CPP ; 1370passThru = SLANG_PASS_THROUGH_GENERIC_C_CPP ; 1371break ; 1372case RenderApiType ::CUDA : 1373target = SLANG_PTX ; 1374nativeLanguage = SLANG_SOURCE_LANGUAGE_CUDA ; 1375passThru = SLANG_PASS_THROUGH_NVRTC ; 1376break ; 1377case RenderApiType ::WebGPU : 1378target = SLANG_WGSL ; 1379nativeLanguage = SLANG_SOURCE_LANGUAGE_WGSL ; 1380passThru = SLANG_PASS_THROUGH_TINT ; 1381break ; 1382 } 1383 1384SlangSourceLanguage sourceLanguage = nativeLanguage ; 1385if (!usePassthru ) 1386 { 1387sourceLanguage = SLANG_SOURCE_LANGUAGE_SLANG ; 1388passThru = SLANG_PASS_THROUGH_NONE ; 1389 } 1390 1391if (passThru == SLANG_PASS_THROUGH_NONE ) 1392 { 1393// Work out backends needed based on the target 1394ioRequirements -> addUsedBackends (_getPassThroughFlagsForTarget (target )); 1395 } 1396else 1397 { 1398ioRequirements -> addUsedBackEnd (passThru ); 1399 } 1400 1401// Add the render api used 1402ioRequirements -> addUsedRenderApi (renderApiType ); 1403 1404return SLANG_OK ; 1405} 1406 1407static SlangResult _extractSlangCTestRequirements ( 1408const CommandLine & cmdLine , 1409TestRequirements * ioRequirements ) 1410{ 1411// This determines what the requirements are for a slangc like command line 1412// First check pass through 1413 { 1414String passThrough ; 1415if (SLANG_SUCCEEDED (_extractArg (cmdLine ,"-pass-through" ,passThrough ))) 1416 { 1417ioRequirements -> addUsedBackEnd ( 1418TypeTextUtil ::findPassThrough (passThrough .getUnownedSlice ())); 1419 } 1420 } 1421 1422// The target if set will also imply a backend 1423 { 1424String targetName ; 1425if (SLANG_SUCCEEDED (_extractArg (cmdLine ,"-target" ,targetName ))) 1426 { 1427const SlangCompileTarget target = 1428TypeTextUtil ::findCompileTargetFromName (targetName .getUnownedSlice ()); 1429ioRequirements -> addUsedBackends (_getPassThroughFlagsForTarget (target )); 1430 } 1431 } 1432return SLANG_OK ; 1433} 1434 1435static SlangResult _extractReflectionTestRequirements ( 1436const CommandLine & cmdLine , 1437TestRequirements * ioRequirements ) 1438{ 1439// There are no specialized constraints for a reflection test 1440return SLANG_OK ; 1441} 1442 1443static SlangResult _extractTestRequirements (const CommandLine & cmdLine ,TestRequirements * ioInfo ) 1444{ 1445String exeName = Path ::getFileNameWithoutExt (cmdLine .m_executableLocation .m_pathOrName ); 1446 1447if (exeName == "render-test" ) 1448 { 1449return _extractRenderTestRequirements (cmdLine ,ioInfo ); 1450 } 1451else if (exeName == "slangc" ) 1452 { 1453return _extractSlangCTestRequirements (cmdLine ,ioInfo ); 1454 } 1455else if (exeName == "slangi" ) 1456 { 1457return SLANG_OK ; 1458 } 1459else if (exeName == "slang-reflection-test" ) 1460 { 1461return _extractReflectionTestRequirements (cmdLine ,ioInfo ); 1462 } 1463 1464SLANG_ASSERT (!"Unknown tool type" ); 1465return SLANG_FAIL ; 1466} 1467 1468static RenderApiFlags _getAvailableRenderApiFlags (TestContext * context ) 1469{ 1470static std::mutex mutex ; 1471 std::lock_guard < std::mutex > lock (mutex ); 1472// Only evaluate if it hasn't already been evaluated (the actual evaluation is slow...) 1473if (!context -> isAvailableRenderApiFlagsValid ) 1474 { 1475// Call the render-test tool asking it only to startup a specified render api 1476// (taking into account adapter options) 1477 1478RenderApiFlags availableRenderApiFlags = 0 ; 1479for (int i = 0 ;i < int (RenderApiType ::CountOf );++ i ) 1480 { 1481const RenderApiType apiType = RenderApiType (i ); 1482 1483if (apiType == RenderApiType ::CPU ) 1484 { 1485if ((context -> availableBackendFlags & PassThroughFlag ::Generic_C_CPP )== 0 ) 1486 { 1487continue ; 1488 } 1489 1490// Check that the session has the generic C/CPP compiler availability - which is all 1491// we should need for CPU target 1492if (SLANG_SUCCEEDED (context -> getSession ()-> checkPassThroughSupport ( 1493SLANG_PASS_THROUGH_GENERIC_C_CPP ))) 1494 { 1495availableRenderApiFlags |=RenderApiFlags (1 ) <<int (apiType ); 1496 } 1497continue ; 1498 } 1499 1500// See if it's possible the api is available 1501if (RenderApiUtil ::calcHasApi (apiType )) 1502 { 1503if (context -> options .skipApiDetection ) 1504 { 1505availableRenderApiFlags |=RenderApiFlags (1 ) <<int (apiType ); 1506continue ; 1507 } 1508// Try starting up the device 1509CommandLine cmdLine ; 1510cmdLine .setExecutableLocation ( 1511ExecutableLocation (context -> options .binDir ,"render-test" )); 1512_addRenderTestOptions (context -> options ,cmdLine ); 1513// We just want to see if the device can be started up 1514cmdLine .addArg ("-only-startup" ); 1515 1516// Select what api to use 1517StringBuilder builder ; 1518builder <<"-" <<RenderApiUtil ::getApiName (apiType ); 1519cmdLine .addArg (builder ); 1520// Run the render-test tool and see if the device could startup 1521ExecuteResult exeRes ; 1522if (SLANG_SUCCEEDED ( 1523spawnAndWaitSharedLibrary (context ,"device-startup" ,cmdLine ,exeRes ))&& 1524TestToolUtil ::getReturnCodeFromInt (exeRes .resultCode )== 1525ToolReturnCode ::Success ) 1526 { 1527availableRenderApiFlags |=RenderApiFlags (1 ) <<int (apiType ); 1528StdWriters ::getOut ()."Check %s: Supported\n" , 1530RenderApiUtil ::getApiName (apiType ).begin ()); 1531 } 1532else 1533 { 1534StdWriters ::getOut ()."Check %s: Not Supported\n" , 1536RenderApiUtil ::getApiName (apiType ).begin ()); 1537const auto out = exeRes .standardOutput ; 1538const auto err = exeRes .standardError ; 1539if (err .getLength ()) 1540StdWriters ::getOut ()."%s\n" ,err .getBuffer ()); 1541if (out .getLength ()) 1542StdWriters ::getOut ()."%s\n" ,out .getBuffer ()); 1543 } 1544 } 1545 } 1546 1547// After determining available APIs, print adapter info for each one 1548if (context -> options .showAdapterInfo && availableRenderApiFlags ) 1549 { 1550StdWriters ::getOut ()."\nAdapter Information for Available APIs:\n" ); 1551for (int i = 0 ;i < int (RenderApiType ::CountOf );++ i ) 1552 { 1553const RenderApiType apiType = RenderApiType (i ); 1554const RenderApiFlags apiFlag = RenderApiFlags (1 ) <<int (apiType ); 1555 1556if (availableRenderApiFlags & apiFlag ) 1557 { 1558// Create command line to query adapter info 1559CommandLine cmdLine ; 1560cmdLine .setExecutableLocation ( 1561ExecutableLocation (context -> options .binDir ,"render-test" )); 1562 1563// Add the API type 1564StringBuilder builder ; 1565builder <<"-" <<RenderApiUtil ::getApiName (apiType ); 1566cmdLine .addArg (builder ); 1567 1568// Add flags to show adapter info and only startup 1569cmdLine .addArg ("-show-adapter-info" ); 1570cmdLine .addArg ("-only-startup" ); 1571 1572// Run render-test to get adapter info 1573ExecuteResult exeRes ; 1574if (SLANG_SUCCEEDED ( 1575spawnAndWaitSharedLibrary (context ,"adapter-info" ,cmdLine ,exeRes ))) 1576 { 1577// Output the adapter info 1578StdWriters ::getOut ()."\n%s:\n%s" , 1580RenderApiUtil ::getApiName (apiType ).begin (), 1581exeRes .standardOutput .getBuffer ()); 1582 } 1583 } 1584 } 1585StdWriters ::getOut ()."\n" ); 1586 } 1587 1588context -> availableRenderApiFlags = availableRenderApiFlags ; 1589context -> isAvailableRenderApiFlagsValid = true; 1590 } 1591 1592return context -> availableRenderApiFlags ; 1593} 1594 1595ToolReturnCode getReturnCode (const ExecuteResult & exeRes ) 1596{ 1597return TestToolUtil ::getReturnCodeFromInt (exeRes .resultCode ); 1598} 1599 1600ToolReturnCode spawnAndWait ( 1601TestContext * context , 1602const String & testPath , 1603SpawnType spawnType , 1604const CommandLine & cmdLine , 1605ExecuteResult & outExeRes ) 1606{ 1607if (context -> isCollectingRequirements ()) 1608 { 1609 std::lock_guard < std::mutex > lock (context -> mutex ); 1610// If we just want info... don't bother running anything 1611const SlangResult res = _extractTestRequirements (cmdLine ,context -> getTestRequirements ()); 1612// Keep compiler happy on release 1613SLANG_UNUSED (res ); 1614SLANG_ASSERT (SLANG_SUCCEEDED (res )); 1615 1616return ToolReturnCode ::Success ; 1617 } 1618 1619const auto & options = context -> options ; 1620 1621const auto finalSpawnType = context -> getFinalSpawnType (spawnType ); 1622 1623SlangResult spawnResult = SLANG_FAIL ; 1624switch (finalSpawnType ) 1625 { 1626case SpawnType ::UseExe : 1627 { 1628spawnResult = spawnAndWaitExe (context ,testPath ,cmdLine ,outExeRes ); 1629break ; 1630 } 1631case SpawnType ::Default : 1632case SpawnType ::UseSharedLibrary : 1633 { 1634spawnResult = spawnAndWaitSharedLibrary (context ,testPath ,cmdLine ,outExeRes ); 1635break ; 1636 } 1637case SpawnType ::UseFullyIsolatedTestServer : 1638case SpawnType ::UseTestServer : 1639 { 1640spawnResult = 1641spawnAndWaitTestServer (context ,finalSpawnType ,testPath ,cmdLine ,outExeRes ); 1642break ; 1643 } 1644default : 1645break ; 1646 } 1647 1648if (SLANG_FAILED (spawnResult )) 1649 { 1650return ToolReturnCode ::FailedToRun ; 1651 } 1652 1653return getReturnCode (outExeRes ); 1654} 1655 1656// Remove embedded source code from SPIR-V assembly output to prevent filecheck from matching 1657// against embedded source instead of actual SPIR-V instructions 1658String removeEmbeddedSourceFromSPIRV (const String & spirvOutput ) 1659{ 1660StringBuilder filteredOutput ; 1661List < UnownedStringSlice > lines ; 1662StringUtil ::calcLines (spirvOutput .getUnownedSlice (),lines ); 1663 1664if (spirvOutput .endsWith ("\n" )) 1665 { 1666// The last empty line should be removed, 1667// because `StringUtil::calcLines()` turns "A\nB\n" into three lines; not two. 1668SLANG_ASSERT (lines [lines .getCount ()- 1 ]== "" ); 1669lines .setCount (lines .getCount ()- 1 ); 1670 } 1671 1672// First pass: Find OpString IDs that are referenced by DebugSource 1673List < String > sourceStringIds ; 1674for (const auto & line :lines ) 1675 { 1676UnownedStringSlice trimmedLine = line .trim (); 1677 1678if (trimmedLine .indexOf (UnownedStringSlice (" DebugSource " ))== Index (-1 )) 1679continue ; 1680 1681// Extract the last parameter which is the source string ID 1682// Pattern: %4 = OpExtInst %void %2 DebugSource %5 %1 1683List < UnownedStringSlice > tokens ; 1684StringUtil ::split (trimmedLine ,' ' ,tokens ); 1685 1686// The last token should be the source string ID 1687UnownedStringSlice lastToken = tokens .getLast (); 1688if (lastToken .startsWith (UnownedStringSlice ("%" ))) 1689 { 1690sourceStringIds .add (String (lastToken )); 1691 } 1692 } 1693 1694// Second pass: Process embedded source strings to replace content with informative message 1695bool insideSourceString = false; 1696for (const auto & line :lines ) 1697 { 1698UnownedStringSlice trimmedLine = line .trim (); 1699 1700if (!insideSourceString ) 1701 { 1702Index equalPos = trimmedLine .indexOf (UnownedStringSlice (" = OpString" )); 1703if (equalPos != Index (-1 )&& trimmedLine .startsWith (UnownedStringSlice ("%" ))) 1704 { 1705String currentStringId = String (trimmedLine .head (equalPos )); 1706if (sourceStringIds .contains (currentStringId )) 1707 { 1708insideSourceString = true; 1709Index quotePos = line .indexOf ('\"' ); 1710if (quotePos != Index (-1 )) 1711 { 1712filteredOutput .append (String (line .head (quotePos + 1 ))); 1713filteredOutput .append ("// slang-test removed the embedded source\n" ); 1714filteredOutput .append ("// Use `" ); 1715filteredOutput .append (kPreserveEmbeddedSourceOption ); 1716filteredOutput .append ("` to keep it explicitly\n\"\n" ); 1717 } 1718continue ; 1719 } 1720 } 1721 } 1722 1723if (insideSourceString ) 1724 { 1725if (trimmedLine .endsWith ("\"" )&& 1726 (trimmedLine .getLength ()< 2 || trimmedLine [trimmedLine .getLength ()- 2 ]!= '\\' )) 1727 { 1728insideSourceString = false; 1729 } 1730 1731// skip the embedded source lines 1732continue ; 1733 } 1734 1735// Add this line to the filtered output 1736filteredOutput .append (line ); 1737filteredOutput .append ("\n" ); 1738 } 1739 1740return filteredOutput .produceString (); 1741} 1742 1743String getOutput (const ExecuteResult & exeRes ,bool removeEmbeddedSource = false) 1744{ 1745ExecuteResult ::ResultCode resultCode = exeRes .resultCode ; 1746 1747String standardOuptut = exeRes .standardOutput ; 1748String standardError = exeRes .standardError ; 1749String debugLayer = exeRes .debugLayer ; 1750 1751// Apply embedded source removal to standard output if requested 1752if (removeEmbeddedSource && standardOuptut .getLength ()> 0 ) 1753 { 1754standardOuptut = removeEmbeddedSourceFromSPIRV (standardOuptut ); 1755 } 1756 1757// We construct a single output string that captures the results 1758StringBuilder actualOutputBuilder ; 1759actualOutputBuilder .append ("result code = " ); 1760actualOutputBuilder .append (resultCode ); 1761actualOutputBuilder .append ("\nstandard error = {\n" ); 1762actualOutputBuilder .append (standardError ); 1763actualOutputBuilder .append ("}\nstandard output = {\n" ); 1764actualOutputBuilder .append (standardOuptut ); 1765actualOutputBuilder .append ("}\n" ); 1766if (debugLayer .getLength ()> 0 ) 1767 { 1768actualOutputBuilder .append ("debug layer = {\n" ); 1769actualOutputBuilder .append (debugLayer ); 1770actualOutputBuilder .append ("}\n" ); 1771 } 1772 1773return actualOutputBuilder .produceString (); 1774} 1775 1776// Finds the specialized or default path for expected data for a test. 1777// If neither are found, will return an empty string 1778String findExpectedPath (const TestInput & input ,const char * postFix ) 1779{ 1780StringBuilder specializedBuf ; 1781 1782// Try the specialized name first 1783specializedBuf <<input .outputStem ; 1784if (postFix ) 1785 { 1786specializedBuf <<postFix ; 1787 } 1788if (File ::exists (specializedBuf )) 1789 { 1790return specializedBuf ; 1791 } 1792 1793 1794// Try the default name 1795StringBuilder defaultBuf ; 1796defaultBuf .clear (); 1797defaultBuf <<input .filePath ; 1798if (postFix ) 1799 { 1800defaultBuf <<postFix ; 1801 } 1802 1803if (File ::exists (defaultBuf )) 1804 { 1805return defaultBuf ; 1806 } 1807 1808// Couldn't find either 1809fprintf ( 1810stderr , 1811"referenceOutput '%s' or '%s' not found.\n" , 1812defaultBuf .getBuffer (), 1813specializedBuf .getBuffer ()); 1814 1815return "" ; 1816} 1817 1818static SlangResult _initSlangInterpreter (TestContext * context ,CommandLine & ioCmdLine ) 1819{ 1820ioCmdLine .setExecutableLocation (ExecutableLocation (context -> options .binDir ,"slangi" )); 1821return SLANG_OK ; 1822} 1823 1824static SlangResult _initSlangCompiler (TestContext * context ,CommandLine & ioCmdLine ) 1825{ 1826ioCmdLine .setExecutableLocation (ExecutableLocation (context -> options .binDir ,"slangc" )); 1827 1828if (context -> options .verbosePaths ) 1829 { 1830ioCmdLine .addArgIfNotFound ("-verbose-paths" ); 1831 } 1832 1833for (auto & capability :context -> options .capabilities ) 1834 { 1835ioCmdLine .addArg ("-capability" ); 1836ioCmdLine .addArg (capability .getBuffer ()); 1837 } 1838 1839// Look for definition of a slot 1840 1841 { 1842const auto prefix = toSlice ("-DNV_SHADER_EXTN_SLOT=" ); 1843 1844bool usesNVAPI = false; 1845 1846for (auto & arg :ioCmdLine .m_args ) 1847 { 1848if (arg .startsWith (prefix )) 1849 { 1850// Has NVAPI prefix, meaning 1851usesNVAPI = true; 1852break ; 1853 } 1854 } 1855 1856// This is necessary because the session can be shared, and the prelude overwritten by the 1857// renderer. 1858if (usesNVAPI ) 1859 { 1860// We want to set the path to NVAPI 1861String rootPath ; 1862SLANG_RETURN_ON_FAIL (TestToolUtil ::getRootPath (context -> exePath .getBuffer (),rootPath )); 1863String includePath ; 1864SLANG_RETURN_ON_FAIL ( 1865TestToolUtil ::getIncludePath (rootPath ,"external/nvapi/nvHLSLExtns.h" ,includePath )) 1866 1867StringBuilder buf ; 1868 1869// Include the NVAPI header 1870buf <<"#include " ; 1871 1872StringEscapeUtil ::appendQuoted ( 1873StringEscapeUtil ::getHandler (StringEscapeUtil ::Style ::Cpp ), 1874includePath .getUnownedSlice (), 1875buf ); 1876buf <<"\n\n" ; 1877 1878context -> getSession ()-> setLanguagePrelude (SLANG_SOURCE_LANGUAGE_HLSL ,buf .getBuffer ()); 1879 } 1880 } 1881 1882return SLANG_OK ; 1883} 1884 1885TestResult asTestResult (ToolReturnCode code ) 1886{ 1887switch (code ) 1888 { 1889case ToolReturnCode ::Success : 1890return TestResult ::Pass ; 1891case ToolReturnCode ::Ignored : 1892return TestResult ::Ignored ; 1893default : 1894return TestResult ::Fail ; 1895 } 1896} 1897 1898#define TEST_RETURN_ON_DONE (x ) \ 1899 { \ 1900 const ToolReturnCode toolRet_ = x; \ 1901 if (TestToolUtil::isDone(toolRet_)) \ 1902 { \ 1903 return asTestResult(toolRet_); \ 1904 } \ 1905 } 1906 1907static SlangResult _createArtifactFromHexDump ( 1908const UnownedStringSlice & hexDump , 1909const ArtifactDesc & desc , 1910ComPtr < IArtifact >& outArtifact ) 1911{ 1912// We need to extract the binary 1913List < uint8_t > data ; 1914SLANG_RETURN_ON_FAIL (HexDumpUtil ::parseWithMarkers (hexDump ,data )); 1915 1916auto blob = ListBlob ::moveCreate (data ); 1917auto artifact = ArtifactUtil ::createArtifact (desc ); 1918artifact -> addRepresentationUnknown (blob ); 1919 1920outArtifact .swap (artifact ); 1921return SLANG_OK ; 1922} 1923 1924static SlangResult _executeBinary (const UnownedStringSlice & hexDump ,ExecuteResult & outExeRes ) 1925{ 1926ComPtr < IArtifact > artifact ; 1927SLANG_RETURN_ON_FAIL (_createArtifactFromHexDump ( 1928hexDump , 1929ArtifactDesc ::make ( 1930ArtifactKind ::Executable , 1931ArtifactPayload ::HostCPU , 1932ArtifactStyle ::Unknown ), 1933artifact )); 1934 1935ComPtr < IOSFileArtifactRepresentation > fileRep ; 1936SLANG_RETURN_ON_FAIL (artifact -> requireFile (ArtifactKeep ::Yes ,fileRep .writeRef ())); 1937 1938const auto fileName = fileRep -> getPath (); 1939 1940// Execute it 1941ExecutableLocation exe ; 1942exe .setPath (fileName ); 1943 1944CommandLine cmdLine ; 1945cmdLine .setExecutableLocation (exe ); 1946 1947return ProcessUtil ::execute (cmdLine ,outExeRes ); 1948} 1949 1950static bool _areDiagnosticsEqual (const UnownedStringSlice & a ,const UnownedStringSlice & b ) 1951{ 1952ParseDiagnosticUtil ::OutputInfo outA ,outB ; 1953 1954// If we can't parse, we can't match, so fail. 1955if (SLANG_FAILED (ParseDiagnosticUtil ::parseOutputInfo (a ,outA ))|| 1956SLANG_FAILED (ParseDiagnosticUtil ::parseOutputInfo (b ,outB ))) 1957 { 1958return false; 1959 } 1960 1961// The result codes must match, and std out 1962if (outA .resultCode != outB .resultCode || 1963 !StringUtil ::areLinesEqual (outA .stdOut .getUnownedSlice (),outB .stdOut .getUnownedSlice ())) 1964 { 1965return false; 1966 } 1967 1968// Parse the compiler diagnostics and make sure they are the same. 1969// Ignores line number differences 1970return ParseDiagnosticUtil ::areEqual ( 1971outA .stdError .getUnownedSlice (), 1972outB .stdError .getUnownedSlice (), 1973ParseDiagnosticUtil ::EqualityFlag ::IgnoreLineNos ); 1974} 1975 1976static bool _areResultsEqual (TestOptions ::Type type ,const String & a ,const String & b ) 1977{ 1978switch (type ) 1979 { 1980case TestOptions ::Type ::Diagnostic : 1981return _areDiagnosticsEqual (a .getUnownedSlice (),b .getUnownedSlice ()); 1982case TestOptions ::Type ::Normal : 1983return a == b ; 1984default : 1985 { 1986SLANG_ASSERT (!"Unknown test type" ); 1987return false; 1988 } 1989 } 1990} 1991 1992static String _calcModulePath (const TestInput & input ) 1993{ 1994// Make the module name the same as the source file 1995auto filePath = input .filePath ; 1996String directory = Path ::getParentDirectory (input .outputStem ); 1997String moduleName = Path ::getFileNameWithoutExt (filePath ); 1998return Path ::combine (directory ,moduleName ); 1999} 2000 2001TestResult runDocTest (TestContext * context ,TestInput & input ) 2002{ 2003// need to execute the stand-alone Slang compiler on the file, and compare its output to what we 2004// expect 2005auto outputStem = input .outputStem ; 2006 2007CommandLine cmdLine ; 2008 2009 2010cmdLine .addArg (input .filePath ); 2011 2012for (auto arg :input .testOptions -> args ) 2013 { 2014cmdLine .addArg (arg ); 2015 } 2016 2017_initSlangCompiler (context ,cmdLine ); 2018 2019ExecuteResult exeRes ; 2020TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 2021 2022if (context -> isCollectingRequirements ()) 2023 { 2024return TestResult ::Pass ; 2025 } 2026 2027String actualOutput = getOutput (exeRes ); 2028 2029String expectedOutputPath = outputStem + ".expected" ; 2030String expectedOutput ; 2031 2032// TODO(JS): Might want to check the result code.. 2033Slang ::File ::readAllText (expectedOutputPath ,expectedOutput ); 2034 2035// If no expected output file was found, then we 2036// expect everything to be empty 2037if (expectedOutput .getLength ()== 0 ) 2038 { 2039expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n" ; 2040 } 2041 2042TestResult result = TestResult ::Pass ; 2043 2044// Otherwise we compare to the expected output 2045if (!_areResultsEqual (input .testOptions -> type ,expectedOutput ,actualOutput )) 2046 { 2047context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 2048result = TestResult ::Fail ; 2049 } 2050 2051// If the test failed, then we write the actual output to a file 2052// so that we can easily diff it from the command line and 2053// diagnose the problem. 2054if (result == TestResult ::Fail ) 2055 { 2056String actualOutputPath = outputStem + ".actual" ; 2057Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 2058 2059context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 2060 } 2061 2062return result ; 2063} 2064 2065TestResult runExecutableTest (TestContext * context ,TestInput & input ) 2066{ 2067IDownstreamCompiler * compiler = context -> getDefaultCompiler (SLANG_SOURCE_LANGUAGE_CPP ); 2068if (!compiler ) 2069 { 2070return TestResult ::Ignored ; 2071 } 2072 2073// If we are just collecting requirements, say it passed 2074if (context -> isCollectingRequirements ()) 2075 { 2076 std::lock_guard < std::mutex > lock (context -> mutex ); 2077context -> getTestRequirements ()-> addUsedBackEnd (SLANG_PASS_THROUGH_GENERIC_C_CPP ); 2078return TestResult ::Pass ; 2079 } 2080 2081auto filePath = input .filePath ; 2082auto outputStem = input .outputStem ; 2083 2084String actualOutputPath = outputStem + ".actual" ; 2085File ::remove (actualOutputPath ); 2086 2087// Make the module name the same as the current executable path, so it can discover 2088// the slang-rt library if needed. 2089String modulePath = Path ::combine ( 2090Path ::getParentDirectory (Path ::getExecutablePath ()), 2091Path ::getFileNameWithoutExt (filePath )); 2092 2093// String testRoot 2094// for(;;) 2095// { 2096// String testRoot = Path::getParentDirectory(filePath); 2097// if (testRoot == "") 2098// { 2099// break; 2100// } 2101// } 2102// printf("test folder = %s\n", testRoot.begin()); 2103 2104String moduleExePath ; 2105 { 2106StringBuilder buf ; 2107buf <<modulePath ; 2108buf <<Process ::getExecutableSuffix (); 2109moduleExePath = buf ; 2110 } 2111 2112// Remove the exe if it exists 2113File ::remove (moduleExePath ); 2114 2115CommandLine cmdLine ; 2116_initSlangCompiler (context ,cmdLine ); 2117 2118StringEscapeHandler * escapeHandler = 2119StringEscapeUtil ::getHandler (StringEscapeUtil ::Style ::Space ); 2120 2121List < String > args ; 2122args .add (filePath ); 2123args .add ("-o" ); 2124args .add (moduleExePath ); 2125args .add ("-target" ); 2126args .add ("exe" ); 2127args .add ("-Xgenericcpp" ); 2128args .add ("-I./include" ); 2129args .add ("-Xgenericcpp" ); 2130args .add ("-I./external/unordered_dense/include" ); 2131for (auto arg :args ) 2132 { 2133// If unescaping is needed, do it 2134if (StringEscapeUtil ::isUnescapeShellLikeNeeded (escapeHandler ,arg .getUnownedSlice ())) 2135 { 2136StringBuilder buf ; 2137StringEscapeUtil ::unescapeShellLike (escapeHandler ,arg .getUnownedSlice (),buf ); 2138cmdLine .addArg (buf .produceString ()); 2139 } 2140else 2141 { 2142cmdLine .addArg (arg ); 2143 } 2144 } 2145ExecuteResult exeRes ; 2146 2147// TODO(Yong) HACK: 2148// Just use shared library now, TestServer spawn mode seems to cause slangc to fail to find its 2149// own executable path, and thus failed to find the `gfx.slang` file sitting along side 2150// `slangc.exe`. We need to figure out what happened to `Path::getExecutablePath()` inside 2151// test-server. 2152SpawnType slangcSpawnType = input .spawnType ; 2153if (slangcSpawnType == SpawnType ::UseTestServer ) 2154slangcSpawnType = SpawnType ::UseExe ; 2155TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,slangcSpawnType ,cmdLine ,exeRes )); 2156 2157String actualOutput ; 2158 2159// If the actual compilation failed, then the output will be the summary 2160if (exeRes .resultCode != 0 ) 2161 { 2162actualOutput = getOutput (exeRes ); 2163 } 2164else 2165 { 2166// Execute the binary and see what we get 2167CommandLine cmdLine ; 2168 2169ExecutableLocation exe ; 2170exe .setPath (moduleExePath ); 2171 2172cmdLine .setExecutableLocation (exe ); 2173 2174ExecuteResult exeRes ; 2175if (SLANG_FAILED (ProcessUtil ::execute (cmdLine ,exeRes ))) 2176 { 2177return TestResult ::Fail ; 2178 } 2179 2180// Write the output, and compare to expected 2181actualOutput = getOutput (exeRes ); 2182 } 2183 2184// Write the output 2185Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 2186 2187// Check that they are the same 2188 { 2189// Read the expected 2190String expectedOutput ; 2191 2192String expectedOutputPath = outputStem + ".expected" ; 2193Slang ::File ::readAllText (expectedOutputPath ,expectedOutput ); 2194 2195// Compare if they are the same 2196if (!StringUtil ::areLinesEqual ( 2197actualOutput .getUnownedSlice (), 2198expectedOutput .getUnownedSlice ())) 2199 { 2200context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 2201return TestResult ::Fail ; 2202 } 2203 } 2204 2205return TestResult ::Pass ; 2206} 2207 2208TestResult runLanguageServerTest (TestContext * context ,TestInput & input ) 2209{ 2210// We don't support running language server tests in parallel yet. 2211 std::lock_guard lock (context -> mutex ); 2212 2213if (!context -> m_languageServerConnection ) 2214 { 2215if (SLANG_FAILED (context -> createLanguageServerJSONRPCConnection ( 2216context -> m_languageServerConnection ))) 2217 { 2218return TestResult ::Fail ; 2219 } 2220 } 2221if (context -> isCollectingRequirements ()) 2222 { 2223return TestResult ::Pass ; 2224 } 2225auto connection = context -> m_languageServerConnection .Ptr (); 2226LanguageServerProtocol ::InitializeParams initParams ; 2227LanguageServerProtocol ::WorkspaceFolder wsFolder ; 2228wsFolder .name = "test" ; 2229String fullPath ; 2230Path ::getCanonical (input .filePath ,fullPath ); 2231wsFolder .uri = URI ::fromLocalFilePath (Path ::getParentDirectory (fullPath ).getUnownedSlice ()).uri ; 2232initParams .workspaceFolders .add (wsFolder ); 2233if (SLANG_FAILED (connection -> sendCall ( 2234LanguageServerProtocol ::InitializeParams ::methodName , 2235& initParams , 2236JSONValue ::makeInt (0 )))) 2237 { 2238return TestResult ::Fail ; 2239 } 2240if (SLANG_FAILED (connection -> waitForResult (-1 ))) 2241 { 2242return TestResult ::Fail ; 2243 } 2244 2245LanguageServerProtocol ::InitializeResult initResult ; 2246if (SLANG_FAILED (connection -> getMessage (& initResult ))) 2247 { 2248return TestResult ::Fail ; 2249 } 2250 2251// Send open document call. 2252String testFileContent ; 2253 2254if (SLANG_FAILED (File ::readAllText (input .filePath ,testFileContent ))) 2255 { 2256return TestResult ::Fail ; 2257 } 2258 2259LanguageServerProtocol ::DidOpenTextDocumentParams openDocParams ; 2260openDocParams .textDocument .version = 0 ; 2261openDocParams .textDocument .uri = URI ::fromLocalFilePath (fullPath .getUnownedSlice ()).uri ; 2262openDocParams .textDocument .text = testFileContent ; 2263connection -> sendCall ( 2264LanguageServerProtocol ::DidOpenTextDocumentParams ::methodName , 2265& openDocParams , 2266JSONValue ::makeInt (1 )); 2267List < LanguageServerProtocol ::PublishDiagnosticsParams > diagnostics ; 2268bool diagnosticsReceived = false; 2269auto waitForNonDiagnosticResponse = [& ]()-> SlangResult 2270 { 2271repeat : 2272if (SLANG_FAILED (connection -> waitForResult (-1 ))) 2273return SLANG_FAIL ; 2274if (connection -> getMessageType ()== JSONRPCMessageType ::Call ) 2275 { 2276JSONRPCCall call ; 2277connection -> getRPC (& call ); 2278if (call .method == "textDocument/publishDiagnostics" ) 2279 { 2280diagnosticsReceived = true; 2281LanguageServerProtocol ::PublishDiagnosticsParams arg ; 2282if (SLANG_FAILED (connection -> getMessage (& arg ))) 2283return SLANG_FAIL ; 2284diagnostics .add (arg ); 2285 gotorepeat ; 2286 } 2287 } 2288return SLANG_OK ; 2289 }; 2290 2291List < UnownedStringSlice > lines ; 2292StringUtil ::calcLines (testFileContent .getUnownedSlice (),lines ); 2293 2294StringBuilder actualOutputSB ; 2295auto parseLocation = [& ](UnownedStringSlice text ,Index startPos ,Int & linePos ,Int & colPos ) 2296 { 2297linePos = StringUtil ::parseIntAndAdvancePos (text .trimStart (),startPos ); 2298startPos ++ ; 2299colPos = StringUtil ::parseIntAndAdvancePos (text .trimStart (),startPos ); 2300return startPos ; 2301 }; 2302int callId = 2 ; 2303for (auto line :lines ) 2304 { 2305line = line .trimStart (); 2306if (!line .startsWith ("//" )) 2307continue ; 2308line = line .tail (2 ).trimStart (); 2309if (line .startsWith ("COMPLETE:" )) 2310 { 2311auto arg = line .tail (UnownedStringSlice ("COMPLETE:" ).getLength ()); 2312Int linePos ,colPos ; 2313parseLocation (arg ,0 ,linePos ,colPos ); 2314 2315LanguageServerProtocol ::CompletionParams params ; 2316params .position .line = int (linePos - 1 ); 2317params .position .character = int (colPos - 1 ); 2318params .textDocument .uri = openDocParams .textDocument .uri ; 2319if (SLANG_FAILED (connection -> sendCall ( 2320LanguageServerProtocol ::CompletionParams ::methodName , 2321& params , 2322JSONValue ::makeInt (callId ++ )))) 2323 { 2324return TestResult ::Fail ; 2325 } 2326if (SLANG_FAILED (waitForNonDiagnosticResponse ())) 2327return TestResult ::Fail ; 2328actualOutputSB <<"--------\n" ; 2329LanguageServerProtocol ::NullResponse nullResponse ; 2330List < LanguageServerProtocol ::CompletionItem > completionItems ; 2331if (SLANG_SUCCEEDED (connection -> getMessage (& nullResponse ))) 2332 { 2333actualOutputSB <<"null\n" ; 2334 } 2335else if (SLANG_SUCCEEDED (connection -> getMessage (& completionItems ))) 2336 { 2337for (auto item :completionItems ) 2338 { 2339actualOutputSB <<item .label <<": " <<item .kind <<" " <<item .detail <<" " ; 2340for (auto ch :item .commitCharacters ) 2341actualOutputSB <<ch ; 2342if (item .sortText .hasValue ) 2343actualOutputSB <<" sort(" <<item .sortText .value <<")" ; 2344actualOutputSB <<"\n" ; 2345 } 2346 } 2347 } 2348else if (line .startsWith ("SIGNATURE:" )) 2349 { 2350auto arg = line .tail (UnownedStringSlice ("SIGNATURE:" ).getLength ()); 2351Int linePos ,colPos ; 2352parseLocation (arg ,0 ,linePos ,colPos ); 2353 2354LanguageServerProtocol ::SignatureHelpParams params ; 2355params .position .line = int (linePos - 1 ); 2356params .position .character = int (colPos - 1 ); 2357params .textDocument .uri = openDocParams .textDocument .uri ; 2358if (SLANG_FAILED (connection -> sendCall ( 2359LanguageServerProtocol ::SignatureHelpParams ::methodName , 2360& params , 2361JSONValue ::makeInt (callId ++ )))) 2362 { 2363return TestResult ::Fail ; 2364 } 2365if (SLANG_FAILED (waitForNonDiagnosticResponse ())) 2366return TestResult ::Fail ; 2367actualOutputSB <<"--------\n" ; 2368LanguageServerProtocol ::NullResponse nullResponse ; 2369LanguageServerProtocol ::SignatureHelp sigInfo ; 2370if (SLANG_SUCCEEDED (connection -> getMessage (& nullResponse ))) 2371 { 2372actualOutputSB <<"null\n" ; 2373 } 2374else if (SLANG_SUCCEEDED (connection -> getMessage (& sigInfo ))) 2375 { 2376actualOutputSB <<"activeParameter: " <<sigInfo .activeParameter <<"\n" ; 2377actualOutputSB <<"activeSignature: " <<sigInfo .activeSignature <<"\n" ; 2378for (Index i = 0 ;i < sigInfo .signatures .getCount ();++ i ) 2379 { 2380auto & item = sigInfo .signatures [i ]; 2381if (i == sigInfo .activeSignature ) 2382 { 2383actualOutputSB <<"(selected) " ; 2384 } 2385actualOutputSB <<item .label <<":" ; 2386for (auto param :item .parameters ) 2387 { 2388actualOutputSB <<" (" <<param .label [0 ] <<"," <<param .label [1 ] <<")" ; 2389 } 2390actualOutputSB <<"\n" ; 2391actualOutputSB <<item .documentation .value <<"\n" ; 2392 } 2393 } 2394 } 2395else if (line .startsWith ("HOVER:" )) 2396 { 2397auto arg = line .tail (UnownedStringSlice ("HOVER:" ).getLength ()); 2398Int linePos ,colPos ; 2399parseLocation (arg ,0 ,linePos ,colPos ); 2400 2401LanguageServerProtocol ::HoverParams params ; 2402params .position .line = int (linePos - 1 ); 2403params .position .character = int (colPos - 1 ); 2404params .textDocument .uri = openDocParams .textDocument .uri ; 2405if (SLANG_FAILED (connection -> sendCall ( 2406LanguageServerProtocol ::HoverParams ::methodName , 2407& params , 2408JSONValue ::makeInt (callId ++ )))) 2409 { 2410return TestResult ::Fail ; 2411 } 2412if (SLANG_FAILED (waitForNonDiagnosticResponse ())) 2413return TestResult ::Fail ; 2414actualOutputSB <<"--------\n" ; 2415LanguageServerProtocol ::NullResponse nullResponse ; 2416LanguageServerProtocol ::Hover hover ; 2417if (SLANG_SUCCEEDED (connection -> getMessage (& nullResponse ))) 2418 { 2419actualOutputSB <<"null\n" ; 2420 } 2421else if (SLANG_SUCCEEDED (connection -> getMessage (& hover ))) 2422 { 2423actualOutputSB <<"range: " <<hover .range .start .line <<"," 2424 <<hover .range .start .character <<" - " <<hover .range .end .line 2425 <<"," <<hover .range .end .character ; 2426actualOutputSB <<"\ncontent:\n" <<hover .contents .value <<"\n" ; 2427 } 2428 } 2429else if (line .startsWith ("DIAGNOSTICS" )) 2430 { 2431if (!diagnosticsReceived ) 2432 { 2433waitForNonDiagnosticResponse (); 2434 } 2435actualOutputSB <<"--------\n" ; 2436for (auto item :diagnostics ) 2437 { 2438actualOutputSB <<item .uri <<"\n" ; 2439for (auto msg :item .diagnostics ) 2440 { 2441actualOutputSB <<msg .range .start .line <<"," <<msg .range .start .character 2442 <<"-" <<msg .range .end .line <<"," <<msg .range .end .character 2443 <<" " <<msg .message ; 2444 } 2445 } 2446 } 2447 } 2448LanguageServerProtocol ::DidCloseTextDocumentParams closeDocParams ; 2449closeDocParams .textDocument .uri = URI ::fromLocalFilePath (fullPath .getUnownedSlice ()).uri ; 2450connection -> sendCall ( 2451LanguageServerProtocol ::DidCloseTextDocumentParams ::methodName , 2452& closeDocParams , 2453JSONValue ::makeInt (1 )); 2454 2455auto outputStem = input .outputStem ; 2456String expectedOutputPath = outputStem + ".expected.txt" ; 2457String expectedOutput ; 2458 2459Slang ::File ::readAllText (expectedOutputPath ,expectedOutput ); 2460expectedOutput = expectedOutput .trim (); 2461 2462TestResult result = TestResult ::Pass ; 2463 2464auto actualOutput = actualOutputSB .produceString (); 2465 2466// Redact absolute file names from actualOutput 2467List < UnownedStringSlice > outputLines ; 2468StringUtil ::calcLines (actualOutput .getUnownedSlice (),outputLines ); 2469StringBuilder redactedSB ; 2470for (auto line :outputLines ) 2471 { 2472Index extIdx = line .indexOf (UnownedStringSlice (".slang" )); 2473if (extIdx == -1 ) 2474 { 2475redactedSB <<line <<"\n" ; 2476continue ; 2477 } 2478redactedSB <<"{REDACTED}" <<line .tail (extIdx ) <<"\n" ; 2479 } 2480 2481actualOutput = redactedSB .produceString ().trim (); 2482 2483String fileCheckPrefix ; 2484const bool isFileCheckTest = input .testOptions -> getFileCheckPrefix (fileCheckPrefix ); 2485if (isFileCheckTest ) 2486 { 2487result = _fileCheckTest (* context ,input .filePath ,fileCheckPrefix ,actualOutput ); 2488 } 2489else 2490 { 2491if (!_areResultsEqual (input .testOptions -> type ,expectedOutput ,actualOutput )) 2492 { 2493if (expectedOutput .startsWith ("CONTAINS" )) 2494 { 2495List < UnownedStringSlice > words ; 2496List < UnownedStringSlice > expectedLines ; 2497StringUtil ::calcLines (expectedOutput .getUnownedSlice (),expectedLines ); 2498if (expectedLines .getCount () >=1 ) 2499 { 2500StringUtil ::split (expectedLines [0 ],' ' ,words ); 2501if (words .getCount () >=2 ) 2502 { 2503if (actualOutput .contains (words [1 ].trim ())) 2504 { 2505return result ; 2506 } 2507 } 2508 } 2509 } 2510context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 2511result = TestResult ::Fail ; 2512 } 2513 } 2514 2515// If the test failed, then we write the actual output to a file 2516// so that we can easily diff it from the command line and 2517// diagnose the problem. 2518if (result == TestResult ::Fail ) 2519 { 2520String actualOutputPath = outputStem + ".actual" ; 2521Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 2522 2523context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 2524 } 2525return result ; 2526} 2527 2528TestResult runSimpleTest (TestContext * context ,TestInput & input ) 2529{ 2530// need to execute the stand-alone Slang compiler on the file, and compare its output to what we 2531// expect 2532auto outputStem = input .outputStem ; 2533 2534CommandLine cmdLine ; 2535 2536if (input .testOptions -> command != "SIMPLE_EX" ) 2537 { 2538cmdLine .addArg (input .filePath ); 2539 } 2540 2541for (auto arg :input .testOptions -> args ) 2542 { 2543// Filter out slang-test specific options that shouldn't be passed to slangc 2544if (arg == kPreserveEmbeddedSourceOption ) 2545continue ; 2546cmdLine .addArg (arg ); 2547 } 2548 2549// If we can't set up for simple compilation, it's because some external resource isn't 2550// available such as NVAPI headers. In that case we just ignore the test. 2551if (SLANG_FAILED (_initSlangCompiler (context ,cmdLine ))) 2552 { 2553return TestResult ::Ignored ; 2554 } 2555 2556ExecuteResult exeRes ; 2557TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 2558 2559if (context -> isCollectingRequirements ()) 2560 { 2561return TestResult ::Pass ; 2562 } 2563 2564// See what kind of target it is 2565SlangCompileTarget target = SLANG_TARGET_UNKNOWN ; 2566 { 2567const auto & args = input .testOptions -> args ; 2568const Index targetIndex = args .indexOf ("-target" ); 2569if (targetIndex != Index (-1 )&& targetIndex + 1 < args .getCount ()) 2570 { 2571target = 2572TypeTextUtil ::findCompileTargetFromName (args [targetIndex + 1 ].getUnownedSlice ()); 2573 } 2574 } 2575 2576// If it's executable we run it and use it's output 2577if (target == SLANG_HOST_EXECUTABLE ) 2578 { 2579ExecuteResult runExeRes ; 2580if (SLANG_FAILED (_executeBinary (exeRes .standardOutput .getUnownedSlice (),runExeRes ))) 2581 { 2582return TestResult ::Fail ; 2583 } 2584exeRes = runExeRes ; 2585 } 2586 2587bool needToRemoveEmbeddedSource = 2588 ((target == SLANG_SPIRV || target == SLANG_SPIRV_ASM )&& 2589input .testOptions -> args .indexOf (kPreserveEmbeddedSourceOption )== Index (-1 )); 2590 2591String actualOutput = getOutput (exeRes ,needToRemoveEmbeddedSource ); 2592 2593return _validateOutput ( 2594context , 2595input , 2596actualOutput , 2597 false, 2598"result code = 0\nstandard error = {\n}\nstandard output = {\n}\n" , 2599 [& input ](auto e ,auto a ) {return _areResultsEqual (input .testOptions -> type ,e ,a ); }); 2600} 2601 2602TestResult runSimpleLineTest (TestContext * context ,TestInput & input ) 2603{ 2604// need to execute the stand-alone Slang compiler on the file, and compare its output to what we 2605// expect 2606auto outputStem = input .outputStem ; 2607 2608CommandLine cmdLine ; 2609_initSlangCompiler (context ,cmdLine ); 2610 2611cmdLine .addArg (input .filePath ); 2612 2613for (auto arg :input .testOptions -> args ) 2614 { 2615cmdLine .addArg (arg ); 2616 } 2617 2618ExecuteResult exeRes ; 2619TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 2620 2621if (context -> isCollectingRequirements ()) 2622 { 2623return TestResult ::Pass ; 2624 } 2625 2626// Parse all the diagnostics so we can extract line numbers 2627auto diagnostics = ArtifactDiagnostics ::create (); 2628if (SLANG_FAILED (ParseDiagnosticUtil ::parseDiagnostics ( 2629exeRes .standardError .getUnownedSlice (), 2630diagnostics ))|| 2631diagnostics -> getCount () <=0 ) 2632 { 2633// Write out the diagnostics which couldn't be parsed. 2634 2635String actualOutputPath = outputStem + ".actual" ; 2636Slang ::File ::writeAllText (actualOutputPath ,exeRes .standardError ); 2637 2638return TestResult ::Fail ; 2639 } 2640 2641StringBuilder actualOutput ; 2642 2643if (diagnostics -> getCount ()> 0 ) 2644 { 2645actualOutput <<diagnostics -> getAt (0 )-> location .line <<"\n" ; 2646 } 2647else 2648 { 2649actualOutput <<"No output diagnostics\n" ; 2650 } 2651 2652return _validateOutput (context ,input ,actualOutput , false); 2653} 2654 2655TestResult runInterpreterTest (TestContext * context ,TestInput & input ) 2656{ 2657// need to execute the stand-alone Slang compiler on the file, and compare its output to what we 2658// expect 2659auto outputStem = input .outputStem ; 2660 2661CommandLine cmdLine ; 2662 2663List < String > args ; 2664 2665for (Index i = 0 ;i < input .testOptions -> args .getCount ();i ++ ) 2666 { 2667auto & arg = input .testOptions -> args [i ]; 2668if (arg == "-disasm" ) 2669cmdLine .addArg (arg ); 2670else if (arg == "-entry" ) 2671 { 2672cmdLine .addArg (arg ); 2673i ++ ; 2674if (i < input .testOptions -> args .getCount ()) 2675 { 2676cmdLine .addArg (input .testOptions -> args [i ]); 2677 } 2678 } 2679else 2680 { 2681args .add (arg ); 2682 } 2683 } 2684 2685cmdLine .addArg (input .filePath ); 2686 2687for (auto arg :args ) 2688 { 2689cmdLine .addArg (arg ); 2690 } 2691 2692if (SLANG_FAILED (_initSlangInterpreter (context ,cmdLine ))) 2693 { 2694return TestResult ::Ignored ; 2695 } 2696 2697ExecuteResult exeRes ; 2698TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 2699 2700if (context -> isCollectingRequirements ()) 2701 { 2702return TestResult ::Pass ; 2703 } 2704 2705String actualOutput = getOutput (exeRes ); 2706 2707return _validateOutput ( 2708context , 2709input , 2710actualOutput , 2711 false, 2712"result code = 0\nstandard error = {\n}\nstandard output = {\n}\n" , 2713 [& input ](auto e ,auto a ) {return _areResultsEqual (input .testOptions -> type ,e ,a ); }); 2714} 2715 2716TestResult runCompile (TestContext * context ,TestInput & input ) 2717{ 2718auto outputStem = input .outputStem ; 2719 2720CommandLine cmdLine ; 2721_initSlangCompiler (context ,cmdLine ); 2722 2723StringEscapeHandler * escapeHandler = 2724StringEscapeUtil ::getHandler (StringEscapeUtil ::Style ::Space ); 2725 2726for (auto arg :input .testOptions -> args ) 2727 { 2728// If unescaping is needed, do it 2729if (StringEscapeUtil ::isUnescapeShellLikeNeeded (escapeHandler ,arg .getUnownedSlice ())) 2730 { 2731StringBuilder buf ; 2732StringEscapeUtil ::unescapeShellLike (escapeHandler ,arg .getUnownedSlice (),buf ); 2733cmdLine .addArg (buf .produceString ()); 2734 } 2735else 2736 { 2737cmdLine .addArg (arg ); 2738 } 2739 } 2740 2741ExecuteResult exeRes ; 2742TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 2743if (context -> isCollectingRequirements ()) 2744 { 2745return TestResult ::Pass ; 2746 } 2747 2748if (exeRes .resultCode != 0 ) 2749 { 2750auto reporter = context -> getTestReporter (); 2751if (reporter ) 2752 { 2753auto output = getOutput (exeRes ); 2754reporter -> message (TestMessageType ::TestFailure ,output ); 2755 } 2756 2757return TestResult ::Fail ; 2758 } 2759 2760return TestResult ::Pass ; 2761} 2762 2763TestResult runSimpleCompareCommandLineTest (TestContext * context ,TestInput & input ) 2764{ 2765TestInput workInput (input ); 2766// Use the original files input to compare with 2767workInput .outputStem = input .filePath ; 2768// Force to using exes 2769workInput .spawnType = SpawnType ::UseExe ; 2770 2771return runSimpleTest (context ,workInput ); 2772} 2773 2774static SlangResult _parseJSON ( 2775const UnownedStringSlice & slice , 2776DiagnosticSink * sink , 2777JSONContainer * container , 2778JSONValue & outValue ) 2779{ 2780SourceManager * sourceManager = sink -> getSourceManager (); 2781 2782SourceFile * sourceFile = 2783sourceManager -> createSourceFileWithString (PathInfo ::makeUnknown (),slice ); 2784SourceView * sourceView = sourceManager -> createSourceView (sourceFile ,nullptr ,SourceLoc ()); 2785 2786JSONLexer lexer ; 2787lexer .init (sourceView ,sink ); 2788 2789JSONBuilder builder (container ); 2790 2791JSONParser parser ; 2792SLANG_RETURN_ON_FAIL (parser .parse (& lexer ,sourceView ,& builder ,sink )); 2793 2794outValue = builder .getRootValue (); 2795return SLANG_OK ; 2796} 2797 2798TestResult runReflectionTest (TestContext * context ,TestInput & input ) 2799{ 2800const auto & options = context -> options ; 2801const auto & filePath = input .filePath ; 2802auto & outputStem = input .outputStem ; 2803 2804bool isCPUTest = input .testOptions -> command .startsWith ("CPU_" ); 2805 2806CommandLine cmdLine ; 2807 2808cmdLine .setExecutableLocation (ExecutableLocation (options .binDir ,"slang-reflection-test" )); 2809cmdLine .addArg (filePath ); 2810 2811for (auto arg :input .testOptions -> args ) 2812 { 2813cmdLine .addArg (arg ); 2814 } 2815 2816ExecuteResult exeRes ; 2817TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 2818 2819if (context -> isCollectingRequirements ()) 2820 { 2821return TestResult ::Pass ; 2822 } 2823 2824String actualOutput = getOutput (exeRes ); 2825 2826if (isCPUTest ) 2827 { 2828#if SLANG_PTR_IS_32 2829outputStem .append (".32" ); 2830#else 2831outputStem .append (".64" ); 2832#endif 2833 } 2834 2835// Extrac the stand 2836ParseDiagnosticUtil ::OutputInfo outputInfo ; 2837if (SLANG_SUCCEEDED ( 2838ParseDiagnosticUtil ::parseOutputInfo (actualOutput .getUnownedSlice (),outputInfo ))) 2839 { 2840const auto toolReturnCode = ToolReturnCode (outputInfo .resultCode ); 2841 2842// The output should be JSON. 2843// Parse it to check that it is valid json 2844if (toolReturnCode == ToolReturnCode ::Success ) 2845 { 2846SourceManager sourceManager ; 2847sourceManager .initialize (nullptr ,nullptr ); 2848 2849JSONContainer container (& sourceManager ); 2850 2851DiagnosticSink sink ; 2852sink .init (& sourceManager ,nullptr ); 2853 2854JSONValue value ; 2855if (SLANG_FAILED ( 2856_parseJSON (outputInfo .stdOut .getUnownedSlice (),& sink ,& container ,value ))) 2857 { 2858// Unable to parse as JSON 2859 2860context -> getTestReporter ()-> messageFormat ( 2861TestMessageType ::RunError , 2862"Unable to parse reflection JSON '%s'\n" , 2863input .outputStem .getBuffer ()); 2864 2865String actualOutputPath = input .outputStem + ".actual" ; 2866Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 2867return TestResult ::Fail ; 2868 } 2869 } 2870 } 2871 2872return _validateOutput (context ,input ,actualOutput ); 2873} 2874 2875static String _calcSummary (IArtifactDiagnostics * inDiagnostics ) 2876{ 2877auto diagnostics = cloneInterface (inDiagnostics ); 2878 2879// We only want to analyze errors for now 2880diagnostics -> removeBySeverity (ArtifactDiagnostic ::Severity ::Info ); 2881diagnostics -> removeBySeverity (ArtifactDiagnostic ::Severity ::Warning ); 2882 2883ComPtr < ISlangBlob > summary ; 2884diagnostics -> calcSimplifiedSummary (summary .writeRef ()); 2885 2886return StringUtil ::getString (summary ); 2887} 2888 2889static TestResult runCPPCompilerCompile (TestContext * context ,TestInput & input ) 2890{ 2891IDownstreamCompiler * compiler = context -> getDefaultCompiler (SLANG_SOURCE_LANGUAGE_CPP ); 2892if (!compiler ) 2893 { 2894return TestResult ::Ignored ; 2895 } 2896 2897// need to execute the stand-alone Slang compiler on the file, and compare its output to what we 2898// expect 2899 2900auto outputStem = input .outputStem ; 2901 2902CommandLine cmdLine ; 2903_initSlangCompiler (context ,cmdLine ); 2904 2905cmdLine .addArg (input .filePath ); 2906for (auto arg :input .testOptions -> args ) 2907 { 2908cmdLine .addArg (arg ); 2909 } 2910 2911ExecuteResult exeRes ; 2912TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 2913if (context -> isCollectingRequirements ()) 2914 { 2915return TestResult ::Pass ; 2916 } 2917 2918// Dump out what happened 2919 { 2920String actualOutputPath = outputStem + ".actual" ; 2921Slang ::File ::writeAllText (actualOutputPath ,getOutput (exeRes )); 2922 } 2923 2924if (exeRes .resultCode != 0 ) 2925 { 2926return TestResult ::Fail ; 2927 } 2928 2929return TestResult ::Pass ; 2930} 2931 2932static TestResult runCPPCompilerSharedLibrary (TestContext * context ,TestInput & input ) 2933{ 2934IDownstreamCompiler * compiler = context -> getDefaultCompiler (SLANG_SOURCE_LANGUAGE_CPP ); 2935if (!compiler ) 2936 { 2937 std::lock_guard < std::mutex > lock (context -> mutex ); 2938return TestResult ::Ignored ; 2939 } 2940 2941// If we are just collecting requirements, say it passed 2942if (context -> isCollectingRequirements ()) 2943 { 2944context -> getTestRequirements ()-> addUsedBackEnd (SLANG_PASS_THROUGH_GENERIC_C_CPP ); 2945return TestResult ::Pass ; 2946 } 2947 2948auto outputStem = input .outputStem ; 2949auto filePath = input .filePath ; 2950 2951String actualOutputPath = outputStem + ".actual" ; 2952File ::remove (actualOutputPath ); 2953 2954// Make the module name the same as the source file 2955String modulePath = _calcModulePath (input ); 2956String ext = Path ::getPathExt (filePath ); 2957 2958// Remove the binary.. 2959String sharedLibraryPath = SharedLibrary ::calcPlatformPath (modulePath .getUnownedSlice ()); 2960File ::remove (sharedLibraryPath ); 2961 2962// Set up the compilation options 2963DownstreamCompileOptions options ; 2964 2965options .sourceLanguage = (ext == "c" ) ?SLANG_SOURCE_LANGUAGE_C :SLANG_SOURCE_LANGUAGE_CPP ; 2966 2967// Build a shared library 2968options .targetType = SLANG_SHADER_SHARED_LIBRARY ; 2969 2970auto helper = DefaultArtifactHelper ::getSingleton (); 2971 2972// Compile this source 2973ComPtr < IArtifact > sourceArtifact ; 2974 2975// If set, we store the artifact in memory without a name. 2976bool checkMemory = false; 2977if (checkMemory ) 2978 { 2979helper -> createArtifact ( 2980ArtifactDescUtil ::makeDescForSourceLanguage (options .sourceLanguage ), 2981"" , 2982sourceArtifact .writeRef ()); 2983 2984ComPtr < IOSFileArtifactRepresentation > fileRep ; 2985// Let's just add a blob with the contents 2986helper -> createOSFileArtifactRepresentation ( 2987IOSFileArtifactRepresentation ::Kind ::Reference , 2988asCharSlice (filePath .getUnownedSlice ()), 2989nullptr , 2990fileRep .writeRef ()); 2991 2992ComPtr < ICastable > castable ; 2993fileRep -> createRepresentation (ISlangBlob ::getTypeGuid (),castable .writeRef ()); 2994 2995sourceArtifact -> addRepresentation (castable ); 2996 } 2997else 2998 { 2999helper -> createOSFileArtifact ( 3000ArtifactDescUtil ::makeDescForSourceLanguage (options .sourceLanguage ), 3001asCharSlice (filePath .getUnownedSlice ()), 3002sourceArtifact .writeRef ()); 3003 } 3004 3005TerminatedCharSlice includePaths []= {TerminatedCharSlice ("." )}; 3006 3007options .sourceArtifacts = makeSlice (sourceArtifact .readRef (),1 ); 3008options .includePaths = makeSlice (includePaths ,SLANG_COUNT_OF (includePaths )); 3009options .modulePath = SliceUtil ::asTerminatedCharSlice (modulePath ); 3010 3011ComPtr < IArtifact > artifact ; 3012if (SLANG_FAILED (compiler -> compile (options ,artifact .writeRef ()))) 3013 { 3014return TestResult ::Fail ; 3015 } 3016 3017auto diagnostics = findAssociatedRepresentation < IArtifactDiagnostics > (artifact ); 3018 3019if (diagnostics && SLANG_FAILED (diagnostics -> getResult ())) 3020 { 3021// Compilation failed 3022String actualOutput = _calcSummary (diagnostics ); 3023 3024// Write the output 3025Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 3026 3027// Check that they are the same 3028 { 3029// Read the expected 3030String expectedOutput ; 3031 3032String expectedOutputPath = outputStem + ".expected" ; 3033Slang ::File ::readAllText (expectedOutputPath ,expectedOutput ); 3034 3035// Compare if they are the same 3036if (!StringUtil ::areLinesEqual ( 3037actualOutput .getUnownedSlice (), 3038expectedOutput .getUnownedSlice ())) 3039 { 3040context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 3041return TestResult ::Fail ; 3042 } 3043 } 3044 } 3045else 3046 { 3047SharedLibrary ::Handle handle ; 3048if (SLANG_FAILED ( 3049SharedLibrary ::loadWithPlatformPath (sharedLibraryPath .getBuffer (),handle ))) 3050 { 3051return TestResult ::Fail ; 3052 } 3053 3054const int inValue = 10 ; 3055const char inBuffer []= "Hello World!" ; 3056 3057char buffer [128 ]= "" ; 3058int value = 0 ; 3059 3060typedef int (* TestFunc )(int intValue ,const char * textValue ,char * outTextValue ); 3061 3062// We could capture output if we passed in a ISlangWriter - but for that to work we'd need a 3063TestFunc testFunc = (TestFunc )SharedLibrary ::findSymbolAddressByName (handle ,"test" ); 3064if (testFunc ) 3065 { 3066value = testFunc (inValue ,inBuffer ,buffer ); 3067 } 3068else 3069 { 3070printf ("Unable to access 'test' function\n" ); 3071 } 3072 3073SharedLibrary ::unload (handle ); 3074 3075if (!(inValue == value && strcmp (inBuffer ,buffer )== 0 )) 3076 { 3077return TestResult ::Fail ; 3078 } 3079 } 3080 3081return TestResult ::Pass ; 3082} 3083 3084static TestResult runCPPCompilerExecute (TestContext * context ,TestInput & input ) 3085{ 3086IDownstreamCompiler * compiler = context -> getDefaultCompiler (SLANG_SOURCE_LANGUAGE_CPP ); 3087if (!compiler ) 3088 { 3089return TestResult ::Ignored ; 3090 } 3091 3092// If we are just collecting requirements, say it passed 3093if (context -> isCollectingRequirements ()) 3094 { 3095 std::lock_guard < std::mutex > lock (context -> mutex ); 3096context -> getTestRequirements ()-> addUsedBackEnd (SLANG_PASS_THROUGH_GENERIC_C_CPP ); 3097return TestResult ::Pass ; 3098 } 3099 3100auto filePath = input .filePath ; 3101auto outputStem = input .outputStem ; 3102 3103String actualOutputPath = outputStem + ".actual" ; 3104File ::remove (actualOutputPath ); 3105 3106// Make the module name the same as the source file 3107String ext = Path ::getPathExt (filePath ); 3108String modulePath = _calcModulePath (input ); 3109 3110// Remove the binary.. 3111String moduleExePath ; 3112 { 3113StringBuilder buf ; 3114buf <<modulePath ; 3115buf <<Process ::getExecutableSuffix (); 3116moduleExePath = buf ; 3117 } 3118 3119// Remove the exe if it exists 3120File ::remove (moduleExePath ); 3121 3122// Set up the compilation options 3123DownstreamCompileOptions options ; 3124 3125options .sourceLanguage = (ext == "c" ) ?SLANG_SOURCE_LANGUAGE_C :SLANG_SOURCE_LANGUAGE_CPP ; 3126 3127TerminatedCharSlice filePaths []= {SliceUtil ::asTerminatedCharSlice (filePath )}; 3128 3129auto helper = DefaultArtifactHelper ::getSingleton (); 3130 3131ComPtr < IArtifact > sourceArtifact ; 3132helper -> createOSFileArtifact ( 3133ArtifactDescUtil ::makeDescForSourceLanguage (options .sourceLanguage ), 3134asCharSlice (filePath .getUnownedSlice ()), 3135sourceArtifact .writeRef ()); 3136 3137// Compile this source 3138options .sourceArtifacts = makeSlice (sourceArtifact .readRef (),1 ); 3139options .modulePath = SliceUtil ::asTerminatedCharSlice (modulePath ); 3140 3141ComPtr < IArtifact > artifact ; 3142if (SLANG_FAILED (compiler -> compile (options ,artifact .writeRef ()))) 3143 { 3144return TestResult ::Fail ; 3145 } 3146 3147String actualOutput ; 3148 3149auto diagnostics = findAssociatedRepresentation < IArtifactDiagnostics > (artifact ); 3150 3151// If the actual compilation failed, then the output will be the summary 3152if (diagnostics && SLANG_FAILED (diagnostics -> getResult ())) 3153 { 3154actualOutput = _calcSummary (diagnostics ); 3155 } 3156else 3157 { 3158// Execute the binary and see what we get 3159CommandLine cmdLine ; 3160 3161ExecutableLocation exe ; 3162exe .setPath (moduleExePath ); 3163 3164cmdLine .setExecutableLocation (exe ); 3165 3166ExecuteResult exeRes ; 3167if (SLANG_FAILED (ProcessUtil ::execute (cmdLine ,exeRes ))) 3168 { 3169return TestResult ::Fail ; 3170 } 3171 3172// Write the output, and compare to expected 3173actualOutput = getOutput (exeRes ); 3174 } 3175 3176// Write the output 3177Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 3178 3179// Check that they are the same 3180 { 3181// Read the expected 3182String expectedOutput ; 3183 3184String expectedOutputPath = outputStem + ".expected" ; 3185Slang ::File ::readAllText (expectedOutputPath ,expectedOutput ); 3186 3187// Compare if they are the same 3188if (!StringUtil ::areLinesEqual ( 3189actualOutput .getUnownedSlice (), 3190expectedOutput .getUnownedSlice ())) 3191 { 3192context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 3193return TestResult ::Fail ; 3194 } 3195 } 3196 3197return TestResult ::Pass ; 3198} 3199 3200// Returns TestResult::Ignored if we don't have the capability to run the passthrough compiler 3201// Returns TestResult::Fail if we can't write the expected output debug file 3202// Otherwise return TestResult::Pass and if we are not just collecting 3203// requirements, writes the output into the `expectedOutput` parameter 3204static TestResult generateExpectedOutput ( 3205TestContext * const context , 3206const TestInput & input , 3207String & expectedOutput ) 3208{ 3209auto filePath = input .filePath ; 3210auto outputStem = input .outputStem ; 3211 3212CommandLine expectedCmdLine ; 3213 3214_initSlangCompiler (context ,expectedCmdLine ); 3215 3216const auto & args = input .testOptions -> args ; 3217 3218const Index targetIndex = args .indexOf ("-target" ); 3219if (targetIndex != Index (-1 )&& targetIndex + 1 < args .getCount ()) 3220 { 3221const SlangCompileTarget target = 3222TypeTextUtil ::findCompileTargetFromName (args [targetIndex + 1 ].getUnownedSlice ()); 3223 3224// Check the session supports it. If not we ignore it 3225if (SLANG_FAILED (context -> getSession ()-> checkCompileTargetSupport (target ))) 3226 { 3227return TestResult ::Ignored ; 3228 } 3229 3230switch (target ) 3231 { 3232case SLANG_DXIL : 3233case SLANG_DXIL_ASM : 3234 { 3235expectedCmdLine .addArg (filePath + ".hlsl" ); 3236expectedCmdLine .addArg ("-pass-through" ); 3237expectedCmdLine .addArg ("dxc" ); 3238break ; 3239 } 3240case SLANG_DXBC : 3241case SLANG_DXBC_ASM : 3242 { 3243expectedCmdLine .addArg (filePath + ".hlsl" ); 3244expectedCmdLine .addArg ("-pass-through" ); 3245expectedCmdLine .addArg ("fxc" ); 3246break ; 3247 } 3248default : 3249 { 3250expectedCmdLine .addArg (filePath + ".glsl" ); 3251expectedCmdLine .addArg ("-emit-spirv-via-glsl" ); 3252expectedCmdLine .addArg ("-pass-through" ); 3253expectedCmdLine .addArg ("glslang" ); 3254break ; 3255 } 3256 } 3257 } 3258 3259for (auto arg :args ) 3260 { 3261expectedCmdLine .addArg (arg ); 3262 } 3263 3264ExecuteResult expectedExeRes ; 3265TEST_RETURN_ON_DONE ( 3266spawnAndWait (context ,outputStem ,input .spawnType ,expectedCmdLine ,expectedExeRes )); 3267 3268if (context -> isCollectingRequirements ()) 3269 { 3270return TestResult ::Pass ; 3271 } 3272 3273expectedOutput = getOutput (expectedExeRes ); 3274String expectedOutputPath = outputStem + ".expected" ; 3275 3276if (SLANG_FAILED (Slang ::File ::writeAllText (expectedOutputPath ,expectedOutput ))) 3277 { 3278context -> getTestReporter ()-> messageFormat ( 3279TestMessageType ::TestFailure , 3280"Failed to write test expected output to %s" , 3281expectedOutputPath .getBuffer ()); 3282return TestResult ::Fail ; 3283 } 3284 3285return TestResult ::Pass ; 3286} 3287 3288// Returns TestResult::Fail if compilation fails 3289// Otherwise return TestResult::Pass and if we are not just collecting 3290// requirements, writes the output into the `expectedOutput` parameter 3291TestResult generateActualOutput ( 3292TestContext * const context , 3293const TestInput & input , 3294String & actualOutput ) 3295{ 3296auto filePath = input .filePath ; 3297 3298CommandLine actualCmdLine ; 3299_initSlangCompiler (context ,actualCmdLine ); 3300actualCmdLine .addArg (filePath ); 3301actualCmdLine .addArg ("-emit-spirv-via-glsl" ); 3302 3303const auto & args = input .testOptions -> args ; 3304 3305for (auto arg :input .testOptions -> args ) 3306 { 3307actualCmdLine .addArg (arg ); 3308 } 3309 3310ExecuteResult actualExeRes ; 3311TEST_RETURN_ON_DONE ( 3312spawnAndWait (context ,input .outputStem ,input .spawnType ,actualCmdLine ,actualExeRes )); 3313 3314// Early out if we're just collecting requirements 3315if (context -> isCollectingRequirements ()) 3316 { 3317return TestResult ::Pass ; 3318 } 3319 3320actualOutput = getOutput (actualExeRes ); 3321 3322// Always fail if the compilation produced a failure, just 3323// to catch situations where, e.g., command-line options parsing 3324// caused the same error in both the Slang and glslang cases. 3325// 3326if (actualExeRes .resultCode != 0 ) 3327 { 3328return TestResult ::Fail ; 3329 } 3330 3331return TestResult ::Pass ; 3332} 3333 3334TestResult runCrossCompilerTest (TestContext * context ,TestInput & input ) 3335{ 3336// Need to execute the stand-alone Slang compiler on the file 3337// then on the same file + `.glsl` and compare output 3338// 3339// Or, in the case of a filecheck test, instead of comparing against the 3340// +".glsl" version, we run some filecheck rules on it 3341 3342String fileCheckPrefix ; 3343const bool isFileCheckTest = input .testOptions -> getFileCheckPrefix (fileCheckPrefix ); 3344 3345String actualOutput ; 3346if (TestResult r = generateActualOutput (context ,input ,actualOutput );r != TestResult ::Pass ) 3347 { 3348return r ; 3349 } 3350 3351// Only generate the expected output if this is a comparison against some 3352// known-good glsl/hlsl input 3353String expectedOutput ; 3354if (!isFileCheckTest ) 3355 { 3356if (TestResult r = generateExpectedOutput (context ,input ,expectedOutput ); 3357r != TestResult ::Pass ) 3358 { 3359return r ; 3360 } 3361 } 3362 3363// Early out if we're just collecting requirements 3364if (context -> isCollectingRequirements ()) 3365 { 3366return TestResult ::Pass ; 3367 } 3368 3369TestResult result = TestResult ::Pass ; 3370 3371if (isFileCheckTest ) 3372 { 3373result = _fileCheckTest (* context ,input .filePath ,fileCheckPrefix ,actualOutput ); 3374// TODO: It might be a good idea to sanity check any expected output 3375// source files against the filecheck rules if they're applicable. 3376// 3377// Something like: 3378// fileCheckTest(context, prefix="HLSL", input, filePath + ".hlsl"); 3379 } 3380else 3381 { 3382if (!StringUtil ::areLinesEqual ( 3383actualOutput .getUnownedSlice (), 3384expectedOutput .getUnownedSlice ())) 3385 { 3386result = TestResult ::Fail ; 3387context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 3388 } 3389 } 3390 3391// If the test failed, then we write the actual output to a file 3392// so that we can easily inspect it from the command line and 3393// diagnose the problem. 3394if (result == TestResult ::Fail ) 3395 { 3396String actualOutputPath = input .outputStem + ".actual" ; 3397Slang ::File ::writeAllText (actualOutputPath ,actualOutput ); 3398 } 3399 3400return result ; 3401} 3402 3403TestResult generateHLSLBaseline ( 3404TestContext * context , 3405TestInput & input , 3406char const * targetFormat , 3407char const * passThroughName ) 3408{ 3409auto filePath999 = input .filePath ; 3410auto outputStem = input .outputStem ; 3411 3412CommandLine cmdLine ; 3413_initSlangCompiler (context ,cmdLine ); 3414 3415cmdLine .addArg (filePath999 ); 3416 3417for (auto arg :input .testOptions -> args ) 3418 { 3419cmdLine .addArg (arg ); 3420 } 3421 3422cmdLine .addArg ("-target" ); 3423cmdLine .addArg (targetFormat ); 3424cmdLine .addArg ("-pass-through" ); 3425cmdLine .addArg (passThroughName ); 3426 3427ExecuteResult exeRes ; 3428TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 3429 3430if (context -> isCollectingRequirements ()) 3431 { 3432return TestResult ::Pass ; 3433 } 3434 3435String expectedOutput = getOutput (exeRes ); 3436String expectedOutputPath = outputStem + ".expected" ; 3437 3438if (SLANG_FAILED (Slang ::File ::writeAllText (expectedOutputPath ,expectedOutput ))) 3439 { 3440return TestResult ::Fail ; 3441 } 3442 3443return TestResult ::Pass ; 3444} 3445 3446TestResult generateHLSLBaseline (TestContext * context ,TestInput & input ) 3447{ 3448return generateHLSLBaseline (context ,input ,"dxbc-assembly" ,"fxc" ); 3449} 3450 3451static TestResult _runHLSLComparisonTest ( 3452TestContext * context , 3453TestInput & input , 3454char const * targetFormat , 3455char const * passThroughName ) 3456{ 3457auto filePath999 = input .filePath ; 3458auto outputStem = input .outputStem ; 3459 3460// We will use the Microsoft compiler to generate out expected output here 3461String expectedOutputPath = outputStem + ".expected" ; 3462 3463// Generate the expected output using standard HLSL compiler 3464generateHLSLBaseline (context ,input ,targetFormat ,passThroughName ); 3465 3466// need to execute the stand-alone Slang compiler on the file, and compare its output to what we 3467// expect 3468 3469CommandLine cmdLine ; 3470_initSlangCompiler (context ,cmdLine ); 3471 3472cmdLine .addArg (filePath999 ); 3473 3474for (auto arg :input .testOptions -> args ) 3475 { 3476cmdLine .addArg (arg ); 3477 } 3478 3479// TODO: The compiler should probably define this automatically... 3480cmdLine .addArg ("-D" ); 3481cmdLine .addArg ("__SLANG__" ); 3482 3483cmdLine .addArg ("-target" ); 3484cmdLine .addArg (targetFormat ); 3485 3486ExecuteResult exeRes ; 3487TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 3488 3489if (context -> isCollectingRequirements ()) 3490 { 3491return TestResult ::Pass ; 3492 } 3493 3494// We ignore output to stdout, and only worry about what the compiler 3495// wrote to stderr. 3496 3497ExecuteResult ::ResultCode resultCode = exeRes .resultCode ; 3498 3499String standardOutput = exeRes .standardOutput ; 3500String standardError = exeRes .standardError ; 3501String debugLayer = exeRes .debugLayer ; 3502 3503// We construct a single output string that captures the results 3504StringBuilder actualOutputBuilder ; 3505actualOutputBuilder .append ("result code = " ); 3506actualOutputBuilder .append (resultCode ); 3507actualOutputBuilder .append ("\nstandard error = {\n" ); 3508actualOutputBuilder .append (standardError ); 3509actualOutputBuilder .append ("}\nstandard output = {\n" ); 3510actualOutputBuilder .append (standardOutput ); 3511actualOutputBuilder .append ("}\n" ); 3512if (debugLayer .getLength ()> 0 ) 3513 { 3514actualOutputBuilder .append ("debug layer = {\n" ); 3515actualOutputBuilder .append (debugLayer ); 3516actualOutputBuilder .append ("}\n" ); 3517 } 3518 3519String actualOutput = actualOutputBuilder .produceString (); 3520 3521// Always fail if the compilation produced a failure, just 3522// to catch situations where, e.g., command-line options parsing 3523// caused the same error in both the Slang and fxc cases. 3524return _validateOutput (context ,input ,actualOutput ,resultCode != 0 ); 3525} 3526 3527static TestResult runDXBCComparisonTest (TestContext * context ,TestInput & input ) 3528{ 3529return _runHLSLComparisonTest (context ,input ,"dxbc-assembly" ,"fxc" ); 3530} 3531 3532static TestResult runDXILComparisonTest (TestContext * context ,TestInput & input ) 3533{ 3534return _runHLSLComparisonTest (context ,input ,"dxil-assembly" ,"dxc" ); 3535} 3536 3537TestResult doGLSLComparisonTestRun ( 3538TestContext * context , 3539TestInput & input , 3540char const * langDefine , 3541char const * passThrough , 3542char const * outputKind , 3543String * outOutput ) 3544{ 3545auto filePath999 = input .filePath ; 3546auto outputStem = input .outputStem ; 3547 3548CommandLine cmdLine ; 3549_initSlangCompiler (context ,cmdLine ); 3550 3551cmdLine .addArg (filePath999 ); 3552 3553if (langDefine ) 3554 { 3555cmdLine .addArg ("-D" ); 3556cmdLine .addArg (langDefine ); 3557 } 3558 3559if (passThrough ) 3560 { 3561cmdLine .addArg ("-pass-through" ); 3562cmdLine .addArg (passThrough ); 3563 } 3564 3565cmdLine .addArg ("-target" ); 3566cmdLine .addArg ("spirv-assembly" ); 3567 3568for (auto arg :input .testOptions -> args ) 3569 { 3570cmdLine .addArg (arg ); 3571 } 3572 3573ExecuteResult exeRes ; 3574TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 3575 3576if (context -> isCollectingRequirements ()) 3577 { 3578return TestResult ::Pass ; 3579 } 3580 3581ExecuteResult ::ResultCode resultCode = exeRes .resultCode ; 3582 3583String standardOuptut = exeRes .standardOutput ; 3584String standardError = exeRes .standardError ; 3585String debugLayer = exeRes .debugLayer ; 3586 3587// We construct a single output string that captures the results 3588StringBuilder outputBuilder ; 3589outputBuilder .append ("result code = " ); 3590outputBuilder .append (resultCode ); 3591outputBuilder .append ("\nstandard error = {\n" ); 3592outputBuilder .append (standardError ); 3593outputBuilder .append ("}\nstandard output = {\n" ); 3594outputBuilder .append (standardOuptut ); 3595outputBuilder .append ("}\n" ); 3596if (debugLayer .getLength ()> 0 ) 3597 { 3598outputBuilder .append ("debug layer = {\n" ); 3599outputBuilder .append (debugLayer ); 3600outputBuilder .append ("}\n" ); 3601 } 3602 3603String outputPath = outputStem + outputKind ; 3604String output = outputBuilder .produceString (); 3605 3606* outOutput = output ; 3607 3608return TestResult ::Pass ; 3609} 3610 3611TestResult runGLSLComparisonTest (TestContext * context ,TestInput & input ) 3612{ 3613auto filePath999 = input .filePath ; 3614auto outputStem = input .outputStem ; 3615 3616String expectedOutput ; 3617String actualOutput ; 3618 3619TestResult hlslResult = doGLSLComparisonTestRun ( 3620context , 3621input , 3622"__GLSL__" , 3623"glslang" , 3624".expected" , 3625& expectedOutput ); 3626TestResult slangResult = 3627doGLSLComparisonTestRun (context ,input ,"__SLANG__" ,nullptr ,".actual" ,& actualOutput ); 3628 3629if (context -> isCollectingRequirements ()) 3630 { 3631return TestResult ::Pass ; 3632 } 3633 3634// If either is ignored, the whole test is 3635if (hlslResult == TestResult ::Ignored || slangResult == TestResult ::Ignored ) 3636 { 3637return TestResult ::Ignored ; 3638 } 3639 3640Slang ::File ::writeAllText (outputStem + ".expected" ,expectedOutput ); 3641Slang ::File ::writeAllText (outputStem + ".actual" ,actualOutput ); 3642 3643if (hlslResult == TestResult ::Fail ) 3644return TestResult ::Fail ; 3645if (slangResult == TestResult ::Fail ) 3646return TestResult ::Fail ; 3647 3648if (!StringUtil ::areLinesEqual ( 3649actualOutput .getUnownedSlice (), 3650expectedOutput .getUnownedSlice ())) 3651 { 3652context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 3653 3654return TestResult ::Fail ; 3655 } 3656 3657return TestResult ::Pass ; 3658} 3659 3660static void _addRenderTestOptions (const Options & options ,CommandLine & ioCmdLine ) 3661{ 3662if (!options .emitSPIRVDirectly ) 3663 { 3664ioCmdLine .addArg ("-emit-spirv-via-glsl" ); 3665 } 3666 3667for (auto capability :options .capabilities ) 3668 { 3669ioCmdLine .addArg ("-capability" ); 3670ioCmdLine .addArg (capability ); 3671 } 3672 3673if (options .enableDebugLayers ) 3674 { 3675ioCmdLine .addArg ("-enable-debug-layers" ); 3676 } 3677 3678if (options .ignoreAbortMsg ) 3679 { 3680ioCmdLine .addArg ("-ignore-abort-msg" ); 3681 } 3682 3683if (options .cacheRhiDevice ) 3684 { 3685ioCmdLine .addArg ("-cache-rhi-device" ); 3686 } 3687} 3688 3689static SlangResult _extractProfileTime (const UnownedStringSlice & text ,double & timeOut ) 3690{ 3691// Need to find the profile figure.. 3692LineParser parser (text ); 3693 3694const auto lineStart = UnownedStringSlice ::fromLiteral ("profile-time=" ); 3695for (auto line :parser ) 3696 { 3697if (line .startsWith (lineStart )) 3698 { 3699UnownedStringSlice remaining (line .begin ()+ lineStart .getLength (),line .end ()); 3700remaining .trim (); 3701 3702timeOut = stringToDouble (String (remaining )); 3703return SLANG_OK ; 3704 } 3705 } 3706 3707return SLANG_FAIL ; 3708} 3709 3710TestResult runPerformanceProfile (TestContext * context ,TestInput & input ) 3711{ 3712auto outputStem = input .outputStem ; 3713 3714CommandLine cmdLine ; 3715 3716cmdLine .setExecutableLocation (ExecutableLocation (context -> options .binDir ,"render-test" )); 3717 3718cmdLine .addArg (input .filePath ); 3719cmdLine .addArg ("-performance-profile" ); 3720 3721_addRenderTestOptions (context -> options ,cmdLine ); 3722 3723for (auto arg :input .testOptions -> args ) 3724 { 3725cmdLine .addArg (arg ); 3726 } 3727 3728ExecuteResult exeRes ; 3729TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 3730if (context -> isCollectingRequirements ()) 3731 { 3732return TestResult ::Pass ; 3733 } 3734 3735auto actualOutput = getOutput (exeRes ); 3736 3737double time ; 3738if (SLANG_FAILED (_extractProfileTime (actualOutput .getUnownedSlice (),time ))) 3739 { 3740return TestResult ::Fail ; 3741 } 3742 3743context -> getTestReporter ()-> addExecutionTime (time ); 3744 3745return TestResult ::Pass ; 3746} 3747 3748 3749static double _textToDouble (const UnownedStringSlice & slice ) 3750{ 3751Index size = Index (slice .getLength ()); 3752// We have to zero terminate to be able to use atof 3753const Index maxSize = 80 ; 3754char buffer [maxSize + 1 ]; 3755 3756size = (size > maxSize ) ?maxSize :size ; 3757 3758memcpy (buffer ,slice .begin (),size ); 3759buffer [size ]= 0 ; 3760 3761return atof (buffer ); 3762} 3763 3764static void _calcLines (const UnownedStringSlice & slice ,List < UnownedStringSlice >& outLines ) 3765{ 3766StringUtil ::calcLines (slice ,outLines ); 3767 3768// Remove any trailing empty lines 3769while (outLines .getCount ()) 3770 { 3771if (outLines .getLast ().trim ()== UnownedStringSlice ()) 3772 { 3773outLines .removeLast (); 3774 } 3775else 3776 { 3777break ; 3778 } 3779 } 3780} 3781 3782static SlangResult _compareWithType ( 3783const UnownedStringSlice & actual , 3784const UnownedStringSlice & ref , 3785double differenceThreshold = 0.0001 ) 3786{ 3787typedef slang::TypeReflection ::ScalarType ScalarType ; 3788 3789ScalarType scalarType = ScalarType ::None ; 3790 3791// We just do straight comparison if there is no type 3792 3793List < UnownedStringSlice > linesActual ,linesRef ; 3794 3795_calcLines (actual ,linesActual ); 3796_calcLines (ref ,linesRef ); 3797 3798// If there are more lines in actual, we just ignore them, to keep same behavior as before 3799if (linesRef .getCount ()< linesActual .getCount ()) 3800 { 3801linesActual .setCount (linesRef .getCount ()); 3802 } 3803 3804if (linesActual .getCount ()!= linesRef .getCount ()) 3805 { 3806return SLANG_FAIL ; 3807 } 3808 3809for (Index i = 0 ;i < linesActual .getCount ();++ i ) 3810 { 3811const UnownedStringSlice lineActual = linesActual [i ]; 3812const UnownedStringSlice lineRef = linesRef [i ]; 3813 3814if (lineActual .startsWith (UnownedStringSlice ::fromLiteral ("type:" ))) 3815 { 3816if (lineActual != lineRef ) 3817 { 3818return SLANG_FAIL ; 3819 } 3820// Get the type 3821List < UnownedStringSlice > split ; 3822StringUtil ::split (lineActual ,':' ,split ); 3823 3824if (split .getCount ()!= 2 ) 3825 { 3826return SLANG_FAIL ; 3827 } 3828 3829scalarType = TypeTextUtil ::findScalarType (split [1 ].trim ()); 3830continue ; 3831 } 3832 3833switch (scalarType ) 3834 { 3835default : 3836 { 3837if (lineActual .trim ()!= lineRef .trim ()) 3838 { 3839return SLANG_FAIL ; 3840 } 3841break ; 3842 } 3843case ScalarType ::Float16 : 3844case ScalarType ::Float32 : 3845case ScalarType ::Float64 : 3846 { 3847 3848// Compare as double 3849double valueA = _textToDouble (lineActual ); 3850double valueB = _textToDouble (lineRef ); 3851 3852if (!Math ::AreNearlyEqual (valueA ,valueB ,differenceThreshold )) 3853 { 3854return SLANG_FAIL ; 3855 } 3856break ; 3857 } 3858 } 3859 } 3860 3861return SLANG_OK ; 3862} 3863 3864TestResult runComputeComparisonImpl ( 3865TestContext * context , 3866TestInput & input , 3867const char * const * langOpts , 3868size_t numLangOpts ) 3869{ 3870// TODO: delete any existing files at the output path(s) to avoid stale outputs leading to a 3871// false pass 3872auto filePath999 = input .filePath ; 3873auto outputStem = input .outputStem ; 3874 3875CommandLine cmdLine ; 3876 3877cmdLine .setExecutableLocation (ExecutableLocation (context -> options .binDir ,"render-test" )); 3878cmdLine .addArg (filePath999 ); 3879 3880_addRenderTestOptions (context -> options ,cmdLine ); 3881 3882for (auto arg :input .testOptions -> args ) 3883 { 3884cmdLine .addArg (arg ); 3885 } 3886 3887for (int i = 0 ;i < int (numLangOpts );++ i ) 3888 { 3889cmdLine .addArg (langOpts [i ]); 3890 } 3891cmdLine .addArg ("-o" ); 3892auto actualOutputFile = outputStem + ".actual.txt" ; 3893cmdLine .addArg (actualOutputFile ); 3894 3895if (context -> isExecuting ()) 3896 { 3897// clear the stale actual output file first. This will allow us to detect error if 3898// render-test fails and outputs nothing. 3899File ::writeAllText (actualOutputFile ,"" ); 3900 } 3901 3902ExecuteResult exeRes ; 3903TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 3904 3905if (context -> isCollectingRequirements ()) 3906 { 3907return TestResult ::Pass ; 3908 } 3909 3910// Check the stdout/stderr from the compiler process 3911auto actualOutput = getOutput (exeRes ); 3912auto compileResult = _validateOutput ( 3913context , 3914input , 3915actualOutput , 3916 false, 3917"result code = 0\nstandard error = {\n}\nstandard output = {\n}\n" ); 3918 3919// check against reference output 3920String actualOutputContent ; 3921if (SLANG_FAILED (File ::readAllText (actualOutputFile ,actualOutputContent ))) 3922 { 3923context -> getTestReporter ()-> messageFormat ( 3924TestMessageType ::RunError , 3925"Unable to read render-test output: %s\n" , 3926actualOutput .getBuffer ()); 3927return TestResult ::Fail ; 3928 } 3929 3930String fileCheckPrefix ; 3931auto bufferResult = 3932input .testOptions -> getFileCheckBufferPrefix (fileCheckPrefix ) 3933 ?_fileCheckTest (* context ,input .filePath ,fileCheckPrefix ,actualOutputContent ) 3934 :_fileComparisonTest ( 3935* context , 3936input , 3937nullptr , 3938".expected.txt" , 3939actualOutputContent , 3940 [](const auto & a ,const auto & e ) { 3941return SLANG_SUCCEEDED ( 3942_compareWithType (a .getUnownedSlice (),e .getUnownedSlice ())); 3943 }); 3944return std::max (compileResult ,bufferResult ); 3945} 3946 3947TestResult runSlangComputeComparisonTest (TestContext * context ,TestInput & input ) 3948{ 3949const char * langOpts []= {"-slang" ,"-compute" }; 3950return runComputeComparisonImpl (context ,input ,langOpts ,SLANG_COUNT_OF (langOpts )); 3951} 3952 3953TestResult runSlangComputeComparisonTestEx (TestContext * context ,TestInput & input ) 3954{ 3955return runComputeComparisonImpl (context ,input ,nullptr ,0 ); 3956} 3957 3958TestResult runHLSLComputeTest (TestContext * context ,TestInput & input ) 3959{ 3960const char * langOpts []= {"--hlsl-rewrite" ,"-compute" }; 3961return runComputeComparisonImpl (context ,input ,langOpts ,SLANG_COUNT_OF (langOpts )); 3962} 3963 3964TestResult runSlangRenderComputeComparisonTest (TestContext * context ,TestInput & input ) 3965{ 3966const char * langOpts []= {"-slang" ,"-gcompute" }; 3967return runComputeComparisonImpl (context ,input ,langOpts ,SLANG_COUNT_OF (langOpts )); 3968} 3969 3970TestResult doRenderComparisonTestRun ( 3971TestContext * context , 3972TestInput & input , 3973char const * langOption , 3974char const * outputKind , 3975String * outOutput ) 3976{ 3977// TODO: delete any existing files at the output path(s) to avoid stale outputs leading to a 3978// false pass 3979 3980auto filePath = input .filePath ; 3981auto outputStem = input .outputStem ; 3982 3983CommandLine cmdLine ; 3984 3985cmdLine .setExecutableLocation (ExecutableLocation (context -> options .binDir ,"render-test" )); 3986cmdLine .addArg (filePath ); 3987 3988_addRenderTestOptions (context -> options ,cmdLine ); 3989 3990for (auto arg :input .testOptions -> args ) 3991 { 3992cmdLine .addArg (arg ); 3993 } 3994 3995cmdLine .addArg (langOption ); 3996cmdLine .addArg ("-o" ); 3997cmdLine .addArg (outputStem + outputKind + ".png" ); 3998 3999ExecuteResult exeRes ; 4000TEST_RETURN_ON_DONE (spawnAndWait (context ,outputStem ,input .spawnType ,cmdLine ,exeRes )); 4001 4002if (context -> isCollectingRequirements ()) 4003 { 4004return TestResult ::Pass ; 4005 } 4006 4007ExecuteResult ::ResultCode resultCode = exeRes .resultCode ; 4008 4009String standardOutput = exeRes .standardOutput ; 4010String standardError = exeRes .standardError ; 4011String debugLayer = exeRes .debugLayer ; 4012 4013// We construct a single output string that captures the results 4014StringBuilder outputBuilder ; 4015outputBuilder .append ("result code = " ); 4016outputBuilder .append (resultCode ); 4017outputBuilder .append ("\nstandard error = {\n" ); 4018outputBuilder .append (standardError ); 4019outputBuilder .append ("}\nstandard output = {\n" ); 4020outputBuilder .append (standardOutput ); 4021outputBuilder .append ("}\n" ); 4022if (debugLayer .getLength ()> 0 ) 4023 { 4024outputBuilder .append ("debug layer = {\n" ); 4025outputBuilder .append (debugLayer ); 4026outputBuilder .append ("}\n" ); 4027 } 4028 4029String outputPath = outputStem + outputKind ; 4030String output = outputBuilder .produceString (); 4031 4032* outOutput = output ; 4033 4034// Always fail if the compilation produced a failure. 4035if (exeRes .resultCode != 0 ) 4036 { 4037return TestResult ::Fail ; 4038 } 4039return TestResult ::Pass ; 4040} 4041 4042class STBImage 4043{ 4044public : 4045typedef STBImage ThisType ; 4046 4047/// Reset back to default initialized state (frees any image set) 4048void reset (); 4049/// True if rhs has same size and amount of channels 4050bool isComparable (const ThisType & rhs )const ; 4051 4052/// The width in pixels 4053int getWidth ()const {return m_width ; } 4054/// The height in pixels 4055int getHeight ()const {return m_height ; } 4056/// The number of channels (typically held as bytes in order) 4057int getNumChannels ()const {return m_numChannels ; } 4058 4059/// Get the contained pixels, nullptr if nothing loaded 4060const unsigned char * getPixels ()const {return m_pixels ; } 4061unsigned char * getPixels () {return m_pixels ; } 4062 4063/// Read an image with filename. SLANG_OK on success 4064SlangResult read (const char * filename ); 4065 4066 ~STBImage () {reset (); } 4067 4068int m_width = 0 ; 4069int m_height = 0 ; 4070int m_numChannels = 0 ; 4071unsigned char * m_pixels = nullptr ; 4072}; 4073 4074void STBImage ::reset () 4075{ 4076if (m_pixels ) 4077 { 4078stbi_image_free (m_pixels ); 4079m_pixels = nullptr ; 4080 } 4081m_width = 0 ; 4082m_height = 0 ; 4083m_numChannels = 0 ; 4084} 4085 4086SlangResult STBImage ::read (const char * filename ) 4087{ 4088reset (); 4089 4090m_pixels = stbi_load (filename ,& m_width ,& m_height ,& m_numChannels ,0 ); 4091if (!m_pixels ) 4092 { 4093return SLANG_FAIL ; 4094 } 4095return SLANG_OK ; 4096} 4097 4098bool STBImage ::isComparable (const ThisType & rhs )const 4099{ 4100return (this == & rhs )|| (m_width == rhs .m_width && m_height == rhs .m_height && 4101m_numChannels == rhs .m_numChannels ); 4102} 4103 4104 4105TestResult doImageComparison (TestContext * context ,String const & filePath ) 4106{ 4107auto reporter = context -> getTestReporter (); 4108 4109// Allow a difference in the low bits of the 8-bit result, just to play it safe 4110static const int kAbsoluteDiffCutoff = 2 ; 4111 4112// Allow a relative 1% difference 4113static const float kRelativeDiffCutoff = 0.01f ; 4114 4115String expectedPath = filePath + ".expected.png" ; 4116String actualPath = filePath + ".actual.png" ; 4117 4118STBImage expectedImage ; 4119if (SLANG_FAILED (expectedImage .read (expectedPath .getBuffer ()))) 4120 { 4121reporter -> messageFormat ( 4122TestMessageType ::RunError , 4123"Unable to load image ;%s'" , 4124expectedPath .getBuffer ()); 4125return TestResult ::Fail ; 4126 } 4127 4128STBImage actualImage ; 4129if (SLANG_FAILED (actualImage .read (actualPath .getBuffer ()))) 4130 { 4131reporter -> messageFormat ( 4132TestMessageType ::RunError , 4133"Unable to load image ;%s'" , 4134actualPath .getBuffer ()); 4135return TestResult ::Fail ; 4136 } 4137 4138if (!expectedImage .isComparable (actualImage )) 4139 { 4140reporter -> messageFormat ( 4141TestMessageType ::TestFailure , 4142"Images are different sizes '%s' '%s'" , 4143actualPath .getBuffer (), 4144expectedPath .getBuffer ()); 4145return TestResult ::Fail ; 4146 } 4147 4148 { 4149const unsigned char * expectedPixels = expectedImage .getPixels (); 4150const unsigned char * actualPixels = actualImage .getPixels (); 4151 4152const int height = actualImage .getHeight (); 4153const int width = actualImage .getWidth (); 4154const int numChannels = actualImage .getNumChannels (); 4155const int rowSize = width * numChannels ; 4156 4157for (int y = 0 ;y < height ;++ y ) 4158 { 4159for (int i = 0 ;i < rowSize ;++ i ) 4160 { 4161int expectedVal = expectedPixels [i ]; 4162int actualVal = actualPixels [i ]; 4163 4164int absoluteDiff = actualVal - expectedVal ; 4165if (absoluteDiff < 0 ) 4166absoluteDiff = - absoluteDiff ; 4167 4168if (absoluteDiff < kAbsoluteDiffCutoff ) 4169 { 4170// There might be a difference, but we'll consider it to be inside tolerance 4171continue ; 4172 } 4173 4174float relativeDiff = 0.0f ; 4175if (expectedVal != 0 ) 4176 { 4177relativeDiff = 4178fabsf (float (actualVal )- float (expectedVal )) /float (expectedVal ); 4179 4180if (relativeDiff < kRelativeDiffCutoff ) 4181 { 4182// relative difference was small enough 4183continue ; 4184 } 4185 } 4186 4187// TODO: may need to do some local search sorts of things, to deal with 4188// cases where vertex shader results lead to rendering that is off 4189// by one pixel... 4190 4191const int x = i /numChannels ; 4192const int channelIndex = i %numChannels ; 4193 4194reporter -> messageFormat ( 4195TestMessageType ::TestFailure , 4196"image compare failure at (%d,%d) channel %d. expected %d got %d (absolute " 4197"error: %d, relative error: %f)\n" , 4198x , 4199y , 4200channelIndex , 4201expectedVal , 4202actualVal , 4203absoluteDiff , 4204relativeDiff ); 4205 4206// There was a difference we couldn't excuse! 4207return TestResult ::Fail ; 4208 } 4209 4210expectedPixels += rowSize ; 4211actualPixels += rowSize ; 4212 } 4213 } 4214 4215return TestResult ::Pass ; 4216} 4217 4218TestResult runHLSLRenderComparisonTestImpl ( 4219TestContext * context , 4220TestInput & input , 4221char const * expectedArg , 4222char const * actualArg ) 4223{ 4224String _fileCheckPrefix ; 4225if (input .testOptions -> getFileCheckPrefix (_fileCheckPrefix )) 4226 { 4227context -> getTestReporter ()-> message ( 4228TestMessageType ::RunError , 4229"FileCheck testing isn't supported for HLSL render tests" ); 4230return TestResult ::Fail ; 4231 } 4232 4233auto filePath = input .filePath ; 4234auto outputStem = input .outputStem ; 4235 4236String expectedOutput ; 4237String actualOutput ; 4238 4239// Run the expected test case only if we're not skipping reference image generation 4240TestResult hlslResult = TestResult ::Pass ; 4241if (!context -> options .skipReferenceImageGeneration ) 4242 { 4243hlslResult = 4244doRenderComparisonTestRun (context ,input ,expectedArg ,".expected" ,& expectedOutput ); 4245if (hlslResult != TestResult ::Pass ) 4246 { 4247return hlslResult ; 4248 } 4249 } 4250 4251// Always run the actual test case 4252TestResult slangResult = 4253doRenderComparisonTestRun (context ,input ,actualArg ,".actual" ,& actualOutput ); 4254if (slangResult != TestResult ::Pass ) 4255 { 4256return slangResult ; 4257 } 4258 4259if (context -> isCollectingRequirements ()) 4260 { 4261return TestResult ::Pass ; 4262 } 4263 4264// Save the expected output if we generated it 4265if (!context -> options .skipReferenceImageGeneration ) 4266 { 4267Slang ::File ::writeAllText (outputStem + ".expected" ,expectedOutput ); 4268 } 4269 4270Slang ::File ::writeAllText (outputStem + ".actual" ,actualOutput ); 4271 4272if (hlslResult == TestResult ::Fail ) 4273return TestResult ::Fail ; 4274if (slangResult == TestResult ::Fail ) 4275return TestResult ::Fail ; 4276 4277// Compare text output only if we generated the expected output 4278if (!context -> options .skipReferenceImageGeneration && !StringUtil ::areLinesEqual ( 4279actualOutput .getUnownedSlice (), 4280expectedOutput .getUnownedSlice ())) 4281 { 4282context -> getTestReporter ()-> dumpOutputDifference (expectedOutput ,actualOutput ); 4283 4284return TestResult ::Fail ; 4285 } 4286 4287// Next do an image comparison on the expected output images! 4288 4289TestResult imageCompareResult = doImageComparison (context ,outputStem ); 4290if (imageCompareResult != TestResult ::Pass ) 4291return imageCompareResult ; 4292 4293return TestResult ::Pass ; 4294} 4295 4296TestResult runHLSLRenderComparisonTest (TestContext * context ,TestInput & input ) 4297{ 4298return runHLSLRenderComparisonTestImpl (context ,input ,"-hlsl" ,"-slang" ); 4299} 4300 4301TestResult runHLSLCrossCompileRenderComparisonTest (TestContext * context ,TestInput & input ) 4302{ 4303return runHLSLRenderComparisonTestImpl (context ,input ,"-slang" ,"-glsl-cross" ); 4304} 4305 4306TestResult runHLSLAndGLSLRenderComparisonTest (TestContext * context ,TestInput & input ) 4307{ 4308return runHLSLRenderComparisonTestImpl (context ,input ,"-hlsl-rewrite" ,"-glsl-rewrite" ); 4309} 4310 4311TestResult skipTest (TestContext * /* context */ ,TestInput & /*input*/ ) 4312{ 4313return TestResult ::Ignored ; 4314} 4315 4316// based on command name, dispatch to an appropriate callback 4317struct TestCommandInfo 4318{ 4319char const * name ; 4320TestCallback callback ; 4321RenderApiFlags requiredRenderApiFlags ;///< An RenderApi types that are needed to run the tests 4322}; 4323 4324static const TestCommandInfo s_testCommandInfos []= { 4325 {"SIMPLE" ,& runSimpleTest ,0 }, 4326 {"SIMPLE_EX" ,& runSimpleTest ,0 }, 4327 {"SIMPLE_LINE" ,& runSimpleLineTest ,0 }, 4328 {"INTERPRET" ,& runInterpreterTest ,0 }, 4329 {"REFLECTION" ,& runReflectionTest ,0 }, 4330 {"CPU_REFLECTION" ,& runReflectionTest ,0 }, 4331 {"COMMAND_LINE_SIMPLE" ,& runSimpleCompareCommandLineTest ,0 }, 4332 {"COMPARE_HLSL" ,& runDXBCComparisonTest ,0 }, 4333 {"COMPARE_DXIL" ,& runDXILComparisonTest ,0 }, 4334 {"COMPARE_HLSL_RENDER" ,& runHLSLRenderComparisonTest ,0 }, 4335 {"COMPARE_HLSL_CROSS_COMPILE_RENDER" ,& runHLSLCrossCompileRenderComparisonTest ,0 }, 4336 {"COMPARE_HLSL_GLSL_RENDER" ,& runHLSLAndGLSLRenderComparisonTest ,0 }, 4337 {"COMPARE_COMPUTE" ,& runSlangComputeComparisonTest ,0 }, 4338 {"COMPARE_COMPUTE_EX" ,& runSlangComputeComparisonTestEx ,0 }, 4339 {"HLSL_COMPUTE" ,& runHLSLComputeTest ,0 }, 4340 {"COMPARE_RENDER_COMPUTE" ,& runSlangRenderComputeComparisonTest ,0 }, 4341 {"COMPARE_GLSL" ,& runGLSLComparisonTest ,0 }, 4342 {"CROSS_COMPILE" ,& runCrossCompilerTest ,0 }, 4343 {"CPP_COMPILER_EXECUTE" ,& runCPPCompilerExecute ,RenderApiFlag ::CPU }, 4344 {"CPP_COMPILER_SHARED_LIBRARY" ,& runCPPCompilerSharedLibrary ,RenderApiFlag ::CPU }, 4345 {"CPP_COMPILER_COMPILE" ,& runCPPCompilerCompile ,RenderApiFlag ::CPU }, 4346 {"PERFORMANCE_PROFILE" ,& runPerformanceProfile ,0 }, 4347 {"COMPILE" ,& runCompile ,0 }, 4348 {"DOC" ,& runDocTest ,0 }, 4349 {"LANG_SERVER" ,& runLanguageServerTest ,0 }, 4350 {"EXECUTABLE" ,& runExecutableTest ,RenderApiFlag ::CPU }}; 4351 4352const TestCommandInfo * _findTestCommandInfoByCommand (const UnownedStringSlice & name ) 4353{ 4354for (const auto & command :s_testCommandInfos ) 4355 { 4356if (name == command .name ) 4357 { 4358return & command ; 4359 } 4360 } 4361return nullptr ; 4362} 4363 4364static RenderApiFlags _getRequiredRenderApisByCommand (const UnownedStringSlice & name ) 4365{ 4366auto info = _findTestCommandInfoByCommand (name ); 4367return info ?info -> requiredRenderApiFlags :0 ; 4368} 4369 4370TestResult runTest ( 4371TestContext * context , 4372String const & filePath , 4373String const & outputStem , 4374String const & testName , 4375TestOptions const & testOptions ) 4376{ 4377// If we are collecting requirements and it's diagnostic test, we always run 4378// (ie no requirements need to be captured - effectively it has 'no requirements') 4379if (context -> isCollectingRequirements ()&& testOptions .type == TestOptions ::Diagnostic ) 4380 { 4381return TestResult ::Pass ; 4382 } 4383 4384auto testInfo = _findTestCommandInfoByCommand (testOptions .command .getUnownedSlice ()); 4385 4386if (testInfo ) 4387 { 4388TestInput testInput ; 4389testInput .filePath = filePath ; 4390testInput .outputStem = outputStem ; 4391testInput .testOptions = & testOptions ; 4392testInput .spawnType = context -> options .defaultSpawnType ; 4393 4394return testInfo -> callback (context ,testInput ); 4395 } 4396 4397// No actual test runner found! 4398return TestResult ::Fail ; 4399} 4400 4401bool testCategoryMatches (TestCategory * sub ,TestCategory * sup ) 4402{ 4403auto ss = sub ; 4404while (ss ) 4405 { 4406if (ss == sup ) 4407return true; 4408 4409ss = ss -> parent ; 4410 } 4411return false; 4412} 4413 4414bool testCategoryMatches ( 4415TestCategory * categoryToMatch , 4416const Dictionary < TestCategory * ,TestCategory *>& categorySet ) 4417{ 4418for (const auto & [_ ,category ] :categorySet ) 4419 { 4420if (testCategoryMatches (categoryToMatch ,category )) 4421return true; 4422 } 4423return false; 4424} 4425 4426bool testPassesCategoryMask (TestContext * context ,TestOptions const & test ) 4427{ 4428// Don't include a test we should filter out 4429for (auto testCategory :test .categories ) 4430 { 4431if (testCategoryMatches (testCategory ,context -> options .excludeCategories )) 4432return false; 4433 } 4434 4435// Otherwise include any test the user asked for 4436for (auto testCategory :test .categories ) 4437 { 4438if (testCategoryMatches (testCategory ,context -> options .includeCategories )) 4439return true; 4440 } 4441 4442// skip by default 4443return false; 4444} 4445 4446static void _calcSynthesizedTests ( 4447TestContext * context , 4448RenderApiType synthRenderApiType , 4449const List < TestDetails >& srcTests , 4450List < TestDetails >& ioSynthTests ) 4451{ 4452// Add the explicit parameter 4453for (const auto & srcTest :srcTests ) 4454 { 4455const auto & requirements = srcTest .requirements ; 4456 4457// Render tests use renderApis... 4458// If it's an explicit test, we don't synth from it now 4459 4460// In the case of CUDA, we can only synth from a CPU source 4461if (synthRenderApiType == RenderApiType ::CUDA ) 4462 { 4463if (requirements .explicitRenderApi != RenderApiType ::CPU ) 4464 { 4465continue ; 4466 } 4467 4468// If the source language is defined, and it's 4469 4470const Index index = srcTest .options .args .indexOf ("-source-language" ); 4471if (index >=0 ) 4472 { 4473// 4474const auto & language = srcTest .options .args [index + 1 ]; 4475SlangSourceLanguage sourceLanguage = 4476TypeTextUtil ::findSourceLanguage (language .getUnownedSlice ()); 4477 4478bool isCrossCompile = true; 4479 4480switch (sourceLanguage ) 4481 { 4482case SLANG_SOURCE_LANGUAGE_GLSL : 4483case SLANG_SOURCE_LANGUAGE_C : 4484case SLANG_SOURCE_LANGUAGE_CPP : 4485 { 4486isCrossCompile = false; 4487 } 4488default : 4489break ; 4490 } 4491 4492if (!isCrossCompile ) 4493 { 4494continue ; 4495 } 4496 } 4497 } 4498else 4499 { 4500// TODO(JS): Arguably we should synthesize from explicit tests. In principal we can 4501// remove the explicit api apply another although that may not always work. If it 4502// doesn't use any render API or only uses CPU, we don't synthesize 4503if (requirements .usedRenderApiFlags == 0 || 4504requirements .usedRenderApiFlags == RenderApiFlag ::CPU || 4505requirements .explicitRenderApi != RenderApiType ::Unknown ) 4506 { 4507continue ; 4508 } 4509 } 4510 4511TestDetails synthTestDetails (srcTest .options ); 4512TestOptions & synthOptions = synthTestDetails .options ; 4513 4514// If there's a category associated with this render api, add it to the synthesized test 4515if (auto c = context -> categorySet .find (RenderApiUtil ::getApiName (synthRenderApiType ))) 4516 { 4517synthOptions .categories .add (c ); 4518 } 4519 4520// Mark as synthesized 4521synthOptions .isSynthesized = true; 4522 4523StringBuilder builder ; 4524builder <<"-" ; 4525builder <<RenderApiUtil ::getApiName (synthRenderApiType ); 4526 4527synthOptions .args .add (builder ); 4528 4529// If the target is vulkan remove the -hlsl option 4530if (synthRenderApiType == RenderApiType ::Vulkan ) 4531 { 4532const Index index = synthOptions .args .indexOf ("-hlsl" ); 4533if (index >=0 ) 4534 { 4535synthOptions .args .removeAt (index ); 4536 } 4537 } 4538else if (synthRenderApiType == RenderApiType ::CUDA ) 4539 { 4540const Index index = synthOptions .args .indexOf ("-cpu" ); 4541if (index >=0 ) 4542 { 4543synthOptions .args .removeAt (index ); 4544 } 4545 } 4546 4547// Work out the info about this tests 4548context -> setTestRequirements (& synthTestDetails .requirements ); 4549runTest (context ,"" ,"" ,"" ,synthOptions ); 4550context -> setTestRequirements (nullptr ); 4551 4552// It does set the explicit render target 4553SLANG_ASSERT (synthTestDetails .requirements .explicitRenderApi == synthRenderApiType ); 4554// Add to the tests 4555ioSynthTests .add (synthTestDetails ); 4556 } 4557} 4558 4559static bool _canIgnore (TestContext * context ,const TestDetails & details ) 4560{ 4561if (details .options .isEnabled == false) 4562 { 4563return true; 4564 } 4565 4566const auto & requirements = details .requirements ; 4567 4568// Check if it's possible in principal to run this test with the render api flags used by this 4569// test 4570if (!context -> canRunTestWithRenderApiFlags (requirements .usedRenderApiFlags )) 4571 { 4572return true; 4573 } 4574 4575// Are all the required backends available? 4576if (((requirements .usedBackendFlags & context -> availableBackendFlags )!= 4577requirements .usedBackendFlags )) 4578 { 4579return true; 4580 } 4581 4582// If there are no render API requirements, then we don't need to ignore. 4583if (requirements .usedRenderApiFlags == 0 ) 4584 { 4585return false; 4586 } 4587 4588// Work out what render api flags are actually available, lazily 4589const RenderApiFlags availableRenderApiFlags = 4590requirements .usedRenderApiFlags ?_getAvailableRenderApiFlags (context ) :0 ; 4591 4592// Are all the required rendering apis available? 4593if ((requirements .usedRenderApiFlags & availableRenderApiFlags )!= 4594requirements .usedRenderApiFlags ) 4595 { 4596return true; 4597 } 4598 4599return false; 4600} 4601 4602static SlangResult _runTestsOnFile (TestContext * context ,String filePath ) 4603{ 4604// Gather a list of tests to run 4605FileTestList testList ; 4606 4607SLANG_RETURN_ON_FAIL (_gatherTestsForFile (& context -> categorySet ,filePath ,& testList ,context )); 4608 4609if (testList .tests .getCount ()== 0 ) 4610 { 4611// Test was explicitly ignored 4612return SLANG_OK ; 4613 } 4614 4615// Note cases where a test file exists, but we found nothing to run 4616if (testList .tests .getCount ()== 0 ) 4617 { 4618context -> getTestReporter ()-> addTest (filePath ,TestResult ::Ignored ); 4619return SLANG_OK ; 4620 } 4621 4622RenderApiFlags apiUsedFlags = 0 ; 4623RenderApiFlags explictUsedApiFlags = 0 ; 4624 4625 { 4626// We can get the test info for each of them 4627for (auto & testDetails :testList .tests ) 4628 { 4629auto & requirements = testDetails .requirements ; 4630 4631// Collect what the test needs (by setting restRequirements the test isn't actually run) 4632context -> setTestRequirements (& requirements ); 4633runTest (context ,filePath ,filePath ,filePath ,testDetails .options ); 4634 4635 4636apiUsedFlags |=requirements .usedRenderApiFlags ; 4637explictUsedApiFlags |= (requirements .explicitRenderApi != RenderApiType ::Unknown ) 4638 ? (RenderApiFlags (1 ) <<int (requirements .explicitRenderApi )) 4639 :0 ; 4640 } 4641context -> setTestRequirements (nullptr ); 4642 } 4643 4644SLANG_ASSERT ((apiUsedFlags & explictUsedApiFlags )== explictUsedApiFlags ); 4645 4646const RenderApiFlags availableRenderApiFlags = 4647apiUsedFlags ?_getAvailableRenderApiFlags (context ) :0 ; 4648 4649// If synthesized tests are wanted look into adding them 4650if (context -> options .synthesizedTestApis && availableRenderApiFlags ) 4651 { 4652List < TestDetails > synthesizedTests ; 4653 4654// What render options do we want to synthesize 4655RenderApiFlags missingApis = 4656 (~apiUsedFlags )& (context -> options .synthesizedTestApis & availableRenderApiFlags ); 4657 4658// const Index numInitialTests = testList.tests.getCount(); 4659 4660while (missingApis ) 4661 { 4662const int index = ByteEncodeUtil ::calcMsb8 (missingApis ); 4663SLANG_ASSERT (index >=0 && index <=int (RenderApiType ::CountOf )); 4664 4665const RenderApiType synthRenderApiType = RenderApiType (index ); 4666 4667_calcSynthesizedTests (context ,synthRenderApiType ,testList .tests ,synthesizedTests ); 4668 4669// Disable the bit 4670missingApis &= ~(RenderApiFlags (1 ) <<index ); 4671 } 4672 4673// Add all the synthesized tests 4674testList .tests .addRange (synthesizedTests ); 4675 } 4676 4677// We have found a test to run! 4678int subTestCount = 0 ; 4679for (auto & testDetails :testList .tests ) 4680 { 4681int subTestIndex = subTestCount ++ ; 4682 4683// Check that the test passes our current category mask 4684if (!testPassesCategoryMask (context ,testDetails .options )) 4685 { 4686continue ; 4687 } 4688 4689// Work out the test stem 4690 4691StringBuilder outputStem ; 4692outputStem <<filePath ; 4693if (subTestIndex != 0 ) 4694 { 4695outputStem <<"." <<subTestIndex ; 4696 } 4697 4698// Work out the test name - taking into account render api / if synthesized 4699StringBuilder testName (outputStem ); 4700 4701if (testDetails .options .isSynthesized ) 4702 { 4703testName <<" syn" ; 4704 } 4705 4706const auto & requirements = testDetails .requirements ; 4707 4708// Display list of used apis on render test 4709if (requirements .usedRenderApiFlags ) 4710 { 4711RenderApiFlags usedFlags = requirements .usedRenderApiFlags ; 4712testName <<" (" ; 4713bool isPrev = false; 4714while (usedFlags ) 4715 { 4716const int index = ByteEncodeUtil ::calcMsb8 (usedFlags ); 4717const RenderApiType renderApiType = RenderApiType (index ); 4718if (isPrev ) 4719 { 4720testName <<"," ; 4721 } 4722testName <<RenderApiUtil ::getApiName (renderApiType ); 4723 4724// Disable bit 4725usedFlags &= ~(RenderApiFlags (1 ) <<index ); 4726isPrev = true; 4727 } 4728testName <<")" ; 4729 } 4730 4731// Report the test and run/ignore 4732 { 4733TestReporter ::TestScope scope (context -> getTestReporter (),testName ); 4734 4735TestResult testResult = TestResult ::Fail ; 4736 4737// If this test can be ignored 4738if (_canIgnore (context ,testDetails )) 4739 { 4740testResult = TestResult ::Ignored ; 4741context -> getTestReporter ()-> addResult (testResult ); 4742 } 4743else 4744 { 4745testResult = runTest (context ,filePath ,outputStem ,testName ,testDetails .options ); 4746if (testResult == TestResult ::Fail && 4747 !context -> getTestReporter ()-> m_expectedFailureList .contains (testName )) 4748 { 4749RefPtr < FileTestInfoImpl > fileTestInfo = new FileTestInfoImpl (); 4750fileTestInfo -> filePath = filePath ; 4751fileTestInfo -> testName = testName ; 4752fileTestInfo -> outputStem = outputStem ; 4753fileTestInfo -> options = testDetails .options ; 4754 4755 std::lock_guard lock (context -> mutexFailedTests ); 4756context -> failedFileTests .add (fileTestInfo ); 4757 } 4758else 4759 { 4760context -> getTestReporter ()-> addResult (testResult ); 4761 } 4762 } 4763 4764 4765// Could determine if to continue or not here... based on result 4766 } 4767 } 4768 4769return SLANG_OK ; 4770} 4771 4772 4773static bool endsWithAllowedExtension (TestContext * /*context*/ ,String filePath ) 4774{ 4775char const * allowedExtensions []= { 4776".slang" , 4777".hlsl" , 4778".fx" , 4779".glsl" , 4780".vert" , 4781".frag" , 4782".geom" , 4783".tesc" , 4784".tese" , 4785".comp" , 4786".internal" , 4787".ahit" , 4788".chit" , 4789".miss" , 4790".rgen" , 4791".c" , 4792".cpp" , 4793".cu" , 4794 }; 4795 4796for (auto allowedExtension :allowedExtensions ) 4797 { 4798if (filePath .endsWith (allowedExtension )) 4799return true; 4800 } 4801 4802return false; 4803} 4804 4805static bool shouldRunTest (TestContext * context ,String filePath ) 4806{ 4807if (!endsWithAllowedExtension (context ,filePath )) 4808return false; 4809 4810// Check exclude prefixes first - if any match, skip the test 4811for (auto & excludePrefix :context -> options .excludePrefixes ) 4812 { 4813if (filePath .startsWith (excludePrefix )) 4814 { 4815if (context -> options .verbosity == VerbosityLevel ::Verbose ) 4816 { 4817context -> getTestReporter ()-> messageFormat ( 4818TestMessageType ::Info , 4819"%s file is excluded from the test because it is found from the exclusion " 4820"list\n" , 4821filePath .getBuffer ()); 4822 } 4823return false; 4824 } 4825 } 4826 4827if (!context -> options .testPrefixes .getCount ()) 4828 { 4829return true; 4830 } 4831 4832// If we have prefixes, it has to match one of them 4833for (auto & p :context -> options .testPrefixes ) 4834 { 4835if (filePath .startsWith (p )) 4836 { 4837return true; 4838 } 4839 } 4840return false; 4841} 4842 4843void getFilesInDirectory (String directoryPath ,List < String >& files ) 4844{ 4845 { 4846List < String > localFiles ; 4847DirectoryUtil ::findFiles (directoryPath ,localFiles ); 4848files .addRange (localFiles ); 4849 } 4850 { 4851List < String > subDirs ; 4852DirectoryUtil ::findDirectories (directoryPath ,subDirs ); 4853for (auto subDir :subDirs ) 4854 { 4855getFilesInDirectory (subDir ,files ); 4856 } 4857 } 4858} 4859 4860template < typename F > 4861void runTestsInParallel (TestContext * context ,int count ,const F & f ) 4862{ 4863auto originalReporter = context -> getTestReporter (); 4864 std::atomic < int > consumePtr ; 4865consumePtr = 0 ; 4866auto threadFunc = [& ](int threadId ) 4867 { 4868TestReporter reporter ; 4869reporter .init (context -> options .outputMode ,context -> options .expectedFailureList , true); 4870TestReporter ::SuiteScope suiteScope (& reporter ,"tests" ); 4871context -> setThreadIndex (threadId ); 4872context -> setTestReporter (& reporter ); 4873do 4874 { 4875int index = consumePtr .fetch_add (1 ); 4876if (index >=count ) 4877break ; 4878f (index ); 4879 }while (true); 4880 { 4881 std::lock_guard < std::mutex > lock (context -> mutex ); 4882originalReporter -> consolidateWith (& reporter ); 4883 } 4884context -> setTestReporter (nullptr ); 4885 }; 4886List < std::thread > threads ; 4887for (int threadId = 0 ;threadId < context -> options .serverCount ;threadId ++ ) 4888 { 4889threads .add (std::thread (threadFunc ,threadId )); 4890 } 4891for (auto & t :threads ) 4892t .join (); 4893context -> setTestReporter (originalReporter ); 4894} 4895 4896void runTestsInDirectory (TestContext * context ) 4897{ 4898List < String > files ; 4899getFilesInDirectory (context -> options .testDir ,files ); 4900 4901// Also add any test prefixes that point to actual files outside the test directory 4902for (const auto & testPrefix :context -> options .testPrefixes ) 4903 { 4904if (File ::exists (testPrefix )) 4905 { 4906// Avoid duplicates - only add if not already in the list 4907if (files .indexOf (testPrefix )== Index (-1 )) 4908 { 4909files .add (testPrefix ); 4910 } 4911 } 4912 } 4913 4914// NTFS on Windows stores files in sorted order but not on Linux/Macos. 4915// Because of that, the testing on Linux/Macos were randomly failing, which 4916// is a good thing because it reveals problems. But it is useless 4917// if we cannot reproduce the failures deterministically. 4918// https://github.com/shader-slang/slang/issues/7388 4919 4920files .sort (); 4921 4922// If asked, shuffle the list using seed for deterministic behavior. 4923if (context -> options .shuffleTests ) 4924 { 4925 std::mt19937 mt (context -> options .shuffleSeed ); 4926 std::shuffle (files .begin (),files .end (),mt ); 4927 } 4928 4929auto processFile = [& ](String file ) 4930 { 4931if (shouldRunTest (context ,file )) 4932 { 4933SlangResult result = _runTestsOnFile (context ,file ); 4934if (SLANG_FAILED (result )) 4935 { 4936 { 4937TestReporter ::TestScope scope (context -> getTestReporter (),file ); 4938context -> getTestReporter ()-> messageFormat ( 4939TestMessageType ::RunError , 4940"slang-test: unable to parse test (error code: 0x%08X)" , 4941 (unsigned int )result ); 4942 4943context -> getTestReporter ()-> addResult (TestResult ::Fail ); 4944 } 4945 4946// Output there was some kind of error trying to run the tests on this file 4947// fprintf(stderr, "slang-test: unable to parse test '%s'\n", file.getBuffer()); 4948 } 4949 } 4950 }; 4951bool useMultiThread = false; 4952switch (context -> options .defaultSpawnType ) 4953 { 4954case SpawnType ::UseFullyIsolatedTestServer : 4955case SpawnType ::UseTestServer : 4956useMultiThread = true; 4957break ; 4958 } 4959if (context -> options .serverCount == 1 ) 4960 { 4961useMultiThread = false; 4962 } 4963if (!useMultiThread ) 4964 { 4965for (auto file :files ) 4966 { 4967processFile (file ); 4968 } 4969 } 4970else 4971 { 4972runTestsInParallel ( 4973context , 4974 (int )files .getCount (), 4975 [& ](int index ) {processFile (files [index ]); }); 4976 } 4977} 4978 4979static void _disableCPPBackends (TestContext * context ) 4980{ 4981const SlangPassThrough cppPassThrus []= { 4982SLANG_PASS_THROUGH_GENERIC_C_CPP , 4983SLANG_PASS_THROUGH_VISUAL_STUDIO , 4984SLANG_PASS_THROUGH_CLANG , 4985SLANG_PASS_THROUGH_GCC , 4986 }; 4987 4988for (auto passThru :cppPassThrus ) 4989 { 4990context -> availableBackendFlags &= ~(PassThroughFlags (1 ) <<int (passThru )); 4991context -> availableRenderApiFlags &= ~(RenderApiFlag ::CPU ); 4992context -> options .enabledApis &= ~(RenderApiFlag ::CPU ); 4993 } 4994} 4995 4996static void _disableD3D12Backend (TestContext * context ) 4997{ 4998context -> options .enabledApis &= ~(RenderApiFlag ::D3D12 ); 4999} 5000 5001static TestResult _asTestResult (ToolReturnCode retCode ) 5002{ 5003switch (retCode ) 5004 { 5005default : 5006return TestResult ::Fail ; 5007case ToolReturnCode ::Success : 5008return TestResult ::Pass ; 5009case ToolReturnCode ::Ignored : 5010return TestResult ::Ignored ; 5011 } 5012} 5013 5014/// Loads a DLL containing unit test functions and run them one by one. 5015static SlangResult runUnitTestModule ( 5016TestContext * context , 5017TestOptions & testOptions , 5018SpawnType spawnType , 5019const char * moduleName ) 5020{ 5021ISlangSharedLibraryLoader * loader = DefaultSharedLibraryLoader ::getSingleton (); 5022ComPtr < ISlangSharedLibrary > moduleLibrary ; 5023 5024SLANG_RETURN_ON_FAIL (loader -> loadSharedLibrary ( 5025Path ::combine (context -> dllDirectoryPath ,moduleName ).getBuffer (), 5026moduleLibrary .writeRef ())); 5027 5028UnitTestGetModuleFunc getModuleFunc = 5029 (UnitTestGetModuleFunc )moduleLibrary -> findFuncByName ("slangUnitTestGetModule" ); 5030if (!getModuleFunc ) 5031return SLANG_FAIL ; 5032 5033IUnitTestModule * testModule = getModuleFunc (); 5034if (!testModule ) 5035return SLANG_FAIL ; 5036 5037 renderer_test::CoreDebugCallback coreDebugCallback ; 5038 renderer_test::CoreToRHIDebugBridge rhiDebugBridge ; 5039rhiDebugBridge .setCoreCallback (& coreDebugCallback ); 5040 5041UnitTestContext unitTestContext ; 5042unitTestContext .slangGlobalSession = context -> getSession (); 5043unitTestContext .workDirectory = "" ; 5044unitTestContext .enabledApis = context -> options .enabledApis ; 5045unitTestContext .enableDebugLayers = context -> options .enableDebugLayers ; 5046unitTestContext .executableDirectory = context -> exeDirectoryPath .getBuffer (); 5047unitTestContext .debugCallback = & rhiDebugBridge ; 5048 5049auto testCount = testModule -> getTestCount (); 5050 5051struct TestItem 5052 { 5053UnitTestFunc testFunc ; 5054String testName ; 5055String command ; 5056 }; 5057 5058List < TestItem > tests ; 5059 5060// Discover all tests first. 5061for (SlangInt i = 0 ;i < testCount ;i ++ ) 5062 { 5063auto testFunc = testModule -> getTestFunc (i ); 5064auto testName = testModule -> getTestName (i ); 5065 5066StringBuilder filePath ; 5067filePath <<moduleName <<"/" <<testName <<".internal" ; 5068auto command = filePath .produceString (); 5069 5070if (shouldRunTest (context ,command )) 5071 { 5072if (testPassesCategoryMask (context ,testOptions )) 5073 { 5074tests .add (TestItem {testFunc ,testName ,command }); 5075 } 5076 } 5077 } 5078 5079auto runUnitTest = [& ](TestItem test ) 5080 { 5081auto reporter = context -> getTestReporter (); 5082TestOptions options = testOptions ; 5083options .command = test .command ; 5084 5085if (spawnType == SpawnType ::UseTestServer || 5086spawnType == SpawnType ::UseFullyIsolatedTestServer ) 5087 { 5088TestServerProtocol ::ExecuteUnitTestArgs args ; 5089args .enabledApis = context -> options .enabledApis ; 5090args .enableDebugLayers = context -> options .enableDebugLayers ; 5091args .moduleName = moduleName ; 5092args .testName = test .testName ; 5093 5094 { 5095TestReporter ::TestScope scopeTest (reporter ,options .command ); 5096ExecuteResult exeRes ; 5097// Initialize the ExecuteResult, otherwise we can get bogus 5098// error results. 5099exeRes .init (); 5100 5101SlangResult rpcRes = _executeRPC ( 5102context , 5103spawnType , 5104TestServerProtocol ::ExecuteUnitTestArgs ::g_methodName , 5105& args , 5106exeRes ); 5107auto testResult = _asTestResult (ToolReturnCode (exeRes .resultCode )); 5108 5109bool isFailed = (SLANG_FAILED (rpcRes )|| testResult == TestResult ::Fail ); 5110 5111// If the rpc failed, output an error message 5112if (SLANG_FAILED (rpcRes )) 5113 { 5114reporter -> message (TestMessageType ::RunError ,"rpc failed" ); 5115 } 5116 5117// Check for VVL errors in unit tests 5118if (exeRes .debugLayer .getLength ()> 0 ) 5119 { 5120testResult = TestResult ::Fail ; 5121reporter -> message (TestMessageType ::TestFailure ,exeRes .debugLayer ); 5122 } 5123 5124// If the test fails, output any output - which might give information about 5125// individual tests that have failed. 5126if (testResult == TestResult ::Fail ) 5127 { 5128String output = getOutput (exeRes ); 5129reporter -> message (TestMessageType ::TestFailure ,output .getBuffer ()); 5130 } 5131 5132// If the test failed and it is not an expected failure, add it to the list of 5133// failed unit tests so that we can retry. 5134if (isFailed && !context -> isRetry && 5135 !context -> getTestReporter ()-> m_expectedFailureList .contains (test .testName )) 5136 { 5137 std::lock_guard lock (context -> mutexFailedTests ); 5138context -> failedUnitTests .add (test .command ); 5139 } 5140else 5141 { 5142reporter -> addResult (testResult ); 5143 } 5144 } 5145 } 5146else 5147 { 5148TestReporter ::TestScope scopeTest (reporter ,options .command ); 5149 5150// TODO(JS): Problem here could be exception not handled properly across 5151// shared library boundary. 5152testModule -> setTestReporter (reporter ); 5153 5154// Clear any previous debug messages 5155coreDebugCallback .clear (); 5156 5157try 5158 { 5159test .testFunc (& unitTestContext ); 5160 5161// Check for VVL errors after test completion 5162String debugMessages = coreDebugCallback .getString (); 5163if (debugMessages .getLength ()> 0 ) 5164 { 5165reporter -> message (TestMessageType ::TestFailure ,debugMessages ); 5166reporter -> addResult (TestResult ::Fail ); 5167 } 5168 } 5169catch (...) 5170 { 5171reporter -> message ( 5172TestMessageType ::TestFailure , 5173"Exception was thrown during execution" ); 5174reporter -> addResult (TestResult ::Fail ); 5175 } 5176 } 5177 }; 5178 5179bool useMultiThread = false; 5180if (spawnType == SpawnType ::UseTestServer || spawnType == SpawnType ::UseFullyIsolatedTestServer ) 5181 { 5182if (context -> options .serverCount > 1 ) 5183 { 5184useMultiThread = true; 5185 } 5186 } 5187 5188if (useMultiThread ) 5189 { 5190runTestsInParallel ( 5191context , 5192 (int )tests .getCount (), 5193 [& ](int index ) {runUnitTest (tests [index ]); }); 5194 } 5195else 5196 { 5197auto reporter = TestReporter ::get (); 5198 5199testModule -> setTestReporter (reporter ); 5200 5201for (auto t :tests ) 5202runUnitTest (t ); 5203 } 5204 5205testModule -> destroy (); 5206return SLANG_OK ; 5207} 5208 5209static void cleanupRenderTestDeviceCache (TestContext & context ) 5210{ 5211auto cleanFunc = context .getCleanDeviceCacheFunc ("render-test" ); 5212if (cleanFunc ) 5213 { 5214cleanFunc (); 5215 } 5216} 5217 5218SlangResult innerMain (int argc ,char ** argv ) 5219{ 5220auto stdWriters = StdWriters ::initDefaultSingleton (); 5221 5222// The context holds useful things used during testing 5223TestContext context ; 5224SLANG_RETURN_ON_FAIL (SLANG_FAILED (context .init (argv [0 ]))) 5225 5226auto & categorySet = context .categorySet ; 5227 5228// Set up our test categories here 5229auto fullTestCategory = categorySet .add ("full" ,nullptr ); 5230auto quickTestCategory = categorySet .add ("quick" ,fullTestCategory ); 5231auto smokeTestCategory = categorySet .add ("smoke" ,quickTestCategory ); 5232auto renderTestCategory = categorySet .add ("render" ,fullTestCategory ); 5233/*auto computeTestCategory = */ categorySet .add ("compute" ,fullTestCategory ); 5234auto vulkanTestCategory = categorySet .add ("vulkan" ,fullTestCategory ); 5235auto unitTestCategory = categorySet .add ("unit-test" ,fullTestCategory ); 5236auto cudaTestCategory = categorySet .add ("cuda" ,fullTestCategory ); 5237auto optixTestCategory = categorySet .add ("optix" ,cudaTestCategory ); 5238 5239auto waveTestCategory = categorySet .add ("wave" ,fullTestCategory ); 5240auto waveMaskCategory = categorySet .add ("wave-mask" ,waveTestCategory ); 5241auto waveActiveCategory = categorySet .add ("wave-active" ,waveTestCategory ); 5242 5243auto compatibilityIssueCategory = categorySet .add ("compatibility-issue" ,fullTestCategory ); 5244 5245auto sharedLibraryCategory = categorySet .add ("shared-library" ,fullTestCategory ); 5246 5247#if SLANG_WINDOWS_FAMILY 5248auto windowsCategory = categorySet .add ("windows" ,fullTestCategory ); 5249#endif 5250 5251#if SLANG_UNIX_FAMILY 5252auto unixCategory = categorySet .add ("unix" ,fullTestCategory ); 5253#endif 5254 5255#if SLANG_PTR_IS_64 5256auto ptr64Category = categorySet .add ("64-bit" ,fullTestCategory ); 5257#else 5258auto ptr32Category = categorySet .add ("32-bit" ,fullTestCategory ); 5259#endif 5260 5261// An un-categorized test will always belong to the `full` category 5262categorySet .defaultCategory = fullTestCategory ; 5263 5264// All following values are initialized to '0', so null. 5265TestCategory * passThroughCategories [SLANG_PASS_THROUGH_COUNT_OF ]= {nullptr }; 5266 5267// Work out what backends/pass-thrus are available 5268 { 5269SlangSession * session = context .getSession (); 5270 5271auto out = StdWriters ::getOut (); 5272out ."Supported backends:" ); 5273 5274for (int i = 0 ;i < SLANG_PASS_THROUGH_COUNT_OF ;++ i ) 5275 { 5276const SlangPassThrough passThru = SlangPassThrough (i ); 5277if (passThru == SLANG_PASS_THROUGH_NONE ) 5278 { 5279continue ; 5280 } 5281 5282if (SLANG_SUCCEEDED (session -> checkPassThroughSupport (passThru ))) 5283 { 5284context .availableBackendFlags |=PassThroughFlags (1 ) <<int (i ); 5285 5286StringBuilder buf ; 5287 5288auto name = TypeTextUtil ::getPassThroughName (passThru ); 5289 5290buf <<" " <<name ; 5291 5292SLANG_ASSERT (passThroughCategories [i ]== nullptr ); 5293passThroughCategories [i ]= categorySet .add (buf .getBuffer ()+ 1 ,fullTestCategory ); 5294 5295out .write (buf .getBuffer (),buf .getLength ()); 5296 } 5297 } 5298 5299out ."\n" ); 5300 } 5301 5302 { 5303SlangSession * session = context .getSession (); 5304 5305const bool hasLlvm = 5306SLANG_SUCCEEDED (session -> checkPassThroughSupport (SLANG_PASS_THROUGH_LLVM )); 5307const auto hostCallableCompiler = session -> getDownstreamCompilerForTransition ( 5308SLANG_CPP_SOURCE , 5309SLANG_SHADER_HOST_CALLABLE ); 5310 5311if (hasLlvm && hostCallableCompiler == SLANG_PASS_THROUGH_LLVM && SLANG_PROCESSOR_X86 ) 5312 { 5313// TODO(JS) 5314// For some reason host-callable with llvm/double produces different results on x86 5315 } 5316else 5317 { 5318// Special category to mark a test only works for targets that work correctly with 5319// double (ie not x86/llvm) 5320categorySet .add ("war-double-host-callable" ,fullTestCategory ); 5321 } 5322 } 5323 5324// Working out what renderApis is worked on on demand through 5325// _getAvailableRenderApiFlags() 5326 5327 { 5328// We can set the slangc command line tool, to just use the function defined here 5329context .setInnerMainFunc ("slangc" ,& SlangCTool ::innerMain ); 5330 } 5331 5332 { 5333// We can set the slangc command line tool, to just use the function defined here 5334context .setInnerMainFunc ("slangi" ,& SlangITool ::innerMain ); 5335 } 5336 5337SLANG_RETURN_ON_FAIL (Options ::parse ( 5338argc , 5339argv , 5340& categorySet , 5341StdWriters ::getOut (), 5342StdWriters ::getError (), 5343& context .options )); 5344 5345Options & options = context .options ; 5346 5347context .setMaxTestRunnerThreadCount (options .serverCount ); 5348 5349// Set up the prelude/s 5350TestToolUtil ::setSessionDefaultPreludeFromExePath (argv [0 ],context .getSession ()); 5351 5352if (options .outputMode == TestOutputMode ::TeamCity ) 5353 { 5354// On TeamCity CI there is an issue with unix/linux targets where test system may be 5355// different from the build system That we rely on having compilation tools present such 5356// that on x64 systems we can build x86 binaries, and that appears to not always be the 5357// case. For now we only allow CPP backends to run on x86_64 targets 5358#if SLANG_UNIX_FAMILY && !SLANG_PROCESSOR_X86_64 5359_disableCPPBackends (& context ); 5360#endif 5361 } 5362 5363#if SLANG_PROCESSOR_X86 5364// Disable d3d12 tests on x86 right now since dxc for 32-bit windows doesn't seem to recognize 5365// sm_6_6. 5366_disableD3D12Backend (& context ); 5367#endif 5368 5369if (options .subCommand .getLength ()) 5370 { 5371// Get the function from the tool 5372auto func = context .getInnerMainFunc (options .binDir ,options .subCommand ); 5373if (!func ) 5374 { 5375StdWriters ::getError ()."error: Unable to launch tool '%s'\n" , 5377options .subCommand .getBuffer ()); 5378return SLANG_FAIL ; 5379 } 5380 5381// Copy args to a char* list 5382const auto & srcArgs = options .subCommandArgs ; 5383List < const char *> args ; 5384args .setCount (srcArgs .getCount ()); 5385for (Index i = 0 ;i < srcArgs .getCount ();++ i ) 5386 { 5387args [i ]= srcArgs [i ].getBuffer (); 5388 } 5389 5390return func ( 5391StdWriters ::getSingleton (), 5392context .getSession (), 5393int (args .getCount ()), 5394args .getBuffer ()); 5395 } 5396 5397if (options .includeCategories .getCount ()== 0 ) 5398 { 5399options .includeCategories .add (fullTestCategory ,fullTestCategory ); 5400 } 5401 5402// Exclude rendering tests when building under AppVeyor. 5403// 5404// TODO: this is very ad hoc, and we should do something cleaner. 5405if (options .outputMode == TestOutputMode ::AppVeyor ) 5406 { 5407options .excludeCategories .add (renderTestCategory ,renderTestCategory ); 5408options .excludeCategories .add (vulkanTestCategory ,vulkanTestCategory ); 5409 } 5410 5411 { 5412// Setup the reporter 5413TestReporter reporter ; 5414SLANG_RETURN_ON_FAIL (reporter .init (options .outputMode ,options .expectedFailureList )); 5415 5416context .setTestReporter (& reporter ); 5417 5418reporter .m_dumpOutputOnFailure = options .dumpOutputOnFailure ; 5419reporter .m_verbosity = options .verbosity ; 5420reporter .m_hideIgnored = options .hideIgnored ; 5421 5422 { 5423TestReporter ::SuiteScope suiteScope (& reporter ,"tests" ); 5424// Enumerate test files according to policy 5425runTestsInDirectory (& context ); 5426 } 5427 5428// Run the unit tests (these are internal C++ tests - not specified via files in a 5429// directory) They are registered with SLANG_UNIT_TEST macro 5430// 5431// 5432if (context .canRunUnitTests ()) 5433 { 5434TestReporter ::SuiteScope suiteScope (& reporter ,"unit tests" ); 5435TestReporter ::set (& reporter ); 5436 5437for (bool isRetry : {false, true}) 5438 { 5439auto spawnType = context .getFinalSpawnType (); 5440context .isRetry = isRetry ; 5441if (isRetry ) 5442 { 5443if (context .failedUnitTests .getCount ()== 0 ) 5444break ; 5445 5446printf ("Retrying unit tests...\n" ); 5447context .options .testPrefixes = context .failedUnitTests ; 5448context .failedUnitTests .clear (); 5449 } 5450 5451// Run the unit tests 5452 { 5453TestOptions testOptions ; 5454testOptions .categories .add (unitTestCategory ); 5455testOptions .categories .add (smokeTestCategory ); 5456runUnitTestModule (& context ,testOptions ,spawnType ,"slang-unit-test-tool" ); 5457 } 5458 5459 { 5460TestOptions testOptions ; 5461testOptions .categories .add (unitTestCategory ); 5462runUnitTestModule (& context ,testOptions ,spawnType ,"gfx-unit-test-tool" ); 5463 } 5464 } 5465 5466TestReporter ::set (nullptr ); 5467 } 5468 5469// If we have a couple failed tests, they maybe intermittent failures due to parallel 5470// excution or driver instability. We can try running them again. Debug build has more 5471// instability at this moment, so we allow more retries. 5472#if _DEBUG 5473static constexpr int kFailedTestLimitForRetry = 100 ; 5474#else 5475static constexpr int kFailedTestLimitForRetry = 16 ; 5476#endif 5477if (context .failedFileTests .getCount () <=kFailedTestLimitForRetry ) 5478 { 5479if (context .failedFileTests .getCount ()> 0 ) 5480printf ("Retrying %d failed tests...\n" , (int )context .failedFileTests .getCount ()); 5481for (auto & test :context .failedFileTests ) 5482 { 5483context .isRetry = true; 5484FileTestInfoImpl * fileTestInfo = static_cast < FileTestInfoImpl *> (test .Ptr ()); 5485TestReporter ::SuiteScope suiteScope (& reporter ,"tests" ); 5486TestReporter ::TestScope scope (& reporter ,fileTestInfo -> testName ); 5487auto newResult = runTest ( 5488& context , 5489fileTestInfo -> filePath , 5490fileTestInfo -> outputStem , 5491fileTestInfo -> testName , 5492fileTestInfo -> options ); 5493reporter .addResult (newResult ); 5494 } 5495 } 5496else 5497 { 5498// If there are too many failed tests, don't bother retrying. 5499for (auto & test :context .failedFileTests ) 5500 { 5501FileTestInfoImpl * fileTestInfo = static_cast < FileTestInfoImpl *> (test .Ptr ()); 5502TestReporter ::SuiteScope suiteScope (& reporter ,"tests" ); 5503TestReporter ::TestScope scope (& reporter ,fileTestInfo -> testName ); 5504reporter .addResult (TestResult ::Fail ); 5505 } 5506 } 5507 5508reporter .outputSummary (); 5509 5510cleanupRenderTestDeviceCache (context ); 5511return reporter .didAllSucceed () ?SLANG_OK :SLANG_FAIL ; 5512 } 5513} 5514 5515int main (int argc ,char ** argv ) 5516{ 5517// Fallback: run without cleanup if context initialization fails 5518SlangResult res = innerMain (argc ,argv ); 5519 slang::shutdown (); 5520Slang ::RttiInfo ::deallocateAll (); 5521 5522#ifdef _MSC_VER 5523_CrtDumpMemoryLeaks (); 5524#endif 5525return SLANG_SUCCEEDED (res ) ?0 :1 ; 5526}