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