[test] Add an FPGA test executor, similar to the Verilator executor.
The new test executor, functional_fpga_test.py, is similar to the
existing Verilator executor, but rather than accepting the Verilator
model, a boot loader, and a binary (in vmem form) it accepts a spiflash
executable, a /dev path (the UART port for the FPGA) and the binary.
Much like the Verilator test requires building the Verilator model,
this test expects a ROM-spliced bitfile has already been flashed to the
FPGA.
This test does not provide everything necessary to do a full FPGA test;
it instead serves as a starting point for future test executors and,
more importantly, provides a smoketest for the FPGA flow.
diff --git a/test/systemtest/conftest.py b/test/systemtest/conftest.py
index c81bfb7..1178d85 100644
--- a/test/systemtest/conftest.py
+++ b/test/systemtest/conftest.py
@@ -20,7 +20,8 @@
parser.addoption("--verilator_model", action="store", default="")
parser.addoption("--openocd", action="store", default="openocd")
parser.addoption("--uart_timeout", action="store", default="60")
-
+ parser.addoption("--fpga_uart", action="store", default="")
+ parser.addoption("--spiflash", action="store", default="")
@pytest.hookimpl(tryfirst=True)
def pytest_exception_interact(node, call, report):
@@ -108,3 +109,17 @@
def uart_timeout(pytestconfig):
"""Return the timeout in seconds for UART to print PASS."""
return int(pytestconfig.getoption('uart_timeout'))
+
+@pytest.fixture(scope="session")
+def fpga_uart(pytestconfig):
+ """Return the path to the UART attached to the FPGA."""
+ path = Path(pytestconfig.getoption('fpga_uart')).resolve()
+ assert path.is_file()
+ return path
+
+@pytest.fixture(scope="session")
+def spiflash(pytestconfig):
+ """Return the path to the spiflash executable."""
+ path = Path(pytestconfig.getoption('spiflash')).resolve()
+ assert path.is_file()
+ return path
diff --git a/test/systemtest/functional_fpga_test.py b/test/systemtest/functional_fpga_test.py
new file mode 100644
index 0000000..ea8383c
--- /dev/null
+++ b/test/systemtest/functional_fpga_test.py
@@ -0,0 +1,76 @@
+#!/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 an FPGA, which is expected to write
+one of "PASS!\r\n" or "FAIL!\r\n" to the UART to determine success or failure.
+Failing to write either will result in a timeout.
+
+This test requires some setup and some configuration. This test expects you
+to have:
+ - A supported FPGA connected to your workstation.
+ - That FPGA must be flashed with a synthesized EarlGrey bitfile, which has
+ been spliced with the OpenTitan bootloader.
+ - A prebuilt spiflash executable.
+
+You must then provide this test with the .bin file for the test, the UART
+device path for the FPGA, and the spiflash executable. For example:
+
+$ cd ${REPO_TOP}
+$ pytest -s -v test/systemtest/functional_fpga_test.py \
+ --test_bin sw/tests/hmac/sw.bin \
+ --fpga_uart /dev/ttyUSB2 \
+ --spiflash sw/host/spiflash/spiflash
+"""
+
+import logging
+from pathlib import Path
+import re
+
+import pytest
+
+import test_utils
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+class TestFunctionalFpga:
+ """
+ Execute a test binary on a locally connected FPGA, using UART
+ output to validate test success or failure.
+ """
+ @pytest.fixture
+ def spiflash_proc(self, tmp_path, spiflash, sw_test_bin):
+ cmd_flash = [str(spiflash), '--input', str(sw_test_bin)]
+ p_flash = test_utils.Process(cmd_flash,
+ logdir=str(tmp_path),
+ cwd=str(tmp_path),
+ startup_done_expect='Running SPI flash update.',
+ startup_timeout=10)
+ p_flash.run()
+
+ yield p_flash
+
+ p_flash.terminate()
+
+ def test_execute_binary(self, spiflash_proc, fpga_uart, uart_timeout):
+ """
+ Executes the binary and inspects its UART for "PASS!\r\n" or "FAIL!\r\n".
+ """
+
+ logger = logging.getLogger(__name__)
+
+ # Open the UART device and read line by line until we pass or fail.
+ with fpga_uart.open('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.')