<commit_before>/*
 * Copyright (C) 2012-2013  ProFUSION embedded systems
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#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/util.h>

#include "testsuite.h"

static const char *ANSI_HIGHLIGHT_GREEN_ON = "\x1B[1;32m";
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 OVERRIDE_LIBDIR ABS_TOP_BUILDDIR "/testsuite/.libs/"

struct _env_config {
	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" },
};

#define USEC_PER_SEC  1000000ULL
#define USEC_PER_MSEC  1000ULL
#define TEST_TIMEOUT_USEC 2 * USEC_PER_SEC
static unsigned long long now_usec(void)
{
	struct timespec ts;

	if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
		return 0;

	return ts_usec(&ts);
}

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;

	unsetenv("LD_PRELOAD");

	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)
		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 'make rootfs' before runnning this test\n",
								rootfs);
			exit(EXIT_FAILURE);
		}
	}

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

static int check_activity(int fd, bool activity,  const char *path,
			  const char *stream)
{
	struct stat st;

	/* not monitoring or monitoring and it has activity */
	if (fd < 0 || activity)
		return 0;

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

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

	return -1;
}

#define BUFSZ 4096
struct buffer {
	char buf[BUFSZ];
	unsigned int head;
};


static bool cmpbuf_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 cmpbuf_regex(const struct test *t, const char *prefix,
			 int fd, int fd_match, struct buffer *buf,
			 struct buffer *buf_match)
{
	char *p, *p_match;
	int done = 0, done_match = 0, r;

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

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

	buf->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(buf->buf + done, '\n', buf->head - done);
		if (!p)
			break;
		*p = '\0';

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

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

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

		done = p - buf->buf + 1;
		done_match = p_match - buf_match->buf + 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 (buf->head - done)
			memmove(buf->buf, buf->buf + done, buf->head - done);
		buf->head -= done;
	}

	if (done_match) {
		if (buf_match->head - done_match)
			memmove(buf_match->buf, buf_match->buf + done_match,
				buf_match->head - done_match);
		buf_match->head -= done_match;
	}

	return true;
}

/* read fd and fd_match, checking they match exactly */
static bool cmpbuf_exact(const struct test *t, const char *prefix,
			 int fd, int fd_match, struct buffer *buf,
			 struct buffer *buf_match)
{
	int r, rmatch, done = 0;

	r = read(fd, buf->buf, sizeof(buf->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_match, buf_match->buf + done, r - done);
		if (rmatch == 0)
			break;

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

		done += rmatch;
	}

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

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

	if (!streq(buf->buf, buf_match->buf)) {
		ERR("Outputs do not match on %s:\n", prefix);
		ERR("correct:\n%s\n", buf_match->buf);
		ERR("wrong:\n%s\n", buf->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)
{
	struct epoll_event ep_outpipe, ep_errpipe, ep_monitor;
	int err, fd_ep, fd_matchout = -1, fd_matcherr = -1;
	bool fd_activityout = false, fd_activityerr = false;
	unsigned long long end_usec, start_usec;
	_cleanup_free_ struct buffer *buf_out = NULL, *buf_err = NULL;

	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) {
		fd_matchout = open(t->output.out, O_RDONLY);
		if (fd_matchout < 0) {
			err = -errno;
			ERR("could not open %s for read: %m\n",
							t->output.out);
			goto out;
		}
		memset(&ep_outpipe, 0, sizeof(struct epoll_event));
		ep_outpipe.events = EPOLLIN;
		ep_outpipe.data.ptr = &fdout;
		if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fdout, &ep_outpipe) < 0) {
			err = -errno;
			ERR("could not add fd to epoll: %m\n");
			goto out;
		}
		buf_out = calloc(2, sizeof(*buf_out));
	} else
		fdout = -1;

	if (t->output.err != NULL) {
		fd_matcherr = open(t->output.err, O_RDONLY);
		if (fd_matcherr < 0) {
			err = -errno;
			ERR("could not open %s for read: %m\n",
					t->output.err);
			goto out;

		}
		memset(&ep_errpipe, 0, sizeof(struct epoll_event));
		ep_errpipe.events = EPOLLIN;
		ep_errpipe.data.ptr = &fderr;
		if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fderr, &ep_errpipe) < 0) {
			err = -errno;
			ERR("could not add fd to epoll: %m\n");
			goto out;
		}
		buf_err = calloc(2, sizeof(*buf_err));
	} else
		fderr = -1;

	memset(&ep_monitor, 0, sizeof(struct epoll_event));
	ep_monitor.events = EPOLLHUP;
	ep_monitor.data.ptr = &fdmonitor;
	if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fdmonitor, &ep_monitor) < 0) {
		err = -errno;
		ERR("could not add monitor fd to epoll: %m\n");
		goto out;
	}

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

	for (err = 0; fdmonitor >= 0 || fdout >= 0 || fderr >= 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++) {
			int fd = *(int *)ev[i].data.ptr;
			bool ret;

			if (ev[i].events & EPOLLIN) {
				int fd_match;
				struct buffer *buf, *buf_match;
				const char *prefix;

				if (fd == fdout) {
					fd_match = fd_matchout;
					buf = buf_out;
					fd_activityout = true;
					prefix = "STDOUT";
				} else if (fd == fderr) {
					fd_match = fd_matcherr;
					buf = buf_err;
					fd_activityerr = true;
					prefix = "STDERR";
				} else {
					ERR("Unexpected activity on monitor pipe\n");
					err = -EINVAL;
					goto out;
				}

				buf_match = buf + 1;

				if (t->output.regex)
					ret = cmpbuf_regex(t, prefix, fd, fd_match,
							   buf, buf_match);
				else
					ret = cmpbuf_exact(t, prefix,  fd, fd_match,
							   buf, buf_match);

				if (!ret) {
					err = -1;
					goto out;
				}
			} else if (ev[i].events & EPOLLHUP) {
				if (epoll_ctl(fd_ep, EPOLL_CTL_DEL, fd, NULL) < 0) {
					ERR("could not remove fd %d from epoll: %m\n", fd);
				}
				*(int *)ev[i].data.ptr = -1;<commit_msg>testsuite: track number of descriptors instead of their state<commit_after>/*
 * Copyright (C) 2012-2013  ProFUSION embedded systems
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#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/util.h>

#include "testsuite.h"

static const char *ANSI_HIGHLIGHT_GREEN_ON = "\x1B[1;32m";
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 OVERRIDE_LIBDIR ABS_TOP_BUILDDIR "/testsuite/.libs/"

struct _env_config {
	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" },
};

#define USEC_PER_SEC  1000000ULL
#define USEC_PER_MSEC  1000ULL
#define TEST_TIMEOUT_USEC 2 * USEC_PER_SEC
static unsigned long long now_usec(void)
{
	struct timespec ts;

	if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
		return 0;

	return ts_usec(&ts);
}

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;

	unsetenv("LD_PRELOAD");

	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)
		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 'make rootfs' before runnning this test\n",
								rootfs);
			exit(EXIT_FAILURE);
		}
	}

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

static int check_activity(int fd, bool activity,  const char *path,
			  const char *stream)
{
	struct stat st;

	/* not monitoring or monitoring and it has activity */
	if (fd < 0 || activity)
		return 0;

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

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

	return -1;
}

#define BUFSZ 4096
struct buffer {
	char buf[BUFSZ];
	unsigned int head;
};


static bool cmpbuf_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 cmpbuf_regex(const struct test *t, const char *prefix,
			 int fd, int fd_match, struct buffer *buf,
			 struct buffer *buf_match)
{
	char *p, *p_match;
	int done = 0, done_match = 0, r;

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

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

	buf->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(buf->buf + done, '\n', buf->head - done);
		if (!p)
			break;
		*p = '\0';

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

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

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

		done = p - buf->buf + 1;
		done_match = p_match - buf_match->buf + 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 (buf->head - done)
			memmove(buf->buf, buf->buf + done, buf->head - done);
		buf->head -= done;
	}

	if (done_match) {
		if (buf_match->head - done_match)
			memmove(buf_match->buf, buf_match->buf + done_match,
				buf_match->head - done_match);
		buf_match->head -= done_match;
	}

	return true;
}

/* read fd and fd_match, checking they match exactly */
static bool cmpbuf_exact(const struct test *t, const char *prefix,
			 int fd, int fd_match, struct buffer *buf,
			 struct buffer *buf_match)
{
	int r, rmatch, done = 0;

	r = read(fd, buf->buf, sizeof(buf->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_match, buf_match->buf + done, r - done);
		if (rmatch == 0)
			break;

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

		done += rmatch;
	}

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

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

	if (!streq(buf->buf, buf_match->buf)) {
		ERR("Outputs do not match on %s:\n", prefix);
		ERR("correct:\n%s\n", buf_match->buf);
		ERR("wrong:\n%s\n", buf->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)
{
	struct epoll_event ep_outpipe, ep_errpipe, ep_monitor;
	int err, fd_ep, fd_matchout = -1, fd_matcherr = -1;
	bool fd_activityout = false, fd_activityerr = false;
	unsigned long long end_usec, start_usec;
	_cleanup_free_ struct buffer *buf_out = NULL, *buf_err = 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) {
		fd_matchout = open(t->output.out, O_RDONLY);
		if (fd_matchout < 0) {
			err = -errno;
			ERR("could not open %s for read: %m\n",
							t->output.out);
			goto out;
		}
		memset(&ep_outpipe, 0, sizeof(struct epoll_event));
		ep_outpipe.events = EPOLLIN;
		ep_outpipe.data.ptr = &fdout;
		if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fdout, &ep_outpipe) < 0) {
			err = -errno;
			ERR("could not add fd to epoll: %m\n");
			goto out;
		}
		buf_out = calloc(2, sizeof(*buf_out));
		n_fd++;
	} else
		fdout = -1;

	if (t->output.err != NULL) {
		fd_matcherr = open(t->output.err, O_RDONLY);
		if (fd_matcherr < 0) {
			err = -errno;
			ERR("could not open %s for read: %m\n",
					t->output.err);
			goto out;

		}
		memset(&ep_errpipe, 0, sizeof(struct epoll_event));
		ep_errpipe.events = EPOLLIN;
		ep_errpipe.data.ptr = &fderr;
		if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fderr, &ep_errpipe) < 0) {
			err = -errno;
			ERR("could not add fd to epoll: %m\n");
			goto out;
		}
		buf_err = calloc(2, sizeof(*buf_err));
		n_fd++;
	} else
		fderr = -1;

	memset(&ep_monitor, 0, sizeof(struct epoll_event));
	ep_monitor.events = EPOLLHUP;
	ep_monitor.data.ptr = &fdmonitor;
	if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fdmonitor, &ep_monitor) < 0) {
		err = -errno;
		ERR("could not add monitor fd to epoll: %m\n");
		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++) {
			int fd = *(int *)ev[i].data.ptr;
			bool ret;

			if (ev[i].events & EPOLLIN) {
				int fd_match;
				struct buffer *buf, *buf_match;
				const char *prefix;

				if (fd == fdout) {
					fd_match = fd_matchout;
					buf = buf_out;
					fd_activityout = true;
					prefix = "STDOUT";
				} else if (fd == fderr) {
					fd_match = fd_matcherr;
					buf = buf_err;
					fd_activityerr = true;
					prefix = "STDERR";
				} else {
					ERR("Unexpected activity on monitor pipe\n");
					err = -EINVAL;
					goto out;
				}

				buf_match = buf + 1;

				if (t->output.regex)
					ret = cmpbuf_regex(t, prefix, fd, fd_match,
							   buf, buf_match);
				else
					ret = cmpbuf_exact(t, prefix,  fd, fd_match,
							   buf, buf_match);

				if (!ret) {
					err = -1;
					goto out;
				}
			} else if (ev[i].events & EPOLLHUP) {
				if (epoll_ctl(fd_ep, EPOLL_CTL_DEL, fd, NULL) < 0) {
					ERR("could not remove fd %d from epoll: %m\n", fd);
				}
				*(int *)ev[i].data.ptr = -1;
				n_fd--;
{'fraction_non_alphanumeric': 0.153012912482066, 'fraction_numerical': 0.00900286944045911, 'mean_word_length': 3.3776102998900925, 'pattern_counts': {'":': 0, '<': 79, '<?xml version=': 0, '>': 213, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6113651', 'n_tokens_mistral': 12100, 'n_tokens_neox': 10829, 'n_words': 2984}