Python Library¶
The run-iocsh package provides a Python library for programmatically running
and controlling IOC processes. The main exports are:
IOC: context manager for managing an IOC processrun_iocsh(): convenience wrapper for simple run-and-check use caseswait_for(): standalone polling utility for readiness checksDEFAULT_DETECTORS: the replaceable set of error detectorsException classes: typed errors for different failure modes
Usage patterns¶
Both patterns use the same IOC class. Output is checked automatically when
the context manager exits cleanly — any errors raise a typed exception.
Three properties expose the captured output. ioc.output is both streams in the
order the lines arrived, and is what patterns are matched against; ioc.stdout
and ioc.stderr are each stream on its own. Prefer output unless you
specifically care which stream a line came from: EPICS sends errlog to stderr,
so nearly everything interesting — including iocRun: All initialization complete — arrives there, while stdout carries the shell’s own echoes.
ANSI escapes are stripped as output is captured. EPICS colourises errlog even when its output is a pipe, which would otherwise defeat any pattern anchored at the start of a line.
Run-and-check¶
Start the IOC, wait for iocInit to complete, then exit. Useful for
module-loading tests, dbl output checks, or startup script validation.
import re
from run_iocsh import IOC
with IOC("st.cmd") as ioc:
ioc.wait_for_output()
# output checked automatically; inspect stdout for assertions
pvs = re.findall(r"^MY_IOC:(.+)$", ioc.stdout, re.MULTILINE)
assert "SomeRecord" in pvs
run_iocsh() is a one-liner for this pattern:
from run_iocsh import run_iocsh
ioc = run_iocsh("st.cmd")
# ioc.output / ioc.stdout / ioc.stderr available for inspection
run_iocsh() names each phase of the run separately:
Argument |
Meaning |
|---|---|
|
what counts as ready |
|
how long to wait for it |
|
whether to wait at all — |
|
how long to keep the IOC running once ready |
|
how long the IOC gets to shut down after being told to exit |
Every timeout follows the same rule: None waits forever, a number is seconds,
0 checks once without blocking.
settle also makes the run require the IOC to stay up: with a non-zero settle,
an IOC that becomes ready and then exits on its own before the window is up
raises IocshExitedError, even on a clean exit code. It defaults to 0, where
no survival is required.
Live IOC¶
Keep the IOC running while tests interact with it over CA or PVA:
import pytest
from p4p.client.thread import Context
from run_iocsh import IOC
@pytest.fixture(scope="session")
def ioc():
with IOC("st.cmd") as proc:
proc.wait_for_output() # wait for iocInit
yield proc
@pytest.fixture(scope="session")
def ctxt():
with Context("pva") as ctx:
yield ctx
def test_pv_value(ioc, ctxt):
assert ctxt.get("MY:IOC:SomePV") == 42
IOC.ready() does the start-and-wait a live fixture would otherwise write by
hand. It constructs the IOC, starts it, and waits until it is ready, then yields
it still running; leaving the block exits the IOC and checks its output, the same
as with IOC(...). Use it instead of run_iocsh() when the test needs the IOC
to stay up rather than run once and exit.
@pytest.fixture(scope="session")
def ioc():
with IOC.ready("st.cmd") as ioc:
yield ioc
Readiness¶
EPICS IOCs typically go through up to three phases before they are fully ready:
Phase 1 — iocInit complete¶
All IOCs print this line when iocInit finishes:
ioc.wait_for_output() # default: "iocRun: All initialization complete"
Phase 2 — protocol or module layer ready (optional)¶
Some modules initialise asynchronously after iocInit. Use wait_for to poll
until a PV becomes available or another condition is met:
wait_for(lambda: ctxt.get("MY:IOC:Ready") is not None, timeout=10)
Phase 3 — background poll settled (optional)¶
Drivers that poll a device in the background may need time after phase 2 before
their first values arrive. Nothing observable marks it: the fetch leaves no
trace in the IOC’s output, and waiting for the value itself would only test that
the test passes. An explicit time.sleep() is the honest option here.
run_iocsh() exposes the same idea as settle, which keeps the IOC running for
a fixed time before exiting. It defaults to 0.
wait_for¶
Poll a predicate until it returns True. Exceptions raised by the predicate
are swallowed and treated as False, which is useful when the condition depends
on a resource that may not yet be available:
from run_iocsh import wait_for
# context.get() raises until the IOC is ready - wait_for retries silently
wait_for(lambda: ctxt.get("MY:PV") is not None, timeout=10)
# Works for any predicate
wait_for(lambda: save_file.exists(), timeout=10)
Raises IocshTimeoutError on expiry, which is also a builtin TimeoutError,
so either will catch it.
wait_for_output¶
Block until a pattern appears in stdout or stderr:
# Default: "iocRun: All initialization complete" - works for all soft IOC variants
ioc.wait_for_output()
# Custom pattern for a specific module readiness signal
ioc.wait_for_output("autosave: All ok", timeout=10)
# Returns immediately if pattern already in accumulated output
ioc.wait_for_output() # second call is instant
If the IOC exits before the pattern appears, the
detectors run first, so a recognised cause is
reported as itself — a missing module raises
IocshModuleNotFoundError rather than a generic startup failure. If nothing is
recognised, IocshStartupError is raised with the last 500 characters of the
output.
If the timeout expires while the IOC is still running, raises
IocshTimeoutError, also with the last 500 characters of the output. For the
default readiness pattern the message names wait_for_init=False: an IOC
started with require’s --no-init, or one deadlocked in asInit, never
reaches iocInit and can only time out here.
timeout=None waits forever and timeout=0 checks the buffered output once
without blocking, as everywhere else in this library.
Tearing down an IOC that will not exit¶
exit() sends the exit command and waits for the IOC to exit, killing it and
raising IocshTimeoutError if it does not within exit_timeout. That is the
right default, but an IOC that deadlocks during asInit never reaches a shell
that reads stdin, so exit() can only time out. For that case, kill() stops
the process outright without raising:
ioc = IOC("st.cmd")
ioc.start()
ioc.wait_for_output("some snippet loaded", timeout=10)
ioc.kill()
ioc.check_output() # captured output is intact; the kill signal is not counted
A killed IOC’s return code is the signal it was sent, so check_output() does
not report it as a process failure — the fail_on and detector checks still
apply.
Non-default executables¶
Pass executable= to use any IOC binary. Extra arguments for the executable
go into positional arguments. softIocPVA is available with EPICS base 7+
and can be used without e3:
# Standard EPICS base soft IOC
IOC("-D", "/path/to/softIoc.dbd", "st.cmd", executable="softIocPVA")
# Compiled IOC application
IOC(executable="/path/to/my/ioc")
On the CLI:
run-iocsh --executable softIocPVA -D /path/to/softIoc.dbd st.cmd
check_output and fail_on¶
check_output() is called automatically when the IOC context manager exits
cleanly. It applies fail_on, then DEFAULT_DETECTORS, then the return code:
Check |
Exception |
|---|---|
|
|
|
|
|
|
a library the dynamic linker could not open |
|
Non-zero exit code |
|
The ^ERROR pattern is stored in RE_BUILTIN_FAIL_ON. It matters because an
IOC can fail and still exit 0 — a database that will not load is reported only
in the output, so the return code alone would call that run a success.
Warning
check_output() runs only when the context manager exits cleanly. If you
drive the IOC with explicit start() and exit() calls, nothing checks the
output and fail_on never applies — the IOC can log errors all run and the test
will pass. Call check_output() yourself after exit() in that case.
Replacing the detectors¶
DEFAULT_DETECTORS matches messages emitted by e3/require, not by this library.
Upstream has changed all of it before without warning, so treat it as a default
rather than a rule. The shared-library detector matches both glibc’s and dyld’s
wording, since the two platforms phrase it differently.
Replace the set for an IOC that is not require-based, or on a platform whose messages differ:
from run_iocsh import DEFAULT_DETECTORS, IOC
def detect_my_failure(output: str) -> None:
if "MY_DRIVER: init failed" in output:
raise RuntimeError("driver failed to initialise")
# Extend
IOC("st.cmd", detectors=(*DEFAULT_DETECTORS, detect_my_failure))
# Or replace entirely, e.g. for an IOC that is not require-based
IOC("st.cmd", detectors=(detect_my_failure,))
A detector takes the captured output and raises if it recognises a failure. Returning without raising means it found nothing.
Adding patterns¶
Pass fail_on to IOC.__init__. It replaces DEFAULT_FAIL_ON rather than
extending it, so include the default explicitly to keep the ^ERROR check —
dropping it re-opens the false negative it exists to catch:
from run_iocsh import DEFAULT_FAIL_ON, IOC
with IOC("st.cmd", fail_on=(*DEFAULT_FAIL_ON, "^WARNING:", r"FATAL\b")) as ioc:
ioc.wait_for_output()
check_output() runs only after the process has exited, so to check explicitly,
drive the lifecycle by hand rather than calling it inside the with block. It
defaults to the fail_on and detectors the IOC was constructed with:
ioc = IOC("st.cmd", fail_on=(*DEFAULT_FAIL_ON, "^WARNING:"))
ioc.start()
ioc.wait_for_output()
ioc.exit()
ioc.check_output() # applies the instance's fail_on
CLI¶
run-iocsh --fail-on "^WARNING" st.cmd
Unlike the library, the CLI adds --fail-on patterns on top of the built-in
^ERROR check; pass --no-default-fail-on to drop it. See the
CLI reference for the full set of flags.
caplog integration¶
IOC output is logged at DEBUG level, line by line, as it arrives. Use
caplog.at_level(logging.DEBUG, logger="run_iocsh") to capture it in pytest.
This is especially useful for asserting on startup output without needing to
wait for the IOC to exit first:
import logging
def test_ioc_loads_correctly(caplog):
with caplog.at_level(logging.DEBUG, logger="run_iocsh"):
with IOC("st.cmd") as ioc:
ioc.wait_for_output()
assert "require_registerRecordDeviceDriver" in caplog.text
On failure, pytest displays the captured log, giving full IOC output context without any extra teardown code.
Exception reference¶
For the full exception hierarchy see the API reference.