PlatSim_Genova/TestEnvironment/scripts/GRIFO_M_PBIT_gui.py

786 lines
32 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 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 = {}
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(1, weight=1)
# 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=2, 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))
# ===== 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))
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'])
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 _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)
# 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()