Repository: cocotb/cocotb

Issue:
ability to force/freeze signal
rather than just being able to deposit signals, it is helpful to force signals inside a design. 
Would be great if cocotb would support that.
Maybe as an signalname attribute.
`dut.signalname._force = 1`


Discussion:

Resolving patch:
diff --git a/cocotb/handle.py b/cocotb/handle.py
--- a/cocotb/handle.py
+++ b/cocotb/handle.py
@@ -577,6 +577,35 @@ def loads(self):
         """An iterator for gathering all loads on a signal."""
         return _SimIterator(self._handle, simulator.LOADS)
 
+class _SetAction:
+    """Base class representing the type of action used while write-accessing a handle."""
+    pass
+
+class _SetValueAction(_SetAction):
+    __slots__ = ("value",)
+    """Base class representing the type of action used while write-accessing a handle with a value."""
+    def __init__(self, value):
+        self.value = value
+
+class Deposit(_SetValueAction):
+    """Action used for placing a value into a given handle."""
+    def _as_gpi_args_for(self, hdl):
+        return self.value, 0  # GPI_DEPOSIT
+
+class Force(_SetValueAction):
+    """Action used to force a handle to a given value until a release is applied."""
+    def _as_gpi_args_for(self, hdl):
+        return self.value, 1  # GPI_FORCE
+
+class Freeze(_SetAction):
+    """Action used to make a handle keep its current value until a release is used."""
+    def _as_gpi_args_for(self, hdl):
+        return hdl.value, 1  # GPI_FORCE
+
+class Release(_SetAction):
+    """Action used to stop the effects of a previously applied force/freeze action."""
+    def _as_gpi_args_for(self, hdl):
+        return 0, 2  # GPI_RELEASE
 
 class ModifiableObject(NonConstantObject):
     """Base class for simulator objects whose values can be modified."""
@@ -598,10 +627,11 @@ def setimmediatevalue(self, value):
             TypeError: If target is not wide enough or has an unsupported type
                  for value assignment.
         """
+        value, set_action = self._check_for_set_action(value)
+
         if isinstance(value, _py_compat.integer_types) and value < 0x7fffffff and len(self) <= 32:
-            simulator.set_signal_val_long(self._handle, value)
+            simulator.set_signal_val_long(self._handle, set_action, value)
             return
-
         if isinstance(value, ctypes.Structure):
             value = BinaryValue(value=cocotb.utils.pack(value), n_bits=len(self))
         elif isinstance(value, _py_compat.integer_types):
@@ -625,7 +655,12 @@ def setimmediatevalue(self, value):
             self._log.critical("Unsupported type for value assignment: %s (%s)", type(value), repr(value))
             raise TypeError("Unable to set simulator value with type %s" % (type(value)))
 
-        simulator.set_signal_val_binstr(self._handle, value.binstr)
+        simulator.set_signal_val_binstr(self._handle, set_action, value.binstr)
+
+    def _check_for_set_action(self, value):
+        if not isinstance(value, _SetAction):
+            return value, 0  # GPI_DEPOSIT
+        return value._as_gpi_args_for(self)
 
     @NonConstantObject.value.getter
     def value(self):
@@ -666,6 +701,8 @@ def setimmediatevalue(self, value):
             TypeError: If target has an unsupported type for
                 real value assignment.
         """
+        value, set_action = self._check_for_set_action(value)
+
         try:
             value = float(value)
         except ValueError:
@@ -673,7 +710,7 @@ def setimmediatevalue(self, value):
                                (type(value), repr(value)))
             raise TypeError("Unable to set simulator value with type %s" % (type(value)))
 
-        simulator.set_signal_val_real(self._handle, value)
+        simulator.set_signal_val_real(self._handle, set_action, value)
 
     @ModifiableObject.value.getter
     def value(self):
@@ -699,13 +736,15 @@ def setimmediatevalue(self, value):
             TypeError: If target has an unsupported type for
                  integer value assignment.
         """
+        value, set_action = self._check_for_set_action(value)
+
         if isinstance(value, BinaryValue):
             value = int(value)
         elif not isinstance(value, _py_compat.integer_types):
             self._log.critical("Unsupported type for integer value assignment: %s (%s)", type(value), repr(value))
             raise TypeError("Unable to set simulator value with type %s" % (type(value)))
 
-        simulator.set_signal_val_long(self._handle, value)
+        simulator.set_signal_val_long(self._handle, set_action, value)
 
     @ModifiableObject.value.getter
     def value(self):
@@ -728,13 +767,15 @@ def setimmediatevalue(self, value):
             TypeError: If target has an unsupported type for
                  integer value assignment.
         """
+        value, set_action = self._check_for_set_action(value)
+
         if isinstance(value, BinaryValue):
             value = int(value)
         elif not isinstance(value, _py_compat.integer_types):
             self._log.critical("Unsupported type for integer value assignment: %s (%s)", type(value), repr(value))
             raise TypeError("Unable to set simulator value with type %s" % (type(value)))
 
-        simulator.set_signal_val_long(self._handle, value)
+        simulator.set_signal_val_long(self._handle, set_action, value)
 
     @ModifiableObject.value.getter
     def value(self):
@@ -757,11 +798,13 @@ def setimmediatevalue(self, value):
             TypeError: If target has an unsupported type for
                  string value assignment.
         """
+        value, set_action = self._check_for_set_action(value)
+
         if not isinstance(value, str):
             self._log.critical("Unsupported type for string value assignment: %s (%s)", type(value), repr(value))
             raise TypeError("Unable to set simulator value with type %s" % (type(value)))
 
-        simulator.set_signal_val_str(self._handle, value)
+        simulator.set_signal_val_str(self._handle, set_action, value)
 
     @ModifiableObject.value.getter
     def value(self):


Test patch:
diff --git a/tests/test_cases/test_force_release/Makefile b/tests/test_cases/test_force_release/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/test_cases/test_force_release/Makefile
@@ -0,0 +1,7 @@
+# Copyright cocotb contributors
+# Licensed under the Revised BSD License, see LICENSE for details.
+# SPDX-License-Identifier: BSD-3-Clause
+
+include ../../designs/sample_module/Makefile
+
+MODULE ?= test_force_release
diff --git a/tests/test_cases/test_force_release/test_force_release.py b/tests/test_cases/test_force_release/test_force_release.py
new file mode 100644
--- /dev/null
+++ b/tests/test_cases/test_force_release/test_force_release.py
@@ -0,0 +1,34 @@
+import logging
+
+import cocotb
+from cocotb.triggers import Timer
+from cocotb.result import TestFailure
+from cocotb.handle import Force, Release
+
+
+@cocotb.test(expect_fail=cocotb.SIM_NAME in ["GHDL"])
+async def test_force_release(dut):
+    """
+    Test force and release on simulation handles
+    """
+    log = logging.getLogger("cocotb.test")
+    await Timer(10, "ns")
+    dut.stream_in_data = 4
+    dut.stream_out_data_comb = Force(5)
+    await Timer(10, "ns")
+    got_in = int(dut.stream_in_data)
+    got_out = int(dut.stream_out_data_comb)
+    log.info("dut.stream_in_data = %d" % got_in)
+    log.info("dut.stream_out_data_comb = %d" % got_out)
+    if got_in == got_out:
+        raise TestFailure("stream_in_data and stream_out_data_comb should not match when force is active!")
+
+    dut.stream_out_data_comb = Release()
+    dut.stream_in_data = 3
+    await Timer(10, "ns")
+    got_in = int(dut.stream_in_data)
+    got_out = int(dut.stream_out_data_comb)
+    log.info("dut.stream_in_data = %d" % got_in)
+    log.info("dut.stream_out_data_comb = %d" % got_out)
+    if got_in != got_out:
+        raise TestFailure("stream_in_data and stream_out_data_comb should match when output was released!")