Python3: resources and libraries
[csit.git] / resources / tools / presentation / input_data_parser.py
index a35a454..46c8b9d 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2018 Cisco and/or its affiliates.
+# Copyright (c) 2019 Cisco and/or its affiliates.
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at:
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at:
 - filter the data using tags,
 """
 
 - filter the data using tags,
 """
 
-import multiprocessing
-import os
+import copy
 import re
 import re
+import resource
 import pandas as pd
 import logging
 import pandas as pd
 import logging
+import prettytable
 
 from robot.api import ExecutionResult, ResultVisitor
 from robot import errors
 from collections import OrderedDict
 from string import replace
 from os import remove
 
 from robot.api import ExecutionResult, ResultVisitor
 from robot import errors
 from collections import OrderedDict
 from string import replace
 from os import remove
+from datetime import datetime as dt
+from datetime import timedelta
+from json import loads
 from jumpavg.AvgStdevMetadataFactory import AvgStdevMetadataFactory
 
 from input_data_files import download_and_unzip_data_file
 from jumpavg.AvgStdevMetadataFactory import AvgStdevMetadataFactory
 
 from input_data_files import download_and_unzip_data_file
-from utils import Worker
+
+
+# Separator used in file names
+SEPARATOR = "__"
 
 
 class ExecutionChecker(ResultVisitor):
 
 
 class ExecutionChecker(ResultVisitor):
@@ -71,7 +78,7 @@ class ExecutionChecker(ResultVisitor):
                 "parent": "Name of the parent of the test",
                 "doc": "Test documentation",
                 "msg": "Test message",
                 "parent": "Name of the parent of the test",
                 "doc": "Test documentation",
                 "msg": "Test message",
-                "vat-history": "DUT1 and DUT2 VAT History",
+                "conf-history": "DUT1 and DUT2 VAT History",
                 "show-run": "Show Run",
                 "tags": ["tag 1", "tag 2", "tag n"],
                 "type": "NDRPDR",
                 "show-run": "Show Run",
                 "tags": ["tag 1", "tag 2", "tag n"],
                 "type": "NDRPDR",
@@ -91,24 +98,28 @@ class ExecutionChecker(ResultVisitor):
                         "direction1": {
                             "min": float,
                             "avg": float,
                         "direction1": {
                             "min": float,
                             "avg": float,
-                            "max": float
+                            "max": float,
+                            "hdrh": str
                         },
                         "direction2": {
                             "min": float,
                             "avg": float,
                         },
                         "direction2": {
                             "min": float,
                             "avg": float,
-                            "max": float
+                            "max": float,
+                            "hdrh": str
                         }
                     },
                     "PDR": {
                         "direction1": {
                             "min": float,
                             "avg": float,
                         }
                     },
                     "PDR": {
                         "direction1": {
                             "min": float,
                             "avg": float,
-                            "max": float
+                            "max": float,
+                            "hdrh": str
                         },
                         "direction2": {
                             "min": float,
                             "avg": float,
                         },
                         "direction2": {
                             "min": float,
                             "avg": float,
-                            "max": float
+                            "max": float,
+                            "hdrh": str
                         }
                     }
                 }
                         }
                     }
                 }
@@ -140,60 +151,6 @@ class ExecutionChecker(ResultVisitor):
                 }
             }
 
                 }
             }
 
-            # TODO: Remove when definitely no NDRPDRDISC tests are used:
-            # NDRPDRDISC tests:
-            "ID": {
-                "name": "Test name",
-                "parent": "Name of the parent of the test",
-                "doc": "Test documentation",
-                "msg": "Test message",
-                "tags": ["tag 1", "tag 2", "tag n"],
-                "type": "PDR" | "NDR",
-                "status": "PASS" | "FAIL",
-                "throughput": {  # Only type: "PDR" | "NDR"
-                    "value": int,
-                    "unit": "pps" | "bps" | "percentage"
-                },
-                "latency": {  # Only type: "PDR" | "NDR"
-                    "direction1": {
-                        "100": {
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        },
-                        "50": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        },
-                        "10": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        }
-                    },
-                    "direction2": {
-                        "100": {
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        },
-                        "50": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        },
-                        "10": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        }
-                    }
-                },
-                "lossTolerance": "lossTolerance",  # Only type: "PDR"
-                "vat-history": "DUT1 and DUT2 VAT History"
-                "show-run": "Show Run"
-            },
             "ID" {
                 # next test
             }
             "ID" {
                 # next test
             }
@@ -228,7 +185,7 @@ class ExecutionChecker(ResultVisitor):
                 "doc": "Test documentation"
                 "msg": "Test message"
                 "tags": ["tag 1", "tag 2", "tag n"],
                 "doc": "Test documentation"
                 "msg": "Test message"
                 "tags": ["tag 1", "tag 2", "tag n"],
-                "vat-history": "DUT1 and DUT2 VAT History"
+                "conf-history": "DUT1 and DUT2 VAT History"
                 "show-run": "Show Run"
                 "status": "PASS" | "FAIL"
             },
                 "show-run": "Show Run"
                 "status": "PASS" | "FAIL"
             },
@@ -244,36 +201,26 @@ class ExecutionChecker(ResultVisitor):
     # TODO: Remove when definitely no NDRPDRDISC tests are used:
     REGEX_RATE = re.compile(r'^[\D\d]*FINAL_RATE:\s(\d+\.\d+)\s(\w+)')
 
     # TODO: Remove when definitely no NDRPDRDISC tests are used:
     REGEX_RATE = re.compile(r'^[\D\d]*FINAL_RATE:\s(\d+\.\d+)\s(\w+)')
 
+    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_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+)')
 
-    # TODO: Remove when definitely no NDRPDRDISC tests are used:
-    REGEX_LAT_NDR = re.compile(r'^[\D\d]*'
-                               r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
-                               r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
-                               r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
-                               r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
-                               r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
-                               r'\s\'(-?\d+/-?\d+/-?\d+)\'\]')
-
-    REGEX_LAT_PDR = re.compile(r'^[\D\d]*'
-                               r'LAT_\d+%PDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
-                               r'\s\'(-?\d+/-?\d+/-?\d+)\'\][\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_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*)(.*)")
+    REGEX_VERSION_VPP = re.compile(r"(return STDOUT Version:\s*|"
+                                   r"VPP Version:\s*|VPP version:\s*)(.*)")
 
 
-    REGEX_VERSION_DPDK = re.compile(r"(return STDOUT testpmd)([\d\D\n]*)"
-                                    r"(RTE Version: 'DPDK )(.*)(')")
+    REGEX_VERSION_DPDK = re.compile(r"(DPDK version:\s*|DPDK Version:\s*)(.*)")
 
 
-    REGEX_TCP = re.compile(r'Total\s(rps|cps|throughput):\s([0-9]*).*$')
+    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_MRR = re.compile(r'MaxReceivedRate_Results\s\[pkts/(\d*)sec\]:\s'
                            r'tx\s(\d*),\srx\s(\d*)')
@@ -281,13 +228,18 @@ class ExecutionChecker(ResultVisitor):
     REGEX_BMRR = re.compile(r'Maximum Receive Rate trial results'
                             r' in packets per second: \[(.*)\]')
 
     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_NAME_NEW = re.compile(r'-\d+[cC]-')
 
     REGEX_TC_TAG = re.compile(r'\d+[tT]\d+[cC]')
 
     REGEX_TC_NAME_OLD = re.compile(r'-\d+[tT]\d+[cC]-')
 
     REGEX_TC_NAME_NEW = re.compile(r'-\d+[cC]-')
 
-    REGEX_TC_NUMBER = re.compile(r'tc[0-9]{2}-')
+    REGEX_TC_NUMBER = re.compile(r'tc\d{2}-')
+
+    REGEX_TC_PAPI_CLI = re.compile(r'.*\((\d+.\d+.\d+.\d+.) - (.*)\)')
 
     def __init__(self, metadata, mapping, ignore):
         """Initialisation.
 
     def __init__(self, metadata, mapping, ignore):
         """Initialisation.
@@ -311,6 +263,9 @@ class ExecutionChecker(ResultVisitor):
         # Timestamp
         self._timestamp = None
 
         # Timestamp
         self._timestamp = None
 
+        # Testbed. The testbed is identified by TG node IP address.
+        self._testbed = None
+
         # Mapping of TCs long names
         self._mapping = mapping
 
         # Mapping of TCs long names
         self._mapping = mapping
 
@@ -322,7 +277,7 @@ class ExecutionChecker(ResultVisitor):
         # 1 - VAT History of DUT1
         # 2 - VAT History of DUT2
         self._lookup_kw_nr = 0
         # 1 - VAT History of DUT1
         # 2 - VAT History of DUT2
         self._lookup_kw_nr = 0
-        self._vat_history_lookup_nr = 0
+        self._conf_history_lookup_nr = 0
 
         # Number of Show Running messages found
         # 0 - no message
 
         # Number of Show Running messages found
         # 0 - no message
@@ -351,7 +306,9 @@ class ExecutionChecker(ResultVisitor):
             "vpp-version": self._get_vpp_version,
             "dpdk-version": self._get_dpdk_version,
             "teardown-vat-history": self._get_vat_history,
             "vpp-version": self._get_vpp_version,
             "dpdk-version": self._get_dpdk_version,
             "teardown-vat-history": self._get_vat_history,
-            "test-show-runtime": self._get_show_run
+            "teardown-papi-history": self._get_papi_history,
+            "test-show-runtime": self._get_show_run,
+            "testbed": self._get_testbed
         }
 
     @property
         }
 
     @property
@@ -363,6 +320,26 @@ class ExecutionChecker(ResultVisitor):
         """
         return self._data
 
         """
         return self._data
 
+    def _get_testbed(self, msg):
+        """Called when extraction of testbed IP is required.
+        The testbed is identified by TG node IP address.
+
+        :param msg: Message to process.
+        :type msg: Message
+        :returns: Nothing.
+        """
+
+        if msg.message.count("Setup of TG node"):
+            reg_tg_ip = re.compile(
+                r'Setup of TG node (\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}) done')
+            try:
+                self._testbed = str(re.search(reg_tg_ip, msg.message).group(1))
+            except (KeyError, ValueError, IndexError, AttributeError):
+                pass
+            finally:
+                self._data["metadata"]["testbed"] = self._testbed
+                self._msg_type = None
+
     def _get_vpp_version(self, msg):
         """Called when extraction of VPP version is required.
 
     def _get_vpp_version(self, msg):
         """Called when extraction of VPP version is required.
 
@@ -371,7 +348,9 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
 
         :returns: Nothing.
         """
 
-        if msg.message.count("return STDOUT Version:"):
+        if msg.message.count("return STDOUT Version:") or \
+            msg.message.count("VPP Version:") or \
+            msg.message.count("VPP version:"):
             self._version = str(re.search(self.REGEX_VERSION_VPP, msg.message).
                                 group(2))
             self._data["metadata"]["version"] = self._version
             self._version = str(re.search(self.REGEX_VERSION_VPP, msg.message).
                                 group(2))
             self._data["metadata"]["version"] = self._version
@@ -385,10 +364,10 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
 
         :returns: Nothing.
         """
 
-        if msg.message.count("return STDOUT testpmd"):
+        if msg.message.count("DPDK Version:"):
             try:
                 self._version = str(re.search(
             try:
                 self._version = str(re.search(
-                    self.REGEX_VERSION_DPDK, msg.message). group(4))
+                    self.REGEX_VERSION_DPDK, msg.message). group(2))
                 self._data["metadata"]["version"] = self._version
             except IndexError:
                 pass
                 self._data["metadata"]["version"] = self._version
             except IndexError:
                 pass
@@ -415,92 +394,121 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
         if msg.message.count("VAT command history:"):
         :returns: Nothing.
         """
         if msg.message.count("VAT command history:"):
-            self._vat_history_lookup_nr += 1
-            if self._vat_history_lookup_nr == 1:
-                self._data["tests"][self._test_ID]["vat-history"] = str()
+            self._conf_history_lookup_nr += 1
+            if self._conf_history_lookup_nr == 1:
+                self._data["tests"][self._test_ID]["conf-history"] = str()
             else:
                 self._msg_type = None
             else:
                 self._msg_type = None
-            text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
+            text = re.sub("\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3} "
                           "VAT command history:", "", msg.message, count=1). \
                 replace("\n\n", "\n").replace('\n', ' |br| ').\
                 replace('\r', '').replace('"', "'")
 
                           "VAT command history:", "", msg.message, count=1). \
                 replace("\n\n", "\n").replace('\n', ' |br| ').\
                 replace('\r', '').replace('"', "'")
 
-            self._data["tests"][self._test_ID]["vat-history"] += " |br| "
-            self._data["tests"][self._test_ID]["vat-history"] += \
-                "**DUT" + str(self._vat_history_lookup_nr) + ":** " + text
+            self._data["tests"][self._test_ID]["conf-history"] += " |br| "
+            self._data["tests"][self._test_ID]["conf-history"] += \
+                "**DUT" + str(self._conf_history_lookup_nr) + ":** " + text
 
 
-    def _get_show_run(self, msg):
-        """Called when extraction of VPP operational data (output of CLI command
-        Show Runtime) is required.
+    def _get_papi_history(self, msg):
+        """Called when extraction of PAPI command history is required.
 
         :param msg: Message to process.
         :type msg: Message
         :returns: Nothing.
         """
 
         :param msg: Message to process.
         :type msg: Message
         :returns: Nothing.
         """
-        if msg.message.count("return STDOUT Thread "):
-            self._show_run_lookup_nr += 1
-            if self._lookup_kw_nr == 1 and self._show_run_lookup_nr == 1:
-                self._data["tests"][self._test_ID]["show-run"] = str()
-            if self._lookup_kw_nr > 1:
+        if msg.message.count("PAPI command history:"):
+            self._conf_history_lookup_nr += 1
+            if self._conf_history_lookup_nr == 1:
+                self._data["tests"][self._test_ID]["conf-history"] = str()
+            else:
                 self._msg_type = None
                 self._msg_type = None
-            if self._show_run_lookup_nr == 1:
-                text = msg.message.replace("vat# ", "").\
-                    replace("return STDOUT ", "").replace("\n\n", "\n").\
-                    replace('\n', ' |br| ').\
-                    replace('\r', '').replace('"', "'")
-                try:
-                    self._data["tests"][self._test_ID]["show-run"] += " |br| "
-                    self._data["tests"][self._test_ID]["show-run"] += \
-                        "**DUT" + str(self._lookup_kw_nr) + ":** |br| " + text
-                except KeyError:
-                    pass
+            text = re.sub("\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3} "
+                          "PAPI command history:", "", msg.message, count=1). \
+                replace("\n\n", "\n").replace('\n', ' |br| ').\
+                replace('\r', '').replace('"', "'")
 
 
-    # TODO: Remove when definitely no NDRPDRDISC tests are used:
-    def _get_latency(self, msg, test_type):
-        """Get the latency data from the test message.
+            self._data["tests"][self._test_ID]["conf-history"] += " |br| "
+            self._data["tests"][self._test_ID]["conf-history"] += \
+                "**DUT" + str(self._conf_history_lookup_nr) + ":** " + text
 
 
-        :param msg: Message to be parsed.
-        :param test_type: Type of the test - NDR or PDR.
-        :type msg: str
-        :type test_type: str
-        :returns: Latencies parsed from the message.
-        :rtype: dict
-        """
-
-        if test_type == "NDR":
-            groups = re.search(self.REGEX_LAT_NDR, msg)
-            groups_range = range(1, 7)
-        elif test_type == "PDR":
-            groups = re.search(self.REGEX_LAT_PDR, msg)
-            groups_range = range(1, 3)
-        else:
-            return {}
+    def _get_show_run(self, msg):
+        """Called when extraction of VPP operational data (output of CLI command
+        Show Runtime) is required.
 
 
-        latencies = list()
-        for idx in groups_range:
+        :param msg: Message to process.
+        :type msg: Message
+        :returns: Nothing.
+        """
+        if not "show-run" in self._data["tests"][self._test_ID].keys():
+            self._data["tests"][self._test_ID]["show-run"] = str()
+
+        if msg.message.count("stats runtime"):
+            host = str(re.search(self.REGEX_TC_PAPI_CLI, msg.message).\
+                       group(1))
+            socket = str(re.search(self.REGEX_TC_PAPI_CLI, msg.message).\
+                         group(2))
+            message = str(msg.message).replace(' ', '').replace('\n', '').\
+                replace("'", '"').replace('b"', '"').replace('u"', '"').\
+                split(":",1)[1]
+            runtime = loads(message)
             try:
             try:
-                lat = [int(item) for item in str(groups.group(idx)).split('/')]
-            except (AttributeError, ValueError):
-                lat = [-1, -1, -1]
-            latencies.append(lat)
-
-        keys = ("min", "avg", "max")
-        latency = {
-            "direction1": {
-            },
-            "direction2": {
-            }
-        }
-
-        latency["direction1"]["100"] = dict(zip(keys, latencies[0]))
-        latency["direction2"]["100"] = dict(zip(keys, latencies[1]))
-        if test_type == "NDR":
-            latency["direction1"]["50"] = dict(zip(keys, latencies[2]))
-            latency["direction2"]["50"] = dict(zip(keys, latencies[3]))
-            latency["direction1"]["10"] = dict(zip(keys, latencies[4]))
-            latency["direction2"]["10"] = dict(zip(keys, latencies[5]))
-
-        return latency
+                threads_nr = len(runtime[0]["clocks"])
+            except (IndexError, KeyError):
+                return
+            tbl_hdr = ["Name", "Calls", "Vectors", "Suspends", "Clocks",
+                       "Vectors/Calls"]
+            table = [[tbl_hdr, ] for _ in range(threads_nr)]
+            for item in runtime:
+                for idx in range(threads_nr):
+                    name = format(item["name"])
+                    calls = format(item["calls"][idx])
+                    vectors = format(item["vectors"][idx])
+                    suspends = format(item["suspends"][idx])
+                    if item["vectors"][idx] > 0:
+                        clocks = format(
+                            item["clocks"][idx]/item["vectors"][idx], ".2e")
+                    elif item["calls"][idx] > 0:
+                        clocks = format(
+                            item["clocks"][idx]/item["calls"][idx], ".2e")
+                    elif item["suspends"][idx] > 0:
+                        clocks = format(
+                            item["clocks"][idx]/item["suspends"][idx], ".2e")
+                    else:
+                        clocks = 0
+                    if item["calls"][idx] > 0:
+                        vectors_call = format(
+                            item["vectors"][idx]/item["calls"][idx], ".2f")
+                    else:
+                        vectors_call = format(0, ".2f")
+                    if int(calls) + int(vectors) + int(suspends):
+                        table[idx].append([
+                            name, calls, vectors, suspends, clocks, vectors_call
+                        ])
+            text = ""
+            for idx in range(threads_nr):
+                text += "Thread {idx} ".format(idx=idx)
+                text += "vpp_main\n" if idx == 0 else \
+                    "vpp_wk_{idx}\n".format(idx=idx-1)
+                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["Name"] = "l"
+                txt_table.align["Calls"] = "r"
+                txt_table.align["Vectors"] = "r"
+                txt_table.align["Suspends"] = "r"
+                txt_table.align["Clocks"] = "r"
+                txt_table.align["Vectors/Calls"] = "r"
+
+                text += txt_table.get_string(sortby="Name") + '\n'
+            text = (" \n **DUT: {host}/{socket}** \n {text}".
+                    format(host=host, socket=socket, text=text))
+            text = text.replace('\n', ' |br| ').replace('\r', '').\
+                replace('"', "'")
+            self._data["tests"][self._test_ID]["show-run"] += text
 
     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
@@ -531,6 +539,33 @@ class ExecutionChecker(ResultVisitor):
 
         return throughput, status
 
 
         return throughput, status
 
+    def _get_plr_throughput(self, msg):
+        """Get PLRsearch lower bound and PLRsearch upper bound from the test
+        message.
+
+        :param msg: The test message to be parsed.
+        :type msg: str
+        :returns: Parsed data as a dict and the status (PASS/FAIL).
+        :rtype: tuple(dict, str)
+        """
+
+        throughput = {
+            "LOWER": -1.0,
+            "UPPER": -1.0
+        }
+        status = "FAIL"
+        groups = re.search(self.REGEX_PLR_RATE, msg)
+
+        if groups is not None:
+            try:
+                throughput["LOWER"] = float(groups.group(1))
+                throughput["UPPER"] = float(groups.group(2))
+                status = "PASS"
+            except (IndexError, ValueError):
+                pass
+
+        return throughput, status
+
     def _get_ndrpdr_latency(self, msg):
         """Get LATENCY from the test message.
 
     def _get_ndrpdr_latency(self, msg):
         """Get LATENCY from the test message.
 
@@ -539,31 +574,52 @@ class ExecutionChecker(ResultVisitor):
         :returns: Parsed data as a dict and the status (PASS/FAIL).
         :rtype: tuple(dict, str)
         """
         :returns: Parsed data as a dict and the status (PASS/FAIL).
         :rtype: tuple(dict, str)
         """
-
+        latency_default = {"min": -1.0, "avg": -1.0, "max": -1.0, "hdrh": ""}
         latency = {
             "NDR": {
         latency = {
             "NDR": {
-                "direction1": {"min": -1.0, "avg": -1.0, "max": -1.0},
-                "direction2": {"min": -1.0, "avg": -1.0, "max": -1.0}
+                "direction1": copy.copy(latency_default),
+                "direction2": copy.copy(latency_default)
             },
             "PDR": {
             },
             "PDR": {
-                "direction1": {"min": -1.0, "avg": -1.0, "max": -1.0},
-                "direction2": {"min": -1.0, "avg": -1.0, "max": -1.0}
+                "direction1": copy.copy(latency_default),
+                "direction2": copy.copy(latency_default)
             }
         }
         status = "FAIL"
         groups = re.search(self.REGEX_NDRPDR_LAT, msg)
 
             }
         }
         status = "FAIL"
         groups = re.search(self.REGEX_NDRPDR_LAT, msg)
 
+        def process_latency(in_str):
+            """Return object with parsed latency values.
+
+            TODO: Define class for the return type.
+
+            :param in_str: Input string, min/avg/max/hdrh format.
+            :type in_str: str
+            :returns: Dict with corresponding keys, except hdrh float values.
+            :rtype dict:
+            :throws IndexError: If in_str does not have enough substrings.
+            :throws ValueError: If a substring does not convert to float.
+            """
+            in_list = in_str.split('/')
+
+            rval = {
+                "min": float(in_list[0]),
+                "avg": float(in_list[1]),
+                "max": float(in_list[2]),
+                "hdrh": ""
+            }
+
+            if len(in_list) == 4:
+                rval["hdrh"] = str(in_list[3])
+
+            return rval
+
         if groups is not None:
         if groups is not None:
-            keys = ("min", "avg", "max")
             try:
             try:
-                latency["NDR"]["direction1"] = dict(
-                    zip(keys, [float(l) for l in groups.group(1).split('/')]))
-                latency["NDR"]["direction2"] = dict(
-                    zip(keys, [float(l) for l in groups.group(2).split('/')]))
-                latency["PDR"]["direction1"] = dict(
-                    zip(keys, [float(l) for l in groups.group(3).split('/')]))
-                latency["PDR"]["direction2"] = dict(
-                    zip(keys, [float(l) for l in groups.group(4).split('/')]))
+                latency["NDR"]["direction1"] = process_latency(groups.group(1))
+                latency["NDR"]["direction2"] = process_latency(groups.group(2))
+                latency["PDR"]["direction1"] = process_latency(groups.group(3))
+                latency["PDR"]["direction2"] = process_latency(groups.group(4))
                 status = "PASS"
             except (IndexError, ValueError):
                 pass
                 status = "PASS"
             except (IndexError, ValueError):
                 pass
@@ -678,6 +734,7 @@ class ExecutionChecker(ResultVisitor):
             groups = re.search(self.REGEX_TC_NAME_OLD, self._test_ID)
             if not groups:
                 tag_count = 0
             groups = re.search(self.REGEX_TC_NAME_OLD, self._test_ID)
             if not groups:
                 tag_count = 0
+                tag_tc = str()
                 for tag in test_result["tags"]:
                     groups = re.search(self.REGEX_TC_TAG, tag)
                     if groups:
                 for tag in test_result["tags"]:
                     groups = re.search(self.REGEX_TC_TAG, tag)
                     if groups:
@@ -696,16 +753,18 @@ class ExecutionChecker(ResultVisitor):
                 else:
                     test_result["status"] = "FAIL"
                     self._data["tests"][self._test_ID] = test_result
                 else:
                     test_result["status"] = "FAIL"
                     self._data["tests"][self._test_ID] = test_result
-                    logging.error("The test '{0}' has no or more than one "
+                    logging.debug("The test '{0}' has no or more than one "
                                   "multi-threading tags.".format(self._test_ID))
                                   "multi-threading tags.".format(self._test_ID))
-                    logging.error("Tags: {0}".format(test_result["tags"]))
+                    logging.debug("Tags: {0}".format(test_result["tags"]))
                     return
 
         if test.status == "PASS" and ("NDRPDRDISC" in tags or
                                       "NDRPDR" in tags or
                     return
 
         if test.status == "PASS" and ("NDRPDRDISC" in tags or
                                       "NDRPDR" in tags or
+                                      "SOAK" in tags or
                                       "TCP" in tags or
                                       "MRR" in tags or
                                       "TCP" in tags or
                                       "MRR" in tags or
-                                      "BMRR" in tags):
+                                      "BMRR" in tags or
+                                      "RECONF" in tags):
             # TODO: Remove when definitely no NDRPDRDISC tests are used:
             if "NDRDISC" in tags:
                 test_result["type"] = "NDR"
             # TODO: Remove when definitely no NDRPDRDISC tests are used:
             if "NDRDISC" in tags:
                 test_result["type"] = "NDR"
@@ -714,12 +773,16 @@ class ExecutionChecker(ResultVisitor):
                 test_result["type"] = "PDR"
             elif "NDRPDR" in tags:
                 test_result["type"] = "NDRPDR"
                 test_result["type"] = "PDR"
             elif "NDRPDR" in tags:
                 test_result["type"] = "NDRPDR"
+            elif "SOAK" in tags:
+                test_result["type"] = "SOAK"
             elif "TCP" in tags:
                 test_result["type"] = "TCP"
             elif "MRR" in tags:
                 test_result["type"] = "MRR"
             elif "FRMOBL" in tags or "BMRR" in tags:
                 test_result["type"] = "BMRR"
             elif "TCP" in tags:
                 test_result["type"] = "TCP"
             elif "MRR" in tags:
                 test_result["type"] = "MRR"
             elif "FRMOBL" in tags or "BMRR" in tags:
                 test_result["type"] = "BMRR"
+            elif "RECONF" in tags:
+                test_result["type"] = "RECONF"
             else:
                 test_result["status"] = "FAIL"
                 self._data["tests"][self._test_ID] = test_result
             else:
                 test_result["status"] = "FAIL"
                 self._data["tests"][self._test_ID] = test_result
@@ -754,6 +817,10 @@ class ExecutionChecker(ResultVisitor):
                 test_result["latency"], test_result["status"] = \
                     self._get_ndrpdr_latency(test.message)
 
                 test_result["latency"], test_result["status"] = \
                     self._get_ndrpdr_latency(test.message)
 
+            elif test_result["type"] in ("SOAK", ):
+                test_result["throughput"], test_result["status"] = \
+                    self._get_plr_throughput(test.message)
+
             elif test_result["type"] in ("TCP", ):
                 groups = re.search(self.REGEX_TCP, test.message)
                 test_result["result"] = int(groups.group(2))
             elif test_result["type"] in ("TCP", ):
                 groups = re.search(self.REGEX_TCP, test.message)
                 test_result["result"] = int(groups.group(2))
@@ -777,6 +844,18 @@ class ExecutionChecker(ResultVisitor):
                         AvgStdevMetadataFactory.from_data([
                             float(groups.group(3)) / float(groups.group(1)), ])
 
                         AvgStdevMetadataFactory.from_data([
                             float(groups.group(3)) / float(groups.group(1)), ])
 
+            elif test_result["type"] == "RECONF":
+                test_result["result"] = None
+                try:
+                    grps_loss = re.search(self.REGEX_RECONF_LOSS, test.message)
+                    grps_time = re.search(self.REGEX_RECONF_TIME, test.message)
+                    test_result["result"] = {
+                        "loss": int(grps_loss.group(1)),
+                        "time": float(grps_time.group(1))
+                    }
+                except (AttributeError, IndexError, ValueError, TypeError):
+                    test_result["status"] = "FAIL"
+
         self._data["tests"][self._test_ID] = test_result
 
     def end_test(self, test):
         self._data["tests"][self._test_ID] = test_result
 
     def end_test(self, test):
@@ -851,7 +930,7 @@ class ExecutionChecker(ResultVisitor):
             self._lookup_kw_nr += 1
             self._show_run_lookup_nr = 0
             self._msg_type = "test-show-runtime"
             self._lookup_kw_nr += 1
             self._show_run_lookup_nr = 0
             self._msg_type = "test-show-runtime"
-        elif test_kw.name.count("Start The L2fwd Test") and not self._version:
+        elif test_kw.name.count("Install Dpdk Test") and not self._version:
             self._msg_type = "dpdk-version"
         else:
             return
             self._msg_type = "dpdk-version"
         else:
             return
@@ -890,10 +969,11 @@ class ExecutionChecker(ResultVisitor):
         if setup_kw.name.count("Show Vpp Version On All Duts") \
                 and not self._version:
             self._msg_type = "vpp-version"
         if setup_kw.name.count("Show Vpp Version On All Duts") \
                 and not self._version:
             self._msg_type = "vpp-version"
-
-        elif setup_kw.name.count("Setup performance global Variables") \
+        elif setup_kw.name.count("Set Global Variable") \
                 and not self._timestamp:
             self._msg_type = "timestamp"
                 and not self._timestamp:
             self._msg_type = "timestamp"
+        elif setup_kw.name.count("Setup Framework") and not self._testbed:
+            self._msg_type = "testbed"
         else:
             return
         setup_kw.messages.visit(self)
         else:
             return
         setup_kw.messages.visit(self)
@@ -930,9 +1010,13 @@ class ExecutionChecker(ResultVisitor):
         """
 
         if teardown_kw.name.count("Show Vat History On All Duts"):
         """
 
         if teardown_kw.name.count("Show Vat History On All Duts"):
-            self._vat_history_lookup_nr = 0
+            self._conf_history_lookup_nr = 0
             self._msg_type = "teardown-vat-history"
             teardown_kw.messages.visit(self)
             self._msg_type = "teardown-vat-history"
             teardown_kw.messages.visit(self)
+        elif teardown_kw.name.count("Show Papi History On All Duts"):
+            self._conf_history_lookup_nr = 0
+            self._msg_type = "teardown-papi-history"
+            teardown_kw.messages.visit(self)
 
     def end_teardown_kw(self, teardown_kw):
         """Called when keyword ends. Default implementation does nothing.
 
     def end_teardown_kw(self, teardown_kw):
         """Called when keyword ends. Default implementation does nothing.
@@ -975,7 +1059,7 @@ class ExecutionChecker(ResultVisitor):
         pass
 
 
         pass
 
 
-class InputData(object):
+class InputData:
     """Input data
 
     The data is extracted from output.xml files generated by Jenkins jobs and
     """Input data
 
     The data is extracted from output.xml files generated by Jenkins jobs and
@@ -1085,13 +1169,10 @@ class InputData(object):
 
         return checker.data
 
 
         return checker.data
 
-    def _download_and_parse_build(self, pid, data_queue, job, build, repeat):
+    def _download_and_parse_build(self, job, build, repeat, pid=10000):
         """Download and parse the input data file.
 
         :param pid: PID of the process executing this method.
         """Download and parse the input data file.
 
         :param pid: PID of the process executing this method.
-        :param data_queue: Shared memory between processes. Queue which keeps
-            the result data. This data is then read by the main process and used
-            in further processing.
         :param job: Name of the Jenkins job which generated the processed input
             file.
         :param build: Information about the Jenkins build which generated the
         :param job: Name of the Jenkins job which generated the processed input
             file.
         :param build: Information about the Jenkins build which generated the
@@ -1099,7 +1180,6 @@ class InputData(object):
         :param repeat: Repeat the download specified number of times if not
             successful.
         :type pid: int
         :param repeat: Repeat the download specified number of times if not
             successful.
         :type pid: int
-        :type data_queue: multiprocessing.Manager().Queue()
         :type job: str
         :type build: dict
         :type repeat: int
         :type job: str
         :type build: dict
         :type repeat: int
@@ -1107,9 +1187,6 @@ class InputData(object):
 
         logs = list()
 
 
         logs = list()
 
-        logging.info("  Processing the job/build: {0}: {1}".
-                     format(job, build["build"]))
-
         logs.append(("INFO", "  Processing the job/build: {0}: {1}".
                      format(job, build["build"])))
 
         logs.append(("INFO", "  Processing the job/build: {0}: {1}".
                      format(job, build["build"])))
 
@@ -1129,7 +1206,7 @@ class InputData(object):
                                   "'{build}', or it is damaged. Skipped.".
                          format(job=job, build=build["build"])))
         if success:
                                   "'{build}', or it is damaged. Skipped.".
                          format(job=job, build=build["build"])))
         if success:
-            logs.append(("INFO", "  Processing data from the build '{0}' ...".
+            logs.append(("INFO", "    Processing data from the build '{0}' ...".
                          format(build["build"])))
             data = self._parse_tests(job, build, logs)
             if data is None:
                          format(build["build"])))
             data = self._parse_tests(job, build, logs)
             if data is None:
@@ -1143,17 +1220,42 @@ class InputData(object):
                 remove(build["file-name"])
             except OSError as err:
                 logs.append(("ERROR", "Cannot remove the file '{0}': {1}".
                 remove(build["file-name"])
             except OSError as err:
                 logs.append(("ERROR", "Cannot remove the file '{0}': {1}".
-                             format(build["file-name"], err)))
+                             format(build["file-name"], repr(err))))
+
+        # If the time-period is defined in the specification file, remove all
+        # files which are outside the time period.
+        timeperiod = self._cfg.input.get("time-period", None)
+        if timeperiod and data:
+            now = dt.utcnow()
+            timeperiod = timedelta(int(timeperiod))
+            metadata = data.get("metadata", None)
+            if metadata:
+                generated = metadata.get("generated", None)
+                if generated:
+                    generated = dt.strptime(generated, "%Y%m%d %H:%M")
+                    if (now - generated) > timeperiod:
+                        # Remove the data and the file:
+                        state = "removed"
+                        data = None
+                        logs.append(
+                            ("INFO",
+                             "    The build {job}/{build} is outdated, will be "
+                             "removed".format(job=job, build=build["build"])))
         logs.append(("INFO", "  Done."))
 
         logs.append(("INFO", "  Done."))
 
-        result = {
-            "data": data,
-            "state": state,
-            "job": job,
-            "build": build,
-            "logs": logs
-        }
-        data_queue.put(result)
+        for level, line in logs:
+            if level == "INFO":
+                logging.info(line)
+            elif level == "ERROR":
+                logging.error(line)
+            elif level == "DEBUG":
+                logging.debug(line)
+            elif level == "CRITICAL":
+                logging.critical(line)
+            elif level == "WARNING":
+                logging.warning(line)
+
+        return {"data": data, "state": state, "job": job, "build": build}
 
     def download_and_parse_data(self, repeat=1):
         """Download the input data files, parse input data from input files and
 
     def download_and_parse_data(self, repeat=1):
         """Download the input data files, parse input data from input files and
@@ -1166,73 +1268,34 @@ class InputData(object):
 
         logging.info("Downloading and parsing input files ...")
 
 
         logging.info("Downloading and parsing input files ...")
 
-        work_queue = multiprocessing.JoinableQueue()
-        manager = multiprocessing.Manager()
-        data_queue = manager.Queue()
-        cpus = multiprocessing.cpu_count()
-
-        workers = list()
-        for cpu in range(cpus):
-            worker = Worker(work_queue,
-                            data_queue,
-                            self._download_and_parse_build)
-            worker.daemon = True
-            worker.start()
-            workers.append(worker)
-            os.system("taskset -p -c {0} {1} > /dev/null 2>&1".
-                      format(cpu, worker.pid))
-
         for job, builds in self._cfg.builds.items():
             for build in builds:
         for job, builds in self._cfg.builds.items():
             for build in builds:
-                work_queue.put((job, build, repeat))
 
 
-        work_queue.join()
+                result = self._download_and_parse_build(job, build, repeat)
+                build_nr = result["build"]["build"]
 
 
-        logging.info("Done.")
+                if result["data"]:
+                    data = result["data"]
+                    build_data = pd.Series({
+                        "metadata": pd.Series(
+                            data["metadata"].values(),
+                            index=data["metadata"].keys()),
+                        "suites": pd.Series(data["suites"].values(),
+                                            index=data["suites"].keys()),
+                        "tests": pd.Series(data["tests"].values(),
+                                           index=data["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_file_name(
+                        job, build_nr, result["build"]["file-name"])
 
 
-        while not data_queue.empty():
-            result = data_queue.get()
-
-            job = result["job"]
-            build_nr = result["build"]["build"]
-
-            if result["data"]:
-                data = result["data"]
-                build_data = pd.Series({
-                    "metadata": pd.Series(data["metadata"].values(),
-                                          index=data["metadata"].keys()),
-                    "suites": pd.Series(data["suites"].values(),
-                                        index=data["suites"].keys()),
-                    "tests": pd.Series(data["tests"].values(),
-                                       index=data["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_file_name(job, build_nr,
-                                              result["build"]["file-name"])
-
-            self._cfg.set_input_state(job, build_nr, result["state"])
-
-            for item in result["logs"]:
-                if item[0] == "INFO":
-                    logging.info(item[1])
-                elif item[0] == "ERROR":
-                    logging.error(item[1])
-                elif item[0] == "DEBUG":
-                    logging.debug(item[1])
-                elif item[0] == "CRITICAL":
-                    logging.critical(item[1])
-                elif item[0] == "WARNING":
-                    logging.warning(item[1])
-
-        del data_queue
-
-        # Terminate all workers
-        for worker in workers:
-            worker.terminate()
-            worker.join()
+                self._cfg.set_input_state(job, build_nr, result["state"])
+
+                logging.info("Memory allocation: {0:,d}MB".format(
+                    resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000))
 
         logging.info("Done.")
 
 
         logging.info("Done.")
 
@@ -1274,7 +1337,7 @@ class InputData(object):
             index += 1
             tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
 
             index += 1
             tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
 
-    def filter_data(self, element, params=None, data_set="tests",
+    def filter_data(self, element, params=None, data=None, data_set="tests",
                     continue_on_error=False):
         """Filter required data from the given jobs and builds.
 
                     continue_on_error=False):
         """Filter required data from the given jobs and builds.
 
@@ -1297,13 +1360,16 @@ class InputData(object):
 
         :param element: Element which will use the filtered data.
         :param params: Parameters which will be included in the output. If None,
 
         :param element: Element which will use the filtered data.
         :param params: Parameters which will be included in the output. If None,
-        all parameters are included.
+            all parameters are included.
+        :param data: If not None, this data is used instead of data specified
+            in the element.
         :param data_set: The set of data to be filtered: tests, suites,
         :param data_set: The set of data to be filtered: tests, suites,
-        metadata.
+            metadata.
         :param continue_on_error: Continue if there is error while reading the
         :param continue_on_error: Continue if there is error while reading the
-        data. The Item will be empty then
+            data. The Item will be empty then
         :type element: pandas.Series
         :type params: list
         :type element: pandas.Series
         :type params: list
+        :type data: dict
         :type data_set: str
         :type continue_on_error: bool
         :returns: Filtered data.
         :type data_set: str
         :type continue_on_error: bool
         :returns: Filtered data.
@@ -1325,9 +1391,10 @@ class InputData(object):
             if params:
                 params.append("type")
 
             if params:
                 params.append("type")
 
+        data_to_filter = data if data else element["data"]
         data = pd.Series()
         try:
         data = pd.Series()
         try:
-            for job, builds in element["data"].items():
+            for job, builds in data_to_filter.items():
                 data[job] = pd.Series()
                 for build in builds:
                     data[job][str(build)] = pd.Series()
                 data[job] = pd.Series()
                 for build in builds:
                     data[job][str(build)] = pd.Series()
@@ -1366,6 +1433,96 @@ class InputData(object):
                           "tags are enclosed by apostrophes.".format(cond))
             return None
 
                           "tags are enclosed by apostrophes.".format(cond))
             return None
 
+    def filter_tests_by_name(self, element, params=None, data_set="tests",
+                             continue_on_error=False):
+        """Filter required data from the given jobs and builds.
+
+        The output data structure is:
+
+        - job 1
+          - build 1
+            - test (or suite) 1 ID:
+              - param 1
+              - param 2
+              ...
+              - param n
+            ...
+            - test (or suite) n ID:
+            ...
+          ...
+          - build n
+        ...
+        - job n
+
+        :param element: Element which will use the filtered data.
+        :param params: Parameters which will be included in the output. If None,
+        all parameters are included.
+        :param data_set: The set of data to be filtered: tests, suites,
+        metadata.
+        :param continue_on_error: Continue if there is error while reading the
+        data. The Item will be empty then
+        :type element: pandas.Series
+        :type params: list
+        :type data_set: str
+        :type continue_on_error: bool
+        :returns: Filtered data.
+        :rtype pandas.Series
+        """
+
+        include = element.get("include", None)
+        if not include:
+            logging.warning("No tests to include, skipping the element.")
+            return None
+
+        if params is None:
+            params = element.get("parameters", None)
+            if params:
+                params.append("type")
+
+        data = pd.Series()
+        try:
+            for job, builds in element["data"].items():
+                data[job] = pd.Series()
+                for build in builds:
+                    data[job][str(build)] = pd.Series()
+                    for test in include:
+                        try:
+                            reg_ex = re.compile(str(test).lower())
+                            for test_ID in self.data[job][str(build)]\
+                                    [data_set].keys():
+                                if re.match(reg_ex, str(test_ID).lower()):
+                                    test_data = self.data[job][str(build)]\
+                                        [data_set][test_ID]
+                                    data[job][str(build)][test_ID] = pd.Series()
+                                    if params is None:
+                                        for param, val in test_data.items():
+                                            data[job][str(build)][test_ID]\
+                                                [param] = val
+                                    else:
+                                        for param in params:
+                                            try:
+                                                data[job][str(build)][test_ID]\
+                                                    [param] = test_data[param]
+                                            except KeyError:
+                                                data[job][str(build)][test_ID]\
+                                                    [param] = "No Data"
+                        except KeyError as err:
+                            logging.error("{err!r}".format(err=err))
+                            if continue_on_error:
+                                continue
+                            else:
+                                return None
+            return data
+
+        except (KeyError, IndexError, ValueError) as err:
+            logging.error("Missing mandatory parameter in the element "
+                          "specification: {err!r}".format(err=err))
+            return None
+        except AttributeError as err:
+            logging.error("{err!r}".format(err=err))
+            return None
+
+
     @staticmethod
     def merge_data(data):
         """Merge data from more jobs and builds to a simple data structure.
     @staticmethod
     def merge_data(data):
         """Merge data from more jobs and builds to a simple data structure.