From 6a9ea1eead0e36c8f71ad40bdd94673539cc28c1 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 18 Nov 2021 13:29:41 +0000 Subject: [PATCH 001/131] Use _default_ in Out records --- softioc/device.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 7d125345..5654a72c 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -107,7 +107,7 @@ def __init__(self, name, **kargs): self.__validate = kargs.pop('validate', None) self.__always_update = kargs.pop('always_update', False) - self._value = kargs.pop('initial_value', None) + self._value = kargs.pop('initial_value', self._default_) self.__enable_write = True self.__super.__init__(name, **kargs) @@ -236,18 +236,18 @@ class GenericDevice(Base): _In = ProcessDeviceSupportIn _Out = ProcessDeviceSupportOut -def _Device_In(type, **kargs): - return _Device(_In, type, **kargs) +def _Device_In(record_type, **kargs): + return _Device(_In, record_type, **kargs) -def _Device_Out(type, convert=True, mlst=True): - return _Device(_Out, type, convert=convert, mlst=mlst, default=None) +def _Device_Out(record_type, convert=True, mlst=True, **kargs): + return _Device(_Out, record_type, convert=convert, mlst=mlst, **kargs) longin = _Device_In('longin') longout = _Device_Out('longout') bi = _Device_In('bi', convert=False) bo = _Device_Out('bo', convert=False) stringin = _Device_In('stringin', mlst=False, default='') -stringout = _Device_Out('stringout', mlst=False) +stringout = _Device_Out('stringout', mlst=False, default='') mbbi = _Device_In('mbbi', convert=False) mbbo = _Device_Out('mbbo', convert=False) @@ -279,6 +279,7 @@ def _process(self, record): class ao(ProcessDeviceSupportOut): _record_type_ = 'ao' _device_name_ = 'devPython_ao' + _default_ = 0.0 _fields_ = ['UDF', 'VAL', 'MLST'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT @@ -296,6 +297,8 @@ class WaveformBase(ProcessDeviceSupportCore): # Allow set() to be called before init_record: dtype = None + _default_ = () + def init_record(self, record): self.dtype = DbfCodeToNumpy[record.FTVL] return self.__super.init_record(record) @@ -327,7 +330,6 @@ def _write_value(self, record, value): class waveform(WaveformBase, ProcessDeviceSupportIn): _record_type_ = 'waveform' _device_name_ = 'devPython_waveform' - _default_ = () # Because arrays are mutable values it's ever so easy to accidentially call # set() with a value which subsequently changes. To avoid this common class From c4ae72c65e12d2b1c40e8d99f00e8529e0c2ce88 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 19 Nov 2021 10:23:30 +0000 Subject: [PATCH 002/131] Rebasing branch from master --- tests/test_records.py | 155 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 2 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 1d7af3d8..7b968052 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,10 +1,60 @@ +import multiprocessing import os import pytest -from softioc import builder - +from softioc import builder, softioc +from epicsdbbuilder import ResetRecords +from softioc.device_core import RecordLookup import sim_records +# Counter for unique record number +counter = 0 + +@pytest.fixture +def record_number(): + """Unique counter for record naming""" + global counter + counter += 1 + return counter + + +def _clear_records(): + # Remove any records created at epicsdbbuilder layer + ResetRecords() + # And at pythonSoftIoc level + # TODO: Remove this hack and use use whatever comes out of + # https://github.com/dls-controls/pythonSoftIOC/issues/56 + RecordLookup._RecordDirectory.clear() + + +@pytest.fixture +def clear_records(): + """Fixture to delete all records before and after a test.""" + _clear_records() + yield + _clear_records() + +def idfn(fixture_value): + """Provide a nice name for the tests in the results list""" + return fixture_value[0].__name__ + "-" + fixture_value[3].__name__ + +@pytest.fixture(params=[ + (builder.aOut, 5.5, 5.5, float), + (builder.aIn, 5.5, 5.5, float), + (builder.longOut, 5, 5, int), + (builder.longIn, 5, 5, int), + (builder.boolOut, 1, 1, int), + (builder.boolIn, 1, 1, int), + (builder.stringOut, "abc", "abc", str), + (builder.stringIn, "abc", "abc", str), + (builder.mbbIn, 1, 1, int), + (builder.mbbOut, 1, 1, int), + (builder.WaveformIn, [1, 2, 3], [1, 2, 3], list), + (builder.WaveformOut, [1, 2, 3], [1, 2, 3], list) + ], + ids=idfn) +def record_values(request): + return request.param # One item from the params list def test_records(tmp_path): path = str(tmp_path / "records.db") @@ -51,3 +101,104 @@ def test_DISP_can_be_overridden(): record = builder.longIn("DISP-OFF", DISP=0) # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 0 +def test_value_retrieval_pre_init_set( + clear_records, + record_values): + """Test that records provide the expected values on get calls when using + .set() and .get() before IOC initialisation occurs""" + + creation_func, initial_value, expected_value, expected_type = record_values + + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func("out-record", **kwarg) + out_rec.set(initial_value) + assert out_rec.get() == expected_value + assert type(out_rec.get()) == expected_type + + +def run_test_function(expected_value, expected_type, inner_func): + """Helper function to handle the multiprocessing process""" + queue = multiprocessing.Queue() + process = multiprocessing.Process(target=inner_func, args=(queue,)) + process.start() + + try: + rec_val = queue.get(timeout=5) + + assert rec_val == expected_value + assert type(rec_val) == expected_type + finally: + process.join() + +def test_value_retrieval_pre_init_initial_value( + clear_records, + record_values): + """Test that records provide the expected values on get calls when using + initial_value and .get() before IOC initialisation occurs""" + + creation_func, initial_value, expected_value, expected_type = record_values + + out_rec = creation_func("out-record", initial_value=initial_value) + assert out_rec.get() == expected_value + assert type(out_rec.get()) == expected_type + +def test_value_retrieval_post_init_set(record_values): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and .get() after initialisation""" + + creation_func, initial_value, expected_value, expected_type = record_values + + def inner_func(queue): + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func("out-record", **kwarg) + out_rec.set(initial_value) + + builder.LoadDatabase() + softioc.iocInit() + + queue.put(out_rec.get()) + + run_test_function(expected_value, expected_type, inner_func) + +def test_value_retrieval_post_init_initial_value(record_values): + """Test that records provide the expected values on get calls when using + initial_value during record creation and .get() after IOC initialisation""" + + creation_func, initial_value, expected_value, expected_type = record_values + + def inner_func(queue): + out_rec = creation_func("out-record", initial_value=initial_value) + + builder.LoadDatabase() + softioc.iocInit() + + queue.put(out_rec.get()) + + run_test_function(expected_value, expected_type, inner_func) + +def test_value_retrieval_post_init_set_after_init(record_values): + """Test that records provide the expected values on get calls when using + .set() and .get() after IOC initialisation""" + + creation_func, initial_value, expected_value, expected_type = record_values + + def inner_func(queue): + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func("out-record", **kwarg) + + builder.LoadDatabase() + softioc.iocInit() + out_rec.set(initial_value) + + queue.put(out_rec.get()) + + run_test_function(expected_value, expected_type, inner_func) From 75f5ecf44da68762f98d3c88e007ffc6a6e410f2 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 19 Nov 2021 10:24:28 +0000 Subject: [PATCH 003/131] Fix records alarm status if no initial value --- softioc/device.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 5654a72c..f3e9408f 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -107,7 +107,7 @@ def __init__(self, name, **kargs): self.__validate = kargs.pop('validate', None) self.__always_update = kargs.pop('always_update', False) - self._value = kargs.pop('initial_value', self._default_) + self._value = kargs.pop('initial_value', None) self.__enable_write = True self.__super.__init__(name, **kargs) @@ -115,7 +115,11 @@ def init_record(self, record): '''Special record initialisation for out records only: implements special record initialisation if an initial value has been specified, allowing out records to have a sensible initial value.''' - if self._value is not None: + if self._value is None: + # Cannot set in __init__, as we want the record alarm status to be + # set if no value was provided + self._value = self._default_ + else: self._write_value(record, self._value) if 'MLST' in self._fields_: record.MLST = self._value From cdbbc8a4240b99b18d10eb7d5eed38b8086ca1f3 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 19 Nov 2021 10:51:06 +0000 Subject: [PATCH 004/131] Fix typo --- docs/reference/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 7efab9e0..4ce41e4b 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -267,7 +267,7 @@ All functions return a wrapped `ProcessDeviceSupportIn` or mbbOut(name, *options, **fields) Create ``mbbi`` and ``mbbo`` records. Up to 16 options can be specified as - either an option name or a tuple of tw fields. The name or first field of + either an option name or a tuple of two fields. The name or first field of the tuple names the option, and the second optional field is the option severity. For example:: From 0c2238f112f999fe926b1982ef783e5e2576bc37 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 19 Nov 2021 11:48:44 +0000 Subject: [PATCH 005/131] Add type information to records --- softioc/device.py | 81 ++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index f3e9408f..a7e9df35 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -1,6 +1,5 @@ import os import time -import inspect from ctypes import * import numpy @@ -20,6 +19,24 @@ EPICS_ERROR = 1 NO_CONVERT = 2 +NumpyCharCodeToDbr = { + # The following type codes are supported directly: + 'S': 0, # DBR_STRING str_ + 'U': 0, # DBR_STRING str_ + 'h': 1, # DBR_SHORT short = int16 + 'f': 2, # DBR_FLOAT single = float32 + 'b': 4, # DBR_CHAR byte = int8 + 'i': 5, # DBR_LONG intc = int32 + 'd': 6, # DBR_DOUBLE float_ = float64 + # These are supported as related types + 'H': 1, # DBR_SHORT ushort = uint16 + '?': 4, # DBR_CHAR bool_ + 'B': 4, # DBR_CHAR ubyte = uint8 + 'I': 5, # DBR_LONG uintc = uint32 + } +if numpy.int_().itemsize == 4: + NumpyCharCodeToDbr.update({'l': 5, 'L': 5}) # int_, uint + class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): '''Implements canonical default processing for records with a _process @@ -86,7 +103,7 @@ def set_alarm(self, severity, alarm, timestamp=None): def get(self): '''Returns the last written value.''' - return self._value[0] + return self._value_type_(self._value[0]) class ProcessDeviceSupportOut(ProcessDeviceSupportCore): @@ -149,24 +166,6 @@ def _process(self, record): dispatcher(self.__on_update, value) return EPICS_OK - - NumpyCharCodeToDbr = { - # The following type codes are supported directly: - 'S': 0, # DBR_STRING str_ - 'h': 1, # DBR_SHORT short = int16 - 'f': 2, # DBR_FLOAT single = float32 - 'b': 4, # DBR_CHAR byte = int8 - 'i': 5, # DBR_LONG intc = int32 - 'd': 6, # DBR_DOUBLE float_ = float64 - # These are supported as related types - 'H': 1, # DBR_SHORT ushort = uint16 - '?': 4, # DBR_CHAR bool_ - 'B': 4, # DBR_CHAR ubyte = uint8 - 'I': 5, # DBR_LONG uintc = uint32 - } - if numpy.int_().itemsize == 4: - NumpyCharCodeToDbr.update({'l': 5, 'L': 5}) # int_, uint - # Converts a Python value into a form suitable for sending over channel # access. Derived from the corresponding cothread implementation. Returns # computed dbrcode, waveform length, raw data pointer and pointer to @@ -179,7 +178,7 @@ def value_to_dbr(self, value): value.shape = (1,) assert value.ndim == 1, 'Can\'t put multidimensional arrays!' - if value.dtype.char == 'S': + if value.dtype.char == 'S' or value.dtype.char == 'U': # Need special processing to hack the array so that strings are # actually 40 characters long. new_value = numpy.empty(value.shape, 'S40') @@ -187,7 +186,7 @@ def value_to_dbr(self, value): value = new_value try: - dbrtype = self.NumpyCharCodeToDbr[value.dtype.char] + dbrtype = NumpyCharCodeToDbr[value.dtype.char] except KeyError: # One more special case. caput() of a list of integers on a 64-bit # system will fail at this point because they were automatically @@ -220,15 +219,16 @@ def set(self, value, process=True): self.__enable_write = True def get(self): - return self._value + return self._value_type_(self._value) -def _Device(Base, record_type, mlst=False, default=0, convert=True): +def _Device(Base, record_type, value_type, mlst=False, default=0, convert=True): '''Wrapper for generating simple records.''' class GenericDevice(Base): _record_type_ = record_type _device_name_ = 'devPython_' + record_type _default_ = default + _value_type_ = value_type _fields_ = ['UDF', 'VAL'] _epics_rc = EPICS_OK if convert else NO_CONVERT if mlst: @@ -240,20 +240,26 @@ class GenericDevice(Base): _In = ProcessDeviceSupportIn _Out = ProcessDeviceSupportOut -def _Device_In(record_type, **kargs): - return _Device(_In, record_type, **kargs) +def _Device_In(record_type, value_type, **kargs): + return _Device(_In, record_type, value_type, **kargs) -def _Device_Out(record_type, convert=True, mlst=True, **kargs): - return _Device(_Out, record_type, convert=convert, mlst=mlst, **kargs) +def _Device_Out(record_type, value_type, convert=True, mlst=True, **kargs): + return _Device( + _Out, + record_type, + value_type, + convert=convert, + mlst=mlst, + **kargs) -longin = _Device_In('longin') -longout = _Device_Out('longout') -bi = _Device_In('bi', convert=False) -bo = _Device_Out('bo', convert=False) -stringin = _Device_In('stringin', mlst=False, default='') -stringout = _Device_Out('stringout', mlst=False, default='') -mbbi = _Device_In('mbbi', convert=False) -mbbo = _Device_Out('mbbo', convert=False) +longin = _Device_In('longin', int) +longout = _Device_Out('longout', int) +bi = _Device_In('bi', int, convert=False) +bo = _Device_Out('bo', int, convert=False) +stringin = _Device_In('stringin', str, mlst=False, default='') +stringout = _Device_Out('stringout', str, mlst=False, default='') +mbbi = _Device_In('mbbi', int, convert=False) +mbbo = _Device_Out('mbbo', int, convert=False) dset_process_linconv = ( @@ -268,6 +274,7 @@ class ai(ProcessDeviceSupportIn): _record_type_ = 'ai' _device_name_ = 'devPython_ai' _default_ = 0.0 + _value_type_ = float _fields_ = ['UDF', 'VAL'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT @@ -284,6 +291,7 @@ class ao(ProcessDeviceSupportOut): _record_type_ = 'ao' _device_name_ = 'devPython_ao' _default_ = 0.0 + _value_type_ = float _fields_ = ['UDF', 'VAL', 'MLST'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT @@ -302,6 +310,7 @@ class WaveformBase(ProcessDeviceSupportCore): dtype = None _default_ = () + _value_type_ = list def init_record(self, record): self.dtype = DbfCodeToNumpy[record.FTVL] From d64e715eb35d985069c44b4ff6885db99d3510d3 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 19 Nov 2021 14:53:47 +0000 Subject: [PATCH 006/131] String handling for waveforms. Remove value casting --- softioc/builder.py | 8 +++++- softioc/device.py | 47 ++++++++++++++------------------ tests/test_records.py | 63 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 78 insertions(+), 40 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index a7c7bd1b..7e90710d 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -150,7 +150,13 @@ def _waveform(value, fields): # If a value is specified it should be the *only* non keyword # argument. value, = value - value = numpy.array(value) + + if isinstance(value, str): + value = value.encode(errors='replace') + value = numpy.frombuffer(value + b"\0", dtype=numpy.uint8) + else: + value = numpy.array(value) + fields['initial_value'] = value # Pick up default length and datatype from initial value diff --git a/softioc/device.py b/softioc/device.py index a7e9df35..a509f66c 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -22,7 +22,6 @@ NumpyCharCodeToDbr = { # The following type codes are supported directly: 'S': 0, # DBR_STRING str_ - 'U': 0, # DBR_STRING str_ 'h': 1, # DBR_SHORT short = int16 'f': 2, # DBR_FLOAT single = float32 'b': 4, # DBR_CHAR byte = int8 @@ -103,7 +102,7 @@ def set_alarm(self, severity, alarm, timestamp=None): def get(self): '''Returns the last written value.''' - return self._value_type_(self._value[0]) + return self._value[0] class ProcessDeviceSupportOut(ProcessDeviceSupportCore): @@ -171,6 +170,10 @@ def _process(self, record): # computed dbrcode, waveform length, raw data pointer and pointer to # underlying data (for lifetime management). def value_to_dbr(self, value): + + if isinstance(value, str): + value = value.encode(errors='replace') + # First convert the data directly into an array. This will help in # subsequent processing: this does most of the type coercion. value = numpy.require(value, requirements = 'C') @@ -178,7 +181,7 @@ def value_to_dbr(self, value): value.shape = (1,) assert value.ndim == 1, 'Can\'t put multidimensional arrays!' - if value.dtype.char == 'S' or value.dtype.char == 'U': + if value.dtype.char == 'S': # Need special processing to hack the array so that strings are # actually 40 characters long. new_value = numpy.empty(value.shape, 'S40') @@ -219,16 +222,15 @@ def set(self, value, process=True): self.__enable_write = True def get(self): - return self._value_type_(self._value) + return self._value -def _Device(Base, record_type, value_type, mlst=False, default=0, convert=True): +def _Device(Base, record_type, mlst=False, default=0, convert=True): '''Wrapper for generating simple records.''' class GenericDevice(Base): _record_type_ = record_type _device_name_ = 'devPython_' + record_type _default_ = default - _value_type_ = value_type _fields_ = ['UDF', 'VAL'] _epics_rc = EPICS_OK if convert else NO_CONVERT if mlst: @@ -240,26 +242,20 @@ class GenericDevice(Base): _In = ProcessDeviceSupportIn _Out = ProcessDeviceSupportOut -def _Device_In(record_type, value_type, **kargs): - return _Device(_In, record_type, value_type, **kargs) +def _Device_In(record_type, **kargs): + return _Device(_In, record_type, **kargs) -def _Device_Out(record_type, value_type, convert=True, mlst=True, **kargs): - return _Device( - _Out, - record_type, - value_type, - convert=convert, - mlst=mlst, - **kargs) +def _Device_Out(record_type, convert=True, mlst=True, **kargs): + return _Device(_Out, record_type, convert=convert, mlst=mlst, **kargs) -longin = _Device_In('longin', int) -longout = _Device_Out('longout', int) -bi = _Device_In('bi', int, convert=False) -bo = _Device_Out('bo', int, convert=False) -stringin = _Device_In('stringin', str, mlst=False, default='') -stringout = _Device_Out('stringout', str, mlst=False, default='') -mbbi = _Device_In('mbbi', int, convert=False) -mbbo = _Device_Out('mbbo', int, convert=False) +longin = _Device_In('longin') +longout = _Device_Out('longout') +bi = _Device_In('bi', convert=False) +bo = _Device_Out('bo', convert=False) +stringin = _Device_In('stringin', mlst=False, default='') +stringout = _Device_Out('stringout', mlst=False, default='') +mbbi = _Device_In('mbbi', convert=False) +mbbo = _Device_Out('mbbo', convert=False) dset_process_linconv = ( @@ -274,7 +270,6 @@ class ai(ProcessDeviceSupportIn): _record_type_ = 'ai' _device_name_ = 'devPython_ai' _default_ = 0.0 - _value_type_ = float _fields_ = ['UDF', 'VAL'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT @@ -291,7 +286,6 @@ class ao(ProcessDeviceSupportOut): _record_type_ = 'ao' _device_name_ = 'devPython_ao' _default_ = 0.0 - _value_type_ = float _fields_ = ['UDF', 'VAL', 'MLST'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT @@ -310,7 +304,6 @@ class WaveformBase(ProcessDeviceSupportCore): dtype = None _default_ = () - _value_type_ = list def init_record(self, record): self.dtype = DbfCodeToNumpy[record.FTVL] diff --git a/tests/test_records.py b/tests/test_records.py index 7b968052..231ce728 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,4 +1,5 @@ import multiprocessing +import numpy import os import pytest @@ -38,6 +39,10 @@ def idfn(fixture_value): """Provide a nice name for the tests in the results list""" return fixture_value[0].__name__ + "-" + fixture_value[3].__name__ +# TODO: +# - unicode chars in strings +# - bytestrings +# - too long strings, exactly 40 char strings, empty strings. @pytest.fixture(params=[ (builder.aOut, 5.5, 5.5, float), (builder.aIn, 5.5, 5.5, float), @@ -50,10 +55,18 @@ def idfn(fixture_value): (builder.mbbIn, 1, 1, int), (builder.mbbOut, 1, 1, int), (builder.WaveformIn, [1, 2, 3], [1, 2, 3], list), - (builder.WaveformOut, [1, 2, 3], [1, 2, 3], list) + (builder.WaveformOut, [1, 2, 3], [1, 2, 3], list), + ( + builder.WaveformIn, + "ABC", + numpy.array([65, 66, 67, 0], dtype=numpy.uint8), + numpy.array + ) ], ids=idfn) def record_values(request): + """A collection of record creation functions, an initial value, an expected + returned value, and the expected type of the returned value""" return request.param # One item from the params list def test_records(tmp_path): @@ -101,6 +114,21 @@ def test_DISP_can_be_overridden(): record = builder.longIn("DISP-OFF", DISP=0) # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 0 +def record_value_asserts( + creation_func, + actual_value, + expected_value, + expected_type): + """Asserts that the expected value and expected type are matched with + the actual value. Handles both scalar and waveform data""" + + if creation_func in [builder.WaveformOut, builder.WaveformIn]: + assert numpy.array_equal(actual_value, expected_value), \ + f"Arrays not equal: {actual_value} {expected_value}" + else: + assert actual_value == expected_value + assert type(actual_value) == expected_type + def test_value_retrieval_pre_init_set( clear_records, record_values): @@ -115,21 +143,28 @@ def test_value_retrieval_pre_init_set( out_rec = creation_func("out-record", **kwarg) out_rec.set(initial_value) - assert out_rec.get() == expected_value - assert type(out_rec.get()) == expected_type + + record_value_asserts( + creation_func, + out_rec.get(), + expected_value, + expected_type) -def run_test_function(expected_value, expected_type, inner_func): +def run_test_function(creation_func, expected_value, expected_type, test_func): """Helper function to handle the multiprocessing process""" queue = multiprocessing.Queue() - process = multiprocessing.Process(target=inner_func, args=(queue,)) + process = multiprocessing.Process(target=test_func, args=(queue,)) process.start() try: rec_val = queue.get(timeout=5) - assert rec_val == expected_value - assert type(rec_val) == expected_type + record_value_asserts( + creation_func, + rec_val, + expected_value, + expected_type) finally: process.join() @@ -142,8 +177,12 @@ def test_value_retrieval_pre_init_initial_value( creation_func, initial_value, expected_value, expected_type = record_values out_rec = creation_func("out-record", initial_value=initial_value) - assert out_rec.get() == expected_value - assert type(out_rec.get()) == expected_type + + record_value_asserts( + creation_func, + out_rec.get(), + expected_value, + expected_type) def test_value_retrieval_post_init_set(record_values): """Test that records provide the expected values on get calls when using @@ -164,7 +203,7 @@ def inner_func(queue): queue.put(out_rec.get()) - run_test_function(expected_value, expected_type, inner_func) + run_test_function(creation_func, expected_value, expected_type, inner_func) def test_value_retrieval_post_init_initial_value(record_values): """Test that records provide the expected values on get calls when using @@ -180,7 +219,7 @@ def inner_func(queue): queue.put(out_rec.get()) - run_test_function(expected_value, expected_type, inner_func) + run_test_function(creation_func, expected_value, expected_type, inner_func) def test_value_retrieval_post_init_set_after_init(record_values): """Test that records provide the expected values on get calls when using @@ -201,4 +240,4 @@ def inner_func(queue): queue.put(out_rec.get()) - run_test_function(expected_value, expected_type, inner_func) + run_test_function(creation_func, expected_value, expected_type, inner_func) From c1fb7dcdaf23df1486a86742fe1732e2462d5d4e Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 19 Nov 2021 16:06:16 +0000 Subject: [PATCH 007/131] Waveform out string handling fixes --- softioc/device.py | 23 +++++++++++++++++++---- tests/test_records.py | 6 ++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index a509f66c..013b4a3a 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -350,21 +350,36 @@ def set(self, value, # the correct behaviour when treating a character array as a string. # Note that the trailing null is needed to work around problems with # some clients. - value = numpy.fromstring(value + '\0', dtype = 'uint8') + value = numpy.frombuffer( + value.encode(errors="replace") + b'\0', + dtype = numpy.uint8 + ) value = numpy.require(value, dtype = self.dtype) - self._value = (+value, severity, alarm, timestamp) if value.shape == (): value.shape = (1,) assert value.ndim == 1, 'Can\'t write multidimensional arrays' - self.trigger() - + super().set(value, severity, alarm, timestamp) class waveform_out(WaveformBase, ProcessDeviceSupportOut): _record_type_ = 'waveform' _device_name_ = 'devPython_waveform_out' + def set(self, value, process=True): + # TODO: This check exists in multiple places + if isinstance(value, str): + # Convert a string into an array of characters. This will produce + # the correct behaviour when treating a character array as a string. + # Note that the trailing null is needed to work around problems with + # some clients. + value = numpy.frombuffer( + value.encode(errors="replace") + b'\0', + dtype = numpy.uint8 + ) + + super().set(value, process) + # Ensure the .dbd file is loaded. dbLoadDatabase("device.dbd", os.path.dirname(__file__), None) diff --git a/tests/test_records.py b/tests/test_records.py index 231ce728..be9c6623 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -60,6 +60,12 @@ def idfn(fixture_value): builder.WaveformIn, "ABC", numpy.array([65, 66, 67, 0], dtype=numpy.uint8), + numpy.array # TODO: replace with something to name this better? + ), + ( + builder.WaveformOut, + "ABC", + numpy.array([65, 66, 67, 0], dtype=numpy.uint8), numpy.array ) ], From 3badfd3bf85518b545a6c2354276e2ba5176923e Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 19 Nov 2021 16:50:28 +0000 Subject: [PATCH 008/131] Fix type returned from waveforms, and tests --- softioc/device.py | 6 +++++- tests/test_records.py | 27 ++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 013b4a3a..ae4a685c 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -378,7 +378,11 @@ def set(self, value, process=True): dtype = numpy.uint8 ) - super().set(value, process) + # Ensure we always convert incoming value into numpy array, regardless + # of whether the record has been initialised or not + datatype, length, data, array = self.value_to_dbr(value) + + super().set(array, process) # Ensure the .dbd file is loaded. diff --git a/tests/test_records.py b/tests/test_records.py index be9c6623..2ca084b8 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -37,7 +37,8 @@ def clear_records(): def idfn(fixture_value): """Provide a nice name for the tests in the results list""" - return fixture_value[0].__name__ + "-" + fixture_value[3].__name__ + return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ + + "-" + fixture_value[3].__name__ # TODO: May not need this part # TODO: # - unicode chars in strings @@ -54,19 +55,29 @@ def idfn(fixture_value): (builder.stringIn, "abc", "abc", str), (builder.mbbIn, 1, 1, int), (builder.mbbOut, 1, 1, int), - (builder.WaveformIn, [1, 2, 3], [1, 2, 3], list), - (builder.WaveformOut, [1, 2, 3], [1, 2, 3], list), + ( + builder.WaveformIn, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.float32), + numpy.ndarray + ), + ( + builder.WaveformOut, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.float32), + numpy.ndarray + ), ( builder.WaveformIn, "ABC", numpy.array([65, 66, 67, 0], dtype=numpy.uint8), - numpy.array # TODO: replace with something to name this better? + numpy.ndarray ), ( builder.WaveformOut, "ABC", numpy.array([65, 66, 67, 0], dtype=numpy.uint8), - numpy.array + numpy.ndarray ) ], ids=idfn) @@ -131,6 +142,7 @@ def record_value_asserts( if creation_func in [builder.WaveformOut, builder.WaveformIn]: assert numpy.array_equal(actual_value, expected_value), \ f"Arrays not equal: {actual_value} {expected_value}" + assert type(actual_value) == expected_type else: assert actual_value == expected_value assert type(actual_value) == expected_type @@ -164,7 +176,7 @@ def run_test_function(creation_func, expected_value, expected_type, test_func): process.start() try: - rec_val = queue.get(timeout=5) + rec_val = queue.get() # TODO: add timeout=5 record_value_asserts( creation_func, @@ -172,7 +184,8 @@ def run_test_function(creation_func, expected_value, expected_type, test_func): expected_value, expected_type) finally: - process.join() + process.terminate() + process.join(timeout=3) def test_value_retrieval_pre_init_initial_value( clear_records, From dcba54c69b52cf0956c646f0d4786c9bf15151f5 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 22 Nov 2021 11:12:24 +0000 Subject: [PATCH 009/131] Add truncating strings before record init Note the mechanism used needs expanding to all other record types. --- softioc/builder.py | 1 + softioc/device.py | 39 ++++++++++++++++++++++++++++++++------- tests/test_records.py | 24 +++++++++++++++++++++--- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index 7e90710d..eb3edd5f 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -23,6 +23,7 @@ def _in_record(record, name, **fields): fields.setdefault('SCAN', 'I/O Intr') if 'initial_value' in fields: + # TODO: Ask about this, as doing .set won't set this extra field? fields.setdefault('PINI', 'YES') fields.setdefault('DISP', 1) return getattr(PythonDevice, record)(name, **fields) diff --git a/softioc/device.py b/softioc/device.py index ae4a685c..9a371ec2 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -51,6 +51,10 @@ class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): # from record init or processing _epics_rc = EPICS_OK + # Function to convert an incoming Python value into EPICS-appropriate form. + def _value_to_epics(self, value): + return value + # Default implementations of read and write, overwritten where necessary. def _read_value(self, record): return getattr(record, 'VAL') @@ -67,11 +71,13 @@ def __init__(self, name, **kargs): # reading a single value is atomic. We therefore cluster all our # variable state into a single tuple which represents a single value # to be processed. + + value = self._value_to_epics( + kargs.pop('initial_value', self._default_)) + # The tuple contains everything needed to be written: the value, # severity, alarm and optional timestamp. - self._value = ( - kargs.pop('initial_value', self._default_), - alarm.NO_ALARM, alarm.UDF_ALARM, None) + self._value = (value, alarm.NO_ALARM, alarm.UDF_ALARM, None) self.__super.__init__(name, **kargs) def _process(self, record, _value=None): @@ -91,6 +97,7 @@ def set(self, value, severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None): '''Updates the stored value and triggers an update. The alarm severity and timestamp can also be specified if appropriate.''' + value = self._value_to_epics(value) self._value = (value, severity, alarm, timestamp) self.trigger() @@ -123,7 +130,7 @@ def __init__(self, name, **kargs): self.__validate = kargs.pop('validate', None) self.__always_update = kargs.pop('always_update', False) - self._value = kargs.pop('initial_value', None) + self._value = self._value_to_epics(kargs.pop('initial_value', None)) self.__enable_write = True self.__super.__init__(name, **kargs) @@ -208,6 +215,7 @@ def value_to_dbr(self, value): def set(self, value, process=True): '''Special routine to set the value directly.''' + value = self._value_to_epics(value) try: _record = self._record except AttributeError: @@ -225,7 +233,13 @@ def get(self): return self._value -def _Device(Base, record_type, mlst=False, default=0, convert=True): +def _Device( + Base, + record_type, + mlst=False, + default=0, + convert=True, + value_to_epics=None): '''Wrapper for generating simple records.''' class GenericDevice(Base): _record_type_ = record_type @@ -233,6 +247,9 @@ class GenericDevice(Base): _default_ = default _fields_ = ['UDF', 'VAL'] _epics_rc = EPICS_OK if convert else NO_CONVERT + if value_to_epics: + _value_to_epics = \ + staticmethod(value_to_epics) if mlst: _fields_.append('MLST') @@ -248,12 +265,20 @@ def _Device_In(record_type, **kargs): def _Device_Out(record_type, convert=True, mlst=True, **kargs): return _Device(_Out, record_type, convert=convert, mlst=mlst, **kargs) + +def truncate_string(value): + """Trim a string to EPICS 40 (39 with null byte) character limit""" + return value[:39] if isinstance(value, str) else None + # TODO: Come back to this if we have to handle byte strings? + longin = _Device_In('longin') longout = _Device_Out('longout') bi = _Device_In('bi', convert=False) bo = _Device_Out('bo', convert=False) -stringin = _Device_In('stringin', mlst=False, default='') -stringout = _Device_Out('stringout', mlst=False, default='') +stringin = _Device_In('stringin', mlst=False, default='', + value_to_epics=truncate_string) +stringout = _Device_Out('stringout', mlst=False, default='', + value_to_epics=truncate_string) mbbi = _Device_In('mbbi', convert=False) mbbo = _Device_Out('mbbo', convert=False) diff --git a/tests/test_records.py b/tests/test_records.py index 2ca084b8..fb2534eb 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -53,6 +53,18 @@ def idfn(fixture_value): (builder.boolIn, 1, 1, int), (builder.stringOut, "abc", "abc", str), (builder.stringIn, "abc", "abc", str), + ( + builder.stringOut, + "this string is much longer than 40 characters", + "this string is much longer than 40 char", + str + ), + ( + builder.stringIn, + "this string is much longer than 40 characters", + "this string is much longer than 40 char", + str + ), (builder.mbbIn, 1, 1, int), (builder.mbbOut, 1, 1, int), ( @@ -82,8 +94,13 @@ def idfn(fixture_value): ], ids=idfn) def record_values(request): - """A collection of record creation functions, an initial value, an expected - returned value, and the expected type of the returned value""" + """A list of parameters for record value setting/getting tests. + + Fields are: + - Record builder function + - Input value passed to .set() or used in initial_value on record creation + - Expected output value after doing .get() + - Expected type of the output value from .get()""" return request.param # One item from the params list def test_records(tmp_path): @@ -173,10 +190,11 @@ def run_test_function(creation_func, expected_value, expected_type, test_func): """Helper function to handle the multiprocessing process""" queue = multiprocessing.Queue() process = multiprocessing.Process(target=test_func, args=(queue,)) + # TODO: Above line doesn't work as we need for Windows. process.start() try: - rec_val = queue.get() # TODO: add timeout=5 + rec_val = queue.get() # TODO: add timeout=5 record_value_asserts( creation_func, From fadaacb8170fad75324e651c3bfa492c94fa69d6 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 22 Nov 2021 12:57:19 +0000 Subject: [PATCH 010/131] Handle byte strings in waveforms --- softioc/builder.py | 2 ++ softioc/device.py | 62 ++++++++++++++++++++++++------------------- tests/test_records.py | 16 ++++++++++- 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index eb3edd5f..a5ca2bc3 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -154,6 +154,8 @@ def _waveform(value, fields): if isinstance(value, str): value = value.encode(errors='replace') + + if isinstance(value, bytes): value = numpy.frombuffer(value + b"\0", dtype=numpy.uint8) else: value = numpy.array(value) diff --git a/softioc/device.py b/softioc/device.py index 9a371ec2..2532156b 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -357,6 +357,25 @@ def _write_value(self, record, value): self.dtype.itemsize * nord) record.NORD = nord +def handle_strings(value): + """Handles strings and bytearrays as values for Waveforms. Returns the + string converted to a numpy array.""" + + if isinstance(value, str): + value = value.encode(errors="replace") + + if isinstance(value, bytes): + # Convert a string into an array of characters. This will produce + # the correct behaviour when treating a character array as a string. + # Note that the trailing null is needed to work around problems with + # some clients. + value = numpy.frombuffer( + value + b'\0', # TODO: This also exists in builder.py + dtype = numpy.uint8 + ) + + return value + class waveform(WaveformBase, ProcessDeviceSupportIn): _record_type_ = 'waveform' @@ -366,49 +385,38 @@ class waveform(WaveformBase, ProcessDeviceSupportIn): # set() with a value which subsequently changes. To avoid this common class # of bug, at the cost of duplicated code and data, here we ensure a copy is # taken of the value. - def set(self, value, - severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None): - '''Updates the stored value and triggers an update. The alarm - severity and timestamp can also be specified if appropriate.''' - if isinstance(value, str): - # Convert a string into an array of characters. This will produce - # the correct behaviour when treating a character array as a string. - # Note that the trailing null is needed to work around problems with - # some clients. - value = numpy.frombuffer( - value.encode(errors="replace") + b'\0', - dtype = numpy.uint8 - ) + def _value_to_epics(self, value): + + # Caused by no initial_value being specified + if value is None: + return value + + value = handle_strings(value) value = numpy.require(value, dtype = self.dtype) if value.shape == (): value.shape = (1,) assert value.ndim == 1, 'Can\'t write multidimensional arrays' - super().set(value, severity, alarm, timestamp) + return value class waveform_out(WaveformBase, ProcessDeviceSupportOut): _record_type_ = 'waveform' _device_name_ = 'devPython_waveform_out' - def set(self, value, process=True): - # TODO: This check exists in multiple places - if isinstance(value, str): - # Convert a string into an array of characters. This will produce - # the correct behaviour when treating a character array as a string. - # Note that the trailing null is needed to work around problems with - # some clients. - value = numpy.frombuffer( - value.encode(errors="replace") + b'\0', - dtype = numpy.uint8 - ) + def _value_to_epics(self, value): + + # Caused by no initial_value being specified + if value is None: + return value + + value = handle_strings(value) # Ensure we always convert incoming value into numpy array, regardless # of whether the record has been initialised or not datatype, length, data, array = self.value_to_dbr(value) - super().set(array, process) - + return array # Ensure the .dbd file is loaded. dbLoadDatabase("device.dbd", os.path.dirname(__file__), None) diff --git a/tests/test_records.py b/tests/test_records.py index fb2534eb..259bfb80 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -38,7 +38,7 @@ def clear_records(): def idfn(fixture_value): """Provide a nice name for the tests in the results list""" return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ - + "-" + fixture_value[3].__name__ # TODO: May not need this part + + "-" + fixture_value[3].__name__ # TODO: # - unicode chars in strings @@ -90,6 +90,20 @@ def idfn(fixture_value): "ABC", numpy.array([65, 66, 67, 0], dtype=numpy.uint8), numpy.ndarray + ), + ( + builder.WaveformIn, + b"HELLO\0WORLD", + numpy.array([72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], + dtype=numpy.uint8), + numpy.ndarray + ), + ( + builder.WaveformOut, + b"HELLO\0WORLD", + numpy.array([72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], + dtype=numpy.uint8), + numpy.ndarray ) ], ids=idfn) From 3e02993ca91f7b8034a0ff94932d385ea38d3c4f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 22 Nov 2021 15:37:16 +0000 Subject: [PATCH 011/131] Support byte strings in stringIn/Out records --- softioc/device.py | 2 ++ tests/test_records.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/softioc/device.py b/softioc/device.py index 2532156b..deb8175a 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -268,6 +268,8 @@ def _Device_Out(record_type, convert=True, mlst=True, **kargs): def truncate_string(value): """Trim a string to EPICS 40 (39 with null byte) character limit""" + if isinstance(value, bytes): + value = value.decode(errors="replace") # TODO: Remove and let it fail? return value[:39] if isinstance(value, str) else None # TODO: Come back to this if we have to handle byte strings? diff --git a/tests/test_records.py b/tests/test_records.py index 259bfb80..aa1e9b07 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -53,6 +53,8 @@ def idfn(fixture_value): (builder.boolIn, 1, 1, int), (builder.stringOut, "abc", "abc", str), (builder.stringIn, "abc", "abc", str), + (builder.stringOut, b"abc", "abc", str), + (builder.stringIn, b"abc", "abc", str), ( builder.stringOut, "this string is much longer than 40 characters", From 9a5b3b2d4274014c43df51466d1bb43403ae1e44 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 22 Nov 2021 15:39:24 +0000 Subject: [PATCH 012/131] Python2-compatible string formatting --- tests/test_records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_records.py b/tests/test_records.py index aa1e9b07..45bcb68a 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -174,7 +174,7 @@ def record_value_asserts( if creation_func in [builder.WaveformOut, builder.WaveformIn]: assert numpy.array_equal(actual_value, expected_value), \ - f"Arrays not equal: {actual_value} {expected_value}" + "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type else: assert actual_value == expected_value From a3af507956ddd2aa352ab3faea357532ad804e1e Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 09:44:02 +0000 Subject: [PATCH 013/131] Add UTF-8 tests --- softioc/device.py | 1 - tests/test_records.py | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index deb8175a..07377287 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -271,7 +271,6 @@ def truncate_string(value): if isinstance(value, bytes): value = value.decode(errors="replace") # TODO: Remove and let it fail? return value[:39] if isinstance(value, str) else None - # TODO: Come back to this if we have to handle byte strings? longin = _Device_In('longin') longout = _Device_Out('longout') diff --git a/tests/test_records.py b/tests/test_records.py index 45bcb68a..09e18562 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -36,7 +36,7 @@ def clear_records(): _clear_records() def idfn(fixture_value): - """Provide a nice name for the tests in the results list""" + """Provide a nice name for the tests in the record_values fixture""" return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ + "-" + fixture_value[3].__name__ @@ -55,6 +55,12 @@ def idfn(fixture_value): (builder.stringIn, "abc", "abc", str), (builder.stringOut, b"abc", "abc", str), (builder.stringIn, b"abc", "abc", str), + (builder.stringOut, b"a\xcfb", "a�b", str), # Invalid UTF-8 + (builder.stringIn, b"a\xcfb", "a�b", str), # Invalid UTF-8 + (builder.stringOut, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 + (builder.stringIn, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 + (builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 + (builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 ( builder.stringOut, "this string is much longer than 40 characters", @@ -81,6 +87,7 @@ def idfn(fixture_value): numpy.array([1, 2, 3], dtype=numpy.float32), numpy.ndarray ), + # TODO: Check dtype of this data - should be float! ( builder.WaveformIn, "ABC", From a6837e7453b0e6b6208315940330177ccf4b96bc Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 10:43:41 +0000 Subject: [PATCH 014/131] Support for more input types to int+float records --- softioc/device.py | 18 ++++++++++++++++-- tests/test_records.py | 16 +++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 07377287..ddf7b532 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -266,14 +266,26 @@ def _Device_Out(record_type, convert=True, mlst=True, **kargs): return _Device(_Out, record_type, convert=convert, mlst=mlst, **kargs) +def convert_to_int(value): + """Convert any type into its integer representation""" + if type(value) == c_int: + return value.value + return int(value) if value else None + +def convert_to_float(value): + """Convert any type into its float representation""" + if type(value) == c_float: + return value.value + return float(value) if value else None + def truncate_string(value): """Trim a string to EPICS 40 (39 with null byte) character limit""" if isinstance(value, bytes): value = value.decode(errors="replace") # TODO: Remove and let it fail? return value[:39] if isinstance(value, str) else None -longin = _Device_In('longin') -longout = _Device_Out('longout') +longin = _Device_In('longin', value_to_epics=convert_to_int) +longout = _Device_Out('longout', value_to_epics=convert_to_int) bi = _Device_In('bi', convert=False) bo = _Device_Out('bo', convert=False) stringin = _Device_In('stringin', mlst=False, default='', @@ -299,6 +311,7 @@ class ai(ProcessDeviceSupportIn): _fields_ = ['UDF', 'VAL'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT + _value_to_epics = staticmethod(convert_to_float) def _process(self, record): _value = self._value @@ -315,6 +328,7 @@ class ao(ProcessDeviceSupportOut): _fields_ = ['UDF', 'VAL', 'MLST'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT + _value_to_epics = staticmethod(convert_to_float) diff --git a/tests/test_records.py b/tests/test_records.py index 09e18562..53375ebd 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,3 +1,4 @@ +import ctypes import multiprocessing import numpy import os @@ -40,19 +41,25 @@ def idfn(fixture_value): return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ + "-" + fixture_value[3].__name__ -# TODO: -# - unicode chars in strings -# - bytestrings -# - too long strings, exactly 40 char strings, empty strings. @pytest.fixture(params=[ (builder.aOut, 5.5, 5.5, float), (builder.aIn, 5.5, 5.5, float), + (builder.aOut, 3, 3., float), + (builder.aIn, 3, 3., float), + (builder.aOut, ctypes.c_float(3.5), 3.5, float), + (builder.aIn, ctypes.c_float(3.5), 3.5, float), (builder.longOut, 5, 5, int), (builder.longIn, 5, 5, int), + (builder.longOut, 9.9, 9, int), + (builder.longIn, 9.9, 9, int), + (builder.longOut, ctypes.c_int(6), 6, int), + (builder.longIn, ctypes.c_int(6), 6, int), (builder.boolOut, 1, 1, int), (builder.boolIn, 1, 1, int), (builder.stringOut, "abc", "abc", str), (builder.stringIn, "abc", "abc", str), + (builder.stringOut, "", "", str), + (builder.stringIn, "", "", str), (builder.stringOut, b"abc", "abc", str), (builder.stringIn, b"abc", "abc", str), (builder.stringOut, b"a\xcfb", "a�b", str), # Invalid UTF-8 @@ -87,7 +94,6 @@ def idfn(fixture_value): numpy.array([1, 2, 3], dtype=numpy.float32), numpy.ndarray ), - # TODO: Check dtype of this data - should be float! ( builder.WaveformIn, "ABC", From b7883f14fecd5c6ca78af3b657a587cfd6c21c88 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 11:10:04 +0000 Subject: [PATCH 015/131] Refactor to have easier Waveform value handling --- softioc/device.py | 149 ++++++++++++++++++------------------------ tests/test_records.py | 2 +- 2 files changed, 65 insertions(+), 86 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index ddf7b532..663d2ddd 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -61,6 +61,45 @@ def _read_value(self, record): def _write_value(self, record, value): setattr(record, 'VAL', value) + # Converts a Python value into a form suitable for sending over channel + # access. Derived from the corresponding cothread implementation. Returns + # computed dbrcode, waveform length, raw data pointer and pointer to + # underlying data (for lifetime management). + def value_to_dbr(self, value): + + if isinstance(value, str): + value = value.encode(errors='replace') + + # First convert the data directly into an array. This will help in + # subsequent processing: this does most of the type coercion. + value = numpy.require(value, requirements = 'C') + if value.shape == (): + value.shape = (1,) + assert value.ndim == 1, 'Can\'t put multidimensional arrays!' + + if value.dtype.char == 'S': + # Need special processing to hack the array so that strings are + # actually 40 characters long. + new_value = numpy.empty(value.shape, 'S40') + new_value[:] = value + value = new_value + + try: + dbrtype = NumpyCharCodeToDbr[value.dtype.char] + except KeyError: + # One more special case. caput() of a list of integers on a 64-bit + # system will fail at this point because they were automatically + # converted to 64-bit integers. Catch this special case and fix it + # up by silently converting to 32-bit integers. Not really the + # right thing to do (as data can be quietly lost), but the + # alternative isn't nice to use either. + if value.dtype.char == 'l': + value = numpy.require(value, dtype = numpy.int32) + dbrtype = 5 + else: + raise + + return dbrtype, len(value), value.ctypes.data, value class ProcessDeviceSupportIn(ProcessDeviceSupportCore): _link_ = 'INP' @@ -172,47 +211,6 @@ def _process(self, record): dispatcher(self.__on_update, value) return EPICS_OK - # Converts a Python value into a form suitable for sending over channel - # access. Derived from the corresponding cothread implementation. Returns - # computed dbrcode, waveform length, raw data pointer and pointer to - # underlying data (for lifetime management). - def value_to_dbr(self, value): - - if isinstance(value, str): - value = value.encode(errors='replace') - - # First convert the data directly into an array. This will help in - # subsequent processing: this does most of the type coercion. - value = numpy.require(value, requirements = 'C') - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t put multidimensional arrays!' - - if value.dtype.char == 'S': - # Need special processing to hack the array so that strings are - # actually 40 characters long. - new_value = numpy.empty(value.shape, 'S40') - new_value[:] = value - value = new_value - - try: - dbrtype = NumpyCharCodeToDbr[value.dtype.char] - except KeyError: - # One more special case. caput() of a list of integers on a 64-bit - # system will fail at this point because they were automatically - # converted to 64-bit integers. Catch this special case and fix it - # up by silently converting to 32-bit integers. Not really the - # right thing to do (as data can be quietly lost), but the - # alternative isn't nice to use either. - if value.dtype.char == 'l': - value = numpy.require(value, dtype = numpy.int32) - dbrtype = 5 - else: - raise - - return dbrtype, len(value), value.ctypes.data, value - - def set(self, value, process=True): '''Special routine to set the value directly.''' value = self._value_to_epics(value) @@ -372,60 +370,32 @@ def _write_value(self, record, value): self.dtype.itemsize * nord) record.NORD = nord -def handle_strings(value): - """Handles strings and bytearrays as values for Waveforms. Returns the - string converted to a numpy array.""" - - if isinstance(value, str): - value = value.encode(errors="replace") - - if isinstance(value, bytes): - # Convert a string into an array of characters. This will produce - # the correct behaviour when treating a character array as a string. - # Note that the trailing null is needed to work around problems with - # some clients. - value = numpy.frombuffer( - value + b'\0', # TODO: This also exists in builder.py - dtype = numpy.uint8 - ) + def _handle_strings(self, value): + """Handles strings and bytearrays as values for Waveforms. Returns the + string converted to a numpy array.""" - return value - - -class waveform(WaveformBase, ProcessDeviceSupportIn): - _record_type_ = 'waveform' - _device_name_ = 'devPython_waveform' - - # Because arrays are mutable values it's ever so easy to accidentially call - # set() with a value which subsequently changes. To avoid this common class - # of bug, at the cost of duplicated code and data, here we ensure a copy is - # taken of the value. - def _value_to_epics(self, value): - - # Caused by no initial_value being specified - if value is None: - return value - - value = handle_strings(value) - - value = numpy.require(value, dtype = self.dtype) - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t write multidimensional arrays' + if isinstance(value, str): + value = value.encode(errors="replace") + + if isinstance(value, bytes): + # Convert a string into an array of characters. This will produce + # the correct behaviour when treating a character array as a string. + # Note that the trailing null is needed to work around problems with + # some clients. Note this also exists in builder.py's _waveform(). + value = numpy.frombuffer( + value + b'\0', + dtype = numpy.uint8 + ) return value -class waveform_out(WaveformBase, ProcessDeviceSupportOut): - _record_type_ = 'waveform' - _device_name_ = 'devPython_waveform_out' - def _value_to_epics(self, value): # Caused by no initial_value being specified if value is None: return value - value = handle_strings(value) + value = self._handle_strings(value) # Ensure we always convert incoming value into numpy array, regardless # of whether the record has been initialised or not @@ -433,5 +403,14 @@ def _value_to_epics(self, value): return array + +class waveform(WaveformBase, ProcessDeviceSupportIn): + _record_type_ = 'waveform' + _device_name_ = 'devPython_waveform' + +class waveform_out(WaveformBase, ProcessDeviceSupportOut): + _record_type_ = 'waveform' + _device_name_ = 'devPython_waveform_out' + # Ensure the .dbd file is loaded. dbLoadDatabase("device.dbd", os.path.dirname(__file__), None) diff --git a/tests/test_records.py b/tests/test_records.py index 53375ebd..2954a69a 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -223,7 +223,7 @@ def run_test_function(creation_func, expected_value, expected_type, test_func): process.start() try: - rec_val = queue.get() # TODO: add timeout=5 + rec_val = queue.get(timeout=5) record_value_asserts( creation_func, From 69683a3840a129b53eb5ecdd2deae07467882528 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 11:26:29 +0000 Subject: [PATCH 016/131] Remove unnecessary TODO --- softioc/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 663d2ddd..2109387f 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -265,13 +265,13 @@ def _Device_Out(record_type, convert=True, mlst=True, **kargs): def convert_to_int(value): - """Convert any type into its integer representation""" + """Attempt to convert any type into its integer representation""" if type(value) == c_int: return value.value return int(value) if value else None def convert_to_float(value): - """Convert any type into its float representation""" + """Attempt to convert any type into its float representation""" if type(value) == c_float: return value.value return float(value) if value else None @@ -279,7 +279,7 @@ def convert_to_float(value): def truncate_string(value): """Trim a string to EPICS 40 (39 with null byte) character limit""" if isinstance(value, bytes): - value = value.decode(errors="replace") # TODO: Remove and let it fail? + value = value.decode(errors="replace") return value[:39] if isinstance(value, str) else None longin = _Device_In('longin', value_to_epics=convert_to_int) From 5fd38199e5973c98693f981ec6bef54648ce77f9 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 13:08:57 +0000 Subject: [PATCH 017/131] Python2 compatibility changes Functions passed to the multiprocess module must be picklable, which means they must be module-level functions. Also adding the "coding" comment to let me use unicode characters --- tests/test_records.py | 113 +++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 2954a69a..2dabdbff 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- import ctypes import multiprocessing import numpy import os import pytest +from enum import Enum + from softioc import builder, softioc from epicsdbbuilder import ResetRecords from softioc.device_core import RecordLookup @@ -215,26 +218,7 @@ def test_value_retrieval_pre_init_set( expected_type) -def run_test_function(creation_func, expected_value, expected_type, test_func): - """Helper function to handle the multiprocessing process""" - queue = multiprocessing.Queue() - process = multiprocessing.Process(target=test_func, args=(queue,)) - # TODO: Above line doesn't work as we need for Windows. - process.start() - - try: - rec_val = queue.get(timeout=5) - - record_value_asserts( - creation_func, - rec_val, - expected_value, - expected_type) - finally: - process.terminate() - process.join(timeout=3) - -def test_value_retrieval_pre_init_initial_value( +def test_value_pre_init_initial_value( clear_records, record_values): """Test that records provide the expected values on get calls when using @@ -250,60 +234,77 @@ def test_value_retrieval_pre_init_initial_value( expected_value, expected_type) -def test_value_retrieval_post_init_set(record_values): - """Test that records provide the expected values on get calls when using - .set() before IOC initialisation and .get() after initialisation""" +class TestEnum(Enum): + """Enum to control when the record's value should be set""" + INITIAL_VALUE = 1 + SET_BEFORE_INIT = 2 + SET_AFTER_INIT = 3 - creation_func, initial_value, expected_value, expected_type = record_values +def value_test_function(creation_func, initial_value, queue, test_enum): + kwarg = {} + + if test_enum == TestEnum.INITIAL_VALUE: + kwarg.update({"initial_value": initial_value}) + elif creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + # Related to this issue: + # https://github.com/dls-controls/pythonSoftIOC/issues/37 - def inner_func(queue): - kwarg = {} - if creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - out_rec = creation_func("out-record", **kwarg) + out_rec = creation_func("out-record", **kwarg) + + if test_enum == TestEnum.SET_BEFORE_INIT: out_rec.set(initial_value) - builder.LoadDatabase() - softioc.iocInit() + builder.LoadDatabase() + softioc.iocInit() - queue.put(out_rec.get()) + if test_enum == TestEnum.SET_AFTER_INIT: + out_rec.set(initial_value) - run_test_function(creation_func, expected_value, expected_type, inner_func) + queue.put(out_rec.get()) -def test_value_retrieval_post_init_initial_value(record_values): - """Test that records provide the expected values on get calls when using - initial_value during record creation and .get() after IOC initialisation""" + +def run_test_function(record_values, test_enum): + """Run the test function using multiprocessing and check returned value is + expected value""" creation_func, initial_value, expected_value, expected_type = record_values - def inner_func(queue): - out_rec = creation_func("out-record", initial_value=initial_value) + queue = multiprocessing.Queue() + process = multiprocessing.Process( + target=value_test_function, + args=(creation_func, initial_value, queue, test_enum) + ) - builder.LoadDatabase() - softioc.iocInit() + process.start() - queue.put(out_rec.get()) + try: + rec_val = queue.get(timeout=5) - run_test_function(creation_func, expected_value, expected_type, inner_func) + record_value_asserts( + creation_func, + rec_val, + expected_value, + expected_type) + finally: + process.terminate() + process.join(timeout=3) -def test_value_retrieval_post_init_set_after_init(record_values): +def test_value_post_init_set(record_values): """Test that records provide the expected values on get calls when using - .set() and .get() after IOC initialisation""" - - creation_func, initial_value, expected_value, expected_type = record_values + .set() before IOC initialisation and .get() after initialisation""" - def inner_func(queue): - kwarg = {} - if creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation + run_test_function(record_values, TestEnum.SET_BEFORE_INIT) - out_rec = creation_func("out-record", **kwarg) +def test_value_post_init_initial_value(record_values): + """Test that records provide the expected values on get calls when using + initial_value during record creation and .get() after IOC initialisation""" - builder.LoadDatabase() - softioc.iocInit() - out_rec.set(initial_value) + run_test_function(record_values, TestEnum.INITIAL_VALUE) - queue.put(out_rec.get()) +def test_value_post_init_set_after_init(record_values): + """Test that records provide the expected values on get calls when using + .set() and .get() after IOC initialisation""" - run_test_function(creation_func, expected_value, expected_type, inner_func) + run_test_function(record_values, TestEnum.SET_AFTER_INIT) From e094ba781ef2d8b43c71e34769a7a2cd31043104 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 13:38:02 +0000 Subject: [PATCH 018/131] Attempt to handle Python2 strings properly --- softioc/device.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 2109387f..b815110c 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -1,8 +1,9 @@ import os +import sys import time -from ctypes import * import numpy +from ctypes import * from . import alarm from .fields import DbfCodeToNumpy, DbrToDbfCode from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field @@ -278,9 +279,12 @@ def convert_to_float(value): def truncate_string(value): """Trim a string to EPICS 40 (39 with null byte) character limit""" - if isinstance(value, bytes): - value = value.decode(errors="replace") - return value[:39] if isinstance(value, str) else None + if sys.version_info >= (3,): + if isinstance(value, bytes): + value = value.decode(errors="replace") + return value[:39] if isinstance(value, str) else None + else: + return value[:39] if isinstance(value, str) else None longin = _Device_In('longin', value_to_epics=convert_to_int) longout = _Device_Out('longout', value_to_epics=convert_to_int) From b5b75862d5b3f220427a97b4bb5fb059717604b5 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 13:38:19 +0000 Subject: [PATCH 019/131] Change name to remove pytest warning --- tests/test_records.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 2dabdbff..35b64d73 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -234,16 +234,16 @@ def test_value_pre_init_initial_value( expected_value, expected_type) -class TestEnum(Enum): - """Enum to control when the record's value should be set""" +class SetValueEnum(Enum): + """Enum to control when and how the record's value should be set""" INITIAL_VALUE = 1 SET_BEFORE_INIT = 2 SET_AFTER_INIT = 3 -def value_test_function(creation_func, initial_value, queue, test_enum): +def value_test_function(creation_func, initial_value, queue, set_enum): kwarg = {} - if test_enum == TestEnum.INITIAL_VALUE: + if set_enum == SetValueEnum.INITIAL_VALUE: kwarg.update({"initial_value": initial_value}) elif creation_func in [builder.WaveformIn, builder.WaveformOut]: kwarg = {"length": 50} # Required when no value on creation @@ -253,19 +253,19 @@ def value_test_function(creation_func, initial_value, queue, test_enum): out_rec = creation_func("out-record", **kwarg) - if test_enum == TestEnum.SET_BEFORE_INIT: + if set_enum == SetValueEnum.SET_BEFORE_INIT: out_rec.set(initial_value) builder.LoadDatabase() softioc.iocInit() - if test_enum == TestEnum.SET_AFTER_INIT: + if set_enum == SetValueEnum.SET_AFTER_INIT: out_rec.set(initial_value) queue.put(out_rec.get()) -def run_test_function(record_values, test_enum): +def run_test_function(record_values, set_enum): """Run the test function using multiprocessing and check returned value is expected value""" @@ -274,7 +274,7 @@ def run_test_function(record_values, test_enum): queue = multiprocessing.Queue() process = multiprocessing.Process( target=value_test_function, - args=(creation_func, initial_value, queue, test_enum) + args=(creation_func, initial_value, queue, set_enum) ) process.start() @@ -295,16 +295,16 @@ def test_value_post_init_set(record_values): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and .get() after initialisation""" - run_test_function(record_values, TestEnum.SET_BEFORE_INIT) + run_test_function(record_values, SetValueEnum.SET_BEFORE_INIT) def test_value_post_init_initial_value(record_values): """Test that records provide the expected values on get calls when using initial_value during record creation and .get() after IOC initialisation""" - run_test_function(record_values, TestEnum.INITIAL_VALUE) + run_test_function(record_values, SetValueEnum.INITIAL_VALUE) def test_value_post_init_set_after_init(record_values): """Test that records provide the expected values on get calls when using .set() and .get() after IOC initialisation""" - run_test_function(record_values, TestEnum.SET_AFTER_INIT) + run_test_function(record_values, SetValueEnum.SET_AFTER_INIT) From 62ac99df99d672155d55a84ad138eb6f1b5933ef Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 14:23:16 +0000 Subject: [PATCH 020/131] Additional Python2 UTF-8 handling --- softioc/device.py | 1 + tests/test_records.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/softioc/device.py b/softioc/device.py index b815110c..8ac288df 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -284,6 +284,7 @@ def truncate_string(value): value = value.decode(errors="replace") return value[:39] if isinstance(value, str) else None else: + # Python2 strings are equivalent to Python3 bytestrings return value[:39] if isinstance(value, str) else None longin = _Device_In('longin', value_to_epics=convert_to_int) diff --git a/tests/test_records.py b/tests/test_records.py index 35b64d73..8022f78c 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -4,6 +4,7 @@ import numpy import os import pytest +import sys from enum import Enum @@ -193,6 +194,10 @@ def record_value_asserts( "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type else: + # Python2 handles UTF-8 differently so needs extra encoding + if sys.version_info == (2, 7) and expected_type == str: + expected_value = expected_value.encode('utf-8') + assert actual_value == expected_value assert type(actual_value) == expected_type From 237d590e017b5e716a42723b420c07937b03ea18 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 15:51:48 +0000 Subject: [PATCH 021/131] Fix incorrect Python2 version test --- tests/test_records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_records.py b/tests/test_records.py index 8022f78c..13e02bd9 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -195,7 +195,7 @@ def record_value_asserts( assert type(actual_value) == expected_type else: # Python2 handles UTF-8 differently so needs extra encoding - if sys.version_info == (2, 7) and expected_type == str: + if sys.version_info < (3,) and expected_type == str: expected_value = expected_value.encode('utf-8') assert actual_value == expected_value From 99334e13409ae1a00c9e139f635d7ac6dc77187d Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 16:16:18 +0000 Subject: [PATCH 022/131] Stop cothread on Win+another Python2 string fix --- tests/test_records.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 13e02bd9..bc6951a2 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -8,7 +8,7 @@ from enum import Enum -from softioc import builder, softioc +from softioc import builder, softioc, asyncio_dispatcher from epicsdbbuilder import ResetRecords from softioc.device_core import RecordLookup import sim_records @@ -196,7 +196,7 @@ def record_value_asserts( else: # Python2 handles UTF-8 differently so needs extra encoding if sys.version_info < (3,) and expected_type == str: - expected_value = expected_value.encode('utf-8') + expected_value = expected_value.decode(errors="replace") assert actual_value == expected_value assert type(actual_value) == expected_type @@ -261,8 +261,10 @@ def value_test_function(creation_func, initial_value, queue, set_enum): if set_enum == SetValueEnum.SET_BEFORE_INIT: out_rec.set(initial_value) + dispatcher = asyncio_dispatcher.AsyncioDispatcher() + builder.LoadDatabase() - softioc.iocInit() + softioc.iocInit(dispatcher) if set_enum == SetValueEnum.SET_AFTER_INIT: out_rec.set(initial_value) From c69272b8120aa8e1389174c2a8b5db7b9837082e Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 16:24:49 +0000 Subject: [PATCH 023/131] Revert "Stop cothread on Win+another Python2 string fix" This reverts commit da1dca99affdd94fa5dcfb93aae83471e8272476. --- tests/test_records.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index bc6951a2..13e02bd9 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -8,7 +8,7 @@ from enum import Enum -from softioc import builder, softioc, asyncio_dispatcher +from softioc import builder, softioc from epicsdbbuilder import ResetRecords from softioc.device_core import RecordLookup import sim_records @@ -196,7 +196,7 @@ def record_value_asserts( else: # Python2 handles UTF-8 differently so needs extra encoding if sys.version_info < (3,) and expected_type == str: - expected_value = expected_value.decode(errors="replace") + expected_value = expected_value.encode('utf-8') assert actual_value == expected_value assert type(actual_value) == expected_type @@ -261,10 +261,8 @@ def value_test_function(creation_func, initial_value, queue, set_enum): if set_enum == SetValueEnum.SET_BEFORE_INIT: out_rec.set(initial_value) - dispatcher = asyncio_dispatcher.AsyncioDispatcher() - builder.LoadDatabase() - softioc.iocInit(dispatcher) + softioc.iocInit() if set_enum == SetValueEnum.SET_AFTER_INIT: out_rec.set(initial_value) From 2a1d5ee7cc484f141c446daf24ba8d97d89ada3e Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 16:25:30 +0000 Subject: [PATCH 024/131] Another Python2 string handling fix --- tests/test_records.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_records.py b/tests/test_records.py index 13e02bd9..dacc88be 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -196,7 +196,7 @@ def record_value_asserts( else: # Python2 handles UTF-8 differently so needs extra encoding if sys.version_info < (3,) and expected_type == str: - expected_value = expected_value.encode('utf-8') + expected_value = expected_value.decode(errors="replace") assert actual_value == expected_value assert type(actual_value) == expected_type From ed464d0236ddf87b8e77434cb1be6d849b80f094 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 23 Nov 2021 16:28:18 +0000 Subject: [PATCH 025/131] Delete unused fixture --- tests/test_records.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index dacc88be..816ba649 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -13,16 +13,6 @@ from softioc.device_core import RecordLookup import sim_records -# Counter for unique record number -counter = 0 - -@pytest.fixture -def record_number(): - """Unique counter for record naming""" - global counter - counter += 1 - return counter - def _clear_records(): # Remove any records created at epicsdbbuilder layer From e4718175112b77c774ff2b3993061c008c8aa466 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 24 Nov 2021 09:06:35 +0000 Subject: [PATCH 026/131] Python2 decoding fix. Skip IOC init on Windows. --- tests/test_records.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 816ba649..b2f29903 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -13,6 +13,11 @@ from softioc.device_core import RecordLookup import sim_records +requires_cothread = pytest.mark.skipif( + sys.platform.startswith("win"), + reason="Cothread doesn't work on windows" + ) + def _clear_records(): # Remove any records created at epicsdbbuilder layer @@ -186,12 +191,12 @@ def record_value_asserts( else: # Python2 handles UTF-8 differently so needs extra encoding if sys.version_info < (3,) and expected_type == str: - expected_value = expected_value.decode(errors="replace") + expected_value = expected_value.decode("utf-8", errors="replace") assert actual_value == expected_value assert type(actual_value) == expected_type -def test_value_retrieval_pre_init_set( +def test_value_pre_init_set( clear_records, record_values): """Test that records provide the expected values on get calls when using @@ -286,18 +291,21 @@ def run_test_function(record_values, set_enum): process.terminate() process.join(timeout=3) +@requires_cothread def test_value_post_init_set(record_values): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and .get() after initialisation""" run_test_function(record_values, SetValueEnum.SET_BEFORE_INIT) +@requires_cothread def test_value_post_init_initial_value(record_values): """Test that records provide the expected values on get calls when using initial_value during record creation and .get() after IOC initialisation""" run_test_function(record_values, SetValueEnum.INITIAL_VALUE) +@requires_cothread def test_value_post_init_set_after_init(record_values): """Test that records provide the expected values on get calls when using .set() and .get() after IOC initialisation""" From 00d622d2a04e3a6dd9db2157834f1d16f8ff0115 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 24 Nov 2021 10:20:34 +0000 Subject: [PATCH 027/131] Swap to using unicode strings in tests. --- softioc/device.py | 3 ++- tests/test_records.py | 16 ++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 8ac288df..58bd86b2 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -284,7 +284,8 @@ def truncate_string(value): value = value.decode(errors="replace") return value[:39] if isinstance(value, str) else None else: - # Python2 strings are equivalent to Python3 bytestrings + if isinstance(value, bytes): + value = value.decode("utf-8", errors="replace") return value[:39] if isinstance(value, str) else None longin = _Device_In('longin', value_to_epics=convert_to_int) diff --git a/tests/test_records.py b/tests/test_records.py index b2f29903..b2f2d10c 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -61,12 +61,12 @@ def idfn(fixture_value): (builder.stringIn, "", "", str), (builder.stringOut, b"abc", "abc", str), (builder.stringIn, b"abc", "abc", str), - (builder.stringOut, b"a\xcfb", "a�b", str), # Invalid UTF-8 - (builder.stringIn, b"a\xcfb", "a�b", str), # Invalid UTF-8 - (builder.stringOut, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 - (builder.stringIn, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 - (builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 - (builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 + (builder.stringOut, b"a\xcfb", u"a�b", str), # Invalid UTF-8 + (builder.stringIn, b"a\xcfb", u"a�b", str), # Invalid UTF-8 + (builder.stringOut, b"a\xe2\x82\xacb", u"a€b", str), # Valid UTF-8 + (builder.stringIn, b"a\xe2\x82\xacb", u"a€b", str), # Valid UTF-8 + (builder.stringOut, u"a€b", u"a€b", str), # Valid UTF-8 + (builder.stringIn, u"a€b", u"a€b", str), # Valid UTF-8 ( builder.stringOut, "this string is much longer than 40 characters", @@ -189,10 +189,6 @@ def record_value_asserts( "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type else: - # Python2 handles UTF-8 differently so needs extra encoding - if sys.version_info < (3,) and expected_type == str: - expected_value = expected_value.decode("utf-8", errors="replace") - assert actual_value == expected_value assert type(actual_value) == expected_type From 9f32a3638188ab24e8f827aeb1ba07530272393d Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 24 Nov 2021 10:29:09 +0000 Subject: [PATCH 028/131] Fix previous commit's unicode handling --- softioc/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 58bd86b2..f6ce322f 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -284,9 +284,9 @@ def truncate_string(value): value = value.decode(errors="replace") return value[:39] if isinstance(value, str) else None else: - if isinstance(value, bytes): + if isinstance(value, bytes): # bytes == str in Python2 value = value.decode("utf-8", errors="replace") - return value[:39] if isinstance(value, str) else None + return value[:39] if isinstance(value, unicode) else None longin = _Device_In('longin', value_to_epics=convert_to_int) longout = _Device_Out('longout', value_to_epics=convert_to_int) From 9656b87be17de586bd3597e6189cb2265102efed Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 1 Dec 2021 08:56:21 +0000 Subject: [PATCH 029/131] Add tests for default value checking --- tests/test_records.py | 61 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/test_records.py b/tests/test_records.py index b2f2d10c..4a18bb17 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -35,6 +35,7 @@ def clear_records(): yield _clear_records() + def idfn(fixture_value): """Provide a nice name for the tests in the record_values fixture""" return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ @@ -235,6 +236,7 @@ class SetValueEnum(Enum): INITIAL_VALUE = 1 SET_BEFORE_INIT = 2 SET_AFTER_INIT = 3 + NO_VALUE = 4 def value_test_function(creation_func, initial_value, queue, set_enum): kwarg = {} @@ -307,3 +309,62 @@ def test_value_post_init_set_after_init(record_values): .set() and .get() after IOC initialisation""" run_test_function(record_values, SetValueEnum.SET_AFTER_INIT) + +@pytest.mark.parametrize("creation_func,expected_value,expected_type", [ + (builder.aOut, None, type(None)), + (builder.aIn, 0.0, float), + (builder.longOut, None, type(None)), + (builder.longIn, 0, int), + (builder.boolOut, None, type(None)), + (builder.boolIn, 0, int), + (builder.stringOut, None, type(None)), + (builder.stringIn, "", str), + (builder.mbbOut, None, type(None)), + (builder.mbbIn, 0, int), + (builder.WaveformOut, None, type(None)), + (builder.WaveformIn, [], numpy.ndarray), +]) +def test_value_default_pre_init( + creation_func, + expected_value, + expected_type, + clear_records): + """Test that the correct default values are returned from .get() (before + record initialisation) when no initial_value or .set() is done""" + + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func("out-record", **kwarg) + record_value_asserts( + creation_func, + out_rec.get(), + expected_value, + expected_type) + + +@requires_cothread +@pytest.mark.parametrize("creation_func,expected_value,expected_type", [ + (builder.aOut, 0.0, float), + (builder.aIn, 0.0, float), + (builder.longOut, 0, int), + (builder.longIn, 0, int), + (builder.boolOut, 0, int), + (builder.boolIn, 0, int), + (builder.stringOut, "", str), + (builder.stringIn, "", str), + (builder.mbbOut, 0, int), + (builder.mbbIn, 0, int), + (builder.WaveformOut, [], numpy.ndarray), + (builder.WaveformIn, [], numpy.ndarray), +]) +def test_value_default_post_init(creation_func, expected_value, expected_type): + """Test that records provide the expected default value after they are + initialised, having never had a value set""" + + # Add a dummy initial_value to the tuple, so we can use the same framework + # as the other tests + tmp = list((creation_func, expected_value, expected_type)) + tmp.insert(1, None) + run_test_function(tuple(tmp), SetValueEnum.NO_VALUE) From cfc13704df3b2a6398767651bbf0023c065c6d1d Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 1 Dec 2021 08:58:51 +0000 Subject: [PATCH 030/131] Remove unnecessary unicode specifiers Now that Python2 support is dropped, Python3 strings are unicode anyway --- tests/test_records.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 4a18bb17..601c2545 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -62,12 +62,12 @@ def idfn(fixture_value): (builder.stringIn, "", "", str), (builder.stringOut, b"abc", "abc", str), (builder.stringIn, b"abc", "abc", str), - (builder.stringOut, b"a\xcfb", u"a�b", str), # Invalid UTF-8 - (builder.stringIn, b"a\xcfb", u"a�b", str), # Invalid UTF-8 - (builder.stringOut, b"a\xe2\x82\xacb", u"a€b", str), # Valid UTF-8 - (builder.stringIn, b"a\xe2\x82\xacb", u"a€b", str), # Valid UTF-8 - (builder.stringOut, u"a€b", u"a€b", str), # Valid UTF-8 - (builder.stringIn, u"a€b", u"a€b", str), # Valid UTF-8 + (builder.stringOut, b"a\xcfb", "a�b", str), # Invalid UTF-8 + (builder.stringIn, b"a\xcfb", "a�b", str), # Invalid UTF-8 + (builder.stringOut, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 + (builder.stringIn, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 + (builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 + (builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 ( builder.stringOut, "this string is much longer than 40 characters", From 2c517811dd75206e2b39c64ffbe7d648422f85c3 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 1 Dec 2021 09:19:19 +0000 Subject: [PATCH 031/131] Cover additional cases --- softioc/device.py | 31 +++++++++++++++---------------- tests/test_records.py | 4 ++++ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index f6ce322f..3e4dbe1c 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -179,8 +179,9 @@ def init_record(self, record): special record initialisation if an initial value has been specified, allowing out records to have a sensible initial value.''' if self._value is None: - # Cannot set in __init__, as we want the record alarm status to be - # set if no value was provided + # Cannot set in __init__ (like we do for In records), as we want + # the record alarm status to be set if no value was provided + # Probably related to PythonSoftIOC issue #53 self._value = self._default_ else: self._write_value(record, self._value) @@ -269,29 +270,27 @@ def convert_to_int(value): """Attempt to convert any type into its integer representation""" if type(value) == c_int: return value.value - return int(value) if value else None + # Can't just check value, as default value of 0 is false! + return int(value) if value is not None else None def convert_to_float(value): """Attempt to convert any type into its float representation""" if type(value) == c_float: return value.value - return float(value) if value else None + # Can't just check value, as default value of 0.0 is false! + return float(value) if value is not None else None def truncate_string(value): """Trim a string to EPICS 40 (39 with null byte) character limit""" - if sys.version_info >= (3,): - if isinstance(value, bytes): - value = value.decode(errors="replace") - return value[:39] if isinstance(value, str) else None - else: - if isinstance(value, bytes): # bytes == str in Python2 - value = value.decode("utf-8", errors="replace") - return value[:39] if isinstance(value, unicode) else None + if isinstance(value, bytes): + value = value.decode(errors="replace") + return value[:39] if isinstance(value, str) else None + longin = _Device_In('longin', value_to_epics=convert_to_int) longout = _Device_Out('longout', value_to_epics=convert_to_int) -bi = _Device_In('bi', convert=False) -bo = _Device_Out('bo', convert=False) +bi = _Device_In('bi', convert=False, value_to_epics=convert_to_int) +bo = _Device_Out('bo', convert=False, value_to_epics=convert_to_int) stringin = _Device_In('stringin', mlst=False, default='', value_to_epics=truncate_string) stringout = _Device_Out('stringout', mlst=False, default='', @@ -347,7 +346,7 @@ class WaveformBase(ProcessDeviceSupportCore): # Allow set() to be called before init_record: dtype = None - _default_ = () + _default_ = numpy.array([]) def init_record(self, record): self.dtype = DbfCodeToNumpy[record.FTVL] @@ -397,7 +396,7 @@ def _handle_strings(self, value): def _value_to_epics(self, value): - # Caused by no initial_value being specified + # Only None when WaveformOut record is not given an initial_value if value is None: return value diff --git a/tests/test_records.py b/tests/test_records.py index 601c2545..0486beb5 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -56,6 +56,10 @@ def idfn(fixture_value): (builder.longIn, ctypes.c_int(6), 6, int), (builder.boolOut, 1, 1, int), (builder.boolIn, 1, 1, int), + (builder.boolOut, True, 1, int), + (builder.boolIn, True, 1, int), + (builder.boolOut, False, 0, int), + (builder.boolIn, False, 0, int), (builder.stringOut, "abc", "abc", str), (builder.stringIn, "abc", "abc", str), (builder.stringOut, "", "", str), From 7f8e57a7474168ee999b2e19c1f2e58593bdf22a Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 1 Dec 2021 09:44:49 +0000 Subject: [PATCH 032/131] Remove TODO Question is asked in the PR instead. --- softioc/builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/softioc/builder.py b/softioc/builder.py index a5ca2bc3..56e05bd4 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -23,7 +23,6 @@ def _in_record(record, name, **fields): fields.setdefault('SCAN', 'I/O Intr') if 'initial_value' in fields: - # TODO: Ask about this, as doing .set won't set this extra field? fields.setdefault('PINI', 'YES') fields.setdefault('DISP', 1) return getattr(PythonDevice, record)(name, **fields) From 4e59eaa311e90421c19a4e0ac00e567dc817bdd1 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 1 Dec 2021 13:29:57 +0000 Subject: [PATCH 033/131] Formatting fixes from code review --- softioc/device.py | 3 ++- tests/test_records.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 3e4dbe1c..42715fc7 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -69,7 +69,7 @@ def _write_value(self, record, value): def value_to_dbr(self, value): if isinstance(value, str): - value = value.encode(errors='replace') + value = value.encode(errors = 'replace') # First convert the data directly into an array. This will help in # subsequent processing: this does most of the type coercion. @@ -417,5 +417,6 @@ class waveform_out(WaveformBase, ProcessDeviceSupportOut): _record_type_ = 'waveform' _device_name_ = 'devPython_waveform_out' + # Ensure the .dbd file is loaded. dbLoadDatabase("device.dbd", os.path.dirname(__file__), None) diff --git a/tests/test_records.py b/tests/test_records.py index 0486beb5..8fea60ae 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -14,9 +14,9 @@ import sim_records requires_cothread = pytest.mark.skipif( - sys.platform.startswith("win"), - reason="Cothread doesn't work on windows" - ) + sys.platform.startswith("win"), + reason="Cothread doesn't work on windows" +) def _clear_records(): From db7b4306efa207db7dd0280318e115178c42e17d Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 1 Dec 2021 16:15:49 +0000 Subject: [PATCH 034/131] Change accepted typed to exclude ctypes and None None (and ctypes) will now raise exceptions if used as a value. Added tests for rejecting None at all stages of processing. --- softioc/device.py | 80 ++++++++++++++++++------------------ tests/test_records.py | 95 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 48 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 42715fc7..801bc87c 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -54,7 +54,8 @@ class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): # Function to convert an incoming Python value into EPICS-appropriate form. def _value_to_epics(self, value): - return value + # Method must be overriden, just defined for type checking/assistance + raise NotImplementedError # Default implementations of read and write, overwritten where necessary. def _read_value(self, record): @@ -170,8 +171,16 @@ def __init__(self, name, **kargs): self.__validate = kargs.pop('validate', None) self.__always_update = kargs.pop('always_update', False) - self._value = self._value_to_epics(kargs.pop('initial_value', None)) self.__enable_write = True + + if "initial_value" in kargs: + value = kargs.pop('initial_value') + if value is None: + raise ValueError("\"None\" is not a valid initial_value") + self._value = self._value_to_epics(value) + else: + self._value = None + self.__super.__init__(name, **kargs) def init_record(self, record): @@ -236,10 +245,10 @@ def get(self): def _Device( Base, record_type, + value_to_epics, mlst=False, default=0, - convert=True, - value_to_epics=None): + convert=True): '''Wrapper for generating simple records.''' class GenericDevice(Base): _record_type_ = record_type @@ -247,9 +256,7 @@ class GenericDevice(Base): _default_ = default _fields_ = ['UDF', 'VAL'] _epics_rc = EPICS_OK if convert else NO_CONVERT - if value_to_epics: - _value_to_epics = \ - staticmethod(value_to_epics) + _value_to_epics = staticmethod(value_to_epics) if mlst: _fields_.append('MLST') @@ -259,44 +266,37 @@ class GenericDevice(Base): _In = ProcessDeviceSupportIn _Out = ProcessDeviceSupportOut -def _Device_In(record_type, **kargs): - return _Device(_In, record_type, **kargs) +def _Device_In(record_type, value_to_epics, **kargs): + return _Device(_In, record_type, value_to_epics, **kargs) -def _Device_Out(record_type, convert=True, mlst=True, **kargs): - return _Device(_Out, record_type, convert=convert, mlst=mlst, **kargs) +def _Device_Out(record_type, value_to_epics, convert=True, mlst=True, **kargs): + return _Device(_Out, + record_type, + value_to_epics, + convert=convert, + mlst=mlst, + **kargs) -def convert_to_int(value): - """Attempt to convert any type into its integer representation""" - if type(value) == c_int: - return value.value - # Can't just check value, as default value of 0 is false! - return int(value) if value is not None else None - -def convert_to_float(value): - """Attempt to convert any type into its float representation""" - if type(value) == c_float: - return value.value - # Can't just check value, as default value of 0.0 is false! - return float(value) if value is not None else None - -def truncate_string(value): +def epics_string(value): """Trim a string to EPICS 40 (39 with null byte) character limit""" + if value is None: + raise ValueError("\"None\" not a valid value") + if isinstance(value, bytes): value = value.decode(errors="replace") - return value[:39] if isinstance(value, str) else None + + return str(value)[:39] -longin = _Device_In('longin', value_to_epics=convert_to_int) -longout = _Device_Out('longout', value_to_epics=convert_to_int) -bi = _Device_In('bi', convert=False, value_to_epics=convert_to_int) -bo = _Device_Out('bo', convert=False, value_to_epics=convert_to_int) -stringin = _Device_In('stringin', mlst=False, default='', - value_to_epics=truncate_string) -stringout = _Device_Out('stringout', mlst=False, default='', - value_to_epics=truncate_string) -mbbi = _Device_In('mbbi', convert=False) -mbbo = _Device_Out('mbbo', convert=False) +longin = _Device_In('longin', int) +longout = _Device_Out('longout', int) +bi = _Device_In('bi', int, convert=False) +bo = _Device_Out('bo', int, convert=False) +stringin = _Device_In('stringin', epics_string, mlst=False, default='') +stringout = _Device_Out('stringout', epics_string, mlst=False, default='') +mbbi = _Device_In('mbbi', int, convert=False) +mbbo = _Device_Out('mbbo', int, convert=False) dset_process_linconv = ( @@ -314,7 +314,7 @@ class ai(ProcessDeviceSupportIn): _fields_ = ['UDF', 'VAL'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT - _value_to_epics = staticmethod(convert_to_float) + _value_to_epics = staticmethod(float) def _process(self, record): _value = self._value @@ -331,7 +331,7 @@ class ao(ProcessDeviceSupportOut): _fields_ = ['UDF', 'VAL', 'MLST'] _dset_extra_ = dset_process_linconv _epics_rc = NO_CONVERT - _value_to_epics = staticmethod(convert_to_float) + _value_to_epics = staticmethod(float) @@ -398,7 +398,7 @@ def _value_to_epics(self, value): # Only None when WaveformOut record is not given an initial_value if value is None: - return value + raise ValueError("\"None\" not a valid value") value = self._handle_strings(value) diff --git a/tests/test_records.py b/tests/test_records.py index 8fea60ae..397d2410 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -import ctypes import multiprocessing import numpy import os @@ -35,8 +33,32 @@ def clear_records(): yield _clear_records() +def record_funcs_names(fixture_value): + """Provide a nice name for the record_funcs fixture""" + return fixture_value.__name__ -def idfn(fixture_value): + +@pytest.fixture(params=[ + builder.aIn, + builder.aOut, + builder.longIn, + builder.longOut, + builder.boolIn, + builder.boolOut, + builder.stringIn, + builder.stringOut, + builder.mbbIn, + builder.mbbOut, + builder.WaveformIn, + builder.WaveformOut, + ], + ids=record_funcs_names) +def record_funcs(request): + """The list of record creation functions""" + return request.param + + +def record_values_names(fixture_value): """Provide a nice name for the tests in the record_values fixture""" return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ + "-" + fixture_value[3].__name__ @@ -46,14 +68,10 @@ def idfn(fixture_value): (builder.aIn, 5.5, 5.5, float), (builder.aOut, 3, 3., float), (builder.aIn, 3, 3., float), - (builder.aOut, ctypes.c_float(3.5), 3.5, float), - (builder.aIn, ctypes.c_float(3.5), 3.5, float), (builder.longOut, 5, 5, int), (builder.longIn, 5, 5, int), (builder.longOut, 9.9, 9, int), (builder.longIn, 9.9, 9, int), - (builder.longOut, ctypes.c_int(6), 6, int), - (builder.longIn, ctypes.c_int(6), 6, int), (builder.boolOut, 1, 1, int), (builder.boolIn, 1, 1, int), (builder.boolOut, True, 1, int), @@ -125,7 +143,7 @@ def idfn(fixture_value): numpy.ndarray ) ], - ids=idfn) + ids=record_values_names) def record_values(request): """A list of parameters for record value setting/getting tests. @@ -372,3 +390,64 @@ def test_value_default_post_init(creation_func, expected_value, expected_type): tmp = list((creation_func, expected_value, expected_type)) tmp.insert(1, None) run_test_function(tuple(tmp), SetValueEnum.NO_VALUE) + + +def test_value_none_rejected_initial_value(clear_records, record_funcs): + """Test that setting \"None\" as the initial_value raises an exception""" + + kwarg = {} + if record_funcs in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises((ValueError, TypeError)): + record_funcs("SOME-NAME", initial_value=None, **kwarg) + +def test_value_none_rejected_set_before_init(clear_records, record_funcs): + """Test that setting \"None\" using .set() raises an exception""" + + kwarg = {} + if record_funcs in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises((ValueError, TypeError)): + record = record_funcs("SOME-NAME", **kwarg) + record.set(None) + +def test_value_none_rejected_set_after_init(record_funcs): + """Test that setting \"None\" using .set() after IOc init raises an + exception""" + + def inner_func(record_func, queue): + """Start the IOC and catch the expected exception""" + kwarg = {} + if record_funcs in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + record = record_funcs("SOME-NAME", **kwarg) + + builder.LoadDatabase() + softioc.iocInit() + + try: + record.set(None) + except Exception as e: + queue.put(e) + + queue.put(Exception("FAIL:Test did not raise exception during .set()")) + + + queue = multiprocessing.Queue() + process = multiprocessing.Process( + target=inner_func, + args=(record_funcs, queue) + ) + + process.start() + + try: + exception = queue.get(timeout=5) + + assert isinstance(exception, (ValueError, TypeError)) + finally: + process.terminate() + process.join(timeout=3) From 7c417af5be0c1fe17f449a209038bef9d008c859 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 1 Dec 2021 16:22:18 +0000 Subject: [PATCH 035/131] Add missing requires_cothread marker --- tests/test_records.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_records.py b/tests/test_records.py index 397d2410..559abed0 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -413,6 +413,7 @@ def test_value_none_rejected_set_before_init(clear_records, record_funcs): record = record_funcs("SOME-NAME", **kwarg) record.set(None) +@requires_cothread def test_value_none_rejected_set_after_init(record_funcs): """Test that setting \"None\" using .set() after IOc init raises an exception""" From 157b934a8aa640199f1870c1d9ad9e38cc36a159 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 2 Dec 2021 08:36:57 +0000 Subject: [PATCH 036/131] Fix Windows fail - move inner function to top level --- tests/test_records.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 559abed0..3bcfbb0a 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -413,33 +413,31 @@ def test_value_none_rejected_set_before_init(clear_records, record_funcs): record = record_funcs("SOME-NAME", **kwarg) record.set(None) -@requires_cothread -def test_value_none_rejected_set_after_init(record_funcs): - """Test that setting \"None\" using .set() after IOc init raises an - exception""" - - def inner_func(record_func, queue): - """Start the IOC and catch the expected exception""" - kwarg = {} - if record_funcs in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - - record = record_funcs("SOME-NAME", **kwarg) +def none_value_test_func(record_func, queue): + """Start the IOC and catch the expected exception""" + kwarg = {} + if record_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation - builder.LoadDatabase() - softioc.iocInit() + record = record_func("SOME-NAME", **kwarg) - try: - record.set(None) - except Exception as e: - queue.put(e) + builder.LoadDatabase() + softioc.iocInit() - queue.put(Exception("FAIL:Test did not raise exception during .set()")) + try: + record.set(None) + except Exception as e: + queue.put(e) + queue.put(Exception("FAIL:Test did not raise exception during .set()")) +@requires_cothread +def test_value_none_rejected_set_after_init(record_funcs): + """Test that setting \"None\" using .set() after IOc init raises an + exception""" queue = multiprocessing.Queue() process = multiprocessing.Process( - target=inner_func, + target=none_value_test_func, args=(record_funcs, queue) ) From f54eaf3d299a51d6433ee79dfc2ec6855544191a Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 2 Dec 2021 10:53:08 +0000 Subject: [PATCH 037/131] Fix formatting issues --- softioc/device.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 801bc87c..da55e233 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -2,8 +2,8 @@ import sys import time import numpy - from ctypes import * + from . import alarm from .fields import DbfCodeToNumpy, DbrToDbfCode from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field @@ -21,19 +21,19 @@ NO_CONVERT = 2 NumpyCharCodeToDbr = { - # The following type codes are supported directly: - 'S': 0, # DBR_STRING str_ - 'h': 1, # DBR_SHORT short = int16 - 'f': 2, # DBR_FLOAT single = float32 - 'b': 4, # DBR_CHAR byte = int8 - 'i': 5, # DBR_LONG intc = int32 - 'd': 6, # DBR_DOUBLE float_ = float64 - # These are supported as related types - 'H': 1, # DBR_SHORT ushort = uint16 - '?': 4, # DBR_CHAR bool_ - 'B': 4, # DBR_CHAR ubyte = uint8 - 'I': 5, # DBR_LONG uintc = uint32 - } + # The following type codes are supported directly: + 'S': 0, # DBR_STRING str_ + 'h': 1, # DBR_SHORT short = int16 + 'f': 2, # DBR_FLOAT single = float32 + 'b': 4, # DBR_CHAR byte = int8 + 'i': 5, # DBR_LONG intc = int32 + 'd': 6, # DBR_DOUBLE float_ = float64 + # These are supported as related types + 'H': 1, # DBR_SHORT ushort = uint16 + '?': 4, # DBR_CHAR bool_ + 'B': 4, # DBR_CHAR ubyte = uint8 + 'I': 5, # DBR_LONG uintc = uint32 +} if numpy.int_().itemsize == 4: NumpyCharCodeToDbr.update({'l': 5, 'L': 5}) # int_, uint From 05c5c7a11945aba9c7213aaa6a7f3165c8c5b9ec Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 2 Dec 2021 11:22:46 +0000 Subject: [PATCH 038/131] Remove unnecessary exception, and fix formatting --- softioc/device.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index da55e233..3a8c0ec5 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -174,10 +174,7 @@ def __init__(self, name, **kargs): self.__enable_write = True if "initial_value" in kargs: - value = kargs.pop('initial_value') - if value is None: - raise ValueError("\"None\" is not a valid initial_value") - self._value = self._value_to_epics(value) + self._value = self._value_to_epics(kargs.pop('initial_value')) else: self._value = None @@ -387,10 +384,7 @@ def _handle_strings(self, value): # the correct behaviour when treating a character array as a string. # Note that the trailing null is needed to work around problems with # some clients. Note this also exists in builder.py's _waveform(). - value = numpy.frombuffer( - value + b'\0', - dtype = numpy.uint8 - ) + value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) return value From da2e38b137e2384a5e98ed6be22b4e98686e87fc Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 6 Dec 2021 09:21:55 +0000 Subject: [PATCH 039/131] Remove another unnecessary exception --- softioc/device.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 3a8c0ec5..c97372e8 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -277,9 +277,6 @@ def _Device_Out(record_type, value_to_epics, convert=True, mlst=True, **kargs): def epics_string(value): """Trim a string to EPICS 40 (39 with null byte) character limit""" - if value is None: - raise ValueError("\"None\" not a valid value") - if isinstance(value, bytes): value = value.decode(errors="replace") From ddf87bdaf3f0469301a48d07a1daa827e5b9264c Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 6 Dec 2021 11:07:01 +0000 Subject: [PATCH 040/131] Skip tests putting None into String records. "None" is considered a valid value for String records - it'll result in the literal string "None" appearing in the output record, which is acceptable behaviour. --- tests/test_records.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 3bcfbb0a..eeab9f5e 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -57,6 +57,12 @@ def record_funcs(request): """The list of record creation functions""" return request.param +@pytest.fixture +def record_funcs_reject_none(record_funcs): + """The list of record creation functions that reject 'None' as a value""" + if record_funcs in [builder.stringIn, builder.stringOut]: + pytest.skip(msg="None is valid value for string records") + return record_funcs def record_values_names(fixture_value): """Provide a nice name for the tests in the record_values fixture""" @@ -392,25 +398,29 @@ def test_value_default_post_init(creation_func, expected_value, expected_type): run_test_function(tuple(tmp), SetValueEnum.NO_VALUE) -def test_value_none_rejected_initial_value(clear_records, record_funcs): +def test_value_none_rejected_initial_value( + clear_records, + record_funcs_reject_none): """Test that setting \"None\" as the initial_value raises an exception""" kwarg = {} - if record_funcs in [builder.WaveformIn, builder.WaveformOut]: + if record_funcs_reject_none in [builder.WaveformIn, builder.WaveformOut]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises((ValueError, TypeError)): - record_funcs("SOME-NAME", initial_value=None, **kwarg) + record_funcs_reject_none("SOME-NAME", initial_value=None, **kwarg) -def test_value_none_rejected_set_before_init(clear_records, record_funcs): +def test_value_none_rejected_set_before_init( + clear_records, + record_funcs_reject_none): """Test that setting \"None\" using .set() raises an exception""" kwarg = {} - if record_funcs in [builder.WaveformIn, builder.WaveformOut]: + if record_funcs_reject_none in [builder.WaveformIn, builder.WaveformOut]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises((ValueError, TypeError)): - record = record_funcs("SOME-NAME", **kwarg) + record = record_funcs_reject_none("SOME-NAME", **kwarg) record.set(None) def none_value_test_func(record_func, queue): @@ -432,13 +442,13 @@ def none_value_test_func(record_func, queue): queue.put(Exception("FAIL:Test did not raise exception during .set()")) @requires_cothread -def test_value_none_rejected_set_after_init(record_funcs): +def test_value_none_rejected_set_after_init(record_funcs_reject_none): """Test that setting \"None\" using .set() after IOc init raises an exception""" queue = multiprocessing.Queue() process = multiprocessing.Process( target=none_value_test_func, - args=(record_funcs, queue) + args=(record_funcs_reject_none, queue) ) process.start() From c5b14fb5fbce993437c430611bb36fffac2ee339 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 9 Dec 2021 08:39:49 +0000 Subject: [PATCH 041/131] Add tests for inf, -inf, and nan values --- tests/test_records.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index eeab9f5e..8caaf7cc 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -5,6 +5,7 @@ import sys from enum import Enum +from math import isnan, inf, nan from softioc import builder, softioc from epicsdbbuilder import ResetRecords @@ -74,6 +75,12 @@ def record_values_names(fixture_value): (builder.aIn, 5.5, 5.5, float), (builder.aOut, 3, 3., float), (builder.aIn, 3, 3., float), + (builder.aOut, inf, inf, float), + (builder.aIn, inf, inf, float), + (builder.aOut, -inf, -inf, float), + (builder.aIn, -inf, -inf, float), + (builder.aOut, nan, nan, float), + (builder.aIn, nan, nan, float), (builder.longOut, 5, 5, int), (builder.longIn, 5, 5, int), (builder.longOut, 9.9, 9, int), @@ -212,8 +219,9 @@ def record_value_asserts( expected_type): """Asserts that the expected value and expected type are matched with the actual value. Handles both scalar and waveform data""" - - if creation_func in [builder.WaveformOut, builder.WaveformIn]: + if type(expected_value) == float and isnan(expected_value): + assert isnan(actual_value) # NaN != Nan, so needs special case + elif creation_func in [builder.WaveformOut, builder.WaveformIn]: assert numpy.array_equal(actual_value, expected_value), \ "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type From 1079739038413e924dc95c41e693adb766e8fd4c Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 9 Dec 2021 09:21:38 +0000 Subject: [PATCH 042/131] Refactor test_records into test classes About to introduce a whole new swathe of tests using caget/caput, and so extra organisation was necessary --- tests/test_records.py | 335 ++++++++++++++++++++++++++---------------- 1 file changed, 212 insertions(+), 123 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 8caaf7cc..ac9599a7 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -325,146 +325,235 @@ def run_test_function(record_values, set_enum): process.terminate() process.join(timeout=3) -@requires_cothread -def test_value_post_init_set(record_values): - """Test that records provide the expected values on get calls when using - .set() before IOC initialisation and .get() after initialisation""" - - run_test_function(record_values, SetValueEnum.SET_BEFORE_INIT) - -@requires_cothread -def test_value_post_init_initial_value(record_values): - """Test that records provide the expected values on get calls when using - initial_value during record creation and .get() after IOC initialisation""" - - run_test_function(record_values, SetValueEnum.INITIAL_VALUE) - -@requires_cothread -def test_value_post_init_set_after_init(record_values): - """Test that records provide the expected values on get calls when using - .set() and .get() after IOC initialisation""" - - run_test_function(record_values, SetValueEnum.SET_AFTER_INIT) - -@pytest.mark.parametrize("creation_func,expected_value,expected_type", [ - (builder.aOut, None, type(None)), - (builder.aIn, 0.0, float), - (builder.longOut, None, type(None)), - (builder.longIn, 0, int), - (builder.boolOut, None, type(None)), - (builder.boolIn, 0, int), - (builder.stringOut, None, type(None)), - (builder.stringIn, "", str), - (builder.mbbOut, None, type(None)), - (builder.mbbIn, 0, int), - (builder.WaveformOut, None, type(None)), - (builder.WaveformIn, [], numpy.ndarray), -]) -def test_value_default_pre_init( +def record_value_asserts( creation_func, + actual_value, expected_value, - expected_type, - clear_records): - """Test that the correct default values are returned from .get() (before - record initialisation) when no initial_value or .set() is done""" + expected_type): + """Asserts that the expected value and expected type are matched with + the actual value. Handles both scalar and waveform data""" + if type(expected_value) == float and isnan(expected_value): + assert isnan(actual_value) # NaN != Nan, so needs special case + elif creation_func in [builder.WaveformOut, builder.WaveformIn]: + assert numpy.array_equal(actual_value, expected_value), \ + "Arrays not equal: {} {}".format(actual_value, expected_value) + assert type(actual_value) == expected_type + else: + assert actual_value == expected_value + assert type(actual_value) == expected_type - kwarg = {} - if creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - out_rec = creation_func("out-record", **kwarg) - record_value_asserts( - creation_func, - out_rec.get(), - expected_value, - expected_type) +def test_records(tmp_path): + path = str(tmp_path / "records.db") + builder.WriteRecords(path) + expected = os.path.join(os.path.dirname(__file__), "expected_records.db") + assert open(path).readlines()[5:] == open(expected).readlines() + +def test_enum_length_restriction(): + with pytest.raises(AssertionError): + builder.mbbIn( + "ManyLabels", "one", "two", "three", "four", "five", "six", + "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", + "fourteen", "fifteen", "sixteen", "seventeen") +class TestGetValue: + """Tests that use .get() to check whether values applied with .set() + or initial_value return the expected value""" + def test_value_pre_init_set( + set, + clear_records, + record_values): + """Test that records provide the expected values on get calls when using + .set() and .get() before IOC initialisation occurs""" -@requires_cothread -@pytest.mark.parametrize("creation_func,expected_value,expected_type", [ - (builder.aOut, 0.0, float), - (builder.aIn, 0.0, float), - (builder.longOut, 0, int), - (builder.longIn, 0, int), - (builder.boolOut, 0, int), - (builder.boolIn, 0, int), - (builder.stringOut, "", str), - (builder.stringIn, "", str), - (builder.mbbOut, 0, int), - (builder.mbbIn, 0, int), - (builder.WaveformOut, [], numpy.ndarray), - (builder.WaveformIn, [], numpy.ndarray), -]) -def test_value_default_post_init(creation_func, expected_value, expected_type): - """Test that records provide the expected default value after they are - initialised, having never had a value set""" - - # Add a dummy initial_value to the tuple, so we can use the same framework - # as the other tests - tmp = list((creation_func, expected_value, expected_type)) - tmp.insert(1, None) - run_test_function(tuple(tmp), SetValueEnum.NO_VALUE) - - -def test_value_none_rejected_initial_value( - clear_records, - record_funcs_reject_none): - """Test that setting \"None\" as the initial_value raises an exception""" + creation_func, initial_value, expected_value, expected_type = \ + record_values - kwarg = {} - if record_funcs_reject_none in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation - with pytest.raises((ValueError, TypeError)): - record_funcs_reject_none("SOME-NAME", initial_value=None, **kwarg) + out_rec = creation_func("out-record", **kwarg) + out_rec.set(initial_value) -def test_value_none_rejected_set_before_init( - clear_records, - record_funcs_reject_none): - """Test that setting \"None\" using .set() raises an exception""" + record_value_asserts( + creation_func, + out_rec.get(), + expected_value, + expected_type) - kwarg = {} - if record_funcs_reject_none in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - with pytest.raises((ValueError, TypeError)): - record = record_funcs_reject_none("SOME-NAME", **kwarg) - record.set(None) + def test_value_pre_init_initial_value( + set, + clear_records, + record_values): + """Test that records provide the expected values on get calls when using + initial_value and .get() before IOC initialisation occurs""" -def none_value_test_func(record_func, queue): - """Start the IOC and catch the expected exception""" - kwarg = {} - if record_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation + creation_func, initial_value, expected_value, expected_type = \ + record_values - record = record_func("SOME-NAME", **kwarg) + out_rec = creation_func("out-record", initial_value=initial_value) - builder.LoadDatabase() - softioc.iocInit() + record_value_asserts( + creation_func, + out_rec.get(), + expected_value, + expected_type) - try: - record.set(None) - except Exception as e: - queue.put(e) + @requires_cothread + def test_value_post_init_set(set, record_values): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and .get() after initialisation""" + + run_test_function(record_values, SetValueEnum.SET_BEFORE_INIT) + + @requires_cothread + def test_value_post_init_initial_value(set, record_values): + """Test that records provide the expected values on get calls when using + initial_value during record creation and .get() after IOC initialisation + """ + + run_test_function(record_values, SetValueEnum.INITIAL_VALUE) + + @requires_cothread + def test_value_post_init_set_after_init(set, record_values): + """Test that records provide the expected values on get calls when using + .set() and .get() after IOC initialisation""" + + run_test_function(record_values, SetValueEnum.SET_AFTER_INIT) + +class TestDefaultValue: + """Tests related to default values""" + + @pytest.mark.parametrize("creation_func,expected_value,expected_type", [ + (builder.aOut, None, type(None)), + (builder.aIn, 0.0, float), + (builder.longOut, None, type(None)), + (builder.longIn, 0, int), + (builder.boolOut, None, type(None)), + (builder.boolIn, 0, int), + (builder.stringOut, None, type(None)), + (builder.stringIn, "", str), + (builder.mbbOut, None, type(None)), + (builder.mbbIn, 0, int), + (builder.WaveformOut, None, type(None)), + (builder.WaveformIn, [], numpy.ndarray), + ]) + def test_value_default_pre_init( + self, + creation_func, + expected_value, + expected_type, + clear_records): + """Test that the correct default values are returned from .get() (before + record initialisation) when no initial_value or .set() is done""" - queue.put(Exception("FAIL:Test did not raise exception during .set()")) + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation -@requires_cothread -def test_value_none_rejected_set_after_init(record_funcs_reject_none): - """Test that setting \"None\" using .set() after IOc init raises an - exception""" - queue = multiprocessing.Queue() - process = multiprocessing.Process( - target=none_value_test_func, - args=(record_funcs_reject_none, queue) - ) + out_rec = creation_func("out-record", **kwarg) + record_value_asserts( + creation_func, + out_rec.get(), + expected_value, + expected_type) - process.start() + @requires_cothread + @pytest.mark.parametrize("creation_func,expected_value,expected_type", [ + (builder.aOut, 0.0, float), + (builder.aIn, 0.0, float), + (builder.longOut, 0, int), + (builder.longIn, 0, int), + (builder.boolOut, 0, int), + (builder.boolIn, 0, int), + (builder.stringOut, "", str), + (builder.stringIn, "", str), + (builder.mbbOut, 0, int), + (builder.mbbIn, 0, int), + (builder.WaveformOut, [], numpy.ndarray), + (builder.WaveformIn, [], numpy.ndarray), + ]) + def test_value_default_post_init( + self, + creation_func, + expected_value, + expected_type): + """Test that records provide the expected default value after they are + initialised, having never had a value set""" + + # Add a dummy initial_value to the tuple, so we can use the same + # framework as the other tests + tmp = list((creation_func, expected_value, expected_type)) + tmp.insert(1, None) + run_test_function(tuple(tmp), SetValueEnum.NO_VALUE) + +class TestNoneValue: + """Various tests regarding record value of None""" + def test_value_none_rejected_initial_value( + self, + clear_records, + record_funcs_reject_none): + """Test setting \"None\" as the initial_value raises an exception""" + + kwarg = {} + if record_funcs_reject_none in \ + [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises((ValueError, TypeError)): + record_funcs_reject_none("SOME-NAME", initial_value=None, **kwarg) + + def test_value_none_rejected_set_before_init( + self, + clear_records, + record_funcs_reject_none): + """Test that setting \"None\" using .set() raises an exception""" + + kwarg = {} + if record_funcs_reject_none in \ + [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises((ValueError, TypeError)): + record = record_funcs_reject_none("SOME-NAME", **kwarg) + record.set(None) + + def none_value_test_func(self, record_func, queue): + """Start the IOC and catch the expected exception""" + kwarg = {} + if record_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + record = record_func("SOME-NAME", **kwarg) + + builder.LoadDatabase() + softioc.iocInit() + + try: + record.set(None) + except Exception as e: + queue.put(e) + + queue.put(Exception("FAIL:Test did not raise exception during .set()")) + + @requires_cothread + def test_value_none_rejected_set_after_init(self, record_funcs_reject_none): + """Test that setting \"None\" using .set() after IOc init raises an + exception""" + queue = multiprocessing.Queue() + process = multiprocessing.Process( + target=self.none_value_test_func, + args=(record_funcs_reject_none, queue) + ) - try: - exception = queue.get(timeout=5) + process.start() - assert isinstance(exception, (ValueError, TypeError)) - finally: - process.terminate() - process.join(timeout=3) + try: + exception = queue.get(timeout=5) + + assert isinstance(exception, (ValueError, TypeError)) + finally: + process.terminate() + process.join(timeout=3) From 1688283e80756e27ebe90a1366a84754a942b326 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 9 Dec 2021 14:25:33 +0000 Subject: [PATCH 043/131] First CAGet tests working Also tweak test names for to make it easier to distinguish them --- tests/test_records.py | 126 +++++++++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 20 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index ac9599a7..498baaec 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -3,14 +3,16 @@ import os import pytest import sys +import asyncio from enum import Enum from math import isnan, inf, nan -from softioc import builder, softioc -from epicsdbbuilder import ResetRecords +# Must import softioc before epicsdbbuilder from softioc.device_core import RecordLookup -import sim_records +from softioc import asyncio_dispatcher, builder, softioc + +from epicsdbbuilder import ResetRecords requires_cothread = pytest.mark.skipif( sys.platform.startswith("win"), @@ -68,7 +70,7 @@ def record_funcs_reject_none(record_funcs): def record_values_names(fixture_value): """Provide a nice name for the tests in the record_values fixture""" return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ - + "-" + fixture_value[3].__name__ + + "-" + str(fixture_value[1]) @pytest.fixture(params=[ (builder.aOut, 5.5, 5.5, float), @@ -274,7 +276,19 @@ class SetValueEnum(Enum): SET_AFTER_INIT = 3 NO_VALUE = 4 -def value_test_function(creation_func, initial_value, queue, set_enum): +class GetValueEnum(Enum): + """Enum to control whether values are retrieved using .get() or caget()""" + GET = 1 + CAGET = 2 + + +DEVICE_NAME = "SOFT-IOC-TESTS" +RECORD_NAME = "OUT-RECORD" +TIMEOUT = 3 + +def run_ioc(creation_func, initial_value, queue, set_enum): + """Creates a record and starts the IOC. `initial_value` will be set on + the record at different times based on the `set_enum` parameter.""" kwarg = {} if set_enum == SetValueEnum.INITIAL_VALUE: @@ -285,36 +299,60 @@ def value_test_function(creation_func, initial_value, queue, set_enum): # https://github.com/dls-controls/pythonSoftIOC/issues/37 - out_rec = creation_func("out-record", **kwarg) + builder.SetDeviceName(DEVICE_NAME) + out_rec = creation_func(RECORD_NAME, **kwarg) if set_enum == SetValueEnum.SET_BEFORE_INIT: out_rec.set(initial_value) + dispatcher = asyncio_dispatcher.AsyncioDispatcher() builder.LoadDatabase() - softioc.iocInit() + softioc.iocInit(dispatcher) if set_enum == SetValueEnum.SET_AFTER_INIT: out_rec.set(initial_value) - queue.put(out_rec.get()) + if queue is not None: + queue.put(out_rec.get()) + else: + # Keep process alive while main thread works. + asyncio.run_coroutine_threadsafe( + asyncio.sleep(TIMEOUT), + dispatcher.loop + ).result() -def run_test_function(record_values, set_enum): +def run_test_function( + record_values, + set_enum: SetValueEnum, + get_enum: GetValueEnum): """Run the test function using multiprocessing and check returned value is expected value""" creation_func, initial_value, expected_value, expected_type = record_values - queue = multiprocessing.Queue() - process = multiprocessing.Process( - target=value_test_function, + queue = None + if get_enum == GetValueEnum.GET: + queue = multiprocessing.Queue() + + ioc_process = multiprocessing.Process( + target=run_ioc, args=(creation_func, initial_value, queue, set_enum) ) - process.start() + ioc_process.start() try: - rec_val = queue.get(timeout=5) + if get_enum == GetValueEnum.GET: + rec_val = queue.get(timeout=TIMEOUT) + else: + from cothread.catools import caget, _channel_cache + rec_val = caget(DEVICE_NAME + ":" + RECORD_NAME, timeout=TIMEOUT) + # cothread remembers connected IOCs. As we restart the same named + # IOC multiple times, we have to purge the cache else the + # result from caget cache would be a DisconnectError during the + # second test + _channel_cache.purge() record_value_asserts( creation_func, @@ -322,8 +360,10 @@ def run_test_function(record_values, set_enum): expected_value, expected_type) finally: - process.terminate() - process.join(timeout=3) + ioc_process.terminate() + ioc_process.join(timeout=TIMEOUT) + if ioc_process.exitcode is None: + pytest.fail("Process did not terminate") def record_value_asserts( creation_func, @@ -332,6 +372,13 @@ def record_value_asserts( expected_type): """Asserts that the expected value and expected type are matched with the actual value. Handles both scalar and waveform data""" + from cothread.dbr import ca_float, ca_array, ca_str, ca_int + if type(actual_value) in (ca_float, ca_array, ca_str, ca_int): + # '+' is used to convert values returned from cothread, which are + # AugmentedValues, back into their Python native forms + actual_value = +actual_value + + if type(expected_value) == float and isnan(expected_value): assert isnan(actual_value) # NaN != Nan, so needs special case elif creation_func in [builder.WaveformOut, builder.WaveformIn]: @@ -344,6 +391,8 @@ def record_value_asserts( def test_records(tmp_path): + import sim_records + path = str(tmp_path / "records.db") builder.WriteRecords(path) expected = os.path.join(os.path.dirname(__file__), "expected_records.db") @@ -406,7 +455,10 @@ def test_value_post_init_set(set, record_values): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and .get() after initialisation""" - run_test_function(record_values, SetValueEnum.SET_BEFORE_INIT) + run_test_function( + record_values, + SetValueEnum.SET_BEFORE_INIT, + GetValueEnum.GET) @requires_cothread def test_value_post_init_initial_value(set, record_values): @@ -414,14 +466,48 @@ def test_value_post_init_initial_value(set, record_values): initial_value during record creation and .get() after IOC initialisation """ - run_test_function(record_values, SetValueEnum.INITIAL_VALUE) + run_test_function( + record_values, + SetValueEnum.INITIAL_VALUE, + GetValueEnum.GET) @requires_cothread def test_value_post_init_set_after_init(set, record_values): """Test that records provide the expected values on get calls when using .set() and .get() after IOC initialisation""" - run_test_function(record_values, SetValueEnum.SET_AFTER_INIT) + run_test_function( + record_values, + SetValueEnum.SET_AFTER_INIT, + GetValueEnum.GET) + +class TestCagetValue: + """Tests that use Caget to check whether values applied with .set() + or initial_value return the expected value""" + + def set_xfail(self, record_values): + """Set xfail where appropraite for pythonSoftIOC issues""" + if record_values[0] in ( + builder.aIn, + builder.longIn, + builder.boolIn, + builder.stringIn, + builder.mbbIn, + builder.WaveformIn): + pytest.xfail(".set() on In records doesn't update correctly. " + "pythonSoftIOC issue #67") + + def test_value_post_init_set(self, record_values): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and caget after initialisation""" + + self.set_xfail(record_values) + + run_test_function( + record_values, + SetValueEnum.SET_BEFORE_INIT, + GetValueEnum.CAGET) + class TestDefaultValue: """Tests related to default values""" @@ -487,7 +573,7 @@ def test_value_default_post_init( # framework as the other tests tmp = list((creation_func, expected_value, expected_type)) tmp.insert(1, None) - run_test_function(tuple(tmp), SetValueEnum.NO_VALUE) + run_test_function(tuple(tmp), SetValueEnum.NO_VALUE, GetValueEnum.GET) class TestNoneValue: """Various tests regarding record value of None""" From cea2448d608fff29b50ee34401c44c4fbe2d454f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 9 Dec 2021 15:56:28 +0000 Subject: [PATCH 044/131] Add more CAGet tests This includes many fixes for handling different types through CAGet and CAPut, as values passed or received need a fair amount of massaging to align with existing test framework --- tests/test_records.py | 92 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 498baaec..d39d4c3d 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -274,7 +274,8 @@ class SetValueEnum(Enum): INITIAL_VALUE = 1 SET_BEFORE_INIT = 2 SET_AFTER_INIT = 3 - NO_VALUE = 4 + NO_SET = 4 + CAPUT = 5 class GetValueEnum(Enum): """Enum to control whether values are retrieved using .get() or caget()""" @@ -327,7 +328,8 @@ def run_test_function( set_enum: SetValueEnum, get_enum: GetValueEnum): """Run the test function using multiprocessing and check returned value is - expected value""" + expected value. set_enum and get_enum determine when the record's value is + set and how the value is retrieved, respectively.""" creation_func, initial_value, expected_value, expected_type = record_values @@ -342,17 +344,40 @@ def run_test_function( ioc_process.start() + # Infer some required keywords from parameters + put_kwargs = {} + get_kwargs = {} + if creation_func in [builder.WaveformOut, builder.WaveformIn]: + from cothread.dbr import DBR_CHAR_STR + if type(initial_value) in [str, bytes]: + put_kwargs.update({"datatype": DBR_CHAR_STR}) + get_kwargs.update({"count": len(initial_value) + 1}) + + try: + from cothread.catools import caget, caput, _channel_cache + + # cothread remembers connected IOCs. As we restart the same named + # IOC multiple times, we have to purge the cache else the + # result from caget/caput cache would be a DisconnectError during the + # second test + _channel_cache.purge() + + if set_enum == SetValueEnum.CAPUT: + caput( + DEVICE_NAME + ":" + RECORD_NAME, + initial_value, + wait=True, + **put_kwargs) + if get_enum == GetValueEnum.GET: rec_val = queue.get(timeout=TIMEOUT) else: - from cothread.catools import caget, _channel_cache - rec_val = caget(DEVICE_NAME + ":" + RECORD_NAME, timeout=TIMEOUT) - # cothread remembers connected IOCs. As we restart the same named - # IOC multiple times, we have to purge the cache else the - # result from caget cache would be a DisconnectError during the - # second test - _channel_cache.purge() + rec_val = caget( + DEVICE_NAME + ":" + RECORD_NAME, + timeout=TIMEOUT, + **get_kwargs) + record_value_asserts( creation_func, @@ -409,7 +434,7 @@ class TestGetValue: """Tests that use .get() to check whether values applied with .set() or initial_value return the expected value""" def test_value_pre_init_set( - set, + self, clear_records, record_values): """Test that records provide the expected values on get calls when using @@ -433,7 +458,7 @@ def test_value_pre_init_set( def test_value_pre_init_initial_value( - set, + self, clear_records, record_values): """Test that records provide the expected values on get calls when using @@ -451,7 +476,7 @@ def test_value_pre_init_initial_value( expected_type) @requires_cothread - def test_value_post_init_set(set, record_values): + def test_value_post_init_set(self, record_values): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and .get() after initialisation""" @@ -461,7 +486,7 @@ def test_value_post_init_set(set, record_values): GetValueEnum.GET) @requires_cothread - def test_value_post_init_initial_value(set, record_values): + def test_value_post_init_initial_value(self, record_values): """Test that records provide the expected values on get calls when using initial_value during record creation and .get() after IOC initialisation """ @@ -472,7 +497,7 @@ def test_value_post_init_initial_value(set, record_values): GetValueEnum.GET) @requires_cothread - def test_value_post_init_set_after_init(set, record_values): + def test_value_post_init_set_after_init(self, record_values): """Test that records provide the expected values on get calls when using .set() and .get() after IOC initialisation""" @@ -481,6 +506,7 @@ def test_value_post_init_set_after_init(set, record_values): SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET) +@requires_cothread class TestCagetValue: """Tests that use Caget to check whether values applied with .set() or initial_value return the expected value""" @@ -508,6 +534,42 @@ def test_value_post_init_set(self, record_values): SetValueEnum.SET_BEFORE_INIT, GetValueEnum.CAGET) + @requires_cothread + def test_value_post_init_initial_value(self, record_values): + """Test that records provide the expected values on get calls when using + initial_value during record creation and caget after IOC initialisation + """ + + run_test_function( + record_values, + SetValueEnum.INITIAL_VALUE, + GetValueEnum.GET) + + @requires_cothread + def test_value_post_init_set_after_init(self, record_values): + """Test that records provide the expected values on get calls when using + .set() and caget after IOC initialisation""" + + run_test_function( + record_values, + SetValueEnum.SET_AFTER_INIT, + GetValueEnum.GET) + + def test_value_post_init_caput(self, record_values): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and caget after initialisation""" + + if ( + record_values[0] in [builder.stringIn, builder.stringOut] + and len(record_values[1]) > 40 + ): + pytest.skip("CAPut blocks strings > 40 characters.") + + run_test_function( + record_values, + SetValueEnum.CAPUT, + GetValueEnum.CAGET) + class TestDefaultValue: """Tests related to default values""" @@ -573,7 +635,7 @@ def test_value_default_post_init( # framework as the other tests tmp = list((creation_func, expected_value, expected_type)) tmp.insert(1, None) - run_test_function(tuple(tmp), SetValueEnum.NO_VALUE, GetValueEnum.GET) + run_test_function(tuple(tmp), SetValueEnum.NO_SET, GetValueEnum.GET) class TestNoneValue: """Various tests regarding record value of None""" From c57e1f12adb73892edd219dc599e830843cc4f1d Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 9 Dec 2021 16:04:52 +0000 Subject: [PATCH 045/131] Allow record_value_asserts to run on Windows cothread isn't on Windows, but we still want to run some tests there --- tests/test_records.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index d39d4c3d..fb18c046 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -378,6 +378,12 @@ def run_test_function( timeout=TIMEOUT, **get_kwargs) + from cothread.dbr import ca_float, ca_array, ca_str, ca_int + if type(rec_val) in (ca_float, ca_array, ca_str, ca_int): + # '+' is used to convert values returned from cothread, which + # are AugmentedValues, back into their Python native forms + rec_val = +rec_val + record_value_asserts( creation_func, @@ -397,13 +403,6 @@ def record_value_asserts( expected_type): """Asserts that the expected value and expected type are matched with the actual value. Handles both scalar and waveform data""" - from cothread.dbr import ca_float, ca_array, ca_str, ca_int - if type(actual_value) in (ca_float, ca_array, ca_str, ca_int): - # '+' is used to convert values returned from cothread, which are - # AugmentedValues, back into their Python native forms - actual_value = +actual_value - - if type(expected_value) == float and isnan(expected_value): assert isnan(actual_value) # NaN != Nan, so needs special case elif creation_func in [builder.WaveformOut, builder.WaveformIn]: From 7871e2a8e03bb97c37d7fce2cc6289b5ed273616 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 9 Dec 2021 16:16:08 +0000 Subject: [PATCH 046/131] Use neater type conversion after caget --- tests/test_records.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index fb18c046..90ba2d05 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -378,11 +378,7 @@ def run_test_function( timeout=TIMEOUT, **get_kwargs) - from cothread.dbr import ca_float, ca_array, ca_str, ca_int - if type(rec_val) in (ca_float, ca_array, ca_str, ca_int): - # '+' is used to convert values returned from cothread, which - # are AugmentedValues, back into their Python native forms - rec_val = +rec_val + rec_val = +rec_val record_value_asserts( From f3b36cfca6aeb60a181e924303f11ac08a07c78e Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 08:33:18 +0000 Subject: [PATCH 047/131] Increase timeout to alleviate test failures MacOS CI test runners seem very slow and have intermittent timeout-related failures --- tests/test_records.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 90ba2d05..f1ebcee5 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -20,6 +20,12 @@ ) +# Test parameters +DEVICE_NAME = "SOFT-IOC-TESTS" +RECORD_NAME = "OUT-RECORD" +TIMEOUT = 5 # Seconds + + def _clear_records(): # Remove any records created at epicsdbbuilder layer ResetRecords() @@ -283,10 +289,6 @@ class GetValueEnum(Enum): CAGET = 2 -DEVICE_NAME = "SOFT-IOC-TESTS" -RECORD_NAME = "OUT-RECORD" -TIMEOUT = 3 - def run_ioc(creation_func, initial_value, queue, set_enum): """Creates a record and starts the IOC. `initial_value` will be set on the record at different times based on the `set_enum` parameter.""" From 0604d8859b320540538635c054b230c6f60547dd Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 09:11:33 +0000 Subject: [PATCH 048/131] Add Yield call to ensure IOC processes new value --- tests/test_records.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_records.py b/tests/test_records.py index f1ebcee5..f82ee7b3 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -357,6 +357,7 @@ def run_test_function( try: + from cothread import Yield from cothread.catools import caget, caput, _channel_cache # cothread remembers connected IOCs. As we restart the same named @@ -371,6 +372,11 @@ def run_test_function( initial_value, wait=True, **put_kwargs) + # Ensure IOC process has time to execute. + # I saw failures on MacOS where it appeared the IOC had not + # processed the put'ted value as the caget returned the same value + # as was originally passed in. + Yield(timeout=TIMEOUT) if get_enum == GetValueEnum.GET: rec_val = queue.get(timeout=TIMEOUT) @@ -379,7 +385,8 @@ def run_test_function( DEVICE_NAME + ":" + RECORD_NAME, timeout=TIMEOUT, **get_kwargs) - + # '+' operator used to convert cothread's types into Python native + # types e.g. "+ca_int" -> int rec_val = +rec_val From 1ca6334c476d94cabde84e5da32d29b0897fcbc6 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 09:44:16 +0000 Subject: [PATCH 049/131] Bump timeout further still for MacOS CI runners --- tests/test_records.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_records.py b/tests/test_records.py index f82ee7b3..c2a4a679 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -23,7 +23,11 @@ # Test parameters DEVICE_NAME = "SOFT-IOC-TESTS" RECORD_NAME = "OUT-RECORD" -TIMEOUT = 5 # Seconds +TIMEOUT = 10 # Seconds +# Value is so large to accomodate very slow GitHub MacOS CI runners. +# Most platforms only need a timeout of 3 to 5 seconds. +# Not a particular problem on faster platforms - this is never used as a sleep +# so tests will still run fast on platforms that can handle it. def _clear_records(): From 6b45466c513d865940c8ddb3cd071ed7e14c0198 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 09:59:50 +0000 Subject: [PATCH 050/131] Use up to date cibuildwheel This should have been done as part of the removal of Python2 support. --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 3d90a6c2..f2b9dd41 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -60,7 +60,7 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel==1.* # The 2.* releases dropped Python2 support. + run: pip install build cibuildwheel - name: Build Sdist if: matrix.sdist From c36a50def933a58119a925046a74bba35dbcb7ba Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 14:34:00 +0000 Subject: [PATCH 051/131] Decode CAPut'd strings in the same way .set() does --- softioc/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/softioc/fields.py b/softioc/fields.py index d44dfb69..ba3c37b9 100644 --- a/softioc/fields.py +++ b/softioc/fields.py @@ -98,7 +98,7 @@ def __getattr__(self, field): if field == 'TIME': return self.__get_time(address) elif field_type == DBF_STRING: - return string_at(cast(address, c_char_p)).decode() + return string_at(cast(address, c_char_p)).decode(errors="replace") elif field_type in [DBF_INLINK, DBF_OUTLINK]: return cast(address, POINTER(c_char_p))[0].decode() else: From 20cf010364259b52e468b8739aba639f7c0aa079 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 14:35:37 +0000 Subject: [PATCH 052/131] Add CAPut - get() test cases This required a large amount of checks in many places to cover edge cases. I think this test code is probably getting too complex and needs simplification. --- tests/test_records.py | 81 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index c2a4a679..6e7ecc80 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -321,12 +321,16 @@ def run_ioc(creation_func, initial_value, queue, set_enum): if queue is not None: queue.put(out_rec.get()) - else: - # Keep process alive while main thread works. - asyncio.run_coroutine_threadsafe( - asyncio.sleep(TIMEOUT), - dispatcher.loop - ).result() + # Some tests need to do a caput and then cause another .get() to happen + val = queue.get(timeout=TIMEOUT) + if val: + queue.put(out_rec.get()) + + # Keep process alive while main thread works. + asyncio.run_coroutine_threadsafe( + asyncio.sleep(TIMEOUT), + dispatcher.loop + ).result() def run_test_function( @@ -371,11 +375,16 @@ def run_test_function( _channel_cache.purge() if set_enum == SetValueEnum.CAPUT: + if queue: + queue.get(timeout=TIMEOUT) caput( DEVICE_NAME + ":" + RECORD_NAME, initial_value, wait=True, **put_kwargs) + + if queue: + queue.put("Do another get!") # Ensure IOC process has time to execute. # I saw failures on MacOS where it appeared the IOC had not # processed the put'ted value as the caget returned the same value @@ -400,6 +409,9 @@ def run_test_function( expected_value, expected_type) finally: + # Purge cache to suppress spurious "IOC disconnected" exceptions + _channel_cache.purge() + ioc_process.terminate() ioc_process.join(timeout=TIMEOUT) if ioc_process.exitcode is None: @@ -415,7 +427,17 @@ def record_value_asserts( if type(expected_value) == float and isnan(expected_value): assert isnan(actual_value) # NaN != Nan, so needs special case elif creation_func in [builder.WaveformOut, builder.WaveformIn]: - assert numpy.array_equal(actual_value, expected_value), \ + + # Special case for lack of default value on Out records before init + if actual_value is None and expected_value is None: + assert type(actual_value) == expected_type + return + + # Using .get() on the array returns entire length, not just filled part. + # Confirm this by ensuring sliced part of array is all zeros + assert not numpy.any(actual_value[expected_value.size:]) + truncated_value = actual_value[:expected_value.size] + assert numpy.array_equal(truncated_value, expected_value), \ "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type else: @@ -423,6 +445,13 @@ def record_value_asserts( assert type(actual_value) == expected_type +def skip_long_strings(record_values): + if ( + record_values[0] in [builder.stringIn, builder.stringOut] + and len(record_values[1]) > 40 + ): + pytest.skip("CAPut blocks strings > 40 characters.") + def test_records(tmp_path): import sim_records @@ -514,6 +543,27 @@ def test_value_post_init_set_after_init(self, record_values): SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET) + @requires_cothread + def test_value_post_init_caput(self, record_values): + """Test that records provide the expected values on get calls when using + caput and .get() after IOC initialisation""" + + if record_values[0] in [ + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn]: + pytest.skip("CAPut to In records doesn't propogate to .get()") + + skip_long_strings(record_values) + + run_test_function( + record_values, + SetValueEnum.CAPUT, + GetValueEnum.GET) + @requires_cothread class TestCagetValue: """Tests that use Caget to check whether values applied with .set() @@ -551,7 +601,7 @@ def test_value_post_init_initial_value(self, record_values): run_test_function( record_values, SetValueEnum.INITIAL_VALUE, - GetValueEnum.GET) + GetValueEnum.CAGET) @requires_cothread def test_value_post_init_set_after_init(self, record_values): @@ -561,17 +611,13 @@ def test_value_post_init_set_after_init(self, record_values): run_test_function( record_values, SetValueEnum.SET_AFTER_INIT, - GetValueEnum.GET) + GetValueEnum.CAGET) def test_value_post_init_caput(self, record_values): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and caget after initialisation""" - if ( - record_values[0] in [builder.stringIn, builder.stringOut] - and len(record_values[1]) > 40 - ): - pytest.skip("CAPut blocks strings > 40 characters.") + skip_long_strings(record_values) run_test_function( record_values, @@ -594,7 +640,7 @@ class TestDefaultValue: (builder.mbbOut, None, type(None)), (builder.mbbIn, 0, int), (builder.WaveformOut, None, type(None)), - (builder.WaveformIn, [], numpy.ndarray), + (builder.WaveformIn, numpy.empty(0), numpy.ndarray), ]) def test_value_default_pre_init( self, @@ -604,6 +650,7 @@ def test_value_default_pre_init( clear_records): """Test that the correct default values are returned from .get() (before record initialisation) when no initial_value or .set() is done""" + # Out records do not have default values until records are initialized kwarg = {} if creation_func in [builder.WaveformIn, builder.WaveformOut]: @@ -628,8 +675,8 @@ def test_value_default_pre_init( (builder.stringIn, "", str), (builder.mbbOut, 0, int), (builder.mbbIn, 0, int), - (builder.WaveformOut, [], numpy.ndarray), - (builder.WaveformIn, [], numpy.ndarray), + (builder.WaveformOut, numpy.empty(0), numpy.ndarray), + (builder.WaveformIn, numpy.empty(0), numpy.ndarray), ]) def test_value_default_post_init( self, From 0bcd86f9a651e11bf210ee21eea6e84e7d9bbeb8 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 15:16:05 +0000 Subject: [PATCH 053/131] Add CAGet tests to the Default tests --- tests/test_records.py | 79 +++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 6e7ecc80..6147fb28 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -174,9 +174,9 @@ def record_values(request): Fields are: - Record builder function - - Input value passed to .set() or used in initial_value on record creation - - Expected output value after doing .get() - - Expected type of the output value from .get()""" + - Input value passed to .set()/initial_value/caput + - Expected output value after doing .get()/caget + - Expected type of the output value from .get()/caget""" return request.param # One item from the params list def test_records(tmp_path): @@ -624,6 +624,9 @@ def test_value_post_init_caput(self, record_values): SetValueEnum.CAPUT, GetValueEnum.CAGET) +def default_values_names(fixture_value): + """Provide nice name for default_values fixture""" + return fixture_value[0].__name__ class TestDefaultValue: """Tests related to default values""" @@ -663,34 +666,50 @@ def test_value_default_pre_init( expected_value, expected_type) + + + @pytest.fixture(params= [ + (builder.aOut, None, 0.0, float), + (builder.aIn, None, 0.0, float), + (builder.longOut, None, 0, int), + (builder.longIn, None, 0, int), + (builder.boolOut, None, 0, int), + (builder.boolIn, None, 0, int), + (builder.stringOut, None, "", str), + (builder.stringIn, None, "", str), + (builder.mbbOut, None, 0, int), + (builder.mbbIn, None, 0, int), + (builder.WaveformOut, None, numpy.empty(0), numpy.ndarray), + (builder.WaveformIn, None, numpy.empty(0), numpy.ndarray), + ], ids=default_values_names) + def default_values(self, request): + """Fixture for default values for initialised records. + + Fields are: + - Record builder function + - (UNUSED) fake initial value, so parameters are same as other fixtures + - Input value passed to .set()/initial_value/caput + - Expected output value after doing .get()/caget + - Expected type of the output value from .get()/caget""" + return request.param + + @requires_cothread - @pytest.mark.parametrize("creation_func,expected_value,expected_type", [ - (builder.aOut, 0.0, float), - (builder.aIn, 0.0, float), - (builder.longOut, 0, int), - (builder.longIn, 0, int), - (builder.boolOut, 0, int), - (builder.boolIn, 0, int), - (builder.stringOut, "", str), - (builder.stringIn, "", str), - (builder.mbbOut, 0, int), - (builder.mbbIn, 0, int), - (builder.WaveformOut, numpy.empty(0), numpy.ndarray), - (builder.WaveformIn, numpy.empty(0), numpy.ndarray), - ]) - def test_value_default_post_init( - self, - creation_func, - expected_value, - expected_type): - """Test that records provide the expected default value after they are - initialised, having never had a value set""" + def test_value_default_post_init(self, default_values): + """Test that records return the expected default value from .get after + they are initialised, having never had a value set""" - # Add a dummy initial_value to the tuple, so we can use the same - # framework as the other tests - tmp = list((creation_func, expected_value, expected_type)) - tmp.insert(1, None) - run_test_function(tuple(tmp), SetValueEnum.NO_SET, GetValueEnum.GET) + run_test_function(default_values, SetValueEnum.NO_SET, GetValueEnum.GET) + + @requires_cothread + def test_value_default_post_init_caget(self, default_values): + """Test that records return the expected default value from CAGet after + they are initialised, having never had a value set""" + + run_test_function( + default_values, + SetValueEnum.NO_SET, + GetValueEnum.CAGET) class TestNoneValue: """Various tests regarding record value of None""" @@ -743,7 +762,7 @@ def none_value_test_func(self, record_func, queue): @requires_cothread def test_value_none_rejected_set_after_init(self, record_funcs_reject_none): - """Test that setting \"None\" using .set() after IOc init raises an + """Test that setting \"None\" using .set() after IOC init raises an exception""" queue = multiprocessing.Queue() process = multiprocessing.Process( From 2389c6a695f1b19abc4771f9121f6681a0501c02 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 15:22:23 +0000 Subject: [PATCH 054/131] Skip building musllinux It appears newer cibuildwheel versions try to build for more platforms. epicscorelibs doesn't support musllinux, so skip building there. --- .github/workflows/code.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index f2b9dd41..dcf420a1 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -77,6 +77,7 @@ jobs: CIBW_REPAIR_WHEEL_COMMAND: "" CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 + CIBW_SKIP: ${{ matrix.python }}*64-musllinux* - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From dd88095ee645f52e5baec91e2307027a406b106f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 15:43:35 +0000 Subject: [PATCH 055/131] Another attempt to stop musllinux builds --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index dcf420a1..809cdd4f 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -77,7 +77,7 @@ jobs: CIBW_REPAIR_WHEEL_COMMAND: "" CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 - CIBW_SKIP: ${{ matrix.python }}*64-musllinux* + CIBW_SKIP: *-musllinux* - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 2e9f37c4217954fe62fc712b6e6514266ed4441e Mon Sep 17 00:00:00 2001 From: AlexWells Date: Fri, 10 Dec 2021 15:49:10 +0000 Subject: [PATCH 056/131] Another attempt to stop musllinux builds --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 809cdd4f..6fa3e470 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -77,7 +77,7 @@ jobs: CIBW_REPAIR_WHEEL_COMMAND: "" CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 - CIBW_SKIP: *-musllinux* + CIBW_SKIP: "*-musllinux*" - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 95d6d833bc1c9243f2c07a1eb2388cba5bb8f5f7 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 11:19:50 +0000 Subject: [PATCH 057/131] Improve synchronisation for parent-child messages This new model will have the parent process wait until IocInit() has successfully completed before continuing with the test The child process had need to do a queue.put() followed immediately by a queue.get(). On most platforms the main process would interuupt by pulling from the queue, but this was not guaranteed. To solve this using queues would have required a second queue for signalling. We work around this by using Pipes instead. These provide the read-write semantics we want - the ability to wait for an explicit message from the parent process - without needing a side-channel for more signalling. Hopefully this will fix the MacOS issues where it seemed caget was returning values from a previous iteration of the tests, possibly because the test process was running much faster than the child process could keep up with. --- tests/test_records.py | 46 +++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 6147fb28..0531a643 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -293,7 +293,7 @@ class GetValueEnum(Enum): CAGET = 2 -def run_ioc(creation_func, initial_value, queue, set_enum): +def run_ioc(creation_func, initial_value, conn, set_enum, get_enum): """Creates a record and starts the IOC. `initial_value` will be set on the record at different times based on the `set_enum` parameter.""" kwarg = {} @@ -316,15 +316,21 @@ def run_ioc(creation_func, initial_value, queue, set_enum): builder.LoadDatabase() softioc.iocInit(dispatcher) + conn.send("IOC started") + if set_enum == SetValueEnum.SET_AFTER_INIT: out_rec.set(initial_value) - if queue is not None: - queue.put(out_rec.get()) + if get_enum == GetValueEnum.GET: + conn.send(out_rec.get()) # Some tests need to do a caput and then cause another .get() to happen - val = queue.get(timeout=TIMEOUT) - if val: - queue.put(out_rec.get()) + if set_enum == SetValueEnum.CAPUT: + if conn.poll(TIMEOUT): + val = conn.recv() + if val is not None: + conn.send(out_rec.get()) + + conn.close() # Keep process alive while main thread works. asyncio.run_coroutine_threadsafe( @@ -343,17 +349,21 @@ def run_test_function( creation_func, initial_value, expected_value, expected_type = record_values - queue = None - if get_enum == GetValueEnum.GET: - queue = multiprocessing.Queue() + parent_conn, child_conn = multiprocessing.Pipe() ioc_process = multiprocessing.Process( target=run_ioc, - args=(creation_func, initial_value, queue, set_enum) + args=(creation_func, initial_value, child_conn, set_enum, get_enum) ) ioc_process.start() + # Wait for message that IOC has started + if parent_conn.poll(TIMEOUT): + parent_conn.recv() + else: + pytest.fail("IOC process did not start before TIMEOUT expired") + # Infer some required keywords from parameters put_kwargs = {} get_kwargs = {} @@ -375,16 +385,19 @@ def run_test_function( _channel_cache.purge() if set_enum == SetValueEnum.CAPUT: - if queue: - queue.get(timeout=TIMEOUT) + if get_enum == GetValueEnum.GET: + if parent_conn.poll(TIMEOUT): + parent_conn.recv() + else: + pytest.fail("IOC did not provide initial record value") caput( DEVICE_NAME + ":" + RECORD_NAME, initial_value, wait=True, **put_kwargs) - if queue: - queue.put("Do another get!") + if get_enum == GetValueEnum.GET: + parent_conn.send("Do another get!") # Ensure IOC process has time to execute. # I saw failures on MacOS where it appeared the IOC had not # processed the put'ted value as the caget returned the same value @@ -392,7 +405,10 @@ def run_test_function( Yield(timeout=TIMEOUT) if get_enum == GetValueEnum.GET: - rec_val = queue.get(timeout=TIMEOUT) + if parent_conn.poll(TIMEOUT): + rec_val = parent_conn.recv() + else: + pytest.fail("IOC did not provide record value in queue") else: rec_val = caget( DEVICE_NAME + ":" + RECORD_NAME, From ceae66cae87f053773150ae47fd6d2fc2bc29044 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 12:03:18 +0000 Subject: [PATCH 058/131] Disable tests - investigating Windows build issues --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 6fa3e470..012267c0 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -71,7 +71,7 @@ jobs: env: CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev - CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} + # CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} # Disable auditwheel as it isn't compatible with setuptools_dso approach # https://github.com/mdavidsaver/setuptools_dso/issues/17 CIBW_REPAIR_WHEEL_COMMAND: "" From 39e2e1808821f8f8ade4530d20ed98f83d0adace Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 12:18:37 +0000 Subject: [PATCH 059/131] Revert cibuildwheel version change Need to investigate issues with Windows no longer running tests properly on cibuildwheel 2+ - the tests fail to find the softioc/iocStatsDb dir as it is no longer packaged on Windows This reverts commit db85ca3ed9e2c3d3772c925a03486add14cbffb8. --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 012267c0..691b2896 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -60,7 +60,7 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel + run: pip install build cibuildwheel==1.* # The 2.* releases dropped Python2 support. - name: Build Sdist if: matrix.sdist From 0998b87a561d18d28f28c2235bc5a8ac7cb234fd Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 13:24:28 +0000 Subject: [PATCH 060/131] Revert "Revert cibuildwheel version change" This reverts commit bf47f63c8a6ca00e9196a2c8c3ca2b62c529c4d7. --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 691b2896..012267c0 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -60,7 +60,7 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel==1.* # The 2.* releases dropped Python2 support. + run: pip install build cibuildwheel - name: Build Sdist if: matrix.sdist From 26e0dd0099e3e822d8a6f53e8533d89368d227f9 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 14:01:15 +0000 Subject: [PATCH 061/131] Use cibuildwheel action This is an attempt to get Windows softlinks built into the wheel --- .github/workflows/code.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 012267c0..c559d55c 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -59,15 +59,12 @@ jobs: with: python-version: "3.7" - - name: Install Python Dependencies - run: pip install build cibuildwheel - - name: Build Sdist if: matrix.sdist run: python -m build --sdist . - name: Build Wheel - run: cibuildwheel --output-dir dist + uses: pypa/cibuildwheel@v2.3.0 env: CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev From 2036b6176dc2e4cbdb19e7786b4f92a9e6d0c0d1 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 14:12:03 +0000 Subject: [PATCH 062/131] Install "build" package Should not have removed it in the previous commit! --- .github/workflows/code.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index c559d55c..d5d623cb 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -59,6 +59,9 @@ jobs: with: python-version: "3.7" + - name: Install Python Dependencies + run: pip install build + - name: Build Sdist if: matrix.sdist run: python -m build --sdist . From 1ad31a28a910efa707674a612394973cfcfa27af Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 14:21:43 +0000 Subject: [PATCH 063/131] Fix path for archive upload --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index d5d623cb..310d66ea 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -83,7 +83,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: dist - path: dist/softioc* + path: ./wheelhouse/*.whl - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 From c6e925721639dbec9c7656d08547e9290daaf7ef Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 14:39:13 +0000 Subject: [PATCH 064/131] Revert changes to this file as action didn't work --- .github/workflows/code.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 310d66ea..012267c0 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -60,14 +60,14 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build + run: pip install build cibuildwheel - name: Build Sdist if: matrix.sdist run: python -m build --sdist . - name: Build Wheel - uses: pypa/cibuildwheel@v2.3.0 + run: cibuildwheel --output-dir dist env: CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev @@ -83,7 +83,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: dist - path: ./wheelhouse/*.whl + path: dist/softioc* - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 From 9330dd48362384972564bf35de8db114035dbfba Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 14:46:50 +0000 Subject: [PATCH 065/131] Explain contents of CIBW_SKIP env var --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 012267c0..e8eac61e 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -77,7 +77,7 @@ jobs: CIBW_REPAIR_WHEEL_COMMAND: "" CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 - CIBW_SKIP: "*-musllinux*" + CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 112a2bf14dd3cfc6d710fc3f842201da18cf485f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 14:53:24 +0000 Subject: [PATCH 066/131] Add directory contents printing to cibuildwheel --- .github/workflows/code.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index e8eac61e..afb5adf6 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -78,6 +78,9 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms + # Some debugging variables to work out why subfolder not on Windows + CIBW_BEFORE_ALL_WINDOWS: tree /F + CIBW_BEFORE_ALL_LINUX: tree - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 5d9ece76ea4ee71479d2148ff4031c75e0876783 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 14:58:43 +0000 Subject: [PATCH 067/131] Try storing output in dist folder --- .github/workflows/code.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index afb5adf6..17d950e9 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -79,8 +79,8 @@ jobs: CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms # Some debugging variables to work out why subfolder not on Windows - CIBW_BEFORE_ALL_WINDOWS: tree /F - CIBW_BEFORE_ALL_LINUX: tree + CIBW_BEFORE_ALL_WINDOWS: tree /F > dist/tree.txt + CIBW_BEFORE_ALL_LINUX: ls pythonSoftIoc/softioc > dist/tree.txt - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 741f70e31bfb86253978278915e7bc7e92374d39 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 15:01:33 +0000 Subject: [PATCH 068/131] Change ls command on linux --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 17d950e9..16f2bf21 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -80,7 +80,7 @@ jobs: CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms # Some debugging variables to work out why subfolder not on Windows CIBW_BEFORE_ALL_WINDOWS: tree /F > dist/tree.txt - CIBW_BEFORE_ALL_LINUX: ls pythonSoftIoc/softioc > dist/tree.txt + CIBW_BEFORE_ALL_LINUX: ls > dist/tree.txt - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From c1d3f8cef7384b759f8f4a21e2d9fe3b66b4cf42 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 15:10:39 +0000 Subject: [PATCH 069/131] Increase verbosity and tweak BEFORE action --- .github/workflows/code.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 16f2bf21..524d99a9 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -79,8 +79,9 @@ jobs: CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms # Some debugging variables to work out why subfolder not on Windows - CIBW_BEFORE_ALL_WINDOWS: tree /F > dist/tree.txt - CIBW_BEFORE_ALL_LINUX: ls > dist/tree.txt + CIBW_BEFORE_ALL_WINDOWS: tree /F + CIBW_BEFORE_ALL_LINUX: ls + CIBW_BUILD_VERBOSITY: 3 - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 946aa31f9f43134a050e81763f94152a189ab537 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 15:40:15 +0000 Subject: [PATCH 070/131] Print tree output early --- .github/workflows/code.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 524d99a9..bf410676 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -54,6 +54,9 @@ jobs: fetch-depth: 0 submodules: true + - name: Print Tree + run: tree /F + - name: Install Python uses: actions/setup-python@v2 with: From 66c1348d7bb38183a30ce6a9ec6f94021c08f357 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 13 Dec 2021 15:49:18 +0000 Subject: [PATCH 071/131] Back to cibuildwheel V1 to examine tree output --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index bf410676..27ab9440 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -63,7 +63,7 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel + run: pip install build cibuildwheel==1.* # The 2.* releases dropped Python2 support. - name: Build Sdist if: matrix.sdist From c81471ec121a3b4f01adce87ae5278f64616a828 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 08:36:06 +0000 Subject: [PATCH 072/131] Tweak debugging instructions for windows --- .github/workflows/code.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 27ab9440..77a2770f 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -54,8 +54,8 @@ jobs: fetch-depth: 0 submodules: true - - name: Print Tree - run: tree /F + - name: Print softioc dir + run: dir softioc - name: Install Python uses: actions/setup-python@v2 @@ -63,7 +63,7 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel==1.* # The 2.* releases dropped Python2 support. + run: pip install build cibuildwheel - name: Build Sdist if: matrix.sdist @@ -82,9 +82,7 @@ jobs: CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms # Some debugging variables to work out why subfolder not on Windows - CIBW_BEFORE_ALL_WINDOWS: tree /F - CIBW_BEFORE_ALL_LINUX: ls - CIBW_BUILD_VERBOSITY: 3 + CIBW_BEFORE_ALL_WINDOWS: dir softioc - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 494e93d97de0ed9f0e92451bbe44bb3cc8132f1f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 08:38:56 +0000 Subject: [PATCH 073/131] Roll back cibuildwheel version to 2.2.2 --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 77a2770f..97751ef7 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -63,7 +63,7 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel + run: pip install build cibuildwheel==2.2.2 - name: Build Sdist if: matrix.sdist From 03a626b29e7d1e64f41f5c7b309450db1dd1f255 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 08:55:09 +0000 Subject: [PATCH 074/131] Remove logging and reinstate running tests The fix is to pin cibuildwheel at 2.2.2, but I'm doing this for the failing build log so I can raise an issue against cibuildwheel --- .github/workflows/code.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 97751ef7..5631857e 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -54,16 +54,13 @@ jobs: fetch-depth: 0 submodules: true - - name: Print softioc dir - run: dir softioc - - name: Install Python uses: actions/setup-python@v2 with: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel==2.2.2 + run: pip install build cibuildwheel - name: Build Sdist if: matrix.sdist @@ -74,15 +71,13 @@ jobs: env: CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev - # CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} + CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} # Disable auditwheel as it isn't compatible with setuptools_dso approach # https://github.com/mdavidsaver/setuptools_dso/issues/17 CIBW_REPAIR_WHEEL_COMMAND: "" CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 CIBW_SKIP: "*-musllinux*" # epicscorelibs doesn't build on musllinux platforms - # Some debugging variables to work out why subfolder not on Windows - CIBW_BEFORE_ALL_WINDOWS: dir softioc - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From 1a2f0a3eaa4ef2ba46ffc8c0a9b734a5d8623c34 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 09:06:02 +0000 Subject: [PATCH 075/131] Pin cibuildwheel to 2.2.2 This appears to resolve issue regarding Windows symlinks not copying. See https://github.com/pypa/cibuildwheel/issues/962 --- .github/workflows/code.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 5631857e..36add2c7 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -60,7 +60,8 @@ jobs: python-version: "3.7" - name: Install Python Dependencies - run: pip install build cibuildwheel + # Pin cibuildwheel due to https://github.com/pypa/cibuildwheel/issues/962 + run: pip install build cibuildwheel==2.2.2 - name: Build Sdist if: matrix.sdist From 954668b8435b1e9810c1fe5a5d7212efb0125e22 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 09:32:16 +0000 Subject: [PATCH 076/131] Change to only run CI once on pull requests Previously it would run twice, for no good reason. This also adds running a weekly build. This may or may not be necessary, but it's part of the new template module so we'll add it for consistency --- .github/workflows/code.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 36add2c7..1162e2e6 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -2,7 +2,15 @@ name: Code CI on: push: + branches: + - master + - main + tags: + - "*" pull_request: + schedule: + # Run every Monday at 8am + - cron: '0 8 * * MON' jobs: lint: From 9618484534944f443bc77780679c376c2fe9fa31 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 09:55:57 +0000 Subject: [PATCH 077/131] Add Unit Test Results action to CI Pytest will now output JUnit-format XML results, which the action should read and display on the PR in a nice format. --- .github/workflows/code.yml | 24 ++++++++++++++++++++++++ .gitignore | 1 + setup.cfg | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 1162e2e6..4a86427f 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -100,6 +100,30 @@ jobs: name: ${{ matrix.os }}/${{ matrix.python }} directory: dist + - name: Upload Unit Test Results + if: always() + uses: actions/upload-artifact@v2 + with: + name: Unit Test Results (${{ matrix.os }}/${{ matrix.python }}) + path: pytest-results.xml + + publish-test-results: + name: "Publish Unit Tests Results" + needs: build + runs-on: ubuntu-latest + if: always() + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v2 + with: + path: artifacts + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + with: + files: artifacts/**/*.xml + release: needs: [build] runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index a39f5419..bc78a48b 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ coverage.xml *.mypy_cache *.pytest_cache cov.xml +pytest-results.xml # DLS build dir and virtual environment /prefix/ diff --git a/setup.cfg b/setup.cfg index 26faed7f..75c219ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,7 @@ extend-ignore = # Don't do flake8 here as we need to separate it out for CI addopts = --tb=native -vv --doctest-modules --ignore=iocStats --ignore=epicscorelibs --ignore=docs - --cov=softioc --cov-report term --cov-report xml:cov.xml + --cov=softioc --cov-report term --cov-report xml:cov.xml --junit-xml=pytest-results.xml [coverage:run] # This is covered in the versiongit test suite so exclude it here From 5881882c6db5d42394b4bcaef39c4c6450ceac7f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 10:09:33 +0000 Subject: [PATCH 078/131] Use different approach to saving JUnit results --- .github/workflows/code.yml | 12 ++++++++---- .gitignore | 1 - setup.cfg | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 4a86427f..f95fd9ec 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -38,15 +38,18 @@ jobs: python: [cp36, cp37, cp38, cp39] include: - # Put coverage in the project directory for mac + # Put coverage and results files in the project directory for mac - os: macos-latest cov_file: "{project}/dist/coverage.xml" + results_file: "{project}/dist/pytest-results.xml" # And for windows - os: windows-latest cov_file: "{project}/dist/coverage.xml" - # But put coverage in the output dir mounted in docker for linux + results_file: "{project}/dist/pytest-results.xml" + # But put coverage and results files in the output dir mounted in docker for linux - os: ubuntu-latest cov_file: /output/coverage.xml + results_file: /output/pytest-results.xml # Build an sdist on linux so it has the right line endings - os: ubuntu-latest sdist: true @@ -80,7 +83,7 @@ jobs: env: CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev - CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} + CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{matrix.results_file}} # Disable auditwheel as it isn't compatible with setuptools_dso approach # https://github.com/mdavidsaver/setuptools_dso/issues/17 CIBW_REPAIR_WHEEL_COMMAND: "" @@ -105,7 +108,8 @@ jobs: uses: actions/upload-artifact@v2 with: name: Unit Test Results (${{ matrix.os }}/${{ matrix.python }}) - path: pytest-results.xml + path: ${{matrix.results_file}} + directory: dist publish-test-results: name: "Publish Unit Tests Results" diff --git a/.gitignore b/.gitignore index bc78a48b..a39f5419 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,6 @@ coverage.xml *.mypy_cache *.pytest_cache cov.xml -pytest-results.xml # DLS build dir and virtual environment /prefix/ diff --git a/setup.cfg b/setup.cfg index 75c219ab..26faed7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,7 @@ extend-ignore = # Don't do flake8 here as we need to separate it out for CI addopts = --tb=native -vv --doctest-modules --ignore=iocStats --ignore=epicscorelibs --ignore=docs - --cov=softioc --cov-report term --cov-report xml:cov.xml --junit-xml=pytest-results.xml + --cov=softioc --cov-report term --cov-report xml:cov.xml [coverage:run] # This is covered in the versiongit test suite so exclude it here From fa88521b9c9e88fc45eef97e09e6a2ee23deadf4 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 10:10:52 +0000 Subject: [PATCH 079/131] Stop docs CI from running multiple times --- .github/workflows/docs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 10e35589..e3866cf4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,6 +2,12 @@ name: Docs CI on: push: + branches: + # Add more branches here to publish docs from other branches + - master + - main + tags: + - "*" pull_request: jobs: From 9a66d8ded5339d86557cd0dcd7e865607ef8297b Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 10:20:41 +0000 Subject: [PATCH 080/131] Tweak results XML upload location --- .github/workflows/code.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index f95fd9ec..5231fc45 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -83,7 +83,7 @@ jobs: env: CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev - CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{matrix.results_file}} + CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{ matrix.results_file }} # Disable auditwheel as it isn't compatible with setuptools_dso approach # https://github.com/mdavidsaver/setuptools_dso/issues/17 CIBW_REPAIR_WHEEL_COMMAND: "" @@ -108,8 +108,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: Unit Test Results (${{ matrix.os }}/${{ matrix.python }}) - path: ${{matrix.results_file}} - directory: dist + path: dist/pytest-results.xml publish-test-results: name: "Publish Unit Tests Results" From 6d56f3a366be0e1c2a479f41c81fc458dacb0e80 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 14 Dec 2021 10:26:14 +0000 Subject: [PATCH 081/131] Fix invalid artifact name --- .github/workflows/code.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 5231fc45..d32244f6 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -107,7 +107,7 @@ jobs: if: always() uses: actions/upload-artifact@v2 with: - name: Unit Test Results (${{ matrix.os }}/${{ matrix.python }}) + name: Unit Test Results (${{ matrix.os }}-${{ matrix.python }}) path: dist/pytest-results.xml publish-test-results: From df3d72405c551718a60a446fe9c453f6e87862ef Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 08:51:17 +0000 Subject: [PATCH 082/131] Bump cibuildwheel version and add pip option cibuildwheel 2.3 upgraded the version of pip it uses, but that version altered how symlinks were handled on Windows. Specifying this environment variable should restore the behaviour for now. It'll be deprecated eventually though so we will need to do something else long term. --- .github/workflows/code.yml | 4 ++-- setup.cfg | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index d32244f6..a0a36f3b 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -72,7 +72,7 @@ jobs: - name: Install Python Dependencies # Pin cibuildwheel due to https://github.com/pypa/cibuildwheel/issues/962 - run: pip install build cibuildwheel==2.2.2 + run: pip install build cibuildwheel>=2.3.1 - name: Build Sdist if: matrix.sdist @@ -111,7 +111,7 @@ jobs: path: dist/pytest-results.xml publish-test-results: - name: "Publish Unit Tests Results" + name: Publish Unit Tests Results needs: build runs-on: ubuntu-latest if: always() diff --git a/setup.cfg b/setup.cfg index 26faed7f..e6db2427 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,3 +64,12 @@ addopts = [coverage:run] # This is covered in the versiongit test suite so exclude it here omit = */_version_git.py + +# cibuildwheel 2.3.0 upgraded to pip v21.3, which no longer completely copies the +# source directory before building, so the symlink'd "iocStatsDb" folder isn't included +# in built wheels. This is a temporary fix for that, until pip drops support for this option. +# The long term fix is to revamp how we include the iocStatsDb folder. +# https://github.com/pypa/cibuildwheel/issues/962 +# https://github.com/pypa/pip/issues/7555 +[tool.cibuildwheel.environment] +PIP_USE_DEPRECATED = "out-of-tree-build" From 2956acba20b60551056a58087c0e1291f9da691c Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 09:43:39 +0000 Subject: [PATCH 083/131] Fix merge issues --- tests/test_records.py | 54 ++++++++++--------------------------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 0531a643..27e978cb 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -224,6 +224,7 @@ def test_DISP_can_be_overridden(): record = builder.longIn("DISP-OFF", DISP=0) # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 0 + def record_value_asserts( creation_func, actual_value, @@ -234,7 +235,17 @@ def record_value_asserts( if type(expected_value) == float and isnan(expected_value): assert isnan(actual_value) # NaN != Nan, so needs special case elif creation_func in [builder.WaveformOut, builder.WaveformIn]: - assert numpy.array_equal(actual_value, expected_value), \ + + # Special case for lack of default value on Out records before init + if actual_value is None and expected_value is None: + assert type(actual_value) == expected_type + return + + # Using .get() on the array returns entire length, not just filled part. + # Confirm this by ensuring sliced part of array is all zeros + assert not numpy.any(actual_value[expected_value.size:]) + truncated_value = actual_value[:expected_value.size] + assert numpy.array_equal(truncated_value, expected_value), \ "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type else: @@ -433,33 +444,6 @@ def run_test_function( if ioc_process.exitcode is None: pytest.fail("Process did not terminate") -def record_value_asserts( - creation_func, - actual_value, - expected_value, - expected_type): - """Asserts that the expected value and expected type are matched with - the actual value. Handles both scalar and waveform data""" - if type(expected_value) == float and isnan(expected_value): - assert isnan(actual_value) # NaN != Nan, so needs special case - elif creation_func in [builder.WaveformOut, builder.WaveformIn]: - - # Special case for lack of default value on Out records before init - if actual_value is None and expected_value is None: - assert type(actual_value) == expected_type - return - - # Using .get() on the array returns entire length, not just filled part. - # Confirm this by ensuring sliced part of array is all zeros - assert not numpy.any(actual_value[expected_value.size:]) - truncated_value = actual_value[:expected_value.size] - assert numpy.array_equal(truncated_value, expected_value), \ - "Arrays not equal: {} {}".format(actual_value, expected_value) - assert type(actual_value) == expected_type - else: - assert actual_value == expected_value - assert type(actual_value) == expected_type - def skip_long_strings(record_values): if ( @@ -468,20 +452,6 @@ def skip_long_strings(record_values): ): pytest.skip("CAPut blocks strings > 40 characters.") -def test_records(tmp_path): - import sim_records - - path = str(tmp_path / "records.db") - builder.WriteRecords(path) - expected = os.path.join(os.path.dirname(__file__), "expected_records.db") - assert open(path).readlines()[5:] == open(expected).readlines() - -def test_enum_length_restriction(): - with pytest.raises(AssertionError): - builder.mbbIn( - "ManyLabels", "one", "two", "three", "four", "five", "six", - "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", - "fourteen", "fifteen", "sixteen", "seventeen") class TestGetValue: """Tests that use .get() to check whether values applied with .set() From dfc8ea939c25c641c8bc2a85bcc9db570c7f3893 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 10:35:58 +0000 Subject: [PATCH 084/131] Refactor tests to massively reduce subprocesses The new model of tests create a lot of records in each subprocess, rather than one subprocess per record. This hugely decreases runtime - down to 0:30 from 4:30 runtime on my machine. Unfortunately this mechanism does reduce resolution of results, as they'll fail on the first issue rather than finding all issues. --- tests/test_records.py | 602 ++++++++++++++++++++++++------------------ 1 file changed, 342 insertions(+), 260 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 27e978cb..5cb857e4 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,9 +1,11 @@ import multiprocessing +from typing import List import numpy import os import pytest import sys import asyncio +import logging from enum import Enum from math import isnan, inf, nan @@ -14,6 +16,8 @@ from epicsdbbuilder import ResetRecords +from softioc.pythonSoftIoc import RecordWrapper + requires_cothread = pytest.mark.skipif( sys.platform.startswith("win"), reason="Cothread doesn't work on windows" @@ -22,8 +26,7 @@ # Test parameters DEVICE_NAME = "SOFT-IOC-TESTS" -RECORD_NAME = "OUT-RECORD" -TIMEOUT = 10 # Seconds +TIMEOUT = 5 # Seconds # Value is so large to accomodate very slow GitHub MacOS CI runners. # Most platforms only need a timeout of 3 to 5 seconds. # Not a particular problem on faster platforms - this is never used as a sleep @@ -79,81 +82,112 @@ def record_funcs_reject_none(record_funcs): def record_values_names(fixture_value): """Provide a nice name for the tests in the record_values fixture""" - return fixture_value[0].__name__ + "-" + type(fixture_value[1]).__name__ \ - + "-" + str(fixture_value[1]) - -@pytest.fixture(params=[ - (builder.aOut, 5.5, 5.5, float), - (builder.aIn, 5.5, 5.5, float), - (builder.aOut, 3, 3., float), - (builder.aIn, 3, 3., float), - (builder.aOut, inf, inf, float), - (builder.aIn, inf, inf, float), - (builder.aOut, -inf, -inf, float), - (builder.aIn, -inf, -inf, float), - (builder.aOut, nan, nan, float), - (builder.aIn, nan, nan, float), - (builder.longOut, 5, 5, int), - (builder.longIn, 5, 5, int), - (builder.longOut, 9.9, 9, int), - (builder.longIn, 9.9, 9, int), - (builder.boolOut, 1, 1, int), - (builder.boolIn, 1, 1, int), - (builder.boolOut, True, 1, int), - (builder.boolIn, True, 1, int), - (builder.boolOut, False, 0, int), - (builder.boolIn, False, 0, int), - (builder.stringOut, "abc", "abc", str), - (builder.stringIn, "abc", "abc", str), - (builder.stringOut, "", "", str), - (builder.stringIn, "", "", str), - (builder.stringOut, b"abc", "abc", str), - (builder.stringIn, b"abc", "abc", str), - (builder.stringOut, b"a\xcfb", "a�b", str), # Invalid UTF-8 - (builder.stringIn, b"a\xcfb", "a�b", str), # Invalid UTF-8 - (builder.stringOut, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 - (builder.stringIn, b"a\xe2\x82\xacb", "a€b", str), # Valid UTF-8 - (builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 - (builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 + return fixture_value[1].__name__ + "-" + type(fixture_value[2]).__name__ \ + + "-" + str(fixture_value[3]) + +record_values_list = [ + ("aIn_float", builder.aIn, 5.5, 5.5, float), + ("aOut_float", builder.aOut, 5.5, 5.5, float), + ("aIn_int", builder.aIn, 3, 3., float), + ("aOut_int", builder.aOut, 3, 3., float), + ("aIn_inf", builder.aIn, inf, inf, float), + ("aOut_inf", builder.aOut, inf, inf, float), + ("aIn_neginf", builder.aIn, -inf, -inf, float), + ("aOut_neginf", builder.aOut, -inf, -inf, float), + ("aIn_nan", builder.aIn, nan, nan, float), + ("aOut_nan", builder.aOut, nan, nan, float), + ("longIn_int", builder.longIn, 5, 5, int), + ("longOut_int", builder.longOut, 5, 5, int), + ("longIn_float", builder.longIn, 9.9, 9, int), + ("longOut_float", builder.longOut, 9.9, 9, int), + ("boolIn_int", builder.boolIn, 1, 1, int), + ("boolOut_int", builder.boolOut, 1, 1, int), + ("boolIn_true", builder.boolIn, True, 1, int), + ("boolOut_true", builder.boolOut, True, 1, int), + ("boolIn_false", builder.boolIn, False, 0, int), + ("boolOut_false", builder.boolOut, False, 0, int), + ("mbbIn_int", builder.mbbIn, 1, 1, int), + ("mbbOut_int", builder.mbbOut, 1, 1, int), + ("strIn_abc", builder.stringIn, "abc", "abc", str), + ("strOut_abc", builder.stringOut, "abc", "abc", str), + ("strIn_empty", builder.stringIn, "", "", str), + ("strOut_empty", builder.stringOut, "", "", str), + ("strIn_bytes", builder.stringIn, b"abc", "abc", str), + ("strOut_bytes", builder.stringOut, b"abc", "abc", str), + ("strin_utf8", builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 + ("strOut_utf8", builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 + ( + "strIn_badutf8", + builder.stringIn, + b"a\xcfb", # Invalid UTF-8 + "a�b", + str + ), + ( + "strOut_badutf8", + builder.stringOut, + b"a\xcfb", # Invalid UTF-8 + "a�b", + str + ), + ( + "strIn_byteutf8", + builder.stringIn, + b"a\xe2\x82\xacb", # Valid UTF-8 + "a€b", + str + ), ( + "strOut_byteutf8", builder.stringOut, + b"a\xe2\x82\xacb", # Valid UTF-8 + "a€b", + str + ), + ( + "strIn_longstr", + builder.stringIn, "this string is much longer than 40 characters", "this string is much longer than 40 char", str ), ( - builder.stringIn, + "strOut_longstr", + builder.stringOut, "this string is much longer than 40 characters", "this string is much longer than 40 char", str ), - (builder.mbbIn, 1, 1, int), - (builder.mbbOut, 1, 1, int), ( + "wIn_list", builder.WaveformIn, [1, 2, 3], numpy.array([1, 2, 3], dtype=numpy.float32), numpy.ndarray ), ( + "wOut_list", builder.WaveformOut, [1, 2, 3], numpy.array([1, 2, 3], dtype=numpy.float32), numpy.ndarray ), ( + "wIn_str", builder.WaveformIn, "ABC", numpy.array([65, 66, 67, 0], dtype=numpy.uint8), numpy.ndarray ), ( + "wOut_str", builder.WaveformOut, "ABC", numpy.array([65, 66, 67, 0], dtype=numpy.uint8), numpy.ndarray ), ( + "wIn_bytes", builder.WaveformIn, b"HELLO\0WORLD", numpy.array([72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], @@ -161,14 +195,20 @@ def record_values_names(fixture_value): numpy.ndarray ), ( + "wOut_bytes", builder.WaveformOut, b"HELLO\0WORLD", numpy.array([72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], dtype=numpy.uint8), numpy.ndarray ) - ], - ids=record_values_names) + ] + + +@pytest.fixture( + params=record_values_list, + ids=record_values_names + ) def record_values(request): """A list of parameters for record value setting/getting tests. @@ -180,6 +220,7 @@ def record_values(request): return request.param # One item from the params list def test_records(tmp_path): + import sim_records path = str(tmp_path / "records.db") builder.WriteRecords(path) expected = os.path.join(os.path.dirname(__file__), "expected_records.db") @@ -225,6 +266,7 @@ def test_DISP_can_be_overridden(): # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 0 + def record_value_asserts( creation_func, actual_value, @@ -232,63 +274,33 @@ def record_value_asserts( expected_type): """Asserts that the expected value and expected type are matched with the actual value. Handles both scalar and waveform data""" - if type(expected_value) == float and isnan(expected_value): - assert isnan(actual_value) # NaN != Nan, so needs special case - elif creation_func in [builder.WaveformOut, builder.WaveformIn]: - - # Special case for lack of default value on Out records before init - if actual_value is None and expected_value is None: + try: + if type(expected_value) == float and isnan(expected_value): + assert isnan(actual_value) # NaN != Nan, so needs special case + elif creation_func in [builder.WaveformOut, builder.WaveformIn]: + + # Special case for lack of default value on Out records before init + if actual_value is None and expected_value is None: + assert type(actual_value) == expected_type + return + + # Using .get() on the array returns entire length, not just filled + # part. Confirm this by ensuring sliced part of array is all zeros + assert not numpy.any(actual_value[expected_value.size:]) + truncated_value = actual_value[:expected_value.size] + assert numpy.array_equal(truncated_value, expected_value), \ + "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type - return - - # Using .get() on the array returns entire length, not just filled part. - # Confirm this by ensuring sliced part of array is all zeros - assert not numpy.any(actual_value[expected_value.size:]) - truncated_value = actual_value[:expected_value.size] - assert numpy.array_equal(truncated_value, expected_value), \ - "Arrays not equal: {} {}".format(actual_value, expected_value) - assert type(actual_value) == expected_type - else: - assert actual_value == expected_value - assert type(actual_value) == expected_type - -def test_value_pre_init_set( - clear_records, - record_values): - """Test that records provide the expected values on get calls when using - .set() and .get() before IOC initialisation occurs""" - - creation_func, initial_value, expected_value, expected_type = record_values - - kwarg = {} - if creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - - out_rec = creation_func("out-record", **kwarg) - out_rec.set(initial_value) - - record_value_asserts( - creation_func, - out_rec.get(), - expected_value, - expected_type) - - -def test_value_pre_init_initial_value( - clear_records, - record_values): - """Test that records provide the expected values on get calls when using - initial_value and .get() before IOC initialisation occurs""" - - creation_func, initial_value, expected_value, expected_type = record_values - - out_rec = creation_func("out-record", initial_value=initial_value) - - record_value_asserts( - creation_func, - out_rec.get(), - expected_value, - expected_type) + else: + assert actual_value == expected_value + assert type(actual_value) == expected_type + except Exception as e: + msg = ( + "Failed with parameters: " + str(creation_func) + ", " + + str(actual_value) + ", " + str(expected_value) + ", " + + str(expected_type)) + logging.error(msg, exc_info=e) + raise e class SetValueEnum(Enum): """Enum to control when and how the record's value should be set""" @@ -304,46 +316,73 @@ class GetValueEnum(Enum): CAGET = 2 -def run_ioc(creation_func, initial_value, conn, set_enum, get_enum): +def run_ioc(record_configurations: list, conn, set_enum, get_enum): """Creates a record and starts the IOC. `initial_value` will be set on the record at different times based on the `set_enum` parameter.""" - kwarg = {} - if set_enum == SetValueEnum.INITIAL_VALUE: - kwarg.update({"initial_value": initial_value}) - elif creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - # Related to this issue: - # https://github.com/dls-controls/pythonSoftIOC/issues/37 + builder.SetDeviceName(DEVICE_NAME) + records: List[RecordWrapper] = [] - builder.SetDeviceName(DEVICE_NAME) - out_rec = creation_func(RECORD_NAME, **kwarg) + # Loop over given list and create the records + for configuration in record_configurations: + kwarg = {} - if set_enum == SetValueEnum.SET_BEFORE_INIT: - out_rec.set(initial_value) + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type + ) = configuration + + if set_enum == SetValueEnum.INITIAL_VALUE: + kwarg.update({"initial_value": initial_value}) + elif creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + # Related to this issue: + # https://github.com/dls-controls/pythonSoftIOC/issues/37 + + + out_rec = creation_func(record_name, **kwarg) + + if set_enum == SetValueEnum.SET_BEFORE_INIT: + out_rec.set(initial_value) + + records.append(out_rec) dispatcher = asyncio_dispatcher.AsyncioDispatcher() builder.LoadDatabase() softioc.iocInit(dispatcher) - conn.send("IOC started") - if set_enum == SetValueEnum.SET_AFTER_INIT: - out_rec.set(initial_value) + # Record list and record config list should always be in line + for record, configuration in zip(records, record_configurations): + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type + ) = configuration - if get_enum == GetValueEnum.GET: - conn.send(out_rec.get()) - # Some tests need to do a caput and then cause another .get() to happen - if set_enum == SetValueEnum.CAPUT: - if conn.poll(TIMEOUT): - val = conn.recv() - if val is not None: - conn.send(out_rec.get()) + if set_enum == SetValueEnum.SET_AFTER_INIT: + record.set(initial_value) + + for record in records: + if get_enum == GetValueEnum.GET: + conn.send(record.get()) + # Some tests do a caput and require another .get() + if set_enum == SetValueEnum.CAPUT: + if conn.poll(TIMEOUT): + val = conn.recv() + if val is not None: + conn.send(record.get()) conn.close() # Keep process alive while main thread works. + # This is most applicable to CAGET tests. asyncio.run_coroutine_threadsafe( asyncio.sleep(TIMEOUT), dispatcher.loop @@ -351,20 +390,18 @@ def run_ioc(creation_func, initial_value, conn, set_enum, get_enum): def run_test_function( - record_values, + record_configurations: list, set_enum: SetValueEnum, get_enum: GetValueEnum): """Run the test function using multiprocessing and check returned value is expected value. set_enum and get_enum determine when the record's value is set and how the value is retrieved, respectively.""" - creation_func, initial_value, expected_value, expected_type = record_values - parent_conn, child_conn = multiprocessing.Pipe() ioc_process = multiprocessing.Process( target=run_ioc, - args=(creation_func, initial_value, child_conn, set_enum, get_enum) + args=(record_configurations, child_conn, set_enum, get_enum) ) ioc_process.start() @@ -375,66 +412,77 @@ def run_test_function( else: pytest.fail("IOC process did not start before TIMEOUT expired") - # Infer some required keywords from parameters - put_kwargs = {} - get_kwargs = {} - if creation_func in [builder.WaveformOut, builder.WaveformIn]: - from cothread.dbr import DBR_CHAR_STR - if type(initial_value) in [str, bytes]: - put_kwargs.update({"datatype": DBR_CHAR_STR}) - get_kwargs.update({"count": len(initial_value) + 1}) + try: from cothread import Yield from cothread.catools import caget, caput, _channel_cache - # cothread remembers connected IOCs. As we restart the same named - # IOC multiple times, we have to purge the cache else the + # cothread remembers connected IOCs. As we potentially restart the same + # named IOC multiple times, we have to purge the cache else the # result from caget/caput cache would be a DisconnectError during the # second test _channel_cache.purge() - if set_enum == SetValueEnum.CAPUT: - if get_enum == GetValueEnum.GET: - if parent_conn.poll(TIMEOUT): - parent_conn.recv() - else: - pytest.fail("IOC did not provide initial record value") - caput( - DEVICE_NAME + ":" + RECORD_NAME, + for configuration in record_configurations: + ( + record_name, + creation_func, initial_value, - wait=True, - **put_kwargs) + expected_value, + expected_type + ) = configuration + + # Infer some required keywords from parameters + put_kwargs = {} + get_kwargs = {} + if creation_func in [builder.WaveformOut, builder.WaveformIn]: + from cothread.dbr import DBR_CHAR_STR + if type(initial_value) in [str, bytes]: + put_kwargs.update({"datatype": DBR_CHAR_STR}) + get_kwargs.update({"count": len(initial_value) + 1}) + + if set_enum == SetValueEnum.CAPUT: + if get_enum == GetValueEnum.GET: + if parent_conn.poll(TIMEOUT): + parent_conn.recv() + else: + pytest.fail("IOC did not provide initial record value") + caput( + DEVICE_NAME + ":" + record_name, + initial_value, + wait=True, + **put_kwargs) + + if get_enum == GetValueEnum.GET: + parent_conn.send("Do another get!") + # Ensure IOC process has time to execute. + # I saw failures on MacOS where it appeared the IOC had not + # processed the put'ted value as the caget returned the same + # value as was originally passed in. + Yield(timeout=TIMEOUT) if get_enum == GetValueEnum.GET: - parent_conn.send("Do another get!") - # Ensure IOC process has time to execute. - # I saw failures on MacOS where it appeared the IOC had not - # processed the put'ted value as the caget returned the same value - # as was originally passed in. - Yield(timeout=TIMEOUT) - - if get_enum == GetValueEnum.GET: - if parent_conn.poll(TIMEOUT): - rec_val = parent_conn.recv() + if parent_conn.poll(TIMEOUT): + rec_val = parent_conn.recv() + else: + pytest.fail("IOC did not provide record value in queue") else: - pytest.fail("IOC did not provide record value in queue") - else: - rec_val = caget( - DEVICE_NAME + ":" + RECORD_NAME, - timeout=TIMEOUT, - **get_kwargs) - # '+' operator used to convert cothread's types into Python native - # types e.g. "+ca_int" -> int - rec_val = +rec_val - - - record_value_asserts( - creation_func, - rec_val, - expected_value, - expected_type) + rec_val = caget( + DEVICE_NAME + ":" + record_name, + timeout=TIMEOUT, + **get_kwargs) + # '+' operator used to convert cothread's types into Python + # native types e.g. "+ca_int" -> int + rec_val = +rec_val + + + record_value_asserts( + creation_func, + rec_val, + expected_value, + expected_type) finally: # Purge cache to suppress spurious "IOC disconnected" exceptions _channel_cache.purge() @@ -454,8 +502,8 @@ def skip_long_strings(record_values): class TestGetValue: - """Tests that use .get() to check whether values applied with .set() - or initial_value return the expected value""" + """Tests that use .get() to check whether values applied with .set(), + initial_value, or caput return the expected value""" def test_value_pre_init_set( self, clear_records, @@ -463,14 +511,19 @@ def test_value_pre_init_set( """Test that records provide the expected values on get calls when using .set() and .get() before IOC initialisation occurs""" - creation_func, initial_value, expected_value, expected_type = \ - record_values + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type + ) = record_values kwarg = {} if creation_func in [builder.WaveformIn, builder.WaveformOut]: kwarg = {"length": 50} # Required when no value on creation - out_rec = creation_func("out-record", **kwarg) + out_rec = creation_func(record_name, **kwarg) out_rec.set(initial_value) record_value_asserts( @@ -487,10 +540,15 @@ def test_value_pre_init_initial_value( """Test that records provide the expected values on get calls when using initial_value and .get() before IOC initialisation occurs""" - creation_func, initial_value, expected_value, expected_type = \ - record_values + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type + ) = record_values - out_rec = creation_func("out-record", initial_value=initial_value) + out_rec = creation_func(record_name, initial_value=initial_value) record_value_asserts( creation_func, @@ -499,121 +557,170 @@ def test_value_pre_init_initial_value( expected_type) @requires_cothread - def test_value_post_init_set(self, record_values): + def test_value_post_init_set(self): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and .get() after initialisation""" - run_test_function( - record_values, + record_values_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.GET) @requires_cothread - def test_value_post_init_initial_value(self, record_values): + def test_value_post_init_initial_value(self): """Test that records provide the expected values on get calls when using initial_value during record creation and .get() after IOC initialisation """ - run_test_function( - record_values, + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.GET) @requires_cothread - def test_value_post_init_set_after_init(self, record_values): + def test_value_post_init_set_after_init(self): """Test that records provide the expected values on get calls when using .set() and .get() after IOC initialisation""" - run_test_function( - record_values, + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET) @requires_cothread - def test_value_post_init_caput(self, record_values): + def test_value_post_init_caput(self): """Test that records provide the expected values on get calls when using caput and .get() after IOC initialisation""" - if record_values[0] in [ - builder.aIn, - builder.boolIn, - builder.longIn, - builder.mbbIn, - builder.stringIn, - builder.WaveformIn]: - pytest.skip("CAPut to In records doesn't propogate to .get()") + # Various conditions mean we cannot use the entire list of cases + filtered_list = [] + for item in record_values_list: + if ( + item[1] in [builder.stringIn, builder.stringOut] + and len(item[2]) > 40 + ): + # caput blocks strings longer than 40 characters + continue + + if item[1] not in [ + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn]: + # In records block caput + filtered_list.append(item) - skip_long_strings(record_values) run_test_function( - record_values, + filtered_list, SetValueEnum.CAPUT, GetValueEnum.GET) @requires_cothread class TestCagetValue: - """Tests that use Caget to check whether values applied with .set() - or initial_value return the expected value""" - - def set_xfail(self, record_values): - """Set xfail where appropraite for pythonSoftIOC issues""" - if record_values[0] in ( - builder.aIn, - builder.longIn, - builder.boolIn, - builder.stringIn, - builder.mbbIn, - builder.WaveformIn): - pytest.xfail(".set() on In records doesn't update correctly. " - "pythonSoftIOC issue #67") - - def test_value_post_init_set(self, record_values): + """Tests that use Caget to check whether values applied with .set(), + initial_value, or caput return the expected value""" + + 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""" - self.set_xfail(record_values) + # 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 ( + builder.aIn, + builder.longIn, + builder.boolIn, + builder.stringIn, + builder.mbbIn, + builder.WaveformIn): + filtered_list.append(item) run_test_function( - record_values, + filtered_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.CAGET) @requires_cothread - def test_value_post_init_initial_value(self, record_values): + def test_value_post_init_initial_value(self): """Test that records provide the expected values on get calls when using initial_value during record creation and caget after IOC initialisation """ run_test_function( - record_values, + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.CAGET) @requires_cothread - def test_value_post_init_set_after_init(self, record_values): + def test_value_post_init_set_after_init(self): """Test that records provide the expected values on get calls when using .set() and caget after IOC initialisation""" run_test_function( - record_values, + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.CAGET) - def test_value_post_init_caput(self, record_values): + def test_value_post_init_caput(self): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and caget after initialisation""" - skip_long_strings(record_values) + # Various conditions mean we cannot use the entire list of cases + filtered_list = [] + for item in record_values_list: + if ( + item[1] in [builder.stringIn, builder.stringOut] + and len(item[2]) > 40 + ): + # caput blocks long strings + continue + + if item[1] not in [ + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn]: + # In records block caput + filtered_list.append(item) run_test_function( - record_values, + filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET) -def default_values_names(fixture_value): - """Provide nice name for default_values fixture""" - return fixture_value[0].__name__ + +default_values_list = [ + ("default_aOut", builder.aOut, None, 0.0, float), + ("default_aIn", builder.aIn, None, 0.0, float), + ("default_longOut", builder.longOut, None, 0, int), + ("default_longIn", builder.longIn, None, 0, int), + ("default_boolOut", builder.boolOut, None, 0, int), + ("default_boolIn", builder.boolIn, None, 0, int), + ("default_stringOut", builder.stringOut, None, "", str), + ("default_stringIn", builder.stringIn, None, "", str), + ("default_mbbOut", builder.mbbOut, None, 0, int), + ("default_mbbIn", builder.mbbIn, None, 0, int), + ( + "default_wOut", + builder.WaveformOut, + None, + numpy.empty(0), + numpy.ndarray + ), + ( + "default_wIn", + builder.WaveformIn, + None, + numpy.empty(0), + numpy.ndarray + ), + ] class TestDefaultValue: """Tests related to default values""" @@ -652,48 +759,23 @@ def test_value_default_pre_init( expected_value, expected_type) - - - @pytest.fixture(params= [ - (builder.aOut, None, 0.0, float), - (builder.aIn, None, 0.0, float), - (builder.longOut, None, 0, int), - (builder.longIn, None, 0, int), - (builder.boolOut, None, 0, int), - (builder.boolIn, None, 0, int), - (builder.stringOut, None, "", str), - (builder.stringIn, None, "", str), - (builder.mbbOut, None, 0, int), - (builder.mbbIn, None, 0, int), - (builder.WaveformOut, None, numpy.empty(0), numpy.ndarray), - (builder.WaveformIn, None, numpy.empty(0), numpy.ndarray), - ], ids=default_values_names) - def default_values(self, request): - """Fixture for default values for initialised records. - - Fields are: - - Record builder function - - (UNUSED) fake initial value, so parameters are same as other fixtures - - Input value passed to .set()/initial_value/caput - - Expected output value after doing .get()/caget - - Expected type of the output value from .get()/caget""" - return request.param - - @requires_cothread - def test_value_default_post_init(self, default_values): + def test_value_default_post_init(self): """Test that records return the expected default value from .get after they are initialised, having never had a value set""" - run_test_function(default_values, SetValueEnum.NO_SET, GetValueEnum.GET) + run_test_function( + default_values_list, + SetValueEnum.NO_SET, + GetValueEnum.GET) @requires_cothread - def test_value_default_post_init_caget(self, default_values): + def test_value_default_post_init_caget(self): """Test that records return the expected default value from CAGet after they are initialised, having never had a value set""" run_test_function( - default_values, + default_values_list, SetValueEnum.NO_SET, GetValueEnum.CAGET) From 8d61dfd686df816046f8204505896ea7d90d2330 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 10:41:22 +0000 Subject: [PATCH 085/131] Apply Black formatting We don't run this formatter anywhere automatically, this was done manually. --- tests/test_records.py | 575 +++++++++++++++++++++--------------------- 1 file changed, 284 insertions(+), 291 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 5cb857e4..f597c05e 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -19,18 +19,13 @@ from softioc.pythonSoftIoc import RecordWrapper requires_cothread = pytest.mark.skipif( - sys.platform.startswith("win"), - reason="Cothread doesn't work on windows" + sys.platform.startswith("win"), reason="Cothread doesn't work on windows" ) # Test parameters DEVICE_NAME = "SOFT-IOC-TESTS" TIMEOUT = 5 # Seconds -# Value is so large to accomodate very slow GitHub MacOS CI runners. -# Most platforms only need a timeout of 3 to 5 seconds. -# Not a particular problem on faster platforms - this is never used as a sleep -# so tests will still run fast on platforms that can handle it. def _clear_records(): @@ -49,12 +44,14 @@ def clear_records(): yield _clear_records() + def record_funcs_names(fixture_value): """Provide a nice name for the record_funcs fixture""" return fixture_value.__name__ -@pytest.fixture(params=[ +@pytest.fixture( + params=[ builder.aIn, builder.aOut, builder.longIn, @@ -68,11 +65,13 @@ def record_funcs_names(fixture_value): builder.WaveformIn, builder.WaveformOut, ], - ids=record_funcs_names) + ids=record_funcs_names, +) def record_funcs(request): """The list of record creation functions""" return request.param + @pytest.fixture def record_funcs_reject_none(record_funcs): """The list of record creation functions that reject 'None' as a value""" @@ -80,135 +79,135 @@ def record_funcs_reject_none(record_funcs): pytest.skip(msg="None is valid value for string records") return record_funcs + def record_values_names(fixture_value): """Provide a nice name for the tests in the record_values fixture""" - return fixture_value[1].__name__ + "-" + type(fixture_value[2]).__name__ \ - + "-" + str(fixture_value[3]) + return ( + fixture_value[1].__name__ + + "-" + + type(fixture_value[2]).__name__ + + "-" + + str(fixture_value[3]) + ) + record_values_list = [ - ("aIn_float", builder.aIn, 5.5, 5.5, float), - ("aOut_float", builder.aOut, 5.5, 5.5, float), - ("aIn_int", builder.aIn, 3, 3., float), - ("aOut_int", builder.aOut, 3, 3., float), - ("aIn_inf", builder.aIn, inf, inf, float), - ("aOut_inf", builder.aOut, inf, inf, float), - ("aIn_neginf", builder.aIn, -inf, -inf, float), - ("aOut_neginf", builder.aOut, -inf, -inf, float), - ("aIn_nan", builder.aIn, nan, nan, float), - ("aOut_nan", builder.aOut, nan, nan, float), - ("longIn_int", builder.longIn, 5, 5, int), - ("longOut_int", builder.longOut, 5, 5, int), - ("longIn_float", builder.longIn, 9.9, 9, int), - ("longOut_float", builder.longOut, 9.9, 9, int), - ("boolIn_int", builder.boolIn, 1, 1, int), - ("boolOut_int", builder.boolOut, 1, 1, int), - ("boolIn_true", builder.boolIn, True, 1, int), - ("boolOut_true", builder.boolOut, True, 1, int), - ("boolIn_false", builder.boolIn, False, 0, int), - ("boolOut_false", builder.boolOut, False, 0, int), - ("mbbIn_int", builder.mbbIn, 1, 1, int), - ("mbbOut_int", builder.mbbOut, 1, 1, int), - ("strIn_abc", builder.stringIn, "abc", "abc", str), - ("strOut_abc", builder.stringOut, "abc", "abc", str), - ("strIn_empty", builder.stringIn, "", "", str), - ("strOut_empty", builder.stringOut, "", "", str), - ("strIn_bytes", builder.stringIn, b"abc", "abc", str), - ("strOut_bytes", builder.stringOut, b"abc", "abc", str), - ("strin_utf8", builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 - ("strOut_utf8", builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 - ( - "strIn_badutf8", - builder.stringIn, - b"a\xcfb", # Invalid UTF-8 - "a�b", - str - ), - ( - "strOut_badutf8", - builder.stringOut, - b"a\xcfb", # Invalid UTF-8 - "a�b", - str - ), - ( - "strIn_byteutf8", - builder.stringIn, - b"a\xe2\x82\xacb", # Valid UTF-8 - "a€b", - str - ), - ( - "strOut_byteutf8", - builder.stringOut, - b"a\xe2\x82\xacb", # Valid UTF-8 - "a€b", - str - ), - ( - "strIn_longstr", - builder.stringIn, - "this string is much longer than 40 characters", - "this string is much longer than 40 char", - str - ), - ( - "strOut_longstr", - builder.stringOut, - "this string is much longer than 40 characters", - "this string is much longer than 40 char", - str - ), - ( - "wIn_list", - builder.WaveformIn, - [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.float32), - numpy.ndarray - ), - ( - "wOut_list", - builder.WaveformOut, - [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.float32), - numpy.ndarray - ), - ( - "wIn_str", - builder.WaveformIn, - "ABC", - numpy.array([65, 66, 67, 0], dtype=numpy.uint8), - numpy.ndarray - ), - ( - "wOut_str", - builder.WaveformOut, - "ABC", - numpy.array([65, 66, 67, 0], dtype=numpy.uint8), - numpy.ndarray + ("aIn_float", builder.aIn, 5.5, 5.5, float), + ("aOut_float", builder.aOut, 5.5, 5.5, float), + ("aIn_int", builder.aIn, 3, 3.0, float), + ("aOut_int", builder.aOut, 3, 3.0, float), + ("aIn_inf", builder.aIn, inf, inf, float), + ("aOut_inf", builder.aOut, inf, inf, float), + ("aIn_neginf", builder.aIn, -inf, -inf, float), + ("aOut_neginf", builder.aOut, -inf, -inf, float), + ("aIn_nan", builder.aIn, nan, nan, float), + ("aOut_nan", builder.aOut, nan, nan, float), + ("longIn_int", builder.longIn, 5, 5, int), + ("longOut_int", builder.longOut, 5, 5, int), + ("longIn_float", builder.longIn, 9.9, 9, int), + ("longOut_float", builder.longOut, 9.9, 9, int), + ("boolIn_int", builder.boolIn, 1, 1, int), + ("boolOut_int", builder.boolOut, 1, 1, int), + ("boolIn_true", builder.boolIn, True, 1, int), + ("boolOut_true", builder.boolOut, True, 1, int), + ("boolIn_false", builder.boolIn, False, 0, int), + ("boolOut_false", builder.boolOut, False, 0, int), + ("mbbIn_int", builder.mbbIn, 1, 1, int), + ("mbbOut_int", builder.mbbOut, 1, 1, int), + ("strIn_abc", builder.stringIn, "abc", "abc", str), + ("strOut_abc", builder.stringOut, "abc", "abc", str), + ("strIn_empty", builder.stringIn, "", "", str), + ("strOut_empty", builder.stringOut, "", "", str), + ("strIn_bytes", builder.stringIn, b"abc", "abc", str), + ("strOut_bytes", builder.stringOut, b"abc", "abc", str), + ("strin_utf8", builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 + ("strOut_utf8", builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 + ("strIn_badutf8", builder.stringIn, b"a\xcfb", "a�b", str), # Invalid UTF-8 + ( + "strOut_badutf8", + builder.stringOut, + b"a\xcfb", # Invalid UTF-8 + "a�b", + str, + ), + ( + "strIn_byteutf8", + builder.stringIn, + b"a\xe2\x82\xacb", # Valid UTF-8 + "a€b", + str, + ), + ( + "strOut_byteutf8", + builder.stringOut, + b"a\xe2\x82\xacb", # Valid UTF-8 + "a€b", + str, + ), + ( + "strIn_longstr", + builder.stringIn, + "this string is much longer than 40 characters", + "this string is much longer than 40 char", + str, + ), + ( + "strOut_longstr", + builder.stringOut, + "this string is much longer than 40 characters", + "this string is much longer than 40 char", + str, + ), + ( + "wIn_list", + builder.WaveformIn, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.float32), + numpy.ndarray, + ), + ( + "wOut_list", + builder.WaveformOut, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.float32), + numpy.ndarray, + ), + ( + "wIn_str", + builder.WaveformIn, + "ABC", + numpy.array([65, 66, 67, 0], dtype=numpy.uint8), + numpy.ndarray, + ), + ( + "wOut_str", + builder.WaveformOut, + "ABC", + numpy.array([65, 66, 67, 0], dtype=numpy.uint8), + numpy.ndarray, + ), + ( + "wIn_bytes", + builder.WaveformIn, + b"HELLO\0WORLD", + numpy.array( + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], dtype=numpy.uint8 ), - ( - "wIn_bytes", - builder.WaveformIn, - b"HELLO\0WORLD", - numpy.array([72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], - dtype=numpy.uint8), - numpy.ndarray + numpy.ndarray, + ), + ( + "wOut_bytes", + builder.WaveformOut, + b"HELLO\0WORLD", + numpy.array( + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], dtype=numpy.uint8 ), - ( - "wOut_bytes", - builder.WaveformOut, - b"HELLO\0WORLD", - numpy.array([72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], - dtype=numpy.uint8), - numpy.ndarray - ) - ] + numpy.ndarray, + ), +] -@pytest.fixture( - params=record_values_list, - ids=record_values_names - ) +@pytest.fixture(params=record_values_list, ids=record_values_names) def record_values(request): """A list of parameters for record value setting/getting tests. @@ -219,19 +218,39 @@ def record_values(request): - Expected type of the output value from .get()/caget""" return request.param # One item from the params list + def test_records(tmp_path): import sim_records + path = str(tmp_path / "records.db") builder.WriteRecords(path) expected = os.path.join(os.path.dirname(__file__), "expected_records.db") assert open(path).readlines()[5:] == open(expected).readlines() + def test_enum_length_restriction(): with pytest.raises(AssertionError): builder.mbbIn( - "ManyLabels", "one", "two", "three", "four", "five", "six", - "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", - "fourteen", "fifteen", "sixteen", "seventeen") + "ManyLabels", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + ) + def test_DISP_defaults_on(): """Test that all IN record types have DISP=1 set by default""" @@ -259,6 +278,7 @@ def test_DISP_defaults_on(): # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 1 + def test_DISP_can_be_overridden(): """Test that DISP can be forced off for In records""" @@ -268,10 +288,8 @@ def test_DISP_can_be_overridden(): def record_value_asserts( - creation_func, - actual_value, - expected_value, - expected_type): + creation_func, actual_value, expected_value, expected_type +): """Asserts that the expected value and expected type are matched with the actual value. Handles both scalar and waveform data""" try: @@ -287,31 +305,42 @@ def record_value_asserts( # Using .get() on the array returns entire length, not just filled # part. Confirm this by ensuring sliced part of array is all zeros assert not numpy.any(actual_value[expected_value.size:]) - truncated_value = actual_value[:expected_value.size] - assert numpy.array_equal(truncated_value, expected_value), \ - "Arrays not equal: {} {}".format(actual_value, expected_value) + truncated_value = actual_value[: expected_value.size] + assert numpy.array_equal( + truncated_value, expected_value + ), "Arrays not equal: {} {}".format(actual_value, expected_value) assert type(actual_value) == expected_type else: assert actual_value == expected_value assert type(actual_value) == expected_type except Exception as e: msg = ( - "Failed with parameters: " + str(creation_func) + ", " + - str(actual_value) + ", " + str(expected_value) + ", " - + str(expected_type)) + "Failed with parameters: " + + str(creation_func) + + ", " + + str(actual_value) + + ", " + + str(expected_value) + + ", " + + str(expected_type) + ) logging.error(msg, exc_info=e) raise e + class SetValueEnum(Enum): """Enum to control when and how the record's value should be set""" + INITIAL_VALUE = 1 SET_BEFORE_INIT = 2 SET_AFTER_INIT = 3 NO_SET = 4 CAPUT = 5 + class GetValueEnum(Enum): """Enum to control whether values are retrieved using .get() or caget()""" + GET = 1 CAGET = 2 @@ -333,7 +362,7 @@ def run_ioc(record_configurations: list, conn, set_enum, get_enum): creation_func, initial_value, expected_value, - expected_type + expected_type, ) = configuration if set_enum == SetValueEnum.INITIAL_VALUE: @@ -343,7 +372,6 @@ def run_ioc(record_configurations: list, conn, set_enum, get_enum): # Related to this issue: # https://github.com/dls-controls/pythonSoftIOC/issues/37 - out_rec = creation_func(record_name, **kwarg) if set_enum == SetValueEnum.SET_BEFORE_INIT: @@ -363,7 +391,7 @@ def run_ioc(record_configurations: list, conn, set_enum, get_enum): creation_func, initial_value, expected_value, - expected_type + expected_type, ) = configuration if set_enum == SetValueEnum.SET_AFTER_INIT: @@ -384,15 +412,13 @@ def run_ioc(record_configurations: list, conn, set_enum, get_enum): # Keep process alive while main thread works. # This is most applicable to CAGET tests. asyncio.run_coroutine_threadsafe( - asyncio.sleep(TIMEOUT), - dispatcher.loop + asyncio.sleep(TIMEOUT), dispatcher.loop ).result() def run_test_function( - record_configurations: list, - set_enum: SetValueEnum, - get_enum: GetValueEnum): + record_configurations: list, set_enum: SetValueEnum, get_enum: GetValueEnum +): """Run the test function using multiprocessing and check returned value is expected value. set_enum and get_enum determine when the record's value is set and how the value is retrieved, respectively.""" @@ -401,7 +427,7 @@ def run_test_function( ioc_process = multiprocessing.Process( target=run_ioc, - args=(record_configurations, child_conn, set_enum, get_enum) + args=(record_configurations, child_conn, set_enum, get_enum), ) ioc_process.start() @@ -412,9 +438,6 @@ def run_test_function( else: pytest.fail("IOC process did not start before TIMEOUT expired") - - - try: from cothread import Yield from cothread.catools import caget, caput, _channel_cache @@ -431,7 +454,7 @@ def run_test_function( creation_func, initial_value, expected_value, - expected_type + expected_type, ) = configuration # Infer some required keywords from parameters @@ -439,6 +462,7 @@ def run_test_function( get_kwargs = {} if creation_func in [builder.WaveformOut, builder.WaveformIn]: from cothread.dbr import DBR_CHAR_STR + if type(initial_value) in [str, bytes]: put_kwargs.update({"datatype": DBR_CHAR_STR}) get_kwargs.update({"count": len(initial_value) + 1}) @@ -453,7 +477,8 @@ def run_test_function( DEVICE_NAME + ":" + record_name, initial_value, wait=True, - **put_kwargs) + **put_kwargs + ) if get_enum == GetValueEnum.GET: parent_conn.send("Do another get!") @@ -472,17 +497,15 @@ def run_test_function( rec_val = caget( DEVICE_NAME + ":" + record_name, timeout=TIMEOUT, - **get_kwargs) + **get_kwargs + ) # '+' operator used to convert cothread's types into Python # native types e.g. "+ca_int" -> int rec_val = +rec_val - record_value_asserts( - creation_func, - rec_val, - expected_value, - expected_type) + creation_func, rec_val, expected_value, expected_type + ) finally: # Purge cache to suppress spurious "IOC disconnected" exceptions _channel_cache.purge() @@ -504,10 +527,8 @@ def skip_long_strings(record_values): class TestGetValue: """Tests that use .get() to check whether values applied with .set(), initial_value, or caput return the expected value""" - def test_value_pre_init_set( - self, - clear_records, - record_values): + + def test_value_pre_init_set(self, clear_records, record_values): """Test that records provide the expected values on get calls when using .set() and .get() before IOC initialisation occurs""" @@ -516,7 +537,7 @@ def test_value_pre_init_set( creation_func, initial_value, expected_value, - expected_type + expected_type, ) = record_values kwarg = {} @@ -527,16 +548,10 @@ def test_value_pre_init_set( out_rec.set(initial_value) record_value_asserts( - creation_func, - out_rec.get(), - expected_value, - expected_type) - + creation_func, out_rec.get(), expected_value, expected_type + ) - def test_value_pre_init_initial_value( - self, - clear_records, - record_values): + def test_value_pre_init_initial_value(self, clear_records, record_values): """Test that records provide the expected values on get calls when using initial_value and .get() before IOC initialisation occurs""" @@ -545,25 +560,22 @@ def test_value_pre_init_initial_value( creation_func, initial_value, expected_value, - expected_type + expected_type, ) = record_values out_rec = creation_func(record_name, initial_value=initial_value) record_value_asserts( - creation_func, - out_rec.get(), - expected_value, - expected_type) + creation_func, out_rec.get(), expected_value, expected_type + ) @requires_cothread def test_value_post_init_set(self): """Test that records provide the expected values on get calls when using .set() before IOC initialisation and .get() after initialisation""" run_test_function( - record_values_list, - SetValueEnum.SET_BEFORE_INIT, - GetValueEnum.GET) + record_values_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.GET + ) @requires_cothread def test_value_post_init_initial_value(self): @@ -571,18 +583,16 @@ def test_value_post_init_initial_value(self): initial_value during record creation and .get() after IOC initialisation """ run_test_function( - record_values_list, - SetValueEnum.INITIAL_VALUE, - GetValueEnum.GET) + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.GET + ) @requires_cothread def test_value_post_init_set_after_init(self): """Test that records provide the expected values on get calls when using .set() and .get() after IOC initialisation""" run_test_function( - record_values_list, - SetValueEnum.SET_AFTER_INIT, - GetValueEnum.GET) + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET + ) @requires_cothread def test_value_post_init_caput(self): @@ -600,20 +610,18 @@ def test_value_post_init_caput(self): continue if item[1] not in [ - builder.aIn, - builder.boolIn, - builder.longIn, - builder.mbbIn, - builder.stringIn, - builder.WaveformIn]: + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn, + ]: # In records block caput filtered_list.append(item) + run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.GET) - run_test_function( - filtered_list, - SetValueEnum.CAPUT, - GetValueEnum.GET) @requires_cothread class TestCagetValue: @@ -630,18 +638,18 @@ def test_value_post_init_set(self): # .set() on In records doesn't update correctly. # pythonSoftIOC issue #67 if item[1] not in ( - builder.aIn, - builder.longIn, - builder.boolIn, - builder.stringIn, - builder.mbbIn, - builder.WaveformIn): + builder.aIn, + builder.longIn, + builder.boolIn, + builder.stringIn, + builder.mbbIn, + builder.WaveformIn, + ): filtered_list.append(item) run_test_function( - filtered_list, - SetValueEnum.SET_BEFORE_INIT, - GetValueEnum.CAGET) + filtered_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.CAGET + ) @requires_cothread def test_value_post_init_initial_value(self): @@ -650,9 +658,8 @@ def test_value_post_init_initial_value(self): """ run_test_function( - record_values_list, - SetValueEnum.INITIAL_VALUE, - GetValueEnum.CAGET) + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.CAGET + ) @requires_cothread def test_value_post_init_set_after_init(self): @@ -660,9 +667,8 @@ def test_value_post_init_set_after_init(self): .set() and caget after IOC initialisation""" run_test_function( - record_values_list, - SetValueEnum.SET_AFTER_INIT, - GetValueEnum.CAGET) + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.CAGET + ) def test_value_post_init_caput(self): """Test that records provide the expected values on get calls when using @@ -679,71 +685,58 @@ def test_value_post_init_caput(self): continue if item[1] not in [ - builder.aIn, - builder.boolIn, - builder.longIn, - builder.mbbIn, - builder.stringIn, - builder.WaveformIn]: + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn, + ]: # In records block caput filtered_list.append(item) - run_test_function( - filtered_list, - SetValueEnum.CAPUT, - GetValueEnum.CAGET) - + run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET) default_values_list = [ - ("default_aOut", builder.aOut, None, 0.0, float), - ("default_aIn", builder.aIn, None, 0.0, float), - ("default_longOut", builder.longOut, None, 0, int), - ("default_longIn", builder.longIn, None, 0, int), - ("default_boolOut", builder.boolOut, None, 0, int), - ("default_boolIn", builder.boolIn, None, 0, int), - ("default_stringOut", builder.stringOut, None, "", str), - ("default_stringIn", builder.stringIn, None, "", str), - ("default_mbbOut", builder.mbbOut, None, 0, int), - ("default_mbbIn", builder.mbbIn, None, 0, int), - ( - "default_wOut", - builder.WaveformOut, - None, - numpy.empty(0), - numpy.ndarray - ), - ( - "default_wIn", - builder.WaveformIn, - None, - numpy.empty(0), - numpy.ndarray - ), - ] + ("default_aOut", builder.aOut, None, 0.0, float), + ("default_aIn", builder.aIn, None, 0.0, float), + ("default_longOut", builder.longOut, None, 0, int), + ("default_longIn", builder.longIn, None, 0, int), + ("default_boolOut", builder.boolOut, None, 0, int), + ("default_boolIn", builder.boolIn, None, 0, int), + ("default_stringOut", builder.stringOut, None, "", str), + ("default_stringIn", builder.stringIn, None, "", str), + ("default_mbbOut", builder.mbbOut, None, 0, int), + ("default_mbbIn", builder.mbbIn, None, 0, int), + ("default_wOut", builder.WaveformOut, None, numpy.empty(0), numpy.ndarray), + ("default_wIn", builder.WaveformIn, None, numpy.empty(0), numpy.ndarray), +] + + class TestDefaultValue: """Tests related to default values""" - @pytest.mark.parametrize("creation_func,expected_value,expected_type", [ - (builder.aOut, None, type(None)), - (builder.aIn, 0.0, float), - (builder.longOut, None, type(None)), - (builder.longIn, 0, int), - (builder.boolOut, None, type(None)), - (builder.boolIn, 0, int), - (builder.stringOut, None, type(None)), - (builder.stringIn, "", str), - (builder.mbbOut, None, type(None)), - (builder.mbbIn, 0, int), - (builder.WaveformOut, None, type(None)), - (builder.WaveformIn, numpy.empty(0), numpy.ndarray), - ]) + @pytest.mark.parametrize( + "creation_func,expected_value,expected_type", + [ + (builder.aOut, None, type(None)), + (builder.aIn, 0.0, float), + (builder.longOut, None, type(None)), + (builder.longIn, 0, int), + (builder.boolOut, None, type(None)), + (builder.boolIn, 0, int), + (builder.stringOut, None, type(None)), + (builder.stringIn, "", str), + (builder.mbbOut, None, type(None)), + (builder.mbbIn, 0, int), + (builder.WaveformOut, None, type(None)), + (builder.WaveformIn, numpy.empty(0), numpy.ndarray), + ], + ) def test_value_default_pre_init( - self, - creation_func, - expected_value, - expected_type, - clear_records): + self, creation_func, expected_value, expected_type, clear_records + ): """Test that the correct default values are returned from .get() (before record initialisation) when no initial_value or .set() is done""" # Out records do not have default values until records are initialized @@ -754,10 +747,8 @@ def test_value_default_pre_init( out_rec = creation_func("out-record", **kwarg) record_value_asserts( - creation_func, - out_rec.get(), - expected_value, - expected_type) + creation_func, out_rec.get(), expected_value, expected_type + ) @requires_cothread def test_value_default_post_init(self): @@ -765,9 +756,8 @@ def test_value_default_post_init(self): they are initialised, having never had a value set""" run_test_function( - default_values_list, - SetValueEnum.NO_SET, - GetValueEnum.GET) + default_values_list, SetValueEnum.NO_SET, GetValueEnum.GET + ) @requires_cothread def test_value_default_post_init_caget(self): @@ -775,35 +765,38 @@ def test_value_default_post_init_caget(self): they are initialised, having never had a value set""" run_test_function( - default_values_list, - SetValueEnum.NO_SET, - GetValueEnum.CAGET) + default_values_list, SetValueEnum.NO_SET, GetValueEnum.CAGET + ) + class TestNoneValue: """Various tests regarding record value of None""" + def test_value_none_rejected_initial_value( - self, - clear_records, - record_funcs_reject_none): + self, clear_records, record_funcs_reject_none + ): """Test setting \"None\" as the initial_value raises an exception""" kwarg = {} - if record_funcs_reject_none in \ - [builder.WaveformIn, builder.WaveformOut]: + if record_funcs_reject_none in [ + builder.WaveformIn, + builder.WaveformOut, + ]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises((ValueError, TypeError)): record_funcs_reject_none("SOME-NAME", initial_value=None, **kwarg) def test_value_none_rejected_set_before_init( - self, - clear_records, - record_funcs_reject_none): + self, clear_records, record_funcs_reject_none + ): """Test that setting \"None\" using .set() raises an exception""" kwarg = {} - if record_funcs_reject_none in \ - [builder.WaveformIn, builder.WaveformOut]: + if record_funcs_reject_none in [ + builder.WaveformIn, + builder.WaveformOut, + ]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises((ValueError, TypeError)): @@ -835,7 +828,7 @@ def test_value_none_rejected_set_after_init(self, record_funcs_reject_none): queue = multiprocessing.Queue() process = multiprocessing.Process( target=self.none_value_test_func, - args=(record_funcs_reject_none, queue) + args=(record_funcs_reject_none, queue), ) process.start() From 48c440d20ec4a2eabd5497889f91c5028eca13ce Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 10:55:21 +0000 Subject: [PATCH 086/131] Specify old pip behaviour in yaml file Specifying it in the setup.cfg file didn't seem to work, so try in line --- .github/workflows/code.yml | 1 + setup.cfg | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index a0a36f3b..4026d244 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -81,6 +81,7 @@ jobs: - name: Build Wheel run: cibuildwheel --output-dir dist env: + PIP_USE_DEPRECATED: out-of-tree-build CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{ matrix.results_file }} diff --git a/setup.cfg b/setup.cfg index e6db2427..26faed7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,12 +64,3 @@ addopts = [coverage:run] # This is covered in the versiongit test suite so exclude it here omit = */_version_git.py - -# cibuildwheel 2.3.0 upgraded to pip v21.3, which no longer completely copies the -# source directory before building, so the symlink'd "iocStatsDb" folder isn't included -# in built wheels. This is a temporary fix for that, until pip drops support for this option. -# The long term fix is to revamp how we include the iocStatsDb folder. -# https://github.com/pypa/cibuildwheel/issues/962 -# https://github.com/pypa/pip/issues/7555 -[tool.cibuildwheel.environment] -PIP_USE_DEPRECATED = "out-of-tree-build" From 7714053895514c994bbfed1155c903483bb683cf Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 11:15:48 +0000 Subject: [PATCH 087/131] Go back to pinning cibuildwheel 2.2.2 Suggested workaround of activating out-of-tree-build seems to not work on python 3.8+ and so it isn't suitable --- .github/workflows/code.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 4026d244..3fa88e43 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -72,7 +72,7 @@ jobs: - name: Install Python Dependencies # Pin cibuildwheel due to https://github.com/pypa/cibuildwheel/issues/962 - run: pip install build cibuildwheel>=2.3.1 + run: pip install build cibuildwheel==2.2.2 - name: Build Sdist if: matrix.sdist @@ -81,7 +81,6 @@ jobs: - name: Build Wheel run: cibuildwheel --output-dir dist env: - PIP_USE_DEPRECATED: out-of-tree-build CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{ matrix.results_file }} From f9a8af173fe6227385eb301345da01b5253d9343 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 12:00:44 +0000 Subject: [PATCH 088/131] Attempt to use cibuildwheel 2.3.1 again This approach specifies the environment variable inside the CIBW_ENVIRONMENT variable, which will mean it won't interfere with cibuildwheel setting up its own environment --- .github/workflows/code.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 3fa88e43..120cb529 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -72,7 +72,7 @@ jobs: - name: Install Python Dependencies # Pin cibuildwheel due to https://github.com/pypa/cibuildwheel/issues/962 - run: pip install build cibuildwheel==2.2.2 + run: pip install build cibuildwheel>=2.3.1 - name: Build Sdist if: matrix.sdist @@ -81,6 +81,7 @@ jobs: - name: Build Wheel run: cibuildwheel --output-dir dist env: + CIBW_ENVIRONMENT: PIP_USE_DEPRECATED=out-of-tree-build CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev CIBW_TEST_COMMAND: pytest {project}/tests --cov-report xml:${{ matrix.cov_file }} --junit-xml=${{ matrix.results_file }} From 51b37d9b1262fe689115ff9dcfb8711c59a34cc4 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 15 Dec 2021 13:08:12 +0000 Subject: [PATCH 089/131] Add comment explaining env var --- .github/workflows/code.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 120cb529..bcf4ddc3 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -81,6 +81,7 @@ jobs: - name: Build Wheel run: cibuildwheel --output-dir dist env: + # Force old behaviour of pip - see https://github.com/pypa/cibuildwheel/issues/962 CIBW_ENVIRONMENT: PIP_USE_DEPRECATED=out-of-tree-build CIBW_BUILD: ${{ matrix.python }}*64 CIBW_TEST_EXTRAS: dev From 7f54987fe8b4beccfe21c47532b67ce440ca7718 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Tue, 4 Jan 2022 09:16:05 +0000 Subject: [PATCH 090/131] Initial rewrite of core device value handling The goal here is to handle the conversion between Python and EPICS value representation in the most consistent way possible. Each record type has an underlying representation corresponding to what is written to EPICS, but the user interface continues to work with standard the Python representation. --- softioc/device.py | 293 +++++++++++++++++++++++++--------------------- softioc/fields.py | 24 +++- 2 files changed, 184 insertions(+), 133 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 7d125345..3984f7cb 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -1,11 +1,11 @@ import os import time -import inspect +import ctypes from ctypes import * import numpy from . import alarm -from .fields import DbfCodeToNumpy, DbrToDbfCode +from . import fields from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field from .device_core import DeviceSupportCore, RecordLookup @@ -33,19 +33,46 @@ class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): # For some record types we want to return a different return code either # from record init or processing - _epics_rc = EPICS_OK + _epics_rc_ = EPICS_OK - # Default implementations of read and write, overwritten where necessary. + + # Most subclasses (all except waveforms) define a ctypes constructor for the + # underlying EPICS compatible value. + def _value_to_epics(self, value): + return self._ctype_(value) + + def _epics_to_value(self, epics): + return epics.value + + def _default_value(self): + return self._ctype_() + + def _compare_values(self, value1, value2): + return value1.value == value2.value + + + # This method is called during Out record processing to return the + # underlying value in Python format. def _read_value(self, record): - return getattr(record, 'VAL') + return record.read_val() + + # This method is called during In record processing to update the + # underlying value (the value must be in EPICS compatible format). This is + # also called during Out record initialisation and value reversion when + # required. def _write_value(self, record, value): - setattr(record, 'VAL', value) + record.write_val(value) class ProcessDeviceSupportIn(ProcessDeviceSupportCore): _link_ = 'INP' def __init__(self, name, **kargs): + if 'initial_value' in kargs: + value = self._value_to_epics(kargs.pop('initial_value')) + else: + value = self._default_value() + # We implement update locking via a simple trick which relies on the # Python global interpreter lock: this ensures that assigning or # reading a single value is atomic. We therefore cluster all our @@ -53,28 +80,26 @@ def __init__(self, name, **kargs): # to be processed. # The tuple contains everything needed to be written: the value, # severity, alarm and optional timestamp. - self._value = ( - kargs.pop('initial_value', self._default_), - alarm.NO_ALARM, alarm.UDF_ALARM, None) + self._value = (value, alarm.NO_ALARM, alarm.UDF_ALARM, None) self.__super.__init__(name, **kargs) - def _process(self, record, _value=None): + def _process(self, record): # For input process we copy the value stored in the instance to the # record. The alarm status is also updated, and a custom timestamp # can also be set. - if _value is None: - _value = self._value - value, severity, alarm, timestamp = _value + value, severity, alarm, timestamp = self._value self._write_value(record, value) self.process_severity(record, severity, alarm) if timestamp is not None: record.TIME = timestamp - return self._epics_rc + record.UDF = 0 + return self._epics_rc_ def set(self, value, severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None): '''Updates the stored value and triggers an update. The alarm severity and timestamp can also be specified if appropriate.''' + value = self._value_to_epics(value) self._value = (value, severity, alarm, timestamp) self.trigger() @@ -86,7 +111,7 @@ def set_alarm(self, severity, alarm, timestamp=None): def get(self): '''Returns the last written value.''' - return self._value[0] + return self._epics_to_value(self._value[0]) class ProcessDeviceSupportOut(ProcessDeviceSupportCore): @@ -107,101 +132,65 @@ def __init__(self, name, **kargs): self.__validate = kargs.pop('validate', None) self.__always_update = kargs.pop('always_update', False) - self._value = kargs.pop('initial_value', None) self.__enable_write = True + + if 'initial_value' in kargs: + self._value = self._value_to_epics(kargs.pop('initial_value')) + else: + self._value = None + self.__super.__init__(name, **kargs) def init_record(self, record): '''Special record initialisation for out records only: implements special record initialisation if an initial value has been specified, allowing out records to have a sensible initial value.''' - if self._value is not None: + if self._value is None: + # Cannot set in __init__ (like we do for In records), as we want + # the record alarm status to be set if no value was provided + # Probably related to PythonSoftIOC issue #53 + self._value = self._default_value() + else: self._write_value(record, self._value) if 'MLST' in self._fields_: record.MLST = self._value record.TIME = time.time() record.UDF = 0 recGblResetAlarms(record) - return self._epics_rc + return self._epics_rc_ def _process(self, record): '''Processing suitable for output records. Performs immediate value validation and asynchronous update notification.''' value = self._read_value(record) - if numpy.all(value == self._value) and not self.__always_update: + if not self.__always_update and \ + self._compare_values(value, self._value): # If the value isn't making a change then don't do anything. return EPICS_OK + + python_value = self._epics_to_value(value) if self.__enable_write and self.__validate and \ - not self.__validate(self, value): - # Asynchronous validation rejects value. It's up to the - # validation routine to do any logging. In this case restore the - # last good value. - if self._value is not None: - self._write_value(record, self._value) + not self.__validate(self, python_value): + # Asynchronous validation rejects value, so restore the last good + # value. + self._write_value(record, self._value) return EPICS_ERROR + else: + # Value is good. Hang onto it, let users know the value has changed + self._value = value + record.UDF = 0 + if self.__on_update and self.__enable_write: + dispatcher(self.__on_update, python_value) + return EPICS_OK - self._value = value - if self.__on_update and self.__enable_write: - dispatcher(self.__on_update, value) - return EPICS_OK - - - NumpyCharCodeToDbr = { - # The following type codes are supported directly: - 'S': 0, # DBR_STRING str_ - 'h': 1, # DBR_SHORT short = int16 - 'f': 2, # DBR_FLOAT single = float32 - 'b': 4, # DBR_CHAR byte = int8 - 'i': 5, # DBR_LONG intc = int32 - 'd': 6, # DBR_DOUBLE float_ = float64 - # These are supported as related types - 'H': 1, # DBR_SHORT ushort = uint16 - '?': 4, # DBR_CHAR bool_ - 'B': 4, # DBR_CHAR ubyte = uint8 - 'I': 5, # DBR_LONG uintc = uint32 - } - if numpy.int_().itemsize == 4: - NumpyCharCodeToDbr.update({'l': 5, 'L': 5}) # int_, uint - - # Converts a Python value into a form suitable for sending over channel - # access. Derived from the corresponding cothread implementation. Returns - # computed dbrcode, waveform length, raw data pointer and pointer to - # underlying data (for lifetime management). - def value_to_dbr(self, value): - # First convert the data directly into an array. This will help in - # subsequent processing: this does most of the type coercion. - value = numpy.require(value, requirements = 'C') - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t put multidimensional arrays!' - - if value.dtype.char == 'S': - # Need special processing to hack the array so that strings are - # actually 40 characters long. - new_value = numpy.empty(value.shape, 'S40') - new_value[:] = value - value = new_value - try: - dbrtype = self.NumpyCharCodeToDbr[value.dtype.char] - except KeyError: - # One more special case. caput() of a list of integers on a 64-bit - # system will fail at this point because they were automatically - # converted to 64-bit integers. Catch this special case and fix it - # up by silently converting to 32-bit integers. Not really the - # right thing to do (as data can be quietly lost), but the - # alternative isn't nice to use either. - if value.dtype.char == 'l': - value = numpy.require(value, dtype = numpy.int32) - dbrtype = 5 - else: - raise - - return dbrtype, len(value), value.ctypes.data, value + def _value_to_dbr(self, value): + return self._dbf_type_, 1, addressof(value), value def set(self, value, process=True): '''Special routine to set the value directly.''' + value = self._value_to_epics(value) try: _record = self._record except AttributeError: @@ -209,24 +198,25 @@ def set(self, value, process=True): # initialisation occurs self._value = value else: - datatype, length, data, array = self.value_to_dbr(value) + # The array parameter is used to keep the raw pointer alive + dbf_code, length, data, array = self._value_to_dbr(value) self.__enable_write = process - db_put_field( - _record.NAME, DbrToDbfCode[datatype], data, length) + db_put_field(_record.NAME, dbf_code, data, length) self.__enable_write = True def get(self): - return self._value + return self._epics_to_value(self._value) -def _Device(Base, record_type, mlst=False, default=0, convert=True): +def _Device(Base, record_type, ctype, dbf_type, epics_rc, mlst = False): '''Wrapper for generating simple records.''' class GenericDevice(Base): _record_type_ = record_type _device_name_ = 'devPython_' + record_type - _default_ = default _fields_ = ['UDF', 'VAL'] - _epics_rc = EPICS_OK if convert else NO_CONVERT + _epics_rc_ = epics_rc + _ctype_ = staticmethod(ctype) + _dbf_type_ = dbf_type if mlst: _fields_.append('MLST') @@ -236,20 +226,46 @@ class GenericDevice(Base): _In = ProcessDeviceSupportIn _Out = ProcessDeviceSupportOut -def _Device_In(type, **kargs): - return _Device(_In, type, **kargs) +def _Device_In(*args, **kargs): + return _Device(_In, mlst = False, *args, **kargs) + +def _Device_Out(*args, **kargs): + return _Device(_Out, mlst = True, *args, **kargs) + +longin = _Device_In ('longin', c_int32, fields.DBF_LONG, EPICS_OK) +longout = _Device_Out('longout', c_int32, fields.DBF_LONG, EPICS_OK) +bi = _Device_In ('bi', c_uint16, fields.DBF_CHAR, NO_CONVERT) +bo = _Device_Out('bo', c_uint16, fields.DBF_CHAR, NO_CONVERT) +mbbi = _Device_In ('mbbi', c_uint16, fields.DBF_SHORT, NO_CONVERT) +mbbo = _Device_Out('mbbo', c_uint16, fields.DBF_SHORT, NO_CONVERT) + + +class EpicsString: + _fields_ = ['UDF', 'VAL'] + _epics_rc_ = EPICS_OK + _ctype_ = c_char * 40 + _dbf_type_ = fields.DBF_STRING + + def _value_to_epics(self, value): + if isinstance(value, str): + value = value.encode() + # It's a little odd: we can't simply construct a value from the byte + # string, but we can update the array in an existing value + result = self._ctype_() + result.value = value + return result -def _Device_Out(type, convert=True, mlst=True): - return _Device(_Out, type, convert=convert, mlst=mlst, default=None) + def _epics_to_value(self, epics): + return epics.value.decode(errors = 'replace') -longin = _Device_In('longin') -longout = _Device_Out('longout') -bi = _Device_In('bi', convert=False) -bo = _Device_Out('bo', convert=False) -stringin = _Device_In('stringin', mlst=False, default='') -stringout = _Device_Out('stringout', mlst=False) -mbbi = _Device_In('mbbi', convert=False) -mbbo = _Device_Out('mbbo', convert=False) + +class stringin(EpicsString, ProcessDeviceSupportIn): + _record_type_ = 'stringin' + _device_name_ = 'devPython_stringin' + +class stringout(EpicsString, ProcessDeviceSupportOut): + _record_type_ = 'stringout' + _device_name_ = 'devPython_stringout' dset_process_linconv = ( @@ -263,26 +279,26 @@ def _Device_Out(type, convert=True, mlst=True): class ai(ProcessDeviceSupportIn): _record_type_ = 'ai' _device_name_ = 'devPython_ai' - _default_ = 0.0 _fields_ = ['UDF', 'VAL'] _dset_extra_ = dset_process_linconv - _epics_rc = NO_CONVERT + _epics_rc_ = NO_CONVERT + _ctype_ = c_double + _dbf_type_ = fields.DBF_DOUBLE def _process(self, record): - _value = self._value - self.__super._process(record, _value) # Because we're returning NO_CONVERT we need to do the .UDF updating # ourself (otherwise the record support layer does this). - record.UDF = int(numpy.isnan(_value[0])) - return NO_CONVERT + record.UDF = int(numpy.isnan(self._value[0])) + return self.__super._process(record) class ao(ProcessDeviceSupportOut): _record_type_ = 'ao' _device_name_ = 'devPython_ao' _fields_ = ['UDF', 'VAL', 'MLST'] _dset_extra_ = dset_process_linconv - _epics_rc = NO_CONVERT - + _epics_rc_ = NO_CONVERT + _ctype_ = c_double + _dbf_type_ = fields.DBF_DOUBLE class WaveformBase(ProcessDeviceSupportCore): @@ -296,8 +312,12 @@ class WaveformBase(ProcessDeviceSupportCore): # Allow set() to be called before init_record: dtype = None + _default_ = numpy.array([]) + def init_record(self, record): - self.dtype = DbfCodeToNumpy[record.FTVL] + self._nelm = record.NELM + self._dbf_type_ = record.FTVL + self.dtype = fields.DbfCodeToNumpy[record.FTVL] return self.__super.init_record(record) def _read_value(self, record): @@ -310,9 +330,6 @@ def _read_value(self, record): def _write_value(self, record, value): value = numpy.require(value, dtype = self.dtype) - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t write multidimensional arrays' nelm = record.NELM nord = len(value) @@ -323,35 +340,49 @@ def _write_value(self, record, value): self.dtype.itemsize * nord) record.NORD = nord + def _compare_values(self, value, other): + return numpy.all(value == other) -class waveform(WaveformBase, ProcessDeviceSupportIn): - _record_type_ = 'waveform' - _device_name_ = 'devPython_waveform' - _default_ = () - - # Because arrays are mutable values it's ever so easy to accidentially call - # set() with a value which subsequently changes. To avoid this common class - # of bug, at the cost of duplicated code and data, here we ensure a copy is - # taken of the value. - def set(self, value, - severity=alarm.NO_ALARM, alarm=alarm.UDF_ALARM, timestamp=None): - '''Updates the stored value and triggers an update. The alarm - severity and timestamp can also be specified if appropriate.''' + def _value_to_epics(self, value): + '''Handles strings and bytearrays as values for Waveforms. Returns the + string converted to a numpy array.''' if isinstance(value, str): + value = value.encode(errors = 'replace') + if isinstance(value, bytes): # Convert a string into an array of characters. This will produce # the correct behaviour when treating a character array as a string. # Note that the trailing null is needed to work around problems with - # some clients. - value = numpy.fromstring(value + '\0', dtype = 'uint8') + # some clients. Note this also exists in builder.py's _waveform(). + value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) + # Ensure we always convert incoming value into numpy array, regardless + # of whether the record has been initialised or not value = numpy.require(value, dtype = self.dtype) - self._value = (+value, severity, alarm, timestamp) if value.shape == (): value.shape = (1,) assert value.ndim == 1, 'Can\'t write multidimensional arrays' - self.trigger() + # Truncate value to fit + if hasattr(self, '_nelm'): + value = value[:self._nelm] + + # Because arrays are mutable values it's ever so easy to accidentially + # call set() with a value which subsequently changes. To avoid this + # common class of bug, at the cost of duplicated code and data, here we + # ensure a copy is taken of the value. + return +value + def _epics_to_value(self, value): + return value + + def _value_to_dbr(self, value): + value = numpy.require(value, dtype = self.dtype) + return self._dbf_type_, len(value), value.ctypes.data, value + + +class waveform(WaveformBase, ProcessDeviceSupportIn): + _record_type_ = 'waveform' + _device_name_ = 'devPython_waveform' class waveform_out(WaveformBase, ProcessDeviceSupportOut): _record_type_ = 'waveform' @@ -359,4 +390,4 @@ class waveform_out(WaveformBase, ProcessDeviceSupportOut): # Ensure the .dbd file is loaded. -dbLoadDatabase("device.dbd", os.path.dirname(__file__), None) +dbLoadDatabase('device.dbd', os.path.dirname(__file__), None) diff --git a/softioc/fields.py b/softioc/fields.py index d44dfb69..2cdef68c 100644 --- a/softioc/fields.py +++ b/softioc/fields.py @@ -26,6 +26,7 @@ # downstream for no good purpose -- for example, enums are of type DBF_ULONG, # but this cannot be written with caput.) DbfCodeToCtypes = { + DBF_STRING: c_char * 40, DBF_CHAR: c_byte, DBF_UCHAR: c_ubyte, DBF_SHORT: c_int16, @@ -98,9 +99,11 @@ def __getattr__(self, field): if field == 'TIME': return self.__get_time(address) elif field_type == DBF_STRING: - return string_at(cast(address, c_char_p)).decode() + raw_string = string_at(cast(address, c_char_p)) + return raw_string.decode(errors = 'replace') elif field_type in [DBF_INLINK, DBF_OUTLINK]: - return cast(address, POINTER(c_char_p))[0].decode() + raw_string = cast(address, POINTER(c_char_p))[0] + return raw_string.decode(errors = 'replace') else: ctypes_type = DbfCodeToCtypes[field_type] return cast(address, POINTER(ctypes_type))[0] @@ -121,6 +124,23 @@ def __setattr__(self, field, value): ctypes_type = DbfCodeToCtypes[field_type] cast(address, POINTER(ctypes_type))[0] = value + # Directly write EPICS compatible value to .VAL field + def write_val(self, value): + offset, size, field_type = self.fields['VAL'] + address = c_void_p(offset + self.record.value) + + ctypes_type = DbfCodeToCtypes[field_type] + assert sizeof(value) == size, \ + 'Incompatible field and value: %d %d' % (sizeof(value), size) + cast(address, POINTER(ctypes_type))[0] = value + + def read_val(self): + offset, size, field_type = self.fields['VAL'] + address = c_void_p(offset + self.record.value) + ctypes_type = DbfCodeToCtypes[field_type] + return cast(address, POINTER(ctypes_type)).contents + + def __get_time(self, address): timestamp = cast(address, POINTER(ca_timestamp))[0] timestamp.secs += EPICS_epoch From f5817cd2bec226f821b96ce07fae55fc89e481aa Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Tue, 4 Jan 2022 10:03:32 +0000 Subject: [PATCH 091/131] Fix missing waveform default value Also destroy readable layout to avoid flake8 test failure. --- softioc/device.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 3984f7cb..fdedb18c 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -232,12 +232,12 @@ def _Device_In(*args, **kargs): def _Device_Out(*args, **kargs): return _Device(_Out, mlst = True, *args, **kargs) -longin = _Device_In ('longin', c_int32, fields.DBF_LONG, EPICS_OK) -longout = _Device_Out('longout', c_int32, fields.DBF_LONG, EPICS_OK) -bi = _Device_In ('bi', c_uint16, fields.DBF_CHAR, NO_CONVERT) -bo = _Device_Out('bo', c_uint16, fields.DBF_CHAR, NO_CONVERT) -mbbi = _Device_In ('mbbi', c_uint16, fields.DBF_SHORT, NO_CONVERT) -mbbo = _Device_Out('mbbo', c_uint16, fields.DBF_SHORT, NO_CONVERT) +longin = _Device_In('longin', c_int32, fields.DBF_LONG, EPICS_OK) +longout = _Device_Out('longout', c_int32, fields.DBF_LONG, EPICS_OK) +bi = _Device_In('bi', c_uint16, fields.DBF_CHAR, NO_CONVERT) +bo = _Device_Out('bo', c_uint16, fields.DBF_CHAR, NO_CONVERT) +mbbi = _Device_In('mbbi', c_uint16, fields.DBF_SHORT, NO_CONVERT) +mbbo = _Device_Out('mbbo', c_uint16, fields.DBF_SHORT, NO_CONVERT) class EpicsString: @@ -312,7 +312,8 @@ class WaveformBase(ProcessDeviceSupportCore): # Allow set() to be called before init_record: dtype = None - _default_ = numpy.array([]) + def _default_value(self): + return numpy.array([]) def init_record(self, record): self._nelm = record.NELM From 8ecdd88ca533bda79316b67794fdaeeac3f2e9c7 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 4 Jan 2022 10:37:08 +0000 Subject: [PATCH 092/131] Update version of manylinux image It seems numpy has stopped publishing manylinux1 builds, in favour of manylinux2014. So we must update to follow (as it's a right pain to build numpy ourselves - the CI runners can't do it at the moment) --- .github/workflows/code.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 3d90a6c2..bab99a54 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -75,8 +75,8 @@ jobs: # Disable auditwheel as it isn't compatible with setuptools_dso approach # https://github.com/mdavidsaver/setuptools_dso/issues/17 CIBW_REPAIR_WHEEL_COMMAND: "" - CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 - CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux1_x86_64 + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_ENVIRONMENT_LINUX: SETUPTOOLS_DSO_PLAT_NAME=manylinux2014_x86_64 - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 From ed1b1934cc4a5e81d9005b4e3d0468f07a63dfcb Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Tue, 4 Jan 2022 14:37:40 +0000 Subject: [PATCH 093/131] Add special support for long string records --- softioc/builder.py | 29 ++++++++++++++++++++++ softioc/device.dbd | 4 ++- softioc/device.py | 52 ++++++++++++++++++++++++++++----------- softioc/pythonSoftIoc.py | 6 +++++ tests/expected_records.db | 2 +- tests/sim_records.py | 2 +- 6 files changed, 77 insertions(+), 18 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index a7c7bd1b..ed386cc2 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -182,6 +182,34 @@ def WaveformOut(name, *value, **fields): return PythonDevice.waveform_out(name, **fields) +def _long_string(value, fields): + if 'initial_value' in fields: + assert not value, 'Can\'t specify initial value twice!' + value = (fields.pop('initial_value'),) + + if value: + # If a value is specified it should be the *only* non keyword + # argument. + value, = value + fields['initial_value'] = value + length = len(value) + else: + # No value specified, so require length and datatype to be specified. + length = fields.pop('length') + + fields['NELM'] = length + fields['FTVL'] = 'UCHAR' + + +def longStringIn(name, *value, **fields): + _long_string(value, fields) + return _in_record('long_stringin', name, **fields) + +def longStringOut(name, *value, **fields): + _long_string(value, fields) + return PythonDevice.long_stringout(name, **fields) + + # ---------------------------------------------------------------------------- # Support routines for builder @@ -227,6 +255,7 @@ def UnsetDevice(): 'stringIn', 'stringOut', 'mbbIn', 'mbbOut', 'Waveform', 'WaveformOut', + 'longStringIn', 'longStringOut', 'Action', # Other builder support functions 'LoadDatabase', diff --git a/softioc/device.dbd b/softioc/device.dbd index 6c01d590..766d508e 100644 --- a/softioc/device.dbd +++ b/softioc/device.dbd @@ -10,5 +10,7 @@ device(waveform, INST_IO, devPython_waveform, "Python") device(mbbi, INST_IO, devPython_mbbi, "Python") device(mbbo, INST_IO, devPython_mbbo, "Python") -# Waveforms as "out" devices need special treatment. +# A variety of specialised versions of waveform need special treatment. device(waveform, INST_IO, devPython_waveform_out, "PythonWfOut") +device(waveform, INST_IO, devPython_long_stringin, "PythonLongStringIn") +device(waveform, INST_IO, devPython_long_stringout, "PythonLongStringOut") diff --git a/softioc/device.py b/softioc/device.py index fdedb18c..4c390cbd 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -247,12 +247,11 @@ class EpicsString: _dbf_type_ = fields.DBF_STRING def _value_to_epics(self, value): - if isinstance(value, str): - value = value.encode() # It's a little odd: we can't simply construct a value from the byte - # string, but we can update the array in an existing value + # string, but we can update the array in an existing value. + # Value being written must be a string. result = self._ctype_() - result.value = value + result.value = value.encode() return result def _epics_to_value(self, epics): @@ -345,17 +344,6 @@ def _compare_values(self, value, other): return numpy.all(value == other) def _value_to_epics(self, value): - '''Handles strings and bytearrays as values for Waveforms. Returns the - string converted to a numpy array.''' - if isinstance(value, str): - value = value.encode(errors = 'replace') - if isinstance(value, bytes): - # Convert a string into an array of characters. This will produce - # the correct behaviour when treating a character array as a string. - # Note that the trailing null is needed to work around problems with - # some clients. Note this also exists in builder.py's _waveform(). - value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) - # Ensure we always convert incoming value into numpy array, regardless # of whether the record has been initialised or not value = numpy.require(value, dtype = self.dtype) @@ -390,5 +378,39 @@ class waveform_out(WaveformBase, ProcessDeviceSupportOut): _device_name_ = 'devPython_waveform_out' +# Convert string into numpy array of char +def encode_string(value, length = None): + value = value.encode(errors = 'replace') + # Convert a string into an array of characters. This will produce + # the correct behaviour when treating a character array as a string. + # Note that the trailing null is needed to work around problems with + # some clients. + value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) + + # Truncate value to fit + if length: + value = value[:length] + return value + + +class LongStringBase(WaveformBase): + dtype = numpy.dtype('uint8') + + def _value_to_epics(self, value): + return encode_string(value, getattr(self, '_nelm', None)) + + def _epics_to_value(self, value): + return value.decode(errors = 'replace') + + +class long_stringin(LongStringBase, ProcessDeviceSupportIn): + _record_type_ = 'waveform' + _device_name_ = 'devPython_long_stringin' + +class long_stringout(LongStringBase, ProcessDeviceSupportOut): + _record_type_ = 'waveform' + _device_name_ = 'devPython_long_stringout' + + # Ensure the .dbd file is loaded. dbLoadDatabase('device.dbd', os.path.dirname(__file__), None) diff --git a/softioc/pythonSoftIoc.py b/softioc/pythonSoftIoc.py index 84880c3e..a2f840d0 100644 --- a/softioc/pythonSoftIoc.py +++ b/softioc/pythonSoftIoc.py @@ -92,6 +92,12 @@ def __init_class__(cls): cls.waveform_out = cls.makeRecord( epicsdbbuilder.records.waveform, device.waveform_out, 'PythonWfOut') + cls.long_stringin = cls.makeRecord( + epicsdbbuilder.records.waveform, device.long_stringin, + 'PythonLongStringIn') + cls.long_stringout = cls.makeRecord( + epicsdbbuilder.records.waveform, device.long_stringout, + 'PythonLongStringOut') class makeRecord: def __init__(self, builder, record, dtyp = 'Python'): diff --git a/tests/expected_records.db b/tests/expected_records.db index c210edc1..77a6aa10 100644 --- a/tests/expected_records.db +++ b/tests/expected_records.db @@ -53,7 +53,7 @@ record(longout, "TS-DI-TEST-01:LONGOUT") record(waveform, "TS-DI-TEST-01:LONGSTRING") { field(DISP, "1") - field(DTYP, "Python") + field(DTYP, "PythonLongStringIn") field(FTVL, "UCHAR") field(INP, "@TS-DI-TEST-01:LONGSTRING") field(NELM, "256") diff --git a/tests/sim_records.py b/tests/sim_records.py index 3009e356..9de112b5 100644 --- a/tests/sim_records.py +++ b/tests/sim_records.py @@ -57,7 +57,7 @@ def update_sin_wf(value): t_waveform_out = WaveformOut('WAVEFORM_OUT', wf, on_update = on_update) t_waveform_in2 = Waveform('WAVEFORM2', length = 10) -t_longstring_in = Waveform('LONGSTRING', length = 256, datatype = numpy.uint8) +t_longstring_in = longStringIn('LONGSTRING', length = 256) def Update(): From 105d2d3807465bdfc016b788e3389840f3b35f73 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 5 Jan 2022 08:24:59 +0000 Subject: [PATCH 094/131] Make usage of quotation marks consistent --- softioc/__init__.py | 6 +++--- softioc/builder.py | 4 ++-- softioc/fields.py | 2 +- softioc/imports.py | 8 ++++---- softioc/softioc.py | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/softioc/__init__.py b/softioc/__init__.py index b9c186b0..d826e87d 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -18,10 +18,10 @@ # Need to do this before calling anything in device.py iocshRegisterCommon() for dbd in ('base.dbd', 'PVAServerRegister.dbd', 'qsrv.dbd'): - dbLoadDatabase(dbd, os.path.join(path.base_path, "dbd"), None) -dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__), None) + dbLoadDatabase(dbd, os.path.join(path.base_path, 'dbd'), None) +dbLoadDatabase('devIocStats.dbd', os.path.dirname(__file__), None) if registerRecordDeviceDriver(pdbbase): raise RuntimeError('Error registering') -__all__ = ["__version__"] +__all__ = ['__version__'] diff --git a/softioc/builder.py b/softioc/builder.py index ed386cc2..d8326d36 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -64,7 +64,7 @@ def longOut(name, DRVL=None, DRVH=None, EGU=None, **fields): 'EI', 'NI', 'TE', 'EL', 'TV', 'TT', 'FT', 'FF'] # 8-15 # All the severity strings supported by SV -_severityStrings = ["NO_ALARM", "MINOR", "MAJOR", "INVALID"] +_severityStrings = ['NO_ALARM', 'MINOR', 'MAJOR', 'INVALID'] # Converts a list of (option [,severity]) values or tuples into field settings # suitable for mbbi and mbbo records. @@ -78,7 +78,7 @@ def process_value(prefix, value, option, severity=None): severity = _severityStrings[severity] fields[prefix + 'SV'] = severity # zip() silently ignores extra values in options, so explicitly check length - assert len(options) <= 16, "May not specify more than 16 enum values" + assert len(options) <= 16, 'May not specify more than 16 enum values' for prefix, (value, option) in zip(_mbbPrefixes, enumerate(options)): if isinstance(option, tuple): # The option is tuple consisting of the option name and an optional diff --git a/softioc/fields.py b/softioc/fields.py index 2cdef68c..d72de995 100644 --- a/softioc/fields.py +++ b/softioc/fields.py @@ -74,7 +74,7 @@ def __init__(self, record_type, fields): self.fields = get_field_offsets(record_type) missing = set(fields) - set(self.fields) assert not missing, \ - "Fields not supported by %s: %s" % (record_type, sorted(missing)) + 'Fields not supported by %s: %s' % (record_type, sorted(missing)) def __call__(self, record): '''Converts a raw pointer to a record structure into a _Record object diff --git a/softioc/imports.py b/softioc/imports.py index eb525fb8..699f2302 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -12,19 +12,19 @@ # These are in the extension def get_DBF_values(): - """Return {DBF_name: DBF_int_value} mapping""" + '''Return {DBF_name: DBF_int_value} mapping''' return _extension.get_DBF_values() def get_field_offsets(record_type): - """Return {field_name: (offset, size, field_type)}""" + '''Return {field_name: (offset, size, field_type)}''' return _extension.get_field_offsets(record_type) def db_put_field(name, dbr_type, pbuffer, length): - """Put field where pbuffer is void* pointer. Returns RC""" + '''Put field where pbuffer is void* pointer. Returns RC''' return _extension.db_put_field(name, dbr_type, pbuffer, length) def install_pv_logging(acf_file): - """Install pv logging""" + '''Install pv logging''' _extension.install_pv_logging(acf_file) def expect_success(status, function, args): diff --git a/softioc/softioc.py b/softioc/softioc.py index c9214142..017db501 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -18,7 +18,7 @@ def epicsAtPyExit(): def iocInit(dispatcher=None): - """This must be called exactly once after loading all EPICS database files. + '''This must be called exactly once after loading all EPICS database files. After this point the EPICS IOC is running and serving PVs. Args: @@ -28,7 +28,7 @@ def iocInit(dispatcher=None): See Also: `softioc.asyncio_dispatcher` is a dispatcher for `asyncio` applications - """ + ''' if dispatcher is None: # Fallback to cothread import cothread @@ -263,7 +263,7 @@ def _add_records_from_file(dirname, file, substitutions): # 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 = [], "" + lines, include_subs = [], '' for line in f.readlines(): line = line.rstrip() if line.startswith('substitute'): @@ -274,7 +274,7 @@ def _add_records_from_file(dirname, file, substitutions): # include "iocQueue.db" subs = substitutions if substitutions and include_subs: - subs = substitutions + ", " + include_subs + subs = substitutions + ', ' + include_subs else: subs = substitutions + include_subs _add_records_from_file(dirname, line.split('"')[1], subs) From 0df634e6707ca7eefdb9a50533413fcf0cbaf79c Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 5 Jan 2022 11:15:02 +0000 Subject: [PATCH 095/131] Fixes to string handling Handle null termination properly and ensure we have a proper copy of the buffer. --- softioc/device.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index 4c390cbd..035af0e9 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -52,7 +52,7 @@ def _compare_values(self, value1, value2): # This method is called during Out record processing to return the - # underlying value in Python format. + # underlying value in EPICS format. def _read_value(self, record): return record.read_val() @@ -240,6 +240,13 @@ def _Device_Out(*args, **kargs): mbbo = _Device_Out('mbbo', c_uint16, fields.DBF_SHORT, NO_CONVERT) +def _string_at(value, count): + # Need string_at() twice to ensure string is size limited *and* null + # terminated. + value = ctypes.string_at(ctypes.string_at(value, count)) + # Convert bytes to unicode string + return value.decode(errors = 'replace') + class EpicsString: _fields_ = ['UDF', 'VAL'] _epics_rc_ = EPICS_OK @@ -249,13 +256,20 @@ class EpicsString: def _value_to_epics(self, value): # It's a little odd: we can't simply construct a value from the byte # string, but we can update the array in an existing value. - # Value being written must be a string. + # Value being written must be a string, and will be automatically null + # terminated where possible. result = self._ctype_() result.value = value.encode() return result def _epics_to_value(self, epics): - return epics.value.decode(errors = 'replace') + return _string_at(epics, 40) + + def _read_value(self, record): + # For strings we need to take a copy of the value read + result = self._ctype_() + result.value = record.read_val().value + return result class stringin(EpicsString, ProcessDeviceSupportIn): @@ -400,7 +414,7 @@ def _value_to_epics(self, value): return encode_string(value, getattr(self, '_nelm', None)) def _epics_to_value(self, value): - return value.decode(errors = 'replace') + return _string_at(value.ctypes, len(value)) class long_stringin(LongStringBase, ProcessDeviceSupportIn): From ed3a2c51377b1fc20fb0b223253c6f0092151fe0 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 5 Jan 2022 12:47:19 +0000 Subject: [PATCH 096/131] Revisit long string initialisation Also a bit of tidying of device.py --- softioc/builder.py | 28 +++++++++++----------------- softioc/device.py | 45 ++++++++++++++++++++------------------------- 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index d8326d36..f873832e 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -182,31 +182,25 @@ def WaveformOut(name, *value, **fields): return PythonDevice.waveform_out(name, **fields) -def _long_string(value, fields): - if 'initial_value' in fields: - assert not value, 'Can\'t specify initial value twice!' - value = (fields.pop('initial_value'),) - - if value: - # If a value is specified it should be the *only* non keyword - # argument. - value, = value - fields['initial_value'] = value - length = len(value) - else: - # No value specified, so require length and datatype to be specified. +def _long_string(fields): + if 'length' in fields: length = fields.pop('length') + elif 'initial_value' in fields: + length = len(fields['initial_value'].encode(errors = 'replace')) + 1 + else: + # Default length of 256 + length = 256 fields['NELM'] = length fields['FTVL'] = 'UCHAR' -def longStringIn(name, *value, **fields): - _long_string(value, fields) +def longStringIn(name, **fields): + _long_string(fields) return _in_record('long_stringin', name, **fields) -def longStringOut(name, *value, **fields): - _long_string(value, fields) +def longStringOut(name, **fields): + _long_string(fields) return PythonDevice.long_stringout(name, **fields) diff --git a/softioc/device.py b/softioc/device.py index 035af0e9..ac4a2250 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -323,7 +323,7 @@ class WaveformBase(ProcessDeviceSupportCore): # NORD Currently reported length of array (0 <= NORD <= NELM) _fields_ = ['UDF', 'FTVL', 'BPTR', 'NELM', 'NORD'] # Allow set() to be called before init_record: - dtype = None + _dtype = None def _default_value(self): return numpy.array([]) @@ -331,19 +331,19 @@ def _default_value(self): def init_record(self, record): self._nelm = record.NELM self._dbf_type_ = record.FTVL - self.dtype = fields.DbfCodeToNumpy[record.FTVL] + self._dtype = fields.DbfCodeToNumpy[record.FTVL] return self.__super.init_record(record) def _read_value(self, record): nord = record.NORD - result = numpy.empty(nord, dtype = self.dtype) + result = numpy.empty(nord, dtype = self._dtype) memmove( result.ctypes.data_as(c_void_p), record.BPTR, - self.dtype.itemsize * nord) + self._dtype.itemsize * nord) return result def _write_value(self, record, value): - value = numpy.require(value, dtype = self.dtype) + value = numpy.require(value, dtype = self._dtype) nelm = record.NELM nord = len(value) @@ -351,7 +351,7 @@ def _write_value(self, record, value): nord = nelm memmove( record.BPTR, value.ctypes.data_as(c_void_p), - self.dtype.itemsize * nord) + self._dtype.itemsize * nord) record.NORD = nord def _compare_values(self, value, other): @@ -360,7 +360,7 @@ def _compare_values(self, value, other): def _value_to_epics(self, value): # Ensure we always convert incoming value into numpy array, regardless # of whether the record has been initialised or not - value = numpy.require(value, dtype = self.dtype) + value = numpy.require(value, dtype = self._dtype) if value.shape == (): value.shape = (1,) assert value.ndim == 1, 'Can\'t write multidimensional arrays' @@ -379,7 +379,7 @@ def _epics_to_value(self, value): return value def _value_to_dbr(self, value): - value = numpy.require(value, dtype = self.dtype) + value = numpy.require(value, dtype = self._dtype) return self._dbf_type_, len(value), value.ctypes.data, value @@ -392,26 +392,21 @@ class waveform_out(WaveformBase, ProcessDeviceSupportOut): _device_name_ = 'devPython_waveform_out' -# Convert string into numpy array of char -def encode_string(value, length = None): - value = value.encode(errors = 'replace') - # Convert a string into an array of characters. This will produce - # the correct behaviour when treating a character array as a string. - # Note that the trailing null is needed to work around problems with - # some clients. - value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) - - # Truncate value to fit - if length: - value = value[:length] - return value - - class LongStringBase(WaveformBase): - dtype = numpy.dtype('uint8') + _dtype = numpy.dtype('uint8') def _value_to_epics(self, value): - return encode_string(value, getattr(self, '_nelm', None)) + value = value.encode(errors = 'replace') + # Convert a string into an array of characters. This will produce + # the correct behaviour when treating a character array as a string. + # Note that the trailing null is needed to work around problems with + # some clients. + value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) + + # Truncate value to fit + if hasattr(self, '_nelm'): + value = value[:self._nelm] + return value def _epics_to_value(self, value): return _string_at(value.ctypes, len(value)) From 2b765930761acd16ba6dc4736192113c861f9f40 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 5 Jan 2022 13:38:52 +0000 Subject: [PATCH 097/131] Add tests for Issue #43 These were developed on the "sort_settings_values" branch but were never committed there. Still valid test cases, although they require a little refactoring to fit with the changes made on this branch. --- tests/test_records.py | 162 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/tests/test_records.py b/tests/test_records.py index f597c05e..fc2f8a42 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -204,6 +204,20 @@ def record_values_names(fixture_value): ), numpy.ndarray, ), + ( + "wIn_unicode", + builder.WaveformIn, + "%a€b", + numpy.array([37, 97, 226, 130, 172, 98, 0], dtype=numpy.uint8), + numpy.ndarray, + ), + ( + "wOut_unicode", + builder.WaveformOut, + "%a€b", + numpy.array([37, 97, 226, 130, 172, 98, 0], dtype=numpy.uint8), + numpy.ndarray, + ), ] @@ -353,7 +367,7 @@ def run_ioc(record_configurations: list, conn, set_enum, get_enum): records: List[RecordWrapper] = [] - # Loop over given list and create the records + # Create records from the given list for configuration in record_configurations: kwarg = {} @@ -840,3 +854,149 @@ def test_value_none_rejected_set_after_init(self, record_funcs_reject_none): finally: process.terminate() process.join(timeout=3) + + +class TestValidate: + """Tests related to the validate callback""" + + @pytest.fixture( + params=[ + (builder.aOut, 7.89, 0), + (builder.boolOut, 1, 0), + (builder.longOut, 7, 0), + (builder.stringOut, "HI", ""), + (builder.mbbOut, 2, 0), + (builder.WaveformOut, [10, 11, 12], []), + ] + ) + def out_records(self, request): + """The list of Out records and an associated value to set """ + return request.param + + def validate_always_pass(self, record, new_val): + """Validation method that always allows changes""" + return True + + def validate_always_fail(self, record, new_val): + """Validation method that always rejects changes""" + return False + + def validate_ioc_test_func(self, record_func, queue, validate_pass: bool): + """Start the IOC with the specified validate method""" + + builder.SetDeviceName(DEVICE_NAME) + + kwarg = {} + if record_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + kwarg.update( + { + "validate": self.validate_always_pass + if validate_pass + else self.validate_always_fail + } + ) + + record_func("VALIDATE-RECORD", **kwarg) + + dispatcher = asyncio_dispatcher.AsyncioDispatcher() + builder.LoadDatabase() + softioc.iocInit(dispatcher) + + queue.put("IOC ready") + + # Keep process alive while main thread works. + # This is most applicable to CAGET tests. + asyncio.run_coroutine_threadsafe( + asyncio.sleep(TIMEOUT), dispatcher.loop + ).result() + + + @requires_cothread + def test_validate_allows_updates(self, out_records): + """Test that record values are updated correctly when validate + method allows it """ + + creation_func, value, _ = out_records + + queue = multiprocessing.Queue() + process = multiprocessing.Process( + target=self.validate_ioc_test_func, + args=(creation_func, queue, True), + ) + + process.start() + + try: + queue.get(timeout=5) # Get the expected IOc initialised message + + from cothread.catools import caget, caput, _channel_cache + + # See other places in this file for why we call it + _channel_cache.purge() + + put_ret = caput( + DEVICE_NAME + ":" + "VALIDATE-RECORD", + value, + wait=True, + ) + assert put_ret.ok, "caput did not succeed" + + ret_val = caget( + DEVICE_NAME + ":" + "VALIDATE-RECORD", + timeout=3 + ) + + if creation_func in [builder.WaveformOut, builder.WaveformIn]: + assert numpy.array_equal(ret_val, value) + else: + assert ret_val == value + + finally: + process.terminate() + process.join(timeout=3) + + @requires_cothread + def test_validate_blocks_updates(self, out_records): + """Test that record values are not updated when validate method + always blocks updates""" + + creation_func, value, default = out_records + + queue = multiprocessing.Queue() + process = multiprocessing.Process( + target=self.validate_ioc_test_func, + args=(creation_func, queue, False), + ) + + process.start() + + try: + queue.get(timeout=5) # Get the expected IOc initialised message + + from cothread.catools import caget, caput, _channel_cache + + # See other places in this file for why we call it + _channel_cache.purge() + + put_ret = caput( + DEVICE_NAME + ":" + "VALIDATE-RECORD", + value, + wait=True, + ) + assert put_ret.ok, "caput did not succeed" + + ret_val = caget( + DEVICE_NAME + ":" + "VALIDATE-RECORD", + timeout=3 + ) + + if creation_func in [builder.WaveformOut, builder.WaveformIn]: + assert numpy.array_equal(ret_val, default) + else: + assert ret_val == default + + finally: + process.terminate() + process.join(timeout=3) From 83bacad77cc967c5d02e10a16d07a1d7a8db610d Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 5 Jan 2022 15:19:19 +0000 Subject: [PATCH 098/131] Add tests for Action, longStringIn, longStringOut Also neaten names for Validate tests, and tweak None tests to add extra exception --- tests/test_records.py | 74 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index fc2f8a42..d14a2d17 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -58,12 +58,15 @@ def record_funcs_names(fixture_value): builder.longOut, builder.boolIn, builder.boolOut, + builder.Action, builder.stringIn, builder.stringOut, builder.mbbIn, builder.mbbOut, builder.WaveformIn, builder.WaveformOut, + builder.longStringIn, + builder.longStringOut, ], ids=record_funcs_names, ) @@ -112,6 +115,9 @@ def record_values_names(fixture_value): ("boolOut_true", builder.boolOut, True, 1, int), ("boolIn_false", builder.boolIn, False, 0, int), ("boolOut_false", builder.boolOut, False, 0, int), + ("Action_int", builder.Action, 1, 1, int), + ("Action_true", builder.Action, True, 1, int), + ("Action_false", builder.Action, False, 0, int), ("mbbIn_int", builder.mbbIn, 1, 1, int), ("mbbOut_int", builder.mbbOut, 1, 1, int), ("strIn_abc", builder.stringIn, "abc", "abc", str), @@ -218,6 +224,48 @@ def record_values_names(fixture_value): numpy.array([37, 97, 226, 130, 172, 98, 0], dtype=numpy.uint8), numpy.ndarray, ), + ( + "longStringIn_str", + builder.longStringIn, + "ABC", + "ABC", + str, + ), + ( + "longStringOut_str", + builder.longStringOut, + "ABC", + "ABC", + str, + ), + ( + "longStringIn_bytes", + builder.longStringIn, + b"HELLO\0WORLD", + "HELLO", + str, + ), + ( + "longStringOut_bytes", + builder.longStringOut, + b"HELLO\0WORLD", + "HELLO", + str, + ), + ( + "longStringIn_unicode", + builder.longStringIn, + "%a€b", + "%a€b", + str + ), + ( + "longStringOut_unicode", + builder.longStringOut, + "%a€b", + "%a€b", + str + ), ] @@ -719,12 +767,15 @@ def test_value_post_init_caput(self): ("default_longIn", builder.longIn, None, 0, int), ("default_boolOut", builder.boolOut, None, 0, int), ("default_boolIn", builder.boolIn, None, 0, int), + ("default_Action", builder.Action, None, 0, int), ("default_stringOut", builder.stringOut, None, "", str), ("default_stringIn", builder.stringIn, None, "", str), ("default_mbbOut", builder.mbbOut, None, 0, int), ("default_mbbIn", builder.mbbIn, None, 0, int), ("default_wOut", builder.WaveformOut, None, numpy.empty(0), numpy.ndarray), ("default_wIn", builder.WaveformIn, None, numpy.empty(0), numpy.ndarray), + ("default_longStringOut", builder.longStringOut, None, "", str), + ("default_longStringIn", builder.longStringIn, None, "", str), ] @@ -740,12 +791,15 @@ class TestDefaultValue: (builder.longIn, 0, int), (builder.boolOut, None, type(None)), (builder.boolIn, 0, int), + (builder.Action, None, type(None)), (builder.stringOut, None, type(None)), (builder.stringIn, "", str), (builder.mbbOut, None, type(None)), (builder.mbbIn, 0, int), (builder.WaveformOut, None, type(None)), (builder.WaveformIn, numpy.empty(0), numpy.ndarray), + (builder.longStringOut, "", str), + (builder.longStringIn, "", str), ], ) def test_value_default_pre_init( @@ -786,6 +840,9 @@ def test_value_default_post_init_caget(self): class TestNoneValue: """Various tests regarding record value of None""" + # We expect using None as a value will trigger one of these exceptions + expected_exceptions = (ValueError, TypeError, AttributeError) + def test_value_none_rejected_initial_value( self, clear_records, record_funcs_reject_none ): @@ -798,7 +855,7 @@ def test_value_none_rejected_initial_value( ]: kwarg = {"length": 50} # Required when no value on creation - with pytest.raises((ValueError, TypeError)): + with pytest.raises(self.expected_exceptions): record_funcs_reject_none("SOME-NAME", initial_value=None, **kwarg) def test_value_none_rejected_set_before_init( @@ -813,7 +870,7 @@ def test_value_none_rejected_set_before_init( ]: kwarg = {"length": 50} # Required when no value on creation - with pytest.raises((ValueError, TypeError)): + with pytest.raises(self.expected_exceptions): record = record_funcs_reject_none("SOME-NAME", **kwarg) record.set(None) @@ -850,12 +907,14 @@ def test_value_none_rejected_set_after_init(self, record_funcs_reject_none): try: exception = queue.get(timeout=5) - assert isinstance(exception, (ValueError, TypeError)) + assert isinstance(exception, self.expected_exceptions) finally: process.terminate() process.join(timeout=3) - +def fixture_names(params): + """Provide nice names for the out_records fixture in TestValidate class""" + return params[0].__name__ class TestValidate: """Tests related to the validate callback""" @@ -863,11 +922,14 @@ class TestValidate: params=[ (builder.aOut, 7.89, 0), (builder.boolOut, 1, 0), + (builder.Action, 1, 0), (builder.longOut, 7, 0), (builder.stringOut, "HI", ""), (builder.mbbOut, 2, 0), (builder.WaveformOut, [10, 11, 12], []), - ] + (builder.longStringOut, "A LONGER HELLO", ""), + ], + ids=fixture_names ) def out_records(self, request): """The list of Out records and an associated value to set """ @@ -973,7 +1035,7 @@ def test_validate_blocks_updates(self, out_records): process.start() try: - queue.get(timeout=5) # Get the expected IOc initialised message + queue.get(timeout=5) # Get the expected IOC initialised message from cothread.catools import caget, caput, _channel_cache From 9b26f91f3036aca6fc2b5b3f5dc0ea2520f18517 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 5 Jan 2022 16:33:24 +0000 Subject: [PATCH 099/131] Add very long string tests --- tests/test_records.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_records.py b/tests/test_records.py index d14a2d17..0227a5d3 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -27,6 +27,10 @@ DEVICE_NAME = "SOFT-IOC-TESTS" TIMEOUT = 5 # Seconds +VERY_LONG_STRING = "This is a fairly long string, the kind that someone " \ + "might think to put into a record that can theoretically hold a huge " \ + "string and so lets test it and prove that shall we?" + def _clear_records(): # Remove any records created at epicsdbbuilder layer @@ -238,6 +242,20 @@ def record_values_names(fixture_value): "ABC", str, ), + ( + "longStringIn_long_str", + builder.longStringIn, + VERY_LONG_STRING, + VERY_LONG_STRING, + str, + ), + ( + "longStringOut_long_str", + builder.longStringOut, + VERY_LONG_STRING, + VERY_LONG_STRING, + str, + ), ( "longStringIn_bytes", builder.longStringIn, From 9fccffdc26a06ebff14574c07c77927edb832c48 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 6 Jan 2022 12:11:59 +0000 Subject: [PATCH 100/131] Refactor the Validate tests The tests for LongStringOut records are broken. Validate is never called - caput doesn't even work. --- tests/test_records.py | 73 ++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 49 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 0227a5d3..d47e1729 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -930,7 +930,7 @@ def test_value_none_rejected_set_after_init(self, record_funcs_reject_none): process.terminate() process.join(timeout=3) -def fixture_names(params): +def validate_fixture_names(params): """Provide nice names for the out_records fixture in TestValidate class""" return params[0].__name__ class TestValidate: @@ -947,7 +947,7 @@ class TestValidate: (builder.WaveformOut, [10, 11, 12], []), (builder.longStringOut, "A LONGER HELLO", ""), ], - ids=fixture_names + ids=validate_fixture_names ) def out_records(self, request): """The list of Out records and an associated value to set """ @@ -992,18 +992,17 @@ def validate_ioc_test_func(self, record_func, queue, validate_pass: bool): asyncio.sleep(TIMEOUT), dispatcher.loop ).result() - - @requires_cothread - def test_validate_allows_updates(self, out_records): - """Test that record values are updated correctly when validate - method allows it """ - - creation_func, value, _ = out_records + def validate_test_runner( + self, + creation_func, + new_value, + expected_value, + validate_pass: bool): queue = multiprocessing.Queue() process = multiprocessing.Process( target=self.validate_ioc_test_func, - args=(creation_func, queue, True), + args=(creation_func, queue, validate_pass), ) process.start() @@ -1018,7 +1017,7 @@ def test_validate_allows_updates(self, out_records): put_ret = caput( DEVICE_NAME + ":" + "VALIDATE-RECORD", - value, + new_value, wait=True, ) assert put_ret.ok, "caput did not succeed" @@ -1029,54 +1028,30 @@ def test_validate_allows_updates(self, out_records): ) if creation_func in [builder.WaveformOut, builder.WaveformIn]: - assert numpy.array_equal(ret_val, value) + assert numpy.array_equal(ret_val, expected_value) else: - assert ret_val == value + assert ret_val == expected_value finally: process.terminate() process.join(timeout=3) - @requires_cothread - def test_validate_blocks_updates(self, out_records): - """Test that record values are not updated when validate method - always blocks updates""" - creation_func, value, default = out_records - - queue = multiprocessing.Queue() - process = multiprocessing.Process( - target=self.validate_ioc_test_func, - args=(creation_func, queue, False), - ) - - process.start() - - try: - queue.get(timeout=5) # Get the expected IOC initialised message + @requires_cothread + def test_validate_allows_updates(self, out_records): + """Test that record values are updated correctly when validate + method allows it """ - from cothread.catools import caget, caput, _channel_cache + creation_func, value, _ = out_records - # See other places in this file for why we call it - _channel_cache.purge() + self.validate_test_runner(creation_func, value, value, True) - put_ret = caput( - DEVICE_NAME + ":" + "VALIDATE-RECORD", - value, - wait=True, - ) - assert put_ret.ok, "caput did not succeed" - ret_val = caget( - DEVICE_NAME + ":" + "VALIDATE-RECORD", - timeout=3 - ) + @requires_cothread + def test_validate_blocks_updates(self, out_records): + """Test that record values are not updated when validate method + always blocks updates""" - if creation_func in [builder.WaveformOut, builder.WaveformIn]: - assert numpy.array_equal(ret_val, default) - else: - assert ret_val == default + creation_func, value, default = out_records - finally: - process.terminate() - process.join(timeout=3) + self.validate_test_runner(creation_func, value, default, False) From 9f57ed1080d21746480329cf06f5dbea54d1a909 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 6 Jan 2022 12:39:20 +0000 Subject: [PATCH 101/131] Improve error message during assertion failure --- tests/test_records.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index d47e1729..73a40321 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -388,12 +388,12 @@ def record_value_asserts( truncated_value = actual_value[: expected_value.size] assert numpy.array_equal( truncated_value, expected_value - ), "Arrays not equal: {} {}".format(actual_value, expected_value) + ), f"Arrays not equal: {actual_value} {expected_value}" assert type(actual_value) == expected_type else: assert actual_value == expected_value assert type(actual_value) == expected_type - except Exception as e: + except AssertionError as e: msg = ( "Failed with parameters: " + str(creation_func) @@ -404,7 +404,11 @@ def record_value_asserts( + ", " + str(expected_type) ) - logging.error(msg, exc_info=e) + # Add the parameters into the exception message. Seems to be the + # cleanest place to add it, as doing a print() or a logging.error() + # both cause the message to appear in a separate section of the pytest + # results, which can be difficult to spot + e.args += (msg,) raise e From 63d26d9a19aeb2a8e2c84297e001299a96f8fd1f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Mon, 10 Jan 2022 14:44:46 +0000 Subject: [PATCH 102/131] Remove tests that are now invalid Discussed with Michael. Probably will be more tests to delete, pending further discussion. --- tests/test_records.py | 66 +++++++++---------------------------------- 1 file changed, 14 insertions(+), 52 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 73a40321..7ba0e1c8 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -49,8 +49,8 @@ def clear_records(): _clear_records() -def record_funcs_names(fixture_value): - """Provide a nice name for the record_funcs fixture""" +def record_func_names(fixture_value): + """Provide a nice name for the record_func fixture""" return fixture_value.__name__ @@ -72,21 +72,12 @@ def record_funcs_names(fixture_value): builder.longStringIn, builder.longStringOut, ], - ids=record_funcs_names, + ids=record_func_names, ) -def record_funcs(request): +def record_func(request): """The list of record creation functions""" return request.param - -@pytest.fixture -def record_funcs_reject_none(record_funcs): - """The list of record creation functions that reject 'None' as a value""" - if record_funcs in [builder.stringIn, builder.stringOut]: - pytest.skip(msg="None is valid value for string records") - return record_funcs - - def record_values_names(fixture_value): """Provide a nice name for the tests in the record_values fixture""" return ( @@ -111,8 +102,6 @@ def record_values_names(fixture_value): ("aOut_nan", builder.aOut, nan, nan, float), ("longIn_int", builder.longIn, 5, 5, int), ("longOut_int", builder.longOut, 5, 5, int), - ("longIn_float", builder.longIn, 9.9, 9, int), - ("longOut_float", builder.longOut, 9.9, 9, int), ("boolIn_int", builder.boolIn, 1, 1, int), ("boolOut_int", builder.boolOut, 1, 1, int), ("boolIn_true", builder.boolIn, True, 1, int), @@ -128,32 +117,8 @@ def record_values_names(fixture_value): ("strOut_abc", builder.stringOut, "abc", "abc", str), ("strIn_empty", builder.stringIn, "", "", str), ("strOut_empty", builder.stringOut, "", "", str), - ("strIn_bytes", builder.stringIn, b"abc", "abc", str), - ("strOut_bytes", builder.stringOut, b"abc", "abc", str), - ("strin_utf8", builder.stringIn, "a€b", "a€b", str), # Valid UTF-8 - ("strOut_utf8", builder.stringOut, "a€b", "a€b", str), # Valid UTF-8 - ("strIn_badutf8", builder.stringIn, b"a\xcfb", "a�b", str), # Invalid UTF-8 - ( - "strOut_badutf8", - builder.stringOut, - b"a\xcfb", # Invalid UTF-8 - "a�b", - str, - ), - ( - "strIn_byteutf8", - builder.stringIn, - b"a\xe2\x82\xacb", # Valid UTF-8 - "a€b", - str, - ), - ( - "strOut_byteutf8", - builder.stringOut, - b"a\xe2\x82\xacb", # Valid UTF-8 - "a€b", - str, - ), + ("strin_utf8", builder.stringIn, "%a€b", "%a€b", str), # Valid UTF-8 + ("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str), # Valid UTF-8 ( "strIn_longstr", builder.stringIn, @@ -866,34 +831,31 @@ class TestNoneValue: expected_exceptions = (ValueError, TypeError, AttributeError) def test_value_none_rejected_initial_value( - self, clear_records, record_funcs_reject_none + self, clear_records, record_func ): """Test setting \"None\" as the initial_value raises an exception""" kwarg = {} - if record_funcs_reject_none in [ + if record_func in [ builder.WaveformIn, builder.WaveformOut, ]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises(self.expected_exceptions): - record_funcs_reject_none("SOME-NAME", initial_value=None, **kwarg) + record_func("SOME-NAME", initial_value=None, **kwarg) def test_value_none_rejected_set_before_init( - self, clear_records, record_funcs_reject_none + self, clear_records, record_func ): """Test that setting \"None\" using .set() raises an exception""" kwarg = {} - if record_funcs_reject_none in [ - builder.WaveformIn, - builder.WaveformOut, - ]: + if record_func in [builder.WaveformIn, builder.WaveformOut]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises(self.expected_exceptions): - record = record_funcs_reject_none("SOME-NAME", **kwarg) + record = record_func("SOME-NAME", **kwarg) record.set(None) def none_value_test_func(self, record_func, queue): @@ -915,13 +877,13 @@ def none_value_test_func(self, record_func, queue): queue.put(Exception("FAIL:Test did not raise exception during .set()")) @requires_cothread - def test_value_none_rejected_set_after_init(self, record_funcs_reject_none): + def test_value_none_rejected_set_after_init(self, record_func): """Test that setting \"None\" using .set() after IOC init raises an exception""" queue = multiprocessing.Queue() process = multiprocessing.Process( target=self.none_value_test_func, - args=(record_funcs_reject_none, queue), + args=(record_func, queue), ) process.start() From c8dfbd05fa8e826041505e71200d16631232c34a Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 11 Jan 2022 11:21:40 +0000 Subject: [PATCH 103/131] Remove invalid tests and apply test fixes Changes made after discussion with Michael regarding what is and is not a valid test scenario, with Michael's latest changes to record value handling. Note there are pending changes to the record code itself. This is just test code changes. --- tests/test_records.py | 118 ++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 67 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 7ba0e1c8..d116dcbe 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -119,20 +119,6 @@ def record_values_names(fixture_value): ("strOut_empty", builder.stringOut, "", "", str), ("strin_utf8", builder.stringIn, "%a€b", "%a€b", str), # Valid UTF-8 ("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str), # Valid UTF-8 - ( - "strIn_longstr", - builder.stringIn, - "this string is much longer than 40 characters", - "this string is much longer than 40 char", - str, - ), - ( - "strOut_longstr", - builder.stringOut, - "this string is much longer than 40 characters", - "this string is much longer than 40 char", - str, - ), ( "wIn_list", builder.WaveformIn, @@ -148,17 +134,31 @@ def record_values_names(fixture_value): numpy.ndarray, ), ( - "wIn_str", + "wIn_int", builder.WaveformIn, - "ABC", - numpy.array([65, 66, 67, 0], dtype=numpy.uint8), + 567, + numpy.array([567], dtype=numpy.int32), numpy.ndarray, ), ( - "wOut_str", + "wOut_int", builder.WaveformOut, - "ABC", - numpy.array([65, 66, 67, 0], dtype=numpy.uint8), + 567, + numpy.array([567], dtype=numpy.int32), + numpy.ndarray, + ), + ( + "wIn_float", + builder.WaveformIn, + 12.345, + numpy.array([12.345], dtype=numpy.float32), + numpy.ndarray, + ), + ( + "wOut_float", + builder.WaveformOut, + 12.345, + numpy.array([12.345], dtype=numpy.float32), numpy.ndarray, ), ( @@ -179,20 +179,6 @@ def record_values_names(fixture_value): ), numpy.ndarray, ), - ( - "wIn_unicode", - builder.WaveformIn, - "%a€b", - numpy.array([37, 97, 226, 130, 172, 98, 0], dtype=numpy.uint8), - numpy.ndarray, - ), - ( - "wOut_unicode", - builder.WaveformOut, - "%a€b", - numpy.array([37, 97, 226, 130, 172, 98, 0], dtype=numpy.uint8), - numpy.ndarray, - ), ( "longStringIn_str", builder.longStringIn, @@ -221,20 +207,6 @@ def record_values_names(fixture_value): VERY_LONG_STRING, str, ), - ( - "longStringIn_bytes", - builder.longStringIn, - b"HELLO\0WORLD", - "HELLO", - str, - ), - ( - "longStringOut_bytes", - builder.longStringOut, - b"HELLO\0WORLD", - "HELLO", - str, - ), ( "longStringIn_unicode", builder.longStringIn, @@ -507,14 +479,10 @@ def run_test_function( ) = configuration # Infer some required keywords from parameters - put_kwargs = {} - get_kwargs = {} - if creation_func in [builder.WaveformOut, builder.WaveformIn]: + kwargs = {} + if creation_func in [builder.longStringIn, builder.longStringOut]: from cothread.dbr import DBR_CHAR_STR - - if type(initial_value) in [str, bytes]: - put_kwargs.update({"datatype": DBR_CHAR_STR}) - get_kwargs.update({"count": len(initial_value) + 1}) + kwargs.update({"datatype": DBR_CHAR_STR}) if set_enum == SetValueEnum.CAPUT: if get_enum == GetValueEnum.GET: @@ -526,7 +494,7 @@ def run_test_function( DEVICE_NAME + ":" + record_name, initial_value, wait=True, - **put_kwargs + **kwargs ) if get_enum == GetValueEnum.GET: @@ -546,7 +514,7 @@ def run_test_function( rec_val = caget( DEVICE_NAME + ":" + record_name, timeout=TIMEOUT, - **get_kwargs + **kwargs, ) # '+' operator used to convert cothread's types into Python # native types e.g. "+ca_int" -> int @@ -830,32 +798,41 @@ class TestNoneValue: # We expect using None as a value will trigger one of these exceptions expected_exceptions = (ValueError, TypeError, AttributeError) + + @pytest.fixture + def record_func_reject_none(self, record_func): + """Parameterized fixture for all records that reject None""" + if record_func in [builder.WaveformIn, builder.WaveformOut]: + pytest.skip("None is accepted by Waveform records, as numpy " + "treats it as NaN") + return record_func + def test_value_none_rejected_initial_value( - self, clear_records, record_func + self, clear_records, record_func_reject_none ): """Test setting \"None\" as the initial_value raises an exception""" kwarg = {} - if record_func in [ + if record_func_reject_none in [ builder.WaveformIn, builder.WaveformOut, ]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises(self.expected_exceptions): - record_func("SOME-NAME", initial_value=None, **kwarg) + record_func_reject_none("SOME-NAME", initial_value=None, **kwarg) def test_value_none_rejected_set_before_init( - self, clear_records, record_func + self, clear_records, record_func_reject_none ): """Test that setting \"None\" using .set() raises an exception""" kwarg = {} - if record_func in [builder.WaveformIn, builder.WaveformOut]: + if record_func_reject_none in [builder.WaveformIn, builder.WaveformOut]: kwarg = {"length": 50} # Required when no value on creation with pytest.raises(self.expected_exceptions): - record = record_func("SOME-NAME", **kwarg) + record = record_func_reject_none("SOME-NAME", **kwarg) record.set(None) def none_value_test_func(self, record_func, queue): @@ -877,13 +854,13 @@ def none_value_test_func(self, record_func, queue): queue.put(Exception("FAIL:Test did not raise exception during .set()")) @requires_cothread - def test_value_none_rejected_set_after_init(self, record_func): + def test_value_none_rejected_set_after_init(self, record_func_reject_none): """Test that setting \"None\" using .set() after IOC init raises an exception""" queue = multiprocessing.Queue() process = multiprocessing.Process( target=self.none_value_test_func, - args=(record_func, queue), + args=(record_func_reject_none, queue), ) process.start() @@ -974,23 +951,30 @@ def validate_test_runner( process.start() try: - queue.get(timeout=5) # Get the expected IOc initialised message + queue.get(timeout=5) # Get the expected IOC initialised message from cothread.catools import caget, caput, _channel_cache # See other places in this file for why we call it _channel_cache.purge() + kwargs = {} + if creation_func in [builder.longStringIn, builder.longStringOut]: + from cothread.dbr import DBR_CHAR_STR + kwargs.update({"datatype": DBR_CHAR_STR}) + put_ret = caput( DEVICE_NAME + ":" + "VALIDATE-RECORD", new_value, wait=True, + **kwargs, ) assert put_ret.ok, "caput did not succeed" ret_val = caget( DEVICE_NAME + ":" + "VALIDATE-RECORD", - timeout=3 + timeout=3, + **kwargs ) if creation_func in [builder.WaveformOut, builder.WaveformIn]: From bdab3d85d438b40d0d838b9c87a4983c23a20716 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Tue, 11 Jan 2022 12:47:13 +0000 Subject: [PATCH 104/131] Revisit waveform initialisation Ensure that the waveform type and length are specified during record creation to avoid problems with .set() later on. --- softioc/builder.py | 59 ++++++++++++++++++++++++++++------------ softioc/device.py | 39 +++++++++++--------------- softioc/fields.py | 13 --------- softioc/pythonSoftIoc.py | 4 +-- 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index f873832e..62152d5f 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -7,7 +7,8 @@ InitialiseDbd() LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd')) -from . import pythonSoftIoc # noqa +from . import device, pythonSoftIoc # noqa + PythonDevice = pythonSoftIoc.PythonDevice() @@ -120,7 +121,7 @@ def Action(name, **fields): 'l': 'LONG', # int_ 'f': 'FLOAT', # single 'd': 'DOUBLE', # float_ - 'S': 'STRING', # str_ +# 'S': 'STRING', # str_ # The following type codes are weakly supported by pretending that # they're related types. @@ -135,6 +136,19 @@ def Action(name, **fields): # O object_ U unicode_ V void } +# Coverts FTVL string to numpy type +DbfStringToNumpy = { +# 'STRING': numpy.dtype('S40'), # Don't think we want this! + 'CHAR': numpy.dtype('int8'), + 'UCHAR': numpy.dtype('uint8'), + 'SHORT': numpy.dtype('int16'), + 'USHORT': numpy.dtype('uint16'), + 'LONG': numpy.dtype('int32'), + 'ULONG': numpy.dtype('uint32'), + 'FLOAT': numpy.dtype('float32'), + 'DOUBLE': numpy.dtype('float64'), +} + def _waveform(value, fields): '''Helper routine for waveform construction. If a value is given it is @@ -146,29 +160,34 @@ def _waveform(value, fields): assert not value, 'Can\'t specify initial value twice!' value = (fields.pop('initial_value'),) + # Datatype can be specified as keyword argument, taken from FTVL, or derived + # from the initial value + if 'datatype' in fields: + assert 'FTVL' not in fields, \ + 'Can\'t specify FTVL and datatype together' + datatype = numpy.dtype(fields.pop('datatype')) + elif 'FTVL' in fields: + datatype = DbfStringToNumpy[fields['FTVL']] + else: + # No datatype specified, will have to infer from initial value + datatype = None + if value: - # If a value is specified it should be the *only* non keyword - # argument. + # If a value is specified it should be the *only* non keyword argument. value, = value - value = numpy.array(value) - fields['initial_value'] = value - - # Pick up default length and datatype from initial value - length = len(value) - FTVL = NumpyCharCodeToFtvl[value.dtype.char] + initial_value = device._require_waveform(value, datatype) + length = fields.pop('length', len(initial_value)) else: - # No value specified, so require length and datatype to be specified. + initial_value = numpy.array([], dtype = datatype) length = fields.pop('length') - FTVL = 'FLOAT' + datatype = initial_value.dtype - datatype = fields.pop('datatype', None) - if datatype is not None: - assert 'FTVL' not in fields, \ - 'Can\'t specify FTVL and datatype together' - FTVL = NumpyCharCodeToFtvl[numpy.dtype(datatype).char] + fields['initial_value'] = initial_value + fields['_wf_nelm'] = length + fields['_wf_dtype'] = datatype fields['NELM'] = length - fields.setdefault('FTVL', FTVL) + fields['FTVL'] = NumpyCharCodeToFtvl[datatype.char] def Waveform(name, *value, **fields): @@ -191,6 +210,10 @@ def _long_string(fields): # Default length of 256 length = 256 + fields.setdefault('initial_value', '') + fields['_wf_nelm'] = length + fields['_wf_dtype'] = numpy.dtype('uint8') + fields['NELM'] = length fields['FTVL'] = 'UCHAR' diff --git a/softioc/device.py b/softioc/device.py index ac4a2250..1640d54e 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -314,6 +314,14 @@ class ao(ProcessDeviceSupportOut): _dbf_type_ = fields.DBF_DOUBLE +def _require_waveform(value, dtype): + value = numpy.require(value, dtype = dtype) + if value.shape == (): + value.shape = (1,) + assert value.ndim == 1, 'Can\'t write multidimensional arrays' + return value + + class WaveformBase(ProcessDeviceSupportCore): _link_ = 'INP' # In the waveform record class, the following four fields are key: @@ -322,16 +330,15 @@ class WaveformBase(ProcessDeviceSupportCore): # NELM Length of allocated array in number of elements # NORD Currently reported length of array (0 <= NORD <= NELM) _fields_ = ['UDF', 'FTVL', 'BPTR', 'NELM', 'NORD'] - # Allow set() to be called before init_record: - _dtype = None - def _default_value(self): - return numpy.array([]) + + def __init__(self, name, _wf_nelm, _wf_dtype, **kargs): + self._dtype = _wf_dtype + self._nelm = _wf_nelm + self.__super.__init__(name, **kargs) def init_record(self, record): - self._nelm = record.NELM self._dbf_type_ = record.FTVL - self._dtype = fields.DbfCodeToNumpy[record.FTVL] return self.__super.init_record(record) def _read_value(self, record): @@ -343,12 +350,7 @@ def _read_value(self, record): return result def _write_value(self, record, value): - value = numpy.require(value, dtype = self._dtype) - - nelm = record.NELM nord = len(value) - if nord > nelm: - nord = nelm memmove( record.BPTR, value.ctypes.data_as(c_void_p), self._dtype.itemsize * nord) @@ -360,26 +362,17 @@ def _compare_values(self, value, other): def _value_to_epics(self, value): # Ensure we always convert incoming value into numpy array, regardless # of whether the record has been initialised or not - value = numpy.require(value, dtype = self._dtype) - if value.shape == (): - value.shape = (1,) - assert value.ndim == 1, 'Can\'t write multidimensional arrays' - - # Truncate value to fit - if hasattr(self, '_nelm'): - value = value[:self._nelm] - + value = _require_waveform(value, self._dtype) # Because arrays are mutable values it's ever so easy to accidentially # call set() with a value which subsequently changes. To avoid this # common class of bug, at the cost of duplicated code and data, here we - # ensure a copy is taken of the value. - return +value + # ensure a copy is taken of the value, after pruning to length. + return +value[:self._nelm] def _epics_to_value(self, value): return value def _value_to_dbr(self, value): - value = numpy.require(value, dtype = self._dtype) return self._dbf_type_, len(value), value.ctypes.data, value diff --git a/softioc/fields.py b/softioc/fields.py index d72de995..63fa476e 100644 --- a/softioc/fields.py +++ b/softioc/fields.py @@ -42,19 +42,6 @@ DBF_NOACCESS: c_void_p, } -# Mapping for record field type to numpy type. -DbfCodeToNumpy = { - DBF_STRING: numpy.dtype('S40'), - DBF_CHAR: numpy.dtype('int8'), - DBF_UCHAR: numpy.dtype('uint8'), - DBF_SHORT: numpy.dtype('int16'), - DBF_USHORT: numpy.dtype('uint16'), - DBF_LONG: numpy.dtype('int32'), - DBF_ULONG: numpy.dtype('uint32'), - DBF_FLOAT: numpy.dtype('float32'), - DBF_DOUBLE: numpy.dtype('float64'), -} - # Mapping from basic DBR_ codes to DBF_ values DbrToDbfCode = { DBR_STRING: DBF_STRING, diff --git a/softioc/pythonSoftIoc.py b/softioc/pythonSoftIoc.py index a2f840d0..8d79627f 100644 --- a/softioc/pythonSoftIoc.py +++ b/softioc/pythonSoftIoc.py @@ -23,8 +23,8 @@ def __init__(self, builder, device, name, **fields): # remaining arguments are passed to the builder. It's a shame we # have to maintain this separately from the corresponding device list. DeviceKeywords = [ - 'on_update', 'on_update_name', 'validate', - 'initial_value', 'always_update'] + 'on_update', 'on_update_name', 'validate', 'always_update', + 'initial_value', '_wf_nelm', '_wf_dtype'] device_kargs = {} for keyword in DeviceKeywords: if keyword in fields: From f719013a4d2486ed6d3c6ffa9c5ca7ed39e4fe2c Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Tue, 11 Jan 2022 14:43:58 +0000 Subject: [PATCH 105/131] Add special support for writing bytes to waveforms --- softioc/builder.py | 4 ++-- softioc/device.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index 62152d5f..f1442412 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -121,7 +121,7 @@ def Action(name, **fields): 'l': 'LONG', # int_ 'f': 'FLOAT', # single 'd': 'DOUBLE', # float_ -# 'S': 'STRING', # str_ +# noqa 'S': 'STRING', # str_ # The following type codes are weakly supported by pretending that # they're related types. @@ -138,7 +138,7 @@ def Action(name, **fields): # Coverts FTVL string to numpy type DbfStringToNumpy = { -# 'STRING': numpy.dtype('S40'), # Don't think we want this! +# noqa 'STRING': numpy.dtype('S40'), # Don't think we want this! 'CHAR': numpy.dtype('int8'), 'UCHAR': numpy.dtype('uint8'), 'SHORT': numpy.dtype('int16'), diff --git a/softioc/device.py b/softioc/device.py index 1640d54e..b6f419df 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -315,6 +315,9 @@ class ao(ProcessDeviceSupportOut): def _require_waveform(value, dtype): + if isinstance(value, bytes): + # Special case hack for byte arrays. Surprisingly tricky: + value = numpy.frombuffer(value, dtype = numpy.uint8) value = numpy.require(value, dtype = dtype) if value.shape == (): value.shape = (1,) From 0feae58d2f30c5bfe161b69f9f9f57b9fab230e4 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 11 Jan 2022 16:01:46 +0000 Subject: [PATCH 106/131] Update tests after latest code changes Numpy now defaults to float64/int64 Catch another case when we have to specify DBR_CHAR_STR Default WaveformOut now returns an array --- tests/test_records.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index d116dcbe..0fb501e0 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -123,14 +123,14 @@ def record_values_names(fixture_value): "wIn_list", builder.WaveformIn, [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.float32), + numpy.array([1, 2, 3], dtype=numpy.int64), numpy.ndarray, ), ( "wOut_list", builder.WaveformOut, [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.float32), + numpy.array([1, 2, 3], dtype=numpy.int64), numpy.ndarray, ), ( @@ -151,14 +151,14 @@ def record_values_names(fixture_value): "wIn_float", builder.WaveformIn, 12.345, - numpy.array([12.345], dtype=numpy.float32), + numpy.array([12.345], dtype=numpy.float64), numpy.ndarray, ), ( "wOut_float", builder.WaveformOut, 12.345, - numpy.array([12.345], dtype=numpy.float32), + numpy.array([12.345], dtype=numpy.float64), numpy.ndarray, ), ( @@ -460,8 +460,11 @@ def run_test_function( pytest.fail("IOC process did not start before TIMEOUT expired") try: + # Cannot do these imports before the subprocess starts, as cothread + # isn't threadsafe (in the way we require) from cothread import Yield from cothread.catools import caget, caput, _channel_cache + from cothread.dbr import DBR_CHAR_STR # cothread remembers connected IOCs. As we potentially restart the same # named IOC multiple times, we have to purge the cache else the @@ -480,10 +483,16 @@ def run_test_function( # Infer some required keywords from parameters kwargs = {} + put_kwarg = {} if creation_func in [builder.longStringIn, builder.longStringOut]: - from cothread.dbr import DBR_CHAR_STR kwargs.update({"datatype": DBR_CHAR_STR}) + if (creation_func in [builder.WaveformIn, builder.WaveformOut] + and type(initial_value) is bytes): + # Want to put bytestrings using this, but then retrieve the + # value as an array so NOT specify datatype there + put_kwarg.update({"datatype": DBR_CHAR_STR}) + if set_enum == SetValueEnum.CAPUT: if get_enum == GetValueEnum.GET: if parent_conn.poll(TIMEOUT): @@ -494,7 +503,8 @@ def run_test_function( DEVICE_NAME + ":" + record_name, initial_value, wait=True, - **kwargs + **kwargs, + **put_kwarg, ) if get_enum == GetValueEnum.GET: @@ -691,16 +701,10 @@ def test_value_post_init_caput(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 + # Various conditions (e.g.) mean we cannot use the entire list of cases filtered_list = [] for item in record_values_list: - if ( - item[1] in [builder.stringIn, builder.stringOut] - and len(item[2]) > 40 - ): - # caput blocks long strings - continue - + # In records block caputs if item[1] not in [ builder.aIn, builder.boolIn, @@ -708,8 +712,8 @@ def test_value_post_init_caput(self): builder.mbbIn, builder.stringIn, builder.WaveformIn, + builder.longStringIn, ]: - # In records block caput filtered_list.append(item) run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET) @@ -751,7 +755,7 @@ class TestDefaultValue: (builder.stringIn, "", str), (builder.mbbOut, None, type(None)), (builder.mbbIn, 0, int), - (builder.WaveformOut, None, type(None)), + (builder.WaveformOut, numpy.empty(0), numpy.ndarray), (builder.WaveformIn, numpy.empty(0), numpy.ndarray), (builder.longStringOut, "", str), (builder.longStringIn, "", str), From fe6b4364ea02e87e1b56ff3d6391579c815bc7b6 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 12 Jan 2022 08:56:03 +0000 Subject: [PATCH 107/131] Fix more tests Expected records now include PINI where appropriate bytestrings should no longer have trailing NULL bytes with waveforms --- tests/expected_records.db | 5 ++++- tests/test_records.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/expected_records.db b/tests/expected_records.db index 77a6aa10..ab764e2f 100644 --- a/tests/expected_records.db +++ b/tests/expected_records.db @@ -57,6 +57,7 @@ record(waveform, "TS-DI-TEST-01:LONGSTRING") field(FTVL, "UCHAR") field(INP, "@TS-DI-TEST-01:LONGSTRING") field(NELM, "256") + field(PINI, "YES") field(SCAN, "I/O Intr") } @@ -97,6 +98,7 @@ record(waveform, "TS-DI-TEST-01:SIN") field(FTVL, "DOUBLE") field(INP, "@TS-DI-TEST-01:SIN") field(NELM, "1024") + field(PINI, "YES") field(SCAN, "I/O Intr") } @@ -145,9 +147,10 @@ record(waveform, "TS-DI-TEST-01:WAVEFORM2") { field(DISP, "1") field(DTYP, "Python") - field(FTVL, "FLOAT") + field(FTVL, "DOUBLE") field(INP, "@TS-DI-TEST-01:WAVEFORM2") field(NELM, "10") + field(PINI, "YES") field(SCAN, "I/O Intr") } diff --git a/tests/test_records.py b/tests/test_records.py index 0fb501e0..7782cf2f 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -166,7 +166,7 @@ def record_values_names(fixture_value): builder.WaveformIn, b"HELLO\0WORLD", numpy.array( - [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], dtype=numpy.uint8 + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 ), numpy.ndarray, ), @@ -175,7 +175,7 @@ def record_values_names(fixture_value): builder.WaveformOut, b"HELLO\0WORLD", numpy.array( - [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68, 0], dtype=numpy.uint8 + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 ), numpy.ndarray, ), From 71361af38a4b25a7158160ce64bf1fb70ce8d103 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 12 Jan 2022 09:41:55 +0000 Subject: [PATCH 108/131] Fix behaviour of early .get() on uninitialised Out records --- softioc/device.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/softioc/device.py b/softioc/device.py index b6f419df..870bb54a 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -205,7 +205,12 @@ def set(self, value, process=True): self.__enable_write = True def get(self): - return self._epics_to_value(self._value) + if self._value is None: + # Before startup complete if no value set return default value + value = self._default_value() + else: + value = self._value + return self._epics_to_value(value) def _Device(Base, record_type, ctype, dbf_type, epics_rc, mlst = False): From a990af548467b09039541ffc59da54f1d701a08b Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 12 Jan 2022 09:42:52 +0000 Subject: [PATCH 109/131] Ensure waveform record and data types match properly --- softioc/builder.py | 64 ++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index f1442412..dda9581f 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -109,44 +109,28 @@ def Action(name, **fields): return boolOut(name, always_update = True, **fields) -# Converts numpy character code to FTVL value. -NumpyCharCodeToFtvl = { - # The following type codes are supported directly: - 'B': 'UCHAR', # ubyte - 'b': 'CHAR', # byte - 'H': 'USHORT', # ushort - 'h': 'SHORT', # short - 'i': 'LONG', # intc - 'L': 'ULONG', # uint - 'l': 'LONG', # int_ - 'f': 'FLOAT', # single - 'd': 'DOUBLE', # float_ -# noqa 'S': 'STRING', # str_ - - # The following type codes are weakly supported by pretending that - # they're related types. - '?': 'CHAR', # bool_ - 'p': 'LONG', # intp - 'I': 'ULONG', # uintc - 'P': 'ULONG', # uintp - - # The following type codes are not supported at all: - # q longlong Q ulonglong g longfloat - # F csingle D complex_ G clongfloat - # O object_ U unicode_ V void +# Converts numpy dtype name to FTVL value. +NumpyDtypeToDbf = { + 'int8': 'CHAR', + 'uint8': 'UCHAR', + 'int16': 'SHORT', + 'uint16': 'USHORT', + 'int32': 'LONG', + 'uint32': 'ULONG', + 'float32': 'FLOAT', + 'float64': 'DOUBLE', } # Coverts FTVL string to numpy type DbfStringToNumpy = { -# noqa 'STRING': numpy.dtype('S40'), # Don't think we want this! - 'CHAR': numpy.dtype('int8'), - 'UCHAR': numpy.dtype('uint8'), - 'SHORT': numpy.dtype('int16'), - 'USHORT': numpy.dtype('uint16'), - 'LONG': numpy.dtype('int32'), - 'ULONG': numpy.dtype('uint32'), - 'FLOAT': numpy.dtype('float32'), - 'DOUBLE': numpy.dtype('float64'), + 'CHAR': 'int8', + 'UCHAR': 'uint8', + 'SHORT': 'int16', + 'USHORT': 'uint16', + 'LONG': 'int32', + 'ULONG': 'uint32', + 'FLOAT': 'float32', + 'DOUBLE': 'float64', } @@ -167,7 +151,7 @@ def _waveform(value, fields): 'Can\'t specify FTVL and datatype together' datatype = numpy.dtype(fields.pop('datatype')) elif 'FTVL' in fields: - datatype = DbfStringToNumpy[fields['FTVL']] + datatype = numpy.dtype(DbfStringToNumpy[fields['FTVL']]) else: # No datatype specified, will have to infer from initial value datatype = None @@ -177,6 +161,14 @@ def _waveform(value, fields): value, = value initial_value = device._require_waveform(value, datatype) length = fields.pop('length', len(initial_value)) + + # Special case for [u]int64: if the initial value comes in as 64 bit + # integers cannot represent that, so recast it as [u]int32 + if datatype is None: + if initial_value.dtype == numpy.int64: + initial_value = numpy.require(initial_value, numpy.int32) + elif initial_value.dtype == numpy.uint64: + initial_value = numpy.require(initial_value, numpy.uint32) else: initial_value = numpy.array([], dtype = datatype) length = fields.pop('length') @@ -187,7 +179,7 @@ def _waveform(value, fields): fields['_wf_dtype'] = datatype fields['NELM'] = length - fields['FTVL'] = NumpyCharCodeToFtvl[datatype.char] + fields['FTVL'] = NumpyDtypeToDbf[datatype.name] def Waveform(name, *value, **fields): From b748856bd30a44bc8d529a2000adeb2ac5b246fc Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 12 Jan 2022 10:34:48 +0000 Subject: [PATCH 110/131] Add second case where we have to reduce int64 to int32 We do this to allow datatype=int to be specified as an argument. --- softioc/builder.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index dda9581f..5bc19b9e 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -149,7 +149,12 @@ def _waveform(value, fields): if 'datatype' in fields: assert 'FTVL' not in fields, \ 'Can\'t specify FTVL and datatype together' - datatype = numpy.dtype(fields.pop('datatype')) + datatype = fields.pop('datatype') + if datatype == int or datatype == 'int': + # Convert Python int to 32-bit integer, it's all we can handle + datatype = numpy.dtype('int32') + else: + datatype = numpy.dtype(datatype) elif 'FTVL' in fields: datatype = numpy.dtype(DbfStringToNumpy[fields['FTVL']]) else: @@ -163,7 +168,7 @@ def _waveform(value, fields): length = fields.pop('length', len(initial_value)) # Special case for [u]int64: if the initial value comes in as 64 bit - # integers cannot represent that, so recast it as [u]int32 + # integers we cannot represent that, so recast it as [u]int32 if datatype is None: if initial_value.dtype == numpy.int64: initial_value = numpy.require(initial_value, numpy.int32) From f8b67f8a5222d20e56e0c46716f62319fa5101e3 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 12 Jan 2022 11:49:24 +0000 Subject: [PATCH 111/131] Fix more tests Default record values updated Removed unnecessary special-case logic for Waveforms Add special handling for bytestrings in Waveforms --- tests/test_records.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 7782cf2f..cbeaed49 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -313,19 +313,7 @@ def record_value_asserts( if type(expected_value) == float and isnan(expected_value): assert isnan(actual_value) # NaN != Nan, so needs special case elif creation_func in [builder.WaveformOut, builder.WaveformIn]: - - # Special case for lack of default value on Out records before init - if actual_value is None and expected_value is None: - assert type(actual_value) == expected_type - return - - # Using .get() on the array returns entire length, not just filled - # part. Confirm this by ensuring sliced part of array is all zeros - assert not numpy.any(actual_value[expected_value.size:]) - truncated_value = actual_value[: expected_value.size] - assert numpy.array_equal( - truncated_value, expected_value - ), f"Arrays not equal: {actual_value} {expected_value}" + assert numpy.array_equal(actual_value, expected_value) assert type(actual_value) == expected_type else: assert actual_value == expected_value @@ -489,9 +477,12 @@ def run_test_function( if (creation_func in [builder.WaveformIn, builder.WaveformOut] and type(initial_value) is bytes): - # Want to put bytestrings using this, but then retrieve the - # value as an array so NOT specify datatype there - put_kwarg.update({"datatype": DBR_CHAR_STR}) + # There's a bug in caput that means DBR_CHAR_STR doesn't + # truncate the array of the target record, meaning .get() + # returns all NELM rather than just NORD elements. Instead we + # encode the data ourselves + initial_value = numpy.frombuffer( + initial_value, dtype = numpy.uint8) if set_enum == SetValueEnum.CAPUT: if get_enum == GetValueEnum.GET: @@ -744,16 +735,16 @@ class TestDefaultValue: @pytest.mark.parametrize( "creation_func,expected_value,expected_type", [ - (builder.aOut, None, type(None)), + (builder.aOut, 0.0, float), (builder.aIn, 0.0, float), - (builder.longOut, None, type(None)), + (builder.longOut, 0, int), (builder.longIn, 0, int), - (builder.boolOut, None, type(None)), + (builder.boolOut, 0, int), (builder.boolIn, 0, int), - (builder.Action, None, type(None)), - (builder.stringOut, None, type(None)), + (builder.Action, 0, int), + (builder.stringOut, "", str), (builder.stringIn, "", str), - (builder.mbbOut, None, type(None)), + (builder.mbbOut, 0, int), (builder.mbbIn, 0, int), (builder.WaveformOut, numpy.empty(0), numpy.ndarray), (builder.WaveformIn, numpy.empty(0), numpy.ndarray), From 2c93f3906d669288d62fe1ba248c23d7b57911e5 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Wed, 12 Jan 2022 14:13:07 +0000 Subject: [PATCH 112/131] Fix bug in waveform change comparison --- softioc/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/softioc/device.py b/softioc/device.py index 870bb54a..5c0be62f 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -365,7 +365,7 @@ def _write_value(self, record, value): record.NORD = nord def _compare_values(self, value, other): - return numpy.all(value == other) + return len(value) == len(other) and numpy.all(value == other) def _value_to_epics(self, value): # Ensure we always convert incoming value into numpy array, regardless From 049da5460ed46601f879c6e0e160504f9bb4307c Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 12 Jan 2022 14:50:02 +0000 Subject: [PATCH 113/131] More test fixes Accept that passing integers into Waveforms always gives a float array Condense three disparate filterings into one list --- tests/test_records.py | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index cbeaed49..923fbafd 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -78,6 +78,17 @@ def record_func(request): """The list of record creation functions""" return request.param +# A list of all In records, used to filter out various tests +in_records = [ + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn, + builder.longStringIn, +] + def record_values_names(fixture_value): """Provide a nice name for the tests in the record_values fixture""" return ( @@ -137,14 +148,14 @@ def record_values_names(fixture_value): "wIn_int", builder.WaveformIn, 567, - numpy.array([567], dtype=numpy.int32), + numpy.array([567.], dtype=numpy.float64), numpy.ndarray, ), ( "wOut_int", builder.WaveformOut, 567, - numpy.array([567], dtype=numpy.int32), + numpy.array([567.], dtype=numpy.float64), numpy.ndarray, ), ( @@ -627,14 +638,7 @@ def test_value_post_init_caput(self): # caput blocks strings longer than 40 characters continue - if item[1] not in [ - builder.aIn, - builder.boolIn, - builder.longIn, - builder.mbbIn, - builder.stringIn, - builder.WaveformIn, - ]: + if item[1] not in in_records: # In records block caput filtered_list.append(item) @@ -655,14 +659,7 @@ def test_value_post_init_set(self): for item in record_values_list: # .set() on In records doesn't update correctly. # pythonSoftIOC issue #67 - if item[1] not in ( - builder.aIn, - builder.longIn, - builder.boolIn, - builder.stringIn, - builder.mbbIn, - builder.WaveformIn, - ): + if item[1] not in in_records: filtered_list.append(item) run_test_function( @@ -696,15 +693,7 @@ def test_value_post_init_caput(self): filtered_list = [] for item in record_values_list: # In records block caputs - if item[1] not in [ - builder.aIn, - builder.boolIn, - builder.longIn, - builder.mbbIn, - builder.stringIn, - builder.WaveformIn, - builder.longStringIn, - ]: + if item[1] not in in_records: filtered_list.append(item) run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET) From f805f026ebd704fbe3cd23d5c3f09dd8e2c4624f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Wed, 12 Jan 2022 16:17:09 +0000 Subject: [PATCH 114/131] Skip caget value checks when waveform is length 1 See note in the print() statement for explanation. Also added tests for floating point list passed to waveform --- tests/test_records.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/tests/test_records.py b/tests/test_records.py index 923fbafd..bcf67dd5 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -131,17 +131,31 @@ def record_values_names(fixture_value): ("strin_utf8", builder.stringIn, "%a€b", "%a€b", str), # Valid UTF-8 ("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str), # Valid UTF-8 ( - "wIn_list", + "wIn_list_int", builder.WaveformIn, [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.int64), + numpy.array([1, 2, 3], dtype=numpy.int32), numpy.ndarray, ), ( - "wOut_list", + "wOut_list_int", builder.WaveformOut, [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.int64), + numpy.array([1, 2, 3], dtype=numpy.int32), + numpy.ndarray, + ), + ( + "wIn_list_float", + builder.WaveformIn, + [1.5, 2.6, 3.7], + numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wOut_list_float", + builder.WaveformOut, + [1.5, 2.6, 3.7], + numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), numpy.ndarray, ), ( @@ -532,6 +546,17 @@ def run_test_function( # native types e.g. "+ca_int" -> int rec_val = +rec_val + if ( + creation_func in [builder.WaveformOut, builder.WaveformIn] + and expected_value.dtype in [numpy.float64, numpy.int32] + ): + print( + "caget cannot distinguish between a waveform with 1" + "element and a scalar value, and so always returns a " + "scalar. Therefore we skip this check.") + continue + + record_value_asserts( creation_func, rec_val, expected_value, expected_type ) From 40cb943e17cf5e8f481d7aa74df60d2ae60eebe1 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Thu, 13 Jan 2022 09:45:26 +0000 Subject: [PATCH 115/131] Use array_equal to compare numpy arrays --- softioc/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/softioc/device.py b/softioc/device.py index 5c0be62f..6be0553a 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -365,7 +365,7 @@ def _write_value(self, record, value): record.NORD = nord def _compare_values(self, value, other): - return len(value) == len(other) and numpy.all(value == other) + return numpy.array_equal(value, other) def _value_to_epics(self, value): # Ensure we always convert incoming value into numpy array, regardless From 61a8a474258cd8d31e585567f2156c338790fd13 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Thu, 13 Jan 2022 10:11:58 +0000 Subject: [PATCH 116/131] Ensure we don't create zero length waveforms Also check and abort writes that are too long for the waveform --- softioc/builder.py | 2 ++ softioc/device.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index 5bc19b9e..679c4c6c 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -179,6 +179,8 @@ def _waveform(value, fields): length = fields.pop('length') datatype = initial_value.dtype + assert length > 0, 'Array cannot be of zero length' + fields['initial_value'] = initial_value fields['_wf_nelm'] = length fields['_wf_dtype'] = datatype diff --git a/softioc/device.py b/softioc/device.py index 6be0553a..cd6f8e68 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -374,8 +374,9 @@ def _value_to_epics(self, value): # Because arrays are mutable values it's ever so easy to accidentially # call set() with a value which subsequently changes. To avoid this # common class of bug, at the cost of duplicated code and data, here we - # ensure a copy is taken of the value, after pruning to length. - return +value[:self._nelm] + # ensure a copy is taken of the value. + assert len(value) <= self._nelm, 'Value too long for waveform' + return +value def _epics_to_value(self, value): return value From 42332fb8412ccd58e6998e4d03081d71cba6b4d8 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 11:05:38 +0000 Subject: [PATCH 117/131] Fix package dependencies Done after seeing a package version conflict in CI between aioca, p4p and softioc's desired version of the epicscorelibs library. --- Pipfile.lock | 442 +++++++++++++++++++++++---------------------------- setup.cfg | 5 +- 2 files changed, 198 insertions(+), 249 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index a057c09d..92bea4ba 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -14,55 +14,19 @@ ] }, "default": { - "aioca": { - "hashes": [ - "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", - "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" - ], - "version": "==1.2" - }, "cothread": { "hashes": [ "sha256:50cf5b5a65e7fcfd113f06c2d6b1f363e37b0caee412c75ac7608377aa0bd10a" ], + "markers": "sys_platform != 'win32'", "version": "==2.17" }, - "epicscorelibs": { - "hashes": [ - "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870", - "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c", - "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800", - "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4", - "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1", - "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be", - "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88", - "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab", - "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c", - "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351", - "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120", - "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3", - "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4", - "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92", - "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb", - "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0", - "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3", - "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358", - "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76", - "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3", - "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131", - "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552", - "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd", - "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681", - "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4" - ], - "version": "==7.0.6.99.1.0" - }, "epicsdbbuilder": { "hashes": [ - "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", - "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" + "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", + "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" ], - "version": "==1.4.3" + "version": "==1.5" }, "numpy": { "hashes": [ @@ -117,36 +81,21 @@ ], "version": "==1.6.3" }, - "setuptools-dso": { - "hashes": [ - "sha256:10f0e4eab7bf6789f76a5544f15a1be0f285413652b7d3d7ac11203ce72569d5", - "sha256:bcf27777d7fc39359ab56c5a393c0aa62ae7bc84f30d1f36a40d6143f0af14b0" - ], - "version": "==2.1" - }, "softioc": { "editable": true, "extras": [ "useful" ], "path": "." - }, - "typing-extensions": { - "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" - ], - "version": "==3.10.0.0" } }, "develop": { "aioca": { "hashes": [ - "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", - "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" + "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae", + "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d" ], - "version": "==1.2" + "version": "==1.3" }, "alabaster": { "hashes": [ @@ -171,87 +120,79 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "version": "==4.0.0" - }, - "cothread": { - "hashes": [ - "sha256:50cf5b5a65e7fcfd113f06c2d6b1f363e37b0caee412c75ac7608377aa0bd10a" - ], - "version": "==2.17" + "markers": "python_version >= '3'", + "version": "==2.0.7" }, "coverage": { - "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" - ], - "version": "==5.5" + "extras": [ + "toml" + ], + "hashes": [ + "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9", + "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b", + "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e", + "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a", + "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2", + "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67", + "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685", + "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f", + "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7", + "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717", + "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5", + "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b", + "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef", + "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489", + "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b", + "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e", + "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926", + "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271", + "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54", + "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026", + "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad", + "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533", + "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6", + "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8", + "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26", + "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b", + "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8", + "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9", + "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271", + "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003", + "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a", + "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0", + "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d", + "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a", + "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0", + "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9", + "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f", + "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33", + "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a", + "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6", + "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201", + "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336", + "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce", + "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4", + "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9", + "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07" + ], + "version": "==6.1.1" }, "docutils": { "hashes": [ - "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", - "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", + "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" ], - "version": "==0.16" + "version": "==0.17.1" }, "epicscorelibs": { "hashes": [ @@ -285,24 +226,25 @@ }, "epicsdbbuilder": { "hashes": [ - "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", - "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" + "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", + "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" ], - "version": "==1.4.3" + "version": "==1.5" }, "flake8": { "hashes": [ - "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", - "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" + "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", + "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" ], - "version": "==3.9.1" + "version": "==4.0.1" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "version": "==2.10" + "markers": "python_version >= '3'", + "version": "==3.3" }, "imagesize": { "hashes": [ @@ -313,11 +255,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb", - "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33" + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" ], "markers": "python_version < '3.8' and python_version < '3.8'", - "version": "==4.6.0" + "version": "==4.0.1" }, "iniconfig": { "hashes": [ @@ -328,67 +270,69 @@ }, "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45", + "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c" ], - "version": "==2.11.3" + "version": "==3.0.2" }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" - ], - "version": "==1.1.1" + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "version": "==2.0.1" }, "mccabe": { "hashes": [ @@ -466,17 +410,17 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", + "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" ], - "version": "==20.9" + "version": "==21.2" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "version": "==0.13.1" + "version": "==1.0.0" }, "ply": { "hashes": [ @@ -494,24 +438,24 @@ }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" ], - "version": "==2.7.0" + "version": "==2.8.0" }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", + "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" ], - "version": "==2.3.1" + "version": "==2.4.0" }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" ], - "version": "==2.9.0" + "version": "==2.10.0" }, "pyparsing": { "hashes": [ @@ -522,25 +466,24 @@ }, "pytest": { "hashes": [ - "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", - "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], - "version": "==6.2.3" + "version": "==6.2.5" }, "pytest-asyncio": { "hashes": [ "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" ], - "markers": "python_version > '3.0'", "version": "==0.14.0" }, "pytest-cov": { "hashes": [ - "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", - "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" ], - "version": "==2.11.1" + "version": "==3.0.0" }, "pytest-flake8": { "hashes": [ @@ -551,17 +494,17 @@ }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", + "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" ], - "version": "==2021.1" + "version": "==2021.3" }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "version": "==2.25.1" + "version": "==2.26.0" }, "setuptools-dso": { "hashes": [ @@ -586,25 +529,24 @@ }, "sphinx": { "hashes": [ - "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", - "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" + "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c", + "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851" ], - "version": "==4.1.2" + "version": "==4.3.2" }, "sphinx-rtd-theme": { "hashes": [ - "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", - "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" + "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", + "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c" ], - "version": "==0.5.2" + "version": "==1.0.0" }, "sphinx-rtd-theme-github-versions": { "hashes": [ - "sha256:ad2822c08b0f075c051e25b9d291152d846c21ed052db3b458d5ef30efac59b3", - "sha256:f84b45baf536c01c2e62f26e8206dd69f0375d9c9b3769d56295292e6e00f43b" + "sha256:0df27ae9a9cd902468c808dbee5a43f4db8ce43cbcf2ecc78d2fe47698bb0ded", + "sha256:23018e51a5d27ef4f69dd86314f73b19088f2cfd91c74a24db1517832233dc07" ], - "markers": "python_version > '3.0'", - "version": "==0.2" + "version": "==1.1" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -655,27 +597,35 @@ ], "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" + ], + "version": "==1.2.2" + }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], - "version": "==3.10.0.0" + "markers": "python_version < '3.8'", + "version": "==3.10.0.2" }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], - "version": "==1.26.5" + "version": "==1.26.7" }, "zipp": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", + "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" ], - "version": "==3.4.1" + "version": "==3.6.0" } } } diff --git a/setup.cfg b/setup.cfg index 26faed7f..e95e934a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,9 +34,9 @@ softioc = [options.extras_require] # Useful extras for use at DLS useful = - cothread + cothread; sys_platform != "win32" scipy - aioca >=1.3 + # Dev and docs dependencies dev = pytest-cov @@ -44,7 +44,6 @@ dev = sphinx-rtd-theme-github-versions pytest-asyncio aioca >=1.3 - cothread; sys_platform != "win32" p4p [flake8] From b544c9e358e3908a88d5115fc567ad4bd6aaae8b Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 11:15:42 +0000 Subject: [PATCH 118/131] Revert "Fix package dependencies" This reverts commit 42332fb8412ccd58e6998e4d03081d71cba6b4d8. --- Pipfile.lock | 442 ++++++++++++++++++++++++++++----------------------- setup.cfg | 5 +- 2 files changed, 249 insertions(+), 198 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 92bea4ba..a057c09d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -14,19 +14,55 @@ ] }, "default": { + "aioca": { + "hashes": [ + "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", + "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" + ], + "version": "==1.2" + }, "cothread": { "hashes": [ "sha256:50cf5b5a65e7fcfd113f06c2d6b1f363e37b0caee412c75ac7608377aa0bd10a" ], - "markers": "sys_platform != 'win32'", "version": "==2.17" }, + "epicscorelibs": { + "hashes": [ + "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870", + "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c", + "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800", + "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4", + "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1", + "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be", + "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88", + "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab", + "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c", + "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351", + "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120", + "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3", + "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4", + "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92", + "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb", + "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0", + "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3", + "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358", + "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76", + "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3", + "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131", + "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552", + "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd", + "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681", + "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4" + ], + "version": "==7.0.6.99.1.0" + }, "epicsdbbuilder": { "hashes": [ - "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", - "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" + "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", + "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" ], - "version": "==1.5" + "version": "==1.4.3" }, "numpy": { "hashes": [ @@ -81,21 +117,36 @@ ], "version": "==1.6.3" }, + "setuptools-dso": { + "hashes": [ + "sha256:10f0e4eab7bf6789f76a5544f15a1be0f285413652b7d3d7ac11203ce72569d5", + "sha256:bcf27777d7fc39359ab56c5a393c0aa62ae7bc84f30d1f36a40d6143f0af14b0" + ], + "version": "==2.1" + }, "softioc": { "editable": true, "extras": [ "useful" ], "path": "." + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "version": "==3.10.0.0" } }, "develop": { "aioca": { "hashes": [ - "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae", - "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d" + "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", + "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" ], - "version": "==1.3" + "version": "==1.2" }, "alabaster": { "hashes": [ @@ -120,79 +171,87 @@ }, "certifi": { "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "version": "==2021.10.8" + "version": "==2021.5.30" }, - "charset-normalizer": { + "chardet": { "hashes": [ - "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", - "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "markers": "python_version >= '3'", - "version": "==2.0.7" + "version": "==4.0.0" + }, + "cothread": { + "hashes": [ + "sha256:50cf5b5a65e7fcfd113f06c2d6b1f363e37b0caee412c75ac7608377aa0bd10a" + ], + "version": "==2.17" }, "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9", - "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b", - "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e", - "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a", - "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2", - "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67", - "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685", - "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f", - "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7", - "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717", - "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5", - "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b", - "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef", - "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489", - "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b", - "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e", - "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926", - "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271", - "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54", - "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026", - "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad", - "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533", - "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6", - "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8", - "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26", - "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b", - "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8", - "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9", - "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271", - "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003", - "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a", - "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0", - "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d", - "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a", - "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0", - "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9", - "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f", - "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33", - "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a", - "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6", - "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201", - "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336", - "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce", - "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4", - "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9", - "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07" - ], - "version": "==6.1.1" + "hashes": [ + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "version": "==5.5" }, "docutils": { "hashes": [ - "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", - "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], - "version": "==0.17.1" + "version": "==0.16" }, "epicscorelibs": { "hashes": [ @@ -226,25 +285,24 @@ }, "epicsdbbuilder": { "hashes": [ - "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", - "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" + "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", + "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" ], - "version": "==1.5" + "version": "==1.4.3" }, "flake8": { "hashes": [ - "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", - "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" + "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", + "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" ], - "version": "==4.0.1" + "version": "==3.9.1" }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '3'", - "version": "==3.3" + "version": "==2.10" }, "imagesize": { "hashes": [ @@ -255,11 +313,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", - "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb", + "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33" ], "markers": "python_version < '3.8' and python_version < '3.8'", - "version": "==4.0.1" + "version": "==4.6.0" }, "iniconfig": { "hashes": [ @@ -270,69 +328,67 @@ }, "jinja2": { "hashes": [ - "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45", - "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c" + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], - "version": "==3.0.2" + "version": "==2.11.3" }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "version": "==2.0.1" + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + ], + "version": "==1.1.1" }, "mccabe": { "hashes": [ @@ -410,17 +466,17 @@ }, "packaging": { "hashes": [ - "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", - "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "version": "==21.2" + "version": "==20.9" }, "pluggy": { "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==1.0.0" + "version": "==0.13.1" }, "ply": { "hashes": [ @@ -438,24 +494,24 @@ }, "pycodestyle": { "hashes": [ - "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", - "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], - "version": "==2.8.0" + "version": "==2.7.0" }, "pyflakes": { "hashes": [ - "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", - "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" + "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", + "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], - "version": "==2.4.0" + "version": "==2.3.1" }, "pygments": { "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", + "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" ], - "version": "==2.10.0" + "version": "==2.9.0" }, "pyparsing": { "hashes": [ @@ -466,24 +522,25 @@ }, "pytest": { "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", + "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" ], - "version": "==6.2.5" + "version": "==6.2.3" }, "pytest-asyncio": { "hashes": [ "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" ], + "markers": "python_version > '3.0'", "version": "==0.14.0" }, "pytest-cov": { "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" + "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", + "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" ], - "version": "==3.0.0" + "version": "==2.11.1" }, "pytest-flake8": { "hashes": [ @@ -494,17 +551,17 @@ }, "pytz": { "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], - "version": "==2021.3" + "version": "==2021.1" }, "requests": { "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "version": "==2.26.0" + "version": "==2.25.1" }, "setuptools-dso": { "hashes": [ @@ -529,24 +586,25 @@ }, "sphinx": { "hashes": [ - "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c", - "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851" + "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", + "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" ], - "version": "==4.3.2" + "version": "==4.1.2" }, "sphinx-rtd-theme": { "hashes": [ - "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", - "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c" + "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", + "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" ], - "version": "==1.0.0" + "version": "==0.5.2" }, "sphinx-rtd-theme-github-versions": { "hashes": [ - "sha256:0df27ae9a9cd902468c808dbee5a43f4db8ce43cbcf2ecc78d2fe47698bb0ded", - "sha256:23018e51a5d27ef4f69dd86314f73b19088f2cfd91c74a24db1517832233dc07" + "sha256:ad2822c08b0f075c051e25b9d291152d846c21ed052db3b458d5ef30efac59b3", + "sha256:f84b45baf536c01c2e62f26e8206dd69f0375d9c9b3769d56295292e6e00f43b" ], - "version": "==1.1" + "markers": "python_version > '3.0'", + "version": "==0.2" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -597,35 +655,27 @@ ], "version": "==0.10.2" }, - "tomli": { - "hashes": [ - "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", - "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" - ], - "version": "==1.2.2" - }, "typing-extensions": { "hashes": [ - "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", - "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", - "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" ], - "markers": "python_version < '3.8'", - "version": "==3.10.0.2" + "version": "==3.10.0.0" }, "urllib3": { "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", + "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" ], - "version": "==1.26.7" + "version": "==1.26.5" }, "zipp": { "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" ], - "version": "==3.6.0" + "version": "==3.4.1" } } } diff --git a/setup.cfg b/setup.cfg index e95e934a..26faed7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,9 +34,9 @@ softioc = [options.extras_require] # Useful extras for use at DLS useful = - cothread; sys_platform != "win32" + cothread scipy - + aioca >=1.3 # Dev and docs dependencies dev = pytest-cov @@ -44,6 +44,7 @@ dev = sphinx-rtd-theme-github-versions pytest-asyncio aioca >=1.3 + cothread; sys_platform != "win32" p4p [flake8] From 7ce0246cb3feefca1b743d5630b65e81857377e8 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 11:28:09 +0000 Subject: [PATCH 119/131] Pin epicscorelibs version to fix dependency errors Required due to dependency version conflicts: aioca 1.3 requiring epicscorelibs>=7.0.3.99.4.0 p4p 3.5.4 requiring epicscorelibs<7.0.6.99.2 and >=7.0.6.99.1.0 p4p does have pre-release versions in the 4.0* stream, so this issue may fix itself once those become full releases --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c08a4d05..a9377c7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.6.99.1.0"] +requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs=7.0.6.99.1.0"] build-backend = "setuptools.build_meta:__legacy__" From d7f02f3e4396787a3873f3463fab4bf2aa17af0f Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 11:33:45 +0000 Subject: [PATCH 120/131] Revert "Pin epicscorelibs version to fix dependency errors" This reverts commit 7ce0246cb3feefca1b743d5630b65e81857377e8. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a9377c7f..c08a4d05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs=7.0.6.99.1.0"] +requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.6.99.1.0"] build-backend = "setuptools.build_meta:__legacy__" From 21d510e5dd0129b67faf36ad04f2650422776078 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 11:34:33 +0000 Subject: [PATCH 121/131] Pin epicscorelibs version to fix dependency errors Required due to dependency version conflicts: aioca 1.3 requiring epicscorelibs>=7.0.3.99.4.0 p4p 3.5.4 requiring epicscorelibs<7.0.6.99.2 and >=7.0.6.99.1.0 p4p does have pre-release versions in the 4.0* stream, so this issue may fix itself once those become full releases --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c08a4d05..a3b8d8a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.6.99.1.0"] +requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs==7.0.6.99.1.0"] build-backend = "setuptools.build_meta:__legacy__" From 31bdb2b0368d72b513cdc7935bb1a30cde7c4cf6 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 12:39:07 +0000 Subject: [PATCH 122/131] Add docs for longStringIn/Out --- docs/reference/api.rst | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 4ce41e4b..e48ed280 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -308,13 +308,28 @@ All functions return a wrapped `ProcessDeviceSupportIn` or if given, or defaults to ``'FLOAT'``. -The following function generates a specialised record. +The following functions generates specialised records. .. function:: Action(name, **fields) Creates a record (using `boolOut`) which will always call the `on_update` - method when processed. Used for action records. The `on_update` keyword - should always be passed. + method when processed. Used for action records. Either the `on_update` or + `on_update_name` keyword should always be passed. + +.. function:: + longStringIn(name, **fields) + longStringOut(name, **fields) + + Creates a Waveform record specialised to contain Python (Unicode) strings. + Only accepts string values, and only returns string values. `initial_value` + and ``length`` keywords accepted as per Waveform records. + + If no ``length`` or `initial_value` is provided a default length of 256 is used. + + .. note:: + If you wish to store byte strings, use a ``WaveformIn/Out`` record + + The following functions manage record names. The record device name must be From 82f91f0ea49eae7b5746c120a523be51f4468d03 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 12:40:50 +0000 Subject: [PATCH 123/131] Clean up misleading docs --- docs/reference/api.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/reference/api.rst b/docs/reference/api.rst index e48ed280..20eb6de3 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -343,8 +343,7 @@ record creation function. Sets up the prefix part of the record name, referred to here as the "device" part. This function must be called before creating any records. Note that - only this function need be used, the three other functions below are - entirely optional. + only this function need be used, function below is entirely optional. .. function:: UnsetDevice() From 7f6ccd3f3188fbfffeb656e943aa9a8a6019886c Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 12:46:29 +0000 Subject: [PATCH 124/131] Add changelog entry for PR#60 --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cdadaac3..d6c29451 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,10 @@ Changed: - `Remove python2 support <../../pull/64>` - `Default DISP to TRUE for all In records <../../pull/74>` +- `Overhaul record value setting/getting <../../pull/60>` + +Added: +- New longStringIn/longStringOut record types 3.2.1_ - 2021-11-25 ------------------- From b5e66ebc2c157c3d300459f1a527ca5964b4a029 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Thu, 13 Jan 2022 13:30:37 +0000 Subject: [PATCH 125/131] Now also block writing over-long strings to longString records --- softioc/device.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/softioc/device.py b/softioc/device.py index cd6f8e68..3ff8d254 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -404,10 +404,8 @@ def _value_to_epics(self, value): # Note that the trailing null is needed to work around problems with # some clients. value = numpy.frombuffer(value + b'\0', dtype = numpy.uint8) - - # Truncate value to fit - if hasattr(self, '_nelm'): - value = value[:self._nelm] + # Ensure string isn't too long to fit into waveform + assert len(value) <= self._nelm, 'Value too long for waveform' return value def _epics_to_value(self, value): From 4cbdb949125300aafb9b8bbb616da2623116f54c Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 15:20:32 +0000 Subject: [PATCH 126/131] Refactor tests to split value tests into own file Also cleaned up sim_records - the test_records test was failing due to test reorganization - there's issues with creating records during a module import so now we have a method to create the required records. --- tests/conftest.py | 23 + tests/sim_records.py | 103 ++--- tests/test_record_values.py | 792 +++++++++++++++++++++++++++++++++++ tests/test_records.py | 811 +----------------------------------- 4 files changed, 868 insertions(+), 861 deletions(-) create mode 100644 tests/test_record_values.py diff --git a/tests/conftest.py b/tests/conftest.py index 3479fe5c..b8689d2b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,14 @@ import pytest +# Must import softioc before epicsdbbuilder +from softioc.device_core import RecordLookup +from epicsdbbuilder import ResetRecords + +requires_cothread = pytest.mark.skipif( + sys.platform.startswith("win"), reason="Cothread doesn't work on windows" +) + class SubprocessIOC: def __init__(self, ioc_py): self.pv_prefix = "".join( @@ -59,3 +67,18 @@ def asyncio_ioc_override(): yield ioc ioc.kill() aioca_cleanup() + +def _clear_records(): + # Remove any records created at epicsdbbuilder layer + ResetRecords() + # And at pythonSoftIoc level + # TODO: Remove this hack and use use whatever comes out of + # https://github.com/dls-controls/pythonSoftIOC/issues/56 + RecordLookup._RecordDirectory.clear() + +@pytest.fixture +def clear_records(): + """Fixture to delete all records before and after a test.""" + _clear_records() + yield + _clear_records() diff --git a/tests/sim_records.py b/tests/sim_records.py index 9de112b5..ee712601 100644 --- a/tests/sim_records.py +++ b/tests/sim_records.py @@ -22,67 +22,54 @@ def on_update(value): def on_update_name(value, name): print('on_update', name, ':', repr(value)) -t_ai = aIn('AI', initial_value=12.34) -t_boolin = boolIn('BOOLIN', 'True', 'False', initial_value=False) -t_longin = longIn('LONGIN', initial_value=33) -t_stringin = stringIn('STRINGIN', initial_value="Testing string") -t_mbbi = mbbIn( - 'MBBI', ('One', alarm.MAJOR_ALARM), 'Two', ('Three', "MINOR"), - initial_value=2) - -t_ao = aOut('AO', initial_value=12.45, on_update_name=on_update_name) -t_boolout = boolOut( - 'BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update) -t_longout = longOut('LONGOUT', initial_value=2008, on_update=on_update) -t_stringout = stringOut( - 'STRINGOUT', initial_value='watevah', on_update=on_update) -t_mbbo = mbbOut( - 'MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1) - -def update_sin_wf(value): - print('update_sin_wf', value) - sin_wf.set(numpy.sin( - numpy.linspace(0, 2*numpy.pi*sin_ph.get(), sin_len.get()))) -sin_wf = Waveform('SIN', datatype = float, length = 1024) -# Check we can update its value before iocInit as per #22 -sin_wf.set([1, 2, 3]) -sin_len = longOut( - 'SINN', 0, 1024, initial_value=1024, on_update=update_sin_wf) -sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf) - - -wf_len = 32 -wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len)) -t_waveform_in = Waveform('WAVEFORM', wf) -t_waveform_out = WaveformOut('WAVEFORM_OUT', wf, on_update = on_update) -t_waveform_in2 = Waveform('WAVEFORM2', length = 10) - -t_longstring_in = longStringIn('LONGSTRING', length = 256) - - -def Update(): - t_ai.set(3.14159) - t_boolin.set(True) - t_longin.set(365) - t_stringin.set('Another different string') - t_mbbi.set(0) - -def UpdateOut(): - t_ao.set(3.14159) - t_boolout.set(True) - t_longout.set(365) - t_stringout.set('Another different string') - t_mbbo.set(2) +# The publicly accessible records. +t_ai = None +t_ao = None + +def create_records(): + global t_ai, t_ao + + t_ai = aIn('AI', initial_value=12.34) + + boolIn('BOOLIN', 'True', 'False', initial_value=False) + longIn('LONGIN', initial_value=33) + stringIn('STRINGIN', initial_value="Testing string") + mbbIn( + 'MBBI', ('One', alarm.MAJOR_ALARM), 'Two', ('Three', "MINOR"), + initial_value=2) + + t_ao = aOut('AO', initial_value=12.45, on_update_name=on_update_name) + + boolOut('BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update) + longOut('LONGOUT', initial_value=2008, on_update=on_update) + stringOut('STRINGOUT', initial_value='watevah', on_update=on_update) + mbbOut('MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1) + + def update_sin_wf(value): + print('update_sin_wf', value) + sin_wf.set(numpy.sin( + numpy.linspace(0, 2*numpy.pi*sin_ph.get(), sin_len.get()))) + sin_wf = Waveform('SIN', datatype = float, length = 1024) + # Check we can update its value before iocInit as per #22 + sin_wf.set([1, 2, 3]) + sin_len = longOut( + 'SINN', 0, 1024, initial_value=1024, on_update=update_sin_wf) + sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf) + + + wf_len = 32 + wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len)) + Waveform('WAVEFORM', wf) + WaveformOut('WAVEFORM_OUT', wf, on_update = on_update) + Waveform('WAVEFORM2', length = 10) + + longStringIn('LONGSTRING', length = 256) + +create_records() softioc.devIocStats(ioc_name) -__all__ = [ - 't_ai', 't_boolin', 't_longin', 't_stringin', 't_mbbi', - 't_ao', 't_boolout', 't_longout', 't_stringout', 't_mbbo', - 't_waveform_in', 't_waveform_in2', 't_waveform_out', 't_longstring_in', - 'wf_len', - 'Update', 'UpdateOut' -] +__all__ = ['t_ai', 't_ao', ] if __name__ == "__main__": WriteRecords("expected_records.db") diff --git a/tests/test_record_values.py b/tests/test_record_values.py new file mode 100644 index 00000000..a4f90c49 --- /dev/null +++ b/tests/test_record_values.py @@ -0,0 +1,792 @@ +import multiprocessing +from typing import List +import numpy +import pytest +import asyncio + +from enum import Enum +from math import isnan, inf, nan + +from conftest import requires_cothread + +from softioc import asyncio_dispatcher, builder, softioc +from softioc.pythonSoftIoc import RecordWrapper + +# Test file for anything related to valid record values getting/setting + + +# Test parameters +DEVICE_NAME = "RECORD-VALUE-TESTS" +TIMEOUT = 5 # Seconds + +VERY_LONG_STRING = "This is a fairly long string, the kind that someone " \ + "might think to put into a record that can theoretically hold a huge " \ + "string and so lets test it and prove that shall we?" + + + +def record_func_names(fixture_value): + """Provide a nice name for the record_func fixture""" + return fixture_value.__name__ + + +@pytest.fixture( + params=[ + builder.aIn, + builder.aOut, + builder.longIn, + builder.longOut, + builder.boolIn, + builder.boolOut, + builder.Action, + builder.stringIn, + builder.stringOut, + builder.mbbIn, + builder.mbbOut, + builder.WaveformIn, + builder.WaveformOut, + builder.longStringIn, + builder.longStringOut, + ], + ids=record_func_names, +) +def record_func(request): + """The list of record creation functions""" + return request.param + +# A list of all In records, used to filter out various tests +in_records = [ + builder.aIn, + builder.boolIn, + builder.longIn, + builder.mbbIn, + builder.stringIn, + builder.WaveformIn, + builder.longStringIn, +] + +def record_values_names(fixture_value): + """Provide a nice name for the tests in the record_values fixture""" + return ( + fixture_value[1].__name__ + + "-" + + type(fixture_value[2]).__name__ + + "-" + + str(fixture_value[3]) + ) + + +record_values_list = [ + ("aIn_float", builder.aIn, 5.5, 5.5, float), + ("aOut_float", builder.aOut, 5.5, 5.5, float), + ("aIn_int", builder.aIn, 3, 3.0, float), + ("aOut_int", builder.aOut, 3, 3.0, float), + ("aIn_inf", builder.aIn, inf, inf, float), + ("aOut_inf", builder.aOut, inf, inf, float), + ("aIn_neginf", builder.aIn, -inf, -inf, float), + ("aOut_neginf", builder.aOut, -inf, -inf, float), + ("aIn_nan", builder.aIn, nan, nan, float), + ("aOut_nan", builder.aOut, nan, nan, float), + ("longIn_int", builder.longIn, 5, 5, int), + ("longOut_int", builder.longOut, 5, 5, int), + ("boolIn_int", builder.boolIn, 1, 1, int), + ("boolOut_int", builder.boolOut, 1, 1, int), + ("boolIn_true", builder.boolIn, True, 1, int), + ("boolOut_true", builder.boolOut, True, 1, int), + ("boolIn_false", builder.boolIn, False, 0, int), + ("boolOut_false", builder.boolOut, False, 0, int), + ("Action_int", builder.Action, 1, 1, int), + ("Action_true", builder.Action, True, 1, int), + ("Action_false", builder.Action, False, 0, int), + ("mbbIn_int", builder.mbbIn, 1, 1, int), + ("mbbOut_int", builder.mbbOut, 1, 1, int), + ("strIn_abc", builder.stringIn, "abc", "abc", str), + ("strOut_abc", builder.stringOut, "abc", "abc", str), + ("strIn_empty", builder.stringIn, "", "", str), + ("strOut_empty", builder.stringOut, "", "", str), + ("strin_utf8", builder.stringIn, "%a€b", "%a€b", str), # Valid UTF-8 + ("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str), # Valid UTF-8 + ( + "wIn_list_int", + builder.WaveformIn, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.int32), + numpy.ndarray, + ), + ( + "wOut_list_int", + builder.WaveformOut, + [1, 2, 3], + numpy.array([1, 2, 3], dtype=numpy.int32), + numpy.ndarray, + ), + ( + "wIn_list_float", + builder.WaveformIn, + [1.5, 2.6, 3.7], + numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wOut_list_float", + builder.WaveformOut, + [1.5, 2.6, 3.7], + numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wIn_int", + builder.WaveformIn, + 567, + numpy.array([567.], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wOut_int", + builder.WaveformOut, + 567, + numpy.array([567.], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wIn_float", + builder.WaveformIn, + 12.345, + numpy.array([12.345], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wOut_float", + builder.WaveformOut, + 12.345, + numpy.array([12.345], dtype=numpy.float64), + numpy.ndarray, + ), + ( + "wIn_bytes", + builder.WaveformIn, + b"HELLO\0WORLD", + numpy.array( + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 + ), + numpy.ndarray, + ), + ( + "wOut_bytes", + builder.WaveformOut, + b"HELLO\0WORLD", + numpy.array( + [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 + ), + numpy.ndarray, + ), + ( + "longStringIn_str", + builder.longStringIn, + "ABC", + "ABC", + str, + ), + ( + "longStringOut_str", + builder.longStringOut, + "ABC", + "ABC", + str, + ), + ( + "longStringIn_long_str", + builder.longStringIn, + VERY_LONG_STRING, + VERY_LONG_STRING, + str, + ), + ( + "longStringOut_long_str", + builder.longStringOut, + VERY_LONG_STRING, + VERY_LONG_STRING, + str, + ), + ( + "longStringIn_unicode", + builder.longStringIn, + "%a€b", + "%a€b", + str + ), + ( + "longStringOut_unicode", + builder.longStringOut, + "%a€b", + "%a€b", + str + ), +] + + +@pytest.fixture(params=record_values_list, ids=record_values_names) +def record_values(request): + """A list of parameters for record value setting/getting tests. + + Fields are: + - Record builder function + - Input value passed to .set()/initial_value/caput + - Expected output value after doing .get()/caget + - Expected type of the output value from .get()/caget""" + return request.param # One item from the params list + + + +def record_value_asserts( + creation_func, actual_value, expected_value, expected_type +): + """Asserts that the expected value and expected type are matched with + the actual value. Handles both scalar and waveform data""" + try: + if type(expected_value) == float and isnan(expected_value): + assert isnan(actual_value) # NaN != Nan, so needs special case + elif creation_func in [builder.WaveformOut, builder.WaveformIn]: + assert numpy.array_equal(actual_value, expected_value) + assert type(actual_value) == expected_type + else: + assert actual_value == expected_value + assert type(actual_value) == expected_type + except AssertionError as e: + msg = ( + "Failed with parameters: " + + str(creation_func) + + ", " + + str(actual_value) + + ", " + + str(expected_value) + + ", " + + str(expected_type) + ) + # Add the parameters into the exception message. Seems to be the + # cleanest place to add it, as doing a print() or a logging.error() + # both cause the message to appear in a separate section of the pytest + # results, which can be difficult to spot + e.args += (msg,) + raise e + + +class SetValueEnum(Enum): + """Enum to control when and how the record's value should be set""" + + INITIAL_VALUE = 1 + SET_BEFORE_INIT = 2 + SET_AFTER_INIT = 3 + NO_SET = 4 + CAPUT = 5 + + +class GetValueEnum(Enum): + """Enum to control whether values are retrieved using .get() or caget()""" + + GET = 1 + CAGET = 2 + + +def run_ioc(record_configurations: list, conn, set_enum, get_enum): + """Creates a record and starts the IOC. `initial_value` will be set on + the record at different times based on the `set_enum` parameter.""" + + builder.SetDeviceName(DEVICE_NAME) + + records: List[RecordWrapper] = [] + + # Create records from the given list + for configuration in record_configurations: + kwarg = {} + + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = configuration + + if set_enum == SetValueEnum.INITIAL_VALUE: + kwarg.update({"initial_value": initial_value}) + elif creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + # Related to this issue: + # https://github.com/dls-controls/pythonSoftIOC/issues/37 + + out_rec = creation_func(record_name, **kwarg) + + if set_enum == SetValueEnum.SET_BEFORE_INIT: + out_rec.set(initial_value) + + records.append(out_rec) + + dispatcher = asyncio_dispatcher.AsyncioDispatcher() + builder.LoadDatabase() + softioc.iocInit(dispatcher) + conn.send("IOC started") + + # Record list and record config list should always be in line + for record, configuration in zip(records, record_configurations): + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = configuration + + if set_enum == SetValueEnum.SET_AFTER_INIT: + record.set(initial_value) + + for record in records: + if get_enum == GetValueEnum.GET: + conn.send(record.get()) + # Some tests do a caput and require another .get() + if set_enum == SetValueEnum.CAPUT: + if conn.poll(TIMEOUT): + val = conn.recv() + if val is not None: + conn.send(record.get()) + + conn.close() + + # Keep process alive while main thread works. + # This is most applicable to CAGET tests. + asyncio.run_coroutine_threadsafe( + asyncio.sleep(TIMEOUT), dispatcher.loop + ).result() + + +def run_test_function( + record_configurations: list, set_enum: SetValueEnum, get_enum: GetValueEnum +): + """Run the test function using multiprocessing and check returned value is + expected value. set_enum and get_enum determine when the record's value is + set and how the value is retrieved, respectively.""" + + parent_conn, child_conn = multiprocessing.Pipe() + + ioc_process = multiprocessing.Process( + target=run_ioc, + args=(record_configurations, child_conn, set_enum, get_enum), + ) + + ioc_process.start() + + # Wait for message that IOC has started + if parent_conn.poll(TIMEOUT): + parent_conn.recv() + else: + pytest.fail("IOC process did not start before TIMEOUT expired") + + try: + # Cannot do these imports before the subprocess starts, as cothread + # isn't threadsafe (in the way we require) + from cothread import Yield + from cothread.catools import caget, caput, _channel_cache + from cothread.dbr import DBR_CHAR_STR + + # cothread remembers connected IOCs. As we potentially restart the same + # named IOC multiple times, we have to purge the cache else the + # result from caget/caput cache would be a DisconnectError during the + # second test + _channel_cache.purge() + + for configuration in record_configurations: + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = configuration + + # Infer some required keywords from parameters + kwargs = {} + put_kwarg = {} + if creation_func in [builder.longStringIn, builder.longStringOut]: + kwargs.update({"datatype": DBR_CHAR_STR}) + + if (creation_func in [builder.WaveformIn, builder.WaveformOut] + and type(initial_value) is bytes): + # There's a bug in caput that means DBR_CHAR_STR doesn't + # truncate the array of the target record, meaning .get() + # returns all NELM rather than just NORD elements. Instead we + # encode the data ourselves + initial_value = numpy.frombuffer( + initial_value, dtype = numpy.uint8) + + if set_enum == SetValueEnum.CAPUT: + if get_enum == GetValueEnum.GET: + if parent_conn.poll(TIMEOUT): + parent_conn.recv() + else: + pytest.fail("IOC did not provide initial record value") + caput( + DEVICE_NAME + ":" + record_name, + initial_value, + wait=True, + **kwargs, + **put_kwarg, + ) + + if get_enum == GetValueEnum.GET: + parent_conn.send("Do another get!") + # Ensure IOC process has time to execute. + # I saw failures on MacOS where it appeared the IOC had not + # processed the put'ted value as the caget returned the same + # value as was originally passed in. + Yield(timeout=TIMEOUT) + + if get_enum == GetValueEnum.GET: + if parent_conn.poll(TIMEOUT): + rec_val = parent_conn.recv() + else: + pytest.fail("IOC did not provide record value in queue") + else: + rec_val = caget( + DEVICE_NAME + ":" + record_name, + timeout=TIMEOUT, + **kwargs, + ) + # '+' operator used to convert cothread's types into Python + # native types e.g. "+ca_int" -> int + rec_val = +rec_val + + if ( + creation_func in [builder.WaveformOut, builder.WaveformIn] + and expected_value.dtype in [numpy.float64, numpy.int32] + ): + print( + "caget cannot distinguish between a waveform with 1" + "element and a scalar value, and so always returns a " + "scalar. Therefore we skip this check.") + continue + + + record_value_asserts( + creation_func, rec_val, expected_value, expected_type + ) + finally: + # Purge cache to suppress spurious "IOC disconnected" exceptions + _channel_cache.purge() + + ioc_process.terminate() + ioc_process.join(timeout=TIMEOUT) + if ioc_process.exitcode is None: + pytest.fail("Process did not terminate") + + +def skip_long_strings(record_values): + if ( + record_values[0] in [builder.stringIn, builder.stringOut] + and len(record_values[1]) > 40 + ): + pytest.skip("CAPut blocks strings > 40 characters.") + + +class TestGetValue: + """Tests that use .get() to check whether values applied with .set(), + initial_value, or caput return the expected value""" + + def test_value_pre_init_set(self, clear_records, record_values): + """Test that records provide the expected values on get calls when using + .set() and .get() before IOC initialisation occurs""" + + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = record_values + + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func(record_name, **kwarg) + out_rec.set(initial_value) + + record_value_asserts( + creation_func, out_rec.get(), expected_value, expected_type + ) + + def test_value_pre_init_initial_value(self, clear_records, record_values): + """Test that records provide the expected values on get calls when using + initial_value and .get() before IOC initialisation occurs""" + + ( + record_name, + creation_func, + initial_value, + expected_value, + expected_type, + ) = record_values + + out_rec = creation_func(record_name, initial_value=initial_value) + + record_value_asserts( + creation_func, out_rec.get(), expected_value, expected_type + ) + + @requires_cothread + def test_value_post_init_set(self): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and .get() after initialisation""" + run_test_function( + record_values_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.GET + ) + + @requires_cothread + def test_value_post_init_initial_value(self): + """Test that records provide the expected values on get calls when using + initial_value during record creation and .get() after IOC initialisation + """ + run_test_function( + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.GET + ) + + @requires_cothread + def test_value_post_init_set_after_init(self): + """Test that records provide the expected values on get calls when using + .set() and .get() after IOC initialisation""" + run_test_function( + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET + ) + + @requires_cothread + def test_value_post_init_caput(self): + """Test that records provide the expected values on get calls when using + caput and .get() after IOC initialisation""" + + # Various conditions mean we cannot use the entire list of cases + filtered_list = [] + for item in record_values_list: + if ( + item[1] in [builder.stringIn, builder.stringOut] + and len(item[2]) > 40 + ): + # caput blocks strings longer than 40 characters + continue + + if item[1] not in in_records: + # In records block caput + filtered_list.append(item) + + run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.GET) + + +@requires_cothread +class TestCagetValue: + """Tests that use Caget to check whether values applied with .set(), + initial_value, or caput return the expected value""" + + 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 + ) + + @requires_cothread + def test_value_post_init_initial_value(self): + """Test that records provide the expected values on get calls when using + initial_value during record creation and caget after IOC initialisation + """ + + run_test_function( + record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.CAGET + ) + + @requires_cothread + def test_value_post_init_set_after_init(self): + """Test that records provide the expected values on get calls when using + .set() and caget after IOC initialisation""" + + run_test_function( + record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.CAGET + ) + + def test_value_post_init_caput(self): + """Test that records provide the expected values on get calls when using + .set() before IOC initialisation and caget after initialisation""" + + # Various conditions (e.g.) mean we cannot use the entire list of cases + filtered_list = [] + for item in record_values_list: + # In records block caputs + if item[1] not in in_records: + filtered_list.append(item) + + run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET) + + +default_values_list = [ + ("default_aOut", builder.aOut, None, 0.0, float), + ("default_aIn", builder.aIn, None, 0.0, float), + ("default_longOut", builder.longOut, None, 0, int), + ("default_longIn", builder.longIn, None, 0, int), + ("default_boolOut", builder.boolOut, None, 0, int), + ("default_boolIn", builder.boolIn, None, 0, int), + ("default_Action", builder.Action, None, 0, int), + ("default_stringOut", builder.stringOut, None, "", str), + ("default_stringIn", builder.stringIn, None, "", str), + ("default_mbbOut", builder.mbbOut, None, 0, int), + ("default_mbbIn", builder.mbbIn, None, 0, int), + ("default_wOut", builder.WaveformOut, None, numpy.empty(0), numpy.ndarray), + ("default_wIn", builder.WaveformIn, None, numpy.empty(0), numpy.ndarray), + ("default_longStringOut", builder.longStringOut, None, "", str), + ("default_longStringIn", builder.longStringIn, None, "", str), +] + + +class TestDefaultValue: + """Tests related to default values""" + + @pytest.mark.parametrize( + "creation_func,expected_value,expected_type", + [ + (builder.aOut, 0.0, float), + (builder.aIn, 0.0, float), + (builder.longOut, 0, int), + (builder.longIn, 0, int), + (builder.boolOut, 0, int), + (builder.boolIn, 0, int), + (builder.Action, 0, int), + (builder.stringOut, "", str), + (builder.stringIn, "", str), + (builder.mbbOut, 0, int), + (builder.mbbIn, 0, int), + (builder.WaveformOut, numpy.empty(0), numpy.ndarray), + (builder.WaveformIn, numpy.empty(0), numpy.ndarray), + (builder.longStringOut, "", str), + (builder.longStringIn, "", str), + ], + ) + def test_value_default_pre_init( + self, creation_func, expected_value, expected_type, clear_records + ): + """Test that the correct default values are returned from .get() (before + record initialisation) when no initial_value or .set() is done""" + # Out records do not have default values until records are initialized + + kwarg = {} + if creation_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + out_rec = creation_func("out-record", **kwarg) + record_value_asserts( + creation_func, out_rec.get(), expected_value, expected_type + ) + + @requires_cothread + def test_value_default_post_init(self): + """Test that records return the expected default value from .get after + they are initialised, having never had a value set""" + + run_test_function( + default_values_list, SetValueEnum.NO_SET, GetValueEnum.GET + ) + + @requires_cothread + def test_value_default_post_init_caget(self): + """Test that records return the expected default value from CAGet after + they are initialised, having never had a value set""" + + run_test_function( + default_values_list, SetValueEnum.NO_SET, GetValueEnum.CAGET + ) + + +class TestNoneValue: + """Various tests regarding record value of None""" + + # We expect using None as a value will trigger one of these exceptions + expected_exceptions = (ValueError, TypeError, AttributeError) + + + @pytest.fixture + def record_func_reject_none(self, record_func): + """Parameterized fixture for all records that reject None""" + if record_func in [builder.WaveformIn, builder.WaveformOut]: + pytest.skip("None is accepted by Waveform records, as numpy " + "treats it as NaN") + return record_func + + def test_value_none_rejected_initial_value( + self, clear_records, record_func_reject_none + ): + """Test setting \"None\" as the initial_value raises an exception""" + + kwarg = {} + if record_func_reject_none in [ + builder.WaveformIn, + builder.WaveformOut, + ]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises(self.expected_exceptions): + record_func_reject_none("SOME-NAME", initial_value=None, **kwarg) + + def test_value_none_rejected_set_before_init( + self, clear_records, record_func_reject_none + ): + """Test that setting \"None\" using .set() raises an exception""" + + kwarg = {} + if record_func_reject_none in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + with pytest.raises(self.expected_exceptions): + record = record_func_reject_none("SOME-NAME", **kwarg) + record.set(None) + + def none_value_test_func(self, record_func, queue): + """Start the IOC and catch the expected exception""" + kwarg = {} + if record_func in [builder.WaveformIn, builder.WaveformOut]: + kwarg = {"length": 50} # Required when no value on creation + + record = record_func("SOME-NAME", **kwarg) + + builder.LoadDatabase() + softioc.iocInit() + + try: + record.set(None) + except Exception as e: + queue.put(e) + + queue.put(Exception("FAIL:Test did not raise exception during .set()")) + + @requires_cothread + def test_value_none_rejected_set_after_init(self, record_func_reject_none): + """Test that setting \"None\" using .set() after IOC init raises an + exception""" + queue = multiprocessing.Queue() + process = multiprocessing.Process( + target=self.none_value_test_func, + args=(record_func_reject_none, queue), + ) + + process.start() + + try: + exception = queue.get(timeout=5) + + assert isinstance(exception, self.expected_exceptions) + finally: + process.terminate() + process.join(timeout=3) diff --git a/tests/test_records.py b/tests/test_records.py index bcf67dd5..cf17ad45 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -1,268 +1,25 @@ import multiprocessing -from typing import List import numpy import os import pytest -import sys import asyncio -import logging -from enum import Enum -from math import isnan, inf, nan +from conftest import requires_cothread, _clear_records -# Must import softioc before epicsdbbuilder -from softioc.device_core import RecordLookup from softioc import asyncio_dispatcher, builder, softioc -from epicsdbbuilder import ResetRecords - -from softioc.pythonSoftIoc import RecordWrapper - -requires_cothread = pytest.mark.skipif( - sys.platform.startswith("win"), reason="Cothread doesn't work on windows" -) - +# Test file for miscellaneous tests related to records # Test parameters -DEVICE_NAME = "SOFT-IOC-TESTS" +DEVICE_NAME = "RECORD-TESTS" TIMEOUT = 5 # Seconds -VERY_LONG_STRING = "This is a fairly long string, the kind that someone " \ - "might think to put into a record that can theoretically hold a huge " \ - "string and so lets test it and prove that shall we?" - - -def _clear_records(): - # Remove any records created at epicsdbbuilder layer - ResetRecords() - # And at pythonSoftIoc level - # TODO: Remove this hack and use use whatever comes out of - # https://github.com/dls-controls/pythonSoftIOC/issues/56 - RecordLookup._RecordDirectory.clear() - - -@pytest.fixture -def clear_records(): - """Fixture to delete all records before and after a test.""" - _clear_records() - yield +def test_records(tmp_path, clear_records): + # Ensure we definitely unload all records that may be hanging over from + # previous tests, then create exactly one instance of expected records. + from sim_records import create_records _clear_records() - - -def record_func_names(fixture_value): - """Provide a nice name for the record_func fixture""" - return fixture_value.__name__ - - -@pytest.fixture( - params=[ - builder.aIn, - builder.aOut, - builder.longIn, - builder.longOut, - builder.boolIn, - builder.boolOut, - builder.Action, - builder.stringIn, - builder.stringOut, - builder.mbbIn, - builder.mbbOut, - builder.WaveformIn, - builder.WaveformOut, - builder.longStringIn, - builder.longStringOut, - ], - ids=record_func_names, -) -def record_func(request): - """The list of record creation functions""" - return request.param - -# A list of all In records, used to filter out various tests -in_records = [ - builder.aIn, - builder.boolIn, - builder.longIn, - builder.mbbIn, - builder.stringIn, - builder.WaveformIn, - builder.longStringIn, -] - -def record_values_names(fixture_value): - """Provide a nice name for the tests in the record_values fixture""" - return ( - fixture_value[1].__name__ - + "-" - + type(fixture_value[2]).__name__ - + "-" - + str(fixture_value[3]) - ) - - -record_values_list = [ - ("aIn_float", builder.aIn, 5.5, 5.5, float), - ("aOut_float", builder.aOut, 5.5, 5.5, float), - ("aIn_int", builder.aIn, 3, 3.0, float), - ("aOut_int", builder.aOut, 3, 3.0, float), - ("aIn_inf", builder.aIn, inf, inf, float), - ("aOut_inf", builder.aOut, inf, inf, float), - ("aIn_neginf", builder.aIn, -inf, -inf, float), - ("aOut_neginf", builder.aOut, -inf, -inf, float), - ("aIn_nan", builder.aIn, nan, nan, float), - ("aOut_nan", builder.aOut, nan, nan, float), - ("longIn_int", builder.longIn, 5, 5, int), - ("longOut_int", builder.longOut, 5, 5, int), - ("boolIn_int", builder.boolIn, 1, 1, int), - ("boolOut_int", builder.boolOut, 1, 1, int), - ("boolIn_true", builder.boolIn, True, 1, int), - ("boolOut_true", builder.boolOut, True, 1, int), - ("boolIn_false", builder.boolIn, False, 0, int), - ("boolOut_false", builder.boolOut, False, 0, int), - ("Action_int", builder.Action, 1, 1, int), - ("Action_true", builder.Action, True, 1, int), - ("Action_false", builder.Action, False, 0, int), - ("mbbIn_int", builder.mbbIn, 1, 1, int), - ("mbbOut_int", builder.mbbOut, 1, 1, int), - ("strIn_abc", builder.stringIn, "abc", "abc", str), - ("strOut_abc", builder.stringOut, "abc", "abc", str), - ("strIn_empty", builder.stringIn, "", "", str), - ("strOut_empty", builder.stringOut, "", "", str), - ("strin_utf8", builder.stringIn, "%a€b", "%a€b", str), # Valid UTF-8 - ("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str), # Valid UTF-8 - ( - "wIn_list_int", - builder.WaveformIn, - [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.int32), - numpy.ndarray, - ), - ( - "wOut_list_int", - builder.WaveformOut, - [1, 2, 3], - numpy.array([1, 2, 3], dtype=numpy.int32), - numpy.ndarray, - ), - ( - "wIn_list_float", - builder.WaveformIn, - [1.5, 2.6, 3.7], - numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), - numpy.ndarray, - ), - ( - "wOut_list_float", - builder.WaveformOut, - [1.5, 2.6, 3.7], - numpy.array([1.5, 2.6, 3.7], dtype=numpy.float64), - numpy.ndarray, - ), - ( - "wIn_int", - builder.WaveformIn, - 567, - numpy.array([567.], dtype=numpy.float64), - numpy.ndarray, - ), - ( - "wOut_int", - builder.WaveformOut, - 567, - numpy.array([567.], dtype=numpy.float64), - numpy.ndarray, - ), - ( - "wIn_float", - builder.WaveformIn, - 12.345, - numpy.array([12.345], dtype=numpy.float64), - numpy.ndarray, - ), - ( - "wOut_float", - builder.WaveformOut, - 12.345, - numpy.array([12.345], dtype=numpy.float64), - numpy.ndarray, - ), - ( - "wIn_bytes", - builder.WaveformIn, - b"HELLO\0WORLD", - numpy.array( - [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 - ), - numpy.ndarray, - ), - ( - "wOut_bytes", - builder.WaveformOut, - b"HELLO\0WORLD", - numpy.array( - [72, 69, 76, 76, 79, 0, 87, 79, 82, 76, 68], dtype=numpy.uint8 - ), - numpy.ndarray, - ), - ( - "longStringIn_str", - builder.longStringIn, - "ABC", - "ABC", - str, - ), - ( - "longStringOut_str", - builder.longStringOut, - "ABC", - "ABC", - str, - ), - ( - "longStringIn_long_str", - builder.longStringIn, - VERY_LONG_STRING, - VERY_LONG_STRING, - str, - ), - ( - "longStringOut_long_str", - builder.longStringOut, - VERY_LONG_STRING, - VERY_LONG_STRING, - str, - ), - ( - "longStringIn_unicode", - builder.longStringIn, - "%a€b", - "%a€b", - str - ), - ( - "longStringOut_unicode", - builder.longStringOut, - "%a€b", - "%a€b", - str - ), -] - - -@pytest.fixture(params=record_values_list, ids=record_values_names) -def record_values(request): - """A list of parameters for record value setting/getting tests. - - Fields are: - - Record builder function - - Input value passed to .set()/initial_value/caput - - Expected output value after doing .get()/caget - - Expected type of the output value from .get()/caget""" - return request.param # One item from the params list - - -def test_records(tmp_path): - import sim_records + create_records() path = str(tmp_path / "records.db") builder.WriteRecords(path) @@ -329,558 +86,6 @@ def test_DISP_can_be_overridden(): assert record.DISP.Value() == 0 -def record_value_asserts( - creation_func, actual_value, expected_value, expected_type -): - """Asserts that the expected value and expected type are matched with - the actual value. Handles both scalar and waveform data""" - try: - if type(expected_value) == float and isnan(expected_value): - assert isnan(actual_value) # NaN != Nan, so needs special case - elif creation_func in [builder.WaveformOut, builder.WaveformIn]: - assert numpy.array_equal(actual_value, expected_value) - assert type(actual_value) == expected_type - else: - assert actual_value == expected_value - assert type(actual_value) == expected_type - except AssertionError as e: - msg = ( - "Failed with parameters: " - + str(creation_func) - + ", " - + str(actual_value) - + ", " - + str(expected_value) - + ", " - + str(expected_type) - ) - # Add the parameters into the exception message. Seems to be the - # cleanest place to add it, as doing a print() or a logging.error() - # both cause the message to appear in a separate section of the pytest - # results, which can be difficult to spot - e.args += (msg,) - raise e - - -class SetValueEnum(Enum): - """Enum to control when and how the record's value should be set""" - - INITIAL_VALUE = 1 - SET_BEFORE_INIT = 2 - SET_AFTER_INIT = 3 - NO_SET = 4 - CAPUT = 5 - - -class GetValueEnum(Enum): - """Enum to control whether values are retrieved using .get() or caget()""" - - GET = 1 - CAGET = 2 - - -def run_ioc(record_configurations: list, conn, set_enum, get_enum): - """Creates a record and starts the IOC. `initial_value` will be set on - the record at different times based on the `set_enum` parameter.""" - - builder.SetDeviceName(DEVICE_NAME) - - records: List[RecordWrapper] = [] - - # Create records from the given list - for configuration in record_configurations: - kwarg = {} - - ( - record_name, - creation_func, - initial_value, - expected_value, - expected_type, - ) = configuration - - if set_enum == SetValueEnum.INITIAL_VALUE: - kwarg.update({"initial_value": initial_value}) - elif creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - # Related to this issue: - # https://github.com/dls-controls/pythonSoftIOC/issues/37 - - out_rec = creation_func(record_name, **kwarg) - - if set_enum == SetValueEnum.SET_BEFORE_INIT: - out_rec.set(initial_value) - - records.append(out_rec) - - dispatcher = asyncio_dispatcher.AsyncioDispatcher() - builder.LoadDatabase() - softioc.iocInit(dispatcher) - conn.send("IOC started") - - # Record list and record config list should always be in line - for record, configuration in zip(records, record_configurations): - ( - record_name, - creation_func, - initial_value, - expected_value, - expected_type, - ) = configuration - - if set_enum == SetValueEnum.SET_AFTER_INIT: - record.set(initial_value) - - for record in records: - if get_enum == GetValueEnum.GET: - conn.send(record.get()) - # Some tests do a caput and require another .get() - if set_enum == SetValueEnum.CAPUT: - if conn.poll(TIMEOUT): - val = conn.recv() - if val is not None: - conn.send(record.get()) - - conn.close() - - # Keep process alive while main thread works. - # This is most applicable to CAGET tests. - asyncio.run_coroutine_threadsafe( - asyncio.sleep(TIMEOUT), dispatcher.loop - ).result() - - -def run_test_function( - record_configurations: list, set_enum: SetValueEnum, get_enum: GetValueEnum -): - """Run the test function using multiprocessing and check returned value is - expected value. set_enum and get_enum determine when the record's value is - set and how the value is retrieved, respectively.""" - - parent_conn, child_conn = multiprocessing.Pipe() - - ioc_process = multiprocessing.Process( - target=run_ioc, - args=(record_configurations, child_conn, set_enum, get_enum), - ) - - ioc_process.start() - - # Wait for message that IOC has started - if parent_conn.poll(TIMEOUT): - parent_conn.recv() - else: - pytest.fail("IOC process did not start before TIMEOUT expired") - - try: - # Cannot do these imports before the subprocess starts, as cothread - # isn't threadsafe (in the way we require) - from cothread import Yield - from cothread.catools import caget, caput, _channel_cache - from cothread.dbr import DBR_CHAR_STR - - # cothread remembers connected IOCs. As we potentially restart the same - # named IOC multiple times, we have to purge the cache else the - # result from caget/caput cache would be a DisconnectError during the - # second test - _channel_cache.purge() - - for configuration in record_configurations: - ( - record_name, - creation_func, - initial_value, - expected_value, - expected_type, - ) = configuration - - # Infer some required keywords from parameters - kwargs = {} - put_kwarg = {} - if creation_func in [builder.longStringIn, builder.longStringOut]: - kwargs.update({"datatype": DBR_CHAR_STR}) - - if (creation_func in [builder.WaveformIn, builder.WaveformOut] - and type(initial_value) is bytes): - # There's a bug in caput that means DBR_CHAR_STR doesn't - # truncate the array of the target record, meaning .get() - # returns all NELM rather than just NORD elements. Instead we - # encode the data ourselves - initial_value = numpy.frombuffer( - initial_value, dtype = numpy.uint8) - - if set_enum == SetValueEnum.CAPUT: - if get_enum == GetValueEnum.GET: - if parent_conn.poll(TIMEOUT): - parent_conn.recv() - else: - pytest.fail("IOC did not provide initial record value") - caput( - DEVICE_NAME + ":" + record_name, - initial_value, - wait=True, - **kwargs, - **put_kwarg, - ) - - if get_enum == GetValueEnum.GET: - parent_conn.send("Do another get!") - # Ensure IOC process has time to execute. - # I saw failures on MacOS where it appeared the IOC had not - # processed the put'ted value as the caget returned the same - # value as was originally passed in. - Yield(timeout=TIMEOUT) - - if get_enum == GetValueEnum.GET: - if parent_conn.poll(TIMEOUT): - rec_val = parent_conn.recv() - else: - pytest.fail("IOC did not provide record value in queue") - else: - rec_val = caget( - DEVICE_NAME + ":" + record_name, - timeout=TIMEOUT, - **kwargs, - ) - # '+' operator used to convert cothread's types into Python - # native types e.g. "+ca_int" -> int - rec_val = +rec_val - - if ( - creation_func in [builder.WaveformOut, builder.WaveformIn] - and expected_value.dtype in [numpy.float64, numpy.int32] - ): - print( - "caget cannot distinguish between a waveform with 1" - "element and a scalar value, and so always returns a " - "scalar. Therefore we skip this check.") - continue - - - record_value_asserts( - creation_func, rec_val, expected_value, expected_type - ) - finally: - # Purge cache to suppress spurious "IOC disconnected" exceptions - _channel_cache.purge() - - ioc_process.terminate() - ioc_process.join(timeout=TIMEOUT) - if ioc_process.exitcode is None: - pytest.fail("Process did not terminate") - - -def skip_long_strings(record_values): - if ( - record_values[0] in [builder.stringIn, builder.stringOut] - and len(record_values[1]) > 40 - ): - pytest.skip("CAPut blocks strings > 40 characters.") - - -class TestGetValue: - """Tests that use .get() to check whether values applied with .set(), - initial_value, or caput return the expected value""" - - def test_value_pre_init_set(self, clear_records, record_values): - """Test that records provide the expected values on get calls when using - .set() and .get() before IOC initialisation occurs""" - - ( - record_name, - creation_func, - initial_value, - expected_value, - expected_type, - ) = record_values - - kwarg = {} - if creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - - out_rec = creation_func(record_name, **kwarg) - out_rec.set(initial_value) - - record_value_asserts( - creation_func, out_rec.get(), expected_value, expected_type - ) - - def test_value_pre_init_initial_value(self, clear_records, record_values): - """Test that records provide the expected values on get calls when using - initial_value and .get() before IOC initialisation occurs""" - - ( - record_name, - creation_func, - initial_value, - expected_value, - expected_type, - ) = record_values - - out_rec = creation_func(record_name, initial_value=initial_value) - - record_value_asserts( - creation_func, out_rec.get(), expected_value, expected_type - ) - - @requires_cothread - def test_value_post_init_set(self): - """Test that records provide the expected values on get calls when using - .set() before IOC initialisation and .get() after initialisation""" - run_test_function( - record_values_list, SetValueEnum.SET_BEFORE_INIT, GetValueEnum.GET - ) - - @requires_cothread - def test_value_post_init_initial_value(self): - """Test that records provide the expected values on get calls when using - initial_value during record creation and .get() after IOC initialisation - """ - run_test_function( - record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.GET - ) - - @requires_cothread - def test_value_post_init_set_after_init(self): - """Test that records provide the expected values on get calls when using - .set() and .get() after IOC initialisation""" - run_test_function( - record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.GET - ) - - @requires_cothread - def test_value_post_init_caput(self): - """Test that records provide the expected values on get calls when using - caput and .get() after IOC initialisation""" - - # Various conditions mean we cannot use the entire list of cases - filtered_list = [] - for item in record_values_list: - if ( - item[1] in [builder.stringIn, builder.stringOut] - and len(item[2]) > 40 - ): - # caput blocks strings longer than 40 characters - continue - - if item[1] not in in_records: - # In records block caput - filtered_list.append(item) - - run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.GET) - - -@requires_cothread -class TestCagetValue: - """Tests that use Caget to check whether values applied with .set(), - initial_value, or caput return the expected value""" - - 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 - ) - - @requires_cothread - def test_value_post_init_initial_value(self): - """Test that records provide the expected values on get calls when using - initial_value during record creation and caget after IOC initialisation - """ - - run_test_function( - record_values_list, SetValueEnum.INITIAL_VALUE, GetValueEnum.CAGET - ) - - @requires_cothread - def test_value_post_init_set_after_init(self): - """Test that records provide the expected values on get calls when using - .set() and caget after IOC initialisation""" - - run_test_function( - record_values_list, SetValueEnum.SET_AFTER_INIT, GetValueEnum.CAGET - ) - - def test_value_post_init_caput(self): - """Test that records provide the expected values on get calls when using - .set() before IOC initialisation and caget after initialisation""" - - # Various conditions (e.g.) mean we cannot use the entire list of cases - filtered_list = [] - for item in record_values_list: - # In records block caputs - if item[1] not in in_records: - filtered_list.append(item) - - run_test_function(filtered_list, SetValueEnum.CAPUT, GetValueEnum.CAGET) - - -default_values_list = [ - ("default_aOut", builder.aOut, None, 0.0, float), - ("default_aIn", builder.aIn, None, 0.0, float), - ("default_longOut", builder.longOut, None, 0, int), - ("default_longIn", builder.longIn, None, 0, int), - ("default_boolOut", builder.boolOut, None, 0, int), - ("default_boolIn", builder.boolIn, None, 0, int), - ("default_Action", builder.Action, None, 0, int), - ("default_stringOut", builder.stringOut, None, "", str), - ("default_stringIn", builder.stringIn, None, "", str), - ("default_mbbOut", builder.mbbOut, None, 0, int), - ("default_mbbIn", builder.mbbIn, None, 0, int), - ("default_wOut", builder.WaveformOut, None, numpy.empty(0), numpy.ndarray), - ("default_wIn", builder.WaveformIn, None, numpy.empty(0), numpy.ndarray), - ("default_longStringOut", builder.longStringOut, None, "", str), - ("default_longStringIn", builder.longStringIn, None, "", str), -] - - -class TestDefaultValue: - """Tests related to default values""" - - @pytest.mark.parametrize( - "creation_func,expected_value,expected_type", - [ - (builder.aOut, 0.0, float), - (builder.aIn, 0.0, float), - (builder.longOut, 0, int), - (builder.longIn, 0, int), - (builder.boolOut, 0, int), - (builder.boolIn, 0, int), - (builder.Action, 0, int), - (builder.stringOut, "", str), - (builder.stringIn, "", str), - (builder.mbbOut, 0, int), - (builder.mbbIn, 0, int), - (builder.WaveformOut, numpy.empty(0), numpy.ndarray), - (builder.WaveformIn, numpy.empty(0), numpy.ndarray), - (builder.longStringOut, "", str), - (builder.longStringIn, "", str), - ], - ) - def test_value_default_pre_init( - self, creation_func, expected_value, expected_type, clear_records - ): - """Test that the correct default values are returned from .get() (before - record initialisation) when no initial_value or .set() is done""" - # Out records do not have default values until records are initialized - - kwarg = {} - if creation_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - - out_rec = creation_func("out-record", **kwarg) - record_value_asserts( - creation_func, out_rec.get(), expected_value, expected_type - ) - - @requires_cothread - def test_value_default_post_init(self): - """Test that records return the expected default value from .get after - they are initialised, having never had a value set""" - - run_test_function( - default_values_list, SetValueEnum.NO_SET, GetValueEnum.GET - ) - - @requires_cothread - def test_value_default_post_init_caget(self): - """Test that records return the expected default value from CAGet after - they are initialised, having never had a value set""" - - run_test_function( - default_values_list, SetValueEnum.NO_SET, GetValueEnum.CAGET - ) - - -class TestNoneValue: - """Various tests regarding record value of None""" - - # We expect using None as a value will trigger one of these exceptions - expected_exceptions = (ValueError, TypeError, AttributeError) - - - @pytest.fixture - def record_func_reject_none(self, record_func): - """Parameterized fixture for all records that reject None""" - if record_func in [builder.WaveformIn, builder.WaveformOut]: - pytest.skip("None is accepted by Waveform records, as numpy " - "treats it as NaN") - return record_func - - def test_value_none_rejected_initial_value( - self, clear_records, record_func_reject_none - ): - """Test setting \"None\" as the initial_value raises an exception""" - - kwarg = {} - if record_func_reject_none in [ - builder.WaveformIn, - builder.WaveformOut, - ]: - kwarg = {"length": 50} # Required when no value on creation - - with pytest.raises(self.expected_exceptions): - record_func_reject_none("SOME-NAME", initial_value=None, **kwarg) - - def test_value_none_rejected_set_before_init( - self, clear_records, record_func_reject_none - ): - """Test that setting \"None\" using .set() raises an exception""" - - kwarg = {} - if record_func_reject_none in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - - with pytest.raises(self.expected_exceptions): - record = record_func_reject_none("SOME-NAME", **kwarg) - record.set(None) - - def none_value_test_func(self, record_func, queue): - """Start the IOC and catch the expected exception""" - kwarg = {} - if record_func in [builder.WaveformIn, builder.WaveformOut]: - kwarg = {"length": 50} # Required when no value on creation - - record = record_func("SOME-NAME", **kwarg) - - builder.LoadDatabase() - softioc.iocInit() - - try: - record.set(None) - except Exception as e: - queue.put(e) - - queue.put(Exception("FAIL:Test did not raise exception during .set()")) - - @requires_cothread - def test_value_none_rejected_set_after_init(self, record_func_reject_none): - """Test that setting \"None\" using .set() after IOC init raises an - exception""" - queue = multiprocessing.Queue() - process = multiprocessing.Process( - target=self.none_value_test_func, - args=(record_func_reject_none, queue), - ) - - process.start() - - try: - exception = queue.get(timeout=5) - - assert isinstance(exception, self.expected_exceptions) - finally: - process.terminate() - process.join(timeout=3) def validate_fixture_names(params): """Provide nice names for the out_records fixture in TestValidate class""" From 98de8f675cb51bd85ffda88dd162dfdbd83de2a3 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 13 Jan 2022 15:32:35 +0000 Subject: [PATCH 127/131] Add tests for issue #55 --- tests/test_records.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_records.py b/tests/test_records.py index cf17ad45..e74fd202 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -85,7 +85,31 @@ def test_DISP_can_be_overridden(): # Note: DISP attribute won't exist if field not specified assert record.DISP.Value() == 0 +def test_waveform_disallows_zero_length(clear_records): + """Test that WaveformIn/Out records throw an exception when being + initialized with a zero length array""" + with pytest.raises(AssertionError): + builder.WaveformIn("W_IN", []) + with pytest.raises(AssertionError): + builder.WaveformOut("W_OUT", []) + +def test_waveform_disallows_too_long_values(clear_records): + """Test that Waveform and longString records throw an exception when + an overlong value is written""" + w_in = builder.WaveformIn("W_IN", [1, 2, 3]) + w_out = builder.WaveformOut("W_OUT", [1, 2, 3]) + + ls_in = builder.longStringIn("LS_IN", initial_value="ABC") + ls_out = builder.longStringOut("LS_OUT", initial_value="ABC") + with pytest.raises(AssertionError): + w_in.set([1, 2, 3, 4]) + with pytest.raises(AssertionError): + w_out.set([1, 2, 3, 4]) + with pytest.raises(AssertionError): + ls_in.set("ABCD") + with pytest.raises(AssertionError): + ls_out.set("ABCD") def validate_fixture_names(params): """Provide nice names for the out_records fixture in TestValidate class""" From dd99311a29f863b5814bf6a09f732fd44fb4a942 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 25 Jan 2022 09:46:44 +0000 Subject: [PATCH 128/131] Add coverage to Multiprocess tests --- tests/conftest.py | 11 +++++++++++ tests/test_record_values.py | 7 ++++++- tests/test_records.py | 5 ++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b8689d2b..94faccf8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -82,3 +82,14 @@ def clear_records(): _clear_records() yield _clear_records() + +def enable_code_coverage(): + """Ensure code coverage works as expected for `multiprocesses` tests. + This method should be called just before any `multiprocess.Process` call + is made.""" + try: + from pytest_cov.embed import cleanup_on_sigterm + except ImportError: + pass + else: + cleanup_on_sigterm() diff --git a/tests/test_record_values.py b/tests/test_record_values.py index a4f90c49..17742c00 100644 --- a/tests/test_record_values.py +++ b/tests/test_record_values.py @@ -7,7 +7,7 @@ from enum import Enum from math import isnan, inf, nan -from conftest import requires_cothread +from conftest import requires_cothread, enable_code_coverage from softioc import asyncio_dispatcher, builder, softioc from softioc.pythonSoftIoc import RecordWrapper @@ -368,6 +368,8 @@ def run_test_function( parent_conn, child_conn = multiprocessing.Pipe() + enable_code_coverage() + ioc_process = multiprocessing.Process( target=run_ioc, args=(record_configurations, child_conn, set_enum, get_enum), @@ -776,6 +778,9 @@ def test_value_none_rejected_set_after_init(self, record_func_reject_none): """Test that setting \"None\" using .set() after IOC init raises an exception""" queue = multiprocessing.Queue() + + enable_code_coverage() + process = multiprocessing.Process( target=self.none_value_test_func, args=(record_func_reject_none, queue), diff --git a/tests/test_records.py b/tests/test_records.py index e74fd202..95d91013 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -4,7 +4,7 @@ import pytest import asyncio -from conftest import requires_cothread, _clear_records +from conftest import requires_cothread, _clear_records, enable_code_coverage from softioc import asyncio_dispatcher, builder, softioc @@ -181,6 +181,9 @@ def validate_test_runner( validate_pass: bool): queue = multiprocessing.Queue() + + enable_code_coverage() + process = multiprocessing.Process( target=self.validate_ioc_test_func, args=(creation_func, queue, validate_pass), From adaa2868794c9f3281d159ba0e0d2cb427f4cd55 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 25 Jan 2022 10:18:20 +0000 Subject: [PATCH 129/131] Restore original epicscorelibs dependency With the release of p4p 3.5.5 there is no longer a version conflict. See discussion here: https://github.com/dls-controls/pythonSoftIOC/pull/60#discussion_r790567382 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3b8d8a1..c08a4d05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs==7.0.6.99.1.0"] +requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.6.99.1.0"] build-backend = "setuptools.build_meta:__legacy__" From e12572f0189f462a96e1c4b358691c3145750b03 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 25 Jan 2022 12:46:19 +0000 Subject: [PATCH 130/131] Update lock file with epicscorelibs changes Looks like the last version of this file is very old - aioca was still at version 1.2 despite being 1.3 in setup.cfg for a long time. --- Pipfile.lock | 700 ++++++++++++++++++++++++++------------------------- 1 file changed, 364 insertions(+), 336 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index a057c09d..1794c8eb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,10 +16,10 @@ "default": { "aioca": { "hashes": [ - "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", - "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" + "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae", + "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d" ], - "version": "==1.2" + "version": "==1.3" }, "cothread": { "hashes": [ @@ -29,69 +29,78 @@ }, "epicscorelibs": { "hashes": [ - "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870", - "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c", - "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800", - "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4", - "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1", - "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be", - "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88", - "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab", - "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c", - "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351", - "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120", - "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3", - "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4", - "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92", - "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb", - "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0", - "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3", - "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358", - "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76", - "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3", - "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131", - "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552", - "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd", - "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681", - "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4" - ], - "version": "==7.0.6.99.1.0" + "sha256:01d45ae25d844afe71893838f36ea649ccc3e1e72a96076b33242437efb1ce13", + "sha256:0269ba469aa8117d86e865b09e9f3b3afdb891f5f544a90d8af037039015b995", + "sha256:1c6f00360ab2e89081c031ff499f5695bd687102a561160cf6304b64cf325ca5", + "sha256:1cbed1cb9b7fa3744723dfb78a1aaa7ff9d475e9dee6e6e21ebb1e682421cd35", + "sha256:223515de1da16fd7a06892e41a1e5bc870b57bbf1a0a4ac3dc3e069b647d7313", + "sha256:329179971288722eb7f7d085fced198ff30468cc225d9677f08175074680b4ef", + "sha256:32f91502afa9639fe576fa5bfa137352ee04f929d2887a2c7c169c24c0658808", + "sha256:3c8958b7c3385dc190948af429a7c10d18e71142d749776250341e1db6b3d05b", + "sha256:3eb16a21760110e0d159e7ee0f97a36250a83abc0ca6ec08806d4ad148c48761", + "sha256:3f07babd8ff1c7499cbe9f95dd80a8cbee45f9f38bda2f6c72855c4c647d2dc7", + "sha256:5766a0dafcd02af7ecead064d5502282eac1c66afdddf7235ae822228eedac11", + "sha256:5a18ab11d8a565b9ea9e035218b5ac44b86d21162d7260ecb54fad43f506d70f", + "sha256:64dd24d80b8dd1bca73ce8ffe0673306cf7018dba61883bd1b7b4ff179ac0623", + "sha256:773d51ade8a9c44333a56aae1224afe4c238750643c5a05a461186d9ce7f4cf6", + "sha256:86921315b855993c0316eb938586c8b99acc21ba5ed26ac0cb66d32e1682f89b", + "sha256:941643c2e90665305deddc4a7fe99dceb5488c80d1c31bd4a96169a92adea73f", + "sha256:9a4b401e22a96b9cacc2429f67179fed4a1414e5629f78865d6c63d92ec19439", + "sha256:9be6cf03ad466812cd659fa2a6af1c8efd33946abdf0a2c5f8901df2289f2dda", + "sha256:a9e07998cca913e6cf5f448c37dc1c2f29acc5fec4aca19ed80cd127b65e5d69", + "sha256:ac260dbd4b28a53c8ebac4f902d03448432b46d3fff7809488a63263b7efbd05", + "sha256:b0d606a0c0c020387136b1b19091d661b1b2f5763377d6a0b13152351f42ad53", + "sha256:b64b38b4f48601054a7bd0423d1559c1a2281cb962fd4cd4c1088f49ac29b2b7", + "sha256:b69e91fe38f818a71fd4fbf22ac17d577bdce914d4940883586e57d629030328", + "sha256:c3deebf918866f5bdd4fbc5f52b1c89c96121619573698442876eecdf1f9a985", + "sha256:cbb5a14534e68eb2a10904ba020191c858860f2dfebbdff969d160a29020724e", + "sha256:d729c35c29681acc3b666e59f041e6bd228c931b8303507bb046e331e5f11f67", + "sha256:f165e4754249ee12bd38ba68ed72dc27764b28a02654c224c690f68b95519455", + "sha256:f40ae0290be4d84c560f848f4b0340b3c9ff466bd52fcbd11899199fa0be9b4b" + ], + "version": "==7.0.6.99.2.0" }, "epicsdbbuilder": { "hashes": [ - "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", - "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" + "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", + "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" ], - "version": "==1.4.3" + "version": "==1.5" }, "numpy": { "hashes": [ - "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", - "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", - "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", - "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", - "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", - "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", - "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", - "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", - "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", - "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", - "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", - "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", - "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", - "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", - "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", - "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", - "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", - "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", - "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", - "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", - "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", - "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", - "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", - "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" - ], - "version": "==1.20.2" + "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb", + "sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939", + "sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc", + "sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced", + "sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441", + "sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b", + "sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166", + "sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640", + "sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13", + "sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8", + "sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5", + "sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611", + "sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76", + "sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c", + "sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7", + "sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a", + "sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5", + "sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee", + "sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449", + "sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02", + "sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a", + "sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815", + "sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593", + "sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e", + "sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa", + "sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954", + "sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354", + "sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9", + "sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a", + "sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089" + ], + "version": "==1.21.5" }, "scipy": { "hashes": [ @@ -133,20 +142,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" ], - "version": "==3.10.0.0" + "version": "==4.0.1" } }, "develop": { "aioca": { "hashes": [ - "sha256:2f01c5fd327916d6c13c7a6106189fd8411ee52f6e34b276368cb831252734f0", - "sha256:f5c6f1d1774f5cd96bd7827eda7f9f6a71f735699b424745c3fd8293146211ae" + "sha256:34621c9952f6cd94b061871264ce592d693dd91b88cf77f3e297d7de37c546ae", + "sha256:94e7b6946ca71e27b57c0fe65a747634a2dc487e15f8a020ae8c2b14b29fe79d" ], - "version": "==1.2" + "version": "==1.3" }, "alabaster": { "hashes": [ @@ -171,17 +179,18 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" ], - "version": "==4.0.0" + "markers": "python_version >= '3'", + "version": "==2.0.7" }, "cothread": { "hashes": [ @@ -190,119 +199,120 @@ "version": "==2.17" }, "coverage": { - "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" - ], - "version": "==5.5" + "extras": [ + "toml" + ], + "hashes": [ + "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9", + "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b", + "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e", + "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a", + "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2", + "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67", + "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685", + "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f", + "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7", + "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717", + "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5", + "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b", + "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef", + "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489", + "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b", + "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e", + "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926", + "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271", + "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54", + "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026", + "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad", + "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533", + "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6", + "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8", + "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26", + "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b", + "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8", + "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9", + "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271", + "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003", + "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a", + "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0", + "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d", + "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a", + "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0", + "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9", + "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f", + "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33", + "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a", + "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6", + "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201", + "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336", + "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce", + "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4", + "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9", + "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07" + ], + "version": "==6.1.1" }, "docutils": { "hashes": [ - "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", - "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", + "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" ], - "version": "==0.16" + "version": "==0.17.1" }, "epicscorelibs": { "hashes": [ - "sha256:04d5118bef5d811fd7edfb07e932b16ffb119b099aa9c438f0796f8c43fde870", - "sha256:15877ff982ded4f18bce10df603f0c8d92a1721028e53af69d06c14f1b55437c", - "sha256:18bc2cea2c532337afc9ba1cd4c029dcbe89e025dea064f3fcff8fc3c2980800", - "sha256:1aae34eab3634f1e7ca5c95a3f54f92491381dbc54d426ba704412be998b9ea4", - "sha256:1db0a62b83dc5ac94b3593f86248c5532277bc48247c9f5112cf199aafe920d1", - "sha256:1e36e8de2abe67c23ff8fcb41912468847f7e4785969b42ef1075f697295c0be", - "sha256:27ece87b4fabd04e536dbbf083461274ed61e2d132307526ca3cca4a98359b88", - "sha256:2aea6ad97b7772f11f94ba7446ad860f4cdd1146cf30e0950ef34f0d6134baab", - "sha256:317dfa567eece806eecefeafe2a7eb6ac90db4f216f39763c39854959ff4e72c", - "sha256:43d4d7701a0bada7ed921ec3c6af069ea02137b55268252ebca49207a7ef6351", - "sha256:4906fbaa76f89dee3d51b051a78fbd4bf12b80cdee4543d741a63b9999add120", - "sha256:49763288bc8fadf7807c8ab19ed0e19795287205537ba463c214c5d0c15b25e3", - "sha256:511fad578f2eddbf9fe163666d2171081fe1785440633eba311f2d531a544de4", - "sha256:521b05a72b66955bc80a19c8c0e96e1daa12cf5b68b340e40d7cf49dd34b6a92", - "sha256:54d4aa0d367e6a639aaa787a4468a33fe359d1dd62bcd10aef7c1e8afc8bd4eb", - "sha256:79a68c491e79984b7b966325686f43d9a1e59ebfb593debedb110ed49735e0e0", - "sha256:7c53db21b5809be7fb6b6f93187103ab200d2a41a0851e7e827fcdb6b7f699b3", - "sha256:8071e1866b57ff6e2579e74dcc53af87421c98bad4684501f526afefc14b7358", - "sha256:874fee009d4a392d4e6fd6e550b8d43ab63ad303c78b3d000267a34c33da4f76", - "sha256:8f803ceaec535741fcd7ff7adccf8dfbca4ca5aeef60e85a0f36f36d16e57fb3", - "sha256:cc4e4158e4ebc288b9d3890c6f8cb14fec38f40bd8ce3f239d9a9e566890d131", - "sha256:cf638d6ccb0cbc82f14c8ed4d0c56f55317ebc5b272a6dec0fbf51e76dfad552", - "sha256:e3d3733e2c433d599a71c9784ff1fdc031fddaeb8e8c3a7d094064b46252f9dd", - "sha256:e45621fd3c9bc46e820835594213313e71f636966c3f2832f031ca7d5466c681", - "sha256:e571e90306b8a8248c182763e94d1a8f17573288f5e61ac6aad35da7597404c4" - ], - "version": "==7.0.6.99.1.0" + "sha256:01d45ae25d844afe71893838f36ea649ccc3e1e72a96076b33242437efb1ce13", + "sha256:0269ba469aa8117d86e865b09e9f3b3afdb891f5f544a90d8af037039015b995", + "sha256:1c6f00360ab2e89081c031ff499f5695bd687102a561160cf6304b64cf325ca5", + "sha256:1cbed1cb9b7fa3744723dfb78a1aaa7ff9d475e9dee6e6e21ebb1e682421cd35", + "sha256:223515de1da16fd7a06892e41a1e5bc870b57bbf1a0a4ac3dc3e069b647d7313", + "sha256:329179971288722eb7f7d085fced198ff30468cc225d9677f08175074680b4ef", + "sha256:32f91502afa9639fe576fa5bfa137352ee04f929d2887a2c7c169c24c0658808", + "sha256:3c8958b7c3385dc190948af429a7c10d18e71142d749776250341e1db6b3d05b", + "sha256:3eb16a21760110e0d159e7ee0f97a36250a83abc0ca6ec08806d4ad148c48761", + "sha256:3f07babd8ff1c7499cbe9f95dd80a8cbee45f9f38bda2f6c72855c4c647d2dc7", + "sha256:5766a0dafcd02af7ecead064d5502282eac1c66afdddf7235ae822228eedac11", + "sha256:5a18ab11d8a565b9ea9e035218b5ac44b86d21162d7260ecb54fad43f506d70f", + "sha256:64dd24d80b8dd1bca73ce8ffe0673306cf7018dba61883bd1b7b4ff179ac0623", + "sha256:773d51ade8a9c44333a56aae1224afe4c238750643c5a05a461186d9ce7f4cf6", + "sha256:86921315b855993c0316eb938586c8b99acc21ba5ed26ac0cb66d32e1682f89b", + "sha256:941643c2e90665305deddc4a7fe99dceb5488c80d1c31bd4a96169a92adea73f", + "sha256:9a4b401e22a96b9cacc2429f67179fed4a1414e5629f78865d6c63d92ec19439", + "sha256:9be6cf03ad466812cd659fa2a6af1c8efd33946abdf0a2c5f8901df2289f2dda", + "sha256:a9e07998cca913e6cf5f448c37dc1c2f29acc5fec4aca19ed80cd127b65e5d69", + "sha256:ac260dbd4b28a53c8ebac4f902d03448432b46d3fff7809488a63263b7efbd05", + "sha256:b0d606a0c0c020387136b1b19091d661b1b2f5763377d6a0b13152351f42ad53", + "sha256:b64b38b4f48601054a7bd0423d1559c1a2281cb962fd4cd4c1088f49ac29b2b7", + "sha256:b69e91fe38f818a71fd4fbf22ac17d577bdce914d4940883586e57d629030328", + "sha256:c3deebf918866f5bdd4fbc5f52b1c89c96121619573698442876eecdf1f9a985", + "sha256:cbb5a14534e68eb2a10904ba020191c858860f2dfebbdff969d160a29020724e", + "sha256:d729c35c29681acc3b666e59f041e6bd228c931b8303507bb046e331e5f11f67", + "sha256:f165e4754249ee12bd38ba68ed72dc27764b28a02654c224c690f68b95519455", + "sha256:f40ae0290be4d84c560f848f4b0340b3c9ff466bd52fcbd11899199fa0be9b4b" + ], + "version": "==7.0.6.99.2.0" }, "epicsdbbuilder": { "hashes": [ - "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", - "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" + "sha256:40e01ca308b667d17b31dc1907816df20c31b389415268d9ec6e2be6c3b8f283", + "sha256:ae8dc724c72478d2c6a68b08145d027a50af98702d17e4692f2d73f145818e74" ], - "version": "==1.4.3" + "version": "==1.5" }, "flake8": { "hashes": [ - "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", - "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" + "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", + "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" ], - "version": "==3.9.1" + "version": "==4.0.1" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "version": "==2.10" + "markers": "python_version >= '3'", + "version": "==3.3" }, "imagesize": { "hashes": [ @@ -313,11 +323,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:4a5611fea3768d3d967c447ab4e93f567d95db92225b43b7b238dbfb855d70bb", - "sha256:c6513572926a96458f8c8f725bf0e00108fba0c9583ade9bd15b869c9d726e33" + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" ], "markers": "python_version < '3.8' and python_version < '3.8'", - "version": "==4.6.0" + "version": "==4.0.1" }, "iniconfig": { "hashes": [ @@ -328,67 +338,69 @@ }, "jinja2": { "hashes": [ - "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", - "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45", + "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c" ], - "version": "==2.11.3" + "version": "==3.0.2" }, "markupsafe": { "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" - ], - "version": "==1.1.1" + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "version": "==2.0.1" }, "mccabe": { "hashes": [ @@ -397,86 +409,91 @@ ], "version": "==0.6.1" }, - "nose": { + "nose2": { "hashes": [ - "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", - "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", - "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + "sha256:886ba617a96de0130c54b24479bd5c2d74d5c940d40f3809c3a275511a0c4a60", + "sha256:aa620e759f2c5018d9ba041340391913e282ecebd3c392027f1575847b093ec6" ], - "version": "==1.3.7" + "version": "==0.10.0" }, "numpy": { "hashes": [ - "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", - "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", - "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", - "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", - "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", - "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", - "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", - "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", - "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", - "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", - "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", - "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", - "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", - "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", - "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", - "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", - "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", - "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", - "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", - "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", - "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", - "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", - "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", - "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" - ], - "version": "==1.20.2" + "sha256:00c9fa73a6989895b8815d98300a20ac993c49ac36c8277e8ffeaa3631c0dbbb", + "sha256:025b497014bc33fc23897859350f284323f32a2fff7654697f5a5fc2a19e9939", + "sha256:08de8472d9f7571f9d51b27b75e827f5296295fa78817032e84464be8bb905bc", + "sha256:1964db2d4a00348b7a60ee9d013c8cb0c566644a589eaa80995126eac3b99ced", + "sha256:2a9add27d7fc0fdb572abc3b2486eb3b1395da71e0254c5552b2aad2a18b5441", + "sha256:2d8adfca843bc46ac199a4645233f13abf2011a0b2f4affc5c37cd552626f27b", + "sha256:301e408a052fdcda5cdcf03021ebafc3c6ea093021bf9d1aa47c54d48bdad166", + "sha256:311283acf880cfcc20369201bd75da907909afc4666966c7895cbed6f9d2c640", + "sha256:341dddcfe3b7b6427a28a27baa59af5ad51baa59bfec3264f1ab287aa3b30b13", + "sha256:3a5098df115340fb17fc93867317a947e1dcd978c3888c5ddb118366095851f8", + "sha256:3c978544be9e04ed12016dd295a74283773149b48f507d69b36f91aa90a643e5", + "sha256:3d893b0871322eaa2f8c7072cdb552d8e2b27645b7875a70833c31e9274d4611", + "sha256:4fe6a006557b87b352c04596a6e3f12a57d6e5f401d804947bd3188e6b0e0e76", + "sha256:507c05c7a37b3683eb08a3ff993bd1ee1e6c752f77c2f275260533b265ecdb6c", + "sha256:58ca1d7c8aef6e996112d0ce873ac9dfa1eaf4a1196b4ff7ff73880a09923ba7", + "sha256:61bada43d494515d5b122f4532af226fdb5ee08fe5b5918b111279843dc6836a", + "sha256:69a5a8d71c308d7ef33ef72371c2388a90e3495dbb7993430e674006f94797d5", + "sha256:6a5928bc6241264dce5ed509e66f33676fc97f464e7a919edc672fb5532221ee", + "sha256:7b9d6b14fc9a4864b08d1ba57d732b248f0e482c7b2ff55c313137e3ed4d8449", + "sha256:a7c4b701ca418cd39e28ec3b496e6388fe06de83f5f0cb74794fa31cfa384c02", + "sha256:a7e8f6216f180f3fd4efb73de5d1eaefb5f5a1ee5b645c67333033e39440e63a", + "sha256:b545ebadaa2b878c8630e5bcdb97fc4096e779f335fc0f943547c1c91540c815", + "sha256:c293d3c0321996cd8ffe84215ffe5d269fd9d1d12c6f4ffe2b597a7c30d3e593", + "sha256:c5562bcc1a9b61960fc8950ade44d00e3de28f891af0acc96307c73613d18f6e", + "sha256:ca9c23848292c6fe0a19d212790e62f398fd9609aaa838859be8459bfbe558aa", + "sha256:cc1b30205d138d1005adb52087ff45708febbef0e420386f58664f984ef56954", + "sha256:dbce7adeb66b895c6aaa1fad796aaefc299ced597f6fbd9ceddb0dd735245354", + "sha256:dc4b2fb01f1b4ddbe2453468ea0719f4dbb1f5caa712c8b21bb3dd1480cd30d9", + "sha256:eed2afaa97ec33b4411995be12f8bdb95c87984eaa28d76cf628970c8a2d689a", + "sha256:fc7a7d7b0ed72589fd8b8486b9b42a564f10b8762be8bd4d9df94b807af4a089" + ], + "version": "==1.21.5" }, "p4p": { "hashes": [ - "sha256:0143a3a3d29bf03f7c828c28d025db81961e9933c750bd9d3d8e8b59cb8b71a3", - "sha256:160aa3dd5ca55583e929bcc939e0479dfca655fba440dc9b931373e733848959", - "sha256:1c688e35e30294c79bfebbda8c460ffb5486cdb64ccbeae93060e6de667942a4", - "sha256:1d58c9149b85e0fd19e00167fb047a04c1e649d394cf8fdcb7a172be73de3f6d", - "sha256:25e98f73ae4cebc3c305f2b11b0e6689d002594fda9330bf99c6414d0a01753f", - "sha256:2b4e193737cb69a20fbed2f86360275ce5dcce5ca83439e1f98b3af5f1aed1cc", - "sha256:30d0e083b5dfecee7693d197125daac3e5da9466e50dea0d01d5356abf5c6143", - "sha256:34ab524b4ce32dc67ef33421db644d6713ed677f601cbbef7007957f9cb6eb9c", - "sha256:3e257761347bf14e66f7a07bb4fda803c1293c3ca8b26baf5818ad66e2d46abb", - "sha256:3ec002d4e04ea5c99b33840f71c1042c33d4b13b20e32980b7a0a7eb71195ec4", - "sha256:79bd0bcb586d043a3565238be2f002ddf125bb710b4a46b330632a706d36ba08", - "sha256:7a004c518ba6abbb4ff96647e5ced0e1329d965448cbdc23709a8b6a3d040b26", - "sha256:977dc659a16481603716c655b7cae406e79d7788bd387b64994c198d3a27e19f", - "sha256:99c9dc61af4e8323bb77418d0c868717a838d424081156ac31b95957965f901f", - "sha256:a922287fd85973bbc900f629f5d475fe9be37eb2d3929fe81405c3ebcd950807", - "sha256:bf58b00da603378027cb1af265d0d0ccfa40cdc02263bdebbb4f56837d809512", - "sha256:c20592bb15a087add723d84484d95a9e428045a20afc7a791ccdad3e48113a39", - "sha256:c46c615896fa0daea5a5942c4b7b51be2bf3f8dfdcc2c05e256f2c957e3353eb", - "sha256:c745d18146bf48762806d724b8f265c046196ddb0a1dd15c7aee0919362cbb63", - "sha256:c9d5769c31b2700ece9c04aa8c0fc2efe95af8d473b35307d03210ff31d28831", - "sha256:cfd500cfa8d3f227704e8f4ff6bafee66bc5459fc56ece7792c334b92be2d35a", - "sha256:dda7008629e8232a372b798197dcebc5583eadc0ed643568543ee4afd0387928", - "sha256:f014907d2fd51ce132a29b1e869ea4058ead0cc14bcc2ed1690a08752c72ba62", - "sha256:f4677018469c7b98434dd465f1210f2ebd13a23d7084187666393a45b2ff7f26", - "sha256:f4ce7f6dd0e45f1acc05e0a633b3e18045b576c43394790719dcafb71a5daa13" - ], - "version": "==3.5.4" + "sha256:06c95f97b531e6dee05cf1080192c0310b72bcc6bb1a5da5bbe0295567abd1ae", + "sha256:0a817f4385c1d119f68bf701b4a73fdad7a273dfa7c7825db63930eda64ce8e7", + "sha256:0fc35bb3ad6b8e2c48d5f94ca1a18d60a763993384d03204d814ac63b7aa7ee1", + "sha256:1779a995e07ba561b8bb0b9474b9043861fc304c19afdcfc552fb758b783277f", + "sha256:178bd8e66b6771be6d270230c8169420ed05c5fb506fb05fc1983b2db469119b", + "sha256:270966fb4ecbf532a09c375c23404d171bb2d05169c7f2d00aa74d791091c21f", + "sha256:2951deb6d551d1a91535a7eaf91962834404218b97cfd5439b22ed9c71dd1214", + "sha256:325f888902e03a443090bd19fd8bd3f8a064989a9cdab0389fb60d6b461a189d", + "sha256:38c849f27d0acd042113cbde4d4d9682b1c45c62ca9c569201e70f128268b5d2", + "sha256:3950bef6486b2cf5690a7f0efbea233540582ebb5ac72736ff76e55048b5bed0", + "sha256:3da53fcc002f2d7b855f7fa075f74cba3fbbeb41f0a81958f45559334440819b", + "sha256:7466c57f66ba39f5c6c17425b0003d5eda2be98ddc5342aca99c02986a0bf185", + "sha256:84dcd9ca598665069bfdb3df1bef3fce98ea1a7ca5ef377cc6f55b1391daec1d", + "sha256:8f7e0577628c15e10ffd8da21e3699391bf5a25a28c5a531bccf614618bbfe3d", + "sha256:98325753e7d38dac9e0ad643814e37162e451c55bf9cd3ff3c5b94c2e6aff1df", + "sha256:9dc3cb1119efd1c29cf5e7a20ecd17dc184fdf66fa7b39e87d10344ecb8001fd", + "sha256:a4f65b20fccc77608591c90675bcc22aaaf128f20b9a648aea00afd592bccfe9", + "sha256:a5fddf5eedfed9d577f651ad53947d31738d893a485e3981ce710ca7c63ab547", + "sha256:b338048f1e96f4c3de6d58a668eeab3667276879db5589838118c174272358bd", + "sha256:b3dd72c29d1a89e9f96de0b3d67fc2dd824acfb026f3ff1603a024d229f7f404", + "sha256:bae9918e7793198b20507cc4a7b4ec4428c51ee33b28db96232ac178ea12400f", + "sha256:d714d0bf14ed9e0ca7cc5e9d4e6cb390c1ddcc3d8b7aeff23c09b0b3f6635e2c", + "sha256:d72f2951878c53e2f3f2e349bc8788f3e11a980e1f044f9c69153494318b340a", + "sha256:e69211b5f9e6aa93cf552491144aec561c3cdf6bf59c1f4013329556f13461f0", + "sha256:fd787fa25b5536d054bebc5e15f80da1948d0d6e9fe83848050eb557f5d35fc8" + ], + "version": "==3.5.5" }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", + "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" ], - "version": "==20.9" + "version": "==21.2" }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "version": "==0.13.1" + "version": "==1.0.0" }, "ply": { "hashes": [ @@ -494,24 +511,24 @@ }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" ], - "version": "==2.7.0" + "version": "==2.8.0" }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", + "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" ], - "version": "==2.3.1" + "version": "==2.4.0" }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" ], - "version": "==2.9.0" + "version": "==2.10.0" }, "pyparsing": { "hashes": [ @@ -522,25 +539,24 @@ }, "pytest": { "hashes": [ - "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", - "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], - "version": "==6.2.3" + "version": "==6.2.5" }, "pytest-asyncio": { "hashes": [ - "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", - "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" + "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4", + "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d" ], - "markers": "python_version > '3.0'", - "version": "==0.14.0" + "version": "==0.17.2" }, "pytest-cov": { "hashes": [ - "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", - "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" ], - "version": "==2.11.1" + "version": "==3.0.0" }, "pytest-flake8": { "hashes": [ @@ -551,17 +567,17 @@ }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", + "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" ], - "version": "==2021.1" + "version": "==2021.3" }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "version": "==2.25.1" + "version": "==2.26.0" }, "setuptools-dso": { "hashes": [ @@ -570,6 +586,13 @@ ], "version": "==2.1" }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "version": "==1.16.0" + }, "snowballstemmer": { "hashes": [ "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", @@ -586,25 +609,24 @@ }, "sphinx": { "hashes": [ - "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", - "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" + "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c", + "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851" ], - "version": "==4.1.2" + "version": "==4.3.2" }, "sphinx-rtd-theme": { "hashes": [ - "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", - "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" + "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", + "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c" ], - "version": "==0.5.2" + "version": "==1.0.0" }, "sphinx-rtd-theme-github-versions": { "hashes": [ - "sha256:ad2822c08b0f075c051e25b9d291152d846c21ed052db3b458d5ef30efac59b3", - "sha256:f84b45baf536c01c2e62f26e8206dd69f0375d9c9b3769d56295292e6e00f43b" + "sha256:0df27ae9a9cd902468c808dbee5a43f4db8ce43cbcf2ecc78d2fe47698bb0ded", + "sha256:23018e51a5d27ef4f69dd86314f73b19088f2cfd91c74a24db1517832233dc07" ], - "markers": "python_version > '3.0'", - "version": "==0.2" + "version": "==1.1" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -655,27 +677,33 @@ ], "version": "==0.10.2" }, + "tomli": { + "hashes": [ + "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee", + "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade" + ], + "version": "==1.2.2" + }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" ], - "version": "==3.10.0.0" + "version": "==4.0.1" }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], - "version": "==1.26.5" + "version": "==1.26.7" }, "zipp": { "hashes": [ - "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", - "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", + "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" ], - "version": "==3.4.1" + "version": "==3.6.0" } } } From 0dd154415586c65c424f454c99feffdf3699d759 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Tue, 25 Jan 2022 14:17:01 +0000 Subject: [PATCH 131/131] Add procedure to update all dependencies Taken from email from Tom Cobb --- CONTRIBUTING.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2606bfb3..a9ba7ece 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -26,6 +26,21 @@ While 100% code coverage does not make a library bug-free, it significantly reduces the number of easily caught bugs! Please make sure coverage remains the same or is improved by a pull request! + +Updating Dependencies +--------------------- + +pythonSoftIoc has a variety of types of dependencies, from Python module to +C files and built libraries. If it is necessary to update any of them, the +safest way to do that is:: + + $ Ensure all changes are committed/saved/backed up + $ pipenv --rm + $ pipenv run gitclean + $ rm Pipfile.lock + $ Update dependencies in setup.cfg / pyproject.toml + $ pipenv install --dev + Code Styling ------------