Skip to content

Commit 01308e0

Browse files
Add exception on overlong string setting
Also add more tests
1 parent 01163d2 commit 01308e0

File tree

6 files changed

+109
-23
lines changed

6 files changed

+109
-23
lines changed

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"type": "python",
1010
"request": "launch",
1111
"program": "${file}",
12-
"console": "integratedTerminal"
12+
"console": "integratedTerminal",
13+
"justMyCode": false,
1314
},
1415
{
1516
"name": "Debug Unit Test",

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Added:
3030

3131
- `Allow "status" and "severity" on In record init <../../pull/111>`_
3232
- `Allow users to reset the list of created records <../../pull/114>`_
33+
- `Allow arrays of strings to be used with Waveforms <../../pull/102>`_
3334

3435
4.1.0_ - 2022-08-05
3536
-------------------

docs/reference/api.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,11 @@ All functions return a wrapped `ProcessDeviceSupportIn` or
342342
field type name. Otherwise the field type is taken from the initial value
343343
if given, or defaults to ``'FLOAT'``.
344344

345+
.. note::
346+
When storing arrays of strings, it is possible to store Unicode characters.
347+
However, as EPICS has no Unicode support the resultant values will be stored
348+
as byte strings. Care must be taken when encoding/decoding the values.
349+
345350

346351
The following functions generates specialised records.
347352

softioc/builder.py

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

1212
from . import device, pythonSoftIoc # noqa
1313
# Re-export this so users only have to import the builder
14-
from .device import SetBlocking # noqa
14+
from .device import SetBlocking, to_epics_str_array # noqa
1515

1616
PythonDevice = pythonSoftIoc.PythonDevice()
1717

@@ -148,7 +148,7 @@ def Action(name, **fields):
148148
'uint32': 'ULONG',
149149
'float32': 'FLOAT',
150150
'float64': 'DOUBLE',
151-
'bytes320': 'STRING', # Numpy's term for a 40-character string (40*8 bits)
151+
'bytes320': 'STRING', # Numpy term for 40-character byte str (40*8 bits)
152152
}
153153

154154
# Coverts FTVL string to numpy type
@@ -220,8 +220,8 @@ def _waveform(value, fields):
220220
initial_value = numpy.require(initial_value, numpy.int32)
221221
elif initial_value.dtype == numpy.uint64:
222222
initial_value = numpy.require(initial_value, numpy.uint32)
223-
elif initial_value.dtype.char in ("S", "U"):
224-
initial_value = numpy.require(initial_value, numpy.dtype("S40"))
223+
elif initial_value.dtype.char in ('S', 'U'):
224+
initial_value = to_epics_str_array(initial_value)
225225
else:
226226
initial_value = numpy.array([], dtype = datatype)
227227
length = _get_length(fields)

softioc/device.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,20 +351,28 @@ class ao(ProcessDeviceSupportOut):
351351
_ctype_ = c_double
352352
_dbf_type_ = fields.DBF_DOUBLE
353353

354+
def to_epics_str_array(value):
355+
"""Convert the given array of Python strings to an array of EPICS
356+
nul-terminated strings"""
357+
result = numpy.empty(len(value), 'S40')
358+
359+
for n, s in enumerate(value):
360+
if isinstance(s, str):
361+
val = EpicsString._ctype_()
362+
val.value = s.encode() + b'\0'
363+
result[n] = val.value
364+
else:
365+
result[n] = s
366+
return result
367+
354368

355369
def _require_waveform(value, dtype):
356370
if isinstance(value, bytes):
357371
# Special case hack for byte arrays. Surprisingly tricky:
358372
value = numpy.frombuffer(value, dtype = numpy.uint8)
359373

360374
if dtype and dtype.char == 'S':
361-
result = numpy.empty(len(value), 'S40')
362-
for n, s in enumerate(value):
363-
if isinstance(s, str):
364-
result[n] = s.encode('UTF-8')
365-
else:
366-
result[n] = s
367-
return result
375+
return to_epics_str_array(value)
368376

369377
value = numpy.require(value, dtype = dtype)
370378
if value.shape == ():

tests/test_record_values.py

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"string and so lets test it and prove that shall we?"
3333

3434
# The numpy dtype of all arrays of strings
35-
DTYPE_STRING = "S40"
35+
NUMPY_DTYPE_STRING = "S40"
3636

3737

3838
def record_func_names(fixture_value):
@@ -116,8 +116,6 @@ def record_values_names(fixture_value):
116116
("strOut_39chars", builder.stringOut, MAX_LEN_STR, MAX_LEN_STR, str),
117117
("strIn_empty", builder.stringIn, "", "", str),
118118
("strOut_empty", builder.stringOut, "", "", str),
119-
# TODO: Add Invalid-utf8 tests?
120-
# TODO: Add tests for bytes-strings to stringIn/Out?
121119
("strin_utf8", builder.stringIn, "%a€b", "%a€b", str), # Valid UTF-8
122120
("strOut_utf8", builder.stringOut, "%a€b", "%a€b", str), # Valid UTF-8
123121
(
@@ -194,15 +192,12 @@ def record_values_names(fixture_value):
194192
),
195193
numpy.ndarray,
196194
),
197-
198-
# TODO: Unicode versions of below tests?
199-
200195
(
201196
"wIn_byte_string_array",
202197
builder.WaveformIn,
203198
[b"AB123", b"CD456", b"EF789"],
204199
numpy.array(
205-
["AB123", "CD456", "EF789"], dtype=DTYPE_STRING
200+
["AB123", "CD456", "EF789"], dtype=NUMPY_DTYPE_STRING
206201
),
207202
numpy.ndarray,
208203
),
@@ -211,7 +206,35 @@ def record_values_names(fixture_value):
211206
builder.WaveformOut,
212207
[b"12AB", b"34CD", b"56EF"],
213208
numpy.array(
214-
["12AB", "34CD", "56EF"], dtype=DTYPE_STRING
209+
["12AB", "34CD", "56EF"], dtype=NUMPY_DTYPE_STRING
210+
),
211+
numpy.ndarray,
212+
),
213+
(
214+
"wIn_unicode_string_array",
215+
builder.WaveformIn,
216+
["12€½", "34¾²", "56¹³"],
217+
numpy.array(
218+
[
219+
b'12\xe2\x82\xac\xc2\xbd',
220+
b'34\xc2\xbe\xc2\xb2',
221+
b'56\xc2\xb9\xc2\xb3'
222+
],
223+
dtype=NUMPY_DTYPE_STRING
224+
),
225+
numpy.ndarray,
226+
),
227+
(
228+
"wOut_unicode_string_array",
229+
builder.WaveformOut,
230+
["12€½", "34¾²", "56¹³"],
231+
numpy.array(
232+
[
233+
b'12\xe2\x82\xac\xc2\xbd',
234+
b'34\xc2\xbe\xc2\xb2',
235+
b'56\xc2\xb9\xc2\xb3'
236+
],
237+
dtype=NUMPY_DTYPE_STRING
215238
),
216239
numpy.ndarray,
217240
),
@@ -220,7 +243,7 @@ def record_values_names(fixture_value):
220243
builder.WaveformIn,
221244
["123abc", "456def", "7890ghi"],
222245
numpy.array(
223-
["123abc", "456def", "7890ghi"], dtype=DTYPE_STRING
246+
["123abc", "456def", "7890ghi"], dtype=NUMPY_DTYPE_STRING
224247
),
225248
numpy.ndarray,
226249
),
@@ -229,7 +252,7 @@ def record_values_names(fixture_value):
229252
builder.WaveformOut,
230253
["123abc", "456def", "7890ghi"],
231254
numpy.array(
232-
["123abc", "456def", "7890ghi"], dtype=DTYPE_STRING
255+
["123abc", "456def", "7890ghi"], dtype=NUMPY_DTYPE_STRING
233256
),
234257
numpy.ndarray,
235258
),
@@ -539,10 +562,19 @@ def is_valid(configuration):
539562
"scalar. Therefore we skip this check.")
540563
continue
541564

565+
# caget on a waveform of strings will return unicode. Have to
566+
# convert it manually to binary.
542567
if isinstance(rec_val, numpy.ndarray) and len(rec_val) > 1 \
543568
and rec_val.dtype.char in ["S", "U"]:
569+
result = numpy.empty(len(rec_val), NUMPY_DTYPE_STRING)
570+
for n, s in enumerate(rec_val):
571+
if isinstance(s, str):
572+
result[n] = s.encode('UTF-8', errors= 'ignore')
573+
else:
574+
result[n] = s
575+
rec_val = result
544576
# caget won't retrieve the full length 40 buffer
545-
rec_val = rec_val.astype(DTYPE_STRING)
577+
rec_val = rec_val.astype(NUMPY_DTYPE_STRING)
546578

547579
record_value_asserts(
548580
creation_func, rec_val, expected_value, expected_type
@@ -958,7 +990,46 @@ def test_waveform_rejects_overlong_values(self):
958990
w_in = builder.WaveformIn("W_IN", [1, 2, 3])
959991
w_out = builder.WaveformOut("W_OUT", [1, 2, 3])
960992

993+
w_in_str = builder.WaveformIn("W_IN_STR", ["ABC", "DEF"])
994+
w_out_str = builder.WaveformOut("W_OUT_STR", ["ABC", "DEF"])
995+
961996
with pytest.raises(AssertionError):
962997
w_in.set([1, 2, 3, 4])
963998
with pytest.raises(AssertionError):
964999
w_out.set([1, 2, 3, 4])
1000+
with pytest.raises(AssertionError):
1001+
w_in_str.set(["ABC", "DEF", "GHI"])
1002+
with pytest.raises(AssertionError):
1003+
w_out_str.set(["ABC", "DEF", "GHI"])
1004+
1005+
def test_waveform_rejects_late_strings(self):
1006+
"""Test that a waveform won't allow a list of strings to be assigned
1007+
if no list was given in initial waveform construction"""
1008+
w_in = builder.WaveformIn("W_IN", length=10)
1009+
w_out = builder.WaveformOut("W_OUT", length=10)
1010+
1011+
with pytest.raises(ValueError):
1012+
w_in.set(["ABC", "DEF"])
1013+
with pytest.raises(ValueError):
1014+
w_out.set(["ABC", "DEF"])
1015+
1016+
def test_waveform_rejects_long_array_of_strings(self):
1017+
"""Test that a waveform of strings won't accept too long strings"""
1018+
w_in = builder.WaveformIn(
1019+
"W_IN",
1020+
initial_value=["123abc", "456def", "7890ghi"]
1021+
)
1022+
w_out = builder.WaveformIn(
1023+
"W_OUT",
1024+
initial_value=["123abc", "456def", "7890ghi"]
1025+
)
1026+
1027+
with pytest.raises(AssertionError):
1028+
w_in.set(["1", "2", "3", "4"])
1029+
with pytest.raises(AssertionError):
1030+
w_out.set(["1", "2", "3", "4"])
1031+
1032+
with pytest.raises(ValueError):
1033+
w_in.set([VERY_LONG_STRING])
1034+
with pytest.raises(ValueError):
1035+
w_out.set([VERY_LONG_STRING])

0 commit comments

Comments
 (0)