Skip to content

Commit 52bebac

Browse files
Check AsyncioDispatcher loop is running
Also added documentation. Also fixed potential permanent block in tests - if the subprocess never creates its Client, the parent process blocks forever on an accept(). This was the case in the asyncio_ioc_override, where initialization was failing due to non-running loop.
1 parent e3d5590 commit 52bebac

7 files changed

Lines changed: 68 additions & 48 deletions

File tree

docs/reference/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ Test Facilities`_ documentation for more details of each function.
134134
If your application uses `asyncio` then this module gives an alternative
135135
dispatcher for caput requests.
136136

137+
.. autoclass:: softioc.asyncio_dispatcher.AsyncioDispatcher
138+
137139
.. automodule:: softioc.builder
138140

139141
Creating Records: `softioc.builder`

softioc/asyncio_dispatcher.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ class AsyncioDispatcher:
88
def __init__(self, loop=None):
99
"""A dispatcher for `asyncio` based IOCs, suitable to be passed to
1010
`softioc.iocInit`. Means that `on_update` callback functions can be
11-
async. If loop is None, will run an Event Loop in a thread when created.
11+
async.
12+
13+
If a ``loop`` is provided it must already be running. Otherwise a new
14+
Event Loop will be created and run in a dedicated thread.
1215
"""
13-
#: `asyncio` event loop that the callbacks will run under.
14-
self.loop = loop
1516
if loop is None:
1617
# Make one and run it in a background thread
1718
self.loop = asyncio.new_event_loop()
@@ -26,6 +27,10 @@ def aioJoin(worker=worker, loop=self.loop):
2627
loop.call_soon_threadsafe(loop.stop)
2728
worker.join()
2829
worker.start()
30+
elif not loop.is_running():
31+
raise ValueError("Provided asyncio event loop is not running")
32+
else:
33+
self.loop = loop
2934

3035
def __call__(self, func, *args):
3136
async def async_wrapper():

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def kill(self):
4949
if self.proc.returncode is None:
5050
# still running, kill it and print the output
5151
self.proc.kill()
52-
out, err = self.proc.communicate()
52+
out, err = self.proc.communicate(timeout=TIMEOUT)
5353
print(out.decode())
5454
print(err.decode())
5555

tests/sim_asyncio_ioc.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@
99
from conftest import ADDRESS, select_and_recv
1010

1111
if __name__ == "__main__":
12-
# Being run as an IOC, so parse args and set prefix
13-
parser = ArgumentParser()
14-
parser.add_argument('prefix', help="The PV prefix for the records")
15-
parsed_args = parser.parse_args()
16-
builder.SetDeviceName(parsed_args.prefix)
17-
18-
import sim_records
19-
2012
with Client(ADDRESS) as conn:
13+
# Being run as an IOC, so parse args and set prefix
14+
parser = ArgumentParser()
15+
parser.add_argument('prefix', help="The PV prefix for the records")
16+
parsed_args = parser.parse_args()
17+
builder.SetDeviceName(parsed_args.prefix)
18+
19+
import sim_records
2120

2221
async def callback(value):
2322
# Set the ao value, which will trigger on_update for it

tests/sim_asyncio_ioc_override.py

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,42 @@
77
from tempfile import NamedTemporaryFile
88
from argparse import ArgumentParser
99
from multiprocessing.connection import Client
10+
import threading
1011

1112
from softioc import softioc, builder, asyncio_dispatcher
1213

1314
from conftest import ADDRESS, select_and_recv
1415

1516
if __name__ == "__main__":
16-
# Being run as an IOC, so parse args and set prefix
17-
parser = ArgumentParser()
18-
parser.add_argument('prefix', help="The PV prefix for the records")
19-
parsed_args = parser.parse_args()
20-
builder.SetDeviceName(parsed_args.prefix)
21-
22-
# Load the base records without DTYP fields
23-
with open(Path(__file__).parent / "hw_records.db", "rb") as inp:
24-
with NamedTemporaryFile(suffix='.db', delete=False) as out:
25-
for line in inp.readlines():
26-
if not re.match(rb"\s*field\s*\(\s*DTYP", line):
27-
out.write(line)
28-
softioc.dbLoadDatabase(
29-
out.name, substitutions=f"device={parsed_args.prefix}")
30-
os.unlink(out.name)
31-
32-
# Override DTYPE and OUT, and provide a callback
33-
gain = builder.boolOut("GAIN", on_update=print)
34-
softioc.devIocStats(parsed_args.prefix)
35-
36-
# Run the IOC
37-
builder.LoadDatabase()
38-
event_loop = asyncio.get_event_loop()
39-
softioc.iocInit(asyncio_dispatcher.AsyncioDispatcher(event_loop))
40-
4117
with Client(ADDRESS) as conn:
18+
# Being run as an IOC, so parse args and set prefix
19+
parser = ArgumentParser()
20+
parser.add_argument('prefix', help="The PV prefix for the records")
21+
parsed_args = parser.parse_args()
22+
builder.SetDeviceName(parsed_args.prefix)
23+
24+
# Load the base records without DTYP fields
25+
with open(Path(__file__).parent / "hw_records.db", "rb") as inp:
26+
with NamedTemporaryFile(suffix='.db', delete=False) as out:
27+
for line in inp.readlines():
28+
if not re.match(rb"\s*field\s*\(\s*DTYP", line):
29+
out.write(line)
30+
softioc.dbLoadDatabase(
31+
out.name, substitutions=f"device={parsed_args.prefix}")
32+
os.unlink(out.name)
33+
34+
# Override DTYPE and OUT, and provide a callback
35+
gain = builder.boolOut("GAIN", on_update=print)
36+
softioc.devIocStats(parsed_args.prefix)
37+
38+
# Run the IOC
39+
builder.LoadDatabase()
40+
event_loop = asyncio.get_event_loop()
41+
worker = threading.Thread(target=event_loop.run_forever)
42+
worker.daemon = True
43+
worker.start()
44+
softioc.iocInit(asyncio_dispatcher.AsyncioDispatcher(event_loop))
45+
4246
conn.send("R") # "Ready"
4347

4448
# Make sure coverage is written on epicsExit

tests/sim_cothread_ioc.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@
77
from conftest import ADDRESS, select_and_recv
88

99
if __name__ == "__main__":
10-
import cothread
10+
with Client(ADDRESS) as conn:
11+
import cothread
1112

12-
# Being run as an IOC, so parse args and set prefix
13-
parser = ArgumentParser()
14-
parser.add_argument('prefix', help="The PV prefix for the records")
15-
parsed_args = parser.parse_args()
16-
builder.SetDeviceName(parsed_args.prefix)
13+
# Being run as an IOC, so parse args and set prefix
14+
parser = ArgumentParser()
15+
parser.add_argument('prefix', help="The PV prefix for the records")
16+
parsed_args = parser.parse_args()
17+
builder.SetDeviceName(parsed_args.prefix)
1718

18-
import sim_records
19+
import sim_records
1920

20-
# Run the IOC
21-
builder.LoadDatabase()
22-
softioc.iocInit()
21+
# Run the IOC
22+
builder.LoadDatabase()
23+
softioc.iocInit()
2324

24-
with Client(ADDRESS) as conn:
2525
conn.send("R") # "Ready"
2626

2727
select_and_recv(conn, "D") # "Done"

tests/test_asyncio.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import asyncio
12
import pytest
23
import sys
34

45
from multiprocessing.connection import Listener
56

67
from conftest import requires_cothread, ADDRESS, select_and_recv
78

9+
from softioc.asyncio_dispatcher import AsyncioDispatcher
10+
811
@pytest.mark.asyncio
912
async def test_asyncio_ioc(asyncio_ioc):
1013
import asyncio
@@ -112,3 +115,10 @@ async def test_asyncio_ioc_override(asyncio_ioc_override):
112115
print("Out:", out)
113116
print("Err:", err)
114117
raise
118+
119+
def test_asyncio_dispatcher_event_loop():
120+
"""Test that passing a non-running event loop to the AsyncioDispatcher
121+
raises an exception"""
122+
event_loop = asyncio.get_event_loop()
123+
with pytest.raises(ValueError):
124+
AsyncioDispatcher(loop=event_loop)

0 commit comments

Comments
 (0)