libinput/test/litest-runner.c

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1136 lines
26 KiB
C
Raw Normal View History

/*
* Copyright © 2024 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 <errno.h>
#include <inttypes.h>
#include <sys/epoll.h>
#include <sys/sysinfo.h>
#include <sys/timerfd.h>
#include <sys/wait.h>
#ifdef HAVE_PIDFD_OPEN
#include <sys/syscall.h>
#endif
#include <fcntl.h>
#include <setjmp.h>
#include <signal.h>
#include <stdlib.h>
#include <valgrind/valgrind.h>
#include "util-files.h"
#include "util-list.h"
test: implement support for parametrizing tests litest supports ranged tests but they are not enough, doubly so with tests where we want to parametrize across multiple options. This patch adds support for just that, in clunky C style. The typical invocation for a test is by giving the test parameter a name, a number of values and then the values themselves: struct litest_parameters *params = litest_parameters_new("axis", 's', 2, "ABS_X", "ABS_Y", "enabled", 'b', '2', true, false, "number", 'u', '2', 10, 11, NULL); litest_add_parametrized(sometest, LITEST_ANY, LITEST_ANY, params); litest_parameters_unref(params); Currently supported are u (uint32), i (int32), d (double), b (bool), c (char) and s (string). In the test itself, the `test_env->params` variable is available and retrieval of the parameters works like this: const char *axis; uint32_t number; bool enabled; litest_test_param_fetch(test_env->params, "axis", &axis, "enabled", &enabled, "number", &number, NULL); Note that since this is an effectively internal test-suite only functionality we don't do type-checking here, it's assumed that if you write the code to pass parameters into a test you remember the type of said params when you write the test code. Because we don't have hashmaps or anything useful other than lists the implementation is a bit clunky: we copy the parameter into the test during litest_add_*, permutate it for our test list which gives us yet another linked list C struct, and finally copy the actual value into the test and test environment as it's executed. Not pretty, but it works. A few tests are switched as simple demonstration. The name of the test has the parameters with their names and values appended now, e.g.: "pointer:pointer_scroll_wheel_hires_send_only_lores:ms-surface-cover:axis:ABS_X" "pointer:pointer_motion_relative_min_decel:mouse-roccat:direction:NW" Filtering by parameters can be done via globs of their string representation: libinput-test-suite --filter-params="axis:ABS_*,enabled:true,number:10*" Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1109>
2024-12-22 00:06:19 +10:00
#include "util-multivalue.h"
#include "util-stringbuf.h"
#include "litest-runner.h"
static bool use_jmpbuf; /* only used for max_forks = 0 */
static jmp_buf jmpbuf;
/* musl doesn't have this one but it's not that important */
#ifndef HAVE_SIGABBREV_NP
#define sigabbrev_np(...) "???"
#endif
static struct litest_runner *global_runner = NULL;
enum litest_runner_logfds {
FD_STDOUT,
FD_STDERR,
FD_LOG,
FD_VALGRIND,
_FD_LAST,
};
struct litest_runner_test {
struct litest_runner_test_description desc;
struct list node;
enum litest_runner_result result;
int sig_or_errno;
struct stringbuf logs[_FD_LAST];
pid_t pid; /* the test's PID, if any */
int read_fds[_FD_LAST]; /* logging fds while the test is running */
int epollfd;
int pidfd;
int timerfd;
struct {
uint64_t start_millis;
uint64_t end_millis;
} times;
};
struct litest_runner {
size_t max_forks;
unsigned int timeout;
bool verbose;
bool use_colors;
bool exit_on_fail;
FILE *fp;
int terminating;
struct list tests; /* struct litest_runner_test */
struct list tests_running; /* struct litest_runner_test */
struct list tests_complete; /* struct litest_runner_test */
struct {
time_t start;
time_t end;
uint64_t start_millis;
} times;
struct {
litest_runner_global_setup_func_t setup;
litest_runner_global_teardown_func_t teardown;
void *userdata;
} global;
};
/**
* A global variable that the tests can use
* to write log data to. Defaults to stdout
* but if used by the tests it shows up as separate
* from stdout in the logs.
*/
int testlog_fd = STDOUT_FILENO;
static void
close_pipes(int fds[_FD_LAST])
{
for (int i = 0; i < _FD_LAST; i++) {
if (fds[i] != -1)
fsync(fds[i]);
xclose(&fds[i]);
}
}
static int
init_pipes(int read_fds[_FD_LAST], int write_fds[_FD_LAST])
{
int r;
int i;
int pipe_max_size = 4194304;
for (i = 0; i < _FD_LAST; i++) {
read_fds[i] = -1;
write_fds[i] = -1;
}
#ifdef __linux__
{
FILE *f;
f = fopen("/proc/sys/fs/pipe-max-size", "re");
if (f) {
if (fscanf(f, "%d", &r) == 1)
pipe_max_size = min(r, pipe_max_size);
fclose(f);
}
}
#endif
for (i = 0; i < _FD_LAST; i++) {
int pipe[2];
r = pipe2(pipe, O_CLOEXEC | O_NONBLOCK);
if (r < 0)
goto error;
read_fds[i] = pipe[0];
write_fds[i] = pipe[1];
#ifdef __linux__
/* Max pipe buffers, to avoid scrambling if reading lags.
* Can't use blocking write fds, since reading too slow
* then affects execution.
*/
fcntl(write_fds[i], F_SETPIPE_SZ, pipe_max_size);
#endif
}
return 0;
error:
r = -errno;
close_pipes(read_fds);
close_pipes(write_fds);
return r;
}
static const char *
litest_runner_result_as_str(enum litest_runner_result result)
{
switch (result) {
CASE_RETURN_STRING(LITEST_PASS);
CASE_RETURN_STRING(LITEST_NOT_APPLICABLE);
CASE_RETURN_STRING(LITEST_FAIL);
CASE_RETURN_STRING(LITEST_SYSTEM_ERROR);
CASE_RETURN_STRING(LITEST_TIMEOUT);
CASE_RETURN_STRING(LITEST_SKIP);
}
litest_abort_msg("Unknown result %d", result);
return NULL;
}
static void
litest_runner_test_close(struct litest_runner_test *t)
{
for (size_t i = 0; i < ARRAY_LENGTH(t->read_fds); i++) {
xclose(&t->read_fds[i]);
}
xclose(&t->epollfd);
xclose(&t->pidfd);
xclose(&t->timerfd);
}
static void
litest_runner_test_destroy(struct litest_runner_test *t)
{
list_remove(&t->node);
close_pipes(t->read_fds);
if (t->pid != 0) {
kill(t->pid, SIGTERM);
t->pid = 0;
}
litest_runner_test_close(t);
for (int i = 0; i < _FD_LAST; i++) {
stringbuf_reset(&t->logs[i]);
}
free(t);
}
static void
litest_runner_detach_tests(struct litest_runner *runner)
{
struct litest_runner_test *t;
list_for_each_safe(t, &runner->tests, node) {
t->pid = 0;
}
list_for_each_safe(t, &runner->tests_complete, node) {
t->pid = 0;
}
list_for_each_safe(t, &runner->tests_running, node) {
t->pid = 0;
}
}
void
litest_runner_destroy(struct litest_runner *runner)
{
struct litest_runner_test *t;
list_for_each_safe(t, &runner->tests, node) {
litest_runner_test_destroy(t);
}
list_for_each_safe(t, &runner->tests_complete, node) {
litest_runner_test_destroy(t);
}
list_for_each_safe(t, &runner->tests_running, node) {
litest_runner_test_destroy(t);
}
free(runner);
}
static enum litest_runner_result
litest_runner_test_run(const struct litest_runner_test_description *desc)
{
const struct litest_runner_test_env env = {
.rangeval = desc->rangeval,
test: implement support for parametrizing tests litest supports ranged tests but they are not enough, doubly so with tests where we want to parametrize across multiple options. This patch adds support for just that, in clunky C style. The typical invocation for a test is by giving the test parameter a name, a number of values and then the values themselves: struct litest_parameters *params = litest_parameters_new("axis", 's', 2, "ABS_X", "ABS_Y", "enabled", 'b', '2', true, false, "number", 'u', '2', 10, 11, NULL); litest_add_parametrized(sometest, LITEST_ANY, LITEST_ANY, params); litest_parameters_unref(params); Currently supported are u (uint32), i (int32), d (double), b (bool), c (char) and s (string). In the test itself, the `test_env->params` variable is available and retrieval of the parameters works like this: const char *axis; uint32_t number; bool enabled; litest_test_param_fetch(test_env->params, "axis", &axis, "enabled", &enabled, "number", &number, NULL); Note that since this is an effectively internal test-suite only functionality we don't do type-checking here, it's assumed that if you write the code to pass parameters into a test you remember the type of said params when you write the test code. Because we don't have hashmaps or anything useful other than lists the implementation is a bit clunky: we copy the parameter into the test during litest_add_*, permutate it for our test list which gives us yet another linked list C struct, and finally copy the actual value into the test and test environment as it's executed. Not pretty, but it works. A few tests are switched as simple demonstration. The name of the test has the parameters with their names and values appended now, e.g.: "pointer:pointer_scroll_wheel_hires_send_only_lores:ms-surface-cover:axis:ABS_X" "pointer:pointer_motion_relative_min_decel:mouse-roccat:direction:NW" Filtering by parameters can be done via globs of their string representation: libinput-test-suite --filter-params="axis:ABS_*,enabled:true,number:10*" Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1109>
2024-12-22 00:06:19 +10:00
.params = desc->params,
};
if (desc->setup)
desc->setup(desc);
enum litest_runner_result result = desc->func(&env);
if (desc->teardown)
desc->teardown(desc);
return result;
}
static void
sighandler_forked_child(int signal)
{
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = SIG_DFL;
sigaction(signal, &act, NULL);
/* abort() was probably called by litest_assert... which inserts
* the backtrace anyway - we only need to backtrace the other signals
*/
if (signal != SIGABRT)
litest_backtrace(NULL);
raise(signal);
}
static int
litest_runner_fork_test(struct litest_runner *runner, struct litest_runner_test *t)
{
pid_t pid;
struct sigaction act;
int write_fds[_FD_LAST];
int r;
r = init_pipes(t->read_fds, write_fds);
if (r < 0) {
return -1;
}
pid = fork();
if (pid < 0) {
int r = -errno;
close_pipes(t->read_fds);
close_pipes(write_fds);
return r;
}
if (pid > 0) { /* parent */
close_pipes(write_fds);
t->pid = pid;
return pid;
}
/* child */
close_pipes(t->read_fds);
/* Catch any crashers so we can insert a backtrace */
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sighandler_forked_child;
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);
litest_assert_errno_success(r);
setlinebuf(stderr);
r = dup2(write_fds[FD_STDOUT], STDOUT_FILENO);
litest_assert_errno_success(r);
setlinebuf(stdout);
/* For convenience in the tests, let this be a global variable. */
testlog_fd = write_fds[FD_LOG];
/* We're forked so we have a full copy of everything - but all we want
* to do is avoid valgrind complaining about memleaks in the child
* process. So let's cleanup everything, copy the one thing we
* care about and proceed to the exit */
struct litest_runner_test_description desc = t->desc;
litest_runner_detach_tests(runner);
litest_runner_destroy(runner);
/* Now run the actual test */
enum litest_runner_result result = litest_runner_test_run(&desc);
close_pipes(write_fds);
exit(result);
}
static char *
valgrind_logfile(pid_t pid)
{
const char *prefix = getenv("LITEST_VALGRIND_LOGDIR");
if (!prefix)
prefix = ".";
char *filename = strdup_printf("%s/valgrind.%d.log", prefix, pid);
litest_assert_ptr_notnull(filename);
return filename;
}
static void
collect_file(const char *filename, struct stringbuf *b)
{
int fd = open(filename, O_RDONLY);
if (fd == -1) {
_autofree_ char *msg =
strdup_printf("Failed to find '%s': %m", filename);
stringbuf_append_string(b, msg);
} else {
stringbuf_append_from_fd(b, fd, 0);
close(fd);
}
}
static bool
litest_runner_test_collect_child(struct litest_runner_test *t)
{
int r;
int status;
r = waitpid(t->pid, &status, WNOHANG);
if (r <= 0)
return false;
if (WIFEXITED(status)) {
t->result = WEXITSTATUS(status);
if (RUNNING_ON_VALGRIND && t->result == 3) {
char msg[64];
snprintf(msg,
sizeof(msg),
"valgrind exited with an error code, see logs\n");
stringbuf_append_string(&t->logs[FD_LOG], msg);
t->result = LITEST_SYSTEM_ERROR;
}
switch (t->result) {
case LITEST_PASS:
case LITEST_SKIP:
case LITEST_NOT_APPLICABLE:
case LITEST_FAIL:
case LITEST_TIMEOUT:
case LITEST_SYSTEM_ERROR:
break;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
case 0:
/* if a test execve's itself allow for the normal
* exit codes to map to the results */
t->result = LITEST_PASS;
break;
#pragma GCC diagnostic pop
default: {
char msg[64];
snprintf(msg,
sizeof(msg),
"Invalid test exit status %d",
t->result);
stringbuf_append_string(&t->logs[FD_LOG], msg);
t->result = LITEST_FAIL;
break;
}
}
} else {
if (WIFSIGNALED(status)) {
t->sig_or_errno = WTERMSIG(status);
t->result = (t->sig_or_errno == t->desc.args.signal)
? LITEST_PASS
: LITEST_FAIL;
} else {
t->result = LITEST_FAIL;
}
}
uint64_t now = 0;
now_in_us(&now);
t->times.end_millis = us2ms(now);
if (RUNNING_ON_VALGRIND) {
_autofree_ char *filename = valgrind_logfile(t->pid);
collect_file(filename, &t->logs[FD_VALGRIND]);
}
t->pid = 0;
return true;
}
static int
litest_runner_test_setup_monitoring(struct litest_runner *runner,
struct litest_runner_test *t)
{
int pidfd = -1, timerfd = -1, epollfd = -1;
struct epoll_event ev[10];
size_t nevents = 0;
int r = 0;
#ifdef HAVE_PIDFD_OPEN
pidfd = syscall(SYS_pidfd_open, t->pid, 0);
#else
errno = ENOSYS;
#endif
/* If we don't have pidfd, we use a timerfd to ping us every 200ms */
if (pidfd < 0 && errno == ENOSYS) {
pidfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
if (pidfd == -1) {
r = -errno;
goto error;
}
r = timerfd_settime(pidfd,
0,
&((struct itimerspec){
.it_interval.tv_nsec = 200 * 1000 * 1000,
.it_value.tv_nsec = 200 * 1000 * 1000,
}),
NULL);
if (r < 0) {
r = -errno;
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) {
r = -errno;
goto error;
}
timerfd_settime(timerfd,
0,
&((struct itimerspec){ .it_value.tv_sec = runner->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 = t->read_fds[FD_STDOUT] };
ev[nevents++] = (struct epoll_event){ .events = EPOLLIN,
.data.fd = t->read_fds[FD_STDERR] };
ev[nevents++] = (struct epoll_event){ .events = EPOLLIN,
.data.fd = t->read_fds[FD_LOG] };
ev[nevents++] = (struct epoll_event){ .events = EPOLLIN, .data.fd = timerfd };
for (size_t i = 0; i < nevents; i++) {
r = epoll_ctl(epollfd, EPOLL_CTL_ADD, ev[i].data.fd, &ev[i]);
if (r < 0) {
r = -errno;
goto error;
}
}
t->epollfd = epollfd;
t->pidfd = pidfd;
t->timerfd = timerfd;
r = 0;
error:
if (r) {
xclose(&pidfd);
xclose(&timerfd);
xclose(&epollfd);
}
return -r;
}
static int
litest_runner_test_check_status(struct litest_runner_test *t)
{
struct epoll_event e;
if (t->pid == 0) /* nofork case */
return 0;
while (true) {
/* FIXME: this is a busy wait ... */
int r = epoll_wait(t->epollfd, &e, 1, 50);
if (r == 0)
return -EAGAIN;
if (r == -1) {
goto error;
}
if (e.data.fd == t->pidfd) {
uint64_t buf;
int ignore = read(t->pidfd,
&buf,
sizeof(buf)); /* for timerfd fallback */
(void)ignore;
if (litest_runner_test_collect_child(t)) {
break;
}
} else if (e.data.fd == t->timerfd) {
/* SIGALARM so we get the backtrace */
kill(t->pid, SIGALRM);
t->result = LITEST_TIMEOUT;
waitpid(t->pid, NULL, 0);
t->pid = 0;
break;
} else {
for (int i = 0; i < _FD_LAST; i++) {
if (e.data.fd == t->read_fds[i]) {
stringbuf_append_from_fd(&t->logs[i],
e.data.fd,
1024);
}
}
}
};
errno = 0;
error:
return -errno;
}
static void
litest_runner_test_update_errno(struct litest_runner_test *t, int error)
{
if (error)
t->sig_or_errno = -error;
if (error == SIGTERM) {
char msg[64];
snprintf(msg, sizeof(msg), "litest: tests terminated by signal\n");
stringbuf_append_string(&t->logs[FD_LOG], msg);
t->result = LITEST_SYSTEM_ERROR;
}
for (size_t i = 0; i < ARRAY_LENGTH(t->read_fds); i++) {
stringbuf_append_from_fd(&t->logs[i], t->read_fds[i], 1024);
}
}
__attribute__((noreturn)) void
litest_runner_abort(void)
{
if (use_jmpbuf) {
longjmp(jmpbuf, SIGABRT);
} else {
abort();
}
}
static int
litest_runner_run_test(struct litest_runner *runner, struct litest_runner_test *t)
{
int r;
t->result = LITEST_SYSTEM_ERROR;
uint64_t now = 0;
now_in_us(&now);
t->times.start_millis = us2ms(now);
if (runner->max_forks == 0) {
if (use_jmpbuf && setjmp(jmpbuf) == 0) {
t->result = litest_runner_test_run(&t->desc);
} else {
t->result = LITEST_FAIL;
}
r = 0; /* -Wclobbered */
} else {
r = litest_runner_fork_test(runner, t);
if (r >= 0)
r = litest_runner_test_setup_monitoring(runner, t);
litest_runner_test_update_errno(t, -r);
}
if (r >= 0) {
list_remove(&t->node);
list_append(&runner->tests_running, &t->node);
}
return r;
}
static void
print_lines(FILE *fp, const char *log, const char *prefix)
{
size_t nlines = 0;
_autostrvfree_ char **lines = strv_from_string(log, "\n", &nlines);
for (size_t i = 0; i < nlines; i++) {
fprintf(fp, "%s%s\n", prefix, lines[i]);
}
}
test: implement support for parametrizing tests litest supports ranged tests but they are not enough, doubly so with tests where we want to parametrize across multiple options. This patch adds support for just that, in clunky C style. The typical invocation for a test is by giving the test parameter a name, a number of values and then the values themselves: struct litest_parameters *params = litest_parameters_new("axis", 's', 2, "ABS_X", "ABS_Y", "enabled", 'b', '2', true, false, "number", 'u', '2', 10, 11, NULL); litest_add_parametrized(sometest, LITEST_ANY, LITEST_ANY, params); litest_parameters_unref(params); Currently supported are u (uint32), i (int32), d (double), b (bool), c (char) and s (string). In the test itself, the `test_env->params` variable is available and retrieval of the parameters works like this: const char *axis; uint32_t number; bool enabled; litest_test_param_fetch(test_env->params, "axis", &axis, "enabled", &enabled, "number", &number, NULL); Note that since this is an effectively internal test-suite only functionality we don't do type-checking here, it's assumed that if you write the code to pass parameters into a test you remember the type of said params when you write the test code. Because we don't have hashmaps or anything useful other than lists the implementation is a bit clunky: we copy the parameter into the test during litest_add_*, permutate it for our test list which gives us yet another linked list C struct, and finally copy the actual value into the test and test environment as it's executed. Not pretty, but it works. A few tests are switched as simple demonstration. The name of the test has the parameters with their names and values appended now, e.g.: "pointer:pointer_scroll_wheel_hires_send_only_lores:ms-surface-cover:axis:ABS_X" "pointer:pointer_motion_relative_min_decel:mouse-roccat:direction:NW" Filtering by parameters can be done via globs of their string representation: libinput-test-suite --filter-params="axis:ABS_*,enabled:true,number:10*" Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1109>
2024-12-22 00:06:19 +10:00
void
_litest_test_param_fetch(const struct litest_test_parameters *params, ...)
{
struct litest_test_param *p;
const char *name;
va_list args;
va_start(args, params);
while ((name = va_arg(args, const char *))) {
bool found = false;
char type = (char)va_arg(args, int);
test: implement support for parametrizing tests litest supports ranged tests but they are not enough, doubly so with tests where we want to parametrize across multiple options. This patch adds support for just that, in clunky C style. The typical invocation for a test is by giving the test parameter a name, a number of values and then the values themselves: struct litest_parameters *params = litest_parameters_new("axis", 's', 2, "ABS_X", "ABS_Y", "enabled", 'b', '2', true, false, "number", 'u', '2', 10, 11, NULL); litest_add_parametrized(sometest, LITEST_ANY, LITEST_ANY, params); litest_parameters_unref(params); Currently supported are u (uint32), i (int32), d (double), b (bool), c (char) and s (string). In the test itself, the `test_env->params` variable is available and retrieval of the parameters works like this: const char *axis; uint32_t number; bool enabled; litest_test_param_fetch(test_env->params, "axis", &axis, "enabled", &enabled, "number", &number, NULL); Note that since this is an effectively internal test-suite only functionality we don't do type-checking here, it's assumed that if you write the code to pass parameters into a test you remember the type of said params when you write the test code. Because we don't have hashmaps or anything useful other than lists the implementation is a bit clunky: we copy the parameter into the test during litest_add_*, permutate it for our test list which gives us yet another linked list C struct, and finally copy the actual value into the test and test environment as it's executed. Not pretty, but it works. A few tests are switched as simple demonstration. The name of the test has the parameters with their names and values appended now, e.g.: "pointer:pointer_scroll_wheel_hires_send_only_lores:ms-surface-cover:axis:ABS_X" "pointer:pointer_motion_relative_min_decel:mouse-roccat:direction:NW" Filtering by parameters can be done via globs of their string representation: libinput-test-suite --filter-params="axis:ABS_*,enabled:true,number:10*" Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1109>
2024-12-22 00:06:19 +10:00
void **ptr = va_arg(args, void *);
list_for_each(p, &params->test_params, link) {
if (streq(p->name, name)) {
if (tolower(p->value.type) != tolower(type))
litest_abort_msg(
"Paramter type mismatch: parameter '%s' is of type %c",
p->name,
p->value.type);
test: implement support for parametrizing tests litest supports ranged tests but they are not enough, doubly so with tests where we want to parametrize across multiple options. This patch adds support for just that, in clunky C style. The typical invocation for a test is by giving the test parameter a name, a number of values and then the values themselves: struct litest_parameters *params = litest_parameters_new("axis", 's', 2, "ABS_X", "ABS_Y", "enabled", 'b', '2', true, false, "number", 'u', '2', 10, 11, NULL); litest_add_parametrized(sometest, LITEST_ANY, LITEST_ANY, params); litest_parameters_unref(params); Currently supported are u (uint32), i (int32), d (double), b (bool), c (char) and s (string). In the test itself, the `test_env->params` variable is available and retrieval of the parameters works like this: const char *axis; uint32_t number; bool enabled; litest_test_param_fetch(test_env->params, "axis", &axis, "enabled", &enabled, "number", &number, NULL); Note that since this is an effectively internal test-suite only functionality we don't do type-checking here, it's assumed that if you write the code to pass parameters into a test you remember the type of said params when you write the test code. Because we don't have hashmaps or anything useful other than lists the implementation is a bit clunky: we copy the parameter into the test during litest_add_*, permutate it for our test list which gives us yet another linked list C struct, and finally copy the actual value into the test and test environment as it's executed. Not pretty, but it works. A few tests are switched as simple demonstration. The name of the test has the parameters with their names and values appended now, e.g.: "pointer:pointer_scroll_wheel_hires_send_only_lores:ms-surface-cover:axis:ABS_X" "pointer:pointer_motion_relative_min_decel:mouse-roccat:direction:NW" Filtering by parameters can be done via globs of their string representation: libinput-test-suite --filter-params="axis:ABS_*,enabled:true,number:10*" Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1109>
2024-12-22 00:06:19 +10:00
found = true;
multivalue_extract(&p->value, ptr);
break;
}
}
if (!found)
litest_abort_msg("Unknown test parameter name '%s'", name);
}
va_end(args);
}
struct litest_test_parameters *
litest_test_parameters_new(void)
{
struct litest_test_parameters *params = zalloc(sizeof *params);
params->refcnt = 1;
list_init(&params->test_params);
return params;
}
struct litest_test_parameters *
litest_test_parameters_unref(struct litest_test_parameters *params)
{
if (params) {
assert(params->refcnt > 0);
if (--params->refcnt == 0) {
struct litest_test_param *p;
list_for_each_safe(p, &params->test_params, link) {
free(p);
}
free(params);
}
}
return NULL;
}
static void
litest_runner_log_test_result(struct litest_runner *runner,
struct litest_runner_test *t)
{
const char *color = NULL;
const char *status = NULL;
litest_assert_int_ge(t->result, (enum litest_runner_result)LITEST_PASS);
litest_assert_int_le(t->result, (enum litest_runner_result)LITEST_SYSTEM_ERROR);
switch (t->result) {
case LITEST_PASS:
color = ANSI_BOLD_GREEN;
break;
case LITEST_FAIL:
color = ANSI_BOLD_RED;
break;
case LITEST_SKIP:
color = ANSI_BOLD_YELLOW;
break;
case LITEST_NOT_APPLICABLE:
color = ANSI_BLUE;
break;
case LITEST_TIMEOUT:
color = ANSI_BOLD_CYAN;
break;
case LITEST_SYSTEM_ERROR:
color = ANSI_BOLD_MAGENTA;
break;
}
fprintf(runner->fp, " - name: \"%s\"\n", t->desc.name);
int min = t->desc.args.range.lower, max = t->desc.args.range.upper;
if (range_is_valid(&t->desc.args.range))
fprintf(runner->fp,
" rangeval: %d # %d..%d\n",
t->desc.rangeval,
min,
max);
test: implement support for parametrizing tests litest supports ranged tests but they are not enough, doubly so with tests where we want to parametrize across multiple options. This patch adds support for just that, in clunky C style. The typical invocation for a test is by giving the test parameter a name, a number of values and then the values themselves: struct litest_parameters *params = litest_parameters_new("axis", 's', 2, "ABS_X", "ABS_Y", "enabled", 'b', '2', true, false, "number", 'u', '2', 10, 11, NULL); litest_add_parametrized(sometest, LITEST_ANY, LITEST_ANY, params); litest_parameters_unref(params); Currently supported are u (uint32), i (int32), d (double), b (bool), c (char) and s (string). In the test itself, the `test_env->params` variable is available and retrieval of the parameters works like this: const char *axis; uint32_t number; bool enabled; litest_test_param_fetch(test_env->params, "axis", &axis, "enabled", &enabled, "number", &number, NULL); Note that since this is an effectively internal test-suite only functionality we don't do type-checking here, it's assumed that if you write the code to pass parameters into a test you remember the type of said params when you write the test code. Because we don't have hashmaps or anything useful other than lists the implementation is a bit clunky: we copy the parameter into the test during litest_add_*, permutate it for our test list which gives us yet another linked list C struct, and finally copy the actual value into the test and test environment as it's executed. Not pretty, but it works. A few tests are switched as simple demonstration. The name of the test has the parameters with their names and values appended now, e.g.: "pointer:pointer_scroll_wheel_hires_send_only_lores:ms-surface-cover:axis:ABS_X" "pointer:pointer_motion_relative_min_decel:mouse-roccat:direction:NW" Filtering by parameters can be done via globs of their string representation: libinput-test-suite --filter-params="axis:ABS_*,enabled:true,number:10*" Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1109>
2024-12-22 00:06:19 +10:00
if (t->desc.params) {
fprintf(runner->fp, " params:\n");
struct litest_test_param *p;
list_for_each(p, &t->desc.params->test_params, link) {
_autofree_ char *val = multivalue_as_str(&p->value);
test: implement support for parametrizing tests litest supports ranged tests but they are not enough, doubly so with tests where we want to parametrize across multiple options. This patch adds support for just that, in clunky C style. The typical invocation for a test is by giving the test parameter a name, a number of values and then the values themselves: struct litest_parameters *params = litest_parameters_new("axis", 's', 2, "ABS_X", "ABS_Y", "enabled", 'b', '2', true, false, "number", 'u', '2', 10, 11, NULL); litest_add_parametrized(sometest, LITEST_ANY, LITEST_ANY, params); litest_parameters_unref(params); Currently supported are u (uint32), i (int32), d (double), b (bool), c (char) and s (string). In the test itself, the `test_env->params` variable is available and retrieval of the parameters works like this: const char *axis; uint32_t number; bool enabled; litest_test_param_fetch(test_env->params, "axis", &axis, "enabled", &enabled, "number", &number, NULL); Note that since this is an effectively internal test-suite only functionality we don't do type-checking here, it's assumed that if you write the code to pass parameters into a test you remember the type of said params when you write the test code. Because we don't have hashmaps or anything useful other than lists the implementation is a bit clunky: we copy the parameter into the test during litest_add_*, permutate it for our test list which gives us yet another linked list C struct, and finally copy the actual value into the test and test environment as it's executed. Not pretty, but it works. A few tests are switched as simple demonstration. The name of the test has the parameters with their names and values appended now, e.g.: "pointer:pointer_scroll_wheel_hires_send_only_lores:ms-surface-cover:axis:ABS_X" "pointer:pointer_motion_relative_min_decel:mouse-roccat:direction:NW" Filtering by parameters can be done via globs of their string representation: libinput-test-suite --filter-params="axis:ABS_*,enabled:true,number:10*" Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1109>
2024-12-22 00:06:19 +10:00
fprintf(runner->fp, " %s: %s\n", p->name, val);
}
}
fprintf(runner->fp,
" duration: %" PRIu64 " # (ms), total test run time: %02" PRIu32
":%02" PRIu32 "\n",
t->times.end_millis - t->times.start_millis,
(ms2s(t->times.end_millis - runner->times.start_millis)) / 60,
(ms2s(t->times.end_millis - runner->times.start_millis)) % 60);
status = litest_runner_result_as_str(t->result);
fprintf(runner->fp,
" status: %s%s%s\n",
runner->use_colors ? color : "",
&status[7], /* skip LITEST_ prefix */
runner->use_colors ? ANSI_NORMAL : "");
switch (t->result) {
case LITEST_PASS:
case LITEST_SKIP:
case LITEST_NOT_APPLICABLE:
if (!runner->verbose)
return;
break;
default:
break;
}
if (t->sig_or_errno > 0)
fprintf(runner->fp,
" signal: %d # SIG%s \n",
t->sig_or_errno,
sigabbrev_np(t->sig_or_errno));
else if (t->sig_or_errno < 0)
fprintf(runner->fp,
" errno: %d # %s\n",
-t->sig_or_errno,
strerror(-t->sig_or_errno));
if (!stringbuf_is_empty(&t->logs[FD_LOG])) {
fprintf(runner->fp, " log: |\n");
print_lines(runner->fp, t->logs[FD_LOG].data, " ");
}
if (!stringbuf_is_empty(&t->logs[FD_STDOUT])) {
fprintf(runner->fp, " stdout: |\n");
print_lines(runner->fp, t->logs[FD_STDOUT].data, " ");
}
if (!stringbuf_is_empty(&t->logs[FD_STDERR])) {
fprintf(runner->fp, " stderr: |\n");
print_lines(runner->fp, t->logs[FD_STDERR].data, " ");
}
if (!stringbuf_is_empty(&t->logs[FD_VALGRIND])) {
fprintf(runner->fp, " valgrind: |\n");
print_lines(runner->fp, t->logs[FD_VALGRIND].data, " ");
}
}
struct litest_runner *
litest_runner_new(void)
{
struct litest_runner *runner = zalloc(sizeof *runner);
list_init(&runner->tests);
list_init(&runner->tests_complete);
list_init(&runner->tests_running);
runner->timeout = LITEST_RUNNER_DEFAULT_TIMEOUT;
runner->max_forks = get_nprocs() * 2;
runner->fp = stderr;
return runner;
}
void
litest_runner_set_timeout(struct litest_runner *runner, unsigned int timeout)
{
runner->timeout = timeout;
}
void
litest_runner_set_output_file(struct litest_runner *runner, FILE *fp)
{
setlinebuf(fp);
runner->fp = fp;
}
void
litest_runner_set_num_parallel(struct litest_runner *runner, size_t num_jobs)
{
runner->max_forks = num_jobs;
}
void
litest_runner_set_verbose(struct litest_runner *runner, bool verbose)
{
runner->verbose = verbose;
}
void
litest_runner_set_use_colors(struct litest_runner *runner, bool use_colors)
{
runner->use_colors = use_colors;
}
void
litest_runner_set_exit_on_fail(struct litest_runner *runner, bool do_exit)
{
runner->exit_on_fail = do_exit;
}
void
litest_runner_set_setup_funcs(struct litest_runner *runner,
litest_runner_global_setup_func_t setup,
litest_runner_global_teardown_func_t teardown,
void *userdata)
{
runner->global.setup = setup;
runner->global.teardown = teardown;
runner->global.userdata = userdata;
}
void
litest_runner_add_test(struct litest_runner *runner,
const struct litest_runner_test_description *desc)
{
struct litest_runner_test *t = zalloc(sizeof(*t));
t->desc = *desc;
t->epollfd = -1;
t->pidfd = -1;
t->timerfd = -1;
for (int i = 0; i < _FD_LAST; i++) {
stringbuf_init(&t->logs[i]);
}
for (size_t i = 0; i < ARRAY_LENGTH(t->read_fds); i++) {
t->read_fds[i] = -1;
}
list_append(&runner->tests, &t->node);
}
static int
litest_runner_check_finished_tests(struct litest_runner *runner)
{
struct litest_runner_test *running;
size_t count = 0;
list_for_each_safe(running, &runner->tests_running, node) {
int r = litest_runner_test_check_status(running);
if (r == -EAGAIN)
continue;
uint64_t now = 0;
now_in_us(&now);
running->times.end_millis = us2ms(now);
if (r < 0)
litest_runner_test_update_errno(running, -r);
litest_runner_log_test_result(runner, running);
litest_runner_test_close(running);
list_remove(&running->node);
list_append(&runner->tests_complete, &running->node);
count++;
}
return count;
}
static void
runner_sighandler(int sig)
{
struct litest_runner_test *t;
struct litest_runner *runner = global_runner;
list_for_each(t, &runner->tests_running, node) {
if (t->pid != 0) {
kill(t->pid, SIGTERM);
t->pid = 0;
}
}
global_runner->terminating = true;
}
static inline void
setup_sighandler(int sig)
{
struct sigaction act, oact;
int rc;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, sig);
act.sa_flags = 0;
act.sa_handler = runner_sighandler;
rc = sigaction(sig, &act, &oact);
litest_assert_int_ne(rc, -1);
}
enum litest_runner_result
litest_runner_run_tests(struct litest_runner *runner)
{
struct litest_runner_test *t;
size_t available_jobs = max(runner->max_forks, 1);
char timestamp[64];
struct tm *ltime;
global_runner = runner; /* sigh, need this for signal handling */
if (runner->global.setup)
runner->global.setup(runner->global.userdata);
use_jmpbuf = runner->max_forks == 0;
setup_sighandler(SIGINT);
uint64_t now = 0;
now_in_us(&now);
runner->times.start_millis = us2ms(now);
runner->times.start = time(NULL);
ltime = localtime(&runner->times.start);
strftime(timestamp, sizeof(timestamp), "%FT%H:%M", ltime);
fprintf(runner->fp,
"start: %lld # \"%s\"\n",
(long long)runner->times.start,
timestamp);
fprintf(runner->fp, "jobs: %zd\n", runner->max_forks);
fprintf(runner->fp, "tests:\n");
list_for_each_safe(t, &runner->tests, node) {
int r = litest_runner_run_test(runner, t);
if (r >= 0) {
available_jobs--;
}
/* Wait for something to become available */
while (available_jobs == 0 && !runner->terminating) {
int complete = litest_runner_check_finished_tests(runner);
available_jobs += complete;
}
if (runner->terminating) {
break;
}
if (runner->exit_on_fail) {
bool do_exit = false;
struct litest_runner_test *complete;
list_for_each(complete, &runner->tests_complete, node) {
switch (complete->result) {
case LITEST_FAIL:
case LITEST_SYSTEM_ERROR:
case LITEST_TIMEOUT:
do_exit = true;
break;
default:
break;
}
if (do_exit)
break;
}
if (do_exit)
break;
}
}
while (!runner->terminating && !list_empty(&runner->tests_running)) {
litest_runner_check_finished_tests(runner);
}
if (runner->global.teardown)
runner->global.teardown(runner->global.userdata);
size_t npass = 0, nfail = 0, nskip = 0, nna = 0;
size_t ncomplete = 0;
list_for_each(t, &runner->tests_complete, node) {
ncomplete++;
switch (t->result) {
case LITEST_PASS:
npass++;
break;
case LITEST_NOT_APPLICABLE:
nna++;
break;
case LITEST_FAIL:
case LITEST_SYSTEM_ERROR:
case LITEST_TIMEOUT:
nfail++;
break;
case LITEST_SKIP:
nskip++;
break;
}
}
runner->times.end = time(NULL);
ltime = localtime(&runner->times.end);
strftime(timestamp, sizeof(timestamp), "%FT%H:%M", ltime);
fprintf(runner->fp,
"end: %lld # \"%s\"\n",
(long long)runner->times.end,
timestamp);
fprintf(runner->fp,
"duration: %lld # (s) %02lld:%02lld\n",
(long long)(runner->times.end - runner->times.start),
(long long)((runner->times.end - runner->times.start) / 60),
(long long)((runner->times.end - runner->times.start) % 60));
fprintf(runner->fp, "summary:\n");
fprintf(runner->fp, " completed: %zd\n", ncomplete);
fprintf(runner->fp, " pass: %zd\n", npass);
fprintf(runner->fp, " na: %zd\n", nna);
fprintf(runner->fp, " fail: %zd\n", nfail);
fprintf(runner->fp, " skip: %zd\n", nskip);
if (nfail > 0) {
fprintf(runner->fp, " failed:\n");
list_for_each(t, &runner->tests_complete, node) {
switch (t->result) {
case LITEST_FAIL:
case LITEST_SYSTEM_ERROR:
case LITEST_TIMEOUT:
litest_runner_log_test_result(runner, t);
break;
default:
break;
}
}
}
if (RUNNING_ON_VALGRIND) {
_autofree_ char *filename = valgrind_logfile(getpid());
_destroy_(stringbuf) *b = stringbuf_new();
collect_file(filename, b);
fprintf(runner->fp, "valgrind:\n");
print_lines(runner->fp, b->data, " ");
fprintf(runner->fp,
"# Valgrind log is incomplete, see %s for full log\n",
filename);
}
enum litest_runner_result result = LITEST_PASS;
/* Didn't finish */
if (!list_empty(&runner->tests) || !list_empty(&runner->tests_running)) {
result = LITEST_SYSTEM_ERROR;
} else {
list_for_each(t, &runner->tests_complete, node) {
switch (t->result) {
case LITEST_PASS:
case LITEST_SKIP:
case LITEST_NOT_APPLICABLE:
break;
default:
result = LITEST_FAIL;
break;
}
}
}
/* Status is always prefixed with LITEST_ */
fprintf(runner->fp, " status: %s\n", &litest_runner_result_as_str(result)[7]);
return result;
}