Trending: Add exception handling for anomalies classification
[csit.git] / resources / tools / presentation / generator_tables.py
index 4a1ac0e..449b235 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2019 Cisco and/or its affiliates.
+# Copyright (c) 2021 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:
@@ -19,22 +19,25 @@ import logging
 import csv
 import re
 
+from collections import OrderedDict
+from xml.etree import ElementTree as ET
+from datetime import datetime as dt
+from datetime import timedelta
+from copy import deepcopy
+from json import loads
+
 import plotly.graph_objects as go
 import plotly.offline as ploff
 import pandas as pd
 
-from string import replace
-from collections import OrderedDict
 from numpy import nan, isnan
-from xml.etree import ElementTree as ET
-from datetime import datetime as dt
-from datetime import timedelta
+from yaml import load, FullLoader, YAMLError
 
-from utils import mean, stdev, relative_change, classify_anomalies, \
-    convert_csv_to_pretty_txt, relative_change_stdev
+from pal_utils import mean, stdev, classify_anomalies, \
+    convert_csv_to_pretty_txt, relative_change_stdev, relative_change
 
 
-REGEX_NIC = re.compile(r'\d*ge\dp\d\D*\d*')
+REGEX_NIC = re.compile(r'(\d*ge\dp\d\D*\d*[a-z]*)')
 
 
 def generate_tables(spec, data):
@@ -46,18 +49,35 @@ def generate_tables(spec, data):
     :type data: InputData
     """
 
-    logging.info("Generating the tables ...")
+    generator = {
+        u"table_merged_details": table_merged_details,
+        u"table_soak_vs_ndr": table_soak_vs_ndr,
+        u"table_perf_trending_dash": table_perf_trending_dash,
+        u"table_perf_trending_dash_html": table_perf_trending_dash_html,
+        u"table_last_failed_tests": table_last_failed_tests,
+        u"table_failed_tests": table_failed_tests,
+        u"table_failed_tests_html": table_failed_tests_html,
+        u"table_oper_data_html": table_oper_data_html,
+        u"table_comparison": table_comparison,
+        u"table_weekly_comparison": table_weekly_comparison
+    }
+
+    logging.info(u"Generating the tables ...")
     for table in spec.tables:
         try:
-            eval(table["algorithm"])(table, data)
+            if table[u"algorithm"] == u"table_weekly_comparison":
+                table[u"testbeds"] = spec.environment.get(u"testbeds", None)
+            generator[table[u"algorithm"]](table, data)
         except NameError as err:
-            logging.error("Probably algorithm '{alg}' is not defined: {err}".
-                          format(alg=table["algorithm"], err=repr(err)))
-    logging.info("Done.")
+            logging.error(
+                f"Probably algorithm {table[u'algorithm']} is not defined: "
+                f"{repr(err)}"
+            )
+    logging.info(u"Done.")
 
 
-def table_details(table, input_data):
-    """Generate the table(s) with algorithm: table_detailed_test_results
+def table_oper_data_html(table, input_data):
+    """Generate the table(s) with algorithm: html_table_oper_data
     specified in the specification file.
 
     :param table: Table to generate.
@@ -66,62 +86,238 @@ def table_details(table, input_data):
     :type input_data: InputData
     """
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
-
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
     # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
-    data = input_data.filter_data(table)
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
+    data = input_data.filter_data(
+        table,
+        params=[u"name", u"parent", u"telemetry-show-run", u"type"],
+        continue_on_error=True
+    )
+    if data.empty:
+        return
+    data = input_data.merge_data(data)
 
-    # Prepare the header of the tables
-    header = list()
-    for column in table["columns"]:
-        header.append('"{0}"'.format(str(column["title"]).replace('"', '""')))
+    sort_tests = table.get(u"sort", None)
+    if sort_tests:
+        args = dict(
+            inplace=True,
+            ascending=(sort_tests == u"ascending")
+        )
+        data.sort_index(**args)
 
-    # Generate the data for the table according to the model in the table
-    # specification
-    job = table["data"].keys()[0]
-    build = str(table["data"][job][0])
-    try:
-        suites = input_data.suites(job, build)
-    except KeyError:
-        logging.error("    No data available. The table will not be generated.")
+    suites = input_data.filter_data(
+        table,
+        continue_on_error=True,
+        data_set=u"suites"
+    )
+    if suites.empty:
         return
+    suites = input_data.merge_data(suites)
 
-    for suite_longname, suite in suites.iteritems():
-        # Generate data
-        suite_name = suite["name"]
-        table_lst = list()
-        for test in data[job][build].keys():
-            if data[job][build][test]["parent"] in suite_name:
-                row_lst = list()
-                for column in table["columns"]:
-                    try:
-                        col_data = str(data[job][build][test][column["data"].
-                                       split(" ")[1]]).replace('"', '""')
-                        if column["data"].split(" ")[1] in ("conf-history",
-                                                            "show-run"):
-                            col_data = replace(col_data, " |br| ", "",
-                                               maxreplace=1)
-                            col_data = " |prein| {0} |preout| ".\
-                                format(col_data[:-5])
-                        row_lst.append('"{0}"'.format(col_data))
-                    except KeyError:
-                        row_lst.append("No data")
-                table_lst.append(row_lst)
+    def _generate_html_table(tst_data):
+        """Generate an HTML table with operational data for the given test.
 
-        # Write the data to file
-        if table_lst:
-            file_name = "{0}_{1}{2}".format(table["output-file"], suite_name,
-                                            table["output-file-ext"])
-            logging.info("      Writing file: '{}'".format(file_name))
-            with open(file_name, "w") as file_handler:
-                file_handler.write(",".join(header) + "\n")
-                for item in table_lst:
-                    file_handler.write(",".join(item) + "\n")
+        :param tst_data: Test data to be used to generate the table.
+        :type tst_data: pandas.Series
+        :returns: HTML table with operational data.
+        :rtype: str
+        """
+
+        colors = {
+            u"header": u"#7eade7",
+            u"empty": u"#ffffff",
+            u"body": (u"#e9f1fb", u"#d4e4f7")
+        }
+
+        tbl = ET.Element(u"table", attrib=dict(width=u"100%", border=u"0"))
+
+        trow = ET.SubElement(tbl, u"tr", attrib=dict(bgcolor=colors[u"header"]))
+        thead = ET.SubElement(
+            trow, u"th", attrib=dict(align=u"left", colspan=u"6")
+        )
+        thead.text = tst_data[u"name"]
+
+        trow = ET.SubElement(tbl, u"tr", attrib=dict(bgcolor=colors[u"empty"]))
+        thead = ET.SubElement(
+            trow, u"th", attrib=dict(align=u"left", colspan=u"6")
+        )
+        thead.text = u"\t"
+
+        if tst_data.get(u"telemetry-show-run", None) is None or \
+                isinstance(tst_data[u"telemetry-show-run"], str):
+            trow = ET.SubElement(
+                tbl, u"tr", attrib=dict(bgcolor=colors[u"header"])
+            )
+            tcol = ET.SubElement(
+                trow, u"td", attrib=dict(align=u"left", colspan=u"6")
+            )
+            tcol.text = u"No Data"
 
-    logging.info("  Done.")
+            trow = ET.SubElement(
+                tbl, u"tr", attrib=dict(bgcolor=colors[u"empty"])
+            )
+            thead = ET.SubElement(
+                trow, u"th", attrib=dict(align=u"left", colspan=u"6")
+            )
+            font = ET.SubElement(
+                thead, u"font", attrib=dict(size=u"12px", color=u"#ffffff")
+            )
+            font.text = u"."
+            return str(ET.tostring(tbl, encoding=u"unicode"))
+
+        tbl_hdr = (
+            u"Name",
+            u"Nr of Vectors",
+            u"Nr of Packets",
+            u"Suspends",
+            u"Cycles per Packet",
+            u"Average Vector Size"
+        )
+
+        for dut_data in tst_data[u"telemetry-show-run"].values():
+            trow = ET.SubElement(
+                tbl, u"tr", attrib=dict(bgcolor=colors[u"header"])
+            )
+            tcol = ET.SubElement(
+                trow, u"td", attrib=dict(align=u"left", colspan=u"6")
+            )
+            if dut_data.get(u"runtime", None) is None:
+                tcol.text = u"No Data"
+                continue
+
+            runtime = dict()
+            for item in dut_data[u"runtime"].get(u"data", tuple()):
+                tid = int(item[u"labels"][u"thread_id"])
+                if runtime.get(tid, None) is None:
+                    runtime[tid] = dict()
+                gnode = item[u"labels"][u"graph_node"]
+                if runtime[tid].get(gnode, None) is None:
+                    runtime[tid][gnode] = dict()
+                try:
+                    runtime[tid][gnode][item[u"name"]] = float(item[u"value"])
+                except ValueError:
+                    runtime[tid][gnode][item[u"name"]] = item[u"value"]
+
+            threads = dict({idx: list() for idx in range(len(runtime))})
+            for idx, run_data in runtime.items():
+                for gnode, gdata in run_data.items():
+                    if gdata[u"vectors"] > 0:
+                        clocks = gdata[u"clocks"] / gdata[u"vectors"]
+                    elif gdata[u"calls"] > 0:
+                        clocks = gdata[u"clocks"] / gdata[u"calls"]
+                    elif gdata[u"suspends"] > 0:
+                        clocks = gdata[u"clocks"] / gdata[u"suspends"]
+                    else:
+                        clocks = 0.0
+                    if gdata[u"calls"] > 0:
+                        vectors_call = gdata[u"vectors"] / gdata[u"calls"]
+                    else:
+                        vectors_call = 0.0
+                    if int(gdata[u"calls"]) + int(gdata[u"vectors"]) + \
+                            int(gdata[u"suspends"]):
+                        threads[idx].append([
+                            gnode,
+                            int(gdata[u"calls"]),
+                            int(gdata[u"vectors"]),
+                            int(gdata[u"suspends"]),
+                            clocks,
+                            vectors_call
+                        ])
+
+            bold = ET.SubElement(tcol, u"b")
+            bold.text = (
+                f"Host IP: {dut_data.get(u'host', '')}, "
+                f"Socket: {dut_data.get(u'socket', '')}"
+            )
+            trow = ET.SubElement(
+                tbl, u"tr", attrib=dict(bgcolor=colors[u"empty"])
+            )
+            thead = ET.SubElement(
+                trow, u"th", attrib=dict(align=u"left", colspan=u"6")
+            )
+            thead.text = u"\t"
+
+            for thread_nr, thread in threads.items():
+                trow = ET.SubElement(
+                    tbl, u"tr", attrib=dict(bgcolor=colors[u"header"])
+                )
+                tcol = ET.SubElement(
+                    trow, u"td", attrib=dict(align=u"left", colspan=u"6")
+                )
+                bold = ET.SubElement(tcol, u"b")
+                bold.text = u"main" if thread_nr == 0 else f"worker_{thread_nr}"
+                trow = ET.SubElement(
+                    tbl, u"tr", attrib=dict(bgcolor=colors[u"header"])
+                )
+                for idx, col in enumerate(tbl_hdr):
+                    tcol = ET.SubElement(
+                        trow, u"td",
+                        attrib=dict(align=u"right" if idx else u"left")
+                    )
+                    font = ET.SubElement(
+                        tcol, u"font", attrib=dict(size=u"2")
+                    )
+                    bold = ET.SubElement(font, u"b")
+                    bold.text = col
+                for row_nr, row in enumerate(thread):
+                    trow = ET.SubElement(
+                        tbl, u"tr",
+                        attrib=dict(bgcolor=colors[u"body"][row_nr % 2])
+                    )
+                    for idx, col in enumerate(row):
+                        tcol = ET.SubElement(
+                            trow, u"td",
+                            attrib=dict(align=u"right" if idx else u"left")
+                        )
+                        font = ET.SubElement(
+                            tcol, u"font", attrib=dict(size=u"2")
+                        )
+                        if isinstance(col, float):
+                            font.text = f"{col:.2f}"
+                        else:
+                            font.text = str(col)
+                trow = ET.SubElement(
+                    tbl, u"tr", attrib=dict(bgcolor=colors[u"empty"])
+                )
+                thead = ET.SubElement(
+                    trow, u"th", attrib=dict(align=u"left", colspan=u"6")
+                )
+                thead.text = u"\t"
+
+        trow = ET.SubElement(tbl, u"tr", attrib=dict(bgcolor=colors[u"empty"]))
+        thead = ET.SubElement(
+            trow, u"th", attrib=dict(align=u"left", colspan=u"6")
+        )
+        font = ET.SubElement(
+            thead, u"font", attrib=dict(size=u"12px", color=u"#ffffff")
+        )
+        font.text = u"."
+
+        return str(ET.tostring(tbl, encoding=u"unicode"))
+
+    for suite in suites.values:
+        html_table = str()
+        for test_data in data.values:
+            if test_data[u"parent"] not in suite[u"name"]:
+                continue
+            html_table += _generate_html_table(test_data)
+        if not html_table:
+            continue
+        try:
+            file_name = f"{table[u'output-file']}{suite[u'name']}.rst"
+            with open(f"{file_name}", u'w') as html_file:
+                logging.info(f"    Writing file: {file_name}")
+                html_file.write(u".. raw:: html\n\n\t")
+                html_file.write(html_table)
+                html_file.write(u"\n\t<p><br><br></p>\n")
+        except KeyError:
+            logging.warning(u"The output file is not defined.")
+            return
+    logging.info(u"  Done.")
 
 
 def table_merged_details(table, input_data):
@@ -134,127 +330,160 @@ def table_merged_details(table, input_data):
     :type input_data: InputData
     """
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
 
     # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
     data = input_data.filter_data(table, continue_on_error=True)
     data = input_data.merge_data(data)
-    data.sort_index(inplace=True)
 
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
+    sort_tests = table.get(u"sort", None)
+    if sort_tests:
+        args = dict(
+            inplace=True,
+            ascending=(sort_tests == u"ascending")
+        )
+        data.sort_index(**args)
+
     suites = input_data.filter_data(
-        table, continue_on_error=True, data_set="suites")
+        table, continue_on_error=True, data_set=u"suites")
     suites = input_data.merge_data(suites)
 
     # Prepare the header of the tables
     header = list()
-    for column in table["columns"]:
-        header.append('"{0}"'.format(str(column["title"]).replace('"', '""')))
+    for column in table[u"columns"]:
+        header.append(
+            u'"{0}"'.format(str(column[u"title"]).replace(u'"', u'""'))
+        )
 
-    for _, suite in suites.iteritems():
+    for suite in suites.values:
         # Generate data
-        suite_name = suite["name"]
+        suite_name = suite[u"name"]
         table_lst = list()
         for test in data.keys():
-            if data[test]["parent"] in suite_name:
-                row_lst = list()
-                for column in table["columns"]:
-                    try:
-                        col_data = str(data[test][column["data"].
-                                       split(" ")[1]]).replace('"', '""')
-                        col_data = replace(col_data, "No Data",
-                                           "Not Captured     ")
-                        if column["data"].split(" ")[1] in ("conf-history",
-                                                            "show-run"):
-                            col_data = replace(col_data, " |br| ", "",
-                                               maxreplace=1)
-                            col_data = " |prein| {0} |preout| ".\
-                                format(col_data[:-5])
-                        row_lst.append('"{0}"'.format(col_data))
-                    except KeyError:
-                        row_lst.append('"Not captured"')
+            if data[test][u"status"] != u"PASS" or \
+                    data[test][u"parent"] not in suite_name:
+                continue
+            row_lst = list()
+            for column in table[u"columns"]:
+                try:
+                    col_data = str(data[test][column[
+                        u"data"].split(u" ")[1]]).replace(u'"', u'""')
+                    # Do not include tests with "Test Failed" in test message
+                    if u"Test Failed" in col_data:
+                        continue
+                    col_data = col_data.replace(
+                        u"No Data", u"Not Captured     "
+                    )
+                    if column[u"data"].split(u" ")[1] in (u"name", ):
+                        if len(col_data) > 30:
+                            col_data_lst = col_data.split(u"-")
+                            half = int(len(col_data_lst) / 2)
+                            col_data = f"{u'-'.join(col_data_lst[:half])}" \
+                                       f"- |br| " \
+                                       f"{u'-'.join(col_data_lst[half:])}"
+                        col_data = f" |prein| {col_data} |preout| "
+                    elif column[u"data"].split(u" ")[1] in (u"msg", ):
+                        # Temporary solution: remove NDR results from message:
+                        if bool(table.get(u'remove-ndr', False)):
+                            try:
+                                col_data = col_data.split(u"\n", 1)[1]
+                            except IndexError:
+                                pass
+                        col_data = col_data.replace(u'\n', u' |br| ').\
+                            replace(u'\r', u'').replace(u'"', u"'")
+                        col_data = f" |prein| {col_data} |preout| "
+                    elif column[u"data"].split(u" ")[1] in (u"conf-history", ):
+                        col_data = col_data.replace(u'\n', u' |br| ')
+                        col_data = f" |prein| {col_data[:-5]} |preout| "
+                    row_lst.append(f'"{col_data}"')
+                except KeyError:
+                    row_lst.append(u'"Not captured"')
+            if len(row_lst) == len(table[u"columns"]):
                 table_lst.append(row_lst)
 
         # Write the data to file
         if table_lst:
-            file_name = "{0}_{1}{2}".format(table["output-file"], suite_name,
-                                            table["output-file-ext"])
-            logging.info("      Writing file: '{}'".format(file_name))
-            with open(file_name, "w") as file_handler:
-                file_handler.write(",".join(header) + "\n")
+            separator = u"" if table[u'output-file'].endswith(u"/") else u"_"
+            file_name = f"{table[u'output-file']}{separator}{suite_name}.csv"
+            logging.info(f"      Writing file: {file_name}")
+            with open(file_name, u"wt") as file_handler:
+                file_handler.write(u",".join(header) + u"\n")
                 for item in table_lst:
-                    file_handler.write(",".join(item) + "\n")
+                    file_handler.write(u",".join(item) + u"\n")
+
+    logging.info(u"  Done.")
 
-    logging.info("  Done.")
 
+def _tpc_modify_test_name(test_name, ignore_nic=False):
+    """Modify a test name by replacing its parts.
 
-def _tpc_modify_test_name(test_name):
-    test_name_mod = test_name.replace("-ndrpdrdisc", ""). \
-        replace("-ndrpdr", "").replace("-pdrdisc", ""). \
-        replace("-ndrdisc", "").replace("-pdr", ""). \
-        replace("-ndr", ""). \
-        replace("1t1c", "1c").replace("2t1c", "1c"). \
-        replace("2t2c", "2c").replace("4t2c", "2c"). \
-        replace("4t4c", "4c").replace("8t4c", "4c")
-    test_name_mod = re.sub(REGEX_NIC, "", test_name_mod)
+    :param test_name: Test name to be modified.
+    :param ignore_nic: If True, NIC is removed from TC name.
+    :type test_name: str
+    :type ignore_nic: bool
+    :returns: Modified test name.
+    :rtype: str
+    """
+    test_name_mod = test_name.\
+        replace(u"-ndrpdr", u"").\
+        replace(u"1t1c", u"1c").\
+        replace(u"2t1c", u"1c"). \
+        replace(u"2t2c", u"2c").\
+        replace(u"4t2c", u"2c"). \
+        replace(u"4t4c", u"4c").\
+        replace(u"8t4c", u"4c")
+
+    if ignore_nic:
+        return re.sub(REGEX_NIC, u"", test_name_mod)
     return test_name_mod
 
 
 def _tpc_modify_displayed_test_name(test_name):
-    return test_name.replace("1t1c", "1c").replace("2t1c", "1c"). \
-        replace("2t2c", "2c").replace("4t2c", "2c"). \
-        replace("4t4c", "4c").replace("8t4c", "4c")
+    """Modify a test name which is displayed in a table by replacing its parts.
+
+    :param test_name: Test name to be modified.
+    :type test_name: str
+    :returns: Modified test name.
+    :rtype: str
+    """
+    return test_name.\
+        replace(u"1t1c", u"1c").\
+        replace(u"2t1c", u"1c"). \
+        replace(u"2t2c", u"2c").\
+        replace(u"4t2c", u"2c"). \
+        replace(u"4t4c", u"4c").\
+        replace(u"8t4c", u"4c")
 
 
 def _tpc_insert_data(target, src, include_tests):
+    """Insert src data to the target structure.
+
+    :param target: Target structure where the data is placed.
+    :param src: Source data to be placed into the target structure.
+    :param include_tests: Which results will be included (MRR, NDR, PDR).
+    :type target: list
+    :type src: dict
+    :type include_tests: str
+    """
     try:
-        if include_tests == "MRR":
-            target.append(src["result"]["receive-rate"])  # .avg)
-        elif include_tests == "PDR":
-            target.append(src["throughput"]["PDR"]["LOWER"])
-        elif include_tests == "NDR":
-            target.append(src["throughput"]["NDR"]["LOWER"])
+        if include_tests == u"MRR":
+            target[u"mean"] = src[u"result"][u"receive-rate"]
+            target[u"stdev"] = src[u"result"][u"receive-stdev"]
+        elif include_tests == u"PDR":
+            target[u"data"].append(src[u"throughput"][u"PDR"][u"LOWER"])
+        elif include_tests == u"NDR":
+            target[u"data"].append(src[u"throughput"][u"NDR"][u"LOWER"])
     except (KeyError, TypeError):
         pass
 
 
-def _tpc_sort_table(table):
-    # Sort the table:
-    # 1. New in CSIT-XXXX
-    # 2. See footnote
-    # 3. Delta
-    tbl_new = list()
-    tbl_see = list()
-    tbl_delta = list()
-    for item in table:
-        if isinstance(item[-1], str):
-            if "New in CSIT" in item[-1]:
-                tbl_new.append(item)
-            elif "See footnote" in item[-1]:
-                tbl_see.append(item)
-        else:
-            tbl_delta.append(item)
-
-    # Sort the tables:
-    tbl_new.sort(key=lambda rel: rel[0], reverse=False)
-    tbl_see.sort(key=lambda rel: rel[0], reverse=False)
-    tbl_see.sort(key=lambda rel: rel[-1], reverse=False)
-    tbl_delta.sort(key=lambda rel: rel[-1], reverse=True)
-
-    # Put the tables together:
-    table = list()
-    table.extend(tbl_new)
-    table.extend(tbl_see)
-    table.extend(tbl_delta)
-
-    return table
-
-
-def _tpc_generate_html_table(header, data, output_file_name):
+def _tpc_generate_html_table(header, data, out_file_name, legend=u"",
+                             footnote=u"", sort_data=True, title=u"",
+                             generate_rst=True):
     """Generate html table from input data with simple sorting possibility.
 
     :param header: Table header.
@@ -262,673 +491,182 @@ def _tpc_generate_html_table(header, data, output_file_name):
         Inner lists are rows in the table. All inner lists must be of the same
         length. The length of these lists must be the same as the length of the
         header.
-    :param output_file_name: The name (relative or full path) where the
+    :param out_file_name: The name (relative or full path) where the
         generated html table is written.
+    :param legend: The legend to display below the table.
+    :param footnote: The footnote to display below the table (and legend).
+    :param sort_data: If True the data sorting is enabled.
+    :param title: The table (and file) title.
+    :param generate_rst: If True, wrapping rst file is generated.
     :type header: list
     :type data: list of lists
-    :type output_file_name: str
+    :type out_file_name: str
+    :type legend: str
+    :type footnote: str
+    :type sort_data: bool
+    :type title: str
+    :type generate_rst: bool
     """
 
-    df = pd.DataFrame(data, columns=header)
-
-    df_sorted = [df.sort_values(
-        by=[key, header[0]], ascending=[True, True]
-        if key != header[0] else [False, True]) for key in header]
-    df_sorted_rev = [df.sort_values(
-        by=[key, header[0]], ascending=[False, True]
-        if key != header[0] else [True, True]) for key in header]
-    df_sorted.extend(df_sorted_rev)
+    try:
+        idx = header.index(u"Test Case")
+    except ValueError:
+        idx = 0
+    params = {
+        u"align-hdr": (
+            [u"left", u"right"],
+            [u"left", u"left", u"right"],
+            [u"left", u"left", u"left", u"right"]
+        ),
+        u"align-itm": (
+            [u"left", u"right"],
+            [u"left", u"left", u"right"],
+            [u"left", u"left", u"left", u"right"]
+        ),
+        u"width": ([15, 9], [4, 24, 10], [4, 4, 32, 10])
+    }
+
+    df_data = pd.DataFrame(data, columns=header)
+
+    if sort_data:
+        df_sorted = [df_data.sort_values(
+            by=[key, header[idx]], ascending=[True, True]
+            if key != header[idx] else [False, True]) for key in header]
+        df_sorted_rev = [df_data.sort_values(
+            by=[key, header[idx]], ascending=[False, True]
+            if key != header[idx] else [True, True]) for key in header]
+        df_sorted.extend(df_sorted_rev)
+    else:
+        df_sorted = df_data
 
-    fill_color = [["#d4e4f7" if idx % 2 else "#e9f1fb"
-                   for idx in range(len(df))]]
+    fill_color = [[u"#d4e4f7" if idx % 2 else u"#e9f1fb"
+                   for idx in range(len(df_data))]]
     table_header = dict(
-        values=["<b>{item}</b>".format(item=item) for item in header],
-        fill_color="#7eade7",
-        align=["left", "center"]
+        values=[f"<b>{item.replace(u',', u',<br>')}</b>" for item in header],
+        fill_color=u"#7eade7",
+        align=params[u"align-hdr"][idx],
+        font=dict(
+            family=u"Courier New",
+            size=12
+        )
     )
 
     fig = go.Figure()
 
-    for table in df_sorted:
-        columns = [table.get(col) for col in header]
+    if sort_data:
+        for table in df_sorted:
+            columns = [table.get(col) for col in header]
+            fig.add_trace(
+                go.Table(
+                    columnwidth=params[u"width"][idx],
+                    header=table_header,
+                    cells=dict(
+                        values=columns,
+                        fill_color=fill_color,
+                        align=params[u"align-itm"][idx],
+                        font=dict(
+                            family=u"Courier New",
+                            size=12
+                        )
+                    )
+                )
+            )
+
+        buttons = list()
+        menu_items = [f"<b>{itm}</b> (ascending)" for itm in header]
+        menu_items.extend([f"<b>{itm}</b> (descending)" for itm in header])
+        for idx, hdr in enumerate(menu_items):
+            visible = [False, ] * len(menu_items)
+            visible[idx] = True
+            buttons.append(
+                dict(
+                    label=hdr.replace(u" [Mpps]", u""),
+                    method=u"update",
+                    args=[{u"visible": visible}],
+                )
+            )
+
+        fig.update_layout(
+            updatemenus=[
+                go.layout.Updatemenu(
+                    type=u"dropdown",
+                    direction=u"down",
+                    x=0.0,
+                    xanchor=u"left",
+                    y=1.002,
+                    yanchor=u"bottom",
+                    active=len(menu_items) - 1,
+                    buttons=list(buttons)
+                )
+            ],
+        )
+    else:
         fig.add_trace(
             go.Table(
-                columnwidth=[30, 10],
+                columnwidth=params[u"width"][idx],
                 header=table_header,
                 cells=dict(
-                    values=columns,
+                    values=[df_sorted.get(col) for col in header],
                     fill_color=fill_color,
-                    align=["left", "right"]
+                    align=params[u"align-itm"][idx],
+                    font=dict(
+                        family=u"Courier New",
+                        size=12
+                    )
                 )
             )
         )
 
-    buttons = list()
-    menu_items = ["<b>{0}</b> (ascending)".format(itm) for itm in header]
-    menu_items_rev = ["<b>{0}</b> (descending)".format(itm) for itm in header]
-    menu_items.extend(menu_items_rev)
-    for idx, hdr in enumerate(menu_items):
-        visible = [False, ] * len(menu_items)
-        visible[idx] = True
-        buttons.append(
-            dict(
-                label=hdr.replace(" [Mpps]", ""),
-                method="update",
-                args=[{"visible": visible}],
-            )
-        )
-
-    fig.update_layout(
-        updatemenus=[
-            go.layout.Updatemenu(
-                type="dropdown",
-                direction="down",
-                x=0.03,
-                xanchor="left",
-                y=1.045,
-                yanchor="top",
-                active=len(menu_items) - 1,
-                buttons=list(buttons)
-            )
-        ],
-        annotations=[
-            go.layout.Annotation(
-                text="<b>Sort by:</b>",
-                x=0,
-                xref="paper",
-                y=1.035,
-                yref="paper",
-                align="left",
-                showarrow=False
-            )
-        ]
+    ploff.plot(
+        fig,
+        show_link=False,
+        auto_open=False,
+        filename=f"{out_file_name}_in.html"
     )
 
-    ploff.plot(fig, show_link=False, auto_open=False, filename=output_file_name)
-
-
-def table_performance_comparison(table, input_data):
-    """Generate the table(s) with algorithm: table_performance_comparison
-    specified in the specification file.
-
-    :param table: Table to generate.
-    :param input_data: Data to process.
-    :type table: pandas.Series
-    :type input_data: InputData
-    """
-
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
-
-    # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
-    data = input_data.filter_data(table, continue_on_error=True)
-
-    # Prepare the header of the tables
-    try:
-        header = ["Test case", ]
-
-        if table["include-tests"] == "MRR":
-            hdr_param = "Rec Rate"
-        else:
-            hdr_param = "Thput"
-
-        history = table.get("history", None)
-        if history:
-            for item in history:
-                header.extend(
-                    ["{0} {1} [Mpps]".format(item["title"], hdr_param),
-                     "{0} Stdev [Mpps]".format(item["title"])])
-        header.extend(
-            ["{0} {1} [Mpps]".format(table["reference"]["title"], hdr_param),
-             "{0} Stdev [Mpps]".format(table["reference"]["title"]),
-             "{0} {1} [Mpps]".format(table["compare"]["title"], hdr_param),
-             "{0} Stdev [Mpps]".format(table["compare"]["title"]),
-             "Delta [%]"])
-        header_str = ",".join(header) + "\n"
-    except (AttributeError, KeyError) as err:
-        logging.error("The model is invalid, missing parameter: {0}".
-                      format(err))
-        return
-
-    # Prepare data to the table:
-    tbl_dict = dict()
-    topo = ""
-    for job, builds in table["reference"]["data"].items():
-        topo = "2n-skx" if "2n-skx" in job else ""
-        for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                tst_name_mod = _tpc_modify_test_name(tst_name)
-                if "across topologies" in table["title"].lower():
-                    tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                if tbl_dict.get(tst_name_mod, None) is None:
-                    groups = re.search(REGEX_NIC, tst_data["parent"])
-                    nic = groups.group(0) if groups else ""
-                    name = "{0}-{1}".format(nic, "-".join(tst_data["name"].
-                                                          split("-")[:-1]))
-                    if "across testbeds" in table["title"].lower() or \
-                            "across topologies" in table["title"].lower():
-                        name = _tpc_modify_displayed_test_name(name)
-                    tbl_dict[tst_name_mod] = {"name": name,
-                                              "ref-data": list(),
-                                              "cmp-data": list()}
-                _tpc_insert_data(target=tbl_dict[tst_name_mod]["ref-data"],
-                                 src=tst_data,
-                                 include_tests=table["include-tests"])
-
-    for job, builds in table["compare"]["data"].items():
-        for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                tst_name_mod = _tpc_modify_test_name(tst_name)
-                if "across topologies" in table["title"].lower():
-                    tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                if tbl_dict.get(tst_name_mod, None) is None:
-                    groups = re.search(REGEX_NIC, tst_data["parent"])
-                    nic = groups.group(0) if groups else ""
-                    name = "{0}-{1}".format(nic, "-".join(tst_data["name"].
-                                                          split("-")[:-1]))
-                    if "across testbeds" in table["title"].lower() or \
-                            "across topologies" in table["title"].lower():
-                        name = _tpc_modify_displayed_test_name(name)
-                    tbl_dict[tst_name_mod] = {"name": name,
-                                              "ref-data": list(),
-                                              "cmp-data": list()}
-                _tpc_insert_data(target=tbl_dict[tst_name_mod]["cmp-data"],
-                                 src=tst_data,
-                                 include_tests=table["include-tests"])
-
-    replacement = table["compare"].get("data-replacement", None)
-    if replacement:
-        create_new_list = True
-        rpl_data = input_data.filter_data(
-            table, data=replacement, continue_on_error=True)
-        for job, builds in replacement.items():
-            for build in builds:
-                for tst_name, tst_data in rpl_data[job][str(build)].iteritems():
-                    tst_name_mod = _tpc_modify_test_name(tst_name)
-                    if "across topologies" in table["title"].lower():
-                        tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                    if tbl_dict.get(tst_name_mod, None) is None:
-                        name = "{0}".format("-".join(tst_data["name"].
-                                                     split("-")[:-1]))
-                        if "across testbeds" in table["title"].lower() or \
-                                "across topologies" in table["title"].lower():
-                            name = _tpc_modify_displayed_test_name(name)
-                        tbl_dict[tst_name_mod] = {"name": name,
-                                                  "ref-data": list(),
-                                                  "cmp-data": list()}
-                    if create_new_list:
-                        create_new_list = False
-                        tbl_dict[tst_name_mod]["cmp-data"] = list()
-
-                    _tpc_insert_data(target=tbl_dict[tst_name_mod]["cmp-data"],
-                                     src=tst_data,
-                                     include_tests=table["include-tests"])
-
-    if history:
-        for item in history:
-            for job, builds in item["data"].items():
-                for build in builds:
-                    for tst_name, tst_data in data[job][str(build)].iteritems():
-                        tst_name_mod = _tpc_modify_test_name(tst_name)
-                        if "across topologies" in table["title"].lower():
-                            tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                        if tbl_dict.get(tst_name_mod, None) is None:
-                            continue
-                        if tbl_dict[tst_name_mod].get("history", None) is None:
-                            tbl_dict[tst_name_mod]["history"] = OrderedDict()
-                        if tbl_dict[tst_name_mod]["history"].\
-                                get(item["title"], None) is None:
-                            tbl_dict[tst_name_mod]["history"][item["title"]] = \
-                                list()
-                        try:
-                            # TODO: Re-work when NDRPDRDISC tests are not used
-                            if table["include-tests"] == "MRR":
-                                tbl_dict[tst_name_mod]["history"][item[
-                                    "title"]].append(tst_data["result"][
-                                        "receive-rate"].avg)
-                            elif table["include-tests"] == "PDR":
-                                if tst_data["type"] == "PDR":
-                                    tbl_dict[tst_name_mod]["history"][
-                                        item["title"]].\
-                                        append(tst_data["throughput"]["value"])
-                                elif tst_data["type"] == "NDRPDR":
-                                    tbl_dict[tst_name_mod]["history"][item[
-                                        "title"]].append(tst_data["throughput"][
-                                            "PDR"]["LOWER"])
-                            elif table["include-tests"] == "NDR":
-                                if tst_data["type"] == "NDR":
-                                    tbl_dict[tst_name_mod]["history"][
-                                        item["title"]].\
-                                        append(tst_data["throughput"]["value"])
-                                elif tst_data["type"] == "NDRPDR":
-                                    tbl_dict[tst_name_mod]["history"][item[
-                                        "title"]].append(tst_data["throughput"][
-                                            "NDR"]["LOWER"])
-                            else:
-                                continue
-                        except (TypeError, KeyError):
-                            pass
-
-    tbl_lst = list()
-    footnote = False
-    for tst_name in tbl_dict.keys():
-        item = [tbl_dict[tst_name]["name"], ]
-        if history:
-            if tbl_dict[tst_name].get("history", None) is not None:
-                for hist_data in tbl_dict[tst_name]["history"].values():
-                    if hist_data:
-                        item.append(round(mean(hist_data) / 1000000, 2))
-                        item.append(round(stdev(hist_data) / 1000000, 2))
-                    else:
-                        item.extend(["Not tested", "Not tested"])
-            else:
-                item.extend(["Not tested", "Not tested"])
-        data_t = tbl_dict[tst_name]["ref-data"]
-        if data_t:
-            item.append(round(mean(data_t) / 1000000, 2))
-            item.append(round(stdev(data_t) / 1000000, 2))
-        else:
-            item.extend(["Not tested", "Not tested"])
-        data_t = tbl_dict[tst_name]["cmp-data"]
-        if data_t:
-            item.append(round(mean(data_t) / 1000000, 2))
-            item.append(round(stdev(data_t) / 1000000, 2))
-        else:
-            item.extend(["Not tested", "Not tested"])
-        if item[-2] == "Not tested":
-            pass
-        elif item[-4] == "Not tested":
-            item.append("New in CSIT-1908")
-        elif topo == "2n-skx" and "dot1q" in tbl_dict[tst_name]["name"]:
-            item.append("See footnote [1]")
-            footnote = True
-        elif item[-4] != 0:
-            item.append(int(relative_change(float(item[-4]), float(item[-2]))))
-        if (len(item) == len(header)) and (item[-3] != "Not tested"):
-            tbl_lst.append(item)
-
-    tbl_lst = _tpc_sort_table(tbl_lst)
-
-    # Generate csv tables:
-    csv_file = "{0}.csv".format(table["output-file"])
-    with open(csv_file, "w") as file_handler:
-        file_handler.write(header_str)
-        for test in tbl_lst:
-            file_handler.write(",".join([str(item) for item in test]) + "\n")
-
-    txt_file_name = "{0}.txt".format(table["output-file"])
-    convert_csv_to_pretty_txt(csv_file, txt_file_name)
-
-    if footnote:
-        with open(txt_file_name, 'a') as txt_file:
-            txt_file.writelines([
-                "\nFootnotes:\n",
-                "[1] CSIT-1908 changed test methodology of dot1q tests in "
-                "2-node testbeds, dot1q encapsulation is now used on both "
-                "links of SUT.\n",
-                "    Previously dot1q was used only on a single link with the "
-                "other link carrying untagged Ethernet frames. This changes "
-                "results\n",
-                "    in slightly lower throughput in CSIT-1908 for these "
-                "tests. See release notes."
-            ])
-
-    # Generate html table:
-    _tpc_generate_html_table(header, tbl_lst,
-                             "{0}.html".format(table["output-file"]))
-
-
-def table_performance_comparison_nic(table, input_data):
-    """Generate the table(s) with algorithm: table_performance_comparison
-    specified in the specification file.
-
-    :param table: Table to generate.
-    :param input_data: Data to process.
-    :type table: pandas.Series
-    :type input_data: InputData
-    """
-
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
-
-    # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
-    data = input_data.filter_data(table, continue_on_error=True)
-
-    # Prepare the header of the tables
-    try:
-        header = ["Test case", ]
-
-        if table["include-tests"] == "MRR":
-            hdr_param = "Rec Rate"
-        else:
-            hdr_param = "Thput"
-
-        history = table.get("history", None)
-        if history:
-            for item in history:
-                header.extend(
-                    ["{0} {1} [Mpps]".format(item["title"], hdr_param),
-                     "{0} Stdev [Mpps]".format(item["title"])])
-        header.extend(
-            ["{0} {1} [Mpps]".format(table["reference"]["title"], hdr_param),
-             "{0} Stdev [Mpps]".format(table["reference"]["title"]),
-             "{0} {1} [Mpps]".format(table["compare"]["title"], hdr_param),
-             "{0} Stdev [Mpps]".format(table["compare"]["title"]),
-             "Delta [%]"])
-        header_str = ",".join(header) + "\n"
-    except (AttributeError, KeyError) as err:
-        logging.error("The model is invalid, missing parameter: {0}".
-                      format(err))
-        return
-
-    # Prepare data to the table:
-    tbl_dict = dict()
-    topo = ""
-    for job, builds in table["reference"]["data"].items():
-        topo = "2n-skx" if "2n-skx" in job else ""
-        for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                if table["reference"]["nic"] not in tst_data["tags"]:
-                    continue
-                tst_name_mod = _tpc_modify_test_name(tst_name)
-                if "across topologies" in table["title"].lower():
-                    tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                if tbl_dict.get(tst_name_mod, None) is None:
-                    name = "{0}".format("-".join(tst_data["name"].
-                                                 split("-")[:-1]))
-                    if "across testbeds" in table["title"].lower() or \
-                            "across topologies" in table["title"].lower():
-                        name = _tpc_modify_displayed_test_name(name)
-                    tbl_dict[tst_name_mod] = {"name": name,
-                                              "ref-data": list(),
-                                              "cmp-data": list()}
-                _tpc_insert_data(target=tbl_dict[tst_name_mod]["ref-data"],
-                                 src=tst_data,
-                                 include_tests=table["include-tests"])
-
-    for job, builds in table["compare"]["data"].items():
-        for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                if table["compare"]["nic"] not in tst_data["tags"]:
-                    continue
-                tst_name_mod = _tpc_modify_test_name(tst_name)
-                if "across topologies" in table["title"].lower():
-                    tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                if tbl_dict.get(tst_name_mod, None) is None:
-                    name = "{0}".format("-".join(tst_data["name"].
-                                                 split("-")[:-1]))
-                    if "across testbeds" in table["title"].lower() or \
-                            "across topologies" in table["title"].lower():
-                        name = _tpc_modify_displayed_test_name(name)
-                    tbl_dict[tst_name_mod] = {"name": name,
-                                              "ref-data": list(),
-                                              "cmp-data": list()}
-                _tpc_insert_data(target=tbl_dict[tst_name_mod]["cmp-data"],
-                                 src=tst_data,
-                                 include_tests=table["include-tests"])
-
-    replacement = table["compare"].get("data-replacement", None)
-    if replacement:
-        create_new_list = True
-        rpl_data = input_data.filter_data(
-            table, data=replacement, continue_on_error=True)
-        for job, builds in replacement.items():
-            for build in builds:
-                for tst_name, tst_data in rpl_data[job][str(build)].iteritems():
-                    if table["compare"]["nic"] not in tst_data["tags"]:
-                        continue
-                    tst_name_mod = _tpc_modify_test_name(tst_name)
-                    if "across topologies" in table["title"].lower():
-                        tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                    if tbl_dict.get(tst_name_mod, None) is None:
-                        name = "{0}".format("-".join(tst_data["name"].
-                                                     split("-")[:-1]))
-                        if "across testbeds" in table["title"].lower() or \
-                                "across topologies" in table["title"].lower():
-                            name = _tpc_modify_displayed_test_name(name)
-                        tbl_dict[tst_name_mod] = {"name": name,
-                                                  "ref-data": list(),
-                                                  "cmp-data": list()}
-                    if create_new_list:
-                        create_new_list = False
-                        tbl_dict[tst_name_mod]["cmp-data"] = list()
-
-                    _tpc_insert_data(target=tbl_dict[tst_name_mod]["cmp-data"],
-                                     src=tst_data,
-                                     include_tests=table["include-tests"])
-
-    if history:
-        for item in history:
-            for job, builds in item["data"].items():
-                for build in builds:
-                    for tst_name, tst_data in data[job][str(build)].iteritems():
-                        if item["nic"] not in tst_data["tags"]:
-                            continue
-                        tst_name_mod = _tpc_modify_test_name(tst_name)
-                        if "across topologies" in table["title"].lower():
-                            tst_name_mod = tst_name_mod.replace("2n1l-", "")
-                        if tbl_dict.get(tst_name_mod, None) is None:
-                            continue
-                        if tbl_dict[tst_name_mod].get("history", None) is None:
-                            tbl_dict[tst_name_mod]["history"] = OrderedDict()
-                        if tbl_dict[tst_name_mod]["history"].\
-                                get(item["title"], None) is None:
-                            tbl_dict[tst_name_mod]["history"][item["title"]] = \
-                                list()
-                        try:
-                            # TODO: Re-work when NDRPDRDISC tests are not used
-                            if table["include-tests"] == "MRR":
-                                tbl_dict[tst_name_mod]["history"][item[
-                                    "title"]].append(tst_data["result"][
-                                        "receive-rate"].avg)
-                            elif table["include-tests"] == "PDR":
-                                if tst_data["type"] == "PDR":
-                                    tbl_dict[tst_name_mod]["history"][
-                                        item["title"]].\
-                                        append(tst_data["throughput"]["value"])
-                                elif tst_data["type"] == "NDRPDR":
-                                    tbl_dict[tst_name_mod]["history"][item[
-                                        "title"]].append(tst_data["throughput"][
-                                            "PDR"]["LOWER"])
-                            elif table["include-tests"] == "NDR":
-                                if tst_data["type"] == "NDR":
-                                    tbl_dict[tst_name_mod]["history"][
-                                        item["title"]].\
-                                        append(tst_data["throughput"]["value"])
-                                elif tst_data["type"] == "NDRPDR":
-                                    tbl_dict[tst_name_mod]["history"][item[
-                                        "title"]].append(tst_data["throughput"][
-                                            "NDR"]["LOWER"])
-                            else:
-                                continue
-                        except (TypeError, KeyError):
-                            pass
-
-    tbl_lst = list()
-    footnote = False
-    for tst_name in tbl_dict.keys():
-        item = [tbl_dict[tst_name]["name"], ]
-        if history:
-            if tbl_dict[tst_name].get("history", None) is not None:
-                for hist_data in tbl_dict[tst_name]["history"].values():
-                    if hist_data:
-                        item.append(round(mean(hist_data) / 1000000, 2))
-                        item.append(round(stdev(hist_data) / 1000000, 2))
-                    else:
-                        item.extend(["Not tested", "Not tested"])
-            else:
-                item.extend(["Not tested", "Not tested"])
-        data_t = tbl_dict[tst_name]["ref-data"]
-        if data_t:
-            item.append(round(mean(data_t) / 1000000, 2))
-            item.append(round(stdev(data_t) / 1000000, 2))
-        else:
-            item.extend(["Not tested", "Not tested"])
-        data_t = tbl_dict[tst_name]["cmp-data"]
-        if data_t:
-            item.append(round(mean(data_t) / 1000000, 2))
-            item.append(round(stdev(data_t) / 1000000, 2))
-        else:
-            item.extend(["Not tested", "Not tested"])
-        if item[-2] == "Not tested":
-            pass
-        elif item[-4] == "Not tested":
-            item.append("New in CSIT-1908")
-        elif topo == "2n-skx" and "dot1q" in tbl_dict[tst_name]["name"]:
-            item.append("See footnote [1]")
-            footnote = True
-        elif item[-4] != 0:
-            item.append(int(relative_change(float(item[-4]), float(item[-2]))))
-        if (len(item) == len(header)) and (item[-3] != "Not tested"):
-            tbl_lst.append(item)
-
-    tbl_lst = _tpc_sort_table(tbl_lst)
-
-    # Generate csv tables:
-    csv_file = "{0}.csv".format(table["output-file"])
-    with open(csv_file, "w") as file_handler:
-        file_handler.write(header_str)
-        for test in tbl_lst:
-            file_handler.write(",".join([str(item) for item in test]) + "\n")
-
-    txt_file_name = "{0}.txt".format(table["output-file"])
-    convert_csv_to_pretty_txt(csv_file, txt_file_name)
-
-    if footnote:
-        with open(txt_file_name, 'a') as txt_file:
-            txt_file.writelines([
-                "\nFootnotes:\n",
-                "[1] CSIT-1908 changed test methodology of dot1q tests in "
-                "2-node testbeds, dot1q encapsulation is now used on both "
-                "links of SUT.\n",
-                "    Previously dot1q was used only on a single link with the "
-                "other link carrying untagged Ethernet frames. This changes "
-                "results\n",
-                "    in slightly lower throughput in CSIT-1908 for these "
-                "tests. See release notes."
-            ])
-
-    # Generate html table:
-    _tpc_generate_html_table(header, tbl_lst,
-                             "{0}.html".format(table["output-file"]))
-
-
-def table_nics_comparison(table, input_data):
-    """Generate the table(s) with algorithm: table_nics_comparison
-    specified in the specification file.
-
-    :param table: Table to generate.
-    :param input_data: Data to process.
-    :type table: pandas.Series
-    :type input_data: InputData
-    """
-
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
-
-    # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
-    data = input_data.filter_data(table, continue_on_error=True)
-
-    # Prepare the header of the tables
-    try:
-        header = ["Test case", ]
-
-        if table["include-tests"] == "MRR":
-            hdr_param = "Rec Rate"
-        else:
-            hdr_param = "Thput"
-
-        header.extend(
-            ["{0} {1} [Mpps]".format(table["reference"]["title"], hdr_param),
-             "{0} Stdev [Mpps]".format(table["reference"]["title"]),
-             "{0} {1} [Mpps]".format(table["compare"]["title"], hdr_param),
-             "{0} Stdev [Mpps]".format(table["compare"]["title"]),
-             "Delta [%]"])
-        header_str = ",".join(header) + "\n"
-    except (AttributeError, KeyError) as err:
-        logging.error("The model is invalid, missing parameter: {0}".
-                      format(err))
+    if not generate_rst:
         return
 
-    # Prepare data to the table:
-    tbl_dict = dict()
-    for job, builds in table["data"].items():
-        for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                tst_name_mod = tst_name.replace("-ndrpdrdisc", "").\
-                    replace("-ndrpdr", "").replace("-pdrdisc", "").\
-                    replace("-ndrdisc", "").replace("-pdr", "").\
-                    replace("-ndr", "").\
-                    replace("1t1c", "1c").replace("2t1c", "1c").\
-                    replace("2t2c", "2c").replace("4t2c", "2c").\
-                    replace("4t4c", "4c").replace("8t4c", "4c")
-                tst_name_mod = re.sub(REGEX_NIC, "", tst_name_mod)
-                if tbl_dict.get(tst_name_mod, None) is None:
-                    name = "-".join(tst_data["name"].split("-")[:-1])
-                    tbl_dict[tst_name_mod] = {"name": name,
-                                              "ref-data": list(),
-                                              "cmp-data": list()}
-                try:
-                    if table["include-tests"] == "MRR":
-                        result = tst_data["result"]["receive-rate"]  # .avg
-                    elif table["include-tests"] == "PDR":
-                        result = tst_data["throughput"]["PDR"]["LOWER"]
-                    elif table["include-tests"] == "NDR":
-                        result = tst_data["throughput"]["NDR"]["LOWER"]
-                    else:
-                        result = None
-
-                    if result:
-                        if table["reference"]["nic"] in tst_data["tags"]:
-                            tbl_dict[tst_name_mod]["ref-data"].append(result)
-                        elif table["compare"]["nic"] in tst_data["tags"]:
-                            tbl_dict[tst_name_mod]["cmp-data"].append(result)
-                except (TypeError, KeyError) as err:
-                    logging.debug("No data for {0}".format(tst_name))
-                    logging.debug(repr(err))
-                    # No data in output.xml for this test
-
-    tbl_lst = list()
-    for tst_name in tbl_dict.keys():
-        item = [tbl_dict[tst_name]["name"], ]
-        data_t = tbl_dict[tst_name]["ref-data"]
-        if data_t:
-            item.append(round(mean(data_t) / 1000000, 2))
-            item.append(round(stdev(data_t) / 1000000, 2))
-        else:
-            item.extend([None, None])
-        data_t = tbl_dict[tst_name]["cmp-data"]
-        if data_t:
-            item.append(round(mean(data_t) / 1000000, 2))
-            item.append(round(stdev(data_t) / 1000000, 2))
-        else:
-            item.extend([None, None])
-        if item[-4] is not None and item[-2] is not None and item[-4] != 0:
-            item.append(int(relative_change(float(item[-4]), float(item[-2]))))
-        if len(item) == len(header):
-            tbl_lst.append(item)
-
-    # Sort the table according to the relative change
-    tbl_lst.sort(key=lambda rel: rel[-1], reverse=True)
-
-    # Generate csv tables:
-    csv_file = "{0}.csv".format(table["output-file"])
-    with open(csv_file, "w") as file_handler:
-        file_handler.write(header_str)
-        for test in tbl_lst:
-            file_handler.write(",".join([str(item) for item in test]) + "\n")
-
-    convert_csv_to_pretty_txt(csv_file, "{0}.txt".format(table["output-file"]))
+    file_name = out_file_name.split(u"/")[-1]
+    if u"vpp" in out_file_name:
+        path = u"_tmp/src/vpp_performance_tests/comparisons/"
+    else:
+        path = u"_tmp/src/dpdk_performance_tests/comparisons/"
+    logging.info(f"    Writing the HTML file to {path}{file_name}.rst")
+    with open(f"{path}{file_name}.rst", u"wt") as rst_file:
+        rst_file.write(
+            u"\n"
+            u".. |br| raw:: html\n\n    <br />\n\n\n"
+            u".. |prein| raw:: html\n\n    <pre>\n\n\n"
+            u".. |preout| raw:: html\n\n    </pre>\n\n"
+        )
+        if title:
+            rst_file.write(f"{title}\n")
+            rst_file.write(f"{u'`' * len(title)}\n\n")
+        rst_file.write(
+            u".. raw:: html\n\n"
+            f'    <iframe frameborder="0" scrolling="no" '
+            f'width="1600" height="1200" '
+            f'src="../..{out_file_name.replace(u"_build", u"")}_in.html">'
+            f'</iframe>\n\n'
+        )
 
-    # Generate html table:
-    _tpc_generate_html_table(header, tbl_lst,
-                             "{0}.html".format(table["output-file"]))
+        if legend:
+            try:
+                itm_lst = legend[1:-2].split(u"\n")
+                rst_file.write(
+                    f"{itm_lst[0]}\n\n- " + u'\n- '.join(itm_lst[1:]) + u"\n\n"
+                )
+            except IndexError as err:
+                logging.error(f"Legend cannot be written to html file\n{err}")
+        if footnote:
+            try:
+                itm_lst = footnote[1:].split(u"\n")
+                rst_file.write(
+                    f"{itm_lst[0]}\n\n- " + u'\n- '.join(itm_lst[1:]) + u"\n\n"
+                )
+            except IndexError as err:
+                logging.error(f"Footnote cannot be written to html file\n{err}")
 
 
 def table_soak_vs_ndr(table, input_data):
@@ -941,126 +679,179 @@ def table_soak_vs_ndr(table, input_data):
     :type input_data: InputData
     """
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
 
     # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
     data = input_data.filter_data(table, continue_on_error=True)
 
     # Prepare the header of the table
     try:
         header = [
-            "Test case",
-            "{0} Thput [Mpps]".format(table["reference"]["title"]),
-            "{0} Stdev [Mpps]".format(table["reference"]["title"]),
-            "{0} Thput [Mpps]".format(table["compare"]["title"]),
-            "{0} Stdev [Mpps]".format(table["compare"]["title"]),
-            "Delta [%]", "Stdev of delta [%]"]
-        header_str = ",".join(header) + "\n"
+            u"Test Case",
+            f"Avg({table[u'reference'][u'title']})",
+            f"Stdev({table[u'reference'][u'title']})",
+            f"Avg({table[u'compare'][u'title']})",
+            f"Stdev{table[u'compare'][u'title']})",
+            u"Diff",
+            u"Stdev(Diff)"
+        ]
+        header_str = u";".join(header) + u"\n"
+        legend = (
+            u"\nLegend:\n"
+            f"Avg({table[u'reference'][u'title']}): "
+            f"Mean value of {table[u'reference'][u'title']} [Mpps] computed "
+            f"from a series of runs of the listed tests.\n"
+            f"Stdev({table[u'reference'][u'title']}): "
+            f"Standard deviation value of {table[u'reference'][u'title']} "
+            f"[Mpps] computed from a series of runs of the listed tests.\n"
+            f"Avg({table[u'compare'][u'title']}): "
+            f"Mean value of {table[u'compare'][u'title']} [Mpps] computed from "
+            f"a series of runs of the listed tests.\n"
+            f"Stdev({table[u'compare'][u'title']}): "
+            f"Standard deviation value of {table[u'compare'][u'title']} [Mpps] "
+            f"computed from a series of runs of the listed tests.\n"
+            f"Diff({table[u'reference'][u'title']},"
+            f"{table[u'compare'][u'title']}): "
+            f"Percentage change calculated for mean values.\n"
+            u"Stdev(Diff): "
+            u"Standard deviation of percentage change calculated for mean "
+            u"values."
+        )
     except (AttributeError, KeyError) as err:
-        logging.error("The model is invalid, missing parameter: {0}".
-                      format(err))
+        logging.error(f"The model is invalid, missing parameter: {repr(err)}")
         return
 
     # Create a list of available SOAK test results:
     tbl_dict = dict()
-    for job, builds in table["compare"]["data"].items():
+    for job, builds in table[u"compare"][u"data"].items():
         for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                if tst_data["type"] == "SOAK":
-                    tst_name_mod = tst_name.replace("-soak", "")
+            for tst_name, tst_data in data[job][str(build)].items():
+                if tst_data[u"type"] == u"SOAK":
+                    tst_name_mod = tst_name.replace(u"-soak", u"")
                     if tbl_dict.get(tst_name_mod, None) is None:
-                        groups = re.search(REGEX_NIC, tst_data["parent"])
-                        nic = groups.group(0) if groups else ""
-                        name = "{0}-{1}".format(nic, "-".join(tst_data["name"].
-                                                              split("-")[:-1]))
+                        groups = re.search(REGEX_NIC, tst_data[u"parent"])
+                        nic = groups.group(0) if groups else u""
+                        name = (
+                            f"{nic}-"
+                            f"{u'-'.join(tst_data[u'name'].split(u'-')[:-1])}"
+                        )
                         tbl_dict[tst_name_mod] = {
-                            "name": name,
-                            "ref-data": list(),
-                            "cmp-data": list()
+                            u"name": name,
+                            u"ref-data": list(),
+                            u"cmp-data": list()
                         }
                     try:
-                        tbl_dict[tst_name_mod]["cmp-data"].append(
-                            tst_data["throughput"]["LOWER"])
+                        tbl_dict[tst_name_mod][u"cmp-data"].append(
+                            tst_data[u"throughput"][u"LOWER"])
                     except (KeyError, TypeError):
                         pass
     tests_lst = tbl_dict.keys()
 
     # Add corresponding NDR test results:
-    for job, builds in table["reference"]["data"].items():
+    for job, builds in table[u"reference"][u"data"].items():
         for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                tst_name_mod = tst_name.replace("-ndrpdr", "").\
-                    replace("-mrr", "")
-                if tst_name_mod in tests_lst:
-                    try:
-                        if tst_data["type"] in ("NDRPDR", "MRR", "BMRR"):
-                            if table["include-tests"] == "MRR":
-                                result = tst_data["result"]["receive-rate"]
-                            elif table["include-tests"] == "PDR":
-                                result = tst_data["throughput"]["PDR"]["LOWER"]
-                            elif table["include-tests"] == "NDR":
-                                result = tst_data["throughput"]["NDR"]["LOWER"]
-                            else:
-                                result = None
-                            if result is not None:
-                                tbl_dict[tst_name_mod]["ref-data"].append(
-                                    result)
-                    except (KeyError, TypeError):
+            for tst_name, tst_data in data[job][str(build)].items():
+                tst_name_mod = tst_name.replace(u"-ndrpdr", u"").\
+                    replace(u"-mrr", u"")
+                if tst_name_mod not in tests_lst:
+                    continue
+                try:
+                    if tst_data[u"type"] not in (u"NDRPDR", u"MRR", u"BMRR"):
                         continue
+                    if table[u"include-tests"] == u"MRR":
+                        result = (tst_data[u"result"][u"receive-rate"],
+                                  tst_data[u"result"][u"receive-stdev"])
+                    elif table[u"include-tests"] == u"PDR":
+                        result = \
+                            tst_data[u"throughput"][u"PDR"][u"LOWER"]
+                    elif table[u"include-tests"] == u"NDR":
+                        result = \
+                            tst_data[u"throughput"][u"NDR"][u"LOWER"]
+                    else:
+                        result = None
+                    if result is not None:
+                        tbl_dict[tst_name_mod][u"ref-data"].append(
+                            result)
+                except (KeyError, TypeError):
+                    continue
 
     tbl_lst = list()
-    for tst_name in tbl_dict.keys():
-        item = [tbl_dict[tst_name]["name"], ]
-        data_r = tbl_dict[tst_name]["ref-data"]
+    for tst_name in tbl_dict:
+        item = [tbl_dict[tst_name][u"name"], ]
+        data_r = tbl_dict[tst_name][u"ref-data"]
         if data_r:
-            data_r_mean = mean(data_r)
-            item.append(round(data_r_mean / 1000000, 2))
-            data_r_stdev = stdev(data_r)
-            item.append(round(data_r_stdev / 1000000, 2))
+            if table[u"include-tests"] == u"MRR":
+                data_r_mean = data_r[0][0]
+                data_r_stdev = data_r[0][1]
+            else:
+                data_r_mean = mean(data_r)
+                data_r_stdev = stdev(data_r)
+            item.append(round(data_r_mean / 1e6, 1))
+            item.append(round(data_r_stdev / 1e6, 1))
         else:
             data_r_mean = None
             data_r_stdev = None
             item.extend([None, None])
-        data_c = tbl_dict[tst_name]["cmp-data"]
+        data_c = tbl_dict[tst_name][u"cmp-data"]
         if data_c:
-            data_c_mean = mean(data_c)
-            item.append(round(data_c_mean / 1000000, 2))
-            data_c_stdev = stdev(data_c)
-            item.append(round(data_c_stdev / 1000000, 2))
+            if table[u"include-tests"] == u"MRR":
+                data_c_mean = data_c[0][0]
+                data_c_stdev = data_c[0][1]
+            else:
+                data_c_mean = mean(data_c)
+                data_c_stdev = stdev(data_c)
+            item.append(round(data_c_mean / 1e6, 1))
+            item.append(round(data_c_stdev / 1e6, 1))
         else:
             data_c_mean = None
             data_c_stdev = None
             item.extend([None, None])
-        if data_r_mean and data_c_mean:
+        if data_r_mean is not None and data_c_mean is not None:
             delta, d_stdev = relative_change_stdev(
                 data_r_mean, data_c_mean, data_r_stdev, data_c_stdev)
-            item.append(round(delta, 2))
-            item.append(round(d_stdev, 2))
+            try:
+                item.append(round(delta))
+            except ValueError:
+                item.append(delta)
+            try:
+                item.append(round(d_stdev))
+            except ValueError:
+                item.append(d_stdev)
             tbl_lst.append(item)
 
     # Sort the table according to the relative change
     tbl_lst.sort(key=lambda rel: rel[-1], reverse=True)
 
     # Generate csv tables:
-    csv_file = "{0}.csv".format(table["output-file"])
-    with open(csv_file, "w") as file_handler:
+    csv_file_name = f"{table[u'output-file']}.csv"
+    with open(csv_file_name, u"wt") as file_handler:
         file_handler.write(header_str)
         for test in tbl_lst:
-            file_handler.write(",".join([str(item) for item in test]) + "\n")
+            file_handler.write(u";".join([str(item) for item in test]) + u"\n")
 
-    convert_csv_to_pretty_txt(csv_file, "{0}.txt".format(table["output-file"]))
+    convert_csv_to_pretty_txt(
+        csv_file_name, f"{table[u'output-file']}.txt", delimiter=u";"
+    )
+    with open(f"{table[u'output-file']}.txt", u'a') as file_handler:
+        file_handler.write(legend)
 
     # Generate html table:
-    _tpc_generate_html_table(header, tbl_lst,
-                             "{0}.html".format(table["output-file"]))
+    _tpc_generate_html_table(
+        header,
+        tbl_lst,
+        table[u'output-file'],
+        legend=legend,
+        title=table.get(u"title", u"")
+    )
 
 
-def table_performance_trending_dashboard(table, input_data):
+def table_perf_trending_dash(table, input_data):
     """Generate the table(s) with algorithm:
-    table_performance_trending_dashboard
+    table_perf_trending_dash
     specified in the specification file.
 
     :param table: Table to generate.
@@ -1069,55 +860,71 @@ def table_performance_trending_dashboard(table, input_data):
     :type input_data: InputData
     """
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
 
     # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
     data = input_data.filter_data(table, continue_on_error=True)
 
     # Prepare the header of the tables
-    header = ["Test Case",
-              "Trend [Mpps]",
-              "Short-Term Change [%]",
-              "Long-Term Change [%]",
-              "Regressions [#]",
-              "Progressions [#]"
-              ]
-    header_str = ",".join(header) + "\n"
+    header = [
+        u"Test Case",
+        u"Trend [Mpps]",
+        u"Short-Term Change [%]",
+        u"Long-Term Change [%]",
+        u"Regressions [#]",
+        u"Progressions [#]"
+    ]
+    header_str = u",".join(header) + u"\n"
+
+    incl_tests = table.get(u"include-tests", u"MRR")
 
     # Prepare data to the table:
     tbl_dict = dict()
-    for job, builds in table["data"].items():
+    for job, builds in table[u"data"].items():
         for build in builds:
-            for tst_name, tst_data in data[job][str(build)].iteritems():
-                if tst_name.lower() in table.get("ignore-list", list()):
+            for tst_name, tst_data in data[job][str(build)].items():
+                if tst_name.lower() in table.get(u"ignore-list", list()):
                     continue
                 if tbl_dict.get(tst_name, None) is None:
-                    groups = re.search(REGEX_NIC, tst_data["parent"])
+                    groups = re.search(REGEX_NIC, tst_data[u"parent"])
                     if not groups:
                         continue
                     nic = groups.group(0)
                     tbl_dict[tst_name] = {
-                        "name": "{0}-{1}".format(nic, tst_data["name"]),
-                        "data": OrderedDict()}
+                        u"name": f"{nic}-{tst_data[u'name']}",
+                        u"data": OrderedDict()
+                    }
                 try:
-                    tbl_dict[tst_name]["data"][str(build)] = \
-                        tst_data["result"]["receive-rate"]
+                    if incl_tests == u"MRR":
+                        tbl_dict[tst_name][u"data"][str(build)] = \
+                            tst_data[u"result"][u"receive-rate"]
+                    elif incl_tests == u"NDR":
+                        tbl_dict[tst_name][u"data"][str(build)] = \
+                            tst_data[u"throughput"][u"NDR"][u"LOWER"]
+                    elif incl_tests == u"PDR":
+                        tbl_dict[tst_name][u"data"][str(build)] = \
+                            tst_data[u"throughput"][u"PDR"][u"LOWER"]
                 except (TypeError, KeyError):
                     pass  # No data in output.xml for this test
 
     tbl_lst = list()
-    for tst_name in tbl_dict.keys():
-        data_t = tbl_dict[tst_name]["data"]
+    for tst_name in tbl_dict:
+        data_t = tbl_dict[tst_name][u"data"]
         if len(data_t) < 2:
             continue
 
-        classification_lst, avgs = classify_anomalies(data_t)
+        try:
+            classification_lst, avgs, _ = classify_anomalies(data_t)
+        except ValueError as err:
+            logging.info(f"{err} Skipping")
+            return
 
-        win_size = min(len(data_t), table["window"])
-        long_win_size = min(len(data_t), table["long-trend-window"])
+        win_size = min(len(data_t), table[u"window"])
+        long_win_size = min(len(data_t), table[u"long-trend-window"])
 
         try:
             max_long_avg = max(
@@ -1132,13 +939,13 @@ def table_performance_trending_dashboard(table, input_data):
             rel_change_last = nan
         else:
             rel_change_last = round(
-                ((last_avg - avg_week_ago) / avg_week_ago) * 100, 2)
+                ((last_avg - avg_week_ago) / avg_week_ago) * 1e2, 2)
 
         if isnan(max_long_avg) or isnan(last_avg) or max_long_avg == 0.0:
             rel_change_long = nan
         else:
             rel_change_long = round(
-                ((last_avg - max_long_avg) / max_long_avg) * 100, 2)
+                ((last_avg - max_long_avg) / max_long_avg) * 1e2, 2)
 
         if classification_lst:
             if isnan(rel_change_last) and isnan(rel_change_long):
@@ -1147,43 +954,41 @@ def table_performance_trending_dashboard(table, input_data):
                     isnan(rel_change_long):
                 continue
             tbl_lst.append(
-                [tbl_dict[tst_name]["name"],
-                 round(last_avg / 1000000, 2),
+                [tbl_dict[tst_name][u"name"],
+                 round(last_avg / 1e6, 2),
                  rel_change_last,
                  rel_change_long,
-                 classification_lst[-win_size:].count("regression"),
-                 classification_lst[-win_size:].count("progression")])
+                 classification_lst[-win_size+1:].count(u"regression"),
+                 classification_lst[-win_size+1:].count(u"progression")])
 
     tbl_lst.sort(key=lambda rel: rel[0])
+    tbl_lst.sort(key=lambda rel: rel[3])
+    tbl_lst.sort(key=lambda rel: rel[2])
 
     tbl_sorted = list()
-    for nrr in range(table["window"], -1, -1):
+    for nrr in range(table[u"window"], -1, -1):
         tbl_reg = [item for item in tbl_lst if item[4] == nrr]
-        for nrp in range(table["window"], -1, -1):
+        for nrp in range(table[u"window"], -1, -1):
             tbl_out = [item for item in tbl_reg if item[5] == nrp]
-            tbl_out.sort(key=lambda rel: rel[2])
             tbl_sorted.extend(tbl_out)
 
-    file_name = "{0}{1}".format(table["output-file"], table["output-file-ext"])
+    file_name = f"{table[u'output-file']}{table[u'output-file-ext']}"
 
-    logging.info("    Writing file: '{0}'".format(file_name))
-    with open(file_name, "w") as file_handler:
+    logging.info(f"    Writing file: {file_name}")
+    with open(file_name, u"wt") as file_handler:
         file_handler.write(header_str)
         for test in tbl_sorted:
-            file_handler.write(",".join([str(item) for item in test]) + '\n')
+            file_handler.write(u",".join([str(item) for item in test]) + u'\n')
 
-    txt_file_name = "{0}.txt".format(table["output-file"])
-    logging.info("    Writing file: '{0}'".format(txt_file_name))
-    convert_csv_to_pretty_txt(file_name, txt_file_name)
+    logging.info(f"    Writing file: {table[u'output-file']}.txt")
+    convert_csv_to_pretty_txt(file_name, f"{table[u'output-file']}.txt")
 
 
-def _generate_url(base, testbed, test_name):
+def _generate_url(testbed, test_name):
     """Generate URL to a trending plot from the name of the test case.
 
-    :param base: The base part of URL common to all test cases.
     :param testbed: The testbed used for testing.
     :param test_name: The name of the test case.
-    :type base: str
     :type testbed: str
     :type test_name: str
     :returns: The URL to the plot with the trending data for the given test
@@ -1191,146 +996,207 @@ def _generate_url(base, testbed, test_name):
     :rtype str
     """
 
-    url = base
-    file_name = ""
-    anchor = ".html#"
-    feature = ""
-
-    if "lbdpdk" in test_name or "lbvpp" in test_name:
-        file_name = "link_bonding"
-
-    elif "114b" in test_name and "vhost" in test_name:
-        file_name = "vts"
+    if u"x520" in test_name:
+        nic = u"x520"
+    elif u"x710" in test_name:
+        nic = u"x710"
+    elif u"xl710" in test_name:
+        nic = u"xl710"
+    elif u"xxv710" in test_name:
+        nic = u"xxv710"
+    elif u"vic1227" in test_name:
+        nic = u"vic1227"
+    elif u"vic1385" in test_name:
+        nic = u"vic1385"
+    elif u"x553" in test_name:
+        nic = u"x553"
+    elif u"cx556" in test_name or u"cx556a" in test_name:
+        nic = u"cx556a"
+    else:
+        nic = u""
+
+    if u"64b" in test_name:
+        frame_size = u"64b"
+    elif u"78b" in test_name:
+        frame_size = u"78b"
+    elif u"imix" in test_name:
+        frame_size = u"imix"
+    elif u"9000b" in test_name:
+        frame_size = u"9000b"
+    elif u"1518b" in test_name:
+        frame_size = u"1518b"
+    elif u"114b" in test_name:
+        frame_size = u"114b"
+    else:
+        frame_size = u""
+
+    if u"1t1c" in test_name or \
+        (u"-1c-" in test_name and
+         testbed in (u"3n-hsw", u"3n-tsh", u"2n-dnv", u"3n-dnv", u"2n-tx2")):
+        cores = u"1t1c"
+    elif u"2t2c" in test_name or \
+         (u"-2c-" in test_name and
+          testbed in (u"3n-hsw", u"3n-tsh", u"2n-dnv", u"3n-dnv", u"2n-tx2")):
+        cores = u"2t2c"
+    elif u"4t4c" in test_name or \
+         (u"-4c-" in test_name and
+          testbed in (u"3n-hsw", u"3n-tsh", u"2n-dnv", u"3n-dnv", u"2n-tx2")):
+        cores = u"4t4c"
+    elif u"2t1c" in test_name or \
+         (u"-1c-" in test_name and
+          testbed in (u"2n-skx", u"3n-skx", u"2n-clx", u"2n-zn2")):
+        cores = u"2t1c"
+    elif u"4t2c" in test_name or \
+         (u"-2c-" in test_name and
+          testbed in (u"2n-skx", u"3n-skx", u"2n-clx", u"2n-zn2")):
+        cores = u"4t2c"
+    elif u"8t4c" in test_name or \
+         (u"-4c-" in test_name and
+          testbed in (u"2n-skx", u"3n-skx", u"2n-clx", u"2n-zn2")):
+        cores = u"8t4c"
+    else:
+        cores = u""
+
+    if u"testpmd" in test_name:
+        driver = u"testpmd"
+    elif u"l3fwd" in test_name:
+        driver = u"l3fwd"
+    elif u"avf" in test_name:
+        driver = u"avf"
+    elif u"rdma" in test_name:
+        driver = u"rdma"
+    elif u"dnv" in testbed or u"tsh" in testbed:
+        driver = u"ixgbe"
+    else:
+        driver = u"dpdk"
+
+    if u"macip-iacl1s" in test_name:
+        bsf = u"features-macip-iacl1"
+    elif u"macip-iacl10s" in test_name:
+        bsf = u"features-macip-iacl10"
+    elif u"macip-iacl50s" in test_name:
+        bsf = u"features-macip-iacl50"
+    elif u"iacl1s" in test_name:
+        bsf = u"features-iacl1"
+    elif u"iacl10s" in test_name:
+        bsf = u"features-iacl10"
+    elif u"iacl50s" in test_name:
+        bsf = u"features-iacl50"
+    elif u"oacl1s" in test_name:
+        bsf = u"features-oacl1"
+    elif u"oacl10s" in test_name:
+        bsf = u"features-oacl10"
+    elif u"oacl50s" in test_name:
+        bsf = u"features-oacl50"
+    elif u"nat44det" in test_name:
+        bsf = u"nat44det-bidir"
+    elif u"nat44ed" in test_name and u"udir" in test_name:
+        bsf = u"nat44ed-udir"
+    elif u"-cps" in test_name and u"ethip4udp" in test_name:
+        bsf = u"udp-cps"
+    elif u"-cps" in test_name and u"ethip4tcp" in test_name:
+        bsf = u"tcp-cps"
+    elif u"-pps" in test_name and u"ethip4udp" in test_name:
+        bsf = u"udp-pps"
+    elif u"-pps" in test_name and u"ethip4tcp" in test_name:
+        bsf = u"tcp-pps"
+    elif u"-tput" in test_name and u"ethip4udp" in test_name:
+        bsf = u"udp-tput"
+    elif u"-tput" in test_name and u"ethip4tcp" in test_name:
+        bsf = u"tcp-tput"
+    elif u"udpsrcscale" in test_name:
+        bsf = u"features-udp"
+    elif u"iacl" in test_name:
+        bsf = u"features"
+    elif u"policer" in test_name:
+        bsf = u"features"
+    elif u"adl" in test_name:
+        bsf = u"features"
+    elif u"cop" in test_name:
+        bsf = u"features"
+    elif u"nat" in test_name:
+        bsf = u"features"
+    elif u"macip" in test_name:
+        bsf = u"features"
+    elif u"scale" in test_name:
+        bsf = u"scale"
+    elif u"base" in test_name:
+        bsf = u"base"
+    else:
+        bsf = u"base"
+
+    if u"114b" in test_name and u"vhost" in test_name:
+        domain = u"vts"
+    elif u"nat44" in test_name or u"-pps" in test_name or u"-cps" in test_name:
+        domain = u"nat44"
+        if u"nat44det" in test_name:
+            domain += u"-det-bidir"
+        else:
+            domain += u"-ed"
+        if u"udir" in test_name:
+            domain += u"-unidir"
+        elif u"-ethip4udp-" in test_name:
+            domain += u"-udp"
+        elif u"-ethip4tcp-" in test_name:
+            domain += u"-tcp"
+        if u"-cps" in test_name:
+            domain += u"-cps"
+        elif u"-pps" in test_name:
+            domain += u"-pps"
+        elif u"-tput" in test_name:
+            domain += u"-tput"
+    elif u"testpmd" in test_name or u"l3fwd" in test_name:
+        domain = u"dpdk"
+    elif u"memif" in test_name:
+        domain = u"container_memif"
+    elif u"srv6" in test_name:
+        domain = u"srv6"
+    elif u"vhost" in test_name:
+        domain = u"vhost"
+        if u"vppl2xc" in test_name:
+            driver += u"-vpp"
+        else:
+            driver += u"-testpmd"
+        if u"lbvpplacp" in test_name:
+            bsf += u"-link-bonding"
+    elif u"ch" in test_name and u"vh" in test_name and u"vm" in test_name:
+        domain = u"nf_service_density_vnfc"
+    elif u"ch" in test_name and u"mif" in test_name and u"dcr" in test_name:
+        domain = u"nf_service_density_cnfc"
+    elif u"pl" in test_name and u"mif" in test_name and u"dcr" in test_name:
+        domain = u"nf_service_density_cnfp"
+    elif u"ipsec" in test_name:
+        domain = u"ipsec"
+        if u"sw" in test_name:
+            bsf += u"-sw"
+        elif u"hw" in test_name:
+            bsf += u"-hw"
+    elif u"ethip4vxlan" in test_name:
+        domain = u"ip4_tunnels"
+    elif u"ethip4udpgeneve" in test_name:
+        domain = u"ip4_tunnels"
+    elif u"ip4base" in test_name or u"ip4scale" in test_name:
+        domain = u"ip4"
+    elif u"ip6base" in test_name or u"ip6scale" in test_name:
+        domain = u"ip6"
+    elif u"l2xcbase" in test_name or \
+            u"l2xcscale" in test_name or \
+            u"l2bdbasemaclrn" in test_name or \
+            u"l2bdscale" in test_name or \
+            u"l2patch" in test_name:
+        domain = u"l2"
+    else:
+        domain = u""
 
-    elif "testpmd" in test_name or "l3fwd" in test_name:
-        file_name = "dpdk"
+    file_name = u"-".join((domain, testbed, nic)) + u".html#"
+    anchor_name = u"-".join((frame_size, cores, bsf, driver))
 
-    elif "memif" in test_name:
-        file_name = "container_memif"
-        feature = "-base"
+    return file_name + anchor_name
 
-    elif "srv6" in test_name:
-        file_name = "srv6"
 
-    elif "vhost" in test_name:
-        if "l2xcbase" in test_name or "l2bdbasemaclrn" in test_name:
-            file_name = "vm_vhost_l2"
-            if "114b" in test_name:
-                feature = ""
-            elif "l2xcbase" in test_name and "x520" in test_name:
-                feature = "-base-l2xc"
-            elif "l2bdbasemaclrn" in test_name and "x520" in test_name:
-                feature = "-base-l2bd"
-            else:
-                feature = "-base"
-        elif "ip4base" in test_name:
-            file_name = "vm_vhost_ip4"
-            feature = "-base"
-
-    elif "ipsecbasetnlsw" in test_name:
-        file_name = "ipsecsw"
-        feature = "-base-scale"
-
-    elif "ipsec" in test_name:
-        file_name = "ipsec"
-        feature = "-base-scale"
-        if "hw-" in test_name:
-            file_name = "ipsechw"
-        elif "sw-" in test_name:
-            file_name = "ipsecsw"
-        if "-int-" in test_name:
-            feature = "-base-scale-int"
-        elif "tnl" in test_name:
-            feature = "-base-scale-tnl"
-
-    elif "ethip4lispip" in test_name or "ethip4vxlan" in test_name:
-        file_name = "ip4_tunnels"
-        feature = "-base"
-
-    elif "ip4base" in test_name or "ip4scale" in test_name:
-        file_name = "ip4"
-        if "xl710" in test_name:
-            feature = "-base-scale-features"
-        elif "iacl" in test_name:
-            feature = "-features-iacl"
-        elif "oacl" in test_name:
-            feature = "-features-oacl"
-        elif "snat" in test_name or "cop" in test_name:
-            feature = "-features"
-        else:
-            feature = "-base-scale"
-
-    elif "ip6base" in test_name or "ip6scale" in test_name:
-        file_name = "ip6"
-        feature = "-base-scale"
-
-    elif "l2xcbase" in test_name or "l2xcscale" in test_name \
-            or "l2bdbasemaclrn" in test_name or "l2bdscale" in test_name \
-            or "l2dbbasemaclrn" in test_name or "l2dbscale" in test_name:
-        file_name = "l2"
-        if "macip" in test_name:
-            feature = "-features-macip"
-        elif "iacl" in test_name:
-            feature = "-features-iacl"
-        elif "oacl" in test_name:
-            feature = "-features-oacl"
-        else:
-            feature = "-base-scale"
-
-    if "x520" in test_name:
-        nic = "x520-"
-    elif "x710" in test_name:
-        nic = "x710-"
-    elif "xl710" in test_name:
-        nic = "xl710-"
-    elif "xxv710" in test_name:
-        nic = "xxv710-"
-    elif "vic1227" in test_name:
-        nic = "vic1227-"
-    elif "vic1385" in test_name:
-        nic = "vic1385-"
-    elif "x553" in test_name:
-        nic = "x553-"
-    else:
-        nic = ""
-    anchor += nic
-
-    if "64b" in test_name:
-        framesize = "64b"
-    elif "78b" in test_name:
-        framesize = "78b"
-    elif "imix" in test_name:
-        framesize = "imix"
-    elif "9000b" in test_name:
-        framesize = "9000b"
-    elif "1518b" in test_name:
-        framesize = "1518b"
-    elif "114b" in test_name:
-        framesize = "114b"
-    else:
-        framesize = ""
-    anchor += framesize + '-'
-
-    if "1t1c" in test_name:
-        anchor += "1t1c"
-    elif "2t2c" in test_name:
-        anchor += "2t2c"
-    elif "4t4c" in test_name:
-        anchor += "4t4c"
-    elif "2t1c" in test_name:
-        anchor += "2t1c"
-    elif "4t2c" in test_name:
-        anchor += "4t2c"
-    elif "8t4c" in test_name:
-        anchor += "8t4c"
-
-    return url + file_name + '-' + testbed + '-' + nic + framesize + \
-        feature.replace("-int", "").replace("-tnl", "") + anchor + feature
-
-
-def table_performance_trending_dashboard_html(table, input_data):
+def table_perf_trending_dash_html(table, input_data):
     """Generate the table(s) with algorithm:
-    table_performance_trending_dashboard_html specified in the specification
+    table_perf_trending_dash_html specified in the specification
     file.
 
     :param table: Table to generate.
@@ -1339,70 +1205,113 @@ def table_performance_trending_dashboard_html(table, input_data):
     :type input_data: InputData
     """
 
-    testbed = table.get("testbed", None)
-    if testbed is None:
-        logging.error("The testbed is not defined for the table '{0}'.".
-                      format(table.get("title", "")))
+    _ = input_data
+
+    if not table.get(u"testbed", None):
+        logging.error(
+            f"The testbed is not defined for the table "
+            f"{table.get(u'title', u'')}. Skipping."
+        )
         return
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
+    test_type = table.get(u"test-type", u"MRR")
+    if test_type not in (u"MRR", u"NDR", u"PDR"):
+        logging.error(
+            f"Test type {table.get(u'test-type', u'MRR')} is not defined. "
+            f"Skipping."
+        )
+        return
+
+    if test_type in (u"NDR", u"PDR"):
+        lnk_dir = u"../ndrpdr_trending/"
+        lnk_sufix = f"-{test_type.lower()}"
+    else:
+        lnk_dir = u"../trending/"
+        lnk_sufix = u""
+
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
 
     try:
-        with open(table["input-file"], 'rb') as csv_file:
-            csv_content = csv.reader(csv_file, delimiter=',', quotechar='"')
-            csv_lst = [item for item in csv_content]
+        with open(table[u"input-file"], u'rt') as csv_file:
+            csv_lst = list(csv.reader(csv_file, delimiter=u',', quotechar=u'"'))
+    except FileNotFoundError as err:
+        logging.warning(f"{err}")
+        return
     except KeyError:
-        logging.warning("The input file is not defined.")
+        logging.warning(u"The input file is not defined.")
         return
     except csv.Error as err:
-        logging.warning("Not possible to process the file '{0}'.\n{1}".
-                        format(table["input-file"], err))
+        logging.warning(
+            f"Not possible to process the file {table[u'input-file']}.\n"
+            f"{repr(err)}"
+        )
         return
 
     # Table:
-    dashboard = ET.Element("table", attrib=dict(width="100%", border='0'))
+    dashboard = ET.Element(u"table", attrib=dict(width=u"100%", border=u'0'))
 
     # Table header:
-    tr = ET.SubElement(dashboard, "tr", attrib=dict(bgcolor="#7eade7"))
+    trow = ET.SubElement(dashboard, u"tr", attrib=dict(bgcolor=u"#7eade7"))
     for idx, item in enumerate(csv_lst[0]):
-        alignment = "left" if idx == 0 else "center"
-        th = ET.SubElement(tr, "th", attrib=dict(align=alignment))
-        th.text = item
+        alignment = u"left" if idx == 0 else u"center"
+        thead = ET.SubElement(trow, u"th", attrib=dict(align=alignment))
+        thead.text = item
 
     # Rows:
-    colors = {"regression": ("#ffcccc", "#ff9999"),
-              "progression": ("#c6ecc6", "#9fdf9f"),
-              "normal": ("#e9f1fb", "#d4e4f7")}
+    colors = {
+        u"regression": (
+            u"#ffcccc",
+            u"#ff9999"
+        ),
+        u"progression": (
+            u"#c6ecc6",
+            u"#9fdf9f"
+        ),
+        u"normal": (
+            u"#e9f1fb",
+            u"#d4e4f7"
+        )
+    }
     for r_idx, row in enumerate(csv_lst[1:]):
         if int(row[4]):
-            color = "regression"
+            color = u"regression"
         elif int(row[5]):
-            color = "progression"
+            color = u"progression"
         else:
-            color = "normal"
-        background = colors[color][r_idx % 2]
-        tr = ET.SubElement(dashboard, "tr", attrib=dict(bgcolor=background))
+            color = u"normal"
+        trow = ET.SubElement(
+            dashboard, u"tr", attrib=dict(bgcolor=colors[color][r_idx % 2])
+        )
 
         # Columns:
         for c_idx, item in enumerate(row):
-            alignment = "left" if c_idx == 0 else "center"
-            td = ET.SubElement(tr, "td", attrib=dict(align=alignment))
+            tdata = ET.SubElement(
+                trow,
+                u"td",
+                attrib=dict(align=u"left" if c_idx == 0 else u"center")
+            )
             # Name:
-            if c_idx == 0:
-                url = _generate_url("../trending/", testbed, item)
-                ref = ET.SubElement(td, "a", attrib=dict(href=url))
+            if c_idx == 0 and table.get(u"add-links", True):
+                ref = ET.SubElement(
+                    tdata,
+                    u"a",
+                    attrib=dict(
+                        href=f"{lnk_dir}"
+                        f"{_generate_url(table.get(u'testbed', ''), item)}"
+                        f"{lnk_sufix}"
+                    )
+                )
                 ref.text = item
             else:
-                td.text = item
+                tdata.text = item
     try:
-        with open(table["output-file"], 'w') as html_file:
-            logging.info("    Writing file: '{0}'".format(table["output-file"]))
-            html_file.write(".. raw:: html\n\n\t")
-            html_file.write(ET.tostring(dashboard))
-            html_file.write("\n\t<p><br><br></p>\n")
+        with open(table[u"output-file"], u'w') as html_file:
+            logging.info(f"    Writing file: {table[u'output-file']}")
+            html_file.write(u".. raw:: html\n\n\t")
+            html_file.write(str(ET.tostring(dashboard, encoding=u"unicode")))
+            html_file.write(u"\n\t<p><br><br></p>\n")
     except KeyError:
-        logging.warning("The output file is not defined.")
+        logging.warning(u"The output file is not defined.")
         return
 
 
@@ -1416,53 +1325,59 @@ def table_last_failed_tests(table, input_data):
     :type input_data: InputData
     """
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
 
     # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
+
     data = input_data.filter_data(table, continue_on_error=True)
 
     if data is None or data.empty:
-        logging.warn("    No data for the {0} '{1}'.".
-                     format(table.get("type", ""), table.get("title", "")))
+        logging.warning(
+            f"    No data for the {table.get(u'type', u'')} "
+            f"{table.get(u'title', u'')}."
+        )
         return
 
     tbl_list = list()
-    for job, builds in table["data"].items():
+    for job, builds in table[u"data"].items():
         for build in builds:
             build = str(build)
             try:
-                version = input_data.metadata(job, build).get("version", "")
+                version = input_data.metadata(job, build).get(u"version", u"")
+                duration = \
+                    input_data.metadata(job, build).get(u"elapsedtime", u"")
             except KeyError:
-                logging.error("Data for {job}: {build} is not present.".
-                              format(job=job, build=build))
+                logging.error(f"Data for {job}: {build} is not present.")
                 return
             tbl_list.append(build)
             tbl_list.append(version)
             failed_tests = list()
             passed = 0
             failed = 0
-            for tst_name, tst_data in data[job][build].iteritems():
-                if tst_data["status"] != "FAIL":
+            for tst_data in data[job][build].values:
+                if tst_data[u"status"] != u"FAIL":
                     passed += 1
                     continue
                 failed += 1
-                groups = re.search(REGEX_NIC, tst_data["parent"])
+                groups = re.search(REGEX_NIC, tst_data[u"parent"])
                 if not groups:
                     continue
                 nic = groups.group(0)
-                failed_tests.append("{0}-{1}".format(nic, tst_data["name"]))
-            tbl_list.append(str(passed))
-            tbl_list.append(str(failed))
+                failed_tests.append(f"{nic}-{tst_data[u'name']}")
+            tbl_list.append(passed)
+            tbl_list.append(failed)
+            tbl_list.append(duration)
             tbl_list.extend(failed_tests)
 
-    file_name = "{0}{1}".format(table["output-file"], table["output-file-ext"])
-    logging.info("    Writing file: '{0}'".format(file_name))
-    with open(file_name, "w") as file_handler:
+    file_name = f"{table[u'output-file']}{table[u'output-file-ext']}"
+    logging.info(f"    Writing file: {file_name}")
+    with open(file_name, u"wt") as file_handler:
         for test in tbl_list:
-            file_handler.write(test + '\n')
+            file_handler.write(f"{test}\n")
 
 
 def table_failed_tests(table, input_data):
@@ -1475,95 +1390,106 @@ def table_failed_tests(table, input_data):
     :type input_data: InputData
     """
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
 
     # Transform the data
-    logging.info("    Creating the data set for the {0} '{1}'.".
-                 format(table.get("type", ""), table.get("title", "")))
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
     data = input_data.filter_data(table, continue_on_error=True)
 
+    test_type = u"MRR"
+    if u"NDRPDR" in table.get(u"filter", list()):
+        test_type = u"NDRPDR"
+
     # Prepare the header of the tables
-    header = ["Test Case",
-              "Failures [#]",
-              "Last Failure [Time]",
-              "Last Failure [VPP-Build-Id]",
-              "Last Failure [CSIT-Job-Build-Id]"]
+    header = [
+        u"Test Case",
+        u"Failures [#]",
+        u"Last Failure [Time]",
+        u"Last Failure [VPP-Build-Id]",
+        u"Last Failure [CSIT-Job-Build-Id]"
+    ]
 
     # Generate the data for the table according to the model in the table
     # specification
 
     now = dt.utcnow()
-    timeperiod = timedelta(int(table.get("window", 7)))
+    timeperiod = timedelta(int(table.get(u"window", 7)))
 
     tbl_dict = dict()
-    for job, builds in table["data"].items():
+    for job, builds in table[u"data"].items():
         for build in builds:
             build = str(build)
-            for tst_name, tst_data in data[job][build].iteritems():
-                if tst_name.lower() in table.get("ignore-list", list()):
+            for tst_name, tst_data in data[job][build].items():
+                if tst_name.lower() in table.get(u"ignore-list", list()):
                     continue
                 if tbl_dict.get(tst_name, None) is None:
-                    groups = re.search(REGEX_NIC, tst_data["parent"])
+                    groups = re.search(REGEX_NIC, tst_data[u"parent"])
                     if not groups:
                         continue
                     nic = groups.group(0)
                     tbl_dict[tst_name] = {
-                        "name": "{0}-{1}".format(nic, tst_data["name"]),
-                        "data": OrderedDict()}
+                        u"name": f"{nic}-{tst_data[u'name']}",
+                        u"data": OrderedDict()
+                    }
                 try:
                     generated = input_data.metadata(job, build).\
-                        get("generated", "")
+                        get(u"generated", u"")
                     if not generated:
                         continue
-                    then = dt.strptime(generated, "%Y%m%d %H:%M")
+                    then = dt.strptime(generated, u"%Y%m%d %H:%M")
                     if (now - then) <= timeperiod:
-                        tbl_dict[tst_name]["data"][build] = (
-                            tst_data["status"],
+                        tbl_dict[tst_name][u"data"][build] = (
+                            tst_data[u"status"],
                             generated,
-                            input_data.metadata(job, build).get("version", ""),
-                            build)
+                            input_data.metadata(job, build).get(u"version",
+                                                                u""),
+                            build
+                        )
                 except (TypeError, KeyError) as err:
-                    logging.warning("tst_name: {} - err: {}".
-                                    format(tst_name, repr(err)))
+                    logging.warning(f"tst_name: {tst_name} - err: {repr(err)}")
 
     max_fails = 0
     tbl_lst = list()
     for tst_data in tbl_dict.values():
         fails_nr = 0
-        fails_last_date = ""
-        fails_last_vpp = ""
-        fails_last_csit = ""
-        for val in tst_data["data"].values():
-            if val[0] == "FAIL":
+        fails_last_date = u""
+        fails_last_vpp = u""
+        fails_last_csit = u""
+        for val in tst_data[u"data"].values():
+            if val[0] == u"FAIL":
                 fails_nr += 1
                 fails_last_date = val[1]
                 fails_last_vpp = val[2]
                 fails_last_csit = val[3]
         if fails_nr:
             max_fails = fails_nr if fails_nr > max_fails else max_fails
-            tbl_lst.append([tst_data["name"],
-                            fails_nr,
-                            fails_last_date,
-                            fails_last_vpp,
-                            "mrr-daily-build-{0}".format(fails_last_csit)])
+            tbl_lst.append([
+                tst_data[u"name"],
+                fails_nr,
+                fails_last_date,
+                fails_last_vpp,
+                f"{u'mrr-daily' if test_type == u'MRR' else u'ndrpdr-weekly'}"
+                f"-build-{fails_last_csit}"
+            ])
 
     tbl_lst.sort(key=lambda rel: rel[2], reverse=True)
     tbl_sorted = list()
     for nrf in range(max_fails, -1, -1):
         tbl_fails = [item for item in tbl_lst if item[1] == nrf]
         tbl_sorted.extend(tbl_fails)
-    file_name = "{0}{1}".format(table["output-file"], table["output-file-ext"])
 
-    logging.info("    Writing file: '{0}'".format(file_name))
-    with open(file_name, "w") as file_handler:
-        file_handler.write(",".join(header) + "\n")
+    file_name = f"{table[u'output-file']}{table[u'output-file-ext']}"
+    logging.info(f"    Writing file: {file_name}")
+    with open(file_name, u"wt") as file_handler:
+        file_handler.write(u",".join(header) + u"\n")
         for test in tbl_sorted:
-            file_handler.write(",".join([str(item) for item in test]) + '\n')
+            file_handler.write(u",".join([str(item) for item in test]) + u'\n')
 
-    txt_file_name = "{0}.txt".format(table["output-file"])
-    logging.info("    Writing file: '{0}'".format(txt_file_name))
-    convert_csv_to_pretty_txt(file_name, txt_file_name)
+    logging.info(f"    Writing file: {table[u'output-file']}.txt")
+    convert_csv_to_pretty_txt(file_name, f"{table[u'output-file']}.txt")
 
 
 def table_failed_tests_html(table, input_data):
@@ -1576,60 +1502,633 @@ def table_failed_tests_html(table, input_data):
     :type input_data: InputData
     """
 
-    testbed = table.get("testbed", None)
-    if testbed is None:
-        logging.error("The testbed is not defined for the table '{0}'.".
-                      format(table.get("title", "")))
+    _ = input_data
+
+    if not table.get(u"testbed", None):
+        logging.error(
+            f"The testbed is not defined for the table "
+            f"{table.get(u'title', u'')}. Skipping."
+        )
+        return
+
+    test_type = table.get(u"test-type", u"MRR")
+    if test_type not in (u"MRR", u"NDR", u"PDR", u"NDRPDR"):
+        logging.error(
+            f"Test type {table.get(u'test-type', u'MRR')} is not defined. "
+            f"Skipping."
+        )
         return
 
-    logging.info("  Generating the table {0} ...".
-                 format(table.get("title", "")))
+    if test_type in (u"NDRPDR", u"NDR", u"PDR"):
+        lnk_dir = u"../ndrpdr_trending/"
+        lnk_sufix = u"-pdr"
+    else:
+        lnk_dir = u"../trending/"
+        lnk_sufix = u""
+
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
 
     try:
-        with open(table["input-file"], 'rb') as csv_file:
-            csv_content = csv.reader(csv_file, delimiter=',', quotechar='"')
-            csv_lst = [item for item in csv_content]
+        with open(table[u"input-file"], u'rt') as csv_file:
+            csv_lst = list(csv.reader(csv_file, delimiter=u',', quotechar=u'"'))
     except KeyError:
-        logging.warning("The input file is not defined.")
+        logging.warning(u"The input file is not defined.")
         return
     except csv.Error as err:
-        logging.warning("Not possible to process the file '{0}'.\n{1}".
-                        format(table["input-file"], err))
+        logging.warning(
+            f"Not possible to process the file {table[u'input-file']}.\n"
+            f"{repr(err)}"
+        )
         return
 
     # Table:
-    failed_tests = ET.Element("table", attrib=dict(width="100%", border='0'))
+    failed_tests = ET.Element(u"table", attrib=dict(width=u"100%", border=u'0'))
 
     # Table header:
-    tr = ET.SubElement(failed_tests, "tr", attrib=dict(bgcolor="#7eade7"))
+    trow = ET.SubElement(failed_tests, u"tr", attrib=dict(bgcolor=u"#7eade7"))
     for idx, item in enumerate(csv_lst[0]):
-        alignment = "left" if idx == 0 else "center"
-        th = ET.SubElement(tr, "th", attrib=dict(align=alignment))
-        th.text = item
+        alignment = u"left" if idx == 0 else u"center"
+        thead = ET.SubElement(trow, u"th", attrib=dict(align=alignment))
+        thead.text = item
 
     # Rows:
-    colors = ("#e9f1fb", "#d4e4f7")
+    colors = (u"#e9f1fb", u"#d4e4f7")
     for r_idx, row in enumerate(csv_lst[1:]):
         background = colors[r_idx % 2]
-        tr = ET.SubElement(failed_tests, "tr", attrib=dict(bgcolor=background))
+        trow = ET.SubElement(
+            failed_tests, u"tr", attrib=dict(bgcolor=background)
+        )
 
         # Columns:
         for c_idx, item in enumerate(row):
-            alignment = "left" if c_idx == 0 else "center"
-            td = ET.SubElement(tr, "td", attrib=dict(align=alignment))
+            tdata = ET.SubElement(
+                trow,
+                u"td",
+                attrib=dict(align=u"left" if c_idx == 0 else u"center")
+            )
             # Name:
-            if c_idx == 0:
-                url = _generate_url("../trending/", testbed, item)
-                ref = ET.SubElement(td, "a", attrib=dict(href=url))
+            if c_idx == 0 and table.get(u"add-links", True):
+                ref = ET.SubElement(
+                    tdata,
+                    u"a",
+                    attrib=dict(
+                        href=f"{lnk_dir}"
+                        f"{_generate_url(table.get(u'testbed', ''), item)}"
+                        f"{lnk_sufix}"
+                    )
+                )
                 ref.text = item
             else:
-                td.text = item
+                tdata.text = item
     try:
-        with open(table["output-file"], 'w') as html_file:
-            logging.info("    Writing file: '{0}'".format(table["output-file"]))
-            html_file.write(".. raw:: html\n\n\t")
-            html_file.write(ET.tostring(failed_tests))
-            html_file.write("\n\t<p><br><br></p>\n")
+        with open(table[u"output-file"], u'w') as html_file:
+            logging.info(f"    Writing file: {table[u'output-file']}")
+            html_file.write(u".. raw:: html\n\n\t")
+            html_file.write(str(ET.tostring(failed_tests, encoding=u"unicode")))
+            html_file.write(u"\n\t<p><br><br></p>\n")
     except KeyError:
-        logging.warning("The output file is not defined.")
+        logging.warning(u"The output file is not defined.")
+        return
+
+
+def table_comparison(table, input_data):
+    """Generate the table(s) with algorithm: table_comparison
+    specified in the specification file.
+
+    :param table: Table to generate.
+    :param input_data: Data to process.
+    :type table: pandas.Series
+    :type input_data: InputData
+    """
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
+
+    # Transform the data
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
+
+    columns = table.get(u"columns", None)
+    if not columns:
+        logging.error(
+            f"No columns specified for {table.get(u'title', u'')}. Skipping."
+        )
+        return
+
+    cols = list()
+    for idx, col in enumerate(columns):
+        if col.get(u"data-set", None) is None:
+            logging.warning(f"No data for column {col.get(u'title', u'')}")
+            continue
+        tag = col.get(u"tag", None)
+        data = input_data.filter_data(
+            table,
+            params=[u"throughput", u"result", u"name", u"parent", u"tags"],
+            data=col[u"data-set"],
+            continue_on_error=True
+        )
+        col_data = {
+            u"title": col.get(u"title", f"Column{idx}"),
+            u"data": dict()
+        }
+        for builds in data.values:
+            for build in builds:
+                for tst_name, tst_data in build.items():
+                    if tag and tag not in tst_data[u"tags"]:
+                        continue
+                    tst_name_mod = \
+                        _tpc_modify_test_name(tst_name, ignore_nic=True).\
+                        replace(u"2n1l-", u"")
+                    if col_data[u"data"].get(tst_name_mod, None) is None:
+                        name = tst_data[u'name'].rsplit(u'-', 1)[0]
+                        if u"across testbeds" in table[u"title"].lower() or \
+                                u"across topologies" in table[u"title"].lower():
+                            name = _tpc_modify_displayed_test_name(name)
+                        col_data[u"data"][tst_name_mod] = {
+                            u"name": name,
+                            u"replace": True,
+                            u"data": list(),
+                            u"mean": None,
+                            u"stdev": None
+                        }
+                    _tpc_insert_data(
+                        target=col_data[u"data"][tst_name_mod],
+                        src=tst_data,
+                        include_tests=table[u"include-tests"]
+                    )
+
+        replacement = col.get(u"data-replacement", None)
+        if replacement:
+            rpl_data = input_data.filter_data(
+                table,
+                params=[u"throughput", u"result", u"name", u"parent", u"tags"],
+                data=replacement,
+                continue_on_error=True
+            )
+            for builds in rpl_data.values:
+                for build in builds:
+                    for tst_name, tst_data in build.items():
+                        if tag and tag not in tst_data[u"tags"]:
+                            continue
+                        tst_name_mod = \
+                            _tpc_modify_test_name(tst_name, ignore_nic=True).\
+                            replace(u"2n1l-", u"")
+                        if col_data[u"data"].get(tst_name_mod, None) is None:
+                            name = tst_data[u'name'].rsplit(u'-', 1)[0]
+                            if u"across testbeds" in table[u"title"].lower() \
+                                    or u"across topologies" in \
+                                    table[u"title"].lower():
+                                name = _tpc_modify_displayed_test_name(name)
+                            col_data[u"data"][tst_name_mod] = {
+                                u"name": name,
+                                u"replace": False,
+                                u"data": list(),
+                                u"mean": None,
+                                u"stdev": None
+                            }
+                        if col_data[u"data"][tst_name_mod][u"replace"]:
+                            col_data[u"data"][tst_name_mod][u"replace"] = False
+                            col_data[u"data"][tst_name_mod][u"data"] = list()
+                        _tpc_insert_data(
+                            target=col_data[u"data"][tst_name_mod],
+                            src=tst_data,
+                            include_tests=table[u"include-tests"]
+                        )
+
+        if table[u"include-tests"] in (u"NDR", u"PDR"):
+            for tst_name, tst_data in col_data[u"data"].items():
+                if tst_data[u"data"]:
+                    tst_data[u"mean"] = mean(tst_data[u"data"])
+                    tst_data[u"stdev"] = stdev(tst_data[u"data"])
+
+        cols.append(col_data)
+
+    tbl_dict = dict()
+    for col in cols:
+        for tst_name, tst_data in col[u"data"].items():
+            if tbl_dict.get(tst_name, None) is None:
+                tbl_dict[tst_name] = {
+                    "name": tst_data[u"name"]
+                }
+            tbl_dict[tst_name][col[u"title"]] = {
+                u"mean": tst_data[u"mean"],
+                u"stdev": tst_data[u"stdev"]
+            }
+
+    if not tbl_dict:
+        logging.warning(f"No data for table {table.get(u'title', u'')}!")
+        return
+
+    tbl_lst = list()
+    for tst_data in tbl_dict.values():
+        row = [tst_data[u"name"], ]
+        for col in cols:
+            row.append(tst_data.get(col[u"title"], None))
+        tbl_lst.append(row)
+
+    comparisons = table.get(u"comparisons", None)
+    rcas = list()
+    if comparisons and isinstance(comparisons, list):
+        for idx, comp in enumerate(comparisons):
+            try:
+                col_ref = int(comp[u"reference"])
+                col_cmp = int(comp[u"compare"])
+            except KeyError:
+                logging.warning(u"Comparison: No references defined! Skipping.")
+                comparisons.pop(idx)
+                continue
+            if not (0 < col_ref <= len(cols) and 0 < col_cmp <= len(cols) or
+                    col_ref == col_cmp):
+                logging.warning(f"Wrong values of reference={col_ref} "
+                                f"and/or compare={col_cmp}. Skipping.")
+                comparisons.pop(idx)
+                continue
+            rca_file_name = comp.get(u"rca-file", None)
+            if rca_file_name:
+                try:
+                    with open(rca_file_name, u"r") as file_handler:
+                        rcas.append(
+                            {
+                                u"title": f"RCA{idx + 1}",
+                                u"data": load(file_handler, Loader=FullLoader)
+                            }
+                        )
+                except (YAMLError, IOError) as err:
+                    logging.warning(
+                        f"The RCA file {rca_file_name} does not exist or "
+                        f"it is corrupted!"
+                    )
+                    logging.debug(repr(err))
+                    rcas.append(None)
+            else:
+                rcas.append(None)
+    else:
+        comparisons = None
+
+    tbl_cmp_lst = list()
+    if comparisons:
+        for row in tbl_lst:
+            new_row = deepcopy(row)
+            for comp in comparisons:
+                ref_itm = row[int(comp[u"reference"])]
+                if ref_itm is None and \
+                        comp.get(u"reference-alt", None) is not None:
+                    ref_itm = row[int(comp[u"reference-alt"])]
+                cmp_itm = row[int(comp[u"compare"])]
+                if ref_itm is not None and cmp_itm is not None and \
+                        ref_itm[u"mean"] is not None and \
+                        cmp_itm[u"mean"] is not None and \
+                        ref_itm[u"stdev"] is not None and \
+                        cmp_itm[u"stdev"] is not None:
+                    delta, d_stdev = relative_change_stdev(
+                        ref_itm[u"mean"], cmp_itm[u"mean"],
+                        ref_itm[u"stdev"], cmp_itm[u"stdev"]
+                    )
+                    if delta is None:
+                        break
+                    new_row.append({
+                        u"mean": delta * 1e6,
+                        u"stdev": d_stdev * 1e6
+                    })
+                else:
+                    break
+            else:
+                tbl_cmp_lst.append(new_row)
+
+    try:
+        tbl_cmp_lst.sort(key=lambda rel: rel[0], reverse=False)
+        tbl_cmp_lst.sort(key=lambda rel: rel[-1][u'mean'], reverse=True)
+    except TypeError as err:
+        logging.warning(f"Empty data element in table\n{tbl_cmp_lst}\n{err}")
+
+    tbl_for_csv = list()
+    for line in tbl_cmp_lst:
+        row = [line[0], ]
+        for idx, itm in enumerate(line[1:]):
+            if itm is None or not isinstance(itm, dict) or\
+                    itm.get(u'mean', None) is None or \
+                    itm.get(u'stdev', None) is None:
+                row.append(u"NT")
+                row.append(u"NT")
+            else:
+                row.append(round(float(itm[u'mean']) / 1e6, 3))
+                row.append(round(float(itm[u'stdev']) / 1e6, 3))
+        for rca in rcas:
+            if rca is None:
+                continue
+            rca_nr = rca[u"data"].get(row[0], u"-")
+            row.append(f"[{rca_nr}]" if rca_nr != u"-" else u"-")
+        tbl_for_csv.append(row)
+
+    header_csv = [u"Test Case", ]
+    for col in cols:
+        header_csv.append(f"Avg({col[u'title']})")
+        header_csv.append(f"Stdev({col[u'title']})")
+    for comp in comparisons:
+        header_csv.append(
+            f"Avg({comp.get(u'title', u'')})"
+        )
+        header_csv.append(
+            f"Stdev({comp.get(u'title', u'')})"
+        )
+    for rca in rcas:
+        if rca:
+            header_csv.append(rca[u"title"])
+
+    legend_lst = table.get(u"legend", None)
+    if legend_lst is None:
+        legend = u""
+    else:
+        legend = u"\n" + u"\n".join(legend_lst) + u"\n"
+
+    footnote = u""
+    if rcas and any(rcas):
+        footnote += u"\nRoot Cause Analysis:\n"
+        for rca in rcas:
+            if rca:
+                footnote += f"{rca[u'data'].get(u'footnote', u'')}\n"
+
+    csv_file_name = f"{table[u'output-file']}-csv.csv"
+    with open(csv_file_name, u"wt", encoding='utf-8') as file_handler:
+        file_handler.write(
+            u",".join([f'"{itm}"' for itm in header_csv]) + u"\n"
+        )
+        for test in tbl_for_csv:
+            file_handler.write(
+                u",".join([f'"{item}"' for item in test]) + u"\n"
+            )
+        if legend_lst:
+            for item in legend_lst:
+                file_handler.write(f'"{item}"\n')
+        if footnote:
+            for itm in footnote.split(u"\n"):
+                file_handler.write(f'"{itm}"\n')
+
+    tbl_tmp = list()
+    max_lens = [0, ] * len(tbl_cmp_lst[0])
+    for line in tbl_cmp_lst:
+        row = [line[0], ]
+        for idx, itm in enumerate(line[1:]):
+            if itm is None or not isinstance(itm, dict) or \
+                    itm.get(u'mean', None) is None or \
+                    itm.get(u'stdev', None) is None:
+                new_itm = u"NT"
+            else:
+                if idx < len(cols):
+                    new_itm = (
+                        f"{round(float(itm[u'mean']) / 1e6, 1)} "
+                        f"\u00B1{round(float(itm[u'stdev']) / 1e6, 1)}".
+                        replace(u"nan", u"NaN")
+                    )
+                else:
+                    new_itm = (
+                        f"{round(float(itm[u'mean']) / 1e6, 1):+} "
+                        f"\u00B1{round(float(itm[u'stdev']) / 1e6, 1)}".
+                        replace(u"nan", u"NaN")
+                    )
+            if len(new_itm.rsplit(u" ", 1)[-1]) > max_lens[idx]:
+                max_lens[idx] = len(new_itm.rsplit(u" ", 1)[-1])
+            row.append(new_itm)
+
+        tbl_tmp.append(row)
+
+    header = [u"Test Case", ]
+    header.extend([col[u"title"] for col in cols])
+    header.extend([comp.get(u"title", u"") for comp in comparisons])
+
+    tbl_final = list()
+    for line in tbl_tmp:
+        row = [line[0], ]
+        for idx, itm in enumerate(line[1:]):
+            if itm in (u"NT", u"NaN"):
+                row.append(itm)
+                continue
+            itm_lst = itm.rsplit(u"\u00B1", 1)
+            itm_lst[-1] = \
+                f"{u' ' * (max_lens[idx] - len(itm_lst[-1]))}{itm_lst[-1]}"
+            itm_str = u"\u00B1".join(itm_lst)
+
+            if idx >= len(cols):
+                # Diffs
+                rca = rcas[idx - len(cols)]
+                if rca:
+                    # Add rcas to diffs
+                    rca_nr = rca[u"data"].get(row[0], None)
+                    if rca_nr:
+                        hdr_len = len(header[idx + 1]) - 1
+                        if hdr_len < 19:
+                            hdr_len = 19
+                        rca_nr = f"[{rca_nr}]"
+                        itm_str = (
+                            f"{u' ' * (4 - len(rca_nr))}{rca_nr}"
+                            f"{u' ' * (hdr_len - 4 - len(itm_str))}"
+                            f"{itm_str}"
+                        )
+            row.append(itm_str)
+        tbl_final.append(row)
+
+    # Generate csv tables:
+    csv_file_name = f"{table[u'output-file']}.csv"
+    logging.info(f"    Writing the file {csv_file_name}")
+    with open(csv_file_name, u"wt", encoding='utf-8') as file_handler:
+        file_handler.write(u";".join(header) + u"\n")
+        for test in tbl_final:
+            file_handler.write(u";".join([str(item) for item in test]) + u"\n")
+
+    # Generate txt table:
+    txt_file_name = f"{table[u'output-file']}.txt"
+    logging.info(f"    Writing the file {txt_file_name}")
+    convert_csv_to_pretty_txt(csv_file_name, txt_file_name, delimiter=u";")
+
+    with open(txt_file_name, u'a', encoding='utf-8') as file_handler:
+        file_handler.write(legend)
+        file_handler.write(footnote)
+
+    # Generate html table:
+    _tpc_generate_html_table(
+        header,
+        tbl_final,
+        table[u'output-file'],
+        legend=legend,
+        footnote=footnote,
+        sort_data=False,
+        title=table.get(u"title", u"")
+    )
+
+
+def table_weekly_comparison(table, in_data):
+    """Generate the table(s) with algorithm: table_weekly_comparison
+    specified in the specification file.
+
+    :param table: Table to generate.
+    :param in_data: Data to process.
+    :type table: pandas.Series
+    :type in_data: InputData
+    """
+    logging.info(f"  Generating the table {table.get(u'title', u'')} ...")
+
+    # Transform the data
+    logging.info(
+        f"    Creating the data set for the {table.get(u'type', u'')} "
+        f"{table.get(u'title', u'')}."
+    )
+
+    incl_tests = table.get(u"include-tests", None)
+    if incl_tests not in (u"NDR", u"PDR"):
+        logging.error(f"Wrong tests to include specified ({incl_tests}).")
+        return
+
+    nr_cols = table.get(u"nr-of-data-columns", None)
+    if not nr_cols or nr_cols < 2:
+        logging.error(
+            f"No columns specified for {table.get(u'title', u'')}. Skipping."
+        )
+        return
+
+    data = in_data.filter_data(
+        table,
+        params=[u"throughput", u"result", u"name", u"parent", u"tags"],
+        continue_on_error=True
+    )
+
+    header = [
+        [u"VPP Version", ],
+        [u"Start Timestamp", ],
+        [u"CSIT Build", ],
+        [u"CSIT Testbed", ]
+    ]
+    tbl_dict = dict()
+    idx = 0
+    tb_tbl = table.get(u"testbeds", None)
+    for job_name, job_data in data.items():
+        for build_nr, build in job_data.items():
+            if idx >= nr_cols:
+                break
+            if build.empty:
+                continue
+
+            tb_ip = in_data.metadata(job_name, build_nr).get(u"testbed", u"")
+            if tb_ip and tb_tbl:
+                testbed = tb_tbl.get(tb_ip, u"")
+            else:
+                testbed = u""
+            header[2].insert(1, build_nr)
+            header[3].insert(1, testbed)
+            header[1].insert(
+                1, in_data.metadata(job_name, build_nr).get(u"generated", u"")
+            )
+            header[0].insert(
+                1, in_data.metadata(job_name, build_nr).get(u"version", u"")
+            )
+
+            for tst_name, tst_data in build.items():
+                tst_name_mod = \
+                    _tpc_modify_test_name(tst_name).replace(u"2n1l-", u"")
+                if not tbl_dict.get(tst_name_mod, None):
+                    tbl_dict[tst_name_mod] = dict(
+                        name=tst_data[u'name'].rsplit(u'-', 1)[0],
+                    )
+                try:
+                    tbl_dict[tst_name_mod][-idx - 1] = \
+                        tst_data[u"throughput"][incl_tests][u"LOWER"]
+                except (TypeError, IndexError, KeyError, ValueError):
+                    pass
+            idx += 1
+
+    if idx < nr_cols:
+        logging.error(u"Not enough data to build the table! Skipping")
         return
+
+    cmp_dict = dict()
+    for idx, cmp in enumerate(table.get(u"comparisons", list())):
+        idx_ref = cmp.get(u"reference", None)
+        idx_cmp = cmp.get(u"compare", None)
+        if idx_ref is None or idx_cmp is None:
+            continue
+        header[0].append(
+            f"Diff({header[0][idx_ref - idx].split(u'~')[-1]} vs "
+            f"{header[0][idx_cmp - idx].split(u'~')[-1]})"
+        )
+        header[1].append(u"")
+        header[2].append(u"")
+        header[3].append(u"")
+        for tst_name, tst_data in tbl_dict.items():
+            if not cmp_dict.get(tst_name, None):
+                cmp_dict[tst_name] = list()
+            ref_data = tst_data.get(idx_ref, None)
+            cmp_data = tst_data.get(idx_cmp, None)
+            if ref_data is None or cmp_data is None:
+                cmp_dict[tst_name].append(float(u'nan'))
+            else:
+                cmp_dict[tst_name].append(
+                    relative_change(ref_data, cmp_data)
+                )
+
+    tbl_lst_none = list()
+    tbl_lst = list()
+    for tst_name, tst_data in tbl_dict.items():
+        itm_lst = [tst_data[u"name"], ]
+        for idx in range(nr_cols):
+            item = tst_data.get(-idx - 1, None)
+            if item is None:
+                itm_lst.insert(1, None)
+            else:
+                itm_lst.insert(1, round(item / 1e6, 1))
+        itm_lst.extend(
+            [
+                None if itm is None else round(itm, 1)
+                for itm in cmp_dict[tst_name]
+            ]
+        )
+        if str(itm_lst[-1]) == u"nan" or itm_lst[-1] is None:
+            tbl_lst_none.append(itm_lst)
+        else:
+            tbl_lst.append(itm_lst)
+
+    tbl_lst_none.sort(key=lambda rel: rel[0], reverse=False)
+    tbl_lst.sort(key=lambda rel: rel[0], reverse=False)
+    tbl_lst.sort(key=lambda rel: rel[-1], reverse=False)
+    tbl_lst.extend(tbl_lst_none)
+
+    # Generate csv table:
+    csv_file_name = f"{table[u'output-file']}.csv"
+    logging.info(f"    Writing the file {csv_file_name}")
+    with open(csv_file_name, u"wt", encoding='utf-8') as file_handler:
+        for hdr in header:
+            file_handler.write(u",".join(hdr) + u"\n")
+        for test in tbl_lst:
+            file_handler.write(u",".join(
+                [
+                    str(item).replace(u"None", u"-").replace(u"nan", u"-").
+                    replace(u"null", u"-") for item in test
+                ]
+            ) + u"\n")
+
+    txt_file_name = f"{table[u'output-file']}.txt"
+    logging.info(f"    Writing the file {txt_file_name}")
+    convert_csv_to_pretty_txt(csv_file_name, txt_file_name, delimiter=u",")
+
+    # Reorganize header in txt table
+    txt_table = list()
+    with open(txt_file_name, u"rt", encoding='utf-8') as file_handler:
+        for line in list(file_handler):
+            txt_table.append(line)
+    try:
+        txt_table.insert(5, txt_table.pop(2))
+        with open(txt_file_name, u"wt", encoding='utf-8') as file_handler:
+            file_handler.writelines(txt_table)
+    except IndexError:
+        pass
+
+    # Generate html table:
+    hdr_html = [
+        u"<br>".join(row) for row in zip(*header)
+    ]
+    _tpc_generate_html_table(
+        hdr_html,
+        tbl_lst,
+        table[u'output-file'],
+        sort_data=True,
+        title=table.get(u"title", u""),
+        generate_rst=False
+    )