1133 lines
46 KiB
Python
1133 lines
46 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 ToolTip:
|
|
"""
|
|
Creates a tooltip for a given widget.
|
|
Shows the tooltip when mouse enters the widget.
|
|
"""
|
|
def __init__(self, widget, text):
|
|
self.widget = widget
|
|
self.text = text
|
|
self.tooltip_window = None
|
|
self.widget.bind("<Enter>", self.show_tooltip)
|
|
self.widget.bind("<Leave>", self.hide_tooltip)
|
|
|
|
def show_tooltip(self, event=None):
|
|
"""Display the tooltip."""
|
|
if self.tooltip_window or not self.text:
|
|
return
|
|
|
|
# Get widget position
|
|
x = self.widget.winfo_rootx() + 20
|
|
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 5
|
|
|
|
# Create tooltip window
|
|
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
|
tw.wm_overrideredirect(True) # Remove window decorations
|
|
tw.wm_geometry(f"+{x}+{y}")
|
|
|
|
# Create label with tooltip text
|
|
label = tk.Label(tw, text=self.text, justify=tk.LEFT,
|
|
background="#ffffe0", relief=tk.SOLID, borderwidth=1,
|
|
font=("Arial", 9))
|
|
label.pack(ipadx=5, ipady=3)
|
|
|
|
def hide_tooltip(self, event=None):
|
|
"""Hide the tooltip."""
|
|
if self.tooltip_window:
|
|
self.tooltip_window.destroy()
|
|
self.tooltip_window = None
|
|
|
|
|
|
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 runs table (row 3) and some to log (row 4)
|
|
main_frame.rowconfigure(3, weight=3)
|
|
main_frame.rowconfigure(4, 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: PER-RUN TABLE (moved up for better visibility) =====
|
|
runs_frame = ttk.LabelFrame(main_frame, text="Runs Overview", padding="5")
|
|
runs_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
|
|
runs_frame.columnconfigure(0, weight=1)
|
|
runs_frame.rowconfigure(1, weight=1) # Row 1 for table (row 0 for custom headers)
|
|
|
|
# Create custom header frame with tooltips
|
|
header_frame = ttk.Frame(runs_frame)
|
|
header_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
|
|
|
|
# Define headers with their tooltips
|
|
headers_with_tooltips = [
|
|
('Run', None, 60),
|
|
('Status', None, 80),
|
|
('PBIT Time', None, 80),
|
|
('B6 Status (P/F/K)', 'P = Pass\nF = Fail\nK = Known Failures', 120),
|
|
('B8 Checks (P/F/K)', 'P = Pass\nF = Fail\nK = Known Failures', 120),
|
|
('Serial Messages (E/F/R)', 'E = Errors\nF = Fatal\nR = Recycles', 120),
|
|
('Target', 'Target distance and cycle\nFormat: XXXXm @ cycle Y', 140),
|
|
('Target Test', None, 80)
|
|
]
|
|
|
|
# Create header labels with tooltips
|
|
for idx, (title, tooltip, width) in enumerate(headers_with_tooltips):
|
|
label = ttk.Label(header_frame, text=title, font=('Arial', 9, 'bold'),
|
|
anchor=tk.CENTER, relief=tk.RAISED, borderwidth=1)
|
|
label.grid(row=0, column=idx, sticky=(tk.W, tk.E), ipadx=2, ipady=3)
|
|
header_frame.columnconfigure(idx, weight=1 if idx > 0 else 0, minsize=width)
|
|
|
|
# Add tooltip if specified
|
|
if tooltip:
|
|
ToolTip(label, tooltip)
|
|
|
|
# Columns aligned with PDF report format for consistency
|
|
cols = ('run', 'status', 'pbit_time', 'b6_status', 'b8_checks', 'serial_msgs', 'target', 'target_test')
|
|
# Taller table for better visibility (show='tree' to hide built-in headers)
|
|
self.widgets['runs_table'] = ttk.Treeview(runs_frame, columns=cols, show='tree', height=12)
|
|
self.widgets['runs_table'].grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
|
self.widgets['runs_table'].column('#0', width=0, stretch=tk.NO) # Hide tree column
|
|
|
|
# Set column widths to match headers
|
|
column_widths = [60, 80, 80, 120, 120, 120, 140, 80]
|
|
for c, width in zip(cols, column_widths):
|
|
self.widgets['runs_table'].column(c, width=width, anchor=tk.CENTER)
|
|
|
|
# Add scrollbar
|
|
runs_scroll = ttk.Scrollbar(runs_frame, orient=tk.VERTICAL, command=self.widgets['runs_table'].yview)
|
|
runs_scroll.grid(row=1, column=1, sticky=(tk.N, tk.S))
|
|
self.widgets['runs_table'].configure(yscrollcommand=runs_scroll.set)
|
|
|
|
# Style tags for visual feedback
|
|
self.widgets['runs_table'].tag_configure('pass', background='#d4edda')
|
|
self.widgets['runs_table'].tag_configure('fail', background='#f8d7da')
|
|
|
|
# Bind double-click for details
|
|
self.widgets['runs_table'].bind('<Double-Button-1>', self._on_run_double_click)
|
|
|
|
# ===== ROW 4: EVENT LOG (moved down) =====
|
|
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')
|
|
|
|
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.
|
|
|
|
Note: Statistics frame removed from GUI (data visible in runs table).
|
|
This method kept for backward compatibility but does nothing.
|
|
"""
|
|
# Update internal data only (no GUI update)
|
|
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]
|
|
# GUI widgets removed - data now visible in runs table
|
|
|
|
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}', '', '', '', '', '', '', '', '', ''))
|
|
# initialize empty metadata for each run
|
|
try:
|
|
self.runs_data[str(i)] = {}
|
|
except Exception:
|
|
pass
|
|
self.widgets['runs_table_initialized'] = True
|
|
|
|
def update_run(self, run: int, **kwargs):
|
|
"""Thread-safe request to update one row in runs table.
|
|
|
|
Accepts keyword arguments matching PDF report format:
|
|
- result: PASS/FAIL status
|
|
- pbit: PBIT time (seconds)
|
|
- b6_pass, b6_fail, b6_known_fail: B6 test results (formatted as P/F/K)
|
|
- b8_pass, b8_fail, b8_checked: B8 test results (formatted as Pass/Fail or -)
|
|
- serial_errors, serial_fatal, serial_recycles: Serial message stats (formatted as E/F/R)
|
|
- target_simulated: Target simulation data (formatted as distance @ cycle)
|
|
- target_detected: Target test result (PASS/FAIL/-)
|
|
|
|
Additional metadata fields for detail dialog:
|
|
- start_time, end_time, run_duration
|
|
- failures, serial_details
|
|
- target_test_time, stby_verified, silence_verified, stby_silence_verify_details
|
|
- target_distance_m, target_distance_nm, target_found_iter
|
|
"""
|
|
data = {'run': run}
|
|
# Merge all kwargs into data to keep API flexible
|
|
try:
|
|
for k, v in kwargs.items():
|
|
data[k] = v
|
|
except Exception:
|
|
pass
|
|
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 = [''] * 8 # Updated to 8 columns (aligned with PDF)
|
|
# Map columns to match PDF format
|
|
new = list(old)
|
|
for k, v in data.items():
|
|
if k == 'run':
|
|
new[0] = f"{v}/{self.data.get('run_total', '')}"
|
|
elif k == 'result':
|
|
# Column 1: Status (PASS/FAIL)
|
|
new[1] = v
|
|
elif k == 'pbit':
|
|
# Column 2: PBIT Time
|
|
new[2] = f"{v:.1f}s" if isinstance(v, (int, float)) else str(v)
|
|
elif k in ('b6_pass', 'b6_fail', 'b6_known_fail'):
|
|
# Column 3: B6 Status (P/F/K) - format like PDF
|
|
b6_pass = data.get('b6_pass', 0)
|
|
b6_fail = data.get('b6_fail', 0)
|
|
b6_known = data.get('b6_known_fail', 0)
|
|
new[3] = f"{b6_pass}/{b6_fail}/{b6_known}"
|
|
elif k in ('b8_pass', 'b8_fail', 'b8_checked', 'b8_known_fail'):
|
|
# Column 4: B8 Checks (Pass/Fail/Known) - format like PDF
|
|
b8_pass = data.get('b8_pass', 0)
|
|
b8_fail = data.get('b8_fail', 0)
|
|
b8_known = data.get('b8_known_fail', 0)
|
|
b8_checked = data.get('b8_checked', 0)
|
|
if b8_checked > 0:
|
|
new[4] = f"{b8_pass}/{b8_fail}/{b8_known}"
|
|
else:
|
|
new[4] = "-"
|
|
elif k in ('serial_errors', 'serial_fatal', 'serial_recycles'):
|
|
# Column 5: Serial Messages (E/F/R) - format like PDF
|
|
serial_e = data.get('serial_errors', 0)
|
|
serial_f = data.get('serial_fatal', 0)
|
|
serial_r = data.get('serial_recycles', 0)
|
|
new[5] = f"{serial_e}/{serial_f}/{serial_r}"
|
|
elif k == 'target_simulated':
|
|
# Column 6: Target (distance @ cycle) - format like PDF
|
|
if v and isinstance(v, dict):
|
|
try:
|
|
distance = v.get('distance', '?')
|
|
cycle = v.get('appeared_after_cycles', v.get('found_at_iter', '?'))
|
|
new[6] = f"{distance}m @ cycle {cycle}"
|
|
except Exception:
|
|
new[6] = "-"
|
|
else:
|
|
new[6] = "-"
|
|
elif k == 'target_detected':
|
|
# Column 7: Target Test (PASS/FAIL/-)
|
|
if v is None:
|
|
new[7] = "-"
|
|
elif v:
|
|
new[7] = "PASS"
|
|
# If target detected but no simulated data yet, keep existing target column
|
|
if new[6] == "-" or not new[6]:
|
|
new[6] = "Detected"
|
|
else:
|
|
new[7] = "FAIL"
|
|
# If target not detected, clear target column
|
|
new[6] = "-"
|
|
|
|
# Merge/remember detailed data for this run (kept on GUI thread)
|
|
try:
|
|
meta = self.runs_data.get(run_num, {})
|
|
for k, v in data.items():
|
|
meta[k] = v
|
|
# also store the displayed columns for convenience (aligned with PDF)
|
|
meta['display'] = {
|
|
'status': new[1], 'pbit_time': new[2], 'b6_status': new[3], 'b8_checks': new[4],
|
|
'serial_msgs': new[5], 'target': new[6], 'target_test': new[7]
|
|
}
|
|
self.runs_data[run_num] = meta
|
|
except Exception:
|
|
pass
|
|
|
|
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 _on_run_double_click(self, event=None):
|
|
"""Handle double-click on runs table: open detailed run dialog."""
|
|
try:
|
|
table = self.widgets.get('runs_table')
|
|
if not table:
|
|
return
|
|
|
|
sel = table.focus()
|
|
if not sel:
|
|
# try selection
|
|
sel_items = table.selection()
|
|
if not sel_items:
|
|
return
|
|
sel = sel_items[0]
|
|
|
|
run_id = str(sel)
|
|
meta = self.runs_data.get(run_id, {})
|
|
|
|
# Build dialog
|
|
dlg = tk.Toplevel(self.root)
|
|
dlg.title(f"Run {run_id} Details")
|
|
dlg.transient(self.root)
|
|
frm = ttk.Frame(dlg, padding=12)
|
|
frm.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Header
|
|
hdr = ttk.Label(frm, text=f"Run {run_id}", font=('Arial', 12, 'bold'))
|
|
hdr.pack(anchor=tk.W)
|
|
|
|
# Key/value grid
|
|
grid = ttk.Frame(frm)
|
|
grid.pack(fill=tk.X, pady=(6, 6))
|
|
|
|
def _kv(row, key, val):
|
|
ttk.Label(grid, text=key+":", font=('Arial', 9, 'bold')).grid(row=row, column=0, sticky=tk.W, padx=(0,6))
|
|
ttk.Label(grid, text=str(val), font=('Arial', 9)).grid(row=row, column=1, sticky=tk.W)
|
|
|
|
display = meta.get('display', {})
|
|
_kv(0, 'Status', display.get('status', ''))
|
|
_kv(1, 'PBIT Time', display.get('pbit_time', ''))
|
|
_kv(2, 'B6 Status (P/F/K)', display.get('b6_status', ''))
|
|
_kv(3, 'B8 Checks', display.get('b8_checks', ''))
|
|
_kv(4, 'Serial Msgs (E/F/R)', display.get('serial_msgs', ''))
|
|
_kv(5, 'Target', display.get('target', ''))
|
|
_kv(6, 'Target Test', display.get('target_test', ''))
|
|
|
|
# Start / End times and duration (if provided)
|
|
start_iso = meta.get('start_time')
|
|
end_iso = meta.get('end_time')
|
|
run_duration = meta.get('run_duration')
|
|
try:
|
|
start_disp = start_iso
|
|
end_disp = end_iso
|
|
dur_disp = f"{run_duration:.2f}s" if isinstance(run_duration, (int, float)) else str(run_duration)
|
|
except Exception:
|
|
start_disp = start_iso
|
|
end_disp = end_iso
|
|
dur_disp = run_duration
|
|
_kv(7, 'Start Time', start_disp)
|
|
_kv(8, 'End Time', end_disp)
|
|
_kv(9, 'Run Duration', dur_disp)
|
|
|
|
# Target info
|
|
tgt_detected = meta.get('target_detected', None)
|
|
tgt_test_time = meta.get('target_test_time', None)
|
|
tgt_dist_m = meta.get('target_distance_m', None)
|
|
tgt_dist_nm = meta.get('target_distance_nm', None)
|
|
tgt_iter = meta.get('target_found_iter', None)
|
|
|
|
tgt_status = 'YES' if tgt_detected is True else ('NO' if tgt_detected is False else 'N/A')
|
|
_kv(10, 'Target Detected', tgt_status)
|
|
_kv(11, 'Target Test Time', f"{tgt_test_time:.2f}s" if isinstance(tgt_test_time, (int, float)) else str(tgt_test_time))
|
|
# Distance formatting
|
|
try:
|
|
dist_str = ''
|
|
if isinstance(tgt_dist_m, (int, float)):
|
|
dist_str = f"{tgt_dist_m:.1f} m"
|
|
if isinstance(tgt_dist_nm, (int, float)):
|
|
dist_str += f" ({tgt_dist_nm:.2f} NM)"
|
|
else:
|
|
dist_str = str(tgt_dist_m)
|
|
except Exception:
|
|
dist_str = str(tgt_dist_m)
|
|
_kv(12, 'Target Distance', dist_str)
|
|
_kv(13, 'Target Found At Iter', tgt_iter)
|
|
|
|
# Failures detail (multi-line)
|
|
ttk.Label(frm, text='Failures (detailed):', font=('Arial', 9, 'bold')).pack(anchor=tk.W, pady=(6,0))
|
|
failures_text = tk.Text(frm, height=6, width=80, font=('Courier', 9), wrap=tk.WORD)
|
|
failures = meta.get('failures') or meta.get('fail_summary') or ''
|
|
# If failures stored as list, pretty-format
|
|
if isinstance(failures, (list, tuple)):
|
|
body = '\n'.join([str(x) for x in failures])
|
|
else:
|
|
body = str(failures)
|
|
failures_text.insert('1.0', body)
|
|
failures_text.config(state='disabled')
|
|
failures_text.pack(fill=tk.BOTH, expand=False, pady=(2,6))
|
|
|
|
# Serial details if present
|
|
serial = meta.get('serial_details') or meta.get('serial_events') or ''
|
|
ttk.Label(frm, text='Serial events/details:', font=('Arial', 9, 'bold')).pack(anchor=tk.W, pady=(6,0))
|
|
serial_text = tk.Text(frm, height=4, width=80, font=('Courier', 9), wrap=tk.WORD)
|
|
serial_text.insert('1.0', str(serial))
|
|
serial_text.config(state='disabled')
|
|
serial_text.pack(fill=tk.BOTH, expand=False, pady=(2,6))
|
|
|
|
# Close button
|
|
btn_frame = ttk.Frame(frm)
|
|
btn_frame.pack(fill=tk.X, pady=(6,0))
|
|
close_btn = ttk.Button(btn_frame, text='Close', command=lambda: dlg.destroy())
|
|
close_btn.pack(side=tk.RIGHT)
|
|
|
|
# Center dialog
|
|
dlg.update_idletasks()
|
|
w = dlg.winfo_reqwidth()
|
|
h = dlg.winfo_reqheight()
|
|
try:
|
|
px = self.root.winfo_rootx()
|
|
py = self.root.winfo_rooty()
|
|
pw = self.root.winfo_width() or self.root.winfo_reqwidth()
|
|
ph = self.root.winfo_height() or self.root.winfo_reqheight()
|
|
x = px + max(0, (pw - w)//2)
|
|
y = py + max(0, (ph - h)//2)
|
|
dlg.geometry(f"{w}x{h}+{x}+{y}")
|
|
except Exception:
|
|
pass
|
|
dlg.transient(self.root)
|
|
dlg.grab_set()
|
|
except Exception as e:
|
|
print(f"Error opening run details: {e}")
|
|
|
|
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()
|