From 8b7166729945affca91ba9d67214b305a12f8a9b Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 12 Aug 2021 11:48:17 +0100 Subject: [PATCH 1/7] Make sure we use epicscorelibs libca in cothread --- softioc/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/softioc/__init__.py b/softioc/__init__.py index 7c6f6360..b9802106 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -5,6 +5,10 @@ from epicscorelibs.ioc import \ iocshRegisterCommon, registerRecordDeviceDriver, pdbbase +# Do this as early as possible, in case we happen to use cothread +# as it's harmless if we don't +import epicscorelibs.path.cothread # noqa + # This import will also pull in the extension, which is needed # before we call iocshRegisterCommon from .imports import dbLoadDatabase From 6f0fe54a3df33faae37a5b9fdf216c89f398d9ba Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 12 Aug 2021 11:48:56 +0100 Subject: [PATCH 2/7] Allow AsyncioDispatcher to take existing event loop --- softioc/asyncio_dispatcher.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index c85ceefb..aa10fe0e 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -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(): From c33d49fcac10637054929010ade5b77546d2542a Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 12 Aug 2021 11:50:07 +0100 Subject: [PATCH 3/7] Add ability to add arbitrary lines to Db file and test --- softioc/builder.py | 16 ++++++- softioc/softioc.py | 11 ++--- tests/conftest.py | 27 +++++++++++- tests/hw_records.db | 8 ++++ tests/sim_asyncio_ioc_override.py | 32 ++++++++++++++ tests/test_asyncio.py | 72 +++++++++++++++++++------------ tests/test_cothread.py | 24 ++--------- 7 files changed, 133 insertions(+), 57 deletions(-) create mode 100644 tests/hw_records.db create mode 100644 tests/sim_asyncio_ioc_override.py diff --git a/softioc/builder.py b/softioc/builder.py index 0fa1447d..b1f2591c 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -1,16 +1,20 @@ import os import numpy -from .softioc import dbLoadDatabase from epicsdbbuilder import * +from epicsdbbuilder.recordset import recordset InitialiseDbd() LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd')) -from . import pythonSoftIoc # noqa +from . import pythonSoftIoc, imports # noqa PythonDevice = pythonSoftIoc.PythonDevice() +def dbLoadDatabase(database, path = None, substitutions = None): + '''Loads a database file and applies any given substitutions.''' + imports.dbLoadDatabase(database, path, substitutions) + # ---------------------------------------------------------------------------- # Wrappers for PythonDevice record constructors. @@ -181,6 +185,14 @@ def WaveformOut(name, *value, **fields): _DatabaseWritten = False +def AddDatabaseLine(line, macros={}): + '''Add a single line to the produced database, substituting $(k) for v for + each entry in macros''' + for k, v in macros.items(): + line = line.replace("$(%s)" % k, v) + recordset.AddBodyLine(line) + + def LoadDatabase(): '''This should be called after all the builder records have been created, but before calling iocInit(). The database is loaded into EPICS memory, diff --git a/softioc/softioc.py b/softioc/softioc.py index 27d08f2b..ab064cd5 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -4,7 +4,7 @@ from epicsdbbuilder.recordset import recordset -from . import imports, device +from . import imports, device, builder __all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc'] @@ -240,10 +240,9 @@ def __call__(self): exit = Exiter() command_names.append('exit') +# For backwards compatibility +dbLoadDatabase = builder.dbLoadDatabase -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, @@ -261,9 +260,7 @@ def _add_records_from_file(dir, file, macros): _add_records_from_file(dir, line.split('"')[1], macros) else: # A record line - for k, v in macros.items(): - line = line.replace('$(%s)' % k, v) - recordset.AddBodyLine(line) + builder.AddDatabaseLine(line, macros) def devIocStats(ioc_name): diff --git a/tests/conftest.py b/tests/conftest.py index 382e0951..01650e25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,30 @@ +import os +import random +import string +import subprocess import sys 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_overide.py" + ] + +PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + + +class SubprocessIOC: + def __init__(self, ioc_py): + sim_ioc = os.path.join(os.path.dirname(__file__), ioc_py) + cmd = [sys.executable, sim_ioc, 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()) diff --git a/tests/hw_records.db b/tests/hw_records.db new file mode 100644 index 00000000..c4287ec1 --- /dev/null +++ b/tests/hw_records.db @@ -0,0 +1,8 @@ +record(bo, "$(device):GAIN") { + field(DTYP, "Hy8001") + field(OMSL, "supervisory") + field(OUT, "#C1 S0 @") + field(DESC, "Gain bit 1") + field(ZNAM, "Off") + field(ONAM, "On") +} diff --git a/tests/sim_asyncio_ioc_override.py b/tests/sim_asyncio_ioc_override.py new file mode 100644 index 00000000..58f6932e --- /dev/null +++ b/tests/sim_asyncio_ioc_override.py @@ -0,0 +1,32 @@ +from argparse import ArgumentParser + +import asyncio +import os +import re +from pathlib import Path + +from softioc import softioc, builder, asyncio_dispatcher + + +if __name__ == "__main__": + # Being run as an IOC, so parse args and set prefix + parser = ArgumentParser() + parser.add_argument('prefix', help="The PV prefix for the records") + parsed_args = parser.parse_args() + builder.SetDeviceName(parsed_args.prefix) + + # Load the base records without DTYP fields + macros = dict(device=parsed_args.prefix) + with open(Path(__file__).parent / "hw_records.db") as f: + for line in f.readlines(): + if not re.match(r"\s*field\s*\(\s*DTYP", line): + builder.AddDatabaseLine(line, macros) + + # Override DTYPE and OUT, and provide a callback + gain = builder.boolOut("GAIN", on_update=print) + + # Run the IOC + builder.LoadDatabase() + event_loop = asyncio.get_event_loop() + softioc.iocInit(asyncio_dispatcher.AsyncioDispatcher(event_loop)) + event_loop.run_forever() diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index d689201e..38b21dac 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -1,45 +1,34 @@ # Will be ignored on Python2 by conftest.py settings -import random -import string -import subprocess -import sys -import os import atexit +import signal import pytest -import time +from tests.conftest import SubprocessIOC, PV_PREFIX -PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + +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(): - sim_ioc = os.path.join(os.path.dirname(__file__), "sim_asyncio_ioc.py") - cmd = [sys.executable, sim_ioc, PV_PREFIX] - proc = subprocess.Popen( - cmd, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - yield proc - # purge the channels before the event loop goes - from aioca import purge_channel_caches - purge_channel_caches() - if proc.returncode is None: - # still running, kill it and print the output - proc.kill() - out, err = proc.communicate() - print(out.decode()) - print(err.decode(), file=sys.stderr) + ioc = SubprocessIOC("sim_asyncio_ioc.py") + yield ioc.proc + ioc.kill() + aioca_cleanup() @pytest.mark.asyncio async def test_asyncio_ioc(asyncio_ioc): import asyncio from aioca import caget, caput, camonitor, CANothing, _catools, FORMAT_TIME - # 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) # Start assert (await caget(PV_PREFIX + ":UPTIME")).startswith("00:00:0") @@ -86,3 +75,32 @@ async def test_asyncio_ioc(asyncio_ioc): assert 'Starting iocInit' in err assert 'iocRun: All initialization complete' in err assert '(InteractiveConsole)' in err + + +@pytest.fixture +def asyncio_ioc_override(): + ioc = SubprocessIOC("sim_asyncio_ioc_override.py") + yield ioc.proc + ioc.kill() + aioca_cleanup() + + +@pytest.mark.asyncio +async def test_asyncio_ioc_override(asyncio_ioc_override): + from aioca import caget, caput + + # Gain bo + assert (await caget(PV_PREFIX + ":GAIN")) == 0 + await caput(PV_PREFIX + ":GAIN", "On", wait=True) + assert (await caget(PV_PREFIX + ":GAIN")) == 1 + + # Stop + asyncio_ioc_override.send_signal(signal.SIGINT) + # check closed and output + out, err = asyncio_ioc_override.communicate() + out = out.decode() + err = err.decode() + # check closed and output + assert '1' in out + assert 'Starting iocInit' in err + assert 'iocRun: All initialization complete' in err diff --git a/tests/test_cothread.py b/tests/test_cothread.py index a722edab..318c6c9d 100644 --- a/tests/test_cothread.py +++ b/tests/test_cothread.py @@ -1,13 +1,8 @@ -import random -import string -import subprocess import sys -import os import signal +from tests.conftest import SubprocessIOC, PV_PREFIX import pytest -PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) - if sys.platform.startswith("win"): pytest.skip("Cothread doesn't work on windows", allow_module_level=True) @@ -15,23 +10,12 @@ @pytest.fixture def cothread_ioc(): - sim_ioc = os.path.join(os.path.dirname(__file__), "sim_cothread_ioc.py") - cmd = [sys.executable, sim_ioc, PV_PREFIX] - proc = subprocess.Popen( - cmd, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - yield proc - if proc.returncode is None: - # still running, kill it and print the output - proc.kill() - out, err = proc.communicate() - print(out.decode()) - print(err.decode()) - + ioc = SubprocessIOC("sim_cothread_ioc.py") + yield ioc.proc + ioc.kill() def test_cothread_ioc(cothread_ioc): - import epicscorelibs.path.cothread import cothread from cothread.catools import ca_nothing, caget, caput, camonitor From df90f39827cce536fea8706b86c8ee30613c5b55 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 12 Aug 2021 12:14:08 +0100 Subject: [PATCH 4/7] Move test fixtures around to avoid imports --- tests/conftest.py | 47 ++++++++++++++++++++++++++---- tests/test_asyncio.py | 65 ++++++++++++------------------------------ tests/test_cothread.py | 30 ++++++++----------- 3 files changed, 73 insertions(+), 69 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 01650e25..11d5afff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,22 +1,25 @@ +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", "sim_asyncio_ioc_overide.py" + "test_asyncio.py", "sim_asyncio_ioc.py", "sim_asyncio_ioc_override.py" ] -PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) - - 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, PV_PREFIX] + cmd = [sys.executable, sim_ioc, self.pv_prefix] self.proc = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -28,3 +31,37 @@ def kill(self): 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() diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 38b21dac..a76959a3 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -1,28 +1,7 @@ # Will be ignored on Python2 by conftest.py settings -import atexit import signal import pytest -from tests.conftest import SubprocessIOC, PV_PREFIX - - -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.proc - ioc.kill() - aioca_cleanup() @pytest.mark.asyncio @@ -31,73 +10,67 @@ async def test_asyncio_ioc(asyncio_ioc): from aioca import caget, caput, camonitor, CANothing, _catools, FORMAT_TIME # Start - assert (await caget(PV_PREFIX + ":UPTIME")).startswith("00:00:0") + pre = asyncio_ioc.pv_prefix + assert (await caget(pre + ":UPTIME")).startswith("00:00:0") # WAVEFORM - await caput(PV_PREFIX + ":SINN", 4, wait=True) + await caput(pre + ":SINN", 4, wait=True) q = asyncio.Queue() - m = camonitor(PV_PREFIX + ":SIN", q.put, notify_disconnect=True) + m = camonitor(pre + ":SIN", q.put, notify_disconnect=True) assert len(await asyncio.wait_for(q.get(), 1)) == 4 # AO - ao_val = await caget(PV_PREFIX + ":ALARM", format=FORMAT_TIME) + ao_val = await caget(pre + ":ALARM", format=FORMAT_TIME) assert ao_val == 0 assert ao_val.severity == 3 # INVALID assert ao_val.status == 17 # UDF - await caput(PV_PREFIX + ":ALARM", 3, wait=True) + await caput(pre + ":ALARM", 3, wait=True) await asyncio.sleep(0.1) - ai_val = await caget(PV_PREFIX + ":AI", format=FORMAT_TIME) + ai_val = await caget(pre + ":AI", format=FORMAT_TIME) assert ai_val == 23.45 assert ai_val.severity == 0 assert ai_val.status == 0 await asyncio.sleep(0.8) - ai_val = await caget(PV_PREFIX + ":AI", format=FORMAT_TIME) + ai_val = await caget(pre + ":AI", format=FORMAT_TIME) assert ai_val == 23.45 assert ai_val.severity == 3 assert ai_val.status == 7 # STATE_ALARM # Check pvaccess works from p4p.client.asyncio import Context with Context("pva") as ctx: - assert await ctx.get(PV_PREFIX + ":AI") == 23.45 + assert await ctx.get(pre + ":AI") == 23.45 # Wait for a bit longer for the print output to flush await asyncio.sleep(2) # Stop - out, err = asyncio_ioc.communicate(b"exit\n", timeout=5) + out, err = asyncio_ioc.proc.communicate(b"exit\n", timeout=5) out = out.decode() err = err.decode() # Disconnect assert isinstance(await asyncio.wait_for(q.get(), 10), CANothing) m.close() # check closed and output - assert "%s:SINN.VAL 1024 -> 4" % PV_PREFIX in out + assert "%s:SINN.VAL 1024 -> 4" % pre in out assert 'update_sin_wf 4' in out - assert "%s:ALARM.VAL 0 -> 3" % PV_PREFIX in out - assert 'on_update %s:AO : 3.0' % PV_PREFIX in out + assert "%s:ALARM.VAL 0 -> 3" % pre in out + assert 'on_update %s:AO : 3.0' % pre in out assert 'async update 3.0 (23.45)' in out assert 'Starting iocInit' in err assert 'iocRun: All initialization complete' in err assert '(InteractiveConsole)' in err -@pytest.fixture -def asyncio_ioc_override(): - ioc = SubprocessIOC("sim_asyncio_ioc_override.py") - yield ioc.proc - ioc.kill() - aioca_cleanup() - - @pytest.mark.asyncio async def test_asyncio_ioc_override(asyncio_ioc_override): from aioca import caget, caput # Gain bo - assert (await caget(PV_PREFIX + ":GAIN")) == 0 - await caput(PV_PREFIX + ":GAIN", "On", wait=True) - assert (await caget(PV_PREFIX + ":GAIN")) == 1 + pre = asyncio_ioc_override.pv_prefix + assert (await caget(pre + ":GAIN")) == 0 + await caput(pre + ":GAIN", "On", wait=True) + assert (await caget(pre + ":GAIN")) == 1 # Stop - asyncio_ioc_override.send_signal(signal.SIGINT) + asyncio_ioc_override.proc.send_signal(signal.SIGINT) # check closed and output - out, err = asyncio_ioc_override.communicate() + out, err = asyncio_ioc_override.proc.communicate() out = out.decode() err = err.decode() # check closed and output diff --git a/tests/test_cothread.py b/tests/test_cothread.py index 318c6c9d..06ccd93b 100644 --- a/tests/test_cothread.py +++ b/tests/test_cothread.py @@ -1,6 +1,5 @@ import sys import signal -from tests.conftest import SubprocessIOC, PV_PREFIX import pytest @@ -8,47 +7,42 @@ pytest.skip("Cothread doesn't work on windows", allow_module_level=True) -@pytest.fixture -def cothread_ioc(): - ioc = SubprocessIOC("sim_cothread_ioc.py") - yield ioc.proc - ioc.kill() - def test_cothread_ioc(cothread_ioc): import cothread from cothread.catools import ca_nothing, caget, caput, camonitor + pre = cothread_ioc.pv_prefix # Start - assert caget(PV_PREFIX + ":UPTIME").startswith("00:00:0") + assert caget(pre + ":UPTIME").startswith("00:00:0") # WAVEFORM - caput(PV_PREFIX + ":SINN", 4, wait=True) + caput(pre + ":SINN", 4, wait=True) q = cothread.EventQueue() - m = camonitor(PV_PREFIX + ":SIN", q.Signal, notify_disconnect=True) + m = camonitor(pre + ":SIN", q.Signal, notify_disconnect=True) assert len(q.Wait(1)) == 4 # STRINGOUT - assert caget(PV_PREFIX + ":STRINGOUT") == "watevah" - caput(PV_PREFIX + ":STRINGOUT", "something", wait=True) - assert caget(PV_PREFIX + ":STRINGOUT") == "something" + assert caget(pre + ":STRINGOUT") == "watevah" + caput(pre + ":STRINGOUT", "something", wait=True) + assert caget(pre + ":STRINGOUT") == "something" # Check pvaccess works from p4p.client.cothread import Context with Context("pva") as ctx: - assert ctx.get(PV_PREFIX + ":STRINGOUT") == "something" + assert ctx.get(pre + ":STRINGOUT") == "something" # Wait for a bit longer for the print output to flush cothread.Sleep(2) # Stop - cothread_ioc.send_signal(signal.SIGINT) + cothread_ioc.proc.send_signal(signal.SIGINT) # Disconnect assert isinstance(q.Wait(10), ca_nothing) m.close() # check closed and output - out, err = cothread_ioc.communicate() + out, err = cothread_ioc.proc.communicate() out = out.decode() err = err.decode() # check closed and output - assert "%s:SINN.VAL 1024 -> 4" % PV_PREFIX in out + assert "%s:SINN.VAL 1024 -> 4" % pre in out assert 'update_sin_wf 4' in out - assert "%s:STRINGOUT.VAL watevah -> something" % PV_PREFIX in out + assert "%s:STRINGOUT.VAL watevah -> something" % pre in out assert 'on_update \'something\'' in out assert 'Starting iocInit' in err assert 'iocRun: All initialization complete' in err From 8b868ebc3b4ca53e5a46f9c142078af8371f693e Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 12 Aug 2021 12:27:24 +0100 Subject: [PATCH 5/7] Make tests work on windows --- tests/sim_asyncio_ioc_override.py | 1 + tests/test_asyncio.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/sim_asyncio_ioc_override.py b/tests/sim_asyncio_ioc_override.py index 58f6932e..955fc817 100644 --- a/tests/sim_asyncio_ioc_override.py +++ b/tests/sim_asyncio_ioc_override.py @@ -24,6 +24,7 @@ # Override DTYPE and OUT, and provide a callback gain = builder.boolOut("GAIN", on_update=print) + softioc.devIocStats(parsed_args.prefix) # Run the IOC builder.LoadDatabase() diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index a76959a3..886b368f 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -1,13 +1,12 @@ # Will be ignored on Python2 by conftest.py settings -import signal import pytest @pytest.mark.asyncio async def test_asyncio_ioc(asyncio_ioc): import asyncio - from aioca import caget, caput, camonitor, CANothing, _catools, FORMAT_TIME + from aioca import caget, caput, camonitor, CANothing, FORMAT_TIME # Start pre = asyncio_ioc.pv_prefix @@ -68,7 +67,7 @@ async def test_asyncio_ioc_override(asyncio_ioc_override): assert (await caget(pre + ":GAIN")) == 1 # Stop - asyncio_ioc_override.proc.send_signal(signal.SIGINT) + await caput(pre + ":SYSRESET", 1) # check closed and output out, err = asyncio_ioc_override.proc.communicate() out = out.decode() @@ -77,3 +76,4 @@ async def test_asyncio_ioc_override(asyncio_ioc_override): assert '1' in out assert 'Starting iocInit' in err assert 'iocRun: All initialization complete' in err + assert 'IOC reboot started' in err From 8158b5d001974483f0e6215d4d2ee2eb97458a34 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 13 Aug 2021 16:52:01 +0100 Subject: [PATCH 6/7] Switch to tempfile + dbLoadRecords as a strategy --- softioc/__init__.py | 3 +- softioc/builder.py | 17 +- softioc/softioc.py | 44 ++- tests/expected_records.db | 567 +----------------------------- tests/sim_asyncio_ioc_override.py | 14 +- 5 files changed, 45 insertions(+), 600 deletions(-) diff --git a/softioc/__init__.py b/softioc/__init__.py index b9802106..b9c186b0 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -6,7 +6,8 @@ iocshRegisterCommon, registerRecordDeviceDriver, pdbbase # Do this as early as possible, in case we happen to use cothread -# as it's harmless if we don't +# 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 # This import will also pull in the extension, which is needed diff --git a/softioc/builder.py b/softioc/builder.py index b1f2591c..964d651e 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -1,21 +1,16 @@ import os import numpy +from .softioc import dbLoadDatabase from epicsdbbuilder import * -from epicsdbbuilder.recordset import recordset InitialiseDbd() LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd')) -from . import pythonSoftIoc, imports # noqa +from . import pythonSoftIoc # noqa PythonDevice = pythonSoftIoc.PythonDevice() -def dbLoadDatabase(database, path = None, substitutions = None): - '''Loads a database file and applies any given substitutions.''' - imports.dbLoadDatabase(database, path, substitutions) - - # ---------------------------------------------------------------------------- # Wrappers for PythonDevice record constructors. # @@ -185,14 +180,6 @@ def WaveformOut(name, *value, **fields): _DatabaseWritten = False -def AddDatabaseLine(line, macros={}): - '''Add a single line to the produced database, substituting $(k) for v for - each entry in macros''' - for k, v in macros.items(): - line = line.replace("$(%s)" % k, v) - recordset.AddBodyLine(line) - - def LoadDatabase(): '''This should be called after all the builder records have been created, but before calling iocInit(). The database is loaded into EPICS memory, diff --git a/softioc/softioc.py b/softioc/softioc.py index ab064cd5..843a4a3a 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -1,10 +1,11 @@ import os import sys from ctypes import * +from tempfile import NamedTemporaryFile from epicsdbbuilder.recordset import recordset -from . import imports, device, builder +from . import imports, device __all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc'] @@ -240,35 +241,48 @@ def __call__(self): exit = Exiter() command_names.append('exit') -# For backwards compatibility -dbLoadDatabase = builder.dbLoadDatabase +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 - builder.AddDatabaseLine(line, macros) + 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): diff --git a/tests/expected_records.db b/tests/expected_records.db index 977e2407..290c5c82 100644 --- a/tests/expected_records.db +++ b/tests/expected_records.db @@ -1,568 +1,7 @@ -# This file was automatically generated on Thu 29 Apr 2021 14:11:13 BST. -# +# This file was automatically generated on Fri 13 Aug 2021 16:49:03 BST. +# # *** Please do not edit this file: edit the source file instead. *** -# - -# Used by Channel Access Security to determine access to this IOC. -record(mbbo, "TS-DI-TEST-01:ACCESS") -{ - field(DESC, "TS-DI-TEST-01 Acc Mode") - field(PINI, "YES") - field(ZRST, "Running") - field(ZRSV, "NO_ALARM") - field(ONST, "Maintenance") - field(ONSV, "MINOR") - field(TWST, "Test") - field(TWSV, "MINOR") - field(THST, "OFFLINE") - field(THSV, "MAJOR") - info(autosaveFields, "VAL") -} -record(stringin, "TS-DI-TEST-01:STARTTOD") -{ - field(DESC, "Time and date of startup") - field(DTYP, "Soft Timestamp") - field(PINI, "YES") - field(INP, "@%m/%d/%Y %H:%M:%S") -} -record(stringin, "TS-DI-TEST-01:TOD") -{ - field(DESC, "Current time and date") - field(DTYP, "Soft Timestamp") - field(SCAN, "1 second") - field(INP, "@%m/%d/%Y %H:%M:%S") -} -record(calcout, "TS-DI-TEST-01:HEARTBEAT") -{ - field(DESC, "1 Hz counter since startup") - field(CALC, "(A<2147483647)?A+1:1") - field(SCAN, "1 second") - field(INPA, "TS-DI-TEST-01:HEARTBEAT") -} -# if autosave is working, START_CNT creates a running count of -# number of times the IOC was started. -record(calcout, "TS-DI-TEST-01:START_CNT") -{ - field(DESC, "Increments at startup") - field(CALC, "A+1") - field(PINI, "YES") - field(INPA, "TS-DI-TEST-01:START_CNT") - info(autosaveFields_pass0, "VAL") -} -# -# Using an existing internal set of subroutines, this -# PV updates the Access Security mechanism dynamically. -# The .acf file is re-read. -# -record( sub, "TS-DI-TEST-01:READACF") -{ - field( DESC, "TS-DI-TEST-01 ACF Update") - field( INAM, "asSubInit") - field( SNAM, "asSubProcess") - field( BRSV, "INVALID") -} -record(sub, "TS-DI-TEST-01:SYSRESET") -{ - alias("TS-DI-TEST-01:SysReset") - field(DESC, "IOC Restart" ) - field(SNAM, "rebootProc") - field(BRSV,"INVALID") - field(L,"1") -} - -record(ai, "TS-DI-TEST-01:CA_CLNT_CNT") { - field(DESC, "Number of CA Clients") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@ca_clients") - field(HOPR, "200") - field(HIHI, "175") - field(HIGH, "100") - field(HHSV, "MAJOR") - field(HSV, "MINOR") - info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") -} - -record(ai, "TS-DI-TEST-01:CA_CONN_CNT") { - field(DESC, "Number of CA Connections") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@ca_connections") - field(HOPR, "5000") - field(HIHI, "4500") - field(HIGH, "4000") - field(HHSV, "MAJOR") - field(HSV, "MINOR") - info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") -} - -record(ai, "TS-DI-TEST-01:RECORD_CNT") { - field(DESC, "Number of Records") - field(PINI, "YES") - field(DTYP, "IOC stats") - field(INP, "@records") -} - -record(ai, "TS-DI-TEST-01:FD_MAX") { - field(DESC, "Max File Descriptors") - field(PINI, "YES") - field(DTYP, "IOC stats") - field(INP, "@maxfd") -} - -record(ai, "TS-DI-TEST-01:FD_CNT") { - field(DESC, "Allocated File Descriptors") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(FLNK, "TS-DI-TEST-01:FD_FREE PP MS") - field(INP, "@fd") -} - -record(calc, "TS-DI-TEST-01:FD_FREE") { - field(DESC, "Available FDs") - field(CALC, "B>0?B-A:C") - field(INPA, "TS-DI-TEST-01:FD_CNT NPP MS") - field(INPB, "TS-DI-TEST-01:FD_MAX NPP MS") - field(INPC, "1000") - field(HOPR, "150") - field(LOLO, "5") - field(LOW, "20") - field(LLSV, "MAJOR") - field(LSV, "MINOR") - info(autosaveFields_pass0, "HOPR LOPR LOW LOLO LSV LLSV") -} - -record(ai, "TS-DI-TEST-01:SYS_CPU_LOAD") { - field(DESC, "System CPU Load") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@sys_cpuload") - field(EGU, "%") - field(PREC, "1") - field(HOPR, "100") - field(HIHI, "80") - field(HIGH, "70") - field(HHSV, "MAJOR") - field(HSV, "MINOR") - info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") -} - -record(ai, "TS-DI-TEST-01:IOC_CPU_LOAD") { - alias("TS-DI-TEST-01:LOAD") - field(DESC, "IOC CPU Load") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@ioc_cpuload") - field(EGU, "%") - field(PREC, "1") - field(HOPR, "100") - field(HIHI, "80") - field(HIGH, "70") - field(HHSV, "MAJOR") - field(HSV, "MINOR") - info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") -} - -record(ai, "TS-DI-TEST-01:CPU_CNT") { - field(DESC, "Number of CPUs") - field(DTYP, "IOC stats") - field(INP, "@no_of_cpus") - field(PINI, "YES") -} - -record(ai, "TS-DI-TEST-01:SUSP_TASK_CNT") { - field(DESC, "Number Suspended Tasks") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@suspended_tasks") - field(HIHI, "1") - field(HHSV, "MAJOR") - info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") -} - -record(ai, "TS-DI-TEST-01:MEM_USED") { - field(DESC, "Allocated Memory") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@allocated_bytes") - field(EGU, "byte") -} - -record(ai, "TS-DI-TEST-01:MEM_FREE") { - field(DESC, "Free Memory") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@free_bytes") - field(EGU, "byte") - field(LLSV, "MAJOR") - field(LSV, "MINOR") - info(autosaveFields_pass0, "HOPR LOPR LOW LOLO LSV LLSV") -} - -record(ai, "TS-DI-TEST-01:MEM_MAX") { - field(DESC, "Maximum Memory") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@total_bytes") - field(EGU, "byte") -} - -record(ao, "TS-DI-TEST-01:CA_UPD_TIME") { - field(DESC, "CA Check Update Period") - field(DTYP, "IOC stats") - field(OUT, "@ca_scan_rate") - field(EGU, "sec") - field(DRVH, "60") - field(DRVL, "1") - field(HOPR, "60") - field(VAL, "15") - field(PINI, "YES") -} - -record(ao, "TS-DI-TEST-01:FD_UPD_TIME") { - field(DESC, "FD Check Update Period") - field(DTYP, "IOC stats") - field(OUT, "@fd_scan_rate") - field(EGU, "sec") - field(DRVH, "60") - field(DRVL, "1") - field(HOPR, "60") - field(VAL, "20") - field(PINI, "YES") -} - -record(ao, "TS-DI-TEST-01:LOAD_UPD_TIME") { - field(DESC, "CPU Check Update Period") - field(DTYP, "IOC stats") - field(OUT, "@cpu_scan_rate") - field(EGU, "sec") - field(DRVH, "60") - field(DRVL, "1") - field(HOPR, "60") - field(VAL, "10") - field(PINI, "YES") -} - -record(ao, "TS-DI-TEST-01:MEM_UPD_TIME") { - field(DESC, "Memory Check Update Period") - field(DTYP, "IOC stats") - field(OUT, "@memory_scan_rate") - field(EGU, "sec") - field(DRVH, "60") - field(DRVL, "1") - field(HOPR, "60") - field(VAL, "10") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:ST_SCRIPT1") { - field(DESC, "Startup Script Part1") - field(DTYP, "IOC stats") - field(INP, "@startup_script_1") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:ST_SCRIPT2") { - field(DESC, "Startup Script Part2") - field(DTYP, "IOC stats") - field(INP, "@startup_script_2") - field(PINI, "YES") -} - -record(waveform, "TS-DI-TEST-01:ST_SCRIPT") { - field(DESC, "Startup Script") - field(DTYP, "IOC stats") - field(INP, "@startup_script") - field(NELM, "120") - field(FTVL, "CHAR") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:KERNEL_VERS") { - field(DESC, "Kernel Version") - field(DTYP, "IOC stats") - field(INP, "@kernel_ver") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:EPICS_VERS") { - field(DESC, "EPICS Version") - field(DTYP, "IOC stats") - field(INP, "@epics_ver") - field(PINI, "YES") -} - -record(waveform, "TS-DI-TEST-01:EPICS_VERSION") { - field(DESC, "EPICS Version") - field(DTYP, "IOC stats") - field(INP, "@epics_ver") - field(NELM, "120") - field(FTVL, "CHAR") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:HOSTNAME") { - field(DESC, "Host Name") - field(DTYP, "IOC stats") - field(INP, "@hostname") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:APP_DIR1") { - field(DESC, "Application Directory Part 1") - field(DTYP, "IOC stats") - field(INP, "@pwd1") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:APP_DIR2") { - field(DESC, "Application Directory Part 2") - field(DTYP, "IOC stats") - field(INP, "@pwd2") - field(PINI, "YES") -} - -record(waveform, "TS-DI-TEST-01:APP_DIR") { - field(DESC, "Application Directory") - field(DTYP, "IOC stats") - field(INP, "@pwd") - field(NELM, "160") - field(FTVL, "CHAR") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:UPTIME") { - field(DESC, "Elapsed Time since Start") - field(SCAN, "1 second") - field(DTYP, "IOC stats") - field(INP, "@up_time") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:ENGINEER") { - field(DESC, "Engineer") - field(DTYP, "IOC stats") - field(INP, "@engineer") - field(PINI, "YES") -} - -record(stringin, "TS-DI-TEST-01:LOCATION") { - field(DESC, "Location") - field(DTYP, "IOC stats") - field(INP, "@location") - field(PINI, "YES") -} - -record(ai, "TS-DI-TEST-01:PROCESS_ID") { - field(DESC, "Process ID") - field(PINI, "YES") - field(DTYP, "IOC stats") - field(INP, "@proc_id") -} - -record(ai, "TS-DI-TEST-01:PARENT_ID") { - field(DESC, "Parent Process ID") - field(PINI, "YES") - field(DTYP, "IOC stats") - field(INP, "@parent_proc_id") -} - -record(ai, "TS-DI-TEST-01:SCANONCE_Q_SIZE") { - field(DESC, "max # entries in IOC scanOnce queue") - field(DTYP, "IOC stats") - field(INP, "@scanOnceQueueSize") - field(PINI, "YES") -} -record(ai, "TS-DI-TEST-01:CB_Q_SIZE") { - field(DESC, "max # entries in IOC callback queues") - field(DTYP, "IOC stats") - field(INP, "@cbQueueSize") - field(PINI, "YES") -} - -record(ai, "TS-DI-TEST-01:SCANONCE_Q_HIGH") { - field(DESC, "max # of elmts in IOC's scanOnce queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@scanOnceQueueHiWtrMrk") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:SCANONCE_Q_HIGHPER") -} - -record(calc, "TS-DI-TEST-01:SCANONCE_Q_HIGHPER") { - field(DESC, "Max. usage of IOC's scanOnce queue") - field(INPA, "TS-DI-TEST-01:SCANONCE_Q_HIGH NPP MS") - field(INPB, "TS-DI-TEST-01:SCANONCE_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:SCANONCE_Q_USED") { - field(DESC, "# of entries in IOC's scanOnce queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@scanOnceQueueUsed") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:SCANONCE_Q_USEDPER") -} - -record(calc, "TS-DI-TEST-01:SCANONCE_Q_USEDPER") { - field(DESC, "Percentage of IOC's scanOnce queue used") - field(INPA, "TS-DI-TEST-01:SCANONCE_Q_USED NPP MS") - field(INPB, "TS-DI-TEST-01:SCANONCE_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:SCANONCE_Q_OVERRUNS") { - field(DESC, "# of overruns of IOC's scanOnce queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@scanOnceQueueOverruns") - field(PINI, "YES") -} - -record(ai, "TS-DI-TEST-01:CBLOW_Q_HIGH") { - field(DESC, "max # of elmts in IOC's cbLow queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbLowQueueHiWtrMrk") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:CBLOW_Q_HIGHPER") -} - -record(calc, "TS-DI-TEST-01:CBLOW_Q_HIGHPER") { - field(DESC, "Max. usage of IOC's cbLow queue") - field(INPA, "TS-DI-TEST-01:CBLOW_Q_HIGH NPP MS") - field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:CBLOW_Q_USED") { - field(DESC, "# of entries in IOC's cbLow queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbLowQueueUsed") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:CBLOW_Q_USEDPER") -} - -record(calc, "TS-DI-TEST-01:CBLOW_Q_USEDPER") { - field(DESC, "Percentage of IOC's cbLow queue used") - field(INPA, "TS-DI-TEST-01:CBLOW_Q_USED NPP MS") - field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:CBLOW_Q_OVERRUNS") { - field(DESC, "# of overruns of IOC's cbLow queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbLowQueueOverruns") - field(PINI, "YES") -} - -record(ai, "TS-DI-TEST-01:CBMEDIUM_Q_HIGH") { - field(DESC, "max # of elmts in IOC's cbMedium queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbMediumQueueHiWtrMrk") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:CBMEDIUM_Q_HIGHPER") -} - -record(calc, "TS-DI-TEST-01:CBMEDIUM_Q_HIGHPER") { - field(DESC, "Max. usage of IOC's cbMedium queue") - field(INPA, "TS-DI-TEST-01:CBMEDIUM_Q_HIGH NPP MS") - field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:CBMEDIUM_Q_USED") { - field(DESC, "# of entries in IOC's cbMedium queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbMediumQueueUsed") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:CBMEDIUM_Q_USEDPER") -} - -record(calc, "TS-DI-TEST-01:CBMEDIUM_Q_USEDPER") { - field(DESC, "Percentage of IOC's cbMedium queue used") - field(INPA, "TS-DI-TEST-01:CBMEDIUM_Q_USED NPP MS") - field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:CBMEDIUM_Q_OVERRUNS") { - field(DESC, "# of overruns of IOC's cbMedium queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbMediumQueueOverruns") - field(PINI, "YES") -} - -record(ai, "TS-DI-TEST-01:CBHIGH_Q_HIGH") { - field(DESC, "max # of elmts in IOC's cbHigh queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbHighQueueHiWtrMrk") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:CBHIGH_Q_HIGHPER") -} - -record(calc, "TS-DI-TEST-01:CBHIGH_Q_HIGHPER") { - field(DESC, "Max. usage of IOC's cbHigh queue") - field(INPA, "TS-DI-TEST-01:CBHIGH_Q_HIGH NPP MS") - field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:CBHIGH_Q_USED") { - field(DESC, "# of entries in IOC's cbHigh queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbHighQueueUsed") - field(PINI, "YES") - field(FLNK, "TS-DI-TEST-01:CBHIGH_Q_USEDPER") -} - -record(calc, "TS-DI-TEST-01:CBHIGH_Q_USEDPER") { - field(DESC, "Percentage of IOC's cbHigh queue used") - field(INPA, "TS-DI-TEST-01:CBHIGH_Q_USED NPP MS") - field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") - field(CALC, "100*A/B") - field(LOPR, "0") - field(HOPR, "100") - field(EGU, "%") -} - -record(ai, "TS-DI-TEST-01:CBHIGH_Q_OVERRUNS") { - field(DESC, "# of overruns of IOC's cbHigh queue") - field(SCAN, "I/O Intr") - field(DTYP, "IOC stats") - field(INP, "@cbHighQueueOverruns") - field(PINI, "YES") -} +# record(ai, "TS-DI-TEST-01:AI") { diff --git a/tests/sim_asyncio_ioc_override.py b/tests/sim_asyncio_ioc_override.py index 955fc817..99aab3c9 100644 --- a/tests/sim_asyncio_ioc_override.py +++ b/tests/sim_asyncio_ioc_override.py @@ -4,6 +4,7 @@ import os import re from pathlib import Path +from tempfile import NamedTemporaryFile from softioc import softioc, builder, asyncio_dispatcher @@ -16,11 +17,14 @@ builder.SetDeviceName(parsed_args.prefix) # Load the base records without DTYP fields - macros = dict(device=parsed_args.prefix) - with open(Path(__file__).parent / "hw_records.db") as f: - for line in f.readlines(): - if not re.match(r"\s*field\s*\(\s*DTYP", line): - builder.AddDatabaseLine(line, macros) + with open(Path(__file__).parent / "hw_records.db", "rb") as inp: + with NamedTemporaryFile(suffix='.db', delete=False) as out: + for line in inp.readlines(): + if not re.match(rb"\s*field\s*\(\s*DTYP", line): + out.write(line) + softioc.dbLoadDatabase( + out.name, substitutions=f"device={parsed_args.prefix}") + os.unlink(out.name) # Override DTYPE and OUT, and provide a callback gain = builder.boolOut("GAIN", on_update=print) From b2cbcffdb9d30d9aee03d5df2fc4dd6ded4d98d5 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 16 Aug 2021 08:22:11 +0100 Subject: [PATCH 7/7] Skip test that doesn't work on MacOS --- tests/test_asyncio.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 886b368f..82c89c20 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -1,6 +1,7 @@ # Will be ignored on Python2 by conftest.py settings import pytest +import sys @pytest.mark.asyncio @@ -57,6 +58,9 @@ async def test_asyncio_ioc(asyncio_ioc): @pytest.mark.asyncio +@pytest.mark.skipif( + sys.platform.startswith("darwin"), + reason="devIocStats reboot doesn't work on MacOS") async def test_asyncio_ioc_override(asyncio_ioc_override): from aioca import caget, caput @@ -69,7 +73,7 @@ async def test_asyncio_ioc_override(asyncio_ioc_override): # Stop await caput(pre + ":SYSRESET", 1) # check closed and output - out, err = asyncio_ioc_override.proc.communicate() + out, err = asyncio_ioc_override.proc.communicate(timeout=5) out = out.decode() err = err.decode() # check closed and output