Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions softioc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from epicscorelibs.ioc import \
iocshRegisterCommon, registerRecordDeviceDriver, pdbbase

# Do this as early as possible, in case we happen to use cothread
# This will set the CATOOLS_LIBCA_PATH environment variable in case we use
# cothread.catools. It works even if we don't have cothread installed
import epicscorelibs.path.cothread # noqa
Comment thread
thomascobb marked this conversation as resolved.

# This import will also pull in the extension, which is needed
# before we call iocshRegisterCommon
from .imports import dbLoadDatabase
Expand Down
27 changes: 11 additions & 16 deletions softioc/asyncio_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,18 @@
import threading


class AsyncioDispatcher(threading.Thread):
"""A dispatcher for `asyncio` based IOCs. Means that `on_update` callback
functions can be async. Will run an Event Loop in a thread when
created.
"""
def __init__(self):
"""Create an AsyncioDispatcher suitable to be used by
`softioc.iocInit`."""
# Docstring specified to suppress threading.Thread's docstring, which
# would otherwise be inherited by this method and be misleading.
super().__init__()
class AsyncioDispatcher:
def __init__(self, loop=None):
"""A dispatcher for `asyncio` based IOCs, suitable to be passed to
`softioc.iocInit`. Means that `on_update` callback functions can be
async. If loop is None, will run an Event Loop in a thread when created.
"""
#: `asyncio` event loop that the callbacks will run under.
self.loop = asyncio.new_event_loop()
self.start()

def run(self):
self.loop.run_forever()
self.loop = loop
if loop is None:
# Make one and run it in a background thread
self.loop = asyncio.new_event_loop()
threading.Thread(target=self.loop.run_forever).start()

def __call__(self, func, *args):
async def async_wrapper():
Expand Down
1 change: 0 additions & 1 deletion softioc/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
PythonDevice = pythonSoftIoc.PythonDevice()



# ----------------------------------------------------------------------------
# Wrappers for PythonDevice record constructors.
#
Expand Down
39 changes: 25 additions & 14 deletions softioc/softioc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import sys
from ctypes import *
from tempfile import NamedTemporaryFile

from epicsdbbuilder.recordset import recordset

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

def _add_records_from_file(dir, file, macros):
# This is very naive, for instance macros are added to but never removed,
# but it works well enough for devIocStats
with open(os.path.join(dir, file)) as f:

def _add_records_from_file(dirname, file, substitutions):
# This is very naive, it loads all includes before their parents which
# possibly can put them out of order, but it works well enough for
# devIocStats
with open(os.path.join(dirname, file)) as f:
lines, include_subs = [], ""
for line in f.readlines():
line = line.rstrip()
if line.startswith('substitute'):
# substitute "QUEUE=scanOnce, QUEUE_CAPS=SCANONCE
for sub in line.split('"')[1].split(','):
k, v = sub.split('=')
macros[k.strip()] = v.strip()
# substitute "QUEUE=scanOnce, QUEUE_CAPS=SCANONCE"
# keep hold of the substitutions
include_subs = line.split('"')[1]
elif line.startswith('include'):
# include "iocQueue.db"
_add_records_from_file(dir, line.split('"')[1], macros)
subs = substitutions
if substitutions and include_subs:
subs = substitutions + ", " + include_subs
else:
subs = substitutions + include_subs
_add_records_from_file(dirname, line.split('"')[1], subs)
else:
# A record line
for k, v in macros.items():
line = line.replace('$(%s)' % k, v)
recordset.AddBodyLine(line)
lines.append(line)
# Write a tempfile and load it
with NamedTemporaryFile(suffix='.db', delete=False) as f:
f.write(os.linesep.join(lines).encode())
dbLoadDatabase(f.name, substitutions=substitutions)
os.unlink(f.name)


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


def interactive_ioc(context = {}, call_exit = True):
Expand Down
64 changes: 63 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
import atexit
import os
import random
import string
import subprocess
import sys

import pytest

if sys.version_info < (3,):
# Python2 has no asyncio, so ignore these tests
collect_ignore = ["test_asyncio.py", "sim_asyncio_ioc.py"]
collect_ignore = [
"test_asyncio.py", "sim_asyncio_ioc.py", "sim_asyncio_ioc_override.py"
]

class SubprocessIOC:
def __init__(self, ioc_py):
self.pv_prefix = "".join(
random.choice(string.ascii_uppercase) for _ in range(12)
)
sim_ioc = os.path.join(os.path.dirname(__file__), ioc_py)
cmd = [sys.executable, sim_ioc, self.pv_prefix]
self.proc = subprocess.Popen(
cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)

def kill(self):
if self.proc.returncode is None:
# still running, kill it and print the output
self.proc.kill()
out, err = self.proc.communicate()
print(out.decode())
print(err.decode())


@pytest.fixture
def cothread_ioc():
ioc = SubprocessIOC("sim_cothread_ioc.py")
yield ioc
ioc.kill()


def aioca_cleanup():
from aioca import purge_channel_caches, _catools
# Unregister the aioca atexit handler as it conflicts with the one installed
# by cothread. If we don't do this we get a seg fault. This is not a problem
# in production as we won't mix aioca and cothread, but we do mix them in
# the tests so need to do this.
atexit.unregister(_catools._catools_atexit)
# purge the channels before the event loop goes
purge_channel_caches()


@pytest.fixture
def asyncio_ioc():
ioc = SubprocessIOC("sim_asyncio_ioc.py")
yield ioc
ioc.kill()
aioca_cleanup()


@pytest.fixture
def asyncio_ioc_override():
ioc = SubprocessIOC("sim_asyncio_ioc_override.py")
yield ioc
ioc.kill()
aioca_cleanup()
Loading