15
15
from .NMOSUtils import NMOSUtils
16
16
17
17
import json
18
+ import os
18
19
import time
19
20
20
21
from copy import deepcopy
25
26
from .GenericTest import NMOSTestException
26
27
from .TestHelper import WebsocketWorker , load_resolved_schema
27
28
29
+ NODE_API_KEY = "node"
30
+ CONTROL_API_KEY = "ncp"
31
+ MS05_API_KEY = "controlframework"
32
+ FEATURE_SETS_KEY = "featuresets"
33
+
28
34
29
35
class MessageTypes (IntEnum ):
30
36
Command = 0
@@ -146,17 +152,21 @@ class StandardClassIds(Enum):
146
152
147
153
148
154
class IS12Utils (NMOSUtils ):
149
- def __init__ (self , url , spec_path , spec_branch ):
150
- NMOSUtils .__init__ (self , url )
151
- self .spec_branch = spec_branch
152
- self .load_is12_schemas (spec_path )
155
+ def __init__ (self , apis ):
156
+ NMOSUtils .__init__ (self , apis [NODE_API_KEY ]["url" ])
157
+ self .apis = apis
158
+ self .spec_path = self .apis [CONTROL_API_KEY ]["spec_path" ]
159
+ self .spec_branch = self .apis [CONTROL_API_KEY ]["spec_branch" ]
160
+ self ._load_is12_schemas ()
153
161
self .ROOT_BLOCK_OID = 1
154
162
self .ncp_websocket = None
155
163
self .command_handle = 0
156
164
self .expect_notifications = False
157
165
self .notifications = []
166
+ self .device_model = None
167
+ self .class_manager = None
158
168
159
- def load_is12_schemas (self , spec_path ):
169
+ def _load_is12_schemas (self ):
160
170
"""Load datatype and control class decriptors and create datatype JSON schemas"""
161
171
# Load IS-12 schemas
162
172
self .schemas = {}
@@ -165,16 +175,17 @@ def load_is12_schemas(self, spec_path):
165
175
"subscription-response-message" ,
166
176
"notification-message" ]
167
177
for schema_name in schema_names :
168
- self .schemas [schema_name ] = load_resolved_schema (spec_path , schema_name + ".json" )
178
+ self .schemas [schema_name ] = load_resolved_schema (self .apis [CONTROL_API_KEY ]["spec_path" ],
179
+ schema_name + ".json" )
169
180
170
- def open_ncp_websocket (self , test , url ):
181
+ def open_ncp_websocket (self , test ):
171
182
"""Create a WebSocket client connection to Node under test. Raises NMOSTestException on error"""
172
183
# Reuse socket if connection already established
173
184
if self .ncp_websocket and self .ncp_websocket .is_open ():
174
185
return
175
186
176
187
# Create a WebSocket connection to NMOS Control Protocol endpoint
177
- self .ncp_websocket = WebsocketWorker (url )
188
+ self .ncp_websocket = WebsocketWorker (self . apis [ CONTROL_API_KEY ][ " url" ] )
178
189
self .ncp_websocket .start ()
179
190
180
191
# Give WebSocket client a chance to start and open its connection
@@ -194,7 +205,26 @@ def close_ncp_websocket(self):
194
205
if self .ncp_websocket :
195
206
self .ncp_websocket .close ()
196
207
197
- def validate_is12_schema (self , test , payload , schema_name , context = "" ):
208
+ def validate_reference_datatype_schema (self , test , payload , datatype_name , context = "" ):
209
+ """Validate payload against reference datatype schema"""
210
+ self .validate_schema (test , payload , self .reference_datatype_schemas [datatype_name ])
211
+
212
+ def validate_schema (self , test , payload , schema , context = "" ):
213
+ """Delegates to validate_schema. Raises NMOSTestExceptions on error"""
214
+ if not schema :
215
+ raise NMOSTestException (test .FAIL (context + "Missing schema. " ))
216
+ try :
217
+ # Validate the JSON schema is correct
218
+ checker = FormatChecker (["ipv4" , "ipv6" , "uri" ])
219
+ validate (payload , schema , format_checker = checker )
220
+ except ValidationError as e :
221
+ raise NMOSTestException (test .FAIL (context + "Schema validation error: " + e .message ))
222
+ except SchemaError as e :
223
+ raise NMOSTestException (test .FAIL (context + "Schema error: " + e .message ))
224
+
225
+ return
226
+
227
+ def _validate_is12_schema (self , test , payload , schema_name , context = "" ):
198
228
"""Delegates to validate_schema. Raises NMOSTestExceptions on error"""
199
229
try :
200
230
# Validate the JSON schema is correct
@@ -245,7 +275,7 @@ def send_command(self, test, command_json):
245
275
parsed_message = json .loads (message )
246
276
247
277
if self .message_type_to_schema_name (parsed_message .get ("messageType" )):
248
- self .validate_is12_schema (
278
+ self ._validate_is12_schema (
249
279
test ,
250
280
parsed_message ,
251
281
self .message_type_to_schema_name (parsed_message ["messageType" ]),
@@ -324,7 +354,7 @@ def execute_command(self, test, oid, method_id, arguments):
324
354
return response ["result" ]
325
355
326
356
def get_property_value (self , test , oid , property_id ):
327
- """Get property from object. Raises NMOSTestException on error"""
357
+ """Get value of property from object. Raises NMOSTestException on error"""
328
358
return self .execute_command (test , oid ,
329
359
NcObjectMethods .GENERIC_GET .value ,
330
360
{'id' : property_id })["value" ]
@@ -542,6 +572,226 @@ def is_manager(self, class_id):
542
572
""" Check class id to determine if this is a manager """
543
573
return len (class_id ) > 1 and class_id [0 ] == 1 and class_id [1 ] == 3
544
574
575
+ def load_reference_resources (self ):
576
+ """Load datatype and control class decriptors and create datatype JSON schemas"""
577
+ # Calculate paths to MS-05 descriptors
578
+ # including Feature Sets specified as additional_paths in test definition
579
+ spec_paths = [os .path .join (self .apis [FEATURE_SETS_KEY ]["spec_path" ], path )
580
+ for path in self .apis [FEATURE_SETS_KEY ]["repo_paths" ]]
581
+ spec_paths .append (self .apis [MS05_API_KEY ]["spec_path" ])
582
+ # Root path for primitive datatypes
583
+ spec_paths .append ('test_data/IS1201' )
584
+
585
+ datatype_paths = []
586
+ classes_paths = []
587
+ for spec_path in spec_paths :
588
+ datatype_path = os .path .abspath (os .path .join (spec_path , 'models/datatypes/' ))
589
+ if os .path .exists (datatype_path ):
590
+ datatype_paths .append (datatype_path )
591
+ classes_path = os .path .abspath (os .path .join (spec_path , 'models/classes/' ))
592
+ if os .path .exists (classes_path ):
593
+ classes_paths .append (classes_path )
594
+
595
+ # Load class and datatype descriptors
596
+ self .reference_class_descriptors = self ._load_model_descriptors (classes_paths )
597
+
598
+ # Load MS-05 datatype descriptors
599
+ self .reference_datatype_descriptors = self ._load_model_descriptors (datatype_paths )
600
+
601
+ # Generate MS-05 datatype schemas from MS-05 datatype descriptors
602
+ self .reference_datatype_schemas = self .generate_json_schemas (
603
+ datatype_descriptors = self .reference_datatype_descriptors ,
604
+ schema_path = os .path .join (self .apis [CONTROL_API_KEY ]["spec_path" ], 'APIs/schemas/' ))
605
+
606
+ def _load_model_descriptors (self , descriptor_paths ):
607
+ descriptors = {}
608
+ for descriptor_path in descriptor_paths :
609
+ for filename in os .listdir (descriptor_path ):
610
+ name , extension = os .path .splitext (filename )
611
+ if extension == ".json" :
612
+ with open (os .path .join (descriptor_path , filename ), 'r' ) as json_file :
613
+ descriptors [name ] = json .load (json_file )
614
+
615
+ return descriptors
616
+
617
+ def generate_json_schemas (self , datatype_descriptors , schema_path ):
618
+ """Generate datatype schemas from datatype descriptors"""
619
+ datatype_schema_names = []
620
+ base_schema_path = os .path .abspath (schema_path )
621
+ if not os .path .exists (base_schema_path ):
622
+ os .makedirs (base_schema_path )
623
+
624
+ for name , descriptor in datatype_descriptors .items ():
625
+ json_schema = self .descriptor_to_schema (descriptor )
626
+ with open (os .path .join (base_schema_path , name + '.json' ), 'w' ) as output_file :
627
+ json .dump (json_schema , output_file , indent = 4 )
628
+ datatype_schema_names .append (name )
629
+
630
+ # Load resolved MS-05 datatype schemas
631
+ datatype_schemas = {}
632
+ for name in datatype_schema_names :
633
+ datatype_schemas [name ] = load_resolved_schema (schema_path , name + '.json' , path_prefix = False )
634
+
635
+ return datatype_schemas
636
+
637
+ def validate_descriptor (self , test , reference , descriptor , context = "" ):
638
+ """Validate descriptor against reference descriptor. Raises NMOSTestException on error"""
639
+ non_normative_keys = ['description' ]
640
+
641
+ if isinstance (reference , dict ):
642
+ reference_keys = set (reference .keys ())
643
+ descriptor_keys = set (descriptor .keys ())
644
+
645
+ # compare the keys to see if any extra/missing
646
+ key_diff = (set (reference_keys ) | set (descriptor_keys )) - (set (reference_keys ) & set (descriptor_keys ))
647
+ if len (key_diff ) > 0 :
648
+ error_description = "Missing keys " if set (key_diff ) <= set (reference_keys ) else "Additional keys "
649
+ raise NMOSTestException (test .FAIL (context + error_description + str (key_diff )))
650
+ for key in reference_keys :
651
+ if key in non_normative_keys and not isinstance (reference [key ], dict ):
652
+ continue
653
+ # Check for class ID
654
+ if key == 'classId' and isinstance (reference [key ], list ):
655
+ if reference [key ] != descriptor [key ]:
656
+ raise NMOSTestException (test .FAIL (context + "Unexpected ClassId. Expected: "
657
+ + str (reference [key ])
658
+ + " actual: " + str (descriptor [key ])))
659
+ else :
660
+ self .validate_descriptor (test , reference [key ], descriptor [key ], context = context + key + "->" )
661
+ elif isinstance (reference , list ):
662
+ if len (reference ) > 0 and isinstance (reference [0 ], dict ):
663
+ # Convert to dict and validate
664
+ references = {item ['name' ]: item for item in reference }
665
+ descriptors = {item ['name' ]: item for item in descriptor }
666
+
667
+ self .validate_descriptor (test , references , descriptors , context )
668
+ elif reference != descriptor :
669
+ raise NMOSTestException (test .FAIL (context + "Unexpected sequence. Expected: "
670
+ + str (reference )
671
+ + " actual: " + str (descriptor )))
672
+ else :
673
+ if reference != descriptor :
674
+ raise NMOSTestException (test .FAIL (context + 'Expected value: '
675
+ + str (reference )
676
+ + ', actual value: '
677
+ + str (descriptor )))
678
+ return
679
+
680
+ def _get_class_manager_descriptors (self , test , class_manager_oid , property_id ):
681
+ response = self .get_property_value (test , class_manager_oid , property_id )
682
+
683
+ if not response :
684
+ return None
685
+
686
+ # Create descriptor dictionary from response array
687
+ # Use classId as key if present, otherwise use name
688
+ def key_lambda (classId , name ): return "." .join (map (str , classId )) if classId else name
689
+ descriptors = {key_lambda (r .get ('classId' ), r ['name' ]): r for r in response }
690
+
691
+ return descriptors
692
+
693
+ def query_device_model (self , test ):
694
+ """ Query Device Model from the Node under test.
695
+ self.device_model_metadata set on Device Model validation error.
696
+ NMOSTestException raised if unable to query Device Model """
697
+ self .open_ncp_websocket (test )
698
+ if not self .device_model :
699
+ self .device_model = self ._nc_object_factory (
700
+ test ,
701
+ StandardClassIds .NCBLOCK .value ,
702
+ self .ROOT_BLOCK_OID ,
703
+ "root" )
704
+
705
+ if not self .device_model :
706
+ raise NMOSTestException (test .FAIL ("Unable to query Device Model" ))
707
+ return self .device_model
708
+
709
+ def get_class_manager (self , test ):
710
+ """Get the Class Manager queried from the Node under test's Device Model"""
711
+ if not self .class_manager :
712
+ self .class_manager = self ._get_manager (test , StandardClassIds .NCCLASSMANAGER .value )
713
+
714
+ return self .class_manager
715
+
716
+ def get_device_manager (self , test ):
717
+ """Get the Device Manager queried from the Node under test's Device Model"""
718
+ return self ._get_manager (test , StandardClassIds .NCDEVICEMANAGER .value )
719
+
720
+ def _get_manager (self , test , class_id ):
721
+ self .open_ncp_websocket (test )
722
+ device_model = self .query_device_model (test )
723
+ members = device_model .find_members_by_class_id (class_id , include_derived = True )
724
+
725
+ spec_link = "https://specs.amwa.tv/ms-05-02/branches/{}/docs/Managers.html" .format (self .spec_branch )
726
+
727
+ if len (members ) == 0 :
728
+ raise NMOSTestException (test .FAIL ("Manager not found in Root Block." , spec_link ))
729
+
730
+ if len (members ) > 1 :
731
+ raise NMOSTestException (test .FAIL ("Manager MUST be a singleton." , spec_link ))
732
+
733
+ return members [0 ]
734
+
735
+ def _nc_object_factory (self , test , class_id , oid , role ):
736
+ """Create NcObject or NcBlock based on class_id"""
737
+ # will set self.device_model_error to True if problems encountered
738
+ try :
739
+ runtime_constraints = self .get_property_value (
740
+ test ,
741
+ oid ,
742
+ NcObjectProperties .RUNTIME_PROPERTY_CONSTRAINTS .value )
743
+
744
+ # Check class id to determine if this is a block
745
+ if len (class_id ) > 1 and class_id [0 ] == 1 and class_id [1 ] == 1 :
746
+ member_descriptors = self .get_property_value (
747
+ test ,
748
+ oid ,
749
+ NcBlockProperties .MEMBERS .value )
750
+
751
+ nc_block = NcBlock (class_id , oid , role , member_descriptors , runtime_constraints )
752
+
753
+ for m in member_descriptors :
754
+ child_object = self ._nc_object_factory (test , m ["classId" ], m ["oid" ], m ["role" ])
755
+ if child_object :
756
+ nc_block .add_child_object (child_object )
757
+
758
+ return nc_block
759
+ else :
760
+ # Check to determine if this is a Class Manager
761
+ if len (class_id ) > 2 and class_id [0 ] == 1 and class_id [1 ] == 3 and class_id [2 ] == 2 :
762
+ class_descriptors = self ._get_class_manager_descriptors (
763
+ test ,
764
+ oid ,
765
+ NcClassManagerProperties .CONTROL_CLASSES .value )
766
+
767
+ datatype_descriptors = self ._get_class_manager_descriptors (
768
+ test ,
769
+ oid ,
770
+ NcClassManagerProperties .DATATYPES .value )
771
+
772
+ if not class_descriptors or not datatype_descriptors :
773
+ # An error has likely occured
774
+ return None
775
+
776
+ return NcClassManager (class_id ,
777
+ oid ,
778
+ role ,
779
+ class_descriptors ,
780
+ datatype_descriptors ,
781
+ runtime_constraints )
782
+
783
+ return NcObject (class_id , oid , role , runtime_constraints )
784
+
785
+ except NMOSTestException as e :
786
+ raise NMOSTestException (test .FAIL ("Error in Device Model " + role + ": " + str (e .args [0 ].detail )))
787
+
788
+ def resolve_datatype (self , test , datatype ):
789
+ """Resolve datatype to its base type"""
790
+ class_manager = self .get_class_manager (test )
791
+ if class_manager .datatype_descriptors [datatype ].get ("parentType" ):
792
+ return self .resolve_datatype (test , class_manager .datatype_descriptors [datatype ].get ("parentType" ))
793
+ return datatype
794
+
545
795
546
796
class NcObject ():
547
797
def __init__ (self , class_id , oid , role , runtime_constraints ):
0 commit comments