Repository: cocotb/cocotb

Issue:
Make `round_mode` for `Timer` available on `wait_timeout`
`with_timeout` is missing the `round_mode` argument available to `Timer`. This would be very convenient to have.



Discussion:

Resolving patch:
diff --git a/cocotb/triggers.py b/cocotb/triggers.py
--- a/cocotb/triggers.py
+++ b/cocotb/triggers.py
@@ -33,7 +33,7 @@
 from collections.abc import Awaitable
 from decimal import Decimal
 from numbers import Real
-from typing import Optional, Union
+from typing import Any, Coroutine, Optional, TypeVar, Union
 
 import cocotb
 from cocotb import outcomes, simulator
@@ -47,6 +47,8 @@
     remove_traceback_frames,
 )
 
+T = TypeVar("T")
+
 
 def _pointer_str(obj):
     """
@@ -190,6 +192,10 @@ def __init__(
                When *units* is ``'step'``,
                the timestep is determined by the simulator (see :make:var:`COCOTB_HDL_TIMEPRECISION`).
 
+            round_mode (str, optional):
+                String specifying how to handle time values that sit between time steps
+                (one of ``'error'``, ``'round'``, ``'ceil'``, ``'floor'``).
+
         Examples:
 
             >>> await Timer(100, units='ps')
@@ -934,7 +940,12 @@ def __repr__(self):
         return fmt.format(type(self).__qualname__, self.signal, self.num_cycles)
 
 
-async def with_timeout(trigger, timeout_time, timeout_unit="step"):
+async def with_timeout(
+    trigger: Union[Trigger, Waitable, Task, Coroutine[Any, Any, T]],
+    timeout_time: Union[float, Decimal],
+    timeout_unit: str = "step",
+    round_mode: Optional[str] = None,
+) -> T:
     r"""
     Waits on triggers or coroutines, throws an exception if it waits longer than the given time.
 
@@ -978,6 +989,9 @@ async def with_timeout(trigger, timeout_time, timeout_unit="step"):
             Simulation time duration before timeout occurs.
         timeout_unit (str, optional):
             Units of timeout_time, accepts any units that :class:`~cocotb.triggers.Timer` does.
+        round_mode (str, optional):
+            String specifying how to handle time values that sit between time steps
+            (one of ``'error'``, ``'round'``, ``'ceil'``, ``'floor'``).
 
     Returns:
         First trigger that completed if timeout did not occur.
@@ -1005,7 +1019,9 @@ async def with_timeout(trigger, timeout_time, timeout_unit="step"):
         shielded = False
     else:
         shielded = True
-    timeout_timer = cocotb.triggers.Timer(timeout_time, timeout_unit)
+    timeout_timer = cocotb.triggers.Timer(
+        timeout_time, timeout_unit, round_mode=round_mode
+    )
     res = await First(timeout_timer, trigger)
     if res is timeout_timer:
         if not shielded:


Test patch:
diff --git a/tests/test_cases/test_cocotb/test_timing_triggers.py b/tests/test_cases/test_cocotb/test_timing_triggers.py
--- a/tests/test_cases/test_cocotb/test_timing_triggers.py
+++ b/tests/test_cases/test_cocotb/test_timing_triggers.py
@@ -319,3 +319,21 @@ async def test_timer_round_mode(_):
     assert Timer(1.2, "step", round_mode="floor").sim_steps == 1
     assert Timer(1.2, "step", round_mode="ceil").sim_steps == 2
     assert Timer(1.2, "step", round_mode="round").sim_steps == 1
+
+    # test with_timeout round_mode
+    with pytest.raises(ValueError):
+        await cocotb.triggers.with_timeout(
+            Timer(1, "step"), timeout_time=2.5, timeout_unit="step", round_mode="error"
+        )
+    await cocotb.triggers.with_timeout(
+        Timer(1, "step"), timeout_time=2, timeout_unit="step", round_mode="error"
+    )
+    await cocotb.triggers.with_timeout(
+        Timer(1, "step"), timeout_time=2.5, timeout_unit="step", round_mode="floor"
+    )
+    await cocotb.triggers.with_timeout(
+        Timer(1, "step"), timeout_time=2.5, timeout_unit="step", round_mode="ceil"
+    )
+    await cocotb.triggers.with_timeout(
+        Timer(1, "step"), timeout_time=2.5, timeout_unit="step", round_mode="round"
+    )