Report: Add mrr stdev to comp tables
[csit.git] / resources / tools / presentation / input_data_parser.py
index af8a854..987b996 100644 (file)
@@ -25,11 +25,15 @@ import resource
 import logging
 
 from collections import OrderedDict
 import logging
 
 from collections import OrderedDict
-from os import remove
+from os import remove, walk, listdir
+from os.path import isfile, isdir, join
 from datetime import datetime as dt
 from datetime import timedelta
 from json import loads
 from datetime import datetime as dt
 from datetime import timedelta
 from json import loads
+from json.decoder import JSONDecodeError
 
 
+import hdrh.histogram
+import hdrh.codec
 import prettytable
 import pandas as pd
 
 import prettytable
 import pandas as pd
 
@@ -38,6 +42,7 @@ from robot import errors
 
 from resources.libraries.python import jumpavg
 from input_data_files import download_and_unzip_data_file
 
 from resources.libraries.python import jumpavg
 from input_data_files import download_and_unzip_data_file
+from pal_errors import PresentationError
 
 
 # Separator used in file names
 
 
 # Separator used in file names
@@ -201,36 +206,74 @@ class ExecutionChecker(ResultVisitor):
     .. note:: ID is the lowercase full path to the test.
     """
 
     .. note:: ID is the lowercase full path to the test.
     """
 
-    REGEX_PLR_RATE = re.compile(r'PLRsearch lower bound::?\s(\d+.\d+).*\n'
-                                r'PLRsearch upper bound::?\s(\d+.\d+)')
-
-    REGEX_NDRPDR_RATE = re.compile(r'NDR_LOWER:\s(\d+.\d+).*\n.*\n'
-                                   r'NDR_UPPER:\s(\d+.\d+).*\n'
-                                   r'PDR_LOWER:\s(\d+.\d+).*\n.*\n'
-                                   r'PDR_UPPER:\s(\d+.\d+)')
-
-    REGEX_NDRPDR_LAT = re.compile(r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
-                                  r'LATENCY.*\[\'(.*)\', \'(.*)\'\]')
-
-    REGEX_TOLERANCE = re.compile(r'^[\D\d]*LOSS_ACCEPTANCE:\s(\d*\.\d*)\s'
-                                 r'[\D\d]*')
-
-    REGEX_VERSION_VPP = re.compile(r"(return STDOUT Version:\s*|"
-                                   r"VPP Version:\s*|VPP version:\s*)(.*)")
-
-    REGEX_VERSION_DPDK = re.compile(r"(DPDK version:\s*|DPDK Version:\s*)(.*)")
-
-    REGEX_TCP = re.compile(r'Total\s(rps|cps|throughput):\s(\d*).*$')
-
-    REGEX_MRR = re.compile(r'MaxReceivedRate_Results\s\[pkts/(\d*)sec\]:\s'
-                           r'tx\s(\d*),\srx\s(\d*)')
-
-    REGEX_BMRR = re.compile(r'Maximum Receive Rate trial results'
-                            r' in packets per second: \[(.*)\]')
-
-    REGEX_RECONF_LOSS = re.compile(r'Packets lost due to reconfig: (\d*)')
-    REGEX_RECONF_TIME = re.compile(r'Implied time lost: (\d*.[\de-]*)')
-
+    REGEX_PLR_RATE = re.compile(
+        r'PLRsearch lower bound::?\s(\d+.\d+).*\n'
+        r'PLRsearch upper bound::?\s(\d+.\d+)'
+    )
+    REGEX_NDRPDR_RATE = re.compile(
+        r'NDR_LOWER:\s(\d+.\d+).*\n.*\n'
+        r'NDR_UPPER:\s(\d+.\d+).*\n'
+        r'PDR_LOWER:\s(\d+.\d+).*\n.*\n'
+        r'PDR_UPPER:\s(\d+.\d+)'
+    )
+    REGEX_PERF_MSG_INFO = re.compile(
+        r'NDR_LOWER:\s(\d+.\d+)\s.*\s(\d+.\d+)\s.*\n.*\n.*\n'
+        r'PDR_LOWER:\s(\d+.\d+)\s.*\s(\d+.\d+)\s.*\n.*\n.*\n'
+        r'Latency at 90% PDR:.*\[\'(.*)\', \'(.*)\'\].*\n'
+        r'Latency at 50% PDR:.*\[\'(.*)\', \'(.*)\'\].*\n'
+        r'Latency at 10% PDR:.*\[\'(.*)\', \'(.*)\'\].*\n'
+    )
+    REGEX_MRR_MSG_INFO = re.compile(r'.*\[(.*)\]')
+
+    # TODO: Remove when not needed
+    REGEX_NDRPDR_LAT_BASE = re.compile(
+        r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
+        r'LATENCY.*\[\'(.*)\', \'(.*)\'\]'
+    )
+    REGEX_NDRPDR_LAT = re.compile(
+        r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
+        r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]'
+    )
+    # TODO: Remove when not needed
+    REGEX_NDRPDR_LAT_LONG = re.compile(
+        r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n.*\n'
+        r'LATENCY.*\[\'(.*)\', \'(.*)\'\]\s\n.*\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]\s\n'
+        r'Latency.*\[\'(.*)\', \'(.*)\'\]'
+    )
+    REGEX_VERSION_VPP = re.compile(
+        r"(return STDOUT Version:\s*|"
+        r"VPP Version:\s*|VPP version:\s*)(.*)"
+    )
+    REGEX_VERSION_DPDK = re.compile(
+        r"(DPDK version:\s*|DPDK Version:\s*)(.*)"
+    )
+    REGEX_TCP = re.compile(
+        r'Total\s(rps|cps|throughput):\s(\d*).*$'
+    )
+    REGEX_MRR = re.compile(
+        r'MaxReceivedRate_Results\s\[pkts/(\d*)sec\]:\s'
+        r'tx\s(\d*),\srx\s(\d*)'
+    )
+    REGEX_BMRR = re.compile(
+        r'Maximum Receive Rate trial results'
+        r' in packets per second: \[(.*)\]'
+    )
+    REGEX_RECONF_LOSS = re.compile(
+        r'Packets lost due to reconfig: (\d*)'
+    )
+    REGEX_RECONF_TIME = re.compile(
+        r'Implied time lost: (\d*.[\de-]*)'
+    )
     REGEX_TC_TAG = re.compile(r'\d+[tT]\d+[cC]')
 
     REGEX_TC_NAME_OLD = re.compile(r'-\d+[tT]\d+[cC]-')
     REGEX_TC_TAG = re.compile(r'\d+[tT]\d+[cC]')
 
     REGEX_TC_NAME_OLD = re.compile(r'-\d+[tT]\d+[cC]-')
@@ -276,13 +319,9 @@ class ExecutionChecker(ResultVisitor):
         # 0 - no message
         # 1 - PAPI History of DUT1
         # 2 - PAPI History of DUT2
         # 0 - no message
         # 1 - PAPI History of DUT1
         # 2 - PAPI History of DUT2
-        self._lookup_kw_nr = 0
         self._conf_history_lookup_nr = 0
 
         self._conf_history_lookup_nr = 0
 
-        # Number of Show Running messages found
-        # 0 - no message
-        # 1 - Show run message found
-        self._show_run_lookup_nr = 0
+        self._sh_run_counter = 0
 
         # Test ID of currently processed test- the lowercase full path to the
         # test
 
         # Test ID of currently processed test- the lowercase full path to the
         # test
@@ -321,6 +360,140 @@ class ExecutionChecker(ResultVisitor):
         """
         return self._data
 
         """
         return self._data
 
+    def _get_data_from_mrr_test_msg(self, msg):
+        """Get info from message of MRR performance tests.
+
+        :param msg: Message to be processed.
+        :type msg: str
+        :returns: Processed message or original message if a problem occurs.
+        :rtype: str
+        """
+
+        groups = re.search(self.REGEX_MRR_MSG_INFO, msg)
+        if not groups or groups.lastindex != 1:
+            return u"Test Failed."
+
+        try:
+            data = groups.group(1).split(u", ")
+        except (AttributeError, IndexError, ValueError, KeyError):
+            return u"Test Failed."
+
+        out_str = u"["
+        try:
+            for item in data:
+                out_str += f"{(float(item) / 1e6):.2f}, "
+            return out_str[:-2] + u"]"
+        except (AttributeError, IndexError, ValueError, KeyError):
+            return u"Test Failed."
+
+    def _get_data_from_perf_test_msg(self, msg):
+        """Get info from message of NDRPDR performance tests.
+
+        :param msg: Message to be processed.
+        :type msg: str
+        :returns: Processed message or original message if a problem occurs.
+        :rtype: str
+        """
+
+        groups = re.search(self.REGEX_PERF_MSG_INFO, msg)
+        if not groups or groups.lastindex != 10:
+            return u"Test Failed."
+
+        try:
+            data = {
+                u"ndr_low": float(groups.group(1)),
+                u"ndr_low_b": float(groups.group(2)),
+                u"pdr_low": float(groups.group(3)),
+                u"pdr_low_b": float(groups.group(4)),
+                u"pdr_lat_90_1": groups.group(5),
+                u"pdr_lat_90_2": groups.group(6),
+                u"pdr_lat_50_1": groups.group(7),
+                u"pdr_lat_50_2": groups.group(8),
+                u"pdr_lat_10_1": groups.group(9),
+                u"pdr_lat_10_2": groups.group(10),
+            }
+        except (AttributeError, IndexError, ValueError, KeyError):
+            return u"Test Failed."
+
+        def _process_lat(in_str_1, in_str_2):
+            """Extract min, avg, max values from latency string.
+
+            :param in_str_1: Latency string for one direction produced by robot
+                framework.
+            :param in_str_2: Latency string for second direction produced by
+                robot framework.
+            :type in_str_1: str
+            :type in_str_2: str
+            :returns: Processed latency string or None if a problem occurs.
+            :rtype: tuple
+            """
+            in_list_1 = in_str_1.split('/', 3)
+            in_list_2 = in_str_2.split('/', 3)
+
+            if len(in_list_1) != 4 and len(in_list_2) != 4:
+                return None
+
+            in_list_1[3] += u"=" * (len(in_list_1[3]) % 4)
+            try:
+                hdr_lat_1 = hdrh.histogram.HdrHistogram.decode(in_list_1[3])
+            except hdrh.codec.HdrLengthException:
+                return None
+
+            in_list_2[3] += u"=" * (len(in_list_2[3]) % 4)
+            try:
+                hdr_lat_2 = hdrh.histogram.HdrHistogram.decode(in_list_2[3])
+            except hdrh.codec.HdrLengthException:
+                return None
+
+            if hdr_lat_1 and hdr_lat_2:
+                hdr_lat = (
+                    hdr_lat_1.get_value_at_percentile(50.0),
+                    hdr_lat_1.get_value_at_percentile(90.0),
+                    hdr_lat_1.get_value_at_percentile(99.0),
+                    hdr_lat_2.get_value_at_percentile(50.0),
+                    hdr_lat_2.get_value_at_percentile(90.0),
+                    hdr_lat_2.get_value_at_percentile(99.0)
+                )
+
+                if all(hdr_lat):
+                    return hdr_lat
+
+            return None
+
+        try:
+            out_msg = (
+                f"1. {(data[u'ndr_low'] / 1e6):5.2f}      "
+                f"{data[u'ndr_low_b']:5.2f}"
+                f"\n2. {(data[u'pdr_low'] / 1e6):5.2f}      "
+                f"{data[u'pdr_low_b']:5.2f}"
+            )
+            latency = (
+                _process_lat(data[u'pdr_lat_10_1'], data[u'pdr_lat_10_2']),
+                _process_lat(data[u'pdr_lat_50_1'], data[u'pdr_lat_50_2']),
+                _process_lat(data[u'pdr_lat_90_1'], data[u'pdr_lat_90_2'])
+            )
+            if all(latency):
+                max_len = len(str(max((max(item) for item in latency))))
+                max_len = 4 if max_len < 4 else max_len
+
+                for idx, lat in enumerate(latency):
+                    if not idx:
+                        out_msg += u"\n"
+                    out_msg += (
+                        f"\n{idx + 3}. "
+                        f"{lat[0]:{max_len}d} "
+                        f"{lat[1]:{max_len}d} "
+                        f"{lat[2]:{max_len}d}      "
+                        f"{lat[3]:{max_len}d} "
+                        f"{lat[4]:{max_len}d} "
+                        f"{lat[5]:{max_len}d} "
+                    )
+
+            return out_msg
+
+        except (AttributeError, IndexError, ValueError, KeyError):
+            return u"Test Failed."
+
     def _get_testbed(self, msg):
         """Called when extraction of testbed IP is required.
         The testbed is identified by TG node IP address.
     def _get_testbed(self, msg):
         """Called when extraction of testbed IP is required.
         The testbed is identified by TG node IP address.
@@ -429,7 +602,6 @@ class ExecutionChecker(ResultVisitor):
                           r"PAPI command history:", u"",
                           msg.message, count=1).replace(u'\n', u' |br| ').\
                 replace(u'"', u"'")
                           r"PAPI command history:", u"",
                           msg.message, count=1).replace(u'\n', u' |br| ').\
                 replace(u'"', u"'")
-
             self._data[u"tests"][self._test_id][u"conf-history"] += (
                 f" |br| **DUT{str(self._conf_history_lookup_nr)}:** {text}"
             )
             self._data[u"tests"][self._test_id][u"conf-history"] += (
                 f" |br| **DUT{str(self._conf_history_lookup_nr)}:** {text}"
             )
@@ -443,94 +615,74 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
 
         :returns: Nothing.
         """
 
+        if not msg.message.count(u"stats runtime"):
+            return
+
+        # Temporary solution
+        if self._sh_run_counter > 1:
+            return
+
         if u"show-run" not in self._data[u"tests"][self._test_id].keys():
         if u"show-run" not in self._data[u"tests"][self._test_id].keys():
-            self._data[u"tests"][self._test_id][u"show-run"] = str()
+            self._data[u"tests"][self._test_id][u"show-run"] = dict()
 
 
-        if msg.message.count(u"stats runtime") or \
-                msg.message.count(u"Runtime"):
-            try:
-                host = str(re.search(self.REGEX_TC_PAPI_CLI, msg.message).
-                           group(1))
-            except (AttributeError, IndexError):
-                host = u""
-            try:
-                socket = str(re.search(self.REGEX_TC_PAPI_CLI, msg.message).
-                             group(2))
-            except (AttributeError, IndexError):
-                socket = u""
-            runtime = loads(
-                str(msg.message).
-                replace(u' ', u'').
-                replace(u'\n', u'').
-                replace(u"'", u'"').
-                replace(u'b"', u'"').
-                replace(u'u"', u'"').
-                split(u":", 1)[1]
-            )
-            try:
-                threads_nr = len(runtime[0][u"clocks"])
-            except (IndexError, KeyError):
-                return
-            tbl_hdr = [
-                u"Name",
-                u"Calls",
-                u"Vectors",
-                u"Suspends",
-                u"Clocks",
-                u"Vectors/Calls"
-            ]
-            table = [[tbl_hdr, ] for _ in range(threads_nr)]
-            for item in runtime:
-                for idx in range(threads_nr):
-                    name = format(item[u"name"])
-                    calls = format(item[u"calls"][idx])
-                    vectors = format(item[u"vectors"][idx])
-                    suspends = format(item[u"suspends"][idx])
-                    if item[u"vectors"][idx] > 0:
-                        clocks = format(
-                            item[u"clocks"][idx]/item[u"vectors"][idx], u".2e")
-                    elif item[u"calls"][idx] > 0:
-                        clocks = format(
-                            item[u"clocks"][idx]/item[u"calls"][idx], u".2e")
-                    elif item[u"suspends"][idx] > 0:
-                        clocks = format(
-                            item[u"clocks"][idx]/item[u"suspends"][idx], u".2e")
-                    else:
-                        clocks = 0
-                    if item[u"calls"][idx] > 0:
-                        vectors_call = format(
-                            item[u"vectors"][idx]/item[u"calls"][idx], u".2f")
-                    else:
-                        vectors_call = format(0, u".2f")
-                    if int(calls) + int(vectors) + int(suspends):
-                        table[idx].append([
-                            name, calls, vectors, suspends, clocks, vectors_call
-                        ])
-            text = ""
+        groups = re.search(self.REGEX_TC_PAPI_CLI, msg.message)
+        if not groups:
+            return
+        try:
+            host = groups.group(1)
+        except (AttributeError, IndexError):
+            host = u""
+        try:
+            sock = groups.group(2)
+        except (AttributeError, IndexError):
+            sock = u""
+
+        runtime = loads(str(msg.message).replace(u' ', u'').replace(u'\n', u'').
+                        replace(u"'", u'"').replace(u'b"', u'"').
+                        replace(u'u"', u'"').split(u":", 1)[1])
+
+        try:
+            threads_nr = len(runtime[0][u"clocks"])
+        except (IndexError, KeyError):
+            return
+
+        dut = u"DUT{nr}".format(
+            nr=len(self._data[u'tests'][self._test_id][u'show-run'].keys()) + 1)
+
+        oper = {
+            u"host": host,
+            u"socket": sock,
+            u"threads": OrderedDict({idx: list() for idx in range(threads_nr)})
+        }
+
+        for item in runtime:
             for idx in range(threads_nr):
             for idx in range(threads_nr):
-                text += f"Thread {idx} "
-                text += u"vpp_main\n" if idx == 0 else f"vpp_wk_{idx-1}\n"
-                txt_table = None
-                for row in table[idx]:
-                    if txt_table is None:
-                        txt_table = prettytable.PrettyTable(row)
-                    else:
-                        if any(row[1:]):
-                            txt_table.add_row(row)
-                txt_table.set_style(prettytable.MSWORD_FRIENDLY)
-                txt_table.align[u"Name"] = u"l"
-                txt_table.align[u"Calls"] = u"r"
-                txt_table.align[u"Vectors"] = u"r"
-                txt_table.align[u"Suspends"] = u"r"
-                txt_table.align[u"Clocks"] = u"r"
-                txt_table.align[u"Vectors/Calls"] = u"r"
-
-                text += txt_table.get_string(sortby=u"Name") + u'\n'
-            text = f" \n **DUT: {host}/{socket}** \n {text}".\
-                replace(u'\n', u' |br| ').\
-                replace(u'\r', u'').\
-                replace(u'"', u"'")
-            self._data[u"tests"][self._test_id][u"show-run"] += text
+                if item[u"vectors"][idx] > 0:
+                    clocks = item[u"clocks"][idx] / item[u"vectors"][idx]
+                elif item[u"calls"][idx] > 0:
+                    clocks = item[u"clocks"][idx] / item[u"calls"][idx]
+                elif item[u"suspends"][idx] > 0:
+                    clocks = item[u"clocks"][idx] / item[u"suspends"][idx]
+                else:
+                    clocks = 0.0
+
+                if item[u"calls"][idx] > 0:
+                    vectors_call = item[u"vectors"][idx] / item[u"calls"][idx]
+                else:
+                    vectors_call = 0.0
+
+                if int(item[u"calls"][idx]) + int(item[u"vectors"][idx]) + \
+                        int(item[u"suspends"][idx]):
+                    oper[u"threads"][idx].append([
+                        item[u"name"],
+                        item[u"calls"][idx],
+                        item[u"vectors"][idx],
+                        item[u"suspends"][idx],
+                        clocks,
+                        vectors_call
+                    ])
+
+        self._data[u'tests'][self._test_id][u'show-run'][dut] = copy.copy(oper)
 
     def _get_ndrpdr_throughput(self, msg):
         """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER from the test
 
     def _get_ndrpdr_throughput(self, msg):
         """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER from the test
@@ -610,10 +762,33 @@ class ExecutionChecker(ResultVisitor):
             u"PDR": {
                 u"direction1": copy.copy(latency_default),
                 u"direction2": copy.copy(latency_default)
             u"PDR": {
                 u"direction1": copy.copy(latency_default),
                 u"direction2": copy.copy(latency_default)
-            }
+            },
+            u"LAT0": {
+                u"direction1": copy.copy(latency_default),
+                u"direction2": copy.copy(latency_default)
+            },
+            u"PDR10": {
+                u"direction1": copy.copy(latency_default),
+                u"direction2": copy.copy(latency_default)
+            },
+            u"PDR50": {
+                u"direction1": copy.copy(latency_default),
+                u"direction2": copy.copy(latency_default)
+            },
+            u"PDR90": {
+                u"direction1": copy.copy(latency_default),
+                u"direction2": copy.copy(latency_default)
+            },
         }
         }
-        status = u"FAIL"
-        groups = re.search(self.REGEX_NDRPDR_LAT, msg)
+
+        # TODO: Rewrite when long and base are not needed
+        groups = re.search(self.REGEX_NDRPDR_LAT_LONG, msg)
+        if groups is None:
+            groups = re.search(self.REGEX_NDRPDR_LAT, msg)
+        if groups is None:
+            groups = re.search(self.REGEX_NDRPDR_LAT_BASE, msg)
+        if groups is None:
+            return latency, u"FAIL"
 
         def process_latency(in_str):
             """Return object with parsed latency values.
 
         def process_latency(in_str):
             """Return object with parsed latency values.
@@ -641,21 +816,97 @@ class ExecutionChecker(ResultVisitor):
 
             return rval
 
 
             return rval
 
-        if groups is not None:
+        try:
+            latency[u"NDR"][u"direction1"] = process_latency(groups.group(1))
+            latency[u"NDR"][u"direction2"] = process_latency(groups.group(2))
+            latency[u"PDR"][u"direction1"] = process_latency(groups.group(3))
+            latency[u"PDR"][u"direction2"] = process_latency(groups.group(4))
+            if groups.lastindex == 4:
+                return latency, u"PASS"
+        except (IndexError, ValueError):
+            pass
+
+        try:
+            latency[u"PDR90"][u"direction1"] = process_latency(groups.group(5))
+            latency[u"PDR90"][u"direction2"] = process_latency(groups.group(6))
+            latency[u"PDR50"][u"direction1"] = process_latency(groups.group(7))
+            latency[u"PDR50"][u"direction2"] = process_latency(groups.group(8))
+            latency[u"PDR10"][u"direction1"] = process_latency(groups.group(9))
+            latency[u"PDR10"][u"direction2"] = process_latency(groups.group(10))
+            latency[u"LAT0"][u"direction1"] = process_latency(groups.group(11))
+            latency[u"LAT0"][u"direction2"] = process_latency(groups.group(12))
+            if groups.lastindex == 12:
+                return latency, u"PASS"
+        except (IndexError, ValueError):
+            pass
+
+        # TODO: Remove when not needed
+        latency[u"NDR10"] = {
+            u"direction1": copy.copy(latency_default),
+            u"direction2": copy.copy(latency_default)
+        }
+        latency[u"NDR50"] = {
+            u"direction1": copy.copy(latency_default),
+            u"direction2": copy.copy(latency_default)
+        }
+        latency[u"NDR90"] = {
+            u"direction1": copy.copy(latency_default),
+            u"direction2": copy.copy(latency_default)
+        }
+        try:
+            latency[u"LAT0"][u"direction1"] = process_latency(groups.group(5))
+            latency[u"LAT0"][u"direction2"] = process_latency(groups.group(6))
+            latency[u"NDR10"][u"direction1"] = process_latency(groups.group(7))
+            latency[u"NDR10"][u"direction2"] = process_latency(groups.group(8))
+            latency[u"NDR50"][u"direction1"] = process_latency(groups.group(9))
+            latency[u"NDR50"][u"direction2"] = process_latency(groups.group(10))
+            latency[u"NDR90"][u"direction1"] = process_latency(groups.group(11))
+            latency[u"NDR90"][u"direction2"] = process_latency(groups.group(12))
+            latency[u"PDR10"][u"direction1"] = process_latency(groups.group(13))
+            latency[u"PDR10"][u"direction2"] = process_latency(groups.group(14))
+            latency[u"PDR50"][u"direction1"] = process_latency(groups.group(15))
+            latency[u"PDR50"][u"direction2"] = process_latency(groups.group(16))
+            latency[u"PDR90"][u"direction1"] = process_latency(groups.group(17))
+            latency[u"PDR90"][u"direction2"] = process_latency(groups.group(18))
+            return latency, u"PASS"
+        except (IndexError, ValueError):
+            pass
+
+        return latency, u"FAIL"
+
+    @staticmethod
+    def _get_hoststack_data(msg, tags):
+        """Get data from the hoststack test message.
+
+        :param msg: The test message to be parsed.
+        :param tags: Test tags.
+        :type msg: str
+        :type tags: list
+        :returns: Parsed data as a JSON dict and the status (PASS/FAIL).
+        :rtype: tuple(dict, str)
+        """
+        result = dict()
+        status = u"FAIL"
+
+        msg = msg.replace(u"'", u'"').replace(u" ", u"")
+        if u"LDPRELOAD" in tags:
             try:
             try:
-                latency[u"NDR"][u"direction1"] = \
-                    process_latency(groups.group(1))
-                latency[u"NDR"][u"direction2"] = \
-                    process_latency(groups.group(2))
-                latency[u"PDR"][u"direction1"] = \
-                    process_latency(groups.group(3))
-                latency[u"PDR"][u"direction2"] = \
-                    process_latency(groups.group(4))
+                result = loads(msg)
                 status = u"PASS"
                 status = u"PASS"
-            except (IndexError, ValueError):
+            except JSONDecodeError:
+                pass
+        elif u"VPPECHO" in tags:
+            try:
+                msg_lst = msg.replace(u"}{", u"} {").split(u" ")
+                result = dict(
+                    client=loads(msg_lst[0]),
+                    server=loads(msg_lst[1])
+                )
+                status = u"PASS"
+            except (JSONDecodeError, IndexError):
                 pass
 
                 pass
 
-        return latency, status
+        return result, status
 
     def visit_suite(self, suite):
         """Implements traversing through the suite and its direct children.
 
     def visit_suite(self, suite):
         """Implements traversing through the suite and its direct children.
@@ -728,6 +979,8 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
 
         :returns: Nothing.
         """
 
+        self._sh_run_counter = 0
+
         longname_orig = test.longname.lower()
 
         # Check the ignore list
         longname_orig = test.longname.lower()
 
         # Check the ignore list
@@ -762,13 +1015,24 @@ class ExecutionChecker(ResultVisitor):
             replace(u'\r', u'').\
             replace(u'[', u' |br| [').\
             replace(u' |br| [', u'[', 1)
             replace(u'\r', u'').\
             replace(u'[', u' |br| [').\
             replace(u' |br| [', u'[', 1)
-        test_result[u"msg"] = test.message.\
-            replace(u'\n', u' |br| ').\
-            replace(u'\r', u'').\
-            replace(u'"', u"'")
         test_result[u"type"] = u"FUNC"
         test_result[u"status"] = test.status
 
         test_result[u"type"] = u"FUNC"
         test_result[u"status"] = test.status
 
+        if test.status == u"PASS":
+            if u"NDRPDR" in tags:
+                test_result[u"msg"] = self._get_data_from_perf_test_msg(
+                    test.message).replace(u'\n', u' |br| ').\
+                    replace(u'\r', u'').replace(u'"', u"'")
+            elif u"MRR" in tags or u"FRMOBL" in tags or u"BMRR" in tags:
+                test_result[u"msg"] = self._get_data_from_mrr_test_msg(
+                    test.message).replace(u'\n', u' |br| ').\
+                    replace(u'\r', u'').replace(u'"', u"'")
+            else:
+                test_result[u"msg"] = test.message.replace(u'\n', u' |br| ').\
+                    replace(u'\r', u'').replace(u'"', u"'")
+        else:
+            test_result[u"msg"] = u"Test Failed."
+
         if u"PERFTEST" in tags:
             # Replace info about cores (e.g. -1c-) with the info about threads
             # and cores (e.g. -1t1c-) in the long test case names and in the
         if u"PERFTEST" in tags:
             # Replace info about cores (e.g. -1c-) with the info about threads
             # and cores (e.g. -1t1c-) in the long test case names and in the
@@ -784,14 +1048,14 @@ class ExecutionChecker(ResultVisitor):
                         tag_tc = tag
 
                 if tag_count == 1:
                         tag_tc = tag
 
                 if tag_count == 1:
-                    self._test_id = re.sub(self.REGEX_TC_NAME_NEW,
-                                           f"-{tag_tc.lower()}-",
-                                           self._test_id,
-                                           count=1)
-                    test_result[u"name"] = re.sub(self.REGEX_TC_NAME_NEW,
-                                                  f"-{tag_tc.lower()}-",
-                                                  test_result["name"],
-                                                  count=1)
+                    self._test_id = re.sub(
+                        self.REGEX_TC_NAME_NEW, f"-{tag_tc.lower()}-",
+                        self._test_id, count=1
+                    )
+                    test_result[u"name"] = re.sub(
+                        self.REGEX_TC_NAME_NEW, f"-{tag_tc.lower()}-",
+                        test_result["name"], count=1
+                    )
                 else:
                     test_result[u"status"] = u"FAIL"
                     self._data[u"tests"][self._test_id] = test_result
                 else:
                     test_result[u"status"] = u"FAIL"
                     self._data[u"tests"][self._test_id] = test_result
@@ -813,6 +1077,10 @@ class ExecutionChecker(ResultVisitor):
                 test_result[u"type"] = u"SOAK"
                 test_result[u"throughput"], test_result[u"status"] = \
                     self._get_plr_throughput(test.message)
                 test_result[u"type"] = u"SOAK"
                 test_result[u"throughput"], test_result[u"status"] = \
                     self._get_plr_throughput(test.message)
+            elif u"HOSTSTACK" in tags:
+                test_result[u"type"] = u"HOSTSTACK"
+                test_result[u"result"], test_result[u"status"] = \
+                    self._get_hoststack_data(test.message, tags)
             elif u"TCP" in tags:
                 test_result[u"type"] = u"TCP"
                 groups = re.search(self.REGEX_TCP, test.message)
             elif u"TCP" in tags:
                 test_result[u"type"] = u"TCP"
                 groups = re.search(self.REGEX_TCP, test.message)
@@ -832,6 +1100,7 @@ class ExecutionChecker(ResultVisitor):
                     # Use whole list in CSIT-1180.
                     stats = jumpavg.AvgStdevStats.for_runs(items_float)
                     test_result[u"result"][u"receive-rate"] = stats.avg
                     # Use whole list in CSIT-1180.
                     stats = jumpavg.AvgStdevStats.for_runs(items_float)
                     test_result[u"result"][u"receive-rate"] = stats.avg
+                    test_result[u"result"][u"receive-stdev"] = stats.stdev
                 else:
                     groups = re.search(self.REGEX_MRR, test.message)
                     test_result[u"result"][u"receive-rate"] = \
                 else:
                     groups = re.search(self.REGEX_MRR, test.message)
                     test_result[u"result"][u"receive-rate"] = \
@@ -848,6 +1117,8 @@ class ExecutionChecker(ResultVisitor):
                     }
                 except (AttributeError, IndexError, ValueError, TypeError):
                     test_result[u"status"] = u"FAIL"
                     }
                 except (AttributeError, IndexError, ValueError, TypeError):
                     test_result[u"status"] = u"FAIL"
+            elif u"DEVICETEST" in tags:
+                test_result[u"type"] = u"DEVICETEST"
             else:
                 test_result[u"status"] = u"FAIL"
                 self._data[u"tests"][self._test_id] = test_result
             else:
                 test_result[u"status"] = u"FAIL"
                 self._data[u"tests"][self._test_id] = test_result
@@ -884,10 +1155,8 @@ class ExecutionChecker(ResultVisitor):
             if keyword.type == u"setup":
                 self.visit_setup_kw(keyword)
             elif keyword.type == u"teardown":
             if keyword.type == u"setup":
                 self.visit_setup_kw(keyword)
             elif keyword.type == u"teardown":
-                self._lookup_kw_nr = 0
                 self.visit_teardown_kw(keyword)
             else:
                 self.visit_teardown_kw(keyword)
             else:
-                self._lookup_kw_nr = 0
                 self.visit_test_kw(keyword)
         except AttributeError:
             pass
                 self.visit_test_kw(keyword)
         except AttributeError:
             pass
@@ -921,11 +1190,12 @@ class ExecutionChecker(ResultVisitor):
         :type test_kw: Keyword
         :returns: Nothing.
         """
         :type test_kw: Keyword
         :returns: Nothing.
         """
-        if test_kw.name.count(u"Show Runtime Counters On All Duts"):
-            self._lookup_kw_nr += 1
-            self._show_run_lookup_nr = 0
+        if test_kw.name.count(u"Show Runtime On All Duts") or \
+                test_kw.name.count(u"Show Runtime Counters On All Duts"):
             self._msg_type = u"test-show-runtime"
             self._msg_type = u"test-show-runtime"
-        elif test_kw.name.count(u"Install Dpdk Test") and not self._version:
+            self._sh_run_counter += 1
+        elif test_kw.name.count(u"Install Dpdk Test On All Duts") and \
+                not self._version:
             self._msg_type = u"dpdk-version"
         else:
             return
             self._msg_type = u"dpdk-version"
         else:
             return
@@ -1037,7 +1307,6 @@ class ExecutionChecker(ResultVisitor):
         :type msg: Message
         :returns: Nothing.
         """
         :type msg: Message
         :returns: Nothing.
         """
-
         if self._msg_type:
             self.parse_msg[self._msg_type](msg)
 
         if self._msg_type:
             self.parse_msg[self._msg_type](msg)
 
@@ -1099,7 +1368,6 @@ class InputData:
         :returns: Metadata
         :rtype: pandas.Series
         """
         :returns: Metadata
         :rtype: pandas.Series
         """
-
         return self.data[job][build][u"metadata"]
 
     def suites(self, job, build):
         return self.data[job][build][u"metadata"]
 
     def suites(self, job, build):
@@ -1112,7 +1380,6 @@ class InputData:
         :returns: Suites.
         :rtype: pandas.Series
         """
         :returns: Suites.
         :rtype: pandas.Series
         """
-
         return self.data[job][str(build)][u"suites"]
 
     def tests(self, job, build):
         return self.data[job][str(build)][u"suites"]
 
     def tests(self, job, build):
@@ -1125,7 +1392,6 @@ class InputData:
         :returns: Tests.
         :rtype: pandas.Series
         """
         :returns: Tests.
         :rtype: pandas.Series
         """
-
         return self.data[job][build][u"tests"]
 
     def _parse_tests(self, job, build, log):
         return self.data[job][build][u"tests"]
 
     def _parse_tests(self, job, build, log):
@@ -1309,6 +1575,122 @@ class InputData:
 
         logging.info(u"Done.")
 
 
         logging.info(u"Done.")
 
+    def process_local_file(self, local_file, job=u"local", build_nr=1,
+                           replace=True):
+        """Process local XML file given as a command-line parameter.
+
+        :param local_file: The file to process.
+        :param job: Job name.
+        :param build_nr: Build number.
+        :param replace: If True, the information about jobs and builds is
+            replaced by the new one, otherwise the new jobs and builds are
+            added.
+        :type local_file: str
+        :type job: str
+        :type build_nr: int
+        :type replace: bool
+        :raises: PresentationError if an error occurs.
+        """
+        if not isfile(local_file):
+            raise PresentationError(f"The file {local_file} does not exist.")
+
+        build = {
+            u"build": build_nr,
+            u"status": u"failed",
+            u"file-name": local_file
+        }
+        if replace:
+            self._cfg.builds = dict()
+        self._cfg.add_build(job, build)
+
+        logging.info(f"Processing {job}: {build_nr:2d}: {local_file}")
+        data = self._parse_tests(job, build, list())
+        if data is None:
+            raise PresentationError(
+                f"Error occurred while parsing the file {local_file}"
+            )
+
+        build_data = pd.Series({
+            u"metadata": pd.Series(
+                list(data[u"metadata"].values()),
+                index=list(data[u"metadata"].keys())
+            ),
+            u"suites": pd.Series(
+                list(data[u"suites"].values()),
+                index=list(data[u"suites"].keys())
+            ),
+            u"tests": pd.Series(
+                list(data[u"tests"].values()),
+                index=list(data[u"tests"].keys())
+            )
+        })
+
+        if self._input_data.get(job, None) is None:
+            self._input_data[job] = pd.Series()
+        self._input_data[job][str(build_nr)] = build_data
+
+        self._cfg.set_input_state(job, build_nr, u"processed")
+
+    def process_local_directory(self, local_dir, replace=True):
+        """Process local directory with XML file(s). The directory is processed
+        as a 'job' and the XML files in it as builds.
+        If the given directory contains only sub-directories, these
+        sub-directories processed as jobs and corresponding XML files as builds
+        of their job.
+
+        :param local_dir: Local directory to process.
+        :param replace: If True, the information about jobs and builds is
+            replaced by the new one, otherwise the new jobs and builds are
+            added.
+        :type local_dir: str
+        :type replace: bool
+        """
+        if not isdir(local_dir):
+            raise PresentationError(
+                f"The directory {local_dir} does not exist."
+            )
+
+        # Check if the given directory includes only files, or only directories
+        _, dirnames, filenames = next(walk(local_dir))
+
+        if filenames and not dirnames:
+            filenames.sort()
+            # local_builds:
+            # key: dir (job) name, value: list of file names (builds)
+            local_builds = {
+                local_dir: [join(local_dir, name) for name in filenames]
+            }
+
+        elif dirnames and not filenames:
+            dirnames.sort()
+            # local_builds:
+            # key: dir (job) name, value: list of file names (builds)
+            local_builds = dict()
+            for dirname in dirnames:
+                builds = [
+                    join(local_dir, dirname, name)
+                    for name in listdir(join(local_dir, dirname))
+                    if isfile(join(local_dir, dirname, name))
+                ]
+                if builds:
+                    local_builds[dirname] = sorted(builds)
+
+        elif not filenames and not dirnames:
+            raise PresentationError(f"The directory {local_dir} is empty.")
+        else:
+            raise PresentationError(
+                f"The directory {local_dir} can include only files or only "
+                f"directories, not both.\nThe directory {local_dir} includes "
+                f"file(s):\n{filenames}\nand directories:\n{dirnames}"
+            )
+
+        if replace:
+            self._cfg.builds = dict()
+
+        for job, files in local_builds.items():
+            for idx, local_file in enumerate(files):
+                self.process_local_file(local_file, job, idx + 1, replace=False)
+
     @staticmethod
     def _end_of_tag(tag_filter, start=0, closer=u"'"):
         """Return the index of character in the string which is the end of tag.
     @staticmethod
     def _end_of_tag(tag_filter, start=0, closer=u"'"):
         """Return the index of character in the string which is the end of tag.
@@ -1322,7 +1704,6 @@ class InputData:
         :returns: The index of the tag closer.
         :rtype: int
         """
         :returns: The index of the tag closer.
         :rtype: int
         """
-
         try:
             idx_opener = tag_filter.index(closer, start)
             return tag_filter.index(closer, idx_opener + 1)
         try:
             idx_opener = tag_filter.index(closer, start)
             return tag_filter.index(closer, idx_opener + 1)
@@ -1338,7 +1719,6 @@ class InputData:
         :returns: Conditional statement which can be evaluated.
         :rtype: str
         """
         :returns: Conditional statement which can be evaluated.
         :rtype: str
         """
-
         index = 0
         while True:
             index = InputData._end_of_tag(tag_filter, index)
         index = 0
         while True:
             index = InputData._end_of_tag(tag_filter, index)
@@ -1352,7 +1732,6 @@ class InputData:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
-
         - job 1
           - build 1
             - test (or suite) 1 ID:
         - job 1
           - build 1
             - test (or suite) 1 ID:
@@ -1387,7 +1766,9 @@ class InputData:
         """
 
         try:
         """
 
         try:
-            if element[u"filter"] in (u"all", u"template"):
+            if data_set == "suites":
+                cond = u"True"
+            elif element[u"filter"] in (u"all", u"template"):
                 cond = u"True"
             else:
                 cond = InputData._condition(element[u"filter"])
                 cond = u"True"
             else:
                 cond = InputData._condition(element[u"filter"])
@@ -1453,7 +1834,6 @@ class InputData:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
         """Filter required data from the given jobs and builds.
 
         The output data structure is:
-
         - job 1
           - build 1
             - test (or suite) 1 ID:
         - job 1
           - build 1
             - test (or suite) 1 ID:
@@ -1567,5 +1947,47 @@ class InputData:
             for item in builds.values:
                 for item_id, item_data in item.items():
                     merged_data[item_id] = item_data
             for item in builds.values:
                 for item_id, item_data in item.items():
                     merged_data[item_id] = item_data
-
         return merged_data
         return merged_data
+
+    def print_all_oper_data(self):
+        """Print all operational data to console.
+        """
+
+        tbl_hdr = (
+            u"Name",
+            u"Nr of Vectors",
+            u"Nr of Packets",
+            u"Suspends",
+            u"Cycles per Packet",
+            u"Average Vector Size"
+        )
+
+        for job in self._input_data.values:
+            for build in job.values:
+                for test_id, test_data in build[u"tests"].items():
+                    print(f"{test_id}")
+                    if test_data.get(u"show-run", None) is None:
+                        continue
+                    for dut_name, data in test_data[u"show-run"].items():
+                        if data.get(u"threads", None) is None:
+                            continue
+                        print(f"Host IP: {data.get(u'host', '')}, "
+                              f"Socket: {data.get(u'socket', '')}")
+                        for thread_nr, thread in data[u"threads"].items():
+                            txt_table = prettytable.PrettyTable(tbl_hdr)
+                            avg = 0.0
+                            for row in thread:
+                                txt_table.add_row(row)
+                                avg += row[-1]
+                            if len(thread) == 0:
+                                avg = u""
+                            else:
+                                avg = f", Average Vector Size per Node: " \
+                                      f"{(avg / len(thread)):.2f}"
+                            th_name = u"main" if thread_nr == 0 \
+                                else f"worker_{thread_nr}"
+                            print(f"{dut_name}, {th_name}{avg}")
+                            txt_table.float_format = u".2"
+                            txt_table.align = u"r"
+                            txt_table.align[u"Name"] = u"l"
+                            print(f"{txt_table.get_string()}\n")