PlatSim_Genova/TestEnvironment/scripts/GRIFO_M_PBIT_gui.py

910 lines
37 KiB
Python

"""
GRIFO_M_PBIT_gui.py - Real-Time Test Monitor GUI
This module provides a tkinter-based GUI to monitor test execution in real-time.
Works with both real hardware and simulation mode.
Features:
- Live test progress visualization
- Current scenario information
- Real-time statistics (B6/B8/Serial)
- Color-coded status indicators
- Event log with filtering
- Thread-safe updates via queue
Requirements:
- Python 3.7+
- tkinter 8.6+ (included in embedded Python)
Usage:
# In test script:
from GRIFO_M_PBIT_gui import TestMonitorGUI
gui = TestMonitorGUI()
gui.start()
# Update from test code:
gui.update_status('run', current=5, total=10)
gui.update_scenario('processor_fail', 'Processor Failure', {...})
gui.log_event('info', 'BIT completed in 15.3s')
# Cleanup:
gui.stop()
Author: Test Automation Team
Date: 2026-01-30
"""
import tkinter as tk
from tkinter import ttk, scrolledtext
import threading
import queue
import time
import gc
import os
from datetime import datetime, timedelta
from typing import Dict, Any, Optional
class TestMonitorGUI:
"""
Real-time GUI monitor for GRIFO PBIT test execution.
Thread-safe GUI that displays:
- Test progress (runs, scenarios)
- Live statistics (B6, B8, Serial)
- Scenario information
- Event log
"""
def __init__(self):
"""Initialize GUI components (creates window but doesn't show it yet)."""
self.root = None
self.thread = None
self.running = False
self.update_queue = queue.Queue()
# Data storage
self.data = {
'run_current': 0,
'run_total': 0,
'scenario_name': 'Initializing...',
'scenario_description': '',
'pbit_time': 0.0,
'pbit_available': False,
'power_on': False,
'b6_total': 0,
'b6_pass': 0,
'b6_fail': 0,
'b6_known': 0,
'b8_checked': 0,
'b8_pass': 0,
'b8_fail': 0,
'serial_total': 0,
'serial_errors': 0,
'serial_fatal': 0,
'serial_recycles': 0,
'success_rate': 0.0,
'expected_failures': [],
'expected_passes': [],
'notes': '',
}
# GUI components (will be created in GUI thread)
self.widgets = {}
# Timing/ETA estimation
self.pbit_samples = [] # list of completed run durations (seconds)
self.run_start_timestamp = None
self.last_run_current = 0
self.default_pbit_sec = 182.0 # fallback per-run estimate
# Per-run detailed data storage (populated on GUI thread)
self.runs_data = {}
def start(self):
"""Start GUI in separate thread."""
if self.running:
return
self.running = True
# Start GUI thread as non-daemon so the process remains alive until user closes the GUI
self.thread = threading.Thread(target=self._run_gui, daemon=False)
self.thread.start()
# Wait for GUI to initialize
time.sleep(0.5)
def stop(self):
"""Stop GUI and close window."""
# Request shutdown from GUI thread to avoid calling tkinter methods
# from the main thread (which can trigger Tcl_AsyncDelete warnings).
if not self.running:
return
self.update_queue.put(('shutdown', {}))
# Wait for GUI thread to exit (safe join); allow longer timeout for slow systems
try:
if self.thread and self.thread.is_alive():
self.thread.join(timeout=15.0)
except Exception:
pass
self.running = False
def _run_gui(self):
"""GUI thread main loop."""
self.root = tk.Tk()
self.root.title("GRIFO M-PBIT Test Monitor")
# Increase default window size so log and runs table are visible without manual resize
self.root.geometry("1100x900")
self.root.minsize(900, 700)
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
# Configure styles
style = ttk.Style()
style.theme_use('clam')
# Create GUI layout
self._create_widgets()
# Start update polling
self._poll_updates()
# Run main loop
self.root.mainloop()
def _create_widgets(self):
"""Create all GUI widgets."""
# Main container
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
# Configure grid weights
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# Give more vertical space to log (row 4) and runs table (row 5)
main_frame.rowconfigure(4, weight=2)
main_frame.rowconfigure(5, weight=1)
# ===== ROW 0: HEADER =====
header_frame = ttk.Frame(main_frame)
header_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
title_label = ttk.Label(header_frame, text="GRIFO M-PBIT Test Monitor",
font=('Arial', 16, 'bold'))
title_label.pack(side=tk.LEFT)
self.widgets['status_indicator'] = ttk.Label(header_frame, text="● IDLE",
font=('Arial', 12, 'bold'),
foreground='gray')
self.widgets['status_indicator'].pack(side=tk.RIGHT)
# ===== ROW 1: PROGRESS SECTION =====
progress_frame = ttk.LabelFrame(main_frame, text="Test Progress", padding="10")
progress_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
progress_frame.columnconfigure(0, weight=0)
progress_frame.columnconfigure(1, weight=1)
progress_frame.columnconfigure(2, weight=0)
progress_frame.columnconfigure(3, weight=0)
# Run info
ttk.Label(progress_frame, text="Current Run:").grid(row=0, column=0, sticky=tk.W)
self.widgets['run_label'] = ttk.Label(progress_frame, text="0 / 0",
font=('Arial', 10, 'bold'))
self.widgets['run_label'].grid(row=0, column=1, sticky=tk.W, padx=(10, 0))
# Progress bar
self.widgets['progress_bar'] = ttk.Progressbar(progress_frame, mode='determinate')
self.widgets['progress_bar'].grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(5, 0))
# PBIT timer
ttk.Label(progress_frame, text="PBIT Time:").grid(row=2, column=0, sticky=tk.W, pady=(5, 0))
self.widgets['pbit_label'] = ttk.Label(progress_frame, text="0.0s",
font=('Arial', 10))
self.widgets['pbit_label'].grid(row=2, column=1, sticky=tk.W, padx=(10, 0), pady=(5, 0))
# Estimated remaining time (ETA) on the same row, right-justified
ttk.Label(progress_frame, text="Estimated Remaining:").grid(row=2, column=2, sticky=tk.W, pady=(5, 0))
self.widgets['eta_label'] = ttk.Label(progress_frame, text="--:--", font=('Arial', 10))
self.widgets['eta_label'].grid(row=2, column=3, sticky=tk.E, padx=(10, 0), pady=(5, 0))
# ===== ROW 2: SCENARIO INFO =====
scenario_frame = ttk.LabelFrame(main_frame, text="Current Scenario", padding="10")
scenario_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
scenario_frame.columnconfigure(1, weight=1)
# Scenario name
ttk.Label(scenario_frame, text="Scenario:").grid(row=0, column=0, sticky=tk.W)
self.widgets['scenario_label'] = ttk.Label(scenario_frame, text="Initializing...",
font=('Arial', 10, 'bold'))
self.widgets['scenario_label'].grid(row=0, column=1, sticky=tk.W, padx=(10, 0))
# Description
ttk.Label(scenario_frame, text="Description:").grid(row=1, column=0, sticky=tk.W, pady=(5, 0))
self.widgets['scenario_desc'] = ttk.Label(scenario_frame, text="",
font=('Arial', 9), wraplength=600)
self.widgets['scenario_desc'].grid(row=1, column=1, sticky=tk.W, padx=(10, 0), pady=(5, 0))
# Expected failures
ttk.Label(scenario_frame, text="Expected Failures:").grid(row=2, column=0, sticky=(tk.W, tk.N), pady=(5, 0))
self.widgets['expected_fail'] = tk.Text(scenario_frame, height=2, width=60,
font=('Courier', 8), wrap=tk.WORD,
background='#fff5f5')
self.widgets['expected_fail'].grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(5, 0))
# ===== ROW 3: STATISTICS =====
stats_frame = ttk.Frame(main_frame)
stats_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
stats_frame.columnconfigure(0, weight=1)
stats_frame.columnconfigure(1, weight=1)
stats_frame.columnconfigure(2, weight=1)
# B6 Stats
b6_frame = ttk.LabelFrame(stats_frame, text="B6 LRU Status", padding="10")
b6_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
self.widgets['b6_stats'] = ttk.Label(b6_frame, text="Total: 0\nPass: 0\nFail: 0\nKnown: 0",
font=('Courier', 9), justify=tk.LEFT)
self.widgets['b6_stats'].pack()
# B8 Stats
b8_frame = ttk.LabelFrame(stats_frame, text="B8 Diagnostics", padding="10")
b8_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=5)
self.widgets['b8_stats'] = ttk.Label(b8_frame, text="Checked: 0\nPass: 0\nFail: 0",
font=('Courier', 9), justify=tk.LEFT)
self.widgets['b8_stats'].pack()
# Serial Stats
serial_frame = ttk.LabelFrame(stats_frame, text="Serial Messages", padding="10")
serial_frame.grid(row=0, column=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
self.widgets['serial_stats'] = ttk.Label(serial_frame, text="Total: 0\nErrors: 0\nFatal: 0\nRecycles: 0",
font=('Courier', 9), justify=tk.LEFT)
self.widgets['serial_stats'].pack()
# ===== ROW 4: EVENT LOG =====
log_frame = ttk.LabelFrame(main_frame, text="Event Log", padding="10")
log_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
self.widgets['log_text'] = scrolledtext.ScrolledText(log_frame, height=15, width=80,
font=('Courier', 8), wrap=tk.WORD)
self.widgets['log_text'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure text tags for color coding
self.widgets['log_text'].tag_config('info', foreground='black')
self.widgets['log_text'].tag_config('success', foreground='green')
self.widgets['log_text'].tag_config('warning', foreground='orange')
self.widgets['log_text'].tag_config('error', foreground='red')
self.widgets['log_text'].tag_config('debug', foreground='gray')
# ===== ROW 5: PER-RUN TABLE =====
runs_frame = ttk.LabelFrame(main_frame, text="Runs Overview", padding="5")
runs_frame.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))
runs_frame.columnconfigure(0, weight=1)
cols = ('run', 'result', 'pbit', 'b6_fail', 'b8_fail', 'known', 'fail_summary', 'serial_events', 'target_done', 'target_result')
# taller default table so more runs are visible
self.widgets['runs_table'] = ttk.Treeview(runs_frame, columns=cols, show='headings', height=10)
self.widgets['runs_table'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Column headers: Result = Overall PBIT result, Target Done and Target Result are new columns
for c, title in zip(cols, ['Run', 'Result', 'PBIT (s)', 'B6 Fail', 'B8 Fail', 'Known', 'Failures', 'Serial', 'Tgt Test', 'Tgt Result']):
self.widgets['runs_table'].heading(c, text=title)
self.widgets['runs_table'].column(c, width=100, anchor=tk.CENTER)
# Horizontal and vertical scrollbars for table
hscroll = ttk.Scrollbar(runs_frame, orient=tk.HORIZONTAL, command=self.widgets['runs_table'].xview)
vscroll = ttk.Scrollbar(runs_frame, orient=tk.VERTICAL, command=self.widgets['runs_table'].yview)
self.widgets['runs_table'].configure(xscrollcommand=hscroll.set, yscrollcommand=vscroll.set)
hscroll.grid(row=1, column=0, sticky=(tk.W, tk.E))
vscroll.grid(row=0, column=1, sticky=(tk.N, tk.S))
# Configure row tags for quick visual pass/fail (background color)
try:
table = self.widgets['runs_table']
table.tag_configure('pass', background='#dff0d8') # light green
table.tag_configure('fail', background='#f2dede') # light red
except Exception:
pass
# Bind double-click on a row to open detailed run dialog
try:
self.widgets['runs_table'].bind('<Double-1>', self._on_run_double_click)
except Exception:
pass
def _poll_updates(self):
"""Poll update queue and apply changes to GUI."""
if not self.running:
return
try:
# Process all queued updates
while not self.update_queue.empty():
update_type, data = self.update_queue.get_nowait()
if update_type == 'status':
self._update_status(data)
elif update_type == 'scenario':
self._update_scenario(data)
elif update_type == 'statistics':
self._update_statistics(data)
elif update_type == 'run_row':
self._update_run_row(data)
elif update_type == 'show_results':
self._show_results_dialog(data)
elif update_type == 'shutdown':
# Perform clean shutdown from the GUI thread
try:
self.running = False
if self.root:
try:
self.root.quit()
except Exception:
pass
try:
self.root.destroy()
except Exception:
pass
# CRITICAL: Clear all Tk references on GUI thread to avoid
# Tcl_AsyncDelete warnings when main thread cleans up
try:
self.root = None
self.widgets.clear()
except Exception:
pass
except Exception:
pass
return
elif update_type == 'gc_collect':
# Run garbage collection on the GUI thread so that any
# PhotoImage/ImageTk destructors that call into Tk run
# on the correct thread and avoid Tcl_AsyncDelete issues.
try:
gc.collect()
except Exception:
pass
elif update_type == 'log':
self._add_log_entry(data)
except queue.Empty:
pass
except Exception as e:
print(f"[GUI] Error processing update: {e}")
# Schedule next poll
if self.root:
self.root.after(100, self._poll_updates)
def _update_status(self, data: Dict[str, Any]):
"""Update test status display."""
if 'run_current' in data:
self.data['run_current'] = data['run_current']
if 'run_total' in data:
self.data['run_total'] = data['run_total']
if 'pbit_time' in data:
self.data['pbit_time'] = data['pbit_time']
if 'pbit_available' in data:
self.data['pbit_available'] = data['pbit_available']
if 'power_on' in data:
self.data['power_on'] = data['power_on']
# Update labels
self.widgets['run_label'].config(text=f"{self.data['run_current']} / {self.data['run_total']}")
self.widgets['pbit_label'].config(text=f"{self.data['pbit_time']:.1f}s")
# Update progress bar
if self.data['run_total'] > 0:
progress = (self.data['run_current'] / self.data['run_total']) * 100
self.widgets['progress_bar']['value'] = progress
# Update status indicator
if self.data['power_on']:
status_text = "● RUNNING"
status_color = 'green'
else:
status_text = "● IDLE"
status_color = 'gray'
self.widgets['status_indicator'].config(text=status_text, foreground=status_color)
# Initialize runs table when run_total is set
if self.data['run_total'] and 'runs_table_initialized' not in self.widgets:
self._init_runs_table(self.data['run_total'])
# ----- ETA / estimation logic -----
try:
now = time.time()
run_current = int(self.data.get('run_current', 0) or 0)
run_total = int(self.data.get('run_total', 0) or 0)
# Initialize run start timestamp when first run is seen
if run_current and self.run_start_timestamp is None:
self.run_start_timestamp = now
self.last_run_current = run_current
# Detect run increment -> previous run finished
if run_current and self.last_run_current and run_current > self.last_run_current:
# previous run finished at 'now'
try:
duration = now - (self.run_start_timestamp or now)
if duration > 0:
self.pbit_samples.append(duration)
except Exception:
pass
# start timestamp for the new run
self.run_start_timestamp = now
self.last_run_current = run_current
# Compute average per-run duration
if self.pbit_samples:
avg = sum(self.pbit_samples) / len(self.pbit_samples)
else:
avg = float(self.default_pbit_sec)
# Remaining runs (not counting current one)
remaining_runs = max(0, run_total - run_current)
current_pbit = float(self.data.get('pbit_time', 0.0) or 0.0)
# Estimate remaining seconds: remaining full runs + remaining of current
remaining_of_current = max(0.0, avg - current_pbit)
est_seconds = remaining_runs * avg + remaining_of_current
# If finished, show completed
if run_total and run_current >= run_total:
eta_text = "Completed"
else:
# Human-friendly duration with units
duration_str = self._format_seconds(est_seconds)
# Estimated finish clock time
try:
finish_dt = datetime.now() + timedelta(seconds=int(round(est_seconds)))
finish_str = finish_dt.strftime("%H:%M")
eta_text = f"{duration_str} ({finish_str})"
except Exception:
eta_text = duration_str
# Update ETA label
try:
self.widgets['eta_label'].config(text=eta_text)
except Exception:
pass
except Exception:
# Keep ETA unchanged on errors
pass
def _update_scenario(self, data: Dict[str, Any]):
"""Update scenario information display."""
if 'name' in data:
self.data['scenario_name'] = data['name']
if 'description' in data:
self.data['scenario_description'] = data['description']
if 'expected_failures' in data:
self.data['expected_failures'] = data['expected_failures']
if 'expected_passes' in data:
self.data['expected_passes'] = data['expected_passes']
if 'notes' in data:
self.data['notes'] = data['notes']
# Update labels
self.widgets['scenario_label'].config(text=self.data['scenario_name'])
self.widgets['scenario_desc'].config(text=self.data['scenario_description'])
# Update expected failures text box (enable for edits then disable)
txt = self.widgets['expected_fail']
try:
txt.config(state='normal')
except Exception:
pass
txt.delete('1.0', tk.END)
if self.data['expected_failures']:
fail_text = '\n'.join([f"{f}" for f in self.data['expected_failures']])
else:
fail_text = "(none expected)"
txt.insert('1.0', fail_text)
txt.config(state='disabled')
def _update_statistics(self, data: Dict[str, Any]):
"""Update statistics display."""
# Update data
for key in ['b6_total', 'b6_pass', 'b6_fail', 'b6_known',
'b8_checked', 'b8_pass', 'b8_fail',
'serial_total', 'serial_errors', 'serial_fatal', 'serial_recycles']:
if key in data:
self.data[key] = data[key]
# Update B6 stats
b6_text = f"Total: {self.data['b6_total']}\nPass: {self.data['b6_pass']}\nFail: {self.data['b6_fail']}\nKnown: {self.data['b6_known']}"
self.widgets['b6_stats'].config(text=b6_text)
# Update B8 stats
b8_text = f"Checked: {self.data['b8_checked']}\nPass: {self.data['b8_pass']}\nFail: {self.data['b8_fail']}"
self.widgets['b8_stats'].config(text=b8_text)
# Update Serial stats
serial_text = f"Total: {self.data['serial_total']}\nErrors: {self.data['serial_errors']}\nFatal: {self.data['serial_fatal']}\nRecycles: {self.data['serial_recycles']}"
self.widgets['serial_stats'].config(text=serial_text)
def _add_log_entry(self, data: Dict[str, str]):
"""Add entry to event log."""
level = data.get('level', 'info')
message = data.get('message', '')
timestamp = time.strftime('%H:%M:%S')
# Format message with timestamp
log_line = f"[{timestamp}] {message}\n"
# Insert with appropriate tag
self.widgets['log_text'].insert(tk.END, log_line, level)
self.widgets['log_text'].see(tk.END)
# Limit log size (keep last 500 lines)
line_count = int(self.widgets['log_text'].index('end-1c').split('.')[0])
if line_count > 500:
self.widgets['log_text'].delete('1.0', '100.0')
def _format_seconds(self, seconds: float) -> str:
"""Format seconds into human-friendly string with units.
Examples:
- 14 -> "14s"
- 134 -> "2m 14s"
- 3670 -> "1h 01m 10s"
"""
try:
sec = int(round(seconds))
if sec <= 0:
return "0s"
hours, rem = divmod(sec, 3600)
minutes, secs = divmod(rem, 60)
parts = []
if hours:
parts.append(f"{hours}h")
if minutes:
parts.append(f"{minutes}m")
if secs or not parts:
parts.append(f"{secs}s")
return ' '.join(parts)
except Exception:
return "--:--"
def _on_closing(self):
"""Handle window close event."""
self.running = False
self.root.destroy()
# ===== PUBLIC API FOR TEST CODE =====
def update_status(self, run_current: Optional[int] = None, run_total: Optional[int] = None,
pbit_time: Optional[float] = None, pbit_available: Optional[bool] = None,
power_on: Optional[bool] = None):
"""Update test status (thread-safe)."""
data = {}
if run_current is not None:
data['run_current'] = run_current
if run_total is not None:
data['run_total'] = run_total
if pbit_time is not None:
data['pbit_time'] = pbit_time
if pbit_available is not None:
data['pbit_available'] = pbit_available
if power_on is not None:
data['power_on'] = power_on
if data:
self.update_queue.put(('status', data))
def update_scenario(self, name: str, description: str,
expected_failures: list, expected_passes: list, notes: str):
"""Update scenario information (thread-safe)."""
data = {
'name': name,
'description': description,
'expected_failures': expected_failures,
'expected_passes': expected_passes,
'notes': notes,
}
self.update_queue.put(('scenario', data))
def update_statistics(self, **kwargs):
"""Update statistics (thread-safe)."""
self.update_queue.put(('statistics', kwargs))
def _init_runs_table(self, total_runs: int):
"""Pre-populate runs table with empty rows for visual overview."""
table = self.widgets.get('runs_table')
if not table:
return
# Clear existing
for r in table.get_children():
table.delete(r)
for i in range(1, total_runs + 1):
table.insert('', 'end', iid=str(i), values=(f'{i}/{total_runs}', '', '', '', '', '', '', '', '', ''))
self.widgets['runs_table_initialized'] = True
def update_run(self, run: int, result: Optional[str] = None, pbit: Optional[float] = None,
b6_fail: Optional[int] = None, b8_fail: Optional[int] = None,
known: Optional[int] = None, fail_summary: Optional[str] = None,
serial_events: Optional[str] = None, target_done: Optional[str] = None,
target_result: Optional[str] = None):
"""Thread-safe request to update one row in runs table."""
data = {'run': run}
if result is not None: data['result'] = result
if pbit is not None: data['pbit'] = pbit
if b6_fail is not None: data['b6_fail'] = b6_fail
if b8_fail is not None: data['b8_fail'] = b8_fail
if known is not None: data['known'] = known
if fail_summary is not None: data['fail_summary'] = fail_summary
if serial_events is not None: data['serial_events'] = serial_events
if target_done is not None: data['target_done'] = target_done
if target_result is not None: data['target_result'] = target_result
self.update_queue.put(('run_row', data))
def _update_run_row(self, data: Dict[str, Any]):
"""Apply an update to the runs table row specified by 'run'."""
table = self.widgets.get('runs_table')
if not table:
return
run_num = str(data.get('run'))
if not table.exists(run_num):
# Insert if missing
total = self.data.get('run_total', 0)
table.insert('', 'end', iid=run_num, values=(f'{run_num}/{total}', '', '', '', '', '', '', '', '', ''))
# Read old values
old = table.item(run_num).get('values', [])
if not old:
old = [''] * 10 # Updated to 10 columns
# Map columns
new = list(old)
for k, v in data.items():
if k == 'run':
new[0] = f"{v}/{self.data.get('run_total', '')}"
elif k == 'result':
new[1] = v
elif k == 'pbit':
new[2] = f"{v:.1f}" if isinstance(v, (int, float)) else str(v)
elif k == 'b6_fail':
new[3] = str(v)
elif k == 'b8_fail':
new[4] = str(v)
elif k == 'known':
new[5] = str(v)
elif k == 'fail_summary':
new[6] = str(v)
elif k == 'serial_events':
new[7] = str(v)
elif k == 'target_done':
new[8] = str(v)
elif k == 'target_result':
new[9] = str(v)
table.item(run_num, values=new)
# Apply visual tag based on result for quick scanning
try:
res = new[1].strip() if len(new) > 1 and isinstance(new[1], str) else ''
if res.upper() == 'PASS':
table.item(run_num, tags=('pass',))
elif res.upper() == 'FAIL':
table.item(run_num, tags=('fail',))
else:
# clear tags
table.item(run_num, tags=())
except Exception:
pass
# Ensure the updated row is visible (scroll to it)
try:
table.see(run_num)
except Exception:
# Fallback: move view to bottom
try:
table.yview_moveto(1.0)
except Exception:
pass
def log_event(self, level: str, message: str):
"""Add log entry (thread-safe).
Args:
level: 'info', 'success', 'warning', 'error', 'debug'
message: Log message text
"""
data = {'level': level, 'message': message}
self.update_queue.put(('log', data))
def show_results(self, folder: str):
"""Request GUI to show results folder dialog (thread-safe).
This enqueues a request processed in the GUI thread.
"""
self.update_queue.put(('show_results', {'folder': folder}))
def _show_results_dialog(self, data: dict):
"""Create a professional dialog showing test completion and output locations."""
folder = data.get('folder') if isinstance(data, dict) else data
if not folder:
return
try:
dlg = tk.Toplevel(self.root)
dlg.title('GRIFO M-PBIT Test Completed')
dlg.transient(self.root)
# Main container with padding
main_frame = ttk.Frame(dlg, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# Header with icon and title
header_frame = ttk.Frame(main_frame)
header_frame.pack(fill=tk.X, pady=(0, 15))
title_label = ttk.Label(header_frame, text="✓ Test Execution Completed",
font=('Arial', 14, 'bold'), foreground='green')
title_label.pack(anchor=tk.W)
# Separator
sep1 = ttk.Separator(main_frame, orient=tk.HORIZONTAL)
sep1.pack(fill=tk.X, pady=(0, 15))
# Info section
info_frame = ttk.LabelFrame(main_frame, text="Test Results", padding="15")
info_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
msg_text = (
"The test execution has been completed successfully.\n\n"
"The following output files have been generated:"
)
msg_label = ttk.Label(info_frame, text=msg_text, justify=tk.LEFT)
msg_label.pack(anchor=tk.W, pady=(0, 10))
# File types list
files_frame = ttk.Frame(info_frame)
files_frame.pack(fill=tk.X, pady=(0, 10))
file_items = [
("PDF Report", "Complete test report with analysis"),
("CSV Export", "Detailed statistics and per-run data"),
("Log Files", "Execution logs and debug information")
]
for icon_text, description in file_items:
item_frame = ttk.Frame(files_frame)
item_frame.pack(fill=tk.X, pady=2)
ttk.Label(item_frame, text=f" {icon_text}:", font=('Arial', 9, 'bold')).pack(side=tk.LEFT)
ttk.Label(item_frame, text=f" {description}", font=('Arial', 9)).pack(side=tk.LEFT)
# Location section with folder path
location_frame = ttk.Frame(info_frame)
location_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Label(location_frame, text="Location:", font=('Arial', 9, 'bold')).pack(anchor=tk.W)
path_text = tk.Text(location_frame, height=2, wrap=tk.WORD,
font=('Courier', 8), relief=tk.FLAT,
background='#f0f0f0', padx=5, pady=5)
path_text.insert('1.0', folder)
path_text.config(state='disabled')
path_text.pack(fill=tk.X, pady=(5, 0))
# Separator
sep2 = ttk.Separator(main_frame, orient=tk.HORIZONTAL)
sep2.pack(fill=tk.X, pady=(0, 15))
# Button frame
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X)
def _open_folder():
try:
if os.name == 'nt':
os.startfile(folder)
else:
import subprocess
subprocess.Popen(['xdg-open', folder])
except Exception:
pass
def _close_all():
try:
dlg.destroy()
except Exception:
pass
try:
self._on_closing()
except Exception:
pass
# Buttons with better styling
open_btn = ttk.Button(btn_frame, text='Open folder', command=_open_folder)
open_btn.pack(side=tk.RIGHT, padx=(10, 0))
close_btn = ttk.Button(btn_frame, text='Close Test', command=_close_all)
close_btn.pack(side=tk.RIGHT)
# Update window to calculate final size, then center on parent and show
dlg.update_idletasks()
# Set minimum size based on content
dlg_width = max(600, dlg.winfo_reqwidth())
dlg_height = max(400, dlg.winfo_reqheight())
# Calculate position to center on parent window (use root's screen coordinates)
try:
parent_x = self.root.winfo_rootx()
parent_y = self.root.winfo_rooty()
parent_width = self.root.winfo_width() or self.root.winfo_reqwidth()
parent_height = self.root.winfo_height() or self.root.winfo_reqheight()
except Exception:
parent_x = 0
parent_y = 0
parent_width = self.root.winfo_screenwidth()
parent_height = self.root.winfo_screenheight()
x = parent_x + max(0, (parent_width - dlg_width) // 2)
y = parent_y + max(0, (parent_height - dlg_height) // 2)
# Clamp to screen bounds
screen_w = self.root.winfo_screenwidth()
screen_h = self.root.winfo_screenheight()
x = max(0, min(x, screen_w - dlg_width))
y = max(0, min(y, screen_h - dlg_height))
# Apply geometry and make non-resizable
dlg.geometry(f'{dlg_width}x{dlg_height}+{x}+{y}')
dlg.resizable(False, False)
# Now grab focus
dlg.grab_set()
# Ensure dialog is on top
dlg.lift()
dlg.focus_force()
except Exception as e:
print(f"Error showing results dialog: {e}")
# ===== SIMPLE TEST/DEMO =====
if __name__ == '__main__':
print("Starting GUI test...")
gui = TestMonitorGUI()
gui.start()
# Simulate test execution
gui.update_status(run_current=0, run_total=10, power_on=True)
gui.log_event('info', 'Test started')
time.sleep(1)
for i in range(1, 11):
# Update run
gui.update_status(run_current=i, run_total=10)
gui.log_event('info', f'Starting run {i}/10')
# Update scenario
scenarios = ['normal', 'processor_fail', 'transmitter_fail']
scenario = scenarios[i % 3]
gui.update_scenario(
name=scenario.replace('_', ' ').title(),
description=f"Test scenario {scenario}",
expected_failures=['Field A', 'Field B'] if scenario != 'normal' else [],
expected_passes=['All other fields'],
notes='This is a test scenario'
)
# Simulate PBIT execution
for t in range(15):
gui.update_status(pbit_time=t + 1.0)
time.sleep(0.2)
# Update statistics
gui.update_statistics(
b6_total=12, b6_pass=10, b6_fail=1, b6_known=1,
b8_checked=0 if scenario == 'normal' else 50,
b8_pass=0 if scenario == 'normal' else 48,
b8_fail=0 if scenario == 'normal' else 2,
serial_total=i * 5, serial_errors=i, serial_fatal=0, serial_recycles=0
)
gui.log_event('success' if scenario == 'normal' else 'warning',
f'Run {i} completed')
time.sleep(1)
gui.log_event('success', 'All tests completed!')
# Keep GUI open
input("Press Enter to close GUI...")
gui.stop()