import json import logging import os from datetime import date, datetime, timedelta import time import traceback import sys CWD = os.path.dirname(__file__) sys.path.append(os.path.join(CWD,'site-packages')) from fpdf import FPDF from fpdf.fonts import FontFace from fpdf.enums import TableCellFillMode from fpdf.drawing import Line class Document(FPDF): RED = (255, 0, 0) PALE_RED = (255, 94, 94) GREEN = (0, 255, 0) PALE_GREEN = (167, 217, 173) WHITE = (255, 255, 255) BLACK = (0, 0, 0) def __init__(self, header_text=None, footer_text=None): super().__init__() self.chapter_no = 0 self.subchapter_no = 0 self.heading_sz = dict(enumerate((18, 16, 14, 12, 10), start=1)) self.header_text = header_text self.footer_text = footer_text self.family = 'times' try: self.add_font('georgia', '', r"c:\WINDOWS\Fonts\georgia.ttf") self.add_font('georgia', 'B', r"c:\WINDOWS\Fonts\georgiab.ttf") self.add_font('georgia', 'I', r"c:\WINDOWS\Fonts\georgiai.ttf") self.add_font('georgia', 'BI', r"c:\WINDOWS\Fonts\georgiaz.ttf") self.family = 'georgia' except FileNotFoundError: pass self.reset_font() def reset_font(self): self.set_font(family=self.family, size=8) def header(self): if self.header_text is not None: # self.set_font(size=12) self.set_font(style='I', size=10) logo = os.path.join(CWD,'images','Leonardo_small.png') self.image(name=logo, x=10, y=14, w=30, keep_aspect_ratio=True) sw=self.get_string_width(s=self.header_text) self.text(x=(200 - sw), y=18, text=self.header_text) self.line(x1=10,y1=21,x2=200,y2=21) # self.cell(0, 10, border='B', align='L', link=self.image(r"c:\temp\Leonardo_small.png", w=30)) # self.cell(0, 10, self.header_text, border='B', align='R') # with self.table() as table: # row = table.row() # row.cell(align='L', link=self.image(r"c:\temp\Leonardo_small.png", w=30)) # row.cell(self.header_text, align='C') self.ln(20) def footer(self): if self.footer_text is not None: self.set_y(-15) self.set_font(style='I', size=8) self.cell(0, 10, f'{self.footer_text} - Page {self.page_no()}', border='T', align='R') def add_chapter(self, title, newpage=True): self.chapter_no += 1 self.subchapter_no = 0 if newpage is True: self.add_page() self.add_heading(3, f'{self.chapter_no} {title}', 'L') self.reset_font() def add_subchapter(self, title): self.subchapter_no += 1 self.add_heading(4, f'{self.chapter_no}.{self.subchapter_no} {title}', 'L') self.reset_font() def add_paragraph(self, text): self.multi_cell(0, 5, text, align='J') self.ln(10) def add_single_line(self, text, alignment='L', markdown=False): self.cell(0, 5, text, align=alignment, markdown=markdown) self.ln(10) def add_heading(self, level, text, alignment='L'): sz = self.heading_sz.get(level, 10) self.set_font(style='B', size=sz) self.cell(0, 10, text, border='0', align=alignment) self.ln(20) self.reset_font() class PDFClass: def __init__(self, time='0'): self.vq1_equipment_info = {} self.radio_ip = "" self.radio_model = "" self.radio_sw_version = "" self.execution_time = time self.radio_serial_number = "" self.session_index = 1 def start_doc(self, json_test: dict, head_foot: bool = True): """ This function creates a latex doc object, sets its geometric option and other configurations. :param json_test: the dict containing template with results and constants. :param head_foot: if True, will add a head if header key is in json_test. Will do nothing otherwise. :return: latex document. """ header_text = json_test["header"] if head_foot else None footer_text = json_test["footer"] if head_foot else None doc = Document(header_text, footer_text) return doc def pdf_preamble(self, doc: Document, json_test: dict): """ This function defines the preamble of a latex document :param doc: latex document created by start_doc. :param json_test: the dict containing template with results and constants. """ test_title = json_test["title"] test_author = 'Test Report' today = datetime.today().strftime('%d/%m/%Y - %H:%M') doc.set_title(test_title) doc.set_author(test_author) doc.add_page() doc.ln(10) doc.add_heading(1, test_title, 'C') doc.set_font(size=12) doc.cell(0, 10, f'{test_author}', align='C') doc.ln(10) doc.cell(0, 10, f'{today}', align='C') doc.reset_font() def pdf_name(self, json_test: dict, new_path: str): """ This creates a test case name depending on the current time. :param json_test: json config file for the actual test case given by the function get_config. :param new_path: path where the executer folder is located. :return: file path. """ logging.info("Setting PDF name") now = datetime.now() date_name = now.strftime("%Y%m%d_%H%M") file_name_only = f"{json_test['file_name']}_{date_name}" file_name = os.path.join(new_path, file_name_only) logging.info(f"Test: {file_name}") return file_name def pdf_test_information_section(self, doc: Document, json_test: dict): """ This function gets the receiver configuration and builds the Test Information chapter. :param doc: latex document created by start_doc. :param json_test: the dict containing template with results and constants. """ possible_info = {"radio_ip": "NaN", "radio_model": "NaN", "radio_sw_version": "NaN"} serial_number = "Not found" if self.radio_serial_number != "": serial_number = self.radio_serial_number rows = (('**ID**', json_test["ID"], '**Requirement**', json_test["requirement"]), ('**Strategy**', json_test["strategy"], '**Technique**', json_test["technique"]), ('**Level**', json_test["level"], '**Setup**', json_test["setup"]), ('**Author**', json_test["author"], '**Reviewer**', json_test["reviewer"]), ('**Equipment SN**', serial_number, '**Execution Time**', f'{self.execution_time}') ) doc.ln(40) doc.add_chapter('Test Information', False) doc.set_font(size=10) with doc.table( borders_layout="INTERNAL", col_widths=(60, 60, 60, 60), markdown=True, headings_style=FontFace(), line_height=6, text_align=("LEFT", "CENTER", "LEFT", "CENTER"), width=180) as table: for data_row in rows: table.row(data_row) if "$" not in json_test["description"]: logging.info("Creating row description") row = table.row() row.cell("**Description**") row.cell(json_test["description"], colspan=3) doc.ln(10) def pdf_step_execution(self, step_list: list, doc: Document): try: logging.info("Writing step results") doc.ln(10) doc.add_subchapter('Step execution and results') doc.set_font(size=9) name_step = 'Step ' step_nb = 0 with doc.table( borders_layout="INTERNAL", col_widths=(30, 90, 60, 60), headings_style=FontFace(emphasis="BOLD", size_pt=10), text_align=("LEFT", "LEFT", "LEFT", "CENTER"), width=180) as table: table.row(('Step', 'Description', 'Result', 'Error')) for step in step_list: step_nb = step_nb + 1 description = step["desc"] naming = name_step + str(step_nb) err_result = "" ex_result = "" color = doc.WHITE if (step.get("result"), step.get("expected")) == (None, None): color = doc.WHITE naming = '' step_nb = step_nb - 1 else: if not step.get("result"): color = doc.PALE_RED elif step.get("expected"): color = doc.PALE_GREEN style = FontFace(fill_color=color) if step.get("expected"): ex_result = step["expected"].replace("var_", "", 1) if step.get("error"): err_result = str(step["error"]) # step["error"].replace("var_", "", 1) row = table.row() for c in (naming, description, ex_result, err_result): row.cell(c, style=style) doc.ln(10) return True except Exception as e: logging.error(e) logging.exception("Error message:") traceback.print_exc() return False def pdf_step_result(self, name, passed, failure, doc: Document): tot = passed + failure fail = 0 if tot > 0: fail = 100 * failure / tot session_name = 'Test {}: {}'.format(self.session_index, name) self.session_index = self.session_index + 1 if failure > 0: line_color = doc.PALE_RED else: line_color = doc.PALE_GREEN rows = (('Total STEP', 'PASS', 'FAIL', '% of Failure'), (str(tot), str(passed), str(failure), '{:.02f}'.format(fail)) ) doc.add_chapter(session_name) doc.set_font(size=9) with doc.table( borders_layout="INTERNAL", cell_fill_color=line_color, cell_fill_mode=TableCellFillMode.ROWS, col_widths=(60, 60, 60, 60), # headings_style=FontFace(emphasis="BOLD", size_pt=10, color=234, fill_color=(128, 128, 128)), headings_style=FontFace(emphasis="BOLD", size_pt=10), line_height=6, text_align=("CENTER", "CENTER", "CENTER", "CENTER"), width=180) as table: for data_row in rows: table.row(data_row) return True def pdf_step_summary(self, passed, failure, doc: Document): tot = passed + failure fail = 100 * failure / tot session_name = 'Test Summary' if failure > 0: line_color = doc.PALE_RED else: line_color = doc.PALE_GREEN rows = (('Total TEST', 'PASS', 'FAIL', '% of Failure'), (str(tot), str(passed), str(failure), '{:.02f}'.format(fail)) ) doc.add_chapter(session_name) doc.set_font(size=9) with doc.table( borders_layout="INTERNAL", cell_fill_color=line_color, cell_fill_mode=TableCellFillMode.ROWS, col_widths=(60, 60, 60, 60), # headings_style=FontFace(), # FontFace(emphasis="BOLD", color=234, fill_color=(128, 128, 128)), headings_style=FontFace(emphasis="BOLD", size_pt=10), line_height=6, text_align=("CENTER", "CENTER", "CENTER", "CENTER"), width=180) as table: for data_row in rows: table.row(data_row) return True def pdf_add_image(self, name, title=None, alt_text=None, width=0, height=0): # self.add_page() doc.image(name=name, title=title, alt_text=alt_text, keep_aspect_ratio=True, w=width, h=height) def pdf_add_per_run_summary(self, doc: Document, repetitions: list): """ Generate professional per-run summary table with both 1553 and Serial statistics. Creates a dedicated chapter with comprehensive per-run metrics including: - Run status (PASS/FAIL) - PBIT timing - B6 status checks (Pass/Fail/Known) - B8 diagnostic checks - Serial communication (Errors/Fatal/Recycles) Args: doc: Document object for PDF generation repetitions: List of per-run statistics dictionaries with keys: - 'repetition': Run number - 'success': Boolean pass/fail - 'pbit_time': BIT completion time in seconds - 'b6_pass', 'b6_fail', 'b6_known_fail': B6 check counts - 'b8_checked', 'b8_pass', 'b8_fail': B8 check counts - 'serial_total', 'serial_errors', 'serial_fatal', 'serial_recycles': Serial stats This table appears as a standalone chapter, NOT mixed with step execution logs. """ doc.add_chapter('Per-Run Summary (1553 + Serial Statistics)') doc.ln(5) doc.set_font(size=9) # Table header row headers = ('Run', 'Status', 'PBIT\nTime', 'B6 Status\n(P/F/K)', 'B8 Checks\n(Pass/Fail)', 'Serial Messages\n(E/F/R)', 'Result') with doc.table( borders_layout="ALL", col_widths=(15, 20, 20, 30, 35, 35, 25), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER", "CENTER"), width=180) as table: table.row(headers) for run in repetitions: # Determine row color based on result if run['success']: row_color = doc.PALE_GREEN status_text = "PASS" result_symbol = "✓" else: row_color = doc.PALE_RED status_text = "FAIL" result_symbol = "✗" style = FontFace(fill_color=row_color, size_pt=9) # Format data compactly pbit_time = f"{run['pbit_time']:.1f}s" if run.get('bit_available', True) else "N/A" b6_status = f"{run['b6_pass']}/{run['b6_fail']}/{run['b6_known_fail']}" b8_checks = f"{run['b8_pass']}/{run['b8_fail']}" if run['b8_checked'] > 0 else "-" serial_stats = f"{run.get('serial_errors', 0)}/{run.get('serial_fatal', 0)}/{run.get('serial_recycles', 0)}" result_text = f"{result_symbol} {status_text}" row = table.row() for cell_text in (str(run['repetition']), status_text, pbit_time, b6_status, b8_checks, serial_stats, result_text): row.cell(cell_text, style=style) doc.ln(5) doc.set_font(size=8, style='I') doc.multi_cell(0, 4, "Legend: B6 Status = Pass/Fail/Known (known failures ignored) | " "B8 Checks = Pass/Fail (diagnostic drill-down) | " "Serial = Errors(%%E)/Fatal(%%F)/Recycles", align='L') doc.reset_font() def pdf_add_global_statistics(self, doc: Document, aggregate: dict): """ Generate professional global aggregate statistics table. Creates a dedicated chapter with combined statistics from all test runs: - Overall test summary (total runs, pass/fail rates) - Aggregate 1553 statistics (B6/B8 checks across all runs) - Aggregate serial statistics (total messages, errors, fatal, recycles) - PBIT timing analysis (avg, min, max, std dev) Args: doc: Document object for PDF generation aggregate: Dictionary with aggregate statistics: - 'total_runs', 'successful_runs', 'failed_runs' - 'total_b6_checks', 'total_b6_pass', 'total_b6_fail', 'total_b6_known' - 'total_b8_checks', 'total_b8_pass', 'total_b8_fail' - 'total_serial_msgs', 'total_serial_errors', 'total_serial_fatal', 'total_serial_recycles' - 'avg_pbit_time', 'min_pbit_time', 'max_pbit_time', 'std_dev_pbit' This table appears as a standalone chapter, NOT mixed with step execution logs. """ doc.add_chapter('Global Aggregate Statistics (All Runs Combined)') doc.ln(5) # Section 1: Overall Test Summary doc.add_subchapter('Overall Test Summary') doc.set_font(size=9) total = aggregate['total_runs'] success = aggregate['successful_runs'] failed = aggregate['failed_runs'] success_rate = (success / total * 100) if total > 0 else 0 rows_summary = ( ('Metric', 'Count', 'Percentage'), ('Total Runs', str(total), '100%'), ('Successful Runs', str(success), f"{success_rate:.1f}%"), ('Failed Runs', str(failed), f"{100-success_rate:.1f}%") ) with doc.table( borders_layout="ALL", col_widths=(60, 60, 60), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("LEFT", "CENTER", "CENTER"), width=180) as table: for data_row in rows_summary: table.row(data_row) doc.ln(10) # Section 2: 1553 Bus Statistics (B6 + B8) doc.add_subchapter('1553 Bus Statistics') doc.set_font(size=9) total_b6 = aggregate['total_b6_checks'] b6_pass_rate = (aggregate['total_b6_pass'] / total_b6 * 100) if total_b6 > 0 else 0 b6_fail_rate = (aggregate['total_b6_fail'] / total_b6 * 100) if total_b6 > 0 else 0 b6_known_rate = (aggregate['total_b6_known'] / total_b6 * 100) if total_b6 > 0 else 0 rows_b6 = ( ('Metric', 'Count', 'Percentage'), ('B6 LRU Status - Total Checks', str(total_b6), '100%'), ('B6 - Pass', str(aggregate['total_b6_pass']), f"{b6_pass_rate:.1f}%"), ('B6 - Fail (Real)', str(aggregate['total_b6_fail']), f"{b6_fail_rate:.1f}%"), ('B6 - Known Failures (Ignored)', str(aggregate['total_b6_known']), f"{b6_known_rate:.1f}%") ) with doc.table( borders_layout="ALL", col_widths=(80, 50, 50), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("LEFT", "CENTER", "CENTER"), width=180) as table: for data_row in rows_b6: table.row(data_row) doc.ln(5) # B8 statistics (if any checks were performed) total_b8 = aggregate['total_b8_checks'] if total_b8 > 0: b8_pass_rate = (aggregate['total_b8_pass'] / total_b8 * 100) b8_fail_rate = (aggregate['total_b8_fail'] / total_b8 * 100) rows_b8 = ( ('Metric', 'Count', 'Percentage'), ('B8 Diagnostic - Total Checks', str(total_b8), '100%'), ('B8 - Pass', str(aggregate['total_b8_pass']), f"{b8_pass_rate:.1f}%"), ('B8 - Fail', str(aggregate['total_b8_fail']), f"{b8_fail_rate:.1f}%") ) with doc.table( borders_layout="ALL", col_widths=(80, 50, 50), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("LEFT", "CENTER", "CENTER"), width=180) as table: for data_row in rows_b8: table.row(data_row) doc.ln(10) else: doc.set_font(size=9, style='I') doc.cell(0, 5, 'B8 Diagnostic checks: Not performed (no real B6 failures detected)', align='L') doc.ln(10) doc.reset_font() # Section 3: Serial Communication Statistics doc.add_subchapter('Serial Communication Statistics') doc.set_font(size=9) total_serial = aggregate['total_serial_msgs'] serial_error_rate = (aggregate['total_serial_errors'] / total_serial * 100) if total_serial > 0 else 0 serial_fatal_rate = (aggregate['total_serial_fatal'] / total_serial * 100) if total_serial > 0 else 0 rows_serial = ( ('Metric', 'Count', 'Notes'), ('Total Serial Messages', str(total_serial), 'All messages processed'), ('Error Messages (%%E)', str(aggregate['total_serial_errors']), f"{serial_error_rate:.1f}% of total"), ('Fatal Messages (%%F)', str(aggregate['total_serial_fatal']), f"{serial_fatal_rate:.1f}% of total"), ('System Recycles', str(aggregate['total_serial_recycles']), 'Power cycles + unexpected resets') ) with doc.table( borders_layout="ALL", col_widths=(70, 40, 70), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("LEFT", "CENTER", "LEFT"), width=180) as table: for data_row in rows_serial: table.row(data_row) doc.ln(10) # Section 4: PBIT Timing Analysis if 'avg_pbit_time' in aggregate and aggregate['avg_pbit_time'] is not None: doc.add_subchapter('PBIT Timing Analysis') doc.set_font(size=9) rows_timing = ( ('Metric', 'Value', 'Unit'), ('Average PBIT Time', f"{aggregate['avg_pbit_time']:.2f}", 'seconds'), ('Minimum PBIT Time', f"{aggregate['min_pbit_time']:.2f}", 'seconds'), ('Maximum PBIT Time', f"{aggregate['max_pbit_time']:.2f}", 'seconds'), ('Standard Deviation', f"{aggregate['std_dev_pbit']:.2f}", 'seconds') ) with doc.table( borders_layout="ALL", col_widths=(70, 50, 60), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("LEFT", "CENTER", "CENTER"), width=180) as table: for data_row in rows_timing: table.row(data_row) doc.ln(5) doc.set_font(size=8, style='I') doc.multi_cell(0, 4, "PBIT timing measures the duration from power-on to BIT report availability. " "Consistent timing indicates stable system behavior.", align='L') doc.reset_font() def pdf_add_machine_reliability_assessment(self, doc: Document, aggregate: dict, repetitions: list): """ Generate Machine Reliability Assessment with problem distribution table. Creates a dedicated chapter analyzing machine reliability with: - Overall reliability percentage (perfect runs vs total) - Problem type distribution table ordered by frequency - Clear identification of main issues to address Args: doc: Document object for PDF generation aggregate: Dictionary with aggregate statistics repetitions: List of per-run statistics to analyze problem types This section provides executive-level reliability assessment for maintenance decisions. """ doc.add_chapter('Machine Reliability Assessment') doc.ln(5) # Calculate overall reliability total = aggregate['total_runs'] perfect_runs = aggregate['successful_runs'] reliability_pct = (perfect_runs / total * 100) if total > 0 else 0 doc.set_font(size=10) doc.multi_cell(0, 5, f"This section provides an executive summary of the machine under test, " f"analyzing {total} complete test cycles to identify reliability metrics and problem patterns.", align='J') doc.ln(5) # Overall Reliability Box doc.add_subchapter('Overall Reliability Score') doc.set_font(size=9) if reliability_pct >= 80: reliability_color = doc.PALE_GREEN assessment = "EXCELLENT" elif reliability_pct >= 60: reliability_color = (255, 255, 200) # Pale yellow assessment = "GOOD" elif reliability_pct >= 40: reliability_color = (255, 200, 150) # Pale orange assessment = "ACCEPTABLE" else: reliability_color = doc.PALE_RED assessment = "CRITICAL" rows_reliability = ( ('Metric', 'Value', 'Assessment'), ('Perfect Runs (No Issues)', f"{perfect_runs}/{total}", f"{reliability_pct:.1f}%"), ('Runs with Issues', f"{total - perfect_runs}/{total}", f"{100 - reliability_pct:.1f}%"), ('Overall Rating', assessment, '') ) with doc.table( borders_layout="ALL", col_widths=(70, 50, 60), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("LEFT", "CENTER", "CENTER"), width=180) as table: for idx, data_row in enumerate(rows_reliability): row = table.row() for cell_text in data_row: if idx == len(rows_reliability) - 1: # Last row (rating) row.cell(cell_text, style=FontFace(fill_color=reliability_color, emphasis="BOLD", size_pt=9)) else: row.cell(cell_text) doc.ln(10) # Known Failures Information Box doc.add_subchapter('Known Failures (Excluded from Statistics)') doc.set_font(size=9) # Import KNOWN_FAILURES from test script try: import sys import os # Add scripts folder to path to import KNOWN_FAILURES script_dir = os.path.join(os.path.dirname(__file__), '..', 'scripts') if script_dir not in sys.path: sys.path.insert(0, script_dir) from GRIFO_M_PBIT import KNOWN_FAILURES doc.multi_cell(0, 5, "The following test checks are expected to fail due to hardware test setup limitations " "(e.g., missing physical components). These failures are tracked separately and do not " "affect the test verdict or reliability calculations:", align='J') doc.ln(3) # List known failures doc.set_font(size=9) for known_field in KNOWN_FAILURES: # Extract clean field name if 'RdrHealthStatusAndBitReport_' in known_field: clean_name = known_field.split('RdrHealthStatusAndBitReport_')[-1] else: clean_name = known_field.split('_')[-1] if '_' in known_field else known_field clean_name = clean_name.replace('_', ' ').title() doc.cell(10, 5, '') doc.cell(0, 5, f"• {clean_name}", ln=True) except ImportError: doc.multi_cell(0, 5, "Note: Known failures configuration could not be loaded.", align='J') doc.ln(10) # Problem Distribution Analysis doc.add_subchapter('Problem Distribution Analysis') doc.set_font(size=9) # Analyze problem types from repetitions problem_counts = {} for run in repetitions: if not run['success']: # Extract FULL field names from failures (not just last parts) for field, value in run['failures']: # Remove common prefix but keep full field identifier # Example: "radar_health_status_and_bit_report_valid_RdrHealthStatusAndBitReport_processor_status" # -> "processor_status" if 'RdrHealthStatusAndBitReport_' in field: # Extract everything after message name test_name = field.split('RdrHealthStatusAndBitReport_')[-1] elif '_' in field and len(field.split('_')) > 3: # For other messages, keep last 4 parts for context parts = field.split('_') test_name = '_'.join(parts[-4:]) else: test_name = field # Clean up for display (capitalize, keep underscores for clarity) test_name = test_name.replace('_', ' ').title() # Count this specific test failure problem_counts[test_name] = problem_counts.get(test_name, 0) + 1 # Also check serial problems if run.get('serial_fatal', 0) > 0: problem_counts['Serial Communication (Fatal)'] = problem_counts.get('Serial Communication (Fatal)', 0) + 1 if run.get('serial_recycles', 0) > 1: # More than expected problem_counts['System Instability (Recycles)'] = problem_counts.get('System Instability (Recycles)', 0) + 1 if problem_counts: # Sort by frequency (descending) sorted_problems = sorted(problem_counts.items(), key=lambda x: x[1], reverse=True) doc.multi_cell(0, 5, f"Analysis of {total - perfect_runs} runs with issues reveals the following problem distribution:", align='J') doc.ln(5) # Problem distribution table headers = ('Problem Type', 'Occurrences', '% of Total Runs', '% of Failed Runs') with doc.table( borders_layout="ALL", col_widths=(70, 35, 37.5, 37.5), headings_style=FontFace(emphasis="BOLD", size_pt=9, fill_color=(200, 200, 200)), line_height=5, text_align=("LEFT", "CENTER", "CENTER", "CENTER"), width=180) as table: table.row(headers) for problem, count in sorted_problems: pct_total = (count / total * 100) pct_failed = (count / (total - perfect_runs) * 100) if (total - perfect_runs) > 0 else 0 # Color code by severity if pct_total >= 20: row_color = doc.PALE_RED elif pct_total >= 10: row_color = (255, 200, 150) # Orange else: row_color = (255, 255, 200) # Yellow style = FontFace(fill_color=row_color, size_pt=9) row = table.row() row.cell(problem, style=style) row.cell(f"{count}/{total}", style=style) row.cell(f"{pct_total:.1f}%", style=style) row.cell(f"{pct_failed:.1f}%", style=style) doc.ln(5) doc.set_font(size=8, style='I') doc.multi_cell(0, 4, "Legend: Red (≥20% of runs) = Critical priority | Orange (≥10%) = High priority | Yellow (<10%) = Medium priority", align='L') doc.reset_font() else: doc.set_font(size=10, style='B') doc.cell(0, 5, '✓ NO PROBLEMS DETECTED - All runs completed successfully', align='L') doc.ln(10) doc.reset_font() def pdf_generate(self, folder_path: str, doc: Document, file_name: str): """ This function generates a pdf. Usually the folder_path + file_name should have less than 260 characters. :param folder_path: path where the PDF will be created. :param doc: latex document created by start_doc. :param file_name: name of the PDF. """ file_path = os.path.join(folder_path, file_name) logging.info(f"Generating pdf file {file_path} and its length is {len(file_path)}") try: doc.output(f'{file_path}.pdf') except Exception as e: logging.warning("PDF not created for this reason: {}".format(e)) if __name__ == '__main__': f = open('..\\rt800_template.json') t = open('..\\RBT_BLACK_ATC_1783.json') json_template = json.load(f) json_test = json.load(t) my_pdf = PDFClass() doc = my_pdf.start_doc(json_template) file_name = my_pdf.pdf_name(json_template, '') my_pdf.pdf_preamble(doc, json_test) my_pdf.pdf_test_information_section(doc, json_test) # my_pdf.pdf_add_image(r'C:\temp\prova.png', 'Prova', 'provax?') my_pdf.pdf_step_summary(7, 3, doc) my_pdf.pdf_step_summary(3, 1, doc) my_pdf.pdf_step_summary(10, 0, doc) my_pdf.pdf_step_summary(0, 3, doc) my_pdf.pdf_step_result('HF - USB Sensitivity - cycle number 1', 17, 0, doc) step_values = [['Start Time: 2024-03-14 16:02:12.199903'], ['Get VQ1MPlatform-MIB::current Waveform', 'DCE:46c8c5bb-d957-4bd8-9be9-98beb34ee888','DCE:46c8c5bb-d957-4bd8-9be9-98beb34ee888'], ['Get VQ1MPlatform-MIB::current Waveform', None, 'DCE:46c8c5bb-d957-4bd8-9be9-98beb34ee888', 'ERROR!'], ['Set HFSuite-MIB::operationalMode 1.0 to 0', '0', '0'], ['Set HFSuite-MIB::rxSquelch1.0 to 0', '0', '0'], ['Set HFSuite-MIB::rfModulation Type1.0 to 0', '0', '0'], ['Set cma180_set_connector to OutputConnector.RFCom', 'OutputConnector.RFCom', 'OutputConnector.RFCom'], ['Set cma180_set_rf_freq to 2000000', '2000000', '2000000'], ['Set cma180_set_level to-111', '-111', '-111'], ['Set HFSuite-MIB::rxSquelch1.0 to 0', '0', '0'], ['Set HFSuite-MIB::rfModulation Type1.0 to 0', '0', '0'], ['Set cma180_set_connector to OutputConnector.RFCom', 'OutputConnector.RFCom', 'OutputConnector.RFCom'], ['Set cma180_set_rf_freq to 2000000', '2000000', '2000000'], ['Set cma180_set_level to-111', '-111', '-111'], ] step_keys = ['desc', 'result', 'expected', 'error'] step_list = [] for v in step_values: d = dict(zip(step_keys, v)) step_list.append(d) my_pdf.pdf_step_execution(step_list, doc) my_pdf.pdf_generate('', doc, file_name)