fault.tester module

Source code
import magma as m
import fault.actions as actions
from fault.magma_simulator_target import MagmaSimulatorTarget
from fault.logging import warning
from fault.vector_builder import VectorBuilder
from fault.value_utils import make_value
from fault.verilator_target import VerilatorTarget
from fault.system_verilog_target import SystemVerilogTarget
from fault.actions import Poke, Expect, Step, Print
from fault.circuit_utils import check_interface_is_subset
from fault.wrapper import CircuitWrapper, PortWrapper, InstanceWrapper
import copy


class Tester:
    """
    The fault `Tester` object provides a mechanism in Python to construct tests
    for magma circuits.  The `Tester` is instantiated with a specific magma
    circuit and provides an API for performing actions on the circuit
    interface.

    ## Actions
        * tester_inst.poke(port, value) - set `port` to be `value`
        * tester_inst.eval() - have the backend simulator evaluate the DUT to
            propogate any poked inputs (this happens implicitly on every poke
            for event driven simulators such as ncsim/vcs, but not for
            verilator)
        * tester_inst.expect(port, value) - expect `port` to equal `value`
        * tester_inst.peek(port) - returns a symbolic handle to the port's
            current value (current in the context of the tester_inst's recorded
            action sequence)
        * tester_inst.step(n) - Step the clock `n` times (defaults to 1)
        * tester_inst.print(port) - Print out the value of `port`


    Tester supports multiple simulation environments (backends) for the actual
    execution of tests.

    ## Backend Targets
        * verilator
        * ncsim
        * vcs
    """

    __test__ = False  # Tell pytest to skip this class for discovery

    def __init__(self, circuit: m.Circuit, clock: m.ClockType = None,
                 default_print_format_str: str = "%x"):
        """
        `circuit`: the device under test (a magma circuit)
        `clock`: optional, a port from `circuit` corresponding to the clock
        `default_print_format_str`: optional, this sets the default format
            string used for the `print` method
        """
        self._circuit = circuit
        self.actions = []
        if clock is not None and not isinstance(clock, m.ClockType):
            raise TypeError(f"Expected clock port: {clock, type(clock)}")
        self.clock = clock
        self.default_print_format_str = default_print_format_str
        self.targets = {}
        # For public verilator modules
        self.verilator_includes = []

    def make_target(self, target: str, **kwargs):
        """
        Called with the string `target`, returns an instance of the
        corresponding target object.

        Supported values of target: "verilator", "coreir", "python",
            "system-verilog"
        """
        if target == "verilator":
            return VerilatorTarget(self._circuit, **kwargs)
        elif target == "coreir":
            return MagmaSimulatorTarget(self._circuit, clock=self.clock,
                                        backend='coreir', **kwargs)
        elif target == "python":
            return MagmaSimulatorTarget(self._circuit, clock=self.clock,
                                        backend='python', **kwargs)
        elif target == "system-verilog":
            return SystemVerilogTarget(self._circuit, **kwargs)
        raise NotImplementedError(target)

    def poke(self, port, value):
        """
        Set `port` to be `value`
        """
        if isinstance(port, m.TupleType):
            for p, v in zip(port, value):
                self.poke(p, v)
        else:
            value = make_value(port, value)
            self.actions.append(actions.Poke(port, value))

    def peek(self, port):
        """
        Returns a symbolic handle to the current value of `port`
        """
        return actions.Peek(port)

    def print(self, port, format_str=None):
        """
        Print out the current value of `port`.

        If `format_str` is not specified, it will use
        `self.default_print_format_str`
        """
        if format_str is None:
            format_str = self.default_print_format_str
        self.actions.append(actions.Print(port, format_str))

    def expect(self, port, value):
        """
        Expect the current value of `port` to be `value`
        """
        is_peek = isinstance(value, actions.Peek)
        is_port_wrapper = isinstance(value, PortWrapper)
        if not (is_peek or is_port_wrapper):
            value = make_value(port, value)
        self.actions.append(actions.Expect(port, value))

    def eval(self):
        """
        Evaluate the DUT given the current input port values
        """
        self.actions.append(actions.Eval())

    def step(self, steps=1):
        """
        Step the clock `steps` times.
        """
        if self.clock is None:
            raise RuntimeError("Stepping tester without a clock (did you "
                               "specify a clock during initialization?)")
        self.actions.append(actions.Step(self.clock, steps))

    def serialize(self):
        """
        Serialize the action sequence into a set of test vectors
        """
        builder = VectorBuilder(self._circuit)
        for action in self.actions:
            builder.process(action)
        return builder.vectors

    def compile(self, target="verilator", **kwargs):
        """
        Create an instance of the target backend.

        For the verilator backend, this will run verilator to compile the C++
        model.  This allows the user to separate the logic to compile the DUT
        into the model and run the actual tests (for example, if the user
        wants to compile a DUT once and run multiple tests with the same model,
        this avoids having to call verilator multiple times)
        """
        self.targets[target] = self.make_target(target, **kwargs)

    def run(self, target="verilator"):
        """
        Run the current action sequence using the specified `target`.  The user
        should call `compile` with `target` before calling `run`.
        """
        try:
            if target == "verilator":
                self.targets[target].run(self.actions, self.verilator_includes)
            else:
                self.targets[target].run(self.actions)
        except KeyError:
            raise Exception(f"Could not find target={target}, did you compile"
                            " it first?")

    def compile_and_run(self, target="verilator", **kwargs):
        """
        Compile and run the current action sequence using `target`
        """
        self.compile(target, **kwargs)
        self.run(target)

    def retarget(self, new_circuit, clock=None):
        """
        Generates a new instance of the Tester object that targets
        `new_circuit`. This allows you to copy a set of actions for a new
        circuit with the same interface (or an interface that is a super set of
        self._circuit)
        """
        # Check that the interface of self._circuit is a subset of new_circuit
        check_interface_is_subset(self._circuit, new_circuit)

        new_tester = Tester(new_circuit, clock, self.default_print_format_str)
        new_tester.actions = [action.retarget(new_circuit, clock) for action in
                              self.actions]
        return new_tester

    def zero_inputs(self):
        """
        Set all the input ports to 0, useful for intiializing everything to a
        known value
        """
        for name, port in self._circuit.IO.ports.items():
            if port.isinput():
                self.poke(self._circuit.interface.ports[name], 0)

    def clear(self):
        """
        Reset the tester by removing any existing actions. Useful for reusing a
        Tester (e.g. one with verilator already compiled).
        """
        self.actions = []

    def __str__(self):
        """
        Returns a string containing a list of the recorded actions stored in
        `self.actions`
        """
        s = object.__str__(self) + "\n"
        s += "Actions:\n"
        for i, action in enumerate(self.actions):
            s += f"    {i}: {action}\n"
        return s

    def verilator_include(self, module_name):
        self.verilator_includes.append(module_name)

    @property
    def circuit(self):
        return CircuitWrapper(self._circuit, self)}

Classes

class Tester

The fault Tester object provides a mechanism in Python to construct tests for magma circuits. The Tester is instantiated with a specific magma circuit and provides an API for performing actions on the circuit interface.

Actions

* tester_inst.poke(port, value) - set `port` to be `value`
* tester_inst.eval() - have the backend simulator evaluate the DUT to
    propogate any poked inputs (this happens implicitly on every poke
    for event driven simulators such as ncsim/vcs, but not for
    verilator)
* tester_inst.expect(port, value) - expect `port` to equal `value`
* tester_inst.peek(port) - returns a symbolic handle to the port's
    current value (current in the context of the tester_inst's recorded
    action sequence)
* tester_inst.step(n) - Step the clock `n` times (defaults to 1)
* tester_inst.print(port) - Print out the value of `port`

Tester supports multiple simulation environments (backends) for the actual execution of tests.

Backend Targets

* verilator
* ncsim
* vcs
Source code
class Tester:
    """
    The fault `Tester` object provides a mechanism in Python to construct tests
    for magma circuits.  The `Tester` is instantiated with a specific magma
    circuit and provides an API for performing actions on the circuit
    interface.

    ## Actions
        * tester_inst.poke(port, value) - set `port` to be `value`
        * tester_inst.eval() - have the backend simulator evaluate the DUT to
            propogate any poked inputs (this happens implicitly on every poke
            for event driven simulators such as ncsim/vcs, but not for
            verilator)
        * tester_inst.expect(port, value) - expect `port` to equal `value`
        * tester_inst.peek(port) - returns a symbolic handle to the port's
            current value (current in the context of the tester_inst's recorded
            action sequence)
        * tester_inst.step(n) - Step the clock `n` times (defaults to 1)
        * tester_inst.print(port) - Print out the value of `port`


    Tester supports multiple simulation environments (backends) for the actual
    execution of tests.

    ## Backend Targets
        * verilator
        * ncsim
        * vcs
    """

    __test__ = False  # Tell pytest to skip this class for discovery

    def __init__(self, circuit: m.Circuit, clock: m.ClockType = None,
                 default_print_format_str: str = "%x"):
        """
        `circuit`: the device under test (a magma circuit)
        `clock`: optional, a port from `circuit` corresponding to the clock
        `default_print_format_str`: optional, this sets the default format
            string used for the `print` method
        """
        self._circuit = circuit
        self.actions = []
        if clock is not None and not isinstance(clock, m.ClockType):
            raise TypeError(f"Expected clock port: {clock, type(clock)}")
        self.clock = clock
        self.default_print_format_str = default_print_format_str
        self.targets = {}
        # For public verilator modules
        self.verilator_includes = []

    def make_target(self, target: str, **kwargs):
        """
        Called with the string `target`, returns an instance of the
        corresponding target object.

        Supported values of target: "verilator", "coreir", "python",
            "system-verilog"
        """
        if target == "verilator":
            return VerilatorTarget(self._circuit, **kwargs)
        elif target == "coreir":
            return MagmaSimulatorTarget(self._circuit, clock=self.clock,
                                        backend='coreir', **kwargs)
        elif target == "python":
            return MagmaSimulatorTarget(self._circuit, clock=self.clock,
                                        backend='python', **kwargs)
        elif target == "system-verilog":
            return SystemVerilogTarget(self._circuit, **kwargs)
        raise NotImplementedError(target)

    def poke(self, port, value):
        """
        Set `port` to be `value`
        """
        if isinstance(port, m.TupleType):
            for p, v in zip(port, value):
                self.poke(p, v)
        else:
            value = make_value(port, value)
            self.actions.append(actions.Poke(port, value))

    def peek(self, port):
        """
        Returns a symbolic handle to the current value of `port`
        """
        return actions.Peek(port)

    def print(self, port, format_str=None):
        """
        Print out the current value of `port`.

        If `format_str` is not specified, it will use
        `self.default_print_format_str`
        """
        if format_str is None:
            format_str = self.default_print_format_str
        self.actions.append(actions.Print(port, format_str))

    def expect(self, port, value):
        """
        Expect the current value of `port` to be `value`
        """
        is_peek = isinstance(value, actions.Peek)
        is_port_wrapper = isinstance(value, PortWrapper)
        if not (is_peek or is_port_wrapper):
            value = make_value(port, value)
        self.actions.append(actions.Expect(port, value))

    def eval(self):
        """
        Evaluate the DUT given the current input port values
        """
        self.actions.append(actions.Eval())

    def step(self, steps=1):
        """
        Step the clock `steps` times.
        """
        if self.clock is None:
            raise RuntimeError("Stepping tester without a clock (did you "
                               "specify a clock during initialization?)")
        self.actions.append(actions.Step(self.clock, steps))

    def serialize(self):
        """
        Serialize the action sequence into a set of test vectors
        """
        builder = VectorBuilder(self._circuit)
        for action in self.actions:
            builder.process(action)
        return builder.vectors

    def compile(self, target="verilator", **kwargs):
        """
        Create an instance of the target backend.

        For the verilator backend, this will run verilator to compile the C++
        model.  This allows the user to separate the logic to compile the DUT
        into the model and run the actual tests (for example, if the user
        wants to compile a DUT once and run multiple tests with the same model,
        this avoids having to call verilator multiple times)
        """
        self.targets[target] = self.make_target(target, **kwargs)

    def run(self, target="verilator"):
        """
        Run the current action sequence using the specified `target`.  The user
        should call `compile` with `target` before calling `run`.
        """
        try:
            if target == "verilator":
                self.targets[target].run(self.actions, self.verilator_includes)
            else:
                self.targets[target].run(self.actions)
        except KeyError:
            raise Exception(f"Could not find target={target}, did you compile"
                            " it first?")

    def compile_and_run(self, target="verilator", **kwargs):
        """
        Compile and run the current action sequence using `target`
        """
        self.compile(target, **kwargs)
        self.run(target)

    def retarget(self, new_circuit, clock=None):
        """
        Generates a new instance of the Tester object that targets
        `new_circuit`. This allows you to copy a set of actions for a new
        circuit with the same interface (or an interface that is a super set of
        self._circuit)
        """
        # Check that the interface of self._circuit is a subset of new_circuit
        check_interface_is_subset(self._circuit, new_circuit)

        new_tester = Tester(new_circuit, clock, self.default_print_format_str)
        new_tester.actions = [action.retarget(new_circuit, clock) for action in
                              self.actions]
        return new_tester

    def zero_inputs(self):
        """
        Set all the input ports to 0, useful for intiializing everything to a
        known value
        """
        for name, port in self._circuit.IO.ports.items():
            if port.isinput():
                self.poke(self._circuit.interface.ports[name], 0)

    def clear(self):
        """
        Reset the tester by removing any existing actions. Useful for reusing a
        Tester (e.g. one with verilator already compiled).
        """
        self.actions = []

    def __str__(self):
        """
        Returns a string containing a list of the recorded actions stored in
        `self.actions`
        """
        s = object.__str__(self) + "\n"
        s += "Actions:\n"
        for i, action in enumerate(self.actions):
            s += f"    {i}: {action}\n"
        return s

    def verilator_include(self, module_name):
        self.verilator_includes.append(module_name)

    @property
    def circuit(self):
        return CircuitWrapper(self._circuit, self)}

Instance variables

var circuit
Source code
@property
def circuit(self):
    return CircuitWrapper(self._circuit, self)}

Methods

def __init__(self, circuit, clock=None, default_print_format_str='%x')

circuit: the device under test (a magma circuit) clock: optional, a port from circuit corresponding to the clock default_print_format_str: optional, this sets the default format string used for the print method

Source code
def __init__(self, circuit: m.Circuit, clock: m.ClockType = None,
             default_print_format_str: str = "%x"):
    """
    `circuit`: the device under test (a magma circuit)
    `clock`: optional, a port from `circuit` corresponding to the clock
    `default_print_format_str`: optional, this sets the default format
        string used for the `print` method
    """
    self._circuit = circuit
    self.actions = []
    if clock is not None and not isinstance(clock, m.ClockType):
        raise TypeError(f"Expected clock port: {clock, type(clock)}")
    self.clock = clock
    self.default_print_format_str = default_print_format_str
    self.targets = {}
    # For public verilator modules
    self.verilator_includes = []}
def clear(self)

Reset the tester by removing any existing actions. Useful for reusing a Tester (e.g. one with verilator already compiled).

Source code
def clear(self):
    """
    Reset the tester by removing any existing actions. Useful for reusing a
    Tester (e.g. one with verilator already compiled).
    """
    self.actions = []}
def compile(self, target='verilator', **kwargs)

Create an instance of the target backend.

For the verilator backend, this will run verilator to compile the C++ model. This allows the user to separate the logic to compile the DUT into the model and run the actual tests (for example, if the user wants to compile a DUT once and run multiple tests with the same model, this avoids having to call verilator multiple times)

Source code
def compile(self, target="verilator", **kwargs):
    """
    Create an instance of the target backend.

    For the verilator backend, this will run verilator to compile the C++
    model.  This allows the user to separate the logic to compile the DUT
    into the model and run the actual tests (for example, if the user
    wants to compile a DUT once and run multiple tests with the same model,
    this avoids having to call verilator multiple times)
    """
    self.targets[target] = self.make_target(target, **kwargs)}
def compile_and_run(self, target='verilator', **kwargs)

Compile and run the current action sequence using target

Source code
def compile_and_run(self, target="verilator", **kwargs):
    """
    Compile and run the current action sequence using `target`
    """
    self.compile(target, **kwargs)
    self.run(target)}
def eval(self)

Evaluate the DUT given the current input port values

Source code
def eval(self):
    """
    Evaluate the DUT given the current input port values
    """
    self.actions.append(actions.Eval())}
def expect(self, port, value)

Expect the current value of port to be value

Source code
def expect(self, port, value):
    """
    Expect the current value of `port` to be `value`
    """
    is_peek = isinstance(value, actions.Peek)
    is_port_wrapper = isinstance(value, PortWrapper)
    if not (is_peek or is_port_wrapper):
        value = make_value(port, value)
    self.actions.append(actions.Expect(port, value))}
def make_target(self, target, **kwargs)

Called with the string target, returns an instance of the corresponding target object.

Supported values of target: "verilator", "coreir", "python", "system-verilog"

Source code
def make_target(self, target: str, **kwargs):
    """
    Called with the string `target`, returns an instance of the
    corresponding target object.

    Supported values of target: "verilator", "coreir", "python",
        "system-verilog"
    """
    if target == "verilator":
        return VerilatorTarget(self._circuit, **kwargs)
    elif target == "coreir":
        return MagmaSimulatorTarget(self._circuit, clock=self.clock,
                                    backend='coreir', **kwargs)
    elif target == "python":
        return MagmaSimulatorTarget(self._circuit, clock=self.clock,
                                    backend='python', **kwargs)
    elif target == "system-verilog":
        return SystemVerilogTarget(self._circuit, **kwargs)
    raise NotImplementedError(target)}
def peek(self, port)

Returns a symbolic handle to the current value of port

Source code
def peek(self, port):
    """
    Returns a symbolic handle to the current value of `port`
    """
    return actions.Peek(port)}
def poke(self, port, value)

Set port to be value

Source code
def poke(self, port, value):
    """
    Set `port` to be `value`
    """
    if isinstance(port, m.TupleType):
        for p, v in zip(port, value):
            self.poke(p, v)
    else:
        value = make_value(port, value)
        self.actions.append(actions.Poke(port, value))}
def print(self, port, format_str=None)

Print out the current value of port.

If format_str is not specified, it will use self.default_print_format_str

Source code
def print(self, port, format_str=None):
    """
    Print out the current value of `port`.

    If `format_str` is not specified, it will use
    `self.default_print_format_str`
    """
    if format_str is None:
        format_str = self.default_print_format_str
    self.actions.append(actions.Print(port, format_str))}
def retarget(self, new_circuit, clock=None)

Generates a new instance of the Tester object that targets new_circuit. This allows you to copy a set of actions for a new circuit with the same interface (or an interface that is a super set of self._circuit)

Source code
def retarget(self, new_circuit, clock=None):
    """
    Generates a new instance of the Tester object that targets
    `new_circuit`. This allows you to copy a set of actions for a new
    circuit with the same interface (or an interface that is a super set of
    self._circuit)
    """
    # Check that the interface of self._circuit is a subset of new_circuit
    check_interface_is_subset(self._circuit, new_circuit)

    new_tester = Tester(new_circuit, clock, self.default_print_format_str)
    new_tester.actions = [action.retarget(new_circuit, clock) for action in
                          self.actions]
    return new_tester}
def run(self, target='verilator')

Run the current action sequence using the specified target. The user should call compile with target before calling run.

Source code
def run(self, target="verilator"):
    """
    Run the current action sequence using the specified `target`.  The user
    should call `compile` with `target` before calling `run`.
    """
    try:
        if target == "verilator":
            self.targets[target].run(self.actions, self.verilator_includes)
        else:
            self.targets[target].run(self.actions)
    except KeyError:
        raise Exception(f"Could not find target={target}, did you compile"
                        " it first?")}
def serialize(self)

Serialize the action sequence into a set of test vectors

Source code
def serialize(self):
    """
    Serialize the action sequence into a set of test vectors
    """
    builder = VectorBuilder(self._circuit)
    for action in self.actions:
        builder.process(action)
    return builder.vectors}
def step(self, steps=1)

Step the clock steps times.

Source code
def step(self, steps=1):
    """
    Step the clock `steps` times.
    """
    if self.clock is None:
        raise RuntimeError("Stepping tester without a clock (did you "
                           "specify a clock during initialization?)")
    self.actions.append(actions.Step(self.clock, steps))}
def verilator_include(self, module_name)
Source code
def verilator_include(self, module_name):
    self.verilator_includes.append(module_name)}
def zero_inputs(self)

Set all the input ports to 0, useful for intiializing everything to a known value

Source code
def zero_inputs(self):
    """
    Set all the input ports to 0, useful for intiializing everything to a
    known value
    """
    for name, port in self._circuit.IO.ports.items():
        if port.isinput():
            self.poke(self._circuit.interface.ports[name], 0)}