From a83b969fb245480a9ed7811b02b0621b56f7ec09 Mon Sep 17 00:00:00 2001 From: Sofia Kurilova Date: Thu, 5 Feb 2026 16:04:16 +0000 Subject: [PATCH 01/11] Test Module Report - TLS Module (#1419) * Generate html report for tls module --------- Co-authored-by: Aliaksandr Nikitsin --- modules/test/base/base.Dockerfile | 20 ++++++ modules/test/base/python/src/test_module.py | 44 +++++++++++++ modules/test/tls/python/src/tls_module.py | 63 ++++++++++++++----- .../report/module_report_base_preview.jinja2 | 41 ++++++++++++ resources/report/module_report_styled.jinja2 | 12 ++++ resources/report/test_report_styles.css | 9 +-- 6 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 resources/report/module_report_base_preview.jinja2 create mode 100644 resources/report/module_report_styled.jinja2 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/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py index cc71bac12..ab578f5b8 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_styled.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 %} +
+
+ Testrun +
+{% endraw %} +
+ {% if module_header %} +

{{ module_header }}

+ {% if summary_headers %} + + {% endif %} + {% elif summary_headers %} +
+ {% endif %} + {% if summary_headers %} + + + {% for header in summary_headers %} + + {% endfor %} + + + + + {% for cell in summary_data %} + + {% endfor %} + + +
{{ header }}
{{ cell }}
+ {% 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 e8f1e4ef1..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 { @@ -564,11 +566,10 @@ } .result-test-required-result-text { + display: inline-block; + vertical-align: middle; line-height: 14px; - flex-grow: 0; - flex-shrink: 0; - flex-basis: max-content; - padding-left: 8px; + padding-left: 6pt; } .result-test-required-result-informational, .result-test-required-result-recommended { From 5b5b1dd47b092d98803db9e63184b250930b1845 Mon Sep 17 00:00:00 2001 From: kurilova Date: Fri, 6 Feb 2026 11:04:47 +0000 Subject: [PATCH 02/11] Adds html report --- modules/test/ntp/python/src/ntp_module.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index 76c3aeb6a..d6b1aebfc 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_styled.jinja2' NTP_SERVER_CAPTURE_FILE = '/runtime/network/ntp.pcap' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' @@ -169,6 +170,7 @@ def generate_module_report(self): rows_on_page = ((page_useful_space) // row_height) - 1 start = 0 report_html = '' + report_jinja_preview = '' 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 @@ -179,10 +181,24 @@ def generate_module_report(self): module_data_headers=module_data_headers, module_data=module_table_data[start:end]) 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=module_table_data[start:end]) + report_jinja_preview += page_html + start = end 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) From 6d43bf0bf17eaa6b5e8c792392f874c8c607cdee Mon Sep 17 00:00:00 2001 From: kurilova Date: Fri, 6 Feb 2026 12:39:12 +0000 Subject: [PATCH 03/11] Fix pylint --- modules/test/ntp/python/src/ntp_module.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index d6b1aebfc..df590f8db 100644 --- a/modules/test/ntp/python/src/ntp_module.py +++ b/modules/test/ntp/python/src/ntp_module.py @@ -182,12 +182,13 @@ def generate_module_report(self): module_data=module_table_data[start:end]) 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=module_table_data[start:end]) + 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=module_table_data[start:end]) report_jinja_preview += page_html start = end From 4b543ade7323dd3ec65c4e5c748e8491470c2cc4 Mon Sep 17 00:00:00 2001 From: kurilova Date: Fri, 6 Feb 2026 10:58:33 +0000 Subject: [PATCH 04/11] Adds html report --- .../test/services/python/src/services_module.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/test/services/python/src/services_module.py b/modules/test/services/python/src/services_module.py index c454f4f93..5de4612a6 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_styled.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) From 39b6c04ff326a7d4dfb0bf699053b97ca47243da Mon Sep 17 00:00:00 2001 From: kurilova Date: Fri, 6 Feb 2026 10:53:55 +0000 Subject: [PATCH 05/11] Adds html report --- modules/test/dns/python/src/dns_module.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/test/dns/python/src/dns_module.py b/modules/test/dns/python/src/dns_module.py index 5c23919f1..06f346f76 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) From cdf6c2e9328ee111dfcb08ee1b426e3aa658cab1 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Fri, 6 Feb 2026 22:04:33 +0100 Subject: [PATCH 06/11] change module reports extension to html --- .../python/src/test_orc/test_orchestrator.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index 66a6c7c86..2084500eb 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() @@ -270,6 +273,27 @@ def _generate_report(self): report["export"] = report["report"].replace("report", "export") 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 removing module template {module_template_path}: {e}") + def _cleanup_old_test_results(self, device): From db9da5bb149169ed51ee23139c46bc21accb5d4d Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Fri, 6 Feb 2026 22:04:47 +0100 Subject: [PATCH 07/11] rename module report file --- modules/test/tls/python/src/tls_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py index ab578f5b8..f20e4686c 100644 --- a/modules/test/tls/python/src/tls_module.py +++ b/modules/test/tls/python/src/tls_module.py @@ -31,7 +31,7 @@ LOG_NAME = 'test_tls' MODULE_REPORT_FILE_NAME = 'tls_report.j2.html' -MODULE_REPORT_STYLED_FILE_NAME = 'tls_report_styled.jinja2' +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' From 1333b662400b1a95d4db5427bc5cd1d50393b95f Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Fri, 6 Feb 2026 22:04:58 +0100 Subject: [PATCH 08/11] rename report --- modules/test/services/python/src/services_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/test/services/python/src/services_module.py b/modules/test/services/python/src/services_module.py index 5de4612a6..ea925b6e9 100644 --- a/modules/test/services/python/src/services_module.py +++ b/modules/test/services/python/src/services_module.py @@ -23,7 +23,7 @@ LOG_NAME = 'test_services' MODULE_REPORT_FILE_NAME = 'services_report.j2.html' -MODULE_REPORT_STYLED_FILE_NAME = 'services_report_styled.jinja2' +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' From d0ecc64e584729bd81df288fe9314701ad4a043c Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Fri, 6 Feb 2026 22:05:16 +0100 Subject: [PATCH 09/11] remane report --- modules/test/ntp/python/src/ntp_module.py | 33 +++++++++---------- modules/test/ntp/python/src/ntp_white_list.py | 16 +++++++-- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index df590f8db..49768d3d1 100644 --- a/modules/test/ntp/python/src/ntp_module.py +++ b/modules/test/ntp/python/src/ntp_module.py @@ -23,7 +23,7 @@ LOG_NAME = 'test_ntp' MODULE_REPORT_FILE_NAME = 'ntp_report.j2.html' -MODULE_REPORT_STYLED_FILE_NAME = 'ntp_report_styled.jinja2' +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' @@ -143,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 @@ -170,7 +170,7 @@ def generate_module_report(self): rows_on_page = ((page_useful_space) // row_height) - 1 start = 0 report_html = '' - report_jinja_preview = '' + 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 @@ -180,29 +180,26 @@ def generate_module_report(self): 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 - - 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=module_table_data[start:end]) - report_jinja_preview += page_html - + report_html_styled += page_html_styled start = end 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) + # 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..faf84dbd7 100644 --- a/modules/test/ntp/python/src/ntp_white_list.py +++ b/modules/test/ntp/python/src/ntp_white_list.py @@ -1,6 +1,8 @@ """Module to resolve NTP whitelist domains to IP addresses asynchronously.""" import asyncio +import concurrent.futures +import threading import dns.asyncresolver from logging import Logger @@ -99,8 +101,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 context + 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: From 96c357f13a81f2f4d7214ed66903b2511bb1e4c2 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Fri, 6 Feb 2026 22:14:52 +0100 Subject: [PATCH 10/11] pylint --- .../python/src/test_orc/test_orchestrator.py | 6 ++-- modules/test/ntp/python/src/ntp_module.py | 32 +++++++++++-------- modules/test/ntp/python/src/ntp_white_list.py | 5 ++- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index 2084500eb..7da1d216d 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -273,7 +273,7 @@ def _generate_report(self): report["export"] = report["report"].replace("report", "export") return report - + def _cleanup_modules_html_reports(self, out_dir): """Cleans up any HTML reports generated by test modules to save space.""" @@ -290,9 +290,9 @@ def _cleanup_modules_html_reports(self, out_dir): 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')) + p.rename(p.with_suffix(".html")) except Exception as e: - LOGGER.error(f"Error removing module template {module_template_path}: {e}") + LOGGER.error(f"Error {module_template_path}: {e}") def _cleanup_old_test_results(self, device): diff --git a/modules/test/ntp/python/src/ntp_module.py b/modules/test/ntp/python/src/ntp_module.py index 49768d3d1..91a5f9376 100644 --- a/modules/test/ntp/python/src/ntp_module.py +++ b/modules/test/ntp/python/src/ntp_module.py @@ -174,18 +174,22 @@ def generate_module_report(self): 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_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]) + 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 @@ -196,7 +200,9 @@ def generate_module_report(self): 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) + 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) diff --git a/modules/test/ntp/python/src/ntp_white_list.py b/modules/test/ntp/python/src/ntp_white_list.py index faf84dbd7..51269a235 100644 --- a/modules/test/ntp/python/src/ntp_white_list.py +++ b/modules/test/ntp/python/src/ntp_white_list.py @@ -2,7 +2,6 @@ import asyncio import concurrent.futures -import threading import dns.asyncresolver from logging import Logger @@ -101,7 +100,7 @@ def _get_ntp_whitelist_ips( semaphore_limit: int = 50, timeout: int = 30 ) -> set[str]: - # Always run in a separate thread to ensure we have a clean event loop context + # 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) @@ -110,7 +109,7 @@ def run_in_thread(): 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() From 43324c7ca5cc620cbed17297d34bbb562f58bd45 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Tue, 10 Feb 2026 14:41:32 +0100 Subject: [PATCH 11/11] fix dns module report --- modules/test/dns/python/src/dns_module.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/test/dns/python/src/dns_module.py b/modules/test/dns/python/src/dns_module.py index 06f346f76..0756d32a5 100644 --- a/modules/test/dns/python/src/dns_module.py +++ b/modules/test/dns/python/src/dns_module.py @@ -185,7 +185,7 @@ def generate_module_report(self): module_data_headers=module_data_headers, module_data=page_rows ) - report_jinja_preview += page_html + report_jinja_preview += page_html LOGGER.debug('Module report:\n' + report_html) @@ -235,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 @@ -253,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'