cocotb 1.9.2__cp39-cp39-win_amd64.whl → 2.0.0rc2__cp39-cp39-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cocotb might be problematic. Click here for more details.

Files changed (161) hide show
  1. cocotb/_ANSI.py +65 -0
  2. cocotb/__init__.py +81 -327
  3. cocotb/_base_triggers.py +515 -0
  4. cocotb/_bridge.py +186 -0
  5. cocotb/_decorators.py +515 -0
  6. cocotb/_deprecation.py +3 -3
  7. cocotb/_exceptions.py +7 -0
  8. cocotb/_extended_awaitables.py +419 -0
  9. cocotb/_gpi_triggers.py +385 -0
  10. cocotb/_init.py +301 -0
  11. cocotb/_outcomes.py +54 -0
  12. cocotb/_profiling.py +46 -0
  13. cocotb/_py_compat.py +114 -29
  14. cocotb/_scheduler.py +448 -0
  15. cocotb/_test.py +248 -0
  16. cocotb/_test_factory.py +312 -0
  17. cocotb/_test_functions.py +42 -0
  18. cocotb/_typing.py +7 -0
  19. cocotb/_utils.py +274 -0
  20. cocotb/_version.py +3 -7
  21. cocotb/_xunit_reporter.py +66 -0
  22. cocotb/clock.py +353 -108
  23. cocotb/debug.py +24 -0
  24. cocotb/handle.py +1370 -793
  25. cocotb/libs/cocotb.dll +0 -0
  26. cocotb/libs/cocotb.exp +0 -0
  27. cocotb/libs/cocotb.lib +0 -0
  28. cocotb/libs/cocotbfli_modelsim.dll +0 -0
  29. cocotb/libs/cocotbfli_modelsim.exp +0 -0
  30. cocotb/libs/cocotbfli_modelsim.lib +0 -0
  31. cocotb/libs/cocotbutils.dll +0 -0
  32. cocotb/libs/cocotbutils.exp +0 -0
  33. cocotb/libs/cocotbutils.lib +0 -0
  34. cocotb/libs/cocotbvhpi_aldec.dll +0 -0
  35. cocotb/libs/cocotbvhpi_aldec.exp +0 -0
  36. cocotb/libs/cocotbvhpi_aldec.lib +0 -0
  37. cocotb/libs/cocotbvhpi_modelsim.dll +0 -0
  38. cocotb/libs/cocotbvhpi_modelsim.exp +0 -0
  39. cocotb/libs/cocotbvhpi_modelsim.lib +0 -0
  40. cocotb/libs/cocotbvpi_aldec.dll +0 -0
  41. cocotb/libs/cocotbvpi_aldec.exp +0 -0
  42. cocotb/libs/cocotbvpi_aldec.lib +0 -0
  43. cocotb/libs/cocotbvpi_ghdl.dll +0 -0
  44. cocotb/libs/cocotbvpi_ghdl.exp +0 -0
  45. cocotb/libs/cocotbvpi_ghdl.lib +0 -0
  46. cocotb/libs/cocotbvpi_icarus.exp +0 -0
  47. cocotb/libs/cocotbvpi_icarus.lib +0 -0
  48. cocotb/libs/cocotbvpi_icarus.vpl +0 -0
  49. cocotb/libs/cocotbvpi_modelsim.dll +0 -0
  50. cocotb/libs/cocotbvpi_modelsim.exp +0 -0
  51. cocotb/libs/cocotbvpi_modelsim.lib +0 -0
  52. cocotb/libs/embed.dll +0 -0
  53. cocotb/libs/embed.exp +0 -0
  54. cocotb/libs/embed.lib +0 -0
  55. cocotb/libs/gpi.dll +0 -0
  56. cocotb/libs/gpi.exp +0 -0
  57. cocotb/libs/gpi.lib +0 -0
  58. cocotb/libs/gpilog.dll +0 -0
  59. cocotb/libs/gpilog.exp +0 -0
  60. cocotb/libs/gpilog.lib +0 -0
  61. cocotb/libs/pygpilog.dll +0 -0
  62. cocotb/libs/pygpilog.exp +0 -0
  63. cocotb/libs/pygpilog.lib +0 -0
  64. cocotb/logging.py +424 -0
  65. cocotb/queue.py +103 -57
  66. cocotb/regression.py +680 -717
  67. cocotb/result.py +17 -188
  68. cocotb/share/def/aldec.exp +0 -0
  69. cocotb/share/def/aldec.lib +0 -0
  70. cocotb/share/def/ghdl.exp +0 -0
  71. cocotb/share/def/ghdl.lib +0 -0
  72. cocotb/share/def/icarus.exp +0 -0
  73. cocotb/share/def/icarus.lib +0 -0
  74. cocotb/share/def/modelsim.def +1 -0
  75. cocotb/share/def/modelsim.exp +0 -0
  76. cocotb/share/def/modelsim.lib +0 -0
  77. cocotb/share/include/cocotb_utils.h +9 -32
  78. cocotb/share/include/embed.h +7 -30
  79. cocotb/share/include/gpi.h +331 -137
  80. cocotb/share/include/gpi_logging.h +221 -142
  81. cocotb/share/include/py_gpi_logging.h +8 -5
  82. cocotb/share/include/vpi_user_ext.h +4 -26
  83. cocotb/share/lib/verilator/verilator.cpp +80 -67
  84. cocotb/simtime.py +230 -0
  85. cocotb/simulator.cp39-win_amd64.exp +0 -0
  86. cocotb/simulator.cp39-win_amd64.lib +0 -0
  87. cocotb/simulator.cp39-win_amd64.pyd +0 -0
  88. cocotb/simulator.pyi +107 -0
  89. cocotb/task.py +478 -213
  90. cocotb/triggers.py +55 -1092
  91. cocotb/types/__init__.py +28 -47
  92. cocotb/types/_abstract_array.py +151 -0
  93. cocotb/types/_array.py +295 -0
  94. cocotb/types/_indexing.py +17 -0
  95. cocotb/types/_logic.py +333 -0
  96. cocotb/types/_logic_array.py +868 -0
  97. cocotb/types/{range.py → _range.py} +47 -48
  98. cocotb/types/_resolve.py +76 -0
  99. cocotb/utils.py +58 -646
  100. cocotb-2.0.0rc2.dist-info/METADATA +60 -0
  101. cocotb-2.0.0rc2.dist-info/RECORD +146 -0
  102. {cocotb-1.9.2.dist-info → cocotb-2.0.0rc2.dist-info}/WHEEL +1 -1
  103. cocotb-2.0.0rc2.dist-info/entry_points.txt +2 -0
  104. {cocotb-1.9.2.dist-info → cocotb-2.0.0rc2.dist-info/licenses}/LICENSE +1 -0
  105. {cocotb-1.9.2.dist-info → cocotb-2.0.0rc2.dist-info}/top_level.txt +1 -0
  106. cocotb_tools/__init__.py +0 -0
  107. cocotb_tools/_coverage.py +33 -0
  108. cocotb_tools/_vendor/__init__.py +3 -0
  109. cocotb_tools/check_results.py +65 -0
  110. cocotb_tools/combine_results.py +152 -0
  111. cocotb_tools/config.py +241 -0
  112. {cocotb → cocotb_tools}/ipython_support.py +29 -22
  113. cocotb_tools/makefiles/Makefile.deprecations +27 -0
  114. {cocotb/share → cocotb_tools}/makefiles/Makefile.inc +77 -55
  115. {cocotb/share → cocotb_tools}/makefiles/Makefile.sim +16 -33
  116. {cocotb/share → cocotb_tools}/makefiles/simulators/Makefile.activehdl +9 -16
  117. cocotb_tools/makefiles/simulators/Makefile.cvc +61 -0
  118. cocotb_tools/makefiles/simulators/Makefile.dsim +39 -0
  119. {cocotb/share → cocotb_tools}/makefiles/simulators/Makefile.ghdl +13 -42
  120. cocotb_tools/makefiles/simulators/Makefile.icarus +80 -0
  121. cocotb_tools/makefiles/simulators/Makefile.ius +93 -0
  122. cocotb_tools/makefiles/simulators/Makefile.modelsim +9 -0
  123. cocotb_tools/makefiles/simulators/Makefile.nvc +60 -0
  124. cocotb_tools/makefiles/simulators/Makefile.questa +29 -0
  125. cocotb/share/makefiles/simulators/Makefile.questa → cocotb_tools/makefiles/simulators/Makefile.questa-compat +26 -54
  126. cocotb_tools/makefiles/simulators/Makefile.questa-qisqrun +149 -0
  127. {cocotb/share → cocotb_tools}/makefiles/simulators/Makefile.riviera +17 -56
  128. cocotb_tools/makefiles/simulators/Makefile.vcs +65 -0
  129. {cocotb/share → cocotb_tools}/makefiles/simulators/Makefile.verilator +15 -22
  130. {cocotb/share → cocotb_tools}/makefiles/simulators/Makefile.xcelium +20 -52
  131. cocotb_tools/py.typed +0 -0
  132. cocotb_tools/runner.py +1868 -0
  133. cocotb/_sim_versions.py → cocotb_tools/sim_versions.py +16 -21
  134. pygpi/entry.py +34 -18
  135. pygpi/py.typed +0 -0
  136. cocotb/ANSI.py +0 -92
  137. cocotb/binary.py +0 -858
  138. cocotb/config.py +0 -289
  139. cocotb/decorators.py +0 -332
  140. cocotb/log.py +0 -303
  141. cocotb/memdebug.py +0 -35
  142. cocotb/outcomes.py +0 -56
  143. cocotb/runner.py +0 -1400
  144. cocotb/scheduler.py +0 -1099
  145. cocotb/share/makefiles/Makefile.deprecations +0 -12
  146. cocotb/share/makefiles/simulators/Makefile.cvc +0 -94
  147. cocotb/share/makefiles/simulators/Makefile.icarus +0 -111
  148. cocotb/share/makefiles/simulators/Makefile.ius +0 -125
  149. cocotb/share/makefiles/simulators/Makefile.modelsim +0 -32
  150. cocotb/share/makefiles/simulators/Makefile.nvc +0 -64
  151. cocotb/share/makefiles/simulators/Makefile.vcs +0 -98
  152. cocotb/types/array.py +0 -309
  153. cocotb/types/logic.py +0 -292
  154. cocotb/types/logic_array.py +0 -298
  155. cocotb/wavedrom.py +0 -199
  156. cocotb/xunit_reporter.py +0 -80
  157. cocotb-1.9.2.dist-info/METADATA +0 -168
  158. cocotb-1.9.2.dist-info/RECORD +0 -121
  159. cocotb-1.9.2.dist-info/entry_points.txt +0 -2
  160. /cocotb/{_vendor/__init__.py → py.typed} +0 -0
  161. {cocotb → cocotb_tools}/_vendor/distutils_version.py +0 -0
cocotb/_scheduler.py ADDED
@@ -0,0 +1,448 @@
1
+ # Copyright cocotb contributors
2
+ # Copyright (c) 2013, 2018 Potential Ventures Ltd
3
+ # Copyright (c) 2013 SolarFlare Communications Inc
4
+ # Licensed under the Revised BSD License, see LICENSE for details.
5
+ # SPDX-License-Identifier: BSD-3-Clause
6
+
7
+ """Task scheduler."""
8
+
9
+ import logging
10
+ import threading
11
+ from bdb import BdbQuit
12
+ from collections import OrderedDict
13
+ from typing import Any, Callable, Coroutine, Dict, List, TypeVar, Union
14
+
15
+ import cocotb
16
+ import cocotb._gpi_triggers
17
+ import cocotb.handle
18
+ from cocotb import debug
19
+ from cocotb._base_triggers import Event, Trigger
20
+ from cocotb._bridge import external_state, external_waiter
21
+ from cocotb._exceptions import InternalError
22
+ from cocotb._gpi_triggers import (
23
+ GPITrigger,
24
+ NextTimeStep,
25
+ ReadWrite,
26
+ )
27
+ from cocotb._outcomes import Error, Outcome, Value, capture
28
+ from cocotb._profiling import profiling_context
29
+ from cocotb._py_compat import ParamSpec, insertion_ordered_dict
30
+ from cocotb.task import Task, _TaskState
31
+
32
+ T = TypeVar("T")
33
+
34
+ P = ParamSpec("P")
35
+
36
+
37
+ class Scheduler:
38
+ """The main Task scheduler.
39
+
40
+ How It Generally Works:
41
+ Tasks are `queued` to run in the scheduler with :meth:`_queue`.
42
+ Queueing adds the Task and an Outcome value to :attr:`_pending_tasks`.
43
+ The main scheduling loop is located in :meth:`_event_loop` and loops over the queued Tasks and `schedules` them.
44
+ :meth:`_schedule` schedules a Task -
45
+ continuing its execution from where it previously yielded control -
46
+ by injecting the Outcome value associated with the Task from the queue.
47
+ The Task's body will run until it finishes or reaches the next :keyword:`await` statement.
48
+ If a Task reaches an :keyword:`await`, :meth:`_schedule` will convert the value yielded from the Task into a Trigger with :meth:`_trigger_from_any` and its friend methods.
49
+ Triggers are then `primed` (with :meth:`~cocotb.triggers.Trigger._prime`)
50
+ with a `react` function (:meth:`_sim_react` or :meth:`_react)
51
+ so as to wake up Tasks waiting for that Trigger to `fire` (when the event encoded by the Trigger occurs).
52
+ This is accomplished by :meth:`_resume_task_upon`.
53
+ :meth:`_resume_task_upon` also associates the Trigger with the Task waiting on it to fire by adding them to the :attr:`_trigger2tasks` map.
54
+ If, instead of reaching an :keyword:`await`, a Task finishes, :meth:`_schedule` will cause the :class:`~cocotb.task.Join` trigger to fire.
55
+ Once a Trigger fires it calls the react function which queues all Tasks waiting for that Trigger to fire.
56
+ Then the process repeats.
57
+
58
+ When a Task is cancelled (:meth:`_unschedule`), it is removed from the Task queue if it is currently queued.
59
+ Also, the Task and Trigger are deassociated in the :attr:`_trigger2tasks` map.
60
+ If the cancelled Task is the last Task waiting on a Trigger, that Trigger is `unprimed` to prevent it from firing.
61
+
62
+ Simulator Phases:
63
+ All GPITriggers (triggers that are fired by the simulator) go through :meth:`_sim_react`
64
+ which looks at the fired GPITriggers to determine and track the current simulator phase cocotb is executing in.
65
+
66
+ Normal phase:
67
+ Corresponds to all non-ReadWrite and non-ReadOnly phases.
68
+ Any writes are cached for the next ReadWrite phase and do not happen immediately.
69
+ Scheduling :class:`~cocotb.triggers.ReadWrite` and :class:`~cocotb.triggers.ReadOnly` are valid.
70
+
71
+ ReadWrite phase:
72
+ Corresponds to ``cbReadWriteSynch`` (VPI) or ``vhpiCbRepLastKnownDeltaCycle`` (VHPI).
73
+ At the start of scheduling in this phase we play back all the *previously* cached write updates.
74
+ Any writes are cached for the next ReadWrite phase and do not happen immediately.
75
+ Scheduling :class:`~cocotb.triggers.ReadWrite` and :class:`~cocotb.triggers.ReadOnly` are valid.
76
+ One caveat is that scheduling a :class:`~cocotb.triggers.ReadWrite` while in this phase may not be valid.
77
+ If there were no writes applied at the beginning of this phase, there will be no more events in this time step,
78
+ and there will not be another ReadWrite phase in this time step.
79
+ Simulators generally handle this caveat gracefully by leaving you in the ReadWrite phase of the next time step.
80
+
81
+ ReadOnly phase
82
+ Corresponds to ``cbReadOnlySynch`` (VPI) or ``vhpiCbRepEndOfTimeStep`` (VHPI).
83
+ In this state we are not allowed to perform writes.
84
+ Scheduling :class:`~cocotb.triggers.ReadWrite` and :class:`~cocotb.triggers.ReadOnly` are *not* valid.
85
+
86
+ Caveats and Special Cases:
87
+ The scheduler treats Tests specially.
88
+ If a Test finishes or a Task ends with an Exception, the scheduler is put into a `terminating` state.
89
+ All currently queued Tasks are cancelled and all pending Triggers are unprimed.
90
+ This is currently spread out between :meth:`_handle_termination` and :meth:`_cleanup`.
91
+ In that mix of functions, the :attr:`_test_complete_cb` callback is called to inform whomever (the regression_manager) the test finished.
92
+ The scheduler also is responsible for starting the next Test in the Normal phase by priming a ``Timer(1)`` with the second half of test completion handling.
93
+
94
+ The scheduler is currently where simulator time phase is tracked.
95
+ This is mostly because this is where :meth:`_sim_react` is most conveniently located.
96
+ The scheduler can't currently be made independent of simulator-specific code because of the above special cases which have to respect simulator phasing.
97
+
98
+ Currently Task cancellation is accomplished with :meth:`Task.kill() <cocotb.task.Task.kill>`.
99
+ This function immediately cancels the Task by re-entering the scheduler.
100
+ This can cause issues if you are trying to cancel the Test Task or the currently executing Task.
101
+
102
+ TODO: There are attributes and methods for dealing with "externals", but I'm not quite sure how it all works yet.
103
+ """
104
+
105
+ # Singleton events, recycled to avoid spurious object creation
106
+ _next_time_step = NextTimeStep()
107
+ _read_write = ReadWrite()
108
+
109
+ def __init__(self) -> None:
110
+ self.log = logging.getLogger("cocotb.scheduler")
111
+
112
+ # A dictionary of pending tasks for each trigger,
113
+ # indexed by trigger
114
+ self._trigger2tasks: Dict[Trigger, list[Task[object]]] = (
115
+ insertion_ordered_dict()
116
+ )
117
+
118
+ self._scheduled_tasks: OrderedDict[Task[object], Union[BaseException, None]] = (
119
+ OrderedDict()
120
+ )
121
+ self._pending_threads: List[external_waiter[Any]] = []
122
+ self._pending_events: List[Event] = []
123
+
124
+ self._main_thread = threading.current_thread()
125
+
126
+ self._current_task: Union[Task[object], None] = None
127
+
128
+ def _sim_react(self, trigger: GPITrigger) -> None:
129
+ """Called when a :class:`~cocotb.triggers.GPITrigger` fires.
130
+
131
+ This is often the entry point into Python from the simulator,
132
+ so this function is in charge of enabling profiling.
133
+ It must also track the current simulator time phase,
134
+ and start the unstarted event loop.
135
+ """
136
+ with profiling_context:
137
+ # TODO: move state tracking to global variable
138
+ # and handle this via some kind of trigger-specific Python callback
139
+ cocotb._gpi_triggers._current_gpi_trigger = trigger
140
+
141
+ # apply inertial writes if ReadWrite
142
+ if trigger is self._read_write:
143
+ cocotb.handle._apply_scheduled_writes()
144
+
145
+ self._react(trigger)
146
+ self._event_loop()
147
+
148
+ def _react(self, trigger: Trigger) -> None:
149
+ """Called when a :class:`~cocotb.triggers.Trigger` fires.
150
+
151
+ Finds all Tasks waiting on the Trigger that fired and queues them.
152
+ """
153
+ if debug.debug:
154
+ self.log.debug("Trigger fired: %s", trigger)
155
+
156
+ # find all tasks waiting on trigger that fired
157
+ try:
158
+ scheduling = self._trigger2tasks.pop(trigger)
159
+ except KeyError:
160
+ # GPI triggers should only be ever pending if there is an
161
+ # associated task waiting on that trigger, otherwise it would
162
+ # have been unprimed already
163
+ if isinstance(trigger, GPITrigger):
164
+ self.log.warning(
165
+ "No tasks waiting on GPITrigger that fired: %s\n"
166
+ "This is due to an issue with the GPI or a simulator bug.",
167
+ trigger,
168
+ )
169
+ # For Python triggers this isn't actually an error - we might do
170
+ # event.set() without knowing whether any tasks are actually
171
+ # waiting on this event, for example
172
+ elif debug.debug:
173
+ self.log.debug("No tasks waiting on trigger that fired: %s", trigger)
174
+ return
175
+
176
+ if debug.debug:
177
+ debugstr = "\n\t".join([str(task) for task in scheduling])
178
+ if len(scheduling) > 0:
179
+ debugstr = "\n\t" + debugstr
180
+ self.log.debug(
181
+ "%d pending tasks for trigger %s%s",
182
+ len(scheduling),
183
+ trigger,
184
+ debugstr,
185
+ )
186
+
187
+ # queue all tasks to wake up
188
+ for task in scheduling:
189
+ # unset trigger
190
+ task._trigger = None
191
+ self._schedule_task_internal(task)
192
+
193
+ # cleanup trigger
194
+ trigger._cleanup()
195
+
196
+ def _event_loop(self) -> None:
197
+ """Run the main event loop.
198
+
199
+ This should only be started by:
200
+ * The beginning of a test, when there is no trigger to react to
201
+ * A GPI trigger
202
+ """
203
+
204
+ while self._scheduled_tasks:
205
+ task, exc = self._scheduled_tasks.popitem(last=False)
206
+
207
+ if debug.debug:
208
+ self.log.debug("Scheduling task %s", task)
209
+ self._resume_task(task, exc)
210
+ if debug.debug:
211
+ self.log.debug("Scheduled task %s", task)
212
+
213
+ # remove our reference to the objects at the end of each loop,
214
+ # to try and avoid them being destroyed at a weird time (as
215
+ # happened in gh-957)
216
+ del task
217
+
218
+ # Schedule may have queued up some events so we'll burn through those
219
+ while self._pending_events:
220
+ if debug.debug:
221
+ self.log.debug(
222
+ "Scheduling pending event %s", self._pending_events[0]
223
+ )
224
+ self._pending_events.pop(0).set()
225
+
226
+ # no more pending tasks
227
+ if debug.debug:
228
+ self.log.debug("All tasks scheduled, handing control back to simulator")
229
+
230
+ def _unschedule(self, task: Task[Any]) -> None:
231
+ """Unschedule a task and unprime dangling pending triggers.
232
+
233
+ Also:
234
+ * enters the scheduler termination state if the Test Task is unscheduled.
235
+ * creates and fires a :class:`~cocotb.task.Join` trigger.
236
+ * forcefully ends the Test if a Task ends with an exception.
237
+ """
238
+
239
+ # remove task from queue
240
+ if task in self._scheduled_tasks:
241
+ self._scheduled_tasks.pop(task)
242
+
243
+ # Unprime the trigger this task is waiting on
244
+ trigger = task._trigger
245
+ if trigger is not None:
246
+ task._trigger = None
247
+ if task in self._trigger2tasks.setdefault(trigger, []):
248
+ self._trigger2tasks[trigger].remove(task)
249
+ if not self._trigger2tasks[trigger]:
250
+ trigger._unprime()
251
+ del self._trigger2tasks[trigger]
252
+
253
+ def _schedule_task_upon(self, task: Task[Any], trigger: Trigger) -> None:
254
+ """Schedule `task` to be resumed when `trigger` fires."""
255
+ # TODO Move this all into Task
256
+ task._trigger = trigger
257
+ task._state = _TaskState.PENDING
258
+
259
+ trigger_tasks = self._trigger2tasks.setdefault(trigger, [])
260
+ trigger_tasks.append(task)
261
+
262
+ try:
263
+ # TODO maybe associate the react method with the trigger object so
264
+ # we don't have to do a type check here.
265
+ if isinstance(trigger, GPITrigger):
266
+ trigger._prime(self._sim_react)
267
+ else:
268
+ trigger._prime(self._react)
269
+ except Exception as e:
270
+ # discard the trigger we associated, it will never fire
271
+ self._trigger2tasks.pop(trigger)
272
+
273
+ # replace it with a new trigger that throws back the exception
274
+ self._schedule_task_internal(task, e)
275
+
276
+ def _schedule_task(self, task: Task[Any]) -> None:
277
+ """Queue *task* for scheduling.
278
+
279
+ It is an error to attempt to queue a task that has already been queued.
280
+ """
281
+ if task.done():
282
+ raise RuntimeError(
283
+ f"{task} has finished executing and can not be scheduled again. Did you call start_soon() on a finished Task?"
284
+ )
285
+ if task in self._scheduled_tasks:
286
+ return
287
+ for tasks in self._trigger2tasks.values():
288
+ if task in tasks:
289
+ return
290
+ self._schedule_task_internal(task)
291
+
292
+ def _schedule_task_internal(
293
+ self, task: Task[Any], exc: Union[BaseException, None] = None
294
+ ) -> None:
295
+ # TODO Move state tracking into Task
296
+ task._state = _TaskState.SCHEDULED
297
+ self._scheduled_tasks[task] = exc
298
+
299
+ def _queue_function(self, task: Coroutine[Trigger, None, T]) -> T:
300
+ """Queue a task for execution and move the containing thread
301
+ so that it does not block execution of the main thread any longer.
302
+ """
303
+ # We should be able to find ourselves inside the _pending_threads list
304
+ matching_threads = [
305
+ t for t in self._pending_threads if t.thread == threading.current_thread()
306
+ ]
307
+ if len(matching_threads) == 0:
308
+ raise RuntimeError("queue_function called from unrecognized thread")
309
+
310
+ # Raises if there is more than one match. This can never happen, since
311
+ # each entry always has a unique thread.
312
+ (t,) = matching_threads
313
+
314
+ outcome: Union[Outcome[T], None] = None
315
+
316
+ async def wrapper() -> None:
317
+ nonlocal outcome
318
+ # This function runs in the scheduler thread
319
+ try:
320
+ outcome = Value(await task)
321
+ except (KeyboardInterrupt, SystemExit, BdbQuit):
322
+ # Allow these to bubble up to the execution root to fail the sim immediately.
323
+ # This follows asyncio's behavior.
324
+ raise
325
+ except BaseException as e:
326
+ outcome = Error(e)
327
+ # Notify the current (scheduler) thread that we are about to wake
328
+ # up the background (`@external`) thread, making sure to do so
329
+ # before the background thread gets a chance to go back to sleep by
330
+ # calling thread_suspend.
331
+ # We need to do this here in the scheduler thread so that no more
332
+ # tasks run until the background thread goes back to sleep.
333
+ t.thread_resume()
334
+ event.set()
335
+
336
+ event = threading.Event()
337
+ # must register this with test as there's no way to clean up with threading
338
+ self._schedule_task_internal(cocotb.start_soon(wrapper()))
339
+ # The scheduler thread blocks in `thread_wait`, and is woken when we
340
+ # call `thread_suspend` - so we need to make sure the task is
341
+ # queued before that.
342
+ t.thread_suspend()
343
+ # This blocks the calling `@external` thread until the task finishes
344
+ event.wait()
345
+ assert outcome is not None
346
+ return outcome.get()
347
+
348
+ def _run_in_executor(
349
+ self, func: "Callable[P, T]", *args: "P.args", **kwargs: "P.kwargs"
350
+ ) -> Coroutine[Trigger, None, T]:
351
+ """Run the task in a separate execution thread
352
+ and return an awaitable object for the caller.
353
+ """
354
+ # Create a thread
355
+ # Create a trigger that is called as a result of the thread finishing
356
+ # Create an Event object that the caller can await on
357
+ # Event object set when the thread finishes execution, this blocks the
358
+ # calling task (but not the thread) until the external completes
359
+
360
+ waiter = external_waiter[T]()
361
+
362
+ def execute_external() -> None:
363
+ waiter._outcome = capture(func, *args, **kwargs)
364
+ if debug.debug:
365
+ self.log.debug(
366
+ "Execution of external routine done %s", threading.current_thread()
367
+ )
368
+ waiter.thread_done()
369
+
370
+ async def wrapper() -> T:
371
+ thread = threading.Thread(
372
+ group=None,
373
+ target=execute_external,
374
+ name=func.__qualname__ + "_thread",
375
+ )
376
+
377
+ waiter.thread = thread
378
+ self._pending_threads.append(waiter)
379
+
380
+ await waiter.event.wait()
381
+
382
+ return waiter.result # raises if there was an exception
383
+
384
+ return wrapper()
385
+
386
+ def _resume_task(self, task: Task[object], exc: Union[BaseException, None]) -> None:
387
+ """Resume *task* with *outcome*.
388
+
389
+ Args:
390
+ task: The task to schedule.
391
+ outcome: The outcome to inject into the *task*.
392
+
393
+ Scheduling runs *task* until it either finishes or reaches the next :keyword:`await` statement.
394
+ If *task* completes, it is unscheduled, a Join trigger fires, and test completion is inspected.
395
+ Otherwise, it reached an :keyword:`await` and we have a result object which is converted to a trigger,
396
+ that trigger is primed,
397
+ then that trigger and the *task* are registered with the :attr:`_trigger2tasks` map.
398
+ """
399
+ if self._current_task is not None:
400
+ raise InternalError("_schedule() called while another Task is executing")
401
+ try:
402
+ self._current_task = task
403
+
404
+ trigger = task._advance(exc)
405
+
406
+ if task.done():
407
+ if debug.debug:
408
+ self.log.debug("%s completed with %s", task, task._outcome)
409
+ assert trigger is None
410
+ self._unschedule(task)
411
+
412
+ if not task.done():
413
+ if debug.debug:
414
+ self.log.debug("%r yielded %s", task, trigger)
415
+ if not isinstance(trigger, Trigger):
416
+ e = TypeError(
417
+ f"Coroutine yielded an object of type {type(trigger)}, which the scheduler can't "
418
+ f"handle: {trigger!r}\n"
419
+ )
420
+ self._schedule_task_internal(task, e)
421
+ else:
422
+ self._schedule_task_upon(task, trigger)
423
+
424
+ # We do not return from here until pending threads have completed, but only
425
+ # from the main thread, this seems like it could be problematic in cases
426
+ # where a sim might change what this thread is.
427
+
428
+ if self._main_thread is threading.current_thread():
429
+ for ext in self._pending_threads:
430
+ ext.thread_start()
431
+ if debug.debug:
432
+ self.log.debug(
433
+ "Blocking from %s on %s",
434
+ threading.current_thread(),
435
+ ext.thread,
436
+ )
437
+ state = ext.thread_wait()
438
+ if debug.debug:
439
+ self.log.debug(
440
+ "Back from wait on self %s with newstate %s",
441
+ threading.current_thread(),
442
+ state,
443
+ )
444
+ if state == external_state.EXITED:
445
+ self._pending_threads.remove(ext)
446
+ self._pending_events.append(ext.event)
447
+ finally:
448
+ self._current_task = None
cocotb/_test.py ADDED
@@ -0,0 +1,248 @@
1
+ # Copyright cocotb contributors
2
+ # Licensed under the Revised BSD License, see LICENSE for details.
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+ import inspect
5
+ import os
6
+ import pdb
7
+ from typing import (
8
+ Any,
9
+ Callable,
10
+ Coroutine,
11
+ List,
12
+ Optional,
13
+ Union,
14
+ )
15
+
16
+ import cocotb
17
+ from cocotb._base_triggers import NullTrigger, Trigger
18
+ from cocotb._deprecation import deprecated
19
+ from cocotb._exceptions import InternalError
20
+ from cocotb._outcomes import Error, Outcome, Value
21
+ from cocotb._test_functions import TestSuccess
22
+ from cocotb.task import ResultType, Task
23
+
24
+ _pdb_on_exception = "COCOTB_PDB_ON_EXCEPTION" in os.environ
25
+
26
+
27
+ class RunningTest:
28
+ """State of the currently executing Test."""
29
+
30
+ # TODO
31
+ # Make the tasks list a TaskManager.
32
+ # Make shutdown errors and outcome be an ExceptionGroup from that TaskManager.
33
+ # Replace result() with passing the outcome to the done callback.
34
+ # Make this and Task the same object which is a Coroutine.
35
+ # Reimplement the logic in the body of an async function.
36
+ # Make RunningTest a normal Task that the RegressionManager runs and registers a
37
+ # done callback with.
38
+
39
+ def __init__(
40
+ self, test_complete_cb: Callable[[], None], main_task: Task[None]
41
+ ) -> None:
42
+ self._test_complete_cb: Callable[[], None] = test_complete_cb
43
+ self._main_task: Task[None] = main_task
44
+ self._main_task._add_done_callback(self._test_done_callback)
45
+
46
+ self.tasks: List[Task[Any]] = [main_task]
47
+
48
+ self._outcome: Union[None, Outcome[None]] = None
49
+ self._shutdown_errors: list[Outcome[None]] = []
50
+
51
+ def _test_done_callback(self, task: Task[None]) -> None:
52
+ self.tasks.remove(task)
53
+ # If cancelled, end the Test without additional error. This case would only
54
+ # occur if a child threw a CancelledError or if the Test was forced to shutdown.
55
+ if task.cancelled():
56
+ self.abort(Value(None))
57
+ return
58
+ # Handle outcome appropriately and shut down the Test.
59
+ e = task.exception()
60
+ if e is None:
61
+ self.abort(Value(task.result()))
62
+ elif isinstance(e, TestSuccess):
63
+ task._log.info("Test stopped early by this task")
64
+ self.abort(Value(None))
65
+ else:
66
+ task._log.warning(e, exc_info=e)
67
+ self.abort(Error(e))
68
+
69
+ def start(self) -> None:
70
+ cocotb._scheduler_inst._schedule_task_internal(self._main_task)
71
+ cocotb._scheduler_inst._event_loop()
72
+
73
+ def result(self) -> Outcome[None]:
74
+ if self._outcome is None: # pragma: no cover
75
+ raise InternalError("Getting result before test is completed")
76
+
77
+ if not isinstance(self._outcome, Error) and self._shutdown_errors:
78
+ return self._shutdown_errors[0]
79
+ return self._outcome
80
+
81
+ def abort(self, outcome: Outcome[None]) -> None:
82
+ """Force this test to end early."""
83
+
84
+ # If we are shutting down, save any errors
85
+ if self._outcome is not None:
86
+ if isinstance(outcome, Error):
87
+ self._shutdown_errors.append(outcome)
88
+ return
89
+
90
+ # Break into pdb on test end before all Tasks are killed.
91
+ if _pdb_on_exception and isinstance(outcome, Error):
92
+ try:
93
+ pdb.post_mortem(outcome.error.__traceback__)
94
+ except BaseException:
95
+ pdb.set_trace()
96
+
97
+ # Set outcome and cancel Tasks.
98
+ self._outcome = outcome
99
+ for task in self.tasks[:]:
100
+ task._cancel_now()
101
+
102
+ self._test_complete_cb()
103
+
104
+ def add_task(self, task: Task[Any]) -> None:
105
+ self.tasks.append(task)
106
+ task._add_done_callback(self._task_done_callback)
107
+
108
+ def _task_done_callback(self, task: Task[Any]) -> None:
109
+ self.tasks.remove(task)
110
+ # if cancelled, do nothing
111
+ if task.cancelled():
112
+ return
113
+ # if there's a Task awaiting this one, don't fail
114
+ if task.complete in cocotb._scheduler_inst._trigger2tasks:
115
+ return
116
+ # if no failure, do nothing
117
+ e = task.exception()
118
+ if e is None:
119
+ return
120
+ # there was a failure and no one is watching, fail test
121
+ elif isinstance(e, TestSuccess):
122
+ task._log.info("Test stopped early by this task")
123
+ self.abort(Value(None))
124
+ else:
125
+ task._log.warning(e, exc_info=e)
126
+ self.abort(Error(e))
127
+
128
+
129
+ def start_soon(
130
+ coro: Union[Task[ResultType], Coroutine[Trigger, None, ResultType]],
131
+ *,
132
+ name: Optional[str] = None,
133
+ ) -> Task[ResultType]:
134
+ """
135
+ Schedule a :term:`coroutine` to be run concurrently in a :class:`~cocotb.task.Task`.
136
+
137
+ Note that this is not an :keyword:`async` function,
138
+ and the new task will not execute until the calling task yields control.
139
+
140
+ Args:
141
+ coro: A :class:`!Task` or :term:`!coroutine` to be run concurrently.
142
+ name:
143
+ The task's name.
144
+
145
+ .. versionadded:: 2.0
146
+
147
+ Returns:
148
+ The :class:`~cocotb.task.Task` that is scheduled to be run.
149
+
150
+ .. versionadded:: 1.6
151
+ """
152
+ task = create_task(coro, name=name)
153
+ cocotb._scheduler_inst._schedule_task(task)
154
+ return task
155
+
156
+
157
+ @deprecated("Use ``cocotb.start_soon`` instead.")
158
+ async def start(
159
+ coro: Union[Task[ResultType], Coroutine[Trigger, None, ResultType]],
160
+ *,
161
+ name: Optional[str] = None,
162
+ ) -> Task[ResultType]:
163
+ """
164
+ Schedule a :term:`coroutine` to be run concurrently, then yield control to allow pending tasks to execute.
165
+
166
+ The calling task will resume execution before control is returned to the simulator.
167
+
168
+ When the calling task resumes, the newly scheduled task may have completed,
169
+ raised an Exception, or be pending on a :class:`~cocotb.triggers.Trigger`.
170
+
171
+ Args:
172
+ coro: A :class:`!Task` or :term:`!coroutine` to be run concurrently.
173
+ name:
174
+ The task's name.
175
+
176
+ .. versionadded:: 2.0
177
+
178
+ Returns:
179
+ The :class:`~cocotb.task.Task` that has been scheduled and allowed to execute.
180
+
181
+ .. versionadded:: 1.6
182
+
183
+ .. deprecated:: 2.0
184
+ Use :func:`cocotb.start_soon` instead.
185
+ If you need the scheduled Task to start before continuing the current Task,
186
+ use an :class:`.Event` to block the current Task until the scheduled Task starts,
187
+ like so:
188
+
189
+ .. code-block:: python
190
+
191
+ async def coro(started: Event) -> None:
192
+ started.set()
193
+ # Do stuff...
194
+
195
+
196
+ task_started = Event()
197
+ task = cocotb.start_soon(coro(task_started))
198
+ await task_started.wait()
199
+ """
200
+ task = start_soon(coro, name=name)
201
+ await NullTrigger()
202
+ return task
203
+
204
+
205
+ def create_task(
206
+ coro: Union[Task[ResultType], Coroutine[Trigger, None, ResultType]],
207
+ *,
208
+ name: Optional[str] = None,
209
+ ) -> Task[ResultType]:
210
+ """
211
+ Construct a :term:`!coroutine` into a :class:`~cocotb.task.Task` without scheduling the task.
212
+
213
+ The task can later be scheduled with :func:`cocotb.start` or :func:`cocotb.start_soon`.
214
+
215
+ Args:
216
+ coro: A :class:`!Task` or a :term:`!coroutine` to be turned into a :class:`!Task`.
217
+ name:
218
+ The task's name.
219
+
220
+ .. versionadded:: 2.0
221
+
222
+ Returns:
223
+ Either the provided :class:`~cocotb.task.Task` or a new Task wrapping the coroutine.
224
+
225
+ .. versionadded:: 1.6
226
+ """
227
+ if isinstance(coro, Task):
228
+ if name is not None:
229
+ coro.set_name(name)
230
+ return coro
231
+ elif isinstance(coro, Coroutine):
232
+ task = Task[ResultType](coro, name=name)
233
+ cocotb._regression_manager._running_test.add_task(task)
234
+ return task
235
+ elif inspect.iscoroutinefunction(coro):
236
+ raise TypeError(
237
+ f"Coroutine function {coro} should be called prior to being scheduled."
238
+ )
239
+ elif inspect.isasyncgen(coro):
240
+ raise TypeError(
241
+ f"{coro.__qualname__} is an async generator, not a coroutine. "
242
+ "You likely used the yield keyword instead of await."
243
+ )
244
+ else:
245
+ raise TypeError(
246
+ f"Attempt to add an object of type {type(coro)} to the scheduler, "
247
+ f"which isn't a coroutine: {coro!r}\n"
248
+ )