Repository: cocotb/cocotb

Issue:
Catch unstarted async coroutines where coroutines can be accepted
In `fork` we catch and provide an error message if the user passes a `cocotb.coroutine` object they did not call, we should do the same for `async` coroutines that weren't called. This is as simple as `inspect.iscoroutinefunction()` (I checked, it is available in 3.5).

I'll add this in with #1550.


Discussion:

Resolving patch:
diff --git a/cocotb/decorators.py b/cocotb/decorators.py
--- a/cocotb/decorators.py
+++ b/cocotb/decorators.py
@@ -93,6 +93,11 @@ def __init__(self, inst):
             self._natively_awaitable = True
         elif inspect.isgenerator(inst):
             self._natively_awaitable = False
+        elif inspect.iscoroutinefunction(inst):
+            raise TypeError(
+                "Coroutine function {} should be called prior to being "
+                "scheduled."
+                .format(inst))
         elif sys.version_info >= (3, 6) and inspect.isasyncgen(inst):
             raise TypeError(
                 "{} is an async generator, not a coroutine. "
diff --git a/cocotb/scheduler.py b/cocotb/scheduler.py
--- a/cocotb/scheduler.py
+++ b/cocotb/scheduler.py
@@ -638,6 +638,13 @@ def add(self, coroutine):
         Just a wrapper around self.schedule which provides some debug and
         useful error messages in the event of common gotchas.
         """
+
+        if inspect.iscoroutinefunction(coroutine):
+            raise TypeError(
+                "Coroutine function {} should be called prior to being "
+                "scheduled."
+                .format(coroutine))
+
         if isinstance(coroutine, cocotb.decorators.coroutine):
             raise TypeError(
                 "Attempt to schedule a coroutine that hasn't started: {}.\n"


Test patch:
diff --git a/tests/test_cases/test_cocotb/common.py b/tests/test_cases/test_cocotb/common.py
--- a/tests/test_cases/test_cocotb/common.py
+++ b/tests/test_cases/test_cocotb/common.py
@@ -45,10 +45,13 @@ def _check_traceback(running_coro, exc_type, pattern):
 
 
 @contextmanager
-def assert_raises(exc_type):
+def assert_raises(exc_type, pattern=None):
     try:
         yield
-    except exc_type:
+    except exc_type as e:
+        if pattern:
+            assert re.match(pattern, str(e)), \
+                "Correct exception type caught, but message did not match pattern"
         pass
     else:
         raise AssertionError("{} was not raised".format(exc_type.__name__))
diff --git a/tests/test_cases/test_cocotb/test_async_coroutines.py b/tests/test_cases/test_cocotb/test_async_coroutines.py
--- a/tests/test_cases/test_cocotb/test_async_coroutines.py
+++ b/tests/test_cases/test_cocotb/test_async_coroutines.py
@@ -8,6 +8,7 @@
 import cocotb
 from cocotb.triggers import Timer
 from cocotb.outcomes import Value, Error
+from common import assert_raises
 
 
 class produce:
@@ -135,3 +136,25 @@ async def example():
 
     yield example()
     assert ran
+
+
+@cocotb.test()
+async def test_fork_coroutine_function_exception(dut):
+    async def coro():
+        pass
+
+    pattern = "Coroutine function {} should be called " \
+        "prior to being scheduled.".format(coro)
+    with assert_raises(TypeError, pattern):
+        cocotb.fork(coro)
+
+
+@cocotb.test()
+async def test_task_coroutine_function_exception(dut):
+    async def coro(dut):
+        pass
+
+    pattern = "Coroutine function {} should be called " \
+        "prior to being scheduled.".format(coro)
+    with assert_raises(TypeError, pattern):
+        cocotb.decorators.RunningTask(coro)