Skip to content

Commit 95fe9e5

Browse files
Moved some functions to IS12Utils
1 parent fd8c9d3 commit 95fe9e5

File tree

3 files changed

+455
-639
lines changed

3 files changed

+455
-639
lines changed

nmostesting/IS12Utils.py

Lines changed: 261 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .NMOSUtils import NMOSUtils
1616

1717
import json
18+
import os
1819
import time
1920

2021
from copy import deepcopy
@@ -25,6 +26,11 @@
2526
from .GenericTest import NMOSTestException
2627
from .TestHelper import WebsocketWorker, load_resolved_schema
2728

29+
NODE_API_KEY = "node"
30+
CONTROL_API_KEY = "ncp"
31+
MS05_API_KEY = "controlframework"
32+
FEATURE_SETS_KEY = "featuresets"
33+
2834

2935
class MessageTypes(IntEnum):
3036
Command = 0
@@ -146,17 +152,21 @@ class StandardClassIds(Enum):
146152

147153

148154
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()
153161
self.ROOT_BLOCK_OID = 1
154162
self.ncp_websocket = None
155163
self.command_handle = 0
156164
self.expect_notifications = False
157165
self.notifications = []
166+
self.device_model = None
167+
self.class_manager = None
158168

159-
def load_is12_schemas(self, spec_path):
169+
def _load_is12_schemas(self):
160170
"""Load datatype and control class decriptors and create datatype JSON schemas"""
161171
# Load IS-12 schemas
162172
self.schemas = {}
@@ -165,16 +175,17 @@ def load_is12_schemas(self, spec_path):
165175
"subscription-response-message",
166176
"notification-message"]
167177
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")
169180

170-
def open_ncp_websocket(self, test, url):
181+
def open_ncp_websocket(self, test):
171182
"""Create a WebSocket client connection to Node under test. Raises NMOSTestException on error"""
172183
# Reuse socket if connection already established
173184
if self.ncp_websocket and self.ncp_websocket.is_open():
174185
return
175186

176187
# 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"])
178189
self.ncp_websocket.start()
179190

180191
# Give WebSocket client a chance to start and open its connection
@@ -194,7 +205,26 @@ def close_ncp_websocket(self):
194205
if self.ncp_websocket:
195206
self.ncp_websocket.close()
196207

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=""):
198228
"""Delegates to validate_schema. Raises NMOSTestExceptions on error"""
199229
try:
200230
# Validate the JSON schema is correct
@@ -245,7 +275,7 @@ def send_command(self, test, command_json):
245275
parsed_message = json.loads(message)
246276

247277
if self.message_type_to_schema_name(parsed_message.get("messageType")):
248-
self.validate_is12_schema(
278+
self._validate_is12_schema(
249279
test,
250280
parsed_message,
251281
self.message_type_to_schema_name(parsed_message["messageType"]),
@@ -324,7 +354,7 @@ def execute_command(self, test, oid, method_id, arguments):
324354
return response["result"]
325355

326356
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"""
328358
return self.execute_command(test, oid,
329359
NcObjectMethods.GENERIC_GET.value,
330360
{'id': property_id})["value"]
@@ -542,6 +572,226 @@ def is_manager(self, class_id):
542572
""" Check class id to determine if this is a manager """
543573
return len(class_id) > 1 and class_id[0] == 1 and class_id[1] == 3
544574

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+
545795

546796
class NcObject():
547797
def __init__(self, class_id, oid, role, runtime_constraints):

0 commit comments

Comments
 (0)