diff --git a/tests/pressure-vessel/adverb-sdl.c b/tests/pressure-vessel/adverb-sdl.c new file mode 100644 index 0000000000000000000000000000000000000000..d6fde6ef8382bbc0591199b871ee950757854901 --- /dev/null +++ b/tests/pressure-vessel/adverb-sdl.c @@ -0,0 +1,451 @@ +/* + * Copyright © 2023-2024 Collabora Ltd. + * SPDX-License-Identifier: MIT + */ + +#include <glib.h> + +#include "steam-runtime-tools/glib-backports-internal.h" +#include "steam-runtime-tools/utils-internal.h" +#include "libglnx.h" + +#include "adverb-sdl.h" +#include "tests/test-utils.h" + +#define SDL_DYNAMIC_API "SDL_DYNAMIC_API" +#define SDL2_SONAME "libSDL2-2.0.so.0" +#define SDL3_DYNAMIC_API "SDL3_DYNAMIC_API" +#define SDL3_SONAME "libSDL3.so.0" + +typedef struct +{ + TestsOpenFdSet old_fds; + FlatpakBwrap *bwrap; + PvPerArchDirs *lib_temp_dirs; + GError *lib_temp_dirs_error; + GLnxTmpDir mock_prefix; + GLnxTmpDir mock_overrides; +} Fixture; + +typedef struct +{ + int unused; +} Config; + +static void +setup (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + g_autoptr(GError) local_error = NULL; + gsize i; + + f->old_fds = tests_check_fd_leaks_enter (); + f->bwrap = flatpak_bwrap_new (flatpak_bwrap_empty_env); + f->lib_temp_dirs = pv_per_arch_dirs_new (&f->lib_temp_dirs_error); + + if (f->lib_temp_dirs != NULL) + { + g_test_message ("Cross-platform module prefix: %s", + f->lib_temp_dirs->libdl_token_path); + + for (i = 0; i < PV_N_SUPPORTED_ARCHITECTURES; i++) + g_test_message ("Concrete path for %s architecture: %s", + pv_multiarch_tuples[i], + f->lib_temp_dirs->abi_paths[i]); + } + + glnx_mkdtemp ("usr-XXXXXX", 0700, &f->mock_prefix, &local_error); + g_assert_no_error (local_error); + + glnx_mkdtemp ("overrides-XXXXXX", 0700, &f->mock_overrides, &local_error); + g_assert_no_error (local_error); +} + +static void +teardown (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + + g_clear_pointer (&f->lib_temp_dirs, pv_per_arch_dirs_free); + g_clear_error (&f->lib_temp_dirs_error); + g_clear_pointer (&f->bwrap, flatpak_bwrap_free); + glnx_tmpdir_cleanup (&f->mock_prefix); + glnx_tmpdir_cleanup (&f->mock_overrides); + tests_check_fd_leaks_leave (f->old_fds); +} + +static void touch (gchar **, const char *, ...) G_GNUC_NULL_TERMINATED; +static void +touch (gchar **path_out, + const char *path, + ...) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *joined = NULL; + g_autofree gchar *parent = NULL; + va_list ap; + + va_start (ap, path); + joined = g_build_filename_valist (path, &ap); + va_end (ap); + parent = g_path_get_dirname (joined); + + glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, NULL, &local_error); + g_assert_no_error (local_error); + glnx_file_replace_contents_at (AT_FDCWD, joined, (const guint8 *) "", 0, + (GLNX_FILE_REPLACE_NODATASYNC + | GLNX_FILE_REPLACE_INCREASING_MTIME), + NULL, &local_error); + g_assert_no_error (local_error); + + if (path_out != NULL) + *path_out = g_steal_pointer (&joined); +} + +static void +assert_symlink (const char *arch_dir, + const char *soname, + const char *expect_target, + GIOErrorEnum expect_error) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *joined = NULL; + g_autofree gchar *target = NULL; + + joined = g_build_filename (arch_dir, soname, NULL); + target = glnx_readlinkat_malloc (AT_FDCWD, joined, NULL, &local_error); + + if (target != NULL) + g_test_message ("%s -> %s", joined, target); + else + g_test_message ("%s doesn't exist", joined); + + if (expect_target != NULL) + { + g_assert_no_error (local_error); + g_assert_cmpstr (target, ==, expect_target); + } + else + { + g_assert_error (local_error, G_IO_ERROR, expect_error); + g_assert_null (target); + } +} + +static void +assert_env (gchar **envp, + const char *var, + const char *expected_dir, + const char *expected_file) +{ + g_autofree gchar *expected = NULL; + const char *actual; + + if (expected_dir != NULL) + expected = g_build_filename (expected_dir, expected_file, NULL); + + actual = g_environ_getenv (envp, var); + + if (actual != NULL) + g_test_message ("%s=%s", var, actual); + else + g_test_message ("$%s is unset", var); + + g_assert_cmpstr (actual, ==, expected); +} + +static void +test_basic (Fixture *f, + gconstpointer context) +{ + g_autofree gchar *sdl2_target = NULL; + g_autofree gchar *sdl3_target = NULL; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + g_test_summary ("Basic setup of " SDL_DYNAMIC_API); + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + touch (&sdl3_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, NULL); + + g_test_message ("With no flags, not setup is done..."); + pv_adverb_set_up_dynamic_sdls (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SRT_STEAM_COMPAT_FLAGS_NONE); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); + assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, NULL, NULL); + + g_test_message ("SDL2 flag sets up SDL2..."); + pv_adverb_set_up_dynamic_sdls (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, sdl2_target, 0); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); + assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, NULL, NULL); + + g_test_message ("SDL3 flag additionally sets up SDL3..."); + pv_adverb_set_up_dynamic_sdls (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, sdl2_target, 0); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL3_SONAME, sdl3_target, 0); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); + assert_env (f->bwrap->envp, SDL3_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL3_SONAME); +} + +static void +test_cannot_symlink (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *sdl2_target = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + touch (NULL, + f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("If we can't create the symlink, setup fails"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_nonnull (local_error); + /* pv-adverb would log the GError as a warning */ + g_test_message ("%s", local_error->message); + g_assert_false (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_INVALID_ARGUMENT); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); +} + +static void +test_impossible (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *sdl2_target = NULL; + gboolean res; + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("If we don't know the $LIB or $PLATFORM, nothing happens, " + "with a warning"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + NULL, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_nonnull (local_error); + /* pv-adverb would log the GError as a warning */ + g_test_message ("%s", local_error->message); + g_assert_false (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); +} + +static void +test_in_gfx_stack (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *sdl2_target = NULL; + g_autofree gchar *gfx_sdl2_target = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + touch (&sdl2_target, + f->mock_prefix.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + touch (&gfx_sdl2_target, + f->mock_overrides.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("We prefer SDL from the graphics provider if present"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_no_error (local_error); + g_assert_true (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, gfx_sdl2_target, 0); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); +} + +static void +test_in_gfx_stack_only (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + g_autofree gchar *gfx_sdl2_target = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + touch (&gfx_sdl2_target, + f->mock_overrides.path, "lib", pv_multiarch_tuples[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL); + + g_test_summary ("We use SDL from the graphics provider if necessary"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_no_error (local_error); + g_assert_true (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, gfx_sdl2_target, 0); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, + f->lib_temp_dirs->libdl_token_path, SDL2_SONAME); +} + +static void +test_missing (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + g_test_summary ("If SDL is missing, nothing happens, with a warning"); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_nonnull (local_error); + /* pv-adverb would log the GError as a warning */ + g_test_message ("%s", local_error->message); + g_assert_false (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, NULL, NULL); +} + +static void +test_overridden (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) local_error = NULL; + gboolean res; + + if (f->lib_temp_dirs == NULL) + { + g_test_skip (f->lib_temp_dirs_error->message); + return; + } + + g_test_summary ("Setting " SDL_DYNAMIC_API " takes precedence"); + flatpak_bwrap_set_env (f->bwrap, SDL_DYNAMIC_API, "/whatever", TRUE); + res = pv_adverb_set_up_dynamic_sdl (f->bwrap, + f->lib_temp_dirs, + f->mock_prefix.path, + f->mock_overrides.path, + SDL_DYNAMIC_API, + SDL2_SONAME, + &local_error); + g_assert_no_error (local_error); + g_assert_true (res); + assert_symlink (f->lib_temp_dirs->abi_paths[PV_PRIMARY_ARCHITECTURE], + SDL2_SONAME, NULL, G_IO_ERROR_NOT_FOUND); + assert_env (f->bwrap->envp, SDL_DYNAMIC_API, "/whatever", NULL); +} + +int +main (int argc, + char **argv) +{ + _srt_setenv_disable_gio_modules (); + + /* In unit tests it isn't always straightforward to find the real + * ${PLATFORM}, so use a predictable mock implementation that always + * uses PvMultiarchDetails.platforms[0] */ + g_setenv ("PRESSURE_VESSEL_TEST_STANDARDIZE_PLATFORM", "1", TRUE); + + _srt_tests_init (&argc, &argv, NULL); + g_test_add ("/pv-adverb/sdl/basic", Fixture, NULL, + setup, test_basic, teardown); + g_test_add ("/pv-adverb/sdl/cannot-symlink", Fixture, NULL, + setup, test_cannot_symlink, teardown); + g_test_add ("/pv-adverb/sdl/impossible", Fixture, NULL, + setup, test_impossible, teardown); + g_test_add ("/pv-adverb/sdl/in_gfx_stack", Fixture, NULL, + setup, test_in_gfx_stack, teardown); + g_test_add ("/pv-adverb/sdl/in_gfx_stack_only", Fixture, NULL, + setup, test_in_gfx_stack_only, teardown); + g_test_add ("/pv-adverb/sdl/missing", Fixture, NULL, + setup, test_missing, teardown); + g_test_add ("/pv-adverb/sdl/overridden", Fixture, NULL, + setup, test_overridden, teardown); + + return g_test_run (); +} diff --git a/tests/pressure-vessel/meson.build b/tests/pressure-vessel/meson.build index c36e79474516fb78647a7b46bc9c3003a269010a..c7cd13675e7ac4f7d78b88c9a6b6e706b6bc2eca 100644 --- a/tests/pressure-vessel/meson.build +++ b/tests/pressure-vessel/meson.build @@ -68,6 +68,7 @@ tests = [ compiled_tests = [ {'name': 'adverb-preload', 'adverb': true}, + {'name': 'adverb-sdl', 'adverb': true}, {'name': 'bwrap', 'wrap': true}, {'name': 'graphics-provider', 'wrap': true}, {'name': 'wrap-setup', 'wrap': true}, diff --git a/tests/utils.c b/tests/utils.c index 8c587ed2c9bfecbc4aaa8d367deeb2bb987b8238..a70beed3320615a71701e62b465e9d77c01ed5f4 100644 --- a/tests/utils.c +++ b/tests/utils.c @@ -152,6 +152,14 @@ test_compat_flags (Fixture *f, { "STEAM_COMPAT_FLAGS=,,,,search-cwd-first,,,,", NULL }, SRT_STEAM_COMPAT_FLAGS_SEARCH_CWD_FIRST, }, + { + { "STEAM_COMPAT_FLAGS=runtime-sdl2", NULL }, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2, + }, + { + { "STEAM_COMPAT_FLAGS=runtime-sdl3", NULL }, + SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3, + }, { { "STEAM_COMPAT_TRACING=1", @@ -165,6 +173,8 @@ test_compat_flags (Fixture *f, { { "STEAM_COMPAT_TRACING=1", NULL }, SRT_STEAM_COMPAT_FLAGS_SYSTEM_TRACING }, { { "STEAM_COMPAT_TRACING=", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE }, { { "STEAM_COMPAT_TRACING=0", NULL }, SRT_STEAM_COMPAT_FLAGS_NONE }, + { { "STEAM_COMPAT_RUNTIME_SDL2=1", NULL }, SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL2 }, + { { "STEAM_COMPAT_RUNTIME_SDL3=1", NULL }, SRT_STEAM_COMPAT_FLAGS_RUNTIME_SDL3 }, { { NULL }, SRT_STEAM_COMPAT_FLAGS_NONE } }; size_t i;