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": WAVEFORM_LENGTH} # Must specify when no value record = record_func("SOME-NAME", **kwarg) log("CHILD: About to start IOC") dispatcher = asyncio_dispatcher.AsyncioDispatcher() builder.LoadDatabase() softioc.iocInit(dispatcher) log("CHILD: Soft IOC started, about to .set(None)") try: record.set(None) log("CHILD: Uh-OH! No exception thrown when setting None!") except Exception as e: log("CHILD: Putting exception into queue %s", e) queue.put(e) else: log("CHILD: No exception raised when using None as value!") queue.put(Exception("FAIL: No exception raised during .set()"))
async def query_record(index): log("SPAWNED: beginning blocking caput ", index) await caput( device_name + ":BLOCKING-REC", 5, # Arbitrary value wait=True, timeout=TIMEOUT ) log("SPAWNED: caput complete ", index)
def on_update_test_func( self, device_name, record_func, conn, always_update ): builder.SetDeviceName(device_name) li = builder.longIn("ON-UPDATE-COUNTER-RECORD", initial_value=0) def on_update_func(new_val): """Increments li record each time main out record receives caput""" li.set(li.get() + 1) kwarg = {} if record_func is builder.WaveformOut: kwarg = {"length": WAVEFORM_LENGTH} # Must specify when no value record_func( "ON-UPDATE-RECORD", on_update=on_update_func, always_update=always_update, **kwarg) def on_update_done(_): conn.send("C") # "Complete" # Put to the action record after we've done all other Puts, so we know # all the callbacks have finished processing builder.Action("ON-UPDATE-DONE", on_update=on_update_done) dispatcher = asyncio_dispatcher.AsyncioDispatcher() builder.LoadDatabase() softioc.iocInit(dispatcher) conn.send("R") # "Ready" log("CHILD: Sent R over Connection to Parent") # Keep process alive while main thread runs CAGET if conn.poll(TIMEOUT): val = conn.recv() assert val == "D", "Did not receive expected Done character" log("CHILD: Received exit command, child exiting")
def blocking_test_func(self, device_name, conn): builder.SetDeviceName(device_name) count_rec = builder.longIn("BLOCKING-COUNTER", initial_value=0) async def blocking_update_func(new_val): """A function that will block for some time""" log("CHILD: blocking_update_func starting") await asyncio.sleep(0.5) log("CHILD: Finished sleep!") completed_count = count_rec.get() + 1 count_rec.set(completed_count) log( "CHILD: blocking_update_func finished, completed ", completed_count ) builder.longOut( "BLOCKING-REC", on_update=blocking_update_func, always_update=True, blocking=True ) dispatcher = asyncio_dispatcher.AsyncioDispatcher() builder.LoadDatabase() softioc.iocInit(dispatcher) conn.send("R") # "Ready" log("CHILD: Sent R over Connection to Parent") # Keep process alive while main thread runs CAGET if conn.poll(TIMEOUT): val = conn.recv() assert val == "D", "Did not receive expected Done character" log("CHILD: Received exit command, child exiting")
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() log("PARENT: Child process started, waiting for returned exception") try: exception = queue.get(timeout=TIMEOUT) assert isinstance(exception, self.expected_exceptions) finally: log("PARENT: Issuing terminate to child process") process.terminate() process.join(timeout=TIMEOUT) if process.exitcode is None: pytest.fail("Process did not terminate")
async def blocking_update_func(new_val): """A function that will block for some time""" log("CHILD: blocking_update_func starting") await asyncio.sleep(0.5) log("CHILD: Finished sleep!") completed_count = count_rec.get() + 1 count_rec.set(completed_count) log( "CHILD: blocking_update_func finished, completed ", completed_count )
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 select_and_recv(parent_conn, "R") # Cannot do these imports before the subprocess starts, as the subprocess # would inherit cothread's internal state which would break things! from cothread import Yield from cothread.catools import caget, caput, _channel_cache from cothread.dbr import DBR_CHAR_STR try: # 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["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: select_and_recv(parent_conn) caput( DEVICE_NAME + ":" + record_name, initial_value, wait=True, **kwargs, **put_kwarg, ) if get_enum == GetValueEnum.GET: parent_conn.send("G") # "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: rec_val = select_and_recv(parent_conn) 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]): log("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() parent_conn.send("D") # "Done" ioc_process.join(timeout=TIMEOUT) if ioc_process.exitcode is None: pytest.fail("Process did not terminate")
async def test_blocking_multiple_threads(self): """Test that a blocking record correctly causes caputs from multiple threads to wait for the expected time""" parent_conn, child_conn = multiprocessing.Pipe() device_name = create_random_prefix() process = multiprocessing.Process( target=self.blocking_test_func, args=(device_name, child_conn), ) process.start() log("PARENT: Child started, waiting for R command") from aioca import caget, caput try: # Wait for message that IOC has started select_and_recv(parent_conn, "R") log("PARENT: received R command") MAX_COUNT = 4 async def query_record(index): log("SPAWNED: beginning blocking caput ", index) await caput( device_name + ":BLOCKING-REC", 5, # Arbitrary value wait=True, timeout=TIMEOUT ) log("SPAWNED: caput complete ", index) queries = [query_record(i) for i in range(MAX_COUNT)] * MAX_COUNT log("PARENT: Gathering list of queries") await asyncio.gather(*queries) log("PARENT: Getting value from counter") ret_val = await caget( device_name + ":BLOCKING-COUNTER", timeout=TIMEOUT, ) assert ret_val.ok, \ f"caget did not succeed: {ret_val.errorcode}, {ret_val}" log(f"PARENT: Received val from COUNTER: {ret_val}") assert ret_val == MAX_COUNT finally: # Clear the cache before stopping the IOC stops # "channel disconnected" error messages aioca_cleanup() log("PARENT: Sending Done command to child") parent_conn.send("D") # "Done" process.join(timeout=TIMEOUT) log(f"PARENT: Join completed with exitcode {process.exitcode}") if process.exitcode is None: pytest.fail("Process did not terminate")
def test_blocking_single_thread_multiple_calls(self): """Test that a blocking record correctly causes multiple caputs from a single thread to wait for the expected time""" parent_conn, child_conn = multiprocessing.Pipe() device_name = create_random_prefix() process = multiprocessing.Process( target=self.blocking_test_func, args=(device_name, child_conn), ) process.start() log("PARENT: Child started, waiting for R command") from cothread.catools import caget, caput, _channel_cache try: # Wait for message that IOC has started select_and_recv(parent_conn, "R") log("PARENT: received R command") # Suppress potential spurious warnings _channel_cache.purge() # Track number of puts sent count = 1 MAX_COUNT = 4 log("PARENT: begining While loop") while count <= MAX_COUNT: put_ret = caput( device_name + ":BLOCKING-REC", 5, # Arbitrary value wait=True, timeout=TIMEOUT ) assert put_ret.ok, f"caput did not succeed: {put_ret.errorcode}" log(f"PARENT: completed caput with count {count}") count += 1 log("PARENT: Getting value from counter") ret_val = caget( device_name + ":BLOCKING-COUNTER", timeout=TIMEOUT, ) assert ret_val.ok, \ f"caget did not succeed: {ret_val.errorcode}, {ret_val}" log(f"PARENT: Received val from COUNTER: {ret_val}") assert ret_val == MAX_COUNT finally: # Suppress potential spurious warnings _channel_cache.purge() log("PARENT: Sending Done command to child") parent_conn.send("D") # "Done" process.join(timeout=TIMEOUT) log(f"PARENT: Join completed with exitcode {process.exitcode}") if process.exitcode is None: pytest.fail("Process did not terminate")
def on_update_runner(self, creation_func, always_update, put_same_value): parent_conn, child_conn = multiprocessing.Pipe() device_name = create_random_prefix() process = multiprocessing.Process( target=self.on_update_test_func, args=(device_name, creation_func, child_conn, always_update), ) process.start() log("PARENT: Child started, waiting for R command") from cothread.catools import caget, caput, _channel_cache try: # Wait for message that IOC has started select_and_recv(parent_conn, "R") log("PARENT: received R command") # Suppress potential spurious warnings _channel_cache.purge() # Use this number to put to records - don't start at 0 as many # record types default to 0 and we usually want to put a different # value to force processing to occur count = 1 log("PARENT: begining While loop") while count < 4: put_ret = caput( device_name + ":ON-UPDATE-RECORD", 9 if put_same_value else count, wait=True, ) assert put_ret.ok, f"caput did not succeed: {put_ret.errorcode}" log(f"PARENT: completed caput with count {count}") count += 1 log("PARENT: Put'ing to DONE record") caput( device_name + ":ON-UPDATE-DONE", 1, wait=True, ) log("PARENT: Waiting for C command") # Wait for action record to process, so we know all the callbacks # have finished processing (This assumes record callbacks are not # re-ordered, and will run in the same order as the caputs we sent) select_and_recv(parent_conn, "C") log("PARENT: Received C command") ret_val = caget( device_name + ":ON-UPDATE-COUNTER-RECORD", timeout=TIMEOUT, ) assert ret_val.ok, \ f"caget did not succeed: {ret_val.errorcode}, {ret_val}" log(f"PARENT: Received val from COUNTER: {ret_val}") # Expected value is either 3 (incremented once per caput) # or 1 (incremented on first caput and not subsequent ones) expected_val = 3 if put_same_value and not always_update: expected_val = 1 assert ret_val == expected_val finally: # Suppress potential spurious warnings _channel_cache.purge() log("PARENT:Sending Done command to child") parent_conn.send("D") # "Done" process.join(timeout=TIMEOUT) log(f"PARENT: Join completed with exitcode {process.exitcode}") if process.exitcode is None: pytest.fail("Process did not terminate")