@@ -204,6 +204,20 @@ def record_values_names(fixture_value):
204204 ),
205205 numpy .ndarray ,
206206 ),
207+ (
208+ "wIn_unicode" ,
209+ builder .WaveformIn ,
210+ "%a€b" ,
211+ numpy .array ([37 , 97 , 226 , 130 , 172 , 98 , 0 ], dtype = numpy .uint8 ),
212+ numpy .ndarray ,
213+ ),
214+ (
215+ "wOut_unicode" ,
216+ builder .WaveformOut ,
217+ "%a€b" ,
218+ numpy .array ([37 , 97 , 226 , 130 , 172 , 98 , 0 ], dtype = numpy .uint8 ),
219+ numpy .ndarray ,
220+ ),
207221]
208222
209223
@@ -353,7 +367,7 @@ def run_ioc(record_configurations: list, conn, set_enum, get_enum):
353367
354368 records : List [RecordWrapper ] = []
355369
356- # Loop over given list and create the records
370+ # Create records from the given list
357371 for configuration in record_configurations :
358372 kwarg = {}
359373
@@ -840,3 +854,149 @@ def test_value_none_rejected_set_after_init(self, record_funcs_reject_none):
840854 finally :
841855 process .terminate ()
842856 process .join (timeout = 3 )
857+
858+
859+ class TestValidate :
860+ """Tests related to the validate callback"""
861+
862+ @pytest .fixture (
863+ params = [
864+ (builder .aOut , 7.89 , 0 ),
865+ (builder .boolOut , 1 , 0 ),
866+ (builder .longOut , 7 , 0 ),
867+ (builder .stringOut , "HI" , "" ),
868+ (builder .mbbOut , 2 , 0 ),
869+ (builder .WaveformOut , [10 , 11 , 12 ], []),
870+ ]
871+ )
872+ def out_records (self , request ):
873+ """The list of Out records and an associated value to set """
874+ return request .param
875+
876+ def validate_always_pass (self , record , new_val ):
877+ """Validation method that always allows changes"""
878+ return True
879+
880+ def validate_always_fail (self , record , new_val ):
881+ """Validation method that always rejects changes"""
882+ return False
883+
884+ def validate_ioc_test_func (self , record_func , queue , validate_pass : bool ):
885+ """Start the IOC with the specified validate method"""
886+
887+ builder .SetDeviceName (DEVICE_NAME )
888+
889+ kwarg = {}
890+ if record_func in [builder .WaveformIn , builder .WaveformOut ]:
891+ kwarg = {"length" : 50 } # Required when no value on creation
892+
893+ kwarg .update (
894+ {
895+ "validate" : self .validate_always_pass
896+ if validate_pass
897+ else self .validate_always_fail
898+ }
899+ )
900+
901+ record_func ("VALIDATE-RECORD" , ** kwarg )
902+
903+ dispatcher = asyncio_dispatcher .AsyncioDispatcher ()
904+ builder .LoadDatabase ()
905+ softioc .iocInit (dispatcher )
906+
907+ queue .put ("IOC ready" )
908+
909+ # Keep process alive while main thread works.
910+ # This is most applicable to CAGET tests.
911+ asyncio .run_coroutine_threadsafe (
912+ asyncio .sleep (TIMEOUT ), dispatcher .loop
913+ ).result ()
914+
915+
916+ @requires_cothread
917+ def test_validate_allows_updates (self , out_records ):
918+ """Test that record values are updated correctly when validate
919+ method allows it """
920+
921+ creation_func , value , _ = out_records
922+
923+ queue = multiprocessing .Queue ()
924+ process = multiprocessing .Process (
925+ target = self .validate_ioc_test_func ,
926+ args = (creation_func , queue , True ),
927+ )
928+
929+ process .start ()
930+
931+ try :
932+ queue .get (timeout = 5 ) # Get the expected IOc initialised message
933+
934+ from cothread .catools import caget , caput , _channel_cache
935+
936+ # See other places in this file for why we call it
937+ _channel_cache .purge ()
938+
939+ put_ret = caput (
940+ DEVICE_NAME + ":" + "VALIDATE-RECORD" ,
941+ value ,
942+ wait = True ,
943+ )
944+ assert put_ret .ok , "caput did not succeed"
945+
946+ ret_val = caget (
947+ DEVICE_NAME + ":" + "VALIDATE-RECORD" ,
948+ timeout = 3
949+ )
950+
951+ if creation_func in [builder .WaveformOut , builder .WaveformIn ]:
952+ assert numpy .array_equal (ret_val , value )
953+ else :
954+ assert ret_val == value
955+
956+ finally :
957+ process .terminate ()
958+ process .join (timeout = 3 )
959+
960+ @requires_cothread
961+ def test_validate_blocks_updates (self , out_records ):
962+ """Test that record values are not updated when validate method
963+ always blocks updates"""
964+
965+ creation_func , value , default = out_records
966+
967+ queue = multiprocessing .Queue ()
968+ process = multiprocessing .Process (
969+ target = self .validate_ioc_test_func ,
970+ args = (creation_func , queue , False ),
971+ )
972+
973+ process .start ()
974+
975+ try :
976+ queue .get (timeout = 5 ) # Get the expected IOc initialised message
977+
978+ from cothread .catools import caget , caput , _channel_cache
979+
980+ # See other places in this file for why we call it
981+ _channel_cache .purge ()
982+
983+ put_ret = caput (
984+ DEVICE_NAME + ":" + "VALIDATE-RECORD" ,
985+ value ,
986+ wait = True ,
987+ )
988+ assert put_ret .ok , "caput did not succeed"
989+
990+ ret_val = caget (
991+ DEVICE_NAME + ":" + "VALIDATE-RECORD" ,
992+ timeout = 3
993+ )
994+
995+ if creation_func in [builder .WaveformOut , builder .WaveformIn ]:
996+ assert numpy .array_equal (ret_val , default )
997+ else :
998+ assert ret_val == default
999+
1000+ finally :
1001+ process .terminate ()
1002+ process .join (timeout = 3 )
0 commit comments