[PATCH i-g-t] runner goes brrrrr

Petri Latvala petri.latvala at intel.com
Fri Oct 7 15:32:14 UTC 2022


---
 lib/igt_core.c                      | 177 ++++++--
 lib/meson.build                     |   1 +
 lib/runnercomms.c                   | 653 ++++++++++++++++++++++++++++
 lib/runnercomms.h                   | 277 ++++++++++++
 lib/tests/igt_runnercomms_packets.c | 281 ++++++++++++
 lib/tests/meson.build               |   1 +
 runner/decoder.c                    | 131 ++++++
 runner/executor.c                   | 354 +++++++++++++--
 runner/executor.h                   |   1 +
 runner/meson.build                  |   8 +
 runner/resultgen.c                  | 629 ++++++++++++++++++++++++++-
 runner/runner_tests.c               | 186 ++++++++
 12 files changed, 2615 insertions(+), 84 deletions(-)
 create mode 100644 lib/runnercomms.c
 create mode 100644 lib/runnercomms.h
 create mode 100644 lib/tests/igt_runnercomms_packets.c
 create mode 100644 runner/decoder.c

diff --git a/lib/igt_core.c b/lib/igt_core.c
index 2aee0d08..db0fee2a 100644
--- a/lib/igt_core.c
+++ b/lib/igt_core.c
@@ -76,6 +76,7 @@
 #include "igt_list.h"
 #include "igt_device_scan.h"
 #include "igt_thread.h"
+#include "runnercomms.h"
 
 #define UNW_LOCAL_ONLY
 #include <libunwind.h>
@@ -456,6 +457,84 @@ static void _igt_log_buffer_reset(void)
 	pthread_mutex_unlock(&log_buffer_mutex);
 }
 
+__attribute__((format(printf, 2, 3)))
+static void _log_line_fprintf(FILE* stream, const char *format, ...)
+{
+	va_list ap;
+	char *str;
+
+	va_start(ap, format);
+
+	if (runner_connected()) {
+		vasprintf(&str, format, ap);
+		send_to_runner(runnerpacket_log(fileno(stream), str));
+		free(str);
+	} else {
+		vfprintf(stream, format, ap);
+	}
+}
+
+enum _subtest_type {
+      _SUBTEST_TYPE_NORMAL,
+      _SUBTEST_TYPE_DYNAMIC,
+};
+
+static void _subtest_result_message(enum _subtest_type subtest_type,
+				    const char *name,
+				    const char *result,
+				    double timeelapsed)
+{
+	char timestr[32];
+
+	snprintf(timestr, sizeof(timestr), "%.3f", timeelapsed);
+
+	if (runner_connected()) {
+		if (subtest_type == _SUBTEST_TYPE_NORMAL)
+			send_to_runner(runnerpacket_subtest_result(name, result, timestr, NULL));
+		else
+			send_to_runner(runnerpacket_dynamic_subtest_result(name, result, timestr, NULL));
+
+		return;
+	}
+
+	printf("%s%s %s: %s (%ss)%s\n",
+	       (!__igt_plain_output) ? "\x1b[1m" : "",
+	       subtest_type == _SUBTEST_TYPE_NORMAL ? "Subtest" : "Dynamic subtest",
+	       name,
+	       result,
+	       timestr,
+	       (!__igt_plain_output) ? "\x1b[0m" : "");
+	fflush(stdout);
+	if (stderr_needs_sentinel)
+		fprintf(stderr, "%s %s: %s (%ss)\n",
+			subtest_type == _SUBTEST_TYPE_NORMAL ? "Subtest" : "Dynamic subtest",
+			name,
+			result,
+			timestr);
+}
+
+static void _subtest_starting_message(enum _subtest_type subtest_type,
+				      const char *name)
+{
+	if (runner_connected()) {
+		if (subtest_type == _SUBTEST_TYPE_NORMAL)
+			send_to_runner(runnerpacket_subtest_start(name));
+		else
+			send_to_runner(runnerpacket_dynamic_subtest_start(name));
+
+		return;
+	}
+
+	igt_info("Starting %s: %s\n",
+		 subtest_type == _SUBTEST_TYPE_NORMAL ? "subtest" : "dynamic subtest",
+		 name);
+	fflush(stdout);
+	if (stderr_needs_sentinel)
+		fprintf(stderr, "Starting %s: %s\n",
+			subtest_type == _SUBTEST_TYPE_NORMAL ? "subtest" : "dynamic subtest",
+			name);
+}
+
 static void _igt_log_buffer_dump(void)
 {
 	uint8_t i;
@@ -478,31 +557,31 @@ static void _igt_log_buffer_dump(void)
 	}
 
 	if (in_dynamic_subtest)
-		fprintf(stderr, "Dynamic subtest %s failed.\n", in_dynamic_subtest);
+		_log_line_fprintf(stderr, "Dynamic subtest %s failed.\n", in_dynamic_subtest);
 	else if (in_subtest)
-		fprintf(stderr, "Subtest %s failed.\n", in_subtest);
+		_log_line_fprintf(stderr, "Subtest %s failed.\n", in_subtest);
 	else
-		fprintf(stderr, "Test %s failed.\n", command_str);
+		_log_line_fprintf(stderr, "Test %s failed.\n", command_str);
 
 	if (log_buffer.start == log_buffer.end) {
-		fprintf(stderr, "No log.\n");
+		_log_line_fprintf(stderr, "No log.\n");
 		return;
 	}
 
 	pthread_mutex_lock(&log_buffer_mutex);
-	fprintf(stderr, "**** DEBUG ****\n");
+	_log_line_fprintf(stderr, "**** DEBUG ****\n");
 
 	i = log_buffer.start;
 	do {
 		char *last_line = log_buffer.entries[i];
-		fprintf(stderr, "%s", last_line);
+		_log_line_fprintf(stderr, "%s", last_line);
 		i++;
 	} while (i != log_buffer.start && i != log_buffer.end);
 
 	/* reset the buffer */
 	log_buffer.start = log_buffer.end = 0;
 
-	fprintf(stderr, "****  END  ****\n");
+	_log_line_fprintf(stderr, "****  END  ****\n");
 	pthread_mutex_unlock(&log_buffer_mutex);
 }
 
@@ -763,9 +842,19 @@ static void print_version(void)
 
 	uname(&uts);
 
-	igt_info("IGT-Version: %s-%s (%s) (%s: %s %s)\n", PACKAGE_VERSION,
-		 IGT_GIT_SHA1, TARGET_CPU_PLATFORM,
-		 uts.sysname, uts.release, uts.machine);
+	if (runner_connected()) {
+		char versionstr[256];
+
+		snprintf(versionstr, sizeof(versionstr),
+			 "IGT-Version: %s-%s (%s) (%s: %s %s)\n", PACKAGE_VERSION,
+			 IGT_GIT_SHA1, TARGET_CPU_PLATFORM,
+			 uts.sysname, uts.release, uts.machine);
+		send_to_runner(runnerpacket_versionstring(versionstr));
+	} else {
+		igt_info("IGT-Version: %s-%s (%s) (%s: %s %s)\n", PACKAGE_VERSION,
+			 IGT_GIT_SHA1, TARGET_CPU_PLATFORM,
+			 uts.sysname, uts.release, uts.machine);
+	}
 }
 
 static void print_usage(const char *help_str, bool output_on_stderr)
@@ -937,6 +1026,11 @@ static void common_init_env(void)
 	if (env) {
 		igt_rc_device = strdup(env);
 	}
+
+	env = getenv("IGT_RUNNER_SOCKET_FD");
+	if (env) {
+		set_runner_socket(atoi(env));
+	}
 }
 
 static int common_init(int *argc, char **argv,
@@ -1329,24 +1423,15 @@ bool __igt_run_subtest(const char *subtest_name, const char *file, const int lin
 
 
 	if (skip_subtests_henceforth) {
-		printf("%sSubtest %s: %s%s\n",
-		       (!__igt_plain_output) ? "\x1b[1m" : "", subtest_name,
-		       skip_subtests_henceforth == SKIP ?
-		       "SKIP" : "FAIL", (!__igt_plain_output) ? "\x1b[0m" : "");
-		fflush(stdout);
-		if (stderr_needs_sentinel)
-			fprintf(stderr, "Subtest %s: %s\n", subtest_name,
-				skip_subtests_henceforth == SKIP ?
-				"SKIP" : "FAIL");
+		_subtest_result_message(_SUBTEST_TYPE_NORMAL, subtest_name,
+					skip_subtests_henceforth == SKIP ? "SKIP" : "FAIL",
+					0.0);
 		return false;
 	}
 
 	igt_kmsg(KMSG_INFO "%s: starting subtest %s\n",
 		 command_str, subtest_name);
-	igt_info("Starting subtest: %s\n", subtest_name);
-	fflush(stdout);
-	if (stderr_needs_sentinel)
-		fprintf(stderr, "Starting subtest: %s\n", subtest_name);
+	_subtest_starting_message(_SUBTEST_TYPE_NORMAL, subtest_name);
 
 	_igt_log_buffer_reset();
 	igt_thread_clear_fail_state();
@@ -1374,10 +1459,7 @@ bool __igt_run_dynamic_subtest(const char *dynamic_subtest_name)
 
 	igt_kmsg(KMSG_INFO "%s: starting dynamic subtest %s\n",
 		 command_str, dynamic_subtest_name);
-	igt_info("Starting dynamic subtest: %s\n", dynamic_subtest_name);
-	fflush(stdout);
-	if (stderr_needs_sentinel)
-		fprintf(stderr, "Starting dynamic subtest: %s\n", dynamic_subtest_name);
+	_subtest_starting_message(_SUBTEST_TYPE_DYNAMIC, dynamic_subtest_name);
 
 	_igt_log_buffer_reset();
 	igt_thread_clear_fail_state();
@@ -1456,23 +1538,16 @@ bool __igt_enter_dynamic_container(void)
 __noreturn static void exit_subtest(const char *result)
 {
 	struct timespec now;
-	const char *subtest_text = in_dynamic_subtest ? "Dynamic subtest" : "Subtest";
 	const char **subtest_name = in_dynamic_subtest ? &in_dynamic_subtest : &in_subtest;
 	struct timespec *thentime = in_dynamic_subtest ? &dynamic_subtest_time : &subtest_time;
 	jmp_buf *jmptarget = in_dynamic_subtest ? &igt_dynamic_jmpbuf : &igt_subtest_jmpbuf;
 
 	igt_gettime(&now);
 
-	igt_info("%s%s %s: %s (%.3fs)%s\n",
-		 (!__igt_plain_output) ? "\x1b[1m" : "",
-		 subtest_text, *subtest_name, result,
-		 igt_time_elapsed(thentime, &now),
-		 (!__igt_plain_output) ? "\x1b[0m" : "");
-	fflush(stdout);
-	if (stderr_needs_sentinel)
-		fprintf(stderr, "%s %s: %s (%.3fs)\n",
-			subtest_text, *subtest_name,
-			result, igt_time_elapsed(thentime, &now));
+	_subtest_result_message(in_dynamic_subtest ? _SUBTEST_TYPE_DYNAMIC : _SUBTEST_TYPE_NORMAL,
+				*subtest_name,
+				result,
+				igt_time_elapsed(thentime, &now));
 
 	igt_terminate_spins();
 
@@ -1539,7 +1614,15 @@ void igt_skip(const char *f, ...)
 
 	if (!igt_only_list_subtests()) {
 		va_start(args, f);
-		vprintf(f, args);
+		if (runner_connected()) {
+			char *str;
+
+			vasprintf(&str, f, args);
+			send_to_runner(runnerpacket_log(STDOUT_FILENO, str));
+			free(str);
+		} else {
+			vprintf(f, args);
+		}
 		va_end(args);
 	}
 
@@ -1827,7 +1910,10 @@ static bool running_under_gdb(void)
 
 static void __write_stderr(const char *str, size_t len)
 {
-	igt_ignore_warn(write(STDERR_FILENO, str, len));
+	if (runner_connected())
+		log_to_runner_sig_safe(str, len);
+	else
+		igt_ignore_warn(write(STDERR_FILENO, str, len));
 }
 
 static void write_stderr(const char *str)
@@ -1840,7 +1926,10 @@ static const char hex[] = "0123456789abcdef";
 static void
 xputch(int c)
 {
-	igt_ignore_warn(write(STDERR_FILENO, (const void *) &c, 1));
+	if (runner_connected())
+		log_to_runner_sig_safe((const void *) &c, 1);
+	else
+		igt_ignore_warn(write(STDERR_FILENO, (const void *) &c, 1));
 }
 
 static int
@@ -2849,11 +2938,9 @@ void igt_vlog(const char *domain, enum igt_log_level level, const char *format,
 	/* prepend all except information messages with process, domain and log
 	 * level information */
 	if (level != IGT_LOG_INFO) {
-		fwrite(formatted_line, sizeof(char), strlen(formatted_line),
-		       file);
+		_log_line_fprintf(file, "%s", formatted_line);
 	} else {
-		fwrite(thread_id, sizeof(char), strlen(thread_id), file);
-		fwrite(line, sizeof(char), strlen(line), file);
+		_log_line_fprintf(file, "%s%s", thread_id, line);
 	}
 
 	pthread_mutex_unlock(&print_mutex);
diff --git a/lib/meson.build b/lib/meson.build
index c665bd25..0f8e862f 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -66,6 +66,7 @@ lib_sources = [
 	'rendercopy_gen7.c',
 	'rendercopy_gen8.c',
 	'rendercopy_gen9.c',
+	'runnercomms.c',
 	'sw_sync.c',
 	'intel_aux_pgtable.c',
 	'intel_reg_map.c',
diff --git a/lib/runnercomms.c b/lib/runnercomms.c
new file mode 100644
index 00000000..0a45fd2e
--- /dev/null
+++ b/lib/runnercomms.c
@@ -0,0 +1,653 @@
+/*
+ * Copyright © 2021 Intel Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include <assert.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "igt_aux.h"
+#include "runnercomms.h"
+
+/**
+ * SECTION:runnercomms
+ * @short_description: Structured communication to igt_runner
+ * @title: runnercomms
+ * @include: runnercomms.h
+ *
+ * This library provides means for the tests to communicate to
+ * igt_runner with a formally specified protocol, avoiding
+ * shortcomings and pain points of text-based communication.
+ */
+
+static sig_atomic_t runner_socket_fd = -1;
+
+/**
+ * set_runner_socket:
+ * @fd: socket connected to runner
+ *
+ * If the passed fd is a valid socket, globally sets it to be the fd
+ * to use to talk to igt_runner.
+ */
+void set_runner_socket(int fd)
+{
+	struct stat sb;
+
+	if (fstat(fd, &sb))
+		return;
+
+	if (!S_ISSOCK(sb.st_mode))
+		return;
+
+	/*
+	 * We only sanity-check that the fd is a socket. We don't
+	 * check that it's a datagram socket etc.
+	 */
+
+	runner_socket_fd = fd;
+}
+
+/**
+ * runner_connected:
+ *
+ * Returns whether set_runner_socket has been called with a valid
+ * socket fd. Note: Will be true forever after that point. This
+ * function is used to mainly determine whether log strings will be
+ * output to the socket or to stdout/stderr and that cannot be changed
+ * even if the socket is lost midway.
+ */
+bool runner_connected(void)
+{
+	return runner_socket_fd >= 0;
+}
+
+/**
+ * send_to_runner:
+ * @packet: packet to send
+ *
+ * Sends the given communications packet to igt_runner. Calls free()
+ * on the packet, don't reuse it.
+ */
+void send_to_runner(struct runnerpacket *packet)
+{
+	if (runner_connected())
+		write(runner_socket_fd, packet, packet->size);
+	free(packet);
+}
+
+/* If enough data left, copy the data to dst, advance p, reduce size */
+static void read_integer(void* dst, size_t bytes, const char **p, uint32_t *size)
+{
+	if (*size < bytes) {
+		*size = 0;
+		return;
+	}
+
+	memcpy(dst, *p, bytes);
+	*p += bytes;
+	*size -= bytes;
+}
+
+/* If nul-termination can be found, set dststr to point to the cstring, advance p, reduce size */
+static void read_cstring(const char **dststr, const char **p, uint32_t *size)
+{
+	const char *end;
+
+	end = memchr(*p, '\0', *size);
+	if (end == NULL) {
+		*size = 0;
+		return;
+	}
+
+	*dststr = *p;
+	*size -= end - *p + 1;
+	*p = end + 1;
+}
+
+/**
+ * read_runnerpacket:
+ * @packet: runner communications packet to read
+ *
+ * Checks that the internal data of the communications packet is valid
+ * and the contents can safely be inspected without further checking
+ * for out-of-bounds etc. Constructs a runnerpacket_read_helper which
+ * will, for c-style strings, point to various sub-values directly in
+ * the #data field within @packet. Those are valid only as long as
+ * @packet is valid.
+ *
+ * Returns: An appropriately constructed runnerpacket_read_helper. On
+ * data validation errors, the #type of the returned value will be
+ * #PACKETTYPE_INVALID.
+ */
+runnerpacket_read_helper read_runnerpacket(const struct runnerpacket *packet)
+{
+	runnerpacket_read_helper ret = {};
+	uint32_t sizeleft;
+	const char *p;
+
+	if (packet->size < sizeof(*packet)) {
+		ret.type = PACKETTYPE_INVALID;
+		return ret;
+	}
+
+	ret.type = packet->type;
+	sizeleft = packet->size - sizeof(*packet);
+	p = packet->data;
+
+	switch (packet->type) {
+	case PACKETTYPE_LOG:
+		read_integer(&ret.log.stream, sizeof(ret.log.stream), &p, &sizeleft);
+		read_cstring(&ret.log.text, &p, &sizeleft);
+
+		if (ret.log.text == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	case PACKETTYPE_EXEC:
+		read_cstring(&ret.exec.cmdline, &p, &sizeleft);
+
+		if (ret.exec.cmdline == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	case PACKETTYPE_EXIT:
+		read_integer(&ret.exit.exitcode, sizeof(ret.exit.exitcode), &p, &sizeleft);
+		read_cstring(&ret.exit.timeused, &p, &sizeleft);
+
+		break;
+	case PACKETTYPE_SUBTEST_START:
+		read_cstring(&ret.subteststart.name, &p, &sizeleft);
+
+		if (ret.subteststart.name == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	case PACKETTYPE_SUBTEST_RESULT:
+		read_cstring(&ret.subtestresult.name, &p, &sizeleft);
+		read_cstring(&ret.subtestresult.result, &p, &sizeleft);
+		read_cstring(&ret.subtestresult.timeused, &p, &sizeleft);
+		read_cstring(&ret.subtestresult.reason, &p, &sizeleft);
+
+		if (ret.subtestresult.name == NULL ||
+		    ret.subtestresult.result == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	case PACKETTYPE_DYNAMIC_SUBTEST_START:
+		read_cstring(&ret.dynamicsubteststart.name, &p, &sizeleft);
+
+		if (ret.dynamicsubteststart.name == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	case PACKETTYPE_DYNAMIC_SUBTEST_RESULT:
+		read_cstring(&ret.dynamicsubtestresult.name, &p, &sizeleft);
+		read_cstring(&ret.dynamicsubtestresult.result, &p, &sizeleft);
+		read_cstring(&ret.dynamicsubtestresult.timeused, &p, &sizeleft);
+		read_cstring(&ret.dynamicsubtestresult.reason, &p, &sizeleft);
+
+		if (ret.dynamicsubtestresult.name == NULL ||
+		    ret.dynamicsubtestresult.result == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	case PACKETTYPE_VERSIONSTRING:
+		read_cstring(&ret.versionstring.text, &p, &sizeleft);
+
+		if (ret.versionstring.text == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	case PACKETTYPE_RESULT_OVERRIDE:
+		read_cstring(&ret.resultoverride.result, &p, &sizeleft);
+
+		if (ret.resultoverride.result == NULL)
+			ret.type = PACKETTYPE_INVALID;
+
+		break;
+	default:
+		ret.type = PACKETTYPE_INVALID;
+		break;
+	}
+
+	return ret;
+}
+
+struct runnerpacket *runnerpacket_log(uint8_t stream, const char *text)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	size = sizeof(struct runnerpacket) + sizeof(stream) + strlen(text) + 1;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_LOG;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	memcpy(p, &stream, sizeof(stream));
+	p += sizeof(stream);
+
+	strcpy(p, text);
+	p += strlen(text) + 1;
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_exec(char **argv)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+	int i;
+
+	size = sizeof(struct runnerpacket);
+
+	for (i = 0; argv[i] != NULL; i++)
+		size += strlen(argv[i]) + 1; // followed by a space of \0 so +1 either way for each
+
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_EXEC;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	for (i = 0; argv[i] != NULL; i++) {
+		if (i != 0)
+			*p++ = ' ';
+
+		strcpy(p, argv[i]);
+		p += strlen(argv[i]);
+	}
+	p[0] = '\0';
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_exit(int32_t exitcode, const char *timeused)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	size = sizeof(struct runnerpacket) + sizeof(exitcode) + strlen(timeused) + 1;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_EXIT;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	memcpy(p, &exitcode, sizeof(exitcode));
+	p += sizeof(exitcode);
+
+	strcpy(p, timeused);
+	p += strlen(timeused) + 1;
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_subtest_start(const char *name)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	size = sizeof(struct runnerpacket) + strlen(name) + 1;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_SUBTEST_START;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	strcpy(p, name);
+	p += strlen(name) + 1;
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_subtest_result(const char *name, const char *result,
+						 const char *timeused, const char *reason)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	if (reason == NULL)
+		reason = "";
+
+	size = sizeof(struct runnerpacket) + strlen(name) + strlen(result) + strlen(timeused) + strlen(reason) + 4;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_SUBTEST_RESULT;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	strcpy(p, name);
+	p += strlen(name) + 1;
+
+	strcpy(p, result);
+	p += strlen(result) + 1;
+
+	strcpy(p, timeused);
+	p += strlen(timeused) + 1;
+
+	strcpy(p, reason);
+	p += strlen(reason) + 1;
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_dynamic_subtest_start(const char *name)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	size = sizeof(struct runnerpacket) + strlen(name) + 1;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_DYNAMIC_SUBTEST_START;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	strcpy(p, name);
+	p += strlen(name) + 1;
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_dynamic_subtest_result(const char *name, const char *result,
+							 const char *timeused, const char *reason)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	if (reason == NULL)
+		reason = "";
+
+	size = sizeof(struct runnerpacket) + strlen(name) + strlen(result) + strlen(timeused) + strlen(reason) + 4;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_DYNAMIC_SUBTEST_RESULT;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	strcpy(p, name);
+	p += strlen(name) + 1;
+
+	strcpy(p, result);
+	p += strlen(result) + 1;
+
+	strcpy(p, timeused);
+	p += strlen(timeused) + 1;
+
+	strcpy(p, reason);
+	p += strlen(reason) + 1;
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_versionstring(const char *text)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	size = sizeof(struct runnerpacket) + strlen(text) + 1;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_VERSIONSTRING;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	strcpy(p, text);
+	p += strlen(text) + 1;
+
+	return packet;
+}
+
+struct runnerpacket *runnerpacket_resultoverride(const char *result)
+{
+	struct runnerpacket *packet;
+	uint32_t size;
+	char *p;
+
+	size = sizeof(struct runnerpacket) + strlen(result) + 1;
+	packet = malloc(size);
+
+	packet->size = size;
+	packet->type = PACKETTYPE_RESULT_OVERRIDE;
+	packet->senderpid = getpid();
+	packet->sendertid = gettid();
+
+	p = packet->data;
+
+	strcpy(p, result);
+	p += strlen(result) + 1;
+
+	return packet;
+}
+
+uint32_t socket_dump_canary(void)
+{
+	return 'I' << 24 | 'G' << 16 | 'T' << 8 | '1';
+}
+
+void log_to_runner_sig_safe(const char *str, size_t len)
+{
+	size_t prlen = len;
+
+	struct runnerpacket_log_sig_safe p = {
+					      .size = sizeof(struct runnerpacket) + sizeof(uint8_t),
+					      .type = PACKETTYPE_LOG,
+					      .senderpid = getpid(),
+					      .sendertid = 0, /* gettid() not signal safe */
+					      .stream = STDERR_FILENO,
+	};
+
+	if (len > sizeof(p.data) - 1)
+		prlen = sizeof(p.data) - 1;
+	memcpy(p.data, str, prlen);
+	p.size += prlen + 1;
+
+	write(runner_socket_fd, &p, p.size);
+
+	len -= prlen;
+	if (len)
+		log_to_runner_sig_safe(str + prlen, len);
+}
+
+/**
+ * comms_read_dump:
+ * @fd: Open fd to a comms dump file
+ * @visitor: Collection of packet handlers
+ *
+ * Reads a comms dump file, calling specified handler functions for
+ * individual packets.
+ *
+ * Returns: #COMMSPARSE_ERROR for failures reading or parsing the
+ * dump, #COMMSPARSE_EMPTY for empty dumps (no comms used),
+ * #COMMSPARSE_SUCCESS for successful read.
+ */
+int comms_read_dump(int fd, struct comms_visitor *visitor)
+{
+	struct stat statbuf;
+	char *buf, *bufend, *p;
+	int ret = COMMSPARSE_EMPTY;
+	bool cont = true;
+
+	if (fd < 0)
+		return COMMSPARSE_EMPTY;
+
+	if (fstat(fd, &statbuf))
+		return COMMSPARSE_ERROR;
+
+	if (statbuf.st_size == 0)
+		return COMMSPARSE_ERROR;
+
+	buf = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
+	if (buf == MAP_FAILED)
+		return COMMSPARSE_ERROR;
+
+	bufend = buf + statbuf.st_size;
+	p = buf;
+
+	while (p != NULL && p != bufend && cont) {
+		const struct runnerpacket *packet;
+		runnerpacket_read_helper helper;
+
+		if (bufend - p >= sizeof(uint32_t)) {
+			uint32_t canary;
+
+			memcpy(&canary, p, sizeof(canary));
+			if (canary != socket_dump_canary()) {
+				fprintf(stderr,
+					"Invalid canary while parsing comms: %"PRIu32", expected %"PRIu32"\n",
+					canary, socket_dump_canary());
+				munmap(buf, statbuf.st_size);
+				return COMMSPARSE_ERROR;
+			}
+		}
+		p += sizeof(uint32_t);
+
+		if (bufend -p < sizeof(struct runnerpacket)) {
+			fprintf(stderr,
+				"Error parsing comms: Expected runnerpacket after canary, truncated file?\n");
+			munmap(buf, statbuf.st_size);
+			return COMMSPARSE_ERROR;
+		}
+
+		packet = (struct runnerpacket *)p;
+		if (bufend -p < packet->size) {
+			fprintf(stderr,
+				"Error parsing comms: Unexpected end of file, truncated file?\n");
+			munmap(buf, statbuf.st_size);
+			return COMMSPARSE_ERROR;
+		}
+		p += packet->size;
+
+		/*
+		 * Runner sends EXEC itself before executing the test.
+		 * If we get other types, it indicates the test really
+		 * uses socket comms.
+		 */
+		if (packet->type != PACKETTYPE_EXEC)
+			ret = COMMSPARSE_SUCCESS;
+
+		switch (packet->type) {
+		case PACKETTYPE_INVALID:
+			printf("Warning: Unknown packet type %"PRIu32", skipping\n", packet->type);
+			break;
+		case PACKETTYPE_LOG:
+			if (visitor->log) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->log(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_EXEC:
+			if (visitor->exec) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->exec(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_EXIT:
+			if (visitor->exit) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->exit(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_SUBTEST_START:
+			if (visitor->subtest_start) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->subtest_start(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_SUBTEST_RESULT:
+			if (visitor->subtest_result) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->subtest_result(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_DYNAMIC_SUBTEST_START:
+			if (visitor->dynamic_subtest_start) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->dynamic_subtest_start(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_DYNAMIC_SUBTEST_RESULT:
+			if (visitor->dynamic_subtest_result) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->dynamic_subtest_result(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_VERSIONSTRING:
+			if (visitor->versionstring) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->versionstring(packet, helper, visitor->userdata);
+			}
+			break;
+		case PACKETTYPE_RESULT_OVERRIDE:
+			if (visitor->result_override) {
+				helper = read_runnerpacket(packet);
+				cont = visitor->result_override(packet, helper, visitor->userdata);
+			}
+			break;
+		default:
+			printf("Warning: Unknown packet type %"PRIu32"\n", helper.type);
+			break;
+		}
+	}
+
+	munmap(buf, statbuf.st_size);
+	return cont ? ret : COMMSPARSE_ERROR;
+}
diff --git a/lib/runnercomms.h b/lib/runnercomms.h
new file mode 100644
index 00000000..0f066008
--- /dev/null
+++ b/lib/runnercomms.h
@@ -0,0 +1,277 @@
+/*
+ * Copyright © 2021 Intel Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#ifndef IGT_RUNNERCOMMS_H
+#define IGT_RUNNERCOMMS_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ * A flat struct that can and will be directly dumped to
+ * disk. Constructed with runnerpacket_<type>() helper functions.
+ */
+struct runnerpacket {
+	uint32_t size; /* Full size of the packet in octets */
+	uint32_t type; /* runnerpacket_type, but fixed width */
+	int32_t senderpid;
+	int32_t sendertid;
+
+	char data[];
+} __attribute__((packed));
+
+_Static_assert(sizeof(struct runnerpacket) == 4 * 4, "runnerpacket structure must not change");
+_Static_assert(offsetof(struct runnerpacket, data) == 4 * 4, "runnerpacket structure must not change");
+
+/*
+ * A helper for reading and parsing runnerpacket structs. Fields will
+ * point directly into the data field of an existing runnerpacket
+ * object. Constructed with read_runnerpacket().
+ *
+ * Some fields can be left as 0 / NULL / some other applicable invalid
+ * value in the case of having older dumps read with binaries that
+ * have extended the data formats.
+ */
+typedef union runnerpacket_read_helper {
+	/*
+	 * All other fields must begin with "uint32_t type" so it's a
+	 * common initial sequence, safe to read no matter what union
+	 * field is active.
+	 */
+	uint32_t type;
+
+	struct {
+		uint32_t type;
+
+		uint8_t stream;
+		const char *text;
+	} log;
+
+	struct {
+		uint32_t type;
+
+		const char *cmdline;
+	} exec;
+
+	struct {
+		uint32_t type;
+
+		int32_t exitcode;
+		const char *timeused;
+	} exit;
+
+	struct {
+		uint32_t type;
+
+		const char *name;
+	} subteststart;
+
+	struct {
+		uint32_t type;
+
+		const char *name;
+		const char *result;
+		const char *timeused;
+		const char *reason;
+	} subtestresult;
+
+	struct {
+		uint32_t type;
+
+		const char *name;
+	} dynamicsubteststart;
+
+	struct {
+		uint32_t type;
+
+		const char *name;
+		const char *result;
+		const char *timeused;
+		const char *reason;
+	} dynamicsubtestresult;
+
+	struct {
+		uint32_t type;
+
+		const char *text;
+	} versionstring;
+
+	struct {
+		uint32_t type;
+
+		const char *result;
+	} resultoverride;
+} runnerpacket_read_helper;
+
+void set_runner_socket(int fd);
+bool runner_connected(void);
+void send_to_runner(struct runnerpacket *packet);
+
+runnerpacket_read_helper read_runnerpacket(const struct runnerpacket *packet);
+
+/*
+ * All packet types must document the format of the data[] array. The
+ * notation used is
+ *
+ * Explanation of the packet
+ * type: explanation of values
+ * type2: explanation of values
+ * (etc)
+ *
+ * The type "cstring" can be used to denote that the content is a
+ * nul-terminated string.
+ */
+enum runnerpacket_type {
+      PACKETTYPE_INVALID,
+      /* No data. This type is only used on parse failures and such. */
+
+      PACKETTYPE_LOG,
+      /*
+       * Normal log message.
+       * uint8_t: 1 = stdout, 2 = stderr
+       * cstring: Log text
+       */
+
+      PACKETTYPE_EXEC,
+      /*
+       * Command line executed. Sent by runner before calling exec().
+       * cstring: command line as one string, argv[0] included, space separated
+       */
+
+      PACKETTYPE_EXIT,
+      /*
+       * Process exit. Written by runner.
+       * int32_t: exitcode
+       * cstring: Time taken by the process from exec to exit, as a floating point value in seconds, as text
+       */
+
+      PACKETTYPE_SUBTEST_START,
+      /*
+       * Subtest begins.
+       * cstring: Name of the subtest
+       */
+
+      PACKETTYPE_SUBTEST_RESULT,
+      /*
+       * Subtest ends. Can appear without a corresponding SUBTEST_START packet.
+       * cstring: Name of the subtest
+       * cstring: Result of the subtest
+       * cstring: Time taken by the subtest, as a floating point value in seconds, as text
+       * cstring: If len > 0, the reason for the subtest result (fail/skip)
+       */
+
+      PACKETTYPE_DYNAMIC_SUBTEST_START,
+      /*
+       * Dynamic subtest begins.
+       * cstring: Name of the dynamic subtest
+       */
+
+      PACKETTYPE_DYNAMIC_SUBTEST_RESULT,
+      /*
+       * Dynamic subtest ends.
+       * cstring: Name of the dynamic subtest
+       * cstring: Result of the dynamic subtest
+       * cstring: Time taken by the dynamic subtest, as a floating point value in seconds, as text
+       * cstring: If len > 0, the reason for the dynamic subtest result (fail/skip)
+       */
+
+      PACKETTYPE_VERSIONSTRING,
+      /*
+       * Version of the running test
+       * cstring: Version string
+       */
+
+      PACKETTYPE_RESULT_OVERRIDE,
+      /*
+       * Override the result of the most recently started test/subtest/dynamic subtest. Used for timeout and abort etc.
+       * cstring: The result to use, as text. All lowercase.
+       */
+
+
+      PACKETTYPE_NUM_TYPES /* must be last */
+};
+
+struct runnerpacket *runnerpacket_log(uint8_t stream, const char *text);
+struct runnerpacket *runnerpacket_exec(char **argv);
+struct runnerpacket *runnerpacket_exit(int32_t exitcode, const char *timeused);
+struct runnerpacket *runnerpacket_subtest_start(const char *name);
+struct runnerpacket *runnerpacket_subtest_result(const char *name, const char *result,
+						 const char *timeused, const char *reason);
+struct runnerpacket *runnerpacket_dynamic_subtest_start(const char *name);
+struct runnerpacket *runnerpacket_dynamic_subtest_result(const char *name, const char *result,
+							 const char *timeused, const char *reason);
+struct runnerpacket *runnerpacket_versionstring(const char *text);
+struct runnerpacket *runnerpacket_resultoverride(const char *result);
+
+uint32_t socket_dump_canary(void);
+
+struct runnerpacket_log_sig_safe {
+	uint32_t size;
+	uint32_t type;
+	int32_t senderpid;
+	int32_t sendertid;
+
+	uint8_t stream;
+	char data[128];
+} __attribute__((packed));
+
+_Static_assert(offsetof(struct runnerpacket_log_sig_safe, stream) == 4 * 4, "signal-safe log runnerpacket must be compatible");
+_Static_assert(offsetof(struct runnerpacket_log_sig_safe, data) == 4 * 4 + 1, "signal-safe log runnerpacket must be compatible");
+
+void log_to_runner_sig_safe(const char *str, size_t len);
+
+/*
+ * Comms dump reader
+ *
+ * A visitor for reading comms dump files. Calls handlers if
+ * corresponding handler is set. Reading stops if a handler returns
+ * false.
+ *
+ * The passed arguments are the packet itself, the already-constructed
+ * read helper, and the userdata pointer from the visitor.
+ */
+typedef bool (*handler_t)(const struct runnerpacket *, runnerpacket_read_helper, void *userdata);
+
+struct comms_visitor {
+	handler_t log;
+	handler_t exec;
+	handler_t exit;
+	handler_t subtest_start;
+	handler_t subtest_result;
+	handler_t dynamic_subtest_start;
+	handler_t dynamic_subtest_result;
+	handler_t versionstring;
+	handler_t result_override;
+
+	void* userdata;
+};
+
+enum {
+	COMMSPARSE_ERROR,
+	COMMSPARSE_EMPTY,
+	COMMSPARSE_SUCCESS
+};
+int comms_read_dump(int fd, struct comms_visitor *visitor);
+
+#endif
diff --git a/lib/tests/igt_runnercomms_packets.c b/lib/tests/igt_runnercomms_packets.c
new file mode 100644
index 00000000..78440f4d
--- /dev/null
+++ b/lib/tests/igt_runnercomms_packets.c
@@ -0,0 +1,281 @@
+/*
+ * Copyright © 2021 Intel Corporation
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "runnercomms.h"
+
+#include "igt_core.h"
+
+static void igt_assert_eqstr(const char *one, const char *two)
+{
+	if (one == NULL && two == NULL)
+		return;
+
+	igt_assert_f(one != NULL && two != NULL, "Strings differ (one is NULL): %s vs %s\n", one, two);
+
+	igt_assert_f(!strcmp(one, two), "Strings differ: '%s' vs '%s'\n", one, two);
+}
+
+
+static const uint8_t num8 = 5;
+static const int32_t num32 = -67;
+static const char *text1 = "Text one";
+static const char *text2 = "Text two";
+static const char *text3 = "Text three";
+static const char *text4 = "Text four";
+
+static struct runnerpacket *create_log(void)
+{
+	return runnerpacket_log(num8, text1);
+}
+
+static void validate_log(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_LOG);
+	igt_assert_eq(helper.type, PACKETTYPE_LOG);
+
+	igt_assert_eq(helper.log.stream, num8);
+	igt_assert_eqstr(helper.log.text, text1);
+}
+
+static struct runnerpacket *create_exec(void)
+{
+	char *argv[] = { strdup(text1), strdup(text2), strdup(text3), strdup(text4), NULL };
+	struct runnerpacket *packet;
+
+	packet = runnerpacket_exec(argv);
+
+	free(argv[0]);
+	free(argv[1]);
+	free(argv[2]);
+	free(argv[3]);
+
+	return packet;
+}
+
+static void validate_exec(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+	char cmpstr[256];
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_EXEC);
+	igt_assert_eq(helper.type, PACKETTYPE_EXEC);
+
+	snprintf(cmpstr, sizeof(cmpstr), "%s %s %s %s", text1, text2, text3, text4);
+	igt_assert_eqstr(helper.exec.cmdline, cmpstr);
+}
+
+static struct runnerpacket *create_exit(void)
+{
+	return runnerpacket_exit(num32, text1);
+}
+
+static void validate_exit(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_EXIT);
+	igt_assert_eq(helper.type, PACKETTYPE_EXIT);
+
+	igt_assert_eq(helper.exit.exitcode, num32);
+	igt_assert_eqstr(helper.exit.timeused, text1);
+}
+
+static struct runnerpacket *create_subtest_start(void)
+{
+	return runnerpacket_subtest_start(text1);
+}
+
+static void validate_subtest_start(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_SUBTEST_START);
+	igt_assert_eq(helper.type, PACKETTYPE_SUBTEST_START);
+
+	igt_assert_eqstr(helper.subteststart.name, text1);
+}
+
+static struct runnerpacket *create_subtest_result(void)
+{
+	return runnerpacket_subtest_result(text1, text2, text3, text4);
+}
+
+static void validate_subtest_result(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_SUBTEST_RESULT);
+	igt_assert_eq(helper.type, PACKETTYPE_SUBTEST_RESULT);
+
+	igt_assert_eqstr(helper.subtestresult.name, text1);
+	igt_assert_eqstr(helper.subtestresult.result, text2);
+	igt_assert_eqstr(helper.subtestresult.timeused, text3);
+	igt_assert_eqstr(helper.subtestresult.reason, text4);
+}
+
+static struct runnerpacket *create_dynamic_subtest_start(void)
+{
+	return runnerpacket_dynamic_subtest_start(text1);
+}
+
+static void validate_dynamic_subtest_start(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_DYNAMIC_SUBTEST_START);
+	igt_assert_eq(helper.type, PACKETTYPE_DYNAMIC_SUBTEST_START);
+
+	igt_assert_eqstr(helper.dynamicsubteststart.name, text1);
+}
+
+static struct runnerpacket *create_dynamic_subtest_result(void)
+{
+	return runnerpacket_dynamic_subtest_result(text1, text2, text3, text4);
+}
+
+static void validate_dynamic_subtest_result(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_DYNAMIC_SUBTEST_RESULT);
+	igt_assert_eq(helper.type, PACKETTYPE_DYNAMIC_SUBTEST_RESULT);
+
+	igt_assert_eqstr(helper.dynamicsubtestresult.name, text1);
+	igt_assert_eqstr(helper.dynamicsubtestresult.result, text2);
+	igt_assert_eqstr(helper.dynamicsubtestresult.timeused, text3);
+	igt_assert_eqstr(helper.dynamicsubtestresult.reason, text4);
+}
+
+static struct runnerpacket *create_versionstring(void)
+{
+	return runnerpacket_versionstring(text1);
+}
+
+static void validate_versionstring(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_VERSIONSTRING);
+	igt_assert_eq(helper.type, PACKETTYPE_VERSIONSTRING);
+
+	igt_assert_eqstr(helper.versionstring.text, text1);
+}
+
+static struct runnerpacket *create_result_override(void)
+{
+	return runnerpacket_resultoverride(text1);
+}
+
+static void validate_result_override(struct runnerpacket *packet)
+{
+	runnerpacket_read_helper helper;
+
+	helper = read_runnerpacket(packet);
+
+	igt_assert_eq(packet->type, PACKETTYPE_RESULT_OVERRIDE);
+	igt_assert_eq(helper.type, PACKETTYPE_RESULT_OVERRIDE);
+
+	igt_assert_eqstr(helper.resultoverride.result, text1);
+}
+
+struct {
+	struct runnerpacket * (*create)(void);
+	void (*validate)(struct runnerpacket *packet);
+} basic_creation[] = {
+		      { create_log, validate_log },
+		      { create_exec, validate_exec },
+		      { create_exit, validate_exit },
+		      { create_subtest_start, validate_subtest_start },
+		      { create_subtest_result, validate_subtest_result },
+		      { create_dynamic_subtest_start, validate_dynamic_subtest_start },
+		      { create_dynamic_subtest_result, validate_dynamic_subtest_result },
+		      { create_versionstring, validate_versionstring },
+		      { create_result_override, validate_result_override },
+		      { NULL, NULL }
+};
+
+igt_main
+{
+	igt_subtest("create-and-parse-normal") {
+		for (typeof (*basic_creation) *t = basic_creation; t->create; t++) {
+			struct runnerpacket *packet;
+
+			packet = t->create();
+			igt_assert(packet != NULL);
+			igt_assert(packet->type != PACKETTYPE_INVALID);
+			t->validate(packet);
+		}
+	}
+
+	igt_subtest("packet-too-short") {
+		struct runnerpacket *packet;
+		runnerpacket_read_helper helper;
+
+		packet = runnerpacket_log(1, "Hello");
+		igt_assert(packet != NULL);
+		igt_assert_eq(packet->type, PACKETTYPE_LOG);
+
+		packet->size = 4; /* not even sizeof(*packet) */
+		helper = read_runnerpacket(packet);
+		igt_assert_eq(helper.type, PACKETTYPE_INVALID);
+
+		free(packet);
+	}
+
+	igt_subtest("nul-termination-missing") {
+		/* Parsing should reject the packet when nul-termination is missing */
+		struct runnerpacket *packet;
+		runnerpacket_read_helper helper;
+
+		uint8_t num = 1;
+		const char *text = "This is text";
+		packet = runnerpacket_log(num, text);
+		igt_assert(packet != NULL);
+		igt_assert_eq(packet->type, PACKETTYPE_LOG);
+
+		/* make the packet too short to include the nul-termination in the string */
+		packet->size -= 2;
+		helper = read_runnerpacket(packet);
+		igt_assert_eq(helper.type, PACKETTYPE_INVALID);
+
+		free(packet);
+	}
+}
diff --git a/lib/tests/meson.build b/lib/tests/meson.build
index d5666c24..7a52a787 100644
--- a/lib/tests/meson.build
+++ b/lib/tests/meson.build
@@ -14,6 +14,7 @@ lib_tests = [
 	'igt_invalid_subtest_name',
 	'igt_nesting',
 	'igt_no_exit',
+	'igt_runnercomms_packets',
 	'igt_segfault',
 	'igt_simulation',
 	'igt_stats',
diff --git a/runner/decoder.c b/runner/decoder.c
new file mode 100644
index 00000000..850b6ad5
--- /dev/null
+++ b/runner/decoder.c
@@ -0,0 +1,131 @@
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "runnercomms.h"
+
+static bool handle_log(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) LOG\tstream=%d,text=%s",
+	       packet->senderpid, packet->sendertid,
+	       helper.log.stream, helper.log.text);
+	if (strlen(helper.log.text) == 0 || helper.log.text[strlen(helper.log.text) - 1] != '\n')
+		printf("\n");
+
+	return true;
+}
+
+static bool handle_exec(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) EXEC\tcmdline=%s\n",
+	       packet->senderpid, packet->sendertid,
+	       helper.exec.cmdline);
+
+	return true;
+}
+
+static bool handle_exit(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) EXIT\texitcode=%d,timeused=%s\n",
+	       packet->senderpid, packet->sendertid,
+	       helper.exit.exitcode, helper.exit.timeused);
+
+	return true;
+}
+
+static bool handle_subtest_start(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) SUBTEST_START\tname=%s\n",
+	       packet->senderpid, packet->sendertid,
+	       helper.subteststart.name);
+
+	return true;
+}
+
+static bool handle_subtest_result(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) SUBTEST_RESULT\tname=%s,result=%s,timeused=%s,reason=%s\n",
+	       packet->senderpid, packet->sendertid,
+	       helper.subtestresult.name,
+	       helper.subtestresult.result,
+	       helper.subtestresult.timeused,
+	       helper.subtestresult.reason ?: "<null>");
+
+	return true;
+}
+
+static bool handle_dynamic_subtest_start(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) DYNAMIC_SUBTEST_START\tname=%s\n",
+	       packet->senderpid, packet->sendertid,
+	       helper.dynamicsubteststart.name);
+
+	return true;
+}
+
+static bool handle_dynamic_subtest_result(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) DYNAMIC_SUBTEST_RESULT\tname=%s,result=%s,timeused=%s,reason=%s\n",
+	       packet->senderpid, packet->sendertid,
+	       helper.dynamicsubtestresult.name,
+	       helper.dynamicsubtestresult.result,
+	       helper.dynamicsubtestresult.timeused,
+	       helper.dynamicsubtestresult.reason ?: "<null>");
+
+	return true;
+}
+
+static bool handle_versionstring(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("(pid=%d tid=%d) VERSIONSTRING\ttext=%s",
+	       packet->senderpid, packet->sendertid,
+	       helper.versionstring.text);
+	if (strlen(helper.versionstring.text) == 0 || helper.versionstring.text[strlen(helper.versionstring.text) - 1] != '\n')
+		printf("\n");
+
+	return true;
+}
+
+static bool handle_result_override(const struct runnerpacket *packet, runnerpacket_read_helper helper, void *userdata)
+{
+	printf("pid=%d tid=%d) RESULT_OVERRIDE\tresult=%s\n",
+	       packet->senderpid, packet->sendertid,
+	       helper.resultoverride.result);
+
+	return true;
+}
+
+struct comms_visitor logger = {
+	.log = handle_log,
+	.exec = handle_exec,
+	.exit = handle_exit,
+	.subtest_start = handle_subtest_start,
+	.subtest_result = handle_subtest_result,
+	.dynamic_subtest_start = handle_dynamic_subtest_start,
+	.dynamic_subtest_result = handle_dynamic_subtest_result,
+	.versionstring = handle_versionstring,
+	.result_override = handle_result_override,
+};
+
+int main(int argc, char **argv)
+{
+	int fd;
+
+	if (argc < 2) {
+		printf("Usage: %s igt-comms-data-file\n", argv[0]);
+		return 2;
+	}
+
+	fd = open(argv[1], O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "Failure opening %s: %m\n", argv[1]);
+		return 1;
+	}
+
+	comms_read_dump(fd, &logger);
+
+	return 0;
+}
diff --git a/runner/executor.c b/runner/executor.c
index 964d0063..11edca51 100644
--- a/runner/executor.c
+++ b/runner/executor.c
@@ -12,9 +12,11 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/ioctl.h>
+#include <sys/mman.h>
 #include <sys/select.h>
 #include <sys/poll.h>
 #include <sys/signalfd.h>
+#include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/time.h>
 #include <sys/types.h>
@@ -29,6 +31,7 @@
 #include "igt_taints.h"
 #include "executor.h"
 #include "output_strings.h"
+#include "runnercomms.h"
 
 #define KMSG_HEADER "[IGT] "
 #define KMSG_WARN 4
@@ -365,7 +368,7 @@ static char *need_to_abort(const struct settings* settings)
 	return NULL;
 }
 
-static void prune_subtest(struct job_list_entry *entry, char *subtest)
+static void prune_subtest(struct job_list_entry *entry, const char *subtest)
 {
 	char *excl;
 
@@ -442,11 +445,77 @@ static bool prune_from_journal(struct job_list_entry *entry, int fd)
 	return pruned > 0;
 }
 
+struct prune_comms_data
+{
+	struct job_list_entry *entry;
+	int pruned;
+	bool got_exit;
+};
+
+static bool prune_handle_subtest_start(const struct runnerpacket *packet,
+				       runnerpacket_read_helper helper,
+				       void *userdata)
+{
+	struct prune_comms_data *data = userdata;
+
+	prune_subtest(data->entry, helper.subteststart.name);
+	data->pruned++;
+
+	return true;
+}
+
+static bool prune_handle_exit(const struct runnerpacket *packet,
+			      runnerpacket_read_helper helper,
+			      void *userdata)
+{
+	struct prune_comms_data *data = userdata;
+
+	data->got_exit = true;
+
+	return true;
+}
+
+static bool prune_from_comms(struct job_list_entry *entry, int fd)
+{
+	struct prune_comms_data data = {
+		.entry = entry,
+		.pruned = 0,
+		.got_exit = false,
+	};
+	struct comms_visitor visitor = {
+		.subtest_start = prune_handle_subtest_start,
+		.exit = prune_handle_exit,
+
+		.userdata = &data,
+	};
+	size_t old_count = entry->subtest_count;
+
+	if (comms_read_dump(fd, &visitor) == COMMSPARSE_ERROR)
+		return false;
+
+	/*
+	 * If we know the subtests we originally wanted to run, check
+	 * if we got an equal amount already.
+	 */
+	if (old_count > 0 && data.pruned >= old_count)
+		entry->binary[0] = '\0';
+
+	/*
+	 * If we don't know how many subtests there should be but we
+	 * got an exit, also consider the test fully finished.
+	 */
+	if (data.got_exit)
+		entry->binary[0] = '\0';
+
+	return data.pruned > 0;
+}
+
 static const char *filenames[_F_LAST] = {
 	[_F_JOURNAL] = "journal.txt",
 	[_F_OUT] = "out.txt",
 	[_F_ERR] = "err.txt",
 	[_F_DMESG] = "dmesg.txt",
+	[_F_SOCKET] = "comms.txt",
 };
 
 static int open_at_end(int dirfd, const char *name)
@@ -478,6 +547,9 @@ bool open_output_files(int dirfd, int *fds, bool write)
 
 	for (i = 0; i < _F_LAST; i++) {
 		if ((fds[i] = openfunc(dirfd, filenames[i])) < 0) {
+			/* Ignore failure to open socket comms for reading */
+			if (i == _F_SOCKET && !write) continue;
+
 			while (--i >= 0)
 				close(fds[i]);
 			return false;
@@ -758,6 +830,16 @@ static int next_kill_signal(int killed)
 	}
 }
 
+static void write_packet_with_canary(int fd, struct runnerpacket *packet, bool sync)
+{
+	uint32_t canary = socket_dump_canary();
+
+	write(fd, &canary, sizeof(canary));
+	write(fd, packet, packet->size);
+	if (sync)
+		fdatasync(fd);
+}
+
 /*
  * Returns:
  *  =0 - Success
@@ -765,7 +847,8 @@ static int next_kill_signal(int killed)
  *  >0 - Timeout happened, need to recreate from journal
  */
 static int monitor_output(pid_t child,
-			  int outfd, int errfd, int kmsgfd, int sigfd,
+			  int outfd, int errfd, int socketfd,
+			  int kmsgfd, int sigfd,
 			  int *outputs,
 			  double *time_spent,
 			  struct settings *settings,
@@ -787,12 +870,15 @@ static int monitor_output(pid_t child,
 	unsigned long taints = 0;
 	bool aborting = false;
 	size_t disk_usage = 0;
+	bool socket_comms_used = false; /* whether the test actually uses comms */
 
 	igt_gettime(&time_beg);
 	time_last_activity = time_last_subtest = time_killed = time_beg;
 
 	if (errfd > nfds)
 		nfds = errfd;
+	if (socketfd > nfds)
+		nfds = socketfd;
 	if (kmsgfd > nfds)
 		nfds = kmsgfd;
 	if (sigfd > nfds)
@@ -831,6 +917,8 @@ static int monitor_output(pid_t child,
 			FD_SET(outfd, &set);
 		if (errfd >= 0)
 			FD_SET(errfd, &set);
+		if (socketfd >= 0)
+			FD_SET(socketfd, &set);
 		if (kmsgfd >= 0)
 			FD_SET(kmsgfd, &set);
 		if (sigfd >= 0)
@@ -963,6 +1051,99 @@ static int monitor_output(pid_t child,
 			}
 		}
 
+		if (socketfd >= 0 && FD_ISSET(socketfd, &set)) {
+			struct runnerpacket *packet;
+
+			time_last_activity = time_now;
+
+			/* Fully drain everything */
+			while (true) {
+				s = recv(socketfd, buf, sizeof(buf), MSG_DONTWAIT);
+
+				if (s < 0) {
+					if (errno == EAGAIN)
+						break;
+
+					errf("Error reading from communication socket: %m\n");
+
+					close(socketfd);
+					socketfd = -1;
+					goto socket_end;
+				}
+
+				packet = (struct runnerpacket *)buf;
+				if (s < sizeof(*packet) || s != packet->size) {
+					errf("Socket communication error: Received %zd bytes, expected %zd\n",
+					     s, s >= sizeof(packet->size) ? packet->size : sizeof(*packet));
+					close(socketfd);
+					socketfd = -1;
+					goto socket_end;
+				}
+
+				write_packet_with_canary(outputs[_F_SOCKET], packet, settings->sync);
+				disk_usage += packet->size;
+
+				/*
+				 * runner sends EXEC itself before executing
+				 * the test, other types indicate the test
+				 * really uses socket comms
+				 */
+				if (packet->type != PACKETTYPE_EXEC)
+					socket_comms_used = true;
+
+				if (packet->type == PACKETTYPE_SUBTEST_START ||
+				    packet->type == PACKETTYPE_DYNAMIC_SUBTEST_START)
+					time_last_subtest = time_now;
+
+				if (settings->log_level >= LOG_LEVEL_VERBOSE) {
+					runnerpacket_read_helper helper = {};
+					const char *time;
+
+					if (packet->type == PACKETTYPE_SUBTEST_START ||
+					    packet->type == PACKETTYPE_SUBTEST_RESULT ||
+					    packet->type == PACKETTYPE_DYNAMIC_SUBTEST_START ||
+					    packet->type == PACKETTYPE_DYNAMIC_SUBTEST_RESULT)
+						helper = read_runnerpacket(packet);
+
+					switch (helper.type) {
+					case PACKETTYPE_SUBTEST_START:
+						if (helper.subteststart.name)
+							outf("Starting subtest: %s\n", helper.subteststart.name);
+						break;
+					case PACKETTYPE_SUBTEST_RESULT:
+						if (helper.subtestresult.name && helper.subtestresult.result) {
+							time = "<unknown>";
+							if (helper.subtestresult.timeused)
+								time = helper.subtestresult.timeused;
+							outf("Subtest %s: %s (%ss)\n",
+							     helper.subtestresult.name,
+							     helper.subtestresult.result,
+							     time);
+						}
+						break;
+					case PACKETTYPE_DYNAMIC_SUBTEST_START:
+						if (helper.dynamicsubteststart.name)
+							outf("Starting dynamic subtest: %s\n", helper.dynamicsubteststart.name);
+						break;
+					case PACKETTYPE_DYNAMIC_SUBTEST_RESULT:
+						if (helper.dynamicsubtestresult.name && helper.dynamicsubtestresult.result) {
+							time = "<unknown>";
+							if (helper.dynamicsubtestresult.timeused)
+								time = helper.dynamicsubtestresult.timeused;
+							outf("Dynamic subtest %s: %s (%ss)\n",
+							     helper.dynamicsubtestresult.name,
+							     helper.dynamicsubtestresult.result,
+							     time);
+						}
+						break;
+					default:
+						break;
+					}
+				}
+			}
+		}
+	socket_end:
+
 		if (kmsgfd >= 0 && FD_ISSET(kmsgfd, &set)) {
 			long dmesgwritten;
 
@@ -1032,11 +1213,23 @@ static int monitor_output(pid_t child,
 					if (settings->log_level >= LOG_LEVEL_NORMAL)
 						outf("Exiting gracefully, currently running test will have a 'notrun' result\n");
 
-					dprintf(outputs[_F_JOURNAL], "%s%d (%.3fs)\n",
-						EXECUTOR_EXIT,
-						-SIGHUP, 0.0);
-					if (settings->sync)
-						fdatasync(outputs[_F_JOURNAL]);
+					if (socket_comms_used) {
+						struct runnerpacket *message, *override;
+
+						message = runnerpacket_log(STDOUT_FILENO, "runner: Exiting gracefully, overriding this test's result to be notrun\n");
+						write_packet_with_canary(outputs[_F_SOCKET], message, false); /* possible sync after the override packet */
+						free(message);
+
+						override = runnerpacket_resultoverride("notrun");
+						write_packet_with_canary(outputs[_F_SOCKET], override, settings->sync);
+						free(override);
+					} else {
+						dprintf(outputs[_F_JOURNAL], "%s%d (%.3fs)\n",
+							EXECUTOR_EXIT,
+							-SIGHUP, 0.0);
+						if (settings->sync)
+							fdatasync(outputs[_F_JOURNAL]);
+					}
 				}
 
 				aborting = true;
@@ -1053,9 +1246,10 @@ static int monitor_output(pid_t child,
 				time = 0.0;
 
 			if (!aborting) {
-				const char *exitline;
+				bool timeoutresult = false;
 
-				exitline = killed ? EXECUTOR_TIMEOUT : EXECUTOR_EXIT;
+				if (killed)
+					timeoutresult = true;
 
 				/* If we're stopping because we killed
 				 * the test for tainting, let's not
@@ -1069,7 +1263,7 @@ static int monitor_output(pid_t child,
 				 * journaling a timeout here.
 				 */
 				if (killed && is_tainted(taints)) {
-					exitline = EXECUTOR_EXIT;
+					timeoutresult = false;
 
 					/*
 					 * Also inject a message to
@@ -1082,11 +1276,22 @@ static int monitor_output(pid_t child,
 					 * have newlines on both ends
 					 * of this injection though.
 					 */
-					dprintf(outputs[_F_OUT],
-						"\nrunner: This test was killed due to a kernel taint (0x%lx).\n",
-						taints);
-					if (settings->sync)
-						fdatasync(outputs[_F_OUT]);
+					if (socket_comms_used) {
+						struct runnerpacket *message;
+						char killmsg[256];
+
+						snprintf(killmsg, sizeof(killmsg),
+							 "runner: This test was killed due to a kernel taint (0x%lx).\n", taints);
+						message = runnerpacket_log(STDOUT_FILENO, killmsg);
+						write_packet_with_canary(outputs[_F_SOCKET], message, settings->sync);
+						free(message);
+					} else {
+						dprintf(outputs[_F_OUT],
+							"\nrunner: This test was killed due to a kernel taint (0x%lx).\n",
+							taints);
+						if (settings->sync)
+							fdatasync(outputs[_F_OUT]);
+					}
 				}
 
 				/*
@@ -1094,21 +1299,58 @@ static int monitor_output(pid_t child,
 				 * exceeded the disk usage limit.
 				 */
 				if (killed && disk_usage_limit_exceeded(settings, disk_usage)) {
-					exitline = EXECUTOR_EXIT;
-					dprintf(outputs[_F_OUT],
-						"\nrunner: This test was killed due to exceeding disk usage limit. "
-						"(Used %zd bytes, limit %zd)\n",
-						disk_usage,
-						settings->disk_usage_limit);
-					if (settings->sync)
-						fdatasync(outputs[_F_OUT]);
+					timeoutresult = false;
+
+					if (socket_comms_used) {
+						struct runnerpacket *message;
+						char killmsg[256];
+
+						snprintf(killmsg, sizeof(killmsg),
+							 "runner: This test was killed due to exceeding disk usage limit. "
+							 "(Used %zd bytes, limit %zd)\n",
+							 disk_usage,
+							 settings->disk_usage_limit);
+						message = runnerpacket_log(STDOUT_FILENO, killmsg);
+						write_packet_with_canary(outputs[_F_SOCKET], message, settings->sync);
+						free(message);
+					} else {
+						dprintf(outputs[_F_OUT],
+							"\nrunner: This test was killed due to exceeding disk usage limit. "
+							"(Used %zd bytes, limit %zd)\n",
+							disk_usage,
+							settings->disk_usage_limit);
+						if (settings->sync)
+							fdatasync(outputs[_F_OUT]);
+					}
 				}
 
-				dprintf(outputs[_F_JOURNAL], "%s%d (%.3fs)\n",
-					exitline,
-					status, time);
-				if (settings->sync) {
-					fdatasync(outputs[_F_JOURNAL]);
+				if (socket_comms_used) {
+					struct runnerpacket *exitpacket;
+					char timestr[32];
+
+					snprintf(timestr, sizeof(timestr), "%.3f", time);
+
+					if (timeoutresult) {
+						struct runnerpacket *override;
+
+						override = runnerpacket_resultoverride("timeout");
+						write_packet_with_canary(outputs[_F_SOCKET], override, false); /* sync after exitpacket */
+						free(override);
+					}
+
+					exitpacket = runnerpacket_exit(status, timestr);
+					write_packet_with_canary(outputs[_F_SOCKET], exitpacket, settings->sync);
+					free(exitpacket);
+				} else {
+					const char *exitline;
+
+					exitline = timeoutresult ? EXECUTOR_TIMEOUT : EXECUTOR_EXIT;
+					dprintf(outputs[_F_JOURNAL], "%s%d (%.3fs)\n",
+						exitline,
+						status, time);
+					if (settings->sync) {
+						fdatasync(outputs[_F_JOURNAL]);
+					}
 				}
 
 				if (status == IGT_EXIT_ABORT) {
@@ -1153,6 +1395,7 @@ static int monitor_output(pid_t child,
 				free(outbuf);
 				close(outfd);
 				close(errfd);
+				close(socketfd);
 				close(kmsgfd);
 				return -1;
 			}
@@ -1176,6 +1419,7 @@ static int monitor_output(pid_t child,
 	free(outbuf);
 	close(outfd);
 	close(errfd);
+	close(socketfd);
 	close(kmsgfd);
 
 	if (aborting)
@@ -1185,7 +1429,7 @@ static int monitor_output(pid_t child,
 }
 
 static void __attribute__((noreturn))
-execute_test_process(int outfd, int errfd,
+execute_test_process(int outfd, int errfd, int socketfd,
 		     struct settings *settings,
 		     struct job_list_entry *entry)
 {
@@ -1237,6 +1481,13 @@ execute_test_process(int outfd, int errfd,
 		}
 	}
 
+	if (socketfd >= 0) {
+		struct runnerpacket *packet;
+
+		packet = runnerpacket_exec(argv);
+		write(socketfd, packet, packet->size);
+	}
+
 	execv(argv[0], argv);
 	fprintf(stderr, "Cannot execute %s\n", argv[0]);
 	exit(IGT_EXIT_INVALID);
@@ -1318,7 +1569,8 @@ static int execute_next_entry(struct execute_state *state,
 	int kmsgfd;
 	int outpipe[2] = { -1, -1 };
 	int errpipe[2] = { -1, -1 };
-	int outfd, errfd;
+	int socket[2] = { -1, -1 };
+	int outfd, errfd, socketfd;
 	char name[32];
 	pid_t child;
 	int result;
@@ -1348,6 +1600,12 @@ static int execute_next_entry(struct execute_state *state,
 		goto out_pipe;
 	}
 
+	if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socket)) {
+		errf("Error creating sockets: %m\n");
+		result = -1;
+		goto out_pipe;
+	}
+
 	if ((kmsgfd = open("/dev/kmsg", O_RDONLY | O_CLOEXEC | O_NONBLOCK)) < 0) {
 		errf("Warning: Cannot open /dev/kmsg\n");
 	} else {
@@ -1388,27 +1646,39 @@ static int execute_next_entry(struct execute_state *state,
 		result = -1;
 		goto out_kmsgfd;
 	} else if (child == 0) {
+		char envstring[16];
+
 		outfd = outpipe[1];
 		errfd = errpipe[1];
+		socketfd = socket[1];
 		close(outpipe[0]);
 		close(errpipe[0]);
+		close(socket[0]);
 
 		sigprocmask(SIG_UNBLOCK, sigmask, NULL);
 
+		if (socketfd >= 0 && !getenv("IGT_RUNNER_DISABLE_SOCKET_COMMUNICATION")) {
+			snprintf(envstring, sizeof(envstring), "%d", socketfd);
+			setenv("IGT_RUNNER_SOCKET_FD", envstring, 1);
+		}
 		setenv("IGT_SENTINEL_ON_STDERR", "1", 1);
 
-		execute_test_process(outfd, errfd, settings, entry);
+		execute_test_process(outfd, errfd, socketfd, settings, entry);
 		/* unreachable */
 	}
 
 	outfd = outpipe[0];
 	errfd = errpipe[0];
+	socketfd = socket[0];
 	close(outpipe[1]);
 	close(errpipe[1]);
-	outpipe[1] = errpipe[1] = -1;
+	close(socket[1]);
+	outpipe[1] = errpipe[1] = socket[1] = -1;
 
-	result = monitor_output(child, outfd, errfd, kmsgfd, sigfd,
-				outputs, time_spent, settings, abortreason);
+	result = monitor_output(child, outfd, errfd, socketfd,
+				kmsgfd, sigfd,
+				outputs, time_spent, settings,
+				abortreason);
 
 out_kmsgfd:
 	close(kmsgfd);
@@ -1580,6 +1850,22 @@ bool initialize_execute_state_from_resume(int dirfd,
 
 	entry = &list->entries[i];
 	state->next = i;
+
+	if ((fd = openat(resdirfd, filenames[_F_SOCKET], O_RDONLY)) >= 0) {
+		if (!prune_from_comms(entry, fd)) {
+			/*
+			 * No subtests, or incomplete before the first
+			 * subtest. Not suitable to re-run.
+			 */
+			state->next = i + 1;
+		} else if (entry->binary[0] == '\0') {
+			/* Full completed */
+			state->next = i + 1;
+		}
+
+		close (fd);
+	}
+
 	if ((fd = openat(resdirfd, filenames[_F_JOURNAL], O_RDONLY)) >= 0) {
 		if (!prune_from_journal(entry, fd)) {
 			/*
diff --git a/runner/executor.h b/runner/executor.h
index 6c83e649..31f4ac16 100644
--- a/runner/executor.h
+++ b/runner/executor.h
@@ -22,6 +22,7 @@ enum {
 	_F_OUT,
 	_F_ERR,
 	_F_DMESG,
+	_F_SOCKET,
 	_F_LAST,
 };
 
diff --git a/runner/meson.build b/runner/meson.build
index c3927af5..dadfc75f 100644
--- a/runner/meson.build
+++ b/runner/meson.build
@@ -10,6 +10,7 @@ runnerlib_sources = [ 'settings.c',
 runner_sources = [ 'runner.c' ]
 resume_sources = [ 'resume.c' ]
 results_sources = [ 'results.c' ]
+decoder_sources = [ 'decoder.c' ]
 runner_test_sources = [ 'runner_tests.c' ]
 runner_json_test_sources = [ 'runner_json_tests.c' ]
 
@@ -56,6 +57,13 @@ if jsonc.found()
 			     install_rpath : bindir_rpathdir,
 			     dependencies : igt_deps)
 
+	decoder = executable('igt_comms_decoder', decoder_sources,
+			     link_with : runnerlib,
+			     install : true,
+			     install_dir : bindir,
+			     install_rpath : bindir_rpathdir,
+			     dependencies : igt_deps)
+
 	runner_test = executable('runner_test', runner_test_sources,
 				 c_args : '-DTESTDATA_DIRECTORY="@0@"'.format(testdata_dir),
 				 link_with : runnerlib,
diff --git a/runner/resultgen.c b/runner/resultgen.c
index 79725ca2..f10343de 100644
--- a/runner/resultgen.c
+++ b/runner/resultgen.c
@@ -1,6 +1,7 @@
 #include <assert.h>
 #include <ctype.h>
 #include <fcntl.h>
+#include <inttypes.h>
 #include <stdio.h>
 #include <string.h>
 #include <sys/mman.h>
@@ -12,6 +13,7 @@
 
 #include "igt_aux.h"
 #include "igt_core.h"
+#include "runnercomms.h"
 #include "resultgen.h"
 #include "settings.h"
 #include "executor.h"
@@ -147,7 +149,7 @@ static const char *next_line(const char *line, const char *bufend)
 		return NULL;
 }
 
-static void append_line(char **buf, size_t *buflen, char *line)
+static void append_line(char **buf, size_t *buflen, const char *line)
 {
 	size_t linelen = strlen(line);
 
@@ -1228,6 +1230,606 @@ static void fill_from_journal(int fd,
 	fclose(f);
 }
 
+typedef enum comms_state {
+	STATE_INITIAL = 0,
+	STATE_AFTER_EXEC,
+	STATE_SUBTEST_STARTED,
+	STATE_DYNAMIC_SUBTEST_STARTED,
+	STATE_BETWEEN_DYNAMIC_SUBTESTS,
+	STATE_BETWEEN_SUBTESTS,
+	STATE_EXITED,
+} comms_state_t;
+
+struct comms_context
+{
+	comms_state_t state;
+
+	struct json_object *binaryruntimeobj;
+	struct json_object *current_test;
+	struct json_object *current_dynamic_subtest;
+	char *current_subtest_name;
+	char *current_dynamic_subtest_name;
+
+	char *outbuf, *errbuf;
+	size_t outbuflen, errbuflen;
+	size_t outidx, nextoutidx;
+	size_t erridx, nexterridx;
+	size_t dynoutidx, nextdynoutidx;
+	size_t dynerridx, nextdynerridx;
+
+	char *igt_version;
+
+	char *subtestresult;
+	char *dynamicsubtestresult;
+
+	char *cmdline;
+	int exitcode;
+
+	struct subtest_list *subtests;
+	struct subtest *subtest;
+	struct results *results;
+	struct job_list_entry *entry;
+	const char *binary;
+};
+
+static void comms_free_context(struct comms_context *context)
+{
+	free(context->current_subtest_name);
+	free(context->current_dynamic_subtest_name);
+	free(context->outbuf);
+	free(context->errbuf);
+	free(context->igt_version);
+	free(context->subtestresult);
+	free(context->dynamicsubtestresult);
+	free(context->cmdline);
+}
+
+static void comms_inject_subtest_start_log(struct comms_context *context,
+					   const char *prefix,
+					   const char *subtestname)
+{
+	char msg[512];
+
+	snprintf(msg, sizeof(msg), "%s%s\n", prefix, subtestname);
+	append_line(&context->outbuf, &context->outbuflen, msg);
+	append_line(&context->errbuf, &context->errbuflen, msg);
+}
+
+static void comms_inject_subtest_end_log(struct comms_context *context,
+					 const char *prefix,
+					 const char *subtestname,
+					 const char *subtestresult,
+					 const char *timeused)
+{
+	char msg[512];
+
+	snprintf(msg, sizeof(msg), "%s%s: %s (%ss)\n", prefix, subtestname, subtestresult, timeused);
+	append_line(&context->outbuf, &context->outbuflen, msg);
+	append_line(&context->errbuf, &context->errbuflen, msg);
+}
+
+static void comms_finish_subtest(struct comms_context *context)
+{
+	json_object_object_add(context->current_test, "out",
+			       new_escaped_json_string(context->outbuf + context->outidx, context->outbuflen - context->outidx));
+	json_object_object_add(context->current_test, "err",
+			       new_escaped_json_string(context->errbuf + context->outidx, context->errbuflen - context->erridx));
+
+	if (context->igt_version)
+		add_igt_version(context->current_test, context->igt_version, strlen(context->igt_version));
+
+	if (context->subtestresult == NULL)
+		context->subtestresult = strdup("incomplete");
+	set_result(context->current_test, context->subtestresult);
+
+	free(context->subtestresult);
+	context->subtestresult = NULL;
+	context->current_test = NULL;
+
+	context->outidx = context->nextoutidx;
+	context->erridx = context->nexterridx;
+}
+
+static void comms_finish_dynamic_subtest(struct comms_context *context)
+{
+	json_object_object_add(context->current_dynamic_subtest, "out",
+			       new_escaped_json_string(context->outbuf + context->dynoutidx, context->outbuflen - context->dynoutidx));
+	json_object_object_add(context->current_dynamic_subtest, "err",
+			       new_escaped_json_string(context->errbuf + context->dynerridx, context->errbuflen - context->dynerridx));
+
+	if (context->igt_version)
+		add_igt_version(context->current_dynamic_subtest, context->igt_version, strlen(context->igt_version));
+
+	if (context->dynamicsubtestresult == NULL)
+		context->dynamicsubtestresult = strdup("incomplete");
+	set_result(context->current_dynamic_subtest, context->dynamicsubtestresult);
+
+	free(context->dynamicsubtestresult);
+	context->dynamicsubtestresult = NULL;
+	context->current_dynamic_subtest = NULL;
+
+	context->dynoutidx = context->nextdynoutidx;
+	context->dynerridx = context->nextdynerridx;
+}
+
+static void comms_add_new_subtest(struct comms_context *context,
+				  const char *subtestname)
+{
+	char piglit_name[256];
+
+	add_subtest(context->subtests, strdup(subtestname));
+	context->subtest = &context->subtests->subs[context->subtests->size - 1];
+	generate_piglit_name(context->binary, subtestname, piglit_name, sizeof(piglit_name));
+	context->current_test = get_or_create_json_object(context->results->tests, piglit_name);
+	free(context->current_subtest_name);
+	context->current_subtest_name = strdup(subtestname);
+}
+
+static void comms_add_new_dynamic_subtest(struct comms_context *context,
+					  const char *dynamic_name)
+{
+	char piglit_name[256];
+	char dynamic_piglit_name[256];
+
+	add_dynamic_subtest(context->subtest, strdup(dynamic_name));
+	generate_piglit_name(context->binary, context->current_subtest_name, piglit_name, sizeof(piglit_name));
+	generate_piglit_name_for_dynamic(piglit_name, dynamic_name, dynamic_piglit_name, sizeof(dynamic_piglit_name));
+	context->current_dynamic_subtest = get_or_create_json_object(context->results->tests, dynamic_piglit_name);
+	free(context->current_dynamic_subtest_name);
+	context->current_dynamic_subtest_name = strdup(dynamic_name);
+}
+
+static bool comms_handle_log(const struct runnerpacket *packet,
+			     runnerpacket_read_helper helper,
+			     void *userdata)
+{
+	struct comms_context *context = userdata;
+	char **textbuf;
+	size_t *textlen;
+
+	if (helper.log.stream == STDOUT_FILENO) {
+		textbuf = &context->outbuf;
+		textlen = &context->outbuflen;
+	} else {
+		textbuf = &context->errbuf;
+		textlen = &context->errbuflen;
+	}
+	append_line(textbuf, textlen, helper.log.text);
+
+	return true;
+}
+
+static bool comms_handle_exec(const struct runnerpacket *packet,
+			      runnerpacket_read_helper helper,
+			      void *userdata)
+{
+	struct comms_context *context = userdata;
+
+	switch (context->state) {
+	case STATE_INITIAL:
+		break;
+
+	case STATE_AFTER_EXEC:
+		/*
+		 * Resume after an exec that didn't involve any
+		 * subtests. Resumes can only happen for tests with
+		 * subtests, so while we might have logs already
+		 * collected, we have nowhere to put them. The joblist
+		 * doesn't help, because the ordering is up to the
+		 * test.
+		 */
+		printf("Warning: Need to discard %zd bytes of logs, no subtest data\n", context->outbuflen + context->errbuflen);
+		context->outbuflen = context->errbuflen = 0;
+		context->outidx = context->erridx = 0;
+		context->nextoutidx = context->nexterridx = 0;
+		break;
+
+	case STATE_SUBTEST_STARTED:
+	case STATE_DYNAMIC_SUBTEST_STARTED:
+	case STATE_BETWEEN_DYNAMIC_SUBTESTS:
+	case STATE_BETWEEN_SUBTESTS:
+	case STATE_EXITED:
+		/* A resume exec, so we're already collecting data. */
+		assert(context->current_test != NULL);
+		comms_finish_subtest(context);
+		break;
+	default:
+		assert(false); /* unreachable */
+	}
+
+	free(context->cmdline);
+	context->cmdline = strdup(helper.exec.cmdline);
+
+	context->state = STATE_AFTER_EXEC;
+
+	return true;
+}
+
+static bool comms_handle_exit(const struct runnerpacket *packet,
+			      runnerpacket_read_helper helper,
+			      void *userdata)
+{
+	struct comms_context *context = userdata;
+	char piglit_name[256];
+
+	if (context->state == STATE_AFTER_EXEC) {
+		/*
+		 * Exit after exec, so we didn't get any
+		 * subtests. Check if there's supposed to be any,
+		 * otherwise stuff logs into the binary's result.
+		 */
+
+		char *subtestname = NULL;
+
+		if (context->entry->subtest_count > 0) {
+			subtestname = context->entry->subtests[0];
+			add_subtest(context->subtests, strdup(subtestname));
+		}
+		generate_piglit_name(context->binary, subtestname, piglit_name, sizeof(piglit_name));
+		context->current_test = get_or_create_json_object(context->results->tests, piglit_name);
+
+		/* Get result from exitcode unless we have an override already */
+		if (context->subtestresult == NULL)
+			context->subtestresult = strdup(result_from_exitcode(helper.exit.exitcode));
+	} else if (helper.exit.exitcode == IGT_EXIT_ABORT || helper.exit.exitcode == GRACEFUL_EXITCODE) {
+		/*
+		 * If we did get subtests, we need to assign the
+		 * special exitcode results to the last subtest,
+		 * normal and dynamic
+		 */
+		const char *result = helper.exit.exitcode == IGT_EXIT_ABORT ? "abort" : "notrun";
+
+		free(context->subtestresult);
+		context->subtestresult = strdup(result);
+		free(context->dynamicsubtestresult);
+		context->dynamicsubtestresult = strdup(result);
+	}
+
+	context->exitcode = helper.exit.exitcode;
+	add_runtime(context->binaryruntimeobj, strtod(helper.exit.timeused, NULL));
+
+	context->state = STATE_EXITED;
+
+	return true;
+}
+
+static bool comms_handle_subtest_start(const struct runnerpacket *packet,
+				       runnerpacket_read_helper helper,
+				       void *userdata)
+{
+	struct comms_context *context = userdata;
+	char errmsg[512];
+
+	switch (context->state) {
+	case STATE_INITIAL:
+	case STATE_EXITED:
+		/* Subtest starts when we're not even running? (Before exec or after exit) */
+		fprintf(stderr, "Error: Unexpected subtest start (binary wasn't running)\n");
+		return false;
+	case STATE_SUBTEST_STARTED:
+	case STATE_DYNAMIC_SUBTEST_STARTED:
+	case STATE_BETWEEN_DYNAMIC_SUBTESTS:
+		/*
+		 * Subtest starts when the previous one was still
+		 * running. Text-based parsing would figure that a
+		 * resume happened, but we know the real deal with
+		 * socket comms.
+		 */
+		snprintf(errmsg, sizeof(errmsg),
+			 "\nrunner: Subtest %s already running when subtest %s starts. This is a test bug.\n",
+			 context->current_subtest_name,
+			 helper.subteststart.name);
+		append_line(&context->errbuf, &context->errbuflen, errmsg);
+
+		if (context->state == STATE_DYNAMIC_SUBTEST_STARTED ||
+		    context->state == STATE_BETWEEN_DYNAMIC_SUBTESTS)
+			comms_finish_dynamic_subtest(context);
+
+		/* fallthrough */
+	case STATE_BETWEEN_SUBTESTS:
+		/* Already collecting for a subtest, finish it up */
+		if (context->current_dynamic_subtest)
+			comms_finish_dynamic_subtest(context);
+
+		comms_finish_subtest(context);
+
+		/* fallthrough */
+	case STATE_AFTER_EXEC:
+		comms_add_new_subtest(context, helper.subteststart.name);
+
+		/* Subtest starting message is not in logs with socket comms, inject it manually */
+		comms_inject_subtest_start_log(context, STARTING_SUBTEST, helper.subteststart.name);
+
+		break;
+	default:
+		assert(false); /* unreachable */
+	}
+
+	context->state = STATE_SUBTEST_STARTED;
+
+	return true;
+}
+
+static bool comms_handle_subtest_result(const struct runnerpacket *packet,
+					runnerpacket_read_helper helper,
+					void *userdata)
+{
+	struct comms_context *context = userdata;
+	char errmsg[512];
+
+	switch (context->state) {
+	case STATE_INITIAL:
+	case STATE_EXITED:
+		/* Subtest result when we're not even running? (Before exec or after exit) */
+		fprintf(stderr, "Error: Unexpected subtest result (binary wasn't running)\n");
+		return false;
+	case STATE_DYNAMIC_SUBTEST_STARTED:
+		/*
+		 * Subtest result when dynamic subtest is still
+		 * running. Text-based parsing would consider that an
+		 * incomplete, we're able to inject a warning.
+		 */
+		snprintf(errmsg, sizeof(errmsg),
+			 "\nrunner: Dynamic subtest %s still running when subtest %s ended. This is a test bug.\n",
+			 context->current_dynamic_subtest_name,
+			 helper.subtestresult.name);
+		append_line(&context->errbuf, &context->errbuflen, errmsg);
+		comms_finish_dynamic_subtest(context);
+		break;
+	case STATE_BETWEEN_SUBTESTS:
+		/* Subtest result without starting it, and we're already collecting logs for a previous test */
+		comms_finish_subtest(context);
+		comms_add_new_subtest(context, helper.subtestresult.name);
+		break;
+	case STATE_AFTER_EXEC:
+		/* Subtest result without starting it, so comes from a fixture. We're not yet collecting logs for anything. */
+		comms_add_new_subtest(context, helper.subtestresult.name);
+		break;
+	case STATE_SUBTEST_STARTED:
+	case STATE_BETWEEN_DYNAMIC_SUBTESTS:
+		/* Normal flow */
+		break;
+	default:
+		assert(false); /* unreachable */
+	}
+
+	comms_inject_subtest_end_log(context,
+				     SUBTEST_RESULT,
+				     helper.subtestresult.name,
+				     helper.subtestresult.result,
+				     helper.subtestresult.timeused);
+
+	/* Next subtest, if any, will begin its logs right after that result line */
+	context->nextoutidx = context->outbuflen;
+	context->nexterridx = context->errbuflen;
+
+	/*
+	 * Only store the actual result from the packet if we don't
+	 * already have one. If we do, it's from an override.
+	 */
+	if (context->subtestresult == NULL) {
+		const char *mappedresult;
+
+		parse_result_string(helper.subtestresult.result,
+				    strlen(helper.subtestresult.result),
+				    &mappedresult, NULL);
+		context->subtestresult = strdup(mappedresult);
+	}
+
+	context->state = STATE_BETWEEN_SUBTESTS;
+
+	return true;
+}
+
+static bool comms_handle_dynamic_subtest_start(const struct runnerpacket *packet,
+					       runnerpacket_read_helper helper,
+					       void *userdata)
+{
+	struct comms_context *context = userdata;
+	char errmsg[512];
+
+	switch (context->state) {
+	case STATE_INITIAL:
+	case STATE_EXITED:
+		/* Dynamic subtest starts when we're not even running? (Before exec or after exit) */
+		fprintf(stderr, "Error: Unexpected dynamic subtest start (binary wasn't running)\n");
+		return false;
+	case STATE_AFTER_EXEC:
+		/* Binary was running but a subtest wasn't. We don't know where to inject an error message. */
+		fprintf(stderr, "Error: Unexpected dynamic subtest start (subtest wasn't running)\n");
+		return false;
+	case STATE_BETWEEN_SUBTESTS:
+		/*
+		 * Dynamic subtest starts when a subtest is not
+		 * running. We can't know which subtest this dynamic
+		 * subtest was supposed to be in. But we can inject a
+		 * warn into the previous subtest.
+		 */
+		snprintf(errmsg, sizeof(errmsg),
+			 "\nrunner: Dynamic subtest %s started when not inside a subtest. This is a test bug.\n",
+			 helper.dynamicsubteststart.name);
+		append_line(&context->errbuf, &context->errbuflen, errmsg);
+
+		/* Leave the state as is and hope for the best */
+		return true;
+	case STATE_DYNAMIC_SUBTEST_STARTED:
+		snprintf(errmsg, sizeof(errmsg),
+			 "\nrunner: Dynamic subtest %s already running when dynamic subtest %s starts. This is a test bug.\n",
+			 context->current_dynamic_subtest_name,
+			 helper.dynamicsubteststart.name);
+		append_line(&context->errbuf, &context->errbuflen, errmsg);
+
+		/* fallthrough */
+	case STATE_BETWEEN_DYNAMIC_SUBTESTS:
+		comms_finish_dynamic_subtest(context);
+		/* fallthrough */
+	case STATE_SUBTEST_STARTED:
+		comms_add_new_dynamic_subtest(context, helper.dynamicsubteststart.name);
+
+		/* Dynamic subtest starting message is not in logs with socket comms, inject it manually */
+		comms_inject_subtest_start_log(context, STARTING_DYNAMIC_SUBTEST, helper.dynamicsubteststart.name);
+
+		break;
+	default:
+		assert(false); /* unreachable */
+	}
+
+	context->state = STATE_DYNAMIC_SUBTEST_STARTED;
+
+	return true;
+}
+
+static bool comms_handle_dynamic_subtest_result(const struct runnerpacket *packet,
+						runnerpacket_read_helper helper,
+						void *userdata)
+{
+	struct comms_context *context = userdata;
+	char errmsg[512];
+
+	switch (context->state) {
+	case STATE_INITIAL:
+	case STATE_EXITED:
+		/* Dynamic subtest result when we're not even running? (Before exec or after exit) */
+		fprintf(stderr, "Error: Unexpected dynamic subtest result (binary wasn't running)\n");
+		return false;
+	case STATE_AFTER_EXEC:
+		/* Binary was running but a subtest wasn't. We don't know where to inject an error message. */
+		fprintf(stderr, "Error: Unexpected dynamic subtest result (subtest wasn't running)\n");
+		return false;
+	case STATE_BETWEEN_SUBTESTS:
+		/*
+		 * Dynamic subtest result when a subtest is not
+		 * running. We can't know which subtest this dynamic
+		 * subtest was supposed to be in. But we can inject a
+		 * warn into the previous subtest.
+		 */
+		snprintf(errmsg, sizeof(errmsg),
+			 "\nrunner: Dynamic subtest %s result when not inside a subtest. This is a test bug.\n",
+			 helper.dynamicsubtestresult.name);
+		append_line(&context->errbuf, &context->errbuflen, errmsg);
+
+		/* Leave the state as is and hope for the best */
+		return true;
+	case STATE_BETWEEN_DYNAMIC_SUBTESTS:
+		/*
+		 * Result without starting. There's no
+		 * skip_subtests_henceforth equivalent for dynamic
+		 * subtests so this shouldn't happen, but we can
+		 * handle it nevertheless.
+		 */
+		comms_finish_dynamic_subtest(context);
+		/* fallthrough */
+	case STATE_SUBTEST_STARTED:
+		/* Result without starting, but we aren't collecting for a dynamic subtest yet */
+		comms_add_new_dynamic_subtest(context, helper.dynamicsubtestresult.name);
+		break;
+	case STATE_DYNAMIC_SUBTEST_STARTED:
+		/* Normal flow */
+		break;
+	default:
+		assert(false); /* unreachable */
+	}
+
+	comms_inject_subtest_end_log(context,
+				     DYNAMIC_SUBTEST_RESULT,
+				     helper.dynamicsubtestresult.name,
+				     helper.dynamicsubtestresult.result,
+				     helper.dynamicsubtestresult.timeused);
+
+	/* Next dynamic subtest, if any, will begin its logs right after that result line */
+	context->nextdynoutidx = context->outbuflen;
+	context->nextdynerridx = context->errbuflen;
+
+	/*
+	 * Only store the actual result from the packet if we don't
+	 * already have one. If we do, it's from an override.
+	 */
+	if (context->dynamicsubtestresult == NULL) {
+		const char *mappedresult;
+
+		parse_result_string(helper.dynamicsubtestresult.result,
+				    strlen(helper.dynamicsubtestresult.result),
+				    &mappedresult, NULL);
+		context->dynamicsubtestresult = strdup(mappedresult);
+	}
+
+	context->state = STATE_BETWEEN_DYNAMIC_SUBTESTS;
+
+	return true;
+}
+
+static bool comms_handle_versionstring(const struct runnerpacket *packet,
+				       runnerpacket_read_helper helper,
+				       void *userdata)
+{
+	struct comms_context *context = userdata;
+
+	free(context->igt_version);
+	context->igt_version = strdup(helper.versionstring.text);
+
+	return true;
+}
+
+static bool comms_handle_result_override(const struct runnerpacket *packet,
+					 runnerpacket_read_helper helper,
+					 void *userdata)
+{
+	struct comms_context *context = userdata;
+
+	if (context->current_dynamic_subtest) {
+		free(context->dynamicsubtestresult);
+		context->dynamicsubtestresult = strdup(helper.resultoverride.result);
+	}
+
+	free(context->subtestresult);
+	context->subtestresult = strdup(helper.resultoverride.result);
+
+	return true;
+}
+
+static int fill_from_comms(int fd,
+			   struct job_list_entry *entry,
+			   struct subtest_list *subtests,
+			   struct results *results)
+{
+	struct comms_context context = {};
+	struct comms_visitor visitor = {
+		.log = comms_handle_log,
+		.exec = comms_handle_exec,
+		.exit = comms_handle_exit,
+		.subtest_start = comms_handle_subtest_start,
+		.subtest_result = comms_handle_subtest_result,
+		.dynamic_subtest_start = comms_handle_dynamic_subtest_start,
+		.dynamic_subtest_result = comms_handle_dynamic_subtest_result,
+		.versionstring = comms_handle_versionstring,
+		.result_override = comms_handle_result_override,
+
+		.userdata = &context,
+	};
+	char piglit_name[256];
+	int ret = COMMSPARSE_EMPTY;
+
+	if (fd < 0)
+		return COMMSPARSE_EMPTY;
+
+	context.entry = entry;
+	context.binary = entry->binary;
+	generate_piglit_name(entry->binary, NULL, piglit_name, sizeof(piglit_name));
+	context.binaryruntimeobj = get_or_create_json_object(results->runtimes, piglit_name);
+	context.results = results;
+	context.subtests = subtests;
+
+	ret = comms_read_dump(fd, &visitor);
+
+	if (context.current_dynamic_subtest != NULL)
+		comms_finish_dynamic_subtest(&context);
+	if (context.current_test != NULL)
+		comms_finish_subtest(&context);
+	comms_free_context(&context);
+
+	return ret;
+}
+
 static bool result_is_requested(struct job_list_entry *entry,
 				const char *subtestname,
 				const char *dynamic_name)
@@ -1505,6 +2107,7 @@ static bool parse_test_directory(int dirfd,
 	int fds[_F_LAST];
 	struct subtest_list subtests = {};
 	bool status = true;
+	int commsparsed;
 
 	if (!open_output_files(dirfd, fds, false)) {
 		fprintf(stderr, "Error opening output files\n");
@@ -1517,10 +2120,26 @@ static bool parse_test_directory(int dirfd,
 	 */
 	fill_from_journal(fds[_F_JOURNAL], entry, &subtests, results);
 
-	if (!fill_from_output(fds[_F_OUT], entry->binary, "out", &subtests, results->tests) ||
-	    !fill_from_output(fds[_F_ERR], entry->binary, "err", &subtests, results->tests) ||
-	    !fill_from_dmesg(fds[_F_DMESG], settings, entry->binary, &subtests, results->tests)) {
-		fprintf(stderr, "Error parsing output files\n");
+	/*
+	 * Get test output from socket comms if it exists, otherwise parse stdout/stderr */
+	commsparsed = fill_from_comms(fds[_F_SOCKET], entry, &subtests, results);
+	if (commsparsed == COMMSPARSE_ERROR) {
+		fprintf(stderr, "Error parsing output files (comms.txt)\n");
+		status = false;
+		goto parse_output_end;
+	}
+
+	if (commsparsed == COMMSPARSE_EMPTY) {
+		if (!fill_from_output(fds[_F_OUT], entry->binary, "out", &subtests, results->tests) ||
+		    !fill_from_output(fds[_F_ERR], entry->binary, "err", &subtests, results->tests)) {
+			fprintf(stderr, "Error parsing output files (out.txt, err.txt)\n");
+			status = false;
+			goto parse_output_end;
+		}
+	}
+
+	if (!fill_from_dmesg(fds[_F_DMESG], settings, entry->binary, &subtests, results->tests)) {
+		fprintf(stderr, "Error parsing output files (dmesg.txt)\n");
 		status = false;
 		goto parse_output_end;
 	}
diff --git a/runner/runner_tests.c b/runner/runner_tests.c
index db0ce2ac..afc6a83b 100644
--- a/runner/runner_tests.c
+++ b/runner/runner_tests.c
@@ -7,6 +7,7 @@
 #include <json.h>
 
 #include "igt.h"
+#include "runnercomms.h"
 
 #include "settings.h"
 #include "job_list.h"
@@ -239,6 +240,16 @@ static void assert_execution_results_exist(int dirfd)
 	assert_execution_created(dirfd, "dmesg.txt");
 }
 
+static void write_packet_with_canary(int fd, struct runnerpacket *packet)
+{
+	uint32_t canary = socket_dump_canary();
+
+	write(fd, &canary, sizeof(canary));
+	write(fd, packet, packet->size);
+
+	free(packet);
+}
+
 igt_main
 {
 	struct settings *settings = malloc(sizeof(*settings));
@@ -1226,6 +1237,62 @@ igt_main
 		}
 	}
 
+	igt_subtest_group {
+		char dirname[] = "tmpdirXXXXXX";
+		struct job_list *list = malloc(sizeof(*list));
+		volatile int dirfd = -1, subdirfd = -1, fd = -1;
+
+		igt_fixture {
+			init_job_list(list);
+			igt_require(mkdtemp(dirname) != NULL);
+		}
+
+		igt_subtest("execute-initialize-subtest-started-comms") {
+			struct execute_state state;
+			const char *argv[] = { "runner",
+					       "--allow-non-root",
+					       "--multiple-mode",
+					       "-t", "successtest",
+					       testdatadir,
+					       dirname,
+			};
+			const char excludestring[] = "!first-subtest";
+
+			igt_assert(parse_options(ARRAY_SIZE(argv), (char**)argv, settings));
+			igt_assert(create_job_list(list, settings));
+			igt_assert(list->size == 1);
+			igt_assert(list->entries[0].subtest_count == 0);
+
+			igt_assert(serialize_settings(settings));
+			igt_assert(serialize_job_list(list, settings));
+
+			igt_assert((dirfd = open(dirname, O_DIRECTORY | O_RDONLY)) >= 0);
+			igt_assert(mkdirat(dirfd, "0", 0770) == 0);
+			igt_assert((subdirfd = openat(dirfd, "0", O_DIRECTORY | O_RDONLY)) >= 0);
+			igt_assert((fd = openat(subdirfd, "comms.txt", O_CREAT | O_WRONLY | O_EXCL, 0660)) >= 0);
+			write_packet_with_canary(fd, runnerpacket_subtest_start("first-subtest"));
+
+			free_job_list(list);
+			clear_settings(settings);
+			igt_assert(initialize_execute_state_from_resume(dirfd, &state, settings, list));
+
+			igt_assert_eq(state.next, 0);
+			igt_assert_eq(list->size, 1);
+			igt_assert_eq(list->entries[0].subtest_count, 2);
+			igt_assert_eqstr(list->entries[0].subtests[0], "*");
+			igt_assert_eqstr(list->entries[0].subtests[1], excludestring);
+		}
+
+		igt_fixture {
+			close(fd);
+			close(subdirfd);
+			close(dirfd);
+			clear_directory(dirname);
+			free_job_list(list);
+			free(list);
+		}
+	}
+
 	igt_subtest_group {
 		char dirname[] = "tmpdirXXXXXX";
 		struct job_list *list = malloc(sizeof(*list));
@@ -1282,6 +1349,62 @@ igt_main
 		}
 	}
 
+	igt_subtest_group {
+		char dirname[] = "tmpdirXXXXXX";
+		struct job_list *list = malloc(sizeof(*list));
+		volatile int dirfd = -1, subdirfd = -1, fd = -1;
+
+		igt_fixture {
+			init_job_list(list);
+			igt_require(mkdtemp(dirname) != NULL);
+		}
+
+		igt_subtest("execute-initialize-all-subtests-started-comms") {
+			struct execute_state state;
+			const char *argv[] = { "runner",
+					       "--allow-non-root",
+					       "--multiple-mode",
+					       "-t", "successtest at first-subtest",
+					       "-t", "successtest at second-subtest",
+					       testdatadir,
+					       dirname,
+			};
+
+			igt_assert(parse_options(ARRAY_SIZE(argv), (char**)argv, settings));
+			igt_assert(create_job_list(list, settings));
+			igt_assert(list->size == 1);
+			igt_assert(list->entries[0].subtest_count == 2);
+
+			igt_assert(serialize_settings(settings));
+			igt_assert(serialize_job_list(list, settings));
+
+			igt_assert((dirfd = open(dirname, O_DIRECTORY | O_RDONLY)) >= 0);
+			igt_assert(mkdirat(dirfd, "0", 0770) == 0);
+			igt_assert((subdirfd = openat(dirfd, "0", O_DIRECTORY | O_RDONLY)) >= 0);
+			igt_assert((fd = openat(subdirfd, "comms.txt", O_CREAT | O_WRONLY | O_EXCL, 0660)) >= 0);
+			write_packet_with_canary(fd, runnerpacket_subtest_start("first-subtest"));
+			write_packet_with_canary(fd, runnerpacket_subtest_start("second-subtest"));
+
+			free_job_list(list);
+			clear_settings(settings);
+			igt_assert(initialize_execute_state_from_resume(dirfd, &state, settings, list));
+
+			/* All subtests are in journal, the entry should be considered completed */
+			igt_assert_eq(state.next, 1);
+			igt_assert_eq(list->size, 1);
+			igt_assert_eq(list->entries[0].subtest_count, 4);
+		}
+
+		igt_fixture {
+			close(fd);
+			close(subdirfd);
+			close(dirfd);
+			clear_directory(dirname);
+			free_job_list(list);
+			free(list);
+		}
+	}
+
 	igt_subtest_group {
 		char dirname[] = "tmpdirXXXXXX";
 		struct job_list *list = malloc(sizeof(*list));
@@ -1341,6 +1464,66 @@ igt_main
 		}
 	}
 
+	igt_subtest_group {
+		char dirname[] = "tmpdirXXXXXX";
+		struct job_list *list = malloc(sizeof(*list));
+		volatile int dirfd = -1, subdirfd = -1, fd = -1;
+
+		igt_fixture {
+			init_job_list(list);
+			igt_require(mkdtemp(dirname) != NULL);
+		}
+
+		igt_subtest("execute-initialize-subtests-complete-comms") {
+			struct execute_state state;
+			const char *argv[] = { "runner",
+					       "--allow-non-root",
+					       "--multiple-mode",
+					       testdatadir,
+					       dirname,
+			};
+
+			igt_assert(parse_options(ARRAY_SIZE(argv), (char**)argv, settings));
+			igt_assert(create_job_list(list, settings));
+			igt_assert(list->size == NUM_TESTDATA_BINARIES);
+
+			if (!strcmp(list->entries[0].binary, "no-subtests")) {
+				struct job_list_entry tmp = list->entries[0];
+				list->entries[0] = list->entries[1];
+				list->entries[1] = tmp;
+			}
+
+			igt_assert(list->entries[0].subtest_count == 0);
+
+			igt_assert(serialize_settings(settings));
+			igt_assert(serialize_job_list(list, settings));
+
+			igt_assert_lte(0, dirfd = open(dirname, O_DIRECTORY | O_RDONLY));
+			igt_assert_eq(mkdirat(dirfd, "0", 0770), 0);
+			igt_assert((subdirfd = openat(dirfd, "0", O_DIRECTORY | O_RDONLY)) >= 0);
+			igt_assert((fd = openat(subdirfd, "comms.txt", O_CREAT | O_WRONLY | O_EXCL, 0660)) >= 0);
+			write_packet_with_canary(fd, runnerpacket_subtest_start("first-subtest"));
+			write_packet_with_canary(fd, runnerpacket_subtest_start("second-subtest"));
+			write_packet_with_canary(fd, runnerpacket_exit(0, "0.000s"));
+
+			free_job_list(list);
+			clear_settings(settings);
+			igt_assert(initialize_execute_state_from_resume(dirfd, &state, settings, list));
+
+			igt_assert_eq(state.next, 1);
+			igt_assert_eq(list->size, NUM_TESTDATA_BINARIES);
+		}
+
+		igt_fixture {
+			close(fd);
+			close(subdirfd);
+			close(dirfd);
+			clear_directory(dirname);
+			free_job_list(list);
+			free(list);
+		}
+	}
+
 	igt_subtest_group {
 		struct job_list *list = malloc(sizeof(*list));
 		volatile int dirfd = -1, subdirfd = -1, fd = -1;
@@ -1490,6 +1673,8 @@ igt_main
 			char dirname[] = "tmpdirXXXXXX";
 
 			igt_fixture {
+				/* This test checks that the stdout parsing for result without time data works, so use that */
+				setenv("IGT_RUNNER_DISABLE_SOCKET_COMMUNICATION", "1", 1);
 				igt_require(mkdtemp(dirname) != NULL);
 				rmdir(dirname);
 			}
@@ -1554,6 +1739,7 @@ igt_main
 				close(dirfd);
 				clear_directory(dirname);
 				free_job_list(list);
+				unsetenv("IGT_RUNNER_DISABLE_SOCKET_COMMUNICATION");
 			}
 		}
 
-- 
2.30.2



More information about the Intel-gfx-trybot mailing list