summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorjsmall-nvidia <jsmall@nvidia.com>2018-12-14 15:24:21 -0500
committerGitHub <noreply@github.com>2018-12-14 15:24:21 -0500
commitd43c566fa29bbc0da1534aea236d54ee5ca104b8 (patch)
tree5a1878687364d28361a2c6aa722c5c8d9c75810e /tools
parentec745c032a8dc16c3e689458c20541a4e7aa64d6 (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
Diffstat (limited to 'tools')
-rw-r--r--tools/slang-reflection-test/main.cpp3
-rw-r--r--tools/slang-test/main.cpp495
-rw-r--r--tools/slang-test/options.cpp245
-rw-r--r--tools/slang-test/options.h83
-rw-r--r--tools/slang-test/slang-test.vcxproj4
-rw-r--r--tools/slang-test/slang-test.vcxproj.filters12
-rw-r--r--tools/slang-test/test-context.cpp598
-rw-r--r--tools/slang-test/test-context.h169
-rw-r--r--tools/slang-test/test-reporter.cpp614
-rw-r--r--tools/slang-test/test-reporter.h178
10 files changed, 1248 insertions, 1153 deletions
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 << "&amp;"; break;
- case '<': out << "&lt;"; break;
- case '>': out << "&gt;"; break;
- case '\'': out << "&apos;"; break;
- case '"': out << "&quot;"; 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 << "&amp;"; break;
+ case '<': out << "&lt;"; break;
+ case '>': out << "&gt;"; break;
+ case '\'': out << "&apos;"; break;
+ case '"': out << "&quot;"; 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
+