diff --git a/config.h.meson b/config.h.meson index 8317cc063..64315ef57 100644 --- a/config.h.meson +++ b/config.h.meson @@ -485,3 +485,5 @@ #mesondefine HAVE_GSTREAMER_DEVICE_PROVIDER #mesondefine HAVE_WEBRTC + +#mesondefine HAVE_GSTACK diff --git a/doc/meson.build b/doc/meson.build index 6d9860db4..032d343ad 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -17,6 +17,7 @@ endforeach foreach h : pipewire_sources inputs += meson.source_root() / 'src' / 'pipewire' / h endforeach +inputs += meson.source_root() / 'test' / 'pwtest.h' # SPA headers use static inline functions. Doxygen doesn't extract those # unless we have EXTRACT_STATIC set - but we don't want it to extract diff --git a/meson.build b/meson.build index d43ba3eac..508aba886 100644 --- a/meson.build +++ b/meson.build @@ -372,10 +372,19 @@ installed_tests_execdir = pipewire_libexecdir / 'installed-tests' / pipewire_nam installed_tests_enabled = not get_option('installed_tests').disabled() installed_tests_template = files('template.test.in') +if not get_option('tests').disabled() + gstack = find_program('gstack', required : false) + cdata.set10('HAVE_GSTACK', gstack.found()) +endif + subdir('po') subdir('spa') subdir('src') +if not get_option('tests').disabled() + subdir('test') +endif + configure_file(input : 'config.h.meson', output : 'config.h', configuration : cdata) diff --git a/spa/include/spa/utils/defs.h b/spa/include/spa/utils/defs.h index 69ce1ccac..e77212ce3 100644 --- a/spa/include/spa/utils/defs.h +++ b/spa/include/spa/utils/defs.h @@ -200,6 +200,7 @@ struct spa_fraction { #define SPA_EXPORT __attribute__((visibility("default"))) #define SPA_SENTINEL __attribute__((__sentinel__)) #define SPA_UNUSED __attribute__ ((unused)) +#define SPA_NORETURN __attribute__ ((noreturn)) #else #define SPA_PRINTF_FUNC(fmt, arg1) #define SPA_ALIGNED(align) @@ -207,6 +208,7 @@ struct spa_fraction { #define SPA_EXPORT #define SPA_SENTINEL #define SPA_UNUSED +#define SPA_NORETURN #endif #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 000000000..e05a539f2 --- /dev/null +++ b/test/meson.build @@ -0,0 +1,38 @@ +pwtest_sources = [ + 'pwtest.h', + 'pwtest-implementation.h', + 'pwtest.c', +] + +pwtest_deps = [ + pipewire_dep, + mathlib, +] + +pwtest_c_args = [ + '-DHAVE_CONFIG_H', + '-DBUILD_ROOT="@0@"'.format(meson.build_root()), + '-DSOURCE_ROOT="@0@"'.format(meson.source_root()), +] + +pwtest_inc = [ + spa_inc, + pipewire_inc, + configinc, + includes_inc, +] + +pwtest_lib = static_library( + 'pwtest', + pwtest_sources, + c_args: pwtest_c_args, + dependencies: pwtest_deps, + include_directories: pwtest_inc, +) + +# Compilation only, this is the example file for how pwtest works and most +# of its tests will fail. +executable('test-example', + 'test-example.c', + include_directories: pwtest_inc, + link_with: pwtest_lib) diff --git a/test/pwtest-implementation.h b/test/pwtest-implementation.h new file mode 100644 index 000000000..2944ef545 --- /dev/null +++ b/test/pwtest-implementation.h @@ -0,0 +1,107 @@ +/* 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. + */ + +#ifndef PWTEST_IMPLEMENTATION_H +#define PWTEST_IMPLEMENTATION_H + +#include + +/* This header should never be included on its own, it merely exists to make + * the user-visible pwtest.h header more readable */ + +void +_pwtest_fail_condition(int exitstatus, const char *file, int line, const char *func, + const char *condition, const char *message, ...); +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); +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); +void +_pwtest_fail_comparison_ptr(const char *file, int line, const char *func, + const char *comparison); + +void +_pwtest_fail_comparison_str(const char *file, int line, const char *func, + const char *comparison, const char *a, const char *b); + +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); + +#define pwtest_comparison_bool_(a_, op_, b_) \ + do { \ + bool _a = !!(a_); \ + bool _b = !!(b_); \ + if (!(_a op_ _b)) \ + _pwtest_fail_comparison_bool(__FILE__, __LINE__, __func__,\ + #op_, _a, _b, #a_, #b_); \ + } while(0) + +#define pwtest_comparison_int_(a_, op_, b_) \ + do { \ + __typeof__(a_) _a = a_; \ + __typeof__(b_) _b = b_; \ + if (trunc(_a) != _a || trunc(_b) != _b) \ + pwtest_error_with_msg("pwtest_int_* used for non-integer value\n"); \ + if (!((_a) op_ (_b))) \ + _pwtest_fail_comparison_int(__FILE__, __LINE__, __func__,\ + #op_, _a, _b, #a_, #b_); \ + } while(0) + +#define pwtest_comparison_ptr_(a_, op_, b_) \ + do { \ + __typeof__(a_) _a = a_; \ + __typeof__(b_) _b = b_; \ + if (!((_a) op_ (_b))) \ + _pwtest_fail_comparison_ptr(__FILE__, __LINE__, __func__,\ + #a_ " " #op_ " " #b_); \ + } while(0) + +#define pwtest_comparison_double_(a_, op_, b_) \ + do { \ + const double EPSILON = 1.0/256; \ + __typeof__(a_) _a = a_; \ + __typeof__(b_) _b = b_; \ + if (!((_a) op_ (_b)) && fabs((_a) - (_b)) > EPSILON) \ + _pwtest_fail_comparison_double(__FILE__, __LINE__, __func__,\ + #op_, _a, _b, #a_, #b_); \ + } while(0) + +void _pwtest_add(struct pwtest_context *ctx, + struct pwtest_suite *suite, + const char *funcname, const void *func, + ...) SPA_SENTINEL; + +struct pwtest_suite_decl { + const char *name; + enum pwtest_result (*setup)(struct pwtest_context *, struct pwtest_suite *); +} __attribute__((aligned(16))); + + +#endif /* PWTEST_IMPLEMENTATION_H */ diff --git a/test/pwtest.c b/test/pwtest.c new file mode 100644 index 000000000..d914c8bfd --- /dev/null +++ b/test/pwtest.c @@ -0,0 +1,996 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "spa/utils/ansi.h" +#include "spa/utils/string.h" +#include "spa/utils/defs.h" +#include "spa/utils/list.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; +}; + +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"); + setenv(prop, value, 1); +} + +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_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); +} + +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 void cleanup(struct pwtest_context *ctx) +{ + struct pwtest_suite *c, *tmp; + + spa_list_for_each_safe(c, tmp, &ctx->suites, link) { + free_suite(c); + } +} + +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 set_test_env(struct pwtest_context *ctx, struct pwtest_test *t) +{ + 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); + + 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; + } + } + + 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 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); + + 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; +} diff --git a/test/pwtest.h b/test/pwtest.h new file mode 100644 index 000000000..e4c374896 --- /dev/null +++ b/test/pwtest.h @@ -0,0 +1,482 @@ +/* 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 + +#ifndef PWTEST_H +#define PWTEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include + +/** + * \defgroup pwtest The pwtest PipeWire Test Suite + * \brief `pwtest` is a test runner framework for PipeWire. + * + * It's modelled after other + * test suites like [check](https://libcheck.github.io/check/) + * and draws a lot of inspiration from the [libinput test + * suite](https://wayland.freedesktop.org/libinput/doc/latest/). + * + * `pwtest` captures logs from the tests (and the pipewire daemon, if + * applicable) and collects the output into YAML files printed to `stderr`. + * + * ## Tests + * + * A `pwtest` test is declared with the `PWTEST()` macro and must return one of + * the `pwtest` status codes. Those codes are: + * - \ref PWTEST_PASS for a successful test + * - \ref PWTEST_FAIL for a test case failure. Usually you should not return this + * value but rely on the `pwtest` macros to handle this case. + * - \ref PWTEST_SKIP to skip the current test + * - \ref PWTEST_SYSTEM_ERROR in case of an error that would cause the test to not run propertly. This is not a test case failure but some required precondition not being met. + * + * ```c + * #include "pwtest.h" + * + * PWTEST(some_test) + * { + * int var = 10; + * const char *str = "foo"; + * + * if (access("/", R_OK)) + * pwtest_error_with_message("Oh dear, no root directory?"); + * + * if (today_is_monday) + * return PWTEST_SKIP; + * + * pwtest_int_lt(var, 20); + * pwtest_ptr_notnull(&var); + * pwtest_str_ne(str, "bar"); + * + * return PWTEST_PASS; + * } + * ... + * ``` + * + * `pwtest` provides comparision macros for most basic data types with the `lt`, + * `le`, `eq`, `gt`, `ge` suffixes (`<, <=, ==, >, >=`). Tests usually should not + * return `PWTEST_FAIL` directly, use the `pwtest_fail()` macros if . + * + * By default, a test runs in a forked process, any changes to the + * process'environment, etc. are discarded in the next test. + * + * ## Suites + * + * Tests are grouped into suites and declared with the PWTEST_SUITE() macro. + * Each test must be added with the required arguments, it is acceptable to + * add the same test multiple times with different arguments. + * + * ```c + * ... + * PWTEST_SUITE(misc) + * { + * if (today_is_monday) + * return PWTEST_SKIP; + * + * // simple test + * pwtest_add(some_test, PWTEST_NOARG); + * // starts with its own pipewire daemon instance + * pwtest_add(some_test, PWTEST_ARG_DAEMON); + * + * return PWTEST_SUCCESS; + * } + * ``` + * For a list of potential arguments, see \ref pwtest_arg and the + * `test-examples.c` file in the source directory. + * + * Suites are auto-discovered, they do not have to be manually added to a test run. + * + * ## Running tests + * + * The `pwtest` framework is built into each test binary, so just execute the + * matching binary. See the `--help` output for the full argument list. + * + * The most useful arguments when running the test suite: + * - `--verbose` to enable logs even when tests pass or are skipped + * - `--filter-test=glob`, `--filter-suite=glob` uses an `fnmatch()` glob to limit which tests or suites are run + * - `--no-fork` - see "Debugging test-case failures" + * + * ## Debugging test-case failures + * + * To debug a single test, disable forking and run the test through gdb: + * + * ``` + * $ gdb path/to/test + * (gdb) break test_function_name + * Breakpoint 1 at 0xfffffffffffff: file ../test/test-file.c, line 123 + * (gdb) r --no-fork --filter-test=test_function_name + * ``` + * Disabling forking makes it easy to debug but should always be used with + * `--filter-test`. Any test that modifies its environment will affect + * subsequent tests and may invalidate the test results. + */ + +/** + * \addtogroup pwtest + * \{ + */ + + +/** \struct pwtest_context */ +struct pwtest_context; +/** \struct pwtest_suite */ +struct pwtest_suite; +/** \struct pwtest_test */ +struct pwtest_test; + +#include "pwtest-implementation.h" + +/** + * Result returned from tests or suites. + */ +enum pwtest_result { + PWTEST_PASS = 75, /**< test successful */ + PWTEST_FAIL = 76, /**< test failed. Should not be returned directly, + Use the pwtest_ macros instead */ + PWTEST_SKIP = 77, /**< test was skipped */ + PWTEST_TIMEOUT = 78, /**< test aborted after timeout */ + PWTEST_SYSTEM_ERROR = 79, /**< unrelated error occured */ +}; + +/** + * If the test was added with a range (see \ref PWTEST_ARG_RANGE), this + * function returns the current iteration within that range. Otherwise, this + * function returns zero. + */ +int pwtest_get_iteration(struct pwtest_test *t); + +/** + * If the test had properties set (see \ref PWTEST_ARG_PROP), this function + * returns the \ref pw_properties. Otherwise, this function returns NULL. + */ +struct pw_properties *pwtest_get_props(struct pwtest_test *t); + +struct pwtest_context *pwtest_get_context(struct pwtest_test *t); + +/** Fail the current test */ +#define pwtest_fail() \ + _pwtest_fail_condition(PWTEST_FAIL, __FILE__, __LINE__, __func__, "aborting", "") + +/** Same as above but more expressive in the code */ +#define pwtest_fail_if_reached() \ + _pwtest_fail_condition(PWTEST_FAIL, __FILE__, __LINE__, __func__, "This line is supposed to be unreachable", "") + +/** Fail the current test with the given message */ +#define pwtest_fail_with_msg(...) \ + _pwtest_fail_condition(PWTEST_FAIL, __FILE__, __LINE__, __func__, \ + "aborting", __VA_ARGS__) + +/** Error out of the current test with the given message */ +#define pwtest_error_with_msg(...) \ + _pwtest_fail_condition(PWTEST_SYSTEM_ERROR, __FILE__, __LINE__, __func__, \ + "error", __VA_ARGS__) + +/** Assert boolean (a == b) */ +#define pwtest_bool_eq(a_, b_) \ + pwtest_comparison_bool_(a_, ==, b_) + +/** Assert boolean (a != b) */ +#define pwtest_bool_ne(a_, b_) \ + pwtest_comparison_bool_(a_, !=, b_) + +/** Assert cond to be true. Convenience wrapper for readability */ +#define pwtest_bool_true(cond_) \ + pwtest_comparison_bool_(cond_, ==, true) + +/** Assert cond to be false. Convenience wrapper for readability */ +#define pwtest_bool_false(cond_) \ + pwtest_comparison_bool_(cond_, ==, false) + +/** Assert a == b */ +#define pwtest_int_eq(a_, b_) \ + pwtest_comparison_int_(a_, ==, b_) + +/** Assert a != b */ +#define pwtest_int_ne(a_, b_) \ + pwtest_comparison_int_(a_, !=, b_) + +/** Assert a < b */ +#define pwtest_int_lt(a_, b_) \ + pwtest_comparison_int_(a_, <, b_) + +/** Assert a <= b */ +#define pwtest_int_le(a_, b_) \ + pwtest_comparison_int_(a_, <=, b_) + +/** Assert a >= b */ +#define pwtest_int_ge(a_, b_) \ + pwtest_comparison_int_(a_, >=, b_) + +/** Assert a > b */ +#define pwtest_int_gt(a_, b_) \ + pwtest_comparison_int_(a_, >, b_) + +/** Assert ptr1 == ptr2 */ +#define pwtest_ptr_eq(a_, b_) \ + pwtest_comparison_ptr_(a_, ==, b_) + +/** Assert ptr1 != ptr2 */ +#define pwtest_ptr_ne(a_, b_) \ + pwtest_comparison_ptr_(a_, !=, b_) + +/** Assert ptr == NULL */ +#define pwtest_ptr_null(a_) \ + pwtest_comparison_ptr_(a_, ==, NULL) + +/** Assert ptr != NULL */ +#define pwtest_ptr_notnull(a_) \ + pwtest_comparison_ptr_(a_, !=, NULL) + +/** Assert a == b for a (hardcoded) epsilon */ +#define pwtest_double_eq(a_, b_)\ + pwtest_comparison_double_((a_), ==, (b_)) + +/** Assert a != b for a (hardcoded) epsilon */ +#define pwtest_double_ne(a_, b_)\ + pwtest_comparison_double_((a_), !=, (b_)) + +/** Assert a < b for a (hardcoded) epsilon */ +#define pwtest_double_lt(a_, b_)\ + pwtest_comparison_double_((a_), <, (b_)) + +/** Assert a <= b for a (hardcoded) epsilon */ +#define pwtest_double_le(a_, b_)\ + pwtest_comparison_double_((a_), <=, (b_)) + +/** Assert a >= b for a (hardcoded) epsilon */ +#define pwtest_double_ge(a_, b_)\ + pwtest_comparison_double_((a_), >=, (b_)) + +/** Assert a > b for a (hardcoded) epsilon */ +#define pwtest_double_gt(a_, b_)\ + pwtest_comparison_double_((a_), >, (b_)) + +#define pwtest_int(a_, op_, b_) \ + pwtest_comparison_int_(a_, op_, b_) + + +/** Assert str1 is equal to str2 */ +#define pwtest_str_eq(a_, b_) \ + do { \ + const char *_a = a_; \ + const char *_b = b_; \ + if (!spa_streq(_a, _b)) \ + _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \ + #a_ " equals " #b_, _a, _b); \ + } while(0) + +/** Assert str1 is equal to str2 for l characters */ +#define pwtest_str_eq_n(a_, b_, l_) \ + do { \ + const char *_a = a_; \ + const char *_b = b_; \ + if (!spa_strneq(_a, _b, l_)) \ + _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \ + #a_ " equals " #b_ ", len: " #l_, _a, _b); \ + } while(0) + +/** Assert str1 is not equal to str2 */ +#define pwtest_str_ne(a_, b_) \ + do { \ + const char *_a = a_; \ + const char *_b = b_; \ + if (spa_streq(_a, _b)) \ + _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \ + #a_ " not equal to " #b_, _a, _b); \ + } while(0) + +/** Assert str1 is not equal to str2 for l characters */ +#define pwtest_str_ne_n(a_, b_, l_) \ + do { \ + __typeof__(a_) _a = a_; \ + __typeof__(b_) _b = b_; \ + if (spa_strneq(_a, _b, l_)) \ + _pwtest_fail_comparison_str(__FILE__, __LINE__, __func__, \ + #a_ " not equal to " #b_ ", len: " #l_, _a, _b); \ + } while(0) + + +/* Needs to be a #define NULL for SPA_SENTINEL */ +enum pwtest_arg { + PWTEST_NOARG = 0, + /** + * The next argument is an int specifying the numerical signal number. + * The test is expected to raise that signal. The test fails if none + * or any other signal is raised. + * + * Example: + * ```c + * pwtest_add(mytest, PWTEST_ARG_SIGNAL, SIGABRT); + * ``` + */ + PWTEST_ARG_SIGNAL, + /** + * The next two int arguments are the minimum (inclusive) and + * maximum (exclusive) range for this test. + * + * Example: + * ```c + * pwtest_add(mytest, PWTEST_ARG_RANGE, -50, 50); + * ``` + * Use pwtest_get_iteration() in the test function to obtain the current iteration. + */ + PWTEST_ARG_RANGE, + /** + * The next two const char * arguments are the key and value + * for a property entry. This argument may be specified multiple times + * to add multiple properties. + * + * Use pwtest_get_props() to get the properties within the test function. + * + * Example: + * ```c + * pwtest_add(mytest, + * PWTEST_ARG_PROP, "key1", "value1", + * PWTEST_ARG_PROP, "key2", "value2"); + * ``` + */ + PWTEST_ARG_PROP, + /** + * The next two const char * arguments are the key and value + * for the environment variable to be set in the test. This argument + * may be specified multiple times to add multiple environment + * variables. + * + * Example: + * ```c + * pwtest_add(mytest, + * PWTEST_ARG_ENV, "env1", "value1", + * PWTEST_ARG_ENV, "env2", "value2"); + * ``` + * + * These environment variables are only set for the test itself, a + * a pipewire daemon started with \ref PWTEST_ARG_DAEMON does not share + * those variables. + * + */ + PWTEST_ARG_ENV, + /** + * Takes no extra arguments. If provided, the test case will start a + * pipewire daemon and stop the daemon when finished. + * + * The `PIPEWIRE_REMOTE` environment variable will be set in the + * test to point to this daemon. + * + * Example: + * ```c + * pwtest_add(mytest, PWTEST_ARG_DAEMON); + * ``` + * + * Environment variables specified with \ref PWTEST_ARG_ENV are + * **not** available to the daemon, only to the test itself. + */ + PWTEST_ARG_DAEMON, +}; +/** + * Add function \a func_ to the current test suite. + * + * This macro should be used within PWTEST_SUITE() to register the test in that suite, for example: + * + * ```c + * PWTEST_SUITE(mysuite) + * { + * pwtest_add(test1, PWTEST_NOARG); + * pwtest_add(test2, PWTEST_ARG_DAEMON); + * pwtest_add(test3, PWTEST_ARG_RANGE, 0, 100, PWTEST_ARG_DAEMON); + * } + * + * ``` + * + * If the test matches the given filters and the suite is executed, the test + * will be executed with the parameters given to pwtest_add(). + * + * Arguments take a argument-dependent number of extra parameters, see + * see the \ref pwtest_arg documentation for details. + */ +#define pwtest_add(func_, ...) \ + _pwtest_add(ctx, suite, #func_, func_, __VA_ARGS__, NULL) + + +/** + * Declare a test case. To execute the test, add the test case name with pwtest_add(). + * + * This macro expands so each test has a struct \ref pwtest_test variable + * named `current_test` available. + * + * ```c + * PWTEST(mytest) + * { + * struct pwtest_test *t = current_test; + * + * ... do stuff ... + * + * return PWTEST_PASS; + * } + * + * PWTEST_SUITE(mysuite) + * { + * pwtest_add(mytest); + * + * return PWTEST_PASS; + * } + * ``` + */ +#define PWTEST(tname) \ + static enum pwtest_result tname(struct pwtest_test *current_test) + +/** + * Initialize a test suite. A test suite is a group of related + * tests that filters and other conditions may apply to. + * + * Test suites are automatically discovered at build-time. + */ +#define PWTEST_SUITE(cname) \ + static enum pwtest_result (cname##__setup)(struct pwtest_context *ctx, struct pwtest_suite *suite); \ + static const struct pwtest_suite_decl _test_suite \ + __attribute__((used)) \ + __attribute((section("pwtest_suite_section"))) = { \ + .name = #cname, \ + .setup = cname##__setup, \ + }; \ + static enum pwtest_result (cname##__setup)(struct pwtest_context *ctx, struct pwtest_suite *suite) + +/** + * \} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* PWTEST_H */ diff --git a/test/test-example.c b/test/test-example.c new file mode 100644 index 000000000..2739ccde9 --- /dev/null +++ b/test/test-example.c @@ -0,0 +1,241 @@ +/* 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 +#include + +#include + +#include "pwtest.h" + +/* The simplest test example (test passes) */ +PWTEST(successful_test) +{ + int x = 10, y = 20, z = 10; + bool t = true, f = false; + const char *a = "foo", *b = "bar", *c = "baz"; + + pwtest_int_lt(x, y); + pwtest_int_le(x, y); + pwtest_int_gt(y, x); + pwtest_int_ge(y, x); + pwtest_int_eq(x, z); + pwtest_int_ne(y, z); + + pwtest_bool_true(t); + pwtest_bool_false(f); + pwtest_bool_eq(t, !f); + pwtest_bool_ne(t, f); + + pwtest_str_eq(a, a); + pwtest_str_ne(a, b); + pwtest_str_eq_n(b, c, 2); + pwtest_str_ne_n(b, c, 3); + + return PWTEST_PASS; +} + +/* Demo failure of an integer comparison (test will fail) */ +PWTEST(failing_test_int) +{ + int x = 10, y = 20; + pwtest_int_gt(x, y); + return PWTEST_PASS; +} + +/* Demo failure of a bool comparison (test will fail) */ +PWTEST(failing_test_bool) +{ + bool oops = true; + pwtest_bool_false(oops); + return PWTEST_PASS; +} + +/* Demo failure of a string comparison (test will fail) */ +PWTEST(failing_test_string) +{ + const char *what = "yes"; + pwtest_str_eq(what, "no"); + return PWTEST_PASS; +} + +/* Demo custom failure (test will fail) */ +PWTEST(general_fail_test) +{ + /* pwtest_fail(); */ + pwtest_fail_with_msg("Some condition wasn't met"); + return PWTEST_PASS; +} + +/* Demo failure (test will fail) */ +PWTEST(failing_test_if_reached) +{ + pwtest_fail_if_reached(); + return PWTEST_SYSTEM_ERROR; +} + +/* Demo a system error (test will fail) */ +PWTEST(system_error_test) +{ + return PWTEST_SYSTEM_ERROR; +} + +/* Demo signal handling of SIGSEGV (test will fail) */ +PWTEST(catch_segfault_test) +{ + int *x = NULL; + *x = 20; + return PWTEST_PASS; +} + +/* Demo signal handling of abort (test will fail) */ +PWTEST(catch_abort_signal_test) +{ + abort(); + return PWTEST_PASS; +} + +/* Demo a timeout (test will fail with default timeout of 30) */ +PWTEST(timeout_test) +{ + /* run with --timeout=1 to make this less annoying */ + sleep(60); + return PWTEST_PASS; +} + +/* Demo a ranged test (test passes, skips the last 2) */ +PWTEST(ranged_test) +{ + struct pwtest_test *t = current_test; + int iteration = pwtest_get_iteration(t); + + pwtest_int_lt(iteration, 10); /* see pwtest_add */ + + printf("Ranged test iteration %d\n", iteration); + if (iteration >= 8) + return PWTEST_SKIP; + + return PWTEST_PASS; +} + +/* Demo the properties passed to tests (test passes) */ +PWTEST(property_test) +{ + struct pwtest_test *t = current_test; + struct pw_properties *p = pwtest_get_props(t); + + pwtest_ptr_notnull(p); + pwtest_str_eq(pw_properties_get(p, "myprop"), "somevalue"); + pwtest_str_eq(pw_properties_get(p, "prop2"), "other"); + + return PWTEST_PASS; +} + +/* Demo the environment passed to tests (test passes) */ +PWTEST(env_test) +{ + pwtest_str_eq(getenv("myenv"), "envval"); + pwtest_str_eq(getenv("env2"), "val"); + + /* Set by pwtest */ + pwtest_str_eq(getenv("PWTEST"), "1"); + + return PWTEST_PASS; +} + +/* Demo the environment passed to tests (test passes) */ +PWTEST(env_reset_test) +{ + /* If run after env_test even with --no-fork this test should + * succeed */ + pwtest_str_eq(getenv("myenv"), NULL); + pwtest_str_eq(getenv("env2"), NULL); + + return PWTEST_PASS; +} + +PWTEST(default_env_test) +{ + /* This one is set automatically */ + pwtest_str_eq(getenv("PWTEST"), "1"); + + return PWTEST_PASS; +} + +PWTEST(daemon_test) +{ + struct pw_context *ctx; + struct pw_core *core; + struct pw_loop *loop; + + pwtest_str_eq_n(getenv("PIPEWIRE_REMOTE"), "pwtest-pw-", 10); + + pw_init(0, NULL); + loop = pw_loop_new(NULL); + ctx = pw_context_new(loop, NULL, 0); + pwtest_ptr_notnull(ctx); + core = pw_context_connect(ctx, NULL, 0); + pwtest_ptr_notnull(core); + + pw_loop_iterate(loop, -1); + pw_core_disconnect(core); + pw_context_destroy(ctx); + pw_loop_destroy(loop); + + return PWTEST_PASS; +} + +PWTEST_SUITE(example_tests) +{ + pwtest_add(successful_test, PWTEST_NOARG); + pwtest_add(failing_test_int, PWTEST_NOARG); + pwtest_add(failing_test_bool, PWTEST_NOARG); + pwtest_add(failing_test_string, PWTEST_NOARG); + pwtest_add(failing_test_if_reached, PWTEST_NOARG); + pwtest_add(general_fail_test, PWTEST_NOARG); + pwtest_add(system_error_test, PWTEST_NOARG); + pwtest_add(catch_segfault_test, PWTEST_NOARG); + pwtest_add(catch_abort_signal_test, PWTEST_ARG_SIGNAL, SIGABRT); + pwtest_add(ranged_test, PWTEST_ARG_RANGE, 0, 10); + pwtest_add(property_test, + PWTEST_ARG_PROP, "myprop", "somevalue", + PWTEST_ARG_PROP, "prop2", "other"); + pwtest_add(env_test, + PWTEST_ARG_ENV, "env2", "val", + PWTEST_ARG_ENV, "myenv", "envval"); + pwtest_add(env_reset_test, PWTEST_NOARG); + pwtest_add(default_env_test, PWTEST_NOARG); + pwtest_add(daemon_test, PWTEST_ARG_DAEMON); + + /* Run this one last so it doesn't matter if we forget --timeout */ + pwtest_add(timeout_test, PWTEST_NOARG); + + return PWTEST_PASS; +}