CTest support for QEMU/Renode.

* Example usage:
    mkdir build
    cd build
    cmake -GNinja ../
    cmake --build . --target AllTests
    ctest --verbose # Optional, run with ctest
* Example 2
    m test_springbok

Change-Id: I0eb8a162a3271af8e26aea1bacf1069a6566d6aa
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 00c9c8d..42a67bf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,4 +36,16 @@
 
 add_subdirectory(pw_unit_test_demo)
 
-add_subdirectory(tests)
\ No newline at end of file
+enable_testing()
+
+add_subdirectory(tests)
+
+add_custom_target(AllTests
+  COMMAND
+    "${CMAKE_CTEST_COMMAND}"
+      --verbose
+      --build-and-test "${CMAKE_CURRENT_SOURCE_DIR}"
+        "${CMAKE_CURRENT_BINARY_DIR}"
+      --build-generator "${CMAKE_GENERATOR}"
+      --test-command "${CMAKE_CTEST_COMMAND}"
+)
diff --git a/cmake/vec_cc_test.cmake b/cmake/vec_cc_test.cmake
index 332a085..8857373 100644
--- a/cmake/vec_cc_test.cmake
+++ b/cmake/vec_cc_test.cmake
@@ -3,7 +3,7 @@
     _RULE
     ""
     "NAME"
-    "SRCS;COPTS;DEFINES;LINKOPTS;DATA;DEPS;LABELS"
+    "SRCS;COPTS;DEFINES;LINKOPTS;DATA;DEPS;LABELS;TIMEOUT"
     ${ARGN}
   )
 
@@ -26,4 +26,17 @@
     ${_RULE_LINKOPTS}
 )
 
+if(NOT DEFINED ${_RULE_TIMEOUT})
+  set(_RULE_TIMEOUT 20)
+endif()
+
+find_program(QEMU_RV32 qemu-system-riscv32 HINTS $ENV{OUT}/host/qemu REQUIRED)
+find_program(RENODE_EXE Renode.exe HINTS $ENV{OUT}/host/renode REQUIRED)
+add_test(NAME "qemu_${_RULE_NAME}"
+         COMMAND test_runner.py qemu $<TARGET_FILE:${_RULE_NAME}.elf> --qemu-path ${QEMU_RV32})
+add_test(NAME "renode_${_RULE_NAME}"
+         COMMAND test_runner.py renode $<TARGET_FILE:${_RULE_NAME}.elf> --renode-path ${RENODE_EXE})
+set_tests_properties("renode_${_RULE_NAME}" PROPERTIES TIMEOUT ${_RULE_TIMEOUT})
+set_tests_properties("qemu_${_RULE_NAME}" PROPERTIES TIMEOUT ${_RULE_TIMEOUT})
+
 endfunction()
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index fe84b42..b46da54 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -2,6 +2,9 @@
 
 enable_language(ASM)
 
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/test_runner.py
+     DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
+
 add_library(test_v_helpers
 	test_v_helpers.cpp)
 
@@ -33,4 +36,5 @@
   LINKOPTS
    -T${LINKER_SCRIPT}
    -Xlinker --defsym=__itcm_length__=256K
+  TIMEOUT 30
 )
diff --git a/tests/test_runner.py b/tests/test_runner.py
new file mode 100755
index 0000000..9a3a257
--- /dev/null
+++ b/tests/test_runner.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+"""Runs test within Qemu and Renode simulators."""
+import argparse
+import os
+import sys
+import tempfile
+
+import io
+import pexpect
+
+
+parser = argparse.ArgumentParser(
+    description="Run a springbok test on an simulator.")
+
+parser.add_argument('simulator',
+                    help='Select a simulator',
+                    choices=['renode', 'qemu'])
+parser.add_argument('elf',
+                    help='Elf to execute on a simulator')
+parser.add_argument('--renode-path',
+                    help="Path to renode simulator")
+parser.add_argument('--qemu-path',
+                    help="Path to qemu simulator")
+
+
+class Simulation: # pylint: disable=too-few-public-methods
+    """ Base class for simulation """
+    def __init__(self, simulator_cmd):
+        self.simulator_cmd = simulator_cmd
+        self.buffer = io.StringIO()
+        self.child = None
+
+    def run(self, timeout=120):
+        """ Run the simulation command and quit the simulation."""
+        self.child = pexpect.spawn(self.simulator_cmd, encoding='utf-8')
+        self.child.logfile = self.buffer
+        self.child.expect("main returned", timeout=timeout)
+        self.child.send("\nq\n")
+        self.child.expect(pexpect.EOF, timeout=timeout)
+        self.child.close()
+        self.buffer.seek(0)
+        return self.buffer.read()
+
+class QemuSimulation(Simulation): # pylint: disable=too-few-public-methods
+    """ Qemu simulation """
+    def __init__(self, path, elf):
+        self.qemu_simulator_cmd = (
+            "%(sim)s -M springbok -nographic -d springbok -device loader,file=%(elf)s")
+        self.sim_params = {"sim": path, "elf": elf}
+        super().__init__(self.qemu_simulator_cmd % self.sim_params)
+
+
+class RenodeSimulation(Simulation): # pylint: disable=too-few-public-methods
+    """ Renode Simulation """
+    def __init__(self, path, elf):
+        # TODO(henryherman): Look at using repl platform file.
+        renode_script = """
+mach create "springbok"
+machine LoadPlatformDescriptionFromString "cpu: CPU.SpringbokRiscV32 @ sysbus"
+machine LoadPlatformDescriptionFromString "ram_vec_imem: Memory.MappedMemory @ sysbus 0x30000000 {size: 0x00040000}"
+machine LoadPlatformDescriptionFromString "ram_vec_dmem: Memory.MappedMemory @ sysbus 0x34000000 {size: 0x00400000}"
+sysbus LoadELF @%s
+sysbus.cpu PC 0x30000000
+sysbus.cpu IsHalted False
+start"""
+        self.renode_script = renode_script % elf
+
+        self.renode_args = [
+            "mono",
+            "%s" % path,
+            '--disable-xwt',
+            ' --console',
+            '--plain',
+        ]
+        self.renode_simulator_cmd = " ".join(self.renode_args)
+        super().__init__(self.renode_simulator_cmd)
+
+    def run(self, timeout=120):
+        file_desc, script_path = tempfile.mkstemp(suffix=".resc")
+        try:
+            with os.fdopen(file_desc, 'w') as tmp:
+                tmp.write(self.renode_script)
+                tmp.flush()
+            self.simulator_cmd += " %s" % script_path
+            test_output = super().run()
+        finally:
+            os.remove(script_path)
+        return test_output
+
+
+Simulators = {
+    "qemu": QemuSimulation,
+    "renode": RenodeSimulation
+}
+
+args = parser.parse_args()
+
+simulators_paths = {
+    "renode": args.renode_path,
+    "qemu": args.qemu_path
+}
+
+
+def main():
+    """ Run a test and check for Pass or Fail """
+    simulator_path = simulators_paths[args.simulator]
+    if simulator_path is None:
+        parser.error(
+            "Must provide path to simulator %s, use argument --%s-path" % (args.simulator,
+                                                                           args.simulator))
+
+    simulator_class = Simulators[args.simulator]
+    simulator = simulator_class(simulator_path, args.elf)
+    output = simulator.run()
+    print(output)
+    if "FAILED" in output:
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()