summaryrefslogtreecommitdiff
path: root/runner
diff options
context:
space:
mode:
authorPetri Latvala <petri.latvala@intel.com>2018-08-08 14:07:00 +0300
committerPetri Latvala <petri.latvala@intel.com>2018-08-09 10:33:32 +0300
commit18c1e7525591b98b53321c26464f3181a5f7cce1 (patch)
treeb2752ef34feaa0a1b729029550298f7615e2fdf1 /runner
parentf0bec8572bfc0960841435155002455fc1dabd67 (diff)
runner: New test runner
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@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@somebinary" will have the runtime of the binary "somebinary", whereas "igt@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. v2: - use clock handling from igt_core instead of copypaste - install results binary - less magic numbers - scanf doesn't give empty strings after all - use designated array initialization with _F_JOURNAL and pals - add more comments to dump_dmesg - use signal in kill_child instead of bool - use more 'usual' return values for execute_entry - use signal number instead of magic integers - use IGT_EXIT_INVALID instead of magic 79 - properly remove files in clear_test_result_directory() - remove magic numbers - warn if results directory contains extra files - fix naming in matches_any - construct command line in a cleaner way in add_subtests() - clarify error in filtered_job_list - replace single string fprintfs with fputs - use getline() more sanely - refactor string constants to a shared header - explain non-nul-terminated string handling in resultgen - saner line parsing - rename gen_igt_name to generate_piglit_name - clean up parse_result_string - explain what we're parsing in resultgen - explain the runtime accumulation in add_runtime - refactor result overriding - stop passing needle sizes to find_line functions - refactor stdout/stderr parsing - fix regex whitelist compiling - add TODO for suppressions.txt - refactor dmesg parsing - fill_from_journal returns void - explain missing result fields with TODO comments - log_level parsing with typeof - pass stdout/stderr to usage() instead of a bool - fix absolute_path overflow - refactor settings serialization - remove maybe_strdup function - refactor job list serialization - refactor resuming, add new resume binary - catch mmap failure correctly v3: - rename runner to igt_runner, etc - add meson option for building the runner - use UPPER_CASE names for string constants - add TODO comments for future refactoring - add a midding close() - const correctness where applicable - also build with autotools Signed-off-by: Petri Latvala <petri.latvala@intel.com> Reviewed-by: Arkadiusz Hiler <arkadiusz.hiler@intel.com>
Diffstat (limited to 'runner')
-rw-r--r--runner/.gitignore3
-rw-r--r--runner/Makefile.am31
-rw-r--r--runner/executor.c1022
-rw-r--r--runner/executor.h49
-rw-r--r--runner/job_list.c484
-rw-r--r--runner/job_list.h37
-rw-r--r--runner/meson.build41
-rw-r--r--runner/output_strings.h55
-rw-r--r--runner/resultgen.c962
-rw-r--r--runner/resultgen.h9
-rw-r--r--runner/results.c26
-rw-r--r--runner/resume.c47
-rw-r--r--runner/runner.c40
-rw-r--r--runner/settings.c502
-rw-r--r--runner/settings.h111
15 files changed, 3419 insertions, 0 deletions
diff --git a/runner/.gitignore b/runner/.gitignore
new file mode 100644
index 00000000..e3919fa7
--- /dev/null
+++ b/runner/.gitignore
@@ -0,0 +1,3 @@
+igt_runner
+igt_resume
+igt_results
diff --git a/runner/Makefile.am b/runner/Makefile.am
new file mode 100644
index 00000000..9c13a83c
--- /dev/null
+++ b/runner/Makefile.am
@@ -0,0 +1,31 @@
+
+if BUILD_RUNNER
+
+runnerlib = librunner.la
+noinst_LTLIBRARIES = $(runnerlib)
+librunner_la_SOURCES = \
+ settings.c \
+ job_list.c \
+ executor.c \
+ resultgen.c \
+ $(NULL)
+
+bin_PROGRAMS = \
+ igt_runner \
+ igt_resume \
+ igt_results \
+ $(NULL)
+
+LDADD = $(runnerlib) $(JSONC_LIBS) ../lib/libintel_tools.la
+
+igt_runner_SOURCES = runner.c
+igt_resume_SOURCES = resume.c
+igt_results_SOURCES = results.c
+
+AM_CFLAGS = $(JSONC_CFLAGS) \
+ $(CWARNFLAGS) -Wno-unused-result $(DEBUG_CFLAGS) \
+ -I$(srcdir)/.. \
+ -I$(srcdir)/../lib \
+ -D_GNU_SOURCE
+
+endif
diff --git a/runner/executor.c b/runner/executor.c
new file mode 100644
index 00000000..60cf4da5
--- /dev/null
+++ b/runner/executor.c
@@ -0,0 +1,1022 @@
+#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 "igt_core.h"
+#include "executor.h"
+#include "output_strings.h"
+
+static struct {
+ 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(void)
+{
+ 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, sizeof(name), "/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(void)
+{
+ 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 (!strncmp(subtest, EXECUTOR_EXIT, strlen(EXECUTOR_EXIT))) {
+ /* Fully done. Mark that by making the binary name invalid. */
+ fscanf(f, " (%*fs)");
+ entry->binary[0] = '\0';
+ free(subtest);
+ continue;
+ }
+
+ if (!strncmp(subtest, EXECUTOR_TIMEOUT, strlen(EXECUTOR_TIMEOUT))) {
+ fscanf(f, " (%*fs)");
+ free(subtest);
+ continue;
+ }
+
+ prune_subtest(entry, subtest);
+
+ free(subtest);
+ any_pruned = true;
+ }
+
+ fclose(f);
+ return any_pruned;
+}
+
+static const char *filenames[_F_LAST] = {
+ [_F_JOURNAL] = "journal.txt",
+ [_F_OUT] = "out.txt",
+ [_F_ERR] = "err.txt",
+ [_F_DMESG] = "dmesg.txt",
+};
+
+static int open_at_end(int dirfd, const 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, const char *name)
+{
+ return openat(dirfd, name, O_RDONLY);
+}
+
+bool open_output_files(int dirfd, int *fds, bool write)
+{
+ int i;
+ int (*openfunc)(int, const 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) {
+ /* Reading comparison record done. */
+ 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) {
+ /*
+ * Comparison record has been read, compare
+ * the sequence number to see if we have read
+ * enough.
+ */
+ if (seq >= cmpseq)
+ return;
+ }
+ }
+}
+
+static bool kill_child(int sig, pid_t child)
+{
+ /*
+ * 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;
+}
+
+/*
+ * 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; /* 0 if not killed, signal number otherwise */
+ struct timespec time_beg, time_end;
+ bool aborting = false;
+
+ igt_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 -1;
+ }
+
+ 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");
+ }
+
+ killed = SIGTERM;
+ if (!kill_child(killed, child))
+ return -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 SIGTERM:
+ if (settings->log_level >= LOG_LEVEL_NORMAL) {
+ printf("Timeout. Killing the current test with SIGKILL.\n");
+ }
+
+ killed = SIGKILL;
+ if (!kill_child(killed, child))
+ return -1;
+
+ intervals_left = timeout_intervals = 1;
+ break;
+ case SIGKILL:
+ /* 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 -1;
+ }
+
+ continue;
+ }
+
+ intervals_left = timeout_intervals;
+ ping_watchdogs();
+
+ /* TODO: Refactor these handlers to their own functions */
+ 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 > strlen(STARTING_SUBTEST) &&
+ !memcmp(outbuf, STARTING_SUBTEST, strlen(STARTING_SUBTEST))) {
+ write(outputs[_F_JOURNAL], outbuf + strlen(STARTING_SUBTEST),
+ linelen - strlen(STARTING_SUBTEST));
+ memcpy(current_subtest, outbuf + strlen(STARTING_SUBTEST),
+ linelen - strlen(STARTING_SUBTEST));
+ current_subtest[linelen - strlen(STARTING_SUBTEST)] = '\0';
+
+ if (settings->log_level >= LOG_LEVEL_VERBOSE) {
+ fwrite(outbuf, 1, linelen, stdout);
+ }
+ }
+ if (linelen > strlen(SUBTEST_RESULT) &&
+ !memcmp(outbuf, SUBTEST_RESULT, strlen(SUBTEST_RESULT))) {
+ char *delim = memchr(outbuf, ':', linelen);
+
+ if (delim != NULL) {
+ size_t subtestlen = delim - outbuf - strlen(SUBTEST_RESULT);
+ if (memcmp(current_subtest, outbuf + strlen(SUBTEST_RESULT),
+ subtestlen)) {
+ /* Result for a test that didn't ever start */
+ write(outputs[_F_JOURNAL],
+ outbuf + strlen(SUBTEST_RESULT),
+ 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");
+
+ aborting = true;
+ timeout = 2;
+ killed = SIGTERM;
+ if (!kill_child(killed, child))
+ return -1;
+
+ continue;
+ }
+
+ igt_gettime(&time_end);
+
+ time = igt_time_elapsed(&time_beg, &time_end);
+ if (time < 0.0)
+ time = 0.0;
+
+ if (!aborting) {
+ dprintf(outputs[_F_JOURNAL], "%s%d (%.3fs)\n",
+ killed ? EXECUTOR_TIMEOUT : EXECUTOR_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 -1;
+
+ return killed;
+}
+
+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] = strdup("--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(IGT_EXIT_INVALID);
+}
+
+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, sizeof(name), "%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 -1;
+ }
+
+ if (!open_output_files(dirfd, outputs, true)) {
+ close(dirfd);
+ fprintf(stderr, "Error opening output files\n");
+ return -1;
+ }
+
+ 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 -1;
+ }
+
+ 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 -1;
+ }
+
+ 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;
+ const 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);
+ }
+
+ /* TODO: Refactor this whole function to use onion teardown */
+ close(outpipe[1]);
+ close(errpipe[1]);
+ close(kmsgfd);
+ close_outputs(outputs);
+ close(dirfd);
+
+ return result;
+}
+
+static int remove_file(int dirfd, const char *name)
+{
+ return unlinkat(dirfd, name, 0) && errno != ENOENT;
+}
+
+static bool clear_test_result_directory(int dirfd)
+{
+ int i;
+
+ for (i = 0; i < _F_LAST; i++) {
+ if (remove_file(dirfd, filenames[i])) {
+ fprintf(stderr, "Error deleting %s from test result directory: %s\n",
+ filenames[i],
+ 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, sizeof(name), "%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);
+ if (unlinkat(dirfd, name, AT_REMOVEDIR)) {
+ fprintf(stderr,
+ "Warning: Result directory %s contains extra files\n",
+ name);
+ }
+ }
+
+ close(dirfd);
+
+ return true;
+}
+
+bool initialize_execute_state_from_resume(int dirfd,
+ struct execute_state *state,
+ struct settings *settings,
+ struct job_list *list)
+{
+ struct job_list_entry *entry;
+ int resdirfd, fd, i;
+
+ free_settings(settings);
+ free_job_list(list);
+ memset(state, 0, sizeof(*state));
+
+ 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, sizeof(name), "%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:
+ 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 (!serialize_settings(settings) ||
+ !serialize_job_list(job_list, settings))
+ return false;
+
+ if (settings->overwrite &&
+ !clear_old_results(settings->results_path))
+ return false;
+
+ 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_watchdogs(settings);
+ if (result > 0) {
+ initialize_execute_state_from_resume(resdirfd, state, settings, job_list);
+ return execute(state, settings, job_list);
+ }
+ close(resdirfd);
+ 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..8fe1605b
--- /dev/null
+++ b/runner/executor.h
@@ -0,0 +1,49 @@
+#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
+ * execute. Will validate the settings and serialize both settings and
+ * the job_list into the result directory, overwriting old files if
+ * settings set to do so.
+ */
+bool initialize_execute_state(struct execute_state *state,
+ struct settings *settings,
+ struct job_list *job_list);
+
+/*
+ * Initialize execute_state object to a state where it's ready to
+ * resume an already existing run. settings and job_list must have
+ * been initialized with init_settings et al, and will be read from
+ * the result directory pointed to by dirfd.
+ */
+bool initialize_execute_state_from_resume(int dirfd,
+ 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..e3f820c3
--- /dev/null
+++ b/runner/job_list.c
@@ -0,0 +1,484 @@
+#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 *str, struct regex_list *list)
+{
+ size_t i;
+
+ for (i = 0; i < list->size; i++) {
+ if (regexec(list->regexes[i], str,
+ (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;
+ int s;
+
+ s = snprintf(cmd, sizeof(cmd), "%s/%s --list-subtests",
+ settings->test_root, binary);
+ if (s < 0) {
+ fprintf(stderr, "Failure generating command string, this shouldn't happen.\n");
+ return;
+ }
+
+ if (s >= sizeof(cmd)) {
+ fprintf(stderr, "Path to binary too long, ignoring: %s/%s\n",
+ settings->test_root, binary);
+ return;
+ }
+
+ 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, "Caller didn't clear the job list, 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;
+ 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 (1) {
+ char *binary;
+ char *delim;
+
+ if (getline(&line, &line_len, f) == -1) {
+ 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 (!settings->overwrite &&
+ faccessat(dirfd, joblist_filename, F_OK, 0) == 0) {
+ fprintf(stderr, "Job list file already exists and not overwriting\n");
+ close(dirfd);
+ return false;
+ }
+
+ if (settings->overwrite &&
+ unlinkat(dirfd, joblist_filename, 0) != 0 &&
+ errno != ENOENT) {
+ fprintf(stderr, "Error removing 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: %s\n", strerror(errno));
+ close(dirfd);
+ 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];
+ fputs(entry->binary, f);
+
+ if (entry->subtest_count) {
+ const 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..9dafb312
--- /dev/null
+++ b/runner/meson.build
@@ -0,0 +1,41 @@
+jsonc = dependency('json-c', required: _runner_required)
+
+runnerlib_sources = [ 'settings.c',
+ 'job_list.c',
+ 'executor.c',
+ 'resultgen.c',
+ ]
+
+runner_sources = [ 'runner.c' ]
+resume_sources = [ 'resume.c' ]
+results_sources = [ 'results.c' ]
+
+if _build_runner and jsonc.found()
+ subdir('testdata')
+
+ runnerlib = static_library('igt_runner', runnerlib_sources,
+ include_directories : inc,
+ dependencies : jsonc)
+
+ runner = executable('igt_runner', runner_sources,
+ link_with : runnerlib,
+ install : true,
+ install_dir : bindir,
+ dependencies : igt_deps)
+
+ resume = executable('igt_resume', resume_sources,
+ link_with : runnerlib,
+ install : true,
+ install_dir : bindir,
+ dependencies : igt_deps)
+
+ results = executable('igt_results', results_sources,
+ link_with : runnerlib,
+ install : true,
+ install_dir : bindir,
+ dependencies : igt_deps)
+
+ build_info += 'Build test runner: Yes'
+else
+ build_info += 'Build test runner: No'
+endif
diff --git a/runner/output_strings.h b/runner/output_strings.h
new file mode 100644
index 00000000..1e52c2ce
--- /dev/null
+++ b/runner/output_strings.h
@@ -0,0 +1,55 @@
+#ifndef RUNNER_OUTPUT_STRINGS_H
+#define RUNNER_OUTPUT_STRINGS_H
+
+/*
+ * Output when a subtest has begun. Is followed by the subtest name.
+ *
+ * Example:
+ * Starting subtest: subtestname
+ */
+static const char STARTING_SUBTEST[] = "Starting subtest: ";
+
+/*
+ * Output when a subtest has ended. Is followed by the subtest name
+ * and optionally its runtime.
+ *
+ * Examples:
+ * Subtest subtestname: SKIP
+ * Subtest subtestname: PASS (0.003s)
+ */
+static const char SUBTEST_RESULT[] = "Subtest ";
+
+/*
+ * Output in dmesg when a subtest has begin. Is followed by the subtest name.
+ *
+ * Example:
+ * [IGT] test-binary-name: starting subtest subtestname
+ */
+static const char STARTING_SUBTEST_DMESG[] = ": starting subtest ";
+
+/*
+ * Output when a test process is executed.
+ *
+ * Example:
+ * IGT-Version: 1.22-gde9af343 (x86_64) (Linux: 4.12.0-1-amd64 x86_64)
+ */
+static const char IGT_VERSIONSTRING[] = "IGT-Version: ";
+
+/*
+ * Output by the executor to mark the test's exit code.
+ *
+ * Example:
+ * exit:77 (0.003s)
+ */
+static const char EXECUTOR_EXIT[] = "exit:";
+
+/*
+ * Output by the executor to mark the test as timeouted, with an exit
+ * code.
+ *
+ * Example:
+ * timeout:-15 (360.000s)
+ */
+static const char EXECUTOR_TIMEOUT[] = "timeout:";
+
+#endif
diff --git a/runner/resultgen.c b/runner/resultgen.c
new file mode 100644
index 00000000..347b52a4
--- /dev/null
+++ b/runner/resultgen.c
@@ -0,0 +1,962 @@
+#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 "igt_core.h"
+#include "resultgen.h"
+#include "settings.h"
+#include "executor.h"
+#include "output_strings.h"
+
+struct subtests
+{
+ char **names;
+ size_t size;
+};
+
+/*
+ * A lot of string handling here operates on an mmapped buffer, and
+ * thus we can't assume null-terminated strings. Buffers will be
+ * passed around as pointer+size, or pointer+pointer-past-the-end, the
+ * mem*() family of functions is used instead of str*().
+ */
+
+static char *find_line_starting_with(char *haystack, const char *needle, char *end)
+{
+ while (haystack < end) {
+ char *line_end = memchr(haystack, '\n', end - haystack);
+
+ if (end - haystack < strlen(needle))
+ return NULL;
+ if (!memcmp(haystack, needle, strlen(needle)))
+ return haystack;
+ if (line_end == NULL)
+ return NULL;
+ haystack = line_end + 1;
+ }
+
+ return NULL;
+}
+
+static char *find_line_starting_with_either(char *haystack,
+ const char *needle1,
+ const char *needle2,
+ char *end)
+{
+ while (haystack < end) {
+ char *line_end = memchr(haystack, '\n', end - haystack);
+ size_t linelen = line_end != NULL ? line_end - haystack : end - haystack;
+ if ((linelen >= strlen(needle1) && !memcmp(haystack, needle1, strlen(needle1))) ||
+ (linelen >= strlen(needle2) && !memcmp(haystack, needle2, strlen(needle2))))
+ return haystack;
+
+ if (line_end == NULL)
+ return NULL;
+
+ haystack = 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++;
+
+ if (ret < bufend)
+ return ret;
+ else
+ return NULL;
+}
+
+static char *find_line_after_last(char *begin,
+ const char *needle1,
+ const char *needle2,
+ char *end)
+{
+ char *one, *two;
+ char *current_pos = begin;
+ char *needle1_newline = malloc(strlen(needle1) + 2);
+ char *needle2_newline = malloc(strlen(needle2) + 2);
+
+ needle1_newline[0] = needle2_newline[0] = '\n';
+ strcpy(needle1_newline + 1, needle1);
+ strcpy(needle2_newline + 1, needle2);
+
+ while (true) {
+ one = memmem(current_pos, end - current_pos, needle1_newline, strlen(needle1_newline));
+ two = memmem(current_pos, end - current_pos, needle2_newline, strlen(needle2_newline));
+ if (one == NULL && two == NULL)
+ break;
+
+ if (one != NULL && current_pos < one)
+ current_pos = one;
+ if (two != NULL && current_pos < two)
+ current_pos = two;
+
+ one = next_line(current_pos, end);
+ if (one != NULL)
+ current_pos = one;
+ }
+ free(needle1_newline);
+ free(needle2_newline);
+
+ one = memchr(current_pos, '\n', end - current_pos);
+ if (one != NULL)
+ return ++one;
+
+ return current_pos;
+}
+
+static size_t count_lines(const char *buf, const char *bufend)
+{
+ size_t ret = 0;
+ while (buf < bufend && (buf = memchr(buf, '\n', bufend - buf)) != NULL) {
+ ret++;
+ buf++;
+ }
+
+ return ret;
+}
+
+static char *lowercase(const 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 void generate_piglit_name(const char *binary, const char *subtest,
+ char *namebuf, size_t namebuf_size)
+{
+ char *lc_binary = lowercase(binary);
+ char *lc_subtest = NULL;
+
+ if (!subtest) {
+ snprintf(namebuf, namebuf_size, "igt@%s", lc_binary);
+ free(lc_binary);
+ return;
+ }
+
+ lc_subtest = lowercase(subtest);
+
+ snprintf(namebuf, namebuf_size, "igt@%s@%s", lc_binary, lc_subtest);
+
+ free(lc_binary);
+ free(lc_subtest);
+}
+
+static const struct {
+ const char *output_str;
+ const char *result_str;
+} resultmap[] = {
+ { "SUCCESS", "pass" },
+ { "SKIP", "skip" },
+ { "FAIL", "fail" },
+ { "CRASH", "crash" },
+ { "TIMEOUT", "timeout" },
+};
+static void parse_result_string(char *resultstring, size_t len, const char **result, double *time)
+{
+ size_t i;
+ size_t wordlen = 0;
+
+ while (wordlen < len && !isspace(resultstring[wordlen])) {
+ wordlen++;
+ }
+
+ *result = NULL;
+ 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 the result string is unknown, use incomplete */
+ if (!*result)
+ *result = "incomplete";
+
+ /*
+ * Check for subtest runtime after the result. The string is
+ * '(' followed by the runtime in seconds as floating point,
+ * followed by 's)'.
+ */
+ wordlen++;
+ if (wordlen < len && resultstring[wordlen] == '(') {
+ char *dup;
+
+ wordlen++;
+ dup = malloc(len - wordlen + 1);
+ memcpy(dup, resultstring + wordlen, len - wordlen);
+ dup[len - wordlen] = '\0';
+ *time = strtod(dup, NULL);
+
+ free(dup);
+ }
+}
+
+static void parse_subtest_result(char *subtest, const 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;
+
+ /*
+ * The result line structure is:
+ *
+ * - The string "Subtest " (`SUBTEST_RESULT` from output_strings.h)
+ * - The subtest name
+ * - The characters ':' and ' '
+ * - Subtest result string
+ * - Optional:
+ * -- The characters ' ' and '('
+ * -- Subtest runtime in seconds as floating point
+ * -- The characters 's' and ')'
+ *
+ * Example:
+ * Subtest subtestname: PASS (0.003s)
+ */
+
+ line = find_line_starting_with(buf, SUBTEST_RESULT, bufend);
+ if (!line) {
+ *result = "incomplete";
+ return;
+ }
+
+ line_end = memchr(line, '\n', bufend - line);
+ linelen = line_end != NULL ? line_end - line : bufend - line;
+
+ if (strlen(SUBTEST_RESULT) + subtestlen + strlen(": ") > linelen ||
+ strncmp(line + strlen(SUBTEST_RESULT), subtest, subtestlen))
+ return parse_subtest_result(subtest, result, time, line + linelen, bufend);
+
+ resultstring = line + strlen(SUBTEST_RESULT) + subtestlen + strlen(": ");
+ parse_result_string(resultstring, linelen - (resultstring - line), result, time);
+}
+
+static struct json_object *get_or_create_json_object(struct json_object *base,
+ const 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 set_result(struct json_object *obj, const char *result)
+{
+ json_object_object_add(obj, "result",
+ json_object_new_string(result));
+}
+
+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;
+ }
+
+ /* Add the runtime to the existing runtime. */
+ oldtime = json_object_get_double(oldend);
+ time += oldtime;
+ json_object_object_add(timeobj, "end",
+ json_object_new_double(time));
+}
+
+static void set_runtime(struct json_object *obj, double time)
+{
+ struct json_object *timeobj = get_or_create_json_object(obj, "time");
+
+ json_object_object_add(timeobj, "__type__",
+ json_object_new_string("TimeAttribute"));
+ json_object_object_add(timeobj, "start",
+ json_object_new_double(0.0));
+ json_object_object_add(timeobj, "end",
+ json_object_new_double(time));
+}
+
+static bool fill_from_output(int fd, const char *binary, const char *key,
+ struct subtests *subtests,
+ struct json_object *tests)
+{
+ char *buf, *bufend;
+ struct stat statbuf;
+ char piglit_name[256];
+ 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;
+
+ if (statbuf.st_size != 0) {
+ buf = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (buf == MAP_FAILED)
+ return false;
+ } else {
+ buf = NULL;
+ }
+
+ bufend = buf + statbuf.st_size;
+
+ igt_version = find_line_starting_with(buf, IGT_VERSIONSTRING, bufend);
+ if (igt_version) {
+ char *newline = memchr(igt_version, '\n', bufend - igt_version);
+ igt_version_len = newline - igt_version;
+ }
+
+ /* TODO: Refactor to helper functions */
+ if (subtests->size == 0) {
+ /* No subtests */
+ generate_piglit_name(binary, NULL, piglit_name, sizeof(piglit_name));
+ current_test = get_or_create_json_object(tests, piglit_name);
+
+ json_object_object_add(current_test, key,
+ json_object_new_string_len(buf, statbuf.st_size));
+ 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 *this_sub_begin, *this_sub_result;
+ const char *resulttext;
+ char *beg, *end, *startline;
+ double time;
+ int begin_len;
+ int result_len;
+
+ generate_piglit_name(binary, subtests->names[i], piglit_name, sizeof(piglit_name));
+ current_test = get_or_create_json_object(tests, piglit_name);
+
+ begin_len = asprintf(&this_sub_begin, "%s%s\n", STARTING_SUBTEST, subtests->names[i]);
+ result_len = asprintf(&this_sub_result, "%s%s: ", SUBTEST_RESULT, subtests->names[i]);
+
+ if (begin_len < 0 || result_len < 0) {
+ fprintf(stderr, "Failure generating strings\n");
+ return false;
+ }
+
+ beg = find_line_starting_with(buf, this_sub_begin, bufend);
+ end = find_line_starting_with(buf, this_sub_result, bufend);
+ startline = beg;
+
+ free(this_sub_begin);
+ free(this_sub_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. Start from the result
+ * line, it gets adjusted below.
+ */
+ beg = end;
+ }
+
+ /* Include the output after the previous subtest output */
+ beg = find_line_after_last(buf,
+ STARTING_SUBTEST,
+ SUBTEST_RESULT,
+ beg);
+
+ if (end == NULL) {
+ /* Incomplete result. Find the next starting subtest or result. */
+ end = next_line(startline, bufend);
+ if (end != NULL) {
+ end = find_line_starting_with_either(end,
+ STARTING_SUBTEST,
+ SUBTEST_RESULT,
+ bufend);
+ }
+ if (end == NULL) {
+ end = bufend;
+ }
+ } else {
+ /*
+ * Now pointing to the line where this sub's
+ * result is. We need to include that of
+ * course.
+ */
+ char *nexttest = next_line(end, bufend);
+
+ /* Stretch onwards until the next subtest begins or ends */
+ if (nexttest != NULL) {
+ nexttest = find_line_starting_with_either(nexttest,
+ STARTING_SUBTEST,
+ SUBTEST_RESULT,
+ bufend);
+ }
+ if (nexttest != NULL) {
+ end = nexttest;
+ } else {
+ end = bufend;
+ }
+ }
+
+ json_object_object_add(current_test, key,
+ json_object_new_string_len(beg, end - beg));
+
+ if (igt_version) {
+ json_object_object_add(current_test, "igt-version",
+ json_object_new_string_len(igt_version,
+ igt_version_len));
+ }
+
+ if (!json_object_object_get_ex(current_test, "result", NULL)) {
+ parse_subtest_result(subtests->names[i], &resulttext, &time, beg, end);
+ set_result(current_test, resulttext);
+ set_runtime(current_test, time);
+ }
+ }
+
+ 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.
+ *
+ * TODO: Move this to external files, i915-suppressions.txt,
+ * general-suppressions.txt et al.
+ */
+
+#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 int init_regex_whitelist(void)
+{
+ static 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 = 1;
+ return false;
+ }
+
+ status = 0;
+ }
+
+ return status;
+}
+
+static bool parse_dmesg_line(char* line,
+ unsigned *flags, unsigned long long *ts_usec,
+ char *continuation, char **message)
+{
+ unsigned long long seq;
+ 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);
+ }
+ return false;
+ }
+
+ *message = strchr(line, ';');
+ if (!message) {
+ fprintf(stderr, "No ; found in kmsg record, this shouldn't happen\n");
+ return false;
+ }
+ (*message)++;
+
+ return true;
+}
+
+static void add_dmesg(struct json_object *obj,
+ const char *dmesg, size_t dmesglen,
+ const char *warnings, size_t warningslen)
+{
+ json_object_object_add(obj, "dmesg",
+ json_object_new_string_len(dmesg, dmesglen));
+
+ if (warnings) {
+ json_object_object_add(obj, "dmesg-warnings",
+ json_object_new_string_len(warnings, warningslen));
+ }
+}
+
+static void add_empty_dmesgs_where_missing(struct json_object *tests,
+ char *binary,
+ struct subtests *subtests)
+{
+ struct json_object *current_test;
+ char piglit_name[256];
+ size_t i;
+
+ for (i = 0; i < subtests->size; i++) {
+ generate_piglit_name(binary, subtests->names[i], piglit_name, sizeof(piglit_name));
+ current_test = get_or_create_json_object(tests, piglit_name);
+ if (!json_object_object_get_ex(current_test, "dmesg", NULL)) {
+ add_dmesg(current_test, "", 0, NULL, 0);
+ }
+ }
+
+}
+
+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 piglit_name[256];
+ ssize_t read;
+ size_t i;
+
+ if (!f) {
+ return false;
+ }
+
+ if (init_regex_whitelist()) {
+ fclose(f);
+ return false;
+ }
+
+ while ((read = getline(&line, &linelen, f)) > 0) {
+ char *formatted;
+ unsigned flags;
+ unsigned long long ts_usec;
+ char continuation;
+ char *message, *subtest;
+
+ if (!parse_dmesg_line(line, &flags, &ts_usec, &continuation, &message))
+ continue;
+
+ asprintf(&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 */
+ add_dmesg(current_test, dmesg, dmesglen, warnings, warningslen);
+
+ free(dmesg);
+ free(warnings);
+ dmesg = warnings = NULL;
+ dmesglen = warningslen = 0;
+ }
+
+ subtest += strlen(STARTING_SUBTEST_DMESG);
+ generate_piglit_name(binary, subtest, piglit_name, sizeof(piglit_name));
+ current_test = get_or_create_json_object(tests, piglit_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);
+ free(formatted);
+ }
+
+ if (current_test != NULL) {
+ add_dmesg(current_test, dmesg, dmesglen, warnings, warningslen);
+ } 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++) {
+ generate_piglit_name(binary, subtests->names[i], piglit_name, sizeof(piglit_name));
+ current_test = get_or_create_json_object(tests, piglit_name);
+ /*
+ * Don't bother with warnings, any subtests
+ * there are would have skip as their result
+ * anyway.
+ */
+ add_dmesg(current_test, dmesg, dmesglen, NULL, 0);
+ }
+
+ if (subtests->size == 0) {
+ generate_piglit_name(binary, NULL, piglit_name, sizeof(piglit_name));
+ current_test = get_or_create_json_object(tests, piglit_name);
+ add_dmesg(current_test, dmesg, dmesglen, warnings, warningslen);
+ }
+ }
+
+ add_empty_dmesgs_where_missing(tests, binary, subtests);
+
+ free(dmesg);
+ free(warnings);
+ fclose(f);
+ return true;
+}
+
+static const 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 void 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:";
+ char timeoutline[] = "timeout:";
+ int exitcode = -1;
+ bool has_timeout = false;
+
+ while ((read = getline(&line, &linelen, f)) > 0) {
+ if (read >= strlen(exitline) && !memcmp(line, exitline, strlen(exitline))) {
+ char *p = strchr(line, '(');
+ char piglit_name[256];
+ double time = 0.0;
+ struct json_object *obj;
+
+ generate_piglit_name(binary, NULL, piglit_name, sizeof(piglit_name));
+ obj = get_or_create_json_object(tests, piglit_name);
+
+ exitcode = atoi(line + strlen(exitline));
+
+ if (p)
+ time = strtod(p + 1, NULL);
+
+ add_runtime(obj, time);
+ } else if (read >= strlen(timeoutline) && !memcmp(line, timeoutline, strlen(timeoutline))) {
+ has_timeout = true;
+
+ if (subtests->size) {
+ /* Assign the timeout to the previously appeared subtest */
+ char *last_subtest = subtests->names[subtests->size - 1];
+ char piglit_name[256];
+ char *p = strchr(line, '(');
+ double time = 0.0;
+ struct json_object *obj;
+
+ generate_piglit_name(binary, last_subtest, piglit_name, sizeof(piglit_name));
+ obj = get_or_create_json_object(tests, piglit_name);
+
+ set_result(obj, "timeout");
+
+ if (p)
+ time = strtod(p + 1, NULL);
+
+ /* Add runtime for the subtest... */
+ add_runtime(obj, time);
+
+ /* ... and also for the binary */
+ generate_piglit_name(binary, NULL, piglit_name, sizeof(piglit_name));
+ obj = get_or_create_json_object(tests, piglit_name);
+ add_runtime(obj, time);
+ }
+ } else {
+ add_subtest(subtests, strdup(line));
+ }
+ }
+
+ if (subtests->size == 0) {
+ char piglit_name[256];
+ struct json_object *obj;
+ const char *result = has_timeout ? "timeout" : result_from_exitcode(exitcode);
+
+ generate_piglit_name(binary, NULL, piglit_name, sizeof(piglit_name));
+ obj = get_or_create_json_object(tests, piglit_name);
+ set_result(obj, result);
+ }
+
+ free(line);
+}
+
+static void override_result_single(struct json_object *obj)
+{
+ const char *errtext = NULL, *result = NULL;
+ struct json_object *textobj;
+ bool dmesgwarns = false;
+
+ if (json_object_object_get_ex(obj, "err", &textobj))
+ errtext = json_object_get_string(textobj);
+ if (json_object_object_get_ex(obj, "result", &textobj))
+ result = json_object_get_string(textobj);
+ if (json_object_object_get_ex(obj, "dmesg-warnings", &textobj))
+ dmesgwarns = true;
+
+ if (!strcmp(result, "pass") &&
+ count_lines(errtext, errtext + strlen(errtext)) > 2) {
+ set_result(obj, "warn");
+ }
+
+ if (dmesgwarns) {
+ if (!strcmp(result, "pass") || !strcmp(result, "warn")) {
+ set_result(obj, "dmesg-warn");
+ } else if (!strcmp(result, "fail")) {
+ set_result(obj, "dmesg-fail");
+ }
+ }
+}
+
+static void override_results(char *binary,
+ struct subtests *subtests,
+ struct json_object *tests)
+{
+ struct json_object *obj;
+ char piglit_name[256];
+ size_t i;
+
+ if (subtests->size == 0) {
+ generate_piglit_name(binary, NULL, piglit_name, sizeof(piglit_name));
+ obj = get_or_create_json_object(tests, piglit_name);
+ override_result_single(obj);
+ return;
+ }
+
+ for (i = 0; i < subtests->size; i++) {
+ generate_piglit_name(binary, subtests->names[i], piglit_name, sizeof(piglit_name));
+ obj = get_or_create_json_object(tests, piglit_name);
+ override_result_single(obj);
+ }
+}
+
+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 fills the subtests struct and adds
+ * timeout results where applicable.
+ */
+ fill_from_journal(fds[_F_JOURNAL], binary, &subtests, tests);
+
+ if (!fill_from_output(fds[_F_OUT], binary, "out", &subtests, tests) ||
+ !fill_from_output(fds[_F_ERR], binary, "err", &subtests, tests) ||
+ !fill_from_dmesg(fds[_F_DMESG], binary, &subtests, tests)) {
+ fprintf(stderr, "Error parsing output files\n");
+ return false;
+ }
+
+ override_results(binary, &subtests, tests);
+
+ 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);
+ }
+
+ /*
+ * Result fields that won't be added:
+ *
+ * - glxinfo
+ * - wglinfo
+ * - clinfo
+ *
+ * Result fields that are TODO:
+ *
+ * - lspci
+ * - 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/resume.c b/runner/resume.c
new file mode 100644
index 00000000..b3a2a71e
--- /dev/null
+++ b/runner/resume.c
@@ -0,0 +1,47 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.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;
+ int dirfd;
+
+ init_settings(&settings);
+ init_job_list(&job_list);
+
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s results-directory\n", argv[0]);
+ return 1;
+ }
+
+ if ((dirfd = open(argv[1], O_RDONLY | O_DIRECTORY)) < 0) {
+ fprintf(stderr, "Failure opening %s: %s\n", argv[1], strerror(errno));
+ return 1;
+ }
+
+ if (!initialize_execute_state_from_resume(dirfd, &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/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..31754a12
--- /dev/null
+++ b/runner/settings.c
@@ -0,0 +1,502 @@
+#include "settings.h"
+
+#include <errno.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)
+{
+ typeof(*log_levels) *it;
+
+ for (it = log_levels; it->name; it++) {
+ if (!strcmp(level, it->name)) {
+ settings->log_level = it->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, FILE *f)
+{
+ if (extra_message)
+ fprintf(f, "%s\n\n", extra_message);
+
+ fputs(usage_str, f);
+}
+
+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, stderr);
+
+ 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, stdout);
+ 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", stderr);
+ 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, stderr);
+ goto error;
+ default:
+ usage("Cannot parse options", stderr);
+ 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", stderr);
+ goto error;
+ default:
+ usage("Extra arguments after results-path", stderr);
+ 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", stderr);
+ 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", stderr);
+ return false;
+ }
+
+ if (!settings->results_path) {
+ usage("No results-path set; this shouldn't happen", stderr);
+ return false;
+ }
+
+ if (!settings->test_root) {
+ usage("No test root set; this shouldn't happen", stderr);
+ 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) {
+ char *ret;
+
+ tmppath = strdup(path);
+ tmpname = basename(tmppath);
+
+ asprintf(&ret, "%s/%s", result, tmpname);
+ free(result);
+ free(tmppath);
+ return ret;
+ }
+
+ 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", stderr);
+ 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", stderr);
+ return false;
+ }
+ }
+
+ if (!settings->overwrite &&
+ faccessat(dirfd, settings_filename, F_OK, 0) == 0) {
+ usage("Settings metadata already exists and not overwriting", stderr);
+ return false;
+ }
+
+ if (settings->overwrite &&
+ unlinkat(dirfd, settings_filename, 0) != 0 &&
+ errno != ENOENT) {
+ usage("Error removing old settings metadata", stderr);
+ return false;
+ }
+
+ if ((fd = openat(dirfd, settings_filename, O_CREAT | O_EXCL | O_WRONLY, 0666)) < 0) {
+ char *msg;
+
+ asprintf(&msg, "Creating settings serialization file failed: %s", strerror(errno));
+ usage(msg, stderr);
+
+ free(msg);
+ 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
+}
+
+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, val ? strdup(val) : NULL);
+ PARSE_LINE(settings, name, val, name, val ? strdup(val) : NULL);
+ 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, val ? strdup(val) : NULL);
+ PARSE_LINE(settings, name, val, results_path, val ? strdup(val) : NULL);
+
+ 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