yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Jay KwakRetry file reads in slang-test to handle intermittent I/O errors (#8713)05cae938c

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