Split MainWindow into TestRunner and TestFileManager coordinators

Extract execution lifecycle (TestRunner) and file/process management
(TestFileManager) from MainWindow, reducing it from ~1170 to ~700 lines.
Validated against full test suite with no new regressions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 13:32:45 +02:00
parent 3ed73d93a7
commit a353511f64
3 changed files with 1109 additions and 1171 deletions

View File

@@ -0,0 +1,171 @@
import os
import sys
import traceback
from queue import Empty
from PySide6.QtWidgets import QApplication, QFileDialog
from interpreter.process import TestProcess
from interpreter.utils.test_ctrl import TestSetController
from main_win.test_controller_service import TestControllerService
import interpreter.utils.settings as prefs
from lib.tum_except import ETUMFileError, ETUMRuntimeError
class TestFileManager:
"""Manages test file loading, process lifecycle, and recent files."""
def __init__(self, win) -> None:
self._win = win
# --- Process lifecycle ---
def clear_process(self):
w = self._win
if (
w.test_proc is not None
and w.test_proc.is_alive()
and w.test_service is not None
):
w.test_service.stop()
w.test_service.close()
w.test_proc.join()
del w.test_proc
w.test_proc = None
del w.test_service
w.test_service = None
del w.ts_controller
w.ts_controller = None
def reload(self, file_name: str):
w = self._win
w.disconnect_signals()
self.clear_process()
self.load(file_name)
w.reconnect_signals()
def load(self, file_name: str) -> bool:
"""Load a test file. Returns True on success, False otherwise."""
w = self._win
try:
if not file_name:
raise ETUMFileError("No file to load")
file_name = os.path.abspath(file_name)
initial_dir = os.path.dirname(file_name)
if not os.path.isdir(initial_dir):
raise ETUMFileError("Could not find %s directory" % initial_dir)
if not os.path.isfile(file_name):
raise ETUMFileError("Could not find %s file" % file_name)
w.testFile = None
w.ts_controller = TestSetController()
w.test_service = TestControllerService(w.ts_controller)
w.test_proc = TestProcess(
file_name,
w.status_queue,
w.ts_controller,
w.config_files,
w.defines,
self._defaults_for_process(),
)
w.test_proc.start()
while w.test_proc.is_alive():
try:
if w.test_service.loaded(timeout=1.0):
break
except Empty:
w.test_service.clear()
if not w.test_proc.is_alive():
del w.test_proc
w.test_proc = None
del w.test_service
w.test_service = None
del w.ts_controller
w.ts_controller = None
raise ETUMRuntimeError(
"Test could not be loaded (test process crashed for any reason)"
)
test_data = w.test_service.tree()
w.treeTests.clear()
w.treeTests.loadTestRecursively(w.treeTests.invisibleRootItem(), test_data)
w.treeTests.setFoldDefault()
w.treeTests.updateTreeSkipState(w.test_service)
w.checkSelect.setChecked(True)
w.testFile = file_name
test_dir = os.path.dirname(w.testFile)
sys.path.append(test_dir)
w.statusBar().showMessage("Test file loaded", 10000)
w.textLog.set_test_dir(test_dir)
self.add_file_to_recent(file_name)
w.setWindowTitle(w.mainWindowTitle + " - " + w.testFile)
w.actionStart_test.setEnabled(True)
w.actionRefresh_test.setEnabled(True)
w.show_checkboxes()
return True
except:
w.statusBar().showMessage("No test file could be loaded", 10000)
w.treeTests.clear()
print(traceback.format_exc())
return False
def _defaults_for_process(self) -> dict:
d = {}
pp = prefs.settings.python_bin
if pp != "":
d["python_bin"] = pp
pp = prefs.settings.lua_bin
if pp != "":
d["lua_bin"] = pp
return d
# --- Recent files ---
def add_file_to_recent(self, filename: str):
files = prefs.settings.recent_files
try:
files.remove(filename)
except ValueError:
pass
files.insert(0, filename)
del files[self._win.MaxRecentFiles:]
prefs.settings.recent_files = files
for widget in QApplication.topLevelWidgets():
from main_win.testium_win import MainWindow
if isinstance(widget, MainWindow):
widget.file_manager.update_recent_file_actions()
def update_recent_file_actions(self):
w = self._win
files = prefs.settings.recent_files
numRecentFiles = min(len(files), w.MaxRecentFiles)
for i in range(numRecentFiles):
text = "&%d %s" % (i + 1, w._stripped_name(files[i]))
w.recentFileActs[i].setText(text)
w.recentFileActs[i].setData(files[i])
w.recentFileActs[i].setVisible(True)
for j in range(numRecentFiles, w.MaxRecentFiles):
w.recentFileActs[j].setVisible(False)
w.separatorAct.setVisible(numRecentFiles > 0)
def on_open_recent_file(self):
w = self._win
action = w.sender()
if action:
self.reload(action.data())
def on_open_test(self):
w = self._win
d = ""
if w.testFile is not None:
d = os.path.dirname(w.testFile)
file_name, _ = QFileDialog.getOpenFileName(
w, "Open the test file", d, "testium file (*.tum);;All Files (*)"
)
if file_name:
self.reload(file_name)

View File

@@ -0,0 +1,237 @@
import os
import traceback
from tempfile import NamedTemporaryFile
from PySide6 import QtGui
from PySide6.QtCore import QDateTime
from PySide6.QtGui import QIcon, QPixmap
from interpreter.utils.icons import icon_prefix
import interpreter.utils.settings as prefs
class TestRunner:
"""Manages the test execution lifecycle: start/pause/stop, timers, log file, UI adaptation."""
def __init__(self, win) -> None:
self._win = win
self.logFileHandler = None
# --- Timer helpers ---
def start_pause_timer(self):
w = self._win
w.timerPause.setSingleShot(False)
w.timerPause.setInterval(500)
w.timerPause.start()
w.timerPause.state = False
# --- Execution control ---
def on_start_test(self):
w = self._win
if w._test_started:
if not w._test_paused:
w.test_service.pause()
self.start_pause_timer()
else:
w.test_service.cont()
w.timerPause.stop()
w.timerPause.state = False
self.on_timer_pause()
w._test_paused = not w._test_paused
return
w.start_time = QDateTime.currentDateTime()
# Log file setup
log_file = w.editLogFilePath.text()
if w.buttLogFileSaved.isChecked() and (log_file != ""):
try:
if not os.path.isabs(log_file):
default_path = prefs.settings.log_path
default_path = w.test_service.process_param(default_path)
log_file = os.path.join(default_path, log_file)
if not os.path.exists(os.path.dirname(log_file)):
os.makedirs(os.path.dirname(log_file))
if os.path.isfile(log_file):
i = 0
fname = log_file
while os.path.isfile(fname):
i += 1
fname = log_file + "-" + str(i) + ".saved"
os.rename(log_file, fname)
self.logFileHandler = open(log_file, "w")
w.out_log.set(self.logFileHandler)
w.logFileName = log_file
except:
self.logFileHandler = NamedTemporaryFile(mode="w", suffix=".log", delete=False)
w.out_log.set(self.logFileHandler)
w.logFileName = self.logFileHandler.name
else:
self.logFileHandler = NamedTemporaryFile(mode="w", suffix=".log", delete=False)
w.out_log.set(self.logFileHandler)
w.logFileName = self.logFileHandler.name
# Report setup and execution
rep_file = w.test_service.process_param(w.reportFileName)
w.test_service.set_report(rep_file, w.report_type, w.report_pattern)
self.adapt_interface_during_test()
w.treeTests.clearAllStatus()
try:
w.textLog.clear()
w.textLog.appendPlainText("Test is started\n")
w.timer.setSingleShot(False)
w.timer.setInterval(100)
w.timer.start()
w.test_service.set_test_outputs([w.logFileName])
w.test_service.execute()
except:
print(traceback.format_exc())
self.restore_interface_after_test()
def on_stop_test(self):
self._win.test_service.stop()
def on_run_finished(self):
w = self._win
w.timer.setSingleShot(True)
w.timer.setInterval(1000)
txt = w.stream.read()
w.textLog.appendPlainText(txt)
self.restore_interface_after_test()
if self.logFileHandler is not None:
w.out_log.reset()
self.logFileHandler.write(txt + "\n")
self.logFileHandler.close()
self.logFileHandler = None
w.textLog.appendPlainText("Test is finished")
if w.runandclose:
w.on_actionExit_triggered()
def on_breakpoint(self):
w = self._win
w._test_paused = True
self.start_pause_timer()
# --- Timer slots ---
def on_timer_event(self):
w = self._win
text_to_append = []
while not w.threads_queue.empty():
text_to_append.append(w.threads_queue.get())
if text_to_append:
for t in text_to_append:
w.textLog.appendPlainText(t)
if self.logFileHandler is not None:
self.logFileHandler.write(t + "\n")
self.logFileHandler.flush()
def on_timer_blink(self):
w = self._win
if w.buttBlink.current_color != "gray":
self.set_blink_gray()
elif w.treeTests.getGlobalSuccess():
self.set_blink_green()
else:
self.set_blink_red()
def on_timer_pause(self):
w = self._win
if w._test_paused:
icon = QtGui.QIcon()
if w.timerPause.state:
icon.addPixmap(QtGui.QPixmap(icon_prefix() + "/pause2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
else:
icon.addPixmap(QtGui.QPixmap(icon_prefix() + "/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
w.timerPause.state = not w.timerPause.state
w.actionStart_test.setIcon(icon)
def on_timer_count(self):
w = self._win
secfromstart = w.start_time.secsTo(QDateTime.currentDateTime())
w.label_runtime.setText(
"%02d:%02d:%02d" % (secfromstart / 3600, (secfromstart / 60) % 60, secfromstart % 60)
)
# --- Interface adaptation ---
def adapt_interface_during_test(self):
w = self._win
try:
w.disconnect_signals()
w.actionOpenTest.setDisabled(True)
w.actionExit.setDisabled(True)
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(icon_prefix() + "/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
w.actionStart_test.setIcon(icon)
w.actionStart_test.setText("Pause test")
w.actionPreferences.setDisabled(True)
w.actionRefresh_test.setDisabled(True)
w.actionShow_Results.setDisabled(True)
w.actionSave_report.setDisabled(True)
w.logSettingsBox.setDisabled(True)
w.actionStop_test.setEnabled(True)
if prefs.settings.show_checkboxes:
w._checklist = w.treeTests.getCheckList()
w.treeTests.removeCheckBoxes()
w.checkSelect.setDisabled(True)
w.checkFold.setDisabled(True)
w.timerBlink.setSingleShot(False)
w.timerBlink.setInterval(1000)
w.timerBlink.start()
self.set_blink_green()
w.treeTests.clearGlobalSuccess()
finally:
w._test_started = True
def restore_interface_after_test(self):
w = self._win
try:
w.timerPause.stop()
w.timerBlink.stop()
w.actionOpenTest.setEnabled(True)
w.actionExit.setEnabled(True)
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(icon_prefix() + "/start.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
w.actionStart_test.setIcon(icon)
w.actionStart_test.setText("Start test")
w.actionPreferences.setEnabled(True)
w.actionRefresh_test.setEnabled(True)
w.actionStop_test.setDisabled(True)
w.actionShow_Results.setEnabled(True)
w.actionSave_report.setEnabled(True)
w.logSettingsBox.setEnabled(True)
if prefs.settings.show_checkboxes:
w.checkSelect.setEnabled(True)
w.treeTests.showCheckBoxes(w._checklist, w.test_service)
w.checkFold.setEnabled(True)
w.treeTests.setChildrenEnabled()
w.reconnect_signals()
if w.treeTests.getGlobalSuccess():
self.set_blink_green()
else:
self.set_blink_red()
finally:
w._test_started = False
# --- Blink indicator ---
def set_blink_green(self):
w = self._win
w.buttBlink.setIcon(w.iconBlinkGreen)
w.buttBlink.current_color = "green"
def set_blink_red(self):
w = self._win
w.buttBlink.setIcon(w.iconBlinkRed)
w.buttBlink.current_color = "red"
def set_blink_gray(self):
w = self._win
w.buttBlink.setIcon(w.iconBlinkGray)
w.buttBlink.current_color = "gray"

File diff suppressed because it is too large Load Diff