diff options
| author | jsmall-nvidia <jsmall@nvidia.com> | 2018-09-12 16:27:42 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-09-12 16:27:42 -0400 |
| commit | f60135cec62c91a9d7923397fe8796d2b3eaa5cb (patch) | |
| tree | 777646cb3611bf5809dc18e120e506117e143e11 /tools | |
| parent | 9a9733091cc7c9628e445313785d561deb229072 (diff) | |
Feature/memory arena (#631)
* First pass at MemoryArena.
* First pass at RandomGenerator.
* Extract TestContext into external source file.
* Fix warning on printf.
* Use enum classes for Test enums.
OutputMode -> TestOutputMode.
* First pass at FreeList unit test.
* Auto registering tests.
Improvements to RandomGenerator.
* Remove the need for unitTest headers - cos can use registering.
* Added unitTest for MemoryArena.
* Do unit tests.
* Fix typo.
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/render-test/main.cpp | 2 | ||||
| -rw-r--r-- | tools/slang-test/main.cpp | 712 | ||||
| -rw-r--r-- | tools/slang-test/slang-test.vcxproj | 4 | ||||
| -rw-r--r-- | tools/slang-test/slang-test.vcxproj.filters | 14 | ||||
| -rw-r--r-- | tools/slang-test/test-context.cpp | 450 | ||||
| -rw-r--r-- | tools/slang-test/test-context.h | 139 | ||||
| -rw-r--r-- | tools/slang-test/unit-test-free-list.cpp | 55 | ||||
| -rw-r--r-- | tools/slang-test/unit-test-memory-arena.cpp | 242 |
8 files changed, 1028 insertions, 590 deletions
diff --git a/tools/render-test/main.cpp b/tools/render-test/main.cpp index 4734c2c8f..77e077b6c 100644 --- a/tools/render-test/main.cpp +++ b/tools/render-test/main.cpp @@ -338,7 +338,7 @@ Result RenderTestApp::writeBindingOutput(const char* fileName) } else { - printf("invalid output type at %d.\n", i); + printf("invalid output type at %d.\n", int(i)); } } } diff --git a/tools/slang-test/main.cpp b/tools/slang-test/main.cpp index 49279e81c..b57dc6d9c 100644 --- a/tools/slang-test/main.cpp +++ b/tools/slang-test/main.cpp @@ -11,6 +11,7 @@ using namespace Slang; #include "os.h" #include "render-api-util.h" +#include "test-context.h" #define STB_IMAGE_IMPLEMENTATION #include "external/stb/stb_image.h" @@ -26,98 +27,6 @@ using namespace Slang; #include <stdio.h> #include <stdlib.h> #include <stdarg.h> -enum OutputMode -{ - // Default mode is to write test results to the console - kOutputMode_Default = 0, - - // When running under AppVeyor continuous integration, we - // need to output test results in a way that the AppVeyor - // environment can pick up and display. - kOutputMode_AppVeyor, - - // We currently don't specialize for Travis, but maybe - // we should. - kOutputMode_Travis, - - // xUnit original format - // https://nose.readthedocs.io/en/latest/plugins/xunit.html - kOutputMode_xUnit, - - // https://xunit.github.io/docs/format-xml-v2 - kOutputMode_xUnit2, -}; - -enum TestResult -{ - kTestResult_Fail, - kTestResult_Pass, - kTestResult_Ignored, -}; - -enum class MessageType -{ - INFO, ///< General info (may not be shown depending on verbosity setting) - TEST_FAILURE, ///< Describes how a test failure took place - RUN_ERROR, ///< Describes an error that caused a test not to actually correctly run -}; - -struct TestContext -{ - struct TestInfo - { - TestResult testResult = TestResult::kTestResult_Ignored; - String name; - String message; ///< Message that is specific for the testResult - }; - - void addResult(const String& testName, TestResult testResult); - - void startTest(const String& testName); - TestResult endTest(TestResult result); - - // Called for an error in the test-runner (not for an error involving - // a test itself). - void message(MessageType type, const String& errorText); - void messageFormat(MessageType type, char const* message, ...); - - void dumpOutputDifference(const String& expectedOutput, const String& actualOutput); - - bool canWriteStdError() const - { - switch (m_outputMode) - { - case kOutputMode_xUnit: - case kOutputMode_xUnit2: - { - return false; - } - default: return true; - } - } - - /// Ctor - TestContext(OutputMode outputMode); - - List<TestInfo> m_testInfos; - - int m_totalTestCount; - int m_passedTestCount; - int m_failedTestCount; - int m_ignoredTestCount; - - OutputMode m_outputMode = kOutputMode_Default; - bool m_dumpOutputOnFailure; - bool m_isVerbose; - - protected: - void _addResult(const TestInfo& info); - - StringBuilder m_currentMessage; - - TestInfo m_currentInfo; - bool m_inTest; -}; // A category that a test can be tagged with struct TestCategory @@ -190,7 +99,7 @@ struct Options bool dumpOutputOnFailure = false; // kind of output to generate - OutputMode outputMode = kOutputMode_Default; + TestOutputMode outputMode = TestOutputMode::eDefault; // Only run tests that match one of the given categories Dictionary<TestCategory*, TestCategory*> includeCategories; @@ -204,6 +113,9 @@ struct Options // 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; + + // Set this to turn on unit tests + bool unitTests = false; }; // Globals @@ -216,271 +128,8 @@ TestCategory* g_defaultTestCategory; TestCategory* findTestCategory(String const& name); -void append(const char* format, va_list args, StringBuilder& buf); - -void appendFormat(StringBuilder& buf, const char* format, ...); -String makeStringWithFormat(const char* format, ...); - -/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! TestContext !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ - -TestContext::TestContext(OutputMode outputMode): - m_outputMode(outputMode) -{ - m_totalTestCount = 0; - m_passedTestCount = 0; - m_failedTestCount = 0; - m_ignoredTestCount = 0; - - m_inTest = false; - m_dumpOutputOnFailure = false; - m_isVerbose = false; -} - -void TestContext::startTest(const String& testName) -{ - assert(!m_inTest); - m_inTest = true; - - m_currentInfo = TestInfo(); - m_currentInfo.name = testName; - m_currentMessage.Clear(); -} - -TestResult TestContext::endTest(TestResult result) -{ - assert(m_inTest); - - m_currentInfo.testResult = result; - m_currentInfo.message = m_currentMessage; - - _addResult(m_currentInfo); - - m_inTest = false; - - return result; -} - -void TestContext::dumpOutputDifference(const String& expectedOutput, const String& actualOutput) -{ - StringBuilder builder; - - 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(MessageType::TEST_FAILURE, builder); -} - -void TestContext::_addResult(const TestInfo& info) -{ - m_totalTestCount++; - - switch (info.testResult) - { - case kTestResult_Fail: - m_failedTestCount++; - break; - - case kTestResult_Pass: - m_passedTestCount++; - break; - - case kTestResult_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 kTestResult_Fail: resultString = "FAILED"; break; - case kTestResult_Pass: resultString = "passed"; break; - case kTestResult_Ignored: resultString = "ignored"; break; - default: - assert(!"unexpected"); - break; - } - printf("%s test: '%S'\n", resultString, info.name.ToWString().begin()); - break; - } - case kOutputMode_xUnit2: - case kOutputMode_xUnit: - { - // Don't output anything -> we'll output all in one go at the end - break; - } - case kOutputMode_AppVeyor: - { - char const* resultString = "None"; - switch (info.testResult) - { - case kTestResult_Fail: resultString = "Failed"; break; - case kTestResult_Pass: resultString = "Passed"; break; - case kTestResult_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(MessageType::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::addResult(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(MessageType type, const String& message) -{ - if (type == MessageType::INFO) - { - if (m_isVerbose && canWriteStdError()) - { - fputs(message.Buffer(), stderr); - } - - // Just dump out if can dump out - return; - } - - if (canWriteStdError()) - { - if (type == MessageType::RUN_ERROR || type == MessageType::TEST_FAILURE) - { - 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(MessageType type, char const* format, ...) -{ - StringBuilder builder; - - va_list args; - va_start(args, format); - append(format, args, builder); - va_end(args); - - message(type, builder); -} - /* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ -void append(const char* format, va_list args, StringBuilder& buf) -{ - int numChars = 0; - -#if SLANG_WINDOWS_FAMILY - numChars = _vscprintf(format, args); -#else - { - va_list argsCopy; - va_copy(argsCopy, args); - numChars = vsnprintf(nullptr, 0, format, argsCopy); - va_end(argsCopy); - } -#endif - - List<char> chars; - chars.SetSize(numChars + 1); - -#if SLANG_WINDOWS_FAMILY - vsnprintf_s(chars.Buffer(), numChars + 1, _TRUNCATE, format, args); -#else - vsnprintf(chars.Buffer(), numChars + 1, format, args); -#endif - - buf.Append(chars.Buffer(), numChars); -} - -void appendFormat(StringBuilder& buf, const char* format, ...) -{ - va_list args; - va_start(args, format); - append(format, args, buf); - va_end(args); -} - -String makeStringWithFormat(const char* format, ...) -{ - StringBuilder builder; - - va_list args; - va_start(args, format); - append(format, args, builder); - va_end(args); - - return builder; -} - Result parseOptions(int* argc, char** argv) { @@ -563,21 +212,21 @@ Result parseOptions(int* argc, char** argv) } else if( strcmp(arg, "-appveyor") == 0 ) { - g_options.outputMode = kOutputMode_AppVeyor; + g_options.outputMode = TestOutputMode::eAppVeyor; g_options.dumpOutputOnFailure = true; } else if( strcmp(arg, "-travis") == 0 ) { - g_options.outputMode = kOutputMode_Travis; + g_options.outputMode = TestOutputMode::eTravis; g_options.dumpOutputOnFailure = true; } else if (strcmp(arg, "-xunit") == 0) { - g_options.outputMode = kOutputMode_xUnit; + g_options.outputMode = TestOutputMode::eXUnit; } else if (strcmp(arg, "-xunit2") == 0) { - g_options.outputMode = kOutputMode_xUnit2; + g_options.outputMode = TestOutputMode::eXUnit2; } else if( strcmp(arg, "-category") == 0 ) { @@ -637,6 +286,10 @@ Result parseOptions(int* argc, char** argv) return res; } } + else if (strcmp(arg, "-unitTests") == 0) + { + g_options.unitTests = true; + } else { fprintf(stderr, "unknown option '%s'\n", arg); @@ -831,7 +484,7 @@ TestResult gatherTestOptions( if(!category) { - return kTestResult_Fail; + return TestResult::eFail; } testOptions.categories.Add(category); @@ -846,7 +499,7 @@ TestResult gatherTestOptions( break; case 0: case '\r': case '\n': - return kTestResult_Fail; + return TestResult::eFail; } break; @@ -863,7 +516,7 @@ TestResult gatherTestOptions( cursor++; else { - return kTestResult_Fail; + return TestResult::eFail; } // Next scan for a sub-command name @@ -880,7 +533,7 @@ TestResult gatherTestOptions( break; case 0: case '\r': case '\n': - return kTestResult_Fail; + return TestResult::eFail; } break; @@ -893,7 +546,7 @@ TestResult gatherTestOptions( cursor++; else { - return kTestResult_Fail; + return TestResult::eFail; } // Now scan for arguments. For now we just assume that @@ -909,7 +562,7 @@ TestResult gatherTestOptions( case 0: case '\r': case '\n': skipToEndOfLine(&cursor); testList->tests.Add(testOptions); - return kTestResult_Pass; + return TestResult::ePass; default: break; @@ -950,7 +603,7 @@ TestResult gatherTestsForFile( } catch (Slang::IOException) { - return kTestResult_Fail; + return TestResult::eFail; } @@ -966,12 +619,12 @@ TestResult gatherTestsForFile( // Look for a pattern that matches what we want if(match(&cursor, "//TEST_IGNORE_FILE")) { - return kTestResult_Ignored; + return TestResult::eIgnored; } else if(match(&cursor, "//TEST")) { - if(gatherTestOptions(&cursor, testList) != kTestResult_Pass) - return kTestResult_Fail; + if(gatherTestOptions(&cursor, testList) != TestResult::ePass) + return TestResult::eFail; } else { @@ -979,7 +632,7 @@ TestResult gatherTestsForFile( } } - return kTestResult_Pass; + return TestResult::ePass; } OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpawner& spawner) @@ -989,14 +642,14 @@ OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpaw if(context->m_isVerbose) { String commandLine = spawner.getCommandLine(); - context->messageFormat(MessageType::INFO, "%s\n", commandLine.begin()); + context->messageFormat(TestMessageType::eInfo, "%s\n", commandLine.begin()); } OSError err = spawner.spawnAndWaitForCompletion(); if (err != kOSError_None) { // fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); - context->messageFormat(MessageType::RUN_ERROR, "failed to run test '%S'", testPath.ToWString().begin()); + context->messageFormat(TestMessageType::eRunError, "failed to run test '%S'", testPath.ToWString().begin()); } return err; } @@ -1078,7 +731,7 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(spawner); @@ -1100,19 +753,19 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; } - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { context->dumpOutputDifference(expectedOutput, actualOutput); - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1140,7 +793,7 @@ TestResult runReflectionTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(spawner); @@ -1162,18 +815,18 @@ TestResult runReflectionTest(TestContext* context, TestInput& input) expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; } - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1225,24 +878,24 @@ TestResult runEvalTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(spawner); String expectedOutput = getExpectedOutput(outputStem); - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1281,7 +934,7 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, expectedSpawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String expectedOutput = getOutput(expectedSpawner); @@ -1292,27 +945,27 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) } catch (Slang::IOException) { - return kTestResult_Fail; + return TestResult::eFail; } if (spawnAndWait(context, outputStem, actualSpawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String actualOutput = getOutput(actualSpawner); - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // Otherwise we compare to the expected output if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1346,7 +999,7 @@ TestResult generateHLSLBaseline(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } String expectedOutput = getOutput(spawner); @@ -1357,9 +1010,9 @@ TestResult generateHLSLBaseline(TestContext* context, TestInput& input) } catch (Slang::IOException) { - return kTestResult_Fail; + return TestResult::eFail; } - return kTestResult_Pass; + return TestResult::ePass; } TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) @@ -1394,7 +1047,7 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } // We ignore output to stdout, and only worry about what the compiler @@ -1426,26 +1079,26 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) { } - TestResult result = kTestResult_Pass; + TestResult result = TestResult::ePass; // If no expected output file was found, then we // expect everything to be empty if (expectedOutput.Length() == 0) { - if (resultCode != 0) result = kTestResult_Fail; - if (standardError.Length() != 0) result = kTestResult_Fail; - if (standardOutput.Length() != 0) result = kTestResult_Fail; + if (resultCode != 0) result = TestResult::eFail; + if (standardError.Length() != 0) result = TestResult::eFail; + if (standardOutput.Length() != 0) result = TestResult::eFail; } // Otherwise we compare to the expected output else if (actualOutput != expectedOutput) { - result = kTestResult_Fail; + result = TestResult::eFail; } // If the test failed, then we write the actual output to a file // so that we can easily diff it from the command line and // diagnose the problem. - if (result == kTestResult_Fail) + if (result == TestResult::eFail) { String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); @@ -1494,7 +1147,7 @@ TestResult doGLSLComparisonTestRun(TestContext* context, if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); @@ -1517,7 +1170,7 @@ TestResult doGLSLComparisonTestRun(TestContext* context, *outOutput = output; - return kTestResult_Pass; + return TestResult::ePass; } TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) @@ -1534,17 +1187,17 @@ TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); Slang::File::WriteAllText(outputStem + ".actual", actualOutput); - if( hlslResult == kTestResult_Fail ) return kTestResult_Fail; - if( slangResult == kTestResult_Fail ) return kTestResult_Fail; + if( hlslResult == TestResult::eFail ) return TestResult::eFail; + if( slangResult == TestResult::eFail ) return TestResult::eFail; if (actualOutput != expectedOutput) { context->dumpOutputDifference(expectedOutput, actualOutput); - return kTestResult_Fail; + return TestResult::eFail; } - return kTestResult_Pass; + return TestResult::ePass; } @@ -1557,7 +1210,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons const String referenceOutput = findExpectedPath(input, ".expected.txt"); if (referenceOutput.Length() <= 0) { - return kTestResult_Fail; + return TestResult::eFail; } OSProcessSpawner spawner; @@ -1581,7 +1234,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { printf("error spawning render-test\n"); - return kTestResult_Fail; + return TestResult::eFail; } auto actualOutput = getOutput(spawner); @@ -1593,7 +1246,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons String actualOutputPath = outputStem + ".actual"; Slang::File::WriteAllText(actualOutputPath, actualOutput); - return kTestResult_Fail; + return TestResult::eFail; } // check against reference output @@ -1601,24 +1254,24 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons { printf("render-test not producing expected outputs.\n"); printf("render-test output:\n%s\n", actualOutput.Buffer()); - return kTestResult_Fail; + return TestResult::eFail; } if (!File::Exists(referenceOutput)) { printf("referenceOutput %s not found.\n", referenceOutput.Buffer()); - return kTestResult_Fail; + return TestResult::eFail; } auto actualOutputContent = File::ReadAllText(actualOutputFile); auto actualProgramOutput = Split(actualOutputContent, '\n'); auto referenceProgramOutput = Split(File::ReadAllText(referenceOutput), '\n'); auto printOutput = [&]() { - context->messageFormat(MessageType::TEST_FAILURE, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); + context->messageFormat(TestMessageType::eTestFailure, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); }; if (actualProgramOutput.Count() < referenceProgramOutput.Count()) { printOutput(); - return kTestResult_Fail; + return TestResult::eFail; } for (int i = 0; i < (int)referenceProgramOutput.Count(); i++) { @@ -1632,13 +1285,13 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons if (actual != uval) { printOutput(); - return kTestResult_Fail; + return TestResult::eFail; } else - return kTestResult_Pass; + return TestResult::ePass; } } - return kTestResult_Pass; + return TestResult::ePass; } TestResult runSlangComputeComparisonTest(TestContext* context, TestInput& input) @@ -1684,7 +1337,7 @@ TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, cha if (spawnAndWait(context, outputStem, spawner) != kOSError_None) { - return kTestResult_Fail; + return TestResult::eFail; } OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); @@ -1707,7 +1360,7 @@ TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, cha *outOutput = output; - return kTestResult_Pass; + return TestResult::ePass; } TestResult doImageComparison(TestContext* context, String const& filePath) @@ -1729,19 +1382,19 @@ TestResult doImageComparison(TestContext* context, String const& filePath) if(!expectedData) { - context->messageFormat(MessageType::RUN_ERROR, "Unable to load image ;%s'", expectedPath.Buffer()); - return kTestResult_Fail; + context->messageFormat(TestMessageType::eRunError, "Unable to load image ;%s'", expectedPath.Buffer()); + return TestResult::eFail; } if(!actualData) { - context->messageFormat(MessageType::RUN_ERROR, "Unable to load image '%s'", actualPath.Buffer()); - return kTestResult_Fail; + context->messageFormat(TestMessageType::eRunError, "Unable to load image '%s'", actualPath.Buffer()); + return TestResult::eFail; } if(expectedX != actualX || expectedY != actualY || expectedN != actualN) { - context->messageFormat(MessageType::TEST_FAILURE, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); - return kTestResult_Fail; + context->messageFormat(TestMessageType::eTestFailure, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); + return TestResult::eFail; } unsigned char* expectedCursor = expectedData; @@ -1781,7 +1434,7 @@ TestResult doImageComparison(TestContext* context, String const& filePath) // cases where vertex shader results lead to rendering that is off // by one pixel... - context->messageFormat(MessageType::TEST_FAILURE, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", + context->messageFormat(TestMessageType::eTestFailure, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", x, y, n, expectedVal, actualVal, @@ -1789,12 +1442,12 @@ TestResult doImageComparison(TestContext* context, String const& filePath) relativeDiff); // There was a difference we couldn't excuse! - return kTestResult_Fail; + return TestResult::eFail; } } } - return kTestResult_Pass; + return TestResult::ePass; } TestResult runHLSLRenderComparisonTestImpl( @@ -1815,23 +1468,23 @@ TestResult runHLSLRenderComparisonTestImpl( Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); Slang::File::WriteAllText(outputStem + ".actual", actualOutput); - if( hlslResult == kTestResult_Fail ) return kTestResult_Fail; - if( slangResult == kTestResult_Fail ) return kTestResult_Fail; + if( hlslResult == TestResult::eFail ) return TestResult::eFail; + if( slangResult == TestResult::eFail ) return TestResult::eFail; if (actualOutput != expectedOutput) { context->dumpOutputDifference(expectedOutput, actualOutput); - return kTestResult_Fail; + return TestResult::eFail; } // Next do an image comparison on the expected output images! TestResult imageCompareResult = doImageComparison(context, outputStem); - if(imageCompareResult != kTestResult_Pass) + if(imageCompareResult != TestResult::ePass) return imageCompareResult; - return kTestResult_Pass; + return TestResult::ePass; } TestResult runHLSLRenderComparisonTest(TestContext* context, TestInput& input) @@ -1851,7 +1504,7 @@ TestResult runHLSLAndGLSLRenderComparisonTest(TestContext* context, TestInput& i TestResult skipTest(TestContext* /* context */, TestInput& /*input*/) { - return kTestResult_Ignored; + return TestResult::eIgnored; } @@ -1957,7 +1610,7 @@ TestResult runTest( // If this test can be ignored if (canIgnoreTestWithDisabledRenderer(testOptions)) { - return kTestResult_Ignored; + return TestResult::eIgnored; } // based on command name, dispatch to an appropriate callback @@ -2008,13 +1661,19 @@ TestResult runTest( testInput.testOptions = &testOptions; testInput.testList = &testList; - context->startTest(outputStem); - return context->endTest(ii->callback(context, testInput)); + { + TestContext::Scope scope(context, outputStem); + + TestResult testResult = ii->callback(context, testInput); + context->addResult(testResult); + + return testResult; + } } // No actual test runner found! - return kTestResult_Fail; + return TestResult::eFail; } bool testCategoryMatches( @@ -2073,7 +1732,7 @@ void runTestsOnFile( // Gather a list of tests to run FileTestList testList; - if( gatherTestsForFile(filePath, &testList) == kTestResult_Ignored ) + if( gatherTestsForFile(filePath, &testList) == TestResult::eIgnored ) { // Test was explicitly ignored return; @@ -2082,7 +1741,7 @@ void runTestsOnFile( // Note cases where a test file exists, but we found nothing to run if( testList.tests.Count() == 0 ) { - context->addResult(filePath, kTestResult_Ignored); + context->addTest(filePath, TestResult::eIgnored); return; } @@ -2233,62 +1892,6 @@ void runTestsInDirectory( } } -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); - } - } -} - // @@ -2330,7 +1933,7 @@ int main( // Exclude rendering tests when building under AppVeyor. // // TODO: this is very ad hoc, and we should do something cleaner. - if( g_options.outputMode == kOutputMode_AppVeyor ) + if( g_options.outputMode == TestOutputMode::eAppVeyor ) { g_options.excludeCategories.Add(renderTestCategory, renderTestCategory); g_options.excludeCategories.Add(vulkanTestCategory, vulkanTestCategory); @@ -2342,104 +1945,37 @@ int main( context.m_dumpOutputOnFailure = g_options.dumpOutputOnFailure; context.m_isVerbose = g_options.shouldBeVerbose; - // 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 - runTestsInDirectory(&context, "tests/"); - - auto passCount = context.m_passedTestCount; - auto rawTotal = context.m_totalTestCount; - auto ignoredCount = context.m_ignoredTestCount; - - auto runTotal = rawTotal - ignoredCount; - - switch (g_options.outputMode) + if (g_options.unitTests) { - default: - { - if (!context.m_totalTestCount) - { - printf("no tests run\n"); - return 0; - } - - printf("\n===\n%d%% of tests passed (%d/%d)", (passCount*100) / runTotal, passCount, runTotal); - if(ignoredCount) - { - printf(", %d tests ignored", ignoredCount); - } - printf("\n===\n\n"); + TestContext::set(&context); - if(context.m_failedTestCount) - { - printf("failing tests:\n"); - printf("---\n"); - for(const auto& testInfo : context.m_testInfos) - { - if (testInfo.testResult == kTestResult_Fail) - { - printf("%s\n", testInfo.name.Buffer()); - } - } - printf("---\n"); - } - break; - } - case kOutputMode_xUnit: + // Run the unit tests + TestRegister* cur = TestRegister::s_first; + while (cur) { - // 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", context.m_totalTestCount, context.m_failedTestCount, context.m_ignoredTestCount); - printf(" <testsuite name=\"all\" tests=\"%d\" failures=\"%d\" disabled=\"%d\" errors=\"0\" time=\"0\">\n", context.m_totalTestCount, context.m_failedTestCount, context.m_ignoredTestCount); + context.startTest(cur->m_name); - for (const auto& testInfo : context.m_testInfos) - { - const int numFailed = (testInfo.testResult == kTestResult_Fail); - const int numIgnored = (testInfo.testResult == kTestResult_Ignored); - //int numPassed = (testInfo.testResult == kTestResult_Pass); + // Run the test function + cur->m_func(); - if (testInfo.testResult == kTestResult_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 kTestResult_Fail: - { - StringBuilder buf; - appendXmlEncode(testInfo.message, buf); - - printf(" <error>\n"); - printf("%s", buf.Buffer()); - printf(" </error>\n"); - break; - } - case kTestResult_Ignored: - { - printf(" <skip>Ignored</skip>\n"); - break; - } - default: break; - } - printf(" </testcase>\n"); - } - } + context.endTest(); - printf(" </testsuite>\n"); - printf("</testSuites>\n"); - break; - } - case kOutputMode_xUnit2: - { - // https://xunit.github.io/docs/format-xml-v2 - assert("Not currently supported"); - break; + // Next + cur = cur->m_next; } + + TestContext::set(nullptr); } + else + { + // 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 + runTestsInDirectory(&context, "tests/"); + + } + + context.outputSummary(); - return passCount == runTotal ? 0 : 1; + return context.didAllSucceed() ? 0 : 1; } diff --git a/tools/slang-test/slang-test.vcxproj b/tools/slang-test/slang-test.vcxproj index 9524529bb..b94f2f627 100644 --- a/tools/slang-test/slang-test.vcxproj +++ b/tools/slang-test/slang-test.vcxproj @@ -164,11 +164,15 @@ <ItemGroup> <ClInclude Include="os.h" /> <ClInclude Include="render-api-util.h" /> + <ClInclude Include="test-context.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="main.cpp" /> <ClCompile Include="os.cpp" /> <ClCompile Include="render-api-util.cpp" /> + <ClCompile Include="test-context.cpp" /> + <ClCompile Include="unit-test-free-list.cpp" /> + <ClCompile Include="unit-test-memory-arena.cpp" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\source\core\core.vcxproj"> diff --git a/tools/slang-test/slang-test.vcxproj.filters b/tools/slang-test/slang-test.vcxproj.filters index f22903aa6..3d80416f8 100644 --- a/tools/slang-test/slang-test.vcxproj.filters +++ b/tools/slang-test/slang-test.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"> @@ -15,6 +15,9 @@ <ClInclude Include="render-api-util.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="test-context.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="main.cpp"> @@ -26,5 +29,14 @@ <ClCompile Include="render-api-util.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="test-context.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="unit-test-free-list.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="unit-test-memory-arena.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> </Project>
\ No newline at end of file diff --git a/tools/slang-test/test-context.cpp b/tools/slang-test/test-context.cpp new file mode 100644 index 000000000..f77262001 --- /dev/null +++ b/tools/slang-test/test-context.cpp @@ -0,0 +1,450 @@ +// test-context.cpp +#include "test-context.h" + +#include "os.h" +#include "../../source/core/slang-string-util.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +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(TestOutputMode outputMode) : + m_outputMode(outputMode) +{ + m_totalTestCount = 0; + m_passedTestCount = 0; + m_failedTestCount = 0; + m_ignoredTestCount = 0; + + m_maxTestResults = 10; + + m_inTest = false; + m_dumpOutputOnFailure = false; + m_isVerbose = false; +} + +bool TestContext::canWriteStdError() const +{ + switch (m_outputMode) + { + case TestOutputMode::eXUnit: + case TestOutputMode::eXUnit2: + { + return false; + } + default: return true; + } +} + +void TestContext::startTest(const String& testName) +{ + assert(!m_inTest); + m_inTest = true; + + m_numCurrentResults = 0; + m_currentInfo = TestInfo(); + m_currentInfo.name = testName; + m_currentMessage.Clear(); +} + +void TestContext::endTest() +{ + 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::eFail) + { + // We don't need to output the result if it + return; + } + + if (m_maxTestResults > 0) + { + if (m_numCurrentResults > m_maxTestResults) + { + if (m_numCurrentResults == m_maxTestResults + 1) + { + message(TestMessageType::eInfo, "..."); + } + return; + } + } + + StringBuilder buf; + buf << testText << " - " << file << " (" << line << ")"; + + message(TestMessageType::eTestFailure, buf); +} + +void TestContext::addResultWithLocation(bool testSucceeded, const char* testText, const char* file, int line) +{ + addResultWithLocation(testSucceeded ? TestResult::ePass : TestResult::eFail, testText, file, line); +} + +TestResult TestContext::addTest(const String& testName, bool isPass) +{ + const TestResult res = isPass ? TestResult::ePass : TestResult::eFail; + 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::eTestFailure, builder); +} + +void TestContext::_addResult(const TestInfo& info) +{ + m_totalTestCount++; + + switch (info.testResult) + { + case TestResult::eFail: + m_failedTestCount++; + break; + + case TestResult::ePass: + m_passedTestCount++; + break; + + case TestResult::eIgnored: + 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::eFail: resultString = "FAILED"; break; + case TestResult::ePass: resultString = "passed"; break; + case TestResult::eIgnored: resultString = "ignored"; break; + default: + assert(!"unexpected"); + break; + } + printf("%s test: '%S'\n", resultString, info.name.ToWString().begin()); + break; + } + case TestOutputMode::eXUnit2: + case TestOutputMode::eXUnit: + { + // Don't output anything -> we'll output all in one go at the end + break; + } + case TestOutputMode::eAppVeyor: + { + char const* resultString = "None"; + switch (info.testResult) + { + case TestResult::eFail: resultString = "Failed"; break; + case TestResult::ePass: resultString = "Passed"; break; + case TestResult::eIgnored: 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::eInfo, "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::eInfo) + { + if (m_isVerbose && canWriteStdError()) + { + fputs(message.Buffer(), stderr); + } + + // Just dump out if can dump out + return; + } + + if (canWriteStdError()) + { + if (type == TestMessageType::eRunError || type == TestMessageType::eTestFailure) + { + 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); +} + +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::eFail) + { + printf("%s\n", testInfo.name.Buffer()); + } + } + printf("---\n"); + } + break; + } + case TestOutputMode::eXUnit: + { + // 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::eFail); + const int numIgnored = (testInfo.testResult == TestResult::eIgnored); + //int numPassed = (testInfo.testResult == TestResult::ePass); + + if (testInfo.testResult == TestResult::ePass) + { + 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::eFail: + { + StringBuilder buf; + appendXmlEncode(testInfo.message, buf); + + printf(" <error>\n"); + printf("%s", buf.Buffer()); + printf(" </error>\n"); + break; + } + case TestResult::eIgnored: + { + printf(" <skip>Ignored</skip>\n"); + break; + } + default: break; + } + printf(" </testcase>\n"); + } + } + + printf(" </testsuite>\n"); + printf("</testSuites>\n"); + break; + } + case TestOutputMode::eXUnit2: + { + // https://xunit.github.io/docs/format-xml-v2 + assert("Not currently supported"); + break; + } + } +}
\ No newline at end of file diff --git a/tools/slang-test/test-context.h b/tools/slang-test/test-context.h new file mode 100644 index 000000000..22042a4e6 --- /dev/null +++ b/tools/slang-test/test-context.h @@ -0,0 +1,139 @@ +// test-context.h + +#include "../../source/core/slang-string-util.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 +{ + eDefault = 0, ///< Default mode is to write test results to the console + eAppVeyor, ///< For AppVeyor continuous integration + eTravis, ///< We currently don't specialize for Travis, but maybe we should. + eXUnit, ///< xUnit original format https://nose.readthedocs.io/en/latest/plugins/xunit.html + eXUnit2, ///< https://xunit.github.io/docs/format-xml-v2 +}; + +enum class TestResult +{ + eIgnored, + ePass, + eFail, +}; + +enum class TestMessageType +{ + eInfo, ///< General info (may not be shown depending on verbosity setting) + eTestFailure, ///< Describes how a test failure took place + eRunError, ///< Describes an error that caused a test not to actually correctly run +}; + +class TestContext +{ + public: + + struct TestInfo + { + TestResult testResult = TestResult::eIgnored; + Slang::String name; + Slang::String message; ///< Message that is specific for the testResult + }; + + class Scope + { + public: + Scope(TestContext* context, const Slang::String& testName) : + m_context(context) + { + context->startTest(testName); + } + ~Scope() + { + m_context->endTest(); + } + + protected: + TestContext* m_context; + }; + + 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; + + /// Call at end of tests + void outputSummary(); + + /// Returns true if all run tests succeeded + bool didAllSucceed() const; + + /// Ctor + TestContext(TestOutputMode outputMode); + + 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; + + int m_totalTestCount; + int m_passedTestCount; + int m_failedTestCount; + int m_ignoredTestCount; + + int m_maxTestResults; ///< Maximum amount of results per test. If 0 it's infinite. + + TestOutputMode m_outputMode = TestOutputMode::eDefault; + bool m_dumpOutputOnFailure; + bool m_isVerbose; + +protected: + void _addResult(const TestInfo& info); + + Slang::StringBuilder m_currentMessage; + TestInfo m_currentInfo; + int m_numCurrentResults; + + bool m_inTest; + + static TestContext* s_context; +}; + + diff --git a/tools/slang-test/unit-test-free-list.cpp b/tools/slang-test/unit-test-free-list.cpp new file mode 100644 index 000000000..649c59571 --- /dev/null +++ b/tools/slang-test/unit-test-free-list.cpp @@ -0,0 +1,55 @@ +// unit-test-free-list.cpp + +#include "../../source/core/slang-free-list.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +#include "test-context.h" + +#include "../../source/core/slang-random-generator.h" +#include "../../source/core/list.h" + +using namespace Slang; + +static void freeListUnitTest() +{ + FreeList freeList; + freeList.init(sizeof(int), sizeof(void*), 10); + + DefaultRandomGenerator randGen(0x24343); + + List<int*> allocs; + + for (int i = 0; i < 1000; i++) + { + const int numAlloc = randGen.nextInt32UpTo(20); + + for (int j = 0; j < numAlloc; j++) + { + int* ptr = (int*)freeList.allocate(); + *ptr = i; + allocs.Add(ptr); + } + + int numDealloc = randGen.nextInt32UpTo(19); + numDealloc = int(allocs.Count()) < numDealloc ? int(allocs.Count()) : numDealloc; + + for (int j = 0; j < numDealloc; j++) + { + const int index = randGen.nextInt32UpTo(int(allocs.Count())); + + int* alloc = allocs[index]; + + SLANG_CHECK(*alloc <= i); + SLANG_CHECK(*alloc >= 0); + + freeList.deallocate(alloc); + + allocs.FastRemoveAt(index); + } + } +} + +SLANG_UNIT_TEST("FreeList", freeListUnitTest);
\ No newline at end of file diff --git a/tools/slang-test/unit-test-memory-arena.cpp b/tools/slang-test/unit-test-memory-arena.cpp new file mode 100644 index 000000000..69eed520a --- /dev/null +++ b/tools/slang-test/unit-test-memory-arena.cpp @@ -0,0 +1,242 @@ +// unit-test-free-list.cpp + +#include "../../source/core/slang-memory-arena.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> + +#include "test-context.h" + +#include "../../source/core/slang-random-generator.h" +#include "../../source/core/list.h" + +using namespace Slang; + + +namespace // anonymous +{ + +struct Block +{ + void* m_data; + size_t m_size; + uint8_t m_value; +}; + +enum class TestMode +{ + eUnaligned, + eImplicitAligned, ///< Alignment is kept implicitly with Unaligned allocs of the right size + eDefaultAligned, + eExplicitAligned, + eCount, +}; + +} // anonymous + +static size_t getAlignment(TestMode mode) +{ + switch (mode) + { + default: + case TestMode::eUnaligned: + return 1; + case TestMode::eExplicitAligned: + return 16; + case TestMode::eImplicitAligned: + return 32; + case TestMode::eDefaultAligned: + return MemoryArena::kMinAlignment; + } +} + +static bool hasValueShort(const uint8_t* data, size_t size, uint8_t value) +{ + for (size_t i = 0; i < size; ++i) + { + if (data[i] != value) + { + return false; + } + } + return true; +} + +static bool hasValue(const uint8_t* data, size_t size, uint8_t value) +{ + const size_t alignMask = sizeof(size_t) - 1; + + if (size <= sizeof(size_t) * 2) + { + return hasValueShort(data, size, value); + } + + if (size_t(data) & alignMask) + { + size_t firstSize = sizeof(size_t) - (size_t(data) & alignMask); + if (!hasValueShort(data, firstSize, value)) + { + return false; + } + size -= firstSize; + data += firstSize; + + assert((size_t(data) & alignMask) == 0); + } + + // Now do the middle + size_t numWords = size / sizeof(size_t); + + // Expand the byte up to a word size + size_t wordValue = (size_t(value) << 8) | value; + wordValue = (wordValue << 16) | wordValue; + wordValue = (sizeof(size_t) > 4) ? ((wordValue << 32) | wordValue) : wordValue; + + const size_t* wordData = (const size_t*)data; + for (size_t i = 0; i < numWords; ++i) + { + if (wordData[i] != wordValue) + { + return false; + } + } + + // Do the end piece + return hasValueShort(data + sizeof(size_t) * numWords, size & alignMask, value); +} + +static void memoryArenaUnitTest() +{ + DefaultRandomGenerator randGen(0x5346536a); + + { + const size_t blockSize = 1024; + MemoryArena arena; + arena.init(blockSize); + + List<void*> blocks; + + blocks.Add(arena.allocate(100)); + blocks.Add(arena.allocate(blockSize * 2)); + blocks.Add(arena.allocate(100)); + blocks.Add(arena.allocate(blockSize * 2)); + blocks.Add(arena.allocate(100)); + + while (blocks.Count()) + { + arena.deallocateLast(blocks.Last()); + blocks.RemoveLast(); + } + } + + { + + const size_t blockSize = 1024; + + for (TestMode mode = TestMode(0); int(mode) < int(TestMode::eCount); mode = TestMode(int(mode) + 1)) + { + const size_t alignment = getAlignment(mode); + + MemoryArena arena; + arena.init(blockSize, alignment); + + List<Block> blocks; + + for (int i = 0; i < 1000; i++) + { + int var = randGen.nextInt32() & 0x3ff; + if (var < 3 && blocks.Count() > 0) + { + if (var == 0) + { + // Do a single dealloc + arena.deallocateLast(blocks.Last().m_data); + blocks.RemoveLast(); + } + else if (var == 1) + { + // Deallocate everything + arena.deallocateAll(); + blocks.Clear(); + } + else + { + // Do a multiple dealloc + int index = randGen.nextInt32UpTo(int(blocks.Count())); + + // Deallocate all afterwards + arena.deallocateAllFrom(blocks[index].m_data); + + blocks.SetSize(index); + } + } + else + { + size_t sizeInBytes = (randGen.nextInt32() & 255) + 1; + + // Lets go for an oversized block + if ((randGen.nextInt32() & 0xff) < 2) + { + sizeInBytes += blockSize; + } + + const uint8_t value = uint8_t(randGen.nextInt32()); + + void* mem = nullptr; + switch (mode) + { + default: + case TestMode::eUnaligned: + { + mem = arena.allocateUnaligned(sizeInBytes); + break; + } + case TestMode::eImplicitAligned: + { + // Fix the size to get implicit alignment + sizeInBytes = (sizeInBytes & ~(alignment - 1)) + alignment; + mem = arena.allocateUnaligned(sizeInBytes); + break; + } + case TestMode::eExplicitAligned: + { + mem = arena.allocateAligned(sizeInBytes, alignment); + break; + } + case TestMode::eDefaultAligned: + { + mem = arena.allocate(sizeInBytes); + break; + } + } + + // Check it is aligned + SLANG_CHECK((size_t(mem) & (alignment - 1)) == 0); + + ::memset(mem, value, sizeInBytes); + + Block block; + + block.m_data = mem; + block.m_size = sizeInBytes; + block.m_value = value; + + blocks.Add(block); + } + + // Check the blocks + for (int j = 0; j < int(blocks.Count()); ++j) + { + const Block& block = blocks[j]; + + SLANG_CHECK(arena.isValid(block.m_data, block.m_size)); + + SLANG_CHECK(hasValue((uint8_t*)block.m_data, block.m_size, block.m_value)); + } + } + } + } +} + +SLANG_UNIT_TEST("MemoryArena", memoryArenaUnitTest);
\ No newline at end of file |
