diff options
Diffstat (limited to 'test/unit')
| -rw-r--r-- | test/unit/ApplicationRunner.c | 297 | ||||
| -rw-r--r-- | test/unit/ApplicationRunner.h | 39 | ||||
| -rw-r--r-- | test/unit/IntegrationTests.c | 175 | ||||
| -rw-r--r-- | test/unit/TestRunner.c | 152 | ||||
| -rw-r--r-- | test/unit/TestRunner.h | 173 | ||||
| -rw-r--r-- | test/unit/UnitTests.c | 160 |
6 files changed, 996 insertions, 0 deletions
diff --git a/test/unit/ApplicationRunner.c b/test/unit/ApplicationRunner.c new file mode 100644 index 0000000..54b4812 --- /dev/null +++ b/test/unit/ApplicationRunner.c @@ -0,0 +1,297 @@ +#include <stdio.h> +#include <stdarg.h> + +#include "ApplicationRunner.h" +#include "base/File.h" +#include "base/PlatformInfo.h" +#include "analysis/AnalyzeFile.h" + +const char *kDefaultTestOutputFileType = "pcm"; +static const char *kApplicationRunnerOutputFolder = "out"; +static const int kApplicationRunnerWaitTimeoutInMs = 1000; + +CharString buildTestArgumentString(const char *arguments, ...) +{ + CharString formattedArguments; + va_list argumentList; + va_start(argumentList, arguments); + formattedArguments = newCharStringWithCapacity(kCharStringLengthLong); + vsnprintf(formattedArguments->data, formattedArguments->capacity, arguments, argumentList); + va_end(argumentList); + return formattedArguments; +} + +CharString getTestResourceFilename(const char *resourcesPath, const char *resourceType, const char *resourceName) +{ + CharString filename = newCharString(); + snprintf(filename->data, filename->capacity, "%s%c%s%c%s", + resourcesPath, PATH_DELIMITER, resourceType, PATH_DELIMITER, resourceName); + return filename; +} + +CharString getTestOutputFilename(const char *testName, const char *fileExtension) +{ + CharString filename = newCharString(); + char *space; + char *spacePtr; + + snprintf(filename->data, filename->capacity, "%s%c%s.%s", + kApplicationRunnerOutputFolder, PATH_DELIMITER, testName, fileExtension); + spacePtr = filename->data; + + do { + space = strchr(spacePtr + 1, ' '); + + if (space == NULL || (unsigned int)(space - filename->data) > strlen(filename->data)) { + break; + } else { + *space = '-'; + } + } while (true); + + return filename; +} + +static CharString _getTestPluginResourcesPath(const char *resourcesPath) +{ + CharString pluginRoot = newCharString(); + PlatformInfo platform = newPlatformInfo(); + snprintf(pluginRoot->data, pluginRoot->capacity, "%s%cvst%c%s", + resourcesPath, PATH_DELIMITER, PATH_DELIMITER, platform->shortName->data); + freePlatformInfo(platform); + return pluginRoot; +} + +static CharString _getDefaultArguments(TestEnvironment testEnvironment, const char *testName, const char *outputFilename) +{ + CharString outString = newCharStringWithCapacity(kCharStringLengthLong); + CharString logfileName = getTestOutputFilename(testName, "txt"); + CharString resourcesPath = _getTestPluginResourcesPath(testEnvironment->resourcesPath); + snprintf(outString->data, outString->capacity, + "--log-file \"%s\" --verbose --output \"%s\" --plugin-root \"%s\"", + logfileName->data, outputFilename, resourcesPath->data); + freeCharString(logfileName); + freeCharString(resourcesPath); + return outString; +} + +static void _removeOutputFile(const char *argument) +{ + CharString outputFilename = newCharStringWithCString(argument); + File outputFile = newFileWithPath(outputFilename); + + if (fileExists(outputFile)) { + fileRemove(outputFile); + } + + freeCharString(outputFilename); + freeFile(outputFile); +} + +static void _removeOutputFiles(const char *testName) +{ + // Remove all possible output files generated during testing + CharString outputFilename; + + outputFilename = getTestOutputFilename(testName, "aif"); + _removeOutputFile(outputFilename->data); + freeCharString(outputFilename); + outputFilename = getTestOutputFilename(testName, "flac"); + _removeOutputFile(outputFilename->data); + freeCharString(outputFilename); + outputFilename = getTestOutputFilename(testName, "pcm"); + _removeOutputFile(outputFilename->data); + freeCharString(outputFilename); + outputFilename = getTestOutputFilename(testName, "wav"); + _removeOutputFile(outputFilename->data); + freeCharString(outputFilename); + outputFilename = getTestOutputFilename(testName, "txt"); + _removeOutputFile(outputFilename->data); + freeCharString(outputFilename); +} + +static const char *_getResultCodeString(const int resultCode) +{ + switch (resultCode) { + case RETURN_CODE_SUCCESS: + return "Success"; + + case RETURN_CODE_NOT_RUN: + return "Not run"; + + case RETURN_CODE_INVALID_ARGUMENT: + return "Invalid argument"; + + case RETURN_CODE_MISSING_REQUIRED_OPTION: + return "Missing required option"; + + case RETURN_CODE_IO_ERROR: + return "I/O error"; + + case RETURN_CODE_PLUGIN_ERROR: + return "Plugin error"; + + case RETURN_CODE_INVALID_PLUGIN_CHAIN: + return "Invalid plugin chain"; + + case RETURN_CODE_UNSUPPORTED_FEATURE: + return "Unsupported feature"; + + case RETURN_CODE_INTERNAL_ERROR: + return "Internal error"; + + case RETURN_CODE_SIGNAL: + return "Caught signal"; + + default: + return "Unknown"; + } +} + +void runIntegrationTest(const TestEnvironment testEnvironment, + const char *testName, CharString testArguments, + ReturnCodes expectedResultCode, const char *outputFileType) +{ + int result = -1; + ReturnCodes resultCode = (ReturnCodes)result; + CharString arguments = newCharStringWithCapacity(kCharStringLengthLong); + CharString defaultArguments; + CharString failedAnalysisFunctionName = newCharString(); + ChannelCount failedAnalysisChannel; + SampleCount failedAnalysisFrame; + File outputFolder = NULL; + CharString outputFilename = getTestOutputFilename(testName, + outputFileType == NULL ? kDefaultTestOutputFileType : outputFileType); + +#if WINDOWS + STARTUPINFOA startupInfo; + PROCESS_INFORMATION processInfo; +#endif + + // Remove files from a previous test run + outputFolder = newFileWithPathCString(kApplicationRunnerOutputFolder); + + if (fileExists(outputFolder)) { + _removeOutputFiles(testName); + } else { + fileCreate(outputFolder, kFileTypeDirectory); + } + + // Create the command line argument + charStringAppendCString(arguments, "\""); + charStringAppendCString(arguments, testEnvironment->applicationPath); + charStringAppendCString(arguments, "\""); + charStringAppendCString(arguments, " "); + defaultArguments = _getDefaultArguments(testEnvironment, testName, outputFilename->data); + charStringAppend(arguments, defaultArguments); + charStringAppendCString(arguments, " "); + charStringAppend(arguments, testArguments); + + if (!testEnvironment->results->onlyPrintFailing) { + printTestName(testName); + } + +#if WINDOWS + memset(&startupInfo, 0, sizeof(startupInfo)); + memset(&processInfo, 0, sizeof(processInfo)); + startupInfo.cb = sizeof(startupInfo); + result = CreateProcessA((LPCSTR)(testEnvironment->applicationPath), (LPSTR)(arguments->data), + 0, 0, false, CREATE_DEFAULT_ERROR_MODE, 0, 0, &startupInfo, &processInfo); + + if (result) { + // TODO: Check return codes for these calls + WaitForSingleObject(processInfo.hProcess, kApplicationRunnerWaitTimeoutInMs); + GetExitCodeProcess(processInfo.hProcess, (LPDWORD)&resultCode); + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + } else { + logCritical("Could not launch process, got error %s", stringForLastError(GetLastError())); + return; + } + +#else + result = system(arguments->data); + resultCode = (ReturnCodes)WEXITSTATUS(result); +#endif + + if (resultCode == RETURN_CODE_FORK_FAILED || + resultCode == RETURN_CODE_SHELL_FAILED || + resultCode == RETURN_CODE_LAUNCH_FAILED_OTHER) { + if (testEnvironment->results->onlyPrintFailing) { + printTestName(testName); + } + + printTestFail(); + logCritical("Could not launch shell, got return code %d\n\ +Please check the executable path specified in the --mrswatson-path argument.", + resultCode); + testEnvironment->results->numFail++; + } else if (resultCode == expectedResultCode) { + if (outputFileType != NULL) { + if (analyzeFile(outputFilename->data, failedAnalysisFunctionName, + &failedAnalysisChannel, &failedAnalysisFrame)) { + testEnvironment->results->numSuccess++; + + if (!testEnvironment->results->keepFiles) { + _removeOutputFiles(testName); + } + + if (!testEnvironment->results->onlyPrintFailing) { + printTestSuccess(); + } + } else { + if (testEnvironment->results->onlyPrintFailing) { + printTestName(testName); + } + + fprintf(stderr, "Audio analysis check for %s failed at channel %d, frame %lu. ", + failedAnalysisFunctionName->data, failedAnalysisChannel, failedAnalysisFrame); + printTestFail(); + testEnvironment->results->numFail++; + } + } else { + testEnvironment->results->numSuccess++; + + if (!testEnvironment->results->keepFiles) { + _removeOutputFiles(testName); + } + + if (!testEnvironment->results->onlyPrintFailing) { + printTestSuccess(); + } + } + } else { + if (testEnvironment->results->onlyPrintFailing) { + printTestName(testName); + } + + fprintf(stderr, "Expected result code %d (%s), got %d (%s). ", + expectedResultCode, _getResultCodeString(expectedResultCode), + resultCode, _getResultCodeString(resultCode)); + printTestFail(); + testEnvironment->results->numFail++; + } + + freeCharString(outputFilename); + freeCharString(arguments); + freeCharString(defaultArguments); + freeCharString(testArguments); + freeCharString(failedAnalysisFunctionName); +} + +void freeTestEnvironment(TestEnvironment self) +{ + if (self != NULL) { + freeTestSuite(self->results); + free(self); + } +} + +TestEnvironment newTestEnvironment(char *applicationPath, char *resourcesPath) +{ + TestEnvironment testEnvironment = (TestEnvironment)malloc(sizeof(TestEnvironmentMembers)); + testEnvironment->applicationPath = applicationPath; + testEnvironment->resourcesPath = resourcesPath; + testEnvironment->results = newTestSuite("Results", NULL, NULL); + return testEnvironment; +} diff --git a/test/unit/ApplicationRunner.h b/test/unit/ApplicationRunner.h new file mode 100644 index 0000000..7682f01 --- /dev/null +++ b/test/unit/ApplicationRunner.h @@ -0,0 +1,39 @@ +#ifndef MrsWatson_ApplicationRunner_h +#define MrsWatson_ApplicationRunner_h + +#if HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <stdlib.h> +#include "unit/TestRunner.h" +#include "base/LinkedList.h" +#include "logging/EventLogger.h" +#include "base/CharString.h" +#include "MrsWatson.h" + +extern const char *kDefaultTestOutputFileType; + +typedef struct { + int currentIndex; + char **outArray; +} ArgumentsCopyData; + +typedef struct { + char *applicationPath; + char *resourcesPath; + TestSuite results; +} TestEnvironmentMembers; +typedef TestEnvironmentMembers *TestEnvironment; +TestEnvironment newTestEnvironment(char *applicationPath, char *resourcesPath); + +void runIntegrationTest(const TestEnvironment testEnvironment, + const char *testName, CharString testArguments, + ReturnCodes expectedResultCode, const char *outputFileType); + +CharString buildTestArgumentString(const char *arguments, ...); +CharString getTestResourceFilename(const char *resourcesPath, const char *resourceType, const char *resourceName); +CharString getTestOutputFilename(const char *testName, const char *fileExtension); + +void freeTestEnvironment(TestEnvironment testEnvironment); + +#endif diff --git a/test/unit/IntegrationTests.c b/test/unit/IntegrationTests.c new file mode 100644 index 0000000..b2180ba --- /dev/null +++ b/test/unit/IntegrationTests.c @@ -0,0 +1,175 @@ +#include "ApplicationRunner.h" + +extern void _printTestSummary(int testsRun, int testsPassed, int testsFailed, int testsSkipped); + +void runIntegrationTests(TestEnvironment environment); +void runIntegrationTests(TestEnvironment environment) +{ + // Test resource paths + const char *resourcesPath = environment->resourcesPath; + CharString _a440_mono_pcm = getTestResourceFilename(resourcesPath, "audio", "a440-mono.pcm"); + CharString _a440_stereo_aiff = getTestResourceFilename(resourcesPath, "audio", "a440-stereo.aif"); + CharString _a440_stereo_flac = getTestResourceFilename(resourcesPath, "audio", "a440-stereo.flac"); + CharString _a440_stereo_pcm = getTestResourceFilename(resourcesPath, "audio", "a440-stereo.pcm"); + CharString _a440_stereo_wav = getTestResourceFilename(resourcesPath, "audio", "a440-stereo.wav"); + CharString _c_scale_mid = getTestResourceFilename(resourcesPath, "midi", "c-scale.mid"); + CharString _again_test_fxp = getTestResourceFilename(resourcesPath, "presets", "again-test.fxp"); + const char *a440_stereo_aiff = _a440_stereo_aiff->data; + const char *a440_stereo_flac = _a440_stereo_flac->data; + const char *a440_mono_pcm = _a440_mono_pcm->data; + const char *a440_stereo_pcm = _a440_stereo_pcm->data; + const char *a440_stereo_wav = _a440_stereo_wav->data; + const char *c_scale_mid = _c_scale_mid->data; + const char *again_test_fxp = _again_test_fxp->data; + + // Basic non-processing operations + runIntegrationTest(environment, "List plugins", + newCharStringWithCString("--list-plugins"), + RETURN_CODE_NOT_RUN, NULL); + runIntegrationTest(environment, "List file types", + newCharStringWithCString("--list-file-types"), + RETURN_CODE_NOT_RUN, NULL); + runIntegrationTest(environment, "Invalid argument", + newCharStringWithCString("--invalid"), + RETURN_CODE_INVALID_ARGUMENT, NULL); + + // Invalid configurations + runIntegrationTest(environment, "Run with no plugins", + newCharString(), + RETURN_CODE_INVALID_PLUGIN_CHAIN, NULL); + runIntegrationTest(environment, "Effect with no input source", + newCharStringWithCString("--plugin again"), + RETURN_CODE_MISSING_REQUIRED_OPTION, NULL); + runIntegrationTest(environment, "Instrument with no MIDI source", + newCharStringWithCString("--plugin vstxsynth"), + RETURN_CODE_MISSING_REQUIRED_OPTION, NULL); + runIntegrationTest(environment, "Plugin chain with instrument not at head", + buildTestArgumentString("--plugin \"again;vstxsynth\" --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_INVALID_PLUGIN_CHAIN, NULL); + runIntegrationTest(environment, "Plugin with invalid preset", + buildTestArgumentString("--plugin \"again,invalid.fxp\" --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Preset for wrong plugin", + buildTestArgumentString("--plugin \"vstxsynth,%s\" --midi-file \"%s\"", again_test_fxp, c_scale_mid), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Set invalid parameter", + buildTestArgumentString("--plugin again --input \"%s\" --parameter 1,0.5", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Set invalid time signature", + buildTestArgumentString("--plugin again --input \"%s\" --time-signature invalid", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Set invalid tempo", + buildTestArgumentString("--plugin again --input \"%s\" --tempo 0", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Set invalid blocksize", + buildTestArgumentString("--plugin again --input \"%s\" --blocksize 0", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Set invalid bit depth", + buildTestArgumentString("--plugin again --input \"%s\" --bit-depth 5", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Set invalid channel count", + buildTestArgumentString("--plugin again --input \"%s\" --channels 0", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Set invalid sample rate", + buildTestArgumentString("--plugin again --input \"%s\" --sample-rate 0", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + + // Audio file types + runIntegrationTest(environment, "Read PCM file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Write PCM file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, "pcm"); + runIntegrationTest(environment, "Read WAV file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_wav), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Write WAV file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, "wav"); + +#if USE_AUDIOFILE + runIntegrationTest(environment, "Read AIFF file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_aiff), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Write AIFF file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, "aif"); +#endif + +#if USE_FLAC + runIntegrationTest(environment, "Read FLAC file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_flac), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Write FLAC file", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, "flac"); +#endif + + // Configuration tests + runIntegrationTest(environment, "Read mono input source", + buildTestArgumentString("--plugin again --input \"%s\" --channels 1", a440_mono_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Read with user-defined sample rate", + buildTestArgumentString("--plugin again --input \"%s\" --sample-rate 48000", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Read with user-defined blocksize", + buildTestArgumentString("--plugin again --input \"%s\" --blocksize 128", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Set parameter", + buildTestArgumentString("--plugin again --input \"%s\" --parameter 0,0.5", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Set time signature", + buildTestArgumentString("--plugin again --input \"%s\" --time-signature 3/4", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + + // Internal plugins + runIntegrationTest(environment, "Process with internal limiter", + buildTestArgumentString("--plugin mrs_limiter --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Process with internal gain plugin", + buildTestArgumentString("--plugin mrs_gain --parameter 0,0.5 --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Process with internal gain plugin and invalid parameter", + buildTestArgumentString("--plugin mrs_gain --parameter 1,0.5 --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_INVALID_ARGUMENT, NULL); + runIntegrationTest(environment, "Process with internal passthru plugin", + buildTestArgumentString("--plugin mrs_passthru --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); +#if 0 + // This test case works, but fails the analysis check for silence (obviously). + // It will remain disabled until we have a smarter way to specify which analysis + // functions should be run for each integration test. + runIntegrationTest(environment, "Process with silence generator", + newCharStringWithCString("--plugin mrs_silence --max-time 1000"), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); +#endif + + // Plugin processing tests + runIntegrationTest(environment, "Process audio with again plugin", + buildTestArgumentString("--plugin again --input \"%s\"", a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Process MIDI with vstxsynth plugin", + buildTestArgumentString("--plugin vstxsynth --midi-file \"%s\"", c_scale_mid), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Process effect chain", + buildTestArgumentString("--plugin vstxsynth,again --midi-file \"%s\"", c_scale_mid), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Load FXP preset to VST", + buildTestArgumentString("--plugin \"again,%s\" --input \"%s\"", again_test_fxp, a440_stereo_pcm), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + runIntegrationTest(environment, "Load internal program to VST", + buildTestArgumentString("--plugin vstxsynth,2 --midi-file \"%s\"", c_scale_mid), + RETURN_CODE_SUCCESS, kDefaultTestOutputFileType); + + _printTestSummary(environment->results->numSuccess + environment->results->numFail + environment->results->numSkips, + environment->results->numSuccess, environment->results->numFail, environment->results->numSkips); + + freeCharString(_a440_stereo_aiff); + freeCharString(_a440_stereo_flac); + freeCharString(_a440_mono_pcm); + freeCharString(_a440_stereo_pcm); + freeCharString(_a440_stereo_wav); + freeCharString(_c_scale_mid); + freeCharString(_again_test_fxp); +} diff --git a/test/unit/TestRunner.c b/test/unit/TestRunner.c new file mode 100644 index 0000000..4c90cb3 --- /dev/null +++ b/test/unit/TestRunner.c @@ -0,0 +1,152 @@ +#include <stdlib.h> +#if WINDOWS +#include <io.h> +#endif + +#include "unit/TestRunner.h" + +void addTestToTestSuite(TestSuite testSuite, TestCase testCase) +{ + linkedListAppend(testSuite->testCases, testCase); +} + +const LogColor getLogColor(TestLogEventType eventType) +{ + switch (eventType) { + case kTestLogEventSection: + return isatty(1) ? COLOR_FG_CYAN : COLOR_NONE; + + case kTestLogEventPass: + return isatty(1) ? COLOR_FG_GREEN : COLOR_NONE; + + case kTestLogEventFail: + return isatty(1) ? COLOR_BG_MAROON : COLOR_NONE; + + case kTestLogEventSkip: + return isatty(1) ? COLOR_FG_YELLOW : COLOR_NONE; + + case kTestLogEventReset: + return isatty(1) ? COLOR_RESET : COLOR_NONE; + + default: + return COLOR_NONE; + } +} + +void printTestSuccess(void) +{ + printToLog(getLogColor(kTestLogEventPass), NULL, "OK"); + flushLog(NULL); +} + +void printTestFail(void) +{ + printToLog(getLogColor(kTestLogEventFail), NULL, "FAIL"); + flushLog(NULL); +} + +static void _printTestSkipped(void) +{ + printToLog(getLogColor(kTestLogEventSkip), NULL, "Skipped"); + flushLog(NULL); +} + +void printTestName(const char *testName) +{ + fprintf(stderr, " %s: ", testName); + // Flush standard output in case the test crashes. That way at least the + // crashing test name is seen. + fflush(stderr); +} + +void runTestCase(void *item, void *extraData) +{ + TestCase testCase = (TestCase)item; + TestSuite testSuite = (TestSuite)extraData; + int result; + + if (!testSuite->onlyPrintFailing) { + printTestName(testCase->name); + } + + if (testCase->testCaseFunc != NULL) { + if (testSuite->setup != NULL) { + testSuite->setup(); + } + + result = testCase->testCaseFunc(); + + if (result == 0) { + if (!testSuite->onlyPrintFailing) { + printTestSuccess(); + } + + testSuite->numSuccess++; + } else { + printTestFail(); + testSuite->numFail++; + } + + if (testSuite->teardown != NULL) { + testSuite->teardown(); + } + } else { + if (!testSuite->onlyPrintFailing) { + _printTestSkipped(); + } + + testSuite->numSkips++; + } +} + +void runTestSuite(void *testSuitePtr, void *extraData) +{ + TestSuite testSuite = (TestSuite)testSuitePtr; + + printToLog(getLogColor(kTestLogEventReset), NULL, "Running tests in "); + printToLog(getLogColor(kTestLogEventSection), NULL, testSuite->name); + flushLog(NULL); + + linkedListForeach(testSuite->testCases, runTestCase, testSuite); +} + +// In both the TestSuite and TestCase objects we assume that we do not need ownership of +// the strings passed in, since they should be allocated on the heap and live for the +// lifetime of the program. +TestSuite newTestSuite(char *name, TestCaseSetupFunc setup, TestCaseTeardownFunc teardown) +{ + TestSuite testSuite = (TestSuite)malloc(sizeof(TestSuiteMembers)); + testSuite->name = name; + testSuite->numSuccess = 0; + testSuite->numFail = 0; + testSuite->numSkips = 0; + testSuite->testCases = newLinkedList(); + testSuite->setup = setup; + testSuite->teardown = teardown; + testSuite->onlyPrintFailing = false; + testSuite->keepFiles = false; + return testSuite; +} + +TestCase newTestCase(char *name, char *filename, int lineNumber, TestCaseExecFunc testCaseFunc) +{ + TestCase testCase = (TestCase)malloc(sizeof(TestCaseMembers)); + testCase->name = name; + testCase->filename = filename; + testCase->lineNumber = lineNumber; + testCase->testCaseFunc = testCaseFunc; + return testCase; +} + +void freeTestCase(TestCase self) +{ + free(self); +} + +void freeTestSuite(TestSuite self) +{ + if (self != NULL) { + freeLinkedListAndItems(self->testCases, (LinkedListFreeItemFunc)freeTestCase); + free(self); + } +} diff --git a/test/unit/TestRunner.h b/test/unit/TestRunner.h new file mode 100644 index 0000000..597a80a --- /dev/null +++ b/test/unit/TestRunner.h @@ -0,0 +1,173 @@ +#ifndef MrsWatsonTest_TestRunner_h +#define MrsWatsonTest_TestRunner_h + +#include <stdio.h> +#include <string.h> +#include <math.h> + +#include "base/CharString.h" +#include "base/File.h" +#include "logging/LogPrinter.h" + +#if UNIX +#include <unistd.h> +#endif + +#ifndef __func__ +#define __func__ __FUNCTION__ +#endif + +typedef enum { + kTestLogEventSection, + kTestLogEventPass, + kTestLogEventFail, + kTestLogEventSkip, + kTestLogEventReset, + kTestLogEventInvalid +} TestLogEventType; +const LogColor getLogColor(TestLogEventType eventType); + +typedef int (*TestCaseExecFunc)(void); +typedef void (*TestCaseSetupFunc)(void); +typedef void (*TestCaseTeardownFunc)(void); + +typedef struct { + char *name; + char *filename; + int lineNumber; + TestCaseExecFunc testCaseFunc; +} TestCaseMembers; +typedef TestCaseMembers *TestCase; + +typedef struct { + char *name; + int numSuccess; + int numFail; + int numSkips; + LinkedList testCases; + TestCaseSetupFunc setup; + TestCaseTeardownFunc teardown; + boolByte onlyPrintFailing; + boolByte keepFiles; +} TestSuiteMembers; +typedef TestSuiteMembers *TestSuite; + +void addTestToTestSuite(TestSuite testSuite, TestCase testCase); +void runTestSuite(void *testSuitePtr, void *extraData); +void runTestCase(void *item, void *extraData); +void printTestName(const char *testName); +void printTestSuccess(void); +void printTestFail(void); + +TestSuite newTestSuite(char *name, TestCaseSetupFunc setup, TestCaseTeardownFunc teardown); +TestCase newTestCase(char *name, char *filename, int lineNumber, TestCaseExecFunc testCaseFunc); + +void freeTestCase(TestCase self); +void freeTestSuite(TestSuite self); + +static const char *_getFileBasename(const char *filename) +{ + const char *lastDelimiter; + + if (filename == NULL) { + return NULL; + } + + lastDelimiter = strrchr(filename, PATH_DELIMITER); + + if (lastDelimiter == NULL) { + return (char *)filename; + } else { + return lastDelimiter + 1; + } +} + +#define addTest(testSuite, name, testCaseFunc) { \ + addTestToTestSuite(testSuite, newTestCase(name, __FILE__, __LINE__, testCaseFunc)); \ + } + +#define assert(_result) { \ + if(!(_result)) { \ + fprintf(stderr, "\nAssertion failed at %s:%d. ", _getFileBasename(__FILE__), __LINE__); \ + return 1; \ + } \ + } + +#define assertFalse(_result) assert((_result) == false) +#define assertIsNull(_result) assert((_result) == NULL) +#define assertNotNull(_result) assert((_result) != NULL) + +#define assertIntEquals(expected, _result) { \ + int _resultInt = _result; \ + if(_resultInt != expected) { \ + fprintf(stderr, "Assertion failed at %s:%d. Expected %d, got %d. ", _getFileBasename(__FILE__), __LINE__, expected, _resultInt); \ + return 1; \ + } \ + } + +#define assertLongEquals(expected, _result) { \ + long _resultLong = _result; \ + if(_resultLong != expected) { \ + fprintf(stderr, "Assertion failed at %s:%d. Expected %lu, got %lu. ", _getFileBasename(__FILE__), __LINE__, expected, _resultLong); \ + return 1; \ + } \ + } + +#define ZERO_UNSIGNED_LONG (unsigned long)0 +#define assertUnsignedLongEquals(expected, _result) { \ + unsigned long _resultULong = _result; \ + if(_resultULong != expected) { \ + fprintf(stderr, "Assertion failed at %s:%d. Expected %ld, got %ld. ", _getFileBasename(__FILE__), __LINE__, expected, _resultULong); \ + return 1; \ + } \ + } + +#define assertSizeEquals(expected, _result) { \ + size_t _resultSize = _result; \ + if(_result != expected) { \ + fprintf(stderr, "Assertion failed at %s:%d. Expected %zu, got %zu. ", _getFileBasename(__FILE__), __LINE__, expected, _resultSize); \ + return 1; \ + } \ + } + +#define TEST_DEFAULT_TOLERANCE 0.01 +#define TEST_EXACT_TOLERANCE 0.0 + +#define assertDoubleEquals(expected, _result, tolerance) { \ + double resultRounded = floor(_result * 100.0) / 100.0; \ + double expectedRounded = floor(expected * 100.0) / 100.0; \ + double _resultDiff = fabs(resultRounded - expectedRounded); \ + if(_resultDiff > tolerance) { \ + fprintf(stderr, "Assertion failed at %s:%d. Expected %g, got %g. ", _getFileBasename(__FILE__), __LINE__, expectedRounded, resultRounded); \ + return 1; \ + } \ + } + + // Timing assertions fail all the time in debug mode, because the binary is + // running in the debugger, is not optimized, is being profiled, etc. So for + // debug builds, we should not return early here, or else that will cause + // valgrind to go crazy and report a bunch of leaks. +#define assertTimeEquals(expected, _result, tolerance) { \ + double resultRounded = floor(_result * 100.0) / 100.0; \ + double expectedRounded = floor(expected * 100.0) / 100.0; \ + double _resultDiff = fabs(resultRounded - expectedRounded); \ + if(_resultDiff > tolerance) { \ + fprintf(stderr, "Warning: timing assertion failed at %s:%d. Expected %gms, got %gms. ", _getFileBasename(__FILE__), __LINE__, expectedRounded, resultRounded); \ + } \ + } + +#define assertCharStringEquals(expected, _result) { \ + if(!charStringIsEqualToCString(_result, expected, false)) { \ + fprintf(stderr, "Assertion failed at %s:%d. Expected '%s', got '%s'. ", _getFileBasename(__FILE__), __LINE__, expected, _result->data); \ + return 1; \ + } \ + } + +#define assertCharStringContains(expected, _result) { \ + if(strstr(_result->data, expected) == NULL) { \ + fprintf(stderr, "Assertion failed at %s:%d. Expected '%s' to contain '%s'. ", _getFileBasename(__FILE__), __LINE__, _result->data, expected); \ + return 1; \ + } \ + } + +#endif diff --git a/test/unit/UnitTests.c b/test/unit/UnitTests.c new file mode 100644 index 0000000..b2bae69 --- /dev/null +++ b/test/unit/UnitTests.c @@ -0,0 +1,160 @@ +#include <stdlib.h> +#include "base/LinkedList.h" +#include "unit/TestRunner.h" + +extern TestSuite addAudioClockTests(void); +extern TestSuite addAudioSettingsTests(void); +extern TestSuite addCharStringTests(void); +extern TestSuite addEndianTests(void); +extern TestSuite addFileTests(void); +extern TestSuite addLinkedListTests(void); +extern TestSuite addMidiSequenceTests(void); +extern TestSuite addMidiSourceTests(void); +extern TestSuite addPlatformInfoTests(void); +extern TestSuite addPluginTests(void); +extern TestSuite addPluginChainTests(void); +extern TestSuite addPluginPresetTests(void); +extern TestSuite addPluginVst2xIdTests(void); +extern TestSuite addProgramOptionTests(void); +extern TestSuite addSampleBufferTests(void); +extern TestSuite addSampleSourceTests(void); +extern TestSuite addTaskTimerTests(void); + +extern TestSuite addAnalysisClippingTests(void); +extern TestSuite addAnalysisDistortionTests(void); +extern TestSuite addAnalysisSilenceTests(void); + +extern void _printTestSummary(int testsRun, int testsPassed, int testsFailed, int testsSkipped); + +static void _sumTestSuiteResults(void *item, void *extraData) +{ + TestSuite testSuite = (TestSuite)item; + TestSuite result = (TestSuite)extraData; + result->numSuccess += testSuite->numSuccess; + result->numFail += testSuite->numFail; + result->numSkips += testSuite->numSkips; +} + +LinkedList getTestSuites(void); +LinkedList getTestSuites(void) +{ + LinkedList unitTestSuites = newLinkedList(); + linkedListAppend(unitTestSuites, addAudioClockTests()); + linkedListAppend(unitTestSuites, addAudioSettingsTests()); + linkedListAppend(unitTestSuites, addCharStringTests()); + linkedListAppend(unitTestSuites, addEndianTests()); + linkedListAppend(unitTestSuites, addFileTests()); + linkedListAppend(unitTestSuites, addLinkedListTests()); + linkedListAppend(unitTestSuites, addMidiSequenceTests()); + linkedListAppend(unitTestSuites, addMidiSourceTests()); + linkedListAppend(unitTestSuites, addPlatformInfoTests()); + linkedListAppend(unitTestSuites, addPluginTests()); + linkedListAppend(unitTestSuites, addPluginChainTests()); + linkedListAppend(unitTestSuites, addPluginPresetTests()); + linkedListAppend(unitTestSuites, addPluginVst2xIdTests()); + linkedListAppend(unitTestSuites, addProgramOptionTests()); + linkedListAppend(unitTestSuites, addSampleBufferTests()); + linkedListAppend(unitTestSuites, addSampleSourceTests()); + linkedListAppend(unitTestSuites, addTaskTimerTests()); + + linkedListAppend(unitTestSuites, addAnalysisClippingTests()); + linkedListAppend(unitTestSuites, addAnalysisDistortionTests()); + linkedListAppend(unitTestSuites, addAnalysisSilenceTests()); + + return unitTestSuites; +} + +static void _setTestSuiteOnlyPrintFailing(void *item, void *userData) +{ + TestSuite testSuite = (TestSuite)item; + testSuite->onlyPrintFailing = true; +} + +TestSuite runUnitTests(LinkedList testSuites, boolByte onlyPrintFailing); +TestSuite runUnitTests(LinkedList testSuites, boolByte onlyPrintFailing) +{ + TestSuite suiteResults; + + if (onlyPrintFailing) { + linkedListForeach(testSuites, _setTestSuiteOnlyPrintFailing, NULL); + } + + linkedListForeach(testSuites, runTestSuite, NULL); + // Create a new test suite to be used as the userData passed to the foreach loop + suiteResults = newTestSuite("Suite results", NULL, NULL); + linkedListForeach(testSuites, _sumTestSuiteResults, suiteResults); + + _printTestSummary(suiteResults->numSuccess + suiteResults->numFail + suiteResults->numSkips, + suiteResults->numSuccess, suiteResults->numFail, suiteResults->numSkips); + + return suiteResults; +} + +TestCase findTestCase(TestSuite testSuite, char *testName); +TestCase findTestCase(TestSuite testSuite, char *testName) +{ + LinkedList iterator = testSuite->testCases; + TestCase currentTestCase = NULL; + + while (iterator != NULL) { + if (iterator->item != NULL) { + currentTestCase = (TestCase)iterator->item; + + if (!strncasecmp(currentTestCase->name, testName, strlen(currentTestCase->name))) { + return currentTestCase; + } + } + + iterator = iterator->nextItem; + } + + return NULL; +} + +TestSuite findTestSuite(LinkedList testSuites, const CharString testSuiteName); +TestSuite findTestSuite(LinkedList testSuites, const CharString testSuiteName) +{ + LinkedList iterator = testSuites; + TestSuite testSuite = NULL; + + if (testSuiteName == NULL || charStringIsEmpty(testSuiteName)) { + return NULL; + } + + while (iterator != NULL) { + if (iterator->item != NULL) { + testSuite = (TestSuite)iterator->item; + + if (charStringIsEqualToCString(testSuiteName, testSuite->name, true)) { + break; + } else { + testSuite = NULL; + } + } + + iterator = iterator->nextItem; + } + + return testSuite; +} + +static void _printTestCases(void *item, void *userData) +{ + TestCase testCase = (TestCase)item; + char *testSuiteName = (char *)userData; + printf("%s:%s\n", testSuiteName, testCase->name); +} + +static void _printTestsInSuite(void *item, void *userData) +{ + TestSuite testSuite = (TestSuite)item; + linkedListForeach(testSuite->testCases, _printTestCases, testSuite->name); +} + +void printUnitTestSuites(void); +void printUnitTestSuites(void) +{ + LinkedList unitTestSuites = getTestSuites(); + linkedListForeach(unitTestSuites, _printTestsInSuite, NULL); + freeLinkedListAndItems(unitTestSuites, (LinkedListFreeItemFunc)freeTestSuite); +} |
