htest: service boilerplate for secondary VMs.

In order to keep it easy to add tests, we need to avoid requiring new
VMs for each case as this will lead to an explosion of duplication and
build artifacts.

Allowing secondary VMs to contain selectable services for different
tests means they don't each require a separate VM and run into resource
limits. The resulting services are also avoid boilerplate so the source
gets to the point of the test more easily.

The primary VMs are also refactored to encourage splitting tests across
file but bundling them into the same VM so as to cut down on build
artifacts and make it easier to find and run tests.

Assertions can now be used in any context of a test, including in a test
service.

Change-Id: Id3b8a7579d0facdfceb9d77f62ef57241b31a88a
diff --git a/kokoro/ubuntu/test.sh b/kokoro/ubuntu/test.sh
index 14f67e4..be49777 100755
--- a/kokoro/ubuntu/test.sh
+++ b/kokoro/ubuntu/test.sh
@@ -44,4 +44,3 @@
 $HFTEST hafnium --initrd gicv3_test
 $HFTEST hafnium --initrd primary_only_test
 $HFTEST hafnium --initrd primary_with_secondaries_test
-$HFTEST hafnium --initrd primary_with_state_secondary_test
diff --git a/project/reference b/project/reference
index 8e1da4f..ad27769 160000
--- a/project/reference
+++ b/project/reference
@@ -1 +1 @@
-Subproject commit 8e1da4f139a409fad2086fb9456bfa836fba6c72
+Subproject commit ad27769360362f6b93f44b5709ec97dc825c829e
diff --git a/test/hftest/BUILD.gn b/test/hftest/BUILD.gn
index bc956f8..5f2469a 100644
--- a/test/hftest/BUILD.gn
+++ b/test/hftest/BUILD.gn
@@ -45,9 +45,16 @@
 source_set("hftest_secondary_vm") {
   testonly = true
 
+  public_configs = [ ":hftest_config" ]
+
+  sources = [
+    "hftest_service.c",
+  ]
+
   deps = [
     "//src:common",
     "//src:dlog",
+    "//src:memiter",
     "//src/arch/${plat_arch}:entry",
     "//src/arch/${plat_arch}/hftest:entry",
     "//src/arch/${plat_arch}/hftest:hf_call",
diff --git a/test/hftest/hftest.c b/test/hftest/hftest.c
index 6a66fed..04a07a6 100644
--- a/test/hftest/hftest.c
+++ b/test/hftest/hftest.c
@@ -19,8 +19,11 @@
 #include <stdalign.h>
 #include <stdint.h>
 
+#include "hf/arch/vm/power_mgmt.h"
+
 #include "hf/fdt.h"
 #include "hf/memiter.h"
+#include "hf/std.h"
 
 alignas(4096) uint8_t kstack[4096];
 
@@ -29,6 +32,13 @@
 extern struct hftest_test hftest_begin[];
 extern struct hftest_test hftest_end[];
 
+static struct hftest_context global_context;
+
+struct hftest_context *hftest_get_context(void)
+{
+	return &global_context;
+}
+
 static void json(void)
 {
 	struct hftest_test *test;
@@ -62,9 +72,11 @@
 			if (!tests_in_suite) {
 				HFTEST_LOG("      \"tests\": [");
 			}
-			/* It's easier to put the comma at the start of the line
+			/*
+			 * It's easier to put the comma at the start of the line
 			 * than the end even
-			 * though the JSON looks a bit funky. */
+			 * though the JSON looks a bit funky.
+			 */
 			HFTEST_LOG("       %c\"%s\"",
 				   tests_in_suite ? ',' : ' ', test->name);
 			++tests_in_suite;
@@ -78,37 +90,43 @@
 	HFTEST_LOG("}");
 }
 
+static noreturn void abort(void)
+{
+	HFTEST_LOG("FAIL");
+	shutdown();
+}
+
 static void run_test(hftest_test_fn set_up, hftest_test_fn test,
 		     hftest_test_fn tear_down)
 {
-	struct hftest_context ctx = {
-		.failures = 0,
-	};
+	/* Prepare the context. */
+	struct hftest_context *ctx = hftest_get_context();
+	memset(ctx, 0, sizeof(*ctx));
+	ctx->abort = abort;
 
+	/* Run any set up functions. */
 	if (set_up) {
-		set_up(&ctx);
-		if (ctx.failures) {
-			goto fail;
+		set_up();
+		if (ctx->failures) {
+			abort();
 		}
 	}
 
-	test(&ctx);
-	if (ctx.failures) {
-		goto fail;
+	/* Run the test. */
+	test();
+	if (ctx->failures) {
+		abort();
 	}
 
+	/* Run any tear down functions. */
 	if (tear_down) {
-		tear_down(&ctx);
-		if (ctx.failures) {
-			goto fail;
+		tear_down();
+		if (ctx->failures) {
+			abort();
 		}
 	}
 
-	HFTEST_LOG("PASS");
-	return;
-
-fail:
-	HFTEST_LOG("FAIL");
+	HFTEST_LOG("FINISHED");
 }
 
 static void run(struct memiter *args)
@@ -151,8 +169,10 @@
 		}
 
 		switch (test->kind) {
-		/* The first entries in the suite are the set up and tear down
-		 * functions. */
+		/*
+		 * The first entries in the suite are the set up and tear down
+		 * functions.
+		 */
 		case HFTEST_KIND_SET_UP:
 			suite_set_up = test->fn;
 			break;
@@ -167,13 +187,16 @@
 				return;
 			}
 			break;
+		default:
+			/* Ignore other kinds. */
+			break;
 		}
 	}
 
 	HFTEST_LOG("Unable to find requested tests.");
 }
 
-void help(void)
+static void help(void)
 {
 	HFTEST_LOG("usage:");
 	HFTEST_LOG("");
@@ -193,7 +216,7 @@
 	HFTEST_LOG("    Run the named test from the named test suite.");
 }
 
-void main(const struct fdt_header *fdt)
+void kmain(const struct fdt_header *fdt)
 {
 	struct fdt_node n;
 	const char *bootargs;
@@ -241,8 +264,3 @@
 
 	help();
 }
-
-void kmain(const struct fdt_header *fdt)
-{
-	main(fdt);
-}
diff --git a/test/hftest/hftest.py b/test/hftest/hftest.py
index 4a00a52..2a2a07e 100755
--- a/test/hftest/hftest.py
+++ b/test/hftest/hftest.py
@@ -35,12 +35,12 @@
 def qemu(image, initrd, args, log):
     qemu_args = [
         "timeout", "--foreground", "5s",
-        "./prebuilts/linux-x64/qemu/qemu-system-aarch64", "-M", "virt,gic_version=3", "-cpu",
-        "cortex-a57", "-smp", "4", "-m", "16M", "-machine", "virtualization=true",
+        "./prebuilts/linux-x64/qemu/qemu-system-aarch64", "-M", "virt,gic_version=3",
+        "-cpu", "cortex-a57", "-smp", "4", "-m", "16M", "-machine", "virtualization=true",
         "-nographic", "-nodefaults", "-serial", "stdio", "-kernel", image,
     ]
     if initrd:
-      qemu_args += ["-initrd", initrd]
+        qemu_args += ["-initrd", initrd]
     if args:
         qemu_args += ["-append", args]
     # Save the log to a file.
@@ -132,7 +132,8 @@
                 sponge_log.write(out)
                 sponge_log.write("\r\n\r\n")
                 hftest_out = hftest_lines(out)
-                if hftest_out[-1] == "PASS":
+                if hftest_out[-1] == "FINISHED" and not any(
+                        l.startswith('Failure:') for l in hftest_out):
                     print("        PASS")
                 else:
                     failures_from_suite += 1
diff --git a/test/hftest/hftest_service.c b/test/hftest/hftest_service.c
new file mode 100644
index 0000000..048d267
--- /dev/null
+++ b/test/hftest/hftest_service.c
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdalign.h>
+#include <stdint.h>
+
+#include "hf/memiter.h"
+#include "hf/std.h"
+
+#include "vmapi/hf/call.h"
+
+#include "hftest.h"
+
+alignas(4096) uint8_t kstack[4096];
+
+HFTEST_ENABLE();
+
+extern struct hftest_test hftest_begin[];
+extern struct hftest_test hftest_end[];
+
+static alignas(HF_MAILBOX_SIZE) uint8_t send[HF_MAILBOX_SIZE];
+static alignas(HF_MAILBOX_SIZE) uint8_t recv[HF_MAILBOX_SIZE];
+
+static hf_ipaddr_t send_addr = (hf_ipaddr_t)send;
+static hf_ipaddr_t recv_addr = (hf_ipaddr_t)recv;
+
+static struct hftest_context global_context;
+
+struct hftest_context *hftest_get_context(void)
+{
+	return &global_context;
+}
+
+/** Find the service with the name passed in the arguments. */
+static hftest_test_fn find_service(struct memiter *args)
+{
+	struct memiter service_name;
+	struct hftest_test *test;
+
+	if (!memiter_parse_str(args, &service_name)) {
+		return NULL;
+	}
+
+	for (test = hftest_begin; test < hftest_end; ++test) {
+		if (test->kind == HFTEST_KIND_SERVICE &&
+		    memiter_iseq(&service_name, test->name)) {
+			return test->fn;
+		}
+	}
+
+	return NULL;
+}
+
+static noreturn void abort(void)
+{
+	HFTEST_LOG("Service contained failures.");
+	for (;;) {
+		/*
+		 * Hang if the service aborts as a secondary can't power down
+		 * the machine.
+		 */
+	}
+}
+
+noreturn void kmain(void)
+{
+	struct memiter args;
+	hftest_test_fn service;
+	struct hf_mailbox_receive_return res;
+	struct hftest_context *ctx;
+
+	/* Prepare the context. */
+
+	/* Set up the mailbox. */
+	hf_vm_configure(send_addr, recv_addr);
+
+	/* Receive the name of the service to run. */
+	res = hf_mailbox_receive(true);
+	memiter_init(&args, recv, res.size);
+	service = find_service(&args);
+	hf_mailbox_clear();
+
+	/* Check the service was found. */
+	if (service == NULL) {
+		HFTEST_LOG_FAILURE();
+		HFTEST_LOG(HFTEST_LOG_INDENT
+			   "Unable to find requested service");
+		for (;;) {
+			/* Hang if the service was unknown. */
+		}
+	}
+
+	/* Clean the context. */
+	ctx = hftest_get_context();
+	memset(ctx, 0, sizeof(*ctx));
+	ctx->abort = abort;
+	ctx->send = send;
+	ctx->recv = recv;
+
+	/* Pause so the next time cycles are given the service will be run. */
+	hf_vcpu_yield();
+
+	/* Let the service run. */
+	service();
+
+	/* Cleanly handle it if the service returns. */
+	if (ctx->failures) {
+		abort();
+	}
+
+	for (;;) {
+		/* Hang if the service returns. */
+	}
+}
diff --git a/test/hftest/inc/hftest.h b/test/hftest/inc/hftest.h
index 290b467..894d7f0 100644
--- a/test/hftest/inc/hftest.h
+++ b/test/hftest/inc/hftest.h
@@ -37,27 +37,39 @@
  */
 #define TEST(suite, test) HFTEST_TEST(suite, test)
 
+/*
+ * Define a test service.
+ */
+#define TEST_SERVICE(service) HFTEST_TEST_SERVICE(service)
+
 /* Assertions. */
-#define ASSERT_EQ(x, y) ASSERT_OP(x, y, ==, true)
-#define ASSERT_NE(x, y) ASSERT_OP(x, y, !=, true)
-#define ASSERT_LE(x, y) ASSERT_OP(x, y, <=, true)
-#define ASSERT_LT(x, y) ASSERT_OP(x, y, <, true)
-#define ASSERT_GE(x, y) ASSERT_OP(x, y, >=, true)
-#define ASSERT_GT(x, y) ASSERT_OP(x, y, >, true)
+#define ASSERT_EQ(x, y) HFTEST_ASSERT_OP(x, y, ==, true)
+#define ASSERT_NE(x, y) HFTEST_ASSERT_OP(x, y, !=, true)
+#define ASSERT_LE(x, y) HFTEST_ASSERT_OP(x, y, <=, true)
+#define ASSERT_LT(x, y) HFTEST_ASSERT_OP(x, y, <, true)
+#define ASSERT_GE(x, y) HFTEST_ASSERT_OP(x, y, >=, true)
+#define ASSERT_GT(x, y) HFTEST_ASSERT_OP(x, y, >, true)
 
 #define ASSERT_TRUE(x) ASSERT_EQ(x, true);
 #define ASSERT_FALSE(x) ASSERT_EQ(x, false);
 
-#define EXPECT_EQ(x, y) ASSERT_OP(x, y, ==, false)
-#define EXPECT_NE(x, y) ASSERT_OP(x, y, !=, false)
-#define EXPECT_LE(x, y) ASSERT_OP(x, y, <=, false)
-#define EXPECT_LT(x, y) ASSERT_OP(x, y, <, false)
-#define EXPECT_GE(x, y) ASSERT_OP(x, y, >=, false)
-#define EXPECT_GT(x, y) ASSERT_OP(x, y, >, false)
+#define EXPECT_EQ(x, y) HFTEST_ASSERT_OP(x, y, ==, false)
+#define EXPECT_NE(x, y) HFTEST_ASSERT_OP(x, y, !=, false)
+#define EXPECT_LE(x, y) HFTEST_ASSERT_OP(x, y, <=, false)
+#define EXPECT_LT(x, y) HFTEST_ASSERT_OP(x, y, <, false)
+#define EXPECT_GE(x, y) HFTEST_ASSERT_OP(x, y, >=, false)
+#define EXPECT_GT(x, y) HFTEST_ASSERT_OP(x, y, >, false)
 
 #define EXPECT_TRUE(x) EXPECT_EQ(x, true);
 #define EXPECT_FALSE(x) EXPECT_EQ(x, false);
 
+/* Service utilities. */
+#define SERVICE_SELECT(vm_id, service, send_buffer) \
+	HFTEST_SERVICE_SELECT(vm_id, service, send_buffer)
+
+#define SERVICE_SEND_BUFFER() HFTEST_SERVICE_SEND_BUFFER()
+#define SERVICE_RECV_BUFFER() HFTEST_SERVICE_RECV_BUFFER()
+
 /*
  * This must be used exactly once in a test image to signal to the linker that
  * the .hftest section is allowed to be included in the generated image.
@@ -69,185 +81,10 @@
  */
 #define HFTEST_LOG_PREFIX "[hftest] "
 
-/* Above this point is the public API. Below are the implementation details. */
-
-/* Log with the HFTEST_LOG_PREFIX and a new line. The zero is added so there is
- * always at least one variadic argument. */
-#define HFTEST_LOG(...) HFTEST_LOG_IMPL(__VA_ARGS__, 0)
-#define HFTEST_LOG_IMPL(format, ...) \
-	dlog("%s" format "\n", HFTEST_LOG_PREFIX, __VA_ARGS__)
-
-/* Helper to wrap the argument in quotes. */
-#define HFTEST_STR(str) #str
-
-/* Sections are names such that when the linker sorts them, all entries for the
- * same test suite are contiguous and the set up and tear down entries come
- * before the tests. This order simplifies test discovery in the running image.
- */
-#define HFTEST_SET_UP_SECTION(suite_name) \
-	HFTEST_STR(.hftest.suite_name .1set_up)
-#define HFTEST_TEAR_DOWN_SECTION(suite_name) \
-	HFTEST_STR(.hftest.suite_name .1tear_down)
-#define HFTEST_TEST_SECTION(suite_name, test_name) \
-	HFTEST_STR(.hftest.suite_name .2test.test_name)
-
-/* Helpers to construct unique identifiers. */
-#define HFTEST_SET_UP_STRUCT(suite_name) hftest_set_up_##suite_name
-#define HFTEST_TEAR_DOWN_STRUCT(suite_name) hftest_tear_down_##suite_name
-#define HFTEST_TEST_STRUCT(suite_name, test_name) \
-	hftest_test_##suite_name##_##test_name
-
-#define HFTEST_SET_UP_FN(suite_name) hftest_set_up_fn_##suite_name
-#define HFTEST_TEAR_DOWN_FN(suite_name) hftest_tear_down_fn_##suite_name
-#define HFTEST_TEST_FN(suite_name, test_name) \
-	hftest_test_fn_##suite_name##_##test_name
-
-/* Register test functions. */
-#define HFTEST_SET_UP(suite_name)                                           \
-	static void HFTEST_SET_UP_FN(suite_name)(struct hftest_context *    \
-						 hftest_ctx);               \
-	const struct hftest_test __attribute__((used))                      \
-		__attribute__((section(HFTEST_SET_UP_SECTION(suite_name)))) \
-			HFTEST_SET_UP_STRUCT(suite_name) = {                \
-				.suite = #suite_name,                       \
-				.kind = HFTEST_KIND_SET_UP,                 \
-				.fn = HFTEST_SET_UP_FN(suite_name),         \
-	};                                                                  \
-	static void HFTEST_SET_UP_FN(suite_name)(                           \
-		__attribute__((unused)) struct hftest_context * hftest_ctx)
-
-#define HFTEST_TEAR_DOWN(suite_name)                                           \
-	static void HFTEST_TEAR_DOWN_FN(suite_name)(struct hftest_context *    \
-						    hftest_ctx);               \
-	const struct hftest_test __attribute__((used))                         \
-		__attribute__((section(HFTEST_TEAR_DOWN_SECTION(suite_name)))) \
-			HFTEST_TEAR_DOWN_STRUCT(suite_name) = {                \
-				.suite = #suite_name,                          \
-				.kind = HFTEST_KIND_TEAR_DOWN,                 \
-				.fn = HFTEST_TEAR_DOWN_FN(suite_name),         \
-	};                                                                     \
-	static void HFTEST_TEAR_DOWN_FN(suite_name)(                           \
-		__attribute__((unused)) struct hftest_context * hftest_ctx)
-
-#define HFTEST_TEST(suite_name, test_name)                                  \
-	static void HFTEST_TEST_FN(                                         \
-		suite_name, test_name)(struct hftest_context * hftest_ctx); \
-	const struct hftest_test __attribute__((used)) __attribute__(       \
-		(section(HFTEST_TEST_SECTION(suite_name, test_name))))      \
-		HFTEST_TEST_STRUCT(suite_name, test_name) = {               \
-			.suite = #suite_name,                               \
-			.kind = HFTEST_KIND_TEST,                           \
-			.name = #test_name,                                 \
-			.fn = HFTEST_TEST_FN(suite_name, test_name),        \
-	};                                                                  \
-	static void HFTEST_TEST_FN(suite_name, test_name)(                  \
-		__attribute__((unused)) struct hftest_context * hftest_ctx)
-
-/* Context for tests. */
-struct hftest_context {
-	uint32_t failures;
-};
-
-/* A test case. */
-typedef void (*hftest_test_fn)(struct hftest_context *);
-
-enum hftest_kind {
-	HFTEST_KIND_SET_UP = 0,
-	HFTEST_KIND_TEST = 1,
-	HFTEST_KIND_TEAR_DOWN = 2,
-};
-
-struct hftest_test {
-	const char *suite;
-	enum hftest_kind kind;
-	const char *name;
-	hftest_test_fn fn;
-};
-
 /*
- * This union can store any of the primitive types supported by the assertion
- * macros.
- *
- * It does not include pointers as comparison of pointers is not often needed
- * and could be a mistake for string comparison. If pointer comparison is needed
- * and explicit assertion such as ASSERT_PTR_EQ() would be more appropriate.
+ * Indentation used e.g. to give the reason for an assertion failure.
  */
-union hftest_any {
-	bool b;
-	char c;
-	signed char sc;
-	unsigned char uc;
-	signed short ss;
-	unsigned short us;
-	signed int si;
-	unsigned int ui;
-	signed long int sli;
-	unsigned long int uli;
-	signed long long int slli;
-	unsigned long long int ulli;
-};
+#define HFTEST_LOG_INDENT "    "
 
-/* _Generic formatting doesn't seem to be supported so doing this manually. */
-/* clang-format off */
-
-/* Select the union member to match the type of the expression. */
-#define hftest_any_get(any, x)                      \
-	_Generic((x),                               \
-		bool:                   (any).b,    \
-		char:                   (any).c,    \
-		signed char:            (any).sc,   \
-		unsigned char:          (any).uc,   \
-		signed short:           (any).ss,   \
-		unsigned short:         (any).us,   \
-		signed int:             (any).si,   \
-		unsigned int:           (any).ui,   \
-		signed long int:        (any).sli,  \
-		unsigned long int:      (any).uli,  \
-		signed long long int:   (any).slli, \
-		unsigned long long int: (any).ulli)
-
-/*
- * dlog format specifier for types. Note, these aren't the standard specifiers
- * for the types.
- */
-#define hftest_dlog_format(x)                 \
-	_Generic((x),                         \
-		bool:                   "%u", \
-		char:                   "%c", \
-		signed char:            "%d", \
-		unsigned char:          "%u", \
-		signed short:           "%d", \
-		unsigned short:         "%u", \
-		signed int:             "%d", \
-		unsigned int:           "%u", \
-		signed long int:        "%d", \
-		unsigned long int:      "%u", \
-		signed long long int:   "%d", \
-		unsigned long long int: "%u")
-
-/* clang-format on */
-
-#define ASSERT_OP(lhs, rhs, op, fatal)                                         \
-	do {                                                                   \
-		union hftest_any lhs_value;                                    \
-		union hftest_any rhs_value;                                    \
-		hftest_any_get(lhs_value, lhs) = (lhs);                        \
-		hftest_any_get(rhs_value, rhs) = (rhs);                        \
-		if (!(hftest_any_get(lhs_value, lhs)                           \
-			      op hftest_any_get(rhs_value, rhs))) {            \
-			++hftest_ctx->failures;                                \
-			dlog(HFTEST_LOG_PREFIX "  %s:%u: Failure\n", __FILE__, \
-			     __LINE__);                                        \
-			dlog(HFTEST_LOG_PREFIX "    %s %s %s (%s=", #lhs, #op, \
-			     #rhs, #lhs);                                      \
-			dlog(hftest_dlog_format(lhs),                          \
-			     hftest_any_get(lhs_value, lhs));                  \
-			dlog(", %s=", #rhs);                                   \
-			dlog(hftest_dlog_format(rhs),                          \
-			     hftest_any_get(rhs_value, rhs));                  \
-			dlog(")\n");                                           \
-			if (fatal) {                                           \
-				return;                                        \
-			}                                                      \
-		}                                                              \
-	} while (0)
+/* Above this point is the public API. Now include the implementation. */
+#include <hftest_impl.h>
diff --git a/test/hftest/inc/hftest_impl.h b/test/hftest/inc/hftest_impl.h
new file mode 100644
index 0000000..7a7be34
--- /dev/null
+++ b/test/hftest/inc/hftest_impl.h
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stdnoreturn.h>
+
+/*
+ * Log with the HFTEST_LOG_PREFIX and a new line. The zero is added so there is
+ * always at least one variadic argument.
+ */
+#define HFTEST_LOG(...) HFTEST_LOG_IMPL(__VA_ARGS__, 0)
+#define HFTEST_LOG_IMPL(format, ...) \
+	dlog("%s" format "\n", HFTEST_LOG_PREFIX, __VA_ARGS__)
+
+/* Helper to wrap the argument in quotes. */
+#define HFTEST_STR(str) #str
+
+/*
+ * Sections are names such that when the linker sorts them, all entries for the
+ * same test suite are contiguous and the set up and tear down entries come
+ * before the tests. This order simplifies test discovery in the running image.
+ */
+#define HFTEST_SET_UP_SECTION(suite_name) \
+	HFTEST_STR(.hftest.suite.suite_name .1set_up)
+#define HFTEST_TEAR_DOWN_SECTION(suite_name) \
+	HFTEST_STR(.hftest.suite.suite_name .1tear_down)
+#define HFTEST_TEST_SECTION(suite_name, test_name) \
+	HFTEST_STR(.hftest.suite.suite_name .2test.test_name)
+#define HFTEST_SERVICE_SECTION(service_name) \
+	HFTEST_STR(.hftest.service.service_name)
+
+/* Helpers to construct unique identifiers. */
+#define HFTEST_SET_UP_STRUCT(suite_name) hftest_set_up_##suite_name
+#define HFTEST_TEAR_DOWN_STRUCT(suite_name) hftest_tear_down_##suite_name
+#define HFTEST_TEST_STRUCT(suite_name, test_name) \
+	hftest_test_##suite_name##_##test_name
+#define HFTEST_SERVICE_STRUCT(service_name) hftest_service_##service_name
+
+#define HFTEST_SET_UP_FN(suite_name) hftest_set_up_fn_##suite_name
+#define HFTEST_TEAR_DOWN_FN(suite_name) hftest_tear_down_fn_##suite_name
+#define HFTEST_TEST_FN(suite_name, test_name) \
+	hftest_test_fn_##suite_name##_##test_name
+#define HFTEST_SERVICE_FN(service_name) hftest_service_fn_##service_name
+
+/* Register test functions. */
+#define HFTEST_SET_UP(suite_name)                                           \
+	static void HFTEST_SET_UP_FN(suite_name)(void);                     \
+	const struct hftest_test __attribute__((used))                      \
+		__attribute__((section(HFTEST_SET_UP_SECTION(suite_name)))) \
+			HFTEST_SET_UP_STRUCT(suite_name) = {                \
+				.suite = #suite_name,                       \
+				.kind = HFTEST_KIND_SET_UP,                 \
+				.fn = HFTEST_SET_UP_FN(suite_name),         \
+	};                                                                  \
+	static void HFTEST_SET_UP_FN(suite_name)(void)
+
+#define HFTEST_TEAR_DOWN(suite_name)                                           \
+	static void HFTEST_TEAR_DOWN_FN(suite_name)(void);                     \
+	const struct hftest_test __attribute__((used))                         \
+		__attribute__((section(HFTEST_TEAR_DOWN_SECTION(suite_name)))) \
+			HFTEST_TEAR_DOWN_STRUCT(suite_name) = {                \
+				.suite = #suite_name,                          \
+				.kind = HFTEST_KIND_TEAR_DOWN,                 \
+				.fn = HFTEST_TEAR_DOWN_FN(suite_name),         \
+	};                                                                     \
+	static void HFTEST_TEAR_DOWN_FN(suite_name)(void)
+
+#define HFTEST_TEST(suite_name, test_name)                             \
+	static void HFTEST_TEST_FN(suite_name, test_name)(void);       \
+	const struct hftest_test __attribute__((used)) __attribute__(  \
+		(section(HFTEST_TEST_SECTION(suite_name, test_name)))) \
+		HFTEST_TEST_STRUCT(suite_name, test_name) = {          \
+			.suite = #suite_name,                          \
+			.kind = HFTEST_KIND_TEST,                      \
+			.name = #test_name,                            \
+			.fn = HFTEST_TEST_FN(suite_name, test_name),   \
+	};                                                             \
+	static void HFTEST_TEST_FN(suite_name, test_name)(void)
+
+#define HFTEST_TEST_SERVICE(service_name)                                      \
+	static void HFTEST_SERVICE_FN(service_name)(void);                     \
+	const struct hftest_test __attribute__((used))                         \
+		__attribute__((section(HFTEST_SERVICE_SECTION(service_name)))) \
+			HFTEST_SERVICE_STRUCT(service_name) = {                \
+				.kind = HFTEST_KIND_SERVICE,                   \
+				.name = #service_name,                         \
+				.fn = HFTEST_SERVICE_FN(service_name),         \
+	};                                                                     \
+	static void HFTEST_SERVICE_FN(service_name)(void)
+
+/* Context for tests. */
+struct hftest_context {
+	uint32_t failures;
+	noreturn void (*abort)(void);
+
+	/* These are used in services. */
+	void *send;
+	void *recv;
+};
+
+struct hftest_context *hftest_get_context(void);
+
+/* A test case. */
+typedef void (*hftest_test_fn)(void);
+
+enum hftest_kind {
+	HFTEST_KIND_SET_UP = 0,
+	HFTEST_KIND_TEST = 1,
+	HFTEST_KIND_TEAR_DOWN = 2,
+	HFTEST_KIND_SERVICE = 3,
+};
+
+/**
+ * The .hftest section contains an array of this struct which describes the test
+ * functions contained in the image allowing the image to inspect the tests it
+ * contains.
+ */
+struct hftest_test {
+	const char *suite;
+	enum hftest_kind kind;
+	const char *name;
+	hftest_test_fn fn;
+};
+
+/*
+ * This union can store any of the primitive types supported by the assertion
+ * macros.
+ *
+ * It does not include pointers as comparison of pointers is not often needed
+ * and could be a mistake for string comparison. If pointer comparison is needed
+ * and explicit assertion such as ASSERT_PTR_EQ() would be more appropriate.
+ */
+union hftest_any {
+	bool b;
+	char c;
+	signed char sc;
+	unsigned char uc;
+	signed short ss;
+	unsigned short us;
+	signed int si;
+	unsigned int ui;
+	signed long int sli;
+	unsigned long int uli;
+	signed long long int slli;
+	unsigned long long int ulli;
+};
+
+/* _Generic formatting doesn't seem to be supported so doing this manually. */
+/* clang-format off */
+
+/* Select the union member to match the type of the expression. */
+#define hftest_any_get(any, x)                      \
+	_Generic((x),                               \
+		bool:                   (any).b,    \
+		char:                   (any).c,    \
+		signed char:            (any).sc,   \
+		unsigned char:          (any).uc,   \
+		signed short:           (any).ss,   \
+		unsigned short:         (any).us,   \
+		signed int:             (any).si,   \
+		unsigned int:           (any).ui,   \
+		signed long int:        (any).sli,  \
+		unsigned long int:      (any).uli,  \
+		signed long long int:   (any).slli, \
+		unsigned long long int: (any).ulli)
+
+/*
+ * dlog format specifier for types. Note, these aren't the standard specifiers
+ * for the types.
+ */
+#define hftest_dlog_format(x)                 \
+	_Generic((x),                         \
+		bool:                   "%u", \
+		char:                   "%c", \
+		signed char:            "%d", \
+		unsigned char:          "%u", \
+		signed short:           "%d", \
+		unsigned short:         "%u", \
+		signed int:             "%d", \
+		unsigned int:           "%u", \
+		signed long int:        "%d", \
+		unsigned long int:      "%u", \
+		signed long long int:   "%d", \
+		unsigned long long int: "%u")
+
+/* clang-format on */
+
+#define HFTEST_LOG_FAILURE() \
+	dlog(HFTEST_LOG_PREFIX "Failure: %s:%u\n", __FILE__, __LINE__);
+
+#define HFTEST_ASSERT_OP(lhs, rhs, op, fatal)                              \
+	do {                                                               \
+		union hftest_any lhs_value;                                \
+		union hftest_any rhs_value;                                \
+		hftest_any_get(lhs_value, lhs) = (lhs);                    \
+		hftest_any_get(rhs_value, rhs) = (rhs);                    \
+		if (!(hftest_any_get(lhs_value, lhs)                       \
+			      op hftest_any_get(rhs_value, rhs))) {        \
+			struct hftest_context *ctx = hftest_get_context(); \
+			++ctx->failures;                                   \
+			HFTEST_LOG_FAILURE();                              \
+			dlog(HFTEST_LOG_PREFIX HFTEST_LOG_INDENT           \
+			     "%s %s %s (%s=",                              \
+			     #lhs, #op, #rhs, #lhs);                       \
+			dlog(hftest_dlog_format(lhs),                      \
+			     hftest_any_get(lhs_value, lhs));              \
+			dlog(", %s=", #rhs);                               \
+			dlog(hftest_dlog_format(rhs),                      \
+			     hftest_any_get(rhs_value, rhs));              \
+			dlog(")\n");                                       \
+			if (fatal) {                                       \
+				ctx->abort();                              \
+			}                                                  \
+		}                                                          \
+	} while (0)
+
+/**
+ * Select the service to run in a service VM.
+ */
+#define HFTEST_SERVICE_SELECT(vm_id, service, send_buffer)                    \
+	do {                                                                  \
+		struct hf_vcpu_run_return run_res;                            \
+                                                                              \
+		/*                                                            \
+		 * Let the service configure its mailbox and wait for a       \
+		 * message.                                                   \
+		 */                                                           \
+		run_res = hf_vcpu_run(vm_id, 0);                              \
+		ASSERT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);      \
+                                                                              \
+		/* Send the selected service to run and let it be handled. */ \
+		memcpy(send_buffer, service, strlen(service));                \
+		ASSERT_EQ(hf_mailbox_send(vm_id, strlen(service)), 0);        \
+		run_res = hf_vcpu_run(vm_id, 0);                              \
+		ASSERT_EQ(run_res.code, HF_VCPU_RUN_YIELD);                   \
+	} while (0)
+
+#define HFTEST_SERVICE_SEND_BUFFER() hftest_get_context()->send
+#define HFTEST_SERVICE_RECV_BUFFER() hftest_get_context()->recv
diff --git a/test/vmapi/BUILD.gn b/test/vmapi/BUILD.gn
index fab748c..073e525 100644
--- a/test/vmapi/BUILD.gn
+++ b/test/vmapi/BUILD.gn
@@ -13,130 +13,12 @@
 # limitations under the License.
 
 import("//build/image/image.gni")
-import("//build/toolchain/platform.gni")
-
-config("config") {
-  include_dirs = [ "inc" ]
-}
 
 group("vmapi") {
   testonly = true
 
   deps = [
-    ":primary_only_test",
-    ":primary_with_secondaries_test",
-    ":primary_with_state_secondary_test",
+    "primary_only:primary_only_test",
+    "primary_with_secondaries:primary_with_secondaries_test",
   ]
 }
-
-# Tests with no secondary VMs.
-vm_kernel("primary_only_test_vm") {
-  testonly = true
-  public_configs = [ "//test/vmapi:config" ]
-
-  sources = [
-    "primary_only.c",
-  ]
-
-  deps = [
-    "//test/hftest:hftest_primary_vm",
-  ]
-}
-
-initrd("primary_only_test") {
-  testonly = true
-
-  primary_vm = ":primary_only_test_vm"
-}
-
-# Tests specific to GICv3.
-vm_kernel("gicv3_test_vm") {
-  testonly = true
-  public_configs = [ "//test/vmapi:config" ]
-
-  sources = [
-    "gicv3.c",
-  ]
-
-  deps = [
-    "//src/arch/${plat_arch}/hftest:interrupts_gicv3",
-    "//test/hftest:hftest_primary_vm",
-  ]
-}
-
-initrd("gicv3_test") {
-  testonly = true
-
-  primary_vm = ":gicv3_test_vm"
-}
-
-# Tests with secondary VMs.
-vm_kernel("primary_with_secondaries_test_vm") {
-  testonly = true
-  public_configs = [ "//test/vmapi:config" ]
-
-  sources = [
-    "primary_with_secondaries.c",
-  ]
-
-  deps = [
-    "//test/hftest:hftest_primary_vm",
-  ]
-}
-
-initrd("primary_with_secondaries_test") {
-  testonly = true
-
-  primary_vm = ":primary_with_secondaries_test_vm"
-  secondary_vms = [
-    [
-      "1048576",
-      "1",
-      "relay_a",
-      "//test/vmapi/secondaries:relay_a",
-    ],
-    [
-      "1048576",
-      "1",
-      "relay_b",
-      "//test/vmapi/secondaries:relay_b",
-    ],
-    [
-      "1048576",
-      "1",
-      "echo",
-      "//test/vmapi/secondaries:echo",
-    ],
-    [
-      "1048576",
-      "1",
-      "interruptible",
-      "//test/vmapi/secondaries:interruptible",
-    ],
-  ]
-}
-
-# Tests with state test secondary VM.
-vm_kernel("primary_with_state_secondary_test_vm") {
-  testonly = true
-
-  sources = [
-    "primary_for_state_test.c",
-  ]
-
-  deps = [
-    "//test/hftest:hftest_primary_vm",
-  ]
-}
-
-initrd("primary_with_state_secondary_test") {
-  testonly = true
-
-  primary_vm = ":primary_with_state_secondary_test_vm"
-  secondary_vms = [ [
-        "1048576",
-        "1",
-        "check_state",
-        "//test/vmapi/secondaries:check_state",
-      ] ]
-}
diff --git a/test/vmapi/gicv3/BUILD.gn b/test/vmapi/gicv3/BUILD.gn
new file mode 100644
index 0000000..4a166f0
--- /dev/null
+++ b/test/vmapi/gicv3/BUILD.gn
@@ -0,0 +1,36 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build/image/image.gni")
+import("//build/toolchain/platform.gni")
+
+# Tests specific to GICv3.
+vm_kernel("gicv3_test_vm") {
+  testonly = true
+
+  sources = [
+    "gicv3.c",
+  ]
+
+  deps = [
+    "//src/arch/${plat_arch}/hftest:interrupts_gicv3",
+    "//test/hftest:hftest_primary_vm",
+  ]
+}
+
+initrd("gicv3_test") {
+  testonly = true
+
+  primary_vm = ":gicv3_test_vm"
+}
diff --git a/test/vmapi/gicv3.c b/test/vmapi/gicv3/gicv3.c
similarity index 100%
rename from test/vmapi/gicv3.c
rename to test/vmapi/gicv3/gicv3.c
diff --git a/test/vmapi/primary_only/BUILD.gn b/test/vmapi/primary_only/BUILD.gn
new file mode 100644
index 0000000..3039f37
--- /dev/null
+++ b/test/vmapi/primary_only/BUILD.gn
@@ -0,0 +1,34 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build/image/image.gni")
+
+# Tests with no secondary VMs.
+vm_kernel("primary_only_test_vm") {
+  testonly = true
+
+  sources = [
+    "primary_only.c",
+  ]
+
+  deps = [
+    "//test/hftest:hftest_primary_vm",
+  ]
+}
+
+initrd("primary_only_test") {
+  testonly = true
+
+  primary_vm = ":primary_only_test_vm"
+}
diff --git a/test/vmapi/primary_only.c b/test/vmapi/primary_only/primary_only.c
similarity index 100%
rename from test/vmapi/primary_only.c
rename to test/vmapi/primary_only/primary_only.c
diff --git a/test/vmapi/primary_with_secondaries.c b/test/vmapi/primary_with_secondaries.c
deleted file mode 100644
index 0570af9..0000000
--- a/test/vmapi/primary_with_secondaries.c
+++ /dev/null
@@ -1,469 +0,0 @@
-/*
- * Copyright 2018 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <assert.h>
-#include <stdalign.h>
-#include <stdint.h>
-
-#include "hf/mm.h"
-#include "hf/std.h"
-
-#include "vmapi/hf/call.h"
-
-#include "constants.h"
-#include "hftest.h"
-
-static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
-static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
-static_assert(sizeof(send_page) == PAGE_SIZE, "Send page is not a page.");
-static_assert(sizeof(recv_page) == PAGE_SIZE, "Recv page is not a page.");
-
-static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
-static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
-
-/* Keep macro alignment */
-/* clang-format off */
-
-#define RELAY_A_VM_ID         1
-#define RELAY_B_VM_ID         2
-#define ECHO_VM_ID            3
-#define INTERRUPTIBLE_VM_ID   4
-
-/* clang-format on */
-
-/**
- * Reverses the order of the elements in the given array.
- */
-void reverse(char *s, size_t len)
-{
-	size_t i;
-
-	for (i = 0; i < len / 2; i++) {
-		char t = s[i];
-		s[i] = s[len - 1 - i];
-		s[len - 1 - i] = t;
-	}
-}
-
-/**
- * Finds the next lexicographic permutation of the given array, if there is one.
- */
-void next_permutation(char *s, size_t len)
-{
-	size_t i, j;
-
-	for (i = len - 2; i < len; i--) {
-		const char t = s[i];
-		if (t >= s[i + 1]) {
-			continue;
-		}
-
-		for (j = len - 1; t >= s[j]; j--) {
-		}
-
-		s[i] = s[j];
-		s[j] = t;
-		reverse(s + i + 1, len - i - 1);
-		return;
-	}
-}
-
-/**
- * Confirms the primary VM has the primary ID.
- */
-TEST(hf_vm_get_id, primary_has_primary_id)
-{
-	EXPECT_EQ(hf_vm_get_id(), HF_PRIMARY_VM_ID);
-}
-
-/**
- * Confirm there are 4 secondary VMs as well as this primary VM.
- */
-TEST(hf_vm_get_count, four_secondary_vms)
-{
-	EXPECT_EQ(hf_vm_get_count(), 5);
-}
-
-/**
- * Confirm that secondary VM has 1 VCPU.
- */
-TEST(hf_vcpu_get_count, secondary_has_one_vcpu)
-{
-	EXPECT_EQ(hf_vcpu_get_count(1), 1);
-}
-
-/**
- * Confirm it is an error to query how many VCPUs are assigned to a nonexistent
- * secondary VM.
- */
-TEST(hf_vcpu_get_count, large_invalid_vm_index)
-{
-	EXPECT_EQ(hf_vcpu_get_count(0xffffffff), -1);
-}
-
-/**
- * The primary can't be run by the hypervisor.
- */
-TEST(hf_vcpu_run, cannot_run_primary)
-{
-	struct hf_vcpu_run_return res = hf_vcpu_run(HF_PRIMARY_VM_ID, 0);
-	EXPECT_EQ(res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-}
-
-/**
- * Can only run a VM that exists.
- */
-TEST(hf_vcpu_run, cannot_run_absent_secondary)
-{
-	struct hf_vcpu_run_return res = hf_vcpu_run(1234, 0);
-	EXPECT_EQ(res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-}
-
-/**
- * Can only run a vcpu that exists.
- */
-TEST(hf_vcpu_run, cannot_run_absent_vcpu)
-{
-	struct hf_vcpu_run_return res = hf_vcpu_run(ECHO_VM_ID, 1234);
-	EXPECT_EQ(res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-}
-
-/**
- * The configured send/receive addresses can't be unaligned.
- */
-TEST(hf_vm_configure, fails_with_unaligned_pointer)
-{
-	uint8_t maybe_aligned[2];
-	hf_ipaddr_t unaligned_addr = (hf_ipaddr_t)&maybe_aligned[1];
-	hf_ipaddr_t aligned_addr = (hf_ipaddr_t)send_page;
-
-	/* Check the the address is unaligned. */
-	ASSERT_EQ(unaligned_addr & 1, 1);
-
-	EXPECT_EQ(hf_vm_configure(aligned_addr, unaligned_addr), -1);
-	EXPECT_EQ(hf_vm_configure(unaligned_addr, aligned_addr), -1);
-	EXPECT_EQ(hf_vm_configure(unaligned_addr, unaligned_addr), -1);
-}
-
-/**
- * The configured send/receive addresses can't be the same page.
- */
-TEST(hf_vm_configure, fails_with_same_page)
-{
-	EXPECT_EQ(hf_vm_configure(send_page_addr, send_page_addr), -1);
-	EXPECT_EQ(hf_vm_configure(recv_page_addr, recv_page_addr), -1);
-}
-
-/**
- * The configuration of the send/receive addresses can only happen once.
- */
-TEST(hf_vm_configure, fails_if_already_succeeded)
-{
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), -1);
-}
-
-/**
- * The configuration of the send/receive address is successful with valid
- * arguments.
- */
-TEST(hf_vm_configure, succeeds)
-{
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-}
-
-/**
- * The primary receives messages from hf_vcpu_run().
- */
-TEST(hf_mailbox_receive, cannot_receive_from_primary_blocking)
-{
-	struct hf_mailbox_receive_return res = hf_mailbox_receive(true);
-	EXPECT_EQ(res.vm_id, HF_INVALID_VM_ID);
-	EXPECT_EQ(res.size, 0);
-}
-
-/**
- * The primary receives messages from hf_vcpu_run().
- */
-TEST(hf_mailbox_receive, cannot_receive_from_primary_non_blocking)
-{
-	struct hf_mailbox_receive_return res = hf_mailbox_receive(false);
-	EXPECT_EQ(res.vm_id, HF_INVALID_VM_ID);
-	EXPECT_EQ(res.size, 0);
-}
-
-/**
- * Send and receive the same message from the echo VM.
- */
-TEST(mailbox, echo)
-{
-	const char message[] = "Echo this back to me!";
-	struct hf_vcpu_run_return run_res;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	run_res = hf_vcpu_run(ECHO_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/* Set the message, echo it and check it didn't change. */
-	memcpy(send_page, message, sizeof(message));
-	EXPECT_EQ(hf_mailbox_send(ECHO_VM_ID, sizeof(message)), 0);
-	run_res = hf_vcpu_run(ECHO_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(message));
-	EXPECT_EQ(memcmp(recv_page, message, sizeof(message)), 0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-}
-
-/**
- * Repeatedly send a message and receive it back from the echo VM.
- */
-TEST(mailbox, repeated_echo)
-{
-	char message[] = "Echo this back to me!";
-	struct hf_vcpu_run_return run_res;
-	uint8_t i;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-
-	for (i = 0; i < 100; i++) {
-		/* Run secondary until it reaches the wait for messages. */
-		run_res = hf_vcpu_run(ECHO_VM_ID, 0);
-		EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-		/* Set the message, echo it and check it didn't change. */
-		next_permutation(message, sizeof(message) - 1);
-		memcpy(send_page, message, sizeof(message));
-		EXPECT_EQ(hf_mailbox_send(ECHO_VM_ID, sizeof(message)), 0);
-		run_res = hf_vcpu_run(ECHO_VM_ID, 0);
-		EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-		EXPECT_EQ(run_res.message.size, sizeof(message));
-		EXPECT_EQ(memcmp(recv_page, message, sizeof(message)), 0);
-		EXPECT_EQ(hf_mailbox_clear(), 0);
-	}
-}
-
-/**
- * Send a message to relay_a which will forward it to relay_b where it will be
- * sent back here.
- */
-TEST(mailbox, relay)
-{
-	const char message[] = "Send this round the relay!";
-	struct hf_vcpu_run_return run_res;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	run_res = hf_vcpu_run(RELAY_A_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-	run_res = hf_vcpu_run(RELAY_B_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/*
-	 * Send the message to relay_a which is then sent to relay_b before
-	 * checking that relay_b send the message back here.
-	 */
-	memcpy(send_page, message, sizeof(message));
-	EXPECT_EQ(hf_mailbox_send(RELAY_A_VM_ID, sizeof(message)), 0);
-	run_res = hf_vcpu_run(RELAY_A_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAKE_UP);
-	EXPECT_EQ(run_res.wake_up.vm_id, RELAY_B_VM_ID);
-	EXPECT_EQ(run_res.wake_up.vcpu, 0);
-	run_res = hf_vcpu_run(RELAY_B_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(message));
-	EXPECT_EQ(memcmp(recv_page, message, sizeof(message)), 0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-}
-
-/**
- * Send a message to the interruptible VM, which will interrupt itself to send a
- * response back.
- */
-TEST(interrupts, interrupt_self)
-{
-	const char message[] = "Ping";
-	const char expected_response[] = "Got IRQ 05.";
-	struct hf_vcpu_run_return run_res;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/* Set the message, echo it and wait for a response. */
-	memcpy(send_page, message, sizeof(message));
-	EXPECT_EQ(hf_mailbox_send(INTERRUPTIBLE_VM_ID, sizeof(message)), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
-	EXPECT_EQ(
-		memcmp(recv_page, expected_response, sizeof(expected_response)),
-		0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-}
-
-/**
- * Inject an interrupt to the interrupt VM, which will send a message back.
- * Repeat this twice to make sure it doesn't get into a bad state after the
- * first one.
- */
-TEST(interrupts, inject_interrupt_twice)
-{
-	const char expected_response[] = "Got IRQ 07.";
-	struct hf_vcpu_run_return run_res;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/* Inject the interrupt and wait for a message. */
-	hf_inject_interrupt(INTERRUPTIBLE_VM_ID, 0, EXTERNAL_INTERRUPT_ID_A);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
-	EXPECT_EQ(
-		memcmp(recv_page, expected_response, sizeof(expected_response)),
-		0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-
-	/* Inject the interrupt again, and wait for the same message. */
-	hf_inject_interrupt(INTERRUPTIBLE_VM_ID, 0, EXTERNAL_INTERRUPT_ID_A);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
-	EXPECT_EQ(
-		memcmp(recv_page, expected_response, sizeof(expected_response)),
-		0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-}
-
-/**
- * Inject two different interrupts to the interrupt VM, which will send a
- * message back each time.
- */
-TEST(interrupts, inject_two_interrupts)
-{
-	const char expected_response[] = "Got IRQ 07.";
-	const char expected_response_2[] = "Got IRQ 08.";
-	struct hf_vcpu_run_return run_res;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/* Inject the interrupt and wait for a message. */
-	hf_inject_interrupt(INTERRUPTIBLE_VM_ID, 0, EXTERNAL_INTERRUPT_ID_A);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
-	EXPECT_EQ(
-		memcmp(recv_page, expected_response, sizeof(expected_response)),
-		0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-
-	/* Inject a different interrupt and wait for a different message. */
-	hf_inject_interrupt(INTERRUPTIBLE_VM_ID, 0, EXTERNAL_INTERRUPT_ID_B);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response_2));
-	EXPECT_EQ(memcmp(recv_page, expected_response_2,
-			 sizeof(expected_response_2)),
-		  0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-}
-
-/**
- * Inject an interrupt then send a message to the interrupt VM, which will send
- * a message back each time. This is to test that interrupt injection doesn't
- * interfere with message reception.
- */
-TEST(interrupts, inject_interrupt_message)
-{
-	const char expected_response[] = "Got IRQ 07.";
-	const char message[] = "Ping";
-	const char expected_response_2[] = "Got IRQ 05.";
-	struct hf_vcpu_run_return run_res;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/* Inject the interrupt and wait for a message. */
-	hf_inject_interrupt(INTERRUPTIBLE_VM_ID, 0, EXTERNAL_INTERRUPT_ID_A);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
-	EXPECT_EQ(
-		memcmp(recv_page, expected_response, sizeof(expected_response)),
-		0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/* Now send a message to the secondary. */
-	memcpy(send_page, message, sizeof(message));
-	EXPECT_EQ(hf_mailbox_send(INTERRUPTIBLE_VM_ID, sizeof(message)), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response_2));
-	EXPECT_EQ(memcmp(recv_page, expected_response_2,
-			 sizeof(expected_response_2)),
-		  0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-}
-
-/**
- * Inject an interrupt which the target VM has not enabled, and then send a
- * message telling it to enable that interrupt ID. It should then (and only
- * then) send a message back.
- */
-TEST(interrupts, inject_interrupt_disabled)
-{
-	const char expected_response[] = "Got IRQ 09.";
-	const char message[] = "Enable interrupt C";
-	struct hf_vcpu_run_return run_res;
-
-	/* Configure mailbox pages. */
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-
-	/* Inject the interrupt and expect not to get a message. */
-	hf_inject_interrupt(INTERRUPTIBLE_VM_ID, 0, EXTERNAL_INTERRUPT_ID_C);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
-	EXPECT_EQ(hf_mailbox_clear(), -1);
-
-	/*
-	 * Now send a message to the secondary to enable the interrupt ID, and
-	 * expect the response from the interrupt we sent before.
-	 */
-	memcpy(send_page, message, sizeof(message));
-	EXPECT_EQ(hf_mailbox_send(INTERRUPTIBLE_VM_ID, sizeof(message)), 0);
-	run_res = hf_vcpu_run(INTERRUPTIBLE_VM_ID, 0);
-	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
-	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
-	EXPECT_EQ(
-		memcmp(recv_page, expected_response, sizeof(expected_response)),
-		0);
-	EXPECT_EQ(hf_mailbox_clear(), 0);
-}
diff --git a/test/vmapi/primary_with_secondaries/BUILD.gn b/test/vmapi/primary_with_secondaries/BUILD.gn
new file mode 100644
index 0000000..87e3b1b
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/BUILD.gn
@@ -0,0 +1,57 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build/image/image.gni")
+
+config("config") {
+  include_dirs = [ "inc" ]
+}
+
+# Tests with secondary VMs.
+vm_kernel("primary_with_secondaries_test_vm") {
+  testonly = true
+  public_configs = [ ":config" ]
+
+  sources = [
+    "no_services.c",
+    "run_race.c",
+    "with_services.c",
+  ]
+
+  sources += [ "util.c" ]
+
+  deps = [
+    "//test/hftest:hftest_primary_vm",
+  ]
+}
+
+initrd("primary_with_secondaries_test") {
+  testonly = true
+
+  primary_vm = ":primary_with_secondaries_test_vm"
+  secondary_vms = [
+    [
+      "1048576",
+      "1",
+      "services0",
+      "services:service_vm0",
+    ],
+    [
+      "1048576",
+      "1",
+      "services1",
+      "services:service_vm1",
+    ],
+  ]
+}
diff --git a/test/vmapi/inc/constants.h b/test/vmapi/primary_with_secondaries/inc/primary_with_secondary.h
similarity index 81%
rename from test/vmapi/inc/constants.h
rename to test/vmapi/primary_with_secondaries/inc/primary_with_secondary.h
index aa30def..2999c5b 100644
--- a/test/vmapi/inc/constants.h
+++ b/test/vmapi/primary_with_secondaries/inc/primary_with_secondary.h
@@ -14,7 +14,19 @@
  * limitations under the License.
  */
 
+#pragma once
+
+#define SERVICE_VM0 1
+#define SERVICE_VM1 2
+
 #define SELF_INTERRUPT_ID 5
 #define EXTERNAL_INTERRUPT_ID_A 7
 #define EXTERNAL_INTERRUPT_ID_B 8
 #define EXTERNAL_INTERRUPT_ID_C 9
+
+struct mailbox_buffers {
+	void *send;
+	void *recv;
+};
+
+struct mailbox_buffers set_up_mailbox(void);
diff --git a/test/vmapi/primary_with_secondaries/no_services.c b/test/vmapi/primary_with_secondaries/no_services.c
new file mode 100644
index 0000000..13adca0
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/no_services.c
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdalign.h>
+#include <stdint.h>
+
+#include "hf/mm.h"
+#include "hf/std.h"
+
+#include "vmapi/hf/call.h"
+
+#include "hftest.h"
+#include "primary_with_secondary.h"
+
+static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
+static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
+static_assert(sizeof(send_page) == PAGE_SIZE, "Send page is not a page.");
+static_assert(sizeof(recv_page) == PAGE_SIZE, "Recv page is not a page.");
+
+static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
+static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
+
+/**
+ * Confirms the primary VM has the primary ID.
+ */
+TEST(hf_vm_get_id, primary_has_primary_id)
+{
+	EXPECT_EQ(hf_vm_get_id(), HF_PRIMARY_VM_ID);
+}
+
+/**
+ * Confirm there are 2 secondary VMs as well as this primary VM.
+ */
+TEST(hf_vm_get_count, four_secondary_vms)
+{
+	EXPECT_EQ(hf_vm_get_count(), 3);
+}
+
+/**
+ * Confirm that secondary VM has 1 VCPU.
+ */
+TEST(hf_vcpu_get_count, secondary_has_one_vcpu)
+{
+	EXPECT_EQ(hf_vcpu_get_count(1), 1);
+}
+
+/**
+ * Confirm it is an error to query how many VCPUs are assigned to a nonexistent
+ * secondary VM.
+ */
+TEST(hf_vcpu_get_count, large_invalid_vm_index)
+{
+	EXPECT_EQ(hf_vcpu_get_count(0xffffffff), -1);
+}
+
+/**
+ * The primary can't be run by the hypervisor.
+ */
+TEST(hf_vcpu_run, cannot_run_primary)
+{
+	struct hf_vcpu_run_return res = hf_vcpu_run(HF_PRIMARY_VM_ID, 0);
+	EXPECT_EQ(res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+}
+
+/**
+ * Can only run a VM that exists.
+ */
+TEST(hf_vcpu_run, cannot_run_absent_secondary)
+{
+	struct hf_vcpu_run_return res = hf_vcpu_run(1234, 0);
+	EXPECT_EQ(res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+}
+
+/**
+ * Can only run a vcpu that exists.
+ */
+TEST(hf_vcpu_run, cannot_run_absent_vcpu)
+{
+	struct hf_vcpu_run_return res = hf_vcpu_run(SERVICE_VM0, 1234);
+	EXPECT_EQ(res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+}
+
+/**
+ * The configured send/receive addresses can't be unaligned.
+ */
+TEST(hf_vm_configure, fails_with_unaligned_pointer)
+{
+	uint8_t maybe_aligned[2];
+	hf_ipaddr_t unaligned_addr = (hf_ipaddr_t)&maybe_aligned[1];
+	hf_ipaddr_t aligned_addr = (hf_ipaddr_t)send_page;
+
+	/* Check the the address is unaligned. */
+	ASSERT_EQ(unaligned_addr & 1, 1);
+
+	EXPECT_EQ(hf_vm_configure(aligned_addr, unaligned_addr), -1);
+	EXPECT_EQ(hf_vm_configure(unaligned_addr, aligned_addr), -1);
+	EXPECT_EQ(hf_vm_configure(unaligned_addr, unaligned_addr), -1);
+}
+
+/**
+ * The configured send/receive addresses can't be the same page.
+ */
+TEST(hf_vm_configure, fails_with_same_page)
+{
+	EXPECT_EQ(hf_vm_configure(send_page_addr, send_page_addr), -1);
+	EXPECT_EQ(hf_vm_configure(recv_page_addr, recv_page_addr), -1);
+}
+
+/**
+ * The configuration of the send/receive addresses can only happen once.
+ */
+TEST(hf_vm_configure, fails_if_already_succeeded)
+{
+	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
+	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), -1);
+}
+
+/**
+ * The configuration of the send/receive address is successful with valid
+ * arguments.
+ */
+TEST(hf_vm_configure, succeeds)
+{
+	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
+}
+
+/**
+ * The primary receives messages from hf_vcpu_run().
+ */
+TEST(hf_mailbox_receive, cannot_receive_from_primary_blocking)
+{
+	struct hf_mailbox_receive_return res = hf_mailbox_receive(true);
+	EXPECT_EQ(res.vm_id, HF_INVALID_VM_ID);
+	EXPECT_EQ(res.size, 0);
+}
+
+/**
+ * The primary receives messages from hf_vcpu_run().
+ */
+TEST(hf_mailbox_receive, cannot_receive_from_primary_non_blocking)
+{
+	struct hf_mailbox_receive_return res = hf_mailbox_receive(false);
+	EXPECT_EQ(res.vm_id, HF_INVALID_VM_ID);
+	EXPECT_EQ(res.size, 0);
+}
diff --git a/test/vmapi/primary_for_state_test.c b/test/vmapi/primary_with_secondaries/run_race.c
similarity index 74%
rename from test/vmapi/primary_for_state_test.c
rename to test/vmapi/primary_with_secondaries/run_race.c
index 9a3b732..dc7a6a4 100644
--- a/test/vmapi/primary_for_state_test.c
+++ b/test/vmapi/primary_with_secondaries/run_race.c
@@ -26,22 +26,13 @@
 #include "vmapi/hf/call.h"
 
 #include "hftest.h"
-
-static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
-static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
-static_assert(sizeof(send_page) == PAGE_SIZE, "Send page is not a page.");
-static_assert(sizeof(recv_page) == PAGE_SIZE, "Recv page is not a page.");
-
-static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
-static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
-
-#define STATE_VM_ID 1
+#include "primary_with_secondary.h"
 
 /**
  * Iterates trying to run vCPU of the secondary VM. Returns when a message
  * of non-zero length is received.
  */
-bool run_loop(void)
+static bool run_loop(struct mailbox_buffers *mb)
 {
 	struct hf_vcpu_run_return run_res;
 	bool ok = false;
@@ -49,7 +40,7 @@
 	for (;;) {
 		/* Run until it manages to schedule vCPU on this CPU. */
 		do {
-			run_res = hf_vcpu_run(STATE_VM_ID, 0);
+			run_res = hf_vcpu_run(SERVICE_VM0, 0);
 		} while (run_res.code == HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
 
 		/* Break out if we received a message with non-zero length. */
@@ -64,7 +55,7 @@
 
 	/* Copies the contents of the received boolean to the return value. */
 	if (run_res.message.size == sizeof(ok)) {
-		memcpy(&ok, recv_page, sizeof(ok));
+		memcpy(&ok, mb->recv, sizeof(ok));
 	}
 
 	hf_mailbox_clear();
@@ -78,8 +69,7 @@
  */
 static void vm_cpu_entry(uintptr_t arg)
 {
-	(void)arg;
-	run_loop();
+	run_loop((struct mailbox_buffers *)arg);
 }
 
 /**
@@ -90,12 +80,16 @@
 TEST(vcpu_state, concurrent_save_restore)
 {
 	alignas(4096) static char stack[4096];
+	static struct mailbox_buffers mb;
 
-	EXPECT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
+	mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "check_state", mb.send);
 
 	/* Start second vCPU. */
-	EXPECT_EQ(cpu_start(1, stack, sizeof(stack), vm_cpu_entry, 0), true);
+	ASSERT_TRUE(cpu_start(1, stack, sizeof(stack), vm_cpu_entry,
+			      (uintptr_t)&mb));
 
 	/* Run on a loop until the secondary VM is done. */
-	EXPECT_EQ(run_loop(), true);
+	EXPECT_TRUE(run_loop(&mb));
 }
diff --git a/test/vmapi/primary_with_secondaries/services/BUILD.gn b/test/vmapi/primary_with_secondaries/services/BUILD.gn
new file mode 100644
index 0000000..3530004
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/services/BUILD.gn
@@ -0,0 +1,89 @@
+# Copyright 2018 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build/image/image.gni")
+
+# Service to expose race conditions when running a vcpu.
+source_set("check_state") {
+  testonly = true
+  public_configs = [ "//test/hftest:hftest_config" ]
+
+  sources = [
+    "check_state.c",
+  ]
+
+  deps = [
+    "//src/arch/aarch64/hftest:state",
+  ]
+}
+
+# Service to listen for messages and echo them back to the sender.
+source_set("echo") {
+  testonly = true
+  public_configs = [ "//test/hftest:hftest_config" ]
+
+  sources = [
+    "echo.c",
+  ]
+}
+
+# Service that can be interrupted.
+source_set("interruptible") {
+  testonly = true
+  public_configs = [
+    "..:config",
+    "//test/hftest:hftest_config",
+  ]
+
+  sources = [
+    "interruptible.c",
+  ]
+
+  deps = [
+    "//src/arch/aarch64/hftest:interrupts_gicv3",
+  ]
+}
+
+# Service to listen for messages and forward them on to another.
+source_set("relay") {
+  testonly = true
+  public_configs = [ "//test/hftest:hftest_config" ]
+
+  sources = [
+    "relay.c",
+  ]
+}
+
+# Group services together into VMs.
+
+vm_kernel("service_vm0") {
+  testonly = true
+
+  deps = [
+    ":check_state",
+    ":echo",
+    ":interruptible",
+    ":relay",
+    "//test/hftest:hftest_secondary_vm",
+  ]
+}
+
+vm_kernel("service_vm1") {
+  testonly = true
+
+  deps = [
+    ":relay",
+    "//test/hftest:hftest_secondary_vm",
+  ]
+}
diff --git a/test/vmapi/secondaries/check_state.c b/test/vmapi/primary_with_secondaries/services/check_state.c
similarity index 68%
rename from test/vmapi/secondaries/check_state.c
rename to test/vmapi/primary_with_secondaries/services/check_state.c
index e05c0a7..402e584 100644
--- a/test/vmapi/secondaries/check_state.c
+++ b/test/vmapi/primary_with_secondaries/services/check_state.c
@@ -14,23 +14,13 @@
  * limitations under the License.
  */
 
-#include <stdalign.h>
-#include <stdint.h>
-
 #include "hf/arch/vm/state.h"
 
-#include "hf/mm.h"
 #include "hf/std.h"
 
 #include "vmapi/hf/call.h"
 
-alignas(4096) uint8_t kstack[4096];
-
-static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
-static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
-
-static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
-static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
+#include "hftest.h"
 
 void send_with_retry(uint32_t vm_id, size_t size)
 {
@@ -42,24 +32,23 @@
 }
 
 /**
- * This VM repeatedly takes the following steps: sets the per-cpu pointer to
- * some value, makes a hypervisor call, check that the value is still what it
+ * This service repeatedly takes the following steps: sets the per-cpu pointer
+ * to some value, makes a hypervisor call, check that the value is still what it
  * was set to.
  *
  * This loop helps detect bugs where the hypervisor inadvertently destroys
  * state.
  *
- * At the end of its iterations, the VM reports the result to the primary VM,
- * which then fails or succeeds the test.
+ * At the end of its iterations, the service reports the result to the primary
+ * VM, which then fails or succeeds the test.
  */
-void kmain(void)
+TEST_SERVICE(check_state)
 {
 	size_t i;
 	bool ok = true;
 	static volatile uintptr_t expected;
 	static volatile uintptr_t actual;
 
-	hf_vm_configure(send_page_addr, recv_page_addr);
 	for (i = 0; i < 100000; i++) {
 		/*
 		 * We store the expected/actual values in volatile static
@@ -74,7 +63,7 @@
 	}
 
 	/* Send two replies, one for each physical CPU. */
-	memcpy(send_page, &ok, sizeof(ok));
+	memcpy(SERVICE_SEND_BUFFER(), &ok, sizeof(ok));
 	send_with_retry(HF_PRIMARY_VM_ID, sizeof(ok));
 	send_with_retry(HF_PRIMARY_VM_ID, sizeof(ok));
 }
diff --git a/test/vmapi/secondaries/echo.c b/test/vmapi/primary_with_secondaries/services/echo.c
similarity index 65%
rename from test/vmapi/secondaries/echo.c
rename to test/vmapi/primary_with_secondaries/services/echo.c
index 6a095b1..8ec08f6 100644
--- a/test/vmapi/secondaries/echo.c
+++ b/test/vmapi/primary_with_secondaries/services/echo.c
@@ -14,30 +14,18 @@
  * limitations under the License.
  */
 
-#include <stdalign.h>
-#include <stdint.h>
-
-#include "hf/mm.h"
 #include "hf/std.h"
 
 #include "vmapi/hf/call.h"
 
-alignas(4096) uint8_t kstack[4096];
+#include "hftest.h"
 
-static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
-static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
-
-static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
-static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
-
-void kmain(void)
+TEST_SERVICE(echo)
 {
-	hf_vm_configure(send_page_addr, recv_page_addr);
-
 	/* Loop, echo messages back to the sender. */
 	for (;;) {
 		struct hf_mailbox_receive_return res = hf_mailbox_receive(true);
-		memcpy(send_page, recv_page, res.size);
+		memcpy(SERVICE_SEND_BUFFER(), SERVICE_RECV_BUFFER(), res.size);
 		hf_mailbox_clear();
 		hf_mailbox_send(res.vm_id, res.size);
 	}
diff --git a/test/vmapi/secondaries/interruptible.c b/test/vmapi/primary_with_secondaries/services/interruptible.c
similarity index 75%
rename from test/vmapi/secondaries/interruptible.c
rename to test/vmapi/primary_with_secondaries/services/interruptible.c
index 34ccd1d..e37689a 100644
--- a/test/vmapi/secondaries/interruptible.c
+++ b/test/vmapi/primary_with_secondaries/services/interruptible.c
@@ -14,35 +14,22 @@
  * limitations under the License.
  */
 
-#include <stdalign.h>
-#include <stdint.h>
-
 #include "hf/arch/vm/interrupts_gicv3.h"
 
 #include "hf/dlog.h"
-#include "hf/mm.h"
 #include "hf/std.h"
 
 #include "vmapi/hf/call.h"
 
 #include "../msr.h"
-#include "constants.h"
+#include "hftest.h"
+#include "primary_with_secondary.h"
 
 /*
  * Secondary VM that sends messages in response to interrupts, and interrupts
  * itself when it receives a message.
  */
 
-alignas(4096) uint8_t kstack[4096];
-
-static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
-static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
-
-static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
-static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
-
-static struct hf_mailbox_receive_return received_message;
-
 void irq_current(void)
 {
 	uint32_t interrupt_id = hf_get_and_acknowledge_interrupt();
@@ -51,7 +38,7 @@
 	dlog("IRQ %d from current\n", interrupt_id);
 	buffer[8] = '0' + interrupt_id / 10;
 	buffer[9] = '0' + interrupt_id % 10;
-	memcpy(send_page, buffer, size);
+	memcpy(SERVICE_SEND_BUFFER(), buffer, size);
 	hf_mailbox_send(HF_PRIMARY_VM_ID, size);
 }
 
@@ -71,9 +58,9 @@
 	return received;
 }
 
-void kmain(void)
+TEST_SERVICE(interruptible)
 {
-	hf_vm_configure(send_page_addr, recv_page_addr);
+	uint32_t this_vm_id = hf_vm_get_id();
 
 	exception_setup();
 	hf_enable_interrupt(SELF_INTERRUPT_ID, true);
@@ -81,20 +68,20 @@
 	hf_enable_interrupt(EXTERNAL_INTERRUPT_ID_B, true);
 	arch_irq_enable();
 
-	/* Loop, echo messages back to the sender. */
 	for (;;) {
 		const char ping_message[] = "Ping";
 		const char enable_message[] = "Enable interrupt C";
-		received_message = mailbox_receive_retry();
+		struct hf_mailbox_receive_return received_message =
+			mailbox_receive_retry();
 		if (received_message.vm_id == HF_PRIMARY_VM_ID &&
 		    received_message.size == sizeof(ping_message) &&
-		    memcmp(recv_page, ping_message, sizeof(ping_message)) ==
-			    0) {
+		    memcmp(SERVICE_RECV_BUFFER(), ping_message,
+			   sizeof(ping_message)) == 0) {
 			/* Interrupt ourselves */
-			hf_inject_interrupt(4, 0, SELF_INTERRUPT_ID);
+			hf_inject_interrupt(this_vm_id, 0, SELF_INTERRUPT_ID);
 		} else if (received_message.vm_id == HF_PRIMARY_VM_ID &&
 			   received_message.size == sizeof(enable_message) &&
-			   memcmp(recv_page, enable_message,
+			   memcmp(SERVICE_RECV_BUFFER(), enable_message,
 				  sizeof(enable_message)) == 0) {
 			/* Enable interrupt ID C. */
 			hf_enable_interrupt(EXTERNAL_INTERRUPT_ID_C, true);
diff --git a/test/vmapi/primary_with_secondaries/services/relay.c b/test/vmapi/primary_with_secondaries/services/relay.c
new file mode 100644
index 0000000..41cd77d
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/services/relay.c
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "hf/std.h"
+
+#include "vmapi/hf/call.h"
+
+#include "hftest.h"
+
+TEST_SERVICE(relay)
+{
+	/*
+	 * Loop, forward messages to the next VM.
+	 *
+	 * The first 32-bits of the message are the little-endian 32-bit ID of
+	 * the VM to forward the message to. This ID will be dropped from the
+	 * message so multiple IDs can be places at the start of the message.
+	 */
+	for (;;) {
+		uint32_t *chain;
+		uint32_t next_vm_id;
+		void *next_message;
+		uint32_t next_message_size;
+
+		/* Receive the message to relay. */
+		struct hf_mailbox_receive_return res = hf_mailbox_receive(true);
+		ASSERT_GE(res.size, sizeof(uint32_t));
+
+		/* Prepare to relay the message. */
+		chain = SERVICE_RECV_BUFFER();
+		next_vm_id = le32toh(*chain);
+		next_message = chain + 1;
+		next_message_size = res.size - sizeof(uint32_t);
+
+		/* Send the message to the next stage. */
+		memcpy(SERVICE_SEND_BUFFER(), next_message, next_message_size);
+		hf_mailbox_clear();
+		hf_mailbox_send(next_vm_id, next_message_size);
+	}
+}
diff --git a/test/vmapi/secondaries/relay_b.c b/test/vmapi/primary_with_secondaries/util.c
similarity index 68%
rename from test/vmapi/secondaries/relay_b.c
rename to test/vmapi/primary_with_secondaries/util.c
index bad509c..7a60ddf 100644
--- a/test/vmapi/secondaries/relay_b.c
+++ b/test/vmapi/primary_with_secondaries/util.c
@@ -14,31 +14,26 @@
  * limitations under the License.
  */
 
-#include <stdalign.h>
-#include <stdint.h>
-
 #include "hf/mm.h"
-#include "hf/std.h"
 
 #include "vmapi/hf/call.h"
 
-alignas(4096) uint8_t kstack[4096];
+#include "hftest.h"
+#include "primary_with_secondary.h"
 
 static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
 static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
+static_assert(sizeof(send_page) == PAGE_SIZE, "Send page is not a page.");
+static_assert(sizeof(recv_page) == PAGE_SIZE, "Recv page is not a page.");
 
 static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
 static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
 
-void kmain(void)
+struct mailbox_buffers set_up_mailbox(void)
 {
-	hf_vm_configure(send_page_addr, recv_page_addr);
-
-	/* Loop, forward messages to the primary. */
-	for (;;) {
-		struct hf_mailbox_receive_return res = hf_mailbox_receive(true);
-		memcpy(send_page, recv_page, res.size);
-		hf_mailbox_clear();
-		hf_mailbox_send(HF_PRIMARY_VM_ID, res.size);
-	}
+	ASSERT_EQ(hf_vm_configure(send_page_addr, recv_page_addr), 0);
+	return (struct mailbox_buffers){
+		.send = send_page,
+		.recv = recv_page,
+	};
 }
diff --git a/test/vmapi/primary_with_secondaries/with_services.c b/test/vmapi/primary_with_secondaries/with_services.c
new file mode 100644
index 0000000..384597d
--- /dev/null
+++ b/test/vmapi/primary_with_secondaries/with_services.c
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdalign.h>
+#include <stdint.h>
+
+#include "hf/std.h"
+
+#include "vmapi/hf/call.h"
+
+#include "hftest.h"
+#include "primary_with_secondary.h"
+
+/**
+ * Reverses the order of the elements in the given array.
+ */
+void reverse(char *s, size_t len)
+{
+	size_t i;
+
+	for (i = 0; i < len / 2; i++) {
+		char t = s[i];
+		s[i] = s[len - 1 - i];
+		s[len - 1 - i] = t;
+	}
+}
+
+/**
+ * Finds the next lexicographic permutation of the given array, if there is one.
+ */
+void next_permutation(char *s, size_t len)
+{
+	size_t i, j;
+
+	for (i = len - 2; i < len; i--) {
+		const char t = s[i];
+		if (t >= s[i + 1]) {
+			continue;
+		}
+
+		for (j = len - 1; t >= s[j]; j--) {
+		}
+
+		s[i] = s[j];
+		s[j] = t;
+		reverse(s + i + 1, len - i - 1);
+		return;
+	}
+}
+
+/**
+ * Send and receive the same message from the echo VM.
+ */
+TEST(mailbox, echo)
+{
+	const char message[] = "Echo this back to me!";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "echo", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+	/* Set the message, echo it and check it didn't change. */
+	memcpy(mb.send, message, sizeof(message));
+	EXPECT_EQ(hf_mailbox_send(SERVICE_VM0, sizeof(message)), 0);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(message));
+	EXPECT_EQ(memcmp(mb.recv, message, sizeof(message)), 0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+}
+
+/**
+ * Repeatedly send a message and receive it back from the echo VM.
+ */
+TEST(mailbox, repeated_echo)
+{
+	char message[] = "Echo this back to me!";
+	struct hf_vcpu_run_return run_res;
+	uint8_t i;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "echo", mb.send);
+
+	for (i = 0; i < 100; i++) {
+		/* Run secondary until it reaches the wait for messages. */
+		run_res = hf_vcpu_run(SERVICE_VM0, 0);
+		EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+		/* Set the message, echo it and check it didn't change. */
+		next_permutation(message, sizeof(message) - 1);
+		memcpy(mb.send, message, sizeof(message));
+		EXPECT_EQ(hf_mailbox_send(SERVICE_VM0, sizeof(message)), 0);
+		run_res = hf_vcpu_run(SERVICE_VM0, 0);
+		EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+		EXPECT_EQ(run_res.message.size, sizeof(message));
+		EXPECT_EQ(memcmp(mb.recv, message, sizeof(message)), 0);
+		EXPECT_EQ(hf_mailbox_clear(), 0);
+	}
+}
+
+/**
+ * Send a message to relay_a which will forward it to relay_b where it will be
+ * sent back here.
+ */
+TEST(mailbox, relay)
+{
+	const char message[] = "Send this round the relay!";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "relay", mb.send);
+	SERVICE_SELECT(SERVICE_VM1, "relay", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+	run_res = hf_vcpu_run(SERVICE_VM1, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+	/*
+	 * Build the message chain so the message is sent from here to
+	 * SERVICE_VM0, then to SERVICE_VM1 and finally back to here.
+	 */
+	{
+		uint32_t *chain = mb.send;
+		*chain++ = htole32(SERVICE_VM1);
+		*chain++ = htole32(HF_PRIMARY_VM_ID);
+		memcpy(chain, message, sizeof(message));
+		EXPECT_EQ(hf_mailbox_send(
+				  SERVICE_VM0,
+				  sizeof(message) + (2 * sizeof(uint32_t))),
+			  0);
+	}
+
+	/* Let SERVICE_VM0 forward the message. */
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAKE_UP);
+	EXPECT_EQ(run_res.wake_up.vm_id, SERVICE_VM1);
+	EXPECT_EQ(run_res.wake_up.vcpu, 0);
+
+	/* Let SERVICE_VM1 forward the message. */
+	run_res = hf_vcpu_run(SERVICE_VM1, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+
+	/* Ensure the message is in tact. */
+	EXPECT_EQ(run_res.message.size, sizeof(message));
+	EXPECT_EQ(memcmp(mb.recv, message, sizeof(message)), 0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+}
+
+/**
+ * Send a message to the interruptible VM, which will interrupt itself to send a
+ * response back.
+ */
+TEST(interrupts, interrupt_self)
+{
+	const char message[] = "Ping";
+	const char expected_response[] = "Got IRQ 05.";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "interruptible", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+	/* Set the message, echo it and wait for a response. */
+	memcpy(mb.send, message, sizeof(message));
+	EXPECT_EQ(hf_mailbox_send(SERVICE_VM0, sizeof(message)), 0);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
+	EXPECT_EQ(memcmp(mb.recv, expected_response, sizeof(expected_response)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+}
+
+/**
+ * Inject an interrupt to the interrupt VM, which will send a message back.
+ * Repeat this twice to make sure it doesn't get into a bad state after the
+ * first one.
+ */
+TEST(interrupts, inject_interrupt_twice)
+{
+	const char expected_response[] = "Got IRQ 07.";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "interruptible", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+	/* Inject the interrupt and wait for a message. */
+	hf_inject_interrupt(SERVICE_VM0, 0, EXTERNAL_INTERRUPT_ID_A);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
+	EXPECT_EQ(memcmp(mb.recv, expected_response, sizeof(expected_response)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+
+	/* Inject the interrupt again, and wait for the same message. */
+	hf_inject_interrupt(SERVICE_VM0, 0, EXTERNAL_INTERRUPT_ID_A);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
+	EXPECT_EQ(memcmp(mb.recv, expected_response, sizeof(expected_response)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+}
+
+/**
+ * Inject two different interrupts to the interrupt VM, which will send a
+ * message back each time.
+ */
+TEST(interrupts, inject_two_interrupts)
+{
+	const char expected_response[] = "Got IRQ 07.";
+	const char expected_response_2[] = "Got IRQ 08.";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "interruptible", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+	/* Inject the interrupt and wait for a message. */
+	hf_inject_interrupt(SERVICE_VM0, 0, EXTERNAL_INTERRUPT_ID_A);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
+	EXPECT_EQ(memcmp(mb.recv, expected_response, sizeof(expected_response)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+
+	/* Inject a different interrupt and wait for a different message. */
+	hf_inject_interrupt(SERVICE_VM0, 0, EXTERNAL_INTERRUPT_ID_B);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response_2));
+	EXPECT_EQ(memcmp(mb.recv, expected_response_2,
+			 sizeof(expected_response_2)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+}
+
+/**
+ * Inject an interrupt then send a message to the interrupt VM, which will send
+ * a message back each time. This is to test that interrupt injection doesn't
+ * interfere with message reception.
+ */
+TEST(interrupts, inject_interrupt_message)
+{
+	const char expected_response[] = "Got IRQ 07.";
+	const char message[] = "Ping";
+	const char expected_response_2[] = "Got IRQ 05.";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "interruptible", mb.send);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+	/* Inject the interrupt and wait for a message. */
+	hf_inject_interrupt(SERVICE_VM0, 0, EXTERNAL_INTERRUPT_ID_A);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
+	EXPECT_EQ(memcmp(mb.recv, expected_response, sizeof(expected_response)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+
+	/* Now send a message to the secondary. */
+	memcpy(mb.send, message, sizeof(message));
+	EXPECT_EQ(hf_mailbox_send(SERVICE_VM0, sizeof(message)), 0);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response_2));
+	EXPECT_EQ(memcmp(mb.recv, expected_response_2,
+			 sizeof(expected_response_2)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+}
+
+/**
+ * Inject an interrupt which the target VM has not enabled, and then send a
+ * message telling it to enable that interrupt ID. It should then (and only
+ * then) send a message back.
+ */
+TEST(interrupts, inject_interrupt_disabled)
+{
+	const char expected_response[] = "Got IRQ 09.";
+	const char message[] = "Enable interrupt C";
+	struct hf_vcpu_run_return run_res;
+	struct mailbox_buffers mb = set_up_mailbox();
+
+	SERVICE_SELECT(SERVICE_VM0, "interruptible", mb.send);
+
+	/* Inject the interrupt and expect not to get a message. */
+	hf_inject_interrupt(SERVICE_VM0, 0, EXTERNAL_INTERRUPT_ID_C);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_WAIT_FOR_INTERRUPT);
+	EXPECT_EQ(hf_mailbox_clear(), -1);
+
+	/*
+	 * Now send a message to the secondary to enable the interrupt ID, and
+	 * expect the response from the interrupt we sent before.
+	 */
+	memcpy(mb.send, message, sizeof(message));
+	EXPECT_EQ(hf_mailbox_send(SERVICE_VM0, sizeof(message)), 0);
+	run_res = hf_vcpu_run(SERVICE_VM0, 0);
+	EXPECT_EQ(run_res.code, HF_VCPU_RUN_MESSAGE);
+	EXPECT_EQ(run_res.message.size, sizeof(expected_response));
+	EXPECT_EQ(memcmp(mb.recv, expected_response, sizeof(expected_response)),
+		  0);
+	EXPECT_EQ(hf_mailbox_clear(), 0);
+}
diff --git a/test/vmapi/secondaries/BUILD.gn b/test/vmapi/secondaries/BUILD.gn
deleted file mode 100644
index 43fb4cc..0000000
--- a/test/vmapi/secondaries/BUILD.gn
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright 2018 Google LLC
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import("//build/image/image.gni")
-
-vm_kernel("echo") {
-  testonly = true
-  public_configs = [ "//test/vmapi:config" ]
-
-  sources = [
-    "echo.c",
-  ]
-
-  deps = [
-    "//test/hftest:hftest_secondary_vm",
-  ]
-}
-
-vm_kernel("relay_a") {
-  testonly = true
-  public_configs = [ "//test/vmapi:config" ]
-
-  sources = [
-    "relay_a.c",
-  ]
-
-  deps = [
-    "//test/hftest:hftest_secondary_vm",
-  ]
-}
-
-vm_kernel("relay_b") {
-  testonly = true
-  public_configs = [ "//test/vmapi:config" ]
-
-  sources = [
-    "relay_b.c",
-  ]
-
-  deps = [
-    "//test/hftest:hftest_secondary_vm",
-  ]
-}
-
-vm_kernel("interruptible") {
-  testonly = true
-  public_configs = [ "//test/vmapi:config" ]
-
-  sources = [
-    "interruptible.c",
-  ]
-
-  deps = [
-    "//src/arch/aarch64/hftest:interrupts_gicv3",
-    "//test/hftest:hftest_secondary_vm",
-  ]
-}
-
-vm_kernel("check_state") {
-  testonly = true
-
-  sources = [
-    "check_state.c",
-  ]
-
-  deps = [
-    "//src/arch/aarch64/hftest:state",
-    "//test/hftest:hftest_secondary_vm",
-  ]
-}
diff --git a/test/vmapi/secondaries/relay_a.c b/test/vmapi/secondaries/relay_a.c
deleted file mode 100644
index 5b515b6..0000000
--- a/test/vmapi/secondaries/relay_a.c
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2018 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <stdalign.h>
-#include <stdint.h>
-
-#include "hf/mm.h"
-#include "hf/std.h"
-
-#include "vmapi/hf/call.h"
-
-#define FORWARD_VM_ID 2
-
-alignas(4096) uint8_t kstack[4096];
-
-static alignas(PAGE_SIZE) uint8_t send_page[PAGE_SIZE];
-static alignas(PAGE_SIZE) uint8_t recv_page[PAGE_SIZE];
-
-static hf_ipaddr_t send_page_addr = (hf_ipaddr_t)send_page;
-static hf_ipaddr_t recv_page_addr = (hf_ipaddr_t)recv_page;
-
-void kmain(void)
-{
-	hf_vm_configure(send_page_addr, recv_page_addr);
-
-	/* Loop, forward messages to the next VM. */
-	for (;;) {
-		struct hf_mailbox_receive_return res = hf_mailbox_receive(true);
-		memcpy(send_page, recv_page, res.size);
-		hf_mailbox_clear();
-		hf_mailbox_send(FORWARD_VM_ID, res.size);
-	}
-}