diff options
| author | jsmall-nvidia <jsmall@nvidia.com> | 2018-12-14 15:24:21 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-12-14 15:24:21 -0500 |
| commit | d43c566fa29bbc0da1534aea236d54ee5ca104b8 (patch) | |
| tree | 5a1878687364d28361a2c6aa722c5c8d9c75810e | |
| parent | ec745c032a8dc16c3e689458c20541a4e7aa64d6 (diff) | |
Fix memory leaks around slang-test (#757)
* Remove circular reference to renderer on Vk & D3D12 DescriptorSetImpl
* Refactor Stbi image loading such that memory is correctly freed when goes out of scope.
Added Crt memory dump at termination.
Reduced erroneous reporting by scoping TestContext.
* Used capitalized acronym for STBImage to keep Tim happy.
* Split out TestReporter - to just handle reporting test results
Split out Options
Made TestContext hold options, and the reporter
Removed remaining memory leaks.
* Small optimization for rawWrite, such that it directly writes over print..
* Improve comments on TestCategorySet
* Fix typos in TestCategorySet
| -rw-r--r-- | slang.sln | 2 | ||||
| -rw-r--r-- | source/core/slang-writer.h | 2 | ||||
| -rw-r--r-- | source/slang/slang.vcxproj.filters | 2 | ||||
| -rw-r--r-- | tools/slang-reflection-test/main.cpp | 3 | ||||
| -rw-r--r-- | tools/slang-test/main.cpp | 495 | ||||
| -rw-r--r-- | tools/slang-test/options.cpp | 245 | ||||
| -rw-r--r-- | tools/slang-test/options.h | 83 | ||||
| -rw-r--r-- | tools/slang-test/slang-test.vcxproj | 4 | ||||
| -rw-r--r-- | tools/slang-test/slang-test.vcxproj.filters | 12 | ||||
| -rw-r--r-- | tools/slang-test/test-context.cpp | 598 | ||||
| -rw-r--r-- | tools/slang-test/test-context.h | 169 | ||||
| -rw-r--r-- | tools/slang-test/test-reporter.cpp | 614 | ||||
| -rw-r--r-- | tools/slang-test/test-reporter.h | 178 |
13 files changed, 1250 insertions, 1157 deletions
@@ -1,8 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 -MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{EB5FC2C6-D72D-B6CC-C0C1-26F3AC2E9231}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hello-world", "examples\hello-world\hello-world.vcxproj", "{010BE414-ED5B-CF56-16C0-BD18027062C0}" diff --git a/source/core/slang-writer.h b/source/core/slang-writer.h index 4c30ddb96..0e67684ec 100644 --- a/source/core/slang-writer.h +++ b/source/core/slang-writer.h @@ -14,7 +14,7 @@ class WriterHelper public: SlangResult print(const char* format, ...); SlangResult put(const char* text); - + SLANG_FORCE_INLINE SlangResult write(const char* chars, size_t numChars) { return m_writer->write(chars, numChars); } SLANG_FORCE_INLINE void flush() { m_writer->flush(); } ISlangWriter* getWriter() const { return m_writer; } diff --git a/source/slang/slang.vcxproj.filters b/source/slang/slang.vcxproj.filters index d72909bc1..edd51db88 100644 --- a/source/slang/slang.vcxproj.filters +++ b/source/slang/slang.vcxproj.filters @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <Filter Include="Header Files"> diff --git a/tools/slang-reflection-test/main.cpp b/tools/slang-reflection-test/main.cpp index 41a21eee8..ad474ee70 100644 --- a/tools/slang-reflection-test/main.cpp +++ b/tools/slang-reflection-test/main.cpp @@ -18,7 +18,8 @@ struct PrettyWriter static void writeRaw(PrettyWriter& writer, char const* begin, char const* end) { - Slang::AppContext::getStdOut().print("%.*s", int(end - begin), begin); + SLANG_ASSERT(end >= begin); + Slang::AppContext::getStdOut().write(begin, size_t(end - begin)); } static void writeRaw(PrettyWriter& writer, char const* begin) diff --git a/tools/slang-test/main.cpp b/tools/slang-test/main.cpp index 8a03a6100..774fbd6e2 100644 --- a/tools/slang-test/main.cpp +++ b/tools/slang-test/main.cpp @@ -13,6 +13,8 @@ using namespace Slang; #include "os.h" #include "render-api-util.h" #include "test-context.h" +#include "test-reporter.h" +#include "options.h" #define STB_IMAGE_IMPLEMENTATION #include "external/stb/stb_image.h" @@ -29,16 +31,6 @@ using namespace Slang; #include <stdlib.h> #include <stdarg.h> -// A category that a test can be tagged with -struct TestCategory -{ - // The name of the category, from the user perspective - String name; - - // The logical "super-category" of this category - TestCategory* parent; -}; - // Options for a particular test struct TestOptions { @@ -75,275 +67,10 @@ struct TestInput typedef TestResult(*TestCallback)(TestContext* context, TestInput& input); -struct Options -{ - char const* appName = "slang-test"; - - // Directory to use when looking for binaries to run - char const* binDir = ""; - - // only run test cases with names that have this prefix - char const* testPrefix = nullptr; - - // generate extra output (notably: command lines we run) - bool shouldBeVerbose = false; - - // force generation of baselines for HLSL tests - bool generateHLSLBaselines = false; - - // Dump expected/actual output on failures, for debugging. - // This is especially intended for use in continuous - // integration builds. - bool dumpOutputOnFailure = false; - - // If set, will force using of executables (not shared library) for tests - bool useExes = false; - - // kind of output to generate - TestOutputMode outputMode = TestOutputMode::Default; - - // Only run tests that match one of the given categories - Dictionary<TestCategory*, TestCategory*> includeCategories; - - // Exclude test that match one these categories - Dictionary<TestCategory*, TestCategory*> excludeCategories; - - // By default we can test against all apis - RenderApiFlags enabledApis = RenderApiFlag::AllOf; - - // By default we potentially synthesize test for all - // TODO: Vulkan is disabled by default for now as the majority as vulkan synthesized tests fail - RenderApiFlags synthesizedTestApis = RenderApiFlag::AllOf & ~RenderApiFlag::Vulkan; -}; - // Globals -static const Options g_defaultOptions; - -Options g_options; -Dictionary<String, TestCategory*> g_testCategories; -TestCategory* g_defaultTestCategory; - -// pre declare - -TestCategory* findTestCategory(String const& name); - /* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ - -Result parseOptions(int argc, char** argv, Slang::WriterHelper stdError) -{ - g_options = g_defaultOptions; - - List<const char*> positionalArgs; - - int argCount = argc; - char const* const* argCursor = argv; - char const* const* argEnd = argCursor + argCount; - - // first argument is the application name - if( argCursor != argEnd ) - { - g_options.appName = *argCursor++; - } - - // now iterate over arguments to collect options - while(argCursor != argEnd) - { - char const* arg = *argCursor++; - if( arg[0] != '-' ) - { - positionalArgs.Add(arg); - continue; - } - - if( strcmp(arg, "--") == 0 ) - { - while(argCursor != argEnd) - { - positionalArgs.Add(*argCursor++); - } - break; - } - - if( strcmp(arg, "-bindir") == 0 ) - { - if( argCursor == argEnd ) - { - stdError.print("error: expected operand for '%s'\n", arg); - return SLANG_FAIL; - } - g_options.binDir = *argCursor++; - } - else if (strcmp(arg, "-useexes") == 0) - { - g_options.useExes = true; - } - else if( strcmp(arg, "-v") == 0 ) - { - g_options.shouldBeVerbose = true; - } - else if( strcmp(arg, "-generate-hlsl-baselines") == 0 ) - { - g_options.generateHLSLBaselines = true; - } - else if( strcmp(arg, "-release") == 0 ) - { - // Assumed to be handle by .bat file that called us - } - else if( strcmp(arg, "-debug") == 0 ) - { - // Assumed to be handle by .bat file that called us - } - else if( strcmp(arg, "-configuration") == 0 ) - { - if( argCursor == argEnd ) - { - stdError.print("error: expected operand for '%s'\n", arg); - return SLANG_FAIL; - } - argCursor++; - // Assumed to be handle by .bat file that called us - } - else if( strcmp(arg, "-platform") == 0 ) - { - if( argCursor == argEnd ) - { - stdError.print("error: expected operand for '%s'\n", arg); - return SLANG_FAIL; - } - argCursor++; - // Assumed to be handle by .bat file that called us - } - else if( strcmp(arg, "-appveyor") == 0 ) - { - g_options.outputMode = TestOutputMode::AppVeyor; - g_options.dumpOutputOnFailure = true; - } - else if( strcmp(arg, "-travis") == 0 ) - { - g_options.outputMode = TestOutputMode::Travis; - g_options.dumpOutputOnFailure = true; - } - else if (strcmp(arg, "-xunit") == 0) - { - g_options.outputMode = TestOutputMode::XUnit; - } - else if (strcmp(arg, "-xunit2") == 0) - { - g_options.outputMode = TestOutputMode::XUnit2; - } - else if (strcmp(arg, "-teamcity") == 0) - { - g_options.outputMode = TestOutputMode::TeamCity; - } - else if( strcmp(arg, "-category") == 0 ) - { - if( argCursor == argEnd ) - { - stdError.print("error: expected operand for '%s'\n", arg); - return SLANG_FAIL; - } - auto category = findTestCategory(*argCursor++); - if(category) - { - g_options.includeCategories.Add(category, category); - } - } - else if( strcmp(arg, "-exclude") == 0 ) - { - if( argCursor == argEnd ) - { - stdError.print("error: expected operand for '%s'\n", arg); - return SLANG_FAIL; - } - auto category = findTestCategory(*argCursor++); - if(category) - { - g_options.excludeCategories.Add(category, category); - } - } - else if (strcmp(arg, "-api") == 0) - { - if (argCursor == argEnd) - { - stdError.print("error: expecting an api expression (eg 'vk+dx12' or '+dx11') '%s'\n", arg); - return SLANG_FAIL; - } - const char* apiList = *argCursor++; - - SlangResult res = RenderApiUtil::parseApiFlags(UnownedStringSlice(apiList), g_options.enabledApis, &g_options.enabledApis); - if (SLANG_FAILED(res)) - { - stdError.print("error: unable to parse api expression '%s'\n", apiList); - return res; - } - } - else if (strcmp(arg, "-synthesizedTestApi") == 0) - { - if (argCursor == argEnd) - { - stdError.print("error: expected an api expression (eg 'vk+dx12' or '+dx11') '%s'\n", arg); - return SLANG_FAIL; - } - const char* apiList = *argCursor++; - - SlangResult res = RenderApiUtil::parseApiFlags(UnownedStringSlice(apiList), g_options.synthesizedTestApis, &g_options.synthesizedTestApis); - if (SLANG_FAILED(res)) - { - stdError.print("error: unable to parse api expression '%s'\n", apiList); - return res; - } - } - else - { - stdError.print("unknown option '%s'\n", arg); - return SLANG_FAIL; - } - } - - { - // Find out what apis are available - const int availableApis = RenderApiUtil::getAvailableApis(); - // Only allow apis we know are available - g_options.enabledApis &= availableApis; - - // Can only synth for apis that are available - g_options.synthesizedTestApis &= g_options.enabledApis; - } - - - // first positional argument is source shader path - if (positionalArgs.Count()) - { - g_options.testPrefix = positionalArgs[0]; - positionalArgs.RemoveAt(0); - } - - // any remaining arguments represent an error - if (positionalArgs.Count() != 0) - { - stdError.print("unexpected arguments\n"); - return SLANG_FAIL; - } - - return SLANG_OK; -} - -// Called for an error in the test-runner (not for an error involving -// a test itself). -void error(char const* message, ...) -{ - fprintf(stderr, "error: "); - - va_list args; - va_start(args, message); - vfprintf(stderr, message, args); - va_end(args); - - fprintf(stderr, "\n"); -} - bool match(char const** ioCursor, char const* expected) { char const* cursor = *ioCursor; @@ -427,30 +154,10 @@ String collectRestOfLine(char const** ioCursor) return getString(textBegin, textEnd); } -TestCategory* addTestCategory(String const& name, TestCategory* parent) -{ - TestCategory* category = new TestCategory(); - category->name = name; - - category->parent = parent; - - g_testCategories.Add(name, category); - - return category; -} -TestCategory* findTestCategory(String const& name) -{ - TestCategory* category = nullptr; - if( !g_testCategories.TryGetValue(name, category) ) - { - error("unknown test category name '%s'\n", name.Buffer()); - return nullptr; - } - return category; -} TestResult gatherTestOptions( + TestCategorySet* categorySet, char const** ioCursor, FileTestList* testList) { @@ -481,12 +188,13 @@ TestResult gatherTestOptions( cursor++; auto categoryName = getString(categoryStart, categoryEnd); - TestCategory* category = findTestCategory(categoryName); + TestCategory* category = categorySet->find(categoryName); if(!category) { return TestResult::Fail; } + testOptions.categories.Add(category); @@ -510,7 +218,7 @@ TestResult gatherTestOptions( // If no categories were specified, then add the default category if(testOptions.categories.Count() == 0) { - testOptions.categories.Add(g_defaultTestCategory); + testOptions.categories.Add(categorySet->defaultCategory); } if(*cursor == ':') @@ -594,6 +302,7 @@ TestResult gatherTestOptions( // Try to read command-line options from the test file itself TestResult gatherTestsForFile( + TestCategorySet* categorySet, String filePath, FileTestList* testList) { @@ -624,7 +333,7 @@ TestResult gatherTestsForFile( } else if(match(&cursor, "//TEST")) { - if(gatherTestOptions(&cursor, testList) != TestResult::Pass) + if(gatherTestOptions(categorySet, &cursor, testList) != TestResult::Pass) return TestResult::Fail; } else @@ -640,17 +349,17 @@ OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpaw { SLANG_UNUSED(context); - if(context->m_isVerbose) + if(context->options.shouldBeVerbose) { String commandLine = spawner.getCommandLine(); - context->messageFormat(TestMessageType::Info, "%s\n", commandLine.begin()); + context->reporter->messageFormat(TestMessageType::Info, "%s\n", commandLine.begin()); } - if (!context->m_useExes) + if (!context->options.useExes) { String exeName = Path::GetFileNameWithoutEXT(spawner.executableName_); - auto func = context->getInnerMainFunc(String(g_options.binDir), exeName); + auto func = context->getInnerMainFunc(String(context->options.binDir), exeName); if (func) { StringBuilder stdErrorString; @@ -691,7 +400,7 @@ OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpaw if (err != kOSError_None) { // fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); - context->messageFormat(TestMessageType::RunError, "failed to run test '%S'", testPath.ToWString().begin()); + context->reporter->messageFormat(TestMessageType::RunError, "failed to run test '%S'", testPath.ToWString().begin()); } return err; } @@ -763,7 +472,7 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) OSProcessSpawner spawner; - spawner.pushExecutablePath(String(g_options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); spawner.pushArgument(filePath999); for( auto arg : input.testOptions->args ) @@ -800,7 +509,7 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); result = TestResult::Fail; } @@ -812,7 +521,7 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); } return result; @@ -820,12 +529,13 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) TestResult runReflectionTest(TestContext* context, TestInput& input) { + const auto& options = context->options; auto filePath = input.filePath; auto outputStem = input.outputStem; OSProcessSpawner spawner; - spawner.pushExecutablePath(String(g_options.binDir) + "slang-reflection-test" + osGetExecutableSuffix()); + spawner.pushExecutablePath(String(options.binDir) + "slang-reflection-test" + osGetExecutableSuffix()); spawner.pushArgument(filePath); for( auto arg : input.testOptions->args ) @@ -873,7 +583,7 @@ TestResult runReflectionTest(TestContext* context, TestInput& input) String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); } return result; @@ -932,8 +642,8 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) OSProcessSpawner actualSpawner; OSProcessSpawner expectedSpawner; - actualSpawner.pushExecutablePath(String(g_options.binDir) + "slangc" + osGetExecutableSuffix()); - expectedSpawner.pushExecutablePath(String(g_options.binDir) + "slangc" + osGetExecutableSuffix()); + actualSpawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); + expectedSpawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); actualSpawner.pushArgument(filePath); @@ -1022,7 +732,7 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); } return result; @@ -1036,7 +746,7 @@ TestResult generateHLSLBaseline(TestContext* context, TestInput& input) auto outputStem = input.outputStem; OSProcessSpawner spawner; - spawner.pushExecutablePath(String(g_options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); spawner.pushArgument(filePath999); for( auto arg : input.testOptions->args ) @@ -1082,7 +792,7 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) OSProcessSpawner spawner; - spawner.pushExecutablePath(String(g_options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); spawner.pushArgument(filePath999); for( auto arg : input.testOptions->args ) @@ -1164,7 +874,7 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); } return result; @@ -1183,7 +893,7 @@ TestResult doGLSLComparisonTestRun(TestContext* context, OSProcessSpawner spawner; - spawner.pushExecutablePath(String(g_options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); spawner.pushArgument(filePath999); if( langDefine ) @@ -1253,7 +963,7 @@ TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) if (actualOutput != expectedOutput) { - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); return TestResult::Fail; } @@ -1276,7 +986,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons OSProcessSpawner spawner; - spawner.pushExecutablePath(String(g_options.binDir) + "render-test" + osGetExecutableSuffix()); + spawner.pushExecutablePath(String(context->options.binDir) + "render-test" + osGetExecutableSuffix()); spawner.pushArgument(filePath999); for (auto arg : input.testOptions->args) @@ -1305,7 +1015,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons auto expectedOutput = getExpectedOutput(outputStem); if (actualOutput != expectedOutput) { - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1330,7 +1040,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons auto referenceProgramOutput = Split(File::ReadAllText(referenceOutput), '\n'); auto printOutput = [&]() { - context->messageFormat(TestMessageType::TestFailure, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); + context->reporter->messageFormat(TestMessageType::TestFailure, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); }; if (actualProgramOutput.Count() < referenceProgramOutput.Count()) { @@ -1390,7 +1100,7 @@ TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, cha OSProcessSpawner spawner; - spawner.pushExecutablePath(String(g_options.binDir) + "render-test" + osGetExecutableSuffix()); + spawner.pushExecutablePath(String(context->options.binDir) + "render-test" + osGetExecutableSuffix()); spawner.pushArgument(filePath); for( auto arg : input.testOptions->args ) @@ -1495,6 +1205,8 @@ bool STBImage::isComparable(const ThisType& rhs) const TestResult doImageComparison(TestContext* context, String const& filePath) { + auto reporter = context->reporter; + // Allow a difference in the low bits of the 8-bit result, just to play it safe static const int kAbsoluteDiffCutoff = 2; @@ -1507,20 +1219,20 @@ TestResult doImageComparison(TestContext* context, String const& filePath) STBImage expectedImage; if (SLANG_FAILED(expectedImage.read(expectedPath.Buffer()))) { - context->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", expectedPath.Buffer()); + reporter->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", expectedPath.Buffer()); return TestResult::Fail; } STBImage actualImage; if (SLANG_FAILED(actualImage.read(actualPath.Buffer()))) { - context->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", actualPath.Buffer()); + reporter->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", actualPath.Buffer()); return TestResult::Fail; } if (!expectedImage.isComparable(actualImage)) { - context->messageFormat(TestMessageType::TestFailure, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); + reporter->messageFormat(TestMessageType::TestFailure, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); return TestResult::Fail; } @@ -1568,7 +1280,7 @@ TestResult doImageComparison(TestContext* context, String const& filePath) const int x = i / numChannels; const int channelIndex = i % numChannels; - context->messageFormat(TestMessageType::TestFailure, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", + reporter->messageFormat(TestMessageType::TestFailure, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", x, y, channelIndex, expectedVal, actualVal, @@ -1610,7 +1322,7 @@ TestResult runHLSLRenderComparisonTestImpl( if (actualOutput != expectedOutput) { - context->dumpOutputDifference(expectedOutput, actualOutput); + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); return TestResult::Fail; } @@ -1644,7 +1356,6 @@ TestResult skipTest(TestContext* /* context */, TestInput& /*input*/) return TestResult::Ignored; } - static bool hasRenderOption(RenderApiType apiType, const List<String>& options) { const RenderApiUtil::Info& info = RenderApiUtil::getInfo(apiType); @@ -1721,14 +1432,14 @@ bool isRenderTest(const String& command) command == "COMPARE_HLSL_GLSL_RENDER"; } -static bool canIgnoreTestWithDisabledRenderer(const TestOptions& testOptions) +static bool canIgnoreTestWithDisabledRenderer(const TestOptions& testOptions, const Options& appOptions) { for (int i = 0; i < int(RenderApiType::CountOf); ++i) { RenderApiType apiType = RenderApiType(i); RenderApiFlag::Enum apiFlag = RenderApiFlag::Enum(1 << i); - if (hasRenderOption(apiType, testOptions) && (g_options.enabledApis & apiFlag) == 0) + if (hasRenderOption(apiType, testOptions) && (appOptions.enabledApis & apiFlag) == 0) { return true; } @@ -1745,7 +1456,7 @@ TestResult runTest( FileTestList const& testList) { // If this test can be ignored - if (canIgnoreTestWithDisabledRenderer(testOptions)) + if (canIgnoreTestWithDisabledRenderer(testOptions, context->options)) { return TestResult::Ignored; } @@ -1798,10 +1509,11 @@ TestResult runTest( testInput.testList = &testList; { - TestContext::TestScope scope(context, outputStem); + TestReporter* reporter = context->reporter; + TestReporter::TestScope scope(reporter, outputStem); TestResult testResult = ii->callback(context, testInput); - context->addResult(testResult); + reporter->addResult(testResult); return testResult; } @@ -1840,20 +1552,20 @@ bool testCategoryMatches( } bool testPassesCategoryMask( - TestContext* /*context*/, + TestContext* context, TestOptions const& test) { // Don't include a test we should filter out for( auto testCategory : test.categories ) { - if(testCategoryMatches(testCategory, g_options.excludeCategories)) + if(testCategoryMatches(testCategory, context->options.excludeCategories)) return false; } // Otherwise inclue any test the user asked for for( auto testCategory : test.categories ) { - if(testCategoryMatches(testCategory, g_options.includeCategories)) + if(testCategoryMatches(testCategory, context->options.includeCategories)) return true; } @@ -1868,7 +1580,7 @@ void runTestsOnFile( // Gather a list of tests to run FileTestList testList; - if( gatherTestsForFile(filePath, &testList) == TestResult::Ignored ) + if( gatherTestsForFile(&context->categorySet, filePath, &testList) == TestResult::Ignored ) { // Test was explicitly ignored return; @@ -1877,14 +1589,14 @@ void runTestsOnFile( // Note cases where a test file exists, but we found nothing to run if( testList.tests.Count() == 0 ) { - context->addTest(filePath, TestResult::Ignored); + context->reporter->addTest(filePath, TestResult::Ignored); return; } List<TestOptions> synthesizedTests; // If dx12 is available synthesize Dx12 test - if ((g_options.synthesizedTestApis & RenderApiFlag::D3D12) != 0) + if ((context->options.synthesizedTestApis & RenderApiFlag::D3D12) != 0) { // If doesn't have option generate dx12 options from dx11 if (!hasRenderOption(RenderApiType::D3D12, testList)) @@ -1907,7 +1619,7 @@ void runTestsOnFile( } // If Vulkan is available synthesize Vulkan test - if ((g_options.synthesizedTestApis & RenderApiFlag::Vulkan) != 0) + if ((context->options.synthesizedTestApis & RenderApiFlag::Vulkan) != 0) { // If doesn't have option generate dx12 options from dx11 if (!hasRenderOption(RenderApiType::Vulkan, testList)) @@ -2004,9 +1716,9 @@ static bool shouldRunTest( if(!endsWithAllowedExtension(context, filePath)) return false; - if( g_options.testPrefix ) + if( context->options.testPrefix ) { - if( strncmp(g_options.testPrefix, filePath.begin(), strlen(g_options.testPrefix)) != 0 ) + if( strncmp(context->options.testPrefix, filePath.begin(), strlen(context->options.testPrefix)) != 0 ) { return false; } @@ -2033,74 +1745,57 @@ void runTestsInDirectory( } } - -// - -int main( - int argcIn, - char** argvIn) +SlangResult innerMain(int argc, char** argv) { AppContext::initDefault(); - // Set up our test categories here + // The context holds useful things used during testing + TestContext context; + auto& categorySet = context.categorySet; - auto fullTestCategory = addTestCategory("full", nullptr); - - auto quickTestCategory = addTestCategory("quick", fullTestCategory); - - /*auto smokeTestCategory = */addTestCategory("smoke", quickTestCategory); - - auto renderTestCategory = addTestCategory("render", fullTestCategory); - - /*auto computeTestCategory = */addTestCategory("compute", fullTestCategory); - - auto vulkanTestCategory = addTestCategory("vulkan", fullTestCategory); - - auto unitTestCatagory = addTestCategory("unit-test", fullTestCategory); - - auto compatibilityIssueCatagory = addTestCategory("compatibility-issue", fullTestCategory); + // Set up our test categories here + auto fullTestCategory = categorySet.add("full", nullptr); + auto quickTestCategory = categorySet.add("quick", fullTestCategory); + /*auto smokeTestCategory = */categorySet.add("smoke", quickTestCategory); + auto renderTestCategory = categorySet.add("render", fullTestCategory); + /*auto computeTestCategory = */categorySet.add("compute", fullTestCategory); + auto vulkanTestCategory = categorySet.add("vulkan", fullTestCategory); + auto unitTestCatagory = categorySet.add("unit-test", fullTestCategory); + auto compatibilityIssueCatagory = categorySet.add("compatibility-issue", fullTestCategory); // An un-categorized test will always belong to the `full` category - g_defaultTestCategory = fullTestCategory; + categorySet.defaultCategory = fullTestCategory; - // + SLANG_RETURN_ON_FAIL(Options::parse(argc, argv, &categorySet, AppContext::getStdError(), &context.options)); + SLANG_RETURN_ON_FAIL(SLANG_FAILED(context.init())) - if (SLANG_FAILED(parseOptions(argcIn, argvIn, AppContext::getStdError()))) + Options& options = context.options; + if( options.includeCategories.Count() == 0 ) { - // Return exit code with error - return 1; - } - - if( g_options.includeCategories.Count() == 0 ) - { - g_options.includeCategories.Add(fullTestCategory, fullTestCategory); + options.includeCategories.Add(fullTestCategory, fullTestCategory); } // Exclude rendering tests when building under AppVeyor. // // TODO: this is very ad hoc, and we should do something cleaner. - if( g_options.outputMode == TestOutputMode::AppVeyor ) + if( options.outputMode == TestOutputMode::AppVeyor ) { - g_options.excludeCategories.Add(renderTestCategory, renderTestCategory); - g_options.excludeCategories.Add(vulkanTestCategory, vulkanTestCategory); + options.excludeCategories.Add(renderTestCategory, renderTestCategory); + options.excludeCategories.Add(vulkanTestCategory, vulkanTestCategory); } - int returnCode = 0; { - // Setup the context - TestContext context; - if (SLANG_FAILED(context.init(g_options.outputMode))) - { - // Unable to initialize context - return 1; - } + // Setup the reporter + TestReporter reporter; + SLANG_RETURN_ON_FAIL(reporter.init(options.outputMode)); + + context.reporter = &reporter; - context.m_dumpOutputOnFailure = g_options.dumpOutputOnFailure; - context.m_useExes = g_options.useExes; - context.m_isVerbose = g_options.shouldBeVerbose; + reporter.m_dumpOutputOnFailure = options.dumpOutputOnFailure; + reporter.m_isVerbose = options.shouldBeVerbose; { - TestContext::SuiteScope suiteScope(&context, "tests"); + TestReporter::SuiteScope suiteScope(&reporter, "tests"); // Enumerate test files according to policy // TODO: add more directories to this list // TODO: allow for a command-line argument to select a particular directory @@ -2110,8 +1805,8 @@ int main( // Run the unit tests (these are internal C++ tests - not specified via files in a directory) // They are registered with SLANG_UNIT_TEST macro { - TestContext::SuiteScope suiteScope(&context, "unit tests"); - TestContext::set(&context); + TestReporter::SuiteScope suiteScope(&reporter, "unit tests"); + TestReporter::set(&reporter); // Run the unit tests TestRegister* cur = TestRegister::s_first; @@ -2128,14 +1823,14 @@ int main( { if (testPassesCategoryMask(&context, testOptions)) { - context.startTest(testOptions.command); + reporter.startTest(testOptions.command); // Run the test function cur->m_func(); - context.endTest(); + reporter.endTest(); } else { - context.addTest(testOptions.command, TestResult::Ignored); + reporter.addTest(testOptions.command, TestResult::Ignored); } } @@ -2143,17 +1838,19 @@ int main( cur = cur->m_next; } - TestContext::set(nullptr); + TestReporter::set(nullptr); } - context.outputSummary(); - - returnCode = context.didAllSucceed() ? 0 : 1; + reporter.outputSummary(); + return reporter.didAllSucceed() ? SLANG_OK : SLANG_FAIL; } +} +int main(int argc, char** argv) +{ + const SlangResult res = innerMain(argc, argv); #ifdef _MSC_VER _CrtDumpMemoryLeaks(); #endif - - return returnCode; + return SLANG_SUCCEEDED(res) ? 0 : 1; } diff --git a/tools/slang-test/options.cpp b/tools/slang-test/options.cpp new file mode 100644 index 000000000..cb6adbc6f --- /dev/null +++ b/tools/slang-test/options.cpp @@ -0,0 +1,245 @@ +// test-context.cpp +#include "options.h" + +#include "os.h" +#include "../../source/core/slang-string-util.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +using namespace Slang; + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! CategorySet !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ + +TestCategory* TestCategorySet::add(String const& name, TestCategory* parent) +{ + RefPtr<TestCategory> category(new TestCategory); + category->name = name; + category->parent = parent; + + m_categoryMap.Add(name, category); + return category; +} + +TestCategory* TestCategorySet::find(String const& name) +{ + RefPtr<TestCategory> category; + if (!m_categoryMap.TryGetValue(name, category)) + { + return nullptr; + } + return category; +} + +TestCategory* TestCategorySet::findOrError(String const& name) +{ + TestCategory* category = find(name); + if (!category) + { + AppContext::getStdError().print("error: unknown test category name '%s'\n", name.Buffer()); + } + return category; +} + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Options !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ + +/* static */Result Options::parse(int argc, char** argv, TestCategorySet* categorySet, Slang::WriterHelper stdError, Options* optionsOut) +{ + // Reset the options + *optionsOut = Options(); + + List<const char*> positionalArgs; + + int argCount = argc; + char const* const* argCursor = argv; + char const* const* argEnd = argCursor + argCount; + + // first argument is the application name + if (argCursor != argEnd) + { + optionsOut->appName = *argCursor++; + } + + // now iterate over arguments to collect options + while (argCursor != argEnd) + { + char const* arg = *argCursor++; + if (arg[0] != '-') + { + positionalArgs.Add(arg); + continue; + } + + if (strcmp(arg, "--") == 0) + { + while (argCursor != argEnd) + { + positionalArgs.Add(*argCursor++); + } + break; + } + + if (strcmp(arg, "-bindir") == 0) + { + if (argCursor == argEnd) + { + stdError.print("error: expected operand for '%s'\n", arg); + return SLANG_FAIL; + } + optionsOut->binDir = *argCursor++; + } + else if (strcmp(arg, "-useexes") == 0) + { + optionsOut->useExes = true; + } + else if (strcmp(arg, "-v") == 0) + { + optionsOut->shouldBeVerbose = true; + } + else if (strcmp(arg, "-generate-hlsl-baselines") == 0) + { + optionsOut->generateHLSLBaselines = true; + } + else if (strcmp(arg, "-release") == 0) + { + // Assumed to be handle by .bat file that called us + } + else if (strcmp(arg, "-debug") == 0) + { + // Assumed to be handle by .bat file that called us + } + else if (strcmp(arg, "-configuration") == 0) + { + if (argCursor == argEnd) + { + stdError.print("error: expected operand for '%s'\n", arg); + return SLANG_FAIL; + } + argCursor++; + // Assumed to be handle by .bat file that called us + } + else if (strcmp(arg, "-platform") == 0) + { + if (argCursor == argEnd) + { + stdError.print("error: expected operand for '%s'\n", arg); + return SLANG_FAIL; + } + argCursor++; + // Assumed to be handle by .bat file that called us + } + else if (strcmp(arg, "-appveyor") == 0) + { + optionsOut->outputMode = TestOutputMode::AppVeyor; + optionsOut->dumpOutputOnFailure = true; + } + else if (strcmp(arg, "-travis") == 0) + { + optionsOut->outputMode = TestOutputMode::Travis; + optionsOut->dumpOutputOnFailure = true; + } + else if (strcmp(arg, "-xunit") == 0) + { + optionsOut->outputMode = TestOutputMode::XUnit; + } + else if (strcmp(arg, "-xunit2") == 0) + { + optionsOut->outputMode = TestOutputMode::XUnit2; + } + else if (strcmp(arg, "-teamcity") == 0) + { + optionsOut->outputMode = TestOutputMode::TeamCity; + } + else if (strcmp(arg, "-category") == 0) + { + if (argCursor == argEnd) + { + stdError.print("error: expected operand for '%s'\n", arg); + return SLANG_FAIL; + } + auto category = categorySet->findOrError(*argCursor++); + if (category) + { + optionsOut->includeCategories.Add(category, category); + } + } + else if (strcmp(arg, "-exclude") == 0) + { + if (argCursor == argEnd) + { + stdError.print("error: expected operand for '%s'\n", arg); + return SLANG_FAIL; + } + auto category = categorySet->findOrError(*argCursor++); + if (category) + { + optionsOut->excludeCategories.Add(category, category); + } + } + else if (strcmp(arg, "-api") == 0) + { + if (argCursor == argEnd) + { + stdError.print("error: expecting an api expression (eg 'vk+dx12' or '+dx11') '%s'\n", arg); + return SLANG_FAIL; + } + const char* apiList = *argCursor++; + + SlangResult res = RenderApiUtil::parseApiFlags(UnownedStringSlice(apiList), optionsOut->enabledApis, &optionsOut->enabledApis); + if (SLANG_FAILED(res)) + { + stdError.print("error: unable to parse api expression '%s'\n", apiList); + return res; + } + } + else if (strcmp(arg, "-synthesizedTestApi") == 0) + { + if (argCursor == argEnd) + { + stdError.print("error: expected an api expression (eg 'vk+dx12' or '+dx11') '%s'\n", arg); + return SLANG_FAIL; + } + const char* apiList = *argCursor++; + + SlangResult res = RenderApiUtil::parseApiFlags(UnownedStringSlice(apiList), optionsOut->synthesizedTestApis, &optionsOut->synthesizedTestApis); + if (SLANG_FAILED(res)) + { + stdError.print("error: unable to parse api expression '%s'\n", apiList); + return res; + } + } + else + { + stdError.print("unknown option '%s'\n", arg); + return SLANG_FAIL; + } + } + + { + // Find out what apis are available + const int availableApis = RenderApiUtil::getAvailableApis(); + // Only allow apis we know are available + optionsOut->enabledApis &= availableApis; + + // Can only synth for apis that are available + optionsOut->synthesizedTestApis &= optionsOut->enabledApis; + } + + + // first positional argument is source shader path + if (positionalArgs.Count()) + { + optionsOut->testPrefix = positionalArgs[0]; + positionalArgs.RemoveAt(0); + } + + // any remaining arguments represent an error + if (positionalArgs.Count() != 0) + { + stdError.print("unexpected arguments\n"); + return SLANG_FAIL; + } + + return SLANG_OK; +} diff --git a/tools/slang-test/options.h b/tools/slang-test/options.h new file mode 100644 index 000000000..ccbd9aede --- /dev/null +++ b/tools/slang-test/options.h @@ -0,0 +1,83 @@ +// options.h + +#ifndef OPTIONS_H_INCLUDED +#define OPTIONS_H_INCLUDED + +#include "../../source/core/dictionary.h" + +#include "test-reporter.h" +#include "render-api-util.h" +#include "../../source/core/smart-pointer.h" + +// A category that a test can be tagged with +struct TestCategory: public Slang::RefObject +{ + // The name of the category, from the user perspective + Slang::String name; + + // The logical "super-category" of this category + TestCategory* parent; +}; + +struct TestCategorySet +{ +public: + /// Find a category with the specified name. Returns nullptr if not found + TestCategory * find(Slang::String const& name); + /// Adds a category with the specified name, and parent. Returns the category object. + /// Parent can be nullptr + TestCategory* add(Slang::String const& name, TestCategory* parent); + /// Finds a category by name, else reports and writes an error + TestCategory* findOrError(Slang::String const& name); + + Slang::RefPtr<TestCategory> defaultCategory; ///< The default category + +protected: + Slang::Dictionary<Slang::String, Slang::RefPtr<TestCategory> > m_categoryMap; +}; + +struct Options +{ + char const* appName = "slang-test"; + + // Directory to use when looking for binaries to run + char const* binDir = ""; + + // only run test cases with names that have this prefix + char const* testPrefix = nullptr; + + // generate extra output (notably: command lines we run) + bool shouldBeVerbose = false; + + // force generation of baselines for HLSL tests + bool generateHLSLBaselines = false; + + // Dump expected/actual output on failures, for debugging. + // This is especially intended for use in continuous + // integration builds. + bool dumpOutputOnFailure = false; + + // If set, will force using of executables (not shared library) for tests + bool useExes = false; + + // kind of output to generate + TestOutputMode outputMode = TestOutputMode::Default; + + // Only run tests that match one of the given categories + Slang::Dictionary<TestCategory*, TestCategory*> includeCategories; + + // Exclude test that match one these categories + Slang::Dictionary<TestCategory*, TestCategory*> excludeCategories; + + // By default we can test against all apis + RenderApiFlags enabledApis = RenderApiFlag::AllOf; + + // By default we potentially synthesize test for all + // TODO: Vulkan is disabled by default for now as the majority as vulkan synthesized tests fail + RenderApiFlags synthesizedTestApis = RenderApiFlag::AllOf & ~RenderApiFlag::Vulkan; + + /// Parse the args, report any errors into stdError, and write the results into optionsOut + static SlangResult parse(int argc, char** argv, TestCategorySet* categorySet, Slang::WriterHelper stdError, Options* optionsOut); +}; + +#endif // OPTIONS_H_INCLUDED diff --git a/tools/slang-test/slang-test.vcxproj b/tools/slang-test/slang-test.vcxproj index 13bf42141..82c3e652a 100644 --- a/tools/slang-test/slang-test.vcxproj +++ b/tools/slang-test/slang-test.vcxproj @@ -162,15 +162,19 @@ </Link> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="options.h" /> <ClInclude Include="os.h" /> <ClInclude Include="render-api-util.h" /> <ClInclude Include="test-context.h" /> + <ClInclude Include="test-reporter.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="main.cpp" /> + <ClCompile Include="options.cpp" /> <ClCompile Include="os.cpp" /> <ClCompile Include="render-api-util.cpp" /> <ClCompile Include="test-context.cpp" /> + <ClCompile Include="test-reporter.cpp" /> <ClCompile Include="unit-test-byte-encode.cpp" /> <ClCompile Include="unit-test-free-list.cpp" /> <ClCompile Include="unit-test-memory-arena.cpp" /> diff --git a/tools/slang-test/slang-test.vcxproj.filters b/tools/slang-test/slang-test.vcxproj.filters index fc65986f4..24ba365a7 100644 --- a/tools/slang-test/slang-test.vcxproj.filters +++ b/tools/slang-test/slang-test.vcxproj.filters @@ -9,6 +9,9 @@ </Filter> </ItemGroup> <ItemGroup> + <ClInclude Include="options.h"> + <Filter>Header Files</Filter> + </ClInclude> <ClInclude Include="os.h"> <Filter>Header Files</Filter> </ClInclude> @@ -18,11 +21,17 @@ <ClInclude Include="test-context.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="test-reporter.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="main.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="options.cpp"> + <Filter>Source Files</Filter> + </ClCompile> <ClCompile Include="os.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -32,6 +41,9 @@ <ClCompile Include="test-context.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="test-reporter.cpp"> + <Filter>Source Files</Filter> + </ClCompile> <ClCompile Include="unit-test-byte-encode.cpp"> <Filter>Source Files</Filter> </ClCompile> diff --git a/tools/slang-test/test-context.cpp b/tools/slang-test/test-context.cpp index efbbb4f65..06a9847bf 100644 --- a/tools/slang-test/test-context.cpp +++ b/tools/slang-test/test-context.cpp @@ -10,93 +10,18 @@ using namespace Slang; -/* static */TestContext* TestContext::s_context = nullptr; -/* static */TestRegister* TestRegister::s_first; - -static void appendXmlEncode(char c, StringBuilder& out) -{ - switch (c) - { - case '&': out << "&"; break; - case '<': out << "<"; break; - case '>': out << ">"; break; - case '\'': out << "'"; break; - case '"': out << """; break; - default: out.Append(c); - } -} - -static bool isXmlEncodeChar(char c) -{ - switch (c) - { - case '&': - case '<': - case '>': - { - return true; - } - } - return false; -} - -static void appendXmlEncode(const String& in, StringBuilder& out) -{ - const char* cur = in.Buffer(); - const char* end = cur + in.Length(); - - while (cur < end) - { - const char* start = cur; - // Look for a run of non encoded - while (cur < end && !isXmlEncodeChar(*cur)) - { - cur++; - } - // Write it - if (cur > start) - { - out.Append(start, UInt(end - start)); - } - - // if not at the end, we must be on an xml encoded character, so just output it xml encoded. - if (cur < end) - { - const char encodeChar = *cur++; - assert(isXmlEncodeChar(encodeChar)); - appendXmlEncode(encodeChar, out); - } - } -} - -TestContext::TestContext() : - m_outputMode(TestOutputMode::Default) +TestContext::TestContext() { - m_totalTestCount = 0; - m_passedTestCount = 0; - m_failedTestCount = 0; - m_ignoredTestCount = 0; - - m_maxFailTestResults = 10; - - m_inTest = false; - m_dumpOutputOnFailure = false; - m_isVerbose = false; - m_useExes = false; - m_session = nullptr; } -Result TestContext::init(TestOutputMode outputMode) +Result TestContext::init() { - m_outputMode = outputMode; - m_session = spCreateSession(nullptr); if (!m_session) { return SLANG_FAIL; } - return SLANG_OK; } @@ -108,379 +33,6 @@ TestContext::~TestContext() } } -bool TestContext::canWriteStdError() const -{ - switch (m_outputMode) - { - case TestOutputMode::XUnit: - case TestOutputMode::XUnit2: - { - return false; - } - default: return true; - } -} - -void TestContext::startTest(const String& testName) -{ - // Must be in a suite - assert(m_suiteStack.Count()); - assert(!m_inTest); - - m_inTest = true; - - m_numCurrentResults = 0; - m_numFailResults = 0; - - m_currentInfo = TestInfo(); - m_currentInfo.name = testName; - m_currentMessage.Clear(); -} - -void TestContext::endTest() -{ - assert(m_suiteStack.Count()); - assert(m_inTest); - - m_currentInfo.message = m_currentMessage; - - _addResult(m_currentInfo); - - m_inTest = false; -} - -void TestContext::addResult(TestResult result) -{ - assert(m_inTest); - - m_currentInfo.testResult = combine(m_currentInfo.testResult, result); - m_numCurrentResults++; -} - -void TestContext::addResultWithLocation(TestResult result, const char* testText, const char* file, int line) -{ - assert(m_inTest); - m_numCurrentResults++; - - m_currentInfo.testResult = combine(m_currentInfo.testResult, result); - if (result != TestResult::Fail) - { - // We don't need to output the result if it - return; - } - - m_numFailResults++; - - if (m_maxFailTestResults > 0) - { - if (m_numFailResults > m_maxFailTestResults) - { - if (m_numFailResults == m_maxFailTestResults + 1) - { - // It's a failure, but to show that there are more than are going to be shown, just show '...' - message(TestMessageType::TestFailure, "..."); - } - return; - } - } - - StringBuilder buf; - buf << testText << " - " << file << " (" << line << ")"; - - message(TestMessageType::TestFailure, buf); -} - -void TestContext::addResultWithLocation(bool testSucceeded, const char* testText, const char* file, int line) -{ - addResultWithLocation(testSucceeded ? TestResult::Pass : TestResult::Fail, testText, file, line); -} - -TestResult TestContext::addTest(const String& testName, bool isPass) -{ - const TestResult res = isPass ? TestResult::Pass : TestResult::Fail; - addTest(testName, res); - return res; -} - -void TestContext::dumpOutputDifference(const String& expectedOutput, const String& actualOutput) -{ - StringBuilder builder; - - StringUtil::appendFormat(builder, - "ERROR:\n" - "EXPECTED{{{\n%s}}}\n" - "ACTUAL{{{\n%s}}}\n", - expectedOutput.Buffer(), - actualOutput.Buffer()); - - - if (m_dumpOutputOnFailure && canWriteStdError()) - { - fprintf(stderr, "%s", builder.Buffer()); - fflush(stderr); - } - - // Add to the m_currentInfo - message(TestMessageType::TestFailure, builder); -} - -static char _getTeamCityEscapeChar(char c) -{ - switch (c) - { - case '|': return '|'; - case '\'': return '\''; - case '\n': return 'n'; - case '\r': return 'r'; - case '[': return '['; - case ']': return ']'; - default: return 0; - } -} - -static void _appendEncodedTeamCityString(const UnownedStringSlice& in, StringBuilder& builder) -{ - const char* start = in.begin(); - const char* cur = start; - const char* end = in.end(); - - for (const char* cur = start; cur < end; cur++) - { - const char c = *cur; - const char escapeChar = _getTeamCityEscapeChar(c); - if (escapeChar) - { - // Flush - if (cur > start) - { - builder.Append(start, UInt(cur - start)); - } - - builder.Append('|'); - builder.Append(escapeChar); - start = cur + 1; - } - } - - // Flush the end - if (end > start) - { - builder.Append(start, UInt(end - start)); - } -} - -void TestContext::_addResult(const TestInfo& info) -{ - m_totalTestCount++; - - switch (info.testResult) - { - case TestResult::Fail: - m_failedTestCount++; - break; - - case TestResult::Pass: - m_passedTestCount++; - break; - - case TestResult::Ignored: - m_ignoredTestCount++; - break; - - default: - assert(!"unexpected"); - break; - } - - m_testInfos.Add(info); - - // printf("OUTPUT_MODE: %d\n", options.outputMode); - switch (m_outputMode) - { - default: - { - char const* resultString = "UNEXPECTED"; - switch (info.testResult) - { - case TestResult::Fail: resultString = "FAILED"; break; - case TestResult::Pass: resultString = "passed"; break; - case TestResult::Ignored: resultString = "ignored"; break; - default: - assert(!"unexpected"); - break; - } - printf("%s test: '%S'\n", resultString, info.name.ToWString().begin()); - break; - } - case TestOutputMode::TeamCity: - { - StringBuilder escapedTestName; - _appendEncodedTeamCityString(info.name.getUnownedSlice(), escapedTestName); - - printf("##teamcity[testStarted name='%s']\n", escapedTestName.begin()); - - switch (info.testResult) - { - case TestResult::Fail: - { - if (info.message.Length()) - { - StringBuilder escapedMessage; - _appendEncodedTeamCityString(info.message.getUnownedSlice(), escapedMessage); - printf("##teamcity[testFailed name='%s' message='%s']\n", escapedTestName.begin(), escapedMessage.begin()); - } - else - { - printf("##teamcity[testFailed name='%s']\n", escapedTestName.begin()); - } - break; - } - case TestResult::Pass: - { - if (info.message.Length()) - { - StringBuilder escapedMessage; - _appendEncodedTeamCityString(info.message.getUnownedSlice(), escapedMessage); - printf("##teamcity[testStdOut name='%s' out='%s']\n", escapedTestName.begin(), escapedMessage.begin()); - } - break; - } - case TestResult::Ignored: - { - if (info.message.Length()) - { - StringBuilder escapedMessage; - _appendEncodedTeamCityString(info.message.getUnownedSlice(), escapedMessage); - - printf("##teamcity[testIgnored name='%s' message='%s']\n", escapedTestName.begin(), escapedMessage.begin()); - } - else - { - printf("##teamcity[testIgnored name='%s']\n", escapedTestName.begin()); - } - break; - } - default: - assert(!"unexpected"); - break; - } - - printf("##teamcity[testFinished name='%s']\n", escapedTestName.begin()); - fflush(stdout); - break; - } - case TestOutputMode::XUnit2: - case TestOutputMode::XUnit: - { - // Don't output anything -> we'll output all in one go at the end - break; - } - case TestOutputMode::AppVeyor: - { - char const* resultString = "None"; - switch (info.testResult) - { - case TestResult::Fail: resultString = "Failed"; break; - case TestResult::Pass: resultString = "Passed"; break; - case TestResult::Ignored: resultString = "Ignored"; break; - default: - assert(!"unexpected"); - break; - } - - OSProcessSpawner spawner; - spawner.pushExecutableName("appveyor"); - spawner.pushArgument("AddTest"); - spawner.pushArgument(info.name); - spawner.pushArgument("-FileName"); - // TODO: this isn't actually a file name in all cases - spawner.pushArgument(info.name); - spawner.pushArgument("-Framework"); - spawner.pushArgument("slang-test"); - spawner.pushArgument("-Outcome"); - spawner.pushArgument(resultString); - - auto err = spawner.spawnAndWaitForCompletion(); - - if (err != kOSError_None) - { - messageFormat(TestMessageType::Info, "failed to add appveyor test results for '%S'\n", info.name.ToWString().begin()); - -#if 0 - fprintf(stderr, "[%d] TEST RESULT: %s {%d} {%s} {%s}\n", err, spawner.commandLine_.Buffer(), - spawner.getResultCode(), - spawner.getStandardOutput().begin(), - spawner.getStandardError().begin()); -#endif - } - - break; - } - } -} - -void TestContext::addTest(const String& testName, TestResult testResult) -{ - // Can't add this way if in test - assert(!m_inTest); - - TestInfo info; - info.name = testName; - info.testResult = testResult; - _addResult(info); -} - -void TestContext::message(TestMessageType type, const String& message) -{ - if (type == TestMessageType::Info) - { - if (m_isVerbose && canWriteStdError()) - { - fputs(message.Buffer(), stderr); - } - - // Just dump out if can dump out - return; - } - - if (canWriteStdError()) - { - if (type == TestMessageType::RunError || type == TestMessageType::TestFailure) - { - fprintf(stderr, "error: "); - fputs(message.Buffer(), stderr); - fprintf(stderr, "\n"); - } - else - { - fputs(message.Buffer(), stderr); - } - } - - if (m_currentMessage.Length() > 0) - { - m_currentMessage << "\n"; - } - m_currentMessage.Append(message); -} - -void TestContext::messageFormat(TestMessageType type, char const* format, ...) -{ - StringBuilder builder; - - va_list args; - va_start(args, format); - StringUtil::append(format, args, builder); - va_end(args); - - message(type, builder); -} - -bool TestContext::didAllSucceed() const -{ - return m_passedTestCount == (m_totalTestCount - m_ignoredTestCount); -} - TestContext::InnerMainFunc TestContext::getInnerMainFunc(const String& dirPath, const String& name) { { @@ -509,149 +61,3 @@ TestContext::InnerMainFunc TestContext::getInnerMainFunc(const String& dirPath, m_sharedLibTools.Add(name, tool); return tool.m_func; } - -void TestContext::outputSummary() -{ - auto passCount = m_passedTestCount; - auto rawTotal = m_totalTestCount; - auto ignoredCount = m_ignoredTestCount; - - auto runTotal = rawTotal - ignoredCount; - - switch (m_outputMode) - { - default: - { - if (!m_totalTestCount) - { - printf("no tests run\n"); - return; - } - - int percentPassed = 0; - if (runTotal > 0) - { - percentPassed = (passCount * 100) / runTotal; - } - - printf("\n===\n%d%% of tests passed (%d/%d)", percentPassed, passCount, runTotal); - if (ignoredCount) - { - printf(", %d tests ignored", ignoredCount); - } - printf("\n===\n\n"); - - if (m_failedTestCount) - { - printf("failing tests:\n"); - printf("---\n"); - for (const auto& testInfo : m_testInfos) - { - if (testInfo.testResult == TestResult::Fail) - { - printf("%s\n", testInfo.name.Buffer()); - } - } - printf("---\n"); - } - break; - } - - case TestOutputMode::XUnit: - { - // xUnit 1.0 format - - printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); - printf("<testsuites tests=\"%d\" failures=\"%d\" disabled=\"%d\" errors=\"0\" name=\"AllTests\">\n", m_totalTestCount, m_failedTestCount, m_ignoredTestCount); - printf(" <testsuite name=\"all\" tests=\"%d\" failures=\"%d\" disabled=\"%d\" errors=\"0\" time=\"0\">\n", m_totalTestCount, m_failedTestCount, m_ignoredTestCount); - - for (const auto& testInfo : m_testInfos) - { - const int numFailed = (testInfo.testResult == TestResult::Fail); - const int numIgnored = (testInfo.testResult == TestResult::Ignored); - //int numPassed = (testInfo.testResult == TestResult::ePass); - - if (testInfo.testResult == TestResult::Pass) - { - printf(" <testcase name=\"%s\" status=\"run\"/>\n", testInfo.name.Buffer()); - } - else - { - printf(" <testcase name=\"%s\" status=\"run\">\n", testInfo.name.Buffer()); - switch (testInfo.testResult) - { - case TestResult::Fail: - { - StringBuilder buf; - appendXmlEncode(testInfo.message, buf); - - printf(" <error>\n"); - printf("%s", buf.Buffer()); - printf(" </error>\n"); - break; - } - case TestResult::Ignored: - { - printf(" <skip>Ignored</skip>\n"); - break; - } - default: break; - } - printf(" </testcase>\n"); - } - } - - printf(" </testsuite>\n"); - printf("</testSuites>\n"); - break; - } - case TestOutputMode::XUnit2: - { - // https://xunit.github.io/docs/format-xml-v2 - assert("Not currently supported"); - break; - } - case TestOutputMode::TeamCity: - { - // Don't output a summary - break; - } - } -} - -void TestContext::startSuite(const String& name) -{ - m_suiteStack.Add(name); - - switch (m_outputMode) - { - case TestOutputMode::TeamCity: - { - StringBuilder escapedSuiteName; - _appendEncodedTeamCityString(name.getUnownedSlice(), escapedSuiteName); - printf("##teamcity[testSuiteStarted name='%s']\n", escapedSuiteName.begin()); - break; - } - default: break; - } -} - -void TestContext::endSuite() -{ - assert(m_suiteStack.Count()); - - switch (m_outputMode) - { - case TestOutputMode::TeamCity: - { - const String& name = m_suiteStack.Last(); - StringBuilder escapedSuiteName; - _appendEncodedTeamCityString(name.getUnownedSlice(), escapedSuiteName); - printf("##teamcity[testSuiteFinished name='%s']\n", escapedSuiteName.begin()); - break; - } - default: break; - } - - m_suiteStack.RemoveLast(); -} diff --git a/tools/slang-test/test-context.h b/tools/slang-test/test-context.h index cf047cec7..18034f4c7 100644 --- a/tools/slang-test/test-context.h +++ b/tools/slang-test/test-context.h @@ -1,170 +1,36 @@ // test-context.h +#ifndef TEST_CONTEXT_H_INCLUDED +#define TEST_CONTEXT_H_INCLUDED + #include "../../source/core/slang-string-util.h" #include "../../source/core/platform.h" #include "../../source/core/slang-app-context.h" #include "../../source/core/dictionary.h" - -#define SLANG_CHECK(x) TestContext::get()->addResultWithLocation((x), #x, __FILE__, __LINE__); - -struct TestRegister -{ - typedef void (*TestFunc)(); - - TestRegister(const char* name, TestFunc func): - m_next(s_first), - m_name(name), - m_func(func) - { - s_first = this; - } - - TestFunc m_func; - const char* m_name; - TestRegister* m_next; - - static TestRegister* s_first; -}; - -#define SLANG_UNIT_TEST(name, func) static TestRegister s_unitTest##__LINE__(name, func) - -enum class TestOutputMode -{ - Default = 0, ///< Default mode is to write test results to the console - AppVeyor, ///< For AppVeyor continuous integration - Travis, ///< We currently don't specialize for Travis, but maybe we should. - XUnit, ///< xUnit original format https://nose.readthedocs.io/en/latest/plugins/xunit.html - XUnit2, ///< https://xunit.github.io/docs/format-xml-v2 - TeamCity, ///< Output suitable for teamcity -}; - -enum class TestResult -{ - // NOTE! Must keep in order such that combine is meaningful. That is larger values are higher precident - and a series of tests that has lots of passes - // and a fail, is still a fail overall. - Ignored, - Pass, - Fail, -}; - -enum class TestMessageType -{ - Info, ///< General info (may not be shown depending on verbosity setting) - TestFailure, ///< Describes how a test failure took place - RunError, ///< Describes an error that caused a test not to actually correctly run -}; +#include "options.h" class TestContext { public: typedef SlangResult(*InnerMainFunc)(Slang::AppContext* appContext, SlangSession* session, int argc, const char*const* argv); - struct TestInfo - { - TestResult testResult = TestResult::Ignored; - Slang::String name; - Slang::String message; ///< Message that is specific for the testResult - }; - - class TestScope - { - public: - TestScope(TestContext* context, const Slang::String& testName) : - m_context(context) - { - context->startTest(testName); - } - ~TestScope() - { - m_context->endTest(); - } - - protected: - TestContext* m_context; - }; - - class SuiteScope - { - public: - SuiteScope(TestContext* context, const Slang::String& suiteName) : - m_context(context) - { - context->startSuite(suiteName); - } - ~SuiteScope() - { - m_context->endSuite(); - } - - protected: - TestContext* m_context; - }; - - void startSuite(const Slang::String& name); - void endSuite(); - - void startTest(const Slang::String& testName); - void addResult(TestResult result); - void addResultWithLocation(TestResult result, const char* testText, const char* file, int line); - void addResultWithLocation(bool testSucceeded, const char* testText, const char* file, int line); - - void endTest(); - - /// Runs start/endTest and outputs the result - TestResult addTest(const Slang::String& testName, bool isPass); - /// Effectively runs start/endTest (so cannot be called inside start/endTest). - void addTest(const Slang::String& testName, TestResult testResult); - - // Called for an error in the test-runner (not for an error involving a test itself). - void message(TestMessageType type, const Slang::String& errorText); - void messageFormat(TestMessageType type, char const* message, ...); - - void dumpOutputDifference(const Slang::String& expectedOutput, const Slang::String& actualOutput); - - /// True if can write output directly to stderr - bool canWriteStdError() const; - - - /// Returns true if all run tests succeeded - bool didAllSucceed() const; - - /// Get the InnerMain function from a shared library tool - InnerMainFunc getInnerMainFunc(const Slang::String& dirPath, const Slang::String& name); - /// Get the slang session SlangSession* getSession() const { return m_session; } - SlangResult init(TestOutputMode outputMode); + SlangResult init(); + + /// Get the inner main function (from shared library) + TestContext::InnerMainFunc getInnerMainFunc(const Slang::String& dirPath, const Slang::String& name); /// Ctor TestContext(); /// Dtor ~TestContext(); - static TestResult combine(TestResult a, TestResult b) { return (a > b) ? a : b; } - - static TestContext* get() { return s_context; } - static void set(TestContext* context) { s_context = context; } - - Slang::List<TestInfo> m_testInfos; - - Slang::List<Slang::String> m_suiteStack; - - int m_totalTestCount; - int m_passedTestCount; - int m_failedTestCount; - int m_ignoredTestCount; - - int m_maxFailTestResults; ///< Maximum amount of results per test. If 0 it's infinite. - - TestOutputMode m_outputMode = TestOutputMode::Default; - bool m_dumpOutputOnFailure; - bool m_isVerbose; - - bool m_useExes; - - void outputSummary(); + Options options; + TestReporter* reporter = nullptr; + TestCategorySet categorySet; protected: struct SharedLibraryTool @@ -173,20 +39,9 @@ protected: InnerMainFunc m_func; }; - void _addResult(const TestInfo& info); - - Slang::StringBuilder m_currentMessage; - TestInfo m_currentInfo; - int m_numCurrentResults; - int m_numFailResults; - - bool m_inTest; - SlangSession* m_session; Slang::Dictionary<Slang::String, SharedLibraryTool> m_sharedLibTools; - - static TestContext* s_context; }; - +#endif // TEST_CONTEXT_H_INCLUDED diff --git a/tools/slang-test/test-reporter.cpp b/tools/slang-test/test-reporter.cpp new file mode 100644 index 000000000..fad5ad837 --- /dev/null +++ b/tools/slang-test/test-reporter.cpp @@ -0,0 +1,614 @@ +// test-reporter.cpp +#include "test-reporter.h" + +#include "os.h" +#include "../../source/core/slang-string-util.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +using namespace Slang; + +/* static */TestReporter* TestReporter::s_reporter = nullptr; +/* static */TestRegister* TestRegister::s_first; + +static void appendXmlEncode(char c, StringBuilder& out) +{ + switch (c) + { + case '&': out << "&"; break; + case '<': out << "<"; break; + case '>': out << ">"; break; + case '\'': out << "'"; break; + case '"': out << """; break; + default: out.Append(c); + } +} + +static bool isXmlEncodeChar(char c) +{ + switch (c) + { + case '&': + case '<': + case '>': + { + return true; + } + } + return false; +} + +static void appendXmlEncode(const String& in, StringBuilder& out) +{ + const char* cur = in.Buffer(); + const char* end = cur + in.Length(); + + while (cur < end) + { + const char* start = cur; + // Look for a run of non encoded + while (cur < end && !isXmlEncodeChar(*cur)) + { + cur++; + } + // Write it + if (cur > start) + { + out.Append(start, UInt(end - start)); + } + + // if not at the end, we must be on an xml encoded character, so just output it xml encoded. + if (cur < end) + { + const char encodeChar = *cur++; + assert(isXmlEncodeChar(encodeChar)); + appendXmlEncode(encodeChar, out); + } + } +} + +TestReporter::TestReporter() : + m_outputMode(TestOutputMode::Default) +{ + m_totalTestCount = 0; + m_passedTestCount = 0; + m_failedTestCount = 0; + m_ignoredTestCount = 0; + + m_maxFailTestResults = 10; + + m_inTest = false; + m_dumpOutputOnFailure = false; + m_isVerbose = false; +} + +Result TestReporter::init(TestOutputMode outputMode) +{ + m_outputMode = outputMode; + return SLANG_OK; +} + +TestReporter::~TestReporter() +{ +} + +bool TestReporter::canWriteStdError() const +{ + switch (m_outputMode) + { + case TestOutputMode::XUnit: + case TestOutputMode::XUnit2: + { + return false; + } + default: return true; + } +} + +void TestReporter::startTest(const String& testName) +{ + // Must be in a suite + assert(m_suiteStack.Count()); + assert(!m_inTest); + + m_inTest = true; + + m_numCurrentResults = 0; + m_numFailResults = 0; + + m_currentInfo = TestInfo(); + m_currentInfo.name = testName; + m_currentMessage.Clear(); +} + +void TestReporter::endTest() +{ + assert(m_suiteStack.Count()); + assert(m_inTest); + + m_currentInfo.message = m_currentMessage; + + _addResult(m_currentInfo); + + m_inTest = false; +} + +void TestReporter::addResult(TestResult result) +{ + assert(m_inTest); + + m_currentInfo.testResult = combine(m_currentInfo.testResult, result); + m_numCurrentResults++; +} + +void TestReporter::addResultWithLocation(TestResult result, const char* testText, const char* file, int line) +{ + assert(m_inTest); + m_numCurrentResults++; + + m_currentInfo.testResult = combine(m_currentInfo.testResult, result); + if (result != TestResult::Fail) + { + // We don't need to output the result if it + return; + } + + m_numFailResults++; + + if (m_maxFailTestResults > 0) + { + if (m_numFailResults > m_maxFailTestResults) + { + if (m_numFailResults == m_maxFailTestResults + 1) + { + // It's a failure, but to show that there are more than are going to be shown, just show '...' + message(TestMessageType::TestFailure, "..."); + } + return; + } + } + + StringBuilder buf; + buf << testText << " - " << file << " (" << line << ")"; + + message(TestMessageType::TestFailure, buf); +} + +void TestReporter::addResultWithLocation(bool testSucceeded, const char* testText, const char* file, int line) +{ + addResultWithLocation(testSucceeded ? TestResult::Pass : TestResult::Fail, testText, file, line); +} + +TestResult TestReporter::addTest(const String& testName, bool isPass) +{ + const TestResult res = isPass ? TestResult::Pass : TestResult::Fail; + addTest(testName, res); + return res; +} + +void TestReporter::dumpOutputDifference(const String& expectedOutput, const String& actualOutput) +{ + StringBuilder builder; + + StringUtil::appendFormat(builder, + "ERROR:\n" + "EXPECTED{{{\n%s}}}\n" + "ACTUAL{{{\n%s}}}\n", + expectedOutput.Buffer(), + actualOutput.Buffer()); + + + if (m_dumpOutputOnFailure && canWriteStdError()) + { + fprintf(stderr, "%s", builder.Buffer()); + fflush(stderr); + } + + // Add to the m_currentInfo + message(TestMessageType::TestFailure, builder); +} + +static char _getTeamCityEscapeChar(char c) +{ + switch (c) + { + case '|': return '|'; + case '\'': return '\''; + case '\n': return 'n'; + case '\r': return 'r'; + case '[': return '['; + case ']': return ']'; + default: return 0; + } +} + +static void _appendEncodedTeamCityString(const UnownedStringSlice& in, StringBuilder& builder) +{ + const char* start = in.begin(); + const char* cur = start; + const char* end = in.end(); + + for (const char* cur = start; cur < end; cur++) + { + const char c = *cur; + const char escapeChar = _getTeamCityEscapeChar(c); + if (escapeChar) + { + // Flush + if (cur > start) + { + builder.Append(start, UInt(cur - start)); + } + + builder.Append('|'); + builder.Append(escapeChar); + start = cur + 1; + } + } + + // Flush the end + if (end > start) + { + builder.Append(start, UInt(end - start)); + } +} + +void TestReporter::_addResult(const TestInfo& info) +{ + m_totalTestCount++; + + switch (info.testResult) + { + case TestResult::Fail: + m_failedTestCount++; + break; + + case TestResult::Pass: + m_passedTestCount++; + break; + + case TestResult::Ignored: + m_ignoredTestCount++; + break; + + default: + assert(!"unexpected"); + break; + } + + m_testInfos.Add(info); + + // printf("OUTPUT_MODE: %d\n", options.outputMode); + switch (m_outputMode) + { + default: + { + char const* resultString = "UNEXPECTED"; + switch (info.testResult) + { + case TestResult::Fail: resultString = "FAILED"; break; + case TestResult::Pass: resultString = "passed"; break; + case TestResult::Ignored: resultString = "ignored"; break; + default: + assert(!"unexpected"); + break; + } + printf("%s test: '%S'\n", resultString, info.name.ToWString().begin()); + break; + } + case TestOutputMode::TeamCity: + { + StringBuilder escapedTestName; + _appendEncodedTeamCityString(info.name.getUnownedSlice(), escapedTestName); + + printf("##teamcity[testStarted name='%s']\n", escapedTestName.begin()); + + switch (info.testResult) + { + case TestResult::Fail: + { + if (info.message.Length()) + { + StringBuilder escapedMessage; + _appendEncodedTeamCityString(info.message.getUnownedSlice(), escapedMessage); + printf("##teamcity[testFailed name='%s' message='%s']\n", escapedTestName.begin(), escapedMessage.begin()); + } + else + { + printf("##teamcity[testFailed name='%s']\n", escapedTestName.begin()); + } + break; + } + case TestResult::Pass: + { + if (info.message.Length()) + { + StringBuilder escapedMessage; + _appendEncodedTeamCityString(info.message.getUnownedSlice(), escapedMessage); + printf("##teamcity[testStdOut name='%s' out='%s']\n", escapedTestName.begin(), escapedMessage.begin()); + } + break; + } + case TestResult::Ignored: + { + if (info.message.Length()) + { + StringBuilder escapedMessage; + _appendEncodedTeamCityString(info.message.getUnownedSlice(), escapedMessage); + + printf("##teamcity[testIgnored name='%s' message='%s']\n", escapedTestName.begin(), escapedMessage.begin()); + } + else + { + printf("##teamcity[testIgnored name='%s']\n", escapedTestName.begin()); + } + break; + } + default: + assert(!"unexpected"); + break; + } + + printf("##teamcity[testFinished name='%s']\n", escapedTestName.begin()); + fflush(stdout); + break; + } + case TestOutputMode::XUnit2: + case TestOutputMode::XUnit: + { + // Don't output anything -> we'll output all in one go at the end + break; + } + case TestOutputMode::AppVeyor: + { + char const* resultString = "None"; + switch (info.testResult) + { + case TestResult::Fail: resultString = "Failed"; break; + case TestResult::Pass: resultString = "Passed"; break; + case TestResult::Ignored: resultString = "Ignored"; break; + default: + assert(!"unexpected"); + break; + } + + OSProcessSpawner spawner; + spawner.pushExecutableName("appveyor"); + spawner.pushArgument("AddTest"); + spawner.pushArgument(info.name); + spawner.pushArgument("-FileName"); + // TODO: this isn't actually a file name in all cases + spawner.pushArgument(info.name); + spawner.pushArgument("-Framework"); + spawner.pushArgument("slang-test"); + spawner.pushArgument("-Outcome"); + spawner.pushArgument(resultString); + + auto err = spawner.spawnAndWaitForCompletion(); + + if (err != kOSError_None) + { + messageFormat(TestMessageType::Info, "failed to add appveyor test results for '%S'\n", info.name.ToWString().begin()); + +#if 0 + fprintf(stderr, "[%d] TEST RESULT: %s {%d} {%s} {%s}\n", err, spawner.commandLine_.Buffer(), + spawner.getResultCode(), + spawner.getStandardOutput().begin(), + spawner.getStandardError().begin()); +#endif + } + + break; + } + } +} + +void TestReporter::addTest(const String& testName, TestResult testResult) +{ + // Can't add this way if in test + assert(!m_inTest); + + TestInfo info; + info.name = testName; + info.testResult = testResult; + _addResult(info); +} + +void TestReporter::message(TestMessageType type, const String& message) +{ + if (type == TestMessageType::Info) + { + if (m_isVerbose && canWriteStdError()) + { + fputs(message.Buffer(), stderr); + } + + // Just dump out if can dump out + return; + } + + if (canWriteStdError()) + { + if (type == TestMessageType::RunError || type == TestMessageType::TestFailure) + { + fprintf(stderr, "error: "); + fputs(message.Buffer(), stderr); + fprintf(stderr, "\n"); + } + else + { + fputs(message.Buffer(), stderr); + } + } + + if (m_currentMessage.Length() > 0) + { + m_currentMessage << "\n"; + } + m_currentMessage.Append(message); +} + +void TestReporter::messageFormat(TestMessageType type, char const* format, ...) +{ + StringBuilder builder; + + va_list args; + va_start(args, format); + StringUtil::append(format, args, builder); + va_end(args); + + message(type, builder); +} + +bool TestReporter::didAllSucceed() const +{ + return m_passedTestCount == (m_totalTestCount - m_ignoredTestCount); +} + +void TestReporter::outputSummary() +{ + auto passCount = m_passedTestCount; + auto rawTotal = m_totalTestCount; + auto ignoredCount = m_ignoredTestCount; + + auto runTotal = rawTotal - ignoredCount; + + switch (m_outputMode) + { + default: + { + if (!m_totalTestCount) + { + printf("no tests run\n"); + return; + } + + int percentPassed = 0; + if (runTotal > 0) + { + percentPassed = (passCount * 100) / runTotal; + } + + printf("\n===\n%d%% of tests passed (%d/%d)", percentPassed, passCount, runTotal); + if (ignoredCount) + { + printf(", %d tests ignored", ignoredCount); + } + printf("\n===\n\n"); + + if (m_failedTestCount) + { + printf("failing tests:\n"); + printf("---\n"); + for (const auto& testInfo : m_testInfos) + { + if (testInfo.testResult == TestResult::Fail) + { + printf("%s\n", testInfo.name.Buffer()); + } + } + printf("---\n"); + } + break; + } + + case TestOutputMode::XUnit: + { + // xUnit 1.0 format + + printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + printf("<testsuites tests=\"%d\" failures=\"%d\" disabled=\"%d\" errors=\"0\" name=\"AllTests\">\n", m_totalTestCount, m_failedTestCount, m_ignoredTestCount); + printf(" <testsuite name=\"all\" tests=\"%d\" failures=\"%d\" disabled=\"%d\" errors=\"0\" time=\"0\">\n", m_totalTestCount, m_failedTestCount, m_ignoredTestCount); + + for (const auto& testInfo : m_testInfos) + { + const int numFailed = (testInfo.testResult == TestResult::Fail); + const int numIgnored = (testInfo.testResult == TestResult::Ignored); + //int numPassed = (testInfo.testResult == TestResult::ePass); + + if (testInfo.testResult == TestResult::Pass) + { + printf(" <testcase name=\"%s\" status=\"run\"/>\n", testInfo.name.Buffer()); + } + else + { + printf(" <testcase name=\"%s\" status=\"run\">\n", testInfo.name.Buffer()); + switch (testInfo.testResult) + { + case TestResult::Fail: + { + StringBuilder buf; + appendXmlEncode(testInfo.message, buf); + + printf(" <error>\n"); + printf("%s", buf.Buffer()); + printf(" </error>\n"); + break; + } + case TestResult::Ignored: + { + printf(" <skip>Ignored</skip>\n"); + break; + } + default: break; + } + printf(" </testcase>\n"); + } + } + + printf(" </testsuite>\n"); + printf("</testSuites>\n"); + break; + } + case TestOutputMode::XUnit2: + { + // https://xunit.github.io/docs/format-xml-v2 + assert("Not currently supported"); + break; + } + case TestOutputMode::TeamCity: + { + // Don't output a summary + break; + } + } +} + +void TestReporter::startSuite(const String& name) +{ + m_suiteStack.Add(name); + + switch (m_outputMode) + { + case TestOutputMode::TeamCity: + { + StringBuilder escapedSuiteName; + _appendEncodedTeamCityString(name.getUnownedSlice(), escapedSuiteName); + printf("##teamcity[testSuiteStarted name='%s']\n", escapedSuiteName.begin()); + break; + } + default: break; + } +} + +void TestReporter::endSuite() +{ + assert(m_suiteStack.Count()); + + switch (m_outputMode) + { + case TestOutputMode::TeamCity: + { + const String& name = m_suiteStack.Last(); + StringBuilder escapedSuiteName; + _appendEncodedTeamCityString(name.getUnownedSlice(), escapedSuiteName); + printf("##teamcity[testSuiteFinished name='%s']\n", escapedSuiteName.begin()); + break; + } + default: break; + } + + m_suiteStack.RemoveLast(); +} diff --git a/tools/slang-test/test-reporter.h b/tools/slang-test/test-reporter.h new file mode 100644 index 000000000..13bcfe6a4 --- /dev/null +++ b/tools/slang-test/test-reporter.h @@ -0,0 +1,178 @@ +// test-reporter.h + +#ifndef TEST_REPORTER_H_INCLUDED +#define TEST_REPORTER_H_INCLUDED + +#include "../../source/core/slang-string-util.h" +#include "../../source/core/platform.h" +#include "../../source/core/slang-app-context.h" +#include "../../source/core/dictionary.h" + + +#define SLANG_CHECK(x) TestReporter::get()->addResultWithLocation((x), #x, __FILE__, __LINE__); + +struct TestRegister +{ + typedef void (*TestFunc)(); + + TestRegister(const char* name, TestFunc func): + m_next(s_first), + m_name(name), + m_func(func) + { + s_first = this; + } + + TestFunc m_func; + const char* m_name; + TestRegister* m_next; + + static TestRegister* s_first; +}; + +#define SLANG_UNIT_TEST(name, func) static TestRegister s_unitTest##__LINE__(name, func) + +enum class TestOutputMode +{ + Default = 0, ///< Default mode is to write test results to the console + AppVeyor, ///< For AppVeyor continuous integration + Travis, ///< We currently don't specialize for Travis, but maybe we should. + XUnit, ///< xUnit original format https://nose.readthedocs.io/en/latest/plugins/xunit.html + XUnit2, ///< https://xunit.github.io/docs/format-xml-v2 + TeamCity, ///< Output suitable for teamcity +}; + +enum class TestResult +{ + // NOTE! Must keep in order such that combine is meaningful. That is larger values are higher precident - and a series of tests that has lots of passes + // and a fail, is still a fail overall. + Ignored, + Pass, + Fail, +}; + +enum class TestMessageType +{ + Info, ///< General info (may not be shown depending on verbosity setting) + TestFailure, ///< Describes how a test failure took place + RunError, ///< Describes an error that caused a test not to actually correctly run +}; + +class TestReporter +{ + public: + + struct TestInfo + { + TestResult testResult = TestResult::Ignored; + Slang::String name; + Slang::String message; ///< Message that is specific for the testResult + }; + + class TestScope + { + public: + TestScope(TestReporter* reporter, const Slang::String& testName) : + m_reporter(reporter) + { + reporter->startTest(testName); + } + ~TestScope() + { + m_reporter->endTest(); + } + + protected: + TestReporter* m_reporter; + }; + + class SuiteScope + { + public: + SuiteScope(TestReporter* reporter, const Slang::String& suiteName) : + m_reporter(reporter) + { + reporter->startSuite(suiteName); + } + ~SuiteScope() + { + m_reporter->endSuite(); + } + + protected: + TestReporter* m_reporter; + }; + + void startSuite(const Slang::String& name); + void endSuite(); + + void startTest(const Slang::String& testName); + void addResult(TestResult result); + void addResultWithLocation(TestResult result, const char* testText, const char* file, int line); + void addResultWithLocation(bool testSucceeded, const char* testText, const char* file, int line); + + void endTest(); + + /// Runs start/endTest and outputs the result + TestResult addTest(const Slang::String& testName, bool isPass); + /// Effectively runs start/endTest (so cannot be called inside start/endTest). + void addTest(const Slang::String& testName, TestResult testResult); + + // Called for an error in the test-runner (not for an error involving a test itself). + void message(TestMessageType type, const Slang::String& errorText); + void messageFormat(TestMessageType type, char const* message, ...); + + void dumpOutputDifference(const Slang::String& expectedOutput, const Slang::String& actualOutput); + + /// True if can write output directly to stderr + bool canWriteStdError() const; + + + /// Returns true if all run tests succeeded + bool didAllSucceed() const; + + void outputSummary(); + + SlangResult init(TestOutputMode outputMode); + + /// Ctor + TestReporter(); + /// Dtor + ~TestReporter(); + + static TestResult combine(TestResult a, TestResult b) { return (a > b) ? a : b; } + + static TestReporter* get() { return s_reporter; } + static void set(TestReporter* reporter) { s_reporter = reporter; } + + Slang::List<TestInfo> m_testInfos; + + Slang::List<Slang::String> m_suiteStack; + + int m_totalTestCount; + int m_passedTestCount; + int m_failedTestCount; + int m_ignoredTestCount; + + int m_maxFailTestResults; ///< Maximum amount of results per test. If 0 it's infinite. + + TestOutputMode m_outputMode = TestOutputMode::Default; + bool m_dumpOutputOnFailure; + bool m_isVerbose = false; + +protected: + + void _addResult(const TestInfo& info); + + Slang::StringBuilder m_currentMessage; + TestInfo m_currentInfo; + int m_numCurrentResults; + int m_numFailResults; + + bool m_inTest; + + static TestReporter* s_reporter; +}; + +#endif // TEST_REPORTER_H_INCLUDED + |
