2021-02-18 16:06:44 +00:00
|
|
|
/* See LICENSE.txt for the full license governing this code. */
|
2015-06-21 15:33:46 +00:00
|
|
|
/**
|
2021-12-21 11:24:20 +00:00
|
|
|
* \file testharness.c
|
2015-06-21 15:33:46 +00:00
|
|
|
*
|
|
|
|
* Source file for the test harness.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <SDL_test.h>
|
|
|
|
#include <SDL.h>
|
|
|
|
#include <SDL_assert.h>
|
|
|
|
#include "SDL_visualtest_harness_argparser.h"
|
|
|
|
#include "SDL_visualtest_process.h"
|
|
|
|
#include "SDL_visualtest_variators.h"
|
|
|
|
#include "SDL_visualtest_screenshot.h"
|
|
|
|
#include "SDL_visualtest_mischelper.h"
|
|
|
|
|
|
|
|
#if defined(__WIN32__) && !defined(__CYGWIN__)
|
|
|
|
#include <direct.h>
|
|
|
|
#elif defined(__WIN32__) && defined(__CYGWIN__)
|
|
|
|
#include <signal.h>
|
|
|
|
#elif defined(__LINUX__)
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include <signal.h>
|
|
|
|
#else
|
|
|
|
#error "Unsupported platform"
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/** Code for the user event triggered when a new action is to be executed */
|
|
|
|
#define ACTION_TIMER_EVENT 0
|
|
|
|
/** Code for the user event triggered when the maximum timeout is reached */
|
|
|
|
#define KILL_TIMER_EVENT 1
|
|
|
|
/** FPS value used for delays in the action loop */
|
|
|
|
#define ACTION_LOOP_FPS 10
|
|
|
|
|
|
|
|
/** Value returned by RunSUTAndTest() when the test has passed */
|
|
|
|
#define TEST_PASSED 1
|
|
|
|
/** Value returned by RunSUTAndTest() when the test has failed */
|
|
|
|
#define TEST_FAILED 0
|
|
|
|
/** Value returned by RunSUTAndTest() on a fatal error */
|
|
|
|
#define TEST_ERROR -1
|
|
|
|
|
|
|
|
static SDL_ProcessInfo pinfo;
|
|
|
|
static SDL_ProcessExitStatus sut_exitstatus;
|
|
|
|
static SDLVisualTest_HarnessState state;
|
|
|
|
static SDLVisualTest_Variator variator;
|
|
|
|
static SDLVisualTest_ActionNode* current; /* the current action being performed */
|
|
|
|
static SDL_TimerID action_timer, kill_timer;
|
|
|
|
|
|
|
|
/* returns a char* to be passed as the format argument of a printf-style function. */
|
|
|
|
static char*
|
|
|
|
usage()
|
|
|
|
{
|
|
|
|
return "Usage: \n%s --sutapp xyz"
|
|
|
|
" [--sutargs abc | --parameter-config xyz.parameters"
|
|
|
|
" [--variator exhaustive|random]"
|
|
|
|
" [--num-variations N] [--no-launch]] [--timeout hh:mm:ss]"
|
|
|
|
" [--action-config xyz.actions]"
|
|
|
|
" [--output-dir /path/to/output]"
|
|
|
|
" [--verify-dir /path/to/verify]"
|
|
|
|
" or --config app.config";
|
|
|
|
}
|
|
|
|
|
|
|
|
/* register Ctrl+C handlers */
|
|
|
|
#if defined(__LINUX__) || defined(__CYGWIN__)
|
|
|
|
static void
|
|
|
|
CtrlCHandlerCallback(int signum)
|
|
|
|
{
|
|
|
|
SDL_Event event;
|
|
|
|
SDLTest_Log("Ctrl+C received");
|
|
|
|
event.type = SDL_QUIT;
|
|
|
|
SDL_PushEvent(&event);
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
static Uint32
|
|
|
|
ActionTimerCallback(Uint32 interval, void* param)
|
|
|
|
{
|
|
|
|
SDL_Event event;
|
|
|
|
SDL_UserEvent userevent;
|
|
|
|
Uint32 next_action_time;
|
|
|
|
|
|
|
|
/* push an event to handle the action */
|
2021-12-21 11:24:20 +00:00
|
|
|
SDL_zero(userevent);
|
2015-06-21 15:33:46 +00:00
|
|
|
userevent.type = SDL_USEREVENT;
|
|
|
|
userevent.code = ACTION_TIMER_EVENT;
|
|
|
|
userevent.data1 = ¤t->action;
|
|
|
|
|
|
|
|
event.type = SDL_USEREVENT;
|
|
|
|
event.user = userevent;
|
|
|
|
SDL_PushEvent(&event);
|
|
|
|
|
|
|
|
/* calculate the new interval and return it */
|
|
|
|
if(current->next)
|
|
|
|
next_action_time = current->next->action.time - current->action.time;
|
|
|
|
else
|
|
|
|
{
|
|
|
|
next_action_time = 0;
|
|
|
|
action_timer = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
current = current->next;
|
|
|
|
return next_action_time;
|
|
|
|
}
|
|
|
|
|
|
|
|
static Uint32
|
|
|
|
KillTimerCallback(Uint32 interval, void* param)
|
|
|
|
{
|
|
|
|
SDL_Event event;
|
|
|
|
SDL_UserEvent userevent;
|
|
|
|
|
2021-12-21 11:24:20 +00:00
|
|
|
SDL_zero(userevent);
|
2015-06-21 15:33:46 +00:00
|
|
|
userevent.type = SDL_USEREVENT;
|
|
|
|
userevent.code = KILL_TIMER_EVENT;
|
|
|
|
|
|
|
|
event.type = SDL_USEREVENT;
|
|
|
|
event.user = userevent;
|
|
|
|
SDL_PushEvent(&event);
|
|
|
|
|
|
|
|
kill_timer = 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
ProcessAction(SDLVisualTest_Action* action, int* sut_running, char* args)
|
|
|
|
{
|
|
|
|
if(!action || !sut_running)
|
|
|
|
return TEST_ERROR;
|
|
|
|
|
|
|
|
switch(action->type)
|
|
|
|
{
|
|
|
|
case SDL_ACTION_KILL:
|
|
|
|
SDLTest_Log("Action: Kill SUT");
|
|
|
|
if(SDL_IsProcessRunning(&pinfo) == 1 &&
|
|
|
|
!SDL_KillProcess(&pinfo, &sut_exitstatus))
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDL_KillProcess() failed");
|
|
|
|
return TEST_ERROR;
|
|
|
|
}
|
|
|
|
*sut_running = 0;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_ACTION_QUIT:
|
|
|
|
SDLTest_Log("Action: Quit SUT");
|
|
|
|
if(SDL_IsProcessRunning(&pinfo) == 1 &&
|
|
|
|
!SDL_QuitProcess(&pinfo, &sut_exitstatus))
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDL_QuitProcess() failed");
|
|
|
|
return TEST_FAILED;
|
|
|
|
}
|
|
|
|
*sut_running = 0;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_ACTION_LAUNCH:
|
|
|
|
{
|
|
|
|
char* path;
|
|
|
|
char* args;
|
|
|
|
SDL_ProcessInfo action_process;
|
|
|
|
SDL_ProcessExitStatus ps;
|
|
|
|
|
|
|
|
path = action->extra.process.path;
|
|
|
|
args = action->extra.process.args;
|
|
|
|
if(args)
|
|
|
|
{
|
|
|
|
SDLTest_Log("Action: Launch process: %s with arguments: %s",
|
|
|
|
path, args);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
SDLTest_Log("Action: Launch process: %s", path);
|
|
|
|
if(!SDL_LaunchProcess(path, args, &action_process))
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDL_LaunchProcess() failed");
|
|
|
|
return TEST_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* small delay so that the process can do its job */
|
|
|
|
SDL_Delay(1000);
|
|
|
|
|
|
|
|
if(SDL_IsProcessRunning(&action_process) > 0)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("Process %s took too long too complete."
|
2021-12-21 11:24:20 +00:00
|
|
|
" Force killing...", action->extra.process.path);
|
2015-06-21 15:33:46 +00:00
|
|
|
if(!SDL_KillProcess(&action_process, &ps))
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDL_KillProcess() failed");
|
|
|
|
return TEST_ERROR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_ACTION_SCREENSHOT:
|
|
|
|
{
|
|
|
|
char path[MAX_PATH_LEN], hash[33];
|
|
|
|
|
|
|
|
SDLTest_Log("Action: Take screenshot");
|
|
|
|
/* can't take a screenshot if the SUT isn't running */
|
|
|
|
if(SDL_IsProcessRunning(&pinfo) != 1)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SUT has quit.");
|
|
|
|
*sut_running = 0;
|
|
|
|
return TEST_FAILED;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* file name for the screenshot image */
|
|
|
|
SDLVisualTest_HashString(args, hash);
|
|
|
|
SDL_snprintf(path, MAX_PATH_LEN, "%s/%s", state.output_dir, hash);
|
|
|
|
if(!SDLVisualTest_ScreenshotProcess(&pinfo, path))
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDLVisualTest_ScreenshotProcess() failed");
|
|
|
|
return TEST_ERROR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SDL_ACTION_VERIFY:
|
|
|
|
{
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
SDLTest_Log("Action: Verify screenshot");
|
|
|
|
ret = SDLVisualTest_VerifyScreenshots(args, state.output_dir,
|
|
|
|
state.verify_dir);
|
|
|
|
|
|
|
|
if(ret == -1)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDLVisualTest_VerifyScreenshots() failed");
|
|
|
|
return TEST_ERROR;
|
|
|
|
}
|
|
|
|
else if(ret == 0)
|
|
|
|
{
|
|
|
|
SDLTest_Log("Verification failed: Images were not equal.");
|
|
|
|
return TEST_FAILED;
|
|
|
|
}
|
|
|
|
else if(ret == 1)
|
|
|
|
SDLTest_Log("Verification successful.");
|
|
|
|
else
|
|
|
|
{
|
|
|
|
SDLTest_Log("Verfication skipped.");
|
|
|
|
return TEST_FAILED;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
SDLTest_LogError("Invalid action type");
|
|
|
|
return TEST_ERROR;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return TEST_PASSED;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
RunSUTAndTest(char* sutargs, int variation_num)
|
|
|
|
{
|
|
|
|
int success, sut_running, return_code;
|
|
|
|
char hash[33];
|
|
|
|
SDL_Event event;
|
|
|
|
|
|
|
|
return_code = TEST_PASSED;
|
|
|
|
|
|
|
|
if(!sutargs)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("sutargs argument cannot be NULL");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
goto runsutandtest_cleanup_generic;
|
|
|
|
}
|
|
|
|
|
|
|
|
SDLVisualTest_HashString(sutargs, hash);
|
|
|
|
SDLTest_Log("Hash: %s", hash);
|
|
|
|
|
|
|
|
success = SDL_LaunchProcess(state.sutapp, sutargs, &pinfo);
|
|
|
|
if(!success)
|
|
|
|
{
|
|
|
|
SDLTest_Log("Could not launch SUT.");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
goto runsutandtest_cleanup_generic;
|
|
|
|
}
|
|
|
|
SDLTest_Log("SUT launch successful.");
|
|
|
|
SDLTest_Log("Process will be killed in %d milliseconds", state.timeout);
|
|
|
|
sut_running = 1;
|
|
|
|
|
|
|
|
/* launch the timers */
|
|
|
|
SDLTest_Log("Performing actions..");
|
|
|
|
current = state.action_queue.front;
|
|
|
|
action_timer = 0;
|
|
|
|
kill_timer = 0;
|
|
|
|
if(current)
|
|
|
|
{
|
|
|
|
action_timer = SDL_AddTimer(current->action.time, ActionTimerCallback, NULL);
|
|
|
|
if(!action_timer)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDL_AddTimer() failed");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
kill_timer = SDL_AddTimer(state.timeout, KillTimerCallback, NULL);
|
|
|
|
if(!kill_timer)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDL_AddTimer() failed");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* the timer stops running if the actions queue is empty, and the
|
|
|
|
SUT stops running if it crashes or if we encounter a KILL/QUIT action */
|
|
|
|
while(sut_running)
|
|
|
|
{
|
|
|
|
/* process the actions by using an event queue */
|
|
|
|
while(SDL_PollEvent(&event))
|
|
|
|
{
|
|
|
|
if(event.type == SDL_USEREVENT)
|
|
|
|
{
|
|
|
|
if(event.user.code == ACTION_TIMER_EVENT)
|
|
|
|
{
|
|
|
|
SDLVisualTest_Action* action;
|
|
|
|
|
|
|
|
action = (SDLVisualTest_Action*)event.user.data1;
|
|
|
|
|
|
|
|
switch(ProcessAction(action, &sut_running, sutargs))
|
|
|
|
{
|
|
|
|
case TEST_PASSED:
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TEST_FAILED:
|
|
|
|
return_code = TEST_FAILED;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
SDLTest_LogError("ProcessAction() failed");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if(event.user.code == KILL_TIMER_EVENT)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("Maximum timeout reached. Force killing..");
|
|
|
|
return_code = TEST_FAILED;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if(event.type == SDL_QUIT)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("Received QUIT event. Testharness is quitting..");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
SDL_Delay(1000/ACTION_LOOP_FPS);
|
|
|
|
}
|
|
|
|
|
|
|
|
SDLTest_Log("SUT exit code was: %d", sut_exitstatus.exit_status);
|
|
|
|
if(sut_exitstatus.exit_status == 0)
|
|
|
|
{
|
|
|
|
return_code = TEST_PASSED;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return_code = TEST_FAILED;
|
|
|
|
goto runsutandtest_cleanup_timer;
|
|
|
|
}
|
|
|
|
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
goto runsutandtest_cleanup_generic;
|
|
|
|
|
|
|
|
runsutandtest_cleanup_timer:
|
|
|
|
if(action_timer && !SDL_RemoveTimer(action_timer))
|
|
|
|
{
|
|
|
|
SDLTest_Log("SDL_RemoveTimer() failed");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(kill_timer && !SDL_RemoveTimer(kill_timer))
|
|
|
|
{
|
|
|
|
SDLTest_Log("SDL_RemoveTimer() failed");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
}
|
|
|
|
/* runsutandtest_cleanup_process: */
|
|
|
|
if(SDL_IsProcessRunning(&pinfo) && !SDL_KillProcess(&pinfo, &sut_exitstatus))
|
|
|
|
{
|
|
|
|
SDLTest_Log("SDL_KillProcess() failed");
|
|
|
|
return_code = TEST_ERROR;
|
|
|
|
}
|
|
|
|
runsutandtest_cleanup_generic:
|
|
|
|
return return_code;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Entry point for testharness */
|
|
|
|
int
|
|
|
|
main(int argc, char* argv[])
|
|
|
|
{
|
|
|
|
int i, passed, return_code, failed;
|
|
|
|
|
|
|
|
/* freeing resources, linux style! */
|
|
|
|
return_code = 0;
|
|
|
|
|
|
|
|
if(argc < 2)
|
|
|
|
{
|
|
|
|
SDLTest_Log(usage(), argv[0]);
|
|
|
|
goto cleanup_generic;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if defined(__LINUX__) || defined(__CYGWIN__)
|
|
|
|
signal(SIGINT, CtrlCHandlerCallback);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* parse arguments */
|
|
|
|
if(!SDLVisualTest_ParseHarnessArgs(argv + 1, &state))
|
|
|
|
{
|
|
|
|
SDLTest_Log(usage(), argv[0]);
|
|
|
|
return_code = 1;
|
|
|
|
goto cleanup_generic;
|
|
|
|
}
|
|
|
|
SDLTest_Log("Parsed harness arguments successfully.");
|
|
|
|
|
|
|
|
/* initialize SDL */
|
|
|
|
if(SDL_Init(SDL_INIT_TIMER) == -1)
|
|
|
|
{
|
|
|
|
SDLTest_LogError("SDL_Init() failed.");
|
|
|
|
SDLVisualTest_FreeHarnessState(&state);
|
|
|
|
return_code = 1;
|
|
|
|
goto cleanup_harness_state;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* create an output directory if none exists */
|
|
|
|
#if defined(__LINUX__) || defined(__CYGWIN__)
|
|
|
|
mkdir(state.output_dir, 0777);
|
|
|
|
#elif defined(__WIN32__)
|
|
|
|
_mkdir(state.output_dir);
|
|
|
|
#else
|
|
|
|
#error "Unsupported platform"
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* test with sutargs */
|
|
|
|
if(SDL_strlen(state.sutargs))
|
|
|
|
{
|
|
|
|
SDLTest_Log("Running: %s %s", state.sutapp, state.sutargs);
|
|
|
|
if(!state.no_launch)
|
|
|
|
{
|
|
|
|
switch(RunSUTAndTest(state.sutargs, 0))
|
|
|
|
{
|
|
|
|
case TEST_PASSED:
|
|
|
|
SDLTest_Log("Status: PASSED");
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TEST_FAILED:
|
|
|
|
SDLTest_Log("Status: FAILED");
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TEST_ERROR:
|
|
|
|
SDLTest_LogError("Some error occurred while testing.");
|
|
|
|
return_code = 1;
|
|
|
|
goto cleanup_sdl;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(state.sut_config.num_options > 0)
|
|
|
|
{
|
|
|
|
char* variator_name = state.variator_type == SDL_VARIATOR_RANDOM ?
|
|
|
|
"RANDOM" : "EXHAUSTIVE";
|
|
|
|
if(state.num_variations > 0)
|
|
|
|
SDLTest_Log("Testing SUT with variator: %s for %d variations",
|
|
|
|
variator_name, state.num_variations);
|
|
|
|
else
|
|
|
|
SDLTest_Log("Testing SUT with variator: %s and ALL variations",
|
|
|
|
variator_name);
|
|
|
|
/* initialize the variator */
|
|
|
|
if(!SDLVisualTest_InitVariator(&variator, &state.sut_config,
|
|
|
|
state.variator_type, 0))
|
|
|
|
{
|
|
|
|
SDLTest_LogError("Could not initialize variator");
|
|
|
|
return_code = 1;
|
|
|
|
goto cleanup_sdl;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* iterate through all the variations */
|
|
|
|
passed = 0;
|
|
|
|
failed = 0;
|
|
|
|
for(i = 0; state.num_variations > 0 ? (i < state.num_variations) : 1; i++)
|
|
|
|
{
|
|
|
|
char* args = SDLVisualTest_GetNextVariation(&variator);
|
|
|
|
if(!args)
|
|
|
|
break;
|
|
|
|
SDLTest_Log("\nVariation number: %d\nArguments: %s", i + 1, args);
|
|
|
|
|
|
|
|
if(!state.no_launch)
|
|
|
|
{
|
|
|
|
switch(RunSUTAndTest(args, i + 1))
|
|
|
|
{
|
|
|
|
case TEST_PASSED:
|
|
|
|
SDLTest_Log("Status: PASSED");
|
|
|
|
passed++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TEST_FAILED:
|
|
|
|
SDLTest_Log("Status: FAILED");
|
|
|
|
failed++;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TEST_ERROR:
|
|
|
|
SDLTest_LogError("Some error occurred while testing.");
|
|
|
|
goto cleanup_variator;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!state.no_launch)
|
|
|
|
{
|
|
|
|
/* report stats */
|
|
|
|
SDLTest_Log("Testing complete.");
|
|
|
|
SDLTest_Log("%d/%d tests passed.", passed, passed + failed);
|
|
|
|
}
|
|
|
|
goto cleanup_variator;
|
|
|
|
}
|
2021-12-21 11:24:20 +00:00
|
|
|
|
2015-06-21 15:33:46 +00:00
|
|
|
goto cleanup_sdl;
|
|
|
|
|
|
|
|
cleanup_variator:
|
|
|
|
SDLVisualTest_FreeVariator(&variator);
|
|
|
|
cleanup_sdl:
|
|
|
|
SDL_Quit();
|
|
|
|
cleanup_harness_state:
|
|
|
|
SDLVisualTest_FreeHarnessState(&state);
|
|
|
|
cleanup_generic:
|
|
|
|
return return_code;
|
|
|
|
}
|