|
34 | 34 | builder.WaveformIn,
|
35 | 35 | ]
|
36 | 36 |
|
| 37 | +# TODO: This should never pass, but somehow does in CI... |
37 | 38 | def test_records(tmp_path):
|
38 | 39 | # Ensure we definitely unload all records that may be hanging over from
|
39 | 40 | # previous tests, then create exactly one instance of expected records.
|
@@ -904,3 +905,203 @@ async def query_record(index):
|
904 | 905 | log(f"PARENT: Join completed with exitcode {process.exitcode}")
|
905 | 906 | if process.exitcode is None:
|
906 | 907 | pytest.fail("Process did not terminate")
|
| 908 | + |
| 909 | +class TestGetSetField: |
| 910 | + """Tests related to get_field and set_field on records""" |
| 911 | + |
| 912 | + test_result_rec = "TestResult" |
| 913 | + |
| 914 | + def test_set_field_before_init_fails(self): |
| 915 | + """Test that calling set_field before iocInit() raises an exception""" |
| 916 | + |
| 917 | + ao = builder.aOut("testAOut") |
| 918 | + |
| 919 | + with pytest.raises(AssertionError) as e: |
| 920 | + ao.set_field("EGU", "Deg") |
| 921 | + |
| 922 | + assert "set_field may only be called after iocInit" in str(e.value) |
| 923 | + |
| 924 | + def test_get_field_before_init_fails(self): |
| 925 | + """Test that calling get_field before iocInit() raises an exception""" |
| 926 | + |
| 927 | + ao = builder.aOut("testAOut") |
| 928 | + |
| 929 | + with pytest.raises(AssertionError) as e: |
| 930 | + ao.get_field("EGU") |
| 931 | + |
| 932 | + assert "get_field may only be called after iocInit" in str(e.value) |
| 933 | + |
| 934 | + def get_set_test_func(self, device_name, conn): |
| 935 | + """Run an IOC and do simple get_field/set_field calls""" |
| 936 | + |
| 937 | + builder.SetDeviceName(device_name) |
| 938 | + |
| 939 | + lo = builder.longOut("TestLongOut", EGU="unset", DRVH=12) |
| 940 | + |
| 941 | + # Record to indicate success/failure |
| 942 | + bi = builder.boolIn(self.test_result_rec, ZNAM="FAILED", ONAM="SUCCESS") |
| 943 | + |
| 944 | + dispatcher = asyncio_dispatcher.AsyncioDispatcher() |
| 945 | + builder.LoadDatabase() |
| 946 | + softioc.iocInit(dispatcher) |
| 947 | + |
| 948 | + conn.send("R") # "Ready" |
| 949 | + |
| 950 | + log("CHILD: Sent R over Connection to Parent") |
| 951 | + |
| 952 | + # Set and then get the EGU field |
| 953 | + egu = "TEST" |
| 954 | + lo.set_field("EGU", egu) |
| 955 | + log("CHILD: set_field successful") |
| 956 | + readback_egu = lo.get_field("EGU") |
| 957 | + log(f"CHILD: get_field returned {readback_egu}") |
| 958 | + assert readback_egu == egu, \ |
| 959 | + f"EGU field was not {egu}, was {readback_egu}" |
| 960 | + |
| 961 | + log("CHILD: assert passed") |
| 962 | + |
| 963 | + # Test completed, report to listening camonitor |
| 964 | + bi.set(True) |
| 965 | + |
| 966 | + # Keep process alive while main thread works. |
| 967 | + while (True): |
| 968 | + if conn.poll(TIMEOUT): |
| 969 | + val = conn.recv() |
| 970 | + if val == "D": # "Done" |
| 971 | + break |
| 972 | + |
| 973 | + log("CHILD: Received exit command, child exiting") |
| 974 | + |
| 975 | + |
| 976 | + @pytest.mark.asyncio |
| 977 | + async def test_get_set(self): |
| 978 | + """Test a simple set_field/get_field is successful""" |
| 979 | + ctx = get_multiprocessing_context() |
| 980 | + parent_conn, child_conn = ctx.Pipe() |
| 981 | + |
| 982 | + device_name = create_random_prefix() |
| 983 | + |
| 984 | + process = ctx.Process( |
| 985 | + target=self.get_set_test_func, |
| 986 | + args=(device_name, child_conn), |
| 987 | + ) |
| 988 | + |
| 989 | + process.start() |
| 990 | + |
| 991 | + log("PARENT: Child started, waiting for R command") |
| 992 | + |
| 993 | + from aioca import camonitor |
| 994 | + |
| 995 | + try: |
| 996 | + # Wait for message that IOC has started |
| 997 | + select_and_recv(parent_conn, "R") |
| 998 | + |
| 999 | + log("PARENT: received R command") |
| 1000 | + |
| 1001 | + queue = asyncio.Queue() |
| 1002 | + record = device_name + ":" + self.test_result_rec |
| 1003 | + monitor = camonitor(record, queue.put) |
| 1004 | + |
| 1005 | + log(f"PARENT: monitoring {record}") |
| 1006 | + new_val = await asyncio.wait_for(queue.get(), TIMEOUT) |
| 1007 | + log(f"PARENT: new_val is {new_val}") |
| 1008 | + assert new_val == 1, \ |
| 1009 | + f"Test failed, value was not 1(True), was {new_val}" |
| 1010 | + |
| 1011 | + |
| 1012 | + finally: |
| 1013 | + monitor.close() |
| 1014 | + # Clear the cache before stopping the IOC stops |
| 1015 | + # "channel disconnected" error messages |
| 1016 | + aioca_cleanup() |
| 1017 | + |
| 1018 | + log("PARENT: Sending Done command to child") |
| 1019 | + parent_conn.send("D") # "Done" |
| 1020 | + process.join(timeout=TIMEOUT) |
| 1021 | + log(f"PARENT: Join completed with exitcode {process.exitcode}") |
| 1022 | + if process.exitcode is None: |
| 1023 | + pytest.fail("Process did not terminate") |
| 1024 | + |
| 1025 | + def get_set_too_long_value(self, device_name, conn): |
| 1026 | + """Run an IOC and deliberately call set_field with a too-long value""" |
| 1027 | + |
| 1028 | + builder.SetDeviceName(device_name) |
| 1029 | + |
| 1030 | + lo = builder.longOut("TestLongOut", EGU="unset", DRVH=12) |
| 1031 | + |
| 1032 | + # Record to indicate success/failure |
| 1033 | + bi = builder.boolIn(self.test_result_rec, ZNAM="FAILED", ONAM="SUCCESS") |
| 1034 | + |
| 1035 | + dispatcher = asyncio_dispatcher.AsyncioDispatcher() |
| 1036 | + builder.LoadDatabase() |
| 1037 | + softioc.iocInit(dispatcher) |
| 1038 | + |
| 1039 | + conn.send("R") # "Ready" |
| 1040 | + |
| 1041 | + log("CHILD: Sent R over Connection to Parent") |
| 1042 | + |
| 1043 | + # Set a too-long value and confirm it reports an error |
| 1044 | + try: |
| 1045 | + lo.set_field("EGU", "ThisStringIsFarTooLongToFitIntoTheEguField") |
| 1046 | + except ValueError as e: |
| 1047 | + # Expected error, report success to listening camonitor |
| 1048 | + assert "byte string too long" in e.args[0] |
| 1049 | + bi.set(True) |
| 1050 | + |
| 1051 | + # Keep process alive while main thread works. |
| 1052 | + while (True): |
| 1053 | + if conn.poll(TIMEOUT): |
| 1054 | + val = conn.recv() |
| 1055 | + if val == "D": # "Done" |
| 1056 | + break |
| 1057 | + |
| 1058 | + log("CHILD: Received exit command, child exiting") |
| 1059 | + |
| 1060 | + @pytest.mark.asyncio |
| 1061 | + async def test_set_too_long_value(self): |
| 1062 | + """Test that set_field with a too-long value raises the expected |
| 1063 | + error""" |
| 1064 | + ctx = get_multiprocessing_context() |
| 1065 | + parent_conn, child_conn = ctx.Pipe() |
| 1066 | + |
| 1067 | + device_name = create_random_prefix() |
| 1068 | + |
| 1069 | + process = ctx.Process( |
| 1070 | + target=self.get_set_too_long_value, |
| 1071 | + args=(device_name, child_conn), |
| 1072 | + ) |
| 1073 | + |
| 1074 | + process.start() |
| 1075 | + |
| 1076 | + log("PARENT: Child started, waiting for R command") |
| 1077 | + |
| 1078 | + from aioca import camonitor |
| 1079 | + |
| 1080 | + try: |
| 1081 | + # Wait for message that IOC has started |
| 1082 | + select_and_recv(parent_conn, "R") |
| 1083 | + |
| 1084 | + log("PARENT: received R command") |
| 1085 | + |
| 1086 | + queue = asyncio.Queue() |
| 1087 | + record = device_name + ":" + self.test_result_rec |
| 1088 | + monitor = camonitor(record, queue.put) |
| 1089 | + |
| 1090 | + log(f"PARENT: monitoring {record}") |
| 1091 | + new_val = await asyncio.wait_for(queue.get(), TIMEOUT) |
| 1092 | + log(f"PARENT: new_val is {new_val}") |
| 1093 | + assert new_val == 1, \ |
| 1094 | + f"Test failed, value was not 1(True), was {new_val}" |
| 1095 | + |
| 1096 | + finally: |
| 1097 | + monitor.close() |
| 1098 | + # Clear the cache before stopping the IOC stops |
| 1099 | + # "channel disconnected" error messages |
| 1100 | + aioca_cleanup() |
| 1101 | + |
| 1102 | + log("PARENT: Sending Done command to child") |
| 1103 | + parent_conn.send("D") # "Done" |
| 1104 | + process.join(timeout=TIMEOUT) |
| 1105 | + log(f"PARENT: Join completed with exitcode {process.exitcode}") |
| 1106 | + if process.exitcode is None: |
| 1107 | + pytest.fail("Process did not terminate") |
0 commit comments