Trending: Fix Alerts
[csit.git] / resources / tools / presentation / input_data_parser.py
index db63660..d23fa84 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:
 - extract data from output.xml files generated by Jenkins jobs and store in
   pandas' Series,
 - provide access to the data.
+- filter the data using tags,
 """
 
 import re
-import pandas as pd
+import copy
+import resource
 import logging
-import xml.etree.ElementTree as ET
+
+from collections import OrderedDict
+from os import remove
+from datetime import datetime as dt
+from datetime import timedelta
+from json import loads
+
+import prettytable
+import pandas as pd
 
 from robot.api import ExecutionResult, ResultVisitor
 from robot import errors
-from collections import OrderedDict
-from string import replace
+
+from resources.libraries.python import jumpavg
+from input_data_files import download_and_unzip_data_file
+
+
+# Separator used in file names
+SEPARATOR = u"__"
 
 
 class ExecutionChecker(ResultVisitor):
@@ -37,84 +52,116 @@ class ExecutionChecker(ResultVisitor):
     Performance tests:
 
     {
-        "metadata": {  # Optional
-            "version": "VPP version",
+        "metadata": {
+            "generated": "Timestamp",
+            "version": "SUT version",
             "job": "Jenkins job name",
             "build": "Information about the build"
         },
         "suites": {
-            "Suite name 1": {
+            "Suite long name 1": {
+                "name": Suite name,
                 "doc": "Suite 1 documentation",
                 "parent": "Suite 1 parent",
                 "level": "Level of the suite in the suite hierarchy"
             }
-            "Suite name N": {
+            "Suite long name N": {
+                "name": Suite name,
                 "doc": "Suite N documentation",
                 "parent": "Suite 2 parent",
                 "level": "Level of the suite in the suite hierarchy"
             }
         }
         "tests": {
+            # NDRPDR tests:
             "ID": {
                 "name": "Test name",
                 "parent": "Name of the parent of the test",
-                "doc": "Test documentation"
-                "msg": "Test message"
+                "doc": "Test documentation",
+                "msg": "Test message",
+                "conf-history": "DUT1 and DUT2 VAT History",
+                "show-run": "Show Run",
                 "tags": ["tag 1", "tag 2", "tag n"],
-                "type": "PDR" | "NDR",
+                "type": "NDRPDR",
+                "status": "PASS" | "FAIL",
                 "throughput": {
-                    "value": int,
-                    "unit": "pps" | "bps" | "percentage"
+                    "NDR": {
+                        "LOWER": float,
+                        "UPPER": float
+                    },
+                    "PDR": {
+                        "LOWER": float,
+                        "UPPER": float
+                    }
                 },
                 "latency": {
-                    "direction1": {
-                        "100": {
-                            "min": int,
-                            "avg": int,
-                            "max": int
+                    "NDR": {
+                        "direction1": {
+                            "min": float,
+                            "avg": float,
+                            "max": float,
+                            "hdrh": str
                         },
-                        "50": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        },
-                        "10": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
+                        "direction2": {
+                            "min": float,
+                            "avg": float,
+                            "max": float,
+                            "hdrh": str
                         }
                     },
-                    "direction2": {
-                        "100": {
-                            "min": int,
-                            "avg": int,
-                            "max": int
-                        },
-                        "50": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
+                    "PDR": {
+                        "direction1": {
+                            "min": float,
+                            "avg": float,
+                            "max": float,
+                            "hdrh": str
                         },
-                        "10": {  # Only for NDR
-                            "min": int,
-                            "avg": int,
-                            "max": int
+                        "direction2": {
+                            "min": float,
+                            "avg": float,
+                            "max": float,
+                            "hdrh": str
                         }
                     }
-                },
-                "lossTolerance": "lossTolerance",  # Only for PDR
-                "vat-history": "DUT1 and DUT2 VAT History"
-                },
-                "show-run": "Show Run"
-            },
+                }
+            }
+
+            # TCP 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": "TCP",
+                "status": "PASS" | "FAIL",
+                "result": int
+            }
+
+            # MRR, BMRR 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": "MRR" | "BMRR",
+                "status": "PASS" | "FAIL",
+                "result": {
+                    "receive-rate": float,
+                    # Average of a list, computed using AvgStdevStats.
+                    # In CSIT-1180, replace with List[float].
+                }
+            }
+
             "ID" {
                 # next test
             }
         }
     }
 
-    Functional tests:
 
+    Functional tests:
 
     {
         "metadata": {  # Optional
@@ -141,7 +188,7 @@ class ExecutionChecker(ResultVisitor):
                 "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"
             },
@@ -154,36 +201,57 @@ class ExecutionChecker(ResultVisitor):
     .. note:: ID is the lowercase full path to the test.
     """
 
-    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_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_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_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_VERSION = 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_TCP = re.compile(r'Total\s(rps|cps|throughput):\s([0-9]*).*$')
+    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*)')
 
-    def __init__(self, **metadata):
+    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_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.
 
         :param metadata: Key-value pairs to be included in "metadata" part of
-        JSON structure.
+            JSON structure.
+        :param mapping: Mapping of the old names of test cases to the new
+            (actual) one.
+        :param ignore: List of TCs to be ignored.
         :type metadata: dict
+        :type mapping: dict
+        :type ignore: list
         """
 
         # Type of message to parse out from the test messages
@@ -192,12 +260,24 @@ class ExecutionChecker(ResultVisitor):
         # VPP version
         self._version = None
 
-        # Number of VAT History messages found:
+        # 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
+
+        # Ignore list
+        self._ignore = ignore
+
+        # Number of PAPI History messages found:
         # 0 - no message
-        # 1 - VAT History of DUT1
-        # 2 - VAT History of DUT2
+        # 1 - PAPI History of DUT1
+        # 2 - PAPI 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
@@ -206,25 +286,30 @@ class ExecutionChecker(ResultVisitor):
 
         # Test ID of currently processed test- the lowercase full path to the
         # test
-        self._test_ID = None
+        self._test_id = None
 
         # The main data structure
         self._data = {
-            "metadata": OrderedDict(),
-            "suites": OrderedDict(),
-            "tests": OrderedDict()
+            u"metadata": OrderedDict(),
+            u"suites": OrderedDict(),
+            u"tests": OrderedDict()
         }
 
         # Save the provided metadata
         for key, val in metadata.items():
-            self._data["metadata"][key] = val
+            self._data[u"metadata"][key] = val
 
         # Dictionary defining the methods used to parse different types of
         # messages
         self.parse_msg = {
-            "setup-version": self._get_version,
-            "teardown-vat-history": self._get_vat_history,
-            "test-show-runtime": self._get_show_run
+            u"timestamp": self._get_timestamp,
+            u"vpp-version": self._get_vpp_version,
+            u"dpdk-version": self._get_dpdk_version,
+            # TODO: Remove when not needed:
+            u"teardown-vat-history": self._get_vat_history,
+            u"teardown-papi-history": self._get_papi_history,
+            u"test-show-runtime": self._get_show_run,
+            u"testbed": self._get_testbed
         }
 
     @property
@@ -236,7 +321,28 @@ class ExecutionChecker(ResultVisitor):
         """
         return self._data
 
-    def _get_version(self, msg):
+    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(u"Setup of TG node") or \
+                msg.message.count(u"Setup of node TG host"):
+            reg_tg_ip = re.compile(
+                r'.*TG .* (\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}).*')
+            try:
+                self._testbed = str(re.search(reg_tg_ip, msg.message).group(1))
+            except (KeyError, ValueError, IndexError, AttributeError):
+                pass
+            finally:
+                self._data[u"metadata"][u"testbed"] = self._testbed
+                self._msg_type = None
+
+    def _get_vpp_version(self, msg):
         """Called when extraction of VPP version is required.
 
         :param msg: Message to process.
@@ -244,35 +350,89 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
 
-        if msg.message.count("return STDOUT Version:"):
-            self._version = str(re.search(self.REGEX_VERSION, msg.message).
+        if msg.message.count(u"return STDOUT Version:") or \
+            msg.message.count(u"VPP Version:") or \
+            msg.message.count(u"VPP version:"):
+            self._version = str(re.search(self.REGEX_VERSION_VPP, msg.message).
                                 group(2))
-            self._data["metadata"]["version"] = self._version
+            self._data[u"metadata"][u"version"] = self._version
             self._msg_type = None
 
-            logging.debug("    VPP version: {0}".format(self._version))
+    def _get_dpdk_version(self, msg):
+        """Called when extraction of DPDK version is required.
+
+        :param msg: Message to process.
+        :type msg: Message
+        :returns: Nothing.
+        """
+
+        if msg.message.count(u"DPDK Version:"):
+            try:
+                self._version = str(re.search(
+                    self.REGEX_VERSION_DPDK, msg.message).group(2))
+                self._data[u"metadata"][u"version"] = self._version
+            except IndexError:
+                pass
+            finally:
+                self._msg_type = None
+
+    def _get_timestamp(self, msg):
+        """Called when extraction of timestamp is required.
+
+        :param msg: Message to process.
+        :type msg: Message
+        :returns: Nothing.
+        """
+
+        self._timestamp = msg.timestamp[:14]
+        self._data[u"metadata"][u"generated"] = self._timestamp
+        self._msg_type = None
 
     def _get_vat_history(self, msg):
         """Called when extraction of VAT command history is required.
 
+        TODO: Remove when not needed.
+
         :param msg: Message to process.
         :type msg: Message
         :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()
+        if msg.message.count(u"VAT command history:"):
+            self._conf_history_lookup_nr += 1
+            if self._conf_history_lookup_nr == 1:
+                self._data[u"tests"][self._test_id][u"conf-history"] = str()
             else:
                 self._msg_type = None
-            text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
-                          "VAT command history:", "", msg.message, count=1). \
-                replace("\n\n", "\n").replace('\n', ' |br| ').\
-                replace('\r', '').replace('"', "'")
+            text = re.sub(r"\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3} "
+                          r"VAT command history:", u"",
+                          msg.message, count=1).replace(u'\n', u' |br| ').\
+                replace(u'"', u"'")
 
-            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[u"tests"][self._test_id][u"conf-history"] += (
+                f" |br| **DUT{str(self._conf_history_lookup_nr)}:** {text}"
+            )
+
+    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.
+        """
+        if msg.message.count(u"PAPI command history:"):
+            self._conf_history_lookup_nr += 1
+            if self._conf_history_lookup_nr == 1:
+                self._data[u"tests"][self._test_id][u"conf-history"] = str()
+            else:
+                self._msg_type = None
+            text = re.sub(r"\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3} "
+                          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}"
+            )
 
     def _get_show_run(self, msg):
         """Called when extraction of VPP operational data (output of CLI command
@@ -282,69 +442,222 @@ class ExecutionChecker(ResultVisitor):
         :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:
-                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
 
-    def _get_latency(self, msg, test_type):
-        """Get the latency data from the test message.
+        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()
 
-        :param msg: Message to be parsed.
-        :param test_type: Type of the test - NDR or PDR.
+        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 = self._data[u"tests"][self._test_id][u"show-run"].\
+                           count(u"DUT:") + 1
+            try:
+                socket = str(re.search(self.REGEX_TC_PAPI_CLI, msg.message).
+                             group(2))
+                socket = f"/{socket}"
+            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 = ""
+            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
+
+    def _get_ndrpdr_throughput(self, msg):
+        """Get NDR_LOWER, NDR_UPPER, PDR_LOWER and PDR_UPPER from the test
+        message.
+
+        :param msg: The test message to be parsed.
         :type msg: str
-        :type test_type: str
-        :returns: Latencies parsed from the message.
-        :rtype: dict
+        :returns: Parsed data as a dict and the status (PASS/FAIL).
+        :rtype: tuple(dict, str)
         """
 
-        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 {}
+        throughput = {
+            u"NDR": {u"LOWER": -1.0, u"UPPER": -1.0},
+            u"PDR": {u"LOWER": -1.0, u"UPPER": -1.0}
+        }
+        status = u"FAIL"
+        groups = re.search(self.REGEX_NDRPDR_RATE, msg)
 
-        latencies = list()
-        for idx in groups_range:
+        if groups is not None:
             try:
-                lat = [int(item) for item in str(groups.group(idx)).split('/')]
-            except (AttributeError, ValueError):
-                lat = [-1, -1, -1]
-            latencies.append(lat)
+                throughput[u"NDR"][u"LOWER"] = float(groups.group(1))
+                throughput[u"NDR"][u"UPPER"] = float(groups.group(2))
+                throughput[u"PDR"][u"LOWER"] = float(groups.group(3))
+                throughput[u"PDR"][u"UPPER"] = float(groups.group(4))
+                status = u"PASS"
+            except (IndexError, ValueError):
+                pass
+
+        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 = {
+            u"LOWER": -1.0,
+            u"UPPER": -1.0
+        }
+        status = u"FAIL"
+        groups = re.search(self.REGEX_PLR_RATE, msg)
 
-        keys = ("min", "avg", "max")
+        if groups is not None:
+            try:
+                throughput[u"LOWER"] = float(groups.group(1))
+                throughput[u"UPPER"] = float(groups.group(2))
+                status = u"PASS"
+            except (IndexError, ValueError):
+                pass
+
+        return throughput, status
+
+    def _get_ndrpdr_latency(self, msg):
+        """Get LATENCY 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)
+        """
+        latency_default = {
+            u"min": -1.0,
+            u"avg": -1.0,
+            u"max": -1.0,
+            u"hdrh": u""
+        }
         latency = {
-            "direction1": {
+            u"NDR": {
+                u"direction1": copy.copy(latency_default),
+                u"direction2": copy.copy(latency_default)
             },
-            "direction2": {
+            u"PDR": {
+                u"direction1": copy.copy(latency_default),
+                u"direction2": copy.copy(latency_default)
             }
         }
+        status = u"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('/', 3)
+
+            rval = {
+                u"min": float(in_list[0]),
+                u"avg": float(in_list[1]),
+                u"max": float(in_list[2]),
+                u"hdrh": u""
+            }
+
+            if len(in_list) == 4:
+                rval[u"hdrh"] = str(in_list[3])
 
-        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 rval
 
-        return latency
+        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))
+                status = u"PASS"
+            except (IndexError, ValueError):
+                pass
+
+        return latency, status
 
     def visit_suite(self, suite):
         """Implements traversing through the suite and its direct children.
@@ -371,17 +684,22 @@ class ExecutionChecker(ResultVisitor):
         except AttributeError:
             return
 
-        doc_str = suite.doc.replace('"', "'").replace('\n', ' ').\
-            replace('\r', '').replace('*[', ' |br| *[').replace("*", "**")
-        doc_str = replace(doc_str, ' |br| *[', '*[', maxreplace=1)
-
-        self._data["suites"][suite.longname.lower().replace('"', "'").
-            replace(" ", "_")] = {
-                "name": suite.name.lower(),
-                "doc": doc_str,
-                "parent": parent_name,
-                "level": len(suite.longname.split("."))
-            }
+        doc_str = suite.doc.\
+            replace(u'"', u"'").\
+            replace(u'\n', u' ').\
+            replace(u'\r', u'').\
+            replace(u'*[', u' |br| *[').\
+            replace(u"*", u"**").\
+            replace(u' |br| *[', u'*[', 1)
+
+        self._data[u"suites"][suite.longname.lower().
+                              replace(u'"', u"'").
+                              replace(u" ", u"_")] = {
+                                  u"name": suite.name.lower(),
+                                  u"doc": doc_str,
+                                  u"parent": parent_name,
+                                  u"level": len(suite.longname.split(u"."))
+                              }
 
         suite.keywords.visit(self)
 
@@ -392,7 +710,6 @@ class ExecutionChecker(ResultVisitor):
         :type suite: Suite
         :returns: Nothing.
         """
-        pass
 
     def visit_test(self, test):
         """Implements traversing through the test.
@@ -413,73 +730,132 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
 
+        longname_orig = test.longname.lower()
+
+        # Check the ignore list
+        if longname_orig in self._ignore:
+            return
+
         tags = [str(tag) for tag in test.tags]
         test_result = dict()
-        test_result["name"] = test.name.lower()
-        test_result["parent"] = test.parent.name.lower()
-        test_result["tags"] = tags
-        doc_str = test.doc.replace('"', "'").replace('\n', ' '). \
-            replace('\r', '').replace('[', ' |br| [')
-        test_result["doc"] = replace(doc_str, ' |br| [', '[', maxreplace=1)
-        test_result["msg"] = test.message.replace('\n', ' |br| '). \
-            replace('\r', '').replace('"', "'")
-        if test.status == "PASS" and ("NDRPDRDISC" in tags or
-                                      "TCP" in tags or
-                                      "MRR" in tags):
-            if "NDRDISC" in tags:
-                test_type = "NDR"
-            elif "PDRDISC" in tags:
-                test_type = "PDR"
-            elif "TCP" in tags:
-                test_type = "TCP"
-            elif "MRR" in tags:
-                test_type = "MRR"
-            else:
-                return
 
-            test_result["type"] = test_type
-
-            if test_type in ("NDR", "PDR"):
-                try:
-                    rate_value = str(re.search(
-                        self.REGEX_RATE, test.message).group(1))
-                except AttributeError:
-                    rate_value = "-1"
-                try:
-                    rate_unit = str(re.search(
-                        self.REGEX_RATE, test.message).group(2))
-                except AttributeError:
-                    rate_unit = "-1"
-
-                test_result["throughput"] = dict()
-                test_result["throughput"]["value"] = \
-                    int(rate_value.split('.')[0])
-                test_result["throughput"]["unit"] = rate_unit
-                test_result["latency"] = \
-                    self._get_latency(test.message, test_type)
-                if test_type == "PDR":
-                    test_result["lossTolerance"] = str(re.search(
-                        self.REGEX_TOLERANCE, test.message).group(1))
-
-            elif test_type in ("TCP", ):
-                groups = re.search(self.REGEX_TCP, test.message)
-                test_result["result"] = dict()
-                test_result["result"]["value"] = int(groups.group(2))
-                test_result["result"]["unit"] = groups.group(1)
-            elif test_type in ("MRR", ):
-                groups = re.search(self.REGEX_MRR, test.message)
-                test_result["result"] = dict()
-                test_result["result"]["duration"] = int(groups.group(1))
-                test_result["result"]["tx"] = int(groups.group(2))
-                test_result["result"]["rx"] = int(groups.group(3))
-                test_result["result"]["throughput"] = int(
-                    test_result["result"]["rx"] /
-                    test_result["result"]["duration"])
+        # Change the TC long name and name if defined in the mapping table
+        longname = self._mapping.get(longname_orig, None)
+        if longname is not None:
+            name = longname.split(u'.')[-1]
+            logging.debug(
+                f"{self._data[u'metadata']}\n{longname_orig}\n{longname}\n"
+                f"{name}"
+            )
         else:
-            test_result["status"] = test.status
+            longname = longname_orig
+            name = test.name.lower()
+
+        # Remove TC number from the TC long name (backward compatibility):
+        self._test_id = re.sub(self.REGEX_TC_NUMBER, u"", longname)
+        # Remove TC number from the TC name (not needed):
+        test_result[u"name"] = re.sub(self.REGEX_TC_NUMBER, "", name)
+
+        test_result[u"parent"] = test.parent.name.lower()
+        test_result[u"tags"] = tags
+        test_result["doc"] = test.doc.\
+            replace(u'"', u"'").\
+            replace(u'\n', u' ').\
+            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
+
+        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
+            # test case names if necessary.
+            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[u"tags"]:
+                    groups = re.search(self.REGEX_TC_TAG, tag)
+                    if groups:
+                        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)
+                else:
+                    test_result[u"status"] = u"FAIL"
+                    self._data[u"tests"][self._test_id] = test_result
+                    logging.debug(
+                        f"The test {self._test_id} has no or more than one "
+                        f"multi-threading tags.\n"
+                        f"Tags: {test_result[u'tags']}"
+                    )
+                    return
+
+        if test.status == u"PASS":
+            if u"NDRPDR" in tags:
+                test_result[u"type"] = u"NDRPDR"
+                test_result[u"throughput"], test_result[u"status"] = \
+                    self._get_ndrpdr_throughput(test.message)
+                test_result[u"latency"], test_result[u"status"] = \
+                    self._get_ndrpdr_latency(test.message)
+            elif u"SOAK" in tags:
+                test_result[u"type"] = u"SOAK"
+                test_result[u"throughput"], test_result[u"status"] = \
+                    self._get_plr_throughput(test.message)
+            elif u"TCP" in tags:
+                test_result[u"type"] = u"TCP"
+                groups = re.search(self.REGEX_TCP, test.message)
+                test_result[u"result"] = int(groups.group(2))
+            elif u"MRR" in tags or u"FRMOBL" in tags or u"BMRR" in tags:
+                if u"MRR" in tags:
+                    test_result[u"type"] = u"MRR"
+                else:
+                    test_result[u"type"] = u"BMRR"
+
+                test_result[u"result"] = dict()
+                groups = re.search(self.REGEX_BMRR, test.message)
+                if groups is not None:
+                    items_str = groups.group(1)
+                    items_float = [float(item.strip()) for item
+                                   in items_str.split(",")]
+                    # Use whole list in CSIT-1180.
+                    stats = jumpavg.AvgStdevStats.for_runs(items_float)
+                    test_result[u"result"][u"receive-rate"] = stats.avg
+                else:
+                    groups = re.search(self.REGEX_MRR, test.message)
+                    test_result[u"result"][u"receive-rate"] = \
+                        float(groups.group(3)) / float(groups.group(1))
+            elif u"RECONF" in tags:
+                test_result[u"type"] = u"RECONF"
+                test_result[u"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[u"result"] = {
+                        u"loss": int(grps_loss.group(1)),
+                        u"time": float(grps_time.group(1))
+                    }
+                except (AttributeError, IndexError, ValueError, TypeError):
+                    test_result[u"status"] = u"FAIL"
+            else:
+                test_result[u"status"] = u"FAIL"
+                self._data[u"tests"][self._test_id] = test_result
+                return
 
-        self._test_ID = test.longname.lower()
-        self._data["tests"][self._test_ID] = test_result
+        self._data[u"tests"][self._test_id] = test_result
 
     def end_test(self, test):
         """Called when test ends.
@@ -488,7 +864,6 @@ class ExecutionChecker(ResultVisitor):
         :type test: Test
         :returns: Nothing.
         """
-        pass
 
     def visit_keyword(self, keyword):
         """Implements traversing through the keyword and its child keywords.
@@ -508,9 +883,9 @@ class ExecutionChecker(ResultVisitor):
         :returns: Nothing.
         """
         try:
-            if keyword.type == "setup":
+            if keyword.type == u"setup":
                 self.visit_setup_kw(keyword)
-            elif keyword.type == "teardown":
+            elif keyword.type == u"teardown":
                 self._lookup_kw_nr = 0
                 self.visit_teardown_kw(keyword)
             else:
@@ -526,7 +901,6 @@ class ExecutionChecker(ResultVisitor):
         :type keyword: Keyword
         :returns: Nothing.
         """
-        pass
 
     def visit_test_kw(self, test_kw):
         """Implements traversing through the test keyword and its child
@@ -549,11 +923,15 @@ class ExecutionChecker(ResultVisitor):
         :type test_kw: Keyword
         :returns: Nothing.
         """
-        if test_kw.name.count("Show Runtime Counters On All Duts"):
+        if test_kw.name.count(u"Show Runtime Counters On All Duts"):
             self._lookup_kw_nr += 1
             self._show_run_lookup_nr = 0
-            self._msg_type = "test-show-runtime"
-            test_kw.messages.visit(self)
+            self._msg_type = u"test-show-runtime"
+        elif test_kw.name.count(u"Install Dpdk Test") and not self._version:
+            self._msg_type = u"dpdk-version"
+        else:
+            return
+        test_kw.messages.visit(self)
 
     def end_test_kw(self, test_kw):
         """Called when keyword ends. Default implementation does nothing.
@@ -562,7 +940,6 @@ class ExecutionChecker(ResultVisitor):
         :type test_kw: Keyword
         :returns: Nothing.
         """
-        pass
 
     def visit_setup_kw(self, setup_kw):
         """Implements traversing through the teardown keyword and its child
@@ -585,10 +962,17 @@ class ExecutionChecker(ResultVisitor):
         :type setup_kw: Keyword
         :returns: Nothing.
         """
-        if setup_kw.name.count("Show Vpp Version On All Duts") \
+        if setup_kw.name.count(u"Show Vpp Version On All Duts") \
                 and not self._version:
-            self._msg_type = "setup-version"
-            setup_kw.messages.visit(self)
+            self._msg_type = u"vpp-version"
+        elif setup_kw.name.count(u"Set Global Variable") \
+                and not self._timestamp:
+            self._msg_type = u"timestamp"
+        elif setup_kw.name.count(u"Setup Framework") and not self._testbed:
+            self._msg_type = u"testbed"
+        else:
+            return
+        setup_kw.messages.visit(self)
 
     def end_setup_kw(self, setup_kw):
         """Called when keyword ends. Default implementation does nothing.
@@ -597,7 +981,6 @@ class ExecutionChecker(ResultVisitor):
         :type setup_kw: Keyword
         :returns: Nothing.
         """
-        pass
 
     def visit_teardown_kw(self, teardown_kw):
         """Implements traversing through the teardown keyword and its child
@@ -613,17 +996,21 @@ class ExecutionChecker(ResultVisitor):
                 self.end_teardown_kw(keyword)
 
     def start_teardown_kw(self, teardown_kw):
-        """Called when teardown keyword starts. Default implementation does
-        nothing.
+        """Called when teardown keyword starts
 
         :param teardown_kw: Keyword to process.
         :type teardown_kw: Keyword
         :returns: Nothing.
         """
 
-        if teardown_kw.name.count("Show Vat History On All Duts"):
-            self._vat_history_lookup_nr = 0
-            self._msg_type = "teardown-vat-history"
+        if teardown_kw.name.count(u"Show Vat History On All Duts"):
+            # TODO: Remove when not needed:
+            self._conf_history_lookup_nr = 0
+            self._msg_type = u"teardown-vat-history"
+            teardown_kw.messages.visit(self)
+        elif teardown_kw.name.count(u"Show Papi History On All Duts"):
+            self._conf_history_lookup_nr = 0
+            self._msg_type = u"teardown-papi-history"
             teardown_kw.messages.visit(self)
 
     def end_teardown_kw(self, teardown_kw):
@@ -633,7 +1020,6 @@ class ExecutionChecker(ResultVisitor):
         :type teardown_kw: Keyword
         :returns: Nothing.
         """
-        pass
 
     def visit_message(self, msg):
         """Implements visiting the message.
@@ -664,10 +1050,9 @@ class ExecutionChecker(ResultVisitor):
         :type msg: Message
         :returns: Nothing.
         """
-        pass
 
 
-class InputData(object):
+class InputData:
     """Input data
 
     The data is extracted from output.xml files generated by Jenkins jobs and
@@ -677,12 +1062,11 @@ class InputData(object):
     - job name
       - build number
         - metadata
-          - job
-          - build
-          - vpp version
+          (as described in ExecutionChecker documentation)
         - suites
+          (as described in ExecutionChecker documentation)
         - tests
-          - ID: test data (as described in ExecutionChecker documentation)
+          (as described in ExecutionChecker documentation)
     """
 
     def __init__(self, spec):
@@ -696,7 +1080,7 @@ class InputData(object):
         self._cfg = spec
 
         # Data store:
-        self._input_data = None
+        self._input_data = pd.Series()
 
     @property
     def data(self):
@@ -718,7 +1102,7 @@ class InputData(object):
         :rtype: pandas.Series
         """
 
-        return self.data[job][build]["metadata"]
+        return self.data[job][build][u"metadata"]
 
     def suites(self, job, build):
         """Getter - suites
@@ -731,7 +1115,7 @@ class InputData(object):
         :rtype: pandas.Series
         """
 
-        return self.data[job][str(build)]["suites"]
+        return self.data[job][str(build)][u"suites"]
 
     def tests(self, job, build):
         """Getter - tests
@@ -744,83 +1128,191 @@ class InputData(object):
         :rtype: pandas.Series
         """
 
-        return self.data[job][build]["tests"]
+        return self.data[job][build][u"tests"]
 
-    @staticmethod
-    def _parse_tests(job, build):
+    def _parse_tests(self, job, build, log):
         """Process data from robot output.xml file and return JSON structured
         data.
 
         :param job: The name of job which build output data will be processed.
         :param build: The build which output data will be processed.
+        :param log: List of log messages.
         :type job: str
         :type build: dict
+        :type log: list of tuples (severity, msg)
         :returns: JSON data structure.
         :rtype: dict
         """
 
-        tree = ET.parse(build["file-name"])
-        root = tree.getroot()
-        generated = root.attrib["generated"]
+        metadata = {
+            u"job": job,
+            u"build": build
+        }
 
-        with open(build["file-name"], 'r') as data_file:
+        with open(build[u"file-name"], u'r') as data_file:
             try:
                 result = ExecutionResult(data_file)
             except errors.DataError as err:
-                logging.error("Error occurred while parsing output.xml: {0}".
-                              format(err))
+                log.append(
+                    (u"ERROR", f"Error occurred while parsing output.xml: "
+                               f"{repr(err)}")
+                )
                 return None
-        checker = ExecutionChecker(job=job, build=build, generated=generated)
+        checker = ExecutionChecker(metadata, self._cfg.mapping,
+                                   self._cfg.ignore)
         result.visit(checker)
 
         return checker.data
 
-    def read_data(self):
-        """Parse input data from input files and store in pandas' Series.
+    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.
+        :param job: Name of the Jenkins job which generated the processed input
+            file.
+        :param build: Information about the Jenkins build which generated the
+            processed input file.
+        :param repeat: Repeat the download specified number of times if not
+            successful.
+        :type pid: int
+        :type job: str
+        :type build: dict
+        :type repeat: int
         """
 
-        logging.info("Parsing input files ...")
+        logs = list()
+
+        logs.append(
+            (u"INFO", f"  Processing the job/build: {job}: {build[u'build']}")
+        )
+
+        state = u"failed"
+        success = False
+        data = None
+        do_repeat = repeat
+        while do_repeat:
+            success = download_and_unzip_data_file(self._cfg, job, build, pid,
+                                                   logs)
+            if success:
+                break
+            do_repeat -= 1
+        if not success:
+            logs.append(
+                (u"ERROR",
+                 f"It is not possible to download the input data file from the "
+                 f"job {job}, build {build[u'build']}, or it is damaged. "
+                 f"Skipped.")
+            )
+        if success:
+            logs.append(
+                (u"INFO",
+                 f"    Processing data from the build {build[u'build']} ...")
+            )
+            data = self._parse_tests(job, build, logs)
+            if data is None:
+                logs.append(
+                    (u"ERROR",
+                     f"Input data file from the job {job}, build "
+                     f"{build[u'build']} is damaged. Skipped.")
+                )
+            else:
+                state = u"processed"
+
+            try:
+                remove(build[u"file-name"])
+            except OSError as err:
+                logs.append(
+                    ("ERROR", f"Cannot remove the file {build[u'file-name']}: "
+                              f"{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(u"time-period", None)
+        if timeperiod and data:
+            now = dt.utcnow()
+            timeperiod = timedelta(int(timeperiod))
+            metadata = data.get(u"metadata", None)
+            if metadata:
+                generated = metadata.get(u"generated", None)
+                if generated:
+                    generated = dt.strptime(generated, u"%Y%m%d %H:%M")
+                    if (now - generated) > timeperiod:
+                        # Remove the data and the file:
+                        state = u"removed"
+                        data = None
+                        logs.append(
+                            (u"INFO",
+                             f"    The build {job}/{build[u'build']} is "
+                             f"outdated, will be removed.")
+                        )
+        logs.append((u"INFO", u"  Done."))
+
+        for level, line in logs:
+            if level == u"INFO":
+                logging.info(line)
+            elif level == u"ERROR":
+                logging.error(line)
+            elif level == u"DEBUG":
+                logging.debug(line)
+            elif level == u"CRITICAL":
+                logging.critical(line)
+            elif level == u"WARNING":
+                logging.warning(line)
+
+        return {u"data": data, u"state": state, u"job": job, u"build": build}
+
+    def download_and_parse_data(self, repeat=1):
+        """Download the input data files, parse input data from input files and
+        store in pandas' Series.
+
+        :param repeat: Repeat the download specified number of times if not
+            successful.
+        :type repeat: int
+        """
+
+        logging.info(u"Downloading and parsing input files ...")
 
-        job_data = dict()
         for job, builds in self._cfg.builds.items():
-            logging.info("  Extracting data from the job '{0}' ...'".
-                         format(job))
-            builds_data = dict()
             for build in builds:
-                if build["status"] == "failed" \
-                        or build["status"] == "not found":
-                    continue
-                logging.info("    Extracting data from the build '{0}'".
-                             format(build["build"]))
-                logging.info("    Processing the file '{0}'".
-                             format(build["file-name"]))
-                data = InputData._parse_tests(job, build)
-                if data is None:
-                    logging.error("Input data file from the job '{job}', build "
-                                  "'{build}' is damaged. Skipped.".
-                                  format(job=job, build=build["build"]))
-                    continue
-
-                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()),
+
+                result = self._download_and_parse_build(job, build, repeat)
+                build_nr = result[u"build"][u"build"]
+
+                if result[u"data"]:
+                    data = result[u"data"]
+                    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())
+                        )
                     })
-                builds_data[str(build["build"])] = build_data
-                logging.info("    Done.")
 
-            job_data[job] = pd.Series(builds_data.values(),
-                                      index=builds_data.keys())
-            logging.info("  Done.")
+                    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[u"build"][u"file-name"])
+
+                self._cfg.set_input_state(job, build_nr, result[u"state"])
 
-        self._input_data = pd.Series(job_data.values(), index=job_data.keys())
-        logging.info("Done.")
+                mem_alloc = \
+                    resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1000
+                logging.info(f"Memory allocation: {mem_alloc:.0f}MB")
+
+        logging.info(u"Done.")
 
     @staticmethod
-    def _end_of_tag(tag_filter, start=0, closer="'"):
+    def _end_of_tag(tag_filter, start=0, closer=u"'"):
         """Return the index of character in the string which is the end of tag.
 
         :param tag_filter: The string where the end of tag is being searched.
@@ -855,9 +1347,9 @@ class InputData(object):
             if index is None:
                 return tag_filter
             index += 1
-            tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
+            tag_filter = tag_filter[:index] + u" 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=u"tests",
                     continue_on_error=False):
         """Filter required data from the given jobs and builds.
 
@@ -865,13 +1357,13 @@ class InputData(object):
 
         - job 1
           - build 1
-            - test (suite) 1 ID:
+            - test (or suite) 1 ID:
               - param 1
               - param 2
               ...
               - param n
             ...
-            - test (suite) n ID:
+            - test (or suite) n ID:
             ...
           ...
           - build n
@@ -880,74 +1372,173 @@ class InputData(object):
 
         :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,
-        metadata.
+            metadata.
         :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 data: dict
         :type data_set: str
         :type continue_on_error: bool
         :returns: Filtered data.
         :rtype pandas.Series
         """
 
-        logging.info("    Creating the data set for the {0} '{1}'.".
-                     format(element.get("type", ""), element.get("title", "")))
-
         try:
-            if element["filter"] in ("all", "template"):
-                cond = "True"
+            if element[u"filter"] in (u"all", u"template"):
+                cond = u"True"
             else:
-                cond = InputData._condition(element["filter"])
-            logging.debug("   Filter: {0}".format(cond))
+                cond = InputData._condition(element[u"filter"])
+            logging.debug(f"   Filter: {cond}")
         except KeyError:
-            logging.error("  No filter defined.")
+            logging.error(u"  No filter defined.")
             return None
 
         if params is None:
-            params = element.get("parameters", None)
+            params = element.get(u"parameters", None)
+            if params:
+                params.append(u"type")
 
+        data_to_filter = data if data else element[u"data"]
         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()
                     try:
-                        data_iter = self.data[job][str(build)][data_set].\
-                            iteritems()
+                        data_dict = dict(
+                            self.data[job][str(build)][data_set].items())
                     except KeyError:
                         if continue_on_error:
                             continue
-                        else:
-                            return None
-                    for test_ID, test_data in data_iter:
-                        if eval(cond, {"tags": test_data.get("tags", "")}):
-                            data[job][str(build)][test_ID] = pd.Series()
+                        return None
+
+                    for test_id, test_data in data_dict.items():
+                        if eval(cond, {u"tags": test_data.get(u"tags", u"")}):
+                            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
+                                    data[job][str(build)][test_id][param] = val
                             else:
                                 for param in params:
                                     try:
-                                        data[job][str(build)][test_ID][param] =\
+                                        data[job][str(build)][test_id][param] =\
                                             test_data[param]
                                     except KeyError:
-                                        data[job][str(build)][test_ID][param] =\
-                                            "No Data"
+                                        data[job][str(build)][test_id][param] =\
+                                            u"No Data"
             return data
 
         except (KeyError, IndexError, ValueError) as err:
-            logging.error("   Missing mandatory parameter in the element "
-                          "specification: {0}".format(err))
+            logging.error(
+                f"Missing mandatory parameter in the element specification: "
+                f"{repr(err)}"
+            )
             return None
-        except AttributeError:
+        except AttributeError as err:
+            logging.error(repr(err))
+            return None
+        except SyntaxError as err:
+            logging.error(
+                f"The filter {cond} is not correct. Check if all tags are "
+                f"enclosed by apostrophes.\n{repr(err)}"
+            )
+            return None
+
+    def filter_tests_by_name(self, element, params=None, data_set=u"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(u"include", None)
+        if not include:
+            logging.warning(u"No tests to include, skipping the element.")
+            return None
+
+        if params is None:
+            params = element.get(u"parameters", None)
+            if params:
+                params.append(u"type")
+
+        data = pd.Series()
+        try:
+            for job, builds in element[u"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] = u"No Data"
+                        except KeyError as err:
+                            logging.error(repr(err))
+                            if continue_on_error:
+                                continue
+                            return None
+            return data
+
+        except (KeyError, IndexError, ValueError) as err:
+            logging.error(
+                f"Missing mandatory parameter in the element "
+                f"specification: {repr(err)}"
+            )
             return None
-        except SyntaxError:
-            logging.error("   The filter '{0}' is not correct. Check if all "
-                          "tags are enclosed by apostrophes.".format(cond))
+        except AttributeError as err:
+            logging.error(repr(err))
             return None
 
     @staticmethod
@@ -971,12 +1562,12 @@ class InputData(object):
         :rtype: pandas.Series
         """
 
-        logging.info("    Merging data ...")
+        logging.info(u"    Merging data ...")
 
         merged_data = pd.Series()
-        for _, builds in data.iteritems():
-            for _, item in builds.iteritems():
-                for ID, item_data in item.iteritems():
-                    merged_data[ID] = item_data
+        for builds in data.values:
+            for item in builds.values:
+                for item_id, item_data in item.items():
+                    merged_data[item_id] = item_data
 
         return merged_data