// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * Copyright (C) 2012-2013  ProFUSION embedded systems
 */

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <regex.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/wait.h>

#include <shared/missing.h>
#include <shared/util.h>

#include "testsuite.h"

static const char *ANSI_HIGHLIGHT_GREEN_ON = "\x1B[1;32m";
static const char *ANSI_HIGHLIGHT_YELLOW_ON = "\x1B[1;33m";
static const char *ANSI_HIGHLIGHT_RED_ON = "\x1B[1;31m";
static const char *ANSI_HIGHLIGHT_OFF = "\x1B[0m";

static const char *progname;
static int oneshot = 0;
static const char options_short[] = "lhn";
static const struct option options[] = {
	{ "list", no_argument, 0, 'l' },
	{ "help", no_argument, 0, 'h' },
	{ NULL, 0, 0, 0 },
};

#define TEST_TIMEOUT_USEC 2 * USEC_PER_SEC

static const struct {
	const char *key;
	const char *ldpreload;
} env_config[_TC_LAST] = {
	[TC_UNAME_R] = { S_TC_UNAME_R, OVERRIDE_LIBDIR "uname.so" },
	[TC_ROOTFS] = { S_TC_ROOTFS, OVERRIDE_LIBDIR "path.so" },
	[TC_INIT_MODULE_RETCODES] = { S_TC_INIT_MODULE_RETCODES,
				      OVERRIDE_LIBDIR "init_module.so" },
	[TC_DELETE_MODULE_RETCODES] = { S_TC_DELETE_MODULE_RETCODES,
					OVERRIDE_LIBDIR "delete_module.so" },
};

static void help(void)
{
	const struct option *itr;
	const char *itr_short;

	printf("Usage:\n"
	       "\t%s [options] <test>\n"
	       "Options:\n",
	       basename(progname));

	for (itr = options, itr_short = options_short; itr->name != NULL;
	     itr++, itr_short++)
		printf("\t-%c, --%s\n", *itr_short, itr->name);
}

static void test_list(const struct test *start, const struct test *stop)
{
	const struct test *t;

	printf("Available tests:\n");
	for (t = start; t < stop; t++)
		printf("\t%s, %s\n", t->name, t->description);
}

int test_init(const struct test *start, const struct test *stop, int argc,
	      char *const argv[])
{
	progname = argv[0];

	for (;;) {
		int c, idx = 0;
		c = getopt_long(argc, argv, options_short, options, &idx);
		if (c == -1)
			break;
		switch (c) {
		case 'l':
			test_list(start, stop);
			return 0;
		case 'h':
			help();
			return 0;
		case 'n':
			oneshot = 1;
			break;
		case '?':
			return -1;
		default:
			ERR("unexpected getopt_long() value %c\n", c);
			return -1;
		}
	}

	if (isatty(STDOUT_FILENO) == 0) {
		ANSI_HIGHLIGHT_OFF = "";
		ANSI_HIGHLIGHT_RED_ON = "";
		ANSI_HIGHLIGHT_GREEN_ON = "";
	}

	return optind;
}

const struct test *test_find(const struct test *start, const struct test *stop,
			     const char *name)
{
	const struct test *t;

	for (t = start; t < stop; t++) {
		if (streq(t->name, name))
			return t;
	}

	return NULL;
}

static int test_spawn_test(const struct test *t)
{
	const char *const args[] = { progname, "-n", t->name, NULL };

	execv(progname, (char *const *)args);

	ERR("failed to spawn %s for %s: %m\n", progname, t->name);
	return EXIT_FAILURE;
}

static int test_run_spawned(const struct test *t)
{
	int err = t->func(t);
	exit(err);

	return EXIT_FAILURE;
}

int test_spawn_prog(const char *prog, const char *const args[])
{
	execv(prog, (char *const *)args);

	ERR("failed to spawn %s\n", prog);
	ERR("did you forget to build tools?\n");
	return EXIT_FAILURE;
}

static void test_export_environ(const struct test *t)
{
	char *preload = NULL;
	size_t preloadlen = 0;
	size_t i;
	const struct keyval *env;

	for (i = 0; i < _TC_LAST; i++) {
		const char *ldpreload;
		size_t ldpreloadlen;
		char *tmp;

		if (t->config[i] == NULL)
			continue;

		setenv(env_config[i].key, t->config[i], 1);

		ldpreload = env_config[i].ldpreload;
		ldpreloadlen = strlen(ldpreload);
		tmp = realloc(preload, preloadlen + 2 + ldpreloadlen);
		if (tmp == NULL) {
			ERR("oom: test_export_environ()\n");
			return;
		}
		preload = tmp;

		if (preloadlen > 0)
			preload[preloadlen++] = ' ';
		memcpy(preload + preloadlen, ldpreload, ldpreloadlen);
		preloadlen += ldpreloadlen;
		preload[preloadlen] = '\0';
	}

	if (preload != NULL) {
		const char *existing_preload = getenv("LD_PRELOAD");
		if (existing_preload) {
			char *tmp;
			size_t len = strlen(existing_preload);

			tmp = malloc(preloadlen + 2 + len);
			if (tmp == NULL) {
				ERR("oom: test_export_environ()\n");
				return;
			}
			memcpy(tmp, existing_preload, len);
			tmp[len++] = ' ';
			memcpy(tmp + len, preload, preloadlen);
			preloadlen += len;
			tmp[preloadlen] = '\0';
			free(preload);
			preload = tmp;
		}
		setenv("LD_PRELOAD", preload, 1);
	}

	free(preload);

	for (env = t->env_vars; env && env->key; env++)
		setenv(env->key, env->val, 1);
}

static inline int test_run_child(const struct test *t, int fdout[2], int fderr[2],
				 int fdmonitor[2])
{
	/* kill child if parent dies */
	prctl(PR_SET_PDEATHSIG, SIGTERM);

	test_export_environ(t);

	/* Close read-fds and redirect std{out,err} to the write-fds */
	if (t->output.out != NULL) {
		close(fdout[0]);
		if (dup2(fdout[1], STDOUT_FILENO) < 0) {
			ERR("could not redirect stdout to pipe: %m\n");
			exit(EXIT_FAILURE);
		}
	}

	if (t->output.err != NULL) {
		close(fderr[0]);
		if (dup2(fderr[1], STDERR_FILENO) < 0) {
			ERR("could not redirect stderr to pipe: %m\n");
			exit(EXIT_FAILURE);
		}
	}

	close(fdmonitor[0]);

	if (t->config[TC_ROOTFS] != NULL) {
		const char *stamp = TESTSUITE_ROOTFS "../stamp-rootfs";
		const char *rootfs = t->config[TC_ROOTFS];
		struct stat rootfsst, stampst;

		if (stat(stamp, &stampst) != 0) {
			ERR("could not stat %s\n - %m", stamp);
			exit(EXIT_FAILURE);
		}

		if (stat(rootfs, &rootfsst) != 0) {
			ERR("could not stat %s\n - %m", rootfs);
			exit(EXIT_FAILURE);
		}

		if (stat_mstamp(&rootfsst) > stat_mstamp(&stampst)) {
			ERR("rootfs %s is dirty, please run 'meson compile testsuite/stamp-rootfs' before running this test\n",
			    rootfs);
			exit(EXIT_FAILURE);
		}
	}

	if (t->need_spawn)
		return test_spawn_test(t);
	else
		return test_run_spawned(t);
}

#define BUFSZ 4096

enum fd_cmp_type {
	FD_CMP_MONITOR,
	FD_CMP_OUT,
	FD_CMP_ERR,
	FD_CMP_MAX = FD_CMP_ERR,
};

struct fd_cmp {
	enum fd_cmp_type type;
	int fd;
	int fd_match;
	bool activity;
	const char *path;
	const char *name;
	char buf[BUFSZ];
	char buf_match[BUFSZ];
	unsigned int head;
	unsigned int head_match;
};

static int fd_cmp_check_activity(struct fd_cmp *fd_cmp)
{
	struct stat st;

	/* not monitoring or monitoring and it has activity */
	if (fd_cmp == NULL || fd_cmp->fd < 0 || fd_cmp->activity)
		return 0;

	/* monitoring, there was no activity and size matches */
	if (stat(fd_cmp->path, &st) == 0 && st.st_size == 0)
		return 0;

	ERR("Expecting output on %s, but test didn't produce any\n", fd_cmp->name);

	return -1;
}

static bool fd_cmp_is_active(struct fd_cmp *fd_cmp)
{
	return fd_cmp->fd != -1;
}

static int fd_cmp_open_monitor(struct fd_cmp *fd_cmp, int fd, int fd_ep)
{
	struct epoll_event ep = {};

	ep.events = EPOLLHUP;
	ep.data.ptr = fd_cmp;
	if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fd, &ep) < 0) {
		ERR("could not add monitor fd to epoll: %m\n");
		return -errno;
	}

	return 0;
}

static int fd_cmp_open_std(struct fd_cmp *fd_cmp, const char *fn, int fd, int fd_ep)
{
	struct epoll_event ep = {};
	int fd_match;

	fd_match = open(fn, O_RDONLY);
	if (fd_match < 0) {
		ERR("could not open %s for read: %m\n", fn);
		return -errno;
	}
	ep.events = EPOLLIN;
	ep.data.ptr = fd_cmp;
	if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fd, &ep) < 0) {
		ERR("could not add fd to epoll: %m\n");
		close(fd_match);
		return -errno;
	}

	return fd_match;
}

/* opens output file AND adds descriptor to epoll */
static int fd_cmp_open(struct fd_cmp **fd_cmp_out, enum fd_cmp_type type, const char *fn,
		       int fd, int fd_ep)
{
	int err = 0;
	struct fd_cmp *fd_cmp;

	fd_cmp = calloc(1, sizeof(*fd_cmp));
	if (fd_cmp == NULL) {
		ERR("could not allocate fd_cmp\n");
		return -ENOMEM;
	}

	switch (type) {
	case FD_CMP_MONITOR:
		err = fd_cmp_open_monitor(fd_cmp, fd, fd_ep);
		break;
	case FD_CMP_OUT:
		fd_cmp->name = "STDOUT";
		err = fd_cmp_open_std(fd_cmp, fn, fd, fd_ep);
		break;
	case FD_CMP_ERR:
		fd_cmp->name = "STDERR";
		err = fd_cmp_open_std(fd_cmp, fn, fd, fd_ep);
		break;
	default:
		ERR("unknown fd type %d\n", type);
		err = -1;
	}

	if (err < 0) {
		free(fd_cmp);
		return err;
	}

	fd_cmp->fd_match = err;
	fd_cmp->fd = fd;
	fd_cmp->type = type;
	fd_cmp->path = fn;

	*fd_cmp_out = fd_cmp;
	return 0;
}

static int fd_cmp_check_ev_in(struct fd_cmp *fd_cmp)
{
	if (fd_cmp->type == FD_CMP_MONITOR) {
		ERR("Unexpected activity on monitor pipe\n");
		return -EINVAL;
	}
	fd_cmp->activity = true;

	return 0;
}

static void fd_cmp_delete_ep(struct fd_cmp *fd_cmp, int fd_ep)
{
	if (epoll_ctl(fd_ep, EPOLL_CTL_DEL, fd_cmp->fd, NULL) < 0) {
		ERR("could not remove fd %d from epoll: %m\n", fd_cmp->fd);
	}
	fd_cmp->fd = -1;
}

static void fd_cmp_close(struct fd_cmp *fd_cmp)
{
	if (fd_cmp == NULL)
		return;

	if (fd_cmp->fd >= 0)
		close(fd_cmp->fd);
	free(fd_cmp);
}

static bool fd_cmp_regex_one(const char *pattern, const char *s)
{
	_cleanup_(regfree) regex_t re = {};

	return !regcomp(&re, pattern, REG_EXTENDED | REG_NOSUB) &&
	       !regexec(&re, s, 0, NULL, 0);
}

/*
 * read fd and fd_match, checking the first matches the regex of the second,
 * line by line
 */
static bool fd_cmp_regex(struct fd_cmp *fd_cmp, const struct test *t)
{
	char *p, *p_match;
	int done = 0, done_match = 0, r;

	if (fd_cmp->head >= sizeof(fd_cmp->buf)) {
		ERR("Read %zu bytes without a newline\n", sizeof(fd_cmp->buf));
		ERR("output: %.*s", (int)sizeof(fd_cmp->buf), fd_cmp->buf);
		return false;
	}

	r = read(fd_cmp->fd, fd_cmp->buf + fd_cmp->head,
		 sizeof(fd_cmp->buf) - fd_cmp->head);
	if (r <= 0)
		return true;

	fd_cmp->head += r;

	/*
	 * Process as many lines as read from fd and that fits in the buffer -
	 * it's assumed that if we get N lines from fd, we should be able to
	 * get the same amount from fd_match
	 */
	for (;;) {
		p = memchr(fd_cmp->buf + done, '\n', fd_cmp->head - done);
		if (!p)
			break;
		*p = '\0';

		p_match = memchr(fd_cmp->buf_match + done_match, '\n',
				 fd_cmp->head_match - done_match);
		if (!p_match) {
			if (fd_cmp->head_match >= sizeof(fd_cmp->buf_match)) {
				ERR("Read %zu bytes without a match\n",
				    sizeof(fd_cmp->buf_match));
				ERR("output: %.*s", (int)sizeof(fd_cmp->buf_match),
				    fd_cmp->buf_match);
				return false;
			}

			/* pump more data from file */
			r = read(fd_cmp->fd_match, fd_cmp->buf_match + fd_cmp->head_match,
				 sizeof(fd_cmp->buf_match) - fd_cmp->head_match);
			if (r <= 0) {
				ERR("could not read match fd %d\n", fd_cmp->fd_match);
				return false;
			}
			fd_cmp->head_match += r;
			p_match = memchr(fd_cmp->buf_match + done_match, '\n',
					 fd_cmp->head_match - done_match);
			if (!p_match) {
				ERR("could not find match line from fd %d\n",
				    fd_cmp->fd_match);
				return false;
			}
		}
		*p_match = '\0';

		if (!fd_cmp_regex_one(fd_cmp->buf_match + done_match,
				      fd_cmp->buf + done)) {
			ERR("Output does not match pattern on %s:\n", fd_cmp->name);
			ERR("pattern: %s\n", fd_cmp->buf_match + done_match);
			ERR("output : %s\n", fd_cmp->buf + done);
			return false;
		}

		done = p - fd_cmp->buf + 1;
		done_match = p_match - fd_cmp->buf_match + 1;
	}

	/*
	 * Prepare for the next call: anything we processed we remove from the
	 * buffer by memmoving the remaining bytes up to the beginning
	 */
	if (done) {
		if (fd_cmp->head - done)
			memmove(fd_cmp->buf, fd_cmp->buf + done, fd_cmp->head - done);
		fd_cmp->head -= done;
	}

	if (done_match) {
		if (fd_cmp->head_match - done_match)
			memmove(fd_cmp->buf_match, fd_cmp->buf_match + done_match,
				fd_cmp->head_match - done_match);
		fd_cmp->head_match -= done_match;
	}

	return true;
}

/* read fd and fd_match, checking they match exactly */
static bool fd_cmp_exact(struct fd_cmp *fd_cmp, const struct test *t)
{
	int r, rmatch, done = 0;

	r = read(fd_cmp->fd, fd_cmp->buf, sizeof(fd_cmp->buf) - 1);
	if (r <= 0)
		/* try again later */
		return true;

	/* read as much data from fd_match as we read from fd */
	for (;;) {
		rmatch = read(fd_cmp->fd_match, fd_cmp->buf_match + done, r - done);
		if (rmatch == 0)
			break;

		if (rmatch < 0) {
			if (errno == EINTR)
				continue;
			ERR("could not read match fd %d\n", fd_cmp->fd_match);
			return false;
		}

		done += rmatch;
	}

	fd_cmp->buf[r] = '\0';
	fd_cmp->buf_match[r] = '\0';

	if (t->print_outputs)
		printf("%s: %s\n", fd_cmp->name, fd_cmp->buf);

	if (!streq(fd_cmp->buf, fd_cmp->buf_match)) {
		ERR("Outputs do not match on %s:\n", fd_cmp->name);
		ERR("correct:\n%s\n", fd_cmp->buf_match);
		ERR("wrong:\n%s\n", fd_cmp->buf);
		return false;
	}

	return true;
}

static bool test_run_parent_check_outputs(const struct test *t, int fdout, int fderr,
					  int fdmonitor, pid_t child)
{
	int err, fd_ep;
	unsigned long long end_usec, start_usec;
	struct fd_cmp *fd_cmp_out = NULL;
	struct fd_cmp *fd_cmp_err = NULL;
	struct fd_cmp *fd_cmp_monitor = NULL;
	int n_fd = 0;

	fd_ep = epoll_create1(EPOLL_CLOEXEC);
	if (fd_ep < 0) {
		ERR("could not create epoll fd: %m\n");
		return false;
	}

	if (t->output.out != NULL) {
		err = fd_cmp_open(&fd_cmp_out, FD_CMP_OUT, t->output.out, fdout, fd_ep);
		if (err < 0)
			goto out;
		n_fd++;
	}

	if (t->output.err != NULL) {
		err = fd_cmp_open(&fd_cmp_err, FD_CMP_ERR, t->output.err, fderr, fd_ep);
		if (err < 0)
			goto out;
		n_fd++;
	}

	err = fd_cmp_open(&fd_cmp_monitor, FD_CMP_MONITOR, NULL, fdmonitor, fd_ep);
	if (err < 0)
		goto out;
	n_fd++;

	start_usec = now_usec();
	end_usec = start_usec + TEST_TIMEOUT_USEC;

	for (err = 0; n_fd > 0;) {
		int fdcount, i, timeout;
		struct epoll_event ev[4];
		unsigned long long curr_usec = now_usec();

		if (curr_usec > end_usec)
			break;

		timeout = (end_usec - curr_usec) / USEC_PER_MSEC;
		fdcount = epoll_wait(fd_ep, ev, 4, timeout);
		if (fdcount < 0) {
			if (errno == EINTR)
				continue;
			err = -errno;
			ERR("could not poll: %m\n");
			goto out;
		}

		for (i = 0; i < fdcount; i++) {
			struct fd_cmp *fd_cmp = ev[i].data.ptr;
			bool ret;

			if (ev[i].events & EPOLLIN) {
				err = fd_cmp_check_ev_in(fd_cmp);
				if (err < 0)
					goto out;

				if (t->output.regex)
					ret = fd_cmp_regex(fd_cmp, t);
				else
					ret = fd_cmp_exact(fd_cmp, t);

				if (!ret) {
					err = -1;
					goto out;
				}
			} else if (ev[i].events & EPOLLHUP) {
				fd_cmp_delete_ep(fd_cmp, fd_ep);
				n_fd--;
			}
		}
	}

	err = fd_cmp_check_activity(fd_cmp_out);
	err |= fd_cmp_check_activity(fd_cmp_err);

	if (err == 0 && fd_cmp_is_active(fd_cmp_monitor)) {
		err = -EINVAL;
		ERR("Test '%s' timed out, killing %d\n", t->name, child);
		kill(child, SIGKILL);
	}

out:
	fd_cmp_close(fd_cmp_out);
	fd_cmp_close(fd_cmp_err);
	fd_cmp_close(fd_cmp_monitor);
	close(fd_ep);

	return err == 0;
}

static inline int safe_read(int fd, void *buf, size_t count)
{
	int r;

	while (1) {
		r = read(fd, buf, count);
		if (r == -1 && errno == EINTR)
			continue;
		break;
	}

	return r;
}

static bool check_generated_files(const struct test *t)
{
	const struct keyval *k;

	/* This is not meant to be a diff replacement, just stupidly check if
	 * the files match. Bear in mind they can be binary files */
	for (k = t->output.files; k && k->key; k++) {
		struct stat sta, stb;
		int fda = -1, fdb = -1;
		char bufa[4096];
		char bufb[4096];

		fda = open(k->key, O_RDONLY);
		if (fda < 0) {
			ERR("could not open %s\n - %m\n", k->key);
			goto fail;
		}

		fdb = open(k->val, O_RDONLY);
		if (fdb < 0) {
			ERR("could not open %s\n - %m\n", k->val);
			goto fail;
		}

		if (fstat(fda, &sta) != 0) {
			ERR("could not fstat %d %s\n - %m\n", fda, k->key);
			goto fail;
		}

		if (fstat(fdb, &stb) != 0) {
			ERR("could not fstat %d %s\n - %m\n", fdb, k->key);
			goto fail;
		}

		if (sta.st_size != stb.st_size) {
			ERR("sizes do not match %s %s\n", k->key, k->val);
			goto fail;
		}

		for (;;) {
			int r, done;

			r = safe_read(fda, bufa, sizeof(bufa));
			if (r < 0)
				goto fail;

			if (r == 0)
				/* size is already checked, go to next file */
				goto next;

			for (done = 0; done < r;) {
				int r2 = safe_read(fdb, bufb + done, r - done);

				if (r2 <= 0)
					goto fail;

				done += r2;
			}

			if (memcmp(bufa, bufb, r) != 0)
				goto fail;
		}

next:
		close(fda);
		close(fdb);
		continue;

fail:
		if (fda >= 0)
			close(fda);
		if (fdb >= 0)
			close(fdb);

		return false;
	}

	return true;
}

static int cmp_modnames(const void *m1, const void *m2)
{
	const char *s1 = *(char *const *)m1;
	const char *s2 = *(char *const *)m2;
	int i;

	for (i = 0; s1[i] || s2[i]; i++) {
		char c1 = s1[i], c2 = s2[i];
		if (c1 == '-')
			c1 = '_';
		if (c2 == '-')
			c2 = '_';
		if (c1 != c2)
			return c1 - c2;
	}
	return 0;
}

/*
 * Auxiliary function to store the module names in buf and return a list
 * of pointers to them.
 */
static const char **read_modules(const char *modules, char **buf, int *count)
{
	const char **res;
	int len;
	int i;
	char *p;

	*buf = strdup(modules);
	if (!*buf) {
		*count = -1;
		return NULL;
	}
	len = 1;
	for (p = *buf; *p; p++)
		if (*p == ',')
			len++;
	res = malloc(sizeof(char *) * len);
	if (!res) {
		perror("malloc");
		*count = -1;
		free(*buf);
		*buf = NULL;
		return NULL;
	}
	i = 0;
	res[i++] = *buf;
	for (p = *buf; i < len; p++)
		if (*p == ',') {
			*p = '\0';
			res[i++] = p + 1;
		}
	*count = len;
	return res;
}

/*
 * Store the expected module names in buf and return a list of pointers to
 * them.
 */
static const char **read_expected_modules(const struct test *t, char **buf, int *count)
{
	if (t->modules_loaded[0] == '\0') {
		*count = 0;
		*buf = NULL;
		return NULL;
	}
	return read_modules(t->modules_loaded, buf, count);
}

/*
 * Store the unexpected module names in buf and return a list of pointers to
 * them.
 */
static const char **read_unexpected_modules(const struct test *t, char **buf, int *count)
{
	if (t->modules_not_loaded[0] == '\0') {
		*count = 0;
		*buf = NULL;
		return NULL;
	}
	return read_modules(t->modules_not_loaded, buf, count);
}

static char **read_loaded_modules(const struct test *t, char **buf, int *count)
{
	char dirname[PATH_MAX];
	DIR *dir;
	struct dirent *dirent;
	int i;
	int len = 0, bufsz;
	char **res = NULL;
	char *p;
	const char *rootfs = t->config[TC_ROOTFS] ? t->config[TC_ROOTFS] : "";

	/* Store the entries in /sys/module to res */
	if (snprintf(dirname, sizeof(dirname), "%s/sys/module", rootfs) >=
	    (int)sizeof(dirname)) {
		ERR("rootfs path too long: %s\n", rootfs);
		*buf = NULL;
		len = -1;
		goto out;
	}
	dir = opendir(dirname);
	/* not an error, simply return empty list */
	if (!dir) {
		*buf = NULL;
		goto out;
	}
	bufsz = 0;
	while ((dirent = readdir(dir))) {
		if (dirent->d_name[0] == '.')
			continue;
		len++;
		bufsz += strlen(dirent->d_name) + 1;
	}
	res = malloc(sizeof(char *) * len);
	if (!res) {
		perror("malloc");
		len = -1;
		goto out_dir;
	}
	*buf = malloc(bufsz);
	if (!*buf) {
		perror("malloc");
		free(res);
		res = NULL;
		len = -1;
		goto out_dir;
	}
	rewinddir(dir);
	i = 0;
	p = *buf;
	while ((dirent = readdir(dir))) {
		int size;

		if (dirent->d_name[0] == '.')
			continue;
		size = strlen(dirent->d_name) + 1;
		memcpy(p, dirent->d_name, size);
		res[i++] = p;
		p += size;
	}
out_dir:
	closedir(dir);
out:
	*count = len;
	return res;
}

static int check_loaded_modules(const struct test *t)
{
	int l1, l2, i1, i2;
	const char **a1;
	char **a2;
	char *buf1, *buf2;
	int err = false;

	a1 = read_expected_modules(t, &buf1, &l1);
	if (l1 < 0)
		return err;
	a2 = read_loaded_modules(t, &buf2, &l2);
	if (l2 < 0)
		goto out_a1;
	if (a1 && l1)
		qsort(a1, l1, sizeof(char *), cmp_modnames);
	if (a2 && l2)
		qsort(a2, l2, sizeof(char *), cmp_modnames);
	i1 = i2 = 0;
	err = true;
	while (i1 < l1 || i2 < l2) {
		int cmp;

		if (i1 >= l1)
			cmp = 1;
		else if (i2 >= l2)
			cmp = -1;
		else
			cmp = cmp_modnames(&a1[i1], &a2[i2]);
		if (cmp == 0) {
			i1++;
			i2++;
		} else if (cmp < 0) {
			err = false;
			ERR("module %s not loaded\n", a1[i1]);
			i1++;
		} else {
			err = false;
			ERR("module %s is loaded but should not be \n", a2[i2]);
			i2++;
		}
	}
	free(a2);
	free(buf2);
out_a1:
	free(a1);
	free(buf1);
	return err;
}

static int check_not_loaded_modules(const struct test *t)
{
	int l1, l2, i1, i2;
	const char **a1;
	char **a2;
	char *buf1, *buf2;
	int err = false;

	a1 = read_unexpected_modules(t, &buf1, &l1);
	if (l1 < 0)
		return err;
	a2 = read_loaded_modules(t, &buf2, &l2);
	if (l2 < 0)
		goto out_a1;
	qsort(a1, l1, sizeof(char *), cmp_modnames);
	qsort(a2, l2, sizeof(char *), cmp_modnames);
	i1 = i2 = 0;
	err = true;
	while (i1 < l1 || i2 < l2) {
		int cmp;

		if (i1 >= l1)
			cmp = 1;
		else if (i2 >= l2)
			cmp = -1;
		else
			cmp = cmp_modnames(&a1[i1], &a2[i2]);
		if (cmp == 0) {
			err = false;
			ERR("module %s loaded\n", a1[i1]);
			i1++;
		} else if (cmp < 0) {
			i1++;
			i2++;
		} else {
			err = false;
			ERR("module %s is loaded but should not be\n", a2[i2]);
			i2++;
		}
	}
	free(a2);
	free(buf2);
out_a1:
	free(a1);
	free(buf1);
	return err;
}

static inline int test_run_parent(const struct test *t, int fdout[2], int fderr[2],
				  int fdmonitor[2], pid_t child)
{
	pid_t pid;
	int err;
	bool matchout, match_modules;

	if (t->skip) {
		LOG("%sSKIPPED%s: %s\n", ANSI_HIGHLIGHT_YELLOW_ON, ANSI_HIGHLIGHT_OFF,
		    t->name);
		err = EXIT_SUCCESS;
		goto exit;
	}

	/* Close write-fds */
	if (t->output.out != NULL)
		close(fdout[1]);
	if (t->output.err != NULL)
		close(fderr[1]);
	close(fdmonitor[1]);

	matchout =
		test_run_parent_check_outputs(t, fdout[0], fderr[0], fdmonitor[0], child);

	/*
	 * break pipe on the other end: either child already closed or we want
	 * to stop it
	 */
	if (t->output.out != NULL)
		close(fdout[0]);
	if (t->output.err != NULL)
		close(fderr[0]);
	close(fdmonitor[0]);

	do {
		pid = wait(&err);
		if (pid == -1) {
			ERR("error waitpid(): %m\n");
			err = EXIT_FAILURE;
			goto exit;
		}
	} while (!WIFEXITED(err) && !WIFSIGNALED(err));

	if (WIFEXITED(err)) {
		if (WEXITSTATUS(err) != 0)
			ERR("'%s' [%u] exited with return code %d\n", t->name, pid,
			    WEXITSTATUS(err));
		else
			LOG("'%s' [%u] exited with return code %d\n", t->name, pid,
			    WEXITSTATUS(err));
	} else if (WIFSIGNALED(err)) {
		ERR("'%s' [%u] terminated by signal %d (%s)\n", t->name, pid,
		    WTERMSIG(err), strsignal(WTERMSIG(err)));
		err = t->expected_fail ? EXIT_SUCCESS : EXIT_FAILURE;
		goto exit;
	}

	if (matchout)
		matchout = check_generated_files(t);
	if (t->modules_loaded)
		match_modules = check_loaded_modules(t);
	else
		match_modules = true;
	if (match_modules && t->modules_not_loaded)
		match_modules = check_not_loaded_modules(t);

	if (t->expected_fail == false) {
		if (err == 0) {
			if (matchout && match_modules)
				LOG("%sPASSED%s: %s\n", ANSI_HIGHLIGHT_GREEN_ON,
				    ANSI_HIGHLIGHT_OFF, t->name);
			else {
				ERR("%sFAILED%s: exit ok but %s do not match: %s\n",
				    ANSI_HIGHLIGHT_RED_ON, ANSI_HIGHLIGHT_OFF,
				    matchout ? "loaded modules" : "outputs", t->name);
				err = EXIT_FAILURE;
			}
		} else {
			ERR("%sFAILED%s: %s\n", ANSI_HIGHLIGHT_RED_ON, ANSI_HIGHLIGHT_OFF,
			    t->name);
		}
	} else {
		if (err == 0) {
			if (matchout) {
				ERR("%sUNEXPECTED PASS%s: exit with 0: %s\n",
				    ANSI_HIGHLIGHT_RED_ON, ANSI_HIGHLIGHT_OFF, t->name);
				err = EXIT_FAILURE;
			} else {
				ERR("%sUNEXPECTED PASS%s: exit with 0 and outputs do not match: %s\n",
				    ANSI_HIGHLIGHT_RED_ON, ANSI_HIGHLIGHT_OFF, t->name);
				err = EXIT_FAILURE;
			}
		} else {
			if (matchout) {
				LOG("%sEXPECTED FAIL%s: %s\n", ANSI_HIGHLIGHT_GREEN_ON,
				    ANSI_HIGHLIGHT_OFF, t->name);
				err = EXIT_SUCCESS;
			} else {
				LOG("%sEXPECTED FAIL%s: exit with %d but outputs do not match: %s\n",
				    ANSI_HIGHLIGHT_GREEN_ON, ANSI_HIGHLIGHT_OFF,
				    WEXITSTATUS(err), t->name);
				err = EXIT_FAILURE;
			}
		}
	}

exit:
	LOG("------\n");
	return err;
}

static int prepend_path(const char *extra)
{
	char *oldpath, *newpath;
	int r;

	if (extra == NULL)
		return 0;

	oldpath = getenv("PATH");
	if (oldpath == NULL)
		return setenv("PATH", extra, 1);

	if (asprintf(&newpath, "%s:%s", extra, oldpath) < 0) {
		ERR("failed to allocate memory to new PATH\n");
		return -1;
	}

	r = setenv("PATH", newpath, 1);
	free(newpath);

	return r;
}

int test_run(const struct test *t)
{
	pid_t pid;
	int fdout[2];
	int fderr[2];
	int fdmonitor[2];

	if (t->need_spawn && oneshot)
		test_run_spawned(t);

	if (t->output.out != NULL) {
		if (pipe(fdout) != 0) {
			ERR("could not create out pipe for %s\n", t->name);
			return EXIT_FAILURE;
		}
	}

	if (t->output.err != NULL) {
		if (pipe(fderr) != 0) {
			ERR("could not create err pipe for %s\n", t->name);
			return EXIT_FAILURE;
		}
	}

	if (pipe(fdmonitor) != 0) {
		ERR("could not create monitor pipe for %s\n", t->name);
		return EXIT_FAILURE;
	}

	if (prepend_path(t->path) < 0) {
		ERR("failed to prepend '%s' to PATH\n", t->path);
		return EXIT_FAILURE;
	}

	LOG("running %s, in forked context\n", t->name);

	pid = fork();
	if (pid < 0) {
		ERR("could not fork(): %m\n");
		LOG("FAILED: %s\n", t->name);
		return EXIT_FAILURE;
	}

	if (pid > 0)
		return test_run_parent(t, fdout, fderr, fdmonitor, pid);

	return test_run_child(t, fdout, fderr, fdmonitor);
}
