Skip to content

Commit c33d49f

Browse files
committed
Add ability to add arbitrary lines to Db file and test
1 parent 6f0fe54 commit c33d49f

File tree

7 files changed

+133
-57
lines changed

7 files changed

+133
-57
lines changed

softioc/builder.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import os
22
import numpy
3-
from .softioc import dbLoadDatabase
43

54
from epicsdbbuilder import *
5+
from epicsdbbuilder.recordset import recordset
66

77
InitialiseDbd()
88
LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd'))
99

10-
from . import pythonSoftIoc # noqa
10+
from . import pythonSoftIoc, imports # noqa
1111
PythonDevice = pythonSoftIoc.PythonDevice()
1212

1313

14+
def dbLoadDatabase(database, path = None, substitutions = None):
15+
'''Loads a database file and applies any given substitutions.'''
16+
imports.dbLoadDatabase(database, path, substitutions)
17+
1418

1519
# ----------------------------------------------------------------------------
1620
# Wrappers for PythonDevice record constructors.
@@ -181,6 +185,14 @@ def WaveformOut(name, *value, **fields):
181185

182186
_DatabaseWritten = False
183187

188+
def AddDatabaseLine(line, macros={}):
189+
'''Add a single line to the produced database, substituting $(k) for v for
190+
each entry in macros'''
191+
for k, v in macros.items():
192+
line = line.replace("$(%s)" % k, v)
193+
recordset.AddBodyLine(line)
194+
195+
184196
def LoadDatabase():
185197
'''This should be called after all the builder records have been created,
186198
but before calling iocInit(). The database is loaded into EPICS memory,

softioc/softioc.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from epicsdbbuilder.recordset import recordset
66

7-
from . import imports, device
7+
from . import imports, device, builder
88

99
__all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc']
1010

@@ -240,10 +240,9 @@ def __call__(self):
240240
exit = Exiter()
241241
command_names.append('exit')
242242

243+
# For backwards compatibility
244+
dbLoadDatabase = builder.dbLoadDatabase
243245

244-
def dbLoadDatabase(database, path = None, substitutions = None):
245-
'''Loads a database file and applies any given substitutions.'''
246-
imports.dbLoadDatabase(database, path, substitutions)
247246

248247
def _add_records_from_file(dir, file, macros):
249248
# 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):
261260
_add_records_from_file(dir, line.split('"')[1], macros)
262261
else:
263262
# A record line
264-
for k, v in macros.items():
265-
line = line.replace('$(%s)' % k, v)
266-
recordset.AddBodyLine(line)
263+
builder.AddDatabaseLine(line, macros)
267264

268265

269266
def devIocStats(ioc_name):

tests/conftest.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
1+
import os
2+
import random
3+
import string
4+
import subprocess
15
import sys
26

37
if sys.version_info < (3,):
48
# Python2 has no asyncio, so ignore these tests
5-
collect_ignore = ["test_asyncio.py", "sim_asyncio_ioc.py"]
9+
collect_ignore = [
10+
"test_asyncio.py", "sim_asyncio_ioc.py", "sim_asyncio_ioc_overide.py"
11+
]
12+
13+
PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12))
14+
15+
16+
class SubprocessIOC:
17+
def __init__(self, ioc_py):
18+
sim_ioc = os.path.join(os.path.dirname(__file__), ioc_py)
19+
cmd = [sys.executable, sim_ioc, PV_PREFIX]
20+
self.proc = subprocess.Popen(
21+
cmd, stdin=subprocess.PIPE,
22+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
23+
24+
def kill(self):
25+
if self.proc.returncode is None:
26+
# still running, kill it and print the output
27+
self.proc.kill()
28+
out, err = self.proc.communicate()
29+
print(out.decode())
30+
print(err.decode())

tests/hw_records.db

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
record(bo, "$(device):GAIN") {
2+
field(DTYP, "Hy8001")
3+
field(OMSL, "supervisory")
4+
field(OUT, "#C1 S0 @")
5+
field(DESC, "Gain bit 1")
6+
field(ZNAM, "Off")
7+
field(ONAM, "On")
8+
}

tests/sim_asyncio_ioc_override.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from argparse import ArgumentParser
2+
3+
import asyncio
4+
import os
5+
import re
6+
from pathlib import Path
7+
8+
from softioc import softioc, builder, asyncio_dispatcher
9+
10+
11+
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+
# Load the base records without DTYP fields
19+
macros = dict(device=parsed_args.prefix)
20+
with open(Path(__file__).parent / "hw_records.db") as f:
21+
for line in f.readlines():
22+
if not re.match(r"\s*field\s*\(\s*DTYP", line):
23+
builder.AddDatabaseLine(line, macros)
24+
25+
# Override DTYPE and OUT, and provide a callback
26+
gain = builder.boolOut("GAIN", on_update=print)
27+
28+
# Run the IOC
29+
builder.LoadDatabase()
30+
event_loop = asyncio.get_event_loop()
31+
softioc.iocInit(asyncio_dispatcher.AsyncioDispatcher(event_loop))
32+
event_loop.run_forever()

tests/test_asyncio.py

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,34 @@
11
# Will be ignored on Python2 by conftest.py settings
22

3-
import random
4-
import string
5-
import subprocess
6-
import sys
7-
import os
83
import atexit
4+
import signal
95
import pytest
10-
import time
6+
from tests.conftest import SubprocessIOC, PV_PREFIX
117

12-
PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12))
8+
9+
def aioca_cleanup():
10+
from aioca import purge_channel_caches, _catools
11+
# Unregister the aioca atexit handler as it conflicts with the one installed
12+
# by cothread. If we don't do this we get a seg fault. This is not a problem
13+
# in production as we won't mix aioca and cothread, but we do mix them in
14+
# the tests so need to do this.
15+
atexit.unregister(_catools._catools_atexit)
16+
# purge the channels before the event loop goes
17+
purge_channel_caches()
1318

1419

1520
@pytest.fixture
1621
def asyncio_ioc():
17-
sim_ioc = os.path.join(os.path.dirname(__file__), "sim_asyncio_ioc.py")
18-
cmd = [sys.executable, sim_ioc, PV_PREFIX]
19-
proc = subprocess.Popen(
20-
cmd, stdin=subprocess.PIPE,
21-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
22-
yield proc
23-
# purge the channels before the event loop goes
24-
from aioca import purge_channel_caches
25-
purge_channel_caches()
26-
if proc.returncode is None:
27-
# still running, kill it and print the output
28-
proc.kill()
29-
out, err = proc.communicate()
30-
print(out.decode())
31-
print(err.decode(), file=sys.stderr)
22+
ioc = SubprocessIOC("sim_asyncio_ioc.py")
23+
yield ioc.proc
24+
ioc.kill()
25+
aioca_cleanup()
3226

3327

3428
@pytest.mark.asyncio
3529
async def test_asyncio_ioc(asyncio_ioc):
3630
import asyncio
3731
from aioca import caget, caput, camonitor, CANothing, _catools, FORMAT_TIME
38-
# Unregister the aioca atexit handler as it conflicts with the one installed
39-
# by cothread. If we don't do this we get a seg fault. This is not a problem
40-
# in production as we won't mix aioca and cothread, but we do mix them in
41-
# the tests so need to do this.
42-
atexit.unregister(_catools._catools_atexit)
4332

4433
# Start
4534
assert (await caget(PV_PREFIX + ":UPTIME")).startswith("00:00:0")
@@ -86,3 +75,32 @@ async def test_asyncio_ioc(asyncio_ioc):
8675
assert 'Starting iocInit' in err
8776
assert 'iocRun: All initialization complete' in err
8877
assert '(InteractiveConsole)' in err
78+
79+
80+
@pytest.fixture
81+
def asyncio_ioc_override():
82+
ioc = SubprocessIOC("sim_asyncio_ioc_override.py")
83+
yield ioc.proc
84+
ioc.kill()
85+
aioca_cleanup()
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_asyncio_ioc_override(asyncio_ioc_override):
90+
from aioca import caget, caput
91+
92+
# Gain bo
93+
assert (await caget(PV_PREFIX + ":GAIN")) == 0
94+
await caput(PV_PREFIX + ":GAIN", "On", wait=True)
95+
assert (await caget(PV_PREFIX + ":GAIN")) == 1
96+
97+
# Stop
98+
asyncio_ioc_override.send_signal(signal.SIGINT)
99+
# check closed and output
100+
out, err = asyncio_ioc_override.communicate()
101+
out = out.decode()
102+
err = err.decode()
103+
# check closed and output
104+
assert '1' in out
105+
assert 'Starting iocInit' in err
106+
assert 'iocRun: All initialization complete' in err

tests/test_cothread.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,21 @@
1-
import random
2-
import string
3-
import subprocess
41
import sys
5-
import os
62
import signal
3+
from tests.conftest import SubprocessIOC, PV_PREFIX
74
import pytest
85

9-
PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12))
10-
116

127
if sys.platform.startswith("win"):
138
pytest.skip("Cothread doesn't work on windows", allow_module_level=True)
149

1510

1611
@pytest.fixture
1712
def cothread_ioc():
18-
sim_ioc = os.path.join(os.path.dirname(__file__), "sim_cothread_ioc.py")
19-
cmd = [sys.executable, sim_ioc, PV_PREFIX]
20-
proc = subprocess.Popen(
21-
cmd, stdin=subprocess.PIPE,
22-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
23-
yield proc
24-
if proc.returncode is None:
25-
# still running, kill it and print the output
26-
proc.kill()
27-
out, err = proc.communicate()
28-
print(out.decode())
29-
print(err.decode())
30-
13+
ioc = SubprocessIOC("sim_cothread_ioc.py")
14+
yield ioc.proc
15+
ioc.kill()
3116

3217

3318
def test_cothread_ioc(cothread_ioc):
34-
import epicscorelibs.path.cothread
3519
import cothread
3620
from cothread.catools import ca_nothing, caget, caput, camonitor
3721

0 commit comments

Comments
 (0)