Skip to content

Commit

Permalink
Generating module reports using Jinja2 (#1073)
Browse files Browse the repository at this point in the history
  • Loading branch information
hitnik authored Jan 31, 2025
1 parent 0285f49 commit 5055ac1
Show file tree
Hide file tree
Showing 32 changed files with 1,800 additions and 1,341 deletions.
167 changes: 38 additions & 129 deletions framework/python/src/common/testreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@
from datetime import datetime
from weasyprint import HTML
from io import BytesIO
from common import util
from common import util, logger
from common.statuses import TestrunStatus
import base64
import os
from test_orc.test_case import TestCase
from jinja2 import Environment, FileSystemLoader
from jinja2 import Environment, FileSystemLoader, BaseLoader
from collections import OrderedDict
import re
from bs4 import BeautifulSoup


Expand All @@ -34,6 +33,8 @@
TEST_REPORT_STYLES = 'test_report_styles.css'
TEST_REPORT_TEMPLATE = 'test_report_template.html'

LOGGER = logger.get_logger('REPORT')

# Locate parent directory
current_dir = os.path.dirname(os.path.realpath(__file__))

Expand Down Expand Up @@ -65,6 +66,7 @@ def __init__(self,
self._total_tests = total_tests
self._results = []
self._module_reports = []
self._module_templates = []
self._report_url = ''
self._cur_page = 0

Expand All @@ -74,6 +76,9 @@ def update_device_profile(self, additional_info):
def add_module_reports(self, module_reports):
self._module_reports = module_reports

def add_module_templates(self, module_templates):
self._module_templates = module_templates

def get_status(self):
return self._status

Expand Down Expand Up @@ -205,7 +210,11 @@ def to_pdf(self):
def to_html(self):

# Jinja template
template_env = Environment(loader=FileSystemLoader(report_resource_dir))
template_env = Environment(
loader=FileSystemLoader(report_resource_dir),
trim_blocks=True,
lstrip_blocks=True
)
template = template_env.get_template(TEST_REPORT_TEMPLATE)
with open(os.path.join(report_resource_dir,
TEST_REPORT_STYLES),
Expand Down Expand Up @@ -245,17 +254,21 @@ def to_html(self):
# Obtain optional recommendations
optional_steps_to_resolve = self._get_optional_steps_to_resolve(json_data)

module_reports = self._get_module_pages()
module_reports = self._module_reports
env_module = Environment(loader=BaseLoader())
pages_num = self._pages_num(json_data)
total_pages = pages_num + len(module_reports) + 1
if len(steps_to_resolve) > 0:
total_pages += 1
if (len(optional_steps_to_resolve) > 0
and json_data['device']['test_pack'] == 'Pilot Assessment'
):
total_pages += 1

return template.render(styles=styles,
module_templates = [
env_module.from_string(s).render(
json_data=json_data,
device=json_data['device'],
logo=logo,
icon_qualification=icon_qualification,
icon_pilot=icon_pilot,
version=self._version,
) for s in self._module_templates
]

return self._add_page_counter(template.render(styles=styles,
logo=logo,
icon_qualification=icon_qualification,
icon_pilot=icon_pilot,
Expand All @@ -272,10 +285,19 @@ def to_html(self):
optional_steps_to_resolve=optional_steps_to_resolve,
module_reports=module_reports,
pages_num=pages_num,
total_pages=total_pages,
tests_first_page=TESTS_FIRST_PAGE,
tests_per_page=TESTS_PER_PAGE,
)
module_templates=module_templates
))

def _add_page_counter(self, html):
# Add page nums and total page
soup = BeautifulSoup(html, features='html5lib')
page_index_divs = soup.find_all('div', class_='page-index')
total_pages = len(page_index_divs)
for index, div in enumerate(page_index_divs):
div.string = f'Page {index+1}/{total_pages}'
return soup.prettify()

def _pages_num(self, json_data):

Expand Down Expand Up @@ -335,116 +357,3 @@ def _get_optional_steps_to_resolve(self, json_data):
tests_with_recommendations.append(test)

return tests_with_recommendations


def _split_module_report_to_pages(self, reports):
"""Split report to pages by headers"""
reports_transformed = []

for report in reports:
if len(re.findall('<table class="module-summary"', report)) > 1:
indices = []
index = report.find('<table class="module-summary"')
while index != -1:
indices.append(index)
index = report.find('<table class="module-summary"', index + 1)
pos = 0
for i in indices[1:]:
page = report[pos:i].replace(
'"module-summary"', '"module-summary not-first"'
)
reports_transformed.append(page)
pos = i
page = report[pos:].replace(
'"module-summary"', '"module-summary not-first"'
)
reports_transformed.append(page)
else:
reports_transformed.append(report)

return reports_transformed


def _get_module_pages(self):
content_max_size = 913

reports = []

module_reports = self._split_module_report_to_pages(self._module_reports)

for module_report in module_reports:
# ToDo: Figure out how to make this dynamic
# Padding values from CSS
# Element sizes from inspection of rendered report
h1_padding = 8
module_summary_padding = 50 # 25 top and 25 bottom

# Reset values for each module report
page_content = ''
content_size = 0

# Convert module report to list of html tags
soup = BeautifulSoup(module_report, features='html5lib')
children = list(
filter(lambda el: el.name is not None, soup.body.children)
)

for index, el in enumerate(children):
current_size = 0
if el.name == 'h1':
current_size += 40 + h1_padding
# Calculating the height of paired tables
elif (el.name == 'div'
and el.get('id') == 'tls_table'):
tables = el.findChildren('table', recursive=True)
current_size = max(
map(lambda t: len(
t.findChildren('tr', recursive=True)
), tables)
) * 42
# Table height
elif el.name == 'table':
if el['class'] == 'module-summary':
current_size = 85 + module_summary_padding
else:
current_size = len(el.findChildren('tr', recursive=True)) * 42
# Other elements height
else:
current_size = 50
# Moving tables to the next page.
# Completely transfer tables that are within the maximum
# allowable size, while splitting those that exceed the page size.
if (content_size + current_size) >= content_max_size:
str_el = ''
if current_size > (content_max_size - 85 - module_summary_padding):
rows = el.findChildren('tr', recursive=True)
table_header = str(rows.pop(0))
table_1 = table_2 = f'''
<table class="module-data" style="width:100%">
<thead>{table_header}</thead><tbody>'''
rows_count = (content_max_size - 85 - module_summary_padding) // 42
table_1 += ''.join(map(str, rows[:rows_count-1]))
table_1 += '</tbody></table>'
table_2 += ''.join(map(str, rows[rows_count-1:]))
table_2 += '</tbody></table>'
page_content += table_1
reports.append(page_content)
page_content = table_2
current_size = len(rows[rows_count:]) * 42
else:
if el.name == 'table':
el_header = children[index-1]
if el_header.name.startswith('h'):
page_content = ''.join(page_content.rsplit(str(el_header), 1))
str_el = str(el_header) + str(el)
content_size = current_size + 50
else:
str_el = str(el)
content_size = current_size
reports.append(page_content)
page_content = str_el
else:
page_content += str(el)
content_size += current_size
reports.append(page_content)
return reports
14 changes: 12 additions & 2 deletions framework/python/src/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ def __init__(self, root_dir):
# All historical reports
self._module_reports = []

# Module report templates
self._module_templates = []

# Parameters specified when starting Testrun
self._runtime_params = []

Expand Down Expand Up @@ -398,6 +401,9 @@ def get_test_results(self):
def get_module_reports(self):
return self._module_reports

def get_module_templates(self):
return self._module_templates

def get_report_tests(self):
"""Returns the current test results in JSON-friendly format
(in Python dictionary)"""
Expand Down Expand Up @@ -467,6 +473,9 @@ def set_test_result_error(self, result, description=None):
def add_module_report(self, module_report):
self._module_reports.append(module_report)

def add_module_template(self, module_template):
self._module_templates.append(module_template)

def get_all_reports(self):

reports = []
Expand Down Expand Up @@ -663,7 +672,7 @@ def _remove_invalid_questions(self, questions):
valid_questions.append(question)

else:
LOGGER.debug(f'Removed unrecognised question: {question["question"]}')
LOGGER.debug(f'Removed unrecognised question: {question["question"]}') # pylint: disable=W1405

# Return the list of valid questions
return valid_questions
Expand Down Expand Up @@ -718,7 +727,7 @@ def validate_profile_json(self, profile_json):
question.get('question'))

if format_q is None:
LOGGER.error(f'Unrecognised question: {question.get("question")}')
LOGGER.error(f'Unrecognised question: {question.get("question")}') # pylint: disable=W1405
# Just ignore additional questions
continue

Expand Down Expand Up @@ -803,6 +812,7 @@ def reset(self):
self._report_url = None
self._total_tests = 0
self._module_reports = []
self._module_templates = []
self._results = []
self._started = None
self._finished = None
Expand Down
12 changes: 11 additions & 1 deletion framework/python/src/test_orc/test_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def run_test_modules(self):
generated_report_json = self._generate_report()
report.from_json(generated_report_json)
report.add_module_reports(self.get_session().get_module_reports())
report.add_module_templates(self.get_session().get_module_templates())
device.add_report(report)

self._write_reports(report)
Expand Down Expand Up @@ -592,8 +593,17 @@ def _run_test_module(self, module):
self.get_session().add_module_report(module_report)
except (FileNotFoundError, PermissionError):
LOGGER.debug("Test module did not produce a html module report")
# Get the Jinja report
jinja_file = f"{module.container_runtime_dir}/{module.name}_report.j2.html"
try:
with open(jinja_file, "r", encoding="utf-8") as f:
module_template = f.read()
LOGGER.debug(f"Adding module template for module {module.name}")
self.get_session().add_module_template(module_template)
except (FileNotFoundError, PermissionError):
LOGGER.debug("Test module did not produce a module template")

LOGGER.info(f"Test module {module.name} has finished")
# LOGGER.info(f"Test module {module.name} has finished")

def _get_container_logs(self, log_stream):
"""Resolve all current log data in the containers log_stream
Expand Down
8 changes: 8 additions & 0 deletions modules/test/base/base.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,13 @@ COPY --from=builder /usr/local/etc/oui.txt /usr/local/etc/oui.txt
# Activate the virtual environment by setting the PATH
ENV PATH="/opt/venv/bin:$PATH"

# Common resource folder
ENV REPORT_TEMPLATE_PATH=/testrun/resources
# Jinja base template
ENV BASE_TEMPLATE_FILE=module_report_base.jinja2

# Copy base template
COPY resources/report/$BASE_TEMPLATE_FILE $REPORT_TEMPLATE_PATH/

# Start the test module
ENTRYPOINT [ "/testrun/bin/start" ]
5 changes: 4 additions & 1 deletion modules/test/base/python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ protobuf==5.28.0
# User defined packages
grpcio==1.67.1
grpcio-tools==1.67.1
netifaces==0.11.0
netifaces==0.11.0

# Requirements for reports generation
Jinja2==3.1.4
8 changes: 5 additions & 3 deletions modules/test/base/python/src/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def __init__(self,
self._ipv6_subnet = os.environ.get('IPV6_SUBNET', '')
self._dev_iface_mac = os.environ.get('DEV_IFACE_MAC', '')
self._device_test_pack = json.loads(os.environ.get('DEVICE_TEST_PACK', ''))
self._report_template_folder = os.environ.get('REPORT_TEMPLATE_PATH')
self._base_template_file=os.environ.get('BASE_TEMPLATE_FILE')
self._add_logger(log_name=log_name)
self._config = self._read_config(
conf_file=conf_file if conf_file is not None else CONF_FILE)
Expand Down Expand Up @@ -137,14 +139,14 @@ def run_tests(self):
else:
result = getattr(self, test_method_name)()
except Exception as e: # pylint: disable=W0718
LOGGER.error(f'An error occurred whilst running {test["name"]}')
LOGGER.error(f'An error occurred whilst running {test["name"]}') # pylint: disable=W1405
LOGGER.error(e)
traceback.print_exc()
else:
LOGGER.error(f'Test {test["name"]} has not been implemented')
LOGGER.error(f'Test {test["name"]} has not been implemented') # pylint: disable=W1405
result = TestResult.ERROR, 'This test could not be found'
else:
LOGGER.debug(f'Test {test["name"]} is disabled')
LOGGER.debug(f'Test {test["name"]} is disabled') # pylint: disable=W1405
result = (TestResult.DISABLED,
'This test did not run because it is disabled')

Expand Down
6 changes: 5 additions & 1 deletion modules/test/dns/dns.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ COPY $MODULE_DIR/conf /testrun/conf
COPY $MODULE_DIR/bin /testrun/bin

# Copy over all python files
COPY $MODULE_DIR/python /testrun/python
COPY $MODULE_DIR/python /testrun/python

# Copy Jinja template
COPY $MODULE_DIR/resources/report_template.jinja2 $REPORT_TEMPLATE_PATH/

Loading

0 comments on commit 5055ac1

Please sign in to comment.