pipewire/test/pwtest.c
Peter Hutterer dc5751b569 test: add a helper function for making tempfiles
Having a helper aids with the file being in the right directory and cleaned up
automatically on exit. Plus, failing the test with the sytem error status code
signals that it's not the actual test failing here.
2021-06-09 18:00:06 +10:00

1226 lines
29 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.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#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>
#include <sys/epoll.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"
#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, 0);
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;
pidfd = syscall(SYS_pidfd_open, pid, 0);
if (pidfd < 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) {
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 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)
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;
}