Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow "status" and "severity" on In record init #111

Merged
merged 3 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Versioning <https://semver.org/spec/v2.0.0.html>`_.
Unreleased_
-----------

- `Allow "status" and "severity" on In record init <../../pull/111>`_

4.1.0_ - 2022-08-05
-------------------

Expand Down
13 changes: 11 additions & 2 deletions docs/reference/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,17 @@ Test Facilities`_ documentation for more details of each function.

This is used to specify an initial value for each record.

.. _status, severity:

`status, severity`
~~~~~~~~~~~~~~~~~~~~~

Only available on IN records. These can be used with the alarm value enums
from `softioc.alarm` to set the initial alarm state and severity of a record.

These are not useful unless ``PINI`` is set to ``NO``, as otherwise the record
will be processed on initialization and the alarm status will be reset.

.. _on_update:

`on_update`
Expand Down Expand Up @@ -243,8 +254,6 @@ Test Facilities`_ documentation for more details of each function.
.. seealso::
`SetBlocking` for configuring a global default blocking value



For all of these functions any EPICS database field can be assigned a value by
passing it as a keyword argument for the corresponding field name (in upper
case) or by assigning to the corresponding field of the returned record object.
Expand Down
15 changes: 15 additions & 0 deletions softioc/builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import numpy

from .softioc import dbLoadDatabase

from epicsdbbuilder import *
Expand All @@ -24,10 +25,19 @@ def _set_in_defaults(fields):
fields.setdefault('SCAN', 'I/O Intr')
fields.setdefault('PINI', 'YES')
fields.setdefault('DISP', 1)
_set_alarm(fields)

def _set_out_defaults(fields):
fields.setdefault('OMSL', 'supervisory')

def _set_alarm(fields):
if 'status' in fields:
assert 'STAT' not in fields, 'Can\'t specify both status and STAT'
fields['STAT'] = _statStrings[fields.pop('status')]
if 'severity' in fields:
assert 'SEVR' not in fields, 'Can\'t specify both severity and SEVR'
fields['SEVR'] = _severityStrings[fields.pop('severity')]

# For longout and ao we want DRV{L,H} to match {L,H}OPR by default
def _set_scalar_out_defaults(fields, DRVL, DRVH):
fields['DRVL'] = DRVL
Expand Down Expand Up @@ -76,6 +86,11 @@ def longOut(name, DRVL=None, DRVH=None, EGU=None, **fields):
# All the severity strings supported by <prefix>SV
_severityStrings = ['NO_ALARM', 'MINOR', 'MAJOR', 'INVALID']

_statStrings = [
'NO_ALARM', 'READ', 'WRITE', 'HIHI', 'HIGH', 'LOLO', 'LOW', 'STATE', 'COS',
'COMM', 'TIMEOUT', 'HWLIMIT', 'CALC', 'SCAN', 'LINK', 'SOFT', 'BAD_SUB',
'UDF', 'DISABLE', 'SIMM', 'READ_ACCESS', 'WRITE_ACCESS']

# Converts a list of (option [,severity]) values or tuples into field settings
# suitable for mbbi and mbbo records.
def _process_mbb_values(options, fields):
Expand Down
90 changes: 68 additions & 22 deletions tests/test_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@
)

from softioc import asyncio_dispatcher, builder, softioc
from softioc import alarm
from softioc.device import SetBlocking

# Test file for miscellaneous tests related to records

# Test parameters
DEVICE_NAME = "RECORD-TESTS"

in_records = [
builder.aIn,
builder.boolIn,
builder.longIn,
builder.mbbIn,
builder.stringIn,
builder.WaveformIn,
]

def test_records(tmp_path):
# Ensure we definitely unload all records that may be hanging over from
# previous tests, then create exactly one instance of expected records.
Expand Down Expand Up @@ -60,32 +70,18 @@ def test_enum_length_restriction():
"seventeen",
)


def test_DISP_defaults_on():
@pytest.mark.parametrize("creation_func", in_records)
def test_DISP_defaults_on(creation_func):
"""Test that all IN record types have DISP=1 set by default"""
in_records = [
builder.aIn,
builder.boolIn,
builder.longIn,
builder.mbbIn,
builder.stringIn,
builder.WaveformIn,
]

record_counter = 0

for creation_func in in_records:
kwargs = {}
record_counter += 1
record_name = "DISP" + str(record_counter)
kwargs = {}

if creation_func == builder.WaveformIn:
kwargs = {"length": 1}
if creation_func == builder.WaveformIn:
kwargs = {"length": 1}

record = creation_func(record_name, **kwargs)
record = creation_func("RECORD", **kwargs)

# Note: DISP attribute won't exist if field not specified
assert record.DISP.Value() == 1
# Note: DISP attribute won't exist if field not specified
assert record.DISP.Value() == 1


def test_DISP_can_be_overridden():
Expand Down Expand Up @@ -183,6 +179,56 @@ def test_pini_always_on():
mbbi = builder.mbbIn("BBB", initial_value=5)
assert mbbi.PINI.Value() == "YES"

@pytest.mark.parametrize("creation_func", in_records)
def test_setting_alarm_in_records(creation_func):
"""Test that In records can have a custom alarm value set using the "status"
and "severity" keywords"""
kwargs = {}
if creation_func == builder.WaveformIn:
kwargs["length"] = 1

record = creation_func(
"NEW_RECORD",
severity=alarm.MINOR_ALARM,
status=alarm.LOLO_ALARM,
**kwargs
)

assert record.STAT.Value() == "LOLO"
assert record.SEVR.Value() == "MINOR"

@pytest.mark.parametrize("creation_func", in_records)
def test_setting_alarm_invalid_keywords(creation_func):
"""Test that In records correctly block specifying both STAT and status,
and SEVR and severity"""

kwargs = {}
if creation_func == builder.WaveformIn:
kwargs["length"] = 1

with pytest.raises(AssertionError) as e:
creation_func(
"NEW_RECORD",
severity=alarm.MINOR_ALARM,
SEVR="MINOR",
status=alarm.LOLO_ALARM,
**kwargs
)

assert e.value.args[0] == 'Can\'t specify both severity and SEVR'

with pytest.raises(AssertionError) as e:
creation_func(
"NEW_RECORD",
severity=alarm.MINOR_ALARM,
status=alarm.LOLO_ALARM,
STAT="LOLO",
**kwargs
)

assert e.value.args[0] == 'Can\'t specify both status and STAT'


def validate_fixture_names(params):
"""Provide nice names for the out_records fixture in TestValidate class"""
return params[0].__name__
Expand Down