mirror of
				https://gitlab.freedesktop.org/pipewire/pipewire.git
				synced 2025-11-03 09:01:54 -05:00 
			
		
		
		
	If we don't have the capability to ptrace, we are probably running inside a container, not the debugger. Check this first so we don't disable forking mode. Make this conditional on libcap - where libcap is not available always assume we *do not* have a debugger attached. This is easier than telling everyone who runs the tests in a confined system to install libcap. Fixes #1285
		
			
				
	
	
		
			1283 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			1283 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
/* 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>
 | 
						|
#include <syscall.h>
 | 
						|
#if 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;
 | 
						|
 | 
						|
	const char *test_filter;
 | 
						|
	char *xdg_dir;
 | 
						|
};
 | 
						|
 | 
						|
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 void pwtest_backtrace(pid_t p)
 | 
						|
{
 | 
						|
#if 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)
 | 
						|
{
 | 
						|
	void **dll;
 | 
						|
	struct spa_handle **hnd;
 | 
						|
 | 
						|
	SPA_FOR_EACH_ELEMENT(plugin->handles, hnd) {
 | 
						|
		if (*hnd)
 | 
						|
			free(*hnd);
 | 
						|
	}
 | 
						|
	SPA_FOR_EACH_ELEMENT(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(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) {
 | 
						|
		dlclose(hnd);
 | 
						|
		free(handle);
 | 
						|
		return -ENOSYS;
 | 
						|
	}
 | 
						|
 | 
						|
	plugin->handles[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));
 | 
						|
}
 | 
						|
 | 
						|
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->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:
 | 
						|
			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:
 | 
						|
			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(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 */
 | 
						|
	if (!tmpdir)
 | 
						|
		tmpdir = "/tmp";
 | 
						|
	spa_assert(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 */
 | 
						|
	if (!tmpdir)
 | 
						|
		tmpdir = "/tmp";
 | 
						|
 | 
						|
	spa_assert(spa_strneq(xdg_dir, tmpdir, strlen(tmpdir)));
 | 
						|
	r = spa_scnprintf(path, sizeof(path), "%s/pwtest.dir", xdg_dir);
 | 
						|
	spa_assert((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;
 | 
						|
 | 
						|
	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);
 | 
						|
	raise(signal);
 | 
						|
}
 | 
						|
 | 
						|
static inline void log_append(struct pw_array *buffer, int fd)
 | 
						|
{
 | 
						|
	int r = 0;
 | 
						|
	const int sz = 1024;
 | 
						|
 | 
						|
	while (true) {
 | 
						|
		r = pw_array_ensure_size(buffer, sz);
 | 
						|
		spa_assert(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(!"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;
 | 
						|
 | 
						|
	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 */
 | 
						|
		setenv("PIPEWIRE_CORE", pw_remote, 1);
 | 
						|
 | 
						|
		dup2(stderr_fd, STDERR_FILENO);
 | 
						|
		setlinebuf(stderr);
 | 
						|
		execl(daemon, daemon, (char*)NULL);
 | 
						|
		return -errno;
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	/* 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(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(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, "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");
 | 
						|
}
 | 
						|
 | 
						|
static void close_pipes(int fds[_FD_LAST])
 | 
						|
{
 | 
						|
	for (int i = 0; i < _FD_LAST; i++) {
 | 
						|
		close(fds[i]);
 | 
						|
		fds[i] = -1;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
static int init_pipes(int read_fds[_FD_LAST], int write_fds[_FD_LAST])
 | 
						|
{
 | 
						|
	int r;
 | 
						|
	int i;
 | 
						|
 | 
						|
	for (i = 0; i < _FD_LAST; i++) {
 | 
						|
		read_fds[i] = -1;
 | 
						|
		write_fds[i] = -1;
 | 
						|
	}
 | 
						|
 | 
						|
	for (i = 0; i < _FD_LAST; i++) {
 | 
						|
		int pipe[2];
 | 
						|
 | 
						|
		r = pipe2(pipe, O_NONBLOCK);
 | 
						|
		if (r < 0)
 | 
						|
			goto error;
 | 
						|
		read_fds[i] = pipe[0];
 | 
						|
		write_fds[i] = pipe[1];
 | 
						|
	}
 | 
						|
 | 
						|
	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);
 | 
						|
 | 
						|
	/* 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(r != -1);
 | 
						|
	setlinebuf(stderr);
 | 
						|
	r = dup2(write_fds[FD_STDOUT], STDOUT_FILENO);
 | 
						|
	spa_assert(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;
 | 
						|
 | 
						|
#if 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;
 | 
						|
			(void)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;
 | 
						|
 | 
						|
	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);
 | 
						|
	chdir(getenv("TMPDIR"));
 | 
						|
 | 
						|
	t->result = PWTEST_SYSTEM_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;
 | 
						|
		}
 | 
						|
 | 
						|
		r = monitor_test_forked(t, pid, read_fds);
 | 
						|
		if (r < 0) {
 | 
						|
			errno = -r;
 | 
						|
			goto error;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	errno = 0;
 | 
						|
error:
 | 
						|
	if (errno)
 | 
						|
		t->sig_or_errno = -errno;
 | 
						|
 | 
						|
	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);
 | 
						|
		r = waitpid(pw_daemon, &status, 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)));
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	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(t->result >= PWTEST_PASS);
 | 
						|
	spa_assert(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((size_t)r == strlen(tmpdir) + 20); /* rough estimate */
 | 
						|
	spa_assert(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((size_t)r == strlen(dir) + 11);
 | 
						|
	fp = fopen(path, "w");
 | 
						|
	spa_assert(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++) {
 | 
						|
				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;
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	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;
 | 
						|
#if 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: 30)\n"
 | 
						|
		"  --filter-test=glob	Run only tests matching the given glob\n"
 | 
						|
		"  --filter-suites=glob	Run only suites matching the given glob\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);
 | 
						|
}
 | 
						|
 | 
						|
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_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 },
 | 
						|
		{ "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 = 30,
 | 
						|
	};
 | 
						|
	enum {
 | 
						|
		MODE_TEST,
 | 
						|
		MODE_LIST,
 | 
						|
	} mode = MODE_TEST;
 | 
						|
	const char *suite_filter = NULL;
 | 
						|
 | 
						|
	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_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);
 | 
						|
 | 
						|
	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}));
 | 
						|
		r = run_tests(ctx);
 | 
						|
		break;
 | 
						|
	}
 | 
						|
 | 
						|
	cleanup(ctx);
 | 
						|
 | 
						|
	return r;
 | 
						|
}
 |