|
1 | 1 | import asyncio
|
2 |
| -import multiprocessing |
| 2 | +import subprocess |
| 3 | +import sys |
3 | 4 | import numpy
|
4 | 5 | import os
|
5 | 6 | import pytest
|
@@ -347,7 +348,6 @@ def test_record_wrapper_str():
|
347 | 348 | # If we never receive R it probably means an assert failed
|
348 | 349 | select_and_recv(parent_conn, "R")
|
349 | 350 |
|
350 |
| - |
351 | 351 | def validate_fixture_names(params):
|
352 | 352 | """Provide nice names for the out_records fixture in TestValidate class"""
|
353 | 353 | return params[0].__name__
|
@@ -1104,3 +1104,114 @@ async def test_set_too_long_value(self):
|
1104 | 1104 | log(f"PARENT: Join completed with exitcode {process.exitcode}")
|
1105 | 1105 | if process.exitcode is None:
|
1106 | 1106 | pytest.fail("Process did not terminate")
|
| 1107 | + |
| 1108 | + |
| 1109 | +class TestRecursiveSet: |
| 1110 | + """Tests related to recursive set() calls. See original issue here: |
| 1111 | + https://github.com/dls-controls/pythonSoftIOC/issues/119""" |
| 1112 | + |
| 1113 | + recursive_record_name = "RecursiveLongOut" |
| 1114 | + |
| 1115 | + def recursive_set_func(self, device_name, conn): |
| 1116 | + from cothread import Event |
| 1117 | + |
| 1118 | + def useless_callback(value): |
| 1119 | + log("CHILD: In callback ", value) |
| 1120 | + useless_pv.set(0) |
| 1121 | + log("CHILD: Exiting callback") |
| 1122 | + |
| 1123 | + def go_away(*args): |
| 1124 | + log("CHILD: received exit signal ", args) |
| 1125 | + event.Signal() |
| 1126 | + |
| 1127 | + builder.SetDeviceName(device_name) |
| 1128 | + |
| 1129 | + |
| 1130 | + useless_pv = builder.aOut( |
| 1131 | + self.recursive_record_name, |
| 1132 | + initial_value=0, |
| 1133 | + on_update=useless_callback |
| 1134 | + ) |
| 1135 | + event = Event() |
| 1136 | + builder.Action("GO_AWAY", on_update = go_away) |
| 1137 | + |
| 1138 | + builder.LoadDatabase() |
| 1139 | + softioc.iocInit() |
| 1140 | + |
| 1141 | + conn.send("R") # "Ready" |
| 1142 | + log("CHILD: Sent R over Connection to Parent") |
| 1143 | + |
| 1144 | + log("CHILD: About to wait") |
| 1145 | + event.Wait() |
| 1146 | + log("CHILD: Exiting") |
| 1147 | + |
| 1148 | + @requires_cothread |
| 1149 | + @pytest.mark.asyncio |
| 1150 | + async def test_recursive_set(self): |
| 1151 | + """Test that recursive sets do not cause a deadlock""" |
| 1152 | + ctx = get_multiprocessing_context() |
| 1153 | + parent_conn, child_conn = ctx.Pipe() |
| 1154 | + |
| 1155 | + device_name = create_random_prefix() |
| 1156 | + |
| 1157 | + process = ctx.Process( |
| 1158 | + target=self.recursive_set_func, |
| 1159 | + args=(device_name, child_conn), |
| 1160 | + ) |
| 1161 | + |
| 1162 | + process.start() |
| 1163 | + |
| 1164 | + log("PARENT: Child started, waiting for R command") |
| 1165 | + |
| 1166 | + from aioca import caput, camonitor |
| 1167 | + |
| 1168 | + try: |
| 1169 | + # Wait for message that IOC has started |
| 1170 | + select_and_recv(parent_conn, "R") |
| 1171 | + log("PARENT: received R command") |
| 1172 | + |
| 1173 | + record = device_name + ":" + self.recursive_record_name |
| 1174 | + |
| 1175 | + log(f"PARENT: monitoring {record}") |
| 1176 | + queue = asyncio.Queue() |
| 1177 | + monitor = camonitor(record, queue.put, all_updates=True) |
| 1178 | + |
| 1179 | + log("PARENT: Beginning first wait") |
| 1180 | + |
| 1181 | + # Expected initial state |
| 1182 | + new_val = await asyncio.wait_for(queue.get(), TIMEOUT) |
| 1183 | + log(f"PARENT: initial new_val: {new_val}") |
| 1184 | + assert new_val == 0 |
| 1185 | + |
| 1186 | + # Try a series of caput calls, to maximise chance to trigger |
| 1187 | + # the deadlock |
| 1188 | + i = 1 |
| 1189 | + while i < 500: |
| 1190 | + log(f"PARENT: begin loop with i={i}") |
| 1191 | + await caput(record, i) |
| 1192 | + new_val = await asyncio.wait_for(queue.get(), 1) |
| 1193 | + assert new_val == i |
| 1194 | + new_val = await asyncio.wait_for(queue.get(), 1) |
| 1195 | + assert new_val == 0 # .set() should reset value |
| 1196 | + i += 1 |
| 1197 | + |
| 1198 | + # Signal the IOC to cleanly shut down |
| 1199 | + await caput(device_name + ":" + "GO_AWAY", 1) |
| 1200 | + |
| 1201 | + except asyncio.TimeoutError as e: |
| 1202 | + raise asyncio.TimeoutError( |
| 1203 | + f"IOC did not send data back - loop froze on iteration {i} " |
| 1204 | + "- it has probably hung/deadlocked." |
| 1205 | + ) from e |
| 1206 | + |
| 1207 | + finally: |
| 1208 | + monitor.close() |
| 1209 | + # Clear the cache before stopping the IOC stops |
| 1210 | + # "channel disconnected" error messages |
| 1211 | + aioca_cleanup() |
| 1212 | + |
| 1213 | + process.join(timeout=TIMEOUT) |
| 1214 | + log(f"PARENT: Join completed with exitcode {process.exitcode}") |
| 1215 | + if process.exitcode is None: |
| 1216 | + process.terminate() |
| 1217 | + pytest.fail("Process did not finish cleanly, terminating") |
0 commit comments