diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py
index 66a6c7c86..7da1d216d 100644
--- a/framework/python/src/test_orc/test_orchestrator.py
+++ b/framework/python/src/test_orc/test_orchestrator.py
@@ -15,6 +15,7 @@
import copy
import os
import json
+import pathlib
import re
import time
import shutil
@@ -234,6 +235,8 @@ def _write_reports(self, test_report):
util.run_command(f"chown -R {self._host_user} {out_dir}")
+ self._cleanup_modules_html_reports(out_dir)
+
def _generate_report(self):
device = self.get_session().get_target_device()
@@ -271,6 +274,27 @@ def _generate_report(self):
return report
+ def _cleanup_modules_html_reports(self, out_dir):
+ """Cleans up any HTML reports generated by test modules to save space."""
+
+ for module in self._test_modules:
+ module_template_path = os.path.join(
+ os.path.join(out_dir, module.name),
+ f"{module.name}_report.j2.html")
+ module_report_path = os.path.join(
+ os.path.join(out_dir, module.name),
+ f"{module.name}_report.jinja2")
+ try:
+ if os.path.exists(module_template_path):
+ os.remove(module_template_path)
+ LOGGER.debug(f"Removed module template: {module_template_path}")
+ if os.path.exists(module_report_path):
+ p = pathlib.Path(module_report_path)
+ p.rename(p.with_suffix(".html"))
+ except Exception as e:
+ LOGGER.error(f"Error {module_template_path}: {e}")
+
+
def _cleanup_old_test_results(self, device):
if device.max_device_reports is not None:
diff --git a/modules/test/base/base.Dockerfile b/modules/test/base/base.Dockerfile
index 7a82301a7..6a13b42e0 100644
--- a/modules/test/base/base.Dockerfile
+++ b/modules/test/base/base.Dockerfile
@@ -84,9 +84,29 @@ ENV PATH="/opt/venv/bin:$PATH"
ENV REPORT_TEMPLATE_PATH=/testrun/resources
# Jinja base template
ENV BASE_TEMPLATE_FILE=module_report_base.jinja2
+# Jinja preview template
+ENV BASE_TEMPLATE_PREVIEW_FILE=module_report_base_preview.jinja2
+# Jinja base template
+ENV BASE_TEMPLATE_STYLED_FILE=module_report_styled.jinja2
+# Styles
+ENV CSS_FILE=test_report_styles.css
+# ICON
+ENV LOGO_FILE=testrun.png
# Copy base template
COPY resources/report/$BASE_TEMPLATE_FILE $REPORT_TEMPLATE_PATH/
+# Copy base preview template
+COPY resources/report/$BASE_TEMPLATE_PREVIEW_FILE $REPORT_TEMPLATE_PATH/
+
+# Copy base template (with styles)
+COPY resources/report/$BASE_TEMPLATE_STYLED_FILE $REPORT_TEMPLATE_PATH/
+
+# Copy styles
+COPY resources/report/$CSS_FILE $REPORT_TEMPLATE_PATH/
+
+# Copy icon
+COPY resources/report/$LOGO_FILE $REPORT_TEMPLATE_PATH/
+
# Start the test module
ENTRYPOINT [ "/testrun/bin/start" ]
\ No newline at end of file
diff --git a/modules/test/base/python/src/test_module.py b/modules/test/base/python/src/test_module.py
index 8030d7757..7e376fa64 100644
--- a/modules/test/base/python/src/test_module.py
+++ b/modules/test/base/python/src/test_module.py
@@ -12,12 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Base class for all core test module functions"""
+
+import base64
import json
import logger
import os
import util
from datetime import datetime
import traceback
+from jinja2 import Environment, FileSystemLoader, BaseLoader
from common.statuses import TestResult
@@ -44,6 +47,12 @@ def __init__(self,
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._base_template_file_preview=os.environ.get(
+ 'BASE_TEMPLATE_PREVIEW_FILE'
+ )
+ self._base_template_styled_file=os.environ.get('BASE_TEMPLATE_STYLED_FILE')
+ self._css_file=os.environ.get('CSS_FILE')
+ self._logo_file=os.environ.get('LOGO_FILE')
self._log_level = os.environ.get('LOG_LEVEL', None)
self._add_logger(log_name=log_name)
self._config = self._read_config(
@@ -229,3 +238,38 @@ def _get_device_ipv4(self):
if text:
return text.split('\n')[0]
return None
+
+ def _render_styled_report(self, jinja_report, result_path):
+ # Report styles
+ with open(os.path.join(self._report_template_folder,
+ self._css_file),
+ 'r',
+ encoding='UTF-8'
+ ) as style_file:
+ styles = style_file.read()
+
+ # Load Testrun logo to base64
+ with open(os.path.join(self._report_template_folder,
+ self._logo_file), 'rb') as f:
+ logo = base64.b64encode(f.read()).decode('utf-8')
+
+ loader=FileSystemLoader(self._report_template_folder)
+ template = Environment(
+ loader=loader,
+ trim_blocks=True,
+ lstrip_blocks=True
+ ).get_template(self._base_template_styled_file)
+
+ module_template = Environment(loader=BaseLoader()
+ ).from_string(jinja_report).render(
+ title = 'Testrun report',
+ logo=logo,
+ )
+
+ report_jinja_styled = template.render(
+ template=module_template,
+ styles=styles
+ )
+ # Write the styled content to a file
+ with open(result_path, 'w', encoding='utf-8') as file:
+ file.write(report_jinja_styled)
diff --git a/modules/test/dns/python/src/dns_module.py b/modules/test/dns/python/src/dns_module.py
index 5c23919f1..0756d32a5 100644
--- a/modules/test/dns/python/src/dns_module.py
+++ b/modules/test/dns/python/src/dns_module.py
@@ -22,6 +22,7 @@
LOG_NAME = 'test_dns'
MODULE_REPORT_FILE_NAME = 'dns_report.j2.html'
+MODULE_REPORT_STYLED_FILE_NAME = 'dns_report.jinja2'
DNS_SERVER_CAPTURE_FILE = '/runtime/network/dns.pcap'
STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap'
MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap'
@@ -161,6 +162,7 @@ def generate_module_report(self):
pages_content.append(current_page_rows)
report_html = ''
+ report_jinja_preview = ''
if not pages_content:
pages_content = [[]]
@@ -175,9 +177,23 @@ def generate_module_report(self):
module_data=page_rows
)
report_html += page_html
+ page_html = template.render(
+ base_template=self._base_template_file_preview,
+ module_header=module_header_repr,
+ summary_headers=summary_headers,
+ summary_data=summary_data,
+ module_data_headers=module_data_headers,
+ module_data=page_rows
+ )
+ report_jinja_preview += page_html
LOGGER.debug('Module report:\n' + report_html)
+ # Generate styled report for a preview
+ jinja_path_styled = os.path.join(
+ self._results_dir, MODULE_REPORT_STYLED_FILE_NAME)
+ self._render_styled_report(report_jinja_preview, jinja_path_styled)
+
# Use os.path.join to create the complete file path
report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
@@ -219,7 +235,6 @@ def extract_dns_data(self):
qname = dns_layer.qd.qname.decode() if dns_layer.qd.qname else 'N/A'
else:
qname = 'N/A'
-
resolved_ip = 'N/A'
# If it's a response packet, extract the resolved IP address
# from the answer section
@@ -237,14 +252,15 @@ def extract_dns_data(self):
elif answer.type == 28: # Indicates AAAA record (IPv6 address)
resolved_ip = answer.rdata # Extract IPv6 address
break # Stop after finding the first valid resolved IP
-
+ qname = qname.rstrip('.') if (isinstance(qname, str)
+ and qname.endswith('.')) else qname
dns_data.append({
'Timestamp': float(packet.time), # Timestamp of the DNS packet
'Source': source_ip,
'Destination': destination_ip,
'ResolvedIP': resolved_ip, # Adding the resolved IP address
'Type': dns_type,
- 'Data': qname[:-1]
+ 'Data': qname,
})
# Filter unique entries based on 'Timestamp'
diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py
index 76c3aeb6a..91a5f9376 100644
--- a/modules/test/ntp/python/src/ntp_module.py
+++ b/modules/test/ntp/python/src/ntp_module.py
@@ -23,6 +23,7 @@
LOG_NAME = 'test_ntp'
MODULE_REPORT_FILE_NAME = 'ntp_report.j2.html'
+MODULE_REPORT_STYLED_FILE_NAME = 'ntp_report.jinja2'
NTP_SERVER_CAPTURE_FILE = '/runtime/network/ntp.pcap'
STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap'
MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap'
@@ -142,7 +143,7 @@ def generate_module_report(self):
# Generate the HTML table with the count column
for (src, dst, typ,
- version), avg_diff in average_time_between_requests.items():
+ version), avg_diff in average_time_between_requests.items():
cnt = len(timestamps[(src, dst, typ, version)])
# Sync Average only applies to client requests
@@ -169,16 +170,28 @@ def generate_module_report(self):
rows_on_page = ((page_useful_space) // row_height) - 1
start = 0
report_html = ''
+ report_html_styled = ''
for page in range(pages + 1):
end = start + min(len(module_table_data), rows_on_page)
module_header_repr = module_header if page == 0 else None
- page_html = template.render(base_template=self._base_template_file,
- module_header=module_header_repr,
- summary_headers=summary_headers,
- summary_data=summary_data,
- module_data_headers=module_data_headers,
- module_data=module_table_data[start:end])
+ page_html = template.render(
+ base_template=self._base_template_file,
+ module_header=module_header_repr,
+ summary_headers=summary_headers,
+ summary_data=summary_data,
+ module_data_headers=module_data_headers,
+ module_data=module_table_data[start:end]
+ )
+ page_html_styled = template.render(
+ base_template=self._base_template_file_preview,
+ module_header=module_header_repr,
+ summary_headers=summary_headers,
+ summary_data=summary_data,
+ module_data_headers=module_data_headers,
+ module_data=module_table_data[start:end]
+ )
report_html += page_html
+ report_html_styled += page_html_styled
start = end
LOGGER.debug('Module report:\n' + report_html)
@@ -186,6 +199,13 @@ def generate_module_report(self):
# Use os.path.join to create the complete file path
report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
+ # Use os.path.join to create the complete file path for styled report
+ report_path_styled = os.path.join(
+ self._results_dir, MODULE_REPORT_STYLED_FILE_NAME
+ )
+ # Generate the styled report for preview
+ self._render_styled_report(report_html_styled, report_path_styled)
+
# Write the content to a file
with open(report_path, 'w', encoding='utf-8') as file:
file.write(report_html)
diff --git a/modules/test/ntp/python/src/ntp_white_list.py b/modules/test/ntp/python/src/ntp_white_list.py
index eafd072f0..51269a235 100644
--- a/modules/test/ntp/python/src/ntp_white_list.py
+++ b/modules/test/ntp/python/src/ntp_white_list.py
@@ -1,6 +1,7 @@
"""Module to resolve NTP whitelist domains to IP addresses asynchronously."""
import asyncio
+import concurrent.futures
import dns.asyncresolver
from logging import Logger
@@ -99,8 +100,18 @@ def _get_ntp_whitelist_ips(
semaphore_limit: int = 50,
timeout: int = 30
) -> set[str]:
- return asyncio.run(
- self._get_ips_whitelist(self.config, semaphore_limit, timeout))
+ # Always run in a separate thread to ensure we have a clean event loop
+ def run_in_thread():
+ new_loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(new_loop)
+ try:
+ return new_loop.run_until_complete(
+ self._get_ips_whitelist(self.config, semaphore_limit, timeout))
+ finally:
+ new_loop.close()
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ return executor.submit(run_in_thread).result()
# Check if an IP is whitelisted
def is_ip_whitelisted(self, ip: str) -> bool:
diff --git a/modules/test/services/python/src/services_module.py b/modules/test/services/python/src/services_module.py
index c454f4f93..ea925b6e9 100644
--- a/modules/test/services/python/src/services_module.py
+++ b/modules/test/services/python/src/services_module.py
@@ -23,6 +23,7 @@
LOG_NAME = 'test_services'
MODULE_REPORT_FILE_NAME = 'services_report.j2.html'
+MODULE_REPORT_STYLED_FILE_NAME = 'services_report.jinja2'
NMAP_SCAN_RESULTS_SCAN_FILE = 'services_scan_results.json'
LOGGER = None
REPORT_TEMPLATE_FILE = 'report_template.jinja2'
@@ -129,9 +130,22 @@ def generate_module_report(self):
module_data_headers=module_data_headers,
module_data=module_data,
)
+ html_content_preview = template.render(
+ base_template=self._base_template_file_preview,
+ module_header=module_header,
+ summary_headers=summary_headers,
+ summary_data=summary_data,
+ module_data_headers=module_data_headers,
+ module_data=module_data,
+ )
LOGGER.debug('Module report:\n' + html_content)
+ # Generate styled report for a preview
+ jinja_path_styled = os.path.join(
+ self._results_dir, MODULE_REPORT_STYLED_FILE_NAME)
+ self._render_styled_report(html_content_preview, jinja_path_styled)
+
# Use os.path.join to create the complete file path
report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py
index cc71bac12..f20e4686c 100644
--- a/modules/test/tls/python/src/tls_module.py
+++ b/modules/test/tls/python/src/tls_module.py
@@ -31,6 +31,7 @@
LOG_NAME = 'test_tls'
MODULE_REPORT_FILE_NAME = 'tls_report.j2.html'
+MODULE_REPORT_STYLED_FILE_NAME = 'tls_report.jinja2'
STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap'
MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap'
TLS_CAPTURE_FILE = '/runtime/output/tls.pcap'
@@ -93,12 +94,14 @@ def generate_module_report(self):
self.tls_capture_file
]
certificates = self.extract_certificates_from_pcap(pcap_files,
- self._device_mac)
+ self._device_mac)
+
+
if len(certificates) > 0:
# pylint: disable=W0612
for cert_num, ((ip_address, port),
- cert) in enumerate(certificates.items()):
+ cert) in enumerate(certificates.items()):
pages[cert_num] = {}
# Extract certificate data
@@ -168,28 +171,45 @@ def generate_module_report(self):
]
report_jinja = ''
+ report_jinja_preview = ''
if pages:
for num,page in pages.items():
module_header_repr = module_header if num == 0 else None
cert_ext=page['cert_ext'] if 'cert_ext' in page else None
page_html = template.render(
- base_template=self._base_template_file,
- module_header=module_header_repr,
- summary_headers=summary_headers,
- summary_data=page['summary_data'],
- cert_info_data=page['cert_info_data'],
- subject_data=page['subject_data'],
- cert_table_headers=cert_table_headers,
- cert_ext=cert_ext,
- ountbound_headers=outbound_headers,
- )
+ base_template=self._base_template_file,
+ module_header=module_header_repr,
+ summary_headers=summary_headers,
+ summary_data=page['summary_data'],
+ cert_info_data=page['cert_info_data'],
+ subject_data=page['subject_data'],
+ cert_table_headers=cert_table_headers,
+ cert_ext=cert_ext,
+ ountbound_headers=outbound_headers,
+ )
report_jinja += page_html
+ page_html = template.render(
+ base_template=self._base_template_file_preview,
+ module_header=module_header_repr,
+ summary_headers=summary_headers,
+ summary_data=page['summary_data'],
+ cert_info_data=page['cert_info_data'],
+ subject_data=page['subject_data'],
+ cert_table_headers=cert_table_headers,
+ cert_ext=cert_ext,
+ ountbound_headers=outbound_headers,
+ )
+ report_jinja_preview += page_html
else:
report_jinja = template.render(
- base_template=self._base_template_file,
- module_header = module_header,
- )
+ base_template=self._base_template_file,
+ module_header = module_header,
+ )
+ report_jinja_preview = template.render(
+ base_template=self._base_template_file_preview,
+ module_header = module_header,
+ )
outbound_conns = self._tls_util.get_all_outbound_connections(
device_mac=self._device_mac, capture_files=pcap_files)
@@ -209,10 +229,22 @@ def generate_module_report(self):
ountbound_headers=outbound_headers,
outbound_conns=outbound_conns_chunk
)
+ out_page_preview = template.render(
+ base_template=self._base_template_file_preview,
+ ountbound_headers=outbound_headers,
+ outbound_conns=outbound_conns_chunk
+ )
+
report_jinja += out_page
+ report_jinja_preview += out_page_preview
LOGGER.debug('Module report:\n' + report_jinja)
+ # Generate styled report for a preview
+ jinja_path_styled = os.path.join(
+ self._results_dir, MODULE_REPORT_STYLED_FILE_NAME)
+ self._render_styled_report(report_jinja_preview, jinja_path_styled)
+
# Use os.path.join to create the complete file path
jinja_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
@@ -221,6 +253,7 @@ def generate_module_report(self):
file.write(report_jinja)
LOGGER.info('Module report generated at: ' + str(jinja_path))
+
return jinja_path
def format_extension_value(self, value):
diff --git a/resources/report/module_report_base_preview.jinja2 b/resources/report/module_report_base_preview.jinja2
new file mode 100644
index 000000000..ffec8fc88
--- /dev/null
+++ b/resources/report/module_report_base_preview.jinja2
@@ -0,0 +1,41 @@
+{% raw %}
+
+
+{% endraw %}
+
+ {% if module_header %}
+
{{ module_header }}
+ {% if summary_headers %}
+
+ {% endif %}
+ {% elif summary_headers %}
+
+ {% endif %}
+ {% if summary_headers %}
+
+
+ {% for header in summary_headers %}
+ | {{ header }} |
+ {% endfor %}
+
+
+
+
+ {% for cell in summary_data %}
+ | {{ cell }} |
+ {% endfor %}
+
+
+
+ {% endif %}
+ {% block content %}{% endblock content %}
+
+{% raw %}
+
+
+
+{% endraw %}
\ No newline at end of file
diff --git a/resources/report/module_report_styled.jinja2 b/resources/report/module_report_styled.jinja2
new file mode 100644
index 000000000..fb39692ac
--- /dev/null
+++ b/resources/report/module_report_styled.jinja2
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Testrun Report
+
+
+
+ {{ template }}
+
+
\ No newline at end of file
diff --git a/resources/report/test_report_styles.css b/resources/report/test_report_styles.css
index dfcfb9816..d6ebf2552 100644
--- a/resources/report/test_report_styles.css
+++ b/resources/report/test_report_styles.css
@@ -555,6 +555,8 @@
background-repeat: no-repeat !important;
background-size: 14px;
background-position: center;
+ padding: 0;
+ margin: 0;
}
.result-test-required-result {