| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| load("@//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] |
| ot_flash_binary = name + "_prog" |
| opentitan_flash_binary( |
| name = ot_flash_binary, |
| signed = signed, |
| 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, |
| ) |