summaryrefslogtreecommitdiffstats
path: root/test/pwtest.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--test/pwtest.c1486
1 files changed, 1486 insertions, 0 deletions
diff --git a/test/pwtest.c b/test/pwtest.c
new file mode 100644
index 0000000..968805b
--- /dev/null
+++ b/test/pwtest.c
@@ -0,0 +1,1486 @@
+/* PipeWire
+ *
+ * Copyright © 2021 Red Hat, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <assert.h>
+#include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <ftw.h>
+#include <getopt.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+#ifdef HAVE_PIDFD_OPEN
+#include <sys/syscall.h>
+#endif
+#ifdef HAVE_LIBCAP
+#include <sys/capability.h>
+#endif
+#include <sys/epoll.h>
+#include <sys/ptrace.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/timerfd.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include <valgrind/valgrind.h>
+
+#include "spa/utils/ansi.h"
+#include "spa/utils/string.h"
+#include "spa/utils/defs.h"
+#include "spa/utils/list.h"
+#include "spa/support/plugin.h"
+
+#include "pipewire/array.h"
+#include "pipewire/utils.h"
+#include "pipewire/properties.h"
+
+#include "pwtest.h"
+
+#include "pwtest-compat.c"
+
+#define pwtest_log(...) dprintf(testlog_fd, __VA_ARGS__)
+#define pwtest_vlog(format_, args_) vdprintf(testlog_fd, format_, args_)
+
+static bool verbose = false;
+
+/** the global context object */
+static struct pwtest_context *ctx;
+
+/**
+ * The various pwtest_assert() etc. functions write to this fd, collected
+ * separately in the log.
+ */
+static int testlog_fd = STDOUT_FILENO;
+
+enum pwtest_logfds {
+ FD_STDOUT,
+ FD_STDERR,
+ FD_LOG,
+ FD_DAEMON,
+ _FD_LAST,
+};
+
+struct pwtest_test {
+ struct spa_list link;
+ const char *name;
+ enum pwtest_result (*func)(struct pwtest_test *test);
+
+ int iteration;
+
+ /* env vars changed by pwtest. These will be restored after the test
+ * run to get close to the original environment. */
+ struct pw_properties *env;
+
+ /* Arguments during pwtest_add() */
+ struct {
+ int signal;
+ struct {
+ int min, max;
+ } range;
+ struct pw_properties *props;
+ struct pw_properties *env;
+ bool pw_daemon;
+ } args;
+
+ /* Results */
+ enum pwtest_result result;
+ int sig_or_errno;
+ struct pw_array logs[_FD_LAST];
+};
+
+struct pwtest_suite {
+ struct spa_list link;
+ const struct pwtest_suite_decl *decl;
+ enum pwtest_result result;
+
+ struct spa_list tests;
+};
+
+struct pwtest_context {
+ struct spa_list suites;
+ unsigned int timeout;
+ bool no_fork;
+ bool terminate;
+ struct spa_list cleanup_pids;
+
+ const char *test_filter;
+ bool has_iteration_filter;
+ int iteration_filter;
+ char *xdg_dir;
+};
+
+struct cleanup_pid {
+ struct spa_list link;
+ pid_t pid;
+};
+
+struct pwtest_context *pwtest_get_context(struct pwtest_test *t)
+{
+ return ctx;
+}
+
+int pwtest_get_iteration(struct pwtest_test *t)
+{
+ return t->iteration;
+}
+
+struct pw_properties *pwtest_get_props(struct pwtest_test *t)
+{
+ return t->args.props;
+}
+
+static void replace_env(struct pwtest_test *t, const char *prop, const char *value)
+{
+ const char *oldval = getenv(prop);
+
+ pw_properties_set(t->env, prop, oldval ? oldval : "pwtest-null");
+ if (value)
+ setenv(prop, value, 1);
+ else
+ unsetenv(prop);
+}
+
+static void restore_env(struct pwtest_test *t)
+{
+ const char *env;
+ void *state = NULL;
+
+ while ((env = pw_properties_iterate(t->env, &state))) {
+ const char *value = pw_properties_get(t->env, env);
+ if (spa_streq(value, "pwtest-null"))
+ unsetenv(env);
+ else
+ setenv(env, value, 1);
+ }
+}
+
+static int add_cleanup_pid(struct pwtest_context *ctx, pid_t pid)
+{
+ struct cleanup_pid *cpid;
+
+ if (pid == 0)
+ return -EINVAL;
+
+ cpid = calloc(1, sizeof(struct cleanup_pid));
+ if (cpid == NULL)
+ return -errno;
+
+ cpid->pid = pid;
+ spa_list_append(&ctx->cleanup_pids, &cpid->link);
+
+ return 0;
+}
+
+static void remove_cleanup_pid(struct pwtest_context *ctx, pid_t pid)
+{
+ struct cleanup_pid *cpid, *t;
+
+ spa_list_for_each_safe(cpid, t, &ctx->cleanup_pids, link) {
+ if (cpid->pid == pid) {
+ spa_list_remove(&cpid->link);
+ free(cpid);
+ }
+ }
+}
+
+static void terminate_cleanup_pids(struct pwtest_context *ctx)
+{
+ struct cleanup_pid *cpid;
+ spa_list_for_each(cpid, &ctx->cleanup_pids, link) {
+ /* Don't free here, to be signal-safe */
+ if (cpid->pid != 0) {
+ kill(cpid->pid, SIGTERM);
+ cpid->pid = 0;
+ }
+ }
+}
+
+static void free_cleanup_pids(struct pwtest_context *ctx)
+{
+ struct cleanup_pid *cpid;
+ spa_list_consume(cpid, &ctx->cleanup_pids, link) {
+ spa_list_remove(&cpid->link);
+ free(cpid);
+ }
+}
+
+static void pwtest_backtrace(pid_t p)
+{
+#ifdef HAVE_GSTACK
+ char pid[11];
+ pid_t parent, child;
+ int status;
+
+ if (RUNNING_ON_VALGRIND)
+ return;
+
+ parent = p == 0 ? getpid() : p;
+ child = fork();
+ if (child == 0) {
+ assert(testlog_fd > 0);
+ /* gstack writes the backtrace to stdout, we re-route to our
+ * custom fd */
+ dup2(testlog_fd, STDOUT_FILENO);
+
+ spa_scnprintf(pid, sizeof(pid), "%d", (uint32_t)parent);
+ execlp("gstack", "gstack", pid, NULL);
+ exit(errno);
+ }
+
+ /* parent */
+ waitpid(child, &status, 0);
+#endif
+}
+
+SPA_PRINTF_FUNC(6, 7)
+SPA_NORETURN
+void _pwtest_fail_condition(int exitstatus,
+ const char *file, int line, const char *func,
+ const char *condition, const char *message, ...)
+{
+ pwtest_log("FAILED: %s\n", condition);
+
+ if (message) {
+ va_list args;
+ va_start(args, message);
+ pwtest_vlog(message, args);
+ va_end(args);
+ pwtest_log("\n");
+ }
+
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(exitstatus);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_bool(const char *file, int line, const char *func,
+ const char *operator, bool a, bool b,
+ const char *astr, const char *bstr)
+{
+ pwtest_log("FAILED COMPARISON: %s %s %s\n", astr, operator, bstr);
+ pwtest_log("Resolved to: %s %s %s\n", a ? "true" : "false", operator, b ? "true" : "false");
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_errno(const char *file, int line, const char *func,
+ int expected, int err_no)
+{
+ pwtest_log("FAILED ERRNO CHECK: expected %d (%s), got %d (%s)\n",
+ expected, strerror(expected), err_no, strerror(err_no));
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+
+SPA_NORETURN
+void _pwtest_fail_comparison_int(const char *file, int line, const char *func,
+ const char *operator, int a, int b,
+ const char *astr, const char *bstr)
+{
+ pwtest_log("FAILED COMPARISON: %s %s %s\n", astr, operator, bstr);
+ pwtest_log("Resolved to: %d %s %d\n", a, operator, b);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_double(const char *file, int line, const char *func,
+ const char *operator, double a, double b,
+ const char *astr, const char *bstr)
+{
+ pwtest_log("FAILED COMPARISON: %s %s %s\n", astr, operator, bstr);
+ pwtest_log("Resolved to: %.3f %s %.3f\n", a, operator, b);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_ptr(const char *file, int line, const char *func,
+ const char *comparison)
+{
+ pwtest_log("FAILED COMPARISON: %s\n", comparison);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+SPA_NORETURN
+void _pwtest_fail_comparison_str(const char *file, int line, const char *func,
+ const char *comparison, const char *a, const char *b)
+{
+ pwtest_log("FAILED COMPARISON: %s, expanded (\"%s\" vs \"%s\")\n", comparison, a, b);
+ pwtest_log("in %s() (%s:%d)\n", func, file, line);
+ pwtest_backtrace(0);
+ exit(PWTEST_FAIL);
+}
+
+struct pwtest_spa_plugin *
+pwtest_spa_plugin_new(void)
+{
+ return calloc(1, sizeof(struct pwtest_spa_plugin));
+}
+
+void
+pwtest_spa_plugin_destroy(struct pwtest_spa_plugin *plugin)
+{
+ SPA_FOR_EACH_ELEMENT_VAR(plugin->handles, hnd) {
+ if (*hnd) {
+ spa_handle_clear(*hnd);
+ free(*hnd);
+ }
+ }
+ SPA_FOR_EACH_ELEMENT_VAR(plugin->dlls, dll) {
+ if (*dll)
+ dlclose(*dll);
+ }
+ free(plugin);
+}
+
+int
+pwtest_spa_plugin_try_load_interface(struct pwtest_spa_plugin *plugin,
+ void **iface_return,
+ const char *libname,
+ const char *factory_name,
+ const char *interface_name,
+ const struct spa_dict *info)
+{
+ char *libdir = getenv("SPA_PLUGIN_DIR");
+ char path[PATH_MAX];
+ void *hnd, *iface;
+ spa_handle_factory_enum_func_t enum_func;
+ const struct spa_handle_factory *factory;
+ uint32_t index = 0;
+ int r;
+ bool found = false;
+ struct spa_handle *handle;
+
+ spa_assert_se(libdir != NULL);
+ spa_scnprintf(path, sizeof(path), "%s/%s.so", libdir, libname);
+
+ hnd = dlopen(path, RTLD_NOW);
+ if (hnd == NULL)
+ return -ENOENT;
+
+ enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME);
+ pwtest_ptr_notnull(enum_func);
+
+ while ((r = enum_func(&factory, &index)) > 0) {
+ pwtest_int_ge(factory->version, 1U);
+ if (spa_streq(factory->name, factory_name)) {
+ found = true;
+ break;
+ }
+ }
+ pwtest_neg_errno_ok(r);
+ if (!found) {
+ dlclose(hnd);
+ return -EINVAL;
+ }
+
+ handle = calloc(1, spa_handle_factory_get_size(factory, info));
+ pwtest_ptr_notnull(handle);
+
+ r = spa_handle_factory_init(factory, handle, info, plugin->support, plugin->nsupport);
+ pwtest_neg_errno_ok(r);
+ if ((r = spa_handle_get_interface(handle, interface_name, &iface)) != 0) {
+ spa_handle_clear(handle);
+ free(handle);
+ dlclose(hnd);
+ return -ENOSYS;
+ }
+
+ plugin->dlls[plugin->ndlls++] = hnd;
+ plugin->handles[plugin->nhandles++] = handle;
+ plugin->support[plugin->nsupport++] = SPA_SUPPORT_INIT(interface_name, iface);
+
+ *iface_return = iface;
+ return 0;
+}
+
+void *
+pwtest_spa_plugin_load_interface(struct pwtest_spa_plugin *plugin,
+ const char *libname,
+ const char *factory_name,
+ const char *interface_name,
+ const struct spa_dict *info)
+{
+ void *iface;
+ int r = pwtest_spa_plugin_try_load_interface(plugin, &iface, libname,
+ factory_name, interface_name, info);
+ pwtest_neg_errno_ok(r);
+ return iface;
+}
+
+void
+pwtest_mkstemp(char path[PATH_MAX])
+{
+ const char *tmpdir = getenv("TMPDIR");
+ int r;
+
+ if (tmpdir == NULL)
+ pwtest_error_with_msg("tmpdir is unset");
+
+ spa_scnprintf(path, PATH_MAX, "%s/%s", tmpdir, "tmp.XXXXXX");
+ r = mkstemp(path);
+ if (r == -1)
+ pwtest_error_with_msg("Unable to create temporary file: %s", strerror(errno));
+}
+
+int
+pwtest_spawn(const char *file, char *const argv[])
+{
+ int r;
+ int status = -1;
+ pid_t pid;
+ const int fail_code = 121;
+
+ pid = fork();
+ if (pid == 0) {
+ /* child process */
+ execvp(file, (char **)argv);
+ exit(fail_code);
+ } else if (pid < 0)
+ pwtest_error_with_msg("Unable to fork: %s", strerror(errno));
+
+ add_cleanup_pid(ctx, pid);
+ r = waitpid(pid, &status, 0);
+ remove_cleanup_pid(ctx, pid);
+ if (r <= 0)
+ pwtest_error_with_msg("waitpid failed: %s", strerror(errno));
+
+ if (WEXITSTATUS(status) == fail_code)
+ pwtest_error_with_msg("exec %s failed", file);
+
+ return status;
+}
+
+void _pwtest_add(struct pwtest_context *ctx, struct pwtest_suite *suite,
+ const char *funcname, const void *func, ...)
+{
+ struct pwtest_test *t;
+ va_list args;
+
+ if (ctx->test_filter != NULL && fnmatch(ctx->test_filter, funcname, 0) != 0)
+ return;
+
+ t = calloc(1, sizeof *t);
+ t->result = PWTEST_SYSTEM_ERROR;
+ t->name = funcname;
+ t->func = func;
+ t->args.range.min = 0;
+ t->args.range.max = 1;
+ t->args.env = pw_properties_new("PWTEST", "1", NULL);
+ t->env = pw_properties_new(NULL, NULL);
+ for (size_t i = 0; i < SPA_N_ELEMENTS(t->logs); i++)
+ pw_array_init(&t->logs[i], 1024);
+
+ va_start(args, func);
+ while (true) {
+ const char *key, *value;
+
+ enum pwtest_arg arg = va_arg(args, enum pwtest_arg);
+ if (!arg)
+ break;
+ switch (arg) {
+ case PWTEST_NOARG:
+ break;
+ case PWTEST_ARG_SIGNAL:
+ if (RUNNING_ON_VALGRIND)
+ t->result = PWTEST_SKIP;
+ t->args.signal = va_arg(args, int);
+ break;
+ case PWTEST_ARG_RANGE:
+ t->args.range.min = va_arg(args, int);
+ t->args.range.max = va_arg(args, int);
+ break;
+ case PWTEST_ARG_PROP:
+ key = va_arg(args, const char *);
+ value = va_arg(args, const char *);
+ if (t->args.props == NULL) {
+ t->args.props = pw_properties_new(key, value, NULL);
+ } else {
+ pw_properties_set(t->args.props, key, value);
+ }
+ break;
+ case PWTEST_ARG_ENV:
+ key = va_arg(args, const char *);
+ value = va_arg(args, const char *);
+ pw_properties_set(t->args.env, key, value);
+ break;
+ case PWTEST_ARG_DAEMON:
+ if (RUNNING_ON_VALGRIND)
+ t->result = PWTEST_SKIP;
+ t->args.pw_daemon = true;
+ break;
+ }
+ }
+ va_end(args);
+
+ spa_list_append(&suite->tests, &t->link);
+}
+
+extern const struct pwtest_suite_decl __start_pwtest_suite_section;
+extern const struct pwtest_suite_decl __stop_pwtest_suite_section;
+
+static void add_suite(struct pwtest_context *ctx,
+ const struct pwtest_suite_decl *decl)
+{
+ struct pwtest_suite *c = calloc(1, sizeof *c);
+
+ c->decl = decl;
+ spa_list_init(&c->tests);
+ spa_list_append(&ctx->suites, &c->link);
+}
+
+static void free_test(struct pwtest_test *t)
+{
+ spa_list_remove(&t->link);
+ for (size_t i = 0; i < SPA_N_ELEMENTS(t->logs); i++)
+ pw_array_clear(&t->logs[i]);
+ pw_properties_free(t->args.props);
+ pw_properties_free(t->args.env);
+ pw_properties_free(t->env);
+ free(t);
+}
+
+static void free_suite(struct pwtest_suite *c)
+{
+ struct pwtest_test *t, *tmp;
+
+ spa_list_for_each_safe(t, tmp, &c->tests, link)
+ free_test(t);
+
+ spa_list_remove(&c->link);
+ free(c);
+}
+
+static void find_suites(struct pwtest_context *ctx, const char *suite_filter)
+{
+ const struct pwtest_suite_decl *c;
+
+ for (c = &__start_pwtest_suite_section; c < &__stop_pwtest_suite_section; c++) {
+ if (suite_filter == NULL || fnmatch(suite_filter, c->name, 0) == 0)
+ add_suite(ctx, c);
+ }
+}
+
+static void add_tests(struct pwtest_context *ctx)
+{
+ struct pwtest_suite *c;
+
+ spa_list_for_each(c, &ctx->suites, link) {
+ c->result = c->decl->setup(ctx, c);
+ spa_assert_se(c->result >= PWTEST_PASS && c->result <= PWTEST_SYSTEM_ERROR);
+ }
+}
+
+static int remove_file(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf)
+{
+ char *tmpdir = getenv("TMPDIR");
+ int r;
+
+ /* Safety check: bail out if somehow we left TMPDIR */
+ spa_assert_se(tmpdir != NULL);
+ spa_assert_se(spa_strneq(fpath, tmpdir, strlen(tmpdir)));
+
+ r = remove(fpath);
+ if (r)
+ fprintf(stderr, "Failed to remove %s: %m", fpath);
+
+ return r;
+}
+
+static void remove_xdg_runtime_dir(const char *xdg_dir)
+{
+ char *tmpdir = getenv("TMPDIR");
+ char path[PATH_MAX];
+ int r;
+
+ if (xdg_dir == NULL)
+ return;
+
+ /* Safety checks, we really don't want to recursively remove a
+ * random directory due to a bug */
+ spa_assert_se(tmpdir != NULL);
+ spa_assert_se(spa_strneq(xdg_dir, tmpdir, strlen(tmpdir)));
+ r = spa_scnprintf(path, sizeof(path), "%s/pwtest.dir", xdg_dir);
+ spa_assert_se((size_t)r == strlen(xdg_dir) + 11);
+ if (access(path, F_OK) != 0) {
+ fprintf(stderr, "XDG_RUNTIME_DIR changed, cannot clean up\n");
+ return;
+ }
+
+ nftw(xdg_dir, remove_file, 16, FTW_DEPTH | FTW_PHYS);
+}
+
+static void cleanup(struct pwtest_context *ctx)
+{
+ struct pwtest_suite *c, *tmp;
+
+ terminate_cleanup_pids(ctx);
+ free_cleanup_pids(ctx);
+
+ spa_list_for_each_safe(c, tmp, &ctx->suites, link) {
+ free_suite(c);
+ }
+
+ remove_xdg_runtime_dir(ctx->xdg_dir);
+ free(ctx->xdg_dir);
+}
+
+static void sighandler(int signal)
+{
+ struct sigaction act;
+ sigemptyset(&act.sa_mask);
+ act.sa_flags = 0;
+ act.sa_handler = SIG_DFL;
+ sigaction(signal, &act, NULL);
+
+ pwtest_backtrace(0);
+ terminate_cleanup_pids(ctx);
+ raise(signal);
+}
+
+static inline void log_append(struct pw_array *buffer, int fd)
+{
+ int r = 0;
+ const int sz = 65536;
+
+ while (true) {
+ r = pw_array_ensure_size(buffer, sz);
+ spa_assert_se(r == 0);
+ r = read(fd, pw_array_end(buffer), sz);
+ if (r <= 0)
+ break;
+ /* We've read directly into the array's buffer, we just add
+ * now to update the array */
+ pw_array_add(buffer, r);
+ }
+}
+
+static bool collect_child(struct pwtest_test *t, pid_t pid)
+{
+ int r;
+ int status;
+
+ r = waitpid(pid, &status, WNOHANG);
+ if (r <= 0)
+ return false;
+
+ if (WIFEXITED(status)) {
+ t->result = WEXITSTATUS(status);
+ switch (t->result) {
+ case PWTEST_PASS:
+ case PWTEST_SKIP:
+ case PWTEST_FAIL:
+ case PWTEST_TIMEOUT:
+ case PWTEST_SYSTEM_ERROR:
+ break;
+ default:
+ spa_assert_se(!"Invalid test result");
+ break;
+ }
+ return true;
+ }
+
+ if (WIFSIGNALED(status)) {
+ t->sig_or_errno = WTERMSIG(status);
+ t->result = (t->sig_or_errno == t->args.signal) ? PWTEST_PASS : PWTEST_FAIL;
+ } else {
+ t->result = PWTEST_FAIL;
+ }
+ return true;
+}
+
+static pid_t start_pwdaemon(struct pwtest_test *t, int stderr_fd, int log_fd)
+{
+ static unsigned int count;
+ const char *daemon = BUILD_ROOT "/src/daemon/pipewire-uninstalled";
+ pid_t pid;
+ char pw_remote[64];
+ int status;
+ int r;
+
+ spa_scnprintf(pw_remote, sizeof(pw_remote), "pwtest-pw-%u\n", count++);
+ replace_env(t, "PIPEWIRE_REMOTE", pw_remote);
+
+ pid = fork();
+ if (pid == 0) {
+ /* child */
+ setpgid(0, 0);
+
+ setenv("PIPEWIRE_CORE", pw_remote, 1);
+ setenv("PIPEWIRE_DEBUG", "4", 0);
+ setenv("WIREPLUMBER_DEBUG", "4", 0);
+
+ r = dup2(stderr_fd, STDERR_FILENO);
+ spa_assert_se(r != -1);
+ r = dup2(stderr_fd, STDOUT_FILENO);
+ spa_assert_se(r != -1);
+
+ execl(daemon, daemon, (char*)NULL);
+ return -errno;
+
+ } else if (pid < 0) {
+ return pid;
+ }
+
+ add_cleanup_pid(ctx, -pid);
+
+ /* parent */
+ sleep(1); /* FIXME how to wait for pw to be ready? */
+ if (waitpid(pid, &status, WNOHANG) > 0) {
+ if (WIFEXITED(status)) {
+ dprintf(log_fd, "pipewire daemon exited with %d before test started\n", WEXITSTATUS(status));
+ return -ESRCH;
+ } else if (WIFSIGNALED(status)) {
+ dprintf(log_fd, "pipewire daemon terminated with %d (SIG%s) before test started\n", WTERMSIG(status),
+ sigabbrev_np(WTERMSIG(status)));
+ return -EHOSTDOWN;
+ }
+ }
+
+ return pid;
+}
+
+static void make_xdg_runtime_test_dir(char dir[PATH_MAX], const char *prefix)
+{
+ static size_t counter;
+ int r;
+
+ r = spa_scnprintf(dir, PATH_MAX, "%s/%zd", prefix, counter++);
+ spa_assert_se(r >= (int)(strlen(prefix) + 2));
+ r = mkdir(dir, 0777);
+ if (r == -1) {
+ fprintf(stderr, "Failed to make XDG_RUNTIME_DIR %s (%m)\n", dir);
+ spa_assert_se(r != -1);
+ }
+}
+
+static void set_test_env(struct pwtest_context *ctx, struct pwtest_test *t)
+{
+ char xdg_runtime_dir[PATH_MAX];
+
+ make_xdg_runtime_test_dir(xdg_runtime_dir, ctx->xdg_dir);
+ replace_env(t, "XDG_RUNTIME_DIR", xdg_runtime_dir);
+ replace_env(t, "TMPDIR", xdg_runtime_dir);
+
+ replace_env(t, "SPA_PLUGIN_DIR", BUILD_ROOT "/spa/plugins");
+ replace_env(t, "SPA_DATA_DIR", SOURCE_ROOT "/spa/plugins");
+ replace_env(t, "PIPEWIRE_CONFIG_DIR", BUILD_ROOT "/src/daemon");
+ replace_env(t, "PIPEWIRE_MODULE_DIR", BUILD_ROOT "/src/modules");
+ replace_env(t, "ACP_PATHS_DIR", SOURCE_ROOT "/spa/plugins/alsa/mixer/paths");
+ replace_env(t, "ACP_PROFILES_DIR", SOURCE_ROOT "/spa/plugins/alsa/mixer/profile-sets");
+ replace_env(t, "PIPEWIRE_LOG_SYSTEMD", "false");
+}
+
+static void close_pipes(int fds[_FD_LAST])
+{
+ for (int i = 0; i < _FD_LAST; i++) {
+ if (fds[i] >= 0)
+ close(fds[i]);
+ fds[i] = -1;
+ }
+}
+
+static int init_pipes(int read_fds[_FD_LAST], int write_fds[_FD_LAST])
+{
+ int r;
+ int i;
+ int pipe_max_size = 4194304;
+
+ for (i = 0; i < _FD_LAST; i++) {
+ read_fds[i] = -1;
+ write_fds[i] = -1;
+ }
+
+#ifdef __linux__
+ {
+ FILE *f;
+ f = fopen("/proc/sys/fs/pipe-max-size", "re");
+ if (f) {
+ if (fscanf(f, "%d", &r) == 1)
+ pipe_max_size = SPA_MIN(r, pipe_max_size);
+ fclose(f);
+ }
+ }
+#endif
+
+ for (i = 0; i < _FD_LAST; i++) {
+ int pipe[2];
+
+ r = pipe2(pipe, O_CLOEXEC | O_NONBLOCK);
+ if (r < 0)
+ goto error;
+ read_fds[i] = pipe[0];
+ write_fds[i] = pipe[1];
+#ifdef __linux__
+ /* Max pipe buffers, to avoid scrambling if reading lags.
+ * Can't use blocking write fds, since reading too slow
+ * then affects execution.
+ */
+ fcntl(write_fds[i], F_SETPIPE_SZ, pipe_max_size);
+#endif
+ }
+
+ return 0;
+error:
+ r = -errno;
+ close_pipes(read_fds);
+ close_pipes(write_fds);
+ return r;
+}
+
+static void start_test_nofork(struct pwtest_test *t)
+{
+ const char *env;
+ void *state = NULL;
+
+ /* This is going to mess with future tests */
+ while ((env = pw_properties_iterate(t->args.env, &state)))
+ replace_env(t, env, pw_properties_get(t->args.env, env));
+
+ /* The actual test function */
+ t->result = t->func(t);
+}
+
+static int start_test_forked(struct pwtest_test *t, int read_fds[_FD_LAST], int write_fds[_FD_LAST])
+{
+ pid_t pid;
+ enum pwtest_result result;
+ struct sigaction act;
+ const char *env;
+ void *state = NULL;
+ int r;
+
+ pid = fork();
+ if (pid < 0) {
+ r = -errno;
+ close_pipes(read_fds);
+ close_pipes(write_fds);
+ return r;
+ }
+
+ if (pid > 0) { /* parent */
+ close_pipes(write_fds);
+ return pid;
+ }
+
+ /* child */
+
+ close_pipes(read_fds);
+
+ /* Reset cleanup pid list */
+ free_cleanup_pids(ctx);
+
+ /* Catch any crashers so we can insert a backtrace */
+ sigemptyset(&act.sa_mask);
+ act.sa_flags = 0;
+ act.sa_handler = sighandler;
+ sigaction(SIGSEGV, &act, NULL);
+ sigaction(SIGBUS, &act, NULL);
+ sigaction(SIGSEGV, &act, NULL);
+ sigaction(SIGABRT, &act, NULL);
+ /* SIGALARM is used for our timeout */
+ sigaction(SIGALRM, &act, NULL);
+
+ r = dup2(write_fds[FD_STDERR], STDERR_FILENO);
+ spa_assert_se(r != -1);
+ setlinebuf(stderr);
+ r = dup2(write_fds[FD_STDOUT], STDOUT_FILENO);
+ spa_assert_se(r != -1);
+ setlinebuf(stdout);
+
+ /* For convenience in the tests, let this be a global variable. */
+ testlog_fd = write_fds[FD_LOG];
+
+ while ((env = pw_properties_iterate(t->args.env, &state)))
+ setenv(env, pw_properties_get(t->args.env, env), 1);
+
+ /* The actual test function */
+ result = t->func(t);
+
+ for (int i = 0; i < _FD_LAST; i++)
+ fsync(write_fds[i]);
+
+ exit(result);
+}
+
+static int monitor_test_forked(struct pwtest_test *t, pid_t pid, int read_fds[_FD_LAST])
+{
+ int pidfd = -1, timerfd = -1, epollfd = -1;
+ struct epoll_event ev[10];
+ size_t nevents = 0;
+ int r;
+
+#ifdef HAVE_PIDFD_OPEN
+ pidfd = syscall(SYS_pidfd_open, pid, 0);
+#else
+ errno = ENOSYS;
+#endif
+ /* If we don't have pidfd, we use a timerfd to ping us every 20ms */
+ if (pidfd < 0 && errno == ENOSYS) {
+ pidfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
+ if (pidfd == -1)
+ goto error;
+ r = timerfd_settime(pidfd, 0,
+ &((struct itimerspec ){
+ .it_interval.tv_nsec = 20 * 1000 * 1000,
+ .it_value.tv_nsec = 20 * 1000 * 1000,
+ }), NULL);
+ if (r < 0)
+ goto error;
+ }
+
+ /* Each test has an epollfd with:
+ * - a timerfd so we can kill() it if it hangs
+ * - a pidfd so we get notified when the test exits
+ * - a pipe for stdout and a pipe for stderr
+ * - a pipe for logging (the various pwtest functions)
+ * - a pipe for the daemon's stdout
+ */
+ timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
+ if (timerfd < 0)
+ goto error;
+ timerfd_settime(timerfd, 0, &((struct itimerspec ){ .it_value.tv_sec = ctx->timeout}), NULL);
+
+ epollfd = epoll_create(1);
+ if (epollfd < 0)
+ goto error;
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = pidfd };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_STDOUT] };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_STDERR] };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_LOG] };
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = timerfd };
+ if (t->args.pw_daemon)
+ ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = read_fds[FD_DAEMON] };
+
+ for (size_t i = 0; i < nevents; i++) {
+ r = epoll_ctl(epollfd, EPOLL_CTL_ADD, ev[i].data.fd, &ev[i]);
+ if (r < 0)
+ goto error;
+ }
+
+ while (true) {
+ struct epoll_event e;
+
+ r = epoll_wait(epollfd, &e, 1, (ctx->timeout * 2) * 1000);
+ if (r == 0)
+ break;
+ if (r == -1) {
+ goto error;
+ }
+
+ if (e.data.fd == pidfd) {
+ uint64_t buf;
+ int ignore SPA_UNUSED;
+ ignore = read(pidfd, &buf, sizeof(buf)); /* for timerfd fallback */
+ if (collect_child(t, pid))
+ break;
+ } else if (e.data.fd == timerfd) {
+ /* SIGALARM so we get the backtrace */
+ kill(pid, SIGALRM);
+ t->result = PWTEST_TIMEOUT;
+ waitpid(pid, NULL, 0);
+ break;
+ } else {
+ for (int i = 0; i < _FD_LAST; i++) {
+ if (e.data.fd == read_fds[i]) {
+ log_append(&t->logs[i], e.data.fd);
+ }
+
+ }
+ }
+ }
+ errno = 0;
+error:
+ r = errno;
+ close(epollfd);
+ close(timerfd);
+ close(pidfd);
+
+ return -r;
+}
+
+static void run_test(struct pwtest_context *ctx, struct pwtest_suite *c, struct pwtest_test *t)
+{
+ pid_t pid;
+ pid_t pw_daemon = 0;
+ int read_fds[_FD_LAST], write_fds[_FD_LAST];
+ int r;
+ const char *tmpdir;
+
+ if (t->result == PWTEST_SKIP) {
+ char *buf = pw_array_add(&t->logs[FD_LOG], 64);
+ spa_scnprintf(buf, 64, "pwtest: test skipped by pwtest\n");
+ return;
+ }
+
+ t->result = PWTEST_SYSTEM_ERROR;
+
+ r = init_pipes(read_fds, write_fds);
+ if (r < 0) {
+ t->sig_or_errno = r;
+ return;
+ }
+
+ set_test_env(ctx, t);
+ tmpdir = getenv("TMPDIR");
+ spa_assert_se(tmpdir != NULL);
+ r = chdir(tmpdir);
+ if (r < 0) {
+ char *buf = pw_array_add(&t->logs[FD_LOG], 256);
+ spa_scnprintf(buf, 256, "pwtest: failed to chdir to '%s'\n", tmpdir);
+ t->sig_or_errno = -errno;
+ goto error;
+ }
+
+ if (t->args.pw_daemon) {
+ pw_daemon = start_pwdaemon(t, write_fds[FD_DAEMON], write_fds[FD_LOG]);
+ if (pw_daemon < 0) {
+ errno = -pw_daemon;
+ goto error;
+ }
+ } else {
+ replace_env(t, "PIPEWIRE_REMOTE", "test-has-no-daemon");
+ }
+
+ if (ctx->no_fork) {
+ start_test_nofork(t);
+ } else {
+ pid = start_test_forked(t, read_fds, write_fds);
+ if (pid < 0) {
+ errno = -r;
+ goto error;
+ }
+ add_cleanup_pid(ctx, pid);
+
+ r = monitor_test_forked(t, pid, read_fds);
+ if (r < 0) {
+ errno = -r;
+ goto error;
+ }
+ remove_cleanup_pid(ctx, pid);
+ }
+
+ errno = 0;
+error:
+ if (errno)
+ t->sig_or_errno = -errno;
+
+ if (ctx->terminate) {
+ char *buf = pw_array_add(&t->logs[FD_LOG], 64);
+ spa_scnprintf(buf, 64, "pwtest: tests terminated by signal\n");
+ t->result = PWTEST_SYSTEM_ERROR;
+ }
+
+ for (size_t i = 0; i < SPA_N_ELEMENTS(read_fds); i++) {
+ log_append(&t->logs[i], read_fds[i]);
+ }
+
+ if (pw_daemon > 0) {
+ int status;
+
+ kill(-pw_daemon, SIGTERM);
+ remove_cleanup_pid(ctx, -pw_daemon);
+
+ /* blocking read. the other end closes when done */
+ close_pipes(write_fds);
+ fcntl(read_fds[FD_DAEMON], F_SETFL, O_CLOEXEC);
+ do {
+ log_append(&t->logs[FD_DAEMON], read_fds[FD_DAEMON]);
+ } while ((r = waitpid(pw_daemon, &status, WNOHANG)) == 0);
+
+ if (r > 0) {
+ /* write_fds are closed in the parent process, so we append directly */
+ char *buf = pw_array_add(&t->logs[FD_DAEMON], 64);
+
+ if (WIFEXITED(status)) {
+ spa_scnprintf(buf, 64, "pwtest: pipewire daemon exited with status %d\n",
+ WEXITSTATUS(status));
+ } else if (WIFSIGNALED(status)) {
+ spa_scnprintf(buf, 64, "pwtest: pipewire daemon crashed with signal %d (SIG%s)\n",
+ WTERMSIG(status), sigabbrev_np(WTERMSIG(status)));
+ }
+ }
+ }
+
+ for (size_t i = 0; i < SPA_N_ELEMENTS(t->logs); i++) {
+ char *e = pw_array_add(&t->logs[i], 1);
+ spa_assert_se(e);
+ *e = '\0';
+ }
+
+ close_pipes(read_fds);
+ close_pipes(write_fds);
+
+ restore_env(t);
+}
+
+static inline void print_lines(FILE *fp, const char *log, const char *prefix)
+{
+ const char *state = NULL;
+ const char *s;
+ size_t len;
+
+ while (true) {
+ if ((s = pw_split_walk(log, "\n", &len, &state)) == NULL)
+ break;
+ fprintf(fp, "%s%.*s\n", prefix, (int)len, s);
+ }
+}
+
+static void log_test_result(struct pwtest_test *t)
+{
+ const struct status *s;
+ const struct status {
+ const char *status;
+ const char *color;
+ } statuses[] = {
+ { "PASS", SPA_ANSI_BOLD_GREEN },
+ { "FAIL", SPA_ANSI_BOLD_RED },
+ { "SKIP", SPA_ANSI_BOLD_YELLOW },
+ { "TIMEOUT", SPA_ANSI_BOLD_CYAN },
+ { "ERROR", SPA_ANSI_BOLD_MAGENTA },
+ };
+
+ spa_assert_se(t->result >= PWTEST_PASS);
+ spa_assert_se(t->result <= PWTEST_SYSTEM_ERROR);
+ s = &statuses[t->result - PWTEST_PASS];
+
+ fprintf(stderr, " status: %s%s%s\n",
+ isatty(STDERR_FILENO) ? s->color : "",
+ s->status,
+ isatty(STDERR_FILENO) ? "\x1B[0m" : "");
+
+ switch (t->result) {
+ case PWTEST_PASS:
+ case PWTEST_SKIP:
+ if (!verbose)
+ return;
+ break;
+ default:
+ break;
+ }
+
+ if (t->sig_or_errno > 0)
+ fprintf(stderr, " signal: %d # SIG%s \n", t->sig_or_errno,
+ sigabbrev_np(t->sig_or_errno));
+ else if (t->sig_or_errno < 0)
+ fprintf(stderr, " errno: %d # %s\n", -t->sig_or_errno,
+ strerror(-t->sig_or_errno));
+ if (t->logs[FD_LOG].size) {
+ fprintf(stderr, " log: |\n");
+ print_lines(stderr, t->logs[FD_LOG].data, " ");
+ }
+ if (t->logs[FD_STDOUT].size) {
+ fprintf(stderr, " stdout: |\n");
+ print_lines(stderr, t->logs[FD_STDOUT].data, " ");
+ }
+ if (t->logs[FD_STDERR].size) {
+ fprintf(stderr, " stderr: |\n");
+ print_lines(stderr, t->logs[FD_STDERR].data, " ");
+ }
+ if (t->logs[FD_DAEMON].size) {
+ fprintf(stderr, " daemon: |\n");
+ print_lines(stderr, t->logs[FD_DAEMON].data, " ");
+ }
+}
+
+static char* make_xdg_runtime_dir(void)
+{
+ time_t t = time(NULL);
+ struct tm *tm = localtime(&t);
+ char *dir;
+ char *tmpdir = getenv("TMPDIR");
+ char path[PATH_MAX];
+ FILE *fp;
+
+ if (!tmpdir)
+ tmpdir = "/tmp";
+
+ int r = asprintf(&dir, "%s/pwtest-%02d:%02d-XXXXXX", tmpdir, tm->tm_hour, tm->tm_min);
+ spa_assert_se((size_t)r == strlen(tmpdir) + 20); /* rough estimate */
+ spa_assert_se(mkdtemp(dir) != NULL);
+
+ /* Marker file to avoid removing a random directory during cleanup */
+ r = spa_scnprintf(path, sizeof(path), "%s/pwtest.dir", dir);
+ spa_assert_se((size_t)r == strlen(dir) + 11);
+ fp = fopen(path, "we");
+ spa_assert_se(fp);
+ fprintf(fp, "pwtest\n");
+ fclose(fp);
+
+ return dir;
+}
+
+static int run_tests(struct pwtest_context *ctx)
+{
+ int r = EXIT_SUCCESS;
+ struct pwtest_suite *c;
+ struct pwtest_test *t;
+
+ fprintf(stderr, "pwtest:\n");
+ spa_list_for_each(c, &ctx->suites, link) {
+ if (c->result != PWTEST_PASS)
+ continue;
+
+ fprintf(stderr, "- suite: \"%s\"\n", c->decl->name);
+ fprintf(stderr, " tests:\n");
+ spa_list_for_each(t, &c->tests, link) {
+ int min = t->args.range.min,
+ max = t->args.range.max;
+ bool have_range = min != 0 || max != 1;
+
+ for (int iteration = min; iteration < max; iteration++) {
+ if (ctx->has_iteration_filter &&
+ ctx->iteration_filter != iteration)
+ continue;
+
+ fprintf(stderr, " - name: \"%s\"\n", t->name);
+ if (have_range)
+ fprintf(stderr, " iteration: %d # %d - %d\n",
+ iteration, min, max);
+ t->iteration = iteration;
+ run_test(ctx, c, t);
+ log_test_result(t);
+
+ switch (t->result) {
+ case PWTEST_PASS:
+ case PWTEST_SKIP:
+ break;
+ default:
+ r = EXIT_FAILURE;
+ break;
+ }
+
+ if (ctx->terminate) {
+ r = EXIT_FAILURE;
+ return r;
+ }
+ }
+ }
+ }
+ return r;
+}
+
+static void list_tests(struct pwtest_context *ctx)
+{
+ struct pwtest_suite *c;
+ struct pwtest_test *t;
+
+ fprintf(stderr, "pwtest:\n");
+ spa_list_for_each(c, &ctx->suites, link) {
+ fprintf(stderr, "- suite: \"%s\"\n", c->decl->name);
+ fprintf(stderr, " tests:\n");
+ spa_list_for_each(t, &c->tests, link) {
+ fprintf(stderr, " - { name: \"%s\" }\n", t->name);
+ }
+ }
+}
+
+static bool is_debugger_attached(void)
+{
+ bool rc = false;
+#ifdef HAVE_LIBCAP
+ int status;
+ int pid = fork();
+
+ if (pid == -1)
+ return 0;
+
+ if (pid == 0) {
+ int ppid = getppid();
+ cap_t caps = cap_get_pid(ppid);
+ cap_flag_value_t cap_val;
+
+ if (cap_get_flag(caps, CAP_SYS_PTRACE, CAP_EFFECTIVE, &cap_val) == -1 ||
+ cap_val != CAP_SET)
+ _exit(false);
+
+ if (ptrace(PTRACE_ATTACH, ppid, NULL, 0) == 0) {
+ waitpid(ppid, NULL, 0);
+ ptrace(PTRACE_CONT, ppid, NULL, 0);
+ ptrace(PTRACE_DETACH, ppid, NULL, 0);
+ rc = false;
+ } else {
+ rc = true;
+ }
+ _exit(rc);
+ } else {
+ waitpid(pid, &status, 0);
+ rc = WEXITSTATUS(status);
+ }
+
+#endif
+ return !!rc;
+}
+
+static void usage(FILE *fp, const char *progname)
+{
+ fprintf(fp, "Usage: %s [OPTIONS]\n"
+ " -h, --help Show this help\n"
+ " --verbose Verbose output\n"
+ " --list List all available suites and tests\n"
+ " --timeout=N Set the test timeout to N seconds (default: 15)\n"
+ " --filter-test=glob Run only tests matching the given glob\n"
+ " --filter-suites=glob Run only suites matching the given glob\n"
+ " --filter-iteration=N Run only iteration N\n"
+ " --no-fork Do not fork for the test (see note below)\n"
+ "\n"
+ "Using --no-fork allows for easy debugging of tests but should only be used.\n"
+ "used with --filter-test. A test that modifies the process state will affect\n"
+ "subsequent tests and invalidate test results.\n",
+ progname);
+}
+
+static void sigterm_handler(int signo)
+{
+ terminate_cleanup_pids(ctx);
+ ctx->terminate = true;
+ if (ctx->no_fork) {
+ signal(SIGTERM, SIG_DFL);
+ signal(SIGINT, SIG_DFL);
+ raise(signo);
+ }
+}
+
+int main(int argc, char **argv)
+{
+ int r = EXIT_SUCCESS;
+ enum {
+ OPT_TIMEOUT = 10,
+ OPT_LIST,
+ OPT_VERBOSE,
+ OPT_FILTER_TEST,
+ OPT_FILTER_SUITE,
+ OPT_FILTER_ITERATION,
+ OPT_NOFORK,
+ };
+ static const struct option opts[] = {
+ { "help", no_argument, 0, 'h' },
+ { "timeout", required_argument, 0, OPT_TIMEOUT },
+ { "list", no_argument, 0, OPT_LIST },
+ { "filter-test", required_argument, 0, OPT_FILTER_TEST },
+ { "filter-suite", required_argument, 0, OPT_FILTER_SUITE },
+ { "filter-iteration", required_argument, 0, OPT_FILTER_ITERATION },
+ { "list", no_argument, 0, OPT_LIST },
+ { "verbose", no_argument, 0, OPT_VERBOSE },
+ { "no-fork", no_argument, 0, OPT_NOFORK },
+ { NULL },
+ };
+ struct pwtest_context test_ctx = {
+ .suites = SPA_LIST_INIT(&test_ctx.suites),
+ .timeout = 15,
+ .has_iteration_filter = false,
+ };
+ enum {
+ MODE_TEST,
+ MODE_LIST,
+ } mode = MODE_TEST;
+ const char *suite_filter = NULL;
+
+ spa_list_init(&test_ctx.cleanup_pids);
+
+ ctx = &test_ctx;
+
+ while (1) {
+ int c;
+ int option_index = 0;
+
+ c = getopt_long(argc, argv, "h", opts, &option_index);
+ if (c == -1)
+ break;
+ switch(c) {
+ case 'h':
+ usage(stdout, argv[0]);
+ exit(EXIT_SUCCESS);
+ case OPT_TIMEOUT:
+ ctx->timeout = atoi(optarg);
+ break;
+ case OPT_LIST:
+ mode = MODE_LIST;
+ break;
+ case OPT_VERBOSE:
+ verbose = true;
+ break;
+ case OPT_FILTER_TEST:
+ ctx->test_filter = optarg;
+ break;
+ case OPT_FILTER_SUITE:
+ suite_filter= optarg;
+ break;
+ case OPT_FILTER_ITERATION:
+ ctx->has_iteration_filter = spa_atoi32(optarg, &ctx->iteration_filter, 10);
+ break;
+ case OPT_NOFORK:
+ ctx->no_fork = true;
+ break;
+ default:
+ usage(stderr, argv[0]);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ if (RUNNING_ON_VALGRIND || is_debugger_attached())
+ ctx->no_fork = true;
+
+ find_suites(ctx, suite_filter);
+ add_tests(ctx);
+
+ if (getenv("TMPDIR") == NULL)
+ setenv("TMPDIR", "/tmp", 1);
+
+ ctx->xdg_dir = make_xdg_runtime_dir();
+
+ switch (mode) {
+ case MODE_LIST:
+ list_tests(ctx);
+ break;
+ case MODE_TEST:
+ setrlimit(RLIMIT_CORE, &((struct rlimit){0, 0}));
+ signal(SIGTERM, sigterm_handler);
+ signal(SIGINT, sigterm_handler);
+ r = run_tests(ctx);
+ break;
+ }
+
+ cleanup(ctx);
+
+ return r;
+}