Skip to content

Commit 41e4cbc

Browse files
jbrudeliJomar Brudelicristian-recoseanu
authored
BCP-006-04 Test Suite (#862)
* Add boilerplate for BCP-006-04 test suite * First version of IS-04 tests ready. Missing some check to be complete. * Fix some lint issues * Linting and refatoring. Adding checks fro manifest_ref and bit_rate on sender and validate RTP transport for senders and receivers * Refactor test after feedback from review, and add missing checks * Be less strict about constrain_sets in receiver capabilities * Improve warning message after feedback from code review * Change behavior of Sender and Receiver tests. Ignore tests with transport set to anything else than RTP. If no such resource is found the test returns with N/A Could not test. * Update nmostesting/suites/BCP00604Test.py Co-authored-by: cristian-recoseanu <cristian.recoseanu@hotmail.com> * Update nmostesting/suites/BCP00604Test.py Co-authored-by: cristian-recoseanu <cristian.recoseanu@hotmail.com> --------- Co-authored-by: Jomar Brudeli <jomar.brudeli@appear.net> Co-authored-by: cristian-recoseanu <cristian.recoseanu@hotmail.com>
1 parent a81619b commit 41e4cbc

File tree

3 files changed

+325
-0
lines changed

3 files changed

+325
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ The following test suites are currently supported.
4141
| - | BCP-004-01 Receiver Capabilities | X | | | Included in IS-04 Node API and IS-05 Interaction with IS-04 suites |
4242
| BCP-006-01-01 | BCP-006-01 NMOS With JPEG XS | X | | | |
4343
| BCP-006-01-02 | BCP-006-01 Controller | | | X | See [Testing Controllers](docs/2.8.%20Usage%20-%20Testing%20Controllers.md) |
44+
| BCP-006-04 | BCP-006-04 NMOS With MPEG-TS | X | | | |
4445

4546
When testing any of the above APIs it is important that they contain representative data. The test results will generate 'Could Not Test' results if no testable entities can be located. In addition, if devices support many modes of operation (including multiple video/audio formats) it is strongly recommended to re-test them in multiple modes.
4647

nmostesting/NMOSTesting.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
from .suites import BCP0050101Test
8989
from .suites import BCP0060101Test
9090
from .suites import BCP0060102Test
91+
from .suites import BCP00604Test
9192

9293

9394
FLASK_APPS = []
@@ -430,6 +431,21 @@
430431
}],
431432
"class": BCP0060102Test.BCP0060102Test
432433
},
434+
"BCP-006-04": {
435+
"name": "BCP-006-04 NMOS With MPEG TS",
436+
"specs": [{
437+
"spec_key": "is-04",
438+
"api_key": "node"
439+
}],
440+
"extra_specs": [{
441+
"spec_key": "nmos-parameter-registers",
442+
"api_key": "flow-register"
443+
}, {
444+
"spec_key": "nmos-parameter-registers",
445+
"api_key": "sender-register"
446+
}],
447+
"class": BCP00604Test.BCP00604Test
448+
},
433449
}
434450

435451

nmostesting/suites/BCP00604Test.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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

Comments
 (0)