summaryrefslogtreecommitdiff
path: root/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'test/unit')
-rw-r--r--test/unit/ApplicationRunner.c297
-rw-r--r--test/unit/ApplicationRunner.h39
-rw-r--r--test/unit/IntegrationTests.c175
-rw-r--r--test/unit/TestRunner.c152
-rw-r--r--test/unit/TestRunner.h173
-rw-r--r--test/unit/UnitTests.c160
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);
+}