|
| 1 | +# Copyright (C) 2022 Advanced Media Workflow Association |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import json |
| 16 | + |
| 17 | +from jsonschema import ValidationError |
| 18 | + |
| 19 | +from ..GenericTest import GenericTest |
| 20 | +from ..IS04Utils import IS04Utils |
| 21 | +from ..TestHelper import load_resolved_schema |
| 22 | + |
| 23 | +NODE_API_KEY = "node" |
| 24 | +FLOW_REGISTER_KEY = "flow-register" |
| 25 | +SENDER_REGISTER_KEY = "sender-register" |
| 26 | + |
| 27 | + |
| 28 | +class BCP00604Test(GenericTest): |
| 29 | + """ |
| 30 | + Runs Node Tests covering BCP-006-04 |
| 31 | + """ |
| 32 | + |
| 33 | + def __init__(self, apis, **kwargs): |
| 34 | + GenericTest.__init__(self, apis, **kwargs) |
| 35 | + self.node_url = self.apis[NODE_API_KEY]["url"] |
| 36 | + self.is04_resources = { |
| 37 | + "senders": [], |
| 38 | + "receivers": [], |
| 39 | + "_requested": [], |
| 40 | + "sources": [], |
| 41 | + "flows": [], |
| 42 | + } |
| 43 | + self.is04_utils = IS04Utils(self.node_url) |
| 44 | + |
| 45 | + # Utility function from IS0502Test |
| 46 | + def get_is04_resources(self, resource_type): |
| 47 | + """Retrieve all Senders or Receivers from a Node API, keeping hold of the returned objects""" |
| 48 | + assert resource_type in ["senders", "receivers", "sources", "flows"] |
| 49 | + |
| 50 | + # Prevent this being executed twice in one test run |
| 51 | + if resource_type in self.is04_resources["_requested"]: |
| 52 | + return True, "" |
| 53 | + |
| 54 | + valid, resources = self.do_request("GET", self.node_url + resource_type) |
| 55 | + if not valid: |
| 56 | + return False, "Node API did not respond as expected: {}".format(resources) |
| 57 | + |
| 58 | + try: |
| 59 | + for resource in resources.json(): |
| 60 | + self.is04_resources[resource_type].append(resource) |
| 61 | + self.is04_resources["_requested"].append(resource_type) |
| 62 | + except json.JSONDecodeError: |
| 63 | + return False, "Non-JSON response returned from Node API" |
| 64 | + |
| 65 | + return True, "" |
| 66 | + |
| 67 | + def has_required_flow_attr(self, flow): |
| 68 | + return flow.get("format") == "urn:x-nmos:format:mux" and flow.get("media_type") == "video/MP2T" |
| 69 | + |
| 70 | + def has_required_source_attr(self, source): |
| 71 | + return source.get("format") == "urn:x-nmos:format:mux" |
| 72 | + |
| 73 | + def has_required_receiver_attr(self, recv): |
| 74 | + return recv.get("format") == "urn:x-nmos:format:mux" and "video/MP2T" in recv.get("caps", {}).get("media_types") |
| 75 | + |
| 76 | + def test_01(self, test): |
| 77 | + """Check that version 1.3 or greater of the Node API is available""" |
| 78 | + |
| 79 | + api = self.apis[NODE_API_KEY] |
| 80 | + if self.is04_utils.compare_api_version(api["version"], "v1.3") >= 0: |
| 81 | + valid, result = self.do_request("GET", self.node_url) |
| 82 | + if valid: |
| 83 | + return test.PASS() |
| 84 | + else: |
| 85 | + return test.FAIL("Node API did not respond as expected: {}".format(result)) |
| 86 | + else: |
| 87 | + return test.FAIL("Node API must be running v1.3 or greater to fully implement BCP-006-04") |
| 88 | + |
| 89 | + def test_02(self, test): |
| 90 | + """The Source associated with a mux Flow MUST have its `format` attribute set to `urn:x-nmos:format:mux`""" |
| 91 | + |
| 92 | + valid, result = self.get_is04_resources("sources") |
| 93 | + if not valid: |
| 94 | + return test.FAIL(result) |
| 95 | + |
| 96 | + valid, result = self.get_is04_resources("flows") |
| 97 | + if not valid: |
| 98 | + return test.FAIL(result) |
| 99 | + |
| 100 | + mux_sources = [source for source in self.is04_resources.get("sources") if self.has_required_source_attr(source)] |
| 101 | + if len(mux_sources) == 0: |
| 102 | + return test.FAIL("No Sources with format=urn:x-nmos:format:mux were found on the Node") |
| 103 | + |
| 104 | + mux_flows = [flow for flow in self.is04_resources.get("flows") if self.has_required_flow_attr(flow)] |
| 105 | + if len(mux_flows) == 0: |
| 106 | + return test.FAILURE( |
| 107 | + "No Flows with format=urn:x-nmos:format:mux and media_type=video/MP2T were found on the Node" |
| 108 | + ) |
| 109 | + |
| 110 | + # check that all mux_sources are linked to a mux flow |
| 111 | + for source in mux_sources: |
| 112 | + if source.get("id") not in [flow.get("source_id") for flow in mux_flows]: |
| 113 | + return test.FAIL("Mux Source {} is not linked to a mux flow".format(source.get("id"))) |
| 114 | + |
| 115 | + return test.PASS() |
| 116 | + |
| 117 | + def test_03(self, test): |
| 118 | + """MPEG-TS Flows have the required attributes""" |
| 119 | + |
| 120 | + valid, result = self.get_is04_resources("flows") |
| 121 | + if not valid: |
| 122 | + return test.FAIL(result) |
| 123 | + |
| 124 | + mp2t_flows = [f for f in self.is04_resources.get("flows") if self.has_required_flow_attr(f)] |
| 125 | + if len(mp2t_flows) == 0: |
| 126 | + return test.FAIL( |
| 127 | + "No Flows with format=urn:x-nmos:format:mux and media_type=video/MP2T were found on the Node" |
| 128 | + ) |
| 129 | + |
| 130 | + reg_api = self.apis[FLOW_REGISTER_KEY] |
| 131 | + reg_path = reg_api["spec_path"] + "/flow-attributes" |
| 132 | + reg_schema = load_resolved_schema(reg_path, "flow_video_register.json", path_prefix=False) |
| 133 | + |
| 134 | + for flow in mp2t_flows: |
| 135 | + try: |
| 136 | + self.validate_schema(flow, reg_schema) |
| 137 | + except ValidationError as e: |
| 138 | + return test.FAIL( |
| 139 | + "Flow {} does not comply with the schema for Video Flow additional and " |
| 140 | + "extensible attributes defined in the NMOS Parameter Registers: " |
| 141 | + "{}".format(flow["id"], str(e)), |
| 142 | + "https://specs.amwa.tv/nmos-parameter-registers/branches/{}" |
| 143 | + "/flow-attributes/flow_video_register.html".format(reg_api["spec_branch"]), |
| 144 | + ) |
| 145 | + |
| 146 | + return test.PASS() |
| 147 | + |
| 148 | + def test_04(self, test): |
| 149 | + """MPEG-TS Senders have the required attributes and is assosicated with a mux flow""" |
| 150 | + |
| 151 | + valid, result = self.get_is04_resources("senders") |
| 152 | + if not valid: |
| 153 | + return test.FAIL(result) |
| 154 | + |
| 155 | + valid, result = self.get_is04_resources("flows") |
| 156 | + if not valid: |
| 157 | + return test.FAIL(result) |
| 158 | + |
| 159 | + # Currently the test does not cover other transports than RTP |
| 160 | + tested_transports = [ |
| 161 | + "urn:x-nmos:transport:rtp", |
| 162 | + "urn:x-nmos:transport:rtp.mcast", |
| 163 | + "urn:x-nmos:transport:rtp.ucast", |
| 164 | + ] |
| 165 | + |
| 166 | + reg_api = self.apis[SENDER_REGISTER_KEY] |
| 167 | + reg_path = reg_api["spec_path"] + "/sender-attributes" |
| 168 | + reg_schema = load_resolved_schema(reg_path, "sender_register.json", path_prefix=False) |
| 169 | + |
| 170 | + mp2t_flows = [f["id"] for f in self.is04_resources["flows"] if self.has_required_flow_attr(f)] |
| 171 | + if len(mp2t_flows) == 0: |
| 172 | + return test.FAIL( |
| 173 | + "No Flows with format=urn:x-nmos:format:mux and media_type=video/MP2T were found on the Node" |
| 174 | + ) |
| 175 | + |
| 176 | + mp2t_senders = [s for s in self.is04_resources.get("senders") if s.get("flow_id") in mp2t_flows] |
| 177 | + if len(mp2t_senders) == 0: |
| 178 | + return test.FAIL("No Senders associate with a mux flow found on the Node") |
| 179 | + |
| 180 | + mp2t_rtp_senders = [s for s in mp2t_senders if s.get("transport") in tested_transports] |
| 181 | + |
| 182 | + if len(mp2t_rtp_senders) == 0: |
| 183 | + return test.NA( |
| 184 | + "Could not test. No MP2T Sender with RTP transport found. " |
| 185 | + "This test suite currently only supports RTP." |
| 186 | + ) |
| 187 | + |
| 188 | + access_error = False |
| 189 | + for sender in mp2t_rtp_senders: |
| 190 | + |
| 191 | + try: |
| 192 | + self.validate_schema(sender, reg_schema) |
| 193 | + except ValidationError as e: |
| 194 | + return test.FAIL( |
| 195 | + "Sender {} does not comply with the schema for Sender additional and " |
| 196 | + "extensible attributes defined in the NMOS Parameter Registers: " |
| 197 | + "{}".format(sender["id"], str(e)), |
| 198 | + "https://specs.amwa.tv/nmos-parameter-registers/branches/{}" |
| 199 | + "/sender-attributes/sender_register.html".format(reg_api["spec_branch"]), |
| 200 | + ) |
| 201 | + |
| 202 | + if "transport" not in sender: |
| 203 | + return test.FAIL("Sender {} MUST indicate the 'transport' attribute.".format(sender["id"])) |
| 204 | + |
| 205 | + if "bit_rate" not in sender: |
| 206 | + return test.FAIL("Sender {} MUST indicate the 'bit_rate' attribute.".format(sender["id"])) |
| 207 | + |
| 208 | + if "manifest_href" not in sender: |
| 209 | + return test.FAIL("Sender {} MUST indicate the 'manifest_hrf' attribute.".format(sender["id"])) |
| 210 | + href = sender["manifest_href"] |
| 211 | + if not href: |
| 212 | + access_error = True |
| 213 | + continue |
| 214 | + |
| 215 | + manifest_href_valid, manifest_href_response = self.do_request("GET", href) |
| 216 | + if manifest_href_valid and manifest_href_response.status_code == 200: |
| 217 | + pass |
| 218 | + elif manifest_href_valid and manifest_href_response.status_code == 404: |
| 219 | + access_error = True |
| 220 | + continue |
| 221 | + else: |
| 222 | + return test.FAIL("Unexpected response from manifest_href '{}': {}".format(href, manifest_href_response)) |
| 223 | + |
| 224 | + sdp = manifest_href_response.text |
| 225 | + if not sdp: |
| 226 | + access_error = True |
| 227 | + continue |
| 228 | + |
| 229 | + if access_error: |
| 230 | + return test.UNCLEAR( |
| 231 | + "One or more of the tested Senders had null or empty 'manifest_href' or " |
| 232 | + "returned a 404 HTTP code. Please ensure all Senders are enabled and re-test." |
| 233 | + ) |
| 234 | + |
| 235 | + return test.PASS() |
| 236 | + |
| 237 | + def test_05(self, test): |
| 238 | + """MPEG-TS Receivers have the required attributes""" |
| 239 | + |
| 240 | + valid, result = self.get_is04_resources("receivers") |
| 241 | + if not valid: |
| 242 | + return test.FAIL(result) |
| 243 | + |
| 244 | + # Currently the test does not cover other transports than RTP |
| 245 | + tested_transports = [ |
| 246 | + "urn:x-nmos:transport:rtp", |
| 247 | + "urn:x-nmos:transport:rtp.mcast", |
| 248 | + "urn:x-nmos:transport:rtp.ucast", |
| 249 | + ] |
| 250 | + |
| 251 | + mp2t_receivers = [r for r in self.is04_resources.get("receivers") if self.has_required_receiver_attr(r)] |
| 252 | + if len(mp2t_receivers) == 0: |
| 253 | + return test.FAIL( |
| 254 | + "No Receivers with format=urn:x-nmos:format:mux " |
| 255 | + "and media_type=video/MP2T in caps were found on the Node" |
| 256 | + ) |
| 257 | + |
| 258 | + mp2t_rtp_receivers = [s for s in mp2t_receivers if s.get("transport") in tested_transports] |
| 259 | + |
| 260 | + if len(mp2t_rtp_receivers) == 0: |
| 261 | + return test.NA( |
| 262 | + "Could not test. No MP2T Receiver with RTP transport found. " |
| 263 | + "This test suite currently only supports RTP." |
| 264 | + ) |
| 265 | + |
| 266 | + media_type_constraint = "urn:x-nmos:cap:format:media_type" |
| 267 | + recommended_constraints = { |
| 268 | + "urn:x-nmos:cap:transport:bit_rate": "bit_rate", |
| 269 | + } |
| 270 | + |
| 271 | + warn_unrestricted = False |
| 272 | + warn_message = "" |
| 273 | + |
| 274 | + for receiver in mp2t_receivers: |
| 275 | + if "transport" not in receiver: |
| 276 | + return test.FAIL("Receiver {} MUST indicate the 'transport' attribute.".format(receiver["id"])) |
| 277 | + |
| 278 | + if "constraint_sets" not in receiver["caps"]: |
| 279 | + warn_unrestricted = True |
| 280 | + warn_message = "No Transport Bit Rate parameter constraint published by receiver {}".format( |
| 281 | + receiver["id"] |
| 282 | + ) |
| 283 | + continue |
| 284 | + |
| 285 | + mp2t_constraint_sets = [ |
| 286 | + constraint_set |
| 287 | + for constraint_set in receiver["caps"]["constraint_sets"] |
| 288 | + if media_type_constraint not in constraint_set |
| 289 | + or ( |
| 290 | + "enum" in constraint_set[media_type_constraint] |
| 291 | + and "video/MP2T" in constraint_set[media_type_constraint]["enum"] |
| 292 | + ) |
| 293 | + ] |
| 294 | + |
| 295 | + # check recommended attributes are present |
| 296 | + for constraint_set in mp2t_constraint_sets: |
| 297 | + for constraint, _target in recommended_constraints.items(): |
| 298 | + if constraint not in constraint_set: |
| 299 | + if not warn_unrestricted: |
| 300 | + warn_unrestricted = True |
| 301 | + warn_message = "No Transport Bit Rate parameter constraint published by receiver {}".format( |
| 302 | + receiver["id"] |
| 303 | + ) |
| 304 | + |
| 305 | + if warn_unrestricted: |
| 306 | + return test.WARNING(warn_message) |
| 307 | + |
| 308 | + return test.PASS() |
0 commit comments