diff --git a/softioc/__init__.py b/softioc/__init__.py index 7c6f6360..b9c186b0 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -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 + # This import will also pull in the extension, which is needed # before we call iocshRegisterCommon from .imports import dbLoadDatabase 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(): diff --git a/softioc/builder.py b/softioc/builder.py index 0fa1447d..964d651e 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -11,7 +11,6 @@ PythonDevice = pythonSoftIoc.PythonDevice() - # ---------------------------------------------------------------------------- # Wrappers for PythonDevice record constructors. # diff --git a/softioc/softioc.py b/softioc/softioc.py index 27d08f2b..843a4a3a 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -1,6 +1,7 @@ import os import sys from ctypes import * +from tempfile import NamedTemporaryFile from epicsdbbuilder.recordset import recordset @@ -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): diff --git a/tests/conftest.py b/tests/conftest.py index 382e0951..11d5afff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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() 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/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..99aab3c9 --- /dev/null +++ b/tests/sim_asyncio_ioc_override.py @@ -0,0 +1,37 @@ +from argparse import ArgumentParser + +import asyncio +import os +import re +from pathlib import Path +from tempfile import NamedTemporaryFile + +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 + 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) + softioc.devIocStats(parsed_args.prefix) + + # 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..82c89c20 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -1,88 +1,83 @@ # Will be ignored on Python2 by conftest.py settings -import random -import string -import subprocess -import sys -import os -import atexit import pytest -import time - -PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) - - -@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) +import sys @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) + from aioca import caget, caput, camonitor, CANothing, 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.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 + + # Gain bo + 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 + await caput(pre + ":SYSRESET", 1) + # check closed and output + out, err = asyncio_ioc_override.proc.communicate(timeout=5) + 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 + assert 'IOC reboot started' in err diff --git a/tests/test_cothread.py b/tests/test_cothread.py index a722edab..06ccd93b 100644 --- a/tests/test_cothread.py +++ b/tests/test_cothread.py @@ -1,70 +1,48 @@ -import random -import string -import subprocess import sys -import os import signal 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) -@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()) - - def test_cothread_ioc(cothread_ioc): - import epicscorelibs.path.cothread 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