Add report tools for report stage at SI pipeline
diff --git a/report-tools/adaptors/__init__.py b/report-tools/adaptors/__init__.py
new file mode 100644
index 0000000..80ef809
--- /dev/null
+++ b/report-tools/adaptors/__init__.py
@@ -0,0 +1,14 @@
+__copyright__ = """
+/*
+ * Copyright (c) 2021, Arm Limited. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ */
+ """
+
+""" __init__.py:
+
+    __init__.py for complexity parser
+
+"""
diff --git a/report-tools/adaptors/sql/__init__.py b/report-tools/adaptors/sql/__init__.py
new file mode 100644
index 0000000..80ef809
--- /dev/null
+++ b/report-tools/adaptors/sql/__init__.py
@@ -0,0 +1,14 @@
+__copyright__ = """
+/*
+ * Copyright (c) 2021, Arm Limited. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ */
+ """
+
+""" __init__.py:
+
+    __init__.py for complexity parser
+
+"""
diff --git a/report-tools/adaptors/sql/yaml_parser.py b/report-tools/adaptors/sql/yaml_parser.py
index b0ce15e..9e6aa17 100755
--- a/report-tools/adaptors/sql/yaml_parser.py
+++ b/report-tools/adaptors/sql/yaml_parser.py
@@ -1,116 +1,113 @@
-##############################################################################

-# Copyright (c) 2021, ARM Limited and Contributors. All rights reserved.

-#

-# SPDX-License-Identifier: BSD-3-Clause

-##############################################################################

-"""

-Class to parse .yaml file to generate a report.db

-"""

-import sys

-import yaml

-import sqlite

-

-

-class YAMLParser:

-    """

-    Class to represent a YAML Parser and creates database

-

-    Methods:

-        create_table: Creates sqlite db table with necessary fields.

-        parse_file: Parses the yaml file to obtain necessary data for the test result table and updates it.

-        update_test_config_table: Parses the yaml file to obtain necessary data fot the test config table and updates it

-    """

-    root_string = ""

-    test_suite_list = []

-

-    # contents of the test_config table

-    test_config_table = [

-        "build_id",

-        "target",

-        "bitbake_version",

-        "yocto_version"

-    ]

-

-    # contents of test_result table

-    test_result_table = [

-        "build_id",

-        "date",

-        "test_suite",

-        "test_case",

-        "result"

-    ]

-

-    def __init__(self, file_name=sys.argv[1]):

-        """Creates an instance for sqlite_obj and loads the contents of the yamlfile to be parsed """

-

-        try:

-            self.sqlite_obj = sqlite.Database("report.db")

-            with open(file_name) as file:

-                self.contents = yaml.load(file, Loader=yaml.FullLoader)

-                self.root_string = [i for i in self.contents.keys()][0]

-        except Exception as err:

-            print(err)

-

-    def create_table(self):

-        """Creates empty tables in the sqlite database from the contents of test_config_table and test_result_table"""

-

-        test_config_query = """

-        CREATE TABLE `test_configuration` (

-        {0} TEXT,

-        {1} TEXT,

-        {2} TEXT,

-        {3} TEXT,

-        PRIMARY KEY ({0})

-        );

-        """.format(self.test_config_table[0], self.test_config_table[1], self.test_config_table[2],

-                   self.test_config_table[3])

-

-        test_results_query = """

-        CREATE TABLE `test_results` (

-        {0} TEXT,

-        {1} TEXT,

-        {2} TEXT,

-        {3} TEXT,

-        {4} TEXT,

-        FOREIGN KEY ({0}) REFERENCES `test_configuration`({0})

-        );

-        """.format(self.test_result_table[0], self.test_result_table[1], self.test_result_table[2],

-                   self.test_result_table[3], self.test_result_table[4])

-

-        self.sqlite_obj.execute_query(test_config_query)

-        self.sqlite_obj.execute_query(test_results_query)

-

-    def parse_file(self):

-        """Parses the yaml file"""

-

-        build_id = self.contents[self.root_string]['metadata']['CI_PIPELINE_ID']

-        # dependent on the generated yaml file. Code will be uncommented based

-        # on the yaml file

-        # self.contents[self.root_string]['metadata']['CI_COMMIT_TIMESTAMP']

-        date = ""

-        for test_suite in self.contents[self.root_string]['test-suites'].keys():

-            for test_case in self.contents[self.root_string]['test-suites'][test_suite]['test-results'].keys():

-                result = self.contents[self.root_string]['test-suites'][test_suite]['test-results'][test_case]["status"]

-                update_result_table_query = "INSERT INTO test_results VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')". \

-                    format(build_id, date, test_suite, test_case, result)

-                self.sqlite_obj.execute_query(update_result_table_query)

-

-    def update_test_config_table(self):

-        """Updates tables in the report.db with the values from the yaml file"""

-

-        build_id = self.contents[self.root_string]['metadata']['CI_PIPELINE_ID']

-        target = self.contents[self.root_string]['target']['platform'] + \

-            "_" + self.contents[self.root_string]['target']['version']

-

-        bitbake_version = "1.0"

-        yocto_version = "2.0"

-        update_table_query = "INSERT INTO test_configuration VALUES ('{0}', '{1}', '{2}', '{3}')".\

-            format(build_id, target, bitbake_version, yocto_version)

-        self.sqlite_obj.execute_query(update_table_query)

-

-

-if __name__ == "__main__":

-    yaml_obj = YAMLParser()

-    yaml_obj.create_table()

-    yaml_obj.parse_file()

-    yaml_obj.update_test_config_table()

+##############################################################################
+# Copyright (c) 2021, ARM Limited and Contributors. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+##############################################################################
+"""
+Class to parse .yaml file to generate a report.db
+"""
+import sys
+import yaml
+import adaptors.sql.sqlite as sqlite
+
+class YAMLParser:
+    """
+    Class to represent a YAML Parser and creates database
+
+    Methods:
+        create_table: Creates sqlite db table with necessary fields.
+        parse_file: Parses the yaml file to obtain necessary data for the test result table and updates it.
+        update_test_config_table: Parses the yaml file to obtain necessary data fot the test config table and updates it
+    """
+    root_string = ""
+    test_suite_list = []
+
+    # contents of the test_config table
+    test_config_table = [
+        "build_id",
+        "target",
+        "bitbake_version",
+        "yocto_version"
+    ]
+
+    # contents of test_result table
+    test_result_table = [
+        "build_id",
+        "date",
+        "test_suite",
+        "test_case",
+        "result"
+    ]
+
+    def __init__(self, file_name=""):
+        """Creates an instance for sqlite_obj and loads the contents of the yamlfile to be parsed """
+
+        try:
+            self.sqlite_obj = sqlite.Database("report.db")
+            with open(file_name) as file:
+                self.contents = yaml.load(file)
+                self.root_string = [i for i in self.contents.keys()][0]
+        except Exception as err:
+            print(err)
+
+    def create_table(self):
+        """Creates empty tables in the sqlite database from the contents of test_config_table and test_result_table"""
+
+        test_config_query = """
+        CREATE TABLE `test_configuration` (
+        {0} TEXT,
+        {1} TEXT,
+        {2} TEXT,
+        {3} TEXT,
+        PRIMARY KEY ({0})
+        );
+        """.format(self.test_config_table[0], self.test_config_table[1], self.test_config_table[2],
+                   self.test_config_table[3])
+
+        test_results_query = """
+        CREATE TABLE `test_results` (
+        {0} TEXT,
+        {1} TEXT,
+        {2} TEXT,
+        {3} TEXT,
+        {4} TEXT,
+        FOREIGN KEY ({0}) REFERENCES `test_configuration`({0})
+        );
+        """.format(self.test_result_table[0], self.test_result_table[1], self.test_result_table[2],
+                   self.test_result_table[3], self.test_result_table[4])
+
+        self.sqlite_obj.execute_query(test_config_query)
+        self.sqlite_obj.execute_query(test_results_query)
+
+    def parse_file(self):
+        """Parses the yaml file"""
+
+        build_id = self.contents[self.root_string]['metadata']['CI_PIPELINE_ID']
+        for test_suite in self.contents[self.root_string]['test-suites'].keys():
+            date = self.contents[self.root_string]['test-suites'][test_suite]['metadata']['DATE']
+            for test_case in self.contents[self.root_string]['test-suites'][test_suite]['test-results'].keys():
+                result = self.contents[self.root_string]['test-suites'][test_suite]['test-results'][test_case]["status"]
+                update_result_table_query = "INSERT INTO test_results VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')". \
+                    format(build_id, date, test_suite, test_case, result)
+                self.sqlite_obj.execute_query(update_result_table_query)
+
+    def update_test_config_table(self):
+        """Updates tables in the report.db with the values from the yaml file"""
+
+        build_id = self.contents[self.root_string]['metadata']['CI_PIPELINE_ID']
+        target = self.contents[self.root_string]['target']['platform'] + \
+            "_" + self.contents[self.root_string]['target']['version']
+
+        bitbake_version = "UNAVAILABLE"
+        yocto_version = "UNAVAILABLE"
+        update_table_query = "INSERT INTO test_configuration VALUES ('{0}', '{1}', '{2}', '{3}')".\
+            format(build_id, target, bitbake_version, yocto_version)
+        self.sqlite_obj.execute_query(update_table_query)
+
+
+if __name__ == "__main__":
+    yaml_obj = YAMLParser()
+    yaml_obj.create_table()
+    yaml_obj.parse_file()
+    yaml_obj.update_test_config_table()
+ 
diff --git a/report-tools/generate_report.py b/report-tools/generate_report.py
index 7c70050..53087c1 100644
--- a/report-tools/generate_report.py
+++ b/report-tools/generate_report.py
@@ -12,7 +12,13 @@
 import argparse
 import os
 import logging
-import time
+import subprocess
+import sys
+import json
+from adaptors.sql.yaml_parser import YAMLParser
+import glob
+
+HTML_TEMPLATE = "html.tar.gz"
 
 
 class TCReport(object):
@@ -55,13 +61,13 @@
             #               {report properties}
             try:
                 with open(report_file) as f:
-                    full_report = yaml.full_load(f)
+                    full_report = yaml.load(f)
                     self._report_name, \
                     self.report = list(full_report.items())[0]
-            except Exception as e:
+            except Exception as ex:
                 logging.exception(
                     f"Exception loading existing report '{report_file}'")
-                raise Exception(e)
+                raise ex
         else:
             self.report = {'metadata': metadata,
                            'test-environments': test_environments,
@@ -69,6 +75,7 @@
                            'target': target,
                            'test-suites': test_suites
                            }
+        self.report_file = report_file
 
     def dump(self, file_name):
         """
@@ -181,7 +188,7 @@
                         break
                     p = pattern.match(rest[0])
                     if p:
-                        id = p.groupdict()['id'].replace(" ", "_")
+                        id = re.sub("[ :]+", "_", p.groupdict()['id'])
                         status = p.groupdict()['status']
                         if not id:
                             print("Warning: missing 'id'")
@@ -230,6 +237,40 @@
     def metadata(self, metadata):
         self.report['metadata'] = metadata
 
+    @property
+    def target(self):
+        return self.report['target']
+
+    @target.setter
+    def target(self, target):
+        self.report['target'] = target
+
+    def merge_into(self, other):
+        """
+        Merge one report object with this.
+
+        :param other: Report object to be merged to this
+        :return:
+        """
+        try:
+            if not self.report_name or self.report_name == "Not-defined":
+                self.report_name = other.report_name
+            if self.report_name != other.report_name:
+                logging.warning(
+                    f'Report name \'{other.report_name}\' does not match '
+                    f'original report name')
+                # Merge metadata where 'other' report will overwrite common key
+            # values
+            self.metadata.update(other.metadata)
+            self.target.update(other.target)
+            self.test_config['test-assets'].update(other.test_config['test'
+                                                                     '-assets'])
+            self.test_environments.update(other.test_environments)
+            self.test_suites.update(other.test_suites)
+        except Exception as ex:
+            logging.exception("Failed to merge reports")
+            raise ex
+            
 
 class KvDictAppendAction(argparse.Action):
     """
@@ -244,7 +285,8 @@
                 (k, v) = value.split("=", 2)
             except ValueError as ex:
                 raise \
-                    argparse.ArgumentError(self, f"Could not parse argument '{values[0]}' as k=v format")
+                    argparse.ArgumentError(self,
+                                           f"Could not parse argument '{values[0]}' as k=v format")
             d[k] = v
         setattr(args, self.dest, d)
 
@@ -308,8 +350,20 @@
     # Get the test results
     results = {}
     if _args.type == 'ptest-report':
+        results_pattern = None
+        suite = _args.suite or _args.test_suite_name
+        if suite == "optee-test":
+            results_pattern = r"(?P<status>(PASS|FAIL|SKIP)): (?P<id>.+ .+) " \
+                              r"- (?P<description>.+)"
+        elif suite == "kernel-selftest":
+            results_pattern = r"(?P<status>(PASS|FAIL|SKIP)): (" \
+                              r"?P<description>selftests): (?P<id>.+: .+)"
+        else:
+            logging.error(f"Suite type uknown or not defined:'{suite}'")
+            sys.exit(-1)
+
         results = TCReport.process_ptest_results(lava_log,
-                                                 results_pattern=_args.results_pattern)
+                                                 results_pattern=results_pattern)
     if _args.report_name:
         _report.report_name = _args.report_name
     _report.parse_fvp_model_version(lava_log)
@@ -321,13 +375,53 @@
                            metadata=metadata)
 
 
+def merge_reports(reportObj, list_reports):
+    """
+    Function to merge a list of yaml report files into a report object
+
+    :param reportObj: Instance of an initial report object to merge the reports
+    :param list_reports: List of yaml report files or file patterns
+    :return: Updated report object
+    """
+    for report_pattern in list_reports:
+        for report_file in glob.glob(report_pattern):
+            to_merge = TCReport(report_file=report_file)
+            reportObj.merge_into(to_merge)
+    return reportObj
+
+
+def generate_html(report_obj, user_args):
+    """
+    Generate html output for the given report_file
+
+    :param report_obj: report object
+    :param user_args: Arguments from user
+    :return: Nothing
+    """
+    script_path = os.path.dirname(sys.argv[0])
+    report_file = user_args.report
+    try:
+        with open(script_path + "/html/js/reporter.js", "a") as write_file:
+            for key in args.html_output:
+                print(f'\nSetting html var "{key}"...')
+                write_file.write(f"\nlet {key}='{args.html_output[key]}'")
+            j = json.dumps({report_obj.report_name: report_obj.report},
+                           indent=4)
+            write_file.write(f"\nlet textReport=`\n{j}\n`")
+        subprocess.run(f'cp -f {report_file} {script_path}/html/report.yaml',
+                       shell=True)
+    except subprocess.CalledProcessError as ex:
+        logging.exception("Error at generating html")
+        raise ex
+
+
 if __name__ == '__main__':
     # Defining logger
     logging.basicConfig(
         level=logging.INFO,
         format="%(asctime)s [%(levelname)s] %(message)s",
         handlers=[
-            logging.FileHandler("log_parser_{}.log".format(time.time())),
+            logging.FileHandler("debug.log"),
             logging.StreamHandler()
         ])
     """
@@ -347,11 +441,10 @@
                                default='ptest-report')
     group_results.add_argument('--report-name', type=str, help='Report name',
                                default="")
-    group_results.add_argument('--results-pattern', type=str,
-                               help='Regex pattern to extract test results',
-                               default=r"(?P<status>(PASS|FAIL|SKIP)): ("
-                                       r"?P<id>.+ .+) - ( "
-                                       r"?P<description>.+)")
+    group_results.add_argument("--suite", required=False,
+                               default=None,
+                               help="Suite type. If not defined takes the "
+                                    "suite name value")
     test_env = parser.add_argument_group("Test environments")
     test_env.add_argument('--test-env-name', type=str,
                           help='Test environment type')
@@ -416,9 +509,31 @@
                         default=None,
                         help="File with key-value pairs lines i.e"
                              "key1=value1\nkey2=value2")
+                             
+    parser.add_argument("--list",
+                        nargs="+",
+                        default={},
+                        help="List of report files.")
+    parser.add_argument("--html-output",
+                        required=False,
+                        nargs="*",
+                        action=KvDictAppendAction,
+                        default={},
+                        metavar="KEY=VALUE",
+                        help="Set a number of key-value pairs i.e. key=value"
+                             "(do not put spaces before or after the = "
+                             "sign). "
+                             "If a value contains spaces, you should define "
+                             "it with double quotes: "
+                             "Valid keys: title, logo_img, logo_href.")
+    parser.add_argument("--sql-output",
+                        required=False,
+                        action="store_true",
+                        help='Sql output produced from the report file')
 
     args = parser.parse_args()
     report = None
+
     # Check if report exists (that can be overwritten) or is a new report
     if os.path.exists(args.report) and not args.new_report:
         report = TCReport(report_file=args.report)  # load existing report
@@ -451,6 +566,14 @@
         report.add_test_asset(args.test_asset_name,
                               merge_dicts(args.test_asset_values,
                                           import_env(args.test_asset_env)))
-
-    # Dump the report object as a yaml report
+    elif args.command == "merge-reports":
+        report = merge_reports(report, args.list)
     report.dump(args.report)
+    if args.html_output:
+        generate_html(report, args)
+
+    if args.sql_output:
+        yaml_obj = YAMLParser(args.report)
+        yaml_obj.create_table()
+        yaml_obj.parse_file()
+        yaml_obj.update_test_config_table()
diff --git a/report-tools/html/css/styles.css b/report-tools/html/css/styles.css
new file mode 100644
index 0000000..58924cb
--- /dev/null
+++ b/report-tools/html/css/styles.css
@@ -0,0 +1,236 @@
+/*##############################################################################
+
+# Copyright (c) 2021, ARM Limited and Contributors. All rights reserved.
+
+#
+
+# SPDX-License-Identifier: BSD-3-Clause
+
+##############################################################################*/
+* {
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+body {
+    line-height: 1.7em;
+    color: #7f8c8d;
+    font-size: 13px;
+    font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+label {
+    color: #34495e;
+    margin: 5px;
+}
+
+.home-menu {
+    padding: 10px;
+    text-align: center;
+    box-shadow: 0 1px 1px rgba(0,0,0, 0.10);
+}
+.home-menu {
+    background: #0091bd;
+    font-weight: 600;
+    font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif;
+    font-size: 16px;
+    color: white;
+}
+
+.home-menu a {
+    color: #6FBEF3;
+}
+.home-menu li a:hover,
+.home-menu li a:focus {
+    background: none;
+    border: none;
+    color: #AECFE5;
+}
+
+
+.block-report {
+    font-size: 18px;
+    font-weight: bold;
+    color: #129fea;
+    border: 1px solid #b9560f;
+    padding: 5px;
+    font-weight: 100;
+    border-radius: 5px;
+    line-height: 1em;
+    margin: 10px;
+}
+.block-report div {
+    font-size: 14px;
+    font-weight: bold;
+    padding-bottom: 5px;
+}
+
+.content-wrapper {
+    /* These styles are required for the "scroll-over" effect */
+    position: absolute;
+    top: 10%;
+    width: 100%;
+    min-height: 12%;
+    z-index: 2;
+    background: white;
+
+}
+
+.content {
+    padding: 1em 1em 3em;
+}
+
+.home-menu {
+    text-align: left;
+}
+.home-menu ul {
+    float: right;
+}
+
+.switch {
+  position: relative;
+  display: inline-block;
+  width: 30px;
+  height: 17px;
+}
+
+.switch input {
+  opacity: 0;
+  width: 0;
+  height: 0;
+}
+
+.slider {
+  position: absolute;
+  cursor: pointer;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: #ccc;
+  -webkit-transition: .4s;
+  transition: .4s;
+}
+
+.slider:before {
+  position: absolute;
+  content: "";
+  height: 13px;
+  width: 13px;
+  left: 2px;
+  bottom: 2px;
+  background-color: white;
+  -webkit-transition: .4s;
+  transition: .4s;
+}
+
+input:checked + .slider {
+  background-color: #2196F3;
+}
+
+input:focus + .slider {
+  box-shadow: 0 0 1px #2196F3;
+}
+
+input:checked + .slider:before {
+  -webkit-transform: translateX(13px);
+  -ms-transform: translateX(13px);
+  transform: translateX(13px);
+}
+
+.slider.round {
+  border-radius: 17px;
+}
+
+.slider.round:before {
+  border-radius: 50%;
+}
+
+.table-wrap {
+  height: 200px;
+  overflow-y: auto;
+  width: 800px;
+  overflow-x: auto;
+}
+
+.styled-table {
+    border-collapse: collapse;
+    margin: 25px 0;
+    font-size: 0.9em;
+    font-family: sans-serif;
+    min-width: 400px;
+    box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
+}
+
+.styled-table thead tr {
+    background-color: #009879;
+    color: #ffffff;
+    text-align: left;
+}
+
+.styled-table th,
+.styled-table td {
+    padding: 12px 15px;
+}
+
+.styled-table tbody tr {
+    border-bottom: 1px solid #dddddd;
+}
+
+.styled-table tbody tr:nth-of-type(even) {
+    background-color: #f3f3f3;
+}
+
+.styled-table tbody tr:last-of-type {
+    border-bottom: 2px solid #009879;
+}
+
+.styled-table tbody tr.active-row {
+    font-weight: bold;
+    color: #009879;
+}
+
+.styled-table tr:hover td{
+    background-color: #ffff99;
+}
+
+span.link {
+    cursor:pointer;
+    color:blue;
+}
+
+span.item {
+    background: white;
+}
+
+div.item {
+	display: inline-block;
+	letter-spacing: normal;
+	word-spacing: normal;
+	vertical-align: top;
+	text-rendering: auto;
+	padding-right: 10px;
+}
+
+div-item-parent {
+	letter-spacing: -.31em;
+	text-rendering: optimizespeed;
+	display: -webkit-box;
+	display: -ms-flexbox;
+	display: flex;
+	-webkit-box-orient: horizontal;
+	-webkit-box-direction: normal;
+	-ms-flex-flow: row wrap;
+	flex-flow: row wrap;
+	-ms-flex-line-pack: start;
+	align-content: flex-start;
+    word-spacing: -0.43em;
+}
+
diff --git a/report-tools/html/index.html b/report-tools/html/index.html
new file mode 100644
index 0000000..003c039
--- /dev/null
+++ b/report-tools/html/index.html
@@ -0,0 +1,49 @@
+<!--##############################################################################
+
+# Copyright (c) 2021, ARM Limited and Contributors. All rights reserved.
+
+#
+
+# SPDX-License-Identifier: BSD-3-Clause
+
+##############################################################################-->
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="Report Pipeline Template">
+    <link rel="stylesheet" href="css/styles.css">
+    <script type="text/javascript" src="js/reporter.js"></script>
+</head>
+<body>
+    <div>
+        <div class="home-menu">
+            <a href="" target="_blank" id="linkLogo"><img id="imgLogo"></a>
+            <span style="padding:10px" id="spanTitle"></span>
+        </div>
+    </div>
+
+
+</div>
+    <div class="content-wrapper">
+        <div class="content" id="report-content">
+            <h2 id="report-name"></h2>
+        </div>
+    </div>
+    <script>
+(function() {
+    document.title = (typeof title === 'undefined') ? 'Report' : title;
+    document.getElementById("spanTitle").innerHTML = document.title
+    document.getElementById("linkLogo").href = (typeof logo_href === 'undefined') ? '' : logo_href;
+    if (typeof logo_img !== 'undefined') {
+        document.getElementById("imgLogo").src = logo_img;
+    }
+    else {
+            document.getElementById("imgLogo").style.display = "none";
+        }
+    report = generateReport("main_container", textReport);
+})();
+    </script>
+</body>
+</html>
diff --git a/report-tools/html/js/reporter.js b/report-tools/html/js/reporter.js
new file mode 100644
index 0000000..410cf5d
--- /dev/null
+++ b/report-tools/html/js/reporter.js
@@ -0,0 +1,238 @@
+/*##############################################################################
+
+# Copyright (c) 2021, ARM Limited and Contributors. All rights reserved.
+
+#
+
+# SPDX-License-Identifier: BSD-3-Clause
+
+##############################################################################*/
+const capitalize = (s) => {
+  if (typeof s !== 'string') return ''
+  return s.charAt(0).toUpperCase() + s.slice(1)
+}
+
+function isValidHttpUrl(str) {
+    var regex = /(http|https):\/\/(\w+:{0,1}\w*)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%!\-\/]))?/;
+    if(!regex .test(str)) {
+        return false;
+    } else {
+        return true;
+    }
+}
+
+const slideDown = element => element.style.height = `${element.scrollHeight}px`;
+
+function show(elemId) {
+    let elem = document.getElementById(elemId)
+
+    if (elem.style.display == "none") {
+        elem.style.display = "block"
+        setTimeout(function() {elem.classList.toggle('hide')}, 50)
+    } else {
+        elem.classList.toggle('hide')
+        setTimeout(function() {elem.style.display = "none"}, 750)
+    }
+
+}
+
+var counter = 0
+
+class TableReport {
+
+    constructor(data, summary) {
+        this.data = data
+        this.summary = summary
+        this.container = document.createElement("div")
+        this.header =  document.createElement("div")
+        this.container.classList.add("table-wrap")
+        this.table = document.createElement("table")
+        this.table.classList.add("styled-table")
+        this.container.appendChild(this.header)
+        this.container.appendChild(this.table)
+        this.generateHeader()
+        this.generateTable() // generate the table first
+        this.generateTableHead() // then the head
+    }
+
+    generateHeader() {
+        this.header.innerHTML = "<h2>Test results: " + this.summary["total"] + " tests, " +
+                                "<label style='color:green'>" + this.summary["pass"] + " passed,</label> " +
+                                "<label style='color:red'>" + this.summary["fail"] +" failed</label></h2>"
+    }
+
+    generateTableHead() {
+      let table = this.table
+      let thead = table.createTHead();
+      let row = thead.insertRow();
+      for (let key in this.data[0]) {
+        let th = document.createElement("th");
+        let text = document.createTextNode(capitalize(key));
+        th.appendChild(text);
+        row.appendChild(th);
+      }
+    }
+
+    generateTable() {
+        let table = this.table
+        for (let element of this.data) {
+            let row = table.insertRow();
+            for (let key in element) {
+                let cell = row.insertCell();
+                let text = document.createTextNode(element[key]);
+                cell.appendChild(text);
+            }
+        }
+    }
+}
+
+class Report {
+
+  constructor(reportObj) {
+    this.reportObj = reportObj
+    this.reportName = Object.keys(this.reportObj)[0]
+    this.report = this.reportObj[this.reportName]
+    this.testSuites = this.report["test-suites"]
+    this.testAssets =  this.report["test-config"]["test-assets"]
+    this.metadata = this.report["metadata"]
+    this.target = this.report["target"]
+    this.testConfig = this.report["test-config"]
+    this.testEnvironments = this.report["test-environments"]
+    this.testSuitesData = {}
+  }
+
+    generateSuitesTables() {
+        this.suitesDivs = {}
+        var results = []
+        var index = 0
+        for (const [testSuiteName, testSuite] of Object.entries(this.testSuites)) {
+            ++index
+            results = []
+            var status = "PASS"
+            var failCount = 0
+            var passCount = 0
+            var counter = 0
+            var metCols = []
+            for (const [testResultName, testResult] of Object.entries(testSuite["test-results"])) {
+                results.push({name: testResultName, status: testResult["status"],
+                    ...testResult["metadata"]})
+                if (testResult["status"] == "FAIL") {
+                    status = "FAIL"
+                    failCount++
+                } else if (testResult["status"] == "PASS") {
+                    passCount++
+                }
+                metCols = Object.keys(testResult["metadata"])
+                ++counter
+            }
+            let summary = {"pass": passCount, "fail": failCount, "total": counter}
+            var tableReport = new TableReport(results, summary)
+            this.testSuitesData[testSuiteName] = {tableReport: tableReport, metadata: testSuite['metadata']}
+        }
+    }
+}
+
+function link(url) {
+    window.open(url, '_blank');
+}
+
+function generateItems(obj, container) {
+    let i = 0
+    let click=""
+    for (var [name, value] of Object.entries(obj)) {
+        if ((i++ % 3) == 0) {
+            divGroup = document.createElement("div")
+            divGroup.classList.add("item-parent")
+            container.appendChild(divGroup)
+        }
+        divElem = document.createElement("div")
+        divElem.classList.add("item")
+        style = ' class=item'
+        if (isValidHttpUrl(value))
+            style = ' class="link" onclick=link("' + value + '")'
+        divElem.innerHTML = "<span style='color:black'>" + name + ": </span>" + "<span" + style +">" + value + "</span>"
+        divGroup.appendChild(divElem)
+    }
+}
+
+function generateBlock(obj, title, expanded, extra) {
+    var divBlock = 0
+    var divGroup = 0
+    var divElem = 0
+    var divData = 0
+
+    if (expanded === undefined)
+        expanded = true
+    let id = title.replace(/\s+/g, '-') + counter++
+    let checked = expanded ? "checked" : ""
+    divBlock = document.createElement("div")
+    divBlock.classList.add("block-report")
+    let divTitle = document.createElement("div")
+    divTitle.innerHTML = '<label>' + title + '</div>' +
+                        '<label class="switch"><input ' +
+                        '" type="checkbox" ' + checked + ' onclick=' +
+                        "show('data-" + id + "')>" +
+                        '<span class="slider"></span></label>'
+    divBlock.appendChild(divTitle)
+    if (obj.tagName == 'DIV')
+        divData = obj
+    else
+        divData = document.createElement("div")
+    divData.id = "data-" + id
+    divData.classList.add("box")
+    divBlock.setAttribute('data-items-container', divData.id)
+    if (expanded == false) {
+        divData.style.display = 'none'
+        divData.classList.add("hide")
+        }
+
+    divBlock.appendChild(divData)
+    generateItems(obj, divData)
+    if (!(extra === undefined))
+        divData.appendChild(extra)
+    return divBlock
+}
+
+function generateReport(containerName, yamlString) {
+    const jsObject = JSON.parse(yamlString)
+    const report = new Report(jsObject)
+    document.getElementById("report-name").innerHTML = report.reportName
+    var divContent = document.getElementById("report-content")
+
+    var metadata = generateBlock(report.metadata, "Metadata", false)
+    divContent.appendChild(metadata)
+
+    divContent.appendChild(generateBlock(report.target, "Target", false))
+
+    var div = document.createElement("div")
+    divAssets = generateBlock(div, "Test assets", false)
+    for (const [name, data] of Object.entries(report.testConfig['test-assets'])) {
+        var divAsset = generateBlock(data, name, false)
+        div.appendChild(divAsset)
+    }
+
+    divConfig = generateBlock(divAssets, "Test configuration", false)
+    divContent.appendChild(divConfig)
+
+    div = document.createElement("div")
+    divEnvs = generateBlock(div, "Test environments", false)
+    for (const [name, data] of Object.entries(report.testEnvironments)) {
+        var divEnv = generateBlock(data, name, false)
+        div.appendChild(divEnv)
+    }
+    divContent.appendChild(divEnvs)
+
+    div = document.createElement("div")
+    divSuites = generateBlock(div, "Test suites")
+    report.generateSuitesTables()
+    var visible = true
+    for (const [name, suiteData] of Object.entries(report.testSuitesData)) {
+        var divSuite = generateBlock(suiteData['metadata'], name, visible, suiteData['tableReport'].container)
+        div.appendChild(divSuite)
+        if (visible)
+            visible = false
+    }
+
+    divContent.appendChild(divSuites)
+    return report
+}