Files
SDL/test/testprocess.c
Sam Lantinga a90ad3b0e2 Removed SDL_bool in favor of plain bool
We require stdbool.h in the build environment, so we might as well use the plain bool type.

If your environment doesn't have stdbool.h, this simple replacement will suffice:
typedef signed char bool;
2024-09-18 08:32:30 -07:00

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;
}