diff --git a/lib/igt_ktap.c b/lib/igt_ktap.c
index 5e9967f980..d46a2433e5 100644
--- a/lib/igt_ktap.c
+++ b/lib/igt_ktap.c
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: MIT
 /*
  * Copyright © 2023 Isabella Basso do Amaral <isabbasso@riseup.net>
+ * Copyright © 2023 Intel Corporation
  */
 
 #include <ctype.h>
@@ -8,12 +9,310 @@
 #include <libkmod.h>
 #include <pthread.h>
 #include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
 
 #include "igt_aux.h"
 #include "igt_core.h"
 #include "igt_ktap.h"
 #include "igt_list.h"
 
+enum ktap_phase {
+	KTAP_START,
+	SUITE_COUNT,
+	SUITE_START,
+	SUITE_NAME,
+	CASE_COUNT,
+	CASE_NAME,
+	SUB_RESULT,
+	CASE_RESULT,
+	SUITE_RESULT,
+};
+
+struct igt_ktap_results {
+	enum ktap_phase expect;
+	unsigned int suite_count;
+	unsigned int suite_last;
+	char *suite_name;
+	unsigned int case_count;
+	unsigned int case_last;
+	char *case_name;
+	unsigned int sub_last;
+	struct igt_list_head *results;
+};
+
+/**
+ * igt_ktap_parse:
+ *
+ * This function parses a line of text for KTAP report data
+ * and passes results back to IGT kunit layer.
+ */
+int igt_ktap_parse(const char *buf, struct igt_ktap_results *ktap)
+{
+	char *suite_name = NULL, *case_name = NULL, *msg = NULL;
+	struct igt_ktap_result *result;
+	int code = IGT_EXIT_INVALID;
+	unsigned int n, len;
+	char s[2];
+
+	/* KTAP report header */
+	if (igt_debug_on(sscanf(buf, "KTAP%*[ ]version%*[ ]%u %n",
+				&n, &len) == 1 && len == strlen(buf))) {
+		if (igt_debug_on(ktap->expect != KTAP_START))
+			return -EPROTO;
+
+		ktap->suite_count = 0;
+		ktap->expect = SUITE_COUNT;
+
+	/* malformed TAP test plan? */
+	} else if (len = 0,
+		   igt_debug_on(sscanf(buf, " 1..%1[ ]", s) == 1)) {
+		return -EINPROGRESS;
+
+	/* valid test plan of a KTAP report */
+	} else if (igt_debug_on(sscanf(buf, "1..%u %n", &n, &len) == 1 &&
+				len == strlen(buf))) {
+		if (igt_debug_on(ktap->expect != SUITE_COUNT))
+			return -EPROTO;
+
+		if (!n)
+			return 0;
+
+		ktap->suite_count = n;
+		ktap->suite_last = 0;
+		ktap->suite_name = NULL;
+		ktap->expect = SUITE_START;
+
+	/* KTAP test suite header */
+	} else if (len = 0,
+		   igt_debug_on(sscanf(buf,
+				       "%*1[ ]%*1[ ]%*1[ ]%*1[ ]KTAP%*[ ]version%*[ ]%u %n",
+				       &n, &len) == 1 && len == strlen(buf))) {
+		/*
+		 * TODO: drop the following workaround as soon as
+		 * kernel side issue of missing lines with top level
+		 * KTAP version and test suite plan is fixed.
+		 */
+		if (ktap->expect == KTAP_START) {
+			ktap->suite_count = 1;
+			ktap->suite_last = 0;
+			ktap->suite_name = NULL;
+			ktap->expect = SUITE_START;
+		}
+
+		if (igt_debug_on(ktap->expect != SUITE_START))
+			return -EPROTO;
+
+		ktap->expect = SUITE_NAME;
+
+	/* KTAP test suite name */
+	} else if (len = 0,
+		   igt_debug_on(sscanf(buf,
+				       "%*1[ ]%*1[ ]%*1[ ]%*1[ ]#%*[ ]Subtest:%*[ ]%ms %n",
+				       &suite_name, &len) == 1 && len == strlen(buf))) {
+		if (igt_debug_on(ktap->expect != SUITE_NAME))
+			return -EPROTO;
+
+		ktap->suite_name = suite_name;
+		suite_name = NULL;
+		ktap->case_count = 0;
+		ktap->expect = CASE_COUNT;
+
+	/* valid test plan of a KTAP test suite */
+	} else if (len = 0, free(suite_name), suite_name = NULL,
+		   igt_debug_on(sscanf(buf,
+				       "%*1[ ]%*1[ ]%*1[ ]%*1[ ]1..%u %n",
+				       &n, &len) == 1 && len == strlen(buf))) {
+		if (igt_debug_on(ktap->expect != CASE_COUNT))
+			return -EPROTO;
+
+		if (n) {
+			ktap->case_count = n;
+			ktap->case_last = 0;
+			ktap->case_name = NULL;
+			ktap->expect = CASE_RESULT;
+		} else {
+			ktap->expect = SUITE_RESULT;
+		}
+
+	/* KTAP parametrized test case header */
+	} else if (len = 0,
+		   igt_debug_on(sscanf(buf,
+				       "%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]KTAP%*[ ]version%*[ ]%u %n",
+				       &n, &len) == 1 && len == strlen(buf))) {
+		if (igt_debug_on(ktap->expect != CASE_RESULT))
+			return -EPROTO;
+
+		ktap->sub_last = 0;
+		ktap->expect = CASE_NAME;
+
+	/* KTAP parametrized test case name */
+	} else if (len = 0,
+		   igt_debug_on(sscanf(buf,
+				       "%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]#%*[ ]Subtest:%*[ ]%ms %n",
+				       &case_name, &len) == 1 && len == strlen(buf))) {
+		if (igt_debug_on(ktap->expect != CASE_NAME))
+			return -EPROTO;
+
+		n = ktap->case_last + 1;
+		ktap->expect = SUB_RESULT;
+
+	/* KTAP parametrized subtest result */
+	} else if (len = 0, free(case_name), case_name = NULL,
+		   igt_debug_on(sscanf(buf,
+				       "%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]ok%*[ ]%u%*[ ]%*[^#\n]%1[#\n]",
+				       &n, s) == 2) ||
+		   igt_debug_on(sscanf(buf,
+				       "%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]%*1[ ]not%*1[ ]ok%*[ ]%u%*[ ]%*[^#\n]%1[#\n]",
+				       &n, s) == 2)) {
+		/* at lease one result of a parametrised subtest expected */
+		if (igt_debug_on(ktap->expect == SUB_RESULT &&
+				 ktap->sub_last == 0))
+			ktap->expect = CASE_RESULT;
+
+		if (igt_debug_on(ktap->expect != CASE_RESULT) ||
+		    igt_debug_on(n != ++ktap->sub_last))
+			return -EPROTO;
+
+	/* KTAP test case skip result */
+	} else if ((igt_debug_on(sscanf(buf,
+					"%*1[ ]%*1[ ]%*1[ ]%*1[ ]ok%*[ ]%u%*[ ]%ms%*[ ]#%*[ ]SKIP %n",
+					&n, &case_name, &len) == 2 &&
+				 len == strlen(buf))) ||
+		   (len = 0, free(case_name), case_name = NULL,
+		    igt_debug_on(sscanf(buf,
+					"%*1[ ]%*1[ ]%*1[ ]%*1[ ]ok%*[ ]%u%*[ ]%ms%*[ ]#%*[ ]SKIP%*[ ]%m[^\n]",
+					&n, &case_name, &msg) == 3))) {
+		code = IGT_EXIT_SKIP;
+
+	/* KTAP test case pass result */
+	} else if ((free(case_name), case_name = NULL, free(msg), msg = NULL,
+		    igt_debug_on(sscanf(buf,
+					"%*1[ ]%*1[ ]%*1[ ]%*1[ ]ok%*[ ]%u%*[ ]%ms %n",
+					&n, &case_name, &len) == 2 &&
+				 len == strlen(buf))) ||
+		   (len = 0, free(case_name), case_name = NULL,
+		    igt_debug_on(sscanf(buf,
+					"%*1[ ]%*1[ ]%*1[ ]%*1[ ]ok%*[ ]%u%*[ ]%ms%*[ ]#%*[ ]%m[^\n]",
+					&n, &case_name, &msg) == 3))) {
+		code = IGT_EXIT_SUCCESS;
+
+	/* KTAP test case fail result */
+	} else if ((free(case_name), case_name = NULL, free(msg), msg = NULL,
+		    igt_debug_on(sscanf(buf,
+					"%*1[ ]%*1[ ]%*1[ ]%*1[ ]not%*1[ ]ok%*[ ]%u%*[ ]%ms %n",
+					&n, &case_name, &len) == 2 &&
+				 len == strlen(buf))) ||
+		   (len = 0, free(case_name), case_name = NULL,
+		    igt_debug_on(sscanf(buf,
+					"%*1[ ]%*1[ ]%*1[ ]%*1[ ]not%*1[ ]ok%*[ ]%u%*[ ]%ms%*[ ]#%*[ ]%m[^\n]",
+					&n, &case_name, &msg) == 3))) {
+		code = IGT_EXIT_FAILURE;
+
+	/* KTAP test suite result */
+	} else if ((free(case_name), free(msg),
+		    igt_debug_on(sscanf(buf, "ok%*[ ]%u%*[ ]%ms %n",
+					&n, &suite_name, &len) == 2 &&
+				 len == strlen(buf))) ||
+		   (len = 0, free(suite_name), suite_name = NULL,
+		    igt_debug_on(sscanf(buf, "ok%*[ ]%u%*[ ]%ms%*[ ]%1[#]",
+					&n, &suite_name, s) == 3)) ||
+		   (free(suite_name), suite_name = NULL,
+		    igt_debug_on(sscanf(buf,
+					"not%*[ ]ok%*[ ]%u%*[ ]%ms %n",
+					&n, &suite_name, &len) == 2 &&
+				 len == strlen(buf))) ||
+		   (len = 0, free(suite_name), suite_name = NULL,
+		    igt_debug_on(sscanf(buf,
+					"not%*[ ]ok%*[ ]%u%*[ ]%ms%*[ ]%1[#]",
+					&n, &suite_name, s) == 3))) {
+		if (igt_debug_on(ktap->expect != SUITE_RESULT) ||
+		    igt_debug_on(strcmp(suite_name, ktap->suite_name)) ||
+		    igt_debug_on(n != ++ktap->suite_last) ||
+		    igt_debug_on(n > ktap->suite_count)) {
+			free(suite_name);
+			return -EPROTO;
+		}
+		free(suite_name);
+
+		/* last test suite? */
+		if (igt_debug_on(n == ktap->suite_count))
+			return 0;
+
+		ktap->suite_name = NULL;
+		ktap->expect = SUITE_START;
+
+	} else {
+		return -EINPROGRESS;
+	}
+
+	/* neither a test case name nor result */
+	if (ktap->expect != SUB_RESULT && code == IGT_EXIT_INVALID)
+		return -EINPROGRESS;
+
+	if (igt_debug_on(ktap->expect == SUB_RESULT &&
+			 code != IGT_EXIT_INVALID) ||
+	    igt_debug_on(code != IGT_EXIT_INVALID &&
+			 ktap->expect != CASE_RESULT) ||
+	    igt_debug_on(!ktap->suite_name) || igt_debug_on(!case_name) ||
+	    igt_debug_on(ktap->expect == CASE_RESULT && ktap->case_name &&
+			 strcmp(case_name, ktap->case_name)) ||
+	    igt_debug_on(n > ktap->case_count) ||
+	    igt_debug_on(n != (ktap->expect == SUB_RESULT ?
+			       ktap->case_last + 1: ++ktap->case_last))) {
+		free(case_name);
+		free(msg);
+		return -EPROTO;
+	}
+
+	if (ktap->expect == SUB_RESULT) {
+		/* KTAP parametrized test case name */
+		ktap->case_name = case_name;
+
+	} else {
+		/* KTAP test case result */
+		ktap->case_name = NULL;
+
+		/* last test case in a suite */
+		if (n == ktap->case_count)
+			ktap->expect = SUITE_RESULT;
+	}
+
+	if (igt_debug_on((result = calloc(1, sizeof(*result)), !result))) {
+		free(case_name);
+		free(msg);
+		return -ENOMEM;
+	}
+
+	result->suite_name = ktap->suite_name;
+	result->case_name = case_name;
+	result->code = code;
+	result->msg = msg;
+	igt_list_add_tail(&result->link, ktap->results);
+
+	return -EINPROGRESS;
+}
+
+struct igt_ktap_results *igt_ktap_alloc(struct igt_list_head *results)
+{
+	struct igt_ktap_results *ktap = calloc(1, sizeof(*ktap));
+
+	if (!ktap)
+		return NULL;
+
+	ktap->expect = KTAP_START;
+	ktap->results = results;
+
+	return ktap;
+}
+
+void igt_ktap_free(struct igt_ktap_results *ktap)
+{
+	free(ktap);
+}
+
 #define DELIMITER "-"
 
 struct ktap_parser_args {
@@ -332,6 +631,7 @@ static int parse_kmsg_for_tap(int fd, char *record, char *test_name)
  * 0 if succeded
  * -1 if error occurred
  */
+__maybe_unused
 static int parse_tap_level(int fd, char *base_test_name, int test_count, bool *failed_tests,
 			   bool *found_tests, bool is_builtin)
 {
@@ -445,62 +745,106 @@ static int parse_tap_level(int fd, char *base_test_name, int test_count, bool *f
  */
 void *igt_ktap_parser(void *unused)
 {
+	char record[BUF_LEN + 1], *buf, *suite_name = NULL, *case_name = NULL;
+	struct igt_ktap_results *ktap = NULL;
 	int fd = ktap_args.fd;
-	char record[BUF_LEN + 1];
-	bool is_builtin = ktap_args.is_builtin;
-	char test_name[BUF_LEN + 1];
-	bool failed_tests, found_tests;
-	int test_count;
+	IGT_LIST_HEAD(list);
+	int err;
 
-	failed_tests = false;
-	found_tests = false;
+	ktap = igt_ktap_alloc(&list);
+	if (igt_debug_on(!ktap))
+		goto igt_ktap_parser_end;
 
-igt_ktap_parser_start:
-	test_name[0] = '\0';
-	test_name[BUF_LEN] = '\0';
+	while (err = read(fd, record, BUF_LEN), err > 0) {
+		struct igt_ktap_result *r, *rn;
 
-	if (read(fd, record, BUF_LEN) < 0) {
-		if (errno == EPIPE)
-			igt_warn("kmsg truncated: too many messages. You may want to increase log_buf_len in kmcdline\n");
-		else if (errno != EINTR)
-			igt_warn("error reading kmsg (%m)\n");
+		/* skip kmsg continuation lines */
+		if (igt_debug_on(*record == ' '))
+			continue;
 
-		goto igt_ktap_parser_end;
-	}
+		/* NULL-terminate the record */
+		record[err] = '\0';
 
-	test_count = find_next_tap_subtest(fd, record, test_name, is_builtin);
+		/* detect start of log message, continue if not found */
+		buf = strchrnul(record, ';');
+		if (igt_debug_on(*buf == '\0'))
+			continue;
+		buf++;
 
-	switch (test_count) {
-	case -2:
-		/* Problems while reading the file */
-		goto igt_ktap_parser_end;
-	case -1:
-		/* No test found */
-		goto igt_ktap_parser_start;
-	case 0:
-		/* Tests found, but they're missing info */
-		found_tests = true;
-		goto igt_ktap_parser_end;
-	default:
-		found_tests = true;
+		err = igt_ktap_parse(buf, ktap);
 
-		if (parse_tap_level(fd, test_name, test_count, &failed_tests, &found_tests,
-				    is_builtin) == -1)
+		/* parsing error */
+		if (err && err != -EINPROGRESS)
 			goto igt_ktap_parser_end;
 
-		break;
+		igt_list_for_each_entry_safe(r, rn, &list, link) {
+			struct ktap_test_results_element *result = NULL;
+			int code = r->code;
+
+			if (code != IGT_EXIT_INVALID)
+				result = calloc(1, sizeof(*result));
+
+			if (result) {
+				snprintf(result->test_name, sizeof(result->test_name),
+					 "%s-%s", r->suite_name, r->case_name);
+
+				if (code == IGT_EXIT_SUCCESS)
+					result->passed = true;
+			}
+
+			igt_list_del(&r->link);
+			if (r->suite_name != suite_name) {
+				free(suite_name);
+				suite_name = r->suite_name;
+			}
+			if (r->case_name != case_name) {
+				free(case_name);
+				case_name = r->case_name;
+			}
+			free(r->msg);
+			free(r);
+
+			/*
+			 * no extra result record expected on start
+			 * of parametrized test case -- skip it
+			 */
+			if (code == IGT_EXIT_INVALID)
+				continue;
+
+			if (!result) {
+				err = -ENOMEM;
+				goto igt_ktap_parser_end;
+			}
+
+			pthread_mutex_lock(&results.mutex);
+			igt_list_add_tail(&result->link, &results.list);
+			pthread_mutex_unlock(&results.mutex);
+		}
+
+		/* end of KTAP report */
+		if (!err)
+			goto igt_ktap_parser_end;
 	}
 
-	/* Parse topmost level */
-	test_name[0] = '\0';
-	parse_tap_level(fd, test_name, test_count, &failed_tests, &found_tests, is_builtin);
+	if (err < 0) {
+		if (errno == EPIPE)
+			igt_warn("kmsg truncated: too many messages. You may want to increase log_buf_len in kmcdline\n");
+		else if (errno != EINTR)
+			igt_warn("error reading kmsg (%m)\n");
+	}
 
 igt_ktap_parser_end:
-	results.still_running = false;
+	free(suite_name);
+	free(case_name);
 
-	if (found_tests && !failed_tests)
+	if (!err)
 		ktap_args.ret = IGT_EXIT_SUCCESS;
 
+	results.still_running = false;
+
+	if (ktap)
+		igt_ktap_free(ktap);
+
 	return NULL;
 }
 
diff --git a/lib/igt_ktap.h b/lib/igt_ktap.h
index b4d7a6dbc7..6f8da3eab6 100644
--- a/lib/igt_ktap.h
+++ b/lib/igt_ktap.h
@@ -1,5 +1,6 @@
 /*
  * Copyright © 2022 Isabella Basso do Amaral <isabbasso@riseup.net>
+ * Copyright © 2023 Intel Corporation
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -30,6 +31,20 @@
 
 #include "igt_list.h"
 
+struct igt_ktap_result {
+	struct igt_list_head link;
+	char *suite_name;
+	char *case_name;
+	char *msg;
+	int code;
+};
+
+struct igt_ktap_results;
+
+struct igt_ktap_results *igt_ktap_alloc(struct igt_list_head *results);
+int igt_ktap_parse(const char *buf, struct igt_ktap_results *ktap);
+void igt_ktap_free(struct igt_ktap_results *ktap);
+
 void *igt_ktap_parser(void *unused);
 
 typedef struct ktap_test_results_element {
diff --git a/lib/tests/igt_ktap_parser.c b/lib/tests/igt_ktap_parser.c
new file mode 100644
index 0000000000..6357bdf6a5
--- /dev/null
+++ b/lib/tests/igt_ktap_parser.c
@@ -0,0 +1,246 @@
+// SPDX-License-Identifier: MIT
+/*
+* Copyright © 2023 Intel Corporation
+*/
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "igt_core.h"
+#include "igt_ktap.h"
+#include "igt_list.h"
+
+static void ktap_list(void)
+{
+	struct igt_ktap_result *result, *rn;
+	struct igt_ktap_results *ktap;
+	int suite = 1, test = 1;
+	IGT_LIST_HEAD(results);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+
+	igt_assert_eq(igt_ktap_parse("KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("1..3\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    # Subtest: test_suite_1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    1..3\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 1 test_case_1 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 2 test_case_2 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 3 test_case_3 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("ok 1 test_suite_1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    # Subtest: test_suite_2\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    1..1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 1 test_case_1 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("ok 2 test_suite_2\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    # Subtest: test_suite_3\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    1..4\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 1 test_case_1 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 2 test_case_2 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 3 test_case_3 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 4 test_case_4 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("ok 3 test_suite_3\n", ktap), 0);
+
+	igt_ktap_free(ktap);
+
+	igt_assert_eq(igt_list_length(&results), 8);
+
+	igt_list_for_each_entry_safe(result, rn, &results, link) {
+		char *case_name, *suite_name;
+
+		igt_list_del(&result->link);
+
+		igt_assert_lt(0, asprintf(&case_name, "test_case_%u", test));
+		igt_assert_lt(0, asprintf(&suite_name, "test_suite_%u", suite));
+
+		igt_assert(result->case_name);
+		igt_assert_eq(strcmp(result->case_name, case_name), 0);
+		free(result->case_name);
+		free(case_name);
+
+		igt_assert(result->suite_name);
+		igt_assert_eq(strcmp(result->suite_name, suite_name), 0);
+		free(suite_name);
+
+		igt_assert(!result->msg);
+		igt_assert_eq(result->code, IGT_EXIT_SKIP);
+
+		if ((suite == 1 && test < 3) || (suite == 3 && test < 4)) {
+			test++;
+		} else {
+			free(result->suite_name);
+			suite++;
+			test = 1;
+		}
+
+		free(result);
+	}
+}
+
+static void ktap_results(void)
+{
+	struct igt_ktap_result *result;
+	struct igt_ktap_results *ktap;
+	char *suite_name, *case_name;
+	IGT_LIST_HEAD(results);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+
+	igt_assert_eq(igt_ktap_parse("KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("1..1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    # Subtest: test_suite\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    1..1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        # Subtest: test_case\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        ok 1 parameter 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        ok 2 parameter 2 # a comment\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        ok 3 parameter 3 # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        ok 4 parameter 4 # SKIP with a message\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        not ok 5 parameter 5\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        not ok 6 parameter 6 # failure message\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    ok 1 test_case\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("not ok 1 test_suite\n", ktap), 0);
+
+	igt_ktap_free(ktap);
+
+	igt_assert_eq(igt_list_length(&results), 2);
+
+	result = igt_list_first_entry(&results, result, link);
+	igt_list_del(&result->link);
+	igt_assert_eq(strcmp(result->suite_name, "test_suite"), 0);
+	igt_assert_eq(strcmp(result->case_name, "test_case"), 0);
+	igt_assert_eq(result->code, IGT_EXIT_INVALID);
+	igt_assert(!result->msg);
+	free(result->msg);
+	suite_name = result->suite_name;
+	case_name = result->case_name;
+	free(result);
+
+	result = igt_list_first_entry(&results, result, link);
+	igt_list_del(&result->link);
+	igt_assert_eq(strcmp(result->suite_name, suite_name), 0);
+	igt_assert_eq(strcmp(result->case_name, case_name), 0);
+	igt_assert_neq(result->code, IGT_EXIT_INVALID);
+	free(result->msg);
+	free(suite_name);
+	free(case_name);
+	free(result);
+}
+
+static void ktap_success(void)
+{
+	struct igt_ktap_result *result;
+	struct igt_ktap_results *ktap;
+	IGT_LIST_HEAD(results);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+
+	igt_assert_eq(igt_ktap_parse("KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("1..1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    # Subtest: test_suite\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("    1..1\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_ktap_parse("        KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_assert(igt_list_empty(&results));
+
+	igt_assert_eq(igt_ktap_parse("        # Subtest: test_case\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_list_length(&results), 1);
+
+	igt_assert_eq(igt_ktap_parse("        ok 1 parameter # SKIP\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_list_length(&results), 1);
+
+	igt_assert_eq(igt_ktap_parse("    ok 1 test_case\n", ktap), -EINPROGRESS);
+	igt_assert_eq(igt_list_length(&results), 2);
+
+	igt_assert_eq(igt_ktap_parse("not ok 1 test_suite\n", ktap), 0);
+	igt_assert_eq(igt_list_length(&results), 2);
+
+	igt_ktap_free(ktap);
+
+	result = igt_list_last_entry(&results, result, link);
+	igt_list_del(&result->link);
+	igt_assert_eq(result->code, IGT_EXIT_SUCCESS);
+	free(result->msg);
+	free(result);
+
+	result = igt_list_last_entry(&results, result, link);
+	igt_list_del(&result->link);
+	free(result->suite_name);
+	free(result->case_name);
+	free(result->msg);
+	free(result);
+}
+
+static void ktap_top_version(void)
+{
+	struct igt_ktap_results *ktap;
+	IGT_LIST_HEAD(results);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("1..1\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	/* TODO: change to -EPROTO as soon as related workaround is dropped */
+	igt_assert_eq(igt_ktap_parse("    KTAP version 1\n", ktap), -EINPROGRESS);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("    # Subtest: test_suite\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("    1..1\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("        KTAP version 1\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("        # Subtest: test_case\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("        ok 1 parameter 1\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("    ok 1 test_case\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+
+	ktap = igt_ktap_alloc(&results);
+	igt_require(ktap);
+	igt_assert_eq(igt_ktap_parse("ok 1 test_suite\n", ktap), -EPROTO);
+	igt_ktap_free(ktap);
+}
+
+igt_main
+{
+	igt_subtest("list")
+		ktap_list();
+
+	igt_subtest("results")
+		ktap_results();
+
+	igt_subtest("success")
+		ktap_success();
+
+	igt_subtest("top-ktap-version")
+		ktap_top_version();
+}
diff --git a/lib/tests/meson.build b/lib/tests/meson.build
index 7a52a7876e..fa3d81de6c 100644
--- a/lib/tests/meson.build
+++ b/lib/tests/meson.build
@@ -10,6 +10,7 @@ lib_tests = [
 	'igt_exit_handler',
 	'igt_fork',
 	'igt_fork_helper',
+        'igt_ktap_parser',
 	'igt_list_only',
 	'igt_invalid_subtest_name',
 	'igt_nesting',
