SDL/test/testprocess.c

601 lines
20 KiB
C

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_test.h>
#ifdef SDL_PLATFORM_WINDOWS
#define EXE ".exe"
#else
#define EXE ""
#endif
/*
* FIXME: Additional tests:
* - stdin to stdout
* - stdin to stderr
* - read env, using env set by parent process
* - exit codes
* - kill process
* - waiting twice on process
* - executing a non-existing program
* - executing a process linking to a shared library not in the search paths
* - piping processes
* - forwarding SDL_IOFromFile stream to process
* - forwarding process to SDL_IOFromFile stream
*/
typedef struct {
const char *childprocess_path;
} TestProcessData;
static TestProcessData parsed_args;
static void SDLCALL setUpProcess(void **arg) {
*arg = &parsed_args;
}
static const char *options[] = { "/path/to/childprocess" EXE, NULL };
static SDL_Environment *DuplicateEnvironment(const char *key0, ...)
{
va_list ap;
const char *keyN;
SDL_Environment *env = SDL_GetEnvironment();
SDL_Environment *new_env = SDL_CreateEnvironment(false);
if (key0) {
char *sep = SDL_strchr(key0, '=');
if (sep) {
*sep = '\0';
SDL_SetEnvironmentVariable(new_env, key0, sep + 1, true);
*sep = '=';
SDL_SetEnvironmentVariable(new_env, key0, sep, true);
} else {
SDL_SetEnvironmentVariable(new_env, key0, SDL_GetEnvironmentVariable(env, key0), true);
}
va_start(ap, key0);
for (;;) {
keyN = va_arg(ap, const char *);
if (keyN) {
sep = SDL_strchr(keyN, '=');
if (sep) {
*sep = '\0';
SDL_SetEnvironmentVariable(new_env, keyN, sep + 1, true);
*sep = '=';
} else {
SDL_SetEnvironmentVariable(new_env, keyN, SDL_GetEnvironmentVariable(env, keyN), true);
}
} else {
break;
}
}
va_end(ap);
}
return new_env;
}
static int SDLCALL process_testArguments(void *arg)
{
TestProcessData *data = (TestProcessData *)arg;
const char *process_args[] = {
data->childprocess_path,
"--print-arguments",
"--",
"",
" ",
"a b c",
"a\tb\tc\t",
"\"a b\" c",
"'a' 'b' 'c'",
"%d%%%s",
"\\t\\c",
"evil\\",
"a\\b\"c\\",
"\"\\^&|<>%", /* characters with a special meaning */
NULL
};
SDL_Process *process = NULL;
char *buffer;
int exit_code;
int i;
process = SDL_CreateProcess(process_args, true);
SDLTest_AssertCheck(process != NULL, "SDL_CreateProcess()");
if (!process) {
goto failed;
}
exit_code = 0xdeadbeef;
buffer = (char *)SDL_ReadProcess(process, NULL, &exit_code);
SDLTest_AssertCheck(buffer != NULL, "SDL_ReadProcess()");
SDLTest_AssertCheck(exit_code == 0, "Exit code should be 0, is %d", exit_code);
if (!buffer) {
goto failed;
}
for (i = 3; process_args[i]; i++) {
char line[64];
SDL_snprintf(line, sizeof(line), "|%d=%s|", i - 3, process_args[i]);
SDLTest_AssertCheck(!!SDL_strstr(buffer, line), "Check %s is in output", line);
}
SDL_free(buffer);
SDLTest_AssertPass("About to destroy process");
SDL_DestroyProcess(process);
return TEST_COMPLETED;
failed:
SDL_DestroyProcess(process);
return TEST_ABORTED;
}
static int SDLCALL process_testInheritedEnv(void *arg)
{
TestProcessData *data = (TestProcessData *)arg;
const char *process_args[] = {
data->childprocess_path,
"--print-environment",
"--expect-env", NULL,
NULL,
};
SDL_PropertiesID props;
SDL_Process *process = NULL;
Sint64 pid;
SDL_IOStream *process_stdout = NULL;
char buffer[256];
bool wait_result;
int exit_code;
static const char *const TEST_ENV_KEY = "testprocess_environment";
char *test_env_val = NULL;
test_env_val = SDLTest_RandomAsciiStringOfSize(32);
SDLTest_AssertPass("Setting parent environment variable %s=%s", TEST_ENV_KEY, test_env_val);
SDL_SetEnvironmentVariable(SDL_GetEnvironment(), TEST_ENV_KEY, test_env_val, true);
SDL_snprintf(buffer, sizeof(buffer), "%s=%s", TEST_ENV_KEY, test_env_val);
process_args[3] = buffer;
props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, (void *)process_args);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
process = SDL_CreateProcessWithProperties(props);
SDL_DestroyProperties(props);
SDLTest_AssertCheck(process != NULL, "SDL_CreateProcessWithProperties()");
if (!process) {
goto failed;
}
props = SDL_GetProcessProperties(process);
SDLTest_AssertCheck(props != 0, "SDL_GetProcessProperties()");
pid = SDL_GetNumberProperty(props, SDL_PROP_PROCESS_PID_NUMBER, 0);
SDLTest_AssertCheck(pid != 0, "Checking process ID, expected non-zero, got %" SDL_PRIs64, pid);
process_stdout = SDL_GetProcessOutput(process);
SDLTest_AssertCheck(process_stdout != NULL, "SDL_GetPointerProperty(SDL_PROP_PROCESS_STDOUT_POINTER) returns a valid IO stream");
if (!process_stdout) {
goto failed;
}
for (;;) {
size_t amount_read;
amount_read = SDL_ReadIO(process_stdout, buffer, sizeof(buffer) - 1);
if (amount_read > 0) {
buffer[amount_read] = '\0';
SDLTest_Log("READ: %s", buffer);
} else if (SDL_GetIOStatus(process_stdout) != SDL_IO_STATUS_NOT_READY) {
break;
}
SDL_Delay(10);
}
SDLTest_AssertPass("About to wait on process");
exit_code = 0xdeadbeef;
wait_result = SDL_WaitProcess(process, true, &exit_code);
SDLTest_AssertCheck(wait_result == true, "Process should have closed when closing stdin");
SDLTest_AssertPass("exit_code will be != 0 when environment variable was not set");
SDLTest_AssertCheck(exit_code == 0, "Exit code should be 0, is %d", exit_code);
SDLTest_AssertPass("About to destroy process");
SDL_DestroyProcess(process);
SDL_free(test_env_val);
return TEST_COMPLETED;
failed:
SDL_free(test_env_val);
SDL_DestroyProcess(process);
return TEST_ABORTED;
}
static int SDLCALL process_testNewEnv(void *arg)
{
TestProcessData *data = (TestProcessData *)arg;
const char *process_args[] = {
data->childprocess_path,
"--print-environment",
"--expect-env", NULL,
NULL,
};
SDL_Environment *process_env;
SDL_PropertiesID props;
SDL_Process *process = NULL;
Sint64 pid;
SDL_IOStream *process_stdout = NULL;
char buffer[256];
bool wait_result;
int exit_code;
static const char *const TEST_ENV_KEY = "testprocess_environment";
char *test_env_val = NULL;
test_env_val = SDLTest_RandomAsciiStringOfSize(32);
SDL_snprintf(buffer, sizeof(buffer), "%s=%s", TEST_ENV_KEY, test_env_val);
process_args[3] = buffer;
process_env = DuplicateEnvironment("PATH", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", buffer, NULL);
props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, (void *)process_args);
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, process_env);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
process = SDL_CreateProcessWithProperties(props);
SDL_DestroyProperties(props);
SDLTest_AssertCheck(process != NULL, "SDL_CreateProcessWithProperties()");
if (!process) {
goto failed;
}
props = SDL_GetProcessProperties(process);
SDLTest_AssertCheck(props != 0, "SDL_GetProcessProperties()");
pid = SDL_GetNumberProperty(props, SDL_PROP_PROCESS_PID_NUMBER, 0);
SDLTest_AssertCheck(pid != 0, "Checking process ID, expected non-zero, got %" SDL_PRIs64, pid);
process_stdout = SDL_GetProcessOutput(process);
SDLTest_AssertCheck(process_stdout != NULL, "SDL_GetPointerProperty(SDL_PROP_PROCESS_STDOUT_POINTER) returns a valid IO stream");
if (!process_stdout) {
goto failed;
}
for (;;) {
size_t amount_read;
amount_read = SDL_ReadIO(process_stdout, buffer, sizeof(buffer) - 1);
if (amount_read > 0) {
buffer[amount_read] = '\0';
SDLTest_Log("READ: %s", buffer);
} else if (SDL_GetIOStatus(process_stdout) != SDL_IO_STATUS_NOT_READY) {
break;
}
SDL_Delay(10);
}
SDLTest_AssertPass("About to wait on process");
exit_code = 0xdeadbeef;
wait_result = SDL_WaitProcess(process, true, &exit_code);
SDLTest_AssertCheck(wait_result == true, "Process should have closed when closing stdin");
SDLTest_AssertPass("exit_code will be != 0 when environment variable was not set");
SDLTest_AssertCheck(exit_code == 0, "Exit code should be 0, is %d", exit_code);
SDLTest_AssertPass("About to destroy process");
SDL_free(test_env_val);
SDL_DestroyProcess(process);
SDL_DestroyEnvironment(process_env);
return TEST_COMPLETED;
failed:
SDL_free(test_env_val);
SDL_DestroyProcess(process);
SDL_DestroyEnvironment(process_env);
return TEST_ABORTED;
}
static int process_testStdinToStdout(void *arg)
{
TestProcessData *data = (TestProcessData *)arg;
const char *process_args[] = {
data->childprocess_path,
"--stdin-to-stdout",
NULL,
};
SDL_PropertiesID props;
SDL_Process *process = NULL;
Sint64 pid;
SDL_IOStream *process_stdin = NULL;
SDL_IOStream *process_stdout = NULL;
const char *text_in = "Tests whether we can write to stdin and read from stdout\r\n{'succes': true, 'message': 'Success!'}\r\nYippie ka yee\r\nEOF";
size_t amount_written;
size_t amount_to_write;
char buffer[128];
size_t total_read;
bool wait_result;
int exit_code;
props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, (void *)process_args);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_APP);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
process = SDL_CreateProcessWithProperties(props);
SDL_DestroyProperties(props);
SDLTest_AssertCheck(process != NULL, "SDL_CreateProcessWithProperties()");
if (!process) {
goto failed;
}
props = SDL_GetProcessProperties(process);
SDLTest_AssertCheck(props != 0, "SDL_GetProcessProperties()");
pid = SDL_GetNumberProperty(props, SDL_PROP_PROCESS_PID_NUMBER, 0);
SDLTest_AssertCheck(pid != 0, "Checking process ID, expected non-zero, got %" SDL_PRIs64, pid);
process_stdin = SDL_GetProcessInput(process);
SDLTest_AssertCheck(process_stdin != NULL, "SDL_GetPointerProperty(SDL_PROP_PROCESS_STDIN_POINTER) returns a valid IO stream");
process_stdout = SDL_GetProcessOutput(process);
SDLTest_AssertCheck(process_stdout != NULL, "SDL_GetPointerProperty(SDL_PROP_PROCESS_STDOUT_POINTER) returns a valid IO stream");
if (!process_stdin || !process_stdout) {
goto failed;
}
SDLTest_AssertPass("About to write to process");
amount_to_write = SDL_strlen(text_in);
amount_written = SDL_WriteIO(process_stdin, text_in, amount_to_write);
SDLTest_AssertCheck(amount_written == amount_to_write, "SDL_WriteIO(subprocess.stdin) wrote %" SDL_PRIu64 " bytes, expected %" SDL_PRIu64, (Uint64)amount_written, (Uint64)amount_to_write);
if (amount_to_write != amount_written) {
goto failed;
}
SDL_FlushIO(process_stdin);
total_read = 0;
buffer[0] = '\0';
for (;;) {
size_t amount_read;
if (total_read >= sizeof(buffer) - 1) {
SDLTest_AssertCheck(0, "Buffer is too small for input data.");
goto failed;
}
SDLTest_AssertPass("About to read from process");
amount_read = SDL_ReadIO(process_stdout, buffer + total_read, sizeof(buffer) - total_read - 1);
if (amount_read == 0 && SDL_GetIOStatus(process_stdout) != SDL_IO_STATUS_NOT_READY) {
break;
}
total_read += amount_read;
buffer[total_read] = '\0';
if (total_read >= sizeof(buffer) - 1 || SDL_strstr(buffer, "EOF")) {
break;
}
SDL_Delay(10);
}
SDLTest_Log("Text read from subprocess: %s", buffer);
SDLTest_AssertCheck(SDL_strcmp(buffer, text_in) == 0, "Subprocess stdout should match text written to stdin");
SDLTest_AssertPass("About to close stdin");
/* Closing stdin of `subprocessstdin --stdin-to-stdout` should close the process */
SDL_CloseIO(process_stdin);
process_stdin = SDL_GetProcessInput(process);
SDLTest_AssertCheck(process_stdin == NULL, "SDL_GetPointerProperty(SDL_PROP_PROCESS_STDIN_POINTER) is cleared after close");
SDLTest_AssertPass("About to wait on process");
exit_code = 0xdeadbeef;
wait_result = SDL_WaitProcess(process, true, &exit_code);
SDLTest_AssertCheck(wait_result == true, "Process should have closed when closing stdin");
SDLTest_AssertCheck(exit_code == 0, "Exit code should be 0, is %d", exit_code);
if (!wait_result) {
bool killed;
SDL_Log("About to kill process");
killed = SDL_KillProcess(process, true);
SDLTest_AssertCheck(killed, "SDL_KillProcess succeeded");
}
SDLTest_AssertPass("About to destroy process");
SDL_DestroyProcess(process);
return TEST_COMPLETED;
failed:
SDL_DestroyProcess(process);
return TEST_ABORTED;
}
static int process_testSimpleStdinToStdout(void *arg)
{
TestProcessData *data = (TestProcessData *)arg;
const char *process_args[] = {
data->childprocess_path,
"--stdin-to-stdout",
NULL,
};
SDL_Process *process = NULL;
SDL_IOStream *input = NULL;
const char *text_in = "Tests whether we can write to stdin and read from stdout\r\n{'succes': true, 'message': 'Success!'}\r\nYippie ka yee\r\nEOF";
char *buffer;
size_t result;
int exit_code;
process = SDL_CreateProcess(process_args, true);
SDLTest_AssertCheck(process != NULL, "SDL_CreateProcess()");
if (!process) {
goto failed;
}
SDLTest_AssertPass("About to write to process");
input = SDL_GetProcessInput(process);
SDLTest_AssertCheck(input != NULL, "SDL_GetProcessInput()");
result = SDL_WriteIO(input, text_in, SDL_strlen(text_in));
SDLTest_AssertCheck(result == SDL_strlen(text_in), "SDL_WriteIO() wrote %d, expected %d", (int)result, (int)SDL_strlen(text_in));
SDL_CloseIO(input);
input = SDL_GetProcessInput(process);
SDLTest_AssertCheck(input == NULL, "SDL_GetProcessInput() after close");
exit_code = 0xdeadbeef;
buffer = (char *)SDL_ReadProcess(process, NULL, &exit_code);
SDLTest_AssertCheck(buffer != NULL, "SDL_ReadProcess()");
SDLTest_AssertCheck(exit_code == 0, "Exit code should be 0, is %d", exit_code);
if (!buffer) {
goto failed;
}
SDLTest_Log("Text read from subprocess: %s", buffer);
SDLTest_AssertCheck(SDL_strcmp(buffer, text_in) == 0, "Subprocess stdout should match text written to stdin");
SDL_free(buffer);
SDLTest_AssertPass("About to destroy process");
SDL_DestroyProcess(process);
return TEST_COMPLETED;
failed:
SDL_DestroyProcess(process);
return TEST_ABORTED;
}
static int process_testMultiprocessStdinToStdout(void *arg)
{
TestProcessData *data = (TestProcessData *)arg;
const char *process_args[] = {
data->childprocess_path,
"--stdin-to-stdout",
NULL,
};
SDL_Process *process1 = NULL;
SDL_Process *process2 = NULL;
SDL_PropertiesID props;
SDL_IOStream *input = NULL;
const char *text_in = "Tests whether we can write to stdin and read from stdout\r\n{'succes': true, 'message': 'Success!'}\r\nYippie ka yee\r\nEOF";
char *buffer;
size_t result;
int exit_code;
process1 = SDL_CreateProcess(process_args, true);
SDLTest_AssertCheck(process1 != NULL, "SDL_CreateProcess()");
if (!process1) {
goto failed;
}
props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, (void *)process_args);
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_REDIRECT);
SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_POINTER, SDL_GetPointerProperty(SDL_GetProcessProperties(process1), SDL_PROP_PROCESS_STDOUT_POINTER, NULL));
SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP);
process2 = SDL_CreateProcessWithProperties(props);
SDL_DestroyProperties(props);
SDLTest_AssertCheck(process2 != NULL, "SDL_CreateProcess()");
if (!process2) {
goto failed;
}
SDLTest_AssertPass("About to write to process");
input = SDL_GetProcessInput(process1);
SDLTest_AssertCheck(input != NULL, "SDL_GetProcessInput()");
result = SDL_WriteIO(input, text_in, SDL_strlen(text_in));
SDLTest_AssertCheck(result == SDL_strlen(text_in), "SDL_WriteIO() wrote %d, expected %d", (int)result, (int)SDL_strlen(text_in));
SDL_CloseIO(input);
exit_code = 0xdeadbeef;
buffer = (char *)SDL_ReadProcess(process2, NULL, &exit_code);
SDLTest_AssertCheck(buffer != NULL, "SDL_ReadProcess()");
SDLTest_AssertCheck(exit_code == 0, "Exit code should be 0, is %d", exit_code);
if (!buffer) {
goto failed;
}
SDLTest_Log("Text read from subprocess: %s", buffer);
SDLTest_AssertCheck(SDL_strcmp(buffer, text_in) == 0, "Subprocess stdout should match text written to stdin");
SDL_free(buffer);
SDLTest_AssertPass("About to destroy processes");
SDL_DestroyProcess(process1);
SDL_DestroyProcess(process2);
return TEST_COMPLETED;
failed:
SDL_DestroyProcess(process1);
SDL_DestroyProcess(process2);
return TEST_ABORTED;
}
static const SDLTest_TestCaseReference processTestArguments = {
process_testArguments, "process_testArguments", "Test passing arguments to child process", TEST_ENABLED
};
static const SDLTest_TestCaseReference processTestIneritedEnv = {
process_testInheritedEnv, "process_testInheritedEnv", "Test inheriting environment from parent process", TEST_ENABLED
};
static const SDLTest_TestCaseReference processTestNewEnv = {
process_testNewEnv, "process_testNewEnv", "Test creating new environment for child process", TEST_ENABLED
};
static const SDLTest_TestCaseReference processTestStdinToStdout = {
process_testStdinToStdout, "process_testStdinToStdout", "Test writing to stdin and reading from stdout", TEST_ENABLED
};
static const SDLTest_TestCaseReference processTestSimpleStdinToStdout = {
process_testSimpleStdinToStdout, "process_testSimpleStdinToStdout", "Test writing to stdin and reading from stdout using the simplified API", TEST_ENABLED
};
static const SDLTest_TestCaseReference processTestMultiprocessStdinToStdout = {
process_testMultiprocessStdinToStdout, "process_testMultiprocessStdinToStdout", "Test writing to stdin and reading from stdout using the simplified API", TEST_ENABLED
};
static const SDLTest_TestCaseReference *processTests[] = {
&processTestArguments,
&processTestIneritedEnv,
&processTestNewEnv,
&processTestStdinToStdout,
&processTestSimpleStdinToStdout,
&processTestMultiprocessStdinToStdout,
NULL
};
static SDLTest_TestSuiteReference processTestSuite = {
"Process",
setUpProcess,
processTests,
NULL
};
static SDLTest_TestSuiteReference *testSuites[] = {
&processTestSuite,
NULL
};
int main(int argc, char *argv[])
{
int i;
int result;
SDLTest_CommonState *state;
SDLTest_TestSuiteRunner *runner;
/* Initialize test framework */
state = SDLTest_CommonCreateState(argv, 0);
if (!state) {
return 1;
}
runner = SDLTest_CreateTestSuiteRunner(state, testSuites);
/* Parse commandline */
for (i = 1; i < argc;) {
int consumed;
consumed = SDLTest_CommonArg(state, i);
if (!consumed) {
if (!parsed_args.childprocess_path) {
parsed_args.childprocess_path = argv[i];
consumed = 1;
}
}
if (consumed <= 0) {
SDLTest_CommonLogUsage(state, argv[0], options);
return 1;
}
i += consumed;
}
if (!parsed_args.childprocess_path) {
SDLTest_CommonLogUsage(state, argv[0], options);
return 1;
}
result = SDLTest_ExecuteTestSuiteRunner(runner);
SDL_Quit();
SDLTest_DestroyTestSuiteRunner(runner);
SDLTest_CommonDestroyState(state);
return result;
}