Support multiple ways of driving hftests, add UART

Move logic that gets the boot arguments of hftest to a separate build
target and implement the same interface for communicating over UART.

Add SerialDriver to hftest.py that drives the communication with a
device over serial port.

The UART backend for hftest depends on an implementation of
'hftest_device' interface to reboot the board when the test has
finished.

Change-Id: Idfcb2c6e9af9cd283e360993fa2580d4fea49594
diff --git a/build/docker/Dockerfile b/build/docker/Dockerfile
index 1eb6b2d..1d4a3d5 100644
--- a/build/docker/Dockerfile
+++ b/build/docker/Dockerfile
@@ -38,5 +38,7 @@
 		python \
 		python-git                     `# for Linux checkpatch` \
 		python-ply                     `# for Linux checkpatch` \
+		python3                        `# for all build scripts` \
+		python3-serial                 `# for hftest.py` \
 		strace                         `# for strace_open.sh` \
 	&& rm -rf /var/lib/apt/lists/*
diff --git a/inc/hf/plat/console.h b/inc/hf/plat/console.h
index 8fb4969..0584fbd 100644
--- a/inc/hf/plat/console.h
+++ b/inc/hf/plat/console.h
@@ -26,5 +26,8 @@
 void plat_console_mm_init(struct mm_stage1_locked stage1_locked,
 			  struct mpool *ppool);
 
-/** Puts a single character on the console. */
+/** Puts a single character on the console. This is a blocking call. */
 void plat_console_putchar(char c);
+
+/** Gets a single character from the console. This is a blocking call. */
+char plat_console_getchar(void);
diff --git a/test/hftest/BUILD.gn b/test/hftest/BUILD.gn
index d65397b..db97cef 100644
--- a/test/hftest/BUILD.gn
+++ b/test/hftest/BUILD.gn
@@ -14,6 +14,11 @@
 
 import("//build/toolchain/platform.gni")
 
+declare_args() {
+  hftest_ctrl = ":ctrl_fdt"
+  hftest_device = ""
+}
+
 config("hftest_config") {
   include_dirs = [ "//test/inc" ]
 }
@@ -96,12 +101,12 @@
     ":mm",
     ":power_mgmt",
     "//src:dlog",
-    "//src:fdt",
     "//src:memiter",
     "//src/arch/${plat_arch}:entry",
     "//src/arch/${plat_arch}/hftest:entry",
     "//src/arch/${plat_arch}/hftest:interrupts",
     "//src/arch/${plat_arch}/hftest:power_mgmt",
+    hftest_ctrl,
   ]
 }
 
@@ -153,3 +158,39 @@
     "//src/arch/${plat_arch}/hftest:power_mgmt",
   ]
 }
+
+source_set("ctrl_fdt") {
+  testonly = true
+
+  public_configs = [ ":hftest_config" ]
+
+  sources = [
+    "ctrl_fdt.c",
+  ]
+
+  deps = [
+    "//src:dlog",
+    "//src:fdt",
+    "//src:memiter",
+  ]
+}
+
+source_set("ctrl_uart") {
+  testonly = true
+
+  public_configs = [ ":hftest_config" ]
+
+  sources = [
+    "ctrl_uart.c",
+  ]
+
+  deps = [
+    "//src:dlog",
+    "//src:memiter",
+    plat_console,
+  ]
+
+  if (hftest_device != "") {
+    deps += [ hftest_device ]
+  }
+}
diff --git a/test/hftest/ctrl_fdt.c b/test/hftest/ctrl_fdt.c
new file mode 100644
index 0000000..85fe1cb
--- /dev/null
+++ b/test/hftest/ctrl_fdt.c
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 The Hafnium Authors.
+ *
+ * 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 "test/hftest.h"
+
+bool hftest_ctrl_start(const struct fdt_header *fdt, struct memiter *cmd)
+{
+	struct fdt_node n;
+	const char *bootargs;
+	uint32_t bootargs_size;
+
+	if (!fdt_root_node(&n, fdt)) {
+		HFTEST_LOG("FDT failed validation.");
+		return false;
+	}
+
+	if (!fdt_find_child(&n, "")) {
+		HFTEST_LOG("Unable to find root node in FDT.");
+		return false;
+	}
+
+	if (!fdt_find_child(&n, "chosen")) {
+		HFTEST_LOG("Unable to find 'chosen' node in FDT.");
+		return false;
+	}
+
+	if (!fdt_read_property(&n, "bootargs", &bootargs, &bootargs_size)) {
+		HFTEST_LOG("Unable to read bootargs.");
+		return false;
+	}
+
+	/* Remove null terminator. */
+	memiter_init(cmd, bootargs, bootargs_size - 1);
+	return true;
+}
+
+void hftest_ctrl_finish(void)
+{
+	/* Nothing to do. */
+}
diff --git a/test/hftest/ctrl_uart.c b/test/hftest/ctrl_uart.c
new file mode 100644
index 0000000..da4d0d6
--- /dev/null
+++ b/test/hftest/ctrl_uart.c
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Hafnium Authors.
+ *
+ * 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/plat/console.h"
+
+#include "test/hftest.h"
+
+/* clang-format off */
+#define CMD_GET_COMMAND_LINE	"[hftest_ctrl:get_command_line]\n"
+#define CMD_FINISHED		"[hftest_ctrl:finished]\n"
+/* clang-format on */
+
+static char command_line[128];
+
+static void write(const char *str)
+{
+	while (*str != '\0') {
+		plat_console_putchar(*str);
+		str++;
+	}
+}
+
+static bool read(char *buf, size_t max_len, struct memiter *str)
+{
+	char c;
+	size_t len = 0;
+
+	while (true) {
+		c = plat_console_getchar();
+		if (c == '\r' || c == '\n') {
+			memiter_init(str, buf, len);
+			return true;
+		}
+
+		if (len < max_len) {
+			buf[len++] = c;
+		} else {
+			return false;
+		}
+	}
+}
+
+bool hftest_ctrl_start(const struct fdt_header *fdt, struct memiter *cmd)
+{
+	(void)fdt;
+
+	/* Initialize the console */
+	plat_console_init();
+
+	/* Inform the host that we are ready to receive the command line. */
+	write(CMD_GET_COMMAND_LINE);
+
+	/* Read command line from the console. */
+	read(command_line, ARRAY_SIZE(command_line), cmd);
+
+	return true;
+}
+
+void hftest_ctrl_finish(void)
+{
+	/*
+	 * Inform the host that this test has finished running and all
+	 * subsequent logs belong to the next run.
+	 */
+	write(CMD_FINISHED);
+
+	/* Reboot the device. */
+	hftest_device_reboot();
+}
diff --git a/test/hftest/hftest.py b/test/hftest/hftest.py
index cd60585..91c2782 100755
--- a/test/hftest/hftest.py
+++ b/test/hftest/hftest.py
@@ -28,6 +28,7 @@
 import json
 import os
 import re
+import serial
 import subprocess
 import sys
 
@@ -35,6 +36,9 @@
 HFTEST_LOG_FAILURE_PREFIX = "Failure:"
 HFTEST_LOG_FINISHED = "FINISHED"
 
+HFTEST_CTRL_GET_COMMAND_LINE = "[hftest_ctrl:get_command_line]"
+HFTEST_CTRL_FINISHED = "[hftest_ctrl:finished]"
+
 HF_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
     os.path.abspath(__file__))))
 DTC_SCRIPT = os.path.join(HF_ROOT, "build", "image", "dtc.py")
@@ -116,7 +120,9 @@
         "kernel",
         "initrd",
         "vm_args",
-        "cpu"
+        "cpu",
+        "serial_dev",
+        "serial_baudrate",
     ])
 
 
@@ -337,6 +343,42 @@
         return self.finish_run(run_state)
 
 
+class SerialDriver(Driver):
+    """Driver which communicates with a device over the serial port."""
+
+    def __init__(self, args):
+        Driver.__init__(self, args)
+        self.tty_file = self.args.serial_dev
+        self.baudrate = self.args.serial_baudrate
+        input("Press ENTER and then reset the device...")
+
+    def run(self, run_name, test_args, is_long_running):
+        """Communicate `test_args` to the device over the serial port."""
+        run_state = self.start_run(run_name)
+
+        with serial.Serial(self.tty_file, self.baudrate, timeout=10) as ser:
+            with open(run_state.log_path, "a") as f:
+                while True:
+                    # Read one line from the serial port.
+                    line = ser.readline().decode('utf-8')
+                    if len(line) == 0:
+                        # Timeout
+                        run_state.set_ret_code(124)
+                        input("Timeout. " +
+                            "Press ENTER and then reset the device...")
+                        break
+                    # Write the line to the log file.
+                    f.write(line)
+                    if HFTEST_CTRL_GET_COMMAND_LINE in line:
+                        # Device is waiting for `test_args`.
+                        ser.write(test_args.encode('ascii'))
+                        ser.write(b'\r')
+                    elif HFTEST_CTRL_FINISHED in line:
+                        # Device has finished running this test and will reboot.
+                        break
+        return self.finish_run(run_state)
+
+
 # Tuple used to return information about the results of running a set of tests.
 TestRunnerResult = collections.namedtuple("TestRunnerResult", [
         "tests_run",
@@ -503,7 +545,9 @@
     parser.add_argument("--suite")
     parser.add_argument("--test")
     parser.add_argument("--vm_args")
-    parser.add_argument("--fvp", action="store_true")
+    parser.add_argument("--driver", default="qemu")
+    parser.add_argument("--serial-dev", default="/dev/ttyUSB0")
+    parser.add_argument("--serial-baudrate", type=int, default=115200)
     parser.add_argument("--skip-long-running-tests", action="store_true")
     parser.add_argument("--cpu",
         help="Selects the CPU configuration for the run environment.")
@@ -523,12 +567,17 @@
     artifacts = ArtifactsManager(os.path.join(args.log, image_name))
 
     # Create a driver for the platform we want to test on.
-    driver_args = DriverArgs(artifacts, image, initrd, vm_args,
-        args.cpu)
-    if args.fvp:
-        driver = FvpDriver(driver_args)
-    else:
+    driver_args = DriverArgs(artifacts, image, initrd, vm_args, args.cpu,
+            args.serial_dev, args.serial_baudrate)
+
+    if args.driver == "qemu":
         driver = QemuDriver(driver_args)
+    elif args.driver == "fvp":
+        driver = FvpDriver(driver_args)
+    elif args.driver == "serial":
+        driver = SerialDriver(driver_args)
+    else:
+        raise Exception("Unknown driver name: {}".format(args.driver))
 
     # Create class which will drive test execution.
     runner = TestRunner(artifacts, driver, image_name, args.suite, args.test,
diff --git a/test/hftest/standalone_main.c b/test/hftest/standalone_main.c
index 586b3b3..91b53b5 100644
--- a/test/hftest/standalone_main.c
+++ b/test/hftest/standalone_main.c
@@ -19,8 +19,6 @@
 
 #include "hf/arch/vm/interrupts.h"
 
-#include "hf/fdt.h"
-#include "hf/memiter.h"
 #include "hf/mm.h"
 
 #include "hftest_common.h"
@@ -33,10 +31,7 @@
 
 void kmain(const struct fdt_header *fdt)
 {
-	struct fdt_node n;
-	const char *bootargs;
-	uint32_t bootargs_size;
-	struct memiter bootargs_iter;
+	struct memiter command_line;
 	struct memiter command;
 
 	/*
@@ -44,7 +39,7 @@
 	 */
 	if ((VM_TOOLCHAIN == 1) && !hftest_mm_init()) {
 		HFTEST_LOG("Memory initialization failed.");
-		return;
+		goto out;
 	}
 
 	/*
@@ -55,55 +50,40 @@
 
 	hftest_use_list(hftest_begin, hftest_end - hftest_begin);
 
-	if (!fdt_root_node(&n, fdt)) {
-		HFTEST_LOG("FDT failed validation.");
-		return;
+	if (!hftest_ctrl_start(fdt, &command_line)) {
+		HFTEST_LOG("Unable to read the command line.");
+		goto out;
 	}
 
-	if (!fdt_find_child(&n, "")) {
-		HFTEST_LOG("Unable to find root node in FDT.");
-		return;
-	}
-
-	if (!fdt_find_child(&n, "chosen")) {
-		HFTEST_LOG("Unable to find 'chosen' node in FDT.");
-		return;
-	}
-
-	if (!fdt_read_property(&n, "bootargs", &bootargs, &bootargs_size)) {
-		HFTEST_LOG("Unable to read bootargs.");
-		return;
-	}
-
-	/* Remove null terminator. */
-	memiter_init(&bootargs_iter, bootargs, bootargs_size - 1);
-
-	if (!memiter_parse_str(&bootargs_iter, &command)) {
+	if (!memiter_parse_str(&command_line, &command)) {
 		HFTEST_LOG("Unable to parse command.");
-		return;
+		goto out;
 	}
 
 	if (memiter_iseq(&command, "json")) {
 		hftest_json();
-		return;
+		goto out;
 	}
 
 	if (memiter_iseq(&command, "run")) {
 		struct memiter suite_name;
 		struct memiter test_name;
 
-		if (!memiter_parse_str(&bootargs_iter, &suite_name)) {
+		if (!memiter_parse_str(&command_line, &suite_name)) {
 			HFTEST_LOG("Unable to parse test suite.");
-			return;
+			goto out;
 		}
 
-		if (!memiter_parse_str(&bootargs_iter, &test_name)) {
+		if (!memiter_parse_str(&command_line, &test_name)) {
 			HFTEST_LOG("Unable to parse test.");
-			return;
+			goto out;
 		}
 		hftest_run(suite_name, test_name, fdt);
-		return;
+		goto out;
 	}
 
 	hftest_help();
+
+out:
+	hftest_ctrl_finish();
 }
diff --git a/test/inc/test/hftest.h b/test/inc/test/hftest.h
index bcd22d8..1dd65aa 100644
--- a/test/inc/test/hftest.h
+++ b/test/inc/test/hftest.h
@@ -19,8 +19,11 @@
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdint.h>
+#include <stdnoreturn.h>
 
 #include "hf/dlog.h"
+#include "hf/fdt.h"
+#include "hf/memiter.h"
 
 /*
  * Define a set up function to be run before every test in a test suite.
@@ -104,6 +107,18 @@
 void hftest_mm_vcpu_init(void);
 
 /**
+ * Inform a host that this is the start of a test run and obtain the command
+ * line arguments for it.
+ */
+bool hftest_ctrl_start(const struct fdt_header *fdt, struct memiter *cmd);
+
+/** Inform a host that this test run has finished and clean up. */
+void hftest_ctrl_finish(void);
+
+/** Reboot the device. */
+noreturn void hftest_device_reboot(void);
+
+/**
  * Starts the CPU with the given ID. It will start at the provided entry point
  * with the provided argument. It is a wrapper around the generic cpu_start()
  * and takes care of MMU initialization.