diff --git a/report-tools/adaptors/__init__.py b/report-tools/adaptors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..80ef809c94820972272c2cbd3159e52425cb22a7 --- /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 0000000000000000000000000000000000000000..80ef809c94820972272c2cbd3159e52425cb22a7 --- /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 b0ce15ee92aff83a43d9d9ea4da41917045bb1a8..9e6aa1719b54902fdcff9cf46b53b3ce91929d4f 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 7c70050f86045c6d23649c7e89f623cfa6e7da42..53087c10ea3644f8bf967de2f6219dc99d8cfe76 100644 --- a/report-tools/generate_report.py +++ b/report-tools/generate_report.py @@ -12,7 +12,13 @@ import yaml 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 @@ class TCReport(object): # {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 @@ class TCReport(object): 'target': target, 'test-suites': test_suites } + self.report_file = report_file def dump(self, file_name): """ @@ -181,7 +188,7 @@ class TCReport(object): 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 @@ class TCReport(object): 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 @@ class KvDictAppendAction(argparse.Action): (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 @@ def process_lava_log(_report, _args): # 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(PASS|FAIL|SKIP)): (?P.+ .+) " \ + r"- (?P.+)" + elif suite == "kernel-selftest": + results_pattern = r"(?P(PASS|FAIL|SKIP)): (" \ + r"?Pselftests): (?P.+: .+)" + 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 @@ def process_lava_log(_report, _args): 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 @@ if __name__ == '__main__': 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(PASS|FAIL|SKIP)): (" - r"?P.+ .+) - ( " - r"?P.+)") + 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 @@ if __name__ == '__main__': 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 @@ if __name__ == '__main__': 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 0000000000000000000000000000000000000000..58924cbdcbb016ab902bde80be6a7bb5127b5572 --- /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 0000000000000000000000000000000000000000..003c0391099c69ca00e13dc0d87b03842875b453 --- /dev/null +++ b/report-tools/html/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + + +
+
+ + +
+
+ + + +
+
+

+
+
+ + + diff --git a/report-tools/html/js/reporter.js b/report-tools/html/js/reporter.js new file mode 100644 index 0000000000000000000000000000000000000000..410cf5dadcd4f8a62435fad3c6b50faa378b9758 --- /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 = "

Test results: " + this.summary["total"] + " tests, " + + " " + + "

" + } + + 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 = "" + name + ": " + "" + value + "" + 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 = '