From c3ff9821da9cea8f3c2abe937cdab750f84a88fc Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 16 Feb 2022 13:39:48 +0000 Subject: [PATCH 1/4] Rework the handling of default values for in and out records This commit addresses both github issues #81 and #67. The arguments to the a and long record builder functions (`aIn`, `aOut`, `longIn`, `longOut`) are brought more into line with the arguments as used in the epics_device module. This means that extra unnamed arguments can be used to specify EGU and PREC (where appropriate), and for out records the DRV limits take precedence. As a side effect, the implementation of this is made closer to that in https://github.com/dls-controls/epics_device/ --- docs/reference/api.rst | 19 +++++++---- softioc/builder.py | 69 +++++++++++++++++++++++---------------- tests/expected_records.db | 5 +++ 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 20eb6de3..9afd2159 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -229,19 +229,20 @@ Test Facilities`_ documentation for more details of each function. 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. -Thus the ``**fields`` argument in all of the definitions below refers to both the -optional keyword arguments listed above and record field names. +Thus the ``**fields`` argument in all of the definitions below refers to both +the optional keyword arguments listed above and record field names. All functions return a wrapped `ProcessDeviceSupportIn` or `ProcessDeviceSupportOut` instance. .. function:: - aIn(name, LOPR=None, HOPR=None, **fields) - aOut(name, LOPR=None, HOPR=None, **fields) + aIn(name, LOPR=None, HOPR=None, EGU=None, PREC=None, **fields) + aOut(name, DRVL=None, DRVH=None, EGU=None, PREC=None, **fields) - Create ``ai`` and ``ao`` records. The lower and upper limits for the - record can be specified, and if specified these will also be used to set the - ``EGUL`` and ``EGUF`` fields. + Create ``ai`` and ``ao`` records. The lower and upper limits for the record + can be specified. For ``ao`` records the ``LOPR`` and ``HOPR`` fields will + be set by default to the values of the ``DRVL`` and ``DRVH`` fields + respectively. .. function:: boolIn(name, ZNAM=None, ONAM=None, **fields) @@ -255,6 +256,10 @@ All functions return a wrapped `ProcessDeviceSupportIn` or longOut(name, DRVL=None, DRVH=None, EGU=None, **fields) Create ``longin`` and ``longout`` records with specified limits and units. + The lower and upper display limits for the record can be specified. For + ``longout`` records the ``LOPR`` and ``HOPR`` fields will be set by default + to the values of the ``DRVL`` and ``DRVH`` fields respectively. + .. function:: stringIn(name, **fields) diff --git a/softioc/builder.py b/softioc/builder.py index 23b435cf..591141c5 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -18,45 +18,52 @@ # The SCAN field for all input records defaults to I/O Intr. -def _in_record(record, name, **fields): - '''For input records we provide some automatic extra features: scanning, - initialisation as appropriate, and blocking puts from outside the IOC.''' - +def _set_in_defaults(fields): fields.setdefault('SCAN', 'I/O Intr') - if 'initial_value' in fields: - fields.setdefault('PINI', 'YES') + fields.setdefault('PINI', 'YES') fields.setdefault('DISP', 1) - return getattr(PythonDevice, record)(name, **fields) + +def _set_out_defaults(fields): + fields.setdefault('OMSL', 'supervisory') + +# 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 + fields['DRVH'] = DRVH + fields.setdefault('LOPR', DRVL) + fields.setdefault('HOPR', DRVH) -def aIn(name, LOPR=None, HOPR=None, **fields): - return _in_record( - 'ai', name, LOPR = LOPR, HOPR = HOPR, **fields) +def aIn(name, LOPR=None, HOPR=None, EGU=None, PREC=None, **fields): + _set_in_defaults(fields) + return PythonDevice.ai( + name, LOPR = LOPR, HOPR = HOPR, EGU = EGU, PREC = PREC, **fields) -def aOut(name, LOPR=None, HOPR=None, **fields): - fields.setdefault('DRVL', LOPR) - fields.setdefault('DRVH', HOPR) - return PythonDevice.ao( - name, LOPR = LOPR, HOPR = HOPR, **fields) +def aOut(name, DRVL=None, DRVH=None, EGU=None, PREC=None, **fields): + _set_out_defaults(fields) + _set_scalar_out_defaults(fields, DRVL, DRVH) + return PythonDevice.ao(name, EGU = EGU, PREC = PREC, **fields) def boolIn(name, ZNAM=None, ONAM=None, **fields): - return _in_record('bi', name, ZNAM = ZNAM, ONAM = ONAM, **fields) + _set_in_defaults(fields) + return PythonDevice.bi(name, ZNAM = ZNAM, ONAM = ONAM, **fields) def boolOut(name, ZNAM=None, ONAM=None, **fields): - return PythonDevice.bo( - name, OMSL = 'supervisory', ZNAM = ZNAM, ONAM = ONAM, **fields) + _set_out_defaults(fields) + return PythonDevice.bo(name, ZNAM = ZNAM, ONAM = ONAM, **fields) def longIn(name, LOPR=None, HOPR=None, EGU=None, **fields): + _set_in_defaults(fields) fields.setdefault('MDEL', -1) - return _in_record( - 'longin', name, EGU = EGU, LOPR = LOPR, HOPR = HOPR, **fields) + return PythonDevice.longin( + name, LOPR = LOPR, HOPR = HOPR, EGU = EGU, **fields) def longOut(name, DRVL=None, DRVH=None, EGU=None, **fields): - return PythonDevice.longout( - name, OMSL = 'supervisory', DRVL = DRVL, DRVH = DRVH, EGU = EGU, - **fields) + _set_out_defaults(fields) + _set_scalar_out_defaults(fields, DRVL, DRVH) + return PythonDevice.longout(name, EGU = EGU, **fields) # Field name prefixes for mbbi/mbbo records. @@ -92,17 +99,21 @@ def process_value(prefix, value, option, severity=None): def mbbIn(name, *options, **fields): _process_mbb_values(options, fields) - return _in_record('mbbi', name, **fields) + _set_in_defaults(fields) + return PythonDevice.mbbi(name, **fields) def mbbOut(name, *options, **fields): _process_mbb_values(options, fields) - return PythonDevice.mbbo(name, OMSL = 'supervisory', **fields) + _set_out_defaults(fields) + return PythonDevice.mbbo(name, **fields) def stringIn(name, **fields): - return _in_record('stringin', name, **fields) + _set_in_defaults(fields) + return PythonDevice.stringin(name, **fields) def stringOut(name, **fields): + _set_out_defaults(fields) return PythonDevice.stringout(name, **fields) def Action(name, **fields): @@ -204,7 +215,8 @@ def _waveform(value, fields): def Waveform(name, *value, **fields): _waveform(value, fields) - return _in_record('waveform', name, **fields) + _set_in_defaults(fields) + return PythonDevice.waveform(name, **fields) WaveformIn = Waveform @@ -232,7 +244,8 @@ def _long_string(fields): def longStringIn(name, **fields): _long_string(fields) - return _in_record('long_stringin', name, **fields) + _set_in_defaults(fields) + return PythonDevice.long_stringin(name, **fields) def longStringOut(name, **fields): _long_string(fields) diff --git a/tests/expected_records.db b/tests/expected_records.db index ab764e2f..f9faaccb 100644 --- a/tests/expected_records.db +++ b/tests/expected_records.db @@ -10,6 +10,7 @@ record(ai, "TS-DI-TEST-01:AI") record(ao, "TS-DI-TEST-01:AO") { field(DTYP, "Python") + field(OMSL, "supervisory") field(OUT, "@TS-DI-TEST-01:AO") } @@ -107,6 +108,8 @@ record(longout, "TS-DI-TEST-01:SINN") field(DRVH, "1024") field(DRVL, "0") field(DTYP, "Python") + field(HOPR, "1024") + field(LOPR, "0") field(OMSL, "supervisory") field(OUT, "@TS-DI-TEST-01:SINN") } @@ -114,6 +117,7 @@ record(longout, "TS-DI-TEST-01:SINN") record(ao, "TS-DI-TEST-01:SINP") { field(DTYP, "Python") + field(OMSL, "supervisory") field(OUT, "@TS-DI-TEST-01:SINP") } @@ -129,6 +133,7 @@ record(stringin, "TS-DI-TEST-01:STRINGIN") record(stringout, "TS-DI-TEST-01:STRINGOUT") { field(DTYP, "Python") + field(OMSL, "supervisory") field(OUT, "@TS-DI-TEST-01:STRINGOUT") } From eb5a7b34d252664c634ba74b3dfb4f27e931e7ad Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 16 Feb 2022 14:15:15 +0000 Subject: [PATCH 2/4] Re-enable tests that were blocked on issue#67 Previously all "in" records were filtered out of this test --- tests/test_record_values.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/test_record_values.py b/tests/test_record_values.py index 20339121..15f77b90 100644 --- a/tests/test_record_values.py +++ b/tests/test_record_values.py @@ -591,16 +591,8 @@ def test_value_post_init_set(self): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and caget after initialisation""" - # Various conditions mean we cannot use the entire list of cases - filtered_list = [] - for item in record_values_list: - # .set() on In records doesn't update correctly. - # pythonSoftIOC issue #67 - if item[1] not in in_records: - filtered_list.append(item) - run_test_function( - filtered_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.CAGET + record_values_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.CAGET ) @requires_cothread From b61327cda4a4cbcf007685ed2adaab918687a861 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Thu, 17 Feb 2022 08:44:36 +0000 Subject: [PATCH 3/4] Update changelog Also fix missing internal changelog links --- CHANGELOG.rst | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f70f78b..40371658 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,12 +3,18 @@ Changelog All notable changes to this project will be documented in this file. -The format is based on `Keep a Changelog `_, -and this project adheres to `Semantic Versioning `_. +The format is based on `Keep a Changelog +`_, and this project adheres to `Semantic +Versioning `_. -4.0.1_ - 2022-02-14 +Unreleased_ ----------- +- `Improve handling of range settings for scalar records <../../pull/82>`_ + +4.0.1_ - 2022-02-14 +------------------- + Removed: - `Remove python2 support <../../pull/64>`_ @@ -121,7 +127,9 @@ Added: Last release as an EPICS module rather than a Python package -.. _Unreleased: https://github.com/dls-controls/pythonIoc/compare/3.2...HEAD +.. _Unreleased: https://github.com/dls-controls/pythonIoc/compare/4.0.1...HEAD +.. _4.0.1: https://github.com/dls-controls/pythonIoc/compare/3.2.1...4.0.1 +.. _3.2.1: https://github.com/dls-controls/pythonIoc/compare/3.2...3.2.1 .. _3.2: https://github.com/dls-controls/pythonIoc/compare/3.1...3.2 .. _3.1: https://github.com/dls-controls/pythonIoc/compare/3.0...3.1 .. _3.0: https://github.com/dls-controls/pythonIoc/compare/3.0b2...3.0 From 3a5586ebc3ff4bd3cb4bb3b2e027e6d60cd4e3eb Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 17 Feb 2022 09:07:36 +0000 Subject: [PATCH 4/4] Add tests for issues #81 and #67 --- tests/test_records.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_records.py b/tests/test_records.py index 9841e8cf..210f6327 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -131,6 +131,55 @@ def test_waveform_construction(): with pytest.raises(AssertionError): builder.WaveformIn("WI11", length=11, NELM=12) +def test_drvhl_hlopr_defaults(): + """Test the DRVH/L and H/LOPR default settings""" + # DRVH/L doesn't exist on In records + ai = builder.aIn("FOO") + # KeyError as fields with no value are simply not present in + # epicsdbbuilder's dictionary of fields + with pytest.raises(KeyError): + assert ai.LOPR.Value() is None + with pytest.raises(KeyError): + assert ai.HOPR.Value() is None + + ao = builder.aOut("BAR") + with pytest.raises(KeyError): + assert ao.LOPR.Value() is None + with pytest.raises(KeyError): + assert ao.HOPR.Value() is None + with pytest.raises(KeyError): + assert ao.DRVH.Value() is None + with pytest.raises(KeyError): + assert ao.DRVL.Value() is None + +def test_hlopr_inherits_drvhl(): + """Test that H/LOPR values are set to the DRVH/L values""" + lo = builder.longOut("ABC", DRVH=5, DRVL=10) + + assert lo.DRVH.Value() == 5 + assert lo.HOPR.Value() == 5 + assert lo.DRVL.Value() == 10 + assert lo.LOPR.Value() == 10 + +def test_hlopr_dvrhl_different_values(): + """Test you can set H/LOPR and DRVH/L to different values""" + ao = builder.aOut("DEF", DRVL=1, LOPR=2, HOPR=3, DRVH=4) + + assert ao.DRVL.Value() == 1 + assert ao.LOPR.Value() == 2 + assert ao.HOPR.Value() == 3 + assert ao.DRVH.Value() == 4 + +def test_pini_always_on(): + """Test that PINI is always on for in records regardless of initial_value""" + bi = builder.boolIn("AAA") + assert bi.PINI.Value() == "YES" + + mbbi = builder.mbbIn("BBB", initial_value=5) + assert mbbi.PINI.Value() == "YES" + + + def validate_fixture_names(params): """Provide nice names for the out_records fixture in TestValidate class""" return params[0].__name__