Skip to content

Commit 2fbf5bb

Browse files
committed
unit tests
1 parent e035c04 commit 2fbf5bb

5 files changed

Lines changed: 499 additions & 0 deletions

tests/test_datatype_conversion.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import struct
2+
3+
import numpy as np
4+
5+
from sciopy.datatype_conversion import (
6+
bytelist_to_int,
7+
byteintarray_to_float,
8+
bytesarray_to_double,
9+
bytesarray_to_float,
10+
del_hex_in_list,
11+
four_byte_to_int,
12+
two_byte_to_int,
13+
)
14+
15+
16+
def test_hex_list_is_normalized_to_two_digits():
17+
assert del_hex_in_list(["0x0", "0xa", "0xff", 1]).tolist() == [
18+
"00",
19+
"0a",
20+
"ff",
21+
"01",
22+
]
23+
24+
25+
def test_float_conversions_use_network_byte_order():
26+
float_bytes = struct.pack("!f", 12.5)
27+
double_bytes = struct.pack("!d", -0.125)
28+
29+
assert byteintarray_to_float(float_bytes) == 12.5
30+
assert bytesarray_to_float([f"{value:02x}" for value in float_bytes]) == 12.5
31+
assert bytesarray_to_double([f"{value:02x}" for value in double_bytes]) == -0.125
32+
33+
34+
def test_integer_conversions_include_every_byte():
35+
assert two_byte_to_int([0x12, 0x34]) == 0x1234
36+
assert four_byte_to_int([0x12, 0x34, 0x56, 0x78]) == 0x12345678
37+
assert bytelist_to_int([0x12, 0x34]) == 0x1234
38+
assert bytelist_to_int(np.array([0x12, 0x34, 0x56])) == 0x123456
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from unittest.mock import Mock
2+
3+
from sciopy.EIT_16_32_64_128 import EIT_16_32_64_128
4+
5+
6+
def test_system_message_callback_uses_configured_message_parser():
7+
device = EIT_16_32_64_128(16)
8+
device.print_msg = True
9+
device.cMessageParser = Mock()
10+
device.cMessageParser.read_usb_till_timeout.return_value = []
11+
12+
result = device.SystemMessageCallback()
13+
14+
assert result == []
15+
assert device.cMessageParser.bPrintMessages is True
16+
device.cMessageParser.read_usb_till_timeout.assert_called_once_with(
17+
bSaveData=False,
18+
bDeleteDataFrame=True,
19+
bStartReset=False,
20+
)
21+
22+
23+
def test_get_device_info_does_not_require_measurement_setup():
24+
device = EIT_16_32_64_128(16)
25+
device.serial_protocol = "HS"
26+
device.device = Mock()
27+
device.cMessageParser = Mock()
28+
29+
device.GetDeviceInfo()
30+
31+
device.device.write_data.assert_called_once_with(bytearray([0xD1, 0x00, 0xD1]))
32+
device.cMessageParser.read_usb_till_timeout.assert_called_once_with(
33+
bSaveData=False,
34+
bDeleteDataFrame=True,
35+
bStartReset=False,
36+
)
37+
38+
39+
def test_read_message_uses_parser_transport_without_recursion():
40+
device = EIT_16_32_64_128(16)
41+
device.cMessageParser = Mock()
42+
device.cMessageParser.device_read.return_value = bytearray([0x18, 0x01, 0x83, 0x18])
43+
44+
assert device.read_message() == bytearray([0x18, 0x01, 0x83, 0x18])
45+
device.cMessageParser.device_read.assert_called_once_with()
46+
47+
48+
def test_update_measurement_mode_builds_valid_command():
49+
device = EIT_16_32_64_128(16)
50+
device.write_command_string = Mock()
51+
52+
device.update_measurement_mode("skip4", "external")
53+
54+
device.write_command_string.assert_called_once_with(
55+
bytearray([0xB0, 0x03, 0x08, 0x04, 0x02, 0xB0])
56+
)
57+
58+
59+
def test_get_measurement_setup_builds_valid_command():
60+
device = EIT_16_32_64_128(16)
61+
device.write_command_string = Mock()
62+
63+
device.GetMeasurementSetup(0x02)
64+
65+
device.write_command_string.assert_called_once_with(
66+
bytearray([0xB1, 0x01, 0x02, 0xB1])
67+
)
68+
69+
70+
def test_measurement_requires_setup_before_parser_reset():
71+
device = EIT_16_32_64_128(16)
72+
device.cMessageParser = Mock()
73+
74+
try:
75+
device.StartStopMeasurement()
76+
except RuntimeError as error:
77+
assert str(error) == "SetMeasurementSetup must be called before measuring."
78+
else:
79+
raise AssertionError("StartStopMeasurement accepted a missing setup")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pickle
2+
from types import SimpleNamespace
3+
4+
import numpy as np
5+
6+
from sciopy.doteit import list_eit_files, load_pickle_to_dict
7+
from sciopy.visualization import norm_data
8+
9+
10+
def test_list_eit_files_is_filtered_case_insensitively_and_sorted(tmp_path):
11+
(tmp_path / "b.eit").touch()
12+
(tmp_path / "A.EIT").touch()
13+
(tmp_path / "ignored.txt").touch()
14+
15+
assert list_eit_files(tmp_path) == ["A.EIT", "b.eit"]
16+
17+
18+
def test_load_pickle_to_dict_reads_pickled_objects(tmp_path):
19+
path = tmp_path / "sample.pickle"
20+
with path.open("wb") as file:
21+
pickle.dump(SimpleNamespace(value=42), file)
22+
23+
assert load_pickle_to_dict(path) == {"value": 42}
24+
25+
26+
def test_norm_data_handles_constant_arrays():
27+
assert np.array_equal(norm_data(np.array([5.0, 5.0])), np.array([0.0, 0.0]))
28+
29+
30+
def test_norm_data_scales_to_unit_interval():
31+
assert np.allclose(norm_data(np.array([2.0, 4.0, 6.0])), [0.0, 0.5, 1.0])

tests/test_isx_3.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
from unittest.mock import Mock
2+
import struct
3+
4+
import numpy as np
5+
6+
from sciopy.ISX_3 import ISXMeasurement, ISX_3
7+
from sciopy.sciopy_dataclasses import EisMeasurementSetup
8+
9+
10+
def command_device():
11+
device = ISX_3()
12+
device.send_command = Mock(return_value=[])
13+
return device
14+
15+
16+
def test_frame_builder_uses_sciospec_framing():
17+
assert ISX_3.build_frame(0xB0, [0x02, 0x01, 0x00, 0x01]) == bytearray(
18+
[0xB0, 0x04, 0x02, 0x01, 0x00, 0x01, 0xB0]
19+
)
20+
21+
22+
def test_stream_parser_handles_partial_and_multiple_frames():
23+
device = ISX_3()
24+
first = bytes([0x18, 0x01, 0x83, 0x18])
25+
second = bytes([0xBA, 0x04, 0x00, 0x00, 0x03, 0xE8, 0xBA])
26+
27+
assert device.parse_received_bytes(first[:2]) == []
28+
assert device.parse_received_bytes(first[2:] + second) == [
29+
list(first),
30+
list(second),
31+
]
32+
33+
34+
def test_set_and_get_frontend_settings_follow_manual():
35+
device = command_device()
36+
37+
device.SetFE_Settings("4-point", "bnc", "auto", "1v")
38+
device.GetFE_Settings()
39+
40+
assert device.send_command.call_args_list == [
41+
((0xB0, [0x02, 0x01, 0x00, 0x01]),),
42+
((0xB1,),),
43+
]
44+
assert device.decode_frame([0xB1, 0x04, 0x02, 0x01, 0x04, 0x02, 0xB1]) == {
45+
"measurement_mode": 0x02,
46+
"measurement_channel": 0x01,
47+
"current_range": 0x04,
48+
"voltage_range": 0x02,
49+
}
50+
51+
52+
def test_options_update_measurement_layout_state():
53+
device = command_device()
54+
55+
device.SetOptions("timestamp_us", True)
56+
device.SetOptions("current_range", True)
57+
58+
assert device.timestamp_mode == "us"
59+
assert device.current_range_output is True
60+
assert device.send_command.call_args_list == [
61+
((0x97, [0x02, 0x01]),),
62+
((0x97, [0x04, 0x01]),),
63+
]
64+
65+
66+
def test_add_frequency_point_encodes_float_and_extended_options():
67+
device = command_device()
68+
69+
device.AddFrequencyPoint(
70+
32_000,
71+
1.0,
72+
0.25,
73+
point_delay=1000,
74+
phase_sync=True,
75+
excitation_type="voltage",
76+
)
77+
78+
payload = device.send_command.call_args.args[1]
79+
assert device.send_command.call_args.args[0] == 0xB6
80+
assert payload[0] == 0x02
81+
assert struct.unpack(">fff", payload[1:13]) == (32_000.0, 1.0, 0.25)
82+
assert payload[13:] == bytearray(
83+
[0x01, 0x00, 0x00, 0x03, 0xE8, 0x02, 0, 0, 0, 1, 0x03, 0, 0, 0, 1]
84+
)
85+
86+
87+
def test_add_frequency_list_encodes_documented_setup():
88+
device = command_device()
89+
90+
device.AddFrequencyList(1_000, 10_000, 10, "log", 1.0, 0.25)
91+
92+
tag, payload = device.send_command.call_args.args
93+
assert tag == 0xB6
94+
assert payload[0] == 0x03
95+
assert struct.unpack(">fff", payload[1:13]) == (1_000.0, 10_000.0, 10.0)
96+
assert payload[13] == 0x01
97+
assert struct.unpack(">ff", payload[14:22]) == (1.0, 0.25)
98+
99+
100+
def test_eis_dataclass_configures_setup():
101+
device = command_device()
102+
setup = EisMeasurementSetup(
103+
start=100,
104+
stop=100_000,
105+
step=20,
106+
stepmode="log",
107+
avg=1,
108+
amplitude=0.1,
109+
precision=1,
110+
measurement_time=0,
111+
)
112+
113+
device.SetMeasurementSetup(setup)
114+
115+
assert device.setup is setup
116+
assert device.send_command.call_args_list[0] == ((0xB6, [0x01]),)
117+
assert device.send_command.call_args_list[1].args[0] == 0xB6
118+
119+
120+
def test_measurement_without_optional_fields_is_decoded():
121+
device = ISX_3()
122+
payload = [0x00, 0x02, *struct.pack(">ff", 123.5, -4.25)]
123+
124+
result = device.decode_frame([0xB8, len(payload), *payload, 0xB8])
125+
126+
assert result == ISXMeasurement(frequency_id=2, impedance=123.5 - 4.25j)
127+
128+
129+
def test_measurement_with_microsecond_timestamp_and_range_is_decoded():
130+
device = ISX_3()
131+
device.timestamp_mode = "us"
132+
device.current_range_output = True
133+
payload = [0x00, 0x03, 0, 0, 0, 1, 2, 0x04, *struct.pack(">ff", 10, 2)]
134+
135+
result = device.decode_frame([0xB8, len(payload), *payload, 0xB8])
136+
137+
assert result == ISXMeasurement(
138+
frequency_id=3,
139+
impedance=10 + 2j,
140+
timestamp=258,
141+
timestamp_unit="us",
142+
current_range=0x04,
143+
)
144+
145+
146+
def test_setup_response_decoders():
147+
device = ISX_3()
148+
frequencies = struct.pack(">ff", 100.0, 1_000.0)
149+
150+
assert device.decode_frame([0xB7, 0x03, 0x01, 0x00, 0x10, 0xB7]) == {
151+
"frequency_count": 16
152+
}
153+
result = device.decode_frame([0xB7, 0x09, 0x04, *frequencies, 0xB7])
154+
assert np.allclose(result["frequencies_hz"], [100, 1_000])
155+
156+
157+
def test_frequency_point_response_decodes_extended_options():
158+
device = ISX_3()
159+
data = [
160+
0x02,
161+
*struct.pack(">fff", 32_000, 1.0, 0.25),
162+
0x01,
163+
0,
164+
0,
165+
3,
166+
0xE8,
167+
0x03,
168+
0,
169+
0,
170+
0,
171+
1,
172+
]
173+
174+
result = device.decode_frame([0xB7, len(data), *data, 0xB7])
175+
176+
assert result == {
177+
"frequency_hz": 32_000.0,
178+
"precision": 1.0,
179+
"amplitude": 0.25,
180+
"point_delay_us": 1_000,
181+
"excitation_type": 1,
182+
}
183+
184+
185+
def test_compensation_record_encodes_three_complex_values():
186+
device = command_device()
187+
188+
device.SetCompensationData(2, 3, 1 + 2j, 3 + 4j, 5 + 6j)
189+
190+
tag, payload = device.send_command.call_args.args
191+
assert tag == 0xB6
192+
assert payload[:5] == bytearray([0x17, 0x02, 0x02, 0x00, 0x03])
193+
assert struct.unpack(">ffffff", payload[5:]) == (1, 2, 3, 4, 5, 6)
194+
195+
196+
def test_sync_ethernet_and_watchdog_commands():
197+
device = command_device()
198+
199+
device.SetSyncTime(1_000)
200+
device.SetEthernetConfiguration(ip_address="192.168.1.25")
201+
device.SetEthernetConfiguration(dhcp=True)
202+
device.GetEthernetConfiguration("mac_address")
203+
device.SetTCPWatchdog(60)
204+
205+
assert device.send_command.call_args_list == [
206+
((0xB9, bytes([0, 0, 3, 0xE8])),),
207+
((0xBD, [0x01, 192, 168, 1, 25]),),
208+
((0xBD, [0x03, 1]),),
209+
((0xBE, [0x02]),),
210+
((0xCF, [0x00, 0, 0, 0, 60]),),
211+
]
212+
213+
214+
def test_firmware_and_device_information_are_decoded():
215+
device = ISX_3()
216+
217+
arm = device.decode_frame([0xD0, 0x06, 1, 2, 0, 3, 0, 4, 0xD0])
218+
identity = device.decode_frame([0xD1, 0x07, 1, 0, 7, 0x12, 0x34, 14, 9, 0xD1])
219+
220+
assert arm == {"developer_information": "0102", "revision": 3, "build": 4}
221+
assert identity == {
222+
"information_version": 1,
223+
"device_identifier": 7,
224+
"serial_number": 0x1234,
225+
"delivery_year": 2024,
226+
"delivery_month": 9,
227+
"developer_information": "",
228+
}
229+
230+
231+
def test_invalid_protocol_values_are_rejected():
232+
device = command_device()
233+
234+
for action in (
235+
lambda: device.SetFE_Settings("invalid", "bnc", "auto"),
236+
lambda: device.SetSyncTime(180_000_001),
237+
lambda: device.SetTCPWatchdog(0),
238+
lambda: device.SetDCBiasValue(1.1),
239+
):
240+
try:
241+
action()
242+
except ValueError:
243+
pass
244+
else:
245+
raise AssertionError("invalid protocol value was accepted")

0 commit comments

Comments
 (0)