blob: 455e3710edc867db1693857269c26aa70548dae9 [file] [log] [blame] [edit]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
load("@lowrisc_opentitan//rules:opentitan.bzl", "opentitan_flash_binary", "opentitan_rom_binary")
load("@bazel_skylib//lib:shell.bzl", "shell")
load("@bazel_skylib//lib:collections.bzl", "collections")
VALID_TARGETS = ["dv", "verilator", "cw310_rom", "cw310_test_rom"]
OTTF_SUCCESS_MSG = r"PASS.*\n"
OTTF_FAILURE_MSG = r"(FAIL|FAULT).*\n"
ROM_BOOT_FAILURE_MSG = "BFV:[0-9a-f]{8}"
# These are defined for positive test cases and should be flipped for negative
# test cases, i.e., when a test failure is the expected outcome.
DEFAULT_TEST_SUCCESS_MSG = OTTF_SUCCESS_MSG
DEFAULT_TEST_FAILURE_MSG = "({})|({})".format(
OTTF_FAILURE_MSG,
ROM_BOOT_FAILURE_MSG,
)
# This constant holds a dictionary of slot-specific linker script dependencies
# that determine how an `opentitan_flash_binary` is built.
_FLASH_SLOTS = {
"silicon_creator_a": ["@//sw/device/lib/testing/test_framework:ottf_ld_silicon_creator_slot_a"],
"silicon_creator_b": ["@//sw/device/lib/testing/test_framework:ottf_ld_silicon_creator_slot_b"],
"silicon_creator_virtual": ["@//sw/device/lib/testing/test_framework:ottf_ld_silicon_creator_slot_virtual"],
}
_BASE_PARAMS = {
"args": [], # Passed to test runner as arguments.
"data": [],
"local": True,
"otp": "@//hw/ip/otp_ctrl/data:img_rma",
"rom": "@//sw/device/lib/testing/test_rom",
"tags": [],
"test_runner": "@//util:opentitan_functest_runner.sh",
"test_cmds": [], # Passed to test_runner via TEST_CMDS env var.
"timeout": "moderate", # 5 minutes
"exit_success": DEFAULT_TEST_SUCCESS_MSG,
"exit_failure": DEFAULT_TEST_FAILURE_MSG,
}
def dv_params(
# Base Parameters
args = _BASE_PARAMS["args"] + [
"$(location {dvsim_config})",
],
data = _BASE_PARAMS["data"],
local = _BASE_PARAMS["local"],
otp = _BASE_PARAMS["otp"],
rom = _BASE_PARAMS["rom"],
tags = _BASE_PARAMS["tags"],
timeout = _BASE_PARAMS["timeout"],
test_runner = "@//util:dvsim_test_runner.sh",
test_cmds = [],
# DV-specific Parameters
dvsim_config = "@//hw/top_earlgrey/dv:chip_sim_cfg.hjson",
**kwargs):
"""A macro to create DV sim parameters for OpenTitan functional tests.
This macro emits a dictionary of parameters which are pasted into the DV
simulation specific test rule.
Parameters:
@param args: Extra arguments to pass to the test runner (`dvsim.py`).
@param data: Data dependencies of the test.
@param local: Whether the test should be run locally without sandboxing.
@param otp: The OTP image to use.
@param rom: The ROM image to use.
@param tags: The test tags to apply to the test rule.
@param timeout: The timeout to apply to the test rule.
@param test_cmds: A list of required commands and args that make up the
immutable portion of the harness invocation.
@param dvsim_config: The dvsim.py Hjson config file for the toplevel.
"""
required_test_cmds = [
"-i",
"chip_sw_{name}",
"--",
]
required_data = [
dvsim_config,
"@//util/dvsim",
"@//hw:all_files",
"@//hw:fusesoc_ignore",
]
required_tags = ["dv"]
kwargs.update(
args = args,
data = required_data + data,
local = local,
otp = otp,
rom = rom,
tags = required_tags + tags,
test_runner = test_runner,
test_cmds = required_test_cmds + test_cmds,
timeout = timeout,
dvsim_config = dvsim_config,
)
return kwargs
def verilator_params(
# Base Parameters
args = _BASE_PARAMS["args"],
data = _BASE_PARAMS["data"],
exit_success = _BASE_PARAMS["exit_success"],
exit_failure = _BASE_PARAMS["exit_failure"],
local = _BASE_PARAMS["local"],
otp = _BASE_PARAMS["otp"],
rom = _BASE_PARAMS["rom"],
tags = _BASE_PARAMS["tags"],
timeout = _BASE_PARAMS["timeout"],
test_runner = _BASE_PARAMS["test_runner"],
test_cmds = _BASE_PARAMS["test_cmds"] + [
"console",
"--exit-success={exit_success}",
"--exit-failure={exit_failure}",
],
# Verilator-specific Parameters
# None
**kwargs):
"""A macro to create Verilator sim parameters for OpenTitan functional tests.
This macro emits a dictionary of parameters which are pasted into the
Verilator simulation specific test rule.
Parameters:
@param args: Extra arguments to pass to the test runner (`opentitantool`).
@param data: Data dependencies of the test.
@param local: Whether the test should be run locally without sandboxing.
@param otp: The OTP image to use.
@param rom: The ROM image to use.
@param tags: The test tags to apply to the test rule.
@param timeout: The timeout to apply to the test rule.
@param test_cmds: A list of required commands and args that make up the
immutable portion of the harness invocation.
"""
default_args = [
"--rcfile=",
"--logging={logging}",
]
required_test_cmds = [
"--interface=verilator",
"--verilator-bin=$(location @//hw:verilator)",
"--verilator-rom=$(location {rom})",
"--verilator-flash=$(location {flash})",
"--verilator-otp=$(location {otp})",
]
required_data = [
"@//hw:verilator",
"@//hw:fusesoc_ignore",
]
required_tags = ["verilator", "cpu:5"]
kwargs.update(
args = default_args + args,
data = required_data + data,
exit_success = exit_success,
exit_failure = exit_failure,
local = local,
otp = otp,
rom = rom,
tags = required_tags + tags,
test_runner = test_runner,
test_cmds = required_test_cmds + test_cmds,
timeout = timeout,
)
return kwargs
def cw310_params(
# Base Parameters
args = _BASE_PARAMS["args"],
data = _BASE_PARAMS["data"] + ["{bitstream}"],
exit_success = _BASE_PARAMS["exit_success"],
exit_failure = _BASE_PARAMS["exit_failure"],
local = _BASE_PARAMS["local"],
otp = _BASE_PARAMS["otp"],
tags = _BASE_PARAMS["tags"],
test_runner = _BASE_PARAMS["test_runner"],
test_cmds = [
"--exec=\"fpga load-bitstream --rom-kind={rom_kind} $(location {bitstream})\"",
"--exec=\"bootstrap --clear-uart=true $(location {flash})\"",
"--exec=\"console --exit-success={exit_success} --exit-failure={exit_failure}\"",
"{clear_bitstream}",
],
# CW310-specific Parameters
bitstream = None, # A bitstream value of None will cause the default bitstream values to be used
rom_kind = None,
clear_bitstream = False,
# None
timeout = "short",
interface = "cw310",
**kwargs):
"""A macro to create CW310 parameters for OpenTitan functional tests.
This macro emits a dictionary of parameters which are pasted into the
ChipWhisperer-310 FPGA specific test rule.
Parameters:
@param args: Extra arguments to pass the test runner `opentitantool`.
@param data: Data dependencies of the test.
@param local: Whether the test should be run locally without sandboxing.
@param interface: Which communication interface to use with the CW310
board. Choices are "cw310" or "hyper310".
@param otp: The OTP image to use.
@param tags: The test tags to apply to the test rule.
@param test_cmds: A list of required commands and args that make up the
immutable portion of the harness invocation.
@param timeout: The timeout to apply to the test rule.
@param bitstream: The bitstream to load into the FPGA (this specifies
the ROM image that is also used, since the ROM is
baked into the bitstream).
@param rom_kind: The ROM type ('testrom' or 'rom') that is baked into the
bitstream that is loaded into the FPGA.
@param clear_bitstream: Clear FPGA bitstream at the end of the test.
"""
if interface not in ("cw310", "hyper310"):
fail("The interface must be either 'cw310' or 'hyper310'")
default_args = [
"--rcfile=",
"--logging={logging}",
]
required_test_cmds = [
"--interface={}".format(interface),
]
required_tags = [
interface,
"exclusive",
]
kwargs.update(
args = default_args + args,
data = data,
exit_success = exit_success,
exit_failure = exit_failure,
local = local,
interface = interface,
otp = otp,
tags = required_tags + tags,
test_runner = test_runner,
test_cmds = required_test_cmds + test_cmds,
timeout = timeout,
bitstream = bitstream,
rom_kind = rom_kind,
clear_bitstream = clear_bitstream,
)
return kwargs
def opentitan_functest(
name,
targets = VALID_TARGETS,
args = [],
data = [],
test_in_rom = False,
ot_flash_binary = None,
signed = True,
manifest = "@//sw/device/silicon_creator/rom_ext:manifest_standard",
slot = "silicon_creator_a",
test_harness = "@//sw/host/opentitantool",
key = "fake_prod_key_0",
logging = "info",
dv = None,
verilator = None,
cw310 = None,
**kwargs):
"""A helper macro for generating OpenTitan functional tests.
This macro is mostly a wrapper around `opentitan_flash_binary`, but creates
testing artifacts for each of the hardware targets in `targets`. The testing
artifacts are then given to an `sh_test` rule which dispatches the test to
the corresponding hardware target via `opentitantool`.
Parameters:
@param name: The name of this rule.
@param targets: A list of hardware targets on which to dispatch tests.
@param args: Extra arguments (in addition to those defined in the target-
specific parameter dictionary) to pass to the test harness
(e.g., `opentitantool`).
@param data: Extra data dependencies (in addition to those defined in the
target-specific parameter dictionary) needed while executing
the test.
@param test_in_rom: Whether to run the test from ROM, runs from flash by
default.
@param ot_flash_binary: Use the named `opentitan_flash_binary` as the
flash image for the test rather than building one
from srcs/deps.
@param signed: Whether to sign the test image. Signed by default.
@param manifest: Partially populated manifest to set boot stage/slot configs.
@param slot: What slot to build the image for.
@param test_harness: The binary on the host side that runs the test.
@param key: Which signed test image (by key) to use.
@param dv: DV test parameters.
@param verilator: Verilator test parameters.
@param cw310: CW310 test parameters.
@param **kwargs: Arguments to forward to `opentitan_flash_binary`.
This macro emits the following rules:
if `test_in_rom`:
opentitan_rom_binary named: {name}_rom_prog (and all emitted rules).
opentitan_flash_binary named: {name}_prog (and all emitted rules).
sh_test named: {name}_sim_dv
sh_test named: {name}_sim_verilator
sh_test named: {name}_fpga_cw310
test_suite named: {name}
"""
deps = kwargs.pop("deps", [])
all_tests = []
target_params = {}
# Only build SW for devices we are running tests on.
devices_to_build_for = []
for target in targets:
if target == "dv":
devices_to_build_for.append("sim_dv")
target_params["sim_dv"] = dv_params() if not dv else dv
elif target == "verilator":
devices_to_build_for.append("sim_verilator")
target_params["sim_verilator"] = verilator_params() if not verilator else verilator
elif target in ["cw310_rom", "cw310_test_rom"]:
devices_to_build_for.append("fpga_cw310")
# Copy `cw310` for each `target`. This is not a deep copy, thus we
# also copy tags below.
cw310_ = cw310_params() if not cw310 else dict(cw310.items())
cw310_["tags"] = [t for t in cw310_["tags"]]
# If the cw310 parameter was not provided or was provided without
# the bitstream field, determine the bitstream argument based on
# the target. Otherwise, use the provided bitstream argument.
if cw310_["interface"] == "cw310":
DEFAULT_BITSTREAM = {
"cw310_test_rom": "@//hw/bitstream:test_rom",
"cw310_rom": "@//hw/bitstream:rom",
}
else:
DEFAULT_BITSTREAM = {
"cw310_test_rom": "@//hw/bitstream/hyperdebug:test_rom",
"cw310_rom": "@//hw/bitstream/hyperdebug:rom",
}
if (cw310 == None) or (cw310.get("bitstream") == None):
cw310_["bitstream"] = DEFAULT_BITSTREAM[target]
# If the bitstream field was provided, it will already have been copied into cw310_
# Fill in the remaining bitstream arguments
if target == "cw310_test_rom":
cw310_["rom_kind"] = "testrom"
cw310_["tags"].append("cw310_test_rom")
target_params["fpga_cw310_test_rom"] = cw310_
elif target == "cw310_rom":
cw310_["rom_kind"] = "rom"
cw310_["tags"].append("cw310_rom")
target_params["fpga_cw310_rom"] = cw310_
else:
fail("Expected `cw310_test_rom` or `cw310_rom` as the target name")
else:
fail("Invalid target. Target must be in {}".format(VALID_TARGETS))
devices_to_build_for = collections.uniq(devices_to_build_for)
# Handle the special case were the test is run at the ROM stage.
if test_in_rom:
if ot_flash_binary:
fail("Tests that run in ROM stage cannot use pre-built flash binary.")
if any(["cw310" in t for t in targets]):
fail("Tests that run in ROM stage cannot run on FPGA.")
ot_flash_binary = name + "_rom_prog"
opentitan_rom_binary(
name = ot_flash_binary,
deps = deps,
devices = devices_to_build_for,
testonly = True,
**kwargs
)
# Generate SW artifacts for the tests.
if not ot_flash_binary:
# Set the linker script for the specified slot.
if slot not in _FLASH_SLOTS:
fail("Invalid slot: {}. Valid slots are: silicon_creator_{a,b,virtual}".format(slot))
deps += _FLASH_SLOTS[slot]
# Get OTP image for sim targets. We need to pass the OTP image to the
# flash scrambling script since it contains the seeds to derive the
# scrambling keys. No need to worry about flash image scrambling for
# FPGA targets as the flash is loaded through bootstrap (i.e., the front
# door), unlike the sim targets which load via backdoor.
sim_otp_ = None
if "sim_dv" in target_params:
sim_otp_ = target_params["sim_dv"]["otp"]
elif "sim_verilator" in target_params:
sim_otp_ = target_params["sim_verilator"]["otp"]
ot_flash_binary = name + "_prog"
opentitan_flash_binary(
name = ot_flash_binary,
signed = signed,
sim_otp = sim_otp_,
deps = deps,
devices = devices_to_build_for,
manifest = manifest,
testonly = True,
**kwargs
)
for target, params in target_params.items():
# Set test name.
test_name = "{}_{}".format(name, target)
all_tests.append(test_name)
# The args/data lists will hold all of the test arguments/artifacts that
# need to be present at runtime.
target_args = args
target_data = data + [test_harness]
target_test_cmds = []
########################################################################
# Retrieve host-side test components.
########################################################################
# Environment variables to pass to the `sh_test`.
env = {}
# Set the test runner (i.e., the script that invokes the "test harness").
test_runner = params.pop("test_runner")
# Set the test harness (i.e., the host-side test component, or
# `opentitantool` in most cases).
env["TEST_HARNESS"] = "$(location {})".format(test_harness)
########################################################################
# Retrieve device-side test components.
########################################################################
# Set ROM image.
# Note: FPGA targets will not specify a ROM image as the ROM imaged is
# specified via the `bitstream` parameter (since the ROM is baked into
# the bitstream).
rom = params.pop("rom", None)
if rom:
if test_in_rom:
rom_filegroup = "{}_rom_prog_{}".format(name, target)
rom = "{}_scr_vmem".format(rom_filegroup)
else:
rom_label = Label(rom)
rom_filegroup = "@{}//{}:{}_{}".format(
rom_label.workspace_name,
rom_label.package,
rom_label.name,
target,
)
rom = "{}_scr_vmem".format(rom_filegroup)
target_data.append(rom)
target_data.append(rom_filegroup)
# Set flash image.
if target in ["sim_dv", "sim_verilator"]:
flash = "{}_{}_scr_vmem64".format(ot_flash_binary, target)
elif target in ["fpga_cw310_rom", "fpga_cw310_test_rom"]:
flash = "{}_fpga_cw310_bin".format(ot_flash_binary)
else:
fail("Unexpected target: {}".format(target))
if signed:
flash += "_signed"
# Multislot flash binaries could have different slots / stages
# signed with different keys. Therefore, the key name will not be
# part of the target name for such images.
if key != "multislot":
flash += "_{}".format(key)
# If test is to be run in ROM we load the same image into flash as a
# as a placeholder (since execution will never reach flash). Moreover,
# there is no need to update the data dependencies list as the ROM
# targets were already added above.
if test_in_rom:
flash = rom
else:
target_data.append(flash)
if target in ["fpga_cw310_rom", "fpga_cw310_test_rom"]:
target_data.append("{}_fpga_cw310".format(ot_flash_binary))
else:
target_data.append("{}_{}".format(ot_flash_binary, target))
# Set OTP image.
otp = params.pop("otp")
target_data.append(otp)
########################################################################
# Retrieve hardware-target-specific parameters.
########################################################################
interface = params.pop("interface", None)
# Set Bitstream (for FPGA targets).
bitstream = params.pop("bitstream", None)
rom_kind = params.pop("rom_kind", None)
if bitstream and not rom_kind:
if "test_rom" in bitstream:
rom_kind = "testrom"
elif "rom" in bitstream:
rom_kind = "rom"
else:
fail("Unknown bitstream type. Expected the bitstream label to contain the string 'test_rom' or 'rom'.")
clear_bitstream = "no-op"
if params.pop("clear_bitstream", False):
clear_bitstream = "fpga clear-bitstream"
# Set success/failure strings for target platforms that print test
# results over the UART (e.g., Verilator and FPGA).
exit_strings_kwargs = {}
if target in ["fpga_cw310_rom", "fpga_cw310_test_rom", "sim_verilator"]:
exit_strings_kwargs = {
"exit_success": shell.quote(params.pop("exit_success")),
"exit_failure": shell.quote(params.pop("exit_failure")),
}
# Set dvsim configuration file (for DV simulation target).
dvsim_config = params.pop("dvsim_config", None)
########################################################################
# Format arguments to send to the test running scripts/tools.
########################################################################
# Concatenate args / data passed into the opentitan_functest macro with
# args / data from device-specific params.
target_args = target_args + params.pop("args")
target_data = target_data + params.pop("data")
target_test_cmds = target_test_cmds + params.pop("test_cmds")
# Fill placeholders in arg/data lists.
format_dict = {
"name": name,
"flash": flash,
"rom": rom,
"otp": otp,
"dvsim_config": dvsim_config,
"clear_bitstream": clear_bitstream,
"bitstream": bitstream,
"rom_kind": rom_kind,
"logging": params.pop("logging", logging),
}
format_dict.update(exit_strings_kwargs)
target_args = [a.format(**format_dict) for a in target_args]
target_data = [d.format(**format_dict) for d in target_data]
target_test_cmds = [s.format(**format_dict) for s in target_test_cmds]
env["TEST_CMDS"] = " ".join(target_test_cmds)
if target in ["fpga_cw310_rom", "fpga_cw310_test_rom"]:
# We attach the UART configuration to the front of the command line
# so that they'll be parsed as global options rather than
# command-specific options.
target_args = select({
# TODO(cfrantz): we may need to do something different here when
# the interface is "hyper310".
"@//ci:lowrisc_fpga_cw310": ["--cw310-uarts=/dev/ttyACM_CW310_1,/dev/ttyACM_CW310_0"],
"//conditions:default": [],
}) + target_args
########################################################################
# Instantiate the test rule.
########################################################################
native.sh_test(
name = test_name,
srcs = [test_runner],
args = target_args,
data = target_data,
env = env,
**params
)
# Guarantee that the test suite will only run tests in `all_tests`. This
# check is necessary because `test_suite()` defaults to all the tests in the
# caller's BUILD file when its `tests` attribute is the empty list.
if all_tests == []:
fail("Zero tests generated for :{}, test_suite() would run the wrong tests.".format(name))
############################################################################
# Instantiate a suite of tests to run the same test on each hardware target.
############################################################################
native.test_suite(
name = name,
tests = all_tests,
# In test_suites, tags perform a filtering function and will select
# matching tags internally instead of allowing filters to select
# test_suites.
# There are exceptions: "small", "medium", "large", "enourmous", "manual"
tags = [
# The manual tag is a special case and is applied to the test suite
# it prevents it from being included in wildcards so that
# --build_tag_filters=-verilator works as expected and excludes
# building verilator tests so the verilator build wont be invoked.
"manual",
# For more see https://bazel.build/reference/be/general#test_suite.tags
],
)
def _manual_test_impl(ctx):
executable = ctx.actions.declare_file("manual_test_wrapper")
ctx.actions.write(
output = executable,
content = "{runner} {testplan}".format(
runner = ctx.executable._runner.short_path,
testplan = ctx.file.testplan.short_path,
),
)
return [
DefaultInfo(
runfiles = ctx.runfiles(
files = [
ctx.executable._runner,
ctx.file.testplan,
],
).merge(ctx.attr._runner[DefaultInfo].default_runfiles),
executable = executable,
),
]
manual_test = rule(
_manual_test_impl,
attrs = {
"testplan": attr.label(
allow_single_file = [".hjson"],
doc = "Testplan with manual testpoints",
mandatory = True,
),
"_runner": attr.label(
default = "//util:run_manual_tests",
executable = True,
cfg = "exec",
),
},
doc = "Walks through the manual testpoints in a testplan",
test = True,
)