Skip to content

Commit a558b5a

Browse files
authored
Merge pull request #28 from dls-controls/tickit-changes
Changes required for integration into tickit simulation framework
2 parents 6caaf1a + b2cbcff commit a558b5a

File tree

10 files changed

+205
-676
lines changed

10 files changed

+205
-676
lines changed

softioc/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from epicscorelibs.ioc import \
66
iocshRegisterCommon, registerRecordDeviceDriver, pdbbase
77

8+
# Do this as early as possible, in case we happen to use cothread
9+
# This will set the CATOOLS_LIBCA_PATH environment variable in case we use
10+
# cothread.catools. It works even if we don't have cothread installed
11+
import epicscorelibs.path.cothread # noqa
12+
813
# This import will also pull in the extension, which is needed
914
# before we call iocshRegisterCommon
1015
from .imports import dbLoadDatabase

softioc/asyncio_dispatcher.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,18 @@
33
import threading
44

55

6-
class AsyncioDispatcher(threading.Thread):
7-
"""A dispatcher for `asyncio` based IOCs. Means that `on_update` callback
8-
functions can be async. Will run an Event Loop in a thread when
9-
created.
10-
"""
11-
def __init__(self):
12-
"""Create an AsyncioDispatcher suitable to be used by
13-
`softioc.iocInit`."""
14-
# Docstring specified to suppress threading.Thread's docstring, which
15-
# would otherwise be inherited by this method and be misleading.
16-
super().__init__()
6+
class AsyncioDispatcher:
7+
def __init__(self, loop=None):
8+
"""A dispatcher for `asyncio` based IOCs, suitable to be passed to
9+
`softioc.iocInit`. Means that `on_update` callback functions can be
10+
async. If loop is None, will run an Event Loop in a thread when created.
11+
"""
1712
#: `asyncio` event loop that the callbacks will run under.
18-
self.loop = asyncio.new_event_loop()
19-
self.start()
20-
21-
def run(self):
22-
self.loop.run_forever()
13+
self.loop = loop
14+
if loop is None:
15+
# Make one and run it in a background thread
16+
self.loop = asyncio.new_event_loop()
17+
threading.Thread(target=self.loop.run_forever).start()
2318

2419
def __call__(self, func, *args):
2520
async def async_wrapper():

softioc/builder.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
PythonDevice = pythonSoftIoc.PythonDevice()
1212

1313

14-
1514
# ----------------------------------------------------------------------------
1615
# Wrappers for PythonDevice record constructors.
1716
#

softioc/softioc.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import sys
33
from ctypes import *
4+
from tempfile import NamedTemporaryFile
45

56
from epicsdbbuilder.recordset import recordset
67

@@ -245,33 +246,43 @@ def dbLoadDatabase(database, path = None, substitutions = None):
245246
'''Loads a database file and applies any given substitutions.'''
246247
imports.dbLoadDatabase(database, path, substitutions)
247248

248-
def _add_records_from_file(dir, file, macros):
249-
# This is very naive, for instance macros are added to but never removed,
250-
# but it works well enough for devIocStats
251-
with open(os.path.join(dir, file)) as f:
249+
250+
def _add_records_from_file(dirname, file, substitutions):
251+
# This is very naive, it loads all includes before their parents which
252+
# possibly can put them out of order, but it works well enough for
253+
# devIocStats
254+
with open(os.path.join(dirname, file)) as f:
255+
lines, include_subs = [], ""
252256
for line in f.readlines():
253257
line = line.rstrip()
254258
if line.startswith('substitute'):
255-
# substitute "QUEUE=scanOnce, QUEUE_CAPS=SCANONCE
256-
for sub in line.split('"')[1].split(','):
257-
k, v = sub.split('=')
258-
macros[k.strip()] = v.strip()
259+
# substitute "QUEUE=scanOnce, QUEUE_CAPS=SCANONCE"
260+
# keep hold of the substitutions
261+
include_subs = line.split('"')[1]
259262
elif line.startswith('include'):
260263
# include "iocQueue.db"
261-
_add_records_from_file(dir, line.split('"')[1], macros)
264+
subs = substitutions
265+
if substitutions and include_subs:
266+
subs = substitutions + ", " + include_subs
267+
else:
268+
subs = substitutions + include_subs
269+
_add_records_from_file(dirname, line.split('"')[1], subs)
262270
else:
263271
# A record line
264-
for k, v in macros.items():
265-
line = line.replace('$(%s)' % k, v)
266-
recordset.AddBodyLine(line)
272+
lines.append(line)
273+
# Write a tempfile and load it
274+
with NamedTemporaryFile(suffix='.db', delete=False) as f:
275+
f.write(os.linesep.join(lines).encode())
276+
dbLoadDatabase(f.name, substitutions=substitutions)
277+
os.unlink(f.name)
267278

268279

269280
def devIocStats(ioc_name):
270281
'''This will load a template for the devIocStats library with the specified
271282
IOC name. This should be called before `iocInit`'''
272-
macros = dict(IOCNAME=ioc_name, TODFORMAT='%m/%d/%Y %H:%M:%S')
283+
substitutions = 'IOCNAME=' + ioc_name + ', TODFORMAT=%m/%d/%Y %H:%M:%S'
273284
iocstats_dir = os.path.join(os.path.dirname(__file__), 'iocStatsDb')
274-
_add_records_from_file(iocstats_dir, 'ioc.template', macros)
285+
_add_records_from_file(iocstats_dir, 'ioc.template', substitutions)
275286

276287

277288
def interactive_ioc(context = {}, call_exit = True):

tests/conftest.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,67 @@
1+
import atexit
2+
import os
3+
import random
4+
import string
5+
import subprocess
16
import sys
27

8+
import pytest
9+
310
if sys.version_info < (3,):
411
# Python2 has no asyncio, so ignore these tests
5-
collect_ignore = ["test_asyncio.py", "sim_asyncio_ioc.py"]
12+
collect_ignore = [
13+
"test_asyncio.py", "sim_asyncio_ioc.py", "sim_asyncio_ioc_override.py"
14+
]
15+
16+
class SubprocessIOC:
17+
def __init__(self, ioc_py):
18+
self.pv_prefix = "".join(
19+
random.choice(string.ascii_uppercase) for _ in range(12)
20+
)
21+
sim_ioc = os.path.join(os.path.dirname(__file__), ioc_py)
22+
cmd = [sys.executable, sim_ioc, self.pv_prefix]
23+
self.proc = subprocess.Popen(
24+
cmd, stdin=subprocess.PIPE,
25+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
26+
27+
def kill(self):
28+
if self.proc.returncode is None:
29+
# still running, kill it and print the output
30+
self.proc.kill()
31+
out, err = self.proc.communicate()
32+
print(out.decode())
33+
print(err.decode())
34+
35+
36+
@pytest.fixture
37+
def cothread_ioc():
38+
ioc = SubprocessIOC("sim_cothread_ioc.py")
39+
yield ioc
40+
ioc.kill()
41+
42+
43+
def aioca_cleanup():
44+
from aioca import purge_channel_caches, _catools
45+
# Unregister the aioca atexit handler as it conflicts with the one installed
46+
# by cothread. If we don't do this we get a seg fault. This is not a problem
47+
# in production as we won't mix aioca and cothread, but we do mix them in
48+
# the tests so need to do this.
49+
atexit.unregister(_catools._catools_atexit)
50+
# purge the channels before the event loop goes
51+
purge_channel_caches()
52+
53+
54+
@pytest.fixture
55+
def asyncio_ioc():
56+
ioc = SubprocessIOC("sim_asyncio_ioc.py")
57+
yield ioc
58+
ioc.kill()
59+
aioca_cleanup()
60+
61+
62+
@pytest.fixture
63+
def asyncio_ioc_override():
64+
ioc = SubprocessIOC("sim_asyncio_ioc_override.py")
65+
yield ioc
66+
ioc.kill()
67+
aioca_cleanup()

0 commit comments

Comments
 (0)