From b4f19e2fd198286c1b4c0b1cd867793b390a1676 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Sun, 9 Feb 2025 17:33:23 +0000 Subject: [PATCH 01/20] - initial proposal for testing both senders and receivers capabilities --- nmostesting/Config.py | 24 +- nmostesting/NMOSTesting.py | 49 +++- nmostesting/suites/CapabilitiesTest.py | 300 +++++++++++++++++++++++++ 3 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 nmostesting/suites/CapabilitiesTest.py diff --git a/nmostesting/Config.py b/nmostesting/Config.py index c663ec91d..31a991d43 100644 --- a/nmostesting/Config.py +++ b/nmostesting/Config.py @@ -163,8 +163,8 @@ # video/raw, etc. "width": 1920, "height": 1080, - "interlace": True, - "exactframerate": "25", + "interlace": False, + "exactframerate": "60", "depth": 10, "sampling": "YCbCr-4:2:2", "colorimetry": "BT709", @@ -316,10 +316,25 @@ } } }, + "bcp-004-02": { + "repo": "bcp-004-02", + # "versions": ["v1.0-dev"], + # "default_version": "v1.0-dev", + "branch": "to_sender_caps", + "versions": ["to_sender_caps"], + "default_version": "to_sender_caps", + "apis": { + "sender-caps": { + "name": "Sender Capabilities" + } + } + }, "nmos-parameter-registers": { "repo": "nmos-parameter-registers", - "versions": ["main"], - "default_version": "main", + "url": "https://github.com/alabou/", + "branch": "bcp-004-02", + "versions": ["bcp-004-02"], + "default_version": "bcp-004-02", "apis": { "caps-register": { "name": "Capabilities Register" @@ -352,3 +367,4 @@ sys.exit(-1) except ImportError: pass + diff --git a/nmostesting/NMOSTesting.py b/nmostesting/NMOSTesting.py index b7b307087..eb8de27ec 100644 --- a/nmostesting/NMOSTesting.py +++ b/nmostesting/NMOSTesting.py @@ -85,6 +85,7 @@ from .suites import BCP00301Test from .suites import BCP0060101Test from .suites import BCP0060102Test +from .suites import CapabilitiesTest FLASK_APPS = [] @@ -379,9 +380,35 @@ }], "class": BCP0060102Test.BCP0060102Test }, + "BCP-004": { + "name": "Capabilities", + "specs": [{ + "spec_key": "is-04", + "api_key": "node" + },{ + "spec_key": "is-05", + "api_key": "connection" + }], + "extra_specs": [{ + "spec_key": "nmos-parameter-registers", + "api_key": "flow-register" + }, { + "spec_key": "nmos-parameter-registers", + "api_key": "sender-register" + }, { + "spec_key": "nmos-parameter-registers", + "api_key": "caps-register" + }, { + "spec_key": "bcp-004-01", + "api_key": "receiver-caps" + }, { + "spec_key": "bcp-004-02", + "api_key": "sender-caps" + }], + "class": CapabilitiesTest.CapabilitiesTest + }, } - def enumerate_tests(class_def, describe=False): if describe: tests = ["all: Runs all tests in the suite", @@ -655,8 +682,24 @@ def init_spec_cache(): if repo_data["repo"] is None: continue if not os.path.exists(path): - print(" * Initialising repository '{}'".format(repo_data["repo"])) - repo = git.Repo.clone_from('https://github.com/AMWA-TV/' + repo_data["repo"] + '.git', path) + + if not "url" in repo_data or repo_data["url"] is None: + repo_url = 'https://github.com/AMWA-TV/' + else: + repo_url = repo_data["url"] + if not "branch" in repo_data or repo_data["branch"] is None: + repo_branch = None + else: + repo_branch = repo_data["branch"] + + print(" * Initialising repository '{}' from branch '{}' at url '{}'".format(repo_data["repo"], repo_branch, repo_url)) + + repo = git.Repo.clone_from(repo_url + repo_data["repo"] + '.git', path) + + if not repo_branch is None: + repo.git.checkout(repo_branch) + print(repo.git.status()) + update_last_pull = True else: repo = git.Repo(path) diff --git a/nmostesting/suites/CapabilitiesTest.py b/nmostesting/suites/CapabilitiesTest.py new file mode 100644 index 000000000..395379c8b --- /dev/null +++ b/nmostesting/suites/CapabilitiesTest.py @@ -0,0 +1,300 @@ +# Copyright (C) 2024 Matrox Graphics Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# python3 nmos-test.py suite BCP-004 --host 127.0.0.1 127.0.0.1 --port 5058 5058 --version v1.3 v1.1 + +import json +import re +import os + +from jsonschema import ValidationError + +from ..GenericTest import GenericTest, NMOSTestException +from ..IS04Utils import IS04Utils +from ..IS05Utils import IS05Utils +from ..TestHelper import load_resolved_schema +from ..TestHelper import check_content_type + +from urllib.parse import urlparse +from pathlib import Path + +NODE_API_KEY = "node" +CONNECTION_API_KEY = "connection" +RECEIVER_CAPS_KEY = "receiver-caps" +SENDER_CAPS_KEY = "sender-caps" + +# Generic capabilities from any namespace +def extract_after_cap(s): + match = re.search(r'^urn:(x-nmos|x-[a-z]+):cap:(.*)', s) + return match.group(2) if match else None + +class CapabilitiesTest(GenericTest): + """ + Runs Node Tests covering sender and receiver capabilities + """ + def __init__(self, apis, **kwargs): + # Don't auto-test /transportfile as it is permitted to generate a 404 when master_enable is false + omit_paths = [ + "/single/senders/{senderId}/transportfile", + "/single/senders/{senderId}/staged", + "/single/senders/{senderId}/active", + "/single/senders/{senderId}/constraints", + "/single/senders/{senderId}/transporttype", + "/single/receivers/{receiverId}/staged", + "/single/receivers/{receiverId}/active", + "/single/receivers/{receiverId}/constraints", + "/single/receivers/{receiverId}/transporttype", + ] + GenericTest.__init__(self, apis, omit_paths, **kwargs) + self.node_url = self.apis[NODE_API_KEY]["url"] + self.connection_url = self.apis[CONNECTION_API_KEY]["url"] + self.is04_resources = {"senders": {}, "receivers": {}, "_requested": [], "sources": {}, "flows": {}} + self.is05_resources = {"senders": [], "receivers": [], "_requested": [], "transport_types": {}, "transport_files": {}} + self.is04_utils = IS04Utils(self.node_url) + self.is05_utils = IS05Utils(self.connection_url) + + # Utility function from IS0502Test + def get_is04_resources(self, resource_type): + """Retrieve all Senders or Receivers from a Node API, keeping hold of the returned objects""" + assert resource_type in ["senders", "receivers", "sources", "flows"] + + # Prevent this being executed twice in one test run + if resource_type in self.is04_resources["_requested"]: + return True, "" + + path_url = resource_type + full_url = self.node_url + path_url + valid, resources = self.do_request("GET", full_url) + if not valid: + return False, "Node API did not respond as expected: {}".format(resources) + schema = self.get_schema(NODE_API_KEY, "GET", "/" + path_url, resources.status_code) + valid, message = self.check_response(schema, "GET", resources) + if not valid: + raise NMOSTestException(message) + + try: + for resource in resources.json(): + self.is04_resources[resource_type][resource["id"]] = resource + self.is04_resources["_requested"].append(resource_type) + except json.JSONDecodeError: + return False, "Non-JSON response returned from Node API" + + return True, "" + + def get_is05_partial_resources(self, resource_type): + """Retrieve all Senders or Receivers from a Connection API, keeping hold of the returned IDs""" + assert resource_type in ["senders", "receivers"] + + # Prevent this being executed twice in one test run + if resource_type in self.is05_resources["_requested"]: + return True, "" + + path_url = "single/" + resource_type + full_url = self.connection_url + path_url + valid, resources = self.do_request("GET", full_url) + if not valid: + return False, "Connection API did not respond as expected: {}".format(resources) + + schema = self.get_schema(CONNECTION_API_KEY, "GET", "/" + path_url, resources.status_code) + valid, message = self.check_response(schema, "GET", resources) + if not valid: + raise NMOSTestException(message) + + # The following call to is05_utils.get_transporttype does not validate against the IS-05 schemas, + # which is good for allowing extended transport. The transporttype-response-schema.json schema is + # broken as it does not allow additional transport, nor x-nmos ones, nor vendor specific ones. + try: + for resource in resources.json(): + resource_id = resource.rstrip("/") + self.is05_resources[resource_type].append(resource_id) + if self.is05_utils.compare_api_version(self.apis[CONNECTION_API_KEY]["version"], "v1.1") >= 0: + transport_type = self.is05_utils.get_transporttype(resource_id, resource_type.rstrip("s")) + self.is05_resources["transport_types"][resource_id] = transport_type + else: + self.is05_resources["transport_types"][resource_id] = "urn:x-nmos:transport:rtp" + if resource_type == "senders": + transport_file = self.is05_utils.get_transportfile(resource_id) + self.is05_resources["transport_files"][resource_id] = transport_file + self.is05_resources["_requested"].append(resource_type) + except json.JSONDecodeError: + return False, "Non-JSON response returned from Node API" + + return True, "" + + def check_response_without_transport_params(self, schema, method, response): + """Confirm that a given Requests response conforms to the expected schema and has any expected headers without considering the 'transport_params' attribute""" + ctype_valid, ctype_message = check_content_type(response.headers) + if not ctype_valid: + return False, ctype_message + + cors_valid, cors_message = self.check_CORS(method, response.headers) + if not cors_valid: + return False, cors_message + + fields_to_ignore = ["transport_params"] + + data = response.json() + + filtered_data = {k: v for k, v in data.items() if k not in fields_to_ignore} + + filtered_data["transport_params"] = [] + + try: + self.validate_schema(filtered_data, schema) + except ValidationError as e: + return False, "Response schema validation error {}".format(e) + except json.JSONDecodeError: + return False, "Invalid JSON received" + + return True, ctype_message + + def test_01(self, test): + """Check that version 1.3 or greater of the Node API is available""" + + api = self.apis[NODE_API_KEY] + if self.is04_utils.compare_api_version(api["version"], "v1.3") >= 0: + valid, result = self.do_request("GET", self.node_url) + if valid: + return test.PASS() + else: + return test.FAIL("Node API did not respond as expected: {}".format(result)) + else: + return test.FAIL("Node API must be running v1.3 or greater in order to run this test suite") + + def test_02(self, test): + + """Check Receiver Capabilities""" + + api = self.apis[RECEIVER_CAPS_KEY] + + reg_api = self.apis["caps-register"] + reg_path = reg_api["spec_path"] + "/capabilities" + + valid, result = self.get_is04_resources("receivers") + if not valid: + return test.FAIL(result) + + schema = load_resolved_schema(api["spec_path"], "receiver_constraint_sets.json") + + reg_schema_file = str(Path(os.path.abspath(reg_path)) / "constraint_set.json") + with open(reg_schema_file, "r") as f: + reg_schema_obj = json.load(f) + reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) + + warning = "" + + for receiver in self.is04_resources["receivers"].values(): + if "constraint_sets" in receiver["caps"]: + try: + self.validate_schema(receiver, schema) + except ValidationError as e: + return test.FAIL("Receiver {} does not comply with schema".format(receiver["id"])) + + for constraint_set in receiver["caps"]["constraint_sets"]: + try: + self.validate_schema(constraint_set, reg_schema) + except ValidationError as e: + return test.FAIL("Receiver {} constraint_sets do not comply with schema".format(receiver["id"])) + + for param_constraint in constraint_set: + # enumeration do not allow empty arrays by schema, disallow empty range by test + if not extract_after_cap(param_constraint).startswith("meta:"): + if "minimum" in param_constraint and "maximum" in param_constraint: + if compare_min_larger_than_max(param_constraint): + warning += "|" + "Receiver {} parameter constraint {} has an invalid empty range".format(receiver["id"], param_constraint) + else: + warning += "|" + "Receiver {} not having constraint_sets".format(receiver["id"]) + + if warning != "": + return test.WARNING(warning) + else: + return test.PASS() + + def test_03(self, test): + + """Check Sender Capabilities""" + + api = self.apis[SENDER_CAPS_KEY] + + reg_api = self.apis["caps-register"] + reg_path = reg_api["spec_path"] + "/capabilities" + + valid, result = self.get_is04_resources("senders") + if not valid: + return test.FAIL(result) + + schema = load_resolved_schema(api["spec_path"], "sender_constraint_sets.json") + + reg_schema_file = str(Path(os.path.abspath(reg_path)) / "constraint_set.json") + with open(reg_schema_file, "r") as f: + reg_schema_obj = json.load(f) + reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) + + warning = "" + + for sender in self.is04_resources["senders"].values(): + + # Make sure Senders do not use the Receiver's specific "media_types" attribute in their caps + if "media_types" in sender["caps"]: + return test.FAIL("Sender {} has an illegal 'media_types' attribute in its caps".format(sender["id"])) + + # Make sure Senders do not use the Receiver's specific "event_types" attribute in their caps + if "event_types" in sender["caps"]: + return test.FAIL("Sender {} has an illegal 'event_types' attribute in its caps".format(sender["id"])) + + if "constraint_sets" in sender["caps"]: + try: + self.validate_schema(sender, schema) + except ValidationError as e: + return test.FAIL("Sender {} does not comply with schema".format(sender["id"])) + + for constraint_set in sender["caps"]["constraint_sets"]: + try: + self.validate_schema(constraint_set, reg_schema) + except ValidationError as e: + return test.FAIL("Sender {} constraint_sets do not comply with schema".format(sender["id"])) + + for param_constraint in constraint_set: + # enumeration do not allow empty arrays by schema, disallow empty range by test + if not extract_after_cap(param_constraint).startswith("meta:"): + if "minimum" in param_constraint and "maximum" in param_constraint: + if compare_min_larger_than_max(param_constraint): + warning += "|" + "Sender {} parameter constraint {} has an invalid empty range".format(sender["id"], param_constraint) + else: + warning += "|" + "Sender {} not having constraint_sets".format(sender["id"]) + + if warning != "": + return test.WARNING(warning) + else: + return test.PASS() + +def compare_min_larger_than_max(param_constraint): + + min_val = param_constraint["minimum"] + max_val = param_constraint["maximum"] + + if isinstance(min_val, int) and isinstance(max_val, int): + return min_val > max_val + elif isinstance(min_val, float) and isinstance(max_val, float): + return min_val > max_val + elif isinstance(min_val, (int, float)) and isinstance(max_val, (int, float)): + return float(min_val) > float(max_val) + elif isinstance(min_val, dict) and isinstance(max_val, dict): + min_num = min_val["numerator"] + max_num = max_val["numerator"] + min_den = min_val.get("denominator", 1) + max_den = max_val.get("denominator", 1) + return (min_num*max_den) > (max_num*min_den) + + return False \ No newline at end of file From 7df0e187fff201e2f4a4981e2e4c08aa5124ee29 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Thu, 13 Feb 2025 19:49:19 +0000 Subject: [PATCH 02/20] add missing verifications based on normative MUST and SHOULD of BCP-004 --- nmostesting/suites/CapabilitiesTest.py | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/nmostesting/suites/CapabilitiesTest.py b/nmostesting/suites/CapabilitiesTest.py index 395379c8b..afb6d05ec 100644 --- a/nmostesting/suites/CapabilitiesTest.py +++ b/nmostesting/suites/CapabilitiesTest.py @@ -192,6 +192,11 @@ def test_02(self, test): reg_schema_obj = json.load(f) reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) + # load the Capabilities register schema as JSON as we're only interested in the list of properties + reg_schema_file = str(Path(os.path.abspath(reg_path)) / "constraint_set.json") + with open(reg_schema_file, "r") as f: + reg_schema_obj = json.load(f) + warning = "" for receiver in self.is04_resources["receivers"].values(): @@ -201,18 +206,51 @@ def test_02(self, test): except ValidationError as e: return test.FAIL("Receiver {} does not comply with schema".format(receiver["id"])) + try: + caps_version = receiver["caps"]["version"] + core_version = receiver["version"] + + if self.is04_utils.compare_resource_version(caps_version, core_version) > 0: + return test.FAIL("Receiver {} caps version is later than resource version".format(receiver["id"])) + + except ValidationError as e: + return test.FAIL("Receiver {} do not comply with schema".format(receiver["id"])) + + has_label = None + warn_label = False + for constraint_set in receiver["caps"]["constraint_sets"]: try: self.validate_schema(constraint_set, reg_schema) except ValidationError as e: return test.FAIL("Receiver {} constraint_sets do not comply with schema".format(receiver["id"])) + has_current_label = "urn:x-nmos:cap:meta:label" in constraint_set + + # Ensure consistent labeling across all constraint_sets + if has_label is None: + has_label = has_current_label + elif has_label != has_current_label: + warn_label = True + + has_pattern_attribute = False for param_constraint in constraint_set: # enumeration do not allow empty arrays by schema, disallow empty range by test if not extract_after_cap(param_constraint).startswith("meta:"): + has_pattern_attribute = True if "minimum" in param_constraint and "maximum" in param_constraint: if compare_min_larger_than_max(param_constraint): warning += "|" + "Receiver {} parameter constraint {} has an invalid empty range".format(receiver["id"], param_constraint) + + if param_constraint.startswith("urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: + warning += "|" + "Receiver {} parameter constraint {} is not registered ".format(receiver["id"], param_constraint) + + if not has_pattern_attribute: + return test.FAIL("Receiver {} has an illegal constraint set without any parameter attribute".format(receiver["id"])) + + if warn_label: + warning += "|" + "Receiver {} constraint_sets should either 'urn:x-nmos:cap:meta:label' for all constraint sets or none".format(receiver["id"]) + else: warning += "|" + "Receiver {} not having constraint_sets".format(receiver["id"]) @@ -241,6 +279,11 @@ def test_03(self, test): reg_schema_obj = json.load(f) reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) + # load the Capabilities register schema as JSON as we're only interested in the list of properties + reg_schema_file = str(Path(os.path.abspath(reg_path)) / "constraint_set.json") + with open(reg_schema_file, "r") as f: + reg_schema_obj = json.load(f) + warning = "" for sender in self.is04_resources["senders"].values(): @@ -254,23 +297,57 @@ def test_03(self, test): return test.FAIL("Sender {} has an illegal 'event_types' attribute in its caps".format(sender["id"])) if "constraint_sets" in sender["caps"]: + try: self.validate_schema(sender, schema) except ValidationError as e: return test.FAIL("Sender {} does not comply with schema".format(sender["id"])) + try: + caps_version = sender["caps"]["version"] + core_version = sender["version"] + + if self.is04_utils.compare_resource_version(caps_version, core_version) > 0: + return test.FAIL("Sender {} caps version is later than resource version".format(sender["id"])) + + except ValidationError as e: + return test.FAIL("Sender {} do not comply with schema".format(sender["id"])) + + has_label = None + warn_label = False + for constraint_set in sender["caps"]["constraint_sets"]: try: self.validate_schema(constraint_set, reg_schema) except ValidationError as e: return test.FAIL("Sender {} constraint_sets do not comply with schema".format(sender["id"])) + has_current_label = "urn:x-nmos:cap:meta:label" in constraint_set + + # Ensure consistent labeling across all constraint_sets + if has_label is None: + has_label = has_current_label + elif has_label != has_current_label: + warn_label = True + + has_pattern_attribute = False for param_constraint in constraint_set: # enumeration do not allow empty arrays by schema, disallow empty range by test if not extract_after_cap(param_constraint).startswith("meta:"): + has_pattern_attribute = True if "minimum" in param_constraint and "maximum" in param_constraint: if compare_min_larger_than_max(param_constraint): warning += "|" + "Sender {} parameter constraint {} has an invalid empty range".format(sender["id"], param_constraint) + + if param_constraint.startswith("urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: + warning += "|" + "Sender {} parameter constraint {} is not registered ".format(sender["id"], param_constraint) + + if not has_pattern_attribute: + return test.FAIL("Sender {} has an illegal constraint set without any parameter attribute".format(sender["id"])) + + if warn_label: + warning += "|" + "Sender {} constraint_sets should either 'urn:x-nmos:cap:meta:label' for all constraint sets or none".format(sender["id"]) + else: warning += "|" + "Sender {} not having constraint_sets".format(sender["id"]) From 2b0757ddadff1cdfef07f2ce5163f6c0d0b3dbd3 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Thu, 13 Feb 2025 23:32:59 +0000 Subject: [PATCH 03/20] - remove duplicate lines --- nmostesting/suites/CapabilitiesTest.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/nmostesting/suites/CapabilitiesTest.py b/nmostesting/suites/CapabilitiesTest.py index afb6d05ec..8d0a6ea0f 100644 --- a/nmostesting/suites/CapabilitiesTest.py +++ b/nmostesting/suites/CapabilitiesTest.py @@ -192,11 +192,6 @@ def test_02(self, test): reg_schema_obj = json.load(f) reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) - # load the Capabilities register schema as JSON as we're only interested in the list of properties - reg_schema_file = str(Path(os.path.abspath(reg_path)) / "constraint_set.json") - with open(reg_schema_file, "r") as f: - reg_schema_obj = json.load(f) - warning = "" for receiver in self.is04_resources["receivers"].values(): @@ -279,11 +274,6 @@ def test_03(self, test): reg_schema_obj = json.load(f) reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) - # load the Capabilities register schema as JSON as we're only interested in the list of properties - reg_schema_file = str(Path(os.path.abspath(reg_path)) / "constraint_set.json") - with open(reg_schema_file, "r") as f: - reg_schema_obj = json.load(f) - warning = "" for sender in self.is04_resources["senders"].values(): From 2e3f91639e5309d9677b09b5d4ef5df383ccc32a Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Thu, 20 Feb 2025 16:54:10 +0000 Subject: [PATCH 04/20] - fix namespace regexp --- nmostesting/suites/CapabilitiesTest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nmostesting/suites/CapabilitiesTest.py b/nmostesting/suites/CapabilitiesTest.py index 8d0a6ea0f..9231e378e 100644 --- a/nmostesting/suites/CapabilitiesTest.py +++ b/nmostesting/suites/CapabilitiesTest.py @@ -35,8 +35,8 @@ SENDER_CAPS_KEY = "sender-caps" # Generic capabilities from any namespace -def extract_after_cap(s): - match = re.search(r'^urn:(x-nmos|x-[a-z]+):cap:(.*)', s) +def cap_without_namespace(s): + match = re.search(r'^urn:[a-z0-9][a-z0-9-]+:cap:(.*)', s) return match.group(2) if match else None class CapabilitiesTest(GenericTest): @@ -231,7 +231,7 @@ def test_02(self, test): has_pattern_attribute = False for param_constraint in constraint_set: # enumeration do not allow empty arrays by schema, disallow empty range by test - if not extract_after_cap(param_constraint).startswith("meta:"): + if not cap_without_namespace(param_constraint).startswith("meta:"): has_pattern_attribute = True if "minimum" in param_constraint and "maximum" in param_constraint: if compare_min_larger_than_max(param_constraint): @@ -323,7 +323,7 @@ def test_03(self, test): has_pattern_attribute = False for param_constraint in constraint_set: # enumeration do not allow empty arrays by schema, disallow empty range by test - if not extract_after_cap(param_constraint).startswith("meta:"): + if not cap_without_namespace(param_constraint).startswith("meta:"): has_pattern_attribute = True if "minimum" in param_constraint and "maximum" in param_constraint: if compare_min_larger_than_max(param_constraint): From a6a3bdf1d8564bb8ad5d52b383ded57a0ec18ad6 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Thu, 13 Mar 2025 19:57:50 +0000 Subject: [PATCH 05/20] - have a test suite for sender capabilities only. - link to v1.0-dev of BCP-004-02 - fix detection of unnecessary media_types and event_types - fix test status if there is no sender or no constraint_sets --- nmostesting/Config.py | 6 +- nmostesting/NMOSTesting.py | 9 +- ...{CapabilitiesTest.py => BCP0040201Test.py} | 132 +++++------------- 3 files changed, 39 insertions(+), 108 deletions(-) rename nmostesting/suites/{CapabilitiesTest.py => BCP0040201Test.py} (67%) diff --git a/nmostesting/Config.py b/nmostesting/Config.py index 31a991d43..930722264 100644 --- a/nmostesting/Config.py +++ b/nmostesting/Config.py @@ -320,9 +320,9 @@ "repo": "bcp-004-02", # "versions": ["v1.0-dev"], # "default_version": "v1.0-dev", - "branch": "to_sender_caps", - "versions": ["to_sender_caps"], - "default_version": "to_sender_caps", + "branch": "v1.0-dev", + "versions": ["v1.0-dev"], + "default_version": "v1.0-dev", "apis": { "sender-caps": { "name": "Sender Capabilities" diff --git a/nmostesting/NMOSTesting.py b/nmostesting/NMOSTesting.py index eb8de27ec..825de6982 100644 --- a/nmostesting/NMOSTesting.py +++ b/nmostesting/NMOSTesting.py @@ -85,8 +85,7 @@ from .suites import BCP00301Test from .suites import BCP0060101Test from .suites import BCP0060102Test -from .suites import CapabilitiesTest - +from .suites import BCP0040201Test FLASK_APPS = [] DNS_SERVER = None @@ -380,8 +379,8 @@ }], "class": BCP0060102Test.BCP0060102Test }, - "BCP-004": { - "name": "Capabilities", + "BCP-004-02": { + "name": "Sender Capabilities", "specs": [{ "spec_key": "is-04", "api_key": "node" @@ -405,7 +404,7 @@ "spec_key": "bcp-004-02", "api_key": "sender-caps" }], - "class": CapabilitiesTest.CapabilitiesTest + "class": BCP0040201Test.BCP0040201Test }, } diff --git a/nmostesting/suites/CapabilitiesTest.py b/nmostesting/suites/BCP0040201Test.py similarity index 67% rename from nmostesting/suites/CapabilitiesTest.py rename to nmostesting/suites/BCP0040201Test.py index 9231e378e..dd9989e1a 100644 --- a/nmostesting/suites/CapabilitiesTest.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Matrox Graphics Inc. +# Copyright (C) 2025 Matrox Graphics Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# python3 nmos-test.py suite BCP-004 --host 127.0.0.1 127.0.0.1 --port 5058 5058 --version v1.3 v1.1 +# python3 nmos-test.py suite BCP-004-02 --host 127.0.0.1 127.0.0.1 --port 5058 5058 --version v1.3 v1.1 import json import re @@ -37,9 +37,9 @@ # Generic capabilities from any namespace def cap_without_namespace(s): match = re.search(r'^urn:[a-z0-9][a-z0-9-]+:cap:(.*)', s) - return match.group(2) if match else None + return match.group(1) if match else None -class CapabilitiesTest(GenericTest): +class BCP0040201Test(GenericTest): """ Runs Node Tests covering sender and receiver capabilities """ @@ -174,88 +174,6 @@ def test_01(self, test): def test_02(self, test): - """Check Receiver Capabilities""" - - api = self.apis[RECEIVER_CAPS_KEY] - - reg_api = self.apis["caps-register"] - reg_path = reg_api["spec_path"] + "/capabilities" - - valid, result = self.get_is04_resources("receivers") - if not valid: - return test.FAIL(result) - - schema = load_resolved_schema(api["spec_path"], "receiver_constraint_sets.json") - - reg_schema_file = str(Path(os.path.abspath(reg_path)) / "constraint_set.json") - with open(reg_schema_file, "r") as f: - reg_schema_obj = json.load(f) - reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) - - warning = "" - - for receiver in self.is04_resources["receivers"].values(): - if "constraint_sets" in receiver["caps"]: - try: - self.validate_schema(receiver, schema) - except ValidationError as e: - return test.FAIL("Receiver {} does not comply with schema".format(receiver["id"])) - - try: - caps_version = receiver["caps"]["version"] - core_version = receiver["version"] - - if self.is04_utils.compare_resource_version(caps_version, core_version) > 0: - return test.FAIL("Receiver {} caps version is later than resource version".format(receiver["id"])) - - except ValidationError as e: - return test.FAIL("Receiver {} do not comply with schema".format(receiver["id"])) - - has_label = None - warn_label = False - - for constraint_set in receiver["caps"]["constraint_sets"]: - try: - self.validate_schema(constraint_set, reg_schema) - except ValidationError as e: - return test.FAIL("Receiver {} constraint_sets do not comply with schema".format(receiver["id"])) - - has_current_label = "urn:x-nmos:cap:meta:label" in constraint_set - - # Ensure consistent labeling across all constraint_sets - if has_label is None: - has_label = has_current_label - elif has_label != has_current_label: - warn_label = True - - has_pattern_attribute = False - for param_constraint in constraint_set: - # enumeration do not allow empty arrays by schema, disallow empty range by test - if not cap_without_namespace(param_constraint).startswith("meta:"): - has_pattern_attribute = True - if "minimum" in param_constraint and "maximum" in param_constraint: - if compare_min_larger_than_max(param_constraint): - warning += "|" + "Receiver {} parameter constraint {} has an invalid empty range".format(receiver["id"], param_constraint) - - if param_constraint.startswith("urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: - warning += "|" + "Receiver {} parameter constraint {} is not registered ".format(receiver["id"], param_constraint) - - if not has_pattern_attribute: - return test.FAIL("Receiver {} has an illegal constraint set without any parameter attribute".format(receiver["id"])) - - if warn_label: - warning += "|" + "Receiver {} constraint_sets should either 'urn:x-nmos:cap:meta:label' for all constraint sets or none".format(receiver["id"]) - - else: - warning += "|" + "Receiver {} not having constraint_sets".format(receiver["id"]) - - if warning != "": - return test.WARNING(warning) - else: - return test.PASS() - - def test_03(self, test): - """Check Sender Capabilities""" api = self.apis[SENDER_CAPS_KEY] @@ -274,20 +192,27 @@ def test_03(self, test): reg_schema_obj = json.load(f) reg_schema = load_resolved_schema(api["spec_path"], schema_obj=reg_schema_obj) + no_constraint_sets = True + warning = "" + if len(self.is04_resources["senders"].values()) == 0: + return test.UNCLEAR("No Senders were found on the Node") + for sender in self.is04_resources["senders"].values(): # Make sure Senders do not use the Receiver's specific "media_types" attribute in their caps if "media_types" in sender["caps"]: - return test.FAIL("Sender {} has an illegal 'media_types' attribute in its caps".format(sender["id"])) + warning += "|" + "Sender {} caps has an unnecessary 'media_types' attribute that is not used with sender capabilities".format(sender["id"]) # Make sure Senders do not use the Receiver's specific "event_types" attribute in their caps if "event_types" in sender["caps"]: - return test.FAIL("Sender {} has an illegal 'event_types' attribute in its caps".format(sender["id"])) + warning += "|" + "Sender {} caps has an unnecessary 'event_types' attribute that is not used with sender capabilities".format(sender["id"]) if "constraint_sets" in sender["caps"]: + no_constraint_sets = False + try: self.validate_schema(sender, schema) except ValidationError as e: @@ -301,7 +226,7 @@ def test_03(self, test): return test.FAIL("Sender {} caps version is later than resource version".format(sender["id"])) except ValidationError as e: - return test.FAIL("Sender {} do not comply with schema".format(sender["id"])) + return test.FAIL("Sender {} caps do not have a version attribute".format(sender["id"])) has_label = None warn_label = False @@ -322,15 +247,22 @@ def test_03(self, test): has_pattern_attribute = False for param_constraint in constraint_set: - # enumeration do not allow empty arrays by schema, disallow empty range by test - if not cap_without_namespace(param_constraint).startswith("meta:"): - has_pattern_attribute = True - if "minimum" in param_constraint and "maximum" in param_constraint: - if compare_min_larger_than_max(param_constraint): - warning += "|" + "Sender {} parameter constraint {} has an invalid empty range".format(sender["id"], param_constraint) - if param_constraint.startswith("urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: - warning += "|" + "Sender {} parameter constraint {} is not registered ".format(sender["id"], param_constraint) + try: + # enumeration do not allow empty arrays by schema, disallow empty range by test + if not cap_without_namespace(param_constraint).startswith("meta:"): + has_pattern_attribute = True + if "minimum" in param_constraint and "maximum" in param_constraint: + if compare_min_larger_than_max(param_constraint): + warning += "|" + "Sender {} parameter constraint {} has an invalid empty range".format(sender["id"], param_constraint) + + # parameter constraints in the x-nmos namespace should be listed in the Capabilities register + if param_constraint.startswith("urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: + warning += "|" + "Sender {} parameter constraint {} is not registered ".format(sender["id"], param_constraint) + + except: + return test.FAIL("Sender {} has an invalid parameter constraint {}".format(sender["id"], param_constraint)) + if not has_pattern_attribute: return test.FAIL("Sender {} has an illegal constraint set without any parameter attribute".format(sender["id"])) @@ -338,8 +270,8 @@ def test_03(self, test): if warn_label: warning += "|" + "Sender {} constraint_sets should either 'urn:x-nmos:cap:meta:label' for all constraint sets or none".format(sender["id"]) - else: - warning += "|" + "Sender {} not having constraint_sets".format(sender["id"]) + if no_constraint_sets: + return test.OPTIONAL("No BCP-004-02 'constraint_sets' were identified in Sender caps") if warning != "": return test.WARNING(warning) @@ -364,4 +296,4 @@ def compare_min_larger_than_max(param_constraint): max_den = max_val.get("denominator", 1) return (min_num*max_den) > (max_num*min_den) - return False \ No newline at end of file + return False From 96836f9b06fec87929294efaa6bf29da816dd34d Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 28 Mar 2025 19:37:25 +0000 Subject: [PATCH 06/20] flake8 and autopep8 --- nmostesting/suites/BCP0040201Test.py | 76 ++++++++++++++++++---------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index dd9989e1a..b9b688118 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -26,7 +26,6 @@ from ..TestHelper import load_resolved_schema from ..TestHelper import check_content_type -from urllib.parse import urlparse from pathlib import Path NODE_API_KEY = "node" @@ -34,15 +33,18 @@ RECEIVER_CAPS_KEY = "receiver-caps" SENDER_CAPS_KEY = "sender-caps" + # Generic capabilities from any namespace def cap_without_namespace(s): match = re.search(r'^urn:[a-z0-9][a-z0-9-]+:cap:(.*)', s) return match.group(1) if match else None + class BCP0040201Test(GenericTest): """ Runs Node Tests covering sender and receiver capabilities """ + def __init__(self, apis, **kwargs): # Don't auto-test /transportfile as it is permitted to generate a 404 when master_enable is false omit_paths = [ @@ -60,7 +62,12 @@ def __init__(self, apis, **kwargs): self.node_url = self.apis[NODE_API_KEY]["url"] self.connection_url = self.apis[CONNECTION_API_KEY]["url"] self.is04_resources = {"senders": {}, "receivers": {}, "_requested": [], "sources": {}, "flows": {}} - self.is05_resources = {"senders": [], "receivers": [], "_requested": [], "transport_types": {}, "transport_files": {}} + self.is05_resources = { + "senders": [], + "receivers": [], + "_requested": [], + "transport_types": {}, + "transport_files": {}} self.is04_utils = IS04Utils(self.node_url) self.is05_utils = IS05Utils(self.connection_url) @@ -133,7 +140,8 @@ def get_is05_partial_resources(self, resource_type): return True, "" def check_response_without_transport_params(self, schema, method, response): - """Confirm that a given Requests response conforms to the expected schema and has any expected headers without considering the 'transport_params' attribute""" + """Confirm that a given Requests response conforms to the expected schema and has any expected headers + without considering the 'transport_params' attribute""" ctype_valid, ctype_message = check_content_type(response.headers) if not ctype_valid: return False, ctype_message @@ -173,7 +181,6 @@ def test_01(self, test): return test.FAIL("Node API must be running v1.3 or greater in order to run this test suite") def test_02(self, test): - """Check Sender Capabilities""" api = self.apis[SENDER_CAPS_KEY] @@ -203,39 +210,42 @@ def test_02(self, test): # Make sure Senders do not use the Receiver's specific "media_types" attribute in their caps if "media_types" in sender["caps"]: - warning += "|" + "Sender {} caps has an unnecessary 'media_types' attribute that is not used with sender capabilities".format(sender["id"]) + warning += ("|" + "Sender {} caps has an unnecessary 'media_types' attribute " + "that is not used with sender capabilities".format(sender["id"])) # Make sure Senders do not use the Receiver's specific "event_types" attribute in their caps if "event_types" in sender["caps"]: - warning += "|" + "Sender {} caps has an unnecessary 'event_types' attribute that is not used with sender capabilities".format(sender["id"]) + warning += ("|" + "Sender {} caps has an unnecessary 'event_types' attribute " + "that is not used with sender capabilities".format(sender["id"])) if "constraint_sets" in sender["caps"]: - + no_constraint_sets = False try: self.validate_schema(sender, schema) except ValidationError as e: - return test.FAIL("Sender {} does not comply with schema".format(sender["id"])) + return test.FAIL("Sender {} does not comply with schema, error {}".format(sender["id"], e)) try: caps_version = sender["caps"]["version"] core_version = sender["version"] - + if self.is04_utils.compare_resource_version(caps_version, core_version) > 0: return test.FAIL("Sender {} caps version is later than resource version".format(sender["id"])) - except ValidationError as e: + except BaseException: return test.FAIL("Sender {} caps do not have a version attribute".format(sender["id"])) has_label = None warn_label = False - + for constraint_set in sender["caps"]["constraint_sets"]: try: self.validate_schema(constraint_set, reg_schema) except ValidationError as e: - return test.FAIL("Sender {} constraint_sets do not comply with schema".format(sender["id"])) + return test.FAIL("Sender {} constraint_sets do not comply with schema, error {}".format( + sender["id"], e)) has_current_label = "urn:x-nmos:cap:meta:label" in constraint_set @@ -244,7 +254,7 @@ def test_02(self, test): has_label = has_current_label elif has_label != has_current_label: warn_label = True - + has_pattern_attribute = False for param_constraint in constraint_set: @@ -254,21 +264,30 @@ def test_02(self, test): has_pattern_attribute = True if "minimum" in param_constraint and "maximum" in param_constraint: if compare_min_larger_than_max(param_constraint): - warning += "|" + "Sender {} parameter constraint {} has an invalid empty range".format(sender["id"], param_constraint) - - # parameter constraints in the x-nmos namespace should be listed in the Capabilities register - if param_constraint.startswith("urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: - warning += "|" + "Sender {} parameter constraint {} is not registered ".format(sender["id"], param_constraint) - - except: - return test.FAIL("Sender {} has an invalid parameter constraint {}".format(sender["id"], param_constraint)) - + warning += "|" + \ + "Sender {} parameter constraint {} has an invalid empty range".format( + sender["id"], param_constraint) + + # parameter constraints in the x-nmos namespace should be listed in the + # Capabilities register + if param_constraint.startswith( + "urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: + warning += ("|" + "Sender {} parameter constraint {}" + " is not registered ".format(sender["id"], param_constraint)) + + except BaseException: + return test.FAIL( + "Sender {} has an invalid parameter constraint {}".format( + sender["id"], param_constraint)) if not has_pattern_attribute: - return test.FAIL("Sender {} has an illegal constraint set without any parameter attribute".format(sender["id"])) + return test.FAIL( + "Sender {} has an illegal constraint set without any parameter attribute".format( + sender["id"])) if warn_label: - warning += "|" + "Sender {} constraint_sets should either 'urn:x-nmos:cap:meta:label' for all constraint sets or none".format(sender["id"]) + warning += ("|" + "Sender {} constraint_sets should either 'urn:x-nmos:cap:meta:label' " + "for all constraint sets or none".format(sender["id"])) if no_constraint_sets: return test.OPTIONAL("No BCP-004-02 'constraint_sets' were identified in Sender caps") @@ -277,9 +296,10 @@ def test_02(self, test): return test.WARNING(warning) else: return test.PASS() - + + def compare_min_larger_than_max(param_constraint): - + min_val = param_constraint["minimum"] max_val = param_constraint["maximum"] @@ -294,6 +314,6 @@ def compare_min_larger_than_max(param_constraint): max_num = max_val["numerator"] min_den = min_val.get("denominator", 1) max_den = max_val.get("denominator", 1) - return (min_num*max_den) > (max_num*min_den) - + return (min_num * max_den) > (max_num * min_den) + return False From 6a26f97850416bc07ffc14a5aa84efae72048050 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Wed, 14 May 2025 19:37:15 -0400 Subject: [PATCH 07/20] Update Config.py - fix lint --- nmostesting/Config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nmostesting/Config.py b/nmostesting/Config.py index 58c808d44..9cce66d6c 100644 --- a/nmostesting/Config.py +++ b/nmostesting/Config.py @@ -432,4 +432,3 @@ sys.exit(-1) except ImportError: pass - From f6171e441b19e1325a4bfa587e7e4395f7dec998 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Wed, 14 May 2025 19:38:55 -0400 Subject: [PATCH 08/20] Update NMOSTesting.py - fix merge --- nmostesting/NMOSTesting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nmostesting/NMOSTesting.py b/nmostesting/NMOSTesting.py index 794c4560f..2256b6dd3 100644 --- a/nmostesting/NMOSTesting.py +++ b/nmostesting/NMOSTesting.py @@ -432,7 +432,7 @@ }], "class": BCP0060102Test.BCP0060102Test }, - "BCP-004-02": { + "BCP-004-02": { "name": "Sender Capabilities", "specs": [{ "spec_key": "is-04", @@ -459,7 +459,6 @@ }], "class": BCP0040201Test.BCP0040201Test }, -} "BCP-006-04": { "name": "BCP-006-04 NMOS With MPEG TS", "specs": [{ From 4cead862a6b654936537122e154bb486ecf3d386 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Wed, 14 May 2025 23:46:52 +0000 Subject: [PATCH 09/20] fix lint --- nmostesting/NMOSTesting.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nmostesting/NMOSTesting.py b/nmostesting/NMOSTesting.py index 2256b6dd3..42d8adb00 100644 --- a/nmostesting/NMOSTesting.py +++ b/nmostesting/NMOSTesting.py @@ -437,7 +437,7 @@ "specs": [{ "spec_key": "is-04", "api_key": "node" - },{ + }, { "spec_key": "is-05", "api_key": "connection" }], @@ -759,20 +759,21 @@ def init_spec_cache(): continue if not os.path.exists(path): - if not "url" in repo_data or repo_data["url"] is None: + if "url" not in repo_data or repo_data["url"] is None: repo_url = 'https://github.com/AMWA-TV/' else: repo_url = repo_data["url"] - if not "branch" in repo_data or repo_data["branch"] is None: + if "branch" not in repo_data or repo_data["branch"] is None: repo_branch = None else: repo_branch = repo_data["branch"] - print(" * Initialising repository '{}' from branch '{}' at url '{}'".format(repo_data["repo"], repo_branch, repo_url)) + print(" * Initialising repository '{}' from branch '{}' at url '{}'".format( + repo_data["repo"], repo_branch, repo_url)) repo = git.Repo.clone_from(repo_url + repo_data["repo"] + '.git', path) - if not repo_branch is None: + if repo_branch is not None: repo.git.checkout(repo_branch) print(repo.git.status()) From 3a446c6e6ca81029c1758b22b8c8a8585e0d5605 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 30 May 2025 14:09:55 -0400 Subject: [PATCH 10/20] Update nmostesting/Config.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/Config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nmostesting/Config.py b/nmostesting/Config.py index 9cce66d6c..7300a0944 100644 --- a/nmostesting/Config.py +++ b/nmostesting/Config.py @@ -377,8 +377,6 @@ }, "bcp-004-02": { "repo": "bcp-004-02", - # "versions": ["v1.0-dev"], - # "default_version": "v1.0-dev", "branch": "v1.0-dev", "versions": ["v1.0-dev"], "default_version": "v1.0-dev", From 4272842f30d3f65ecf21b619e22208b5fc2884f4 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 30 May 2025 14:11:14 -0400 Subject: [PATCH 11/20] Update nmostesting/NMOSTesting.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/NMOSTesting.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/nmostesting/NMOSTesting.py b/nmostesting/NMOSTesting.py index 42d8adb00..4f6912469 100644 --- a/nmostesting/NMOSTesting.py +++ b/nmostesting/NMOSTesting.py @@ -758,25 +758,9 @@ def init_spec_cache(): if repo_data["repo"] is None: continue if not os.path.exists(path): - - if "url" not in repo_data or repo_data["url"] is None: - repo_url = 'https://github.com/AMWA-TV/' - else: - repo_url = repo_data["url"] - if "branch" not in repo_data or repo_data["branch"] is None: - repo_branch = None - else: - repo_branch = repo_data["branch"] - - print(" * Initialising repository '{}' from branch '{}' at url '{}'".format( - repo_data["repo"], repo_branch, repo_url)) - + repo_url = repo_data.get("url", "https://github.com/AMWA-TV/") + print(" * Initialising repository '{}' at url '{}'".format(repo_data["repo"], repo_url)) repo = git.Repo.clone_from(repo_url + repo_data["repo"] + '.git', path) - - if repo_branch is not None: - repo.git.checkout(repo_branch) - print(repo.git.status()) - update_last_pull = True else: repo = git.Repo(path) From 4c91d75b322594920d7740df6a9291518c56336f Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 30 May 2025 14:12:08 -0400 Subject: [PATCH 12/20] Update nmostesting/suites/BCP0040201Test.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/suites/BCP0040201Test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index b9b688118..f07d1552e 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -286,7 +286,7 @@ def test_02(self, test): sender["id"])) if warn_label: - warning += ("|" + "Sender {} constraint_sets should either 'urn:x-nmos:cap:meta:label' " + warning += ("|Sender {} constraint_sets should either 'urn:x-nmos:cap:meta:label' " "for all constraint sets or none".format(sender["id"])) if no_constraint_sets: From 301c1faf55c40710f3bac069adefb0db91866097 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 30 May 2025 14:12:19 -0400 Subject: [PATCH 13/20] Update nmostesting/suites/BCP0040201Test.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/suites/BCP0040201Test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index f07d1552e..307628446 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -272,7 +272,7 @@ def test_02(self, test): # Capabilities register if param_constraint.startswith( "urn:x-nmos:") and param_constraint not in reg_schema_obj["properties"]: - warning += ("|" + "Sender {} parameter constraint {}" + warning += ("|Sender {} parameter constraint {}" " is not registered ".format(sender["id"], param_constraint)) except BaseException: From 862f8439c8a518b2ebb4e9bdf255f1dde3f18dbb Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 30 May 2025 14:12:27 -0400 Subject: [PATCH 14/20] Update nmostesting/suites/BCP0040201Test.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/suites/BCP0040201Test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index 307628446..497f5f031 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -215,7 +215,7 @@ def test_02(self, test): # Make sure Senders do not use the Receiver's specific "event_types" attribute in their caps if "event_types" in sender["caps"]: - warning += ("|" + "Sender {} caps has an unnecessary 'event_types' attribute " + warning += ("|Sender {} caps has an unnecessary 'event_types' attribute " "that is not used with sender capabilities".format(sender["id"])) if "constraint_sets" in sender["caps"]: From 2f1c5c6f44f3d8e6a5e90167b5c951c72e86caf8 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 30 May 2025 14:12:35 -0400 Subject: [PATCH 15/20] Update nmostesting/suites/BCP0040201Test.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/suites/BCP0040201Test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index 497f5f031..2921f8816 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -210,7 +210,7 @@ def test_02(self, test): # Make sure Senders do not use the Receiver's specific "media_types" attribute in their caps if "media_types" in sender["caps"]: - warning += ("|" + "Sender {} caps has an unnecessary 'media_types' attribute " + warning += ("|Sender {} caps has an unnecessary 'media_types' attribute " "that is not used with sender capabilities".format(sender["id"])) # Make sure Senders do not use the Receiver's specific "event_types" attribute in their caps From 59887a779fda0ed1724772450835714bb736ceaf Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Fri, 30 May 2025 14:12:43 -0400 Subject: [PATCH 16/20] Update nmostesting/NMOSTesting.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/NMOSTesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmostesting/NMOSTesting.py b/nmostesting/NMOSTesting.py index 4f6912469..b09b616bf 100644 --- a/nmostesting/NMOSTesting.py +++ b/nmostesting/NMOSTesting.py @@ -433,7 +433,7 @@ "class": BCP0060102Test.BCP0060102Test }, "BCP-004-02": { - "name": "Sender Capabilities", + "name": "BCP-004-02 Sender Capabilities", "specs": [{ "spec_key": "is-04", "api_key": "node" From e1734949b2f374efac78e3fac4a6fc3d5b864315 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Tue, 3 Jun 2025 12:42:30 +0000 Subject: [PATCH 17/20] use a message from Test with NMOSTestException --- nmostesting/suites/BCP0040201Test.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index 2921f8816..847eb25e4 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -25,6 +25,7 @@ from ..IS05Utils import IS05Utils from ..TestHelper import load_resolved_schema from ..TestHelper import check_content_type +from ..TestResult import Test from pathlib import Path @@ -70,6 +71,7 @@ def __init__(self, apis, **kwargs): "transport_files": {}} self.is04_utils = IS04Utils(self.node_url) self.is05_utils = IS05Utils(self.connection_url) + self.test = Test("default") # Utility function from IS0502Test def get_is04_resources(self, resource_type): @@ -88,7 +90,7 @@ def get_is04_resources(self, resource_type): schema = self.get_schema(NODE_API_KEY, "GET", "/" + path_url, resources.status_code) valid, message = self.check_response(schema, "GET", resources) if not valid: - raise NMOSTestException(message) + raise NMOSTestException(self.test.FAIL(message)) try: for resource in resources.json(): @@ -116,7 +118,7 @@ def get_is05_partial_resources(self, resource_type): schema = self.get_schema(CONNECTION_API_KEY, "GET", "/" + path_url, resources.status_code) valid, message = self.check_response(schema, "GET", resources) if not valid: - raise NMOSTestException(message) + raise NMOSTestException(self.test.FAIL(message)) # The following call to is05_utils.get_transporttype does not validate against the IS-05 schemas, # which is good for allowing extended transport. The transporttype-response-schema.json schema is @@ -170,6 +172,8 @@ def check_response_without_transport_params(self, schema, method, response): def test_01(self, test): """Check that version 1.3 or greater of the Node API is available""" + self.test = test + api = self.apis[NODE_API_KEY] if self.is04_utils.compare_api_version(api["version"], "v1.3") >= 0: valid, result = self.do_request("GET", self.node_url) @@ -183,6 +187,8 @@ def test_01(self, test): def test_02(self, test): """Check Sender Capabilities""" + self.test = test + api = self.apis[SENDER_CAPS_KEY] reg_api = self.apis["caps-register"] From b5403a96bd5887f1ada4a25d4f361cd6510881d8 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Tue, 3 Jun 2025 08:58:00 -0400 Subject: [PATCH 18/20] Update nmostesting/suites/BCP0040201Test.py Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- nmostesting/suites/BCP0040201Test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index 847eb25e4..7f47c47d5 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# python3 nmos-test.py suite BCP-004-02 --host 127.0.0.1 127.0.0.1 --port 5058 5058 --version v1.3 v1.1 import json import re From 826e0a916c76ab10c751221cfdc63268d38ada21 Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Tue, 3 Jun 2025 12:59:26 +0000 Subject: [PATCH 19/20] add AMWA copyright --- nmostesting/suites/BCP0040201Test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nmostesting/suites/BCP0040201Test.py b/nmostesting/suites/BCP0040201Test.py index 7f47c47d5..e2bb62a30 100644 --- a/nmostesting/suites/BCP0040201Test.py +++ b/nmostesting/suites/BCP0040201Test.py @@ -1,4 +1,5 @@ # Copyright (C) 2025 Matrox Graphics Inc. +# Copyright (C) 2025 Advanced Media Workflow Association # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 5f828184999e6ed101009e299c46039f3508027b Mon Sep 17 00:00:00 2001 From: Alain Bouchard Date: Mon, 11 Aug 2025 11:23:52 -0400 Subject: [PATCH 20/20] Update NMOSTesting.py - fix test suite name to add -01 --- nmostesting/NMOSTesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nmostesting/NMOSTesting.py b/nmostesting/NMOSTesting.py index b09b616bf..b8ca5611d 100644 --- a/nmostesting/NMOSTesting.py +++ b/nmostesting/NMOSTesting.py @@ -432,7 +432,7 @@ }], "class": BCP0060102Test.BCP0060102Test }, - "BCP-004-02": { + "BCP-004-02-01": { "name": "BCP-004-02 Sender Capabilities", "specs": [{ "spec_key": "is-04", @@ -1301,3 +1301,4 @@ def main(args): # Exit the application with the desired code sys.exit(exit_code) +