diff --git a/.gitlab/ci/post_deploy.yml b/.gitlab/ci/post_deploy.yml
index 41050c2eaa..d4d037e253 100644
--- a/.gitlab/ci/post_deploy.yml
+++ b/.gitlab/ci/post_deploy.yml
@@ -9,7 +9,7 @@ generate_failed_jobs_report:
when: always
dependencies: [] # Do not download artifacts from the previous stages
artifacts:
- expire_in: 1 week
+ expire_in: 2 week
when: always
paths:
- job_report.html
diff --git a/tools/ci/dynamic_pipelines/constants.py b/tools/ci/dynamic_pipelines/constants.py
index 23efe9e625..2fdb1eac60 100644
--- a/tools/ci/dynamic_pipelines/constants.py
+++ b/tools/ci/dynamic_pipelines/constants.py
@@ -1,4 +1,4 @@
-# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
+# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
@@ -34,6 +34,8 @@ TEST_RELATED_APPS_DOWNLOAD_URLS_FILENAME = 'test_related_apps_download_urls.yml'
REPORT_TEMPLATE_FILEPATH = os.path.join(
IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'report.template.html'
)
+CSS_STYLES_FILEPATH = os.path.join(IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'styles.css')
+JS_SCRIPTS_FILEPATH = os.path.join(IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'scripts.js')
TOP_N_APPS_BY_SIZE_DIFF = 10
SIZE_DIFFERENCE_BYTES_THRESHOLD = 500
BINARY_SIZE_METRIC_NAME = 'binary_size'
diff --git a/tools/ci/dynamic_pipelines/report.py b/tools/ci/dynamic_pipelines/report.py
index b2a688c1e6..2f51a6e6c2 100644
--- a/tools/ci/dynamic_pipelines/report.py
+++ b/tools/ci/dynamic_pipelines/report.py
@@ -20,7 +20,10 @@ from idf_ci_local.uploader import AppUploader
from prettytable import PrettyTable
from .constants import BINARY_SIZE_METRIC_NAME
+from .constants import CI_DASHBOARD_API
from .constants import COMMENT_START_MARKER
+from .constants import CSS_STYLES_FILEPATH
+from .constants import JS_SCRIPTS_FILEPATH
from .constants import REPORT_TEMPLATE_FILEPATH
from .constants import RETRY_JOB_PICTURE_LINK
from .constants import RETRY_JOB_PICTURE_PATH
@@ -56,6 +59,14 @@ class ReportGenerator:
self.output_filepath = self.title.lower().replace(' ', '_') + '.html'
self.additional_info = ''
+ @property
+ def get_commit_summary(self) -> str:
+ """Return a formatted commit summary string."""
+ return (
+ f'with CI commit SHA: {self.commit_id[:8]}, '
+ f'local commit SHA: {os.getenv("CI_MERGE_REQUEST_SOURCE_BRANCH_SHA", "")[:8]}'
+ )
+
@staticmethod
def get_download_link_for_url(url: str) -> str:
if url:
@@ -83,14 +94,36 @@ class ReportGenerator:
report_url: str = get_artifacts_url(job_id, output_filepath)
return report_url
+ @staticmethod
+ def _load_file_content(filepath: str) -> str:
+ """
+ Load the content of a file as string
+
+ :param filepath: Path to the file to load
+ :return: Content of the file as string
+ """
+ try:
+ with open(filepath, 'r', encoding='utf-8') as f:
+ return f.read()
+ except (IOError, FileNotFoundError) as e:
+ print(f'Warning: Could not read file {filepath}: {e}')
+ return ''
+
def generate_html_report(self, table_str: str) -> str:
# we're using bootstrap table
table_str = table_str.replace(
'
',
'',
)
- with open(REPORT_TEMPLATE_FILEPATH) as fr:
- template = fr.read()
+
+ template = self._load_file_content(REPORT_TEMPLATE_FILEPATH)
+ css_content = self._load_file_content(CSS_STYLES_FILEPATH)
+ js_content = self._load_file_content(JS_SCRIPTS_FILEPATH)
+
+ template = template.replace('{{css_content}}', css_content)
+ template = template.replace('{{js_content}}', js_content)
+ template = template.replace('{{pipeline_id}}', str(self.pipeline_id))
+ template = template.replace('{{apiBaseUrl}}', CI_DASHBOARD_API)
return template.replace('{{title}}', self.title).replace('{{table}}', table_str)
@@ -136,17 +169,23 @@ class ReportGenerator:
return report_sections
@staticmethod
- def generate_additional_info_section(title: str, count: int, report_url: t.Optional[str] = None) -> str:
+ def generate_additional_info_section(
+ title: str, count: int, report_url: t.Optional[str] = None, add_permalink: bool = True
+ ) -> str:
"""
Generate a section for the additional info string.
:param title: The title of the section.
:param count: The count of test cases.
:param report_url: The URL of the report. If count = 0, only the count will be included.
+ :param add_permalink: Whether to include a permalink in the report URL. Defaults to True.
:return: The formatted additional info section string.
"""
if count != 0 and report_url:
- return f'- **{title}:** [{count}]({report_url}/#{format_permalink(title)})\n'
+ if add_permalink:
+ return f'- **{title}:** [{count}]({report_url}/#{format_permalink(title)})\n'
+ else:
+ return f'- **{title}:** [{count}]({report_url})\n'
else:
return f'- **{title}:** {count}\n'
@@ -673,7 +712,11 @@ class BuildReportGenerator(ReportGenerator):
return skipped_apps_table_section
def _get_report_str(self) -> str:
- self.additional_info = f'**Build Summary (with commit {self.commit_id[:8]}):**\n'
+ self.additional_info = (
+ f'**Build Summary ({self.get_commit_summary}):**\n'
+ '\n'
+ '> ℹ️ Note: Binary artifacts stored in MinIO are retained for 4 DAYS from their build date\n'
+ )
failed_apps_report_parts = self.get_failed_apps_report_parts()
skipped_apps_report_parts = self.get_skipped_apps_report_parts()
built_apps_report_parts = self.get_built_apps_report_parts()
@@ -700,8 +743,8 @@ class TargetTestReportGenerator(ReportGenerator):
self.test_cases = test_cases
self._known_failure_cases_set = None
self.report_titles_map = {
- 'failed_yours': 'Failed Test Cases on Your branch (Excludes Known Failure Cases)',
- 'failed_others': 'Failed Test Cases on Other branches (Excludes Known Failure Cases)',
+ 'failed_yours': 'Testcases failed ONLY on your branch (known failures are excluded)',
+ 'failed_others': 'Testcases failed on your branch as well as on others (known failures are excluded)',
'failed_known': 'Known Failure Cases',
'skipped': 'Skipped Test Cases',
'succeeded': 'Succeeded Test Cases',
@@ -794,7 +837,7 @@ class TargetTestReportGenerator(ReportGenerator):
'Test Case',
'Test App Path',
'Failure Reason',
- 'Failures on your branch (40 latest testcases)',
+ 'These test cases failed exclusively on your branch in the latest 40 runs',
'Dut Log URL',
'Create Known Failure Case Jira',
'Job URL',
@@ -803,9 +846,9 @@ class TargetTestReportGenerator(ReportGenerator):
row_attrs=['name', 'app_path', 'failure', 'dut_log_url', 'ci_job_url', 'ci_dashboard_url'],
value_functions=[
(
- 'Failures on your branch (40 latest testcases)',
- lambda item: f'{getattr(item, "latest_failed_count", "")} '
- f'/ {getattr(item, "latest_total_count", "")}',
+ 'These test cases failed exclusively on your branch in the latest 40 runs',
+ lambda item: f'{getattr(item, "latest_failed_count", "")} / '
+ f'{getattr(item, "latest_total_count", "")}',
),
('Create Known Failure Case Jira', known_failure_issue_jira_fast_link),
],
@@ -901,7 +944,10 @@ class TargetTestReportGenerator(ReportGenerator):
self.succeeded_cases_report_file,
)
self.additional_info += self.generate_additional_info_section(
- self.report_titles_map['succeeded'], len(succeeded_test_cases), succeeded_cases_report_url
+ self.report_titles_map['succeeded'],
+ len(succeeded_test_cases),
+ succeeded_cases_report_url,
+ add_permalink=False,
)
self.additional_info += '\n'
return succeeded_cases_table_section
@@ -911,7 +957,7 @@ class TargetTestReportGenerator(ReportGenerator):
Generate a complete HTML report string by processing test cases.
:return: Complete HTML report string.
"""
- self.additional_info = f'**Test Case Summary (with commit {self.commit_id[:8]}):**\n'
+ self.additional_info = f'**Test Case Summary ({self.get_commit_summary}):**\n'
failed_cases_report_parts = self.get_failed_cases_report_parts()
skipped_cases_report_parts = self.get_skipped_cases_report_parts()
succeeded_cases_report_parts = self.get_succeeded_cases_report_parts()
@@ -960,7 +1006,7 @@ class JobReportGenerator(ReportGenerator):
)
succeeded_jobs = self._filter_items(self.jobs, lambda job: job.is_success)
- self.additional_info = f'**Job Summary (with commit {self.commit_id[:8]}):**\n'
+ self.additional_info = f'**Job Summary ({self.get_commit_summary}):**\n'
self.additional_info += self.generate_additional_info_section(
self.report_titles_map['succeeded'], len(succeeded_jobs)
)
diff --git a/tools/ci/dynamic_pipelines/templates/fast_pipeline.yml b/tools/ci/dynamic_pipelines/templates/fast_pipeline.yml
index ea23b84abb..5f2678eb11 100644
--- a/tools/ci/dynamic_pipelines/templates/fast_pipeline.yml
+++ b/tools/ci/dynamic_pipelines/templates/fast_pipeline.yml
@@ -5,7 +5,7 @@
artifacts:
paths:
- target_test_report.html
- expire_in: 1 week
+ expire_in: 2 week
when: always
fast_pipeline:pipeline_ended:always_failed:
diff --git a/tools/ci/dynamic_pipelines/templates/generate_target_test_report.yml b/tools/ci/dynamic_pipelines/templates/generate_target_test_report.yml
index 1631f949f4..22acff0e0e 100644
--- a/tools/ci/dynamic_pipelines/templates/generate_target_test_report.yml
+++ b/tools/ci/dynamic_pipelines/templates/generate_target_test_report.yml
@@ -9,7 +9,7 @@ generate_pytest_report:
- failed_cases.html
- skipped_cases.html
- succeeded_cases.html
- expire_in: 1 week
+ expire_in: 2 week
when: always
script:
diff --git a/tools/ci/dynamic_pipelines/templates/report.template.html b/tools/ci/dynamic_pipelines/templates/report.template.html
index cefa904b46..adbdf4710f 100644
--- a/tools/ci/dynamic_pipelines/templates/report.template.html
+++ b/tools/ci/dynamic_pipelines/templates/report.template.html
@@ -1,8 +1,15 @@
-
+
+
{{title}}
+
+
+
-
- {{table}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/tools/ci/dynamic_pipelines/templates/scripts.js b/tools/ci/dynamic_pipelines/templates/scripts.js
new file mode 100644
index 0000000000..403aa23597
--- /dev/null
+++ b/tools/ci/dynamic_pipelines/templates/scripts.js
@@ -0,0 +1,1127 @@
+function debounce(func, wait) {
+ let timeout;
+ return function () {
+ const context = this;
+ const args = arguments;
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func.apply(context, args), wait);
+ };
+}
+
+function extractPipelineId() {
+ return $("body").data("pipeline-id");
+}
+
+function updateTabsAvailability(jobs) {
+ $("#build-report-tab, #job-report-tab, #test-report-tab").each(function () {
+ const $tab = $(this);
+ if (!$tab.hasClass("active")) {
+ $tab.addClass("disabled");
+ $tab.attr("title", "This report is not available yet");
+ $tab.css("opacity", "0.5");
+ $tab.css("cursor", "not-allowed");
+ $tab.on("click", function (e) {
+ if ($(this).hasClass("disabled")) {
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+ });
+ }
+ });
+
+ const buildReportPatterns = [
+ "generate_pytest_build_report",
+ "built_apps",
+ "build_report",
+ "skipped_apps",
+ "failed_apps",
+ ];
+
+ const jobReportPatterns = [
+ "generate_failed_jobs_report",
+ "job_report",
+ "pipeline_jobs",
+ ];
+
+ const testReportPatterns = [
+ "generate_pytest_report",
+ "target_test_report",
+ "test_report",
+ "pytest_report",
+ ];
+
+ let buildReportJob = jobs.find((job) =>
+ buildReportPatterns.some((pattern) => job.name.includes(pattern))
+ );
+
+ let jobReportJob = jobs.find((job) =>
+ jobReportPatterns.some((pattern) => job.name.includes(pattern))
+ );
+
+ let testReportJob = jobs.find((job) =>
+ testReportPatterns.some((pattern) => job.name.includes(pattern))
+ );
+
+ if (buildReportJob) reportJobIds.build = buildReportJob.job_id;
+ if (jobReportJob) reportJobIds.job = jobReportJob.job_id;
+ if (testReportJob) reportJobIds.test = testReportJob.job_id;
+
+ const hasBuildReport = buildReportJob !== undefined;
+ const hasJobReport = jobReportJob !== undefined;
+ const hasTestReport = testReportJob !== undefined;
+
+ if (hasBuildReport && !$("#build-report-tab").hasClass("active")) {
+ $("#build-report-tab").removeClass("disabled");
+ $("#build-report-tab").attr(
+ "title",
+ "View application build results including binary sizes and downloads"
+ );
+ $("#build-report-tab").css("opacity", "1");
+ $("#build-report-tab").css("cursor", "pointer");
+ }
+
+ if (hasJobReport && !$("#job-report-tab").hasClass("active")) {
+ $("#job-report-tab").removeClass("disabled");
+ $("#job-report-tab").attr(
+ "title",
+ "View CI job results and failed jobs"
+ );
+ $("#job-report-tab").css("opacity", "1");
+ $("#job-report-tab").css("cursor", "pointer");
+ }
+
+ if (hasTestReport && !$("#test-report-tab").hasClass("active")) {
+ $("#test-report-tab").removeClass("disabled");
+ $("#test-report-tab").attr(
+ "title",
+ "View test results including success, failure, and skipped tests"
+ );
+ $("#test-report-tab").css("opacity", "1");
+ $("#test-report-tab").css("cursor", "pointer");
+ }
+}
+
+$(document).ready(function () {
+ const currentPath = window.location.pathname;
+ const currentFile = currentPath.substring(currentPath.lastIndexOf("/") + 1);
+
+ $(".report-nav-tabs .nav-tab").removeClass("active");
+
+ if (currentFile.includes("built_apps")) {
+ $("#build-report-tab").addClass("active");
+ } else if (currentFile.includes("job_report")) {
+ $("#job-report-tab").addClass("active");
+ } else if (currentFile.includes("target_test_report")) {
+ $("#test-report-tab").addClass("active");
+ }
+
+ wrapTablesInCollapsibleSections();
+ makeTablesCollapsible();
+ setupScrollProgressBar();
+ setupFloatingActions();
+ enhanceTableStatusDisplay();
+ markTestCaseColumns();
+ setupResponsiveText();
+ setupTextToggles();
+ setupEventHandlers();
+ scrollToHashLocation();
+ initBootstrapTable();
+ fixStickyHeaderAlignment();
+ forceEnableStickyHeaders();
+ fixTableHeaderText();
+ lazyLoadVisibleImages();
+ setupPagination();
+ optimizeScrollPerformance();
+});
+
+function fixStickyHeaderAlignment() {
+ if (window.isScrolling) return;
+
+ $(".sticky-header-container").each(function () {
+ const $container = $(this);
+ const $table = $container
+ .closest(".bootstrap-table")
+ .find(".fixed-table-body table");
+
+ let tableWidth = "100%";
+ if ($table.length) {
+ const actualWidth = $table.width();
+ if (actualWidth > 0) {
+ tableWidth = actualWidth + "px";
+ }
+ }
+
+ $container.css({
+ left: "0",
+ "margin-left": "0",
+ "padding-left": "0",
+ width: tableWidth,
+ "z-index": "100",
+ transform: "translateZ(0)",
+ });
+
+ this.style.setProperty("left", "0", "important");
+ this.style.setProperty("margin-left", "0", "important");
+ this.style.setProperty("padding-left", "0", "important");
+ this.style.setProperty("width", tableWidth, "important");
+ this.style.setProperty("z-index", "100", "important");
+ this.style.setProperty("transform", "translateZ(0)", "important");
+
+ const $headerTable = $container.find("table");
+ if ($headerTable.length) {
+ $headerTable.css("width", tableWidth);
+ $headerTable[0].style.setProperty("width", tableWidth, "important");
+ }
+
+ if (!window.isScrolling) {
+ if ($headerTable.length && $table.length) {
+ const $headerCols = $headerTable.find("th");
+ const $bodyCols = $table.find("tr:first-child td");
+
+ if ($headerCols.length === $bodyCols.length) {
+ $headerCols.each(function (i) {
+ if (i < $bodyCols.length) {
+ const bodyColWidth = $($bodyCols[i]).outerWidth();
+ if (bodyColWidth > 0) {
+ $(this).css("min-width", bodyColWidth + "px");
+ $(this).css("width", bodyColWidth + "px");
+
+ const $thInner = $(this).find(".th-inner");
+ if ($thInner.length) {
+ $thInner.css({
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ display: "block",
+ });
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+ });
+
+ $(".fixed-table-container").each(function () {
+ if ($(this).css("left") === "20px") {
+ $(this).css("left", "0");
+ this.style.setProperty("left", "0", "important");
+ }
+ });
+
+ $(".bootstrap-table .table thead th").css({
+ position: "sticky",
+ top: "0",
+ "z-index": "100",
+ "background-color": "var(--esp-light)",
+ transform: "translateZ(0)",
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ });
+
+ $(
+ ".bootstrap-table .table thead th .th-inner, .sticky-header-container th .th-inner"
+ ).css({
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "20px",
+ display: "block",
+ "line-height": "1.4",
+ });
+}
+
+function setupScrollProgressBar() {
+ const progressBar = $("#nav-progress-bar");
+ let lastScrollPosition = 0;
+ let ticking = false;
+
+ $(window).on("scroll", function () {
+ if (ticking) return;
+
+ const scrollPosition = window.scrollY;
+
+ if (Math.abs(scrollPosition - lastScrollPosition) < 5) {
+ return;
+ }
+
+ lastScrollPosition = scrollPosition;
+
+ window.requestAnimationFrame(function () {
+ const windowHeight = $(document).height() - $(window).height();
+ const scrollPercentage = (scrollPosition / windowHeight) * 100;
+ progressBar.css("width", scrollPercentage + "%");
+ ticking = false;
+ });
+ ticking = true;
+ });
+}
+
+function setupFloatingActions() {
+ $("#back-to-top").on("click", function () {
+ $("html, body").animate({ scrollTop: 0 }, 300);
+ });
+}
+
+function enhanceTableStatusDisplay() {
+ $("table.table th").each(function (index) {
+ const $header = $(this);
+ const headerText = $header.text().trim().toLowerCase();
+
+ if (
+ headerText.includes("status") ||
+ headerText.includes("result") ||
+ headerText.includes("state")
+ ) {
+ const colIndex = index + 1;
+
+ $(`table.table td:nth-child(${colIndex})`).each(function () {
+ const $cell = $(this);
+ const cellText = $cell.text().trim().toLowerCase();
+
+ if (cellText.includes("pass") || cellText.includes("success")) {
+ $cell.html(
+ `${$cell.text()}
`
+ );
+ } else if (
+ cellText.includes("fail") ||
+ cellText.includes("error")
+ ) {
+ $cell.html(
+ `${$cell.text()}
`
+ );
+ } else if (
+ cellText.includes("warn") ||
+ cellText.includes("skip")
+ ) {
+ $cell.html(
+ `${$cell.text()}
`
+ );
+ }
+ });
+ }
+ });
+}
+
+function setupTextToggles() {
+ setupResponsiveText();
+
+ $(".toggle-link").off("click").on("click", toggleText);
+}
+
+function setupResponsiveText() {
+ $("table.table td").each(function () {
+ const $cell = $(this);
+ const text = $cell.text();
+
+ if (
+ text.length > 100 &&
+ !$cell.hasClass("test-case-name") &&
+ !$cell.find(".text-toggle").length
+ ) {
+ const displayText = text.substring(0, 100) + "...";
+ $cell.html(
+ `${displayText}` +
+ `${text}` +
+ ` Show More`
+ );
+ }
+ });
+}
+
+function makeTablesCollapsible() {
+ $("table.table").each(function (index) {
+ const table = $(this);
+ const tableId = `table-${index}`;
+ table.attr("id", tableId);
+
+ const bootstrapTableWrapper = table.closest(".bootstrap-table");
+
+ let container = bootstrapTableWrapper.parent(".table-container");
+ if (!container.length) {
+ container = $('');
+ }
+
+ let tableControls = bootstrapTableWrapper.prev(".table-controls");
+
+ if (!tableControls.length) {
+ const initialButtonHtml =
+ ' Collapse Table';
+
+ const toggleButton = $(`
+
+ `);
+
+ tableControls = $('').append(
+ toggleButton
+ );
+
+ bootstrapTableWrapper.before(tableControls);
+ } else {
+ const existingButton = tableControls.find(".table-collapse-btn");
+ if (existingButton.length) {
+ existingButton.detach();
+ tableControls.append(existingButton);
+ }
+ }
+
+ if (!bootstrapTableWrapper.parent().hasClass("table-container")) {
+ bootstrapTableWrapper.wrap(container);
+ }
+
+ bootstrapTableWrapper.show();
+ bootstrapTableWrapper.addClass("expanded");
+
+ const toggleButton = tableControls.find(".table-collapse-btn");
+ toggleButton.off("click").on("click", function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const tableId = $(this).data("table");
+
+ const targetTable = $(`#${tableId}`);
+ if (!targetTable.length) {
+ console.error("Target table not found:", tableId);
+ return;
+ }
+
+ const bootstrapWrapper = targetTable.closest(".bootstrap-table");
+ if (!bootstrapWrapper.length) {
+ console.error(
+ "Bootstrap wrapper not found for table:",
+ tableId
+ );
+ return;
+ }
+
+ const isVisible = bootstrapWrapper.is(":visible");
+
+ if (isVisible) {
+ bootstrapWrapper.slideUp(300);
+ bootstrapWrapper.removeClass("expanded");
+ $(this).html(
+ ' Expand Table'
+ );
+ } else {
+ bootstrapWrapper.slideDown(300);
+ bootstrapWrapper.addClass("expanded");
+ $(this).html(
+ ' Collapse Table'
+ );
+ }
+ });
+ });
+
+ setTimeout(function () {
+ $(".bootstrap-table[data-auto-collapse='true']").each(function () {
+ const bootstrapWrapper = $(this);
+ const tableId = bootstrapWrapper.find("table").attr("id");
+ const toggleButton = $(`button[data-table="${tableId}"]`);
+
+ bootstrapWrapper.slideUp(300);
+ bootstrapWrapper.removeClass("expanded");
+
+ if (toggleButton.length) {
+ toggleButton.html(
+ ' Expand Table'
+ );
+ }
+ });
+ }, 500);
+}
+
+function expandAllTables() {
+ $(".bootstrap-table").slideDown(300);
+ $(".bootstrap-table").addClass("expanded");
+
+ $(".table-collapse-btn").each(function () {
+ $(this).html(
+ ' Collapse Table'
+ );
+ });
+
+ $(".section-header").attr("aria-expanded", "true");
+ $(".section-body").slideDown(300);
+}
+
+function collapseAllTables() {
+ $(".bootstrap-table").slideUp(300);
+ $(".bootstrap-table").removeClass("expanded");
+
+ $(".table-collapse-btn").each(function () {
+ $(this).html(' Expand Table');
+ });
+}
+
+function initBootstrapTable() {
+ $("table.table").each(function () {
+ const $table = $(this);
+
+ $table.bootstrapTable({
+ classes: "table table-bordered table-striped",
+ height: 800,
+ pagination: false,
+ showColumns: true,
+ showColumnsToggleAll: true,
+ showToggle: false,
+ clickToSelect: false,
+ minimumCountColumns: 2,
+ stickyHeader: true,
+ stickyHeaderOffsetY: 0,
+ theadClasses: "thead-light",
+ toolbar: "#toolbar",
+ resizable: true,
+ checkOnInit: false,
+ fixedColumns: true,
+ fixedNumber: 0,
+ sortStable: true,
+ undefinedText: "-",
+ showFullscreen: true,
+ widthUnit: "%",
+ headerStyle: function () {
+ return {
+ css: {
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "50px",
+ },
+ };
+ },
+ });
+
+ $table.closest(".bootstrap-table").css({
+ width: "100%",
+ "max-width": "100%",
+ });
+
+ $table.closest(".fixed-table-body").css({
+ "overflow-x": "auto",
+ });
+
+ $table.css({
+ width: "100%",
+ "min-width": "100%",
+ });
+
+ $table.find("thead th").each(function () {
+ const $th = $(this);
+ const $thInner = $th.find(".th-inner");
+
+ if ($thInner.length) {
+ $thInner.css({
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "20px",
+ display: "block",
+ });
+ }
+ });
+ });
+
+ $(window).on(
+ "resize",
+ debounce(function () {
+ $("table.table").bootstrapTable("resetView");
+ }, 100)
+ );
+}
+
+function wrapTablesInCollapsibleSections() {
+ $("h2").each(function (index) {
+ if ($(this).closest(".section-header").length > 0) {
+ return;
+ }
+
+ const header = $(this);
+ const headerText = header.text();
+ const headerId = header.attr("id") || `section-header-${index}`;
+
+ const nextTable = header.nextAll("table.table:first");
+
+ if (nextTable.length) {
+ if (nextTable.closest(".section-body").length > 0) {
+ return;
+ }
+
+ const wrapper = $('');
+
+ const sectionHeader = $(`
+
+ `);
+
+ const sectionBody = $(
+ ``
+ );
+
+ const bootstrapTable = nextTable.closest(".bootstrap-table");
+ const tableContainer = bootstrapTable.closest(".table-container");
+
+ if (tableContainer.length) {
+ tableContainer.detach().appendTo(sectionBody);
+ } else if (bootstrapTable.length) {
+ bootstrapTable.detach().appendTo(sectionBody);
+ } else {
+ nextTable.detach().appendTo(sectionBody);
+ }
+
+ wrapper.append(sectionHeader).append(sectionBody);
+
+ header.replaceWith(wrapper);
+ }
+ });
+
+ $(".section-header")
+ .off("click")
+ .on("click", function (e) {
+ if (
+ $(e.target).hasClass("copy-link-icon") ||
+ $(e.target).closest(".copy-link-icon").length ||
+ $(e.target).hasClass("table-collapse-btn") ||
+ $(e.target).closest(".table-collapse-btn").length
+ ) {
+ return;
+ }
+
+ const header = $(this);
+ const targetId = header.data("target");
+ const sectionBody = $(targetId);
+ const isExpanded = header.attr("aria-expanded") === "true";
+
+ if (isExpanded) {
+ header.attr("aria-expanded", "false");
+ sectionBody.slideUp(300);
+ } else {
+ header.attr("aria-expanded", "true");
+ sectionBody.slideDown(300);
+ }
+ });
+}
+
+function markTestCaseColumns() {
+ $("table.table th").each(function (index) {
+ const headerText = $(this).text().trim().toLowerCase();
+ if (
+ headerText.includes("test") &&
+ (headerText.includes("case") || headerText.includes("name"))
+ ) {
+ const colIndex = index + 1;
+ $(`table.table td:nth-child(${colIndex})`).addClass(
+ "test-case-name"
+ );
+ }
+ });
+}
+
+function setupEventHandlers() {
+ $(window).on("load", scrollToHashLocation);
+ $("body").on("click", ".toggle-link", toggleText);
+
+ $("#expand-all-tables")
+ .off("click")
+ .on("click", function (e) {
+ e.preventDefault();
+ expandAllTables();
+ });
+
+ $("#collapse-all-tables")
+ .off("click")
+ .on("click", function (e) {
+ e.preventDefault();
+ collapseAllTables();
+ });
+
+ $("#pagination-size").on("change", function () {
+ const pageSize = parseInt($(this).val(), 10);
+ applyPaginationToTables(pageSize);
+ });
+
+ $("#show-all-columns").on("click", function (e) {
+ e.preventDefault();
+ $("table.table").bootstrapTable("showAllColumns");
+ });
+
+ $("#show-failed-only").on("click", function (e) {
+ e.preventDefault();
+ filterByStatus("fail");
+ });
+
+ $("#show-passed-only").on("click", function (e) {
+ e.preventDefault();
+ filterByStatus("pass");
+ });
+
+ $("#export-csv").on("click", function (e) {
+ e.preventDefault();
+ $("table.table:visible").bootstrapTable("exportTable", {
+ type: "csv",
+ fileName: "report_export_" + new Date().toISOString().slice(0, 10),
+ });
+ });
+
+ $("#clear-all-filters").on("click", function () {
+ clearAllFilters();
+ });
+}
+
+function filterByStatus(status) {
+ $("#active-filters").show();
+ $("#filter-badges").html(
+ `Status: ${status}`
+ );
+
+ $("table.table").each(function () {
+ const $table = $(this);
+
+ let statusColIndex = -1;
+ $table.find("th").each(function (index) {
+ const headerText = $(this).text().trim().toLowerCase();
+ if (
+ headerText.includes("status") ||
+ headerText.includes("result") ||
+ headerText.includes("state")
+ ) {
+ statusColIndex = index;
+ return false;
+ }
+ });
+
+ if (statusColIndex >= 0) {
+ $table.bootstrapTable(
+ "filterBy",
+ {
+ [statusColIndex]: status,
+ },
+ {
+ filterAlgorithm: function (row, filters) {
+ const cellText = $(row[statusColIndex])
+ .text()
+ .trim()
+ .toLowerCase();
+ return cellText.includes(status);
+ },
+ }
+ );
+ }
+ });
+}
+
+function clearAllFilters() {
+ $("table.table").each(function () {
+ const $table = $(this);
+ $table.bootstrapTable("clearFilterControl");
+ $table.bootstrapTable("resetSearch");
+ });
+
+ $("#active-filters").hide();
+ $("#filter-badges").empty();
+}
+
+function scrollToHashLocation() {
+ const hash = window.location.hash;
+ if (hash) {
+ setTimeout(() => {
+ const target = $(hash);
+ if (target.length) {
+ const sectionBody = target.closest(".section-body");
+ if (sectionBody.length) {
+ const sectionHeader = sectionBody.prev(".section-header");
+ if (sectionHeader.attr("aria-expanded") !== "true") {
+ sectionHeader.attr("aria-expanded", "true");
+ sectionBody.slideDown(0);
+ }
+
+ const containingTable = target.closest("table.table");
+ if (containingTable.length) {
+ const bootstrapWrapper =
+ containingTable.closest(".bootstrap-table");
+ if (!bootstrapWrapper.is(":visible")) {
+ bootstrapWrapper.show();
+ const tableBtn = bootstrapWrapper
+ .parent()
+ .find(".table-controls")
+ .find(".table-collapse-btn");
+ tableBtn.html(
+ ' Collapse Table'
+ );
+ }
+ }
+ }
+
+ $("html, body").animate(
+ { scrollTop: target.offset().top - 20 },
+ 100
+ );
+ }
+ }, 300);
+ }
+}
+
+function copyPermalink(anchorId) {
+ const fullUrl = `${window.location.origin}${window.location.pathname}${anchorId}`;
+ history.pushState(null, null, anchorId);
+ navigator.clipboard.writeText(fullUrl);
+
+ const tooltip = $(`
+
+ Link copied to clipboard!
+
+ `);
+
+ const icon = $(event.target).closest(".copy-link-icon");
+ if (icon.length) {
+ const originalClass = icon.attr("class");
+
+ icon.removeClass()
+ .addClass("fas fa-check copy-link-icon")
+ .css("color", "var(--esp-success)");
+
+ setTimeout(() => {
+ icon.attr("class", originalClass).css("color", "");
+ }, 1500);
+ }
+
+ $("body").append(tooltip);
+ setTimeout(() => tooltip.remove(), 2000);
+ scrollToHashLocation();
+}
+
+function toggleText(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const link = $(this);
+ const textSpan = link.siblings(".full-text");
+ const toggleSpan = link.siblings(".text-toggle");
+
+ const visible = textSpan.is(":visible");
+
+ if (visible) {
+ link.html(' Show More');
+ textSpan.hide();
+ toggleSpan.show();
+ } else {
+ link.html(' Show Less');
+ textSpan.show();
+ toggleSpan.hide();
+ }
+}
+
+function setupPagination() {
+ applyPaginationToTables(25);
+}
+
+function applyPaginationToTables(pageSize) {
+ localStorage.setItem("paginationSize", pageSize);
+
+ $("table.table").each(function () {
+ const $table = $(this);
+
+ if (pageSize > 0) {
+ $table.bootstrapTable("refreshOptions", {
+ pagination: true,
+ pageSize: pageSize,
+ pageList: [10, 25, 50, "All"],
+ });
+
+ $table
+ .closest(".bootstrap-table")
+ .find(".fixed-table-pagination")
+ .css({
+ display: "block",
+ visibility: "visible",
+ "background-color": "var(--esp-light)",
+ padding: "10px",
+ "border-top": "1px solid rgba(0, 0, 0, 0.05)",
+ });
+ } else {
+ $table.bootstrapTable("refreshOptions", {
+ pagination: false,
+ });
+
+ $table
+ .closest(".bootstrap-table")
+ .find(".fixed-table-pagination")
+ .css({
+ display: "none",
+ visibility: "hidden",
+ });
+ }
+ });
+}
+
+function optimizeScrollPerformance() {
+ window.isScrolling = false;
+ let scrollTimer = null;
+ let lastScrollTop = 0;
+
+ cacheAndFixDomElements();
+
+ const progressBar = document.getElementById("nav-progress-bar");
+ const backToTop = document.getElementById("back-to-top");
+
+ window.addEventListener(
+ "scroll",
+ function () {
+ window.isScrolling = true;
+
+ const scrollTop = window.scrollY;
+
+ if (Math.abs(scrollTop - lastScrollTop) > 5) {
+ lastScrollTop = scrollTop;
+
+ handleEssentialScrollUpdates(scrollTop, progressBar, backToTop);
+ }
+
+ clearTimeout(scrollTimer);
+ scrollTimer = setTimeout(function () {
+ window.isScrolling = false;
+ requestAnimationFrame(function () {
+ fixStickyHeaderAlignment();
+ fixTableHeaderText();
+ });
+ }, 150);
+ },
+ { passive: true }
+ );
+
+ lazyLoadVisibleImages();
+}
+
+function handleEssentialScrollUpdates(scrollTop, progressBar, backToTop) {
+ requestAnimationFrame(function () {
+ const docHeight =
+ Math.max(
+ document.body.scrollHeight,
+ document.body.offsetHeight,
+ document.documentElement.clientHeight,
+ document.documentElement.scrollHeight,
+ document.documentElement.offsetHeight
+ ) - window.innerHeight;
+ const scrollPercentage = (scrollTop / docHeight) * 100;
+ progressBar.style.width = scrollPercentage + "%";
+
+ if (scrollTop > 200) {
+ backToTop.classList.add("visible");
+ } else {
+ backToTop.classList.remove("visible");
+ }
+ });
+}
+
+function cacheAndFixDomElements() {
+ const stickyElements = document.querySelectorAll(
+ ".sticky-header-container, .section-header, .bootstrap-table thead, .fixed-table-header"
+ );
+
+ for (let i = 0; i < stickyElements.length; i++) {
+ const elem = stickyElements[i];
+ elem.style.transform = "translateZ(0)";
+ elem.style.willChange = "transform";
+ elem.style.backfaceVisibility = "hidden";
+ }
+
+ document.body.style.willChange = "scroll-position";
+ document.body.style.backfaceVisibility = "hidden";
+
+ const headers = document.querySelectorAll(
+ ".bootstrap-table .table thead th, .sticky-header-container th"
+ );
+ for (let i = 0; i < headers.length; i++) {
+ const header = headers[i];
+ header.style.position = "sticky";
+ header.style.top = "0";
+ header.style.zIndex = "100";
+ header.style.transform = "translateZ(0)";
+
+ const thInners = header.querySelectorAll(".th-inner");
+ for (let j = 0; j < thInners.length; j++) {
+ const inner = thInners[j];
+ inner.style.whiteSpace = "normal";
+ inner.style.overflow = "visible";
+ inner.style.textOverflow = "clip";
+ inner.style.height = "auto";
+ inner.style.minHeight = "20px";
+ inner.style.display = "block";
+ }
+ }
+}
+
+function lazyLoadVisibleImages() {
+ const lazyImages = document.querySelectorAll("img[data-src]");
+ if (lazyImages.length === 0) return;
+
+ const loadImage = function (img) {
+ if (img.dataset.src) {
+ img.src = img.dataset.src;
+ img.removeAttribute("data-src");
+ }
+ };
+
+ lazyImages.forEach((img) => {
+ if (isElementInViewport(img)) {
+ loadImage(img);
+ }
+ });
+
+ document.addEventListener(
+ "scroll",
+ debounce(function () {
+ lazyImages.forEach((img) => {
+ if (img.dataset.src && isElementInViewport(img)) {
+ loadImage(img);
+ }
+ });
+ }, 200),
+ { passive: true }
+ );
+}
+
+function isElementInViewport(el) {
+ const rect = el.getBoundingClientRect();
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <=
+ (window.innerHeight || document.documentElement.clientHeight) &&
+ rect.right <=
+ (window.innerWidth || document.documentElement.clientWidth)
+ );
+}
+
+function fixTableHeaderText() {
+ if (window.isScrolling) return;
+
+ $(".bootstrap-table .table thead th, .sticky-header-container th").each(
+ function () {
+ const $th = $(this);
+
+ $th.css({
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "50px",
+ });
+
+ const $thInner = $th.find(".th-inner");
+ if ($thInner.length) {
+ $thInner.css({
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "20px",
+ display: "block",
+ "line-height": "1.4",
+ });
+
+ $thInner.find("span, div").css({
+ "white-space": "normal",
+ overflow: "visible",
+ });
+ }
+ }
+ );
+}
+
+function forceEnableStickyHeaders() {
+ if (!window.isScrolling) {
+ $("table.table thead th").css({
+ position: "sticky",
+ top: "0",
+ "z-index": "100",
+ "background-color": "var(--esp-light)",
+ transform: "translateZ(0)",
+ "will-change": "transform",
+ "backface-visibility": "hidden",
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "50px",
+ });
+
+ $("table.table").each(function () {
+ const $table = $(this);
+ const bootstrapTable = $table.closest(".bootstrap-table");
+
+ if (
+ bootstrapTable.length &&
+ bootstrapTable.find(".sticky-header-container").length === 0
+ ) {
+ const $thead = $table.find("thead").clone();
+ const $stickyContainer = $(
+ ''
+ );
+ const $stickyTable = $('').append(
+ $thead
+ );
+
+ $stickyContainer.append($stickyTable);
+ bootstrapTable.prepend($stickyContainer);
+
+ const tableWidth = $table.width();
+ $stickyContainer.css({
+ position: "sticky",
+ top: "0",
+ "z-index": "100",
+ width: tableWidth + "px",
+ overflow: "hidden",
+ "background-color": "var(--esp-light)",
+ "will-change": "transform",
+ "backface-visibility": "hidden",
+ transform: "translateZ(0)",
+ });
+
+ const $originalThs = $table.find("thead th");
+ const $stickyThs = $stickyTable.find("th");
+
+ $originalThs.each(function (i) {
+ if (i < $stickyThs.length) {
+ const width = $(this).outerWidth();
+ $($stickyThs[i]).css({
+ width: width + "px",
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "50px",
+ });
+
+ const $inner = $($stickyThs[i]).find(".th-inner");
+ if ($inner.length) {
+ $inner.css({
+ "white-space": "normal",
+ overflow: "visible",
+ "text-overflow": "clip",
+ height: "auto",
+ "min-height": "20px",
+ display: "block",
+ "line-height": "1.4",
+ });
+ }
+ }
+ });
+ }
+ });
+ }
+}
diff --git a/tools/ci/dynamic_pipelines/templates/styles.css b/tools/ci/dynamic_pipelines/templates/styles.css
new file mode 100644
index 0000000000..cc3c43b9b4
--- /dev/null
+++ b/tools/ci/dynamic_pipelines/templates/styles.css
@@ -0,0 +1,1095 @@
+/**
+ * Espressif Report Template Styles
+ * Optimized for desktop-only application
+ */
+
+/* Variables and base settings */
+:root {
+ /* Color palette */
+ --esp-primary: #e83711;
+ --esp-secondary: #f3c300;
+ --esp-dark: #282430;
+ --esp-light: #f8f9fa;
+ --esp-blue: #3a86ff;
+ --esp-gray: #707070;
+ --esp-success: #25be7b;
+ --esp-warning: #f3c300;
+ --esp-danger: #e83711;
+
+ /* UI elements */
+ --body-bg: #f5f7fa;
+ --card-bg: #ffffff;
+ --text-color: #282430;
+ --border-radius: 8px;
+ --box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
+ --transition-speed: 0.3s;
+
+ /* Fixed desktop layout variables */
+ --container-width: 1600px;
+ --sidebar-width: 280px;
+ --header-height: 60px;
+}
+
+/* ---------------------------------- */
+/* Base styles */
+/* ---------------------------------- */
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Helvetica Neue", Arial, sans-serif;
+ line-height: 1.6;
+ color: var(--text-color);
+ background-color: var(--body-bg);
+ transition: background-color var(--transition-speed);
+ min-width: 1024px;
+ overflow-y: scroll;
+}
+
+.container-fluid {
+ max-width: 100% !important;
+ padding: 20px !important;
+ margin: 0 auto;
+}
+
+/* Typography */
+h2 {
+ padding-top: 3rem;
+ margin-bottom: 0px;
+ color: var(--esp-dark);
+ font-weight: 600;
+ background-color: #f5f7fa;
+}
+
+/* ---------------------------------- */
+/* Performance Optimizations */
+/* ---------------------------------- */
+/* Hardware acceleration utility class */
+.hw-accelerated {
+ transform: translateZ(0);
+ will-change: transform;
+ backface-visibility: hidden;
+}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.05);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(0, 0, 0, 0.3);
+}
+
+/* Reduced motion for accessibility */
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ .bootstrap-table,
+ .section-body,
+ .sticky-header-container,
+ .section-header {
+ transition: none !important;
+ }
+}
+
+/* ---------------------------------- */
+/* Navigation and Header */
+/* ---------------------------------- */
+
+/* Header styling */
+.report-header {
+ display: flex;
+ align-items: center;
+ padding: 1.2rem 0;
+ margin-bottom: 2rem;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ position: relative;
+ justify-content: space-between;
+ background-color: #f8f9fa;
+}
+
+.report-header .logo {
+ height: 30px;
+ margin-bottom: 0;
+ padding-left: 15px;
+}
+
+.report-header .title-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: center;
+ margin: 0 auto;
+}
+
+.report-header h1 {
+ margin: 0;
+ font-weight: 700;
+ font-size: 1.8rem;
+ line-height: 1.2;
+ display: inline-block;
+ text-align: center;
+}
+
+.report-header p {
+ margin-bottom: 0;
+ font-size: 0.85rem;
+ text-align: center;
+ color: #707070;
+}
+
+.report-header::after {
+ content: "";
+ position: absolute;
+ bottom: -3px;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background-color: var(--esp-primary);
+}
+
+.logo-container {
+ width: 180px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.spacer {
+ width: 180px; /* Same as logo-container for balance */
+}
+
+/* Navigation progress bar */
+.nav-progress-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ background-color: transparent;
+ z-index: 1001;
+}
+
+.nav-progress-bar {
+ height: 100%;
+ width: 0;
+ background-color: var(--esp-primary);
+ transition: width 0.2s ease-out;
+}
+
+/* ---------------------------------- */
+/* Tables - Complete Reset & Rebuild */
+/* ---------------------------------- */
+
+/* Base table appearance */
+.table {
+ width: 100%;
+ margin-bottom: 0;
+ background-color: var(--card-bg);
+ border-collapse: separate;
+ border-spacing: 0;
+}
+
+/* Table header cells */
+.table > thead > tr > th {
+ padding: 12px 15px;
+ font-weight: 600;
+ color: var(--esp-dark);
+ background-color: var(--esp-light);
+ border-bottom: 2px solid rgba(0, 0, 0, 0.1);
+ text-align: left;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+/* Table data cells */
+.table > tbody > tr > td {
+ padding: 12px 15px;
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+/* Striped rows */
+.table-striped > tbody > tr:nth-of-type(odd) {
+ background-color: rgba(0, 0, 0, 0.02);
+}
+
+.table-striped > tbody > tr:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+/* Bootstrap Table container */
+.bootstrap-table {
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ box-shadow: var(--box-shadow);
+ margin-bottom: 1rem;
+}
+
+/* Bootstrap Table scrollable body */
+.bootstrap-table .fixed-table-body {
+ max-height: 800px;
+ overflow-y: auto;
+ border: none !important;
+}
+
+/* Remove any borders from the fixed-table-container */
+.bootstrap-table .fixed-table-container {
+ border: none !important;
+}
+
+/* Override Bootstrap's fixed header styles to avoid conflicts */
+.bootstrap-table .fixed-table-header table {
+ background-color: var(--esp-light);
+}
+
+.bootstrap-table .fixed-table-header {
+ background-color: var(--esp-light);
+}
+
+/* Column width settings for better layout */
+.table > thead > tr > th:nth-child(1),
+.table > tbody > tr > td:nth-child(1) {
+ width: 20%;
+ min-width: 150px;
+}
+
+.table > thead > tr > th:nth-child(2),
+.table > tbody > tr > td:nth-child(2) {
+ width: 20%;
+ min-width: 150px;
+}
+
+.table > thead > tr > th:nth-child(3),
+.table > tbody > tr > td:nth-child(3) {
+ width: 20%;
+ min-width: 150px;
+}
+
+.table > thead > tr > th:nth-child(n + 4),
+.table > tbody > tr > td:nth-child(n + 4) {
+ width: auto;
+ min-width: 100px;
+}
+
+/* Ensure sticky section headers are above table headers */
+.sticky-section-header {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ background-color: #f5f7fa;
+}
+
+/* Test case names */
+td.test-case-name {
+ white-space: normal;
+ overflow: visible;
+ word-break: break-word;
+}
+
+/* Bootstrap table customizations */
+.bootstrap-table {
+ margin-bottom: 0;
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow);
+ background-color: var(--card-bg);
+ overflow: hidden;
+ max-height: 2000px;
+ transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
+ opacity: 1;
+ width: 100% !important;
+ position: relative;
+}
+
+.bootstrap-table.expanded {
+ max-height: 2000px;
+ opacity: 1;
+}
+
+/* Main container setup */
+.bootstrap-table .fixed-table-container {
+ position: relative !important;
+ overflow: visible !important;
+}
+
+/* Table headers - sticky relative to .fixed-table-body */
+.bootstrap-table .table thead th {
+ position: sticky !important;
+ top: 0 !important;
+ z-index: 10;
+ background-color: var(--esp-light);
+ border-bottom: 2px solid rgba(0, 0, 0, 0.1);
+ padding: 12px 15px !important;
+}
+
+/* Fixed-table-header should be hidden when scrolling */
+.bootstrap-table .fixed-table-header {
+ display: none !important;
+}
+
+/* Handle Bootstrap Table sticky header container */
+.bootstrap-table .sticky-header-container {
+ display: none !important; /* Use our own sticky headers instead */
+}
+
+/* Ensure th-inner elements are visible */
+.bootstrap-table .fixed-table-container .table thead th .th-inner {
+ white-space: normal !important;
+ overflow: visible !important;
+ text-overflow: clip !important;
+ height: auto !important;
+ min-height: 20px;
+ line-height: 1.4;
+ padding: 8px;
+ background-color: var(--esp-light);
+ display: block !important;
+}
+
+/* Table container shouldn't have overflow-x at the top level */
+.table-container {
+ width: 100%;
+ max-width: 100%;
+ margin: 0 auto;
+ background-color: var(--card-bg);
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ box-shadow: var(--box-shadow);
+ position: relative;
+}
+
+/* Fix for table header rendering in the sticky context */
+.bootstrap-table thead {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+/* Table controls */
+.table-controls {
+ background-color: var(--esp-light);
+ border-top-left-radius: var(--border-radius);
+ border-top-right-radius: var(--border-radius);
+ padding: 12px 15px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.table-controls .row-count {
+ margin-bottom: 0;
+ white-space: nowrap;
+}
+
+.table-controls .btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ transition: all 0.2s ease;
+}
+
+/* Table toolbar */
+.fixed-table-toolbar {
+ padding: 10px;
+ background-color: var(--esp-light);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.fixed-table-toolbar .search {
+ display: flex;
+ align-items: center;
+}
+
+.fixed-table-toolbar .search input {
+ border-radius: var(--border-radius);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ padding: 5px 10px;
+ margin-right: 10px;
+}
+
+.fixed-table-toolbar .columns {
+ margin-left: auto;
+}
+
+.fixed-table-toolbar .btn {
+ border-radius: var(--border-radius);
+ padding: 5px 10px;
+ margin-left: 5px;
+}
+
+.fixed-table-toolbar .dropdown-menu {
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow);
+ padding: 8px;
+ border: none;
+}
+
+/* Table pagination */
+.fixed-table-pagination {
+ padding: 10px;
+ background-color: var(--esp-light);
+ border-bottom-left-radius: var(--border-radius);
+ border-bottom-right-radius: var(--border-radius);
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.fixed-table-pagination .pagination {
+ float: none !important;
+ order: 1;
+ flex: 1 1 auto;
+ display: flex;
+ justify-content: center;
+ margin: 0 auto;
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.fixed-table-pagination .pagination-detail .pagination-info {
+ display: none;
+ float: none !important;
+ order: 2;
+ flex: 0 0 auto;
+}
+
+.fixed-table-pagination .pagination li a {
+ border-radius: 4px;
+ margin: 0 2px;
+ border: 1px solid #dee2e6;
+ min-width: 34px;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--esp-dark);
+ transition: all 0.2s ease;
+}
+
+.fixed-table-pagination .pagination li.active a {
+ background-color: var(--esp-primary);
+ border-color: var(--esp-primary);
+ color: white;
+}
+
+.fixed-table-pagination .pagination li a:hover:not(.active) {
+ background-color: #e9ecef;
+ color: var(--esp-primary);
+}
+
+/* Pagination options */
+.pagination-options .form-select-sm {
+ border-radius: var(--border-radius);
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ height: 31px;
+ padding: 0.25rem 2rem 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ background-color: var(--esp-light);
+ color: var(--esp-dark);
+ cursor: pointer;
+ transition: all var(--transition-speed);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+.pagination-options .form-select-sm:hover {
+ border-color: var(--esp-primary);
+}
+
+.pagination-options .form-select-sm:focus {
+ border-color: var(--esp-primary);
+ box-shadow: 0 0 0 0.15rem rgba(232, 55, 17, 0.25);
+ outline: none;
+}
+
+/* ---------------------------------- */
+/* Collapsible Sections */
+/* ---------------------------------- */
+.section-collapsible {
+ margin-bottom: 2rem;
+ border-radius: var(--border-radius);
+ overflow: hidden;
+ box-shadow: var(--box-shadow);
+ background-color: var(--card-bg);
+ transition: transform var(--transition-speed);
+}
+
+.section-collapsible:hover {
+ transform: translateY(-2px);
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ padding: 1rem 1.5rem;
+ background-color: #f5f7fa;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+ transition: background-color var(--transition-speed),
+ box-shadow var(--transition-speed);
+}
+
+.section-header:hover {
+ background-color: #e9ecef;
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
+}
+
+.section-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ flex-grow: 1;
+ background-color: transparent;
+ position: relative;
+ z-index: 2;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+}
+
+.section-header h2 .heading-text {
+ margin-right: 6px;
+}
+
+.section-header .collapse-indicator {
+ margin-left: 10px;
+ font-size: 1.5rem;
+ transition: transform var(--transition-speed);
+ color: var(--esp-primary);
+ position: relative;
+ z-index: 2;
+}
+
+.section-header[aria-expanded="false"] .collapse-indicator {
+ transform: rotate(-90deg);
+}
+
+.copy-link-icon {
+ margin-left: 8px;
+ font-size: 1.5rem;
+ color: var(--esp-gray);
+ opacity: 0.7;
+ cursor: pointer;
+ transition: opacity 0.2s ease, color 0.2s ease;
+ position: relative;
+ z-index: 2;
+}
+
+.copy-link-icon:hover {
+ opacity: 1;
+ color: var(--esp-primary);
+}
+
+.sticky-section-header {
+ position: sticky;
+ top: 0;
+ z-index: 99;
+ background-color: #f5f7fa;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+}
+
+.section-body {
+ padding: 1.5rem;
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out,
+ opacity 0.3s ease-in-out;
+ padding-top: 0;
+ padding-bottom: 0;
+ opacity: 0;
+}
+
+.section-body.expanded {
+ max-height: 5000px;
+ padding: 1.5rem;
+ opacity: 1;
+}
+
+.section-body.collapsed {
+ display: none;
+}
+
+/* ---------------------------------- */
+/* Status Badges */
+/* ---------------------------------- */
+.status-badge {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ font-weight: 500;
+ text-align: center;
+ min-width: 80px;
+ transition: background-color 0.2s ease, transform 0.2s ease;
+}
+
+.status-badge.success {
+ background-color: rgba(37, 190, 123, 0.1);
+ color: var(--esp-success);
+ border: 1px solid rgba(37, 190, 123, 0.2);
+}
+
+.status-badge.warning {
+ background-color: rgba(243, 195, 0, 0.1);
+ color: var(--esp-warning);
+ border: 1px solid rgba(243, 195, 0, 0.2);
+}
+
+.status-badge.danger {
+ background-color: rgba(232, 55, 17, 0.1);
+ color: var(--esp-danger);
+ border: 1px solid rgba(232, 55, 17, 0.2);
+}
+
+.status-badge:hover {
+ transform: translateY(-1px);
+}
+
+/* ---------------------------------- */
+/* Buttons */
+/* ---------------------------------- */
+.btn-outline-secondary {
+ border-color: #ced4da;
+ color: var(--esp-dark);
+}
+
+.btn-outline-secondary:hover {
+ background-color: var(--esp-light);
+ border-color: var(--esp-gray);
+ color: var(--esp-dark);
+}
+
+.btn-esp {
+ background-color: var(--esp-primary);
+ border: none;
+ color: #fff;
+ transition: all var(--transition-speed);
+}
+
+.btn-esp:hover {
+ background-color: #cf3110;
+ color: #fff;
+ box-shadow: 0 4px 8px rgba(232, 55, 17, 0.2);
+}
+
+.btn-esp-secondary {
+ background-color: var(--esp-secondary);
+ border: none;
+ color: var(--esp-dark);
+ transition: all var(--transition-speed);
+}
+
+.btn-esp-secondary:hover {
+ background-color: #dab000;
+ color: var(--esp-dark);
+ box-shadow: 0 4px 8px rgba(243, 195, 0, 0.2);
+}
+
+/* Table collapse button - updated to appear on the right */
+.table-collapse-btn {
+ background-color: var(--esp-light);
+ border: none;
+ color: var(--esp-gray);
+ padding: 6px 12px;
+ border-radius: var(--border-radius);
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: all var(--transition-speed);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ margin-left: auto; /* Push to the right side */
+}
+
+.table-collapse-btn:hover {
+ background-color: #e9ecef;
+ color: var(--esp-primary);
+}
+
+.table-collapse-btn i {
+ margin-right: 5px;
+}
+
+.clear-filter-btn {
+ transition: all 0.2s ease;
+ margin-top: 10px;
+}
+
+.clear-filter-btn:hover {
+ background-color: var(--esp-light);
+ color: var(--esp-primary);
+}
+
+.close-alert {
+ background: none;
+ border: none;
+ color: #856404;
+ font-size: 0.8rem;
+ cursor: pointer;
+ padding: 4px;
+ margin-left: auto;
+ opacity: 0.7;
+ transition: opacity 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.close-alert:hover {
+ opacity: 1;
+}
+
+/* Floating action buttons */
+.floating-actions {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.floating-action-btn {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background-color: var(--esp-primary);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
+ cursor: pointer;
+ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275),
+ background-color 0.3s ease, box-shadow 0.3s ease;
+ font-size: 1.2rem;
+}
+
+.floating-action-btn:hover {
+ transform: scale(1.1) translateY(-2px);
+}
+
+.back-to-top {
+ background-color: var(--esp-secondary);
+ color: var(--esp-dark);
+}
+
+/* ---------------------------------- */
+/* Utilities */
+/* ---------------------------------- */
+/* Text toggles */
+.text-toggle,
+.full-text {
+ cursor: pointer;
+}
+
+.toggle-link {
+ display: inline-block;
+ margin-left: 8px;
+ font-size: 0.8rem;
+ color: var(--esp-blue);
+ text-decoration: none;
+}
+
+.toggle-link:hover {
+ text-decoration: underline;
+ color: var(--esp-primary);
+}
+
+/* Row count label */
+.row-count {
+ font-size: 0.8rem;
+ color: var(--esp-gray);
+ margin-bottom: 15px;
+ display: block;
+}
+
+/* Table state indicators */
+.table-empty-state,
+.table-loading-state {
+ text-align: center;
+ padding: 50px 20px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.table-empty-state {
+ color: var(--esp-gray);
+ background-color: rgba(0, 0, 0, 0.01);
+ border-radius: var(--border-radius);
+}
+
+.table-empty-state p {
+ margin-top: 10px;
+ font-size: 1.1rem;
+}
+
+.table-filtered-indicator {
+ text-align: center;
+ padding: 30px 20px;
+ color: var(--esp-warning);
+ background-color: rgba(243, 195, 0, 0.05);
+ border-radius: var(--border-radius);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border: 1px dashed rgba(243, 195, 0, 0.3);
+ margin: 20px;
+}
+
+.table-filtered-indicator p {
+ margin: 10px 0;
+ font-size: 1rem;
+}
+
+.table-loading-indicator {
+ padding: 20px;
+ background-color: rgba(0, 0, 0, 0.02);
+ border-radius: var(--border-radius);
+ margin: 10px;
+ text-align: center;
+ color: var(--esp-gray);
+ animation: pulse 1.5s infinite ease-in-out;
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 0.6;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0.6;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* Dropdown and other UI elements */
+.dropdown-menu {
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow);
+ border: none;
+ padding: 0.5rem;
+}
+
+.dropdown-item {
+ border-radius: 4px;
+ padding: 8px 12px;
+}
+
+.dropdown-item:hover {
+ background-color: rgba(232, 55, 17, 0.1);
+}
+
+/* Column visibility controls */
+.column-visibility-controls {
+ margin-right: 10px;
+}
+
+.column-visibility-menu {
+ max-height: 350px;
+ overflow-y: auto;
+ padding: 8px 0;
+ min-width: 220px;
+}
+
+.column-visibility-menu .dropdown-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 16px;
+ white-space: normal;
+ word-break: break-word;
+}
+
+.column-visibility-menu .dropdown-item:hover {
+ background-color: rgba(232, 55, 17, 0.05);
+}
+
+.column-visibility-menu .dropdown-header {
+ font-weight: 600;
+ color: var(--esp-dark);
+ padding: 8px 16px;
+}
+
+.column-visibility-menu .dropdown-divider {
+ margin: 5px 0;
+}
+
+.column-toggle .column-hidden {
+ width: 16px;
+ color: var(--esp-primary);
+}
+
+.column-toggle .column-hidden {
+ color: var(--esp-gray);
+}
+
+/* Active filters */
+#active-filters {
+ margin-top: 10px;
+ margin-bottom: 15px !important;
+ display: flex;
+ align-items: center;
+ background-color: #f8f9fa;
+ padding: 8px 12px;
+ border-radius: var(--border-radius);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+#filter-badges .badge {
+ margin-right: 5px;
+ padding: 6px 10px;
+ font-weight: normal;
+}
+
+#clear-all-filters {
+ margin-left: auto;
+ color: var(--esp-primary);
+ text-decoration: none;
+ font-size: 0.9rem;
+ padding: 4px 8px;
+}
+
+#clear-all-filters:hover {
+ text-decoration: underline;
+ background-color: rgba(232, 55, 17, 0.05);
+ border-radius: 4px;
+}
+
+/* Tooltip actions */
+.tooltip-actions {
+ position: absolute;
+ right: 60px;
+ background-color: white;
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow);
+ padding: 10px;
+ width: 200px;
+ display: none;
+}
+
+.tooltip-actions.show {
+ display: block;
+ animation: fadeIn var(--transition-speed);
+}
+
+/* Smooth scrolling */
+html.scrolling-top {
+ scroll-behavior: smooth;
+}
+
+/* ---------------------------------- */
+/* Responsive Styles */
+/* ---------------------------------- */
+@media (max-width: 1200px) {
+ .bootstrap-table .fixed-table-body {
+ overflow-x: auto;
+ max-height: 600px;
+ }
+
+ .table-container {
+ width: 100% !important;
+ padding: 0 !important;
+ }
+}
+
+@media (max-width: 768px) {
+ .container-fluid {
+ padding-left: 20px !important;
+ padding-right: 20px !important;
+ }
+
+ .table-responsive {
+ overflow-x: auto;
+ }
+
+ .bootstrap-table .fixed-table-body {
+ max-height: 500px;
+ }
+
+ .table-controls {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .table-controls-right {
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ .logo-container,
+ .spacer {
+ width: 100px;
+ }
+}
+
+/* ---------------------------------- */
+/* Bootstrap Table Sticky Header Specific Styles */
+/* ---------------------------------- */
+
+/* Make sure sticky header container is visible and properly positioned */
+.bootstrap-table .sticky-header-container {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ background-color: var(--esp-light);
+}
+
+/* Ensure the sticky header is properly styled and visible */
+.bootstrap-table .sticky-header {
+ overflow: visible;
+ position: relative;
+}
+
+/* Styles for the headers in the sticky container */
+.bootstrap-table .sticky-header-container thead th {
+ background-color: var(--esp-light);
+ color: var(--esp-dark);
+ font-weight: 600;
+ padding: 12px 15px;
+ border-bottom: 2px solid rgba(0, 0, 0, 0.1);
+ text-align: left;
+ position: static !important; /* Important to avoid double sticky behavior */
+}
+
+/* Styles for header cells inner content */
+.bootstrap-table .sticky-header-container th .th-inner {
+ padding: 0 !important;
+ line-height: 1.4;
+ white-space: normal !important;
+ overflow: visible !important;
+ text-overflow: clip !important;
+ font-weight: inherit !important;
+}
diff --git a/tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml b/tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml
index b3a615652d..66f17fde01 100644
--- a/tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml
+++ b/tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml
@@ -12,10 +12,11 @@ generate_pytest_build_report:
- skipped_apps.html
- build_report.html
- test_related_apps_download_urls.yml
- expire_in: 1 week
+ expire_in: 2 week
when: always
script:
+ - env
- python tools/ci/dynamic_pipelines/scripts/generate_report.py --report-type build
- python tools/ci/previous_stage_job_status.py --stage build
diff --git a/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_build_report.html b/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_build_report.html
index bcece17430..bd064576a1 100644
--- a/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_build_report.html
+++ b/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_build_report.html
@@ -1,8 +1,15 @@
-
+
+
Build Report
+
+
+
-
- Failed Apps
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_job_report.html b/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_job_report.html
index 3c35a534fd..2a9c367d9a 100644
--- a/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_job_report.html
+++ b/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_job_report.html
@@ -1,8 +1,15 @@
-
+
+
Job Report
+
+
+
-
- Failed Jobs (Excludes "integration_test" and "target_test" jobs)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Failed Jobs (Excludes "integration_test" and "target_test" jobs)
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_target_test_report.html b/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_target_test_report.html
index 1e38c58e35..225c2877f5 100644
--- a/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_target_test_report.html
+++ b/tools/ci/dynamic_pipelines/tests/test_report_generator/reports_sample_data/expected_target_test_report.html
@@ -1,8 +1,15 @@
-
+
+
Test Report
+
+
+
-
- Failed Test Cases on Other branches (Excludes Known Failure Cases)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Testcases failed on your branch as well as on others (known failures are excluded)
Test Case |
@@ -266,78 +1410,1155 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/tools/ci/dynamic_pipelines/utils.py b/tools/ci/dynamic_pipelines/utils.py
index 69489bd7cc..20f4a16f67 100644
--- a/tools/ci/dynamic_pipelines/utils.py
+++ b/tools/ci/dynamic_pipelines/utils.py
@@ -58,7 +58,7 @@ def parse_testcases_from_filepattern(junit_report_filepattern: str) -> t.List[Te
"""
Parses test cases from XML files matching the provided file pattern.
- >>> test_cases = parse_testcases_from_filepattern("path/to/your/junit/reports/*.xml")
+ >>> test_cases = parse_testcases_from_filepattern('path/to/your/junit/reports/*.xml')
:param junit_report_filepattern: The file pattern to match XML files containing JUnit test reports.
:return: List[TestCase]: A list of TestCase objects parsed from the XML files.
@@ -124,7 +124,10 @@ def fetch_failed_jobs(commit_id: str) -> t.List[GitlabJob]:
response = requests.post(
f'{CI_DASHBOARD_API}/jobs/failure_ratio',
headers={'CI-Job-Token': CI_JOB_TOKEN},
- json={'job_names': failed_job_names, 'exclude_branches': [os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', '')]},
+ json={
+ 'job_names': failed_job_names,
+ 'exclude_branches': [os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', '')],
+ },
)
if response.status_code != 200:
print(f'Failed to fetch jobs failure rate data: {response.status_code} with error: {response.text}')
@@ -185,15 +188,14 @@ def fetch_app_metrics(
json={
'source_commit_sha': source_commit_sha,
'target_commit_sha': target_commit_sha,
- }
+ },
)
if response.status_code != 200:
print(f'Failed to fetch build info: {response.status_code} - {response.text}')
else:
response_data = response.json()
build_info_map = {
- f"{info['app_path']}_{info['config_name']}_{info['target']}": info
- for info in response_data.get('data', [])
+ f'{info["app_path"]}_{info["config_name"]}_{info["target"]}': info for info in response_data.get('data', [])
}
return build_info_map
@@ -253,7 +255,7 @@ def get_repository_file_url(file_path: str) -> str:
def known_failure_issue_jira_fast_link(_item: TestCase) -> str:
"""
- Generate a JIRA fast link for known issues with relevant test case details.
+ Generate a JIRA fast link for known issues with relevant test case details.
"""
jira_url = os.getenv('JIRA_SERVER')
jira_pid = os.getenv('JIRA_KNOWN_FAILURE_PID', '10514')
@@ -269,14 +271,14 @@ def known_failure_issue_jira_fast_link(_item: TestCase) -> str:
'issuetype': jira_issuetype,
'summary': f'[Test Case]{_item.name}',
'description': (
- f"job_url: {quote(_item.ci_job_url, safe=':/')}\n\n"
- f"dut_log_url: {quote(_item.dut_log_url, safe=':/')}\n\n"
+ f'job_url: {quote(_item.ci_job_url, safe=":/")}\n\n'
+ f'dut_log_url: {quote(_item.dut_log_url, safe=":/")}\n\n'
f'ci_dashboard_url: {_item.ci_dashboard_url}\n\n'
),
'components': jira_component,
'priority': jira_priority,
'assignee': jira_assignee,
- 'versions': jira_affected_versions
+ 'versions': jira_affected_versions,
}
query_string = urlencode(params)
return f'Create'