#include <SDL3/SDL_test.h>
#include "SDL_test_internal.h"
#include <stdlib.h>
#define SDLTEST_INVALID_NAME_FORMAT "(Invalid)"
static void SDLTest_LogSummary(bool success, const char *name, int total, int passed, int failed, int skipped)
{
SDLTest_LogMessage(success ? SDL_LOG_PRIORITY_INFO : SDL_LOG_PRIORITY_ERROR,
"%s Summary: Total=%d " "%s" "Passed=%d" "%s" " " "%s" "Failed=%d" "%s" " " "%s" "Skipped=%d" "%s",
name, total, COLOR_GREEN, passed, COLOR_END, success ? COLOR_GREEN : COLOR_RED, failed, COLOR_END, COLOR_BLUE, skipped, COLOR_END);
}
static void SDLTest_LogFinalResult(bool success, const char *stage, const char *name, const char *color_message, const char *message)
{
SDL_LogPriority priority = success ? SDL_LOG_PRIORITY_INFO : SDL_LOG_PRIORITY_ERROR;
SDLTest_LogMessage(priority, "%s>>> %s '%s':" "%s" " " "%s" "%s" "%s", COLOR_YELLOW, stage, name, COLOR_END, color_message ? color_message : "", message, color_message ? COLOR_END : "");
}
struct SDLTest_TestSuiteRunner {
struct
{
SDLTest_TestSuiteReference **testSuites;
char *runSeed;
Uint64 execKey;
char *filter;
int testIterations;
bool randomOrder;
} user;
SDLTest_ArgumentParser argparser;
};
static Uint32 SDLTest_TestCaseTimeout = 3600;
static const char *common_harness_usage[] = {
"[--iterations #]",
"[--execKey #]",
"[--seed string]",
"[--filter suite_name|test_name]",
"[--random-order]",
NULL
};
char *SDLTest_GenerateRunSeed(char *buffer, int length)
{
Uint64 randomContext = SDL_GetPerformanceCounter();
int counter;
if (!buffer) {
SDLTest_LogError("Input buffer must not be NULL.");
return NULL;
}
if (length <= 0) {
SDLTest_LogError("The length of the harness seed must be >0.");
return NULL;
}
for (counter = 0; counter < length; counter++) {
char ch;
int v = SDL_rand_r(&randomContext, 10 + 26);
if (v < 10) {
ch = (char)('0' + v);
} else {
ch = (char)('A' + v - 10);
}
buffer[counter] = ch;
}
buffer[length] = '\0';
return buffer;
}
static Uint64 SDLTest_GenerateExecKey(const char *runSeed, const char *suiteName, const char *testName, int iteration)
{
SDLTest_Md5Context md5Context;
Uint64 *keys;
char iterationString[16];
size_t runSeedLength;
size_t suiteNameLength;
size_t testNameLength;
size_t iterationStringLength;
size_t entireStringLength;
char *buffer;
if (!runSeed || runSeed[0] == '\0') {
SDLTest_LogError("Invalid runSeed string.");
return 0;
}
if (!suiteName || suiteName[0] == '\0') {
SDLTest_LogError("Invalid suiteName string.");
return 0;
}
if (!testName || testName[0] == '\0') {
SDLTest_LogError("Invalid testName string.");
return 0;
}
if (iteration <= 0) {
SDLTest_LogError("Invalid iteration count.");
return 0;
}
SDL_memset(iterationString, 0, sizeof(iterationString));
(void)SDL_snprintf(iterationString, sizeof(iterationString) - 1, "%d", iteration);
runSeedLength = SDL_strlen(runSeed);
suiteNameLength = SDL_strlen(suiteName);
testNameLength = SDL_strlen(testName);
iterationStringLength = SDL_strlen(iterationString);
entireStringLength = runSeedLength + suiteNameLength + testNameLength + iterationStringLength + 1;
buffer = (char *)SDL_malloc(entireStringLength);
if (!buffer) {
SDLTest_LogError("Failed to allocate buffer for execKey generation.");
return 0;
}
(void)SDL_snprintf(buffer, entireStringLength, "%s%s%s%d", runSeed, suiteName, testName, iteration);
SDLTest_Md5Init(&md5Context);
SDLTest_Md5Update(&md5Context, (unsigned char *)buffer, (unsigned int)entireStringLength);
SDLTest_Md5Final(&md5Context);
SDL_free(buffer);
keys = (Uint64 *)md5Context.digest;
return keys[0];
}
static SDL_TimerID SDLTest_SetTestTimeout(int timeout, SDL_TimerCallback callback)
{
Uint32 timeoutInMilliseconds;
SDL_TimerID timerID;
if (!callback) {
SDLTest_LogError("Timeout callback can't be NULL");
return 0;
}
if (timeout < 0) {
SDLTest_LogError("Timeout value must be bigger than zero.");
return 0;
}
timeoutInMilliseconds = timeout * 1000;
timerID = SDL_AddTimer(timeoutInMilliseconds, callback, NULL);
if (timerID == 0) {
SDLTest_LogError("Creation of SDL timer failed: %s", SDL_GetError());
return 0;
}
return timerID;
}
static Uint32 SDLCALL SDLTest_BailOut(void *userdata, SDL_TimerID timerID, Uint32 interval)
{
SDLTest_LogError("TestCaseTimeout timer expired. Aborting test run.");
exit(TEST_ABORTED);
return 0;
}
static int SDLTest_RunTest(SDLTest_TestSuiteReference *testSuite, const SDLTest_TestCaseReference *testCase, Uint64 execKey, bool forceTestRun)
{
SDL_TimerID timer = 0;
int testCaseResult = 0;
int testResult = 0;
int fuzzerCount;
void *data = NULL;
if (!testSuite || !testCase || !testSuite->name || !testCase->name) {
SDLTest_LogError("Setup failure: testSuite or testCase references NULL");
return TEST_RESULT_SETUP_FAILURE;
}
if (!testCase->enabled && forceTestRun == false) {
SDLTest_LogFinalResult(true, "Test", testCase->name, NULL, "Skipped (Disabled)");
return TEST_RESULT_SKIPPED;
}
SDLTest_FuzzerInit(execKey);
SDLTest_ResetAssertSummary();
timer = SDLTest_SetTestTimeout(SDLTest_TestCaseTimeout, SDLTest_BailOut);
if (testSuite->testSetUp) {
testSuite->testSetUp(&data);
if (SDLTest_AssertSummaryToTestResult() == TEST_RESULT_FAILED) {
SDLTest_LogFinalResult(false, "Suite Setup", testSuite->name, COLOR_RED, "Failed");
return TEST_RESULT_SETUP_FAILURE;
}
}
testCaseResult = testCase->testCase(data);
if (testCaseResult == TEST_SKIPPED) {
testResult = TEST_RESULT_SKIPPED;
} else if (testCaseResult == TEST_STARTED) {
testResult = TEST_RESULT_FAILED;
} else if (testCaseResult == TEST_ABORTED) {
testResult = TEST_RESULT_FAILED;
} else {
testResult = SDLTest_AssertSummaryToTestResult();
}
if (testSuite->testTearDown) {
testSuite->testTearDown(data);
}
if (timer) {
SDL_RemoveTimer(timer);
}
fuzzerCount = SDLTest_GetFuzzerInvocationCount();
if (fuzzerCount > 0) {
SDLTest_Log("Fuzzer invocations: %d", fuzzerCount);
}
if (testCaseResult == TEST_SKIPPED) {
SDLTest_LogFinalResult(true, "Test", testCase->name, COLOR_BLUE, "Skipped (Programmatically)");
} else if (testCaseResult == TEST_STARTED) {
SDLTest_LogFinalResult(false, "Test", testCase->name, COLOR_RED, "Skipped (test started, but did not return TEST_COMPLETED)");
} else if (testCaseResult == TEST_ABORTED) {
SDLTest_LogFinalResult(false, "Test", testCase->name, COLOR_RED, "Failed (Aborted)");
} else {
SDLTest_LogAssertSummary();
}
return testResult;
}
#if 0#endif
static float GetClock(void)
{
float currentClock = SDL_GetPerformanceCounter() / (float)SDL_GetPerformanceFrequency();
return currentClock;
}
int SDLTest_ExecuteTestSuiteRunner(SDLTest_TestSuiteRunner *runner)
{
int totalNumberOfTests = 0;
int failedNumberOfTests = 0;
int suiteCounter;
int testCounter;
int iterationCounter;
SDLTest_TestSuiteReference *testSuite;
const SDLTest_TestCaseReference *testCase;
const char *runSeed = NULL;
const char *currentSuiteName;
const char *currentTestName;
Uint64 execKey;
float runStartSeconds;
float suiteStartSeconds;
float testStartSeconds;
float runEndSeconds;
float suiteEndSeconds;
float testEndSeconds;
float runtime;
int suiteFilter = 0;
const char *suiteFilterName = NULL;
int testFilter = 0;
const char *testFilterName = NULL;
bool forceTestRun = false;
int testResult = 0;
int runResult = 0;
int totalTestFailedCount = 0;
int totalTestPassedCount = 0;
int totalTestSkippedCount = 0;
int testFailedCount = 0;
int testPassedCount = 0;
int testSkippedCount = 0;
int countSum = 0;
const SDLTest_TestCaseReference **failedTests;
char generatedSeed[16 + 1];
int nbSuites = 0;
int i = 0;
int *arraySuites = NULL;
if (runner->user.testIterations < 1) {
runner->user.testIterations = 1;
}
if (!runner->user.runSeed || runner->user.runSeed[0] == '\0') {
runSeed = SDLTest_GenerateRunSeed(generatedSeed, 16);
if (!runSeed) {
SDLTest_LogError("Generating a random seed failed");
return 2;
}
} else {
runSeed = runner->user.runSeed;
}
totalTestFailedCount = 0;
totalTestPassedCount = 0;
totalTestSkippedCount = 0;
runStartSeconds = GetClock();
SDLTest_Log("::::: Test Run /w seed '%s' started\n", runSeed);
suiteCounter = 0;
while (runner->user.testSuites[suiteCounter]) {
testSuite = runner->user.testSuites[suiteCounter];
suiteCounter++;
testCounter = 0;
while (testSuite->testCases[testCounter]) {
testCounter++;
totalNumberOfTests++;
}
}
if (totalNumberOfTests == 0) {
SDLTest_LogError("No tests to run?");
return -1;
}
failedTests = (const SDLTest_TestCaseReference **)SDL_malloc(totalNumberOfTests * sizeof(SDLTest_TestCaseReference *));
if (!failedTests) {
SDLTest_LogError("Unable to allocate cache for failed tests");
return -1;
}
if (runner->user.filter && runner->user.filter[0] != '\0') {
suiteCounter = 0;
while (runner->user.testSuites[suiteCounter] && suiteFilter == 0) {
testSuite = runner->user.testSuites[suiteCounter];
suiteCounter++;
if (testSuite->name && SDL_strcasecmp(runner->user.filter, testSuite->name) == 0) {
suiteFilter = 1;
suiteFilterName = testSuite->name;
SDLTest_Log("Filtering: running only suite '%s'", suiteFilterName);
break;
}
testCounter = 0;
while (testSuite->testCases[testCounter] && testFilter == 0) {
testCase = testSuite->testCases[testCounter];
testCounter++;
if (testCase->name && SDL_strcasecmp(runner->user.filter, testCase->name) == 0) {
suiteFilter = 1;
suiteFilterName = testSuite->name;
testFilter = 1;
testFilterName = testCase->name;
SDLTest_Log("Filtering: running only test '%s' in suite '%s'", testFilterName, suiteFilterName);
break;
}
}
}
if (suiteFilter == 0 && testFilter == 0) {
SDLTest_LogError("Filter '%s' did not match any test suite/case.", runner->user.filter);
for (suiteCounter = 0; runner->user.testSuites[suiteCounter]; ++suiteCounter) {
testSuite = runner->user.testSuites[suiteCounter];
if (testSuite->name) {
SDLTest_Log("Test suite: %s", testSuite->name);
}
for (testCounter = 0; testSuite->testCases[testCounter]; ++testCounter) {
testCase = testSuite->testCases[testCounter];
SDLTest_Log(" test: %s%s", testCase->name, testCase->enabled ? "" : " (disabled)");
}
}
SDLTest_Log("Exit code: 2");
SDL_free((void *)failedTests);
return 2;
}
runner->user.randomOrder = false;
}
while (runner->user.testSuites[nbSuites]) {
nbSuites++;
}
arraySuites = SDL_malloc(nbSuites * sizeof(int));
if (!arraySuites) {
SDL_free((void *)failedTests);
return SDL_OutOfMemory();
}
for (i = 0; i < nbSuites; i++) {
arraySuites[i] = i;
}
{
nbSuites--;
if (runner->user.execKey != 0) {
execKey = runner->user.execKey;
} else {
execKey = SDLTest_GenerateExecKey(runSeed, "random testSuites", "initialisation", 1);
}
SDLTest_FuzzerInit(execKey);
i = 100;
while (i--) {
int a, b;
int tmp;
a = SDLTest_RandomIntegerInRange(0, nbSuites - 1);
b = SDLTest_RandomIntegerInRange(0, nbSuites - 1);
if (runner->user.randomOrder) {
tmp = arraySuites[b];
arraySuites[b] = arraySuites[a];
arraySuites[a] = tmp;
}
}
nbSuites++;
}
for (i = 0; i < nbSuites; i++) {
suiteCounter = arraySuites[i];
testSuite = runner->user.testSuites[suiteCounter];
currentSuiteName = (testSuite->name ? testSuite->name : SDLTEST_INVALID_NAME_FORMAT);
suiteCounter++;
if (suiteFilter == 1 && suiteFilterName && testSuite->name &&
SDL_strcasecmp(suiteFilterName, testSuite->name) != 0) {
SDLTest_Log("===== Test Suite %i: '%s' " "%s" "skipped" "%s" "\n",
suiteCounter,
currentSuiteName,
COLOR_BLUE,
COLOR_END);
} else {
int nbTestCases = 0;
int *arrayTestCases;
int j;
while (testSuite->testCases[nbTestCases]) {
nbTestCases++;
}
arrayTestCases = SDL_malloc(nbTestCases * sizeof(int));
if (!arrayTestCases) {
SDL_free(arraySuites);
SDL_free((void *)failedTests);
return SDL_OutOfMemory();
}
for (j = 0; j < nbTestCases; j++) {
arrayTestCases[j] = j;
}
j = 100;
while (j--) {
int a, b;
int tmp;
a = SDLTest_RandomIntegerInRange(0, nbTestCases - 1);
b = SDLTest_RandomIntegerInRange(0, nbTestCases - 1);
if (runner->user.randomOrder) {
tmp = arrayTestCases[b];
arrayTestCases[b] = arrayTestCases[a];
arrayTestCases[a] = tmp;
}
}
testFailedCount = 0;
testPassedCount = 0;
testSkippedCount = 0;
suiteStartSeconds = GetClock();
SDLTest_Log("===== Test Suite %i: '%s' started\n",
suiteCounter,
currentSuiteName);
for (j = 0; j < nbTestCases; j++) {
testCounter = arrayTestCases[j];
testCase = testSuite->testCases[testCounter];
currentTestName = (testCase->name ? testCase->name : SDLTEST_INVALID_NAME_FORMAT);
testCounter++;
if (testFilter == 1 && testFilterName && testCase->name &&
SDL_strcasecmp(testFilterName, testCase->name) != 0) {
SDLTest_Log("===== Test Case %i.%i: '%s' " "%s" "skipped" "%s" "\n",
suiteCounter,
testCounter,
currentTestName,
COLOR_BLUE,
COLOR_END);
} else {
if (testFilter == 1 && !testCase->enabled) {
SDLTest_Log("Force run of disabled test since test filter was set");
forceTestRun = true;
}
testStartSeconds = GetClock();
SDLTest_Log("%s" "----- Test Case %i.%i: '%s' started" "%s",
COLOR_YELLOW,
suiteCounter,
testCounter,
currentTestName,
COLOR_END);
if (testCase->description && testCase->description[0] != '\0') {
SDLTest_Log("Test Description: '%s'",
(testCase->description) ? testCase->description : SDLTEST_INVALID_NAME_FORMAT);
}
iterationCounter = 0;
while (iterationCounter < runner->user.testIterations) {
iterationCounter++;
if (runner->user.execKey != 0) {
execKey = runner->user.execKey;
} else {
execKey = SDLTest_GenerateExecKey(runSeed, testSuite->name, testCase->name, iterationCounter);
}
SDLTest_Log("Test Iteration %i: execKey %" SDL_PRIu64, iterationCounter, execKey);
testResult = SDLTest_RunTest(testSuite, testCase, execKey, forceTestRun);
if (testResult == TEST_RESULT_PASSED) {
testPassedCount++;
totalTestPassedCount++;
} else if (testResult == TEST_RESULT_SKIPPED) {
testSkippedCount++;
totalTestSkippedCount++;
} else {
testFailedCount++;
totalTestFailedCount++;
}
}
testEndSeconds = GetClock();
runtime = testEndSeconds - testStartSeconds;
if (runtime < 0.0f) {
runtime = 0.0f;
}
if (runner->user.testIterations > 1) {
SDLTest_Log("Runtime of %i iterations: %.1f sec", runner->user.testIterations, runtime);
SDLTest_Log("Average Test runtime: %.5f sec", runtime / (float)runner->user.testIterations);
} else {
SDLTest_Log("Total Test runtime: %.1f sec", runtime);
}
switch (testResult) {
case TEST_RESULT_PASSED:
SDLTest_LogFinalResult(true, "Test", currentTestName, COLOR_GREEN, "Passed");
break;
case TEST_RESULT_FAILED:
SDLTest_LogFinalResult(false, "Test", currentTestName, COLOR_RED, "Failed");
break;
case TEST_RESULT_NO_ASSERT:
SDLTest_LogFinalResult(false, "Test", currentTestName, COLOR_BLUE, "No Asserts");
break;
}
if (testResult == TEST_RESULT_FAILED) {
failedTests[failedNumberOfTests] = testCase;
failedNumberOfTests++;
}
}
}
suiteEndSeconds = GetClock();
runtime = suiteEndSeconds - suiteStartSeconds;
if (runtime < 0.0f) {
runtime = 0.0f;
}
SDLTest_Log("Total Suite runtime: %.1f sec", runtime);
countSum = testPassedCount + testFailedCount + testSkippedCount;
if (testFailedCount == 0) {
SDLTest_LogSummary(true, "Suite", countSum, testPassedCount, testFailedCount, testSkippedCount);
SDLTest_LogFinalResult(true, "Suite", currentSuiteName, COLOR_GREEN, "Passed");
} else {
SDLTest_LogSummary(false, "Suite", countSum, testPassedCount, testFailedCount, testSkippedCount);
SDLTest_LogFinalResult(false, "Suite", currentSuiteName, COLOR_RED, "Failed");
}
SDL_free(arrayTestCases);
}
}
SDL_free(arraySuites);
runEndSeconds = GetClock();
runtime = runEndSeconds - runStartSeconds;
if (runtime < 0.0f) {
runtime = 0.0f;
}
SDLTest_Log("Total Run runtime: %.1f sec", runtime);
countSum = totalTestPassedCount + totalTestFailedCount + totalTestSkippedCount;
if (totalTestFailedCount == 0) {
runResult = 0;
SDLTest_LogSummary(true, "Run", countSum, totalTestPassedCount, totalTestFailedCount, totalTestSkippedCount);
SDLTest_LogFinalResult(true, "Run /w seed", runSeed, COLOR_GREEN, "Passed");
} else {
runResult = 1;
SDLTest_LogSummary(false, "Run", countSum, totalTestPassedCount, totalTestFailedCount, totalTestSkippedCount);
SDLTest_LogFinalResult(false, "Run /w seed", runSeed, COLOR_RED, "Failed");
}
if (failedNumberOfTests > 0) {
SDLTest_Log("Harness input to repro failures:");
for (testCounter = 0; testCounter < failedNumberOfTests; testCounter++) {
SDLTest_Log("%s" " --seed %s --filter %s" "%s", COLOR_RED, runSeed, failedTests[testCounter]->name, COLOR_END);
}
}
SDL_free((void *)failedTests);
SDLTest_Log("Exit code: %d", runResult);
return runResult;
}
static int SDLCALL SDLTest_TestSuiteCommonArg(void *data, char **argv, int index)
{
SDLTest_TestSuiteRunner *runner = data;
if (SDL_strcasecmp(argv[index], "--iterations") == 0) {
if (argv[index + 1]) {
runner->user.testIterations = SDL_atoi(argv[index + 1]);
if (runner->user.testIterations < 1) {
runner->user.testIterations = 1;
}
return 2;
}
}
else if (SDL_strcasecmp(argv[index], "--execKey") == 0) {
if (argv[index + 1]) {
(void)SDL_sscanf(argv[index + 1], "%" SDL_PRIu64, &runner->user.execKey);
return 2;
}
}
else if (SDL_strcasecmp(argv[index], "--seed") == 0) {
if (argv[index + 1]) {
runner->user.runSeed = SDL_strdup(argv[index + 1]);
return 2;
}
}
else if (SDL_strcasecmp(argv[index], "--filter") == 0) {
if (argv[index + 1]) {
runner->user.filter = SDL_strdup(argv[index + 1]);
return 2;
}
}
else if (SDL_strcasecmp(argv[index], "--random-order") == 0) {
runner->user.randomOrder = true;
return 1;
}
return 0;
}
SDLTest_TestSuiteRunner *SDLTest_CreateTestSuiteRunner(SDLTest_CommonState *state, SDLTest_TestSuiteReference *testSuites[])
{
SDLTest_TestSuiteRunner *runner;
SDLTest_ArgumentParser *argparser;
if (!state) {
SDLTest_LogError("SDL Test Suites require a common state");
return NULL;
}
runner = SDL_calloc(1, sizeof(SDLTest_TestSuiteRunner));
if (!runner) {
SDLTest_LogError("Failed to allocate memory for test suite runner");
return NULL;
}
runner->user.testSuites = testSuites;
runner->argparser.parse_arguments = SDLTest_TestSuiteCommonArg;
runner->argparser.usage = common_harness_usage;
runner->argparser.data = runner;
argparser = state->argparser;
for (;;) {
if (argparser->next == NULL) {
argparser->next = &runner->argparser;
break;
}
argparser = argparser->next;
}
return runner;
}
void SDLTest_DestroyTestSuiteRunner(SDLTest_TestSuiteRunner *runner) {
SDL_free(runner->user.filter);
SDL_free(runner->user.runSeed);
SDL_free(runner);
}