[test] Add functional verilator tests.
|functional_verilator_test.py| takes a flag pointing to an executable,
which is then executed under a Verilator build. The test listens
over UART, and passes or fails the test depending on whether it prints
PASS! or FAIL!; see the file comment for details.
diff --git a/test/systemtest/conftest.py b/test/systemtest/conftest.py
index b2504ba..19a3afa 100644
--- a/test/systemtest/conftest.py
+++ b/test/systemtest/conftest.py
@@ -19,6 +19,7 @@
parser.addoption("--rom_bin", action="store", default="")
parser.addoption("--verilator_model", action="store", default="")
parser.addoption("--openocd", action="store", default="openocd")
+ parser.addoption("--uart_timeout", action="store", default="60")
@pytest.hookimpl(tryfirst=True)
@@ -93,3 +94,8 @@
def openocd(pytestconfig):
"""Return path to OpenOCD executable."""
return pytestconfig.getoption('openocd')
+
+@pytest.fixture(scope="session")
+def uart_timeout(pytestconfig):
+ """Return the timeout in seconds for UART to print PASS."""
+ return int(pytestconfig.getoption('uart_timeout'))
diff --git a/test/systemtest/functional_verilator_test.py b/test/systemtest/functional_verilator_test.py
new file mode 100644
index 0000000..baddfe9
--- /dev/null
+++ b/test/systemtest/functional_verilator_test.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Runs a binary on the verilated system, which is expected to write
+one of "PASS!\r\n" or "FAIL!\r\n" to UART to determine success or failure.
+Failing to write either will result in a timeout.
+
+This test requires some configuration options. Use the following steps to
+run the test manually after building Verilator and the sw/boot_rom and
+sw/examples/hello_world targets.
+
+$ cd ${REPO_TOP}
+$ pytest -s -v test/systemtest/functional_verilator_test.py \
+ --test_bin sw/tests/hmac/sw.vmem \
+ --rom_bin sw/boot_rom/rom.vmem \
+ --verilator_model build/lowrisc_systems_top_earlgrey_verilator_0.1/sim-verilator/Vtop_earlgrey_verilator
+"""
+
+import logging
+import os
+from pathlib import Path
+import re
+import select
+import subprocess
+import time
+
+import pytest
+
+import test_utils
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+class TestFunctionalVerilator:
+ """
+ Execute a test binary in a Verilator-simulated hardware build, using UART
+ output to validate test success or failure.
+ """
+ @pytest.fixture
+ def sim_top_earlgrey(self, tmp_path, sim_top_build, sw_test_bin, rom_bin):
+ assert Path(sw_test_bin).is_file()
+ assert Path(rom_bin).is_file()
+ assert Path(sim_top_build).is_file()
+
+ cmd_sim = [
+ str(Path(sim_top_build).resolve()), '--flashinit',
+ str(Path(sw_test_bin).resolve()), '--rominit',
+ str(Path(rom_bin).resolve())
+ ]
+ p_sim = test_utils.Process(cmd_sim,
+ logdir=str(tmp_path),
+ cwd=str(tmp_path),
+ startup_done_expect='Simulation running',
+ startup_timeout=10)
+ p_sim.run()
+
+ yield p_sim
+
+ p_sim.terminate()
+
+ def test_execute_binary(self, sim_top_earlgrey, uart_timeout):
+ """
+ Executes the binary and inspects its UART for "PASS!\r\n" or "FAIL!\r\n".
+ """
+
+ logger = logging.getLogger(__name__)
+
+ # Verilator will print the string "UART: created /dev/pts/#" to
+ # indicate which pseudoterminal the UART port is bound to.
+ uart_match = sim_top_earlgrey.find_in_output(
+ re.compile('UART: Created (/dev/pts/\\d+)'), 5)
+
+ assert uart_match != None
+ uart_path = uart_match.group(1)
+ logger.info("Found UART port at %s." % uart_path)
+
+ # Now, open the UART device and read line by line until we pass or
+ # fail.
+ with open(uart_path, 'rb') as uart_device:
+ uart_fd = uart_device.fileno()
+ pattern = re.compile('.*?(PASS!\r\n|FAIL!\r\n)')
+ match = test_utils.stream_fd_to_log(uart_fd, logger, pattern,
+ uart_timeout)
+ if match == None:
+ pytest.fail('Deadline exceeded: did not see PASS! or FAIL! within %ds.', uart_timeout)
+
+ if match.group(1) == 'PASS!\r\n':
+ logger.debug('Got PASS! from binary.')
+ else:
+ pytest.fail('Got FAIL! from binary.')
diff --git a/test/systemtest/test_utils.py b/test/systemtest/test_utils.py
index 87f0565..8640bf2 100644
--- a/test/systemtest/test_utils.py
+++ b/test/systemtest/test_utils.py
@@ -9,6 +9,7 @@
import logging
import os
import re
+import select
import shlex
import signal
import subprocess
@@ -17,7 +18,6 @@
class Process:
"""Utility class used to spawn an interact with processs.s"""
-
def __init__(self,
cmd,
logdir,
@@ -80,14 +80,13 @@
self._f_stdout = open(logfile_stdout, 'w')
self._f_stderr = open(logfile_stderr, 'w')
- self.proc = subprocess.Popen(
- cmd,
- cwd=self.cwd,
- universal_newlines=True,
- bufsize=1,
- stdin=subprocess.PIPE,
- stdout=self._f_stdout,
- stderr=self._f_stderr)
+ self.proc = subprocess.Popen(cmd,
+ cwd=self.cwd,
+ universal_newlines=True,
+ bufsize=1,
+ stdin=subprocess.PIPE,
+ stdout=self._f_stdout,
+ stderr=self._f_stderr)
self._f_stdout_r = open(logfile_stdout, 'r')
self._f_stderr_r = open(logfile_stderr, 'r')
@@ -98,8 +97,8 @@
# check if the string indicating a successful startup appears in the
# the program output (STDOUT or STDERR)
- init_done = self._find_in_output(
- pattern=self.startup_done_expect, timeout=self.startup_timeout)
+ init_done = self.find_in_output(pattern=self.startup_done_expect,
+ timeout=self.startup_timeout)
if init_done == None:
raise subprocess.TimeoutExpired
@@ -144,9 +143,9 @@
if pattern == None:
return True
- return self._find_in_output(pattern, timeout) != None
+ return self.find_in_output(pattern, timeout) != None
- def _find_in_output(self, pattern, timeout):
+ def find_in_output(self, pattern, timeout):
"""Read STDOUT and STDERR to find an expected pattern.
Both streams are reset to the start of the stream before searching.
@@ -169,7 +168,6 @@
if timeout != None:
t_end = time.time() + timeout
-
# reset streams
self._f_stdout_r.seek(0)
self._f_stderr_r.seek(0)
@@ -217,3 +215,51 @@
pass
return None
+
+
+def stream_fd_to_log(fd, logger, pattern, timeout=None):
+ """
+ Streams lines from the given fd to log until pattern matches.
+
+ Returns the match object derived from pattern.match(), or None if
+ the timeout expires.
+ """
+
+ deadline = None
+ if timeout != None:
+ deadline = time.monotonic() + timeout
+
+ os.set_blocking(fd, False)
+ line_of_output = b''
+ while True:
+ if deadline != None and time.monotonic() > deadline:
+ return None
+
+ if line_of_output.endswith(b'\n'):
+ line_of_output = b''
+
+ # select() on the fd so that we don't waste time reading when
+ # we wouldn't get anything out of it.
+ if deadline != None:
+ rlist, _, _ = select.select([fd], [], [],
+ deadline - time.monotonic())
+ else:
+ rlist, _, _ = select.select([fd], [], [])
+
+ if len(rlist) == 0:
+ continue
+
+ raw_bytes = os.read(fd, 1024)
+ lines = raw_bytes.splitlines(True)
+
+ for line in lines:
+ line_of_output += line
+ if not line_of_output.endswith(b'\n'):
+ break
+
+ logger.debug('fd#%d: %s' % (fd, line_of_output))
+ match = pattern.match(line_of_output.decode('utf-8'))
+ if match != None:
+ return match
+
+ line_of_output = b''