[igt-dev] [PATCH i-g-t 3/4] runner: New test runner

Petri Latvala petri.latvala at intel.com
Mon Apr 30 09:28:47 UTC 2018


This is a new test runner to replace piglit. Piglit has been very
useful as a test runner, but certain improvements have been very
difficult if possible at all in a generic test running framework.

Important improvements over piglit:

- Faster to launch. Being able to make assumptions about what we're
  executing makes it possible to save significant amounts of time. For
  example, a testlist file's line "igt at somebinary@somesubtest" already
  has all the information we need to construct the correct command
  line to execute that particular subtest, instead of listing all
  subtests of all test binaries and mapping them to command
  lines. Same goes for the regexp filters command line flags -t and
  -x; If we use -x somebinaryname, we don't need to list subtests from
  somebinaryname, we already know none of them will get executed.

- Logs of incomplete tests. Piglit collects test output to memory and
  dumps them to a file when the test is complete. The new runner
  writes all output to disk immediately.

- Ability to execute multiple subtests in one binary execution. This
  was possible with piglit, but its semantics made it very hard to
  implement in practice. For example, having a testlist file not only
  selected a subset of tests to run, but also mandated that they be
  executed in the same order.

- Flexible timeout support. Instead of mandating a time tests cannot
  exceed, the new runner has a timeout on inactivity. Activity is
  any output on the test's stdout or stderr, or kernel activity via
  /dev/kmsg.

The runner is fairly piglit compatible. The command line is very
similar, with a few additions. IGT_TEST_ROOT environment flag is still
supported, but can also be set via command line (in place of igt.py in
piglit command line).

The results are a set of log files, processed into a piglit-compatible
results.json file (BZ2 compression TODO). There are some new fields in
the json for extra information:

- "igt-version" contains the IGT version line. In
  multiple-subtests-mode the version information is only printed once,
  so it needs to be duplicated to all subtest results this way.
- "dmesg-warnings" contains the dmesg lines that triggered a
  dmesg-warn/dmesg-fail state.
- Runtime information will be different. Piglit takes a timestamp at
  the beginning and at the end of execution for runtime. The new
  runner uses the subtest output text. The binary execution time will
  also be included; The key "igt at somebinary" will have the runtime of
  the binary "somebinary", whereas "igt at somebinary@a" etc will have
  the runtime of the subtests. Substracting the subtest runtimes from
  the binary runtime yields the total time spent doing setup in
  igt_fixture blocks.

Signed-off-by: Petri Latvala <petri.latvala at intel.com>
Cc: Maarten Lankhorst <maarten.lankhorst at linux.intel.com>
Cc: Arkadiusz Hiler <arkadiusz.hiler at intel.com>
Cc: Tomi Sarvela <tomi.p.sarvela at intel.com>
Cc: Martin Peres <martin.peres at linux.intel.com>
---
 meson.build        |    1 +
 runner/executor.c  | 1076 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 runner/executor.h  |   42 ++
 runner/job_list.c  |  492 ++++++++++++++++++++++++
 runner/job_list.h  |   37 ++
 runner/meson.build |   24 ++
 runner/resultgen.c |  864 +++++++++++++++++++++++++++++++++++++++++
 runner/resultgen.h |    9 +
 runner/results.c   |   26 ++
 runner/runner.c    |   40 ++
 runner/settings.c  |  506 ++++++++++++++++++++++++
 runner/settings.h  |  111 ++++++
 12 files changed, 3228 insertions(+)
 create mode 100644 runner/executor.c
 create mode 100644 runner/executor.h
 create mode 100644 runner/job_list.c
 create mode 100644 runner/job_list.h
 create mode 100644 runner/meson.build
 create mode 100644 runner/resultgen.c
 create mode 100644 runner/resultgen.h
 create mode 100644 runner/results.c
 create mode 100644 runner/runner.c
 create mode 100644 runner/settings.c
 create mode 100644 runner/settings.h

diff --git a/meson.build b/meson.build
index 5b783e5d..35e42631 100644
--- a/meson.build
+++ b/meson.build
@@ -136,6 +136,7 @@ subdir('lib')
 subdir('tests')
 subdir('benchmarks')
 subdir('tools')
+subdir('runner')
 if libdrm_intel.found()
 	subdir('assembler')
 	if ['x86', 'x86_64'].contains(host_machine.cpu_family())
diff --git a/runner/executor.c b/runner/executor.c
new file mode 100644
index 00000000..2754f5a2
--- /dev/null
+++ b/runner/executor.c
@@ -0,0 +1,1076 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/watchdog.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/signalfd.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "executor.h"
+
+/* Clock handling copied from igt_core.c */
+
+static clockid_t igt_clock;
+
+#define time_valid(ts) ((ts)->tv_sec || (ts)->tv_nsec)
+
+static double
+time_elapsed(struct timespec *then,
+	     struct timespec *now)
+{
+	double elapsed = -1.;
+
+	if (time_valid(then) && time_valid(now)) {
+		elapsed = now->tv_sec - then->tv_sec;
+		elapsed += (now->tv_nsec - then->tv_nsec) * 1e-9;
+	}
+
+	return elapsed;
+}
+
+static int gettime(struct timespec *ts)
+{
+	memset(ts, 0, sizeof(*ts));
+	errno = 0;
+
+	/* Stay on the same clock for consistency. */
+	if (igt_clock != (clockid_t)-1) {
+		if (clock_gettime(igt_clock, ts))
+			goto error;
+		return 0;
+	}
+
+#ifdef CLOCK_MONOTONIC_RAW
+	if (!clock_gettime(igt_clock = CLOCK_MONOTONIC_RAW, ts))
+		return 0;
+#endif
+#ifdef CLOCK_MONOTONIC_COARSE
+	if (!clock_gettime(igt_clock = CLOCK_MONOTONIC_COARSE, ts))
+		return 0;
+#endif
+	if (!clock_gettime(igt_clock = CLOCK_MONOTONIC, ts))
+		return 0;
+error:
+	fprintf(stderr, "Warning: Could not read monotonic time: %s\n",
+		strerror(errno));
+
+	return -errno;
+}
+
+struct watchdogs
+{
+	int *fds;
+	size_t num_dogs;
+} watchdogs;
+
+static void close_watchdogs(struct settings *settings)
+{
+	size_t i;
+
+	if (settings && settings->log_level >= LOG_LEVEL_VERBOSE)
+		printf("Closing watchdogs\n");
+
+	for (i = 0; i < watchdogs.num_dogs; i++) {
+		write(watchdogs.fds[i], "V", 1);
+		close(watchdogs.fds[i]);
+	}
+}
+
+static void close_watchdogs_atexit()
+{
+	close_watchdogs(NULL);
+}
+
+static void init_watchdogs(struct settings *settings)
+{
+	int i;
+	char name[32];
+	int fd;
+
+	memset(&watchdogs, 0, sizeof(watchdogs));
+
+	if (!settings->use_watchdog || settings->inactivity_timeout <= 0)
+		return;
+
+	if (settings->log_level >= LOG_LEVEL_VERBOSE) {
+		printf("Initializing watchdogs\n");
+	}
+
+	atexit(close_watchdogs_atexit);
+
+	for (i = 0; ; i++) {
+		snprintf(name, 32, "/dev/watchdog%d", i);
+		if ((fd = open(name, O_RDWR | O_CLOEXEC)) < 0)
+			break;
+
+		watchdogs.num_dogs++;
+		watchdogs.fds = realloc(watchdogs.fds, watchdogs.num_dogs * sizeof(int));
+		watchdogs.fds[i] = fd;
+
+		if (settings->log_level >= LOG_LEVEL_VERBOSE)
+			printf(" %s\n", name);
+	}
+}
+
+static int watchdogs_set_timeout(int timeout)
+{
+	size_t i;
+	int orig_timeout = timeout;
+
+	for (i = 0; i < watchdogs.num_dogs; i++) {
+		if (ioctl(watchdogs.fds[i], WDIOC_SETTIMEOUT, &timeout)) {
+			write(watchdogs.fds[i], "V", 1);
+			close(watchdogs.fds[i]);
+			watchdogs.fds[i] = -1;
+			continue;
+		}
+
+		if (timeout < orig_timeout) {
+			/*
+			 * Timeout of this caliber refused. We want to
+			 * use the same timeout for all devices.
+			 */
+			return watchdogs_set_timeout(timeout);
+		}
+	}
+
+	return timeout;
+}
+
+static void ping_watchdogs()
+{
+	size_t i;
+
+	for (i = 0; i < watchdogs.num_dogs; i++) {
+		ioctl(watchdogs.fds[i], WDIOC_KEEPALIVE, 0);
+	}
+}
+
+static void prune_subtest(struct job_list_entry *entry, char *subtest)
+{
+	char *excl;
+
+	/*
+	 * Subtest pruning is done by adding exclusion strings to the
+	 * subtest list. The last matching item on the subtest
+	 * selection command line flag decides whether to run a
+	 * subtest, see igt_core.c for details.  If the list is empty,
+	 * the expected subtest set is unknown, so we need to add '*'
+	 * first so we can start excluding.
+	 */
+
+	if (entry->subtest_count == 0) {
+		entry->subtest_count++;
+		entry->subtests = realloc(entry->subtests, entry->subtest_count * sizeof(*entry->subtests));
+		entry->subtests[0] = strdup("*");
+	}
+
+	excl = malloc(strlen(subtest) + 2);
+	excl[0] = '!';
+	strcpy(excl + 1, subtest);
+
+	entry->subtest_count++;
+	entry->subtests = realloc(entry->subtests, entry->subtest_count * sizeof(*entry->subtests));
+	entry->subtests[entry->subtest_count - 1] = excl;
+}
+
+static bool prune_from_journal(struct job_list_entry *entry, int fd)
+{
+	char *subtest;
+	FILE *f;
+	bool any_pruned = false;
+
+	/*
+	 * Each journal line is a subtest that has been started, or
+	 * the line 'exit:$exitcode (time)', or 'timeout:$exitcode (time)'.
+	 */
+
+	f = fdopen(fd, "r");
+	if (!f)
+		return false;
+
+	while (fscanf(f, "%ms", &subtest) == 1) {
+		if (subtest[0] == '\0') {
+			/* EOF */
+			free(subtest);
+			break;
+		}
+
+		if (!strncmp(subtest, "exit:", 5)) {
+			/* Fully done. Mark that by making the binary name invalid. */
+			fscanf(f, " (%*fs)");
+			entry->binary[0] = '\0';
+			free(subtest);
+			continue;
+		}
+
+		if (!strncmp(subtest, "timeout:", 8)) {
+			fscanf(f, " (%*fs)");
+			free(subtest);
+			continue;
+		}
+
+		prune_subtest(entry, subtest);
+
+		free(subtest);
+		any_pruned = true;
+	}
+
+	fclose(f);
+	return any_pruned;
+}
+
+static char *filenames[] = {
+	"journal.txt",
+	"out.txt",
+	"err.txt",
+	"dmesg.txt",
+};
+
+static int open_at_end(int dirfd, char *name)
+{
+	int fd = openat(dirfd, name, O_RDWR | O_CREAT | O_CLOEXEC, 0666);
+	char last;
+
+	if (fd >= 0) {
+		if (lseek(fd, -1, SEEK_END) >= 0 &&
+		    read(fd, &last, 1) == 1 &&
+		    last != '\n') {
+			write(fd, "\n", 1);
+		}
+		lseek(fd, 0, SEEK_END);
+	}
+
+	return fd;
+}
+
+static int open_for_reading(int dirfd, char *name)
+{
+	return openat(dirfd, name, O_RDONLY);
+}
+
+bool open_output_files(int dirfd, int *fds, bool write)
+{
+	int i;
+	int (*openfunc)(int, char*) = write ? open_at_end : open_for_reading;
+
+	for (i = 0; i < _F_LAST; i++) {
+		if ((fds[i] = openfunc(dirfd, filenames[i])) < 0) {
+			while (--i >= 0)
+				close(fds[i]);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+void close_outputs(int *fds)
+{
+	int i;
+
+	for (i = 0; i < _F_LAST; i++) {
+		close(fds[i]);
+	}
+}
+
+static void dump_dmesg(int kmsgfd, int outfd)
+{
+	/*
+	 * Write kernel messages to the log file until we reach
+	 * 'now'. Unfortunately, /dev/kmsg doesn't support seeking to
+	 * -1 from SEEK_END so we need to use a second fd to read a
+	 * message to match against, or stop when we reach EAGAIN.
+	 */
+
+	int comparefd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+	unsigned flags;
+	unsigned long long seq, cmpseq, usec;
+	char cont;
+	char buf[256];
+	ssize_t r;
+
+	if (comparefd < 0)
+		return;
+
+	if (fcntl(kmsgfd, F_SETFL, O_NONBLOCK))
+		return;
+
+	while (1) {
+		if (comparefd >= 0) {
+			r = read(comparefd, buf, sizeof(buf) - 1);
+			if (r < 0) {
+				if (errno != EAGAIN && errno != EPIPE)
+					return;
+			} else {
+				buf[r] = '\0';
+				if (sscanf(buf, "%u,%llu,%llu,%c;",
+					   &flags, &cmpseq, &usec, &cont) == 4) {
+					close(comparefd);
+					comparefd = -1;
+				}
+			}
+		}
+
+		r = read(kmsgfd, buf, sizeof(buf));
+		if (r <= 0) {
+			if (errno == EPIPE)
+				continue;
+
+			/*
+			 * If EAGAIN, we're done. If some other error,
+			 * we can't do anything anyway.
+			 */
+			return;
+		}
+
+		write(outfd, buf, r);
+
+		if (comparefd < 0 && sscanf(buf, "%u,%llu,%llu,%c;",
+					    &flags, &seq, &usec, &cont) == 4) {
+			if (seq >= cmpseq)
+				return;
+		}
+	}
+}
+
+static bool kill_child(bool use_sigkill,
+		       struct settings *settings,
+		       pid_t child)
+{
+	int sig = use_sigkill ? SIGKILL : SIGTERM;
+
+	/*
+	 * Send the signal to the child directly, and to the child's
+	 * process group.
+	 */
+	kill(-child, sig);
+	if (kill(child, sig) && errno == ESRCH) {
+		fprintf(stderr, "Child process does not exist. This shouldn't happen.\n");
+		return false;
+	}
+
+	return true;
+}
+
+static char starting_subtest[] = "Starting subtest: ";
+static size_t starting_len = sizeof(starting_subtest) - 1;
+static char subtest_result_beg[] = "Subtest ";
+static size_t subtest_result_len = sizeof(subtest_result_beg) - 1;
+/*
+ * Returns:
+ *  >0 - Success
+ *  =0 - Failure executing
+ *  <0 - Timeout happened, need to recreate from journal
+ */
+static int monitor_output(pid_t child,
+			   int outfd, int errfd, int kmsgfd, int sigfd,
+			   int *outputs,
+			   struct settings *settings)
+{
+	fd_set set;
+	char buf[256];
+	char *outbuf = NULL;
+	size_t outbufsize = 0;
+	char current_subtest[256] = {};
+	struct signalfd_siginfo siginfo;
+	ssize_t s;
+	int n, status;
+	int nfds = outfd;
+	int timeout = settings->inactivity_timeout;
+	int timeout_intervals = 1, intervals_left;
+	int wd_extra = 10;
+	int killed = 0; /* 1 = sigterm sent, 2 = sigkill sent */
+	struct timespec time_beg, time_end;
+	bool aborting = false;
+
+	gettime(&time_beg);
+
+	if (errfd > nfds)
+		nfds = errfd;
+	if (kmsgfd > nfds)
+		nfds = kmsgfd;
+	if (sigfd > nfds)
+		nfds = sigfd;
+	nfds++;
+
+	if (timeout > 0) {
+		/*
+		 * Use original timeout plus some leeway. If we're still
+		 * alive, we want to kill the test process instead of cutting
+		 * power.
+		 */
+		int wd_timeout = watchdogs_set_timeout(timeout + wd_extra);
+
+		if (wd_timeout < timeout + wd_extra) {
+			/* Watchdog timeout smaller, so ping it more often */
+			if (wd_timeout - wd_extra < 0)
+				wd_extra = wd_timeout / 2;
+			timeout_intervals = timeout / (wd_timeout - wd_extra);
+			intervals_left = timeout_intervals;
+			timeout /= timeout_intervals;
+
+			if (settings->log_level >= LOG_LEVEL_VERBOSE) {
+				printf("Watchdog doesn't support the timeout we requested (shortened to %d seconds).\n"
+				       "Using %d intervals of %d seconds.\n",
+				       wd_timeout, timeout_intervals, timeout);
+			}
+		}
+	}
+
+	while (outfd >= 0 || errfd >= 0 || sigfd >= 0) {
+		struct timeval tv = { .tv_sec = timeout };
+
+		FD_ZERO(&set);
+		if (outfd >= 0)
+			FD_SET(outfd, &set);
+		if (errfd >= 0)
+			FD_SET(errfd, &set);
+		if (kmsgfd >= 0)
+			FD_SET(kmsgfd, &set);
+		if (sigfd >= 0)
+			FD_SET(sigfd, &set);
+
+		n = select(nfds, &set, NULL, NULL, timeout == 0 ? NULL : &tv);
+		if (n < 0) {
+			/* TODO */
+			return 0;
+		}
+
+		if (n == 0) {
+			intervals_left--;
+			if (intervals_left) {
+				continue;
+			}
+
+			ping_watchdogs();
+
+			switch (killed) {
+			case 0:
+				if (settings->log_level >= LOG_LEVEL_NORMAL) {
+					printf("Timeout. Killing the current test with SIGTERM.\n");
+				}
+
+				if (!kill_child(false, settings, child))
+					return 0;
+				killed = 1;
+
+				/*
+				 * Now continue the loop and let the
+				 * dying child be handled normally.
+				 */
+				timeout = 2; /* Timeout for waiting selected by fair dice roll. */
+				watchdogs_set_timeout(20);
+				intervals_left = timeout_intervals = 1;
+				break;
+			case 1:
+				if (settings->log_level >= LOG_LEVEL_NORMAL) {
+					printf("Timeout. Killing the current test with SIGKILL.\n");
+				}
+
+				if (!kill_child(true, settings, child))
+					return 0;
+
+				killed = 2;
+				break;
+			case 2:
+				/* Nothing that can be done, really. Let's tell the caller we want to abort. */
+				if (settings->log_level >= LOG_LEVEL_NORMAL) {
+					fprintf(stderr, "Child refuses to die. Aborting.\n");
+				}
+				close_watchdogs(settings);
+				free(outbuf);
+				close(outfd);
+				close(errfd);
+				close(kmsgfd);
+				close(sigfd);
+				return 0;
+			}
+
+			continue;
+		}
+
+		intervals_left = timeout_intervals;
+		ping_watchdogs();
+
+		if (outfd >= 0 && FD_ISSET(outfd, &set)) {
+			char *newline;
+
+			s = read(outfd, buf, sizeof(buf));
+			if (s <= 0) {
+				if (s < 0) {
+					fprintf(stderr, "Error reading test's stdout: %s\n",
+						strerror(errno));
+				}
+
+				close(outfd);
+				outfd = -1;
+				goto out_end;
+			}
+
+			write(outputs[_F_OUT], buf, s);
+			if (settings->sync) {
+				fdatasync(outputs[_F_OUT]);
+			}
+
+			outbuf = realloc(outbuf, outbufsize + s);
+			memcpy(outbuf + outbufsize, buf, s);
+			outbufsize += s;
+
+			while ((newline = memchr(outbuf, '\n', outbufsize)) != NULL) {
+				size_t linelen = newline - outbuf + 1;
+
+				if (linelen > starting_len &&
+				    !memcmp(outbuf, starting_subtest, starting_len)) {
+					write(outputs[_F_JOURNAL], outbuf + starting_len,
+					      linelen - starting_len);
+					memcpy(current_subtest, outbuf + starting_len,
+					       linelen - starting_len);
+					current_subtest[linelen - starting_len] = '\0';
+
+					if (settings->log_level >= LOG_LEVEL_VERBOSE) {
+						fwrite(outbuf, 1, linelen, stdout);
+					}
+				}
+				if (linelen > subtest_result_len &&
+				    !memcmp(outbuf, subtest_result_beg, subtest_result_len)) {
+					char *delim = memchr(outbuf, ':', linelen);
+
+					if (delim != NULL) {
+						size_t subtestlen = delim - outbuf - subtest_result_len;
+						if (memcmp(current_subtest, outbuf + subtest_result_len,
+							   subtestlen)) {
+							/* Result for a test that didn't ever start */
+							write(outputs[_F_JOURNAL],
+							      outbuf + subtest_result_len,
+							      subtestlen);
+							write(outputs[_F_JOURNAL], "\n", 1);
+							if (settings->sync) {
+								fdatasync(outputs[_F_JOURNAL]);
+							}
+							current_subtest[0] = '\0';
+						}
+
+						if (settings->log_level >= LOG_LEVEL_VERBOSE) {
+							fwrite(outbuf, 1, linelen, stdout);
+						}
+					}
+				}
+
+				memmove(outbuf, newline + 1, outbufsize - linelen);
+				outbufsize -= linelen;
+			}
+		}
+	out_end:
+
+		if (errfd >= 0 && FD_ISSET(errfd, &set)) {
+			s = read(errfd, buf, sizeof(buf));
+			if (s <= 0) {
+				if (s < 0) {
+					fprintf(stderr, "Error reading test's stderr: %s\n",
+						strerror(errno));
+				}
+				close(errfd);
+				errfd = -1;
+			} else {
+				write(outputs[_F_ERR], buf, s);
+				if (settings->sync) {
+					fdatasync(outputs[_F_ERR]);
+				}
+			}
+		}
+
+		if (kmsgfd >= 0 && FD_ISSET(kmsgfd, &set)) {
+			s = read(kmsgfd, buf, sizeof(buf));
+			if (s < 0) {
+				if (errno != EPIPE) {
+					fprintf(stderr, "Error reading from kmsg, stopping monitoring: %s\n",
+						strerror(errno));
+					close(kmsgfd);
+					kmsgfd = -1;
+				}
+			} else {
+				write(outputs[_F_DMESG], buf, s);
+				if (settings->sync) {
+					fdatasync(outputs[_F_DMESG]);
+				}
+			}
+		}
+
+		if (sigfd >= 0 && FD_ISSET(sigfd, &set)) {
+			double time;
+
+			s = read(sigfd, &siginfo, sizeof(siginfo));
+			if (s < 0) {
+				fprintf(stderr, "Error reading from signalfd: %s\n",
+					strerror(errno));
+				continue;
+			} else if (siginfo.ssi_signo == SIGCHLD) {
+				if (child != waitpid(child, &status, WNOHANG)) {
+					fprintf(stderr, "Failed to reap child\n");
+					status = 9999;
+				} else if (WIFEXITED(status)) {
+					status = WEXITSTATUS(status);
+					if (status >= 128) {
+						status = 128 - status;
+					}
+				} else if (WIFSIGNALED(status)) {
+					status = -WTERMSIG(status);
+				} else {
+					status = 9999;
+				}
+			} else {
+				/* We're dying, so we're taking them with us */
+				if (settings->log_level >= LOG_LEVEL_NORMAL)
+					printf("Abort requested, terminating children\n");
+
+				if (!kill_child(false, settings, child))
+					return 0;
+				aborting = true;
+				timeout = 2;
+				killed = 1;
+
+				continue;
+			}
+
+			gettime(&time_end);
+
+			time = time_elapsed(&time_beg, &time_end);
+			if (time < 0.0)
+				time = 0.0;
+
+			if (!aborting) {
+				dprintf(outputs[_F_JOURNAL], "%s:%d (%.3fs)\n",
+					killed ? "timeout" : "exit",
+					status, time);
+				if (settings->sync) {
+					fdatasync(outputs[_F_JOURNAL]);
+				}
+			}
+
+			close(sigfd);
+			sigfd = -1;
+			child = 0;
+		}
+	}
+
+	dump_dmesg(kmsgfd, outputs[_F_DMESG]);
+	if (settings->sync)
+		fdatasync(outputs[_F_DMESG]);
+
+	free(outbuf);
+	close(outfd);
+	close(errfd);
+	close(kmsgfd);
+	close(sigfd);
+
+	if (aborting)
+		return 0;
+
+	return killed ? -1 : 1;
+}
+
+static void execute_test_process(int outfd, int errfd,
+				 struct settings *settings,
+				 struct job_list_entry *entry)
+{
+	char *argv[4] = {};
+	size_t rootlen;
+
+	dup2(outfd, STDOUT_FILENO);
+	dup2(errfd, STDERR_FILENO);
+
+	setpgid(0, 0);
+
+	rootlen = strlen(settings->test_root);
+	argv[0] = malloc(rootlen + strlen(entry->binary) + 2);
+	strcpy(argv[0], settings->test_root);
+	argv[0][rootlen] = '/';
+	strcpy(argv[0] + rootlen + 1, entry->binary);
+
+	if (entry->subtest_count) {
+		size_t argsize;
+		size_t i;
+
+		argv[1] = "--run-subtest";
+		argsize = strlen(entry->subtests[0]);
+		argv[2] = malloc(argsize + 1);
+		strcpy(argv[2], entry->subtests[0]);
+
+		for (i = 1; i < entry->subtest_count; i++) {
+			char *sub = entry->subtests[i];
+			size_t sublen = strlen(sub);
+
+			argv[2] = realloc(argv[2], argsize + sublen + 2);
+			argv[2][argsize] = ',';
+			strcpy(argv[2] + argsize + 1, sub);
+			argsize += sublen + 1;
+		}
+	}
+
+	execv(argv[0], argv);
+	fprintf(stderr, "Cannot execute %s\n", argv[0]);
+	exit(79);
+}
+
+static int digits(size_t num)
+{
+	int ret = 0;
+	while (num) {
+		num /= 10;
+		ret++;
+	}
+
+	if (ret == 0) ret++;
+	return ret;
+}
+
+/*
+ * Returns:
+ *  >0 - Success
+ *  =0 - Failure executing
+ *  <0 - Timeout happened, need to recreate from journal
+ */
+static int execute_entry(size_t idx,
+			  size_t total,
+			  struct settings *settings,
+			  struct job_list_entry *entry,
+			  int testdirfd, int resdirfd)
+{
+	int dirfd;
+	int outputs[_F_LAST];
+	int kmsgfd;
+	int sigfd;
+	sigset_t mask;
+	int outpipe[2] = { -1, -1 };
+	int errpipe[2] = { -1, -1 };
+	char name[32];
+	pid_t child;
+	int result;
+
+	snprintf(name, 32, "%zd", idx);
+	mkdirat(resdirfd, name, 0777);
+	if ((dirfd = openat(resdirfd, name, O_DIRECTORY | O_RDONLY | O_CLOEXEC)) < 0) {
+		fprintf(stderr, "Error accessing individual test result directory\n");
+		return 0;
+	}
+
+	if (!open_output_files(dirfd, outputs, true)) {
+		close(dirfd);
+		fprintf(stderr, "Error opening output files\n");
+		return 0;
+	}
+
+	if (settings->sync) {
+		fsync(dirfd);
+		fsync(resdirfd);
+	}
+
+	if (pipe(outpipe) || pipe(errpipe)) {
+		close_outputs(outputs);
+		close(dirfd);
+		close(outpipe[0]);
+		close(outpipe[1]);
+		close(errpipe[0]);
+		close(errpipe[1]);
+		fprintf(stderr, "Error creating pipes: %s\n", strerror(errno));
+		return 0;
+	}
+
+	if ((kmsgfd = open("/dev/kmsg", O_RDONLY | O_CLOEXEC)) < 0) {
+		fprintf(stderr, "Warning: Cannot open /dev/kmsg\n");
+	} else {
+		/* TODO: Checking of abort conditions in pre-execute dmesg */
+		lseek(kmsgfd, 0, SEEK_END);
+	}
+
+	sigemptyset(&mask);
+	sigaddset(&mask, SIGCHLD);
+	sigaddset(&mask, SIGINT);
+	sigaddset(&mask, SIGTERM);
+	sigaddset(&mask, SIGQUIT);
+	sigprocmask(SIG_BLOCK, &mask, NULL);
+	sigfd = signalfd(-1, &mask, O_CLOEXEC);
+
+	if (sigfd < 0) {
+		/* TODO: Handle better */
+		fprintf(stderr, "Cannot monitor child process with signalfd\n");
+		close(outpipe[0]);
+		close(errpipe[0]);
+		close(outpipe[1]);
+		close(errpipe[1]);
+		close(kmsgfd);
+		close_outputs(outputs);
+		close(dirfd);
+		return 0;
+	}
+
+	if (settings->log_level >= LOG_LEVEL_NORMAL) {
+		int width = digits(total);
+		printf("[%0*zd/%0*zd] %s", width, idx + 1, width, total, entry->binary);
+		if (entry->subtest_count > 0) {
+			size_t i;
+			char *delim = "";
+
+			printf(" (");
+			for (i = 0; i < entry->subtest_count; i++) {
+				printf("%s%s", delim, entry->subtests[i]);
+				delim = ", ";
+			}
+			printf(")");
+		}
+		printf("\n");
+	}
+
+	if ((child = fork())) {
+		int outfd = outpipe[0];
+		int errfd = errpipe[0];
+		close(outpipe[1]);
+		close(errpipe[1]);
+
+		result = monitor_output(child, outfd, errfd, kmsgfd, sigfd,
+					outputs, settings);
+	} else {
+		int outfd = outpipe[1];
+		int errfd = errpipe[1];
+		close(outpipe[0]);
+		close(errpipe[0]);
+
+		sigprocmask(SIG_UNBLOCK, &mask, NULL);
+
+		setenv("IGT_SENTINEL_ON_STDERR", "1", 1);
+
+		execute_test_process(outfd, errfd, settings, entry);
+	}
+
+	close(outpipe[1]);
+	close(errpipe[1]);
+	close(kmsgfd);
+	close_outputs(outputs);
+	close(dirfd);
+
+	return result;
+}
+
+static bool clear_test_result_directory(int dirfd)
+{
+	if (unlinkat(dirfd, "out.txt", 0) ||
+	    unlinkat(dirfd, "err.txt", 0) ||
+	    unlinkat(dirfd, "dmesg.txt", 0) ||
+	    unlinkat(dirfd, "journal.txt", 0)) {
+		if (errno != ENOENT) {
+			fprintf(stderr, "Error clearing test result directories: %s\n",
+				strerror(errno));
+			return false;
+		}
+	}
+
+	return true;
+}
+
+static bool clear_old_results(char *path)
+{
+	int dirfd;
+	size_t i;
+
+	if ((dirfd = open(path, O_DIRECTORY | O_RDONLY)) < 0) {
+		if (errno == ENOENT) {
+			/* Successfully cleared if it doesn't even exist */
+			return true;
+		}
+
+		fprintf(stderr, "Error clearing old results: %s\n", strerror(errno));
+		return false;
+	}
+
+	if (unlinkat(dirfd, "uname.txt", 0) && errno != ENOENT) {
+		close(dirfd);
+		fprintf(stderr, "Error clearing old results: %s\n", strerror(errno));
+		return false;
+	}
+
+	for (i = 0; true; i++) {
+		char name[32];
+		int resdirfd;
+
+		snprintf(name, 32, "%zd", i);
+		if ((resdirfd = openat(dirfd, name, O_DIRECTORY | O_RDONLY)) < 0)
+			break;
+
+		if (!clear_test_result_directory(resdirfd)) {
+			close(resdirfd);
+			close(dirfd);
+			return false;
+		}
+		close(resdirfd);
+		unlinkat(dirfd, name, AT_REMOVEDIR);
+	}
+
+	close(dirfd);
+
+	return true;
+}
+
+static bool initialize_execute_from_resume(struct execute_state *state,
+					   struct settings *orig_settings,
+					   struct job_list *orig_job_list)
+{
+	struct settings settings;
+	struct job_list list;
+	struct job_list_entry *entry;
+	int dirfd, resdirfd, fd, i;
+
+	init_settings(&settings);
+	init_job_list(&list);
+
+	if ((dirfd = open(orig_settings->results_path, O_DIRECTORY | O_RDONLY)) < 0)
+		return false;
+
+	if (!read_settings(&settings, dirfd) ||
+	    !read_job_list(&list, dirfd)) {
+		close(dirfd);
+		return false;
+	}
+
+	for (i = list.size; i >= 0; i--) {
+		char name[32];
+
+		snprintf(name, 32, "%d", i);
+		if ((resdirfd = openat(dirfd, name, O_DIRECTORY | O_RDONLY)) >= 0) {
+			break;
+		}
+	}
+
+	if (i < 0) {
+		/* Nothing has been executed yet, state is fine as is */
+		goto success;
+	}
+
+	entry = &list.entries[i];
+	state->next = i;
+	if ((fd = openat(resdirfd, filenames[_F_JOURNAL], O_RDONLY)) >= 0) {
+		if (!prune_from_journal(entry, fd)) {
+			/*
+			 * The test does not have subtests, or
+			 * incompleted before the first subtest
+			 * began. Either way, not suitable to
+			 * re-run.
+			 */
+			state->next = i + 1;
+		} else if (entry->binary[0] == '\0') {
+			/* This test is fully completed */
+			state->next = i + 1;
+		}
+
+		close(fd);
+	}
+
+ success:
+	free_settings(orig_settings);
+	free_job_list(orig_job_list);
+	*orig_settings = settings;
+	*orig_job_list = list;
+	close(resdirfd);
+	close(dirfd);
+
+	return true;
+}
+
+bool initialize_execute_state(struct execute_state *state,
+			      struct settings *settings,
+			      struct job_list *job_list)
+{
+	memset(state, 0, sizeof(*state));
+
+	if (!validate_settings(settings))
+		return false;
+
+	if (settings->overwrite &&
+	    !clear_old_results(settings->results_path))
+		return false;
+
+	if (!serialize_settings(settings) ||
+	    !serialize_job_list(job_list, settings))
+		return initialize_execute_from_resume(state, settings, job_list);
+
+	return true;
+}
+
+bool execute(struct execute_state *state,
+	     struct settings *settings,
+	     struct job_list *job_list)
+{
+	struct utsname unamebuf;
+	int resdirfd, testdirfd, unamefd;
+
+	if ((resdirfd = open(settings->results_path, O_DIRECTORY | O_RDONLY)) < 0) {
+		/* Initialize state should have done this */
+		fprintf(stderr, "Error: Failure opening results path %s\n",
+			settings->results_path);
+		return false;
+	}
+
+	if ((testdirfd = open(settings->test_root, O_DIRECTORY | O_RDONLY)) < 0) {
+		fprintf(stderr, "Error: Failure opening test root %s\n",
+			settings->test_root);
+		close(resdirfd);
+		return false;
+	}
+
+	/* TODO: On resume, don't rewrite, verify that content matches current instead */
+	if ((unamefd = openat(resdirfd, "uname.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666)) < 0) {
+		fprintf(stderr, "Error: Failure creating opening uname.txt: %s\n",
+			strerror(errno));
+		close(testdirfd);
+		close(resdirfd);
+		return false;
+	}
+
+	init_watchdogs(settings);
+
+	if (!uname(&unamebuf)) {
+		dprintf(unamefd, "%s %s %s %s %s\n",
+			unamebuf.sysname,
+			unamebuf.nodename,
+			unamebuf.release,
+			unamebuf.version,
+			unamebuf.machine);
+	} else {
+		dprintf(unamefd, "uname() failed\n");
+	}
+	close(unamefd);
+
+	for (; state->next < job_list->size;
+	     state->next++) {
+		int result = execute_entry(state->next,
+					   job_list->size,
+					   settings,
+					   &job_list->entries[state->next],
+					   testdirfd, resdirfd);
+		if (result <= 0) {
+			close(testdirfd);
+			close(resdirfd);
+			close_watchdogs(settings);
+			if (result < 0) {
+				memset(state, 0, sizeof(*state));
+				initialize_execute_from_resume(state, settings, job_list);
+				return execute(state, settings, job_list);
+			}
+			return false;
+		}
+	}
+
+	close(testdirfd);
+	close(resdirfd);
+	close_watchdogs(settings);
+	return true;
+}
diff --git a/runner/executor.h b/runner/executor.h
new file mode 100644
index 00000000..bdda983a
--- /dev/null
+++ b/runner/executor.h
@@ -0,0 +1,42 @@
+#ifndef RUNNER_EXECUTOR_H
+#define RUNNER_EXECUTOR_H
+
+#include "job_list.h"
+#include "settings.h"
+
+struct execute_state
+{
+	size_t next;
+};
+
+enum {
+	_F_JOURNAL,
+	_F_OUT,
+	_F_ERR,
+	_F_DMESG,
+	_F_LAST,
+};
+
+bool open_output_files(int dirfd, int *fds, bool write);
+void close_outputs(int *fds);
+
+/*
+ * Initialize execute_state object to a state where it's ready to
+ * resume. Will validate the settings and serialize both settings and
+ * the job_list into the result directory if they are not yet written
+ * there.
+ *
+ * If executions have already started, will communicate the resume
+ * point by setting the appropriate next_job_list_entry value, and
+ * possibly modifying the job_list object.
+ */
+bool initialize_execute_state(struct execute_state *state,
+			      struct settings *settings,
+			      struct job_list *job_list);
+
+bool execute(struct execute_state *state,
+	     struct settings *settings,
+	     struct job_list *job_list);
+
+
+#endif
diff --git a/runner/job_list.c b/runner/job_list.c
new file mode 100644
index 00000000..ec942481
--- /dev/null
+++ b/runner/job_list.c
@@ -0,0 +1,492 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "job_list.h"
+#include "igt_core.h"
+
+static bool matches_any(const char *needle, struct regex_list *haystacks)
+{
+	size_t i;
+
+	for (i = 0; i < haystacks->size; i++) {
+		if (regexec(haystacks->regexes[i], needle,
+			    (size_t)0, NULL, 0) == 0) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static void add_job_list_entry(struct job_list *job_list,
+			       char *binary,
+			       char **subtests,
+			       size_t subtest_count)
+{
+	struct job_list_entry *entry;
+
+	job_list->size++;
+	job_list->entries = realloc(job_list->entries, job_list->size * sizeof(*job_list->entries));
+	entry = &job_list->entries[job_list->size - 1];
+
+	entry->binary = binary;
+	entry->subtests = subtests;
+	entry->subtest_count = subtest_count;
+}
+
+static void add_subtests(struct job_list *job_list, struct settings *settings,
+			 char *binary,
+			 struct regex_list *include, struct regex_list *exclude)
+{
+	FILE *p;
+	char cmd[256] = {};
+	char *subtestname;
+	char **subtests = NULL;
+	size_t num_subtests = 0;
+	size_t rootlen = strlen(settings->test_root);
+	size_t binarylen = strlen(binary);
+	int idx, s;
+
+	if (rootlen + binarylen + strlen(" --list-subtests") + 1 > 256) {
+		/* This shouldn't happen */
+		fprintf(stderr, "Path to binary too long, ignoring: %s/%s\n",
+			settings->test_root, binary);
+		return;
+	}
+
+	strcpy(cmd, settings->test_root);
+	idx = rootlen;
+	cmd[idx++] = '/';
+	strcpy(cmd + idx, binary);
+	idx += binarylen;
+	strcpy(cmd + idx, " --list-subtests");
+
+	p = popen(cmd, "r");
+	if (!p) {
+		fprintf(stderr, "popen failed when executing %s: %s\n",
+			cmd,
+			strerror(errno));
+		return;
+	}
+
+	while (fscanf(p, "%ms", &subtestname) == 1) {
+		if (exclude && exclude->size && matches_any(subtestname, exclude)) {
+			free(subtestname);
+			continue;
+		}
+
+		if (include && include->size && !matches_any(subtestname, include)) {
+			free(subtestname);
+			continue;
+		}
+
+		if (settings->multiple_mode) {
+			num_subtests++;
+			subtests = realloc(subtests, num_subtests * sizeof(*subtests));
+			subtests[num_subtests - 1] = strdup(subtestname);
+		} else {
+			subtests = malloc(sizeof(*subtests));
+			*subtests = strdup(subtestname);
+			add_job_list_entry(job_list, strdup(binary), subtests, 1);
+			subtests = NULL;
+		}
+
+		free(subtestname);
+	}
+
+	if (num_subtests) {
+		add_job_list_entry(job_list, strdup(binary), subtests, num_subtests);
+	}
+
+	s = pclose(p);
+	if (s == 0) {
+		return;
+	} else if (s == -1) {
+		fprintf(stderr, "popen error when executing %s: %s\n", binary, strerror(errno));
+	} else if (WIFEXITED(s)) {
+		if (WEXITSTATUS(s) == IGT_EXIT_INVALID) {
+			/* No subtests on this one */
+			if (exclude && exclude->size && matches_any(binary, exclude)) {
+				return;
+			}
+			if (!include || !include->size || matches_any(binary, include)) {
+				add_job_list_entry(job_list, strdup(binary), NULL, 0);
+				return;
+			}
+		}
+	} else {
+		fprintf(stderr, "Test binary %s died unexpectedly\n", binary);
+	}
+}
+
+static bool filtered_job_list(struct job_list *job_list,
+			      struct settings *settings,
+			      int fd)
+{
+	FILE *f;
+	char buf[128];
+
+	if (job_list->entries != NULL) {
+		fprintf(stderr, "Job list not cleared, this shouldn't happen\n");
+		exit(1);
+	}
+
+	f = fdopen(fd, "r");
+
+	while (fscanf(f, "%127s", buf) == 1) {
+		if (!strcmp(buf, "TESTLIST") || !(strcmp(buf, "END")))
+			continue;
+
+		/*
+		 * If the binary name matches exclude filters, no
+		 * subtests are added.
+		 */
+		if (settings->exclude_regexes.size && matches_any(buf, &settings->exclude_regexes))
+			continue;
+
+		/*
+		 * If the binary name matches include filters (or include filters not present),
+		 * all subtests except those matching exclude filters are added.
+		 */
+		if (!settings->include_regexes.size || matches_any(buf, &settings->include_regexes)) {
+			if (settings->multiple_mode && !settings->exclude_regexes.size)
+				/*
+				 * Optimization; we know that all
+				 * subtests will be included, so we
+				 * get to omit executing
+				 * --list-subtests.
+				 */
+				add_job_list_entry(job_list, strdup(buf), NULL, 0);
+			else
+				add_subtests(job_list, settings, buf,
+					     NULL, &settings->exclude_regexes);
+			continue;
+		}
+
+		/*
+		 * Binary name doesn't match exclude or include filters.
+		 */
+		add_subtests(job_list, settings, buf,
+			     &settings->include_regexes,
+			     &settings->exclude_regexes);
+	}
+
+	return job_list->size != 0;
+}
+
+static bool job_list_from_test_list(struct job_list *job_list,
+				    struct settings *settings)
+{
+	FILE *f;
+	ssize_t read;
+	char *line = NULL;
+	size_t line_len = 0;
+	struct job_list_entry entry = {};
+	bool any = false;
+
+	if ((f = fopen(settings->test_list, "r")) == NULL) {
+		fprintf(stderr, "Cannot open test list file %s\n", settings->test_list);
+		return false;
+	}
+
+	while ((read = getline(&line, &line_len, f))) {
+		char *binary;
+		char *delim;
+
+		if (read < 0) {
+			if (errno == EINTR)
+				continue;
+			else
+				break;
+		}
+
+		/* # starts a comment */
+		if ((delim = strchr(line, '#')) != NULL) {
+			*delim = '\0';
+		}
+
+		if (sscanf(line, "igt@%ms", &binary) == 1) {
+			if ((delim = strchr(binary, '@')) != NULL) {
+				*delim++ = '\0';
+			}
+
+			if (!settings->multiple_mode) {
+				char **subtests = NULL;
+				if (delim) {
+					subtests = malloc(sizeof(char*));
+					subtests[0] = strdup(delim);
+				}
+				add_job_list_entry(job_list, strdup(binary),
+						   subtests, (size_t)(subtests != NULL));
+				any = true;
+				free(binary);
+				binary = NULL;
+				continue;
+			}
+
+			/*
+			 * If the currently built entry has the same
+			 * binary, add a subtest. Otherwise submit
+			 * what's already built and start a new one.
+			 */
+			if (entry.binary && !strcmp(entry.binary, binary)) {
+				if (!delim) {
+					/* ... except we didn't get a subtest */
+					fprintf(stderr,
+						"Error: Unexpected test without subtests "
+						"after same test had subtests\n");
+					free(binary);
+					fclose(f);
+					return false;
+				}
+				entry.subtest_count++;
+				entry.subtests = realloc(entry.subtests,
+							 entry.subtest_count *
+							 sizeof(*entry.subtests));
+				entry.subtests[entry.subtest_count - 1] = strdup(delim);
+				free(binary);
+				binary = NULL;
+				continue;
+			}
+
+			if (entry.binary) {
+				add_job_list_entry(job_list, entry.binary, entry.subtests, entry.subtest_count);
+				any = true;
+			}
+
+			memset(&entry, 0, sizeof(entry));
+			entry.binary = strdup(binary);
+			if (delim) {
+				entry.subtests = malloc(sizeof(*entry.subtests));
+				entry.subtests[0] = strdup(delim);
+				entry.subtest_count = 1;
+			}
+
+			free(binary);
+			binary = NULL;
+		}
+	}
+
+	if (entry.binary) {
+		add_job_list_entry(job_list, entry.binary, entry.subtests, entry.subtest_count);
+		any = true;
+	}
+
+	free(line);
+	fclose(f);
+	return any;
+}
+
+void init_job_list(struct job_list *job_list)
+{
+	memset(job_list, 0, sizeof(*job_list));
+}
+
+void free_job_list(struct job_list *job_list)
+{
+	int i, k;
+
+	for (i = 0; i < job_list->size; i++) {
+		struct job_list_entry *entry = &job_list->entries[i];
+
+		free(entry->binary);
+		for (k = 0; k < entry->subtest_count; k++) {
+			free(entry->subtests[k]);
+		}
+		free(entry->subtests);
+	}
+	free(job_list->entries);
+	init_job_list(job_list);
+}
+
+bool create_job_list(struct job_list *job_list,
+		     struct settings *settings)
+{
+	int dirfd, fd;
+	bool result;
+
+	if (!settings->test_root) {
+		fprintf(stderr, "No test root set; this shouldn't happen\n");
+		return false;
+	}
+
+	free_job_list(job_list);
+
+	dirfd = open(settings->test_root, O_DIRECTORY | O_RDONLY);
+	if (dirfd < 0) {
+		fprintf(stderr, "Test directory %s cannot be opened\n", settings->test_root);
+		return false;
+	}
+
+	fd = openat(dirfd, "test-list.txt", O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "Cannot open %s/test-list.txt\n", settings->test_root);
+		close(dirfd);
+		return false;
+	}
+
+	/*
+	 * If a test_list is given (not to be confused with
+	 * test-list.txt), we use it directly without making tests
+	 * list their subtests. If include/exclude filters are given
+	 * we filter them directly from the test_list.
+	 */
+	if (settings->test_list)
+		result = job_list_from_test_list(job_list, settings);
+	else
+		result = filtered_job_list(job_list, settings, fd);
+
+	close(fd);
+	close(dirfd);
+
+	return result;
+}
+
+static char joblist_filename[] = "joblist.txt";
+bool serialize_job_list(struct job_list *job_list, struct settings *settings)
+{
+	int dirfd, fd;
+	size_t i, k;
+	FILE *f;
+
+	if (!settings->results_path) {
+		fprintf(stderr, "No results-path set; this shouldn't happen\n");
+		return false;
+	}
+
+	if ((dirfd = open(settings->results_path, O_DIRECTORY | O_RDONLY)) < 0) {
+		mkdir(settings->results_path, 0777);
+		if ((dirfd = open(settings->results_path, O_DIRECTORY | O_RDONLY)) < 0) {
+			fprintf(stderr, "Creating results-path failed\n");
+			return false;
+		}
+	}
+
+	if ((fd = openat(dirfd, joblist_filename, O_RDONLY)) >= 0) {
+		close(fd);
+
+		if (!settings->overwrite) {
+			/* Serialization data already exists, not overwriting. */
+			close(fd);
+			close(dirfd);
+			return false;
+		}
+
+		if (unlinkat(dirfd, joblist_filename, 0) != 0) {
+			fprintf(stderr, "Error overwriting old job list\n");
+			close(dirfd);
+			return false;
+		}
+	}
+
+	if ((fd = openat(dirfd, joblist_filename, O_CREAT | O_EXCL | O_WRONLY, 0666)) < 0) {
+		fprintf(stderr, "Creating job list serialization file failed\n");
+		return false;
+	}
+
+	f = fdopen(fd, "w");
+	if (!f) {
+		close(fd);
+		close(dirfd);
+		return false;
+	}
+
+	for (i = 0; i < job_list->size; i++) {
+		struct job_list_entry *entry = &job_list->entries[i];
+		fprintf(f, "%s", entry->binary);
+
+		if (entry->subtest_count) {
+			char *delim = "";
+
+			fprintf(f, " ");
+
+			for (k = 0; k < entry->subtest_count; k++) {
+				fprintf(f, "%s%s", delim, entry->subtests[k]);
+				delim = ",";
+			}
+		}
+
+		fprintf(f, "\n");
+	}
+
+	if (settings->sync) {
+		fsync(fd);
+		fsync(dirfd);
+	}
+
+	fclose(f);
+	close(dirfd);
+	return true;
+}
+
+bool read_job_list(struct job_list *job_list, int dirfd)
+{
+	int fd;
+	FILE *f;
+	ssize_t read;
+	char *line = NULL;
+	size_t line_len = 0;
+
+	free_job_list(job_list);
+
+	if ((fd = openat(dirfd, joblist_filename, O_RDONLY)) < 0)
+		return false;
+
+	f = fdopen(fd, "r");
+	if (!f) {
+		close(fd);
+		return false;
+	}
+
+	while ((read = getline(&line, &line_len, f))) {
+		char *binary, *sublist, *comma;
+		char **subtests = NULL;
+		size_t num_subtests = 0, len;
+
+		if (read < 0) {
+			if (errno == EINTR)
+				continue;
+			else
+				break;
+		}
+
+		len = strlen(line);
+		if (len > 0 && line[len - 1] == '\n')
+			line[len - 1] = '\0';
+
+		sublist = strchr(line, ' ');
+		if (!sublist) {
+			add_job_list_entry(job_list, strdup(line), NULL, 0);
+			continue;
+		}
+
+		*sublist++ = '\0';
+		binary = strdup(line);
+
+		do {
+			comma = strchr(sublist, ',');
+			if (comma) {
+				*comma++ = '\0';
+			}
+
+			++num_subtests;
+			subtests = realloc(subtests, num_subtests * sizeof(*subtests));
+			subtests[num_subtests - 1] = strdup(sublist);
+			sublist = comma;
+		} while (comma != NULL);
+
+		add_job_list_entry(job_list, binary, subtests, num_subtests);
+	}
+
+	free(line);
+	fclose(f);
+
+	return true;
+}
diff --git a/runner/job_list.h b/runner/job_list.h
new file mode 100644
index 00000000..c726ab09
--- /dev/null
+++ b/runner/job_list.h
@@ -0,0 +1,37 @@
+#ifndef RUNNER_JOB_LIST_H
+#define RUNNER_JOB_LIST_H
+
+#include <stdbool.h>
+
+#include "settings.h"
+
+struct job_list_entry {
+	char *binary;
+	char **subtests;
+	/*
+	 * 0 = all, or test has no subtests.
+	 *
+	 * If the original job_list was to run all subtests of a
+	 * binary and such a run was incomplete, resuming from the
+	 * execution journal will fill the subtest array with already
+	 * started subtests prepended with '!' so the test binary will
+	 * not run them. subtest_count will still reflect the size of
+	 * the above array.
+	 */
+	size_t subtest_count;
+};
+
+struct job_list
+{
+	struct job_list_entry *entries;
+	size_t size;
+};
+
+void init_job_list(struct job_list *job_list);
+void free_job_list(struct job_list *job_list);
+bool create_job_list(struct job_list *job_list, struct settings *settings);
+
+bool serialize_job_list(struct job_list *job_list, struct settings *settings);
+bool read_job_list(struct job_list *job_list, int dirfd);
+
+#endif
diff --git a/runner/meson.build b/runner/meson.build
new file mode 100644
index 00000000..773b4f3c
--- /dev/null
+++ b/runner/meson.build
@@ -0,0 +1,24 @@
+jsonc = dependency('json-c', required: true)
+
+runnerlib_sources = [ 'settings.c',
+		      'job_list.c',
+		      'executor.c',
+		      'resultgen.c',
+		    ]
+
+runner_sources = [ 'runner.c',
+		 ]
+
+runnerlib = static_library('igt_runner', runnerlib_sources,
+			   include_directories : inc,
+			   dependencies : jsonc)
+
+runner = executable('runner', runner_sources,
+		    link_with : runnerlib,
+		    install : true,
+		    install_dir : bindir,
+		    dependencies : igt_deps)
+
+results = executable('results', 'results.c',
+		     link_with : runnerlib,
+)
diff --git a/runner/resultgen.c b/runner/resultgen.c
new file mode 100644
index 00000000..978c07be
--- /dev/null
+++ b/runner/resultgen.c
@@ -0,0 +1,864 @@
+#include <ctype.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <json.h>
+
+#include "resultgen.h"
+#include "settings.h"
+#include "executor.h"
+#include "igt_core.h"
+
+static char subtest_result_beg[] = "Subtest ";
+static size_t subtest_result_len = sizeof(subtest_result_beg) - 1;
+static char starting_subtest[] = "Starting subtest: ";
+static size_t starting_len = sizeof(starting_subtest) - 1;
+static char starting_subtest_dmesg[] = ": starting subtest ";
+static size_t starting_subtest_dmesg_len = sizeof(starting_subtest_dmesg) - 1;
+
+struct subtests
+{
+	char **names;
+	size_t size;
+};
+
+static char *find_line(char *haystack, char *needle, size_t needle_size, char *end)
+{
+	char *line = haystack;
+
+	while (line < end) {
+		char *line_end = memchr(line, '\n', end - line);
+
+		if (end - line < needle_size)
+			return NULL;
+		if (!memcmp(line, needle, needle_size))
+			return line;
+		if (line_end == NULL)
+			return NULL;
+		line = line_end + 1;
+	}
+
+	return NULL;
+}
+
+static char *find_either_line(char *haystack,
+			      char *needle1, size_t needle1_size,
+			      char *needle2, size_t needle2_size,
+			      char *end)
+{
+	char *line = haystack;
+
+	while (line < end) {
+		char *line_end = memchr(line, '\n', end - line);
+		size_t linelen = line_end != NULL ? line_end - line : end - line;
+		if ((linelen >= needle1_size && !memcmp(line, needle1, needle1_size)) ||
+		    (linelen >= needle2_size && !memcmp(line, needle2, needle2_size)))
+			return line;
+
+		if (line_end == NULL)
+			return NULL;
+
+		line = line_end + 1;
+	}
+
+	return NULL;
+}
+
+static char *next_line(char *line, char *bufend)
+{
+	char *ret;
+
+	if (!line)
+		return NULL;
+
+	ret = memchr(line, '\n', bufend - line);
+	if (ret)
+		ret++;
+
+	return ret;
+}
+
+static size_t count_lines(char *buf, char *bufend)
+{
+	size_t ret = 0;
+	while ((buf = next_line(buf, bufend)) != NULL)
+		ret++;
+
+	return ret;
+}
+
+static char *lowercase(char *str)
+{
+	char *ret = malloc(strlen(str) + 1);
+	char *q = ret;
+
+	while (*str) {
+		if (isspace(*str))
+			break;
+
+		*q++ = tolower(*str++);
+	}
+	*q = '\0';
+
+	return ret;
+}
+
+static void append_line(char **buf, size_t *buflen, char *line)
+{
+	size_t linelen = strlen(line);
+
+	*buf = realloc(*buf, *buflen + linelen + 1);
+	strcpy(*buf + *buflen, line);
+	*buflen += linelen;
+}
+
+static char *gen_igt_name(char *binary, char *subtest)
+{
+	static char namebuf[256];
+
+	char *lc_binary = lowercase(binary);
+	char *lc_subtest = NULL;
+
+	if (!subtest) {
+		snprintf(namebuf, 256, "igt@%s", lc_binary);
+		free(lc_binary);
+		return namebuf;
+	}
+
+	lc_subtest = lowercase(subtest);
+
+	snprintf(namebuf, 256, "igt@%s@%s", lc_binary, lc_subtest);
+
+	free(lc_binary);
+	free(lc_subtest);
+	return namebuf;
+}
+
+static struct {
+	char *output_str;
+	char *result_str;
+} resultmap[] = {
+	{ "SUCCESS", "pass" },
+	{ "SKIP", "skip" },
+	{ "FAIL", "fail" },
+	{ "CRASH", "crash" },
+	{ "TIMEOUT", "timeout" },
+};
+static void parse_result_string(char *resultstring, size_t len, char **result, double *time)
+{
+	size_t i;
+	size_t wordlen = 0;
+
+	while (wordlen < len) {
+		if (isspace(resultstring[wordlen]))
+			break;
+		wordlen++;
+	}
+
+	for (i = 0; i < (sizeof(resultmap) / sizeof(resultmap[0])); i++) {
+		if (!strncmp(resultstring, resultmap[i].output_str, wordlen)) {
+			*result = resultmap[i].result_str;
+			break;
+		}
+	}
+
+	/* If we don't find a result line, the test didn't finish. */
+	if (!*result)
+		*result = "incomplete";
+
+	wordlen++;
+	if (wordlen < len && resultstring[wordlen] == '(') {
+		wordlen++;
+		char *dup = malloc(len - wordlen);
+		memcpy(dup, resultstring + wordlen, len - wordlen);
+		dup[len - wordlen] = '\0';
+		*time = strtod(dup, NULL);
+
+		free(dup);
+	}
+}
+
+static void parse_subtest_result(char *subtest, char **result, double *time, char *buf, char *bufend)
+{
+	char *line;
+	char *line_end;
+	char *resultstring;
+	size_t linelen;
+	size_t subtestlen = strlen(subtest);
+
+	*result = NULL;
+	*time = 0.0;
+
+	if (!buf) return;
+
+	line = find_line(buf, subtest_result_beg, subtest_result_len, bufend);
+	if (!line) {
+		*result = "incomplete";
+		return;
+	}
+
+	line_end = memchr(line, '\n', bufend - line);
+	linelen = line_end != NULL ? line_end - line : bufend - line;
+
+	if (subtest_result_len + subtestlen + 2 > linelen ||
+	    strncmp(line + subtest_result_len, subtest, subtestlen))
+		return parse_subtest_result(subtest, result, time, line + linelen, bufend);
+
+	resultstring = line + subtest_result_len + subtestlen + 2;
+	parse_result_string(resultstring, linelen - (resultstring - line), result, time);
+}
+
+static struct json_object *get_or_create_json_object(struct json_object *base,
+						     char *key)
+{
+	struct json_object *ret;
+
+	if (json_object_object_get_ex(base, key, &ret))
+		return ret;
+
+	ret = json_object_new_object();
+	json_object_object_add(base, key, ret);
+
+	return ret;
+}
+
+static void add_or_override_result(struct json_object *obj, char *from, char *to)
+{
+	struct json_object *current;
+	const char *oldresult;
+
+	if (!json_object_object_get_ex(obj, "result", &current)) {
+		json_object_object_add(obj, "result",
+				       json_object_new_string(to));
+		return;
+	}
+
+	oldresult = json_object_get_string(current);
+	if (from != NULL && !strcmp(oldresult, from))
+		json_object_object_add(obj, "result",
+				       json_object_new_string(to));
+}
+
+static void add_runtime(struct json_object *obj, double time)
+{
+	double oldtime;
+	struct json_object *timeobj = get_or_create_json_object(obj, "time");
+	struct json_object *oldend;
+
+	json_object_object_add(timeobj, "__type__",
+			       json_object_new_string("TimeAttribute"));
+	json_object_object_add(timeobj, "start",
+			       json_object_new_double(0.0));
+
+	if (!json_object_object_get_ex(timeobj, "end", &oldend)) {
+		json_object_object_add(timeobj, "end",
+				       json_object_new_double(time));
+		return;
+	}
+
+	oldtime = json_object_get_double(oldend);
+	time += oldtime;
+	json_object_object_add(timeobj, "end",
+			       json_object_new_double(time));
+}
+
+static char versionstring[] = "IGT-Version: ";
+static size_t versionlen = sizeof(versionstring) - 1;
+static bool fill_from_output(int fd, char *binary, bool is_stdout,
+			     struct subtests *subtests,
+			     struct json_object *tests)
+{
+	char *buf, *bufend;
+	struct stat statbuf;
+	char *igt_name = NULL;
+	char *igt_version = NULL;
+	size_t igt_version_len = 0;
+	struct json_object *current_test = NULL;
+	size_t i;
+
+	if (fstat(fd, &statbuf))
+		return false;
+
+	buf = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
+	if (!buf)
+		return false;
+
+	bufend = buf + statbuf.st_size;
+
+	if (is_stdout) {
+		char *newline;
+
+		igt_version = find_line(buf, versionstring, versionlen, bufend);
+		if (igt_version) {
+			newline = memchr(igt_version, '\n', bufend - igt_version);
+			igt_version_len = newline - igt_version;
+		} else {
+			igt_version = NULL;
+		}
+	}
+
+	if (subtests->size == 0) {
+		/* No subtests */
+		igt_name = gen_igt_name(binary, NULL);
+		current_test = get_or_create_json_object(tests, igt_name);
+
+		json_object_object_add(current_test, is_stdout ? "out" : "err",
+				       json_object_new_string_len(buf, statbuf.st_size));
+		if (!is_stdout && count_lines(buf, buf + statbuf.st_size) > 2) {
+			add_or_override_result(current_test, "pass", "warn");
+		}
+		if (igt_version) {
+			json_object_object_add(current_test, "igt-version",
+					       json_object_new_string_len(igt_version,
+									  igt_version_len));
+		}
+		return true;
+	}
+
+	for (i = 0; i < subtests->size; i++) {
+		char *begin_subtest;
+		char *subtest_result;
+		char *beg, *end, *startline;
+		int begin_len;
+		int result_len;
+
+		igt_name = gen_igt_name(binary, subtests->names[i]);
+		current_test = get_or_create_json_object(tests, igt_name);
+
+		begin_len = asprintf(&begin_subtest, "%s%s\n", starting_subtest, subtests->names[i]);
+		result_len = asprintf(&subtest_result, "%s%s: ", subtest_result_beg, subtests->names[i]);
+
+		if (begin_len < 0 || result_len < 0) {
+			fprintf(stderr, "Failure generating strings\n");
+			return false;
+		}
+
+		beg = find_line(buf, begin_subtest, begin_len, bufend);
+		end = find_line(buf, subtest_result, result_len, bufend);
+		startline = beg;
+
+		free(begin_subtest);
+		free(subtest_result);
+
+		if (beg == NULL && end == NULL) {
+			/* No output at all */
+			beg = bufend;
+			end = bufend;
+		}
+
+		if (beg == NULL) {
+			/*
+			 * Subtest didn't start, probably skipped from
+			 * fixture already. Find the previous subtest
+			 * output.
+			 */
+			beg = find_either_line(buf,
+					       starting_subtest, starting_len,
+					       subtest_result_beg, subtest_result_len,
+					       end);
+			if (beg == NULL) {
+				beg = buf;
+			} else {
+				while (beg < end) {
+					char *result;
+					beg = next_line(beg, bufend);
+					result = find_either_line(beg,
+								  starting_subtest, starting_len,
+								  subtest_result_beg, subtest_result_len,
+								  bufend);
+					if (result == NULL || result > end) {
+						break;
+					}
+					beg = result;
+				}
+			}
+		} else {
+			/* Include the output after the previous subtest output */
+			char *prevtest = find_either_line(buf,
+							  starting_subtest, starting_len,
+							  subtest_result_beg, subtest_result_len,
+							  beg);
+
+			if (prevtest == NULL) {
+				beg = buf;
+			} else {
+				while (prevtest != NULL && prevtest < beg) {
+					char *result;
+					prevtest = next_line(prevtest, beg);
+					result = find_either_line(prevtest,
+								  starting_subtest, starting_len,
+								  subtest_result_beg, subtest_result_len,
+								  beg);
+					if (result == NULL) {
+						beg = prevtest;
+						break;
+					}
+					prevtest = result;
+				}
+			}
+		}
+
+		if (end == NULL) {
+			/* Incomplete result. Find the next starting subtest or result. */
+			end = next_line(startline, bufend);
+			if (end != NULL) {
+				end = find_either_line(end,
+						       starting_subtest, starting_len,
+						       subtest_result_beg, subtest_result_len,
+						       bufend);
+			}
+			if (end == NULL) {
+				end = bufend;
+			}
+		} else {
+			/* Stretch onwards until the next subtest begins or ends */
+			char *nexttest = next_line(end, bufend);
+			if (nexttest != NULL) {
+				nexttest = find_either_line(nexttest,
+							    starting_subtest, starting_len,
+							    subtest_result_beg, subtest_result_len,
+							    bufend);
+			}
+			if (nexttest != NULL) {
+				end = nexttest;
+			} else {
+				end = bufend;
+			}
+		}
+
+		json_object_object_add(current_test, is_stdout ? "out" : "err",
+				       json_object_new_string_len(beg, end - beg));
+
+		if (is_stdout) {
+			char *result;
+			double time;
+			parse_subtest_result(subtests->names[i], &result, &time, beg, end);
+			add_or_override_result(current_test, NULL, result);
+			add_runtime(current_test, time);
+			if (igt_version) {
+				json_object_object_add(current_test, "igt-version",
+						       json_object_new_string_len(igt_version,
+										  igt_version_len));
+			}
+		} else if (count_lines(startline, end) > 2) {
+			add_or_override_result(current_test, "pass", "warn");
+		}
+	}
+
+	return true;
+}
+
+/*
+ * This regexp controls the kmsg handling. All kernel log records that
+ * have log level of warning or higher convert the result to
+ * dmesg-warn/dmesg-fail unless they match this regexp.
+ */
+
+#define _ "|"
+static const char igt_dmesg_whitelist[] =
+	"ACPI: button: The lid device is not compliant to SW_LID" _
+	"ACPI: .*: Unable to dock!" _
+	"IRQ [0-9]+: no longer affine to CPU[0-9]+" _
+	"IRQ fixup: irq [0-9]+ move in progress, old vector [0-9]+" _
+	/* i915 tests set module options, expected message */
+	"Setting dangerous option [a-z_]+ - tainting kernel" _
+	/* Raw printk() call, uses default log level (warn) */
+	"Suspending console\\(s\\) \\(use no_console_suspend to debug\\)" _
+	"atkbd serio[0-9]+: Failed to (deactivate|enable) keyboard on isa[0-9]+/serio[0-9]+" _
+	"cache: parent cpu[0-9]+ should not be sleeping" _
+	"hpet[0-9]+: lost [0-9]+ rtc interrupts" _
+	/* i915 selftests terminate normally with ENODEV from the
+	 * module load after the testing finishes, which produces this
+	 * message.
+	 */
+	"i915: probe of [0-9:.]+ failed with error -25" _
+	/* swiotbl warns even when asked not to */
+	"mock: DMA: Out of SW-IOMMU space for [0-9]+ bytes" _
+	"usb usb[0-9]+: root hub lost power or was reset"
+	;
+#undef _
+
+static regex_t re;
+
+static bool init_regex_whitelist()
+{
+	int status = -1;
+
+	if (status == -1) {
+		if (regcomp(&re, igt_dmesg_whitelist, REG_EXTENDED | REG_NOSUB) != 0) {
+			fprintf(stderr, "Cannot compile dmesg whitelist regexp\n");
+			status = 0;
+			return false;
+		}
+
+		status = 1;
+	}
+
+	return status;
+}
+
+static bool fill_from_dmesg(int fd, char *binary,
+			    struct subtests *subtests,
+			    struct json_object *tests)
+{
+	char *line = NULL, *warnings = NULL, *dmesg = NULL;
+	size_t linelen = 0, warningslen = 0, dmesglen = 0;
+	struct json_object *current_test = NULL;
+	FILE *f = fdopen(fd, "r");
+	char *igt_name = NULL;
+	ssize_t read;
+	size_t i;
+
+	if (!f) {
+		return false;
+	}
+
+	if (init_regex_whitelist() != 1) {
+		fclose(f);
+		return false;
+	}
+
+	while ((read = getline(&line, &linelen, f)) > 0) {
+		char formatted[256];
+		unsigned flags;
+		unsigned long long seq, ts_usec;
+		char continuation;
+		char *message, *subtest;
+		int s;
+
+		s = sscanf(line, "%u,%llu,%llu,%c;", &flags, &seq, &ts_usec, &continuation);
+
+		if (s != 4) {
+			/*
+			 * Machine readable key/value pairs begin with
+			 * a space. We ignore them.
+			 */
+			if (line[0] != ' ') {
+				fprintf(stderr, "Cannot parse kmsg record: %s\n", line);
+			}
+
+			continue;
+		}
+
+		message = strchr(line, ';');
+		if (!message) {
+			fprintf(stderr, "No ; found, this shouldn't happen\n");
+			return false;
+		}
+		message++;
+		snprintf(formatted, sizeof(formatted), "<%u> [%llu.%06llu] %s",
+			 flags & 0x07, ts_usec / 1000000, ts_usec % 1000000, message);
+
+		if ((subtest = strstr(message, starting_subtest_dmesg)) != NULL) {
+			if (current_test != NULL) {
+				/* Done with the previous subtest, file up */
+				json_object_object_add(current_test, "dmesg",
+						       json_object_new_string_len(dmesg, dmesglen));
+				if (warnings) {
+					json_object_object_add(current_test, "dmesg-warnings",
+							       json_object_new_string_len(warnings, warningslen));
+					add_or_override_result(current_test, "pass", "dmesg-warn");
+					add_or_override_result(current_test, "fail", "dmesg-fail");
+					add_or_override_result(current_test, "warn", "dmesg-warn");
+				}
+				free(dmesg);
+				free(warnings);
+				dmesg = warnings = NULL;
+				dmesglen = warningslen = 0;
+			}
+
+			subtest += starting_subtest_dmesg_len;
+			igt_name = gen_igt_name(binary, subtest);
+			current_test = get_or_create_json_object(tests, igt_name);
+		}
+
+		if ((flags & 0x07) <= 4 && continuation != 'c' &&
+		    regexec(&re, message, (size_t)0, NULL, 0) == REG_NOMATCH) {
+			append_line(&warnings, &warningslen, formatted);
+		}
+		append_line(&dmesg, &dmesglen, formatted);
+	}
+
+	if (current_test != NULL) {
+		json_object_object_add(current_test, "dmesg",
+				       json_object_new_string_len(dmesg, dmesglen));
+		if (warnings) {
+			json_object_object_add(current_test, "dmesg-warnings",
+					       json_object_new_string_len(warnings, warningslen));
+			add_or_override_result(current_test, "pass", "dmesg-warn");
+			add_or_override_result(current_test, "fail", "dmesg-fail");
+			add_or_override_result(current_test, "warn", "dmesg-warn");
+		}
+	} else {
+		/*
+		 * Didn't get any subtest messages at all. If there
+		 * are subtests, add all of the dmesg gotten to all of
+		 * them.
+		 */
+		for (i = 0; i < subtests->size; i++) {
+			igt_name = gen_igt_name(binary, subtests->names[i]);
+			current_test = get_or_create_json_object(tests, igt_name);
+			json_object_object_add(current_test, "dmesg",
+					       json_object_new_string_len(dmesg, dmesglen));
+			/*
+			 * Don't bother with warnings, any subtests
+			 * there are would have skip as their result
+			 * anyway.
+			 */
+		}
+
+		if (i == 0) {
+			/* There were no subtests */
+			igt_name = gen_igt_name(binary, NULL);
+			current_test = get_or_create_json_object(tests, igt_name);
+			json_object_object_add(current_test, "dmesg",
+					       json_object_new_string_len(dmesg, dmesglen));
+			if (warnings) {
+				json_object_object_add(current_test, "dmesg-warnings",
+						       json_object_new_string_len(warnings, warningslen));
+				add_or_override_result(current_test, "pass", "dmesg-warn");
+				add_or_override_result(current_test, "fail", "dmesg-fail");
+				add_or_override_result(current_test, "warn", "dmesg-warn");
+			}
+		}
+	}
+
+	/*
+	 * Add an empty string as the dmesg of all subtests that
+	 * didn't get any dmesg yet.
+	 */
+	for (i = 0; i < subtests->size; i++) {
+		igt_name = gen_igt_name(binary, subtests->names[i]);
+		current_test = get_or_create_json_object(tests, igt_name);
+		if (!json_object_object_get_ex(current_test, "dmesg", NULL)) {
+			json_object_object_add(current_test, "dmesg",
+					       json_object_new_string(""));
+		}
+	}
+
+	free(dmesg);
+	free(warnings);
+	fclose(f);
+	return true;
+}
+
+static char *result_from_exitcode(int exitcode)
+{
+	switch (exitcode) {
+	case IGT_EXIT_TIMEOUT:
+		return "timeout";
+	case IGT_EXIT_SKIP:
+		return "skip";
+	case IGT_EXIT_SUCCESS:
+		return "pass";
+	case IGT_EXIT_INVALID:
+		return "notrun";
+	default:
+		return "fail";
+	}
+}
+
+static void add_subtest(struct subtests *subtests, char *subtest)
+{
+	size_t len = strlen(subtest);
+
+	if (len == 0)
+		return;
+
+	if (subtest[len - 1] == '\n')
+		subtest[len - 1] = '\0';
+
+	subtests->size++;
+	subtests->names = realloc(subtests->names, sizeof(*subtests->names) * subtests->size);
+	subtests->names[subtests->size - 1] = subtest;
+}
+
+static bool fill_from_journal(int fd, char *binary,
+				  struct subtests *subtests,
+				  struct json_object *tests)
+{
+	FILE *f = fdopen(fd, "r");
+	char *line = NULL;
+	size_t linelen = 0;
+	ssize_t read;
+	char exitline[] = "exit:";
+	size_t exitlen = sizeof(exitline) - 1;
+	char timeoutline[] = "timeout:";
+	size_t timeoutlen = sizeof(timeoutline) - 1;
+	int exitcode = 10000;
+
+	while ((read = getline(&line, &linelen, f)) > 0) {
+		if (read >= exitlen && !memcmp(line, exitline, exitlen)) {
+			char *p = strchr(line, '(');
+			char *igt_name = gen_igt_name(binary, NULL);
+			double time = 0.0;
+			struct json_object *obj = get_or_create_json_object(tests, igt_name);
+
+			exitcode = atoi(line + exitlen);
+
+			if (p) {
+				time = strtod(p + 1, NULL);
+			}
+
+			add_runtime(obj, time);
+		} else if (read >= timeoutlen && !memcmp(line, timeoutline, timeoutlen)) {
+			if (subtests->size) {
+				char *last_subtest = subtests->names[subtests->size - 1];
+				char *igt_name = gen_igt_name(binary, last_subtest);
+				char *p = strchr(line, '(');
+				double time = 0.0;
+				struct json_object *obj = get_or_create_json_object(tests, igt_name);
+
+				json_object_object_add(obj, "result",
+						       json_object_new_string("timeout"));
+
+				if (p) {
+					time = strtod(p + 1, NULL);
+				}
+
+				add_runtime(obj, time);
+
+				igt_name = gen_igt_name(binary, NULL);
+				obj = get_or_create_json_object(tests, igt_name);
+				add_runtime(obj, time);
+			}
+		} else {
+			add_subtest(subtests, strdup(line));
+		}
+	}
+
+	if (subtests->size == 0 && exitcode != 10000) {
+		char *igt_name = gen_igt_name(binary, NULL);
+		struct json_object *obj = get_or_create_json_object(tests, igt_name);
+		char *result = result_from_exitcode(exitcode);
+		json_object_object_add(obj, "result",
+				       json_object_new_string(result));
+	}
+
+	free(line);
+	return true;
+}
+
+static bool parse_test_directory(int dirfd, char *binary, struct json_object *tests)
+{
+	int fds[_F_LAST];
+	struct subtests subtests = {};
+
+	if (!open_output_files(dirfd, fds, false)) {
+		fprintf(stderr, "Error opening output files\n");
+		return false;
+	}
+
+	/* fill_from_journal also fills the subtests struct */
+	if (!fill_from_journal(fds[_F_JOURNAL], binary, &subtests, tests)) {
+		fprintf(stderr, "Error reading from journal\n");
+		return false;
+	}
+
+	/* Order of these is important */
+	if (!fill_from_output(fds[_F_OUT], binary, true, &subtests, tests) ||
+	    !fill_from_output(fds[_F_ERR], binary, false, &subtests, tests) ||
+	    !fill_from_dmesg(fds[_F_DMESG], binary, &subtests, tests)) {
+		fprintf(stderr, "Error parsing output files\n");
+		return false;
+	}
+
+	close_outputs(fds);
+
+	return true;
+}
+
+bool generate_results(int dirfd)
+{
+	struct settings settings;
+	struct job_list job_list;
+	struct json_object *obj, *tests;
+	int resultsfd, testdirfd, unamefd;
+	const char *json_string;
+	size_t i;
+
+	init_settings(&settings);
+	init_job_list(&job_list);
+
+	if (!read_settings(&settings, dirfd)) {
+		fprintf(stderr, "resultgen: Cannot parse settings\n");
+		return false;
+	}
+
+	if (!read_job_list(&job_list, dirfd)) {
+		fprintf(stderr, "resultgen: Cannot parse job list\n");
+		return false;
+	}
+
+	/* TODO: settings.overwrite */
+	if ((resultsfd = openat(dirfd, "results.json", O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0) {
+		fprintf(stderr, "resultgen: Cannot create results file\n");
+		return false;
+	}
+
+	obj = json_object_new_object();
+	json_object_object_add(obj, "__type__", json_object_new_string("TestrunResult"));
+	json_object_object_add(obj, "results_version", json_object_new_int(9));
+	json_object_object_add(obj, "name",
+			       settings.name ?
+			       json_object_new_string(settings.name) :
+			       json_object_new_string(""));
+
+	if ((unamefd = openat(dirfd, "uname.txt", O_RDONLY)) >= 0) {
+		char buf[128];
+		ssize_t r = read(unamefd, buf, 128);
+
+		if (r > 0 && buf[r - 1] == '\n')
+			r--;
+
+		json_object_object_add(obj, "uname",
+				       json_object_new_string_len(buf, r));
+		close(unamefd);
+	}
+
+	/* lspci */
+	/* results_version */
+	/* glxinfo */
+	/* wglinfo */
+	/* clinfo */
+	/* options */
+	/* time_elapsed */
+	/* totals */
+
+	tests = json_object_new_object();
+	json_object_object_add(obj, "tests", tests);
+
+	for (i = 0; i < job_list.size; i++) {
+		char name[16];
+
+		snprintf(name, 16, "%zd", i);
+		if ((testdirfd = openat(dirfd, name, O_DIRECTORY | O_RDONLY)) < 0) {
+			fprintf(stderr, "Warning: Cannot open result directory %s\n", name);
+			break;
+		}
+
+		if (!parse_test_directory(testdirfd, job_list.entries[i].binary, tests)) {
+			close(resultsfd);
+			return false;
+		}
+	}
+
+	json_string = json_object_to_json_string_ext(obj, JSON_C_TO_STRING_PRETTY);
+	write(resultsfd, json_string, strlen(json_string));
+	return true;
+}
+
+bool generate_results_path(char *resultspath)
+{
+	int dirfd = open(resultspath, O_DIRECTORY | O_RDONLY);
+
+	if (dirfd < 0)
+		return false;
+
+	return generate_results(dirfd);
+}
diff --git a/runner/resultgen.h b/runner/resultgen.h
new file mode 100644
index 00000000..83a0876b
--- /dev/null
+++ b/runner/resultgen.h
@@ -0,0 +1,9 @@
+#ifndef RUNNER_RESULTGEN_H
+#define RUNNER_RESULTGEN_H
+
+#include <stdbool.h>
+
+bool generate_results(int dirfd);
+bool generate_results_path(char *resultspath);
+
+#endif
diff --git a/runner/results.c b/runner/results.c
new file mode 100644
index 00000000..3eb7cb15
--- /dev/null
+++ b/runner/results.c
@@ -0,0 +1,26 @@
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "resultgen.h"
+
+int main(int argc, char **argv)
+{
+	int dirfd;
+
+	if (argc < 2)
+		exit(1);
+
+	dirfd = open(argv[1], O_DIRECTORY | O_RDONLY);
+	if (dirfd < 0)
+		exit(1);
+
+	if (generate_results(dirfd)) {
+		printf("Results generated\n");
+		exit(0);
+	}
+
+	exit(1);
+}
diff --git a/runner/runner.c b/runner/runner.c
new file mode 100644
index 00000000..b685786a
--- /dev/null
+++ b/runner/runner.c
@@ -0,0 +1,40 @@
+#include <stdio.h>
+#include <string.h>
+
+#include "settings.h"
+#include "job_list.h"
+#include "executor.h"
+#include "resultgen.h"
+
+int main(int argc, char **argv)
+{
+	struct settings settings;
+	struct job_list job_list;
+	struct execute_state state;
+
+	init_settings(&settings);
+	init_job_list(&job_list);
+
+	if (!parse_options(argc, argv, &settings)) {
+		return 1;
+	}
+
+	if (!create_job_list(&job_list, &settings)) {
+		return 1;
+	}
+
+	if (!initialize_execute_state(&state, &settings, &job_list)) {
+		return 1;
+	}
+
+	if (!execute(&state, &settings, &job_list)) {
+		return 1;
+	}
+
+	if (!generate_results_path(settings.results_path)) {
+		return 1;
+	}
+
+	printf("Done.\n");
+	return 0;
+}
diff --git a/runner/settings.c b/runner/settings.c
new file mode 100644
index 00000000..50e4ea10
--- /dev/null
+++ b/runner/settings.c
@@ -0,0 +1,506 @@
+#include "settings.h"
+
+#include <fcntl.h>
+#include <getopt.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+enum {
+	OPT_ABORT_ON_ERROR,
+	OPT_TEST_LIST,
+	OPT_IGNORE_MISSING,
+	OPT_HELP = 'h',
+	OPT_NAME = 'n',
+	OPT_DRY_RUN = 'd',
+	OPT_INCLUDE = 't',
+	OPT_EXCLUDE = 'x',
+	OPT_SYNC = 's',
+	OPT_LOG_LEVEL = 'l',
+	OPT_OVERWRITE = 'o',
+	OPT_MULTIPLE = 'm',
+	OPT_TIMEOUT = 'c',
+	OPT_WATCHDOG = 'g',
+};
+
+static struct {
+	int level;
+	const char *name;
+} log_levels[] = {
+	{ LOG_LEVEL_NORMAL, "normal" },
+	{ LOG_LEVEL_QUIET, "quiet" },
+	{ LOG_LEVEL_VERBOSE, "verbose" },
+	{ 0, 0 },
+};
+
+static bool set_log_level(struct settings* settings, const char *level)
+{
+	int c;
+
+	for (c = 0; log_levels[c].name != NULL; c++) {
+		if (!strcmp(level, log_levels[c].name)) {
+			settings->log_level = log_levels[c].level;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static const char *usage_str =
+	"usage: runner [options] [test_root] results-path\n\n"
+	"Options:\n"
+	" Piglit compatible:\n"
+	"  -h, --help            Show this help message and exit\n"
+	"  -n <test name>, --name <test name>\n"
+	"                        Name of this test run\n"
+	"  -d, --dry-run         Do not execute the tests\n"
+	"  -t <regex>, --include-tests <regex>\n"
+	"                        Run only matching tests (can be used more than once)\n"
+	"  -x <regex>, --exclude-tests <regex>\n"
+	"                        Exclude matching tests (can be used more than once)\n"
+	"  --abort-on-monitored-error\n"
+	"                        Abort execution when a fatal condition is detected.\n"
+	"                        <TODO>\n"
+	"  -s, --sync            Sync results to disk after every test\n"
+	"  -l {quiet,verbose,dummy}, --log-level {quiet,verbose,dummy}\n"
+	"                        Set the logger verbosity level\n"
+	"  --test-list TEST_LIST\n"
+	"                        A file containing a list of tests to run\n"
+	"  -o, --overwrite       If the results-path already exists, delete it\n"
+	"  --ignore-missing      Ignored but accepted, for piglit compatibility\n"
+	"\n"
+	" Incompatible options:\n"
+	"  -m, --multiple-mode   Run multiple subtests in the same binary execution.\n"
+	"                        If a testlist file is given, consecutive subtests are\n"
+	"                        run in the same execution if they are from the same\n"
+	"                        binary. Note that in that case relative ordering of the\n"
+	"                        subtest execution is dictated by the test binary, not\n"
+	"                        the testlist\n"
+	"  --inactivity-timeout <seconds>\n"
+	"                        Kill the running test after <seconds> of inactivity in\n"
+	"                        the test's stdout, stderr, or dmesg\n"
+	"  --use-watchdog        Use hardware watchdog for lethal enforcement of the\n"
+	"                        above timeout. Killing the test process is still\n"
+	"                        attempted at timeout trigger.\n"
+	"  [test_root]           Directory that contains the IGT tests. The environment\n"
+	"                        variable IGT_TEST_ROOT will be used if set, overriding\n"
+	"                        this option if given.\n"
+	;
+
+static void usage(const char *extra_message, bool use_stderr)
+{
+	FILE *f = use_stderr ? stderr : stdout;
+
+	if (extra_message)
+		fprintf(f, "%s\n\n", extra_message);
+
+	fprintf(f, "%s", usage_str);
+}
+
+static bool add_regex(struct regex_list *list, char *new)
+{
+	regex_t *regex;
+	size_t buflen;
+	char *buf;
+	int s;
+
+	regex = malloc(sizeof(*regex));
+
+	if ((s = regcomp(regex, new,
+			 REG_EXTENDED | REG_NOSUB)) != 0) {
+		buflen = regerror(s, regex, NULL, 0);
+		buf = malloc(buflen);
+		regerror(s, regex, buf, buflen);
+		usage(buf, true);
+
+		free(buf);
+		regfree(regex);
+		free(regex);
+		return false;
+	}
+
+	list->regexes = realloc(list->regexes,
+				(list->size + 1) * sizeof(*list->regexes));
+	list->regex_strings = realloc(list->regex_strings,
+				      (list->size + 1) * sizeof(*list->regex_strings));
+	list->regexes[list->size] = regex;
+	list->regex_strings[list->size] = new;
+	list->size++;
+
+	return true;
+}
+
+static void free_regexes(struct regex_list *regexes)
+{
+	size_t i;
+
+	for (i = 0; i < regexes->size; i++) {
+		free(regexes->regex_strings[i]);
+		regfree(regexes->regexes[i]);
+		free(regexes->regexes[i]);
+	}
+	free(regexes->regex_strings);
+	free(regexes->regexes);
+}
+
+static bool readable_file(char *filename)
+{
+	return !access(filename, R_OK);
+}
+
+void init_settings(struct settings *settings)
+{
+	memset(settings, 0, sizeof(*settings));
+}
+
+void free_settings(struct settings *settings)
+{
+	free(settings->test_list);
+	free(settings->name);
+	free(settings->test_root);
+	free(settings->results_path);
+
+	free_regexes(&settings->include_regexes);
+	free_regexes(&settings->exclude_regexes);
+
+	init_settings(settings);
+}
+
+bool parse_options(int argc, char **argv,
+		   struct settings *settings)
+{
+	int c;
+	char *env_test_root;
+
+	static struct option long_options[] = {
+		{"help", no_argument, NULL, OPT_HELP},
+		{"name", required_argument, NULL, OPT_NAME},
+		{"dry-run", no_argument, NULL, OPT_DRY_RUN},
+		{"include-tests", required_argument, NULL, OPT_INCLUDE},
+		{"exclude-tests", required_argument, NULL, OPT_EXCLUDE},
+		{"abort-on-monitored-error", no_argument, NULL, OPT_ABORT_ON_ERROR},
+		{"sync", no_argument, NULL, OPT_SYNC},
+		{"log-level", required_argument, NULL, OPT_LOG_LEVEL},
+		{"test-list", required_argument, NULL, OPT_TEST_LIST},
+		{"overwrite", no_argument, NULL, OPT_OVERWRITE},
+		{"ignore-missing", no_argument, NULL, OPT_IGNORE_MISSING},
+		{"multiple-mode", no_argument, NULL, OPT_MULTIPLE},
+		{"inactivity-timeout", required_argument, NULL, OPT_TIMEOUT},
+		{"use-watchdog", no_argument, NULL, OPT_WATCHDOG},
+		{ 0, 0, 0, 0},
+	};
+
+	free_settings(settings);
+
+	optind = 1;
+
+	while ((c = getopt_long(argc, argv, "hn:dt:x:sl:om", long_options, NULL)) != -1) {
+		switch (c) {
+		case OPT_HELP:
+			usage(NULL, false);
+			goto error;
+		case OPT_NAME:
+			settings->name = strdup(optarg);
+			break;
+		case OPT_DRY_RUN:
+			settings->dry_run = true;
+			break;
+		case OPT_INCLUDE:
+			if (!add_regex(&settings->include_regexes, strdup(optarg)))
+				goto error;
+			break;
+		case OPT_EXCLUDE:
+			if (!add_regex(&settings->exclude_regexes, strdup(optarg)))
+				goto error;
+			break;
+		case OPT_ABORT_ON_ERROR:
+			settings->abort_on_error = true;
+			break;
+		case OPT_SYNC:
+			settings->sync = true;
+			break;
+		case OPT_LOG_LEVEL:
+			if (!set_log_level(settings, optarg)) {
+				usage("Cannot parse log level", true);
+				goto error;
+			}
+			break;
+		case OPT_TEST_LIST:
+			settings->test_list = absolute_path(optarg);
+			break;
+		case OPT_OVERWRITE:
+			settings->overwrite = true;
+			break;
+		case OPT_IGNORE_MISSING:
+			/* Ignored, piglit compatibility */
+			break;
+		case OPT_MULTIPLE:
+			settings->multiple_mode = true;
+			break;
+		case OPT_TIMEOUT:
+			settings->inactivity_timeout = atoi(optarg);
+			break;
+		case OPT_WATCHDOG:
+			settings->use_watchdog = true;
+			break;
+		case '?':
+			usage(NULL, true);
+			goto error;
+		default:
+			usage("Cannot parse options", true);
+			goto error;
+		}
+	}
+
+	switch (argc - optind) {
+	case 2:
+		settings->test_root = absolute_path(argv[optind]);
+		++optind;
+		/* fallthrough */
+	case 1:
+		settings->results_path = absolute_path(argv[optind]);
+		break;
+	case 0:
+		usage("Results-path missing", true);
+		goto error;
+	default:
+		usage("Extra arguments after results-path", true);
+		goto error;
+	}
+
+	if ((env_test_root = getenv("IGT_TEST_ROOT")) != NULL) {
+		free(settings->test_root);
+		settings->test_root = absolute_path(env_test_root);
+	}
+
+	if (!settings->test_root) {
+		usage("Test root not set", true);
+		goto error;
+	}
+
+	if (!settings->name) {
+		char *name = strdup(settings->results_path);
+		settings->name = strdup(basename(name));
+		free(name);
+	}
+
+	return true;
+
+ error:
+	free_settings(settings);
+	return false;
+}
+
+bool validate_settings(struct settings *settings)
+{
+	int dirfd, fd;
+
+	if (settings->test_list && !readable_file(settings->test_list)) {
+		usage("Cannot open test-list file", true);
+		return false;
+	}
+
+	if (!settings->results_path) {
+		usage("No results-path set; this shouldn't happen", true);
+		return false;
+	}
+
+	if (!settings->test_root) {
+		usage("No test root set; this shouldn't happen", true);
+		return false;
+	}
+
+	dirfd = open(settings->test_root, O_DIRECTORY | O_RDONLY);
+	if (dirfd < 0) {
+		fprintf(stderr, "Test directory %s cannot be opened\n", settings->test_root);
+		return false;
+	}
+
+	fd = openat(dirfd, "test-list.txt", O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "Cannot open %s/test-list.txt\n", settings->test_root);
+		close(dirfd);
+		return false;
+	}
+
+	close(fd);
+	close(dirfd);
+
+	return true;
+}
+
+char *absolute_path(char *path)
+{
+	char *result = NULL;
+	char *tmppath, *tmpname;
+
+	result = realpath(path, NULL);
+	if (result != NULL)
+		return result;
+
+	tmppath = strdup(path);
+	tmpname = dirname(tmppath);
+	free(result);
+	result = realpath(tmpname, NULL);
+	free(tmppath);
+
+	if (result != NULL) {
+		tmppath = strdup(path);
+		tmpname = basename(tmppath);
+		strcat(result, "/");
+		strcat(result, tmpname);
+		free(tmppath);
+		return result;
+	}
+
+	free(result);
+	return NULL;
+}
+
+static char settings_filename[] = "metadata.txt";
+bool serialize_settings(struct settings *settings)
+{
+#define SERIALIZE_LINE(f, s, name, format) fprintf(f, "%s : " format "\n", #name, s->name)
+
+	int dirfd, fd;
+	FILE *f;
+
+	if (!settings->results_path) {
+		usage("No results-path set; this shouldn't happen", true);
+		return false;
+	}
+
+	if ((dirfd = open(settings->results_path, O_DIRECTORY | O_RDONLY)) < 0) {
+		mkdir(settings->results_path, 0755);
+		if ((dirfd = open(settings->results_path, O_DIRECTORY | O_RDONLY)) < 0) {
+			usage("Creating results-path failed", true);
+			return false;
+		}
+	}
+
+	if ((fd = openat(dirfd, settings_filename, O_RDONLY)) >= 0) {
+		close(fd);
+
+		if (!settings->overwrite) {
+			/* Serialization data already exists, not overwriting */
+			close(dirfd);
+			return false;
+		}
+
+		if (unlinkat(dirfd, settings_filename, 0) != 0) {
+			usage("Error overwriting old settings metadata", true);
+			close(dirfd);
+			return false;
+		}
+	}
+
+	if ((fd = openat(dirfd, settings_filename, O_CREAT | O_EXCL | O_WRONLY, 0666)) < 0) {
+		usage("Creating settings serialization file failed", true);
+		close(dirfd);
+		return false;
+	}
+
+	f = fdopen(fd, "w");
+	if (!f) {
+		close(fd);
+		close(dirfd);
+		return false;
+	}
+
+	SERIALIZE_LINE(f, settings, abort_on_error, "%d");
+	if (settings->test_list)
+		SERIALIZE_LINE(f, settings, test_list, "%s");
+	if (settings->name)
+		SERIALIZE_LINE(f, settings, name, "%s");
+	SERIALIZE_LINE(f, settings, dry_run, "%d");
+	SERIALIZE_LINE(f, settings, sync, "%d");
+	SERIALIZE_LINE(f, settings, log_level, "%d");
+	SERIALIZE_LINE(f, settings, overwrite, "%d");
+	SERIALIZE_LINE(f, settings, multiple_mode, "%d");
+	SERIALIZE_LINE(f, settings, inactivity_timeout, "%d");
+	SERIALIZE_LINE(f, settings, use_watchdog, "%d");
+	SERIALIZE_LINE(f, settings, test_root, "%s");
+	SERIALIZE_LINE(f, settings, results_path, "%s");
+
+	if (settings->sync) {
+		fsync(fd);
+		fsync(dirfd);
+	}
+
+	fclose(f);
+	close(dirfd);
+	return true;
+
+#undef SERIALIZE_LINE
+}
+
+static char *maybe_strdup(char *str)
+{
+	if (!str)
+		return NULL;
+
+	return strdup(str);
+}
+
+bool read_settings(struct settings *settings, int dirfd)
+{
+#define PARSE_LINE(s, name, val, field, write) \
+	if (!strcmp(name, #field)) {	       \
+		s->field = write;	       \
+		free(name);		       \
+		free(val);		       \
+		name = val = NULL;	       \
+		continue;		       \
+	}
+
+	int fd;
+	FILE *f;
+	char *name = NULL, *val = NULL;
+
+	free_settings(settings);
+
+	if ((fd = openat(dirfd, settings_filename, O_RDONLY)) < 0)
+		return false;
+
+	f = fdopen(fd, "r");
+	if (!f) {
+		close(fd);
+		return false;
+	}
+
+	while (fscanf(f, "%ms : %ms", &name, &val) == 2) {
+		int numval = atoi(val);
+		PARSE_LINE(settings, name, val, abort_on_error, numval);
+		PARSE_LINE(settings, name, val, test_list, maybe_strdup(val));
+		PARSE_LINE(settings, name, val, name, maybe_strdup(val));
+		PARSE_LINE(settings, name, val, dry_run, numval);
+		PARSE_LINE(settings, name, val, sync, numval);
+		PARSE_LINE(settings, name, val, log_level, numval);
+		PARSE_LINE(settings, name, val, overwrite, numval);
+		PARSE_LINE(settings, name, val, multiple_mode, numval);
+		PARSE_LINE(settings, name, val, inactivity_timeout, numval);
+		PARSE_LINE(settings, name, val, use_watchdog, numval);
+		PARSE_LINE(settings, name, val, test_root, maybe_strdup(val));
+		PARSE_LINE(settings, name, val, results_path, maybe_strdup(val));
+
+		printf("Warning: Unknown field in settings file: %s = %s\n",
+		       name, val);
+		free(name);
+		free(val);
+		name = val = NULL;
+	}
+
+	free(name);
+	free(val);
+	fclose(f);
+
+	return true;
+
+#undef PARSE_LINE
+}
diff --git a/runner/settings.h b/runner/settings.h
new file mode 100644
index 00000000..9d1f03fb
--- /dev/null
+++ b/runner/settings.h
@@ -0,0 +1,111 @@
+#ifndef RUNNER_SETTINGS_H
+#define RUNNER_SETTINGS_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <sys/types.h>
+#include <regex.h>
+
+enum {
+	LOG_LEVEL_NORMAL = 0,
+	LOG_LEVEL_QUIET = -1,
+	LOG_LEVEL_VERBOSE = 1,
+};
+
+struct regex_list {
+	char **regex_strings;
+	regex_t** regexes;
+	size_t size;
+};
+
+struct settings {
+	bool abort_on_error;
+	char *test_list;
+	char *name;
+	bool dry_run;
+	struct regex_list include_regexes;
+	struct regex_list exclude_regexes;
+	bool sync;
+	int log_level;
+	bool overwrite;
+	bool multiple_mode;
+	int inactivity_timeout;
+	bool use_watchdog;
+	char *test_root;
+	char *results_path;
+};
+
+/**
+ * init_settings:
+ *
+ * Initializes a settings object to an empty state (all values NULL, 0
+ * or false).
+ *
+ * @settings: Object to initialize. Storage for it must exist.
+ */
+void init_settings(struct settings *settings);
+
+/**
+ * free_settings:
+ *
+ * Releases all allocated resources for a settings object and
+ * initializes it to an empty state (see #init_settings).
+ *
+ * @settings: Object to release and initialize.
+ */
+void free_settings(struct settings *settings);
+
+/**
+ * parse_options:
+ *
+ * Parses command line options and sets the settings object to
+ * designated values.
+ *
+ * The function can be called again on the same settings object. The
+ * old values will be properly released and cleared. On a parse
+ * failure, the settings object will be in an empty state (see
+ * #init_settings) and usage instructions will be printed with an
+ * error message.
+ *
+ * @argc: Argument count
+ * @argv: Argument array. First element is the program name.
+ * @settings: Settings object to fill with values. Must have proper
+ * storage.
+ *
+ * Returns: True on successful parse, false on error.
+ */
+bool parse_options(int argc, char **argv,
+		   struct settings *settings);
+
+/**
+ * validate_settings:
+ *
+ * Checks the settings object against the system to see if executing
+ * on it can be done. Checks pathnames for existence and access
+ * rights. Note that this function will not check that the designated
+ * job listing (through a test-list file or the -t/-x flags) yields a
+ * non-zero amount of testing to be done. On errors, usage
+ * instructions will be printed with an error message.
+ *
+ * @settings: Settings object to check.
+ *
+ * Returns: True on valid settings, false on any error.
+ */
+bool validate_settings(struct settings *settings);
+
+/* TODO: Better place for this */
+char *absolute_path(char *path);
+
+/**
+ * serialize_settings:
+ *
+ * Serializes the settings object to a file in the results_path
+ * directory.
+ *
+ * @settings: Settings object to serialize.
+ */
+bool serialize_settings(struct settings *settings);
+
+bool read_settings(struct settings *settings, int dirfd);
+
+#endif
-- 
2.14.1



More information about the igt-dev mailing list