Skip to content

Commit 6550fd7

Browse files
author
Jose Ignacio Tamayo Segarra
authored
[oc_config_validate] Improve Subscription tests with multiple paths (#322)
* Improve Subscription tests with multiple paths. * Send a single SubscribeRequest with many paths is sent. * Do path inclusion comparison deal with sorted keys. * Compare Subscription Update paths with requested paths. * Check paths in Subscription tests. * Small format corrections
1 parent b3702cf commit 6550fd7

File tree

7 files changed

+97
-42
lines changed

7 files changed

+97
-42
lines changed

oc_config_validate/docs/testclasses.md

+2
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ By default, the tests do 5 retries, with 15 seconds delay, if the assertion fail
313313

314314
Uses gNMI Subscribe messages, of type ONCE.
315315

316+
This testcase supports sending multiple xpaths in the gNMI Subscribe request.
317+
316318
Optionally, every Update messages can have its timestamp value checked
317319
against the local time when the Subscription message was sent. The absolute
318320
time drift is compared against a max value in secs.

oc_config_validate/oc_config_validate/runner.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def setInitConfigs(ctx: context.TestContext,
228228
"applied at {init_config.xpath}")
229229
except (IOError, json.JSONDecodeError, target.BaseError) as err:
230230
msg = (f"Unable to set configuration '{init_config.filename}' "
231-
"at '{init_config.xpath}': {err}")
231+
f"at '{init_config.xpath}': {err}")
232232
if stop_on_error:
233233
raise InitConfigError(msg) from err
234234
logging.error(msg)

oc_config_validate/oc_config_validate/schema.py

+28-27
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import os
2020
import re
2121
from inspect import isclass
22-
from typing import Any, Iterator, List, Tuple, Union
22+
from typing import Any, List, Tuple, Union
2323

2424
from pyangbind.lib import yangtypes
2525
from pyangbind.lib.base import PybindBase
@@ -302,25 +302,30 @@ def isPathOK(xpath: str) -> bool:
302302
return True
303303

304304

305-
def isPathIn(xpath: str, xpaths: List[str]) -> bool:
306-
"""Returns True if xpath in included or equal to any path in xpaths.
305+
def isPathInRequestedPaths(xpath: str, xpaths: List[str]) -> bool:
306+
"""Returns True if xpath in included or equal to any request path.
307307
308-
xpaths can contain paths with wildcard '*'.
308+
The request paths can contain paths with wildcard '*'.
309309
310310
Args:
311311
xpath: Non-wildcard xpath to check.
312312
xpaths: List of xpaths (can have wildcards) to contain xpath.
313313
"""
314314
def _inOrEqual(x: str):
315+
# Ensure the path has keys sorted
316+
x = pathToString(parsePath(x))
315317
x_regex = re.escape(x).replace(r"\*", r"(.+)")
316318
return re.match(x_regex, xpath) is not None
317319

320+
xpath = pathToString(parsePath(xpath))
318321
return any(map(_inOrEqual, xpaths))
319322

320323

321324
def pathToString(path: gnmi_pb2.Path) -> str:
322325
"""Parse a gNMI Path to a URI-like string.
323326
327+
The keys are sorted alphabetically.
328+
324329
Args:
325330
A gNMI.Path oject.
326331
@@ -335,7 +340,7 @@ def pathToString(path: gnmi_pb2.Path) -> str:
335340
elem = e.name
336341
if hasattr(e, "key"):
337342
keys = [f"[{k}={v}]" for k, v in e.key.items()]
338-
elem += f"{''.join(keys)}"
343+
elem += f"{''.join(sorted(keys))}"
339344
path_elems.append(elem)
340345
return "/" + "/".join(path_elems)
341346

@@ -463,47 +468,43 @@ def intersectListCmp(want: list, got: list) -> Tuple[bool, str]:
463468
return True, ""
464469

465470

466-
def gNMISubscriptionOnceRequests(
471+
def gNMISubscriptionOnceRequest(
467472
xpaths: List[str],
468473
encoding: str = 'JSON_IETF'
469-
) -> Iterator[gnmi_pb2.SubscribeRequest]:
474+
) -> gnmi_pb2.SubscribeRequest:
470475
"""
471-
Returns an Iterator of gNMI Subscription ONCE requests for the xpaths.
476+
Returns a gNMI Subscription ONCE requests for the xpaths.
472477
473478
Args:
474479
xpaths: List of gNMI xpaths to subscribe to.
475480
encoding: Encoding requested for the Updates.
476481
"""
477482
paths = [parsePath(xpath) for xpath in xpaths]
478-
for r in [
479-
gnmi_pb2.SubscribeRequest(subscribe=gnmi_pb2.SubscriptionList(
480-
subscription=[gnmi_pb2.Subscription(path=path)
481-
for path in paths],
482-
mode=gnmi_pb2.SubscriptionList.ONCE,
483-
encoding=encoding))]:
484-
yield r
483+
return gnmi_pb2.SubscribeRequest(subscribe=gnmi_pb2.SubscriptionList(
484+
subscription=[gnmi_pb2.Subscription(path=path)
485+
for path in paths],
486+
mode=gnmi_pb2.SubscriptionList.ONCE,
487+
encoding=encoding))
485488

486489

487-
def gNMISubscriptionStreamSampleRequests(
490+
def gNMISubscriptionStreamSampleRequest(
488491
xpaths: List[str],
489492
sample_interval: int,
490493
encoding: str = 'JSON_IETF'
491-
) -> Iterator[gnmi_pb2.SubscribeRequest]:
494+
) -> gnmi_pb2.SubscribeRequest:
492495
"""
493-
Returns a gNMI Subscription STREAM SAMPLE request for the xpath.
496+
Returns a gNMI Subscription STREAM SAMPLE request for the xpaths.
494497
495498
Args:
496499
xpaths: gNMI xpaths to subscribe to.
497500
sample_interval: Nanoseconds interval between Updates.
498501
encoding: Encoding requested for the Updates.
499502
"""
500503
paths = [parsePath(xpath) for xpath in xpaths]
501-
for r in [
502-
gnmi_pb2.SubscribeRequest(subscribe=gnmi_pb2.SubscriptionList(
503-
subscription=[gnmi_pb2.Subscription(
504-
path=path,
505-
mode=gnmi_pb2.SubscriptionMode.SAMPLE,
506-
sample_interval=sample_interval) for path in paths],
507-
mode=gnmi_pb2.SubscriptionList.STREAM,
508-
encoding=encoding))]:
509-
yield r
504+
return gnmi_pb2.SubscribeRequest(subscribe=gnmi_pb2.SubscriptionList(
505+
subscription=[gnmi_pb2.Subscription(
506+
path=path,
507+
mode=gnmi_pb2.SubscriptionMode.SAMPLE,
508+
sample_interval=sample_interval) for path in paths],
509+
mode=gnmi_pb2.SubscriptionList.STREAM,
510+
encoding=encoding))

oc_config_validate/oc_config_validate/target.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import logging
2626
import ssl
2727
import time
28-
from typing import Any, Iterator, List, Optional, Tuple
28+
from typing import Any, List, Optional, Tuple
2929

3030
import grpc
3131
from grpc._channel import _InactiveRpcError, _MultiThreadedRendezvous
@@ -278,7 +278,7 @@ def gNMISetConfigFile(self, file_path: str, xpath: str):
278278
self.gNMISetUpdate(xpath, json_data)
279279

280280
def _gNMISubscribe(self,
281-
requests: Iterator[gnmi_pb2.SubscribeRequest],
281+
request: gnmi_pb2.SubscribeRequest,
282282
timeout: int = 30,
283283
check_sync_response: bool = False
284284
) -> List[gnmi_pb2.Notification]:
@@ -308,7 +308,7 @@ def _gNMISubscribe(self,
308308
got_sync_response = False
309309
try:
310310
for resp in self.stub.Subscribe(
311-
requests, timeout=timeout, metadata=auth):
311+
iter([request]), timeout=timeout, metadata=auth):
312312
if resp.sync_response:
313313
got_sync_response = True
314314
elif resp.update:
@@ -336,8 +336,8 @@ def gNMISubsOnce(self, xpaths: List[str]) -> List[gnmi_pb2.Notification]:
336336
Returns:
337337
A list of gnmi_pb2.Notification objects received.
338338
"""
339-
requests = schema.gNMISubscriptionOnceRequests(xpaths)
340-
return self._gNMISubscribe(requests)
339+
request = schema.gNMISubscriptionOnceRequest(xpaths)
340+
return self._gNMISubscribe(request)
341341

342342
def gNMISubsStreamSample(self, xpath: str, sample_interval: int,
343343
timeout: int) -> List[gnmi_pb2.Notification]:
@@ -353,9 +353,9 @@ def gNMISubsStreamSample(self, xpath: str, sample_interval: int,
353353
A list of gnmi_pb2.Notification objects received.
354354
355355
"""
356-
requests = schema.gNMISubscriptionStreamSampleRequests(
356+
request = schema.gNMISubscriptionStreamSampleRequest(
357357
[xpath], sample_interval)
358-
return self._gNMISubscribe(requests, timeout=timeout)
358+
return self._gNMISubscribe(request, timeout=timeout)
359359

360360
def validate(self):
361361
"""Ensures the Target is defined appropriately.

oc_config_validate/oc_config_validate/testcases/telemetry_once.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ def test100(self):
6666
for n in self.responses:
6767
updates += len(n.update)
6868
for u in n.update:
69+
got_path = schema.pathToString(u.path)
70+
self.assertTrue(
71+
schema.isPathInRequestedPaths(got_path, self.xpaths),
72+
f"Unexpected update path {got_path} for subscription")
6973
self.assertTrue(
7074
u.val.HasField(self.values_type),
7175
f"Value of Update {schema.pathToString(u.path)} "
@@ -115,9 +119,8 @@ def test100(self):
115119
got_path = schema.pathToString(u.path)
116120
got_paths.append(got_path)
117121
self.assertTrue(
118-
schema.isPathIn(got_path, want_paths),
119-
f"Unexpected update path {got_path} for OC model {self.model},"
120-
f"expected {want_paths}")
122+
schema.isPathInRequestedPaths(got_path, want_paths),
123+
f"Unexpected update path {got_path} for subscription")
121124

122125
if self.check_missing_model_paths:
123126
for want_path in want_paths:

oc_config_validate/oc_config_validate/testcases/telemetry_sample.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def testSubscribeSample(self):
9494
got_paths, self.update_paths_count,
9595
f"Expected {self.update_paths_count} Update paths, "
9696
f"got: {got_paths}")
97+
<<<<<<< HEAD
9798

9899

99100
class CheckLeafs(SubsSampleTestCase):
@@ -121,9 +122,8 @@ def testSubscribeSample(self):
121122
got_paths = len(self.responses)
122123
self.assertGreater(got_paths, 0,
123124
"There are no Update replies to the Subscription")
124-
if self.responses:
125+
if self.responses:
125126
for path, _ in self.responses.items():
126127
self.assertTrue(
127-
schema.isPathIn(path, want_paths),
128-
f"Unexpected update path {path} for subscription to"
129-
f" {self.xpath} with model {self.model}")
128+
schema.isPathInRequestedPaths(path, [self.xpath]),
129+
f"Unexpected update path {path} for subscription")

oc_config_validate/py_tests/test_schema.py

+49
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,54 @@ def test_pathStrings(self, name, path_str, is_ok):
138138
self.assertEqual(path_str, schema.pathToString(got_obj), name)
139139

140140

141+
class TestisPathInRequestedPathsRequestedPaths(unittest.TestCase):
142+
"""Test for isPathInRequestedPaths."""
143+
144+
@parameterized.expand([
145+
("_in", "/interfaces/interface[name=ethernet1/2]/state", True),
146+
("_deep_in",
147+
"/interfaces/interface[name=ethernet1/2]/state/counters/out-errors",
148+
True),
149+
("_not_int",
150+
"/interfaces/interface[name=ethernet1/2]/config/name", False),
151+
])
152+
def test_isPathInRequestedPathsRequestedPaths_singleReq(self, name, xpath,
153+
want):
154+
request_xpaths = ["/interfaces/interface[name=ethernet1/2]/state"]
155+
self.assertEqual(schema.isPathInRequestedPaths(
156+
xpath, request_xpaths), want)
157+
158+
@parameterized.expand([
159+
("_in", "/interfaces/interface[name=ethernet1/0]/state", True),
160+
("_deep_in",
161+
"/interfaces/interface[name=ethernet1/1]/state/counters/out-errors",
162+
True),
163+
("_not_int",
164+
"/interfaces/interface[name=ethernet1/2]/config/name", False),
165+
])
166+
def test_isPathInRequestedPathsRequestedPaths_singleWcardReq(self, name,
167+
xpath, want):
168+
request_xpaths = ["/interfaces/interface[name=*]/state"]
169+
self.assertEqual(schema.isPathInRequestedPaths(
170+
xpath, request_xpaths), want)
171+
172+
@parameterized.expand([
173+
("_in", "/interfaces/interface[name=ethernet1/0][id=0]/state", True),
174+
("_deep_in",
175+
"/interfaces/interface[id=1][name=1/1]/state/counters/out-errors",
176+
True),
177+
("_not_int",
178+
"/interfaces/interface[name=ethernet1/2]/config/state", False),
179+
])
180+
def test_isPathInRequestedPathsRequestedPaths_manyKeysReq(self, name,
181+
xpath, want):
182+
request_xpaths = ["/interfaces/interface[name=*][id=*]/state"]
183+
self.assertEqual(schema.isPathInRequestedPaths(
184+
xpath, request_xpaths), want)
185+
request_xpaths = ["/interfaces/interface[id=*][name=*]/state"]
186+
self.assertEqual(schema.isPathInRequestedPaths(
187+
xpath, request_xpaths), want)
188+
189+
141190
if __name__ == '__main__':
142191
unittest.main()

0 commit comments

Comments
 (0)