diff --git a/oc_config_validate/docs/testclasses.md b/oc_config_validate/docs/testclasses.md index 8aa894e9..b78d4aba 100644 --- a/oc_config_validate/docs/testclasses.md +++ b/oc_config_validate/docs/testclasses.md @@ -394,7 +394,7 @@ Update paths received in the Subscription reply are checked against the Leafs of the Model (Update paths must match an OC Model Leaf). Optionally, use `check_missing_model_paths` to assert that all OC model paths -are present in the Updates. Usually, the Subscription replies might not +are present in the Updates. The Subscription replies might not have all Leaf paths that the OC mode has. > This check does NOT check the type of the values returned. @@ -406,6 +406,23 @@ Args: * *check_missing_model_paths*: If True, it asserts that all OC Model Leaf paths are in the received Updates. Defaults to False. +#### `telemetry_once.CheckLeafsFromList` + +In addition to the default checks of the module, this test checks that the +subscription responses have all expected update paths. + +The Update paths received in the Subscription reply are checked against the +provided list of paths in the test. The Subscription reply must have all paths +in the list. + +> This check does NOT check the type of the values returned. + +> Update paths not in the list of expected paths are ignored. + +Args: + * **xpaths**: List of gNMI paths to subscribe to. + * **update_paths**: List of gNMI paths that the Subscription response must have. + ### Module telemetry_sample Uses gNMI Subscribe messages, of type STREAM, mode SAMPLE. @@ -497,12 +514,44 @@ Update path | Time > This check does NOT check the type of the values returned. Args: - * **xpath**: gNMI path to subscribe to. - Can contain wildcard '*' only in keys. + * **xpath**: gNMI path to subscribe to. Can contain wildcard '*' only in keys. * **model**: Python binding class to check the replies against. * *check_missing_model_paths*: If True, it asserts that all OC Model Leaf paths are in the received Updates. Defaults to False. +#### `telemetry_sample.CheckLeafsFromList` + +In addition to the default checks of the module, this test checks that the +subscription updates have all have the expected paths in the responses. + +E.g: +Suppose the test is subscribing to an xpath `//`, with +15 secs interval for 65 secs. After collecting subscription responses for 65 +secs, the test checks that there are updates for all paths listed in the test. + +Assuming the test expects a Updates for 2 paths `[///, ///]`, the following Subscription Updates will FAIL this test: + +Update path | Time + ----------- | ---- +`///` | Update[t0] | Update[t15] | Update[t30] | +`///` | Update[t0] | Update[t15] | Update[t30] | + +In the same condition, the following Subscription Updates will PASS this test: + +Update path | Time + ----------- | ---- +`///` | Update[t0] | Update[t15] | Update[t30] | +`///` | Update[t0] | Update[t15] | Update[t30] | +`///` | Update[t0] | Update[t15] | Update[t30] | + +> This check does NOT check the type of the values returned. + +> Update paths not in the list of expected paths are ignored. + +Args: + * **xpath**: gNMI paths to subscribe to. Can contain wildcard '*' only in keys. + * **update_paths**: List of gNMI paths that the Subscription response must have. + ### Module telemetry_onchange Uses gNMI Subscribe messages, of type STREAM, mode ON_CHANGE. diff --git a/oc_config_validate/oc_config_validate/testcases/telemetry_once.py b/oc_config_validate/oc_config_validate/testcases/telemetry_once.py index 463bf75c..5036f537 100644 --- a/oc_config_validate/oc_config_validate/testcases/telemetry_once.py +++ b/oc_config_validate/oc_config_validate/testcases/telemetry_once.py @@ -155,3 +155,35 @@ def testSubscribeOnce(self): self.assertIn( want_path, list(got_paths), f"Missing update path for OC model {self.model}") + + +class CheckLeafsFromList(SubsOnceTestCase): + """Subscribes ONCE and checks the updates againts a list of expected paths. + + All arguments are read from the Test YAML description. + + Args: + xpaths: List of gNMI paths to subscribe to. + update_paths: List of gNMI paths that the Subscription response must have. + """ + update_paths = None + + def testSubscribeOnce(self): + """""" + self.assertArgs(["xpaths", "update_paths"]) + + self.subscribeOnce() + + got_updates = [] + for n in self.responses: + got_updates.extend(n.update) + + got_paths = set() + for u in got_updates: + got_path = schema.pathToString(u.path) + got_paths.add(got_path) + + for want_path in self.update_paths: + self.assertIn( + want_path, list(got_paths), + "Missing update path") diff --git a/oc_config_validate/oc_config_validate/testcases/telemetry_sample.py b/oc_config_validate/oc_config_validate/testcases/telemetry_sample.py index c453edce..4f02d455 100644 --- a/oc_config_validate/oc_config_validate/testcases/telemetry_sample.py +++ b/oc_config_validate/oc_config_validate/testcases/telemetry_sample.py @@ -93,7 +93,7 @@ class CountUpdatePaths(SubsSampleTestCase): This tests that a Subscription consistenly reports all Update paths. Args: - xpath: gNMI paths to subscribe to. Can contain wildcards. + xpath: gNMI path to subscribe to. Can contain wildcards. update_paths_count: Number of expected disctinct Update paths. """ update_paths_count = None @@ -116,7 +116,7 @@ class CheckLeafsFromModel(SubsSampleTestCase): that the paths corresponds to Leafs in the OC model. Args: - xpath: gNMI paths to subscribe to. Can contain wildcards only on the + xpath: gNMI path to subscribe to. Can contain wildcards only on the keys. model: Python binding class to check the replies against. check_missing_model_paths: If True, missing OC Model leaf paths in the @@ -147,3 +147,29 @@ def testSubscribeSample(self): self.assertIn( want_path, got_paths, f"Missing update path for OC model {self.model}") + + +class CheckLeafsFromList(SubsSampleTestCase): + """Subscribes SAMPLE and checks the updates againts a list of expeted paths. + + Tests that a Sample Subscription consistenly reports all Update paths, and + that the all paths listed as expected are there. + + Args: + xpath: gNMI paths to subscribe to. Can contain wildcard '*' only in keys. + update_paths: List of gNMI paths that the Subscription responses must have. + """ + update_paths = None + + def testSubscribeSample(self): + """""" + self.assertArgs(["update_paths", "xpath"]) + + self.subscribeSample() + + got_paths = list(self.responses.keys()) + + for want_path in self.update_paths: + self.assertIn( + want_path, got_paths, + "Missing update path") diff --git a/oc_config_validate/py_tests/testcases/test_telemetry_once.py b/oc_config_validate/py_tests/testcases/test_telemetry_once.py index 9c105425..f2d142c6 100644 --- a/oc_config_validate/py_tests/testcases/test_telemetry_once.py +++ b/oc_config_validate/py_tests/testcases/test_telemetry_once.py @@ -418,6 +418,29 @@ def check_ok(self, mock_gNMISubsOnce): mock_gNMISubsOnce.return_value = [response_interface_status] self.testSubscribeOnce() + def check_path_not_in_model(self, mock_gNMISubsOnce): + """Test that CheckLeafsFromModel fails as expected.""" + self.xpaths = ['/interfaces/interface[name=eth0]/state'] + self.model = "interfaces.openconfig_interfaces" + mock_gNMISubsOnce.return_value = [gnmi_pb2.Notification( + timestamp=(int(time.time())) * 1000000000, + update=[ + gnmi_pb2.Update( + path=gnmi_pb2.Path(elem=[ + gnmi_pb2.PathElem(name='interfaces'), + gnmi_pb2.PathElem( + name='interface', key={'name': 'eth0'}), + gnmi_pb2.PathElem(name='state'), + gnmi_pb2.PathElem(name='foo') + ]), + val=gnmi_pb2.TypedValue(string_val='bar') + )])] + with self.assertRaisesRegex( + AssertionError, + "Update path /interfaces/interface\\[name=eth0\\]/state/foo " + "NOT in OC Model interfaces.openconfig_interfaces"): + self.testSubscribeOnce() + def check_missing_paths(self, mock_gNMISubsOnce): """Test that CheckLeafsFromModel works as expected with missing paths from model.""" self.check_missing_model_paths = True @@ -452,5 +475,66 @@ def check_bad_model(self, mock_gNMISubsOnce): self.testSubscribeOnce() +@mock.patch('oc_config_validate.testbase.TestCase.gNMISubsOnce') +class TestCheckLeafsFromList(telemetry_once.CheckLeafsFromList): + """Test for CheckLeafsFromList class.""" + + def check_bad_args(self, mock_gNMISubsOnce): + """Test that CheckLeafsFromList raises an error for missing argument.""" + with self.assertRaises(AssertionError): + self.testSubscribeOnce() + + self.xpaths = ['/valid/path'] + with self.assertRaises(AssertionError): + self.testSubscribeOnce() + + self.update_paths = ['/valid/update/path'] + self.xpaths = None + with self.assertRaises(AssertionError): + self.testSubscribeOnce() + + mock_gNMISubsOnce.assert_not_called() + + def check_ok(self, mock_gNMISubsOnce): + """Test that CheckLeafsFromList works as expected.""" + self.xpaths = ['/interfaces/interface[name=eth0]/state'] + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth0]/state/enabled'] + mock_gNMISubsOnce.return_value = [response_interface_eth0_state] + self.testSubscribeOnce() + + self.xpaths = ['/interfaces/interface[name=*]/state'] + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth1]/state/oper-status'] + mock_gNMISubsOnce.return_value = [response_interface_status] + self.testSubscribeOnce() + + self.xpaths = ['/interfaces/interface[name=eth0]/state'] + self.update_paths = [] + mock_gNMISubsOnce.return_value = [response_interface_eth0_state] + self.testSubscribeOnce() + + def check_missing_paths(self, mock_gNMISubsOnce): + """Test that CheckLeafsFromList works as expected with missing paths from list.""" + + self.xpaths = ['/interfaces/interface[name=eth0]/state'] + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth0]/state/admin-status'] + mock_gNMISubsOnce.return_value = [response_interface_eth0_state] + with self.assertRaisesRegex( + AssertionError, + "Missing update path"): + self.testSubscribeOnce() + + self.xpaths = ['/interfaces/interface[name=*]/state/oper-status'] + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth1]/state/oper-status', + '/interfaces/interface[name=eth2]/state/oper-status'] + mock_gNMISubsOnce.return_value = [response_interface_status] + with self.assertRaises( + AssertionError): + self.testSubscribeOnce() + + if __name__ == '__main__': unittest.main() diff --git a/oc_config_validate/py_tests/testcases/test_telemetry_sample.py b/oc_config_validate/py_tests/testcases/test_telemetry_sample.py index 2c44ef8d..ea0c4da9 100644 --- a/oc_config_validate/py_tests/testcases/test_telemetry_sample.py +++ b/oc_config_validate/py_tests/testcases/test_telemetry_sample.py @@ -497,5 +497,136 @@ def check_bad_model(self, mock_gNMISubsStreamSample): self.testSubscribeSample() +@mock.patch('oc_config_validate.testbase.TestCase.gNMISubsStreamSample') +class TestCheckLeafsFromList(telemetry_sample.CheckLeafsFromList): + """Test for CheckLeafsFromList class.""" + + def setUp(self): + self.sample_interval = 10 + self.sample_timeout = 30 + self.xpath = '/interfaces/interface[name=*]/state' + + def check_bad_args(self, mock_gNMISubsStreamSample): + """Test that CheckLeafsFromList raises an error for missing argument.""" + self.xpath = None + with self.assertRaises(AssertionError): + self.testSubscribeSample() + + self.xpath = '/valid/path' + with self.assertRaises(AssertionError): + self.testSubscribeSample() + + self.update_paths = ['/valid/update/path'] + self.xpath = None + with self.assertRaises(AssertionError): + self.testSubscribeSample() + + mock_gNMISubsStreamSample.assert_not_called() + + def check_ok(self, mock_gNMISubsStreamSample): + """Test that CheckLeafsFromList works as expected.""" + now = int(time.time()) + + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth1]/state/oper-status'] + + mock_gNMISubsStreamSample.return_value = [ + gnmi_pb2.Notification( + timestamp=now * 1000000000, + update=updates_interface_status + ), + gnmi_pb2.Notification( + timestamp=(now + 11) * 1000000000, + update=updates_interface_status + ), + gnmi_pb2.Notification( + timestamp=(now + 20) * 1000000000, + update=updates_interface_status + ), + gnmi_pb2.Notification( + timestamp=(now + 30) * 1000000000, + update=updates_interface_status + ) + ] + self.testSubscribeSample() + + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth0]/state/enabled'] + mock_gNMISubsStreamSample.return_value = [ + gnmi_pb2.Notification( + timestamp=now * 1000000000, + update=updates_interface_eth0_state + ), + gnmi_pb2.Notification( + timestamp=(now + 11) * 1000000000, + update=updates_interface_eth0_state + ), + gnmi_pb2.Notification( + timestamp=(now + 20) * 1000000000, + update=updates_interface_eth0_state + ), + gnmi_pb2.Notification( + timestamp=(now + 30) * 1000000000, + update=updates_interface_eth0_state + ) + ] + self.testSubscribeSample() + + def check_missing_paths(self, mock_gNMISubsStreamSample): + """Test that CheckLeafsFromList works as expected with missing paths from list.""" + + now = int(time.time()) + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth1]/state/oper-status', + '/interfaces/interface[name=eth2]/state/oper-status'] + mock_gNMISubsStreamSample.return_value = [ + gnmi_pb2.Notification( + timestamp=now * 1000000000, + update=updates_interface_status + ), + gnmi_pb2.Notification( + timestamp=(now + 11) * 1000000000, + update=updates_interface_status + ), + gnmi_pb2.Notification( + timestamp=(now + 20) * 1000000000, + update=updates_interface_status + ), + gnmi_pb2.Notification( + timestamp=(now + 30) * 1000000000, + update=updates_interface_status + ) + ] + with self.assertRaisesRegex( + AssertionError, + "Missing update path"): + self.testSubscribeSample() + + self.update_paths = ['/interfaces/interface[name=eth0]/state/oper-status', + '/interfaces/interface[name=eth0]/state/admin-status'] + mock_gNMISubsStreamSample.return_value = [ + gnmi_pb2.Notification( + timestamp=now * 1000000000, + update=updates_interface_eth0_state + ), + gnmi_pb2.Notification( + timestamp=(now + 11) * 1000000000, + update=updates_interface_eth0_state + ), + gnmi_pb2.Notification( + timestamp=(now + 20) * 1000000000, + update=updates_interface_eth0_state + ), + gnmi_pb2.Notification( + timestamp=(now + 30) * 1000000000, + update=updates_interface_eth0_state + ) + ] + with self.assertRaisesRegex( + AssertionError, + "Missing update path"): + self.testSubscribeSample() + + if __name__ == '__main__': unittest.main()