tests: add enhanced packet counter verification 75/35375/4
authorKlement Sekera <klement@graphiant.com>
Fri, 18 Feb 2022 10:34:35 +0000 (10:34 +0000)
committerNeale Ranns <neale@graphiant.com>
Mon, 21 Feb 2022 08:29:00 +0000 (08:29 +0000)
Add support for inline packet counter verification to send_and_* functions.

Diff dictionary is a dictionary of dictionaries of interesting stats:

    diff_dictionary =
    {
        "err" : { '/error/counter1' : 4, },
        sw_if_index1 : { '/stat/segment/counter1' : 5,
                         '/stat/segment/counter2' : 6,
                       },
        sw_if_index2 : { '/stat/segment/counter1' : 7,
                       },
    }

It describes a per sw-if-index diffset, where each key is stat segment
path and value is the expected change for that counter for sw-if-index.
Special case string "err" is used for error counters.

This then allows more precise packet counter verification by first
defining a "zero" dictionary, e.g. for ED NAT:

        cls.no_diff = StatsDiff({
            pg.sw_if_index: {
                '/nat44-ed/in2out/fastpath/tcp': 0,
                '/nat44-ed/in2out/fastpath/udp': 0,
                '/nat44-ed/in2out/fastpath/icmp': 0,
                '/nat44-ed/in2out/fastpath/drops': 0,
                '/nat44-ed/in2out/slowpath/tcp': 0,
                '/nat44-ed/in2out/slowpath/udp': 0,
                '/nat44-ed/in2out/slowpath/icmp': 0,
                '/nat44-ed/in2out/slowpath/drops': 0,
                '/nat44-ed/in2out/fastpath/tcp': 0,
                '/nat44-ed/in2out/fastpath/udp': 0,
                '/nat44-ed/in2out/fastpath/icmp': 0,
                '/nat44-ed/in2out/fastpath/drops': 0,
                '/nat44-ed/in2out/slowpath/tcp': 0,
                '/nat44-ed/in2out/slowpath/udp': 0,
                '/nat44-ed/in2out/slowpath/icmp': 0,
                '/nat44-ed/in2out/slowpath/drops': 0,
            }
            for pg in cls.pg_interfaces
        })

and then to specify only changed counters directly when calling
one of send_and_* functions:

        self.send_and_assert_no_replies(
            self.pg0, pkts, msg="i2o pkts",
            stats_diff=self.no_diff | {
                "err": {
                    '/err/nat44-ed-in2out-slowpath/out of ports': len(pkts),
                },
                self.pg0.sw_if_index: {
                    '/nat44-ed/in2out/slowpath/drops': len(pkts),
                },
            }
        )

operator | is overloaded by StatsDiff class to perform a deep merge operation,
so in above case, dictionaries for "err" and self.pg0.sw_if_index do not
overwrite whole sub-dictionaries, rather the contents are merged,
assuring that all the remaining counters are verified to be zero.

Type: improvement
Signed-off-by: Klement Sekera <klement.sekera@gmail.com>
Change-Id: I2b87f7bd58a7d4b34ee72344e2f871b2f372e2d9

test/framework.py
test/test_nat44_ed.py
test/util.py

index f0d916f..1c81e8a 100644 (file)
@@ -1,7 +1,6 @@
 #!/usr/bin/env python3
 
 from __future__ import print_function
-import gc
 import logging
 import sys
 import os
@@ -1264,26 +1263,82 @@ class VppTestCase(CPUInterface, unittest.TestCase):
         self.pg_enable_capture(self.pg_interfaces)
         self.pg_start(trace=trace)
 
+    def snapshot_stats(self, stats_diff):
+        """Return snapshot of interesting stats based on diff dictionary."""
+        stats_snapshot = {}
+        for sw_if_index in stats_diff:
+            for counter in stats_diff[sw_if_index]:
+                stats_snapshot[counter] = self.statistics[counter]
+        self.logger.debug(f"Took statistics stats_snapshot: {stats_snapshot}")
+        return stats_snapshot
+
+    def compare_stats_with_snapshot(self, stats_diff, stats_snapshot):
+        """Assert appropriate difference between current stats and snapshot."""
+        for sw_if_index in stats_diff:
+            for cntr, diff in stats_diff[sw_if_index].items():
+                if sw_if_index == "err":
+                    self.assert_equal(
+                        self.statistics[cntr].sum(),
+                        stats_snapshot[cntr].sum() + diff,
+                        f"'{cntr}' counter value (previous value: "
+                        f"{stats_snapshot[cntr].sum()}, "
+                        f"expected diff: {diff})")
+                else:
+                    try:
+                        self.assert_equal(
+                            self.statistics[cntr][:, sw_if_index].sum(),
+                            stats_snapshot[cntr][:, sw_if_index].sum() + diff,
+                            f"'{cntr}' counter value (previous value: "
+                            f"{stats_snapshot[cntr][:, sw_if_index].sum()}, "
+                            f"expected diff: {diff})")
+                    except IndexError:
+                        # if diff is 0, then this most probably a case where
+                        # test declares multiple interfaces but traffic hasn't
+                        # passed through this one yet - which means the counter
+                        # value is 0 and can be ignored
+                        if 0 != diff:
+                            raise
+
     def send_and_assert_no_replies(self, intf, pkts, remark="", timeout=None,
-                                   trace=True):
+                                   stats_diff=None, trace=True, msg=None):
+        if stats_diff:
+            stats_snapshot = self.snapshot_stats(stats_diff)
+
         self.pg_send(intf, pkts)
-        if not timeout:
-            timeout = 1
-        for i in self.pg_interfaces:
-            i.get_capture(0, timeout=timeout)
-            i.assert_nothing_captured(remark=remark)
-            timeout = 0.1
-        if trace:
-            self.logger.debug(self.vapi.cli("show trace"))
+
+        try:
+            if not timeout:
+                timeout = 1
+            for i in self.pg_interfaces:
+                i.get_capture(0, timeout=timeout)
+                i.assert_nothing_captured(remark=remark)
+                timeout = 0.1
+        finally:
+            if trace:
+                if msg:
+                    self.logger.debug(f"send_and_assert_no_replies: {msg}")
+                self.logger.debug(self.vapi.cli("show trace"))
+
+        if stats_diff:
+            self.compare_stats_with_snapshot(stats_diff, stats_snapshot)
 
     def send_and_expect(self, intf, pkts, output, n_rx=None, worker=None,
-                        trace=True):
+                        trace=True, msg=None, stats_diff=None):
+        if stats_diff:
+            stats_snapshot = self.snapshot_stats(stats_diff)
+
         if not n_rx:
             n_rx = 1 if isinstance(pkts, Packet) else len(pkts)
         self.pg_send(intf, pkts, worker=worker, trace=trace)
         rx = output.get_capture(n_rx)
         if trace:
+            if msg:
+                self.logger.debug(f"send_and_expect: {msg}")
             self.logger.debug(self.vapi.cli("show trace"))
+
+        if stats_diff:
+            self.compare_stats_with_snapshot(stats_diff, stats_snapshot)
+
         return rx
 
     def send_and_expect_load_balancing(self, input, pkts, outputs,
@@ -1298,7 +1353,11 @@ class VppTestCase(CPUInterface, unittest.TestCase):
             self.logger.debug(self.vapi.cli("show trace"))
         return rxs
 
-    def send_and_expect_only(self, intf, pkts, output, timeout=None):
+    def send_and_expect_only(self, intf, pkts, output, timeout=None,
+                             stats_diff=None):
+        if stats_diff:
+            stats_snapshot = self.snapshot_stats(stats_diff)
+
         self.pg_send(intf, pkts)
         rx = output.get_capture(len(pkts))
         outputs = [output]
@@ -1310,6 +1369,9 @@ class VppTestCase(CPUInterface, unittest.TestCase):
                 i.assert_nothing_captured()
                 timeout = 0.1
 
+        if stats_diff:
+            self.compare_stats_with_snapshot(stats_diff, stats_snapshot)
+
         return rx
 
 
index 9bb803e..764693d 100644 (file)
@@ -2,7 +2,7 @@
 
 import unittest
 from io import BytesIO
-from random import randint, shuffle, choice
+from random import randint, choice
 
 import scapy.compat
 from framework import VppTestCase, VppTestRunner
@@ -17,6 +17,7 @@ from util import ppp, ip4_range
 from vpp_acl import AclRule, VppAcl, VppAclInterface
 from vpp_ip_route import VppIpRoute, VppRoutePath
 from vpp_papi import VppEnum
+from util import StatsDiff
 
 
 class TestNAT44ED(VppTestCase):
@@ -213,6 +214,28 @@ class TestNAT44ED(VppTestCase):
         for r in rl:
             r.add_vpp_config()
 
+        cls.no_diff = StatsDiff({
+            pg.sw_if_index: {
+                '/nat44-ed/in2out/fastpath/tcp': 0,
+                '/nat44-ed/in2out/fastpath/udp': 0,
+                '/nat44-ed/in2out/fastpath/icmp': 0,
+                '/nat44-ed/in2out/fastpath/drops': 0,
+                '/nat44-ed/in2out/slowpath/tcp': 0,
+                '/nat44-ed/in2out/slowpath/udp': 0,
+                '/nat44-ed/in2out/slowpath/icmp': 0,
+                '/nat44-ed/in2out/slowpath/drops': 0,
+                '/nat44-ed/in2out/fastpath/tcp': 0,
+                '/nat44-ed/in2out/fastpath/udp': 0,
+                '/nat44-ed/in2out/fastpath/icmp': 0,
+                '/nat44-ed/in2out/fastpath/drops': 0,
+                '/nat44-ed/in2out/slowpath/tcp': 0,
+                '/nat44-ed/in2out/slowpath/udp': 0,
+                '/nat44-ed/in2out/slowpath/icmp': 0,
+                '/nat44-ed/in2out/slowpath/drops': 0,
+            }
+            for pg in cls.pg_interfaces
+        })
+
     def get_err_counter(self, path):
         return self.statistics.get_err_counter(path)
 
@@ -2622,37 +2645,41 @@ class TestNAT44EDMW(TestNAT44ED):
         self.nat_add_outside_interface(self.pg1)
 
         # in2out and no NAT addresses added
-        err_old = self.statistics.get_err_counter(
-            '/err/nat44-ed-in2out-slowpath/out of ports')
-
         pkts = self.create_stream_in(self.pg0, self.pg1)
-        self.pg0.add_stream(pkts)
-        self.pg_enable_capture(self.pg_interfaces)
-        self.pg_start()
-        self.pg1.get_capture(0, timeout=1)
 
-        err_new = self.statistics.get_err_counter(
-            '/err/nat44-ed-in2out-slowpath/out of ports')
-
-        self.assertEqual(err_new - err_old, len(pkts))
+        self.send_and_assert_no_replies(
+            self.pg0, pkts, msg="i2o pkts",
+            stats_diff=self.no_diff | {
+                "err": {
+                    '/err/nat44-ed-in2out-slowpath/out of ports': len(pkts),
+                },
+                self.pg0.sw_if_index: {
+                    '/nat44-ed/in2out/slowpath/drops': len(pkts),
+                },
+            }
+        )
 
         # in2out after NAT addresses added
         self.nat_add_address(self.nat_addr)
 
-        err_old = self.statistics.get_err_counter(
-            '/err/nat44-ed-in2out-slowpath/out of ports')
-
-        pkts = self.create_stream_in(self.pg0, self.pg1)
-        self.pg0.add_stream(pkts)
-        self.pg_enable_capture(self.pg_interfaces)
-        self.pg_start()
-        capture = self.pg1.get_capture(len(pkts))
-        self.verify_capture_out(capture, ignore_port=True)
-
-        err_new = self.statistics.get_err_counter(
-            '/err/nat44-ed-in2out-slowpath/out of ports')
-
-        self.assertEqual(err_new, err_old)
+        tcpn, udpn, icmpn = (sum(x) for x in
+                             zip(*((TCP in p, UDP in p, ICMP in p)
+                                 for p in pkts)))
+
+        self.send_and_expect(
+            self.pg0, pkts, self.pg1, msg="i2o pkts",
+            stats_diff=self.no_diff | {
+                "err": {
+                    '/err/nat44-ed-in2out-slowpath/out of ports': 0,
+                },
+                self.pg0.sw_if_index: {
+                    '/nat44-ed/in2out/slowpath/drops': 0,
+                    '/nat44-ed/in2out/slowpath/tcp': tcpn,
+                    '/nat44-ed/in2out/slowpath/udp': udpn,
+                    '/nat44-ed/in2out/slowpath/icmp': icmpn,
+                },
+            }
+        )
 
     def test_unknown_proto(self):
         """ NAT44ED translate packet with unknown protocol """
index e21fdb8..653b667 100644 (file)
@@ -1,12 +1,11 @@
 """ test framework utilities """
 
-import abc
 import ipaddress
 import logging
 import socket
 from socket import AF_INET6
-import sys
 import os.path
+from copy import deepcopy
 
 import scapy.compat
 from scapy.layers.l2 import Ether
@@ -452,3 +451,50 @@ def reassemble4_ether(listoffragments):
 
 def reassemble4(listoffragments):
     return reassemble4_core(listoffragments, True)
+
+
+def recursive_dict_merge(dict_base, dict_update):
+    """Recursively merge base dict with update dict, return merged dict"""
+    for key in dict_update:
+        if key in dict_base:
+            if type(dict_update[key]) is dict:
+                dict_base[key] = recursive_dict_merge(dict_base[key],
+                                                      dict_update[key])
+            else:
+                dict_base[key] = dict_update[key]
+        else:
+            dict_base[key] = dict_update[key]
+    return dict_base
+
+
+class StatsDiff:
+    """
+    Diff dictionary is a dictionary of dictionaries of interesting stats:
+
+        diff_dictionary =
+        {
+            "err" : { '/error/counter1' : 4, },
+            sw_if_index1 : { '/stat/segment/counter1' : 5,
+                             '/stat/segment/counter2' : 6,
+                           },
+            sw_if_index2 : { '/stat/segment/counter1' : 7,
+                           },
+        }
+
+    It describes a per sw-if-index diffset, where each key is stat segment
+    path and value is the expected change for that counter for sw-if-index.
+    Special case string "err" is used for error counters, which are not per
+    sw-if-index.
+    """
+
+    def __init__(self, stats_diff={}):
+        self.stats_diff = stats_diff
+
+    def update(self, sw_if_index, key, value):
+        if sw_if_index in self.stats_diff:
+            self.stats_diff[sw_if_index][key] = value
+        else:
+            self.stats_diff[sw_if_index] = {key: value}
+
+    def __or__(self, other):
+        return recursive_dict_merge(deepcopy(self.stats_diff), other)